@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
JavaScript
"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,
});
}
};