manicanldes-backend/src/identity/recovery.service.ts

59 lines
2.0 KiB
TypeScript

import { Inject, Injectable, Logger } from '@nestjs/common';
import { ENVIRONMENT } from '../config/environment.module';
import type { Environment } from '../config/environment';
import { AuthStore } from './auth.store';
import { RecoveryStore } from './recovery.store';
import { RecoveryMailer } from './recovery-mailer';
import { RateLimitService } from './rate-limit.service';
import { PasswordService } from './password.service';
import { hashToken, issueToken } from './tokens';
import type { RecoveryInput } from './identity.schemas';
@Injectable()
export class RecoveryService {
private readonly logger = new Logger(RecoveryService.name);
constructor(
private readonly users: AuthStore,
private readonly store: RecoveryStore,
private readonly mailer: RecoveryMailer,
private readonly limits: RateLimitService,
private readonly passwords: PasswordService,
@Inject(ENVIRONMENT) private readonly env: Environment,
) {}
async request(input: RecoveryInput): Promise<void> {
this.mailer.assertConfigured();
await this.limits.consume(
`recovery:${input.organizationId}:${input.email}`,
3,
900,
);
const user = await this.users.findUser(input.organizationId, input.email);
if (!user || user.status !== 'ACTIVE') return;
const { token, tokenHash } = issueToken();
const created = await this.store.create(
user.id,
tokenHash,
new Date(Date.now() + this.env.RECOVERY_TTL_MINUTES * 60_000),
);
if (!created) return;
try {
await this.mailer.send(user.email, token);
} catch {
await this.store.discard(tokenHash);
// SMTP errors may contain credentials and addresses; do not log the raw error.
this.logger.error(
JSON.stringify({
event: 'RECOVERY_DELIVERY_FAILED',
message: 'Password recovery delivery failed',
}),
);
}
}
async reset(token: string, password: string): Promise<void> {
await this.store.reset(
hashToken(token),
await this.passwords.hash(password),
);
}
}