Neuer Inline-Atom mention {userId, username}: Markdown-Regel @username
(E-Mail-sicher über Wortgrenzen), Serializer, HTML-Span dt-mention,
Plain-Text für die Suche, Extraktor extractMentionUserIds. Neue
Endpoints GET /users/search (auth, min. 2 Zeichen, Limit 10,
Rate-Limit) und GET /users/brief (Batch-Auflösung für live
Anzeigenamen; gelöschte Nutzer → toter Chip). Editor: MentionView mit
Live-displayName, MentionAutocomplete (Klon des Wikilink-Musters),
Chip-CSS. 5 Unit-Tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
58 lines
2.1 KiB
TypeScript
58 lines
2.1 KiB
TypeScript
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<UserBriefView[]> {
|
|
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<UserBriefView[]> {
|
|
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 },
|
|
});
|
|
}
|
|
}
|