mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 01:26:44 +00:00
253 lines
7.3 KiB
TypeScript
253 lines
7.3 KiB
TypeScript
import type { RoleChangeRequestDto } from '@n8n/api-types';
|
|
import { Logger } from '@n8n/backend-common';
|
|
import type { PublicUser } from '@n8n/db';
|
|
import { User, UserRepository } from '@n8n/db';
|
|
import { Service } from '@n8n/di';
|
|
import { getGlobalScopes, type AssignableGlobalRole } from '@n8n/permissions';
|
|
import type { IUserSettings } from 'n8n-workflow';
|
|
import { UnexpectedError } from 'n8n-workflow';
|
|
|
|
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
|
import { EventService } from '@/events/event.service';
|
|
import type { Invitation } from '@/interfaces';
|
|
import type { PostHogClient } from '@/posthog';
|
|
import type { UserRequest } from '@/requests';
|
|
import { UrlService } from '@/services/url.service';
|
|
import { UserManagementMailer } from '@/user-management/email';
|
|
|
|
import { PublicApiKeyService } from './public-api-key.service';
|
|
|
|
@Service()
|
|
export class UserService {
|
|
constructor(
|
|
private readonly logger: Logger,
|
|
private readonly userRepository: UserRepository,
|
|
private readonly mailer: UserManagementMailer,
|
|
private readonly urlService: UrlService,
|
|
private readonly eventService: EventService,
|
|
private readonly publicApiKeyService: PublicApiKeyService,
|
|
) {}
|
|
|
|
async update(userId: string, data: Partial<User>) {
|
|
const user = await this.userRepository.findOneBy({ id: userId });
|
|
|
|
if (user) {
|
|
await this.userRepository.save({ ...user, ...data }, { transaction: true });
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
getManager() {
|
|
return this.userRepository.manager;
|
|
}
|
|
|
|
async updateSettings(userId: string, newSettings: Partial<IUserSettings>) {
|
|
const user = await this.userRepository.findOneOrFail({ where: { id: userId } });
|
|
|
|
if (user.settings) {
|
|
Object.assign(user.settings, newSettings);
|
|
} else {
|
|
user.settings = newSettings;
|
|
}
|
|
|
|
await this.userRepository.save(user);
|
|
}
|
|
|
|
async toPublic(
|
|
user: User,
|
|
options?: {
|
|
withInviteUrl?: boolean;
|
|
inviterId?: string;
|
|
posthog?: PostHogClient;
|
|
withScopes?: boolean;
|
|
},
|
|
) {
|
|
const { password, updatedAt, authIdentities, mfaRecoveryCodes, mfaSecret, ...rest } = user;
|
|
|
|
const providerType = authIdentities?.[0]?.providerType;
|
|
|
|
let publicUser: PublicUser = {
|
|
...rest,
|
|
signInType: providerType ?? 'email',
|
|
isOwner: user.role === 'global:owner',
|
|
};
|
|
|
|
if (options?.withInviteUrl && !options?.inviterId) {
|
|
throw new UnexpectedError('Inviter ID is required to generate invite URL');
|
|
}
|
|
|
|
if (options?.withInviteUrl && options?.inviterId && publicUser.isPending) {
|
|
publicUser = this.addInviteUrl(options.inviterId, publicUser);
|
|
}
|
|
|
|
if (options?.posthog) {
|
|
publicUser = await this.addFeatureFlags(publicUser, options.posthog);
|
|
}
|
|
|
|
// TODO: resolve these directly in the frontend
|
|
if (options?.withScopes) {
|
|
publicUser.globalScopes = getGlobalScopes(user);
|
|
}
|
|
|
|
return publicUser;
|
|
}
|
|
|
|
private addInviteUrl(inviterId: string, invitee: PublicUser) {
|
|
const url = new URL(this.urlService.getInstanceBaseUrl());
|
|
url.pathname = '/signup';
|
|
url.searchParams.set('inviterId', inviterId);
|
|
url.searchParams.set('inviteeId', invitee.id);
|
|
|
|
invitee.inviteAcceptUrl = url.toString();
|
|
|
|
return invitee;
|
|
}
|
|
|
|
private async addFeatureFlags(publicUser: PublicUser, posthog: PostHogClient) {
|
|
// native PostHog implementation has default 10s timeout and 3 retries.. which cannot be updated without affecting other functionality
|
|
// https://github.com/PostHog/posthog-js-lite/blob/a182de80a433fb0ffa6859c10fb28084d0f825c2/posthog-core/src/index.ts#L67
|
|
const timeoutPromise = new Promise<PublicUser>((resolve) => {
|
|
setTimeout(() => {
|
|
resolve(publicUser);
|
|
}, 1500);
|
|
});
|
|
|
|
const fetchPromise = new Promise<PublicUser>(async (resolve) => {
|
|
publicUser.featureFlags = await posthog.getFeatureFlags(publicUser);
|
|
resolve(publicUser);
|
|
});
|
|
|
|
return await Promise.race([fetchPromise, timeoutPromise]);
|
|
}
|
|
|
|
private async sendEmails(
|
|
owner: User,
|
|
toInviteUsers: { [key: string]: string },
|
|
role: AssignableGlobalRole,
|
|
) {
|
|
const domain = this.urlService.getInstanceBaseUrl();
|
|
|
|
return await Promise.all(
|
|
Object.entries(toInviteUsers).map(async ([email, id]) => {
|
|
const inviteAcceptUrl = `${domain}/signup?inviterId=${owner.id}&inviteeId=${id}`;
|
|
const invitedUser: UserRequest.InviteResponse = {
|
|
user: {
|
|
id,
|
|
email,
|
|
inviteAcceptUrl,
|
|
emailSent: false,
|
|
role,
|
|
},
|
|
error: '',
|
|
};
|
|
|
|
try {
|
|
const result = await this.mailer.invite({
|
|
email,
|
|
inviteAcceptUrl,
|
|
});
|
|
if (result.emailSent) {
|
|
invitedUser.user.emailSent = true;
|
|
delete invitedUser.user?.inviteAcceptUrl;
|
|
|
|
this.eventService.emit('user-transactional-email-sent', {
|
|
userId: id,
|
|
messageType: 'New user invite',
|
|
publicApi: false,
|
|
});
|
|
}
|
|
|
|
this.eventService.emit('user-invited', {
|
|
user: owner,
|
|
targetUserId: Object.values(toInviteUsers),
|
|
publicApi: false,
|
|
emailSent: result.emailSent,
|
|
inviteeRole: role, // same role for all invited users
|
|
});
|
|
} catch (e) {
|
|
if (e instanceof Error) {
|
|
this.eventService.emit('email-failed', {
|
|
user: owner,
|
|
messageType: 'New user invite',
|
|
publicApi: false,
|
|
});
|
|
this.logger.error('Failed to send email', {
|
|
userId: owner.id,
|
|
inviteAcceptUrl,
|
|
email,
|
|
});
|
|
invitedUser.error = e.message;
|
|
}
|
|
}
|
|
|
|
return invitedUser;
|
|
}),
|
|
);
|
|
}
|
|
|
|
async inviteUsers(owner: User, invitations: Invitation[]) {
|
|
const emails = invitations.map(({ email }) => email);
|
|
|
|
const existingUsers = await this.userRepository.findManyByEmail(emails);
|
|
|
|
const existUsersEmails = existingUsers.map((user) => user.email);
|
|
|
|
const toCreateUsers = invitations.filter(({ email }) => !existUsersEmails.includes(email));
|
|
|
|
const pendingUsersToInvite = existingUsers.filter((email) => email.isPending);
|
|
|
|
const createdUsers = new Map<string, string>();
|
|
|
|
this.logger.debug(
|
|
toCreateUsers.length > 1
|
|
? `Creating ${toCreateUsers.length} user shells...`
|
|
: 'Creating 1 user shell...',
|
|
);
|
|
|
|
try {
|
|
await this.getManager().transaction(
|
|
async (transactionManager) =>
|
|
await Promise.all(
|
|
toCreateUsers.map(async ({ email, role }) => {
|
|
const { user: savedUser } = await this.userRepository.createUserWithProject(
|
|
{ email, role },
|
|
transactionManager,
|
|
);
|
|
createdUsers.set(email, savedUser.id);
|
|
return savedUser;
|
|
}),
|
|
),
|
|
);
|
|
} catch (error) {
|
|
this.logger.error('Failed to create user shells', { userShells: createdUsers });
|
|
throw new InternalServerError('An error occurred during user creation', error);
|
|
}
|
|
|
|
pendingUsersToInvite.forEach(({ email, id }) => createdUsers.set(email, id));
|
|
|
|
const usersInvited = await this.sendEmails(
|
|
owner,
|
|
Object.fromEntries(createdUsers),
|
|
invitations[0].role, // same role for all invited users
|
|
);
|
|
|
|
return { usersInvited, usersCreated: toCreateUsers.map(({ email }) => email) };
|
|
}
|
|
|
|
async changeUserRole(user: User, targetUser: User, newRole: RoleChangeRequestDto) {
|
|
return await this.userRepository.manager.transaction(async (trx) => {
|
|
await trx.update(User, { id: targetUser.id }, { role: newRole.newRoleName });
|
|
|
|
const adminDowngradedToMember =
|
|
user.role === 'global:owner' &&
|
|
targetUser.role === 'global:admin' &&
|
|
newRole.newRoleName === 'global:member';
|
|
|
|
if (adminDowngradedToMember) {
|
|
await this.publicApiKeyService.removeOwnerOnlyScopesFromApiKeys(targetUser, trx);
|
|
}
|
|
});
|
|
}
|
|
}
|