import { Controller, Get, Query } from '@nestjs/common'; import type { UserBriefView } from '@dorfteich/shared'; import { AuthenticatedOnly } from '../permissions/permission.decorators'; import { PrismaService } from '../prisma/prisma.service'; import { RateLimit } from '../rate-limit/rate-limit.guard'; /** Cap for the batch `brief` lookup — a page mentions a handful of people. */ const BRIEF_MAX_IDS = 50; /** * Instance-wide user lookup for `@` mentions (issue #150). Deliberately * minimal: only id/username/displayName, only active accounts, only for * signed-in users, rate-limited, and never enumerable without a query — a * documented consequence of instance-wide mentions is that logged-in users * can discover usernames this way. */ @Controller('users') @AuthenticatedOnly() export class UserSearchController { constructor(private readonly prisma: PrismaService) {} @Get('search') @RateLimit({ scope: 'user-search', limit: 60, windowSeconds: 60 }) async search(@Query('q') q: string | undefined): Promise { const query = (q ?? '').trim(); if (query.length < 2) return []; return this.prisma.user.findMany({ where: { status: 'ACTIVE', OR: [ { username: { contains: query, mode: 'insensitive' } }, { displayName: { contains: query, mode: 'insensitive' } }, ], }, select: { id: true, username: true, displayName: true }, orderBy: { username: 'asc' }, take: 10, }); } /** Batch resolution of mentioned users for live display names; unknown or * disabled ids are simply absent (the mention renders as a dead chip). */ @Get('brief') async brief(@Query('ids') ids: string | undefined): Promise { const wanted = (ids ?? '') .split(',') .map((id) => id.trim()) .filter(Boolean) .slice(0, BRIEF_MAX_IDS); if (wanted.length === 0) return []; return this.prisma.user.findMany({ where: { id: { in: wanted }, status: 'ACTIVE' }, select: { id: true, username: true, displayName: true }, }); } }