Any authenticated user can invite an e-mail address; the mailed single-use token lets exactly one signup through even while registration is closed. Open (pending, unexpired) invitations count against the new instance setting invitations.maxOpenPerUser (default 5, 0 disables inviting) — plus a 20/day per-user rate limit so a revoke-and-recreate loop cannot become a mail cannon. Only the SHA-256 token hash is stored (auth-tokens pattern); a failed signup (taken username) un-redeems the token so the invitee can retry. Surfaces: invitations section in the user settings (list, invite, revoke, quota line; wide table in a focusable .table-scroll region), signup page reads ?invitation=<token> (preview banner, e-mail prefill, closed-mode gate opens only for a previewed-valid token), admin general card gets the quota field (flat RHF name per #322; VS-NfD marked and hideable). Governance: audit actions invitation.created/revoked/accepted (catalogue 1.10), VS-NfD profile entry (compliant: 0) + hardening-guide row, i18n de+en including the invitation mail template. Tests: api e2e-db (mail link, closed-mode single-use signup with un-redeem on failure, quota + revoke frees slot, quota 0 = 403, auth matrix), new web e2e pack invitations.spec.ts (full UI loop through Mailpit, wired into ci.yml with its own rate-limit reset), a11y scan waits for the new section. Full api suite (107 files / 607 tests), auth/admin-settings/a11y packs green against a fresh local stack. Closes #332
54 lines
2.1 KiB
TypeScript
54 lines
2.1 KiB
TypeScript
import { Logger, Module, OnModuleInit } from '@nestjs/common';
|
|
import { APP_GUARD } from '@nestjs/core';
|
|
|
|
import { AppConfig } from '../config/app-config.service';
|
|
import { GrantsModule } from '../grants/grants.module';
|
|
import { InvitationsModule } from '../invitations/invitations.module';
|
|
|
|
import { MailModule } from '../mail/mail.module';
|
|
import { PondsModule } from '../ponds/ponds.module';
|
|
import { UsersModule } from '../users/users.module';
|
|
import { AuthController } from './auth.controller';
|
|
import { AuthGuard } from './auth.guard';
|
|
import { AuthService } from './auth.service';
|
|
import { AuthTokensService } from './auth-tokens.service';
|
|
import { ClaimMappingService } from './claim-mapping.service';
|
|
import { OidcController } from './oidc.controller';
|
|
import { OidcService } from './oidc.service';
|
|
import { ProxyIdentityService } from './proxy-identity.service';
|
|
import { SessionsModule } from './sessions.module';
|
|
|
|
@Module({
|
|
imports: [UsersModule, MailModule, SessionsModule, PondsModule, GrantsModule, InvitationsModule],
|
|
controllers: [AuthController, OidcController],
|
|
providers: [
|
|
AuthService,
|
|
AuthTokensService,
|
|
ClaimMappingService,
|
|
OidcService,
|
|
ProxyIdentityService,
|
|
// Global default-protected: every route needs a session unless it
|
|
// opts out with @Public().
|
|
{ provide: APP_GUARD, useClass: AuthGuard },
|
|
],
|
|
exports: [AuthTokensService, AuthService, OidcService],
|
|
})
|
|
export class AuthModule implements OnModuleInit {
|
|
constructor(
|
|
private readonly config: AppConfig,
|
|
private readonly oidc: OidcService,
|
|
private readonly proxyIdentity: ProxyIdentityService,
|
|
) {}
|
|
|
|
onModuleInit(): void {
|
|
// #216: local auth off without ANY external path means nobody can ever
|
|
// sign in — loudly stated at boot, because the operator will otherwise
|
|
// discover it at the login screen.
|
|
if (!this.config.env.AUTH_LOCAL_ENABLED && !this.oidc.enabled && !this.proxyIdentity.enabled) {
|
|
new Logger(AuthModule.name).warn(
|
|
'AUTH_LOCAL_ENABLED=false with neither OIDC nor proxy authentication configured — no sign-in path exists',
|
|
);
|
|
}
|
|
}
|
|
}
|