diff --git a/packages/cli/src/UserManagement/routes/passwordReset.ts b/packages/cli/src/UserManagement/routes/passwordReset.ts index 073a65d085ce1..8125ee277f737 100644 --- a/packages/cli/src/UserManagement/routes/passwordReset.ts +++ b/packages/cli/src/UserManagement/routes/passwordReset.ts @@ -14,6 +14,7 @@ import * as UserManagementMailer from '../email'; import type { PasswordResetRequest } from '../../requests'; import { issueCookie } from '../auth/jwt'; import { getBaseUrl } from '../../GenericHelpers'; +import config = require('../../../config'); export function passwordResetNamespace(this: N8nApp): void { /** @@ -22,6 +23,14 @@ export function passwordResetNamespace(this: N8nApp): void { this.app.post( `/${this.restEndpoint}/forgot-password`, ResponseHelper.send(async (req: PasswordResetRequest.Email) => { + if (config.get('userManagement.emails.mode') === '') { + 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) { diff --git a/packages/cli/test/integration/passwordReset.endpoints.test.ts b/packages/cli/test/integration/passwordReset.endpoints.test.ts new file mode 100644 index 0000000000000..748eb5c54770d --- /dev/null +++ b/packages/cli/test/integration/passwordReset.endpoints.test.ts @@ -0,0 +1,241 @@ +import express = require('express'); +import { getConnection } from 'typeorm'; +import { v4 as uuid } from 'uuid'; + +import * as utils from './shared/utils'; +import { Db } from '../../src'; +import config = require('../../config'); +import { compare } from 'bcryptjs'; +import { + randomEmail, + randomInvalidPassword, + randomName, + randomValidPassword, +} from './shared/random'; +import { Role } from '../../src/databases/entities/Role'; + +let app: express.Application; +let globalOwnerRole: Role; + +beforeAll(async () => { + app = utils.initTestServer({ namespaces: ['passwordReset'], applyAuth: true }); + await utils.initTestDb(); + await utils.truncate(['User']); + + globalOwnerRole = await Db.collections.Role!.findOneOrFail({ + name: 'owner', + scope: 'global', + }); +}); + +beforeEach(async () => { + jest.isolateModules(() => { + jest.mock('../../config'); + }); + + config.set('userManagement.hasOwner', true); + config.set('userManagement.emails.mode', ''); + + await utils.createUser({ + id: INITIAL_TEST_USER.id, + email: INITIAL_TEST_USER.email, + password: INITIAL_TEST_USER.password, + firstName: INITIAL_TEST_USER.firstName, + lastName: INITIAL_TEST_USER.lastName, + role: globalOwnerRole, + }); +}); + +afterEach(async () => { + await utils.truncate(['User']); +}); + +afterAll(() => { + return getConnection().close(); +}); + +test('POST /forgot-password should send password reset email', async () => { + const authlessAgent = await utils.createAgent(app); + + const { + user, + pass, + smtp: { host, port, secure }, + } = await utils.getSmtpTestAccount(); + + config.set('userManagement.emails.mode', 'smtp'); + config.set('userManagement.emails.smtp.host', host); + config.set('userManagement.emails.smtp.port', port); + config.set('userManagement.emails.smtp.secure', secure); + config.set('userManagement.emails.smtp.auth.user', user); + config.set('userManagement.emails.smtp.auth.pass', pass); + + const response = await authlessAgent + .post('/forgot-password') + .send({ email: INITIAL_TEST_USER.email }); + + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({}); + + const owner = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email }); + expect(owner.resetPasswordToken).toBeDefined(); +}); + +test('POST /forgot-password should fail if emailing is not set up', async () => { + const authlessAgent = await utils.createAgent(app); + + const response = await authlessAgent + .post('/forgot-password') + .send({ email: INITIAL_TEST_USER.email }); + + expect(response.statusCode).toBe(500); + + const owner = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email }); + expect(owner.resetPasswordToken).toBeNull(); +}); + +test('POST /forgot-password should fail with invalid inputs', async () => { + const authlessAgent = await utils.createAgent(app); + + config.set('userManagement.emails.mode', 'smtp'); + + const invalidPayloads = [ + randomEmail(), + [randomEmail()], + {}, + [{ name: randomName() }], + [{ email: randomName() }], + ]; + + for (const invalidPayload of invalidPayloads) { + const response = await authlessAgent.post('/forgot-password').send(invalidPayload); + expect(response.statusCode).toBe(400); + + const owner = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email }); + expect(owner.resetPasswordToken).toBeNull(); + } +}); + +test('POST /forgot-password should fail if user is not found', async () => { + const authlessAgent = await utils.createAgent(app); + + config.set('userManagement.emails.mode', 'smtp'); + + const response = await authlessAgent.post('/forgot-password').send({ email: randomEmail() }); + + expect(response.statusCode).toBe(404); +}); + +test('GET /resolve-password-token should succeed with valid inputs', async () => { + const authlessAgent = await utils.createAgent(app); + + const resetPasswordToken = uuid(); + + await Db.collections.User!.update(INITIAL_TEST_USER.id, { resetPasswordToken }); + + const response = await authlessAgent + .get('/resolve-password-token') + .query({ userId: INITIAL_TEST_USER.id, token: resetPasswordToken }); + + expect(response.statusCode).toBe(200); +}); + +test('GET /resolve-password-token should fail with invalid inputs', async () => { + const authlessAgent = await utils.createAgent(app); + + config.set('userManagement.emails.mode', 'smtp'); + + const first = await authlessAgent.get('/resolve-password-token').query({ token: uuid() }); + + const second = await authlessAgent + .get('/resolve-password-token') + .query({ userId: INITIAL_TEST_USER.id }); + + for (const response of [first, second]) { + expect(response.statusCode).toBe(400); + } +}); + +test('GET /resolve-password-token should fail if user is not found', async () => { + const authlessAgent = await utils.createAgent(app); + + config.set('userManagement.emails.mode', 'smtp'); + + const response = await authlessAgent + .get('/resolve-password-token') + .query({ userId: INITIAL_TEST_USER.id, token: uuid() }); + + expect(response.statusCode).toBe(404); +}); + +test('POST /change-password should succeed with valid inputs', async () => { + const authlessAgent = await utils.createAgent(app); + + const resetPasswordToken = uuid(); + await Db.collections.User!.update(INITIAL_TEST_USER.id, { resetPasswordToken }); + + const passwordToStore = randomValidPassword(); + + const response = await authlessAgent.post('/change-password').send({ + token: resetPasswordToken, + userId: INITIAL_TEST_USER.id, + password: passwordToStore, + }); + + expect(response.statusCode).toBe(200); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeDefined(); + + const { password: storedPassword } = await Db.collections.User!.findOneOrFail( + INITIAL_TEST_USER.id, + ); + + const comparisonResult = await compare(passwordToStore, storedPassword!); + expect(comparisonResult).toBe(true); + expect(storedPassword).not.toBe(passwordToStore); +}); + +test('POST /change-password should fail with invalid inputs', async () => { + const authlessAgent = await utils.createAgent(app); + + const resetPasswordToken = uuid(); + await Db.collections.User!.update(INITIAL_TEST_USER.id, { resetPasswordToken }); + + const invalidPayloads = [ + { token: uuid() }, + { id: INITIAL_TEST_USER.id }, + { password: randomValidPassword() }, + { token: uuid(), id: INITIAL_TEST_USER.id }, + { token: uuid(), password: randomValidPassword() }, + { id: INITIAL_TEST_USER.id, password: randomValidPassword() }, + { + id: INITIAL_TEST_USER.id, + password: randomInvalidPassword(), + token: resetPasswordToken, + }, + { + id: INITIAL_TEST_USER.id, + password: randomValidPassword(), + token: uuid(), + }, + ]; + + const { password: originalHashedPassword } = await Db.collections.User!.findOneOrFail(); + + for (const invalidPayload of invalidPayloads) { + const response = await authlessAgent.post('/change-password').query(invalidPayload); + expect(response.statusCode).toBe(400); + + const { password: fetchedHashedPassword } = await Db.collections.User!.findOneOrFail(); + expect(originalHashedPassword).toBe(fetchedHashedPassword); + } +}); + +const INITIAL_TEST_USER = { + id: uuid(), + email: randomEmail(), + firstName: randomName(), + lastName: randomName(), + password: randomValidPassword(), +}; diff --git a/packages/cli/test/integration/shared/types.d.ts b/packages/cli/test/integration/shared/types.d.ts index ad70b3eef01a2..be577afde2895 100644 --- a/packages/cli/test/integration/shared/types.d.ts +++ b/packages/cli/test/integration/shared/types.d.ts @@ -10,6 +10,6 @@ export type SmtpTestAccount = { }; }; -export type EndpointNamespace = 'me' | 'users' | 'auth' | 'owner'; +export type EndpointNamespace = 'me' | 'users' | 'auth' | 'owner' | 'passwordReset'; export type NamespacesMap = Readonly void>>; diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 65f6eeaa58769..b099d584af360 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -17,6 +17,7 @@ import { meNamespace as meEndpoints } from '../../../src/UserManagement/routes/m import { usersNamespace as usersEndpoints } from '../../../src/UserManagement/routes/users'; import { authenticationMethods as authEndpoints } from '../../../src/UserManagement/routes/auth'; import { ownerNamespace as ownerEndpoints } from '../../../src/UserManagement/routes/owner'; +import { passwordResetNamespace as passwordResetEndpoints } from '../../../src/UserManagement/routes/passwordReset'; import { getConnection } from 'typeorm'; import { issueJWT } from '../../../src/UserManagement/auth/jwt'; import { randomEmail, randomValidPassword, randomName } from './random'; @@ -67,6 +68,7 @@ export function initTestServer({ users: usersEndpoints, auth: authEndpoints, owner: ownerEndpoints, + passwordReset: passwordResetEndpoints, }; for (const namespace of namespaces) { @@ -176,6 +178,7 @@ export function getAllRoles() { ]); } + // ---------------------------------- // request agent // ----------------------------------