2024-09-20 12:14:06 -07:00
|
|
|
import { RoleChangeRequestDto, SettingsUpdateRequestDto } from '@n8n/api-types';
|
|
|
|
import { Response } from 'express';
|
2024-02-28 04:12:28 -08:00
|
|
|
|
|
|
|
import { AuthService } from '@/auth/auth.service';
|
2024-09-12 09:07:18 -07:00
|
|
|
import { CredentialsService } from '@/credentials/credentials.service';
|
|
|
|
import { AuthIdentity } from '@/databases/entities/auth-identity';
|
|
|
|
import { Project } from '@/databases/entities/project';
|
2024-08-28 08:57:46 -07:00
|
|
|
import { User } from '@/databases/entities/user';
|
2024-09-12 09:07:18 -07:00
|
|
|
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';
|
2024-09-20 12:14:06 -07:00
|
|
|
import { GlobalScope, Delete, Get, RestController, Patch, Licensed, Body } from '@/decorators';
|
|
|
|
import { Param } from '@/decorators/args';
|
2024-09-12 09:07:18 -07:00
|
|
|
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 { Logger } from '@/logger';
|
|
|
|
import { listQueryMiddleware } from '@/middlewares';
|
2024-09-20 12:14:06 -07:00
|
|
|
import { AuthenticatedRequest, ListQuery, UserRequest } from '@/requests';
|
2024-09-12 09:07:18 -07:00
|
|
|
import { ProjectService } from '@/services/project.service';
|
2023-08-22 06:58:05 -07:00
|
|
|
import { UserService } from '@/services/user.service';
|
2024-05-17 01:53:15 -07:00
|
|
|
import { WorkflowService } from '@/workflows/workflow.service';
|
2023-01-27 02:19:47 -08:00
|
|
|
|
|
|
|
@RestController('/users')
|
|
|
|
export class UsersController {
|
2023-08-25 04:23:22 -07:00
|
|
|
constructor(
|
2023-10-25 07:35:22 -07:00
|
|
|
private readonly logger: Logger,
|
2023-12-27 02:50:43 -08:00
|
|
|
private readonly externalHooks: ExternalHooks,
|
2023-08-25 04:23:22 -07:00
|
|
|
private readonly sharedCredentialsRepository: SharedCredentialsRepository,
|
|
|
|
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
|
2024-01-03 00:33:35 -08:00
|
|
|
private readonly userRepository: UserRepository,
|
2024-02-28 04:12:28 -08:00
|
|
|
private readonly authService: AuthService,
|
2023-08-25 04:23:22 -07:00
|
|
|
private readonly userService: UserService,
|
2024-05-17 01:53:15 -07:00
|
|
|
private readonly projectRepository: ProjectRepository,
|
|
|
|
private readonly workflowService: WorkflowService,
|
|
|
|
private readonly credentialsService: CredentialsService,
|
|
|
|
private readonly projectService: ProjectService,
|
2024-07-19 03:55:38 -07:00
|
|
|
private readonly eventService: EventService,
|
2023-08-25 04:23:22 -07:00
|
|
|
) {}
|
2023-01-27 02:19:47 -08:00
|
|
|
|
2023-11-24 02:40:08 -08:00
|
|
|
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(
|
2023-08-28 07:13:17 -07:00
|
|
|
publicUsers: Array<Partial<PublicUser>>,
|
|
|
|
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) {
|
2024-01-24 04:38:57 -08:00
|
|
|
for (const user of publicUsers) delete user.role;
|
2023-08-28 07:13:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// remove computed fields (unselectable)
|
|
|
|
|
|
|
|
if (select) {
|
|
|
|
for (const user of publicUsers) {
|
|
|
|
delete user.isOwner;
|
|
|
|
delete user.isPending;
|
|
|
|
delete user.signInType;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return publicUsers;
|
2023-01-27 02:19:47 -08:00
|
|
|
}
|
|
|
|
|
2023-08-28 07:13:17 -07:00
|
|
|
@Get('/', { middlewares: listQueryMiddleware })
|
2024-02-28 05:40:02 -08:00
|
|
|
@GlobalScope('user:list')
|
2023-08-28 07:13:17 -07:00
|
|
|
async listUsers(req: ListQuery.Request) {
|
|
|
|
const { listQueryOptions } = req;
|
|
|
|
|
2024-01-24 04:38:57 -08:00
|
|
|
const findManyOptions = await this.userRepository.toFindManyOptions(listQueryOptions);
|
2023-08-28 07:13:17 -07:00
|
|
|
|
2024-01-02 08:53:24 -08:00
|
|
|
const users = await this.userRepository.find(findManyOptions);
|
2023-08-28 07:13:17 -07:00
|
|
|
|
|
|
|
const publicUsers: Array<Partial<PublicUser>> = await Promise.all(
|
2024-01-17 07:08:50 -08:00
|
|
|
users.map(
|
|
|
|
async (u) =>
|
|
|
|
await this.userService.toPublic(u, { withInviteUrl: true, inviterId: req.user.id }),
|
2023-12-07 01:53:31 -08:00
|
|
|
),
|
2023-01-27 02:19:47 -08:00
|
|
|
);
|
2023-08-28 07:13:17 -07:00
|
|
|
|
|
|
|
return listQueryOptions
|
|
|
|
? this.removeSupplementaryFields(publicUsers, listQueryOptions)
|
|
|
|
: publicUsers;
|
2023-01-27 02:19:47 -08:00
|
|
|
}
|
|
|
|
|
2023-05-30 03:52:02 -07:00
|
|
|
@Get('/:id/password-reset-link')
|
2024-02-28 05:40:02 -08:00
|
|
|
@GlobalScope('user:resetPassword')
|
2023-05-30 03:52:02 -07:00
|
|
|
async getUserPasswordResetLink(req: UserRequest.PasswordResetLink) {
|
2024-01-02 08:53:24 -08:00
|
|
|
const user = await this.userRepository.findOneOrFail({
|
2023-05-30 03:52:02 -07:00
|
|
|
where: { id: req.params.id },
|
|
|
|
});
|
|
|
|
if (!user) {
|
|
|
|
throw new NotFoundError('User not found');
|
|
|
|
}
|
2023-07-24 14:40:17 -07:00
|
|
|
|
2024-05-22 07:13:56 -07:00
|
|
|
if (req.user.role === 'global:admin' && user.role === 'global:owner') {
|
|
|
|
throw new ForbiddenError('Admin cannot reset password of global owner');
|
|
|
|
}
|
|
|
|
|
2024-02-28 04:12:28 -08:00
|
|
|
const link = this.authService.generatePasswordResetUrl(user);
|
2023-11-07 06:35:43 -08:00
|
|
|
return { link };
|
2023-05-30 03:52:02 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
@Patch('/:id/settings')
|
2024-02-28 05:40:02 -08:00
|
|
|
@GlobalScope('user:update')
|
2024-09-20 12:14:06 -07:00
|
|
|
async updateUserSettings(
|
|
|
|
_req: AuthenticatedRequest,
|
|
|
|
_res: Response,
|
|
|
|
@Body payload: SettingsUpdateRequestDto,
|
|
|
|
@Param('id') id: string,
|
|
|
|
) {
|
2023-08-22 06:58:05 -07:00
|
|
|
await this.userService.updateSettings(id, payload);
|
2023-05-30 03:52:02 -07:00
|
|
|
|
2024-01-02 08:53:24 -08:00
|
|
|
const user = await this.userRepository.findOneOrFail({
|
2023-05-30 03:52:02 -07:00
|
|
|
select: ['settings'],
|
|
|
|
where: { id },
|
|
|
|
});
|
|
|
|
|
|
|
|
return user.settings;
|
|
|
|
}
|
|
|
|
|
2023-01-27 02:19:47 -08:00
|
|
|
/**
|
|
|
|
* Delete a user. Optionally, designate a transferee for their workflows and credentials.
|
|
|
|
*/
|
|
|
|
@Delete('/:id')
|
2024-02-28 05:40:02 -08:00
|
|
|
@GlobalScope('user:delete')
|
2023-01-27 02:19:47 -08:00
|
|
|
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;
|
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
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',
|
2023-01-27 02:19:47 -08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-05-22 07:23:40 -07:00
|
|
|
if (userToDelete.role === 'global:owner') {
|
|
|
|
throw new ForbiddenError('Instance owner cannot be deleted.');
|
|
|
|
}
|
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
const personalProjectToDelete = await this.projectRepository.getPersonalProjectForUserOrFail(
|
|
|
|
userToDelete.id,
|
|
|
|
);
|
2023-01-27 02:19:47 -08:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
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',
|
2023-01-27 02:19:47 -08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-08-05 03:07:42 -07:00
|
|
|
let transfereeId;
|
2023-01-27 02:19:47 -08:00
|
|
|
|
|
|
|
if (transferId) {
|
2024-05-17 01:53:15 -07:00
|
|
|
const transfereePersonalProject = await this.projectRepository.findOneBy({ id: transferId });
|
2023-01-27 02:19:47 -08:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
if (!transfereePersonalProject) {
|
|
|
|
throw new NotFoundError(
|
|
|
|
'Request to delete a user failed because the transferee project was not found in DB',
|
2024-01-05 04:06:24 -08:00
|
|
|
);
|
2024-05-17 01:53:15 -07:00
|
|
|
}
|
2023-01-27 02:19:47 -08:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
const transferee = await this.userRepository.findOneByOrFail({
|
|
|
|
projectRelations: {
|
|
|
|
projectId: transfereePersonalProject.id,
|
|
|
|
role: 'project:personalOwner',
|
|
|
|
},
|
|
|
|
});
|
2023-01-27 02:19:47 -08:00
|
|
|
|
2024-08-05 03:07:42 -07:00
|
|
|
transfereeId = transferee.id;
|
2023-01-27 02:19:47 -08:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
await this.userService.getManager().transaction(async (trx) => {
|
|
|
|
await this.workflowService.transferAll(
|
|
|
|
personalProjectToDelete.id,
|
|
|
|
transfereePersonalProject.id,
|
|
|
|
trx,
|
|
|
|
);
|
|
|
|
await this.credentialsService.transferAll(
|
|
|
|
personalProjectToDelete.id,
|
|
|
|
transfereePersonalProject.id,
|
|
|
|
trx,
|
2023-01-27 02:19:47 -08:00
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
await this.projectService.clearCredentialCanUseExternalSecretsCache(
|
|
|
|
transfereePersonalProject.id,
|
|
|
|
);
|
2023-01-27 02:19:47 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
const [ownedSharedWorkflows, ownedSharedCredentials] = await Promise.all([
|
|
|
|
this.sharedWorkflowRepository.find({
|
2024-05-17 01:53:15 -07:00
|
|
|
select: { workflowId: true },
|
|
|
|
where: { projectId: personalProjectToDelete.id, role: 'workflow:owner' },
|
2023-01-27 02:19:47 -08:00
|
|
|
}),
|
|
|
|
this.sharedCredentialsRepository.find({
|
2024-05-17 01:53:15 -07:00
|
|
|
relations: { credentials: true },
|
|
|
|
where: { projectId: personalProjectToDelete.id, role: 'credential:owner' },
|
2023-01-27 02:19:47 -08:00
|
|
|
}),
|
|
|
|
]);
|
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
const ownedCredentials = ownedSharedCredentials.map(({ credentials }) => credentials);
|
2023-01-27 02:19:47 -08:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
for (const { workflowId } of ownedSharedWorkflows) {
|
|
|
|
await this.workflowService.delete(userToDelete, workflowId);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const credential of ownedCredentials) {
|
|
|
|
await this.credentialsService.delete(credential);
|
|
|
|
}
|
|
|
|
|
|
|
|
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 });
|
2023-01-27 02:19:47 -08:00
|
|
|
});
|
|
|
|
|
2024-08-05 03:07:42 -07:00
|
|
|
this.eventService.emit('user-deleted', {
|
2023-01-27 02:19:47 -08:00
|
|
|
user: req.user,
|
|
|
|
publicApi: false,
|
2024-08-05 03:07:42 -07:00
|
|
|
targetUserOldStatus: userToDelete.isPending ? 'invited' : 'active',
|
|
|
|
targetUserId: idToDelete,
|
|
|
|
migrationStrategy: transferId ? 'transfer_data' : 'delete_data',
|
|
|
|
migrationUserId: transfereeId,
|
2023-01-27 02:19:47 -08:00
|
|
|
});
|
|
|
|
|
2023-08-28 07:13:17 -07:00
|
|
|
await this.externalHooks.run('user.deleted', [await this.userService.toPublic(userToDelete)]);
|
2024-05-17 01:53:15 -07:00
|
|
|
|
2023-01-27 02:19:47 -08:00
|
|
|
return { success: true };
|
|
|
|
}
|
2023-11-24 02:40:08 -08:00
|
|
|
|
|
|
|
@Patch('/:id/role')
|
2024-02-28 05:40:02 -08:00
|
|
|
@GlobalScope('user:changeRole')
|
2024-01-03 00:33:35 -08:00
|
|
|
@Licensed('feat:advancedPermissions')
|
2024-09-20 12:14:06 -07:00
|
|
|
async changeGlobalRole(
|
|
|
|
req: AuthenticatedRequest,
|
|
|
|
_: Response,
|
|
|
|
@Body payload: RoleChangeRequestDto,
|
|
|
|
@Param('id') id: string,
|
|
|
|
) {
|
2024-01-03 00:33:35 -08:00
|
|
|
const { NO_ADMIN_ON_OWNER, NO_USER, NO_OWNER_ON_OWNER } =
|
|
|
|
UsersController.ERROR_MESSAGES.CHANGE_ROLE;
|
2023-11-24 02:40:08 -08:00
|
|
|
|
2024-09-20 12:14:06 -07:00
|
|
|
const targetUser = await this.userRepository.findOneBy({ id });
|
2023-11-24 02:40:08 -08:00
|
|
|
if (targetUser === null) {
|
|
|
|
throw new NotFoundError(NO_USER);
|
|
|
|
}
|
|
|
|
|
2024-01-24 04:38:57 -08:00
|
|
|
if (req.user.role === 'global:admin' && targetUser.role === 'global:owner') {
|
2024-05-17 01:53:15 -07:00
|
|
|
throw new ForbiddenError(NO_ADMIN_ON_OWNER);
|
2023-11-24 02:40:08 -08:00
|
|
|
}
|
|
|
|
|
2024-01-24 04:38:57 -08:00
|
|
|
if (req.user.role === 'global:owner' && targetUser.role === 'global:owner') {
|
2024-05-17 01:53:15 -07:00
|
|
|
throw new ForbiddenError(NO_OWNER_ON_OWNER);
|
2023-11-24 02:40:08 -08:00
|
|
|
}
|
|
|
|
|
2024-01-24 04:38:57 -08:00
|
|
|
await this.userService.update(targetUser.id, { role: payload.newRoleName });
|
2023-11-24 02:40:08 -08:00
|
|
|
|
2024-08-05 03:07:42 -07:00
|
|
|
this.eventService.emit('user-changed-role', {
|
|
|
|
userId: req.user.id,
|
|
|
|
targetUserId: targetUser.id,
|
2024-08-05 04:24:26 -07:00
|
|
|
targetUserNewRole: payload.newRoleName,
|
2024-08-05 03:07:42 -07:00
|
|
|
publicApi: false,
|
2023-12-13 03:22:11 -08:00
|
|
|
});
|
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
const projects = await this.projectService.getUserOwnedOrAdminProjects(targetUser.id);
|
|
|
|
await Promise.all(
|
|
|
|
projects.map(
|
|
|
|
async (p) => await this.projectService.clearCredentialCanUseExternalSecretsCache(p.id),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
2023-11-24 02:40:08 -08:00
|
|
|
return { success: true };
|
|
|
|
}
|
2023-01-27 02:19:47 -08:00
|
|
|
}
|