@krypton-org/krypton-auth
Version:
Express authentication middleware, using GraphQL and JSON Web Tokens.
419 lines • 17.6 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/**
* Service logic for user management.
* @module controllers/UserController
*/
const ejs_1 = __importDefault(require("ejs"));
const agenda_1 = __importDefault(require("../agenda/agenda"));
const config_1 = __importDefault(require("../config"));
const TokenGenerator_1 = __importDefault(require("../crypto/TokenGenerator"));
const ErrorTypes_1 = require("../error/ErrorTypes");
const SessionModel_1 = __importDefault(require("../model/SessionModel"));
const UserModel_1 = __importDefault(require("../model/UserModel"));
const TOKEN_LENGTH = 64;
const DELAY_TO_CHANGE_PASSWORD_IN_MINUTS = 60;
/**
* Return true if user issuing the request is logged in.
* @param {Request} req
* @returns {boolean} user is logged in
*/
const isUserLoggedIn = (req) => req.user !== undefined;
/**
* Returns true if a notificaiton should be sent to client with the preview link in case of a mock email.
* @param {Request} req
* @returns {boolean} true if a notificaiton should be sent to client with the preview link in case of a mock email
*/
const isMockEmailAndClientCanReceivePreview = (req) => {
return !config_1.default.nodemailerConfig && config_1.default.graphiql && req.cookies.clientId;
};
/**
* Send confirmation email to `user`.
* @param {any} user - user
* @param {string} confirmationToken
* @param {string} host - Service public address
* @returns {void}
*/
const sendConfirmationEmail = (user, confirmationToken, host, clientId) => {
agenda_1.default.now('email', {
clientId,
locals: {
link: host + '/user/email/confirmation?token=' + confirmationToken,
user,
},
recipient: user.email,
subject: 'Activate your account',
template: config_1.default.verifyEmailTemplate,
});
};
/**
* Returns the user data of the logged in user.
* @throws {UnauthorizedError} User does not exist
* @param {Request} req
* @param {Response} res
* @returns {Promise<{ user: any }>} Promise to the user data
*/
exports.getUser = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
try {
return yield UserModel_1.default.findById(req.user._id);
}
catch (err) {
throw new ErrorTypes_1.UnauthorizedError('User not found, please log in.');
}
});
/**
* Returns true email address not already taken by another user.
* @param {string} email
* @returns {Promise<boolean>} Promise returning true if`isAvailable`
*/
exports.checkEmailAvailable = (email) => __awaiter(void 0, void 0, void 0, function* () {
const emailExists = yield UserModel_1.default.userExists({ email });
return !emailExists;
});
/**
* Verifying link clicked by users in the account verification email.
* @throws {UnauthorizedError} User does not exist
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @renders notification page informing users of the operation success.
*/
exports.confirmEmail = (req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
const notifications = [];
const token = req.query.token;
try {
const exists = yield UserModel_1.default.userExists({ verificationToken: token });
if (!exists) {
throw new ErrorTypes_1.UnauthorizedError('This link is no longer valid.');
}
const user = yield UserModel_1.default.getUser({ verificationToken: token });
yield UserModel_1.default.updateUser({ email: user.email }, { verificationToken: null, email_verified: true });
notifications.push({ type: 'success', message: 'You are now verified.' });
}
catch (err) {
notifications.push({ type: 'error', message: 'This link is not valid.' });
}
finally {
ejs_1.default.renderFile(config_1.default.notificationPageTemplate, { notifications }, { notifications }, (err, html) => {
if (err) {
next(err);
}
else {
res.send(html);
}
});
}
});
/**
* Create a new user.
* @throws {UsernameAlreadyExistsError}
* @throws {EmailAlreadyExistsError}
* @throws {UserValidationError}
* @param {any} user
* @param {Request} req
* @returns {Promise<boolean>} Promise returning true on request success
*/
exports.createUser = (user, req) => __awaiter(void 0, void 0, void 0, function* () {
user.verificationToken = TokenGenerator_1.default(TOKEN_LENGTH);
if (!user.password) {
throw new ErrorTypes_1.UserValidationError('Please provide a password.');
}
if (user.password.length < 8) {
throw new ErrorTypes_1.UserValidationError('The password must contain at least 8 characters.');
}
try {
yield UserModel_1.default.createUser(user);
let clientId;
if (isMockEmailAndClientCanReceivePreview(req)) {
clientId = req.cookies.clientId;
}
sendConfirmationEmail(user, user.verificationToken, config_1.default.getRouterAddress(req), clientId);
return true;
}
catch (err) {
if (err.message.includes('email') && err.message.includes('duplicate key')) {
throw new ErrorTypes_1.EmailAlreadyExistsError('Email already exists.');
}
throw new ErrorTypes_1.UserValidationError(err.message.replace('user validation failed: email: Path ', ''));
}
});
/**
* Resend an account verification email to logged in user.
* @throws {UnauthorizedError}
* @throws {EmailAlreadyConfirmedError}
* @param {Request} req
* @returns {Promise<boolean>} Promise returning true on request success
*/
exports.resendConfirmationEmail = (req) => __awaiter(void 0, void 0, void 0, function* () {
if (!isUserLoggedIn(req)) {
throw new ErrorTypes_1.UnauthorizedError('Please login.');
}
const user = yield UserModel_1.default.getUser({ _id: req.user._id });
if (user.email_verified) {
throw new ErrorTypes_1.EmailAlreadyConfirmedError('Your email adress has already been confirmed.');
}
else {
let clientId;
if (isMockEmailAndClientCanReceivePreview(req)) {
clientId = req.cookies.clientId;
}
sendConfirmationEmail(req.user, user.verificationToken, config_1.default.getRouterAddress(req), clientId);
}
return true;
});
/**
* Updating user password from the password recovery form.
* @throws {UnauthorizedError}
* @throws {UpdatePasswordTooLateError}
* @param {string} password - new password
* @param {string} passwordRecoveryToken - token guaranteeing user identity
* @returns {Promise<{ notifications: Notification[] }>} Promise to the notifications of success or failure
*/
exports.recoverPassword = (password, passwordRecoveryToken) => __awaiter(void 0, void 0, void 0, function* () {
const notifications = [];
if (password.length < 8) {
throw new ErrorTypes_1.UserValidationError('The password must contain at least 8 characters.');
}
const userExists = yield UserModel_1.default.userExists({ passwordRecoveryToken });
if (!userExists) {
throw new ErrorTypes_1.UnauthorizedError('Unvalid token.');
}
const user = yield UserModel_1.default.getUser({ passwordRecoveryToken });
const resetDate = new Date(user.passwordRecoveryRequestDate);
const actualDate = new Date();
const diff = Math.abs(actualDate.getTime() - resetDate.getTime());
const minutes = Math.floor(diff / 1000 / 60);
if (minutes >= DELAY_TO_CHANGE_PASSWORD_IN_MINUTS) {
throw new ErrorTypes_1.UpdatePasswordTooLateError('This link has expired, please ask a new one.');
}
yield UserModel_1.default.updateUser({ _id: user.id }, { password, passwordRecoveryToken: undefined, passwordRecoveryRequestDate: undefined });
notifications.push({ type: 'success', message: 'Your password is updated.' });
return { notifications };
});
/**
* Update the different user fields of logged in user.
* @throws {UnauthorizedError}
* @throws {TokenEncryptionError}
* @throws {WrongPasswordError}
* @throws {UsernameAlreadyExistsError}
* @throws {EmailAlreadyExistsError}
* @throws {UserValidationError}
* @param {any} userUpdates - fields to update
* @param {Request} req
* @param {Response} res
* @returns {Promise<{ user: any; notifications: Notification[] }>} Promise to the new user data and the notifications of success or failure
*/
exports.updateUser = (userUpdates, req, res) => __awaiter(void 0, void 0, void 0, function* () {
if (!isUserLoggedIn(req) || !(yield SessionModel_1.default.isValid(req.user._id, req.cookies.refreshToken))) {
res.status(401);
throw new ErrorTypes_1.UnauthorizedError('Please login.');
}
if (userUpdates.password && userUpdates.password !== userUpdates.previousPassword) {
const isValid = yield UserModel_1.default.isPasswordValid({ email: req.user.email }, userUpdates.previousPassword);
if (!isValid) {
throw new ErrorTypes_1.WrongPasswordError('Your previous password is wrong.');
}
if (userUpdates.password.length < 8) {
throw new ErrorTypes_1.UserValidationError('The password must contain at least 8 characters.');
}
delete userUpdates.previousPassword;
}
try {
let isEmailVerified = true;
if (req.user.email_verified && userUpdates.email) {
userUpdates.verificationToken = TokenGenerator_1.default(TOKEN_LENGTH);
userUpdates.email_verified = false;
isEmailVerified = false;
}
yield UserModel_1.default.updateUser({ _id: req.user._id }, userUpdates);
req.user = yield UserModel_1.default.getUserNonInternalFields({ _id: req.user._id });
if (!isEmailVerified) {
let clientId;
if (isMockEmailAndClientCanReceivePreview(req)) {
clientId = req.cookies.clientId;
}
sendConfirmationEmail(req.user, userUpdates.verificationToken, config_1.default.getRouterAddress(req), clientId);
}
const payload = yield UserModel_1.default.refreshAuthToken({ _id: req.user._id }, config_1.default.privateKey);
const { refreshToken, expiryDate } = yield SessionModel_1.default.updateSession(req.user._id, req.cookies.refreshToken);
const params = { httpOnly: true, expires: expiryDate };
if (config_1.default.host) {
params.domain = '.' + config_1.default.getDomainAddress();
}
res.cookie('refreshToken', refreshToken, params);
return payload;
}
catch (err) {
if (err.message.includes('email') && err.message.includes('duplicate key')) {
throw new ErrorTypes_1.EmailAlreadyExistsError('Email already exists.');
}
throw new ErrorTypes_1.UserValidationError(err.message.replace('user validation failed: email: ', ''));
}
});
/**
* Delete logged in user.
* @throws {UnauthorizedError}
* @throws {WrongPasswordError}
* @param {string} password
* @param {Request} req
* @returns {Promise<boolean>}
*/
exports.deleteUser = (password, req) => __awaiter(void 0, void 0, void 0, function* () {
if (!isUserLoggedIn(req)) {
throw new ErrorTypes_1.UnauthorizedError('Please login.');
}
const isValid = yield UserModel_1.default.isPasswordValid({ _id: req.user._id }, password);
if (!isValid) {
throw new ErrorTypes_1.WrongPasswordError('You entered a wrong password.');
}
yield UserModel_1.default.removeUser({ _id: req.user._id });
return true;
});
/**
* User log-out.
* @throws {UnauthorizedError}
* @returns {Promise<boolean>}
*/
exports.logout = (req) => __awaiter(void 0, void 0, void 0, function* () {
const { user, session } = yield SessionModel_1.default.getUserAndSessionFromRefreshToken(req.cookies.refreshToken);
if (user && session) {
yield SessionModel_1.default.removeSession(req.user._id, req.cookies.refreshToken);
}
else {
throw new ErrorTypes_1.UnauthorizedError('Please login.');
}
return true;
});
/**
* User log-in.
* @throws {UserNotFoundError}
* @throws {TokenEncryptionError}
* @param {string} email
* @param {string} password
* @param {Request} req
* @param {Response} res
* @returns {Promise<{ token: string; user: any }>} Promise to the user token and user data.
*/
exports.login = (email, password, req, res) => __awaiter(void 0, void 0, void 0, function* () {
let payload;
const emailExists = yield UserModel_1.default.userExists({ email });
if (emailExists) {
payload = yield UserModel_1.default.sign({ email }, password, config_1.default.privateKey);
}
else {
throw new ErrorTypes_1.UserNotFoundError('Wrong credentials.');
}
if (req.cookies.refreshToken) {
yield SessionModel_1.default.removeSession(payload.user._id, req.cookies.refreshToken);
}
yield SessionModel_1.default.removeOutdatedSessions(payload.user._id);
const { refreshToken, expiryDate } = yield SessionModel_1.default.createSession(payload.user._id);
const params = { httpOnly: true, expires: expiryDate };
if (config_1.default.host) {
params.domain = '.' + config_1.default.getDomainAddress();
}
res.cookie('refreshToken', refreshToken, params);
return payload;
});
/**
* Send password recovery email, when a user has lost his password.
* @throws {AlreadyLoggedInError}
* @param {string} email
* @param {Request} req
* @returns {Promise<boolean} Returns true
*/
exports.sendPasswordRecoveryEmail = (email, req) => __awaiter(void 0, void 0, void 0, function* () {
if (req.user !== undefined) {
throw new ErrorTypes_1.AlreadyLoggedInError('Oups, you are already logged in.');
}
const exists = yield UserModel_1.default.userExists({ email });
if (!exists) {
return true;
}
const passwordRecoveryToken = TokenGenerator_1.default(TOKEN_LENGTH);
const passwordRecoveryRequestDate = new Date();
yield UserModel_1.default.updateUser({ email }, { passwordRecoveryToken, passwordRecoveryRequestDate });
const user = yield UserModel_1.default.getUser({ email });
const host = config_1.default.getRouterAddress(req);
let clientId;
if (isMockEmailAndClientCanReceivePreview(req)) {
clientId = req.cookies.clientId;
}
agenda_1.default.now('email', {
clientId,
locals: {
link: host + '/form/reset/password?token=' + passwordRecoveryToken,
user,
},
recipient: email,
subject: 'Password Recovery',
template: config_1.default.resetPasswordEmailTemplate,
});
return true;
});
/**
* Send an HTML page with the password reset form.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns an HTML page with the password reset form
*/
exports.resetPasswordForm = (req, res, next) => {
const notifications = [];
if (req.user) {
notifications.push({ type: 'error', message: 'Oups, you are already logged in.' });
res.json({ notifications });
return;
}
const host = config_1.default.getRouterAddress(req);
const locals = {
link: host,
token: req.query.token,
};
ejs_1.default.renderFile(config_1.default.resetPasswordFormTemplate, locals, {}, (err, html) => {
if (err) {
next(err);
}
else {
res.send(html);
}
});
};
/**
* Refresh the auth token and the refresh token. This last one is set in an httpOnly cookie.
* @throws {UnauthorizedError}
* @throws {TokenEncryptionError}
* @param {Request} req
* @param {Response} res
* @returns {Promise<{ token: string; expiryDate: Date }>} Promise to the new authentication token and its expiry date.
*/
exports.refreshTokens = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const { user, session } = yield SessionModel_1.default.getUserAndSessionFromRefreshToken(req.cookies.refreshToken);
const now = new Date();
if (user && session && now.getTime() < session.expiryDate) {
const payload = yield UserModel_1.default.refreshAuthToken({ _id: user._id }, config_1.default.privateKey);
const { refreshToken, expiryDate } = yield SessionModel_1.default.updateSession(user._id, session.refreshToken);
const params = { httpOnly: true, expires: expiryDate };
if (config_1.default.host) {
params.domain = '.' + config_1.default.getDomainAddress();
}
res.cookie('refreshToken', refreshToken, params);
return payload;
}
else {
throw new ErrorTypes_1.UnauthorizedError('Please login.');
}
});
//# sourceMappingURL=UserController.js.map