From 8b349b0341b8cde518558c8ef83523263a95abae Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Sat, 13 Jun 2026 22:12:38 +0800 Subject: [PATCH] feat: make password policy configurable via admin settings --- apps/api/src/lib/settings-helpers.ts | 18 ++++++++++++++++ apps/api/src/plugins/auth.ts | 31 ++++++++++++++++++---------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/apps/api/src/lib/settings-helpers.ts b/apps/api/src/lib/settings-helpers.ts index 5e7c394f..3f89b07c 100644 --- a/apps/api/src/lib/settings-helpers.ts +++ b/apps/api/src/lib/settings-helpers.ts @@ -21,3 +21,21 @@ export async function getSettingNumber(key: string, defaultValue = 0): Promise { + try { + const result = await db + .select({ value: schema.settings.value }) + .from(schema.settings) + .where(eq(schema.settings.key, key)) + .limit(1); + if (result.length > 0) return result[0].value; + } catch { + /* DB not ready or key absent -- fall through */ + } + return defaultValue; +} diff --git a/apps/api/src/plugins/auth.ts b/apps/api/src/plugins/auth.ts index 487e4e8f..b564d852 100644 --- a/apps/api/src/plugins/auth.ts +++ b/apps/api/src/plugins/auth.ts @@ -7,7 +7,7 @@ import { env } from "../config.js"; import { db, schema } from "../db/index.js"; import { sharedRedis } from "../jobs/connection.js"; import { auditFromRequest, sanitizeAuditInput } from "../lib/audit.js"; -import { getSettingNumber } from "../lib/settings-helpers.js"; +import { getSettingNumber, getSettingString } from "../lib/settings-helpers.js"; import { getPermissions, requirePermission } from "../permissions.js"; const scryptAsync = promisify(scrypt); @@ -51,14 +51,23 @@ export function computeKeyPrefix(rawKey: string): string { return createHash("sha256").update(rawKey).digest("hex").slice(0, 16); } -const PASSWORD_RULES = - "Password must be at least 8 characters with uppercase, lowercase, and a number"; +async function validatePasswordStrength(password: string): Promise { + const minLength = await getSettingNumber("passwordMinLength", 8); + if (password.length < minLength) return `Password must be at least ${minLength} characters`; + + const requireUpper = await getSettingString("passwordRequireUppercase", "true"); + const requireLower = await getSettingString("passwordRequireLowercase", "true"); + const requireDigit = await getSettingString("passwordRequireDigit", "true"); + const requireSpecial = await getSettingString("passwordRequireSpecial", "false"); + + if (requireUpper === "true" && !/[A-Z]/.test(password)) + return "Password must contain an uppercase letter"; + if (requireLower === "true" && !/[a-z]/.test(password)) + return "Password must contain a lowercase letter"; + if (requireDigit === "true" && !/\d/.test(password)) return "Password must contain a digit"; + if (requireSpecial === "true" && !/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password)) + return "Password must contain a special character"; -function validatePasswordStrength(password: string): string | null { - if (password.length < 8) return PASSWORD_RULES; - if (!/[A-Z]/.test(password)) return PASSWORD_RULES; - if (!/[a-z]/.test(password)) return PASSWORD_RULES; - if (!/[0-9]/.test(password)) return PASSWORD_RULES; return null; } @@ -475,7 +484,7 @@ export async function authRoutes(app: FastifyInstance): Promise { } const body = parsed.data; - const pwError = validatePasswordStrength(body.newPassword); + const pwError = await validatePasswordStrength(body.newPassword); if (pwError) { return reply.status(400).send({ error: pwError, @@ -590,7 +599,7 @@ export async function authRoutes(app: FastifyInstance): Promise { }); } - const registerPwError = validatePasswordStrength(body.password); + const registerPwError = await validatePasswordStrength(body.password); if (registerPwError) { return reply.status(400).send({ error: registerPwError, @@ -832,7 +841,7 @@ export async function authRoutes(app: FastifyInstance): Promise { } const body = parsed.data; - const pwError = validatePasswordStrength(body.newPassword); + const pwError = await validatePasswordStrength(body.newPassword); if (pwError) { return reply.status(400).send({ error: pwError,