import { Inject, Injectable } from '@nestjs/common'; import { ENVIRONMENT } from '../config/environment.module'; import type { Environment } from '../config/environment'; import { AppError } from '../common/errors/app-error'; import { AuthStore } from './auth.store'; import { PasswordService } from './password.service'; import { RateLimitService } from './rate-limit.service'; import { issueToken } from './tokens'; import type { LoginInput } from './identity.schemas'; @Injectable() export class AuthService { constructor( private readonly store: AuthStore, private readonly passwords: PasswordService, private readonly limits: RateLimitService, @Inject(ENVIRONMENT) private readonly env: Environment, ) {} async login(input: LoginInput) { await this.limits.consume( `login:${input.organizationId}:${input.email}`, 10, 900, ); const user = await this.store.findUser(input.organizationId, input.email); let valid = false; if (user) valid = await this.passwords.verify(input.password, user.passwordHash); else await this.passwords.dummyVerify(input.password); if (!valid || !user || user.status !== 'ACTIVE') { if (user) await this.store.failedLogin(user.organizationId, user.id); const reason = !user ? 'ACCOUNT_UNKNOWN' : !valid ? 'PASSWORD_MISMATCH' : 'ACCOUNT_INACTIVE'; throw new AppError('AUTH_INVALID_CREDENTIALS', reason); } const { token, tokenHash } = issueToken(); const expiresAt = new Date( Date.now() + this.env.SESSION_TTL_MINUTES * 60_000, ); await this.store.createSession( user.id, user.passwordHash, tokenHash, expiresAt, ); return { accessToken: token, tokenType: 'Bearer', expiresAt }; } }