59 lines
2.0 KiB
TypeScript
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),
|
|
);
|
|
}
|
|
}
|