initial
This commit is contained in:
24
apps/api/src/account/account-delete-request.entity.ts
Normal file
24
apps/api/src/account/account-delete-request.entity.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
export type AccountDeleteRequestStatus = 'pending' | 'resolved';
|
||||
|
||||
@Entity({ name: 'account_delete_requests' })
|
||||
export class AccountDeleteRequest {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
username!: string;
|
||||
|
||||
@Column({ default: 'pending' })
|
||||
status!: AccountDeleteRequestStatus;
|
||||
|
||||
@Column({ nullable: true })
|
||||
reason?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
resolvedAt?: Date;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
}
|
||||
125
apps/api/src/account/account.controller.ts
Normal file
125
apps/api/src/account/account.controller.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
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 { AuditService } from '../audit/audit.service';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { RequestUser } from '../common/request-user';
|
||||
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,
|
||||
@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),
|
||||
}),
|
||||
);
|
||||
await this.mail.sendEmailChangeMail(dto.newEmail, token);
|
||||
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 ungueltig 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');
|
||||
}
|
||||
}
|
||||
22
apps/api/src/account/account.module.ts
Normal file
22
apps/api/src/account/account.module.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuditModule } from '../audit/audit.module';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { LldapModule } from '../lldap/lldap.module';
|
||||
import { MailModule } from '../mail/mail.module';
|
||||
import { AccountDeleteRequest } from './account-delete-request.entity';
|
||||
import { AccountController } from './account.controller';
|
||||
import { EmailChangeRequest } from './email-change-request.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([EmailChangeRequest, AccountDeleteRequest]),
|
||||
AuthModule,
|
||||
LldapModule,
|
||||
MailModule,
|
||||
AuditModule,
|
||||
],
|
||||
controllers: [AccountController],
|
||||
exports: [TypeOrmModule],
|
||||
})
|
||||
export class AccountModule {}
|
||||
7
apps/api/src/account/dto/confirm-email-change.dto.ts
Normal file
7
apps/api/src/account/dto/confirm-email-change.dto.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { IsString, Length } from 'class-validator';
|
||||
|
||||
export class ConfirmEmailChangeDto {
|
||||
@IsString()
|
||||
@Length(20, 256)
|
||||
token!: string;
|
||||
}
|
||||
8
apps/api/src/account/dto/delete-request.dto.ts
Normal file
8
apps/api/src/account/dto/delete-request.dto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { IsOptional, IsString, Length } from 'class-validator';
|
||||
|
||||
export class DeleteRequestDto {
|
||||
@IsString()
|
||||
@Length(0, 1000)
|
||||
@IsOptional()
|
||||
reason?: string;
|
||||
}
|
||||
6
apps/api/src/account/dto/request-email-change.dto.ts
Normal file
6
apps/api/src/account/dto/request-email-change.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { IsEmail } from 'class-validator';
|
||||
|
||||
export class RequestEmailChangeDto {
|
||||
@IsEmail()
|
||||
newEmail!: string;
|
||||
}
|
||||
22
apps/api/src/account/dto/update-profile.dto.ts
Normal file
22
apps/api/src/account/dto/update-profile.dto.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { IsOptional, IsString, Length } from 'class-validator';
|
||||
|
||||
export class UpdateProfileDto {
|
||||
@IsString()
|
||||
@Length(1, 128)
|
||||
@IsOptional()
|
||||
displayName?: string;
|
||||
|
||||
@IsString()
|
||||
@Length(0, 128)
|
||||
@IsOptional()
|
||||
firstName?: string;
|
||||
|
||||
@IsString()
|
||||
@Length(0, 128)
|
||||
@IsOptional()
|
||||
lastName?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
avatar?: string;
|
||||
}
|
||||
25
apps/api/src/account/email-change-request.entity.ts
Normal file
25
apps/api/src/account/email-change-request.entity.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'email_change_requests' })
|
||||
export class EmailChangeRequest {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
username!: string;
|
||||
|
||||
@Column()
|
||||
newEmail!: string;
|
||||
|
||||
@Column({ unique: true })
|
||||
tokenHash!: string;
|
||||
|
||||
@Column()
|
||||
expiresAt!: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
consumedAt?: Date;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
}
|
||||
Reference in New Issue
Block a user