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