import { RoleChangeRequestDto, SettingsUpdateRequestDto } from '@n8n/api-types'; import { Response } from 'express'; import { Logger } from 'n8n-core'; import { AuthService } from '@/auth/auth.service'; import { CredentialsService } from '@/credentials/credentials.service'; import { AuthIdentity } from '@/databases/entities/auth-identity'; import { Project } from '@/databases/entities/project'; import { User } from '@/databases/entities/user'; import { ProjectRepository } from '@/databases/repositories/project.repository'; import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { UserRepository } from '@/databases/repositories/user.repository'; import { GlobalScope, Delete, Get, RestController, Patch, Licensed, Body, Param, } from '@/decorators'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { EventService } from '@/events/event.service'; import { ExternalHooks } from '@/external-hooks'; import type { PublicUser } from '@/interfaces'; import { listQueryMiddleware } from '@/middlewares'; import { AuthenticatedRequest, ListQuery, UserRequest } from '@/requests'; import { FolderService } from '@/services/folder.service'; import { ProjectService } from '@/services/project.service.ee'; import { UserService } from '@/services/user.service'; import { WorkflowService } from '@/workflows/workflow.service'; @RestController('/users') export class UsersController { constructor( private readonly logger: Logger, private readonly externalHooks: ExternalHooks, private readonly sharedCredentialsRepository: SharedCredentialsRepository, private readonly sharedWorkflowRepository: SharedWorkflowRepository, private readonly userRepository: UserRepository, private readonly authService: AuthService, private readonly userService: UserService, private readonly projectRepository: ProjectRepository, private readonly workflowService: WorkflowService, private readonly credentialsService: CredentialsService, private readonly projectService: ProjectService, private readonly eventService: EventService, private readonly folderService: FolderService, ) {} static ERROR_MESSAGES = { CHANGE_ROLE: { NO_USER: 'Target user not found', NO_ADMIN_ON_OWNER: 'Admin cannot change role on global owner', NO_OWNER_ON_OWNER: 'Owner cannot change role on global owner', }, } as const; private removeSupplementaryFields( publicUsers: Array>, listQueryOptions: ListQuery.Options, ) { const { take, select, filter } = listQueryOptions; // remove fields added to satisfy query if (take && select && !select?.id) { for (const user of publicUsers) delete user.id; } if (filter?.isOwner) { for (const user of publicUsers) delete user.role; } // remove computed fields (unselectable) if (select) { for (const user of publicUsers) { delete user.isOwner; delete user.isPending; delete user.signInType; } } return publicUsers; } @Get('/', { middlewares: listQueryMiddleware }) @GlobalScope('user:list') async listUsers(req: ListQuery.Request) { const { listQueryOptions } = req; const findManyOptions = await this.userRepository.toFindManyOptions(listQueryOptions); const users = await this.userRepository.find(findManyOptions); const publicUsers: Array> = await Promise.all( users.map( async (u) => await this.userService.toPublic(u, { withInviteUrl: true, inviterId: req.user.id }), ), ); return listQueryOptions ? this.removeSupplementaryFields(publicUsers, listQueryOptions) : publicUsers; } @Get('/:id/password-reset-link') @GlobalScope('user:resetPassword') async getUserPasswordResetLink(req: UserRequest.PasswordResetLink) { const user = await this.userRepository.findOneOrFail({ where: { id: req.params.id }, }); if (!user) { throw new NotFoundError('User not found'); } if (req.user.role === 'global:admin' && user.role === 'global:owner') { throw new ForbiddenError('Admin cannot reset password of global owner'); } const link = this.authService.generatePasswordResetUrl(user); return { link }; } @Patch('/:id/settings') @GlobalScope('user:update') async updateUserSettings( _req: AuthenticatedRequest, _res: Response, @Body payload: SettingsUpdateRequestDto, @Param('id') id: string, ) { await this.userService.updateSettings(id, payload); const user = await this.userRepository.findOneOrFail({ select: ['settings'], where: { id }, }); return user.settings; } /** * Delete a user. Optionally, designate a transferee for their workflows and credentials. */ @Delete('/:id') @GlobalScope('user:delete') async deleteUser(req: UserRequest.Delete) { const { id: idToDelete } = req.params; if (req.user.id === idToDelete) { this.logger.debug( 'Request to delete a user failed because it attempted to delete the requesting user', { userId: req.user.id }, ); throw new BadRequestError('Cannot delete your own user'); } const { transferId } = req.query; const userToDelete = await this.userRepository.findOneBy({ id: idToDelete }); if (!userToDelete) { throw new NotFoundError( 'Request to delete a user failed because the user to delete was not found in DB', ); } if (userToDelete.role === 'global:owner') { throw new ForbiddenError('Instance owner cannot be deleted.'); } const personalProjectToDelete = await this.projectRepository.getPersonalProjectForUserOrFail( userToDelete.id, ); if (transferId === personalProjectToDelete.id) { throw new BadRequestError( 'Request to delete a user failed because the user to delete and the transferee are the same user', ); } let transfereeId; if (transferId) { const transfereeProject = await this.projectRepository.findOneBy({ id: transferId }); if (!transfereeProject) { throw new NotFoundError( 'Request to delete a user failed because the transferee project was not found in DB', ); } const transferee = await this.userRepository.findOneByOrFail({ projectRelations: { projectId: transfereeProject.id, }, }); transfereeId = transferee.id; await this.userService.getManager().transaction(async (trx) => { await this.workflowService.transferAll( personalProjectToDelete.id, transfereeProject.id, trx, ); await this.credentialsService.transferAll( personalProjectToDelete.id, transfereeProject.id, trx, ); await this.folderService.transferAllFoldersToProject( personalProjectToDelete.id, transfereeProject.id, trx, ); }); await this.projectService.clearCredentialCanUseExternalSecretsCache(transfereeProject.id); } const [ownedSharedWorkflows, ownedSharedCredentials] = await Promise.all([ this.sharedWorkflowRepository.find({ select: { workflowId: true }, where: { projectId: personalProjectToDelete.id, role: 'workflow:owner' }, }), this.sharedCredentialsRepository.find({ relations: { credentials: true }, where: { projectId: personalProjectToDelete.id, role: 'credential:owner' }, }), ]); const ownedCredentials = ownedSharedCredentials.map(({ credentials }) => credentials); for (const { workflowId } of ownedSharedWorkflows) { await this.workflowService.delete(userToDelete, workflowId); } for (const credential of ownedCredentials) { await this.credentialsService.delete(userToDelete, credential.id); } await this.userService.getManager().transaction(async (trx) => { await trx.delete(AuthIdentity, { userId: userToDelete.id }); await trx.delete(Project, { id: personalProjectToDelete.id }); await trx.delete(User, { id: userToDelete.id }); }); this.eventService.emit('user-deleted', { user: req.user, publicApi: false, targetUserOldStatus: userToDelete.isPending ? 'invited' : 'active', targetUserId: idToDelete, migrationStrategy: transferId ? 'transfer_data' : 'delete_data', migrationUserId: transfereeId, }); await this.externalHooks.run('user.deleted', [await this.userService.toPublic(userToDelete)]); return { success: true }; } @Patch('/:id/role') @GlobalScope('user:changeRole') @Licensed('feat:advancedPermissions') async changeGlobalRole( req: AuthenticatedRequest, _: Response, @Body payload: RoleChangeRequestDto, @Param('id') id: string, ) { const { NO_ADMIN_ON_OWNER, NO_USER, NO_OWNER_ON_OWNER } = UsersController.ERROR_MESSAGES.CHANGE_ROLE; const targetUser = await this.userRepository.findOneBy({ id }); if (targetUser === null) { throw new NotFoundError(NO_USER); } if (req.user.role === 'global:admin' && targetUser.role === 'global:owner') { throw new ForbiddenError(NO_ADMIN_ON_OWNER); } if (req.user.role === 'global:owner' && targetUser.role === 'global:owner') { throw new ForbiddenError(NO_OWNER_ON_OWNER); } await this.userService.update(targetUser.id, { role: payload.newRoleName }); this.eventService.emit('user-changed-role', { userId: req.user.id, targetUserId: targetUser.id, targetUserNewRole: payload.newRoleName, publicApi: false, }); const projects = await this.projectService.getUserOwnedOrAdminProjects(targetUser.id); await Promise.all( projects.map( async (p) => await this.projectService.clearCredentialCanUseExternalSecretsCache(p.id), ), ); return { success: true }; } }