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 });
});