feat(auth): make password length configurable via env (min 5–30, max 30–128)

Add PASSWORD_MIN_LENGTH / PASSWORD_MAX_LENGTH (validated in src/env.ts) and a
shared passwordPolicy helper. The policy drives the zod schemas in register,
password reset and the admin create-user dialog, plus the live requirement
checks and strength meter in the register form.

Locale messages now interpolate the configured minimum via {min} instead of
hard-coding 12.
This commit is contained in:
openhands committed 2026-10-18 19:20:38 +02:00
1 parent aef0939ee9
commit 9cf1eb615c
36 files changed
+227 -143

No files matched your search

+6
View File
@@ -28,6 +28,8 @@ export interface AuthFieldProps {
| "search";
required?: boolean;
disabled?: boolean;
minLength?: number;
maxLength?: number;
/** Marks the field with `aria-invalid` and a danger border. */
invalid?: boolean;
/** id of the element describing this field (e.g. an error banner). */
@@ -61,6 +63,8 @@ export function AuthField({
inputMode,
required,
disabled,
minLength,
maxLength,
invalid,
describedBy,
iconColor = "var(--color-text-muted)",
@@ -93,6 +97,8 @@ export function AuthField({
inputMode={inputMode}
required={required}
disabled={disabled}
minLength={minLength}
maxLength={maxLength}
aria-invalid={invalid ? true : undefined}
aria-describedby={describedBy}
className={`auth-input has-icon ${trailing ? "has-action" : ""}`}
+18 -5
View File
@@ -30,19 +30,23 @@ interface RegisterFormProps {
captcha: { provider: string; siteKey?: string };
error?: string;
nonce?: string;
/** Effective policy from `PASSWORD_MIN_LENGTH` / `PASSWORD_MAX_LENGTH`. */
passwordMinLength: number;
passwordMaxLength: number;
}
function passwordStrength(
pw: string,
t: (key: string) => string,
minLength: number,
): {
score: number;
label: string;
color: string;
} {
let score = 0;
if (pw.length >= 8) score += 1;
if (pw.length >= 12) score += 1;
if (pw.length >= minLength) score += 1;
if (pw.length >= minLength + 4) score += 1;
if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) score += 1;
if (/\d/.test(pw)) score += 1;
if (/[^a-zA-Z0-9]/.test(pw)) score += 1;
@@ -103,6 +107,8 @@ export function RegisterForm({
captcha,
error,
nonce,
passwordMinLength,
passwordMaxLength,
}: RegisterFormProps) {
const t = useTranslations("pages.register");
const showCaptcha = captcha.provider !== "none" && !!captcha.siteKey;
@@ -138,7 +144,7 @@ export function RegisterForm({
);
const autoLoginStartedRef = useRef(false);
const [referralCode, setReferralCode] = useState("");
const strength = passwordStrength(password, t);
const strength = passwordStrength(password, t, passwordMinLength);
/**
* The password policy the server enforces, evaluated live so the visitor can
@@ -146,7 +152,10 @@ export function RegisterForm({
* single error back.
*/
const requirements = [
{ label: t("passwordMinLength"), met: password.length >= 12 },
{
label: t("passwordMinLength", { min: passwordMinLength }),
met: password.length >= passwordMinLength,
},
{ label: t("passwordUpper"), met: /[A-Z]/.test(password) },
{ label: t("passwordLower"), met: /[a-z]/.test(password) },
{ label: t("passwordDigit"), met: /\d/.test(password) },
@@ -295,9 +304,13 @@ export function RegisterForm({
type={showPassword ? "text" : "password"}
value={password}
onChange={setPassword}
placeholder={t("passwordPlaceholder")}
placeholder={t("passwordPlaceholder", {
min: passwordMinLength,
})}
autoComplete="new-password"
required
minLength={passwordMinLength}
maxLength={passwordMaxLength}
trailing={
<PasswordToggle
show={showPassword}