feat(core): Add filtering, selection and pagination to users (#6994)
https://linear.app/n8n/issue/PAY-646
This commit is contained in:
@@ -8,16 +8,15 @@ import {
|
||||
InternalServerError,
|
||||
UnauthorizedError,
|
||||
} from '@/ResponseHelper';
|
||||
import { sanitizeUser, withFeatureFlags } from '@/UserManagement/UserManagementHelper';
|
||||
import { issueCookie, resolveJwt } from '@/auth/jwt';
|
||||
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from '@/constants';
|
||||
import { Request, Response } from 'express';
|
||||
import { ILogger } from 'n8n-workflow';
|
||||
import type { User } from '@db/entities/User';
|
||||
import { LoginRequest, UserRequest } from '@/requests';
|
||||
import type { PublicUser } from '@/Interfaces';
|
||||
import { Config } from '@/config';
|
||||
import { IInternalHooksClass } from '@/Interfaces';
|
||||
import type { PublicUser, CurrentUser } from '@/Interfaces';
|
||||
import { handleEmailLogin, handleLdapLogin } from '@/auth';
|
||||
import { PostHogClient } from '@/posthog';
|
||||
import {
|
||||
@@ -98,7 +97,8 @@ export class AuthController {
|
||||
user,
|
||||
authenticationMethod: usedAuthenticationMethod,
|
||||
});
|
||||
return withFeatureFlags(this.postHog, sanitizeUser(user));
|
||||
|
||||
return this.userService.toPublic(user, { posthog: this.postHog });
|
||||
}
|
||||
void Container.get(InternalHooks).onUserLoginFailed({
|
||||
user: email,
|
||||
@@ -112,7 +112,7 @@ export class AuthController {
|
||||
* Manually check the `n8n-auth` cookie.
|
||||
*/
|
||||
@Get('/login')
|
||||
async currentUser(req: Request, res: Response): Promise<CurrentUser> {
|
||||
async currentUser(req: Request, res: Response): Promise<PublicUser> {
|
||||
// Manually check the existing cookie.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const cookieContents = req.cookies?.[AUTH_COOKIE_NAME] as string | undefined;
|
||||
@@ -123,7 +123,7 @@ export class AuthController {
|
||||
try {
|
||||
user = await resolveJwt(cookieContents);
|
||||
|
||||
return await withFeatureFlags(this.postHog, sanitizeUser(user));
|
||||
return await this.userService.toPublic(user, { posthog: this.postHog });
|
||||
} catch (error) {
|
||||
res.clearCookie(AUTH_COOKIE_NAME);
|
||||
}
|
||||
@@ -146,7 +146,7 @@ export class AuthController {
|
||||
}
|
||||
|
||||
await issueCookie(res, user);
|
||||
return withFeatureFlags(this.postHog, sanitizeUser(user));
|
||||
return this.userService.toPublic(user, { posthog: this.postHog });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,7 +183,10 @@ export class AuthController {
|
||||
}
|
||||
}
|
||||
|
||||
const users = await this.userService.findMany({ where: { id: In([inviterId, inviteeId]) } });
|
||||
const users = await this.userService.findMany({
|
||||
where: { id: In([inviterId, inviteeId]) },
|
||||
relations: ['globalRole'],
|
||||
});
|
||||
if (users.length !== 2) {
|
||||
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',
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import validator from 'validator';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { Authorized, Delete, Get, Patch, Post, RestController } from '@/decorators';
|
||||
import {
|
||||
compareHash,
|
||||
hashPassword,
|
||||
sanitizeUser,
|
||||
validatePassword,
|
||||
} from '@/UserManagement/UserManagementHelper';
|
||||
import { compareHash, hashPassword, validatePassword } from '@/UserManagement/UserManagementHelper';
|
||||
import { BadRequestError } from '@/ResponseHelper';
|
||||
import { validateEntity } from '@/GenericHelpers';
|
||||
import { issueCookie } from '@/auth/jwt';
|
||||
@@ -89,9 +84,11 @@ export class MeController {
|
||||
fields_changed: updatedKeys,
|
||||
});
|
||||
|
||||
await this.externalHooks.run('user.profile.update', [currentEmail, sanitizeUser(user)]);
|
||||
const publicUser = await this.userService.toPublic(user);
|
||||
|
||||
return sanitizeUser(user);
|
||||
await this.externalHooks.run('user.profile.update', [currentEmail, publicUser]);
|
||||
|
||||
return publicUser;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,12 +2,7 @@ import validator from 'validator';
|
||||
import { validateEntity } from '@/GenericHelpers';
|
||||
import { Authorized, Post, RestController } from '@/decorators';
|
||||
import { BadRequestError } from '@/ResponseHelper';
|
||||
import {
|
||||
hashPassword,
|
||||
sanitizeUser,
|
||||
validatePassword,
|
||||
withFeatureFlags,
|
||||
} from '@/UserManagement/UserManagementHelper';
|
||||
import { hashPassword, validatePassword } from '@/UserManagement/UserManagementHelper';
|
||||
import { issueCookie } from '@/auth/jwt';
|
||||
import { Response } from 'express';
|
||||
import { ILogger } from 'n8n-workflow';
|
||||
@@ -106,7 +101,7 @@ export class OwnerController {
|
||||
|
||||
void this.internalHooks.onInstanceOwnerSetup({ user_id: userId });
|
||||
|
||||
return withFeatureFlags(this.postHog, sanitizeUser(owner));
|
||||
return this.userService.toPublic(owner, { posthog: this.postHog });
|
||||
}
|
||||
|
||||
@Post('/dismiss-banner')
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import validator from 'validator';
|
||||
import { In } from 'typeorm';
|
||||
import type { FindManyOptions } from 'typeorm';
|
||||
import { In, Not } from 'typeorm';
|
||||
import { ILogger, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
||||
import { User } from '@db/entities/User';
|
||||
import { SharedCredentials } from '@db/entities/SharedCredentials';
|
||||
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
|
||||
import { Authorized, NoAuthRequired, Delete, Get, Post, RestController, Patch } from '@/decorators';
|
||||
import {
|
||||
addInviteLinkToUser,
|
||||
generateUserInviteUrl,
|
||||
getInstanceBaseUrl,
|
||||
hashPassword,
|
||||
isEmailSetUp,
|
||||
sanitizeUser,
|
||||
validatePassword,
|
||||
withFeatureFlags,
|
||||
} from '@/UserManagement/UserManagementHelper';
|
||||
import { issueCookie } from '@/auth/jwt';
|
||||
import {
|
||||
@@ -23,12 +21,12 @@ import {
|
||||
UnauthorizedError,
|
||||
} from '@/ResponseHelper';
|
||||
import { Response } from 'express';
|
||||
import { Config } from '@/config';
|
||||
import { UserRequest, UserSettingsUpdatePayload } from '@/requests';
|
||||
import { ListQuery, UserRequest, UserSettingsUpdatePayload } from '@/requests';
|
||||
import { UserManagementMailer } from '@/UserManagement/email';
|
||||
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
||||
import { Config } from '@/config';
|
||||
import { IExternalHooksClass, IInternalHooksClass } from '@/Interfaces';
|
||||
import type { PublicUser, ITelemetryUserDeletionData } from '@/Interfaces';
|
||||
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
||||
import { AuthIdentity } from '@db/entities/AuthIdentity';
|
||||
import { PostHogClient } from '@/posthog';
|
||||
import { isSamlLicensedAndEnabled } from '../sso/saml/samlHelpers';
|
||||
@@ -40,6 +38,7 @@ import { RESPONSE_ERROR_MESSAGES } from '@/constants';
|
||||
import { JwtService } from '@/services/jwt.service';
|
||||
import { RoleService } from '@/services/role.service';
|
||||
import { UserService } from '@/services/user.service';
|
||||
import { listQueryMiddleware } from '@/middlewares';
|
||||
|
||||
@Authorized(['global', 'owner'])
|
||||
@RestController('/users')
|
||||
@@ -131,6 +130,7 @@ export class UsersController {
|
||||
// remove/exclude existing users from creation
|
||||
const existingUsers = await this.userService.findMany({
|
||||
where: { email: In(Object.keys(createUsers)) },
|
||||
relations: ['globalRole'],
|
||||
});
|
||||
existingUsers.forEach((user) => {
|
||||
if (user.password) {
|
||||
@@ -306,20 +306,98 @@ export class UsersController {
|
||||
was_disabled_ldap_user: false,
|
||||
});
|
||||
|
||||
await this.externalHooks.run('user.profile.update', [invitee.email, sanitizeUser(invitee)]);
|
||||
const publicInvitee = await this.userService.toPublic(invitee);
|
||||
|
||||
await this.externalHooks.run('user.profile.update', [invitee.email, publicInvitee]);
|
||||
await this.externalHooks.run('user.password.update', [invitee.email, invitee.password]);
|
||||
|
||||
return withFeatureFlags(this.postHog, sanitizeUser(updatedUser));
|
||||
return this.userService.toPublic(updatedUser, { posthog: this.postHog });
|
||||
}
|
||||
|
||||
private async toFindManyOptions(listQueryOptions?: ListQuery.Options) {
|
||||
const findManyOptions: FindManyOptions<User> = {};
|
||||
|
||||
if (!listQueryOptions) {
|
||||
findManyOptions.relations = ['globalRole', 'authIdentities'];
|
||||
return findManyOptions;
|
||||
}
|
||||
|
||||
const { filter, select, take, skip } = listQueryOptions;
|
||||
|
||||
if (select) findManyOptions.select = select;
|
||||
if (take) findManyOptions.take = take;
|
||||
if (skip) findManyOptions.skip = skip;
|
||||
|
||||
if (take && !select) {
|
||||
findManyOptions.relations = ['globalRole', 'authIdentities'];
|
||||
}
|
||||
|
||||
if (take && select && !select?.id) {
|
||||
findManyOptions.select = { ...findManyOptions.select, id: true }; // pagination requires id
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
const { isOwner, ...otherFilters } = filter;
|
||||
|
||||
findManyOptions.where = otherFilters;
|
||||
|
||||
if (isOwner !== undefined) {
|
||||
const ownerRole = await this.roleService.findGlobalOwnerRole();
|
||||
|
||||
findManyOptions.relations = ['globalRole'];
|
||||
findManyOptions.where.globalRole = { id: isOwner ? ownerRole.id : Not(ownerRole.id) };
|
||||
}
|
||||
}
|
||||
|
||||
return findManyOptions;
|
||||
}
|
||||
|
||||
removeSupplementaryFields(
|
||||
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) {
|
||||
for (const user of publicUsers) delete user.globalRole;
|
||||
}
|
||||
|
||||
// remove computed fields (unselectable)
|
||||
|
||||
if (select) {
|
||||
for (const user of publicUsers) {
|
||||
delete user.isOwner;
|
||||
delete user.isPending;
|
||||
delete user.signInType;
|
||||
delete user.hasRecoveryCodesLeft;
|
||||
}
|
||||
}
|
||||
|
||||
return publicUsers;
|
||||
}
|
||||
|
||||
@Authorized('any')
|
||||
@Get('/')
|
||||
async listUsers(req: UserRequest.List) {
|
||||
const users = await this.userService.findMany({ relations: ['globalRole', 'authIdentities'] });
|
||||
return users.map(
|
||||
(user): PublicUser =>
|
||||
addInviteLinkToUser(sanitizeUser(user, ['personalizationAnswers']), req.user.id),
|
||||
@Get('/', { middlewares: listQueryMiddleware })
|
||||
async listUsers(req: ListQuery.Request) {
|
||||
const { listQueryOptions } = req;
|
||||
|
||||
const findManyOptions = await this.toFindManyOptions(listQueryOptions);
|
||||
|
||||
const users = await this.userService.findMany(findManyOptions);
|
||||
|
||||
const publicUsers: Array<Partial<PublicUser>> = await Promise.all(
|
||||
users.map(async (u) => this.userService.toPublic(u, { withInviteUrl: true })),
|
||||
);
|
||||
|
||||
return listQueryOptions
|
||||
? this.removeSupplementaryFields(publicUsers, listQueryOptions)
|
||||
: publicUsers;
|
||||
}
|
||||
|
||||
@Authorized(['global', 'owner'])
|
||||
@@ -393,6 +471,7 @@ export class UsersController {
|
||||
|
||||
const users = await this.userService.findMany({
|
||||
where: { id: In([transferId, idToDelete]) },
|
||||
relations: ['globalRole'],
|
||||
});
|
||||
|
||||
if (!users.length || (transferId && users.length !== 2)) {
|
||||
@@ -483,7 +562,7 @@ export class UsersController {
|
||||
telemetryData,
|
||||
publicApi: false,
|
||||
});
|
||||
await this.externalHooks.run('user.deleted', [sanitizeUser(userToDelete)]);
|
||||
await this.externalHooks.run('user.deleted', [await this.userService.toPublic(userToDelete)]);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@@ -521,7 +600,7 @@ export class UsersController {
|
||||
publicApi: false,
|
||||
});
|
||||
|
||||
await this.externalHooks.run('user.deleted', [sanitizeUser(userToDelete)]);
|
||||
await this.externalHooks.run('user.deleted', [await this.userService.toPublic(userToDelete)]);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user