routes/auth.jsjavascript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.authRouter = void 0;
const express_1 = require("express");
const zod_1 = require("zod");
const sequelize_1 = require("sequelize");
const UserService_1 = __importDefault(require("../services/UserService"));
const MailService_1 = __importDefault(require("../services/MailService"));
const NotificationService_1 = __importDefault(require("../services/NotificationService"));
const FileService_1 = __importDefault(require("../services/FileService"));
const validateMiddleware_1 = require("../middlewares/validateMiddleware.js");
const passport_1 = __importDefault(require("passport"));
const FeatureFlagService_1 = __importStar(require("../services/FeatureFlagService.js"));
const passport_2 = require("../plugins/passport.js");
const winston_1 = require("../plugins/winston.js");
const bcrypt = require("bcrypt");
const path = require("node:path");
const { default: AuthService } = require("../services/AuthService");
const registerSchema = zod_1.z.object({
username: zod_1.z.string().min(6),
email: zod_1.z.email().optional().nullable().default(null),
password: zod_1.z.string().min(8),
});
const loginSchema = zod_1.z.object({
username: zod_1.z.string().min(1),
password: zod_1.z.string().min(1),
});
const ssoRequestSchema = zod_1.z.object({
email: zod_1.z.email(),
});
const ssoSchema = zod_1.z.object({
ssoToken: zod_1.z.string().min(1),
});
const profilePictureParams = zod_1.z.object({
filename: zod_1.z.string().min(1),
});
const updateMeSchema = zod_1.z.object({
username: zod_1.z.string().min(6).optional(),
email: zod_1.z.email().optional(),
emailConfirmed: zod_1.z.boolean().optional(),
});
const mcpTokenSchema = zod_1.z.object({
expiresIn: zod_1.z.string().optional().default("30d"),
});
/**
* @swagger
* tags:
* name: Authorization & Account Management
* description: Endpoints for authentication and authorization
* components:
* requestBodies:
* RegistrationRequestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - username
* - email
* - password
* properties:
* username:
* type: string
* email:
* type: string
* format: email
* password:
* type: string
* format: password
* LoginRequestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - username
* - password
* properties:
* username:
* type: string
* password:
* type: string
* format: password
* ssoRequestRequestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - email
* properties:
* email:
* type: string
* format: email
* ssoRequestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - ssoToken
* properties:
* ssoToken:
* $ref: '#/components/schema/JWT'
* UserUpdateRequestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - username
* - email
* properties:
* username:
* type: string
* email:
* type: string
* format: email
*/
/** @ignore */
const router = (0, express_1.Router)();
exports.authRouter = router;
/**
* @openapi
* /auth/:
* post:
* description: Registration endpoint
* tags:
* - Authorization & Account Management
* requestBody:
* $ref: '#/components/requestBodies/RegistrationRequestBody'
* responses:
* 200:
* description: User successfully registered
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/JWT'
* 400:
* description: User account could not be created
* content:
* application/json:
* schema:
* type: string
* example: User already exists
*/
router.post("/", (0, validateMiddleware_1.validate)(registerSchema), (req, res, next) => {
const { username, password, email } = req.body;
const searchIdentifier = email ? [username, email] : username;
UserService_1.default.findDeletedByUsernameOrEmail(searchIdentifier)
.then((deletedUser) => {
if (deletedUser) {
res.status(403).send(req.t("errors.account-deleted"));
return next();
}
return createUser();
})
.catch((e) => next(e));
function createUser() {
UserService_1.default.create(username, password, email, false, req.locale)
.then((user) => {
const token = AuthService.generateAccessToken(user.id);
res.send(token);
next();
})
.catch((e) => {
if (e instanceof sequelize_1.UniqueConstraintError) {
const ue = e;
res
.status(400)
.send(Object.keys(ue.fields).includes("username") ||
Object.keys(ue.fields).includes("email")
? req.t("errors.user-already-exists")
: null);
return next();
}
next(e);
});
}
});
/**
* @openapi
* /auth/login:
* post:
* description: Login endpoint
* tags:
* - Authorization & Account Management
* requestBody:
* $ref: '#/components/requestBodies/LoginRequestBody'
* responses:
* 200:
* description: Login successful
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/JWT'
* 404:
* description: User was not found
*/
router.post("/login", (0, validateMiddleware_1.validate)(loginSchema), (req, res, next) => {
const { username, password } = req.body;
UserService_1.default.findByUsernameOrEmail(username, { scope: "withPasswordHash" })
.then(async (user) => {
if (!user) {
const deletedUser = await UserService_1.default.findDeletedByUsernameOrEmail(username);
if (deletedUser) {
res.status(403).send(req.t("errors.account-deleted"));
return;
}
res.status(404).send();
return;
}
if (!bcrypt.compareSync(password, user.password)) {
res.status(404).send();
return;
}
// if (user.email && !user.emailConfirmed)
// return res.status(400).send({
// type: "EmailUnconfirmed",
// message:
// "Du musst dein E-Mail-Adresse bestätigen, um dein Konto zu aktivieren",
// });
await UserService_1.default.update(user.id, { lastLoggedIn: Date.now() });
const token = AuthService.generateAccessToken(user.id);
res.send(token);
next();
})
.catch((e) => next(e));
});
/**
* @openapi
* /auth/ssoRequest:
* post:
* description: Single Sign-On Request endpoint
* tags:
* - Authorization & Account Management
* requestBody:
* $ref: '#/components/requestBodies/ssoRequestRequestBody'
* responses:
* 200:
* description: SSO link was sent
* content:
* application/json:
* schema:
* type: string
* example: Single Sign-On link was sent to your email inbox.
* 400:
* description: Email address not provided
* content:
* application/json:
* schema:
* type: string
* example: An email address is required.
*/
router.post("/ssoRequest", (0, validateMiddleware_1.validate)(ssoRequestSchema), (req, res, next) => {
const { email } = req.body;
AuthService.generateSsoToken(email, req.locale)
.then(() => {
res.send(req.t("responses.sso-link-sent")); // njsscan-ignore: express_xss
return next();
})
.catch((e) => next(e));
});
/**
* @openapi
* /auth/sso:
* post:
* description: Single Sign-On Login endpoint
* tags:
* - Authorization & Account Management
* requestBody:
* $ref: '#/components/requestBodies/ssoRequestBody'
* responses:
* 200:
* description: Login successful
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/JWT'
* 400:
* description: User was not found
* content:
* application/json:
* schema:
* type: string
* example: A SSO token is required. Please contact admin@choreo-planer.de
*/
router.post("/sso", (0, validateMiddleware_1.validate)(ssoSchema), (req, res, next) => {
const { ssoToken } = req.body;
AuthService.resolveSsoToken(ssoToken, req.locale)
.then((user) => {
if (!user) {
res.status(404).send(req.t("responses.user-not-found"));
return next();
}
const token = AuthService.generateAccessToken(user.id);
res.send(token);
return next();
})
.catch((e) => {
res.status(400).send(e.message);
return next();
});
});
/**
* @openapi
* /auth/me:
* get:
* description: User information endpoint
* tags:
* - Authorization & Account Management
* security:
* - userAuthentication: []
* responses:
* 200:
* description: Success
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/User'
* 401:
* $ref: '#/components/responses/UnauthorizedError'
*/
router.get("/me", AuthService.authenticateUser(), (req, res, next) => {
UserService_1.default.findById(req.actingUserId)
.then((user) => {
res.send(user);
})
.catch((e) => next(e));
});
/**
* @openapi
* /auth/me:
* put:
* description: User update endpoint, sends confirmation email if email is updated
* tags:
* - Authorization & Account Management
* security:
* - userAuthentication: []
* requestBody:
* $ref: '#/components/requestBodies/UserUpdateRequestBody'
* responses:
* 200:
* description: Update successful
* 401:
* $ref: '#/components/responses/UnauthorizedError'
* 404:
* description: User was not found
*/
router.put("/me", AuthService.authenticateUser(), (0, validateMiddleware_1.validate)(updateMeSchema), (req, res, next) => {
const { username, email, emailConfirmed } = req.body;
const data = { username, email, emailConfirmed };
if (email && email != req.ActingUser.email)
data.emailConfirmed = false;
UserService_1.default.update(req.actingUserId, data)
.then((user) => {
if (user && email != req.ActingUser.email)
return MailService_1.default.sendEmailConfirmationEmail(user.username, user.id, user.email, req.locale).then(() => {
res.send();
});
res.send(user);
})
.catch((e) => next(e));
});
/**
* @openapi
* /auth/me/profilePicture:
* put:
* description: Upload endpoint for profile picture
* tags:
* - Authorization & Account Management
* security:
* - userAuthentication: []
* requestBody:
* required: true
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* profilePicture:
* type: string
* format: binary
* responses:
* 200:
* description: Upload successful
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/User'
* 400:
* description: No file uploaded
* content:
* application/json:
* schema:
* type: string
* example: No file uploaded.
* 401:
* $ref: '#/components/responses/UnauthorizedError'
*/
router.put("/me/profilePicture", AuthService.authenticateUser(), FileService_1.default.singleFileMiddleware("profilePicture"), (req, res, next) => {
if (!req.file)
res.status(400).send(req.t("responses.no-file-uploaded"));
else {
const profilePictureExtension = req.file.filename.split(".").pop();
UserService_1.default.update(req.actingUserId, {
profilePictureExtension,
}).then((user) => {
FileService_1.default.clearProfilePictureFolder(req.actingUserId, profilePictureExtension);
res.send(user);
return next();
});
}
});
/**
* @openapi
* /auth/me/profilePicture/:filename:
* get:
* description: Profile picture retrieval endpoint
* tags:
* - Authorization & Account Management
* security:
* - userAuthentication: []
* parameters:
* - name: filename
* in: path
* description: file name of the stored profilePicture. Defaults to the user id + the profilePictureExtension
* required: true
* schema:
* type: string
* example: a7ddc23a-3220-46ae-b640-22d8d480f5d1.png
* responses:
* 200:
* description: Success
* 401:
* $ref: '#/components/responses/UnauthorizedError'
* 404:
* description: File not found
*/
router.get("/me/profilePicture/:filename", AuthService.authenticateUser(), (0, validateMiddleware_1.validate)(profilePictureParams, "params"), (req, res, next) => {
const { filename } = req.params;
const root = path.join(__dirname, "../../uploads/profilePictures");
if (filename.startsWith(req.actingUserId))
return res.sendFile(filename, { root });
return next();
});
/**
* @openapi
* /auth/me/profilePicture:
* delete:
* description: Delete the profile picture
* tags:
* - Authorization & Account Management
* security:
* - userAuthentication: []
* responses:
* 200:
* description: Deletion successful or no profile picture to be deleted
* 401:
* $ref: '#/components/responses/UnauthorizedError'
*/
router.delete("/me/profilePicture", AuthService.authenticateUser(), (req, res, next) => {
UserService_1.default.update(req.actingUserId, { profilePictureExtension: null })
.then(() => {
FileService_1.default.clearProfilePictureFolder(req.actingUserId);
res.send();
return next();
})
.catch((e) => next(e));
});
const supportedSocialProviders = [
passport_2.AuthProvider.GOOGLE,
passport_2.AuthProvider.GITHUB,
passport_2.AuthProvider.FACEBOOK,
];
async function checkSocialLoginEnabled(_req, res, next) {
const enabled = await FeatureFlagService_1.default.isEnabled(FeatureFlagService_1.FeatureFlagKey.SOCIAL_LOGIN);
if (!enabled) {
winston_1.logger.warn("Social login request blocked because social login is disabled");
return res.status(404).json({ error: "Social login is disabled" });
}
next();
}
// Important: place dynamic provider routes at the end so they don't shadow static routes like /me
router.get("/:provider", checkSocialLoginEnabled, (req, res, next) => {
const { provider } = req.params;
if (!supportedSocialProviders.includes(provider)) {
winston_1.logger.warn(`Unsupported social provider requested: ${provider}`);
return res.status(400).json({
error: `Unsupported provider: ${provider}`,
fix: "Use one of: google, github, facebook",
});
}
if (!(0, passport_2.isProviderConfigured)(provider)) {
winston_1.logger.warn(`Social provider ${provider} requested but not configured; check environment variables`);
return res.status(503).json({
error: `${provider} login is currently not available`,
});
}
const authOptions = {};
if (provider === passport_2.AuthProvider.GOOGLE) {
authOptions.scope = ["profile", "email"];
}
else if (provider === passport_2.AuthProvider.GITHUB) {
authOptions.scope = ["user:email"];
}
else if (provider === passport_2.AuthProvider.FACEBOOK) {
authOptions.scope = ["email"];
}
// passport-oauth2 requires `state: true` (boolean, not string) to activate
// the SessionStore which auto-generates and verifies a random state for CSRF.
// The @types/passport type has state?:string which is inaccurate here.
authOptions.state = true;
passport_1.default.authenticate(provider, authOptions)(req, res, next);
});
router.get("/:provider/callback", checkSocialLoginEnabled, (req, res, next) => {
const { provider } = req.params;
if (!supportedSocialProviders.includes(provider)) {
winston_1.logger.warn(`Unsupported social provider callback: ${provider}`);
return res.status(400).json({
error: `Unsupported provider: ${provider}`,
fix: "Use one of: google, github, facebook",
});
}
if (!(0, passport_2.isProviderConfigured)(provider)) {
winston_1.logger.warn(`Social provider ${provider} callback received but provider not configured`);
const redirectUrl = `${process.env.OAUTH_REDIRECT_BASE_URL}/auth/callback?error=provider_not_available&errorDescription=${encodeURIComponent(`${provider} login is currently not available`)}`;
return res.redirect(redirectUrl);
}
passport_1.default.authenticate(provider, { session: false }, (err, user) => {
if (err || !user) {
const errorCode = err instanceof Error
? "authentication_error"
: "authentication_failed";
const errorDescription = err instanceof Error
? err.message
: `Authentication callback failed for provider ${provider}`;
const redirectUrl = `${process.env.OAUTH_REDIRECT_BASE_URL}/auth/callback?error=${encodeURIComponent(errorCode)}&errorDescription=${encodeURIComponent(errorDescription)}`;
winston_1.logger.error(`Social auth callback error for provider=${provider}: ${errorDescription}`);
return res.redirect(redirectUrl);
}
const userId = user.id;
const token = AuthService.generateAccessToken(userId);
const redirectUrl = `${process.env.OAUTH_REDIRECT_BASE_URL}/auth/callback?token=${encodeURIComponent(token)}`;
winston_1.logger.debug(`Social auth callback succeeded for provider=${provider} userId=${userId}`);
return res.redirect(redirectUrl);
})(req, res, next);
});
/**
* @openapi
* /auth/me/resendEmailConfirmationLink:
* get:
* description: Resend the email confirmation link in a new email, creates a notification
* tags:
* - Authorization & Account Management
* security:
* - userAuthentication: []
* responses:
* 200:
* description: Email was sent
* 401:
* $ref: '#/components/responses/UnauthorizedError'
*/
router.get("/me/resendEmailConfirmationLink", AuthService.authenticateUser(), (req, res, next) => {
UserService_1.default.findById(req.actingUserId)
.then((user) => {
if (!user) {
res.status(404).send(req.t("responses.user-not-found"));
return next();
}
return MailService_1.default.sendEmailConfirmationEmail(user.username, user.id, user.email, req.locale).then(() => {
NotificationService_1.default.createOne(req.t("notifications.confirm-email.title"), req.t("notifications.confirm-email.message"), user.id);
res.send();
return next();
});
})
.catch((e) => next(e));
});
router.post("/mcp-token", AuthService.authenticateUser(), (0, validateMiddleware_1.validate)(mcpTokenSchema), (req, res) => {
const { expiresIn } = req.body;
const token = AuthService.generateAccessToken(req.actingUserId, {
expiresIn,
});
res.send({ token });
});