services/AuthService.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 });
const user_1 = __importDefault(require("../db/models/user"));
const UserService_1 = __importDefault(require("./UserService"));
const UserAccessService_1 = __importDefault(require("./UserAccessService"));
const FeatureFlagService_1 = __importStar(require("./FeatureFlagService"));
const MailService_1 = __importDefault(require("./MailService"));
const NotificationService_1 = __importDefault(require("./NotificationService"));
const AdminService_1 = __importDefault(require("./AdminService"));
const errors_1 = require("../utils/errors.js");
const jwt = require("jsonwebtoken");
const { logger } = require("../plugins/winston");
const basicAuth = require("express-basic-auth");
const bcrypt = require("bcrypt");
const i18n = require("i18n");
const _express = require("express");
/**
 * Secret to encode the JWTs with
 *
 * @type {string}
 * @ignore
 */
const TOKEN_SECRET = process.env.TOKEN_SECRET;
/**
 * Description string for expiration time of JWTs
 *
 * @type {string}
 * @ignore
 */
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN;
/**
 * Service for authentication operations.
 * Handles user login, registration, and authentication logic.
 *
 * @class AuthService
 */
class AuthService {
    /**
     * Generates an access token
     *
     * @param {UUID} UserId
     * @param {Object} [options={}]
     * @param {string | undefined | null} [options.expiresIn=null]
     * @returns {string}
     */
    generateAccessToken(UserId, { expiresIn = null } = {}) {
        logger.debug(`AuthService generateAccessToken ${JSON.stringify({ UserId, expiresIn })}`);
        return jwt.sign({ UserId }, TOKEN_SECRET, {
            expiresIn: expiresIn || JWT_EXPIRES_IN,
        });
    }
    /**
     * Middleware generator for user authentication
     *
     * @param {boolean} [failIfNotLoggedIn=true]
     * @returns {function(): any} middleware function
     */
    authenticateUser(failIfNotLoggedIn = true) {
        /**
         * @param {Request} req
         * @param {Response} res
         * @param {NextFunction} next
         * @returns {any}
         */
        return function (req, res, next) {
            const authHeader = req.headers?.authorization;
            const token = authHeader && authHeader.split(" ")[1];
            if (!token) {
                if (!failIfNotLoggedIn)
                    return next();
                return res.status(401).send();
            }
            jwt.verify(token, TOKEN_SECRET, async (err, content) => {
                if (err) {
                    if (!failIfNotLoggedIn)
                        return next();
                    return res.status(403).send();
                }
                user_1.default.findByPk(content.UserId)
                    .then(async (user) => {
                    if (!user) {
                        if (!failIfNotLoggedIn)
                            return next();
                        return res.status(403).send();
                    }
                    req.actingUserId = user.id;
                    req.ActingUser = user;
                    const accessSharingEnabled = await FeatureFlagService_1.default.isEnabled(FeatureFlagService_1.FeatureFlagKey.ACCESS_SHARING);
                    if (accessSharingEnabled) {
                        const ownerAccess = await UserAccessService_1.default.getOwners(user.id);
                        if (ownerAccess.length > 0) {
                            req.ownerIds = ownerAccess.map((oa) => oa.ownerUserId);
                            req.Owners = ownerAccess.map((oa) => oa.owner);
                        }
                        else {
                            req.ownerIds = [user.id];
                            req.Owners = [user];
                        }
                        logger.debug(`User ${user.username} authenticated. Token: ${token}, ownerRoles: ${JSON.stringify(ownerAccess.map((oa) => ({
                            ownerId: oa.ownerUserId,
                            ownerUsername: oa.owner?.username,
                            role: oa.role,
                            enabled: oa.enabled,
                        })))}`);
                    }
                    else {
                        req.ownerIds = [];
                        req.Owners = [];
                        logger.debug(`User ${user.username} authenticated. Token: ${token}, ownerRoles: []`);
                    }
                    next();
                })
                    .catch((e) => next(e));
            });
        };
    }
    /**
     * Resolves Single Sign-On Tokens to a user it belongs to
     *
     * @param {string} token
     * @param {string} [locale="en"]
     * @returns {Promise<User>}
     * @throws {Error} Token has to be a valid and not-expired JWT token
     * @throws {Error} User the token belongs to has to (still) exist
     */
    resolveSsoToken(token, locale = "en") {
        return new Promise((resolve, reject) => {
            jwt.verify(token, TOKEN_SECRET, async (err, content) => {
                if (err) {
                    return reject(new errors_1.AuthorizationError(i18n.__({ phrase: "errors.invalid-sso-token", locale })));
                }
                const user = await UserService_1.default.findById(content.UserId);
                if (!user) {
                    const deletedUser = await UserService_1.default.findDeletedByUsernameOrEmail(content.UserId);
                    if (deletedUser) {
                        return reject(new errors_1.AuthorizationError(i18n.__({ phrase: "errors.account-deleted", locale })));
                    }
                    return reject(new errors_1.AuthorizationError(i18n.__({ phrase: "errors.sso-token-user-missing", locale })));
                }
                logger.debug(`User ${user.username} with id ${user.id} used this SSO token: ${token}`);
                resolve(user);
            });
        });
    }
    /**
     * Generate a Single Sign-On token for login via login-link
     *
     * @param {string} email Username or email address of a user
     * @param {string} [locale="en"]
     * @returns {Promise}
     * @throws {Error} User with give email/username has to (still) exist
     * @throws {Error} User with given username has to have an email address
     */
    generateSsoToken(email, locale = "en") {
        logger.debug(`AuthService generateSsoToken ${JSON.stringify({ email, locale })}`);
        return UserService_1.default.findByUsernameOrEmail(email).then(async (user) => {
            if (!user) {
                const deletedUser = await UserService_1.default.findDeletedByUsernameOrEmail(email);
                if (deletedUser) {
                    throw new errors_1.AuthorizationError(i18n.__({ phrase: "errors.account-deleted", locale }));
                }
                logger.warn(`No user with email ${email} found`);
                throw new errors_1.NotFoundError(i18n.__({ phrase: "errors.entity-not-found", locale }, { entity: "user" }));
            }
            if (!user.email) {
                logger.warn("Found user has no email");
                throw new errors_1.FaultyInputError(i18n.__({ phrase: "errors.user-has-no-email", locale }));
            }
            const token = this.generateAccessToken(user.id, {
                expiresIn: process.env.SSO_TOKEN_EXPIRES_IN,
            });
            return MailService_1.default.sendSsoEmail(user.email, user.username, token, locale).then(() => NotificationService_1.default.createOne(i18n.__({
                phrase: "notifications.sso-link-was-sent.title",
                locale,
            }), i18n.__({
                phrase: "notifications.sso-link-was-sent.message",
                locale,
            }), user.id));
        });
    }
    /**
     * Middleware generator for admin authentication
     *
     * @returns {function(): any}
     */
    authenticateAdmin() {
        /**
         * Authorizes an admin user based on username and password.
         * @param {string} username - Admin's username.
         * @param {string} password - Admin's password.
         * @param {function} callback - Callback function to handle authorization result.
         * @returns {void}
         */
        function myAuthorizer(username, password, callback) {
            return AdminService_1.default.findByUsername(username, {
                scope: "withPasswordHash",
            })
                .then((admin) => {
                if (!admin || !bcrypt.compareSync(password, admin.password))
                    return callback(null, false);
                return callback(null, true);
            })
                .catch((e) => {
                logger.error(e);
                return callback(null, false);
            });
        }
        /**
         * @param {_express.Request} req
         * @param {_express.Response} res
         * @param {_express.NextFunction} next
         * @returns {any}
         */
        return basicAuth({
            authorizer: myAuthorizer,
            authorizeAsync: true,
            challenge: true,
            realm: "ChoreoPlanerAdmin",
        });
    }
    /**
     * Middleware for resolving admin auth tokens to the Admin
     *
     * @param {_express.Request} req
     * @param {_express.Response} res
     * @param {_express.NextFunction} next
     * @returns {Promise}
     */
    resolveAdmin(req, _res, next) {
        const authHeader = req.headers?.authorization;
        const token = authHeader && authHeader.split(" ")[1];
        if (!token) {
            return next(new errors_1.AuthorizationError("No token provided"));
        }
        const [username, ..._] = atob(token).split(":");
        return AdminService_1.default.findByUsername(username, {
            scope: "withPasswordHash",
        })
            .then((admin) => {
            if (admin) {
                req.AdminId = admin.id;
                req.Admin = admin;
            }
            next();
        })
            .catch((e) => next(e));
    }
}
exports.default = new AuthService();