initial
This commit is contained in:
108
apps/api/src/password/password.service.ts
Normal file
108
apps/api/src/password/password.service.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { assertPasswordPolicy } from '../common/password-policy';
|
||||
import { hashToken, randomToken } from '../common/token.util';
|
||||
import { LdapAuthService } from '../lldap/ldap-auth.service';
|
||||
import { LldapService } from '../lldap/lldap.service';
|
||||
import { PortalMailService } from '../mail/portal-mail.service';
|
||||
import { PasswordResetToken } from './password-reset-token.entity';
|
||||
|
||||
@Injectable()
|
||||
export class PasswordService {
|
||||
constructor(
|
||||
@InjectRepository(PasswordResetToken)
|
||||
private readonly resetTokens: Repository<PasswordResetToken>,
|
||||
private readonly config: ConfigService,
|
||||
private readonly ldapAuth: LdapAuthService,
|
||||
private readonly lldap: LldapService,
|
||||
private readonly mail: PortalMailService,
|
||||
private readonly audit: AuditService,
|
||||
) {}
|
||||
|
||||
async changePassword(
|
||||
username: string,
|
||||
currentPassword: string,
|
||||
newPassword: string,
|
||||
ipAddress?: string,
|
||||
userAgent?: string,
|
||||
) {
|
||||
assertPasswordPolicy(newPassword);
|
||||
|
||||
const valid = await this.ldapAuth.verifyPassword(username, currentPassword);
|
||||
if (!valid) {
|
||||
await this.audit.record({ type: 'password.change_failed', username, ipAddress, userAgent });
|
||||
throw new UnauthorizedException('Das aktuelle Passwort ist nicht korrekt.');
|
||||
}
|
||||
|
||||
await this.lldap.setPassword(username, newPassword);
|
||||
await this.audit.record({ type: 'password.changed', username, ipAddress, userAgent });
|
||||
return { message: 'Das Passwort wurde geaendert.' };
|
||||
}
|
||||
|
||||
async requestReset(email: string, ipAddress?: string, userAgent?: string) {
|
||||
const normalizedEmail = email.toLowerCase();
|
||||
const neutral = {
|
||||
message: 'Falls ein Konto mit dieser E-Mail existiert, wurde ein Reset-Link versendet.',
|
||||
};
|
||||
|
||||
const user = await this.lldap.findUserByEmail(normalizedEmail).catch(() => null);
|
||||
if (!user?.email) {
|
||||
await this.audit.record({
|
||||
type: 'password.reset_requested_unknown',
|
||||
ipAddress,
|
||||
userAgent,
|
||||
metadata: { email: normalizedEmail },
|
||||
});
|
||||
return neutral;
|
||||
}
|
||||
|
||||
const token = randomToken();
|
||||
await this.resetTokens.save(
|
||||
this.resetTokens.create({
|
||||
username: user.id,
|
||||
email: user.email,
|
||||
tokenHash: hashToken(token, this.tokenSecret),
|
||||
expiresAt: new Date(Date.now() + 60 * 60_000),
|
||||
}),
|
||||
);
|
||||
|
||||
await this.mail.sendPasswordResetMail(user.email, token);
|
||||
await this.audit.record({
|
||||
type: 'password.reset_requested',
|
||||
username: user.id,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
|
||||
return neutral;
|
||||
}
|
||||
|
||||
async confirmReset(token: string, newPassword: string, ipAddress?: string, userAgent?: string) {
|
||||
assertPasswordPolicy(newPassword);
|
||||
|
||||
const tokenHash = hashToken(token, this.tokenSecret);
|
||||
const record = await this.resetTokens.findOne({ where: { tokenHash, consumedAt: IsNull() } });
|
||||
if (!record || record.expiresAt.getTime() < Date.now()) {
|
||||
throw new BadRequestException('Der Reset-Link ist ungueltig oder abgelaufen.');
|
||||
}
|
||||
|
||||
await this.lldap.setPassword(record.username, newPassword);
|
||||
record.consumedAt = new Date();
|
||||
await this.resetTokens.save(record);
|
||||
await this.audit.record({
|
||||
type: 'password.reset_completed',
|
||||
username: record.username,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
|
||||
return { message: 'Das Passwort wurde zurueckgesetzt.' };
|
||||
}
|
||||
|
||||
private get tokenSecret(): string {
|
||||
return this.config.getOrThrow<string>('TOKEN_SECRET');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user