chore(core): Expose mfaEnabled field to users list endpoint (#16654)

This commit is contained in:
Andreas Fitzek
2025-06-24 14:21:43 +02:00
committed by GitHub
parent bc53c21e15
commit c4a50df824
5 changed files with 74 additions and 3 deletions

View File

@@ -11,6 +11,8 @@ const USERS_LIST_SORT_OPTIONS = [
'lastName:desc',
'role:asc', // ascending order by role is Owner, Admin, Member
'role:desc',
'mfaEnabled:asc',
'mfaEnabled:desc',
// 'lastActive:asc',
// 'lastActive:desc',
] as const;
@@ -32,6 +34,7 @@ const userFilterSchema = z.object({
firstName: z.string().optional(),
lastName: z.string().optional(),
email: z.string().optional(),
mfaEnabled: z.boolean().optional(),
fullText: z.string().optional(), // Full text search across firstName, lastName, and email
});

View File

@@ -35,6 +35,7 @@ export const userListItemSchema = z.object({
personalizationAnswers: z.object({}).passthrough().nullable().optional(),
lastActive: z.string().optional(),
projectRelations: z.array(userProjectSchema).nullable().optional(),
mfaEnabled: z.boolean().optional(),
});
export const usersListSchema = z.object({

View File

@@ -182,6 +182,12 @@ export class UserRepository extends Repository<User> {
});
}
if (filter?.mfaEnabled !== undefined) {
queryBuilder.andWhere('user.mfaEnabled = :mfaEnabled', {
mfaEnabled: filter.mfaEnabled,
});
}
if (filter?.isOwner !== undefined) {
if (filter.isOwner) {
queryBuilder.andWhere('user.role = :role', {
@@ -240,13 +246,13 @@ export class UserRepository extends Repository<User> {
if (sortBy) {
for (const sort of sortBy) {
const [field, order] = sort.split(':');
if (field === 'firstName' || field === 'lastName') {
queryBuilder.addOrderBy(`user.${field}`, order.toUpperCase() as 'ASC' | 'DESC');
} else if (field === 'role') {
if (field === 'role') {
queryBuilder.addOrderBy(
"CASE WHEN user.role='global:owner' THEN 0 WHEN user.role='global:admin' THEN 1 ELSE 2 END",
order.toUpperCase() as 'ASC' | 'DESC',
);
} else {
queryBuilder.addOrderBy(`user.${field}`, order.toUpperCase() as 'ASC' | 'DESC');
}
}
}

View File

@@ -13,6 +13,7 @@ export const validateUser = (user: PublicUser) => {
expect(user.personalizationAnswers).toBeNull();
expect(user.password).toBeUndefined();
expect(user.role).toBeDefined();
expect(typeof (user as any).mfaEnabled).toBe('boolean');
};
export type UserInvitationResult = {

View File

@@ -61,6 +61,7 @@ describe('GET /users', () => {
email: 'member1@n8n.io',
firstName: 'Member1FirstName',
lastName: 'Member1LastName',
mfaEnabled: true,
});
await createUser({
role: 'global:member',
@@ -73,6 +74,7 @@ describe('GET /users', () => {
email: 'admin@n8n.io',
firstName: 'AdminFirstName',
lastName: 'AdminLastName',
mfaEnabled: true,
});
ownerAgent = testServer.authAgentFor(owner);
@@ -206,6 +208,38 @@ describe('GET /users', () => {
expect(_user.isOwner).toBe(false);
});
test('should filter users by mfaEnabled field', async () => {
const response = await ownerAgent
.get('/users')
.query('filter={ "mfaEnabled": true }')
.expect(200);
expect(response.body.data).toEqual({
count: 2,
items: expect.arrayContaining([]),
});
expect(response.body.data.items).toHaveLength(2);
const [user] = response.body.data.items;
expect(user.mfaEnabled).toBe(true);
const _response = await ownerAgent
.get('/users')
.query('filter={ "mfaEnabled": false }')
.expect(200);
expect(_response.body.data).toEqual({
count: 2,
items: expect.arrayContaining([]),
});
expect(_response.body.data.items).toHaveLength(2);
const [_user] = _response.body.data.items;
expect(_user.mfaEnabled).toBe(false);
});
test('should filter users by field: fullText', async () => {
const response = await ownerAgent
.get('/users')
@@ -574,6 +608,32 @@ describe('GET /users', () => {
expect(response.body.data.items[3].lastName).toBe('AdminLastName');
});
test('should sort by mfaEnabled:asc', async () => {
const response = await ownerAgent
.get('/users')
.query('sortBy[]=mfaEnabled:asc')
.expect(200);
expect(response.body.data.items).toHaveLength(4);
expect(response.body.data.items[0].mfaEnabled).toBe(false);
expect(response.body.data.items[1].mfaEnabled).toBe(false);
expect(response.body.data.items[2].mfaEnabled).toBe(true);
expect(response.body.data.items[3].mfaEnabled).toBe(true);
});
test('should sort by mfaEnabled:desc', async () => {
const response = await ownerAgent
.get('/users')
.query('sortBy[]=mfaEnabled:desc')
.expect(200);
expect(response.body.data.items).toHaveLength(4);
expect(response.body.data.items[0].mfaEnabled).toBe(true);
expect(response.body.data.items[1].mfaEnabled).toBe(true);
expect(response.body.data.items[2].mfaEnabled).toBe(false);
expect(response.body.data.items[3].mfaEnabled).toBe(false);
});
test('should sort by firstName and lastName combined', async () => {
const user1 = await createUser({
role: 'global:member',