feat(core): Extend user list to allow expanding the user list to projects (#16314)

Co-authored-by: Guillaume Jacquart <jacquart.guillaume@gmail.com>
Co-authored-by: Csaba Tuncsik <csaba@n8n.io>
Co-authored-by: Csaba Tuncsik <csaba.tuncsik@gmail.com>
This commit is contained in:
Andreas Fitzek
2025-06-18 22:26:50 +02:00
committed by GitHub
parent 701c31cfbc
commit c0d1ff6e4c
20 changed files with 780 additions and 180 deletions

View File

@@ -1,4 +1,9 @@
import { RoleChangeRequestDto, SettingsUpdateRequestDto } from '@n8n/api-types';
import {
RoleChangeRequestDto,
SettingsUpdateRequestDto,
UsersListFilterDto,
usersListSchema,
} from '@n8n/api-types';
import { Logger } from '@n8n/backend-common';
import type { PublicUser } from '@n8n/db';
import {
@@ -19,6 +24,7 @@ import {
Licensed,
Body,
Param,
Query,
} from '@n8n/decorators';
import { Response } from 'express';
@@ -29,8 +35,7 @@ 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 { listQueryMiddleware } from '@/middlewares';
import { ListQuery, AuthenticatedRequest, UserRequest } from '@/requests';
import { AuthenticatedRequest, UserRequest } from '@/requests';
import { FolderService } from '@/services/folder.service';
import { ProjectService } from '@/services/project.service.ee';
import { UserService } from '@/services/user.service';
@@ -64,20 +69,16 @@ export class UsersController {
private removeSupplementaryFields(
publicUsers: Array<Partial<PublicUser>>,
listQueryOptions: ListQuery.Options,
listQueryOptions: UsersListFilterDto,
) {
const { take, select, filter } = listQueryOptions;
const { select } = listQueryOptions;
// remove fields added to satisfy query
if (take && select && !select?.id) {
if (select !== undefined && !select.includes('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) {
@@ -91,25 +92,40 @@ export class UsersController {
return publicUsers;
}
@Get('/', { middlewares: listQueryMiddleware })
@Get('/')
@GlobalScope('user:list')
async listUsers(req: ListQuery.Request) {
const { listQueryOptions } = req;
async listUsers(
req: AuthenticatedRequest,
_res: Response,
@Query listQueryOptions: UsersListFilterDto,
) {
const userQuery = this.userRepository.buildUserQuery(listQueryOptions);
const findManyOptions = await this.userRepository.toFindManyOptions(listQueryOptions);
const response = await userQuery.getManyAndCount();
const users = await this.userRepository.find(findManyOptions);
const [users, count] = response;
const publicUsers: Array<Partial<PublicUser>> = await Promise.all(
users.map(
async (u) =>
await this.userService.toPublic(u, { withInviteUrl: true, inviterId: req.user.id }),
),
const publicUsers = await Promise.all(
users.map(async (u) => {
const user = await this.userService.toPublic(u, {
withInviteUrl: true,
inviterId: req.user.id,
});
return {
...user,
projectRelations: u.projectRelations?.map((pr) => ({
id: pr.projectId,
role: pr.role, // normalize role for frontend
name: pr.project.name,
})),
};
}),
);
return listQueryOptions
? this.removeSupplementaryFields(publicUsers, listQueryOptions)
: publicUsers;
return usersListSchema.parse({
count,
items: this.removeSupplementaryFields(publicUsers, listQueryOptions),
});
}
@Get('/:id/password-reset-link')