feat(backend): JIT-provision the local User on first Authentik login
AuthentikStrategy no longer rejects a valid token whose user has no local row — it creates the User from the token claims (given_name/family_name/ email) via the new shared resolveOrProvisionAuthentikUser helper, which is race-safe (P2002 -> re-read) and captures the User to the sync log. The WS token path (TokenVerificationService.verifyAuthentik) and OnboardingService now use the same helper, removing three copies of the lookup/create logic. A provisioned user still has no Membership and therefore no rights: LT role assignment from Authentik groups is the remaining gap; Verantwortliche go through the onboarding approval flow. Tests: provision-user.spec.ts (existing/new/race/rethrow); npm test green at 51. Docs updated. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import { MembershipStatus, Role, SyncOperation } from '@prisma/client';
|
||||
import { PrismaClient } from '../prisma/prisma.module';
|
||||
import { SyncService } from '../sync/sync.service';
|
||||
import { TokenVerificationService } from '../auth/token-verification.service';
|
||||
import { resolveOrProvisionAuthentikUser } from '../auth/provision-user';
|
||||
|
||||
/// Self-service onboarding for Gemeinde Verantwortliche. The person signs in
|
||||
/// with their Konfi-Castle-ID (Authentik) and submits a KC invite code plus
|
||||
@@ -52,7 +53,7 @@ export class OnboardingService {
|
||||
throw new BadRequestException('Gemeinde does not belong to this KC');
|
||||
}
|
||||
|
||||
const user = await this.upsertUser(claims);
|
||||
const user = await resolveOrProvisionAuthentikUser(this.prisma, this.sync, claims);
|
||||
|
||||
const existing = await this.prisma.membership.findUnique({
|
||||
where: {
|
||||
@@ -119,31 +120,6 @@ export class OnboardingService {
|
||||
return membership;
|
||||
}
|
||||
|
||||
private async upsertUser(claims: {
|
||||
sub: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}) {
|
||||
const email = claims.email.toLowerCase();
|
||||
const existing = await this.prisma.user.findUnique({
|
||||
where: { authentikSub: claims.sub },
|
||||
});
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const user = await this.prisma.user.create({
|
||||
data: {
|
||||
authentikSub: claims.sub,
|
||||
email,
|
||||
firstName: claims.firstName,
|
||||
lastName: claims.lastName,
|
||||
},
|
||||
});
|
||||
await this.sync.capture('User', SyncOperation.CREATE, user.id, user);
|
||||
return user;
|
||||
}
|
||||
|
||||
private summary(
|
||||
membershipId: string,
|
||||
status: MembershipStatus,
|
||||
|
||||
Reference in New Issue
Block a user