refactor(core): Move all user DB access to UserRepository (#6910)

Prep for https://linear.app/n8n/issue/PAY-646
This commit is contained in:
Iván Ovejero 2023-08-22 15:58:05 +02:00 committed by GitHub
parent 67b88f75f4
commit 96a9de68a0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 209 additions and 184 deletions

View file

@ -167,8 +167,6 @@ import { SourceControlService } from '@/environments/sourceControl/sourceControl
import { SourceControlController } from '@/environments/sourceControl/sourceControl.controller.ee'; import { SourceControlController } from '@/environments/sourceControl/sourceControl.controller.ee';
import { ExecutionRepository } from '@db/repositories'; import { ExecutionRepository } from '@db/repositories';
import type { ExecutionEntity } from '@db/entities/ExecutionEntity'; import type { ExecutionEntity } from '@db/entities/ExecutionEntity';
import { JwtService } from './services/jwt.service';
import { RoleService } from './services/role.service';
const exec = promisify(callbackExec); const exec = promisify(callbackExec);
@ -485,22 +483,34 @@ export class Server extends AbstractServer {
const internalHooks = Container.get(InternalHooks); const internalHooks = Container.get(InternalHooks);
const mailer = Container.get(UserManagementMailer); const mailer = Container.get(UserManagementMailer);
const postHog = this.postHog; const postHog = this.postHog;
const jwtService = Container.get(JwtService);
const controllers: object[] = [ const controllers: object[] = [
new EventBusController(), new EventBusController(),
new AuthController({ config, internalHooks, repositories, logger, postHog }), new AuthController({
new OwnerController({ config, internalHooks, repositories, logger, postHog }), config,
new MeController({ externalHooks, internalHooks, repositories, logger }), internalHooks,
repositories,
logger,
postHog,
}),
new OwnerController({
config,
internalHooks,
repositories,
logger,
}),
new MeController({
externalHooks,
internalHooks,
logger,
}),
new NodeTypesController({ config, nodeTypes }), new NodeTypesController({ config, nodeTypes }),
new PasswordResetController({ new PasswordResetController({
config, config,
externalHooks, externalHooks,
internalHooks, internalHooks,
mailer, mailer,
repositories,
logger, logger,
jwtService,
}), }),
Container.get(TagsController), Container.get(TagsController),
new TranslationController(config, this.credentialTypes), new TranslationController(config, this.credentialTypes),
@ -513,8 +523,6 @@ export class Server extends AbstractServer {
activeWorkflowRunner, activeWorkflowRunner,
logger, logger,
postHog, postHog,
jwtService,
roleService: Container.get(RoleService),
}), }),
Container.get(SamlController), Container.get(SamlController),
Container.get(SourceControlController), Container.get(SourceControlController),

View file

@ -11,7 +11,7 @@ import config from '@/config';
import type { SharedCredentials } from '@db/entities/SharedCredentials'; import type { SharedCredentials } from '@db/entities/SharedCredentials';
import { isSharingEnabled } from './UserManagementHelper'; import { isSharingEnabled } from './UserManagementHelper';
import { WorkflowsService } from '@/workflows/workflows.services'; import { WorkflowsService } from '@/workflows/workflows.services';
import { UserService } from '@/user/user.service'; import { UserService } from '@/services/user.service';
import { OwnershipService } from '@/services/ownership.service'; import { OwnershipService } from '@/services/ownership.service';
import Container from 'typedi'; import Container from 'typedi';
import { RoleService } from '@/services/role.service'; import { RoleService } from '@/services/role.service';
@ -135,7 +135,7 @@ export class PermissionChecker {
} }
if (policy === 'workflowsFromSameOwner') { if (policy === 'workflowsFromSameOwner') {
const user = await UserService.get({ id: userId }); const user = await Container.get(UserService).findOne({ where: { id: userId } });
if (!user) { if (!user) {
throw new WorkflowOperationError( throw new WorkflowOperationError(
'Fatal error: user not found. Please contact the system administrator.', 'Fatal error: user not found. Please contact the system administrator.',

View file

@ -39,7 +39,7 @@ import omit from 'lodash/omit';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import { PermissionChecker } from './UserManagement/PermissionChecker'; import { PermissionChecker } from './UserManagement/PermissionChecker';
import { isWorkflowIdValid } from './utils'; import { isWorkflowIdValid } from './utils';
import { UserService } from './user/user.service'; import { UserService } from './services/user.service';
import type { SharedWorkflow } from '@db/entities/SharedWorkflow'; import type { SharedWorkflow } from '@db/entities/SharedWorkflow';
import type { RoleNames } from '@db/entities/Role'; import type { RoleNames } from '@db/entities/Role';
import { RoleService } from './services/role.service'; import { RoleService } from './services/role.service';
@ -517,7 +517,7 @@ export async function isBelowOnboardingThreshold(user: User): Promise<boolean> {
// user is above threshold --> set flag in settings // user is above threshold --> set flag in settings
if (!belowThreshold) { if (!belowThreshold) {
void UserService.updateUserSettings(user.id, { isOnboarded: true }); void Container.get(UserService).updateSettings(user.id, { isOnboarded: true });
} }
return belowThreshold; return belowThreshold;

View file

@ -29,9 +29,9 @@ import {
isLdapCurrentAuthenticationMethod, isLdapCurrentAuthenticationMethod,
isSamlCurrentAuthenticationMethod, isSamlCurrentAuthenticationMethod,
} from '@/sso/ssoHelpers'; } from '@/sso/ssoHelpers';
import type { UserRepository } from '@db/repositories';
import { InternalHooks } from '../InternalHooks'; import { InternalHooks } from '../InternalHooks';
import { License } from '@/License'; import { License } from '@/License';
import { UserService } from '@/services/user.service';
@RestController() @RestController()
export class AuthController { export class AuthController {
@ -41,7 +41,7 @@ export class AuthController {
private readonly internalHooks: IInternalHooksClass; private readonly internalHooks: IInternalHooksClass;
private readonly userRepository: UserRepository; private readonly userService: UserService;
private readonly postHog?: PostHogClient; private readonly postHog?: PostHogClient;
@ -49,7 +49,6 @@ export class AuthController {
config, config,
logger, logger,
internalHooks, internalHooks,
repositories,
postHog, postHog,
}: { }: {
config: Config; config: Config;
@ -61,8 +60,8 @@ export class AuthController {
this.config = config; this.config = config;
this.logger = logger; this.logger = logger;
this.internalHooks = internalHooks; this.internalHooks = internalHooks;
this.userRepository = repositories.User;
this.postHog = postHog; this.postHog = postHog;
this.userService = Container.get(UserService);
} }
/** /**
@ -137,10 +136,7 @@ export class AuthController {
} }
try { try {
user = await this.userRepository.findOneOrFail({ user = await this.userService.findOneOrFail({ where: {} });
relations: ['globalRole'],
where: {},
});
} catch (error) { } catch (error) {
throw new InternalServerError( throw new InternalServerError(
'No users found in database - did you wipe the users table? Create at least one user.', 'No users found in database - did you wipe the users table? Create at least one user.',
@ -189,7 +185,7 @@ export class AuthController {
} }
} }
const users = await this.userRepository.find({ where: { id: In([inviterId, inviteeId]) } }); const users = await this.userService.findMany({ where: { id: In([inviterId, inviteeId]) } });
if (users.length !== 2) { if (users.length !== 2) {
this.logger.debug( this.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', 'Request to resolve signup token failed because the ID of the inviter and/or the ID of the invitee were not found in database',

View file

@ -11,7 +11,6 @@ import { BadRequestError } from '@/ResponseHelper';
import { validateEntity } from '@/GenericHelpers'; import { validateEntity } from '@/GenericHelpers';
import { issueCookie } from '@/auth/jwt'; import { issueCookie } from '@/auth/jwt';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import type { UserRepository } from '@db/repositories';
import { Response } from 'express'; import { Response } from 'express';
import type { ILogger } from 'n8n-workflow'; import type { ILogger } from 'n8n-workflow';
import { import {
@ -20,15 +19,11 @@ import {
UserSettingsUpdatePayload, UserSettingsUpdatePayload,
UserUpdatePayload, UserUpdatePayload,
} from '@/requests'; } from '@/requests';
import type { import type { PublicUser, IExternalHooksClass, IInternalHooksClass } from '@/Interfaces';
PublicUser,
IDatabaseCollections,
IExternalHooksClass,
IInternalHooksClass,
} from '@/Interfaces';
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
import { isSamlLicensedAndEnabled } from '../sso/saml/samlHelpers'; import { isSamlLicensedAndEnabled } from '../sso/saml/samlHelpers';
import { UserService } from '@/user/user.service'; import { UserService } from '@/services/user.service';
import Container from 'typedi';
@Authorized() @Authorized()
@RestController('/me') @RestController('/me')
@ -39,23 +34,21 @@ export class MeController {
private readonly internalHooks: IInternalHooksClass; private readonly internalHooks: IInternalHooksClass;
private readonly userRepository: UserRepository; private readonly userService: UserService;
constructor({ constructor({
logger, logger,
externalHooks, externalHooks,
internalHooks, internalHooks,
repositories,
}: { }: {
logger: ILogger; logger: ILogger;
externalHooks: IExternalHooksClass; externalHooks: IExternalHooksClass;
internalHooks: IInternalHooksClass; internalHooks: IInternalHooksClass;
repositories: Pick<IDatabaseCollections, 'User'>;
}) { }) {
this.logger = logger; this.logger = logger;
this.externalHooks = externalHooks; this.externalHooks = externalHooks;
this.internalHooks = internalHooks; this.internalHooks = internalHooks;
this.userRepository = repositories.User; this.userService = Container.get(UserService);
} }
/** /**
@ -99,11 +92,8 @@ export class MeController {
} }
} }
await this.userRepository.update(userId, payload); await this.userService.update(userId, payload);
const user = await this.userRepository.findOneOrFail({ const user = await this.userService.findOneOrFail({ where: { id: userId } });
where: { id: userId },
relations: { globalRole: true },
});
this.logger.info('User updated successfully', { userId }); this.logger.info('User updated successfully', { userId });
@ -154,7 +144,7 @@ export class MeController {
req.user.password = await hashPassword(validPassword); req.user.password = await hashPassword(validPassword);
const user = await this.userRepository.save(req.user); const user = await this.userService.save(req.user);
this.logger.info('Password updated successfully', { userId: user.id }); this.logger.info('Password updated successfully', { userId: user.id });
await issueCookie(res, user); await issueCookie(res, user);
@ -186,8 +176,9 @@ export class MeController {
throw new BadRequestError('Personalization answers are mandatory'); throw new BadRequestError('Personalization answers are mandatory');
} }
await this.userRepository.save({ await this.userService.save({
id: req.user.id, id: req.user.id,
// @ts-ignore
personalizationAnswers, personalizationAnswers,
}); });
@ -205,9 +196,7 @@ export class MeController {
async createAPIKey(req: AuthenticatedRequest) { async createAPIKey(req: AuthenticatedRequest) {
const apiKey = `n8n_api_${randomBytes(40).toString('hex')}`; const apiKey = `n8n_api_${randomBytes(40).toString('hex')}`;
await this.userRepository.update(req.user.id, { await this.userService.update(req.user.id, { apiKey });
apiKey,
});
void this.internalHooks.onApiKeyCreated({ void this.internalHooks.onApiKeyCreated({
user: req.user, user: req.user,
@ -230,9 +219,7 @@ export class MeController {
*/ */
@Delete('/api-key') @Delete('/api-key')
async deleteAPIKey(req: AuthenticatedRequest) { async deleteAPIKey(req: AuthenticatedRequest) {
await this.userRepository.update(req.user.id, { await this.userService.update(req.user.id, { apiKey: null });
apiKey: null,
});
void this.internalHooks.onApiKeyDeleted({ void this.internalHooks.onApiKeyDeleted({
user: req.user, user: req.user,
@ -250,9 +237,9 @@ export class MeController {
const payload = plainToInstance(UserSettingsUpdatePayload, req.body); const payload = plainToInstance(UserSettingsUpdatePayload, req.body);
const { id } = req.user; const { id } = req.user;
await UserService.updateUserSettings(id, payload); await this.userService.updateSettings(id, payload);
const user = await this.userRepository.findOneOrFail({ const user = await this.userService.findOneOrFail({
select: ['settings'], select: ['settings'],
where: { id }, where: { id },
}); });

View file

@ -14,7 +14,9 @@ import type { ILogger } from 'n8n-workflow';
import type { Config } from '@/config'; import type { Config } from '@/config';
import { OwnerRequest } from '@/requests'; import { OwnerRequest } from '@/requests';
import type { IDatabaseCollections, IInternalHooksClass } from '@/Interfaces'; import type { IDatabaseCollections, IInternalHooksClass } from '@/Interfaces';
import type { SettingsRepository, UserRepository } from '@db/repositories'; import type { SettingsRepository } from '@db/repositories';
import { UserService } from '@/services/user.service';
import Container from 'typedi';
import type { PostHogClient } from '@/posthog'; import type { PostHogClient } from '@/posthog';
@Authorized(['global', 'owner']) @Authorized(['global', 'owner'])
@ -26,7 +28,7 @@ export class OwnerController {
private readonly internalHooks: IInternalHooksClass; private readonly internalHooks: IInternalHooksClass;
private readonly userRepository: UserRepository; private readonly userService: UserService;
private readonly settingsRepository: SettingsRepository; private readonly settingsRepository: SettingsRepository;
@ -42,13 +44,13 @@ export class OwnerController {
config: Config; config: Config;
logger: ILogger; logger: ILogger;
internalHooks: IInternalHooksClass; internalHooks: IInternalHooksClass;
repositories: Pick<IDatabaseCollections, 'User' | 'Settings'>; repositories: Pick<IDatabaseCollections, 'Settings'>;
postHog?: PostHogClient; postHog?: PostHogClient;
}) { }) {
this.config = config; this.config = config;
this.logger = logger; this.logger = logger;
this.internalHooks = internalHooks; this.internalHooks = internalHooks;
this.userRepository = repositories.User; this.userService = Container.get(UserService);
this.settingsRepository = repositories.Settings; this.settingsRepository = repositories.Settings;
this.postHog = postHog; this.postHog = postHog;
} }
@ -112,7 +114,7 @@ export class OwnerController {
await validateEntity(owner); await validateEntity(owner);
owner = await this.userRepository.save(owner); owner = await this.userService.save(owner);
this.logger.info('Owner was set up successfully', { userId }); this.logger.info('Owner was set up successfully', { userId });

View file

@ -18,18 +18,18 @@ import type { UserManagementMailer } from '@/UserManagement/email';
import { Response } from 'express'; import { Response } from 'express';
import type { ILogger } from 'n8n-workflow'; import type { ILogger } from 'n8n-workflow';
import type { Config } from '@/config'; import type { Config } from '@/config';
import type { UserRepository } from '@db/repositories';
import { PasswordResetRequest } from '@/requests'; import { PasswordResetRequest } from '@/requests';
import type { IDatabaseCollections, IExternalHooksClass, IInternalHooksClass } from '@/Interfaces'; import type { IExternalHooksClass, IInternalHooksClass } from '@/Interfaces';
import { issueCookie } from '@/auth/jwt'; import { issueCookie } from '@/auth/jwt';
import { isLdapEnabled } from '@/Ldap/helpers'; import { isLdapEnabled } from '@/Ldap/helpers';
import { isSamlCurrentAuthenticationMethod } from '@/sso/ssoHelpers'; import { isSamlCurrentAuthenticationMethod } from '@/sso/ssoHelpers';
import { UserService } from '@/user/user.service'; import { UserService } from '@/services/user.service';
import { License } from '@/License'; import { License } from '@/License';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { TokenExpiredError } from 'jsonwebtoken'; import { TokenExpiredError } from 'jsonwebtoken';
import type { JwtService, JwtPayload } from '@/services/jwt.service'; import type { JwtPayload } from '@/services/jwt.service';
import { JwtService } from '@/services/jwt.service';
@RestController() @RestController()
export class PasswordResetController { export class PasswordResetController {
@ -43,34 +43,30 @@ export class PasswordResetController {
private readonly mailer: UserManagementMailer; private readonly mailer: UserManagementMailer;
private readonly userRepository: UserRepository;
private readonly jwtService: JwtService; private readonly jwtService: JwtService;
private readonly userService: UserService;
constructor({ constructor({
config, config,
logger, logger,
externalHooks, externalHooks,
internalHooks, internalHooks,
mailer, mailer,
repositories,
jwtService,
}: { }: {
config: Config; config: Config;
logger: ILogger; logger: ILogger;
externalHooks: IExternalHooksClass; externalHooks: IExternalHooksClass;
internalHooks: IInternalHooksClass; internalHooks: IInternalHooksClass;
mailer: UserManagementMailer; mailer: UserManagementMailer;
repositories: Pick<IDatabaseCollections, 'User'>;
jwtService: JwtService;
}) { }) {
this.config = config; this.config = config;
this.logger = logger; this.logger = logger;
this.externalHooks = externalHooks; this.externalHooks = externalHooks;
this.internalHooks = internalHooks; this.internalHooks = internalHooks;
this.mailer = mailer; this.mailer = mailer;
this.userRepository = repositories.User; this.jwtService = Container.get(JwtService);
this.jwtService = jwtService; this.userService = Container.get(UserService);
} }
/** /**
@ -105,7 +101,7 @@ export class PasswordResetController {
} }
// User should just be able to reset password if one is already present // User should just be able to reset password if one is already present
const user = await this.userRepository.findOne({ const user = await this.userService.findOne({
where: { where: {
email, email,
password: Not(IsNull()), password: Not(IsNull()),
@ -154,7 +150,7 @@ export class PasswordResetController {
}, },
); );
const url = await UserService.generatePasswordResetUrl(baseUrl, resetPasswordToken); const url = this.userService.generatePasswordResetUrl(baseUrl, resetPasswordToken);
try { try {
await this.mailer.passwordReset({ await this.mailer.passwordReset({
@ -204,10 +200,8 @@ export class PasswordResetController {
const decodedToken = this.verifyResetPasswordToken(resetPasswordToken); const decodedToken = this.verifyResetPasswordToken(resetPasswordToken);
const user = await this.userRepository.findOne({ const user = await this.userService.findOne({
where: { where: { id: decodedToken.sub },
id: decodedToken.sub,
},
relations: ['globalRole'], relations: ['globalRole'],
}); });
@ -255,7 +249,7 @@ export class PasswordResetController {
const decodedToken = this.verifyResetPasswordToken(resetPasswordToken); const decodedToken = this.verifyResetPasswordToken(resetPasswordToken);
const user = await this.userRepository.findOne({ const user = await this.userService.findOne({
where: { id: decodedToken.sub }, where: { id: decodedToken.sub },
relations: ['authIdentities'], relations: ['authIdentities'],
}); });
@ -272,9 +266,7 @@ export class PasswordResetController {
const passwordHash = await hashPassword(validPassword); const passwordHash = await hashPassword(validPassword);
await this.userRepository.update(user.id, { await this.userService.update(user.id, { password: passwordHash });
password: passwordHash,
});
this.logger.info('User password updated successfully', { userId: user.id }); this.logger.info('User password updated successfully', { userId: user.id });

View file

@ -38,18 +38,14 @@ import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { AuthIdentity } from '@db/entities/AuthIdentity'; import { AuthIdentity } from '@db/entities/AuthIdentity';
import type { PostHogClient } from '@/posthog'; import type { PostHogClient } from '@/posthog';
import { isSamlLicensedAndEnabled } from '../sso/saml/samlHelpers'; import { isSamlLicensedAndEnabled } from '../sso/saml/samlHelpers';
import type { import type { SharedCredentialsRepository, SharedWorkflowRepository } from '@db/repositories';
SharedCredentialsRepository,
SharedWorkflowRepository,
UserRepository,
} from '@db/repositories';
import { UserService } from '@/user/user.service';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { License } from '@/License'; import { License } from '@/License';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import type { JwtService } from '@/services/jwt.service'; import { JwtService } from '@/services/jwt.service';
import type { RoleService } from '@/services/role.service'; import { RoleService } from '@/services/role.service';
import { UserService } from '@/services/user.service';
@Authorized(['global', 'owner']) @Authorized(['global', 'owner'])
@RestController('/users') @RestController('/users')
@ -62,8 +58,6 @@ export class UsersController {
private internalHooks: IInternalHooksClass; private internalHooks: IInternalHooksClass;
private userRepository: UserRepository;
private sharedCredentialsRepository: SharedCredentialsRepository; private sharedCredentialsRepository: SharedCredentialsRepository;
private sharedWorkflowRepository: SharedWorkflowRepository; private sharedWorkflowRepository: SharedWorkflowRepository;
@ -78,6 +72,8 @@ export class UsersController {
private roleService: RoleService; private roleService: RoleService;
private userService: UserService;
constructor({ constructor({
config, config,
logger, logger,
@ -86,33 +82,29 @@ export class UsersController {
repositories, repositories,
activeWorkflowRunner, activeWorkflowRunner,
mailer, mailer,
jwtService,
postHog, postHog,
roleService,
}: { }: {
config: Config; config: Config;
logger: ILogger; logger: ILogger;
externalHooks: IExternalHooksClass; externalHooks: IExternalHooksClass;
internalHooks: IInternalHooksClass; internalHooks: IInternalHooksClass;
repositories: Pick<IDatabaseCollections, 'User' | 'SharedCredentials' | 'SharedWorkflow'>; repositories: Pick<IDatabaseCollections, 'SharedCredentials' | 'SharedWorkflow'>;
activeWorkflowRunner: ActiveWorkflowRunner; activeWorkflowRunner: ActiveWorkflowRunner;
mailer: UserManagementMailer; mailer: UserManagementMailer;
jwtService: JwtService;
postHog?: PostHogClient; postHog?: PostHogClient;
roleService: RoleService;
}) { }) {
this.config = config; this.config = config;
this.logger = logger; this.logger = logger;
this.externalHooks = externalHooks; this.externalHooks = externalHooks;
this.internalHooks = internalHooks; this.internalHooks = internalHooks;
this.userRepository = repositories.User;
this.sharedCredentialsRepository = repositories.SharedCredentials; this.sharedCredentialsRepository = repositories.SharedCredentials;
this.sharedWorkflowRepository = repositories.SharedWorkflow; this.sharedWorkflowRepository = repositories.SharedWorkflow;
this.activeWorkflowRunner = activeWorkflowRunner; this.activeWorkflowRunner = activeWorkflowRunner;
this.mailer = mailer; this.mailer = mailer;
this.jwtService = jwtService; this.jwtService = Container.get(JwtService);
this.postHog = postHog; this.postHog = postHog;
this.roleService = roleService; this.roleService = Container.get(RoleService);
this.userService = Container.get(UserService);
} }
/** /**
@ -185,7 +177,7 @@ export class UsersController {
} }
// remove/exclude existing users from creation // remove/exclude existing users from creation
const existingUsers = await this.userRepository.find({ const existingUsers = await this.userService.findMany({
where: { email: In(Object.keys(createUsers)) }, where: { email: In(Object.keys(createUsers)) },
}); });
existingUsers.forEach((user) => { existingUsers.forEach((user) => {
@ -202,7 +194,7 @@ export class UsersController {
this.logger.debug(total > 1 ? `Creating ${total} user shells...` : 'Creating 1 user shell...'); this.logger.debug(total > 1 ? `Creating ${total} user shells...` : 'Creating 1 user shell...');
try { try {
await this.userRepository.manager.transaction(async (transactionManager) => await this.userService.getManager().transaction(async (transactionManager) =>
Promise.all( Promise.all(
usersToSetUp.map(async (email) => { usersToSetUp.map(async (email) => {
const newUser = Object.assign(new User(), { const newUser = Object.assign(new User(), {
@ -323,7 +315,7 @@ export class UsersController {
const validPassword = validatePassword(password); const validPassword = validatePassword(password);
const users = await this.userRepository.find({ const users = await this.userService.findMany({
where: { id: In([inviterId, inviteeId]) }, where: { id: In([inviterId, inviteeId]) },
relations: ['globalRole'], relations: ['globalRole'],
}); });
@ -353,7 +345,7 @@ export class UsersController {
invitee.lastName = lastName; invitee.lastName = lastName;
invitee.password = await hashPassword(validPassword); invitee.password = await hashPassword(validPassword);
const updatedUser = await this.userRepository.save(invitee); const updatedUser = await this.userService.save(invitee);
await issueCookie(res, updatedUser); await issueCookie(res, updatedUser);
@ -371,7 +363,7 @@ export class UsersController {
@Authorized('any') @Authorized('any')
@Get('/') @Get('/')
async listUsers(req: UserRequest.List) { async listUsers(req: UserRequest.List) {
const users = await this.userRepository.find({ relations: ['globalRole', 'authIdentities'] }); const users = await this.userService.findMany({ relations: ['globalRole', 'authIdentities'] });
return users.map( return users.map(
(user): PublicUser => (user): PublicUser =>
addInviteLinkToUser(sanitizeUser(user, ['personalizationAnswers']), req.user.id), addInviteLinkToUser(sanitizeUser(user, ['personalizationAnswers']), req.user.id),
@ -381,7 +373,7 @@ export class UsersController {
@Authorized(['global', 'owner']) @Authorized(['global', 'owner'])
@Get('/:id/password-reset-link') @Get('/:id/password-reset-link')
async getUserPasswordResetLink(req: UserRequest.PasswordResetLink) { async getUserPasswordResetLink(req: UserRequest.PasswordResetLink) {
const user = await this.userRepository.findOneOrFail({ const user = await this.userService.findOneOrFail({
where: { id: req.params.id }, where: { id: req.params.id },
}); });
if (!user) { if (!user) {
@ -397,7 +389,7 @@ export class UsersController {
const baseUrl = getInstanceBaseUrl(); const baseUrl = getInstanceBaseUrl();
const link = await UserService.generatePasswordResetUrl(baseUrl, resetPasswordToken); const link = this.userService.generatePasswordResetUrl(baseUrl, resetPasswordToken);
return { return {
link, link,
}; };
@ -410,9 +402,9 @@ export class UsersController {
const id = req.params.id; const id = req.params.id;
await UserService.updateUserSettings(id, payload); await this.userService.updateSettings(id, payload);
const user = await this.userRepository.findOneOrFail({ const user = await this.userService.findOneOrFail({
select: ['settings'], select: ['settings'],
where: { id }, where: { id },
}); });
@ -443,7 +435,7 @@ export class UsersController {
); );
} }
const users = await this.userRepository.find({ const users = await this.userService.findMany({
where: { id: In([transferId, idToDelete]) }, where: { id: In([transferId, idToDelete]) },
}); });
@ -475,7 +467,7 @@ export class UsersController {
if (transferId) { if (transferId) {
const transferee = users.find((user) => user.id === transferId); const transferee = users.find((user) => user.id === transferId);
await this.userRepository.manager.transaction(async (transactionManager) => { await this.userService.getManager().transaction(async (transactionManager) => {
// Get all workflow ids belonging to user to delete // Get all workflow ids belonging to user to delete
const sharedWorkflowIds = await transactionManager const sharedWorkflowIds = await transactionManager
.getRepository(SharedWorkflow) .getRepository(SharedWorkflow)
@ -550,7 +542,7 @@ export class UsersController {
}), }),
]); ]);
await this.userRepository.manager.transaction(async (transactionManager) => { await this.userService.getManager().transaction(async (transactionManager) => {
const ownedWorkflows = await Promise.all( const ownedWorkflows = await Promise.all(
ownedSharedWorkflows.map(async ({ workflow }) => { ownedSharedWorkflows.map(async ({ workflow }) => {
if (workflow.active) { if (workflow.active) {
@ -597,7 +589,7 @@ export class UsersController {
throw new InternalServerError('Email sending must be set up in order to invite other users'); throw new InternalServerError('Email sending must be set up in order to invite other users');
} }
const reinvitee = await this.userRepository.findOneBy({ id: idToReinvite }); const reinvitee = await this.userService.findOneBy({ id: idToReinvite });
if (!reinvitee) { if (!reinvitee) {
this.logger.debug( this.logger.debug(
'Request to reinvite a user failed because the ID of the reinvitee was not found in database', 'Request to reinvite a user failed because the ID of the reinvitee was not found in database',

View file

@ -4,7 +4,7 @@ import * as Db from '@/Db';
import { CredentialsEntity } from '@db/entities/CredentialsEntity'; import { CredentialsEntity } from '@db/entities/CredentialsEntity';
import { SharedCredentials } from '@db/entities/SharedCredentials'; import { SharedCredentials } from '@db/entities/SharedCredentials';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { UserService } from '@/user/user.service'; import { UserService } from '@/services/user.service';
import { CredentialsService } from './credentials.service'; import { CredentialsService } from './credentials.service';
import type { CredentialWithSharings } from './credentials.types'; import type { CredentialWithSharings } from './credentials.types';
import { RoleService } from '@/services/role.service'; import { RoleService } from '@/services/role.service';
@ -78,7 +78,7 @@ export class EECredentialsService extends CredentialsService {
credential: CredentialsEntity, credential: CredentialsEntity,
shareWithIds: string[], shareWithIds: string[],
): Promise<SharedCredentials[]> { ): Promise<SharedCredentials[]> {
const users = await UserService.getByIds(transaction, shareWithIds); const users = await Container.get(UserService).getByIds(transaction, shareWithIds);
const role = await Container.get(RoleService).findCredentialUserRole(); const role = await Container.get(RoleService).findCredentialUserRole();
const newSharedCredentials = users const newSharedCredentials = users

View file

@ -4,7 +4,7 @@ import type { INode, IRun, IWorkflowBase } from 'n8n-workflow';
import { LoggerProxy } from 'n8n-workflow'; import { LoggerProxy } from 'n8n-workflow';
import { StatisticsNames } from '@db/entities/WorkflowStatistics'; import { StatisticsNames } from '@db/entities/WorkflowStatistics';
import { WorkflowStatisticsRepository } from '@db/repositories'; import { WorkflowStatisticsRepository } from '@db/repositories';
import { UserService } from '@/user/user.service'; import { UserService } from '@/services/user.service';
import { OwnershipService } from './ownership.service'; import { OwnershipService } from './ownership.service';
@Service() @Service()
@ -51,7 +51,7 @@ export class EventsService extends EventEmitter {
}; };
if (!owner.settings?.userActivated) { if (!owner.settings?.userActivated) {
await UserService.updateUserSettings(owner.id, { await Container.get(UserService).updateSettings(owner.id, {
firstSuccessfulWorkflowId: workflowId, firstSuccessfulWorkflowId: workflowId,
userActivated: true, userActivated: true,
}); });

View file

@ -1,8 +1,9 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import { CacheService } from './cache.service'; import { CacheService } from './cache.service';
import { SharedWorkflowRepository, UserRepository } from '@/databases/repositories'; import { SharedWorkflowRepository } from '@/databases/repositories';
import type { User } from '@/databases/entities/User'; import type { User } from '@/databases/entities/User';
import { RoleService } from './role.service'; import { RoleService } from './role.service';
import { UserService } from './user.service';
import type { ListQuery } from '@/requests'; import type { ListQuery } from '@/requests';
import type { Role } from '@/databases/entities/Role'; import type { Role } from '@/databases/entities/Role';
@ -10,7 +11,7 @@ import type { Role } from '@/databases/entities/Role';
export class OwnershipService { export class OwnershipService {
constructor( constructor(
private cacheService: CacheService, private cacheService: CacheService,
private userRepository: UserRepository, private userService: UserService,
private roleService: RoleService, private roleService: RoleService,
private sharedWorkflowRepository: SharedWorkflowRepository, private sharedWorkflowRepository: SharedWorkflowRepository,
) {} ) {}
@ -21,7 +22,7 @@ export class OwnershipService {
async getWorkflowOwnerCached(workflowId: string) { async getWorkflowOwnerCached(workflowId: string) {
const cachedValue = (await this.cacheService.get(`cache:workflow-owner:${workflowId}`)) as User; const cachedValue = (await this.cacheService.get(`cache:workflow-owner:${workflowId}`)) as User;
if (cachedValue) return this.userRepository.create(cachedValue); if (cachedValue) return this.userService.create(cachedValue);
const workflowOwnerRole = await this.roleService.findWorkflowOwnerRole(); const workflowOwnerRole = await this.roleService.findWorkflowOwnerRole();

View file

@ -0,0 +1,61 @@
import { Service } from 'typedi';
import type { EntityManager, FindManyOptions, FindOneOptions, FindOptionsWhere } from 'typeorm';
import { In } from 'typeorm';
import { User } from '@db/entities/User';
import type { IUserSettings } from 'n8n-workflow';
import { UserRepository } from '@/databases/repositories';
@Service()
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
async findOne(options: FindOneOptions<User>) {
return this.userRepository.findOne({ relations: ['globalRole'], ...options });
}
async findOneOrFail(options: FindOneOptions<User>) {
return this.userRepository.findOneOrFail({ relations: ['globalRole'], ...options });
}
async findMany(options: FindManyOptions<User>) {
return this.userRepository.find({ relations: ['globalRole'], ...options });
}
async findOneBy(options: FindOptionsWhere<User>) {
return this.userRepository.findOneBy(options);
}
create(data: Partial<User>) {
return this.userRepository.create(data);
}
async save(user: Partial<User>) {
return this.userRepository.save(user);
}
async update(userId: string, data: Partial<User>) {
return this.userRepository.update(userId, data);
}
async getByIds(transaction: EntityManager, ids: string[]) {
return transaction.find(User, { where: { id: In(ids) } });
}
getManager() {
return this.userRepository.manager;
}
async updateSettings(userId: string, newSettings: Partial<IUserSettings>) {
const { settings } = await this.userRepository.findOneOrFail({ where: { id: userId } });
return this.userRepository.update(userId, { settings: { ...settings, ...newSettings } });
}
generatePasswordResetUrl(instanceBaseUrl: string, token: string) {
const url = new URL(`${instanceBaseUrl}/change-password`);
url.searchParams.append('token', token);
return url.toString();
}
}

View file

@ -1,31 +0,0 @@
import type { EntityManager, FindOptionsWhere } from 'typeorm';
import { In } from 'typeorm';
import * as Db from '@/Db';
import { User } from '@db/entities/User';
import type { IUserSettings } from 'n8n-workflow';
export class UserService {
static async get(where: FindOptionsWhere<User>): Promise<User | null> {
return Db.collections.User.findOne({
relations: ['globalRole'],
where,
});
}
static async getByIds(transaction: EntityManager, ids: string[]) {
return transaction.find(User, { where: { id: In(ids) } });
}
static async updateUserSettings(id: string, userSettings: Partial<IUserSettings>) {
const { settings: currentSettings } = await Db.collections.User.findOneOrFail({
where: { id },
});
return Db.collections.User.update(id, { settings: { ...currentSettings, ...userSettings } });
}
static async generatePasswordResetUrl(instanceBaseUrl: string, token: string): Promise<string> {
const url = new URL(`${instanceBaseUrl}/change-password`);
url.searchParams.append('token', token);
return url.toString();
}
}

View file

@ -7,7 +7,7 @@ import type { ICredentialsDb } from '@/Interfaces';
import { SharedWorkflow } from '@db/entities/SharedWorkflow'; import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { UserService } from '@/user/user.service'; import { UserService } from '@/services/user.service';
import { WorkflowsService } from './workflows.services'; import { WorkflowsService } from './workflows.services';
import type { import type {
CredentialUsedByWorkflow, CredentialUsedByWorkflow,
@ -61,7 +61,7 @@ export class EEWorkflowsService extends WorkflowsService {
workflow: WorkflowEntity, workflow: WorkflowEntity,
shareWithIds: string[], shareWithIds: string[],
): Promise<SharedWorkflow[]> { ): Promise<SharedWorkflow[]> {
const users = await UserService.getByIds(transaction, shareWithIds); const users = await Container.get(UserService).getByIds(transaction, shareWithIds);
const role = await Container.get(RoleService).findWorkflowEditorRole(); const role = await Container.get(RoleService).findWorkflowEditorRole();
const newSharedWorkflows = users.reduce<SharedWorkflow[]>((acc, user) => { const newSharedWorkflows = users.reduce<SharedWorkflow[]>((acc, user) => {

View file

@ -50,7 +50,6 @@ import { AUTHLESS_ENDPOINTS, PUBLIC_API_REST_PATH_SEGMENT, REST_PATH_SEGMENT } f
import type { EndpointGroup, SetupProps, TestServer } from '../types'; import type { EndpointGroup, SetupProps, TestServer } from '../types';
import { mockInstance } from './mocking'; import { mockInstance } from './mocking';
import { JwtService } from '@/services/jwt.service'; import { JwtService } from '@/services/jwt.service';
import { RoleService } from '@/services/role.service';
import { MetricsService } from '@/services/metrics.service'; import { MetricsService } from '@/services/metrics.service';
/** /**
@ -198,7 +197,12 @@ export const setupTestServer = ({
registerController( registerController(
app, app,
config, config,
new AuthController({ config, logger, internalHooks, repositories }), new AuthController({
config,
logger,
internalHooks,
repositories,
}),
); );
break; break;
case 'ldap': case 'ldap':
@ -229,7 +233,11 @@ export const setupTestServer = ({
registerController( registerController(
app, app,
config, config,
new MeController({ logger, externalHooks, internalHooks, repositories }), new MeController({
logger,
externalHooks,
internalHooks,
}),
); );
break; break;
case 'passwordReset': case 'passwordReset':
@ -242,8 +250,6 @@ export const setupTestServer = ({
externalHooks, externalHooks,
internalHooks, internalHooks,
mailer, mailer,
repositories,
jwtService,
}), }),
); );
break; break;
@ -251,7 +257,12 @@ export const setupTestServer = ({
registerController( registerController(
app, app,
config, config,
new OwnerController({ config, logger, internalHooks, repositories }), new OwnerController({
config,
logger,
internalHooks,
repositories,
}),
); );
break; break;
case 'users': case 'users':
@ -266,8 +277,6 @@ export const setupTestServer = ({
repositories, repositories,
activeWorkflowRunner: Container.get(ActiveWorkflowRunner), activeWorkflowRunner: Container.get(ActiveWorkflowRunner),
logger, logger,
jwtService,
roleService: Container.get(RoleService),
}), }),
); );
break; break;

View file

@ -10,7 +10,7 @@ import { User } from '@db/entities/User';
import { SharedWorkflow } from '@db/entities/SharedWorkflow'; import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import { UserService } from '@/user/user.service'; import { UserService } from '@/services/user.service';
import { PermissionChecker } from '@/UserManagement/PermissionChecker'; import { PermissionChecker } from '@/UserManagement/PermissionChecker';
import * as UserManagementHelper from '@/UserManagement/UserManagementHelper'; import * as UserManagementHelper from '@/UserManagement/UserManagementHelper';
import { WorkflowsService } from '@/workflows/workflows.services'; import { WorkflowsService } from '@/workflows/workflows.services';
@ -234,6 +234,8 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
const sharedWorkflowNotOwner = new SharedWorkflow(); const sharedWorkflowNotOwner = new SharedWorkflow();
sharedWorkflowNotOwner.role = nonOwnerMockRole; sharedWorkflowNotOwner.role = nonOwnerMockRole;
const userService = mockInstance(UserService);
test('sets default policy from environment when subworkflow has none', async () => { test('sets default policy from environment when subworkflow has none', async () => {
config.set('workflows.callerPolicyDefaultOption', 'none'); config.set('workflows.callerPolicyDefaultOption', 'none');
jest jest
@ -260,7 +262,7 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
.spyOn(ownershipService, 'getWorkflowOwnerCached') .spyOn(ownershipService, 'getWorkflowOwnerCached')
.mockImplementation(async (workflowId) => fakeUser); .mockImplementation(async (workflowId) => fakeUser);
jest.spyOn(UserManagementHelper, 'isSharingEnabled').mockReturnValue(false); jest.spyOn(UserManagementHelper, 'isSharingEnabled').mockReturnValue(false);
jest.spyOn(UserService, 'get').mockImplementation(async () => fakeUser); jest.spyOn(userService, 'findOne').mockImplementation(async () => fakeUser);
jest.spyOn(WorkflowsService, 'getSharing').mockImplementation(async () => { jest.spyOn(WorkflowsService, 'getSharing').mockImplementation(async () => {
return sharedWorkflowNotOwner; return sharedWorkflowNotOwner;
}); });
@ -294,7 +296,7 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
.spyOn(ownershipService, 'getWorkflowOwnerCached') .spyOn(ownershipService, 'getWorkflowOwnerCached')
.mockImplementation(async (workflowId) => fakeUser); .mockImplementation(async (workflowId) => fakeUser);
jest.spyOn(UserManagementHelper, 'isSharingEnabled').mockReturnValue(true); jest.spyOn(UserManagementHelper, 'isSharingEnabled').mockReturnValue(true);
jest.spyOn(UserService, 'get').mockImplementation(async () => fakeUser); jest.spyOn(userService, 'findOne').mockImplementation(async () => fakeUser);
jest.spyOn(WorkflowsService, 'getSharing').mockImplementation(async () => { jest.spyOn(WorkflowsService, 'getSharing').mockImplementation(async () => {
return sharedWorkflowNotOwner; return sharedWorkflowNotOwner;
}); });
@ -320,7 +322,7 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
.spyOn(ownershipService, 'getWorkflowOwnerCached') .spyOn(ownershipService, 'getWorkflowOwnerCached')
.mockImplementation(async (workflowId) => fakeUser); .mockImplementation(async (workflowId) => fakeUser);
jest.spyOn(UserManagementHelper, 'isSharingEnabled').mockReturnValue(false); jest.spyOn(UserManagementHelper, 'isSharingEnabled').mockReturnValue(false);
jest.spyOn(UserService, 'get').mockImplementation(async () => fakeUser); jest.spyOn(userService, 'findOne').mockImplementation(async () => fakeUser);
jest.spyOn(WorkflowsService, 'getSharing').mockImplementation(async () => { jest.spyOn(WorkflowsService, 'getSharing').mockImplementation(async () => {
return sharedWorkflowOwner; return sharedWorkflowOwner;
}); });
@ -343,7 +345,7 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
.spyOn(ownershipService, 'getWorkflowOwnerCached') .spyOn(ownershipService, 'getWorkflowOwnerCached')
.mockImplementation(async (workflowId) => fakeUser); .mockImplementation(async (workflowId) => fakeUser);
jest.spyOn(UserManagementHelper, 'isSharingEnabled').mockReturnValue(true); jest.spyOn(UserManagementHelper, 'isSharingEnabled').mockReturnValue(true);
jest.spyOn(UserService, 'get').mockImplementation(async () => fakeUser); jest.spyOn(userService, 'findOne').mockImplementation(async () => fakeUser);
jest.spyOn(WorkflowsService, 'getSharing').mockImplementation(async () => { jest.spyOn(WorkflowsService, 'getSharing').mockImplementation(async () => {
return sharedWorkflowNotOwner; return sharedWorkflowNotOwner;
}); });
@ -369,7 +371,7 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
.spyOn(ownershipService, 'getWorkflowOwnerCached') .spyOn(ownershipService, 'getWorkflowOwnerCached')
.mockImplementation(async (workflowId) => fakeUser); .mockImplementation(async (workflowId) => fakeUser);
jest.spyOn(UserManagementHelper, 'isSharingEnabled').mockReturnValue(true); jest.spyOn(UserManagementHelper, 'isSharingEnabled').mockReturnValue(true);
jest.spyOn(UserService, 'get').mockImplementation(async () => fakeUser); jest.spyOn(userService, 'findOne').mockImplementation(async () => fakeUser);
jest.spyOn(WorkflowsService, 'getSharing').mockImplementation(async () => { jest.spyOn(WorkflowsService, 'getSharing').mockImplementation(async () => {
return sharedWorkflowNotOwner; return sharedWorkflowNotOwner;
}); });

View file

@ -4,23 +4,24 @@ import { mock, anyObject, captor } from 'jest-mock-extended';
import type { ILogger } from 'n8n-workflow'; import type { ILogger } from 'n8n-workflow';
import type { IExternalHooksClass, IInternalHooksClass } from '@/Interfaces'; import type { IExternalHooksClass, IInternalHooksClass } from '@/Interfaces';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import type { UserRepository } from '@db/repositories';
import { MeController } from '@/controllers'; import { MeController } from '@/controllers';
import { AUTH_COOKIE_NAME } from '@/constants'; import { AUTH_COOKIE_NAME } from '@/constants';
import { BadRequestError } from '@/ResponseHelper'; import { BadRequestError } from '@/ResponseHelper';
import type { AuthenticatedRequest, MeRequest } from '@/requests'; import type { AuthenticatedRequest, MeRequest } from '@/requests';
import { badPasswords } from '../shared/testData'; import { badPasswords } from '../shared/testData';
import { UserService } from '@/services/user.service';
import Container from 'typedi';
describe('MeController', () => { describe('MeController', () => {
const logger = mock<ILogger>(); const logger = mock<ILogger>();
const externalHooks = mock<IExternalHooksClass>(); const externalHooks = mock<IExternalHooksClass>();
const internalHooks = mock<IInternalHooksClass>(); const internalHooks = mock<IInternalHooksClass>();
const userRepository = mock<UserRepository>(); const userService = mock<UserService>();
Container.set(UserService, userService);
const controller = new MeController({ const controller = new MeController({
logger, logger,
externalHooks, externalHooks,
internalHooks, internalHooks,
repositories: { User: userRepository },
}); });
describe('updateCurrentUser', () => { describe('updateCurrentUser', () => {
@ -48,12 +49,12 @@ describe('MeController', () => {
const reqBody = { email: 'valid@email.com', firstName: 'John', lastName: 'Potato' }; const reqBody = { email: 'valid@email.com', firstName: 'John', lastName: 'Potato' };
const req = mock<MeRequest.UserUpdate>({ user, body: reqBody }); const req = mock<MeRequest.UserUpdate>({ user, body: reqBody });
const res = mock<Response>(); const res = mock<Response>();
userRepository.findOneOrFail.mockResolvedValue(user); userService.findOneOrFail.mockResolvedValue(user);
jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token'); jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token');
await controller.updateCurrentUser(req, res); await controller.updateCurrentUser(req, res);
expect(userRepository.update).toHaveBeenCalled(); expect(userService.update).toHaveBeenCalled();
const cookieOptions = captor<CookieOptions>(); const cookieOptions = captor<CookieOptions>();
expect(res.cookie).toHaveBeenCalledWith(AUTH_COOKIE_NAME, 'signed-token', cookieOptions); expect(res.cookie).toHaveBeenCalledWith(AUTH_COOKIE_NAME, 'signed-token', cookieOptions);
@ -76,7 +77,7 @@ describe('MeController', () => {
const reqBody = { email: 'valid@email.com', firstName: 'John', lastName: 'Potato' }; const reqBody = { email: 'valid@email.com', firstName: 'John', lastName: 'Potato' };
const req = mock<MeRequest.UserUpdate>({ user, body: reqBody }); const req = mock<MeRequest.UserUpdate>({ user, body: reqBody });
const res = mock<Response>(); const res = mock<Response>();
userRepository.findOneOrFail.mockResolvedValue(user); userService.findOneOrFail.mockResolvedValue(user);
jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token'); jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token');
// Add invalid data to the request payload // Add invalid data to the request payload
@ -84,9 +85,9 @@ describe('MeController', () => {
await controller.updateCurrentUser(req, res); await controller.updateCurrentUser(req, res);
expect(userRepository.update).toHaveBeenCalled(); expect(userService.update).toHaveBeenCalled();
const updatedUser = userRepository.update.mock.calls[0][1]; const updatedUser = userService.update.mock.calls[0][1];
expect(updatedUser.email).toBe(reqBody.email); expect(updatedUser.email).toBe(reqBody.email);
expect(updatedUser.firstName).toBe(reqBody.firstName); expect(updatedUser.firstName).toBe(reqBody.firstName);
expect(updatedUser.lastName).toBe(reqBody.lastName); expect(updatedUser.lastName).toBe(reqBody.lastName);
@ -138,7 +139,7 @@ describe('MeController', () => {
body: { currentPassword: 'old_password', newPassword: 'NewPassword123' }, body: { currentPassword: 'old_password', newPassword: 'NewPassword123' },
}); });
const res = mock<Response>(); const res = mock<Response>();
userRepository.save.calledWith(req.user).mockResolvedValue(req.user); userService.save.calledWith(req.user).mockResolvedValue(req.user);
jest.spyOn(jwt, 'sign').mockImplementation(() => 'new-signed-token'); jest.spyOn(jwt, 'sign').mockImplementation(() => 'new-signed-token');
await controller.updatePassword(req, res); await controller.updatePassword(req, res);
@ -171,7 +172,7 @@ describe('MeController', () => {
describe('createAPIKey', () => { describe('createAPIKey', () => {
it('should create and save an API key', async () => { it('should create and save an API key', async () => {
const { apiKey } = await controller.createAPIKey(req); const { apiKey } = await controller.createAPIKey(req);
expect(userRepository.update).toHaveBeenCalledWith(req.user.id, { apiKey }); expect(userService.update).toHaveBeenCalledWith(req.user.id, { apiKey });
}); });
}); });
@ -185,7 +186,7 @@ describe('MeController', () => {
describe('deleteAPIKey', () => { describe('deleteAPIKey', () => {
it('should delete the API key', async () => { it('should delete the API key', async () => {
await controller.deleteAPIKey(req); await controller.deleteAPIKey(req);
expect(userRepository.update).toHaveBeenCalledWith(req.user.id, { apiKey: null }); expect(userService.update).toHaveBeenCalledWith(req.user.id, { apiKey: null });
}); });
}); });
}); });

View file

@ -4,26 +4,29 @@ import type { ILogger } from 'n8n-workflow';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import type { IInternalHooksClass } from '@/Interfaces'; import type { IInternalHooksClass } from '@/Interfaces';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import type { SettingsRepository, UserRepository } from '@db/repositories'; import type { SettingsRepository } from '@db/repositories';
import type { Config } from '@/config'; import type { Config } from '@/config';
import { BadRequestError } from '@/ResponseHelper'; import { BadRequestError } from '@/ResponseHelper';
import type { OwnerRequest } from '@/requests'; import type { OwnerRequest } from '@/requests';
import { OwnerController } from '@/controllers'; import { OwnerController } from '@/controllers';
import { badPasswords } from '../shared/testData'; import { badPasswords } from '../shared/testData';
import { AUTH_COOKIE_NAME } from '@/constants'; import { AUTH_COOKIE_NAME } from '@/constants';
import { UserService } from '@/services/user.service';
import Container from 'typedi';
import { mockInstance } from '../../integration/shared/utils';
describe('OwnerController', () => { describe('OwnerController', () => {
const config = mock<Config>(); const config = mock<Config>();
const logger = mock<ILogger>(); const logger = mock<ILogger>();
const internalHooks = mock<IInternalHooksClass>(); const internalHooks = mock<IInternalHooksClass>();
const userRepository = mock<UserRepository>(); const userService = mockInstance(UserService);
Container.set(UserService, userService);
const settingsRepository = mock<SettingsRepository>(); const settingsRepository = mock<SettingsRepository>();
const controller = new OwnerController({ const controller = new OwnerController({
config, config,
logger, logger,
internalHooks, internalHooks,
repositories: { repositories: {
User: userRepository,
Settings: settingsRepository, Settings: settingsRepository,
}, },
}); });
@ -83,12 +86,12 @@ describe('OwnerController', () => {
}); });
const res = mock<Response>(); const res = mock<Response>();
config.getEnv.calledWith('userManagement.isInstanceOwnerSetUp').mockReturnValue(false); config.getEnv.calledWith('userManagement.isInstanceOwnerSetUp').mockReturnValue(false);
userRepository.save.calledWith(anyObject()).mockResolvedValue(user); userService.save.calledWith(anyObject()).mockResolvedValue(user);
jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token'); jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token');
await controller.setupOwner(req, res); await controller.setupOwner(req, res);
expect(userRepository.save).toHaveBeenCalledWith(user); expect(userService.save).toHaveBeenCalledWith(user);
const cookieOptions = captor<CookieOptions>(); const cookieOptions = captor<CookieOptions>();
expect(res.cookie).toHaveBeenCalledWith(AUTH_COOKIE_NAME, 'signed-token', cookieOptions); expect(res.cookie).toHaveBeenCalledWith(AUTH_COOKIE_NAME, 'signed-token', cookieOptions);

View file

@ -14,7 +14,7 @@ import type { User } from '@db/entities/User';
import type { WorkflowStatistics } from '@db/entities/WorkflowStatistics'; import type { WorkflowStatistics } from '@db/entities/WorkflowStatistics';
import { WorkflowStatisticsRepository } from '@db/repositories'; import { WorkflowStatisticsRepository } from '@db/repositories';
import { EventsService } from '@/services/events.service'; import { EventsService } from '@/services/events.service';
import { UserService } from '@/user/user.service'; import { UserService } from '@/services/user.service';
import { OwnershipService } from '@/services/ownership.service'; import { OwnershipService } from '@/services/ownership.service';
import { mockInstance } from '../../integration/shared/utils'; import { mockInstance } from '../../integration/shared/utils';
@ -24,6 +24,7 @@ describe('EventsService', () => {
const dbType = config.getEnv('database.type'); const dbType = config.getEnv('database.type');
const fakeUser = mock<User>({ id: 'abcde-fghij' }); const fakeUser = mock<User>({ id: 'abcde-fghij' });
const ownershipService = mockInstance(OwnershipService); const ownershipService = mockInstance(OwnershipService);
const userService = mockInstance(UserService);
const entityManager = mock<EntityManager>(); const entityManager = mock<EntityManager>();
const dataSource = mock<DataSource>({ const dataSource = mock<DataSource>({
@ -39,7 +40,7 @@ describe('EventsService', () => {
config.set('diagnostics.enabled', true); config.set('diagnostics.enabled', true);
config.set('deployment.type', 'n8n-testing'); config.set('deployment.type', 'n8n-testing');
mocked(ownershipService.getWorkflowOwnerCached).mockResolvedValue(fakeUser); mocked(ownershipService.getWorkflowOwnerCached).mockResolvedValue(fakeUser);
const updateUserSettingsMock = jest.spyOn(UserService, 'updateUserSettings').mockImplementation(); const updateSettingsMock = jest.spyOn(userService, 'updateSettings').mockImplementation();
const eventsService = new EventsService( const eventsService = new EventsService(
new WorkflowStatisticsRepository(dataSource), new WorkflowStatisticsRepository(dataSource),
@ -88,7 +89,7 @@ describe('EventsService', () => {
mockDBCall(); mockDBCall();
await eventsService.workflowExecutionCompleted(workflow, runData); await eventsService.workflowExecutionCompleted(workflow, runData);
expect(updateUserSettingsMock).toHaveBeenCalledTimes(1); expect(updateSettingsMock).toHaveBeenCalledTimes(1);
expect(onFirstProductionWorkflowSuccess).toBeCalledTimes(1); expect(onFirstProductionWorkflowSuccess).toBeCalledTimes(1);
expect(onFirstProductionWorkflowSuccess).toHaveBeenNthCalledWith(1, { expect(onFirstProductionWorkflowSuccess).toHaveBeenNthCalledWith(1, {
user_id: fakeUser.id, user_id: fakeUser.id,

View file

@ -1,5 +1,5 @@
import { OwnershipService } from '@/services/ownership.service'; import { OwnershipService } from '@/services/ownership.service';
import { SharedWorkflowRepository, UserRepository } from '@/databases/repositories'; import { SharedWorkflowRepository } from '@/databases/repositories';
import { mockInstance } from '../../integration/shared/utils'; import { mockInstance } from '../../integration/shared/utils';
import { Role } from '@/databases/entities/Role'; import { Role } from '@/databases/entities/Role';
import { randomInteger } from '../../integration/shared/random'; import { randomInteger } from '../../integration/shared/random';
@ -7,6 +7,7 @@ import { SharedWorkflow } from '@/databases/entities/SharedWorkflow';
import { CacheService } from '@/services/cache.service'; import { CacheService } from '@/services/cache.service';
import { User } from '@/databases/entities/User'; import { User } from '@/databases/entities/User';
import { RoleService } from '@/services/role.service'; import { RoleService } from '@/services/role.service';
import { UserService } from '@/services/user.service';
const wfOwnerRole = () => const wfOwnerRole = () =>
Object.assign(new Role(), { Object.assign(new Role(), {
@ -18,12 +19,12 @@ const wfOwnerRole = () =>
describe('OwnershipService', () => { describe('OwnershipService', () => {
const cacheService = mockInstance(CacheService); const cacheService = mockInstance(CacheService);
const roleService = mockInstance(RoleService); const roleService = mockInstance(RoleService);
const userRepository = mockInstance(UserRepository); const userService = mockInstance(UserService);
const sharedWorkflowRepository = mockInstance(SharedWorkflowRepository); const sharedWorkflowRepository = mockInstance(SharedWorkflowRepository);
const ownershipService = new OwnershipService( const ownershipService = new OwnershipService(
cacheService, cacheService,
userRepository, userService,
roleService, roleService,
sharedWorkflowRepository, sharedWorkflowRepository,
); );