refactor(auth): single digest registry, extracted 2FA and login-log, dep bumps

- password.ts: derive plain and salted digest detection from one DIGEST_SCHEMES
  table instead of parallel hardcoded lists, so adding a family is one row.
- auth.ts: move 2FA challenge verification into twofactor-verification.ts and
  the website login-log insert into website-login-log.ts, slimming the
  NextAuth provider to orchestration only.
- deps: bump @formatjs/icu-messageformat-parser, @tanstack/react-query, jszip,
  lucide-react, motion (patch/minor only). @types/react stay pinned per
  pnpm-workspace.yaml; next-auth is already at the newest available (v5 beta).
This commit is contained in:
openhands
2026-09-17 15:26:25 +02:00
parent 5d7c9fccdc
commit 2e25b39364
6 changed files with 157 additions and 122 deletions
+18 -13
View File
@@ -44,6 +44,21 @@ async function isHexDigestOf(
return (await hashFn(password)) === stored.toLowerCase();
}
/**
* Single source of truth for the supported unsalted digest families. Both the
* plain single-digest checks and the salted detection below derive from here,
* so adding a family (e.g. sha384) means adding a single row.
*/
const DIGEST_SCHEMES: ReadonlyArray<{
length: number;
hash: (input: string) => Promise<string>;
}> = [
{ length: 32, hash: md5Hex },
{ length: 40, hash: sha1Hex },
{ length: 64, hash: sha256Hex },
{ length: 128, hash: sha512Hex },
];
export async function isMd5Of(
password: string,
stored: string,
@@ -72,16 +87,6 @@ export async function isSha512Of(
return isHexDigestOf(password, stored, 128, sha512Hex);
}
const DIGEST_HEX_LENGTHS: ReadonlyArray<{
length: number;
hash: (input: string) => Promise<string>;
}> = [
{ length: 32, hash: md5Hex },
{ length: 40, hash: sha1Hex },
{ length: 64, hash: sha256Hex },
{ length: 128, hash: sha512Hex },
];
/**
* Combined/double digest conventions used by legacy CMSes and forums, e.g.
* md5(md5(pass)) (UberCMS/Butterfly), md5(sha1(pass)), sha1(md5(pass)),
@@ -133,7 +138,7 @@ export async function isSaltedDigestOf(
): Promise<boolean> {
if (!SALTED_HASH_DELIMITER_RE.test(stored)) return false;
for (const { length, hash: hashFn } of DIGEST_HEX_LENGTHS) {
for (const { length, hash: hashFn } of DIGEST_SCHEMES) {
const hashFirst = stored.match(
new RegExp(`^([a-f0-9]{${length}})[:\$@_](.{1,64})$`, "i"),
);
@@ -225,8 +230,8 @@ export async function checkLogin(
}
// Legacy digest formats (hex) — verify + auto-upgrade to bcrypt.
for (const check of [isMd5Of, isSha1Of, isSha256Of, isSha512Of]) {
if (await check(password, stored)) {
for (const { length, hash: hashFn } of DIGEST_SCHEMES) {
if (await isHexDigestOf(password, stored, length, hashFn)) {
return { valid: true, upgradedHash: await hashPassword(password) };
}
}