refactor(auth): move the password-hash helpers into their own module
compareLocalHash, OTP_ONLY_PASSWORD_HASH and a new isPasswordHash now live in password-hash.ts, which depends on bcryptjs only. The data-import script needs them, and importing auth.service would load NestJS and the ESM-only @nestjs/jwt. auth.service re-exports the first two, so callers and tests are unchanged. Its own bcrypt import, now unused, is dropped. No behaviour change: 21/21 auth tests, 178/178 overall. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3ac9e92e9f
commit
7019cad932
@@ -1,6 +1,5 @@
|
||||
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import {
|
||||
IamInvalidCredentialsError,
|
||||
@@ -10,30 +9,9 @@ import {
|
||||
} from '../iam/iam.service';
|
||||
import { AdminSessionService } from '../sessions/admin-session.service';
|
||||
import type { LoginDto } from './dto/login.dto';
|
||||
import { compareLocalHash } from './password-hash';
|
||||
|
||||
/**
|
||||
* Value written into `passwordHash` for accounts that must never log in with a
|
||||
* password — the ~1,590 legacy customers who only ever used OTP
|
||||
* (docs/DATA-MIGRATION-MAP.md §10.4). The `!` prefix is the Django/Rails
|
||||
* "unusable password" convention: readable by anyone looking at the row, and
|
||||
* matched by no password.
|
||||
*/
|
||||
export const OTP_ONLY_PASSWORD_HASH = '!OTP_ONLY_NO_PASSWORD_LOGIN';
|
||||
|
||||
/**
|
||||
* A real bcrypt hash always starts with `$2` ($2a$, $2b$, or PHP's $2y$ from
|
||||
* the legacy server — all three verify with bcryptjs). Anything else is not a
|
||||
* password: '' on accounts created without one, or OTP_ONLY_PASSWORD_HASH.
|
||||
*
|
||||
* Such hashes are refused here, BEFORE bcrypt is called, so the outcome never
|
||||
* depends on how a particular bcrypt build treats a malformed hash. bcryptjs 3
|
||||
* happens to return false today; a future version or a native binding might
|
||||
* throw and turn a wrong password into a 500 (owner ruling 2026-10-07).
|
||||
*/
|
||||
export async function compareLocalHash(password: string, passwordHash: string | null | undefined) {
|
||||
if (typeof passwordHash !== 'string' || !passwordHash.startsWith('$2')) return false;
|
||||
return bcrypt.compare(password, passwordHash);
|
||||
}
|
||||
export { compareLocalHash, OTP_ONLY_PASSWORD_HASH } from './password-hash';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
|
||||
/**
|
||||
* Value written into `passwordHash` for accounts that must never log in with a
|
||||
* password — the ~1,590 legacy customers who only ever used OTP
|
||||
* (docs/DATA-MIGRATION-MAP.md §10.4). The `!` prefix is the Django/Rails
|
||||
* "unusable password" convention: readable by anyone looking at the row, and
|
||||
* matched by no password.
|
||||
*
|
||||
* Kept in this dependency-free module (bcryptjs only) so the data-import
|
||||
* script can use it without loading NestJS.
|
||||
*/
|
||||
export const OTP_ONLY_PASSWORD_HASH = '!OTP_ONLY_NO_PASSWORD_LOGIN';
|
||||
|
||||
/**
|
||||
* A real bcrypt hash always starts with `$2` ($2a$, $2b$, or PHP's $2y$ from
|
||||
* the legacy server — all three verify with bcryptjs). Anything else is not a
|
||||
* password: '' on accounts created without one, or OTP_ONLY_PASSWORD_HASH.
|
||||
*
|
||||
* Such hashes are refused here, BEFORE bcrypt is called, so the outcome never
|
||||
* depends on how a particular bcrypt build treats a malformed hash. bcryptjs 3
|
||||
* happens to return false today; a future version or a native binding might
|
||||
* throw and turn a wrong password into a 500 (owner ruling 2026-10-07).
|
||||
*/
|
||||
export async function compareLocalHash(password: string, passwordHash: string | null | undefined) {
|
||||
if (typeof passwordHash !== 'string' || !passwordHash.startsWith('$2')) return false;
|
||||
return bcrypt.compare(password, passwordHash);
|
||||
}
|
||||
|
||||
/** True when a stored legacy hash can be carried over as a working password. */
|
||||
export function isPasswordHash(hash: unknown): hash is string {
|
||||
return typeof hash === 'string' && hash.startsWith('$2');
|
||||
}
|
||||
Reference in New Issue
Block a user