UNPKG

@replyke/express

Version:

Replyke: Build interactive apps with social features like comments, votes, feeds, user lists, notifications, and more.

259 lines (258 loc) 10.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); const models_1 = require("../../../models"); const models_2 = require("../../../models"); const validateUserCreated_1 = __importDefault(require("../../../helpers/webhooks/validateUserCreated")); const deepEqual_1 = __importDefault(require("../../../utils/deepEqual")); const reduceAuthenticatedUserDetails_1 = __importDefault(require("../../../helpers/reduceAuthenticatedUserDetails")); const config_1 = require("../../../config"); exports.default = async (req, res) => { const { userJwt } = req.body; const projectId = req.project.id; if (!userJwt) { res .status(400) .json({ error: "Missing userJwt", code: "auth/missing-jwt" }); return; } try { const jwtKeys = req.project.keys.jwt; if (!jwtKeys || !jwtKeys.publicKey) { res .status(403) .json({ error: "Missing JWT keys", code: "auth/missing-keys" }); return; } const publicKeyPem = Buffer.from(jwtKeys.publicKey, "base64").toString("utf-8"); const previousKeyPem = jwtKeys.previousPublicKey ? Buffer.from(jwtKeys.previousPublicKey, "base64").toString("utf-8") : null; let decoded; try { // Try verifying with the current public key decoded = jsonwebtoken_1.default.verify(userJwt, publicKeyPem, { algorithms: ["RS256"] }); } catch (err) { if (!previousKeyPem) { res .status(403) .json({ error: "Invalid token", code: "auth/invalid-token" }); return; } try { // Fallback to the previous public key if available decoded = jsonwebtoken_1.default.verify(userJwt, previousKeyPem, { algorithms: ["RS256"], }); } catch { res .status(403) .json({ error: "Invalid token", code: "auth/invalid-token" }); return; } } // `decoded` should now be the payload the external project signed const { sub: externalUserId, iss: providedProjectId, userData, } = decoded; if (projectId !== providedProjectId) { res .status(403) .json({ error: "Project ID mismatch", code: "auth/project-mismatch" }); return; } const { email, name, username, avatar, bio, location, birthdate, metadata, secureMetadata, } = userData; const { sequelize, refreshTokenSecret } = (0, config_1.getCoreConfig)(); const { user, refreshTokenJWT } = await sequelize.transaction(async (transaction) => { // Try to find an existing user by projectId + foreignId let user = (await models_1.User.findOne({ where: { projectId, foreignId: String(externalUserId) }, transaction, })); if (user) { // Update only if there's a change let shouldUpdate = false; if (email && user.email !== email) { user.email = email; shouldUpdate = true; } if (user.name !== name) { user.name = name; shouldUpdate = true; } if (user.username !== username) { user.username = username; shouldUpdate = true; } if (user.avatar !== avatar) { user.avatar = avatar; shouldUpdate = true; } if (user.bio !== bio) { user.bio = bio; shouldUpdate = true; } if (user.location !== location) { user.location = location; shouldUpdate = true; } if (user.birthdate !== birthdate) { user.birthdate = birthdate; shouldUpdate = true; } if (!(0, deepEqual_1.default)(user.metadata, metadata ?? {})) { user.metadata = metadata || {}; shouldUpdate = true; } if (!(0, deepEqual_1.default)(user.secureMetadata, secureMetadata ?? {})) { user.metadata = metadata || {}; shouldUpdate = true; } if (shouldUpdate) { await user.save({ transaction }); } } else { if (email) { // Try to find an existing user by projectId + email user = (await models_1.User.findOne({ where: { projectId, email }, transaction, })); } if (user) { if (email && user.email !== email) { user.email = email; } if (user.name !== name) { user.name = name; } if (user.username !== username) { user.username = username; } if (user.avatar !== avatar) { user.avatar = avatar; } if (user.bio !== bio) { user.bio = bio; } if (user.location !== location) { user.location = location; } if (user.birthdate !== birthdate) { user.birthdate = birthdate; } if (!(0, deepEqual_1.default)(user.metadata, metadata ?? {})) { user.metadata = metadata; } if (!(0, deepEqual_1.default)(user.secureMetadata, secureMetadata ?? {})) { user.metadata = metadata; } if (externalUserId) { user.referenceId = externalUserId; user.foreignId = externalUserId; } await user.save({ transaction }); } else { const newUserData = { projectId, referenceId: String(externalUserId), foreignId: String(externalUserId), role: "visitor", email, name, username, avatar, bio, location, birthdate, metadata, secureMetadata, }; const { projectId: _, ...restOfUserData } = newUserData; // Call the webhook to validate the user creation await (0, validateUserCreated_1.default)(req, res, { projectId, data: restOfUserData, }); // Create a new user if it doesn't exist user = (await models_1.User.create(newUserData, { transaction })); } } // Create a new Token entry const TokenEntry = (await models_2.Token.create({ userId: user.id, projectId, }, { transaction })); // Generate the JWT for the refresh token const refreshTokenJWT = jsonwebtoken_1.default.sign({ sub: user.id, projectId, aud: "replyke.com", iss: "replyke.com", jti: TokenEntry.id, }, refreshTokenSecret, { expiresIn: "30d" }); // Update the Token with the generated JWT TokenEntry.refreshToken = refreshTokenJWT; await TokenEntry.save({ transaction }); return { user, refreshTokenJWT }; }); const SuspensionModel = models_1.User.associations.suspensions .target; const ActiveSuspensionModel = SuspensionModel.scope({ method: ["active", new Date()], }); // 2) Re-fetch the user, this time including only active suspensions: const userWithSuspensions = (await models_1.User.findByPk(user.id, { include: [ { model: ActiveSuspensionModel, as: "suspensions", required: false, }, ], })); if (!userWithSuspensions) { res.status(500).json({ error: "Unexpected error fetching user after login", code: "auth/missing-user", }); return; } // Generate the JWT for the access token const accessTokenJWT = jsonwebtoken_1.default.sign({ sub: user.id, // Subject, representing the user ID projectId, // Project ID, indicating which project this token is tied to role: "user", // User role aud: "replyke.com", // Audience, your authentication service iss: "replyke.com", // Issuer, your service }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: "30m" } // Access token expiry ); res.cookie("replyke-refresh-jwt", refreshTokenJWT, { httpOnly: true, sameSite: "none", secure: true, maxAge: 30 * 24 * 60 * 60 * 1000, path: "/", }); const reducedAuthenticatedUser = (0, reduceAuthenticatedUserDetails_1.default)(userWithSuspensions); res.status(200).json({ success: true, accessToken: accessTokenJWT, refreshToken: refreshTokenJWT, user: reducedAuthenticatedUser, }); } catch (err) { console.error("Verification failed:", err); res.status(500).json({ error: "Internal server error", code: "auth/server-error", details: err.message, }); } };