The web's password forms read the server's minimum length
The server's MIN_PASSWORD_LEN was 8, and the web wrote 8 out five times: two checks and three placeholders. The constant moves beside the other policy numbers in settings.py (auth.py imports it), /api/config serves it as min_password_length, and the config store hands it to Register, Reset and Account. 8 stays only as the fallback until the config answers. DRY pass #2, batch 3 (#5372). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,9 @@ export interface PublicConfig {
|
||||
// Whether this server can email a reset link, so sign-in offers "Forgot password?"
|
||||
// (#5266). Absent on an older server and on the offline desktop: no.
|
||||
password_reset_by_email?: boolean;
|
||||
// The shortest password an account may have (the server's MIN_PASSWORD_LEN).
|
||||
// Absent on an older server and on the offline desktop, which has no accounts.
|
||||
min_password_length?: number;
|
||||
// Every client this server holds, keyed by platform id. Absent on a server that
|
||||
// holds none, and absent on the desktop's own offline config — the Tauri build
|
||||
// answers `config_get` locally and has no clients to hand out.
|
||||
@@ -66,6 +69,8 @@ export const useConfigStore = defineStore("config", () => {
|
||||
// that never had the field, both correctly offer no downloads.
|
||||
const clients = ref<Record<string, ClientRelease>>({});
|
||||
const passwordResetByEmail = ref(false);
|
||||
// The server's own number; 8 only until /api/config answers, or if it never does.
|
||||
const minPasswordLength = ref(8);
|
||||
const loaded = ref(false);
|
||||
|
||||
async function load(): Promise<void> {
|
||||
@@ -79,6 +84,7 @@ export const useConfigStore = defineStore("config", () => {
|
||||
trashRetentionDays.value = cfg.trash_retention_days ?? 30;
|
||||
clients.value = cfg.clients ?? {};
|
||||
passwordResetByEmail.value = cfg.password_reset_by_email ?? false;
|
||||
minPasswordLength.value = cfg.min_password_length ?? 8;
|
||||
} catch {
|
||||
// Keep defaults if the config endpoint is unreachable.
|
||||
} finally {
|
||||
@@ -99,6 +105,7 @@ export const useConfigStore = defineStore("config", () => {
|
||||
trashRetentionDays,
|
||||
clients,
|
||||
passwordResetByEmail,
|
||||
minPasswordLength,
|
||||
loaded,
|
||||
load,
|
||||
reload,
|
||||
|
||||
@@ -264,7 +264,7 @@ onMounted(() => {
|
||||
type="password"
|
||||
label="New password"
|
||||
autocomplete="new-password"
|
||||
placeholder="At least 8 characters"
|
||||
:placeholder="`At least ${config.minPasswordLength} characters`"
|
||||
:error="passwordError"
|
||||
required
|
||||
/>
|
||||
|
||||
@@ -24,8 +24,8 @@ const loading = ref(false);
|
||||
|
||||
async function submit() {
|
||||
error.value = "";
|
||||
if (password.value.length < 8) {
|
||||
error.value = "Password must be at least 8 characters.";
|
||||
if (password.value.length < config.minPasswordLength) {
|
||||
error.value = `Password must be at least ${config.minPasswordLength} characters.`;
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
@@ -80,7 +80,7 @@ async function submit() {
|
||||
label="Password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
placeholder="At least 8 characters"
|
||||
:placeholder="`At least ${config.minPasswordLength} characters`"
|
||||
required
|
||||
/>
|
||||
<p
|
||||
|
||||
@@ -25,8 +25,8 @@ const loading = ref(false);
|
||||
|
||||
async function submit() {
|
||||
error.value = "";
|
||||
if (password.value.length < 8) {
|
||||
error.value = "Password must be at least 8 characters.";
|
||||
if (password.value.length < config.minPasswordLength) {
|
||||
error.value = `Password must be at least ${config.minPasswordLength} characters.`;
|
||||
return;
|
||||
}
|
||||
if (password.value !== confirm.value) {
|
||||
@@ -77,7 +77,7 @@ async function submit() {
|
||||
label="New password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
placeholder="At least 8 characters"
|
||||
:placeholder="`At least ${config.minPasswordLength} characters`"
|
||||
required
|
||||
/>
|
||||
<BaseInput
|
||||
|
||||
+1
-2
@@ -26,7 +26,7 @@ from .ratelimit import (
|
||||
sign_in_by_address,
|
||||
)
|
||||
from .security import dummy_verify, generate_token, hash_password, hash_token, verify_password
|
||||
from .settings import get_setting, mail_configured, set_settings
|
||||
from .settings import MIN_PASSWORD_LEN, get_setting, mail_configured, set_settings
|
||||
from .storage import storage_limit, storage_used
|
||||
|
||||
bp = Blueprint("auth", __name__, url_prefix="/api/auth")
|
||||
@@ -46,7 +46,6 @@ SESSION_KEY = "user_id"
|
||||
# signed cookies in other people's browsers. A cookie from before epochs existed has
|
||||
# no key and reads as 0, the epoch every account started at.
|
||||
EPOCH_KEY = "epoch"
|
||||
MIN_PASSWORD_LEN = 8
|
||||
|
||||
# The first account can only be created this long after the server starts (family
|
||||
# idea #5105, practice 8). Without it, a fresh server on a public address belongs to
|
||||
|
||||
@@ -16,6 +16,10 @@ SettingType = Literal["string", "bool", "int"]
|
||||
# attachment an admin may allow is a megabyte under it.
|
||||
MAX_BODY_MB = 64
|
||||
|
||||
# The shortest password an account may have. Enforced by auth.py, and served in the
|
||||
# public config so the web's forms say the same number before a round-trip does.
|
||||
MIN_PASSWORD_LEN = 8
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SettingDef:
|
||||
@@ -320,6 +324,7 @@ async def get_public_config(db) -> dict:
|
||||
"trash_retention_days": await get_setting(db, "trash_retention_days"),
|
||||
# Whether the sign-in screen offers "Forgot password?" (#5266).
|
||||
"password_reset_by_email": await mail_configured(db),
|
||||
"min_password_length": MIN_PASSWORD_LEN,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user