import { Body, Controller, Get, Patch, Post, Req, UseGuards } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Request } from 'express'; import { InjectRepository } from '@nestjs/typeorm'; import { IsNull, Repository } from 'typeorm'; import { ApplicationErrorCategory, ApplicationErrorCode } from '../application-error-log/application-error-codes'; import { ApplicationErrorLoggerService } from '../application-error-log/application-error-logger.service'; import { maskEmail } from '../application-error-log/application-error-sanitizer'; import { AuditService } from '../audit/audit.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { RequestUser } from '../common/request-user'; import { RequestContextService } from '../common/request-context.service'; import { hashToken, randomToken } from '../common/token.util'; import { LldapService } from '../lldap/lldap.service'; import { PortalMailService } from '../mail/portal-mail.service'; import { AccountDeleteRequest } from './account-delete-request.entity'; import { ConfirmEmailChangeDto } from './dto/confirm-email-change.dto'; import { DeleteRequestDto } from './dto/delete-request.dto'; import { RequestEmailChangeDto } from './dto/request-email-change.dto'; import { UpdateProfileDto } from './dto/update-profile.dto'; import { EmailChangeRequest } from './email-change-request.entity'; @Controller('account') @UseGuards(JwtAuthGuard) export class AccountController { constructor( private readonly lldap: LldapService, private readonly mail: PortalMailService, private readonly config: ConfigService, private readonly audit: AuditService, private readonly applicationErrorLogger: ApplicationErrorLoggerService, private readonly requestContext: RequestContextService, @InjectRepository(EmailChangeRequest) private readonly emailChanges: Repository, @InjectRepository(AccountDeleteRequest) private readonly deleteRequests: Repository, ) {} @Get('me') me(@Req() request: Request & { user: RequestUser }) { return this.lldap.getAccount(request.user.username); } @Patch('profile') async updateProfile( @Body() dto: UpdateProfileDto, @Req() request: Request & { user: RequestUser }, ) { await this.lldap.updateUser(request.user.username, dto); await this.audit.record({ type: 'account.profile_updated', username: request.user.username, ipAddress: request.ip, userAgent: request.headers['user-agent'], }); return this.lldap.getAccount(request.user.username); } @Post('email-change/request') async requestEmailChange( @Body() dto: RequestEmailChangeDto, @Req() request: Request & { user: RequestUser }, ) { const token = randomToken(); await this.emailChanges.save( this.emailChanges.create({ username: request.user.username, newEmail: dto.newEmail.toLowerCase(), tokenHash: hashToken(token, this.tokenSecret), expiresAt: new Date(Date.now() + 24 * 60 * 60_000), }), ); try { await this.mail.sendEmailChangeMail(dto.newEmail, token); } catch (error) { await this.applicationErrorLogger.log({ error, category: ApplicationErrorCategory.EMAIL, code: ApplicationErrorCode.ACCOUNT_EMAIL_CHANGE_EMAIL_SEND_FAILED, module: 'AccountModule', service: AccountController.name, operation: 'sendEmailChangeMail', requestContext: { ...this.requestContext.get(), userId: request.user.username, }, context: { maskedRecipient: maskEmail(dto.newEmail.toLowerCase()), mailProvider: this.config.get('SMTP_HOST') ?? 'smtp', }, handled: true, }); throw error; } await this.audit.record({ type: 'account.email_change_requested', username: request.user.username, ipAddress: request.ip, userAgent: request.headers['user-agent'], metadata: { newEmail: dto.newEmail.toLowerCase() }, }); return { message: 'Bitte bestaetige die neue E-Mail-Adresse.' }; } @Post('email-change/confirm') async confirmEmailChange( @Body() dto: ConfirmEmailChangeDto, @Req() request: Request & { user: RequestUser }, ) { const tokenHash = hashToken(dto.token, this.tokenSecret); const record = await this.emailChanges.findOne({ where: { tokenHash, consumedAt: IsNull() } }); if (!record || record.expiresAt.getTime() < Date.now() || record.username !== request.user.username) { return { message: 'Der Bestaetigungslink ist ungültig oder abgelaufen.' }; } await this.lldap.updateUser(request.user.username, { email: record.newEmail }); record.consumedAt = new Date(); await this.emailChanges.save(record); await this.audit.record({ type: 'account.email_changed', username: request.user.username, ipAddress: request.ip, userAgent: request.headers['user-agent'], metadata: { newEmail: record.newEmail }, }); return { message: 'Die E-Mail-Adresse wurde aktualisiert.' }; } @Post('delete-request') async requestDeletion( @Body() dto: DeleteRequestDto, @Req() request: Request & { user: RequestUser }, ) { await this.deleteRequests.save( this.deleteRequests.create({ username: request.user.username, reason: dto.reason, }), ); await this.audit.record({ type: 'account.delete_requested', username: request.user.username, ipAddress: request.ip, userAgent: request.headers['user-agent'], }); return { message: 'Die Anfrage wurde gespeichert.' }; } private get tokenSecret(): string { return this.config.getOrThrow('TOKEN_SECRET'); } }