diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index a95b02876f6ae..fb460070a7eb6 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -1,10 +1,12 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable import/no-cycle */ +import express = require('express'); import { IsNull, Not } from 'typeorm'; import { Db, GenericHelpers, ResponseHelper } from '..'; import config = require('../../config'); import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH, User } from '../databases/entities/User'; +import { AuthenticatedRequest } from '../requests'; import { PublicUser } from './Interfaces'; export const isEmailSetUp = Boolean(config.get('userManagement.emails.mode')); @@ -53,3 +55,7 @@ export function sanitizeUser(user: User): PublicUser { } = user; return sanitizedUser; } + +export function isAuthenticatedRequest(request: express.Request): request is AuthenticatedRequest { + return request.user !== undefined; +} diff --git a/packages/cli/src/UserManagement/email/NodeMailer.ts b/packages/cli/src/UserManagement/email/NodeMailer.ts index ed8f9d1c017b3..931f896de148c 100644 --- a/packages/cli/src/UserManagement/email/NodeMailer.ts +++ b/packages/cli/src/UserManagement/email/NodeMailer.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTransport, Transporter } from 'nodemailer'; +import { LoggerProxy as Logger } from 'n8n-workflow'; import config = require('../../../config'); import { MailData, SendEmailResult, UserManagementMailerImplementation } from './Interfaces'; @@ -27,7 +28,11 @@ export class NodeMailer implements UserManagementMailerImplementation { text: mailData.textOnly, html: mailData.body, }); + Logger.verbose( + `Email sent successfully to the following recipients: ${mailData.emailRecipients.toString()}`, + ); } catch (error) { + Logger.error('Failed to send email', { recipients: mailData.emailRecipients, error }); return { success: false, error, diff --git a/packages/cli/src/UserManagement/routes/index.ts b/packages/cli/src/UserManagement/routes/index.ts index dd3121d957623..d8849c4eb966c 100644 --- a/packages/cli/src/UserManagement/routes/index.ts +++ b/packages/cli/src/UserManagement/routes/index.ts @@ -7,17 +7,18 @@ import * as passport from 'passport'; import { Strategy } from 'passport-jwt'; import { NextFunction, Request, Response } from 'express'; import * as jwt from 'jsonwebtoken'; +import { LoggerProxy as Logger } from 'n8n-workflow'; import { JwtPayload, N8nApp } from '../Interfaces'; import { authenticationMethods } from './auth'; import config = require('../../../config'); -import { User } from '../../databases/entities/User'; import { issueCookie, resolveJwtContent } from '../auth/jwt'; import { meNamespace } from './me'; import { usersNamespace } from './users'; import { passwordResetNamespace } from './passwordReset'; import { AuthenticatedRequest } from '../../requests'; import { ownerNamespace } from './owner'; +import { isAuthenticatedRequest } from '../UserManagementHelper'; export function addRoutes(this: N8nApp, ignoredEndpoints: string[], restEndpoint: string): void { this.app.use(cookieParser()); @@ -36,6 +37,7 @@ export function addRoutes(this: N8nApp, ignoredEndpoints: string[], restEndpoint const user = await resolveJwtContent(jwtPayload); return done(null, user); } catch (error) { + Logger.debug('Failed to extract user from JWT payload', { jwtPayload }); return done(null, false, { message: 'User not found' }); } }), @@ -81,10 +83,10 @@ export function addRoutes(this: N8nApp, ignoredEndpoints: string[], restEndpoint return passport.authenticate('jwt', { session: false })(req, res, next); }); - this.app.use((req: Request, res: Response, next: NextFunction) => { + this.app.use((req: Request | AuthenticatedRequest, res: Response, next: NextFunction) => { // req.user is empty for public routes, so just proceed // owner can do anything, so proceed as well - if (req.user === undefined || (req.user && (req.user as User).globalRole.name === 'owner')) { + if (!req.user || (isAuthenticatedRequest(req) && req.user.globalRole.name === 'owner')) { next(); return; } @@ -101,6 +103,10 @@ export function addRoutes(this: N8nApp, ignoredEndpoints: string[], restEndpoint (req.method === 'POST' && new RegExp(`/${restEndpoint}/users/[^/]/reinvite+`, 'gm').test(trimmedUrl)) ) { + Logger.verbose('User attempted to access endpoint without authorization', { + endpoint: `${req.method} ${trimmedUrl}`, + userId: isAuthenticatedRequest(req) ? req.user.id : 'unknown', + }); res.status(403).json({ status: 'error', message: 'Unauthorized' }); return; } diff --git a/packages/cli/src/UserManagement/routes/me.ts b/packages/cli/src/UserManagement/routes/me.ts index 636bfc269fd48..0c8347250d560 100644 --- a/packages/cli/src/UserManagement/routes/me.ts +++ b/packages/cli/src/UserManagement/routes/me.ts @@ -4,6 +4,7 @@ import { genSaltSync, hashSync } from 'bcryptjs'; import express = require('express'); import validator from 'validator'; +import { LoggerProxy as Logger } from 'n8n-workflow'; import { Db, ResponseHelper } from '../..'; import { issueCookie } from '../auth/jwt'; @@ -32,10 +33,18 @@ export function meNamespace(this: N8nApp): void { ResponseHelper.send( async (req: MeRequest.Settings, res: express.Response): Promise => { if (!req.body.email) { + Logger.debug('Request to update user email failed because of missing email in payload', { + userId: req.user.id, + payload: req.body, + }); throw new ResponseHelper.ResponseError('Email is mandatory', undefined, 400); } if (!validator.isEmail(req.body.email)) { + Logger.debug('Request to update user email failed because of invalid email in payload', { + userId: req.user.id, + invalidEmail: req.body.email, + }); throw new ResponseHelper.ResponseError('Invalid email address', undefined, 400); } @@ -47,6 +56,8 @@ export function meNamespace(this: N8nApp): void { const user = await Db.collections.User!.save(newUser); + Logger.info('User updated successfully', { userId: user.id }); + await issueCookie(res, user); return sanitizeUser(user); @@ -80,6 +91,12 @@ export function meNamespace(this: N8nApp): void { const { body: personalizationAnswers } = req; if (!personalizationAnswers) { + Logger.debug( + 'Request to store user personalization survey failed because of empty payload', + { + userId: req.user.id, + }, + ); throw new ResponseHelper.ResponseError( 'Personalization answers are mandatory', undefined, @@ -92,6 +109,8 @@ export function meNamespace(this: N8nApp): void { personalizationAnswers, }); + Logger.info('User survey updated successfully', { userId: req.user.id }); + return { success: true }; }), ); diff --git a/packages/cli/src/UserManagement/routes/owner.ts b/packages/cli/src/UserManagement/routes/owner.ts index 7bb66076054f1..64b16b5ffc37f 100644 --- a/packages/cli/src/UserManagement/routes/owner.ts +++ b/packages/cli/src/UserManagement/routes/owner.ts @@ -3,6 +3,7 @@ import { hashSync, genSaltSync } from 'bcryptjs'; import * as express from 'express'; import validator from 'validator'; +import { LoggerProxy as Logger } from 'n8n-workflow'; import { Db, ResponseHelper } from '../..'; import config = require('../../../config'); @@ -25,16 +26,30 @@ export function ownerNamespace(this: N8nApp): void { const { id: userId } = req.user; if (config.get('userManagement.hasOwner')) { + Logger.debug( + 'Request to claim instance ownership failed because instance owner already exists', + { + userId, + }, + ); throw new ResponseHelper.ResponseError('Invalid request', undefined, 400); } if (!email || !validator.isEmail(email)) { + Logger.debug('Request to claim instance ownership failed because of invalid email', { + userId, + invalidEmail: email, + }); throw new ResponseHelper.ResponseError('Invalid email address', undefined, 400); } const validPassword = validatePassword(password); if (!firstName || !lastName) { + Logger.debug( + 'Request to claim instance ownership failed because of missing first name or last name in payload', + { userId, payload: req.body }, + ); throw new ResponseHelper.ResponseError( 'First and last names are mandatory', undefined, @@ -62,6 +77,8 @@ export function ownerNamespace(this: N8nApp): void { const owner = await Db.collections.User!.save(newUser); + Logger.info('Owner updated successfully', { userId: req.user.id }); + config.set('userManagement.hasOwner', true); await Db.collections.Settings!.update( @@ -69,6 +86,8 @@ export function ownerNamespace(this: N8nApp): void { { value: JSON.stringify(true) }, ); + Logger.debug('Setting hasOwner updated successfully', { userId: req.user.id }); + await issueCookie(res, owner); return sanitizeUser(owner); diff --git a/packages/cli/src/UserManagement/routes/passwordReset.ts b/packages/cli/src/UserManagement/routes/passwordReset.ts index 0f1254ecfe166..5d27f353f7528 100644 --- a/packages/cli/src/UserManagement/routes/passwordReset.ts +++ b/packages/cli/src/UserManagement/routes/passwordReset.ts @@ -7,6 +7,7 @@ import { URL } from 'url'; import { genSaltSync, hashSync } from 'bcryptjs'; import validator from 'validator'; import { IsNull, MoreThanOrEqual, Not } from 'typeorm'; +import { LoggerProxy as Logger } from 'n8n-workflow'; import { Db, ResponseHelper } from '../..'; import { N8nApp } from '../Interfaces'; @@ -25,6 +26,7 @@ export function passwordResetNamespace(this: N8nApp): void { `/${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, @@ -35,10 +37,18 @@ export function passwordResetNamespace(this: N8nApp): void { 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); } @@ -46,6 +56,10 @@ export function passwordResetNamespace(this: N8nApp): void { 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; } @@ -62,13 +76,15 @@ export function passwordResetNamespace(this: N8nApp): void { url.searchParams.append('userId', id); url.searchParams.append('token', resetPasswordToken); - void UserManagementMailer.getInstance().passwordReset({ + await UserManagementMailer.getInstance().passwordReset({ email, firstName, lastName, passwordResetUrl: url.toString(), domain: baseUrl, }); + + Logger.info('Sent password reset email successfully', { userId: user.id, email }); }), ); @@ -81,6 +97,12 @@ export function passwordResetNamespace(this: N8nApp): void { 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); } @@ -94,8 +116,17 @@ export function passwordResetNamespace(this: N8nApp): void { }); 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 }); }), ); @@ -105,10 +136,20 @@ export function passwordResetNamespace(this: N8nApp): void { this.app.post( `/${this.restEndpoint}/change-password`, ResponseHelper.send(async (req: PasswordResetRequest.NewPassword, res: express.Response) => { - const { token: resetPasswordToken, userId: id, password } = req.body; - - if (!resetPasswordToken || !id || !password) { - throw new ResponseHelper.ResponseError('Parameter missing', undefined, 400); + 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); @@ -117,21 +158,30 @@ export function passwordResetNamespace(this: N8nApp): void { const currentTimestamp = Math.floor(Date.now() / 1000); const user = await Db.collections.User!.findOne({ - id, + 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(id, { + await Db.collections.User!.update(userId, { password: hashSync(validPassword, genSaltSync(10)), resetPasswordToken: null, resetPasswordTokenExpiration: null, }); + Logger.info('User password updated successfully', { userId }); + await issueCookie(res, user); }), ); diff --git a/packages/cli/src/UserManagement/routes/users.ts b/packages/cli/src/UserManagement/routes/users.ts index 4853107da975b..88334a5fcbcae 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -4,7 +4,7 @@ import { Response } from 'express'; import { getConnection, In } from 'typeorm'; import { genSaltSync, hashSync } from 'bcryptjs'; import validator from 'validator'; -import { LoggerProxy } from 'n8n-workflow'; +import { LoggerProxy as Logger } from 'n8n-workflow'; import { Db, ResponseHelper } from '../..'; import { N8nApp } from '../Interfaces'; @@ -30,23 +30,35 @@ export function usersNamespace(this: N8nApp): void { this.app.post( `/${this.restEndpoint}/users`, ResponseHelper.send(async (req: UserRequest.Invite) => { - if (!config.get('userManagement.hasOwner')) { + if (config.get('userManagement.emails.mode') === '') { + Logger.debug( + 'Request to send email invite(s) to user(s) failed because emailing was not set up', + ); throw new ResponseHelper.ResponseError( - 'You must set up your own account before inviting others', + 'Email sending must be set up in order to request a password reset email', undefined, - 400, + 500, ); } - if (!isEmailSetUp) { + if (!config.get('userManagement.hasOwner')) { + Logger.debug( + 'Request to send email invite(s) to user(s) failed because emailing was not set up', + ); throw new ResponseHelper.ResponseError( - 'Email sending must be set up in order to invite other users', + 'You must set up your own account before inviting others', undefined, - 500, + 400, ); } if (!Array.isArray(req.body)) { + Logger.debug( + 'Request to send email invite(s) to user(s) failed because the payload is not an array', + { + payload: req.body, + }, + ); throw new ResponseHelper.ResponseError('Invalid payload', undefined, 400); } @@ -56,12 +68,17 @@ export function usersNamespace(this: N8nApp): void { // Validate payload req.body.forEach((invite) => { if (typeof invite !== 'object' || !invite.email) { - throw new ResponseHelper.ResponseError('Invalid payload', undefined, 400); + throw new ResponseHelper.ResponseError( + 'Request to send email invite(s) to user(s) failed because the payload is not an array shaped Array<{ email: string }>', + undefined, + 400, + ); } if (!validator.isEmail(invite.email)) { + Logger.debug('Invalid email in payload', { invalidEmail: invite.email }); throw new ResponseHelper.ResponseError( - `Invalid email address ${invite.email}`, + `Request to send email invite(s) to user(s) failed because of an invalid email address: ${invite.email}`, undefined, 400, ); @@ -71,6 +88,17 @@ export function usersNamespace(this: N8nApp): void { const role = await Db.collections.Role!.findOne({ scope: 'global', name: 'member' }); + if (!role) { + Logger.error( + 'Request to send email invite(s) to user(s) failed because no global member role was found in database', + ); + throw new ResponseHelper.ResponseError( + 'Members role not found in database - inconsistent state', + undefined, + 500, + ); + } + // remove/exclude existing users from creation const existingUsers = await Db.collections.User!.find({ where: { email: In(Object.keys(createUsers)) }, @@ -83,56 +111,76 @@ export function usersNamespace(this: N8nApp): void { createUsers[user.email] = user.id; }); + const usersToSetUp = Object.keys(createUsers).filter((email) => createUsers[email] === null); + const total = usersToSetUp.length; + + Logger.debug(total > 1 ? `Creating ${total} user shells...` : `Creating 1 user shell...`); + try { await getConnection().transaction(async (transactionManager) => { return Promise.all( - Object.keys(createUsers) - .filter((email) => createUsers[email] === null) - .map(async (email) => { - const newUser = Object.assign(new User(), { - email, - globalRole: role, - }); - const savedUser = await transactionManager.save(newUser); - createUsers[savedUser.email] = savedUser.id; - return savedUser; - }), + usersToSetUp.map(async (email) => { + const newUser = Object.assign(new User(), { + email, + globalRole: role, + }); + const savedUser = await transactionManager.save(newUser); + createUsers[savedUser.email] = savedUser.id; + return savedUser; + }), ); }); } catch (error) { - // TODO: Logger - throw new ResponseHelper.ResponseError(`An error occurred during user creation`); + Logger.error('Failed to create user shells', { userShells: createUsers }); + throw new ResponseHelper.ResponseError('An error occurred during user creation'); } + Logger.info('Created user shells successfully', { userId: req.user.id }); + Logger.verbose(total > 1 ? `${total} user shells created` : `1 user shell created`, { + userShells: createUsers, + }); + const baseUrl = getInstanceBaseUrl(); + const usersPendingSetup = Object.entries(createUsers).filter(([email, id]) => id && email); + // send invite email to new or not yet setup users const mailer = getInstance(); - return Promise.all( - Object.entries(createUsers) - .filter(([email, id]) => id && email) - .map(async ([email, id]) => { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - const inviteAcceptUrl = `${baseUrl}/signup?inviterId=${req.user.id}&inviteeId=${id}`; - const result = await mailer.invite({ - email, + const emailingResults = await Promise.all( + usersPendingSetup.map(async ([email, id]) => { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + const inviteAcceptUrl = `${baseUrl}/signup/inviterId=${req.user.id}&inviteeId=${id}`; + const result = await mailer.invite({ + email, + inviteAcceptUrl, + domain: baseUrl, + }); + const resp: { id: string | null; email: string; error?: string } = { + id, + email, + }; + if (!result.success) { + Logger.error('Failed to send email', { + userId: req.user.id, inviteAcceptUrl, domain: baseUrl, + email, }); - const resp: { user: { id: string | null; email: string }; error?: string } = { - user: { - id, - email, - }, - }; - if (!result.success) { - // TODO: Logger - resp.error = `Email could not be sent`; - } - return resp; - }), + resp.error = `Email could not be sent`; + } + return { user: resp }; + }), + ); + + Logger.debug( + usersPendingSetup.length > 1 + ? `Sent ${usersPendingSetup.length} invite emails successfully` + : `Sent 1 invite email successfully`, + { userShells: createUsers }, ); + + return emailingResults; }), ); @@ -145,24 +193,27 @@ export function usersNamespace(this: N8nApp): void { const { inviterId, inviteeId } = req.query; if (!inviterId || !inviteeId) { - LoggerProxy.error('Invalid invite URL - did not receive user IDs', { - inviterId, - inviteeId, - }); + Logger.debug( + 'Request to resolve signup token failed because of missing user IDs in query string', + { inviterId, inviteeId }, + ); throw new ResponseHelper.ResponseError('Invalid payload', undefined, 400); } const users = await Db.collections.User!.find({ where: { id: In([inviterId, inviteeId]) } }); if (users.length !== 2) { - LoggerProxy.error('Invalid invite URL - did not find users', { inviterId, inviteeId }); + Logger.debug( + 'Request to resolve signup token failed because the ID of the inviter and/or the ID of the invitee were not found in database', + { inviterId, inviteeId }, + ); throw new ResponseHelper.ResponseError('Invalid invite URL', undefined, 400); } const invitee = users.find((user) => user.id === inviteeId); if (!invitee || invitee.password) { - LoggerProxy.error('Invalid invite URL - invitee already setup', { + Logger.error('Invalid invite URL - invitee already setup', { inviterId, inviteeId, }); @@ -172,10 +223,12 @@ export function usersNamespace(this: N8nApp): void { const inviter = users.find((user) => user.id === inviterId); if (!inviter || !inviter.email || !inviter.firstName) { - LoggerProxy.error('Invalid invite URL - inviter does not have email set', { - inviterId, - inviteeId, - }); + Logger.error( + 'Request to resolve signup token failed because inviter does not exist or is not set up', + { + inviterId: inviter?.id, + }, + ); throw new ResponseHelper.ResponseError('Invalid request', undefined, 400); } @@ -198,6 +251,10 @@ export function usersNamespace(this: N8nApp): void { const { inviterId, firstName, lastName, password } = req.body; if (!inviterId || !inviteeId || !firstName || !lastName || !password) { + Logger.debug( + 'Request to fill out a user shell failed because of missing properties in payload', + { payload: req.body }, + ); throw new ResponseHelper.ResponseError('Invalid payload', undefined, 400); } @@ -209,12 +266,23 @@ export function usersNamespace(this: N8nApp): void { }); if (users.length !== 2) { + Logger.debug( + 'Request to fill out a user shell failed because the inviter ID and/or invitee ID were not found in database', + { + inviterId, + inviteeId, + }, + ); throw new ResponseHelper.ResponseError('Invalid payload or URL', undefined, 400); } const invitee = users.find((user) => user.id === inviteeId) as User; if (invitee.password) { + Logger.debug( + 'Request to fill out a user shell failed because the invite had already been accepted', + { inviteeId }, + ); throw new ResponseHelper.ResponseError( 'This invite has been accepted already', undefined, @@ -252,6 +320,10 @@ export function usersNamespace(this: N8nApp): void { const { id: idToDelete } = req.params; if (req.user.id === idToDelete) { + Logger.debug( + 'Request to delete a user failed because it attempted to delete the requesting user', + { userId: req.user.id }, + ); throw new ResponseHelper.ResponseError('Cannot delete your own user', undefined, 400); } @@ -259,7 +331,7 @@ export function usersNamespace(this: N8nApp): void { if (transferId === idToDelete) { throw new ResponseHelper.ResponseError( - 'User to delete and transferee cannot be the same', + 'Request to delete a user failed because the user to delete and the transferee are the same user', undefined, 400, ); @@ -270,7 +342,11 @@ export function usersNamespace(this: N8nApp): void { }); if (!users.length || (transferId && users.length !== 2)) { - throw new ResponseHelper.ResponseError('Could not find user', undefined, 404); + throw new ResponseHelper.ResponseError( + 'Request to delete a user failed because the ID of the user to delete and/or the ID of the transferee were not found in DB', + undefined, + 404, + ); } const userToDelete = users.find((user) => user.id === req.params.id) as User; @@ -335,6 +411,7 @@ export function usersNamespace(this: N8nApp): void { const { id: idToReinvite } = req.params; if (!isEmailSetUp) { + Logger.error('Request to reinvite a user failed because email sending was not set up'); throw new ResponseHelper.ResponseError( 'Email sending must be set up in order to invite other users', undefined, @@ -345,10 +422,17 @@ export function usersNamespace(this: N8nApp): void { const reinvitee = await Db.collections.User!.findOne({ id: idToReinvite }); if (!reinvitee) { + Logger.debug( + 'Request to reinvite a user failed because the ID of the reinvitee was not found in database', + ); throw new ResponseHelper.ResponseError('Could not find user', undefined, 404); } if (reinvitee.password) { + Logger.debug( + 'Request to reinvite a user failed because the invite had already been accepted', + { userId: reinvitee.id }, + ); throw new ResponseHelper.ResponseError( 'User has already accepted the invite', undefined, @@ -357,14 +441,20 @@ export function usersNamespace(this: N8nApp): void { } const baseUrl = getInstanceBaseUrl(); + const inviteAcceptUrl = `${baseUrl}/signup/inviterId=${req.user.id}&inviteeId=${reinvitee.id}`; const result = await getInstance().invite({ email: reinvitee.email, - inviteAcceptUrl: `${baseUrl}/signup?inviterId=${req.user.id}&inviteeId=${reinvitee.id}`, + inviteAcceptUrl, domain: baseUrl, }); if (!result.success) { + Logger.error('Failed to send email', { + email: reinvitee.email, + inviteAcceptUrl, + domain: baseUrl, + }); throw new ResponseHelper.ResponseError( `Failed to send email to ${reinvitee.email}`, undefined, diff --git a/packages/cli/src/databases/mysqldb/migrations/1644424784709-AddExecutionEntityIndexes.ts b/packages/cli/src/databases/mysqldb/migrations/1644424784709-AddExecutionEntityIndexes.ts index 62ced030772bd..045c3c047db7c 100644 --- a/packages/cli/src/databases/mysqldb/migrations/1644424784709-AddExecutionEntityIndexes.ts +++ b/packages/cli/src/databases/mysqldb/migrations/1644424784709-AddExecutionEntityIndexes.ts @@ -1,32 +1,77 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; import * as config from '../../../../config'; +import { isTestRun } from '../../../../test/integration/shared/utils'; export class AddExecutionEntityIndexes1644424784709 implements MigrationInterface { - name = 'AddExecutionEntityIndexes1644424784709' + name = 'AddExecutionEntityIndexes1644424784709'; - public async up(queryRunner: QueryRunner): Promise { - console.log('\n\nINFO: Started migration for execution entity indexes.\n Depending on the number of saved executions, it may take a while.\n\n'); + public async up(queryRunner: QueryRunner): Promise { + !isTestRun && + console.log( + '\n\nINFO: Started migration for execution entity indexes.\n Depending on the number of saved executions, it may take a while.\n\n', + ); - const tablePrefix = config.get('database.tablePrefix'); + const tablePrefix = config.get('database.tablePrefix'); - await queryRunner.query('DROP INDEX `IDX_c4d999a5e90784e8caccf5589d` ON `' + tablePrefix + 'execution_entity`'); - await queryRunner.query('DROP INDEX `IDX_ca4a71b47f28ac6ea88293a8e2` ON `' + tablePrefix + 'execution_entity`'); - await queryRunner.query('CREATE INDEX `IDX_06da892aaf92a48e7d3e400003` ON `' + tablePrefix + 'execution_entity` (`workflowId`, `waitTill`, `id`)'); - await queryRunner.query('CREATE INDEX `IDX_78d62b89dc1433192b86dce18a` ON `' + tablePrefix + 'execution_entity` (`workflowId`, `finished`, `id`)'); - await queryRunner.query('CREATE INDEX `IDX_1688846335d274033e15c846a4` ON `' + tablePrefix + 'execution_entity` (`finished`, `id`)'); - await queryRunner.query('CREATE INDEX `IDX_b94b45ce2c73ce46c54f20b5f9` ON `' + tablePrefix + 'execution_entity` (`waitTill`, `id`)'); - await queryRunner.query('CREATE INDEX `IDX_81fc04c8a17de15835713505e4` ON `' + tablePrefix + 'execution_entity` (`workflowId`, `id`)'); - } - - public async down(queryRunner: QueryRunner): Promise { - const tablePrefix = config.get('database.tablePrefix'); - await queryRunner.query('DROP INDEX `IDX_81fc04c8a17de15835713505e4` ON `' + tablePrefix + 'execution_entity`'); - await queryRunner.query('DROP INDEX `IDX_b94b45ce2c73ce46c54f20b5f9` ON `' + tablePrefix + 'execution_entity`'); - await queryRunner.query('DROP INDEX `IDX_1688846335d274033e15c846a4` ON `' + tablePrefix + 'execution_entity`'); - await queryRunner.query('DROP INDEX `IDX_78d62b89dc1433192b86dce18a` ON `' + tablePrefix + 'execution_entity`'); - await queryRunner.query('DROP INDEX `IDX_06da892aaf92a48e7d3e400003` ON `' + tablePrefix + 'execution_entity`'); - await queryRunner.query('CREATE INDEX `IDX_ca4a71b47f28ac6ea88293a8e2` ON `' + tablePrefix + 'execution_entity` (`waitTill`)'); - await queryRunner.query('CREATE INDEX `IDX_c4d999a5e90784e8caccf5589d` ON `' + tablePrefix + 'execution_entity` (`workflowId`)'); - } + await queryRunner.query( + 'DROP INDEX `IDX_c4d999a5e90784e8caccf5589d` ON `' + tablePrefix + 'execution_entity`', + ); + await queryRunner.query( + 'DROP INDEX `IDX_ca4a71b47f28ac6ea88293a8e2` ON `' + tablePrefix + 'execution_entity`', + ); + await queryRunner.query( + 'CREATE INDEX `IDX_06da892aaf92a48e7d3e400003` ON `' + + tablePrefix + + 'execution_entity` (`workflowId`, `waitTill`, `id`)', + ); + await queryRunner.query( + 'CREATE INDEX `IDX_78d62b89dc1433192b86dce18a` ON `' + + tablePrefix + + 'execution_entity` (`workflowId`, `finished`, `id`)', + ); + await queryRunner.query( + 'CREATE INDEX `IDX_1688846335d274033e15c846a4` ON `' + + tablePrefix + + 'execution_entity` (`finished`, `id`)', + ); + await queryRunner.query( + 'CREATE INDEX `IDX_b94b45ce2c73ce46c54f20b5f9` ON `' + + tablePrefix + + 'execution_entity` (`waitTill`, `id`)', + ); + await queryRunner.query( + 'CREATE INDEX `IDX_81fc04c8a17de15835713505e4` ON `' + + tablePrefix + + 'execution_entity` (`workflowId`, `id`)', + ); + } + public async down(queryRunner: QueryRunner): Promise { + const tablePrefix = config.get('database.tablePrefix'); + await queryRunner.query( + 'DROP INDEX `IDX_81fc04c8a17de15835713505e4` ON `' + tablePrefix + 'execution_entity`', + ); + await queryRunner.query( + 'DROP INDEX `IDX_b94b45ce2c73ce46c54f20b5f9` ON `' + tablePrefix + 'execution_entity`', + ); + await queryRunner.query( + 'DROP INDEX `IDX_1688846335d274033e15c846a4` ON `' + tablePrefix + 'execution_entity`', + ); + await queryRunner.query( + 'DROP INDEX `IDX_78d62b89dc1433192b86dce18a` ON `' + tablePrefix + 'execution_entity`', + ); + await queryRunner.query( + 'DROP INDEX `IDX_06da892aaf92a48e7d3e400003` ON `' + tablePrefix + 'execution_entity`', + ); + await queryRunner.query( + 'CREATE INDEX `IDX_ca4a71b47f28ac6ea88293a8e2` ON `' + + tablePrefix + + 'execution_entity` (`waitTill`)', + ); + await queryRunner.query( + 'CREATE INDEX `IDX_c4d999a5e90784e8caccf5589d` ON `' + + tablePrefix + + 'execution_entity` (`workflowId`)', + ); + } } diff --git a/packages/cli/src/databases/postgresdb/migrations/1644422880309-AddExecutionEntityIndexes.ts b/packages/cli/src/databases/postgresdb/migrations/1644422880309-AddExecutionEntityIndexes.ts index b0b6814b90d0c..3e6811e06eaf1 100644 --- a/packages/cli/src/databases/postgresdb/migrations/1644422880309-AddExecutionEntityIndexes.ts +++ b/packages/cli/src/databases/postgresdb/migrations/1644422880309-AddExecutionEntityIndexes.ts @@ -1,47 +1,82 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; import * as config from '../../../../config'; +import { isTestRun } from '../../../../test/integration/shared/utils'; export class AddExecutionEntityIndexes1644422880309 implements MigrationInterface { - name = 'AddExecutionEntityIndexes1644422880309' + name = 'AddExecutionEntityIndexes1644422880309'; - public async up(queryRunner: QueryRunner): Promise { - console.log('\n\nINFO: Started migration for execution entity indexes.\n Depending on the number of saved executions, it may take a while.\n\n'); + public async up(queryRunner: QueryRunner): Promise { + !isTestRun && + console.log( + '\n\nINFO: Started migration for execution entity indexes.\n Depending on the number of saved executions, it may take a while.\n\n', + ); - let tablePrefix = config.get('database.tablePrefix'); + let tablePrefix = config.get('database.tablePrefix'); const tablePrefixPure = tablePrefix; const schema = config.get('database.postgresdb.schema'); - if (schema) { + if (schema) { tablePrefix = schema + '.' + tablePrefix; } - await queryRunner.query(`DROP INDEX IF EXISTS "${schema}"."IDX_${tablePrefixPure}c4d999a5e90784e8caccf5589d"`); - await queryRunner.query(`DROP INDEX IF EXISTS "${schema}"."IDX_${tablePrefixPure}ca4a71b47f28ac6ea88293a8e2"`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_${tablePrefixPure}33228da131bb1112247cf52a42" ON ${tablePrefix}execution_entity ("stoppedAt") `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_${tablePrefixPure}58154df94c686818c99fb754ce" ON ${tablePrefix}execution_entity ("workflowId", "waitTill", "id") `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_${tablePrefixPure}4f474ac92be81610439aaad61e" ON ${tablePrefix}execution_entity ("workflowId", "finished", "id") `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_${tablePrefixPure}72ffaaab9f04c2c1f1ea86e662" ON ${tablePrefix}execution_entity ("finished", "id") `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_${tablePrefixPure}85b981df7b444f905f8bf50747" ON ${tablePrefix}execution_entity ("waitTill", "id") `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_${tablePrefixPure}d160d4771aba5a0d78943edbe3" ON ${tablePrefix}execution_entity ("workflowId", "id") `); - } - - public async down(queryRunner: QueryRunner): Promise { - let tablePrefix = config.get('database.tablePrefix'); + await queryRunner.query( + `DROP INDEX IF EXISTS "${schema}"."IDX_${tablePrefixPure}c4d999a5e90784e8caccf5589d"`, + ); + await queryRunner.query( + `DROP INDEX IF EXISTS "${schema}"."IDX_${tablePrefixPure}ca4a71b47f28ac6ea88293a8e2"`, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "IDX_${tablePrefixPure}33228da131bb1112247cf52a42" ON ${tablePrefix}execution_entity ("stoppedAt") `, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "IDX_${tablePrefixPure}58154df94c686818c99fb754ce" ON ${tablePrefix}execution_entity ("workflowId", "waitTill", "id") `, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "IDX_${tablePrefixPure}4f474ac92be81610439aaad61e" ON ${tablePrefix}execution_entity ("workflowId", "finished", "id") `, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "IDX_${tablePrefixPure}72ffaaab9f04c2c1f1ea86e662" ON ${tablePrefix}execution_entity ("finished", "id") `, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "IDX_${tablePrefixPure}85b981df7b444f905f8bf50747" ON ${tablePrefix}execution_entity ("waitTill", "id") `, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "IDX_${tablePrefixPure}d160d4771aba5a0d78943edbe3" ON ${tablePrefix}execution_entity ("workflowId", "id") `, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + let tablePrefix = config.get('database.tablePrefix'); const tablePrefixPure = tablePrefix; const schema = config.get('database.postgresdb.schema'); - if (schema) { + if (schema) { tablePrefix = schema + '.' + tablePrefix; } - await queryRunner.query(`DROP INDEX IF EXISTS "${schema}"."IDX_${tablePrefixPure}d160d4771aba5a0d78943edbe3"`); - await queryRunner.query(`DROP INDEX IF EXISTS "${schema}"."IDX_${tablePrefixPure}85b981df7b444f905f8bf50747"`); - await queryRunner.query(`DROP INDEX IF EXISTS "${schema}"."IDX_${tablePrefixPure}72ffaaab9f04c2c1f1ea86e662"`); - await queryRunner.query(`DROP INDEX IF EXISTS "${schema}"."IDX_${tablePrefixPure}4f474ac92be81610439aaad61e"`); - await queryRunner.query(`DROP INDEX IF EXISTS "${schema}"."IDX_${tablePrefixPure}58154df94c686818c99fb754ce"`); - await queryRunner.query(`DROP INDEX IF EXISTS "${schema}"."IDX_${tablePrefixPure}33228da131bb1112247cf52a42"`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_${tablePrefixPure}ca4a71b47f28ac6ea88293a8e2" ON ${tablePrefix}execution_entity ("waitTill") `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_${tablePrefixPure}c4d999a5e90784e8caccf5589d" ON ${tablePrefix}execution_entity ("workflowId") `); - } - + await queryRunner.query( + `DROP INDEX IF EXISTS "${schema}"."IDX_${tablePrefixPure}d160d4771aba5a0d78943edbe3"`, + ); + await queryRunner.query( + `DROP INDEX IF EXISTS "${schema}"."IDX_${tablePrefixPure}85b981df7b444f905f8bf50747"`, + ); + await queryRunner.query( + `DROP INDEX IF EXISTS "${schema}"."IDX_${tablePrefixPure}72ffaaab9f04c2c1f1ea86e662"`, + ); + await queryRunner.query( + `DROP INDEX IF EXISTS "${schema}"."IDX_${tablePrefixPure}4f474ac92be81610439aaad61e"`, + ); + await queryRunner.query( + `DROP INDEX IF EXISTS "${schema}"."IDX_${tablePrefixPure}58154df94c686818c99fb754ce"`, + ); + await queryRunner.query( + `DROP INDEX IF EXISTS "${schema}"."IDX_${tablePrefixPure}33228da131bb1112247cf52a42"`, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "IDX_${tablePrefixPure}ca4a71b47f28ac6ea88293a8e2" ON ${tablePrefix}execution_entity ("waitTill") `, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "IDX_${tablePrefixPure}c4d999a5e90784e8caccf5589d" ON ${tablePrefix}execution_entity ("workflowId") `, + ); + } } diff --git a/packages/cli/src/databases/sqlite/migrations/1644421939510-AddExecutionEntityIndexes.ts b/packages/cli/src/databases/sqlite/migrations/1644421939510-AddExecutionEntityIndexes.ts index 1f6695626ff73..f8b72537d39fd 100644 --- a/packages/cli/src/databases/sqlite/migrations/1644421939510-AddExecutionEntityIndexes.ts +++ b/packages/cli/src/databases/sqlite/migrations/1644421939510-AddExecutionEntityIndexes.ts @@ -1,31 +1,46 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; import * as config from '../../../../config'; +import { isTestRun } from '../../../../test/integration/shared/utils'; export class AddExecutionEntityIndexes1644421939510 implements MigrationInterface { - name = 'AddExecutionEntityIndexes1644421939510' + name = 'AddExecutionEntityIndexes1644421939510'; - public async up(queryRunner: QueryRunner): Promise { - console.log('\n\nINFO: Started migration for execution entity indexes.\n Depending on the number of saved executions, it may take a while.\n\n'); + public async up(queryRunner: QueryRunner): Promise { + !isTestRun && + console.log( + '\n\nINFO: Started migration for execution entity indexes.\n Depending on the number of saved executions, it may take a while.\n\n', + ); - const tablePrefix = config.get('database.tablePrefix'); + const tablePrefix = config.get('database.tablePrefix'); - await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}ca4a71b47f28ac6ea88293a8e2'`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS 'IDX_${tablePrefix}06da892aaf92a48e7d3e400003' ON '${tablePrefix}execution_entity' ('workflowId', 'waitTill', 'id') `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS 'IDX_${tablePrefix}78d62b89dc1433192b86dce18a' ON '${tablePrefix}execution_entity' ('workflowId', 'finished', 'id') `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS 'IDX_${tablePrefix}1688846335d274033e15c846a4' ON '${tablePrefix}execution_entity' ('finished', 'id') `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS 'IDX_${tablePrefix}b94b45ce2c73ce46c54f20b5f9' ON '${tablePrefix}execution_entity' ('waitTill', 'id') `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS 'IDX_${tablePrefix}81fc04c8a17de15835713505e4' ON '${tablePrefix}execution_entity' ('workflowId', 'id') `); - } + await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}ca4a71b47f28ac6ea88293a8e2'`); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS 'IDX_${tablePrefix}06da892aaf92a48e7d3e400003' ON '${tablePrefix}execution_entity' ('workflowId', 'waitTill', 'id') `, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS 'IDX_${tablePrefix}78d62b89dc1433192b86dce18a' ON '${tablePrefix}execution_entity' ('workflowId', 'finished', 'id') `, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS 'IDX_${tablePrefix}1688846335d274033e15c846a4' ON '${tablePrefix}execution_entity' ('finished', 'id') `, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS 'IDX_${tablePrefix}b94b45ce2c73ce46c54f20b5f9' ON '${tablePrefix}execution_entity' ('waitTill', 'id') `, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS 'IDX_${tablePrefix}81fc04c8a17de15835713505e4' ON '${tablePrefix}execution_entity' ('workflowId', 'id') `, + ); + } - public async down(queryRunner: QueryRunner): Promise { - const tablePrefix = config.get('database.tablePrefix'); - - await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}81fc04c8a17de15835713505e4'`); - await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}b94b45ce2c73ce46c54f20b5f9'`); - await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}1688846335d274033e15c846a4'`); - await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}78d62b89dc1433192b86dce18a'`); - await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}06da892aaf92a48e7d3e400003'`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS 'IDX_${tablePrefix}ca4a71b47f28ac6ea88293a8e2' ON '${tablePrefix}execution_entity' ('waitTill') `); - } + public async down(queryRunner: QueryRunner): Promise { + const tablePrefix = config.get('database.tablePrefix'); + await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}81fc04c8a17de15835713505e4'`); + await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}b94b45ce2c73ce46c54f20b5f9'`); + await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}1688846335d274033e15c846a4'`); + await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}78d62b89dc1433192b86dce18a'`); + await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}06da892aaf92a48e7d3e400003'`); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS 'IDX_${tablePrefix}ca4a71b47f28ac6ea88293a8e2' ON '${tablePrefix}execution_entity' ('waitTill') `, + ); + } } diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index 64c898318a031..f37ba32608a27 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -24,6 +24,7 @@ describe('auth endpoints', () => { await utils.truncate(['User']); globalOwnerRole = await getGlobalOwnerRole(); + utils.initLogger(); }); beforeEach(async () => { diff --git a/packages/cli/test/integration/auth.middleware.test.ts b/packages/cli/test/integration/auth.middleware.test.ts index 0e193f596079c..beca5767ae3aa 100644 --- a/packages/cli/test/integration/auth.middleware.test.ts +++ b/packages/cli/test/integration/auth.middleware.test.ts @@ -9,6 +9,7 @@ describe('/me endpoints', () => { beforeAll(async () => { app = utils.initTestServer({ applyAuth: true }); + utils.initLogger(); }); describe('Unauthorized requests', () => { diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index 174b59f79b84f..94f496f64c9c9 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -30,6 +30,7 @@ describe('/me endpoints', () => { await utils.initTestDb(); globalOwnerRole = await getGlobalOwnerRole(); + utils.initLogger(); }); beforeEach(async () => { diff --git a/packages/cli/test/integration/owner.endpoints.test.ts b/packages/cli/test/integration/owner.endpoints.test.ts index b72dda6ebbadf..c5c31717ed8ad 100644 --- a/packages/cli/test/integration/owner.endpoints.test.ts +++ b/packages/cli/test/integration/owner.endpoints.test.ts @@ -19,6 +19,8 @@ describe('/owner endpoints', () => { beforeAll(async () => { app = utils.initTestServer({ namespaces: ['owner'], applyAuth: true }); await utils.initTestDb(); + + utils.initLogger(); }); beforeEach(async () => { diff --git a/packages/cli/test/integration/passwordReset.endpoints.test.ts b/packages/cli/test/integration/passwordReset.endpoints.test.ts index d56af1367fdc8..7adbfd94a9d75 100644 --- a/packages/cli/test/integration/passwordReset.endpoints.test.ts +++ b/packages/cli/test/integration/passwordReset.endpoints.test.ts @@ -26,6 +26,7 @@ beforeAll(async () => { name: 'owner', scope: 'global', }); + utils.initLogger(); }); beforeEach(async () => {