dorfteich/apps/api/src/app.module.ts
Claude Opus 4.8 4d48d72c40
All checks were successful
CD / Build and push images (push) Successful in 3m3s
CI / Lint, typecheck, test (push) Successful in 2m21s
CI / Auth e2e pack (push) Successful in 2m58s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m13s
CD / Promote to Int (push) Successful in 11s
Add grant model and shared permission-resolution algorithm (#51)
The heart of the security model: one algorithm, implemented once, for API,
collab, and UI (permissions.md — authoritative).

- shared `permissions/`: pure resolution (`resolvePageCapability`) exactly per
  permissions.md — specificity page > label (incl. ancestor labels) > pond,
  deny wins within a level, default-closed, Site Admin bypass — plus the trash
  rule (`canAccessPage` / `canAccessTrashedPage`, ADR 0013). `grantValidationError`
  enforces the structural constraints. Documented, I/O-free signatures for
  API/collab reuse.
- prisma: `RoleGrant` (+ grant enums) per data-model.md, unique on
  (pond, subject, role, scope); migration adds a CHECK backstop that a
  POND_ADMIN grant is pond-scope + user-subject.
- api `grants/`: `GrantsService.createGrant` validates before insert (structural
  + no extra admin on a personal pond), rejects duplicates; `grantsForPond`
  returns the shared resolver model (what #52/#53 consume); enum mappers between
  the DB and the shared model. Interim "who may manage grants" stays until #52.
- tests: exhaustive table-driven resolver suite — every worked example from
  permissions.md §Resolution, edge cases (multi-label deny-wins, ancestor
  inheritance, anonymous/public, most-specific-allow-beats-less-specific-deny,
  trash) and a property test (a less-specific grant never overrides a
  more-specific decision); validation unit tests; grants db test proving
  write-time rejection of invalid grants.
- i18n: grant error codes (de + en).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
2026-07-09 14:02:07 +02:00

65 lines
2.3 KiB
TypeScript

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { LoggerModule } from 'nestjs-pino';
import { AdminModule } from './admin/admin.module';
import { AuthModule } from './auth/auth.module';
import { ApiExceptionFilter } from './common/api-exception.filter';
import { CompactionModule } from './compaction/compaction.module';
import { AppConfig } from './config/app-config.service';
import { ConfigModule } from './config/config.module';
import { FilesModule } from './files/files.module';
import { GrantsModule } from './grants/grants.module';
import { HealthModule } from './health/health.module';
import { LabelsModule } from './labels/labels.module';
import { LinksModule } from './links/links.module';
import { MailModule } from './mail/mail.module';
import { PagesModule } from './pages/pages.module';
import { PondsModule } from './ponds/ponds.module';
import { PrismaModule } from './prisma/prisma.module';
import { RateLimitModule } from './rate-limit/rate-limit.module';
import { SearchModule } from './search/search.module';
import { SettingsModule } from './settings/settings.module';
import { TrashModule } from './trash/trash.module';
import { UsersModule } from './users/users.module';
import { VersionsModule } from './versions/versions.module';
@Module({
imports: [
ConfigModule,
PrismaModule,
RateLimitModule,
MailModule,
SettingsModule,
UsersModule,
PondsModule,
PagesModule,
FilesModule,
TrashModule,
CompactionModule,
VersionsModule,
LabelsModule,
LinksModule,
SearchModule,
GrantsModule,
AuthModule,
AdminModule,
LoggerModule.forRootAsync({
inject: [AppConfig],
useFactory: (config: AppConfig) => ({
pinoHttp: {
level: config.env.LOG_LEVEL,
// Human-readable logs in local development, JSON everywhere else.
transport: config.env.NODE_ENV === 'development' ? { target: 'pino-pretty' } : undefined,
autoLogging: config.env.NODE_ENV !== 'test',
// Request bodies are never logged (operations.md logging rules).
redact: { paths: ['req.headers.authorization', 'req.headers.cookie'], remove: true },
},
}),
}),
HealthModule,
],
providers: [{ provide: APP_FILTER, useClass: ApiExceptionFilter }],
})
export class AppModule {}