feat(backend): self-registration for Gemeinde Verantwortliche
New onboarding/ module. A prospective Verantwortliche/r signs in with their
Konfi-Castle-ID (Authentik), looks up a KC by invite code, picks an existing
Gemeinde, and registers:
- GET /api/onboarding/kc/:inviteCode -> KC name + its Gemeinden (public;
the invite code is the shared secret)
- POST /api/onboarding/verantwortliche -> verifies the raw Authentik bearer
token's claims (no local Membership required yet via new
TokenVerificationService.verifyAuthentikClaims), JIT-provisions the local
User, and creates a Membership with status PENDING. Idempotent per
(user, kc, gemeinde).
- GET /api/onboarding/requests?kcId= (LT) list pending
- POST /api/onboarding/requests/:id/approve|reject (LT) approve flips to
ACTIVE, reject deletes.
Schema: Membership gains status (enum MembershipStatus { ACTIVE, PENDING },
default ACTIVE). AuthentikStrategy / TokenVerificationService / TeamAuthService
now load only ACTIVE memberships, so a pending request grants nothing until
approved. Membership create/update/delete flow through the sync log.
Tests: onboarding.service.spec.ts (14 cases); npm test green at 46.
Docs (plan + backend README) updated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
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'), RolesGuard)
|
||||
@Roles(Role.LEITUNGSTEAM)
|
||||
listRequests(@Query('kcId') kcId: string) {
|
||||
return this.onboarding.listRequests(kcId);
|
||||
}
|
||||
|
||||
@Post('requests/:membershipId/approve')
|
||||
@UseGuards(AuthGuard('authentik'), RolesGuard)
|
||||
@Roles(Role.LEITUNGSTEAM)
|
||||
approve(@Param('membershipId') membershipId: string) {
|
||||
return this.onboarding.approve(membershipId);
|
||||
}
|
||||
|
||||
@Post('requests/:membershipId/reject')
|
||||
@UseGuards(AuthGuard('authentik'), RolesGuard)
|
||||
@Roles(Role.LEITUNGSTEAM)
|
||||
reject(@Param('membershipId') membershipId: string) {
|
||||
return this.onboarding.reject(membershipId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user