Files
ldap-identidy/apps/api/src/registration/registration.service.ts
Bastian Wagner d0600ab6ac login
2026-07-15 17:08:17 +02:00

161 lines
5.8 KiB
TypeScript

import { BadRequestException, ConflictException, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { In, IsNull, Repository } from 'typeorm';
import { AuditService } from '../audit/audit.service';
import { assertPasswordPolicy } from '../common/password-policy';
import { decryptSecret, encryptSecret, hashToken, randomToken } from '../common/token.util';
import { LldapService } from '../lldap/lldap.service';
import { PortalMailService } from '../mail/portal-mail.service';
import { RegisterDto } from './dto/register.dto';
import { EmailToken } from './email-token.entity';
import { RegistrationRequest } from './registration-request.entity';
@Injectable()
export class RegistrationService {
constructor(
@InjectRepository(RegistrationRequest)
private readonly registrations: Repository<RegistrationRequest>,
@InjectRepository(EmailToken)
private readonly emailTokens: Repository<EmailToken>,
private readonly config: ConfigService,
private readonly lldap: LldapService,
private readonly mail: PortalMailService,
private readonly audit: AuditService,
) {}
async register(dto: RegisterDto, ipAddress?: string, userAgent?: string) {
assertPasswordPolicy(dto.password);
const email = dto.email.toLowerCase();
const existingLdapUser = await this.lldap.findUserByUsername(email).catch(() => null);
if (existingLdapUser) {
throw new ConflictException('Diese E-Mail-Adresse ist bereits registriert.');
}
const pending = await this.registrations.findOne({
where: { username: email, status: 'pending_email' },
});
if (pending) {
throw new ConflictException('Fuer diese E-Mail-Adresse existiert bereits eine offene Registrierung.');
}
const registration = await this.registrations.save(
this.registrations.create({
username: email,
email,
displayName: dto.displayName,
encryptedPassword: encryptSecret(dto.password, this.tokenSecret),
status: 'pending_email',
expiresAt: new Date(Date.now() + 24 * 60 * 60_000),
}),
);
const token = randomToken();
await this.emailTokens.save(
this.emailTokens.create({
registrationId: registration.id,
tokenHash: hashToken(token, this.tokenSecret),
expiresAt: new Date(Date.now() + 24 * 60 * 60_000),
}),
);
await this.mail.sendVerificationMail(registration.email, token);
await this.audit.record({
type: 'registration.started',
username: registration.username,
ipAddress,
userAgent,
});
return { message: 'Bitte pruefe dein E-Mail-Postfach, um die Registrierung abzuschliessen.' };
}
async verify(token: string, ipAddress?: string, userAgent?: string) {
const tokenHash = hashToken(token, this.tokenSecret);
const tokenRecord = await this.emailTokens.findOne({ where: { tokenHash, consumedAt: IsNull() } });
if (!tokenRecord || tokenRecord.expiresAt.getTime() < Date.now()) {
throw new BadRequestException('Der Bestaetigungslink ist ungültig oder abgelaufen.');
}
const registration = await this.registrations.findOneByOrFail({ id: tokenRecord.registrationId });
if (registration.status !== 'pending_email' || registration.expiresAt.getTime() < Date.now()) {
throw new BadRequestException('Diese Registrierung ist nicht mehr gueltig.');
}
tokenRecord.consumedAt = new Date();
registration.status = 'pending_approval';
registration.verifiedAt = new Date();
await this.emailTokens.save(tokenRecord);
await this.registrations.save(registration);
await this.audit.record({
type: 'registration.email_verified',
username: registration.username,
ipAddress,
userAgent,
});
return { message: 'Die E-Mail wurde bestaetigt. Die Registrierung wartet jetzt auf Freigabe.' };
}
async list() {
return this.registrations.find({
where: { status: In(['pending_email', 'pending_approval']) },
order: { createdAt: 'DESC' },
});
}
async approve(id: string, reviewer: string, ipAddress?: string, userAgent?: string) {
const registration = await this.registrations.findOneByOrFail({ id });
if (registration.status !== 'pending_approval') {
throw new BadRequestException('Diese Registrierung wartet nicht auf Freigabe.');
}
await this.lldap.createUser({
username: registration.username,
email: registration.email,
displayName: registration.displayName,
password: decryptSecret(registration.encryptedPassword, this.tokenSecret),
});
registration.status = 'approved';
registration.reviewedAt = new Date();
registration.reviewedBy = reviewer;
await this.registrations.save(registration);
await this.audit.record({
type: 'registration.approved',
username: registration.username,
ipAddress,
userAgent,
metadata: { reviewer },
});
return registration;
}
async reject(id: string, reviewer: string, reason?: string, ipAddress?: string, userAgent?: string) {
const registration = await this.registrations.findOneByOrFail({ id });
if (registration.status !== 'pending_approval') {
throw new BadRequestException('Diese Registrierung wartet nicht auf Freigabe.');
}
registration.status = 'rejected';
registration.reviewedAt = new Date();
registration.reviewedBy = reviewer;
registration.rejectionReason = reason;
await this.registrations.save(registration);
await this.audit.record({
type: 'registration.rejected',
username: registration.username,
ipAddress,
userAgent,
metadata: { reviewer, reason },
});
return registration;
}
private get tokenSecret(): string {
return this.config.getOrThrow<string>('TOKEN_SECRET');
}
}