diff --git a/fluxer_api/src/api/admin/services/AdminSearchService.ts b/fluxer_api/src/api/admin/services/AdminSearchService.ts index 6c9340e8e..801b1c62c 100644 --- a/fluxer_api/src/api/admin/services/AdminSearchService.ts +++ b/fluxer_api/src/api/admin/services/AdminSearchService.ts @@ -5,6 +5,7 @@ import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidat import type {WorkerJobPayload} from '@pkgs/worker/src/contracts/WorkerTypes'; import type {ApiContext} from '../../ApiContext'; import {createGuildID, createUserID, type UserID} from '../../BrandedTypes'; +import {isSyntheticUserId} from '../../constants/Core'; import type {IGuildRepositoryAggregate} from '../../guild/repositories/IGuildRepositoryAggregate'; import {Logger} from '../../Logger'; import {getGuildSearchService, getUserSearchService} from '../../SearchFactory'; @@ -129,12 +130,11 @@ export class AdminSearchService { throw new FeatureTemporarilyDisabledError(); } const query = data.query?.trim() || ''; - const isIdQuery = /^\d+$/.test(query); + const directUserId = /^\d+$/.test(query) ? createUserID(BigInt(query)) : null; + const canResolveDirectUser = directUserId !== null && !isSyntheticUserId(directUserId) && data.offset === 0; const [searchResult, directUser] = await Promise.all([ userSearchService.search(query, {}, {limit: data.limit, offset: data.offset}), - isIdQuery && data.offset === 0 - ? userRepository.findUnique(createUserID(BigInt(query))).catch(() => null) - : Promise.resolve(null), + canResolveDirectUser ? userRepository.findUnique(directUserId).catch(() => null) : Promise.resolve(null), ]); const {hits, total} = searchResult; const userIds = hits.map((hit) => createUserID(BigInt(hit.id))); diff --git a/fluxer_api/src/api/admin/services/AdminUserLookupService.ts b/fluxer_api/src/api/admin/services/AdminUserLookupService.ts index ee17e3e03..2aacbfbcc 100644 --- a/fluxer_api/src/api/admin/services/AdminUserLookupService.ts +++ b/fluxer_api/src/api/admin/services/AdminUserLookupService.ts @@ -3,6 +3,7 @@ import type {LookupUserRequest} from '@fluxer/schema/src/domains/admin/AdminUserSchemas'; import type {ApiContext} from '../../ApiContext'; import {createUserID} from '../../BrandedTypes'; +import {isSyntheticUserId} from '../../constants/Core'; import {Logger} from '../../Logger'; import {mapUserToAdminResponse} from '../models/UserTypes'; @@ -32,7 +33,7 @@ export class AdminUserLookupService { } else if (/^\d+$/.test(query)) { try { const userId = createUserID(BigInt(query)); - user = await userRepository.findUnique(userId); + user = isSyntheticUserId(userId) ? null : await userRepository.findUnique(userId); } catch (error) { Logger.debug({query, error}, 'Failed to lookup user by numeric ID, invalid ID format'); user = null; diff --git a/fluxer_api/src/api/admin/tests/AdminUserDirectory.test.ts b/fluxer_api/src/api/admin/tests/AdminUserDirectory.test.ts index 4651122b0..71e8a7987 100644 --- a/fluxer_api/src/api/admin/tests/AdminUserDirectory.test.ts +++ b/fluxer_api/src/api/admin/tests/AdminUserDirectory.test.ts @@ -21,6 +21,8 @@ interface UserListResponse { total: number; } +const SYNTHETIC_USER_IDS = ['0', '1']; + async function setLastActiveIp(harness: ApiTestHarness, token: string, ip: string): Promise { await createBuilder(harness, `${token}`) .get('/users/@me') @@ -170,5 +172,35 @@ describe('Admin user directory', () => { expect(result.users.map((user) => user.id)).toEqual([target.userId]); expect(result.users[0]?.email).toBeNull(); }); + test.each(SYNTHETIC_USER_IDS)('omits the synthetic account %s from the resolve selector', async (userId) => { + const admin = await createTestAccount(harness); + await setUserACLs(harness, admin, [AdminACLs.AUTHENTICATE, AdminACLs.USER_LOOKUP]); + const result = await createBuilder(harness, `${admin.token}`) + .get(`/admin/users?resolve=${userId}`) + .expect(HTTP_STATUS.OK) + .execute(); + expect(result.users).toEqual([]); + expect(result.total).toBe(0); + }); + test.each(SYNTHETIC_USER_IDS)('omits the synthetic account %s from the q selector', async (userId) => { + const admin = await createTestAccount(harness); + await setUserACLs(harness, admin, [AdminACLs.AUTHENTICATE, AdminACLs.USER_LOOKUP]); + const result = await createBuilder(harness, `${admin.token}`) + .get(`/admin/users?q=${userId}`) + .expect(HTTP_STATUS.OK) + .execute(); + expect(result.users.map((user) => user.id)).not.toContain(userId); + }); + }); + describe('GET /admin/users/:user_id', () => { + test.each(SYNTHETIC_USER_IDS)('reports no user for the synthetic account %s', async (userId) => { + const admin = await createTestAccount(harness); + await setUserACLs(harness, admin, [AdminACLs.AUTHENTICATE, AdminACLs.USER_LOOKUP]); + const result = await createBuilder(harness, `${admin.token}`) + .get(`/admin/users/${userId}`) + .expect(HTTP_STATUS.OK) + .execute(); + expect(result.users).toEqual([]); + }); }); }); diff --git a/fluxer_api/src/api/constants/Core.ts b/fluxer_api/src/api/constants/Core.ts index bbf750b33..b16d96a91 100644 --- a/fluxer_api/src/api/constants/Core.ts +++ b/fluxer_api/src/api/constants/Core.ts @@ -1,5 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import {createUserID} from '../BrandedTypes'; +import {DELETED_USER_ID} from '@fluxer/constants/src/UserConstants'; +import {createUserID, type UserID} from '../BrandedTypes'; export const SYSTEM_USER_ID = createUserID(0n); + +export function isSyntheticUserId(userId: UserID): boolean { + return userId === SYSTEM_USER_ID || userId === DELETED_USER_ID; +}