Backend: - AuthentikStrategy / TokenVerificationService: normalise the issuer's trailing slash and accept both `iss` spellings (Authentik's discovery issuer and token `iss` carry a trailing slash; the JWKS URL must not double it). Wire the real konfi-castle issuer into .env.example. - team token path now goes through toAuthenticatedUser too, so a local account flagged isLeitungsteam gets the synthetic global LT membership regardless of token kind. - LT-admin controllers (kc, gemeinde, onboarding, sync, teamer) accept ['authentik','team'] so such an account can use them. RolesGuard still enforces the actual LT/role check. - app.module serves the Flutter web build from client/app/build/web (SPA fallback covers the OIDC redirect path /v1/auth/callback), falling back to the interim client/web/ if it isn't built. Client (client/app/): - oidc.dart: Authorization-Code + PKCE against Authentik (discovery, S256 challenge, state, token exchange, refresh). Browser bits (sessionStorage, redirect, URL) behind a conditional import so `flutter test` still compiles on the VM. - AppState handles the ?code= callback on bootstrap, stores access + refresh, refreshes an expired token on restart. - Login screen: "Mit Konfi-Castle-ID anmelden" button (Leitungsteam / Verantwortliche) alongside the local Teamer password form. - admin_screen.dart: LT-only "Verwaltung" — list/create KCs, per KC the Gemeinden (list/create) and pending Verantwortlichen requests (approve/reject). Verified end to end against local Postgres with an isLeitungsteam account (create KC/Gemeinde, list + approve a request). flutter analyze/test/build web green; backend npm test 56. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
68 lines
2.1 KiB
TypeScript
68 lines
2.1 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
Get,
|
|
Headers,
|
|
Param,
|
|
Post,
|
|
Query,
|
|
UseGuards,
|
|
} from '@nestjs/common';
|
|
import { AuthGuard } from '@nestjs/passport';
|
|
import { OnboardingService } from './onboarding.service';
|
|
import { RegisterVerantwortlicheDto } from './dto/register-verantwortliche.dto';
|
|
import { Roles } from '../common/roles.decorator';
|
|
import { RolesGuard } from '../common/roles.guard';
|
|
import { Role } from '../common/role.enum';
|
|
|
|
function bearer(header?: string): string | undefined {
|
|
return header?.startsWith('Bearer ') ? header.slice('Bearer '.length) : undefined;
|
|
}
|
|
|
|
@Controller('onboarding')
|
|
export class OnboardingController {
|
|
constructor(private readonly onboarding: OnboardingService) {}
|
|
|
|
/// Public lookup: invite code -> KC name + selectable Gemeinden.
|
|
@Get('kc/:inviteCode')
|
|
resolveInvite(@Param('inviteCode') inviteCode: string) {
|
|
return this.onboarding.resolveInvite(inviteCode);
|
|
}
|
|
|
|
/// Self-registration as Gemeinde Verantwortliche/r. Authenticated by the
|
|
/// caller's raw Authentik bearer token (no local Membership required yet).
|
|
@Post('verantwortliche')
|
|
registerVerantwortliche(
|
|
@Body() dto: RegisterVerantwortlicheDto,
|
|
@Headers('authorization') authorization?: string,
|
|
) {
|
|
return this.onboarding.registerVerantwortliche(
|
|
bearer(authorization),
|
|
dto.inviteCode,
|
|
dto.gemeindeId,
|
|
);
|
|
}
|
|
|
|
/// Leitungsteam: review and act on pending self-registrations.
|
|
@Get('requests')
|
|
@UseGuards(AuthGuard(['authentik', 'team']), RolesGuard)
|
|
@Roles(Role.LEITUNGSTEAM)
|
|
listRequests(@Query('kcId') kcId: string) {
|
|
return this.onboarding.listRequests(kcId);
|
|
}
|
|
|
|
@Post('requests/:membershipId/approve')
|
|
@UseGuards(AuthGuard(['authentik', 'team']), RolesGuard)
|
|
@Roles(Role.LEITUNGSTEAM)
|
|
approve(@Param('membershipId') membershipId: string) {
|
|
return this.onboarding.approve(membershipId);
|
|
}
|
|
|
|
@Post('requests/:membershipId/reject')
|
|
@UseGuards(AuthGuard(['authentik', 'team']), RolesGuard)
|
|
@Roles(Role.LEITUNGSTEAM)
|
|
reject(@Param('membershipId') membershipId: string) {
|
|
return this.onboarding.reject(membershipId);
|
|
}
|
|
}
|