* ⚡ Enable `esModuleInterop` for /core * ⚡ Adjust imports in /core * ⚡ Enable `esModuleInterop` for /cli * ⚡ Adjust imports in /cli * ⚡ Enable `esModuleInterop` for /nodes-base * ⚡ Adjust imports in /nodes-base * ⚡ Make imports consistent * ⬆️ Upgrade TypeScript to 4.6 (#3109) * ⬆️ Upgrade TypeScript to 4.6 * 📦 Update package-lock.json * 🔧 Avoid erroring on untyped errors * 📘 Fix type error Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
219 lines
6.4 KiB
TypeScript
219 lines
6.4 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
/* eslint-disable import/no-cycle */
|
|
|
|
import express from 'express';
|
|
import { v4 as uuid } from 'uuid';
|
|
import { URL } from 'url';
|
|
import validator from 'validator';
|
|
import { IsNull, MoreThanOrEqual, Not } from 'typeorm';
|
|
import { LoggerProxy as Logger } from 'n8n-workflow';
|
|
|
|
import { Db, InternalHooksManager, ResponseHelper } from '../..';
|
|
import { N8nApp } from '../Interfaces';
|
|
import { getInstanceBaseUrl, hashPassword, validatePassword } from '../UserManagementHelper';
|
|
import * as UserManagementMailer from '../email';
|
|
import type { PasswordResetRequest } from '../../requests';
|
|
import { issueCookie } from '../auth/jwt';
|
|
import * as config from '../../../config';
|
|
|
|
export function passwordResetNamespace(this: N8nApp): void {
|
|
/**
|
|
* Send a password reset email.
|
|
*
|
|
* Authless endpoint.
|
|
*/
|
|
this.app.post(
|
|
`/${this.restEndpoint}/forgot-password`,
|
|
ResponseHelper.send(async (req: PasswordResetRequest.Email) => {
|
|
if (config.getEnv('userManagement.emails.mode') === '') {
|
|
Logger.debug('Request to send password reset email failed because emailing was not set up');
|
|
throw new ResponseHelper.ResponseError(
|
|
'Email sending must be set up in order to request a password reset email',
|
|
undefined,
|
|
500,
|
|
);
|
|
}
|
|
|
|
const { email } = req.body;
|
|
|
|
if (!email) {
|
|
Logger.debug(
|
|
'Request to send password reset email failed because of missing email in payload',
|
|
{ payload: req.body },
|
|
);
|
|
throw new ResponseHelper.ResponseError('Email is mandatory', undefined, 400);
|
|
}
|
|
|
|
if (!validator.isEmail(email)) {
|
|
Logger.debug(
|
|
'Request to send password reset email failed because of invalid email in payload',
|
|
{ invalidEmail: email },
|
|
);
|
|
throw new ResponseHelper.ResponseError('Invalid email address', undefined, 400);
|
|
}
|
|
|
|
// User should just be able to reset password if one is already present
|
|
const user = await Db.collections.User!.findOne({ email, password: Not(IsNull()) });
|
|
|
|
if (!user || !user.password) {
|
|
Logger.debug(
|
|
'Request to send password reset email failed because no user was found for the provided email',
|
|
{ invalidEmail: email },
|
|
);
|
|
return;
|
|
}
|
|
|
|
user.resetPasswordToken = uuid();
|
|
|
|
const { id, firstName, lastName, resetPasswordToken } = user;
|
|
|
|
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 7200;
|
|
|
|
await Db.collections.User!.update(id, { resetPasswordToken, resetPasswordTokenExpiration });
|
|
|
|
const baseUrl = getInstanceBaseUrl();
|
|
const url = new URL(`${baseUrl}/change-password`);
|
|
url.searchParams.append('userId', id);
|
|
url.searchParams.append('token', resetPasswordToken);
|
|
|
|
try {
|
|
const mailer = await UserManagementMailer.getInstance();
|
|
await mailer.passwordReset({
|
|
email,
|
|
firstName,
|
|
lastName,
|
|
passwordResetUrl: url.toString(),
|
|
domain: baseUrl,
|
|
});
|
|
} catch (error) {
|
|
void InternalHooksManager.getInstance().onEmailFailed({
|
|
user_id: user.id,
|
|
message_type: 'Reset password',
|
|
});
|
|
if (error instanceof Error) {
|
|
throw new ResponseHelper.ResponseError(
|
|
`Please contact your administrator: ${error.message}`,
|
|
undefined,
|
|
500,
|
|
);
|
|
}
|
|
}
|
|
|
|
Logger.info('Sent password reset email successfully', { userId: user.id, email });
|
|
void InternalHooksManager.getInstance().onUserTransactionalEmail({
|
|
user_id: id,
|
|
message_type: 'Reset password',
|
|
});
|
|
|
|
void InternalHooksManager.getInstance().onUserPasswordResetRequestClick({
|
|
user_id: id,
|
|
});
|
|
}),
|
|
);
|
|
|
|
/**
|
|
* Verify password reset token and user ID.
|
|
*
|
|
* Authless endpoint.
|
|
*/
|
|
this.app.get(
|
|
`/${this.restEndpoint}/resolve-password-token`,
|
|
ResponseHelper.send(async (req: PasswordResetRequest.Credentials) => {
|
|
const { token: resetPasswordToken, userId: id } = req.query;
|
|
|
|
if (!resetPasswordToken || !id) {
|
|
Logger.debug(
|
|
'Request to resolve password token failed because of missing password reset token or user ID in query string',
|
|
{
|
|
queryString: req.query,
|
|
},
|
|
);
|
|
throw new ResponseHelper.ResponseError('', undefined, 400);
|
|
}
|
|
|
|
// Timestamp is saved in seconds
|
|
const currentTimestamp = Math.floor(Date.now() / 1000);
|
|
|
|
const user = await Db.collections.User!.findOne({
|
|
id,
|
|
resetPasswordToken,
|
|
resetPasswordTokenExpiration: MoreThanOrEqual(currentTimestamp),
|
|
});
|
|
|
|
if (!user) {
|
|
Logger.debug(
|
|
'Request to resolve password token failed because no user was found for the provided user ID and reset password token',
|
|
{
|
|
userId: id,
|
|
resetPasswordToken,
|
|
},
|
|
);
|
|
throw new ResponseHelper.ResponseError('', undefined, 404);
|
|
}
|
|
|
|
Logger.info('Reset-password token resolved successfully', { userId: id });
|
|
void InternalHooksManager.getInstance().onUserPasswordResetEmailClick({
|
|
user_id: id,
|
|
});
|
|
}),
|
|
);
|
|
|
|
/**
|
|
* Verify password reset token and user ID and update password.
|
|
*
|
|
* Authless endpoint.
|
|
*/
|
|
this.app.post(
|
|
`/${this.restEndpoint}/change-password`,
|
|
ResponseHelper.send(async (req: PasswordResetRequest.NewPassword, res: express.Response) => {
|
|
const { token: resetPasswordToken, userId, password } = req.body;
|
|
|
|
if (!resetPasswordToken || !userId || !password) {
|
|
Logger.debug(
|
|
'Request to change password failed because of missing user ID or password or reset password token in payload',
|
|
{
|
|
payload: req.body,
|
|
},
|
|
);
|
|
throw new ResponseHelper.ResponseError(
|
|
'Missing user ID or password or reset password token',
|
|
undefined,
|
|
400,
|
|
);
|
|
}
|
|
|
|
const validPassword = validatePassword(password);
|
|
|
|
// Timestamp is saved in seconds
|
|
const currentTimestamp = Math.floor(Date.now() / 1000);
|
|
|
|
const user = await Db.collections.User!.findOne({
|
|
id: userId,
|
|
resetPasswordToken,
|
|
resetPasswordTokenExpiration: MoreThanOrEqual(currentTimestamp),
|
|
});
|
|
|
|
if (!user) {
|
|
Logger.debug(
|
|
'Request to resolve password token failed because no user was found for the provided user ID and reset password token',
|
|
{
|
|
userId,
|
|
resetPasswordToken,
|
|
},
|
|
);
|
|
throw new ResponseHelper.ResponseError('', undefined, 404);
|
|
}
|
|
|
|
await Db.collections.User!.update(userId, {
|
|
password: await hashPassword(validPassword),
|
|
resetPasswordToken: null,
|
|
resetPasswordTokenExpiration: null,
|
|
});
|
|
|
|
Logger.info('User password updated successfully', { userId });
|
|
|
|
await issueCookie(res, user);
|
|
}),
|
|
);
|
|
}
|