feat(search): Meilisearch message search — indexer + permission-scoped query
New search module: a message index (Meilisearch, MEILI_URL-gated; no-op when unset), a SearchProjector that indexes on message.sent, and a SearchService whose permission fence runs server-side — a query only touches the caller's own scope AND the threads they currently belong to (resolved live from participant rows, so membership changes reflect immediately). Every conversation kind (chat/mail/sms) is a message with a TEXT part, so all are searchable through one index. POST /v1/search + /reindex (backfill). 5 fence tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { BadRequestException, Body, Controller, Headers, Post } from '@nestjs/common';
|
||||
import { SessionVerifier } from '../platform/session.verifier';
|
||||
import { ActorResolver, type MessagePrincipal } from '../identity/actor.resolver';
|
||||
import { SearchService } from './search.service';
|
||||
|
||||
interface SearchDto { query?: string; limit?: number }
|
||||
|
||||
/** Message search (session-auth). Results are permission-scoped inside the service to the caller's
|
||||
* own scope + threads. Reindex backfills the caller's scope (idempotent). */
|
||||
@Controller('v1/search')
|
||||
export class SearchController {
|
||||
constructor(
|
||||
private readonly search: SearchService,
|
||||
private readonly session: SessionVerifier,
|
||||
private readonly actors: ActorResolver,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
async run(@Body() body: SearchDto, @Headers('authorization') authorization?: string) {
|
||||
const principal = this.principal(authorization);
|
||||
return this.search.search(principal, { query: body.query ?? '', ...(body.limit != null ? { limit: body.limit } : {}) });
|
||||
}
|
||||
|
||||
@Post('reindex')
|
||||
async reindex(@Headers('authorization') authorization?: string) {
|
||||
const principal = this.principal(authorization);
|
||||
const scope = await this.actors.findScope(principal);
|
||||
return { indexed: await this.search.reindexAll(scope?.id) };
|
||||
}
|
||||
|
||||
private principal(authorization?: string): MessagePrincipal {
|
||||
const token = (authorization ?? '').replace(/^Bearer\s+/i, '');
|
||||
if (!token) throw new BadRequestException('Authorization bearer token is required');
|
||||
return this.session.verify(token);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user