153 lines
5.7 KiB
TypeScript
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');
|
|
}
|
|
}
|