2023-01-27 02:19:47 -08:00
|
|
|
import validator from 'validator';
|
2023-01-27 05:56:56 -08:00
|
|
|
import { In } from 'typeorm';
|
|
|
|
import type { ILogger } from 'n8n-workflow';
|
|
|
|
import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
2023-01-27 02:19:47 -08:00
|
|
|
import { User } from '@db/entities/User';
|
|
|
|
import { SharedCredentials } from '@db/entities/SharedCredentials';
|
|
|
|
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
|
2023-05-30 03:52:02 -07:00
|
|
|
import { Authorized, NoAuthRequired, Delete, Get, Post, RestController, Patch } from '@/decorators';
|
2023-01-27 02:19:47 -08:00
|
|
|
import {
|
|
|
|
addInviteLinkToUser,
|
|
|
|
generateUserInviteUrl,
|
|
|
|
getInstanceBaseUrl,
|
|
|
|
hashPassword,
|
|
|
|
isEmailSetUp,
|
|
|
|
sanitizeUser,
|
|
|
|
validatePassword,
|
2023-02-21 00:35:35 -08:00
|
|
|
withFeatureFlags,
|
2023-01-27 02:19:47 -08:00
|
|
|
} from '@/UserManagement/UserManagementHelper';
|
|
|
|
import { issueCookie } from '@/auth/jwt';
|
2023-07-12 05:11:46 -07:00
|
|
|
import {
|
|
|
|
BadRequestError,
|
|
|
|
InternalServerError,
|
|
|
|
NotFoundError,
|
|
|
|
UnauthorizedError,
|
|
|
|
} from '@/ResponseHelper';
|
2023-01-27 05:56:56 -08:00
|
|
|
import { Response } from 'express';
|
2023-01-27 02:19:47 -08:00
|
|
|
import type { Config } from '@/config';
|
2023-05-30 03:52:02 -07:00
|
|
|
import { UserRequest, UserSettingsUpdatePayload } from '@/requests';
|
2023-01-27 02:19:47 -08:00
|
|
|
import type { UserManagementMailer } from '@/UserManagement/email';
|
|
|
|
import type {
|
|
|
|
PublicUser,
|
|
|
|
IDatabaseCollections,
|
|
|
|
IExternalHooksClass,
|
|
|
|
IInternalHooksClass,
|
|
|
|
ITelemetryUserDeletionData,
|
|
|
|
} from '@/Interfaces';
|
|
|
|
import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
|
|
|
import { AuthIdentity } from '@db/entities/AuthIdentity';
|
2023-02-21 00:35:35 -08:00
|
|
|
import type { PostHogClient } from '@/posthog';
|
2023-03-23 07:12:19 -07:00
|
|
|
import { isSamlLicensedAndEnabled } from '../sso/saml/samlHelpers';
|
2023-04-12 01:59:14 -07:00
|
|
|
import type {
|
|
|
|
RoleRepository,
|
|
|
|
SharedCredentialsRepository,
|
|
|
|
SharedWorkflowRepository,
|
|
|
|
UserRepository,
|
|
|
|
} from '@db/repositories';
|
2023-07-12 05:11:46 -07:00
|
|
|
import { UserService } from '@/user/user.service';
|
2023-05-30 03:52:02 -07:00
|
|
|
import { plainToInstance } from 'class-transformer';
|
2023-07-12 05:11:46 -07:00
|
|
|
import { License } from '@/License';
|
|
|
|
import { Container } from 'typedi';
|
|
|
|
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
|
2023-01-27 02:19:47 -08:00
|
|
|
|
2023-04-24 02:45:31 -07:00
|
|
|
@Authorized(['global', 'owner'])
|
2023-01-27 02:19:47 -08:00
|
|
|
@RestController('/users')
|
|
|
|
export class UsersController {
|
|
|
|
private config: Config;
|
|
|
|
|
|
|
|
private logger: ILogger;
|
|
|
|
|
|
|
|
private externalHooks: IExternalHooksClass;
|
|
|
|
|
|
|
|
private internalHooks: IInternalHooksClass;
|
|
|
|
|
2023-04-12 01:59:14 -07:00
|
|
|
private userRepository: UserRepository;
|
2023-01-27 02:19:47 -08:00
|
|
|
|
2023-04-12 01:59:14 -07:00
|
|
|
private roleRepository: RoleRepository;
|
2023-01-27 02:19:47 -08:00
|
|
|
|
2023-04-12 01:59:14 -07:00
|
|
|
private sharedCredentialsRepository: SharedCredentialsRepository;
|
2023-01-27 02:19:47 -08:00
|
|
|
|
2023-04-12 01:59:14 -07:00
|
|
|
private sharedWorkflowRepository: SharedWorkflowRepository;
|
2023-01-27 02:19:47 -08:00
|
|
|
|
|
|
|
private activeWorkflowRunner: ActiveWorkflowRunner;
|
|
|
|
|
|
|
|
private mailer: UserManagementMailer;
|
|
|
|
|
2023-02-21 00:35:35 -08:00
|
|
|
private postHog?: PostHogClient;
|
|
|
|
|
2023-01-27 02:19:47 -08:00
|
|
|
constructor({
|
|
|
|
config,
|
|
|
|
logger,
|
|
|
|
externalHooks,
|
|
|
|
internalHooks,
|
|
|
|
repositories,
|
|
|
|
activeWorkflowRunner,
|
|
|
|
mailer,
|
2023-02-21 00:35:35 -08:00
|
|
|
postHog,
|
2023-01-27 02:19:47 -08:00
|
|
|
}: {
|
|
|
|
config: Config;
|
|
|
|
logger: ILogger;
|
|
|
|
externalHooks: IExternalHooksClass;
|
|
|
|
internalHooks: IInternalHooksClass;
|
|
|
|
repositories: Pick<
|
|
|
|
IDatabaseCollections,
|
|
|
|
'User' | 'Role' | 'SharedCredentials' | 'SharedWorkflow'
|
|
|
|
>;
|
|
|
|
activeWorkflowRunner: ActiveWorkflowRunner;
|
|
|
|
mailer: UserManagementMailer;
|
2023-02-21 00:35:35 -08:00
|
|
|
postHog?: PostHogClient;
|
2023-01-27 02:19:47 -08:00
|
|
|
}) {
|
|
|
|
this.config = config;
|
|
|
|
this.logger = logger;
|
|
|
|
this.externalHooks = externalHooks;
|
|
|
|
this.internalHooks = internalHooks;
|
|
|
|
this.userRepository = repositories.User;
|
|
|
|
this.roleRepository = repositories.Role;
|
|
|
|
this.sharedCredentialsRepository = repositories.SharedCredentials;
|
|
|
|
this.sharedWorkflowRepository = repositories.SharedWorkflow;
|
|
|
|
this.activeWorkflowRunner = activeWorkflowRunner;
|
|
|
|
this.mailer = mailer;
|
2023-02-21 00:35:35 -08:00
|
|
|
this.postHog = postHog;
|
2023-01-27 02:19:47 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Send email invite(s) to one or multiple users and create user shell(s).
|
|
|
|
*/
|
2023-06-07 07:53:53 -07:00
|
|
|
@Post('/')
|
2023-01-27 02:19:47 -08:00
|
|
|
async sendEmailInvites(req: UserRequest.Invite) {
|
2023-07-12 05:11:46 -07:00
|
|
|
const isWithinUsersLimit = Container.get(License).isWithinUsersLimit();
|
|
|
|
|
2023-03-23 07:12:19 -07:00
|
|
|
if (isSamlLicensedAndEnabled()) {
|
2023-01-27 02:19:47 -08:00
|
|
|
this.logger.debug(
|
2023-03-23 07:12:19 -07:00
|
|
|
'SAML is enabled, so users are managed by the Identity Provider and cannot be added through invites',
|
|
|
|
);
|
|
|
|
throw new BadRequestError(
|
|
|
|
'SAML is enabled, so users are managed by the Identity Provider and cannot be added through invites',
|
2023-01-27 02:19:47 -08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-07-12 05:11:46 -07:00
|
|
|
if (!isWithinUsersLimit) {
|
|
|
|
this.logger.debug(
|
|
|
|
'Request to send email invite(s) to user(s) failed because the user limit quota has been reached',
|
|
|
|
);
|
|
|
|
throw new UnauthorizedError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
|
|
|
|
}
|
|
|
|
|
2023-01-27 02:19:47 -08:00
|
|
|
if (!this.config.getEnv('userManagement.isInstanceOwnerSetUp')) {
|
|
|
|
this.logger.debug(
|
|
|
|
'Request to send email invite(s) to user(s) failed because the owner account is not set up',
|
|
|
|
);
|
|
|
|
throw new BadRequestError('You must set up your own account before inviting others');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!Array.isArray(req.body)) {
|
|
|
|
this.logger.debug(
|
|
|
|
'Request to send email invite(s) to user(s) failed because the payload is not an array',
|
|
|
|
{
|
|
|
|
payload: req.body,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
throw new BadRequestError('Invalid payload');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!req.body.length) return [];
|
|
|
|
|
|
|
|
const createUsers: { [key: string]: string | null } = {};
|
|
|
|
// Validate payload
|
|
|
|
req.body.forEach((invite) => {
|
|
|
|
if (typeof invite !== 'object' || !invite.email) {
|
|
|
|
throw new BadRequestError(
|
|
|
|
'Request to send email invite(s) to user(s) failed because the payload is not an array shaped Array<{ email: string }>',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!validator.isEmail(invite.email)) {
|
|
|
|
this.logger.debug('Invalid email in payload', { invalidEmail: invite.email });
|
|
|
|
throw new BadRequestError(
|
|
|
|
`Request to send email invite(s) to user(s) failed because of an invalid email address: ${invite.email}`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
createUsers[invite.email.toLowerCase()] = null;
|
|
|
|
});
|
|
|
|
|
2023-04-12 01:59:14 -07:00
|
|
|
const role = await this.roleRepository.findGlobalMemberRole();
|
2023-01-27 02:19:47 -08:00
|
|
|
|
|
|
|
if (!role) {
|
|
|
|
this.logger.error(
|
|
|
|
'Request to send email invite(s) to user(s) failed because no global member role was found in database',
|
|
|
|
);
|
|
|
|
throw new InternalServerError('Members role not found in database - inconsistent state');
|
|
|
|
}
|
|
|
|
|
|
|
|
// remove/exclude existing users from creation
|
|
|
|
const existingUsers = await this.userRepository.find({
|
|
|
|
where: { email: In(Object.keys(createUsers)) },
|
|
|
|
});
|
|
|
|
existingUsers.forEach((user) => {
|
|
|
|
if (user.password) {
|
|
|
|
delete createUsers[user.email];
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
createUsers[user.email] = user.id;
|
|
|
|
});
|
|
|
|
|
|
|
|
const usersToSetUp = Object.keys(createUsers).filter((email) => createUsers[email] === null);
|
|
|
|
const total = usersToSetUp.length;
|
|
|
|
|
|
|
|
this.logger.debug(total > 1 ? `Creating ${total} user shells...` : 'Creating 1 user shell...');
|
|
|
|
|
|
|
|
try {
|
|
|
|
await this.userRepository.manager.transaction(async (transactionManager) =>
|
|
|
|
Promise.all(
|
|
|
|
usersToSetUp.map(async (email) => {
|
|
|
|
const newUser = Object.assign(new User(), {
|
|
|
|
email,
|
|
|
|
globalRole: role,
|
|
|
|
});
|
|
|
|
const savedUser = await transactionManager.save<User>(newUser);
|
|
|
|
createUsers[savedUser.email] = savedUser.id;
|
|
|
|
return savedUser;
|
|
|
|
}),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
} catch (error) {
|
|
|
|
ErrorReporter.error(error);
|
|
|
|
this.logger.error('Failed to create user shells', { userShells: createUsers });
|
|
|
|
throw new InternalServerError('An error occurred during user creation');
|
|
|
|
}
|
|
|
|
|
|
|
|
this.logger.debug('Created user shell(s) successfully', { userId: req.user.id });
|
|
|
|
this.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 emailingResults = await Promise.all(
|
|
|
|
usersPendingSetup.map(async ([email, id]) => {
|
|
|
|
if (!id) {
|
|
|
|
// This should never happen since those are removed from the list before reaching this point
|
|
|
|
throw new InternalServerError('User ID is missing for user with email address');
|
|
|
|
}
|
|
|
|
const inviteAcceptUrl = generateUserInviteUrl(req.user.id, id);
|
|
|
|
const resp: {
|
|
|
|
user: { id: string | null; email: string; inviteAcceptUrl: string; emailSent: boolean };
|
|
|
|
error?: string;
|
|
|
|
} = {
|
|
|
|
user: {
|
|
|
|
id,
|
|
|
|
email,
|
|
|
|
inviteAcceptUrl,
|
|
|
|
emailSent: false,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
try {
|
|
|
|
const result = await this.mailer.invite({
|
|
|
|
email,
|
|
|
|
inviteAcceptUrl,
|
|
|
|
domain: baseUrl,
|
|
|
|
});
|
|
|
|
if (result.emailSent) {
|
|
|
|
resp.user.emailSent = true;
|
|
|
|
void this.internalHooks.onUserTransactionalEmail({
|
|
|
|
user_id: id,
|
|
|
|
message_type: 'New user invite',
|
|
|
|
public_api: false,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void this.internalHooks.onUserInvite({
|
|
|
|
user: req.user,
|
|
|
|
target_user_id: Object.values(createUsers) as string[],
|
|
|
|
public_api: false,
|
|
|
|
email_sent: result.emailSent,
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
if (error instanceof Error) {
|
|
|
|
void this.internalHooks.onEmailFailed({
|
|
|
|
user: req.user,
|
|
|
|
message_type: 'New user invite',
|
|
|
|
public_api: false,
|
|
|
|
});
|
|
|
|
this.logger.error('Failed to send email', {
|
|
|
|
userId: req.user.id,
|
|
|
|
inviteAcceptUrl,
|
|
|
|
domain: baseUrl,
|
|
|
|
email,
|
|
|
|
});
|
|
|
|
resp.error = error.message;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return resp;
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.externalHooks.run('user.invited', [usersToSetUp]);
|
|
|
|
|
|
|
|
this.logger.debug(
|
|
|
|
usersPendingSetup.length > 1
|
|
|
|
? `Sent ${usersPendingSetup.length} invite emails successfully`
|
|
|
|
: 'Sent 1 invite email successfully',
|
|
|
|
{ userShells: createUsers },
|
|
|
|
);
|
|
|
|
|
|
|
|
return emailingResults;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fill out user shell with first name, last name, and password.
|
|
|
|
*/
|
2023-04-24 02:45:31 -07:00
|
|
|
@NoAuthRequired()
|
2023-01-27 02:19:47 -08:00
|
|
|
@Post('/:id')
|
|
|
|
async updateUser(req: UserRequest.Update, res: Response) {
|
|
|
|
const { id: inviteeId } = req.params;
|
|
|
|
|
|
|
|
const { inviterId, firstName, lastName, password } = req.body;
|
|
|
|
|
|
|
|
if (!inviterId || !inviteeId || !firstName || !lastName || !password) {
|
|
|
|
this.logger.debug(
|
|
|
|
'Request to fill out a user shell failed because of missing properties in payload',
|
|
|
|
{ payload: req.body },
|
|
|
|
);
|
|
|
|
throw new BadRequestError('Invalid payload');
|
|
|
|
}
|
|
|
|
|
|
|
|
const validPassword = validatePassword(password);
|
|
|
|
|
|
|
|
const users = await this.userRepository.find({
|
|
|
|
where: { id: In([inviterId, inviteeId]) },
|
|
|
|
relations: ['globalRole'],
|
|
|
|
});
|
|
|
|
|
|
|
|
if (users.length !== 2) {
|
|
|
|
this.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 BadRequestError('Invalid payload or URL');
|
|
|
|
}
|
|
|
|
|
|
|
|
const invitee = users.find((user) => user.id === inviteeId) as User;
|
|
|
|
|
|
|
|
if (invitee.password) {
|
|
|
|
this.logger.debug(
|
|
|
|
'Request to fill out a user shell failed because the invite had already been accepted',
|
|
|
|
{ inviteeId },
|
|
|
|
);
|
|
|
|
throw new BadRequestError('This invite has been accepted already');
|
|
|
|
}
|
|
|
|
|
|
|
|
invitee.firstName = firstName;
|
|
|
|
invitee.lastName = lastName;
|
|
|
|
invitee.password = await hashPassword(validPassword);
|
|
|
|
|
|
|
|
const updatedUser = await this.userRepository.save(invitee);
|
|
|
|
|
|
|
|
await issueCookie(res, updatedUser);
|
|
|
|
|
|
|
|
void this.internalHooks.onUserSignup(updatedUser, {
|
|
|
|
user_type: 'email',
|
|
|
|
was_disabled_ldap_user: false,
|
|
|
|
});
|
|
|
|
|
|
|
|
await this.externalHooks.run('user.profile.update', [invitee.email, sanitizeUser(invitee)]);
|
|
|
|
await this.externalHooks.run('user.password.update', [invitee.email, invitee.password]);
|
|
|
|
|
2023-02-21 00:35:35 -08:00
|
|
|
return withFeatureFlags(this.postHog, sanitizeUser(updatedUser));
|
2023-01-27 02:19:47 -08:00
|
|
|
}
|
|
|
|
|
2023-04-24 02:45:31 -07:00
|
|
|
@Authorized('any')
|
2023-01-27 02:19:47 -08:00
|
|
|
@Get('/')
|
|
|
|
async listUsers(req: UserRequest.List) {
|
|
|
|
const users = await this.userRepository.find({ relations: ['globalRole', 'authIdentities'] });
|
|
|
|
return users.map(
|
|
|
|
(user): PublicUser =>
|
|
|
|
addInviteLinkToUser(sanitizeUser(user, ['personalizationAnswers']), req.user.id),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-05-30 03:52:02 -07:00
|
|
|
@Authorized(['global', 'owner'])
|
|
|
|
@Get('/:id/password-reset-link')
|
|
|
|
async getUserPasswordResetLink(req: UserRequest.PasswordResetLink) {
|
|
|
|
const user = await this.userRepository.findOneOrFail({
|
|
|
|
where: { id: req.params.id },
|
|
|
|
});
|
|
|
|
if (!user) {
|
|
|
|
throw new NotFoundError('User not found');
|
|
|
|
}
|
|
|
|
const link = await UserService.generatePasswordResetUrl(user);
|
|
|
|
return {
|
|
|
|
link,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
@Authorized(['global', 'owner'])
|
|
|
|
@Patch('/:id/settings')
|
|
|
|
async updateUserSettings(req: UserRequest.UserSettingsUpdate) {
|
|
|
|
const payload = plainToInstance(UserSettingsUpdatePayload, req.body);
|
|
|
|
|
|
|
|
const id = req.params.id;
|
|
|
|
|
|
|
|
await UserService.updateUserSettings(id, payload);
|
|
|
|
|
|
|
|
const user = await this.userRepository.findOneOrFail({
|
|
|
|
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')
|
|
|
|
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;
|
|
|
|
|
|
|
|
if (transferId === idToDelete) {
|
|
|
|
throw new BadRequestError(
|
|
|
|
'Request to delete a user failed because the user to delete and the transferee are the same user',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const users = await this.userRepository.find({
|
|
|
|
where: { id: In([transferId, idToDelete]) },
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!users.length || (transferId && users.length !== 2)) {
|
|
|
|
throw new NotFoundError(
|
|
|
|
'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',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const userToDelete = users.find((user) => user.id === req.params.id) as User;
|
|
|
|
|
|
|
|
const telemetryData: ITelemetryUserDeletionData = {
|
|
|
|
user_id: req.user.id,
|
|
|
|
target_user_old_status: userToDelete.isPending ? 'invited' : 'active',
|
|
|
|
target_user_id: idToDelete,
|
|
|
|
};
|
|
|
|
|
|
|
|
telemetryData.migration_strategy = transferId ? 'transfer_data' : 'delete_data';
|
|
|
|
|
|
|
|
if (transferId) {
|
|
|
|
telemetryData.migration_user_id = transferId;
|
|
|
|
}
|
|
|
|
|
|
|
|
const [workflowOwnerRole, credentialOwnerRole] = await Promise.all([
|
2023-04-12 01:59:14 -07:00
|
|
|
this.roleRepository.findWorkflowOwnerRole(),
|
|
|
|
this.roleRepository.findCredentialOwnerRole(),
|
2023-01-27 02:19:47 -08:00
|
|
|
]);
|
|
|
|
|
|
|
|
if (transferId) {
|
|
|
|
const transferee = users.find((user) => user.id === transferId);
|
|
|
|
|
|
|
|
await this.userRepository.manager.transaction(async (transactionManager) => {
|
|
|
|
// Get all workflow ids belonging to user to delete
|
|
|
|
const sharedWorkflowIds = await transactionManager
|
|
|
|
.getRepository(SharedWorkflow)
|
|
|
|
.find({
|
|
|
|
select: ['workflowId'],
|
|
|
|
where: { userId: userToDelete.id, roleId: workflowOwnerRole?.id },
|
|
|
|
})
|
|
|
|
.then((sharedWorkflows) => sharedWorkflows.map(({ workflowId }) => workflowId));
|
|
|
|
|
|
|
|
// Prevents issues with unique key constraints since user being assigned
|
|
|
|
// workflows and credentials might be a sharee
|
|
|
|
await transactionManager.delete(SharedWorkflow, {
|
|
|
|
user: transferee,
|
|
|
|
workflowId: In(sharedWorkflowIds),
|
|
|
|
});
|
|
|
|
|
|
|
|
// Transfer ownership of owned workflows
|
|
|
|
await transactionManager.update(
|
|
|
|
SharedWorkflow,
|
|
|
|
{ user: userToDelete, role: workflowOwnerRole },
|
|
|
|
{ user: transferee },
|
|
|
|
);
|
|
|
|
|
|
|
|
// Now do the same for creds
|
|
|
|
|
|
|
|
// Get all workflow ids belonging to user to delete
|
|
|
|
const sharedCredentialIds = await transactionManager
|
|
|
|
.getRepository(SharedCredentials)
|
|
|
|
.find({
|
|
|
|
select: ['credentialsId'],
|
|
|
|
where: { userId: userToDelete.id, roleId: credentialOwnerRole?.id },
|
|
|
|
})
|
|
|
|
.then((sharedCredentials) => sharedCredentials.map(({ credentialsId }) => credentialsId));
|
|
|
|
|
|
|
|
// Prevents issues with unique key constraints since user being assigned
|
|
|
|
// workflows and credentials might be a sharee
|
|
|
|
await transactionManager.delete(SharedCredentials, {
|
|
|
|
user: transferee,
|
|
|
|
credentialsId: In(sharedCredentialIds),
|
|
|
|
});
|
|
|
|
|
|
|
|
// Transfer ownership of owned credentials
|
|
|
|
await transactionManager.update(
|
|
|
|
SharedCredentials,
|
|
|
|
{ user: userToDelete, role: credentialOwnerRole },
|
|
|
|
{ user: transferee },
|
|
|
|
);
|
|
|
|
|
|
|
|
await transactionManager.delete(AuthIdentity, { userId: userToDelete.id });
|
|
|
|
|
|
|
|
// This will remove all shared workflows and credentials not owned
|
|
|
|
await transactionManager.delete(User, { id: userToDelete.id });
|
|
|
|
});
|
|
|
|
|
|
|
|
void this.internalHooks.onUserDeletion({
|
|
|
|
user: req.user,
|
|
|
|
telemetryData,
|
|
|
|
publicApi: false,
|
|
|
|
});
|
|
|
|
await this.externalHooks.run('user.deleted', [sanitizeUser(userToDelete)]);
|
|
|
|
return { success: true };
|
|
|
|
}
|
|
|
|
|
|
|
|
const [ownedSharedWorkflows, ownedSharedCredentials] = await Promise.all([
|
|
|
|
this.sharedWorkflowRepository.find({
|
|
|
|
relations: ['workflow'],
|
|
|
|
where: { userId: userToDelete.id, roleId: workflowOwnerRole?.id },
|
|
|
|
}),
|
|
|
|
this.sharedCredentialsRepository.find({
|
|
|
|
relations: ['credentials'],
|
|
|
|
where: { userId: userToDelete.id, roleId: credentialOwnerRole?.id },
|
|
|
|
}),
|
|
|
|
]);
|
|
|
|
|
|
|
|
await this.userRepository.manager.transaction(async (transactionManager) => {
|
|
|
|
const ownedWorkflows = await Promise.all(
|
|
|
|
ownedSharedWorkflows.map(async ({ workflow }) => {
|
|
|
|
if (workflow.active) {
|
|
|
|
// deactivate before deleting
|
|
|
|
await this.activeWorkflowRunner.remove(workflow.id);
|
|
|
|
}
|
|
|
|
return workflow;
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
await transactionManager.remove(ownedWorkflows);
|
|
|
|
await transactionManager.remove(ownedSharedCredentials.map(({ credentials }) => credentials));
|
|
|
|
|
|
|
|
await transactionManager.delete(AuthIdentity, { userId: userToDelete.id });
|
|
|
|
await transactionManager.delete(User, { id: userToDelete.id });
|
|
|
|
});
|
|
|
|
|
|
|
|
void this.internalHooks.onUserDeletion({
|
|
|
|
user: req.user,
|
|
|
|
telemetryData,
|
|
|
|
publicApi: false,
|
|
|
|
});
|
|
|
|
|
|
|
|
await this.externalHooks.run('user.deleted', [sanitizeUser(userToDelete)]);
|
|
|
|
return { success: true };
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Resend email invite to user.
|
|
|
|
*/
|
|
|
|
@Post('/:id/reinvite')
|
|
|
|
async reinviteUser(req: UserRequest.Reinvite) {
|
|
|
|
const { id: idToReinvite } = req.params;
|
2023-07-12 05:11:46 -07:00
|
|
|
const isWithinUsersLimit = Container.get(License).isWithinUsersLimit();
|
|
|
|
|
|
|
|
if (!isWithinUsersLimit) {
|
|
|
|
this.logger.debug(
|
|
|
|
'Request to send email invite(s) to user(s) failed because the user limit quota has been reached',
|
|
|
|
);
|
|
|
|
throw new UnauthorizedError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
|
|
|
|
}
|
2023-01-27 02:19:47 -08:00
|
|
|
|
|
|
|
if (!isEmailSetUp()) {
|
|
|
|
this.logger.error('Request to reinvite a user failed because email sending was not set up');
|
|
|
|
throw new InternalServerError('Email sending must be set up in order to invite other users');
|
|
|
|
}
|
|
|
|
|
|
|
|
const reinvitee = await this.userRepository.findOneBy({ id: idToReinvite });
|
|
|
|
if (!reinvitee) {
|
|
|
|
this.logger.debug(
|
|
|
|
'Request to reinvite a user failed because the ID of the reinvitee was not found in database',
|
|
|
|
);
|
|
|
|
throw new NotFoundError('Could not find user');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (reinvitee.password) {
|
|
|
|
this.logger.debug(
|
|
|
|
'Request to reinvite a user failed because the invite had already been accepted',
|
|
|
|
{ userId: reinvitee.id },
|
|
|
|
);
|
|
|
|
throw new BadRequestError('User has already accepted the invite');
|
|
|
|
}
|
|
|
|
|
|
|
|
const baseUrl = getInstanceBaseUrl();
|
|
|
|
const inviteAcceptUrl = `${baseUrl}/signup?inviterId=${req.user.id}&inviteeId=${reinvitee.id}`;
|
|
|
|
|
|
|
|
try {
|
|
|
|
const result = await this.mailer.invite({
|
|
|
|
email: reinvitee.email,
|
|
|
|
inviteAcceptUrl,
|
|
|
|
domain: baseUrl,
|
|
|
|
});
|
|
|
|
if (result.emailSent) {
|
|
|
|
void this.internalHooks.onUserReinvite({
|
|
|
|
user: req.user,
|
|
|
|
target_user_id: reinvitee.id,
|
|
|
|
public_api: false,
|
|
|
|
});
|
|
|
|
|
|
|
|
void this.internalHooks.onUserTransactionalEmail({
|
|
|
|
user_id: reinvitee.id,
|
|
|
|
message_type: 'Resend invite',
|
|
|
|
public_api: false,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
void this.internalHooks.onEmailFailed({
|
|
|
|
user: reinvitee,
|
|
|
|
message_type: 'Resend invite',
|
|
|
|
public_api: false,
|
|
|
|
});
|
|
|
|
this.logger.error('Failed to send email', {
|
|
|
|
email: reinvitee.email,
|
|
|
|
inviteAcceptUrl,
|
|
|
|
domain: baseUrl,
|
|
|
|
});
|
|
|
|
throw new InternalServerError(`Failed to send email to ${reinvitee.email}`);
|
|
|
|
}
|
|
|
|
return { success: true };
|
|
|
|
}
|
|
|
|
}
|