Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | 3x 3x 3x 3x 3x 7x 7x 7x 19x 19x 7x 6x 7x 15x 7x 5x 2x 2x 5x 5x 5x 3x | import { timingSafeEqual } from 'node:crypto';
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { AuthenticatedUser, UserRole } from './auth.decorators';
interface DemoUser {
email: string;
password: string;
role: UserRole;
}
/**
* Demo credential store, seeded from AUTH_USERS ("email:password:role" CSV).
* A real deployment delegates this to an identity provider (ADR-0008); the
* seam that matters is that the rest of the system only ever sees the JWT.
*/
const DEFAULT_USERS =
'manager@propflow.dev:propflow:manager,' +
'tenant@propflow.dev:propflow:tenant,' +
'tech@propflow.dev:propflow:technician';
@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
private readonly users: DemoUser[];
constructor(private readonly jwt: JwtService) {
this.users = (process.env.AUTH_USERS ?? DEFAULT_USERS)
.split(',')
.map((entry) => {
const [email, password, role] = entry.split(':');
return { email, password, role: role as UserRole };
});
if (!process.env.AUTH_USERS) {
this.logger.warn('AUTH_USERS not set, using built-in demo users');
}
}
async login(
email: string,
password: string,
): Promise<{ accessToken: string; role: UserRole }> {
const user = this.users.find(
(candidate) =>
candidate.email === email &&
this.passwordMatches(candidate.password, password),
);
if (!user) {
// One error for both unknown user and wrong password: the response
// must not reveal which emails exist.
throw new UnauthorizedException('invalid credentials');
}
const payload: AuthenticatedUser = { sub: user.email, role: user.role };
return {
accessToken: await this.jwt.signAsync(payload),
role: user.role,
};
}
/** Constant-time comparison — a plain === leaks the match length/prefix
* through timing, the classic credential-check mistake. */
private passwordMatches(expected: string, given: string): boolean {
const a = Buffer.from(expected);
const b = Buffer.from(given);
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
}
|