mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
* ⚡ Declutter test logs * 🐛 Fix random passwords length * 🐛 Fix password hashing in test user creation * 🐛 Hash leftover password * ⚡ Improve error message for `compare` * ⚡ Restore `randomInvalidPassword` contant * ⚡ Mock Telemetry module to prevent `--forceExit` * 🔥 Remove unused imports * 🔥 Remove unused import * ⚡ Add util for configuring test SMTP * ⚡ Isolate user creation * 🔥 De-duplicate `createFullUser` * ⚡ Centralize hashing * 🔥 Remove superfluous arg * 🔥 Remove outdated comment * ⚡ Prioritize shared tables during trucation * 🧪 Add login tests * ⚡ Use token helper * ✏️ Improve naming * ⚡ Make `createMemberShell` consistent * 🔥 Remove unneeded helper * 🔥 De-duplicate `beforeEach` * ✏️ Improve naming * 🚚 Move `categorize` to utils * ✏️ Update comment * 🧪 Simplify test * 📘 Improve `User.password` type * ⚡ Silence logger * ⚡ Simplify condition * ⚡ Unhash password in payload * 🐛 Fix comparison against unhashed password * ⚡ Increase timeout for fake SMTP service * 🔥 Remove unneeded import * ⚡ Use `isNull()` * 🧪 Use `Promise.all()` in creds tests * 🧪 Use `Promise.all()` in me tests * 🧪 Use `Promise.all()` in owner tests * 🧪 Use `Promise.all()` in password tests * 🧪 Use `Promise.all()` in users tests * ⚡ Re-set cookie if UM disabled * 🔥 Remove repeated line * ⚡ Refactor out shared owner data * 🔥 Remove unneeded import * 🔥 Remove repeated lines * ⚡ Organize imports * ⚡ Reuse helper * 🚚 Rename tests to match routers * 🚚 Rename `createFullUser()` to `createUser()` * ⚡ Consolidate user shell creation * ⚡ Make hashing async * ⚡ Add email to user shell * ⚡ Optimize array building * 🛠 refactor user shell factory * 🐛 Fix MySQL tests * ⚡ Silence logger in other DBs Co-authored-by: Ben Hesseldieck <b.hesseldieck@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 = require('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 config = require('../../../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.get('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);
|
|
}),
|
|
);
|
|
}
|