Files
ldap-identidy/apps/api/src/account/account.controller.ts
Bastian Wagner 45a41c2077 features
2026-07-17 14:07:04 +02:00

153 lines
5.7 KiB
TypeScript

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<EmailChangeRequest>,
@InjectRepository(AccountDeleteRequest)
private readonly deleteRequests: Repository<AccountDeleteRequest>,
) {}
@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<string>('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<string>('TOKEN_SECRET');
}
}