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?"
|
// 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.
|
// (#5266). Absent on an older server and on the offline desktop: no.
|
||||||
password_reset_by_email?: boolean;
|
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
|
// 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
|
// 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.
|
// 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.
|
// that never had the field, both correctly offer no downloads.
|
||||||
const clients = ref<Record<string, ClientRelease>>({});
|
const clients = ref<Record<string, ClientRelease>>({});
|
||||||
const passwordResetByEmail = ref(false);
|
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);
|
const loaded = ref(false);
|
||||||
|
|
||||||
async function load(): Promise<void> {
|
async function load(): Promise<void> {
|
||||||
@@ -79,6 +84,7 @@ export const useConfigStore = defineStore("config", () => {
|
|||||||
trashRetentionDays.value = cfg.trash_retention_days ?? 30;
|
trashRetentionDays.value = cfg.trash_retention_days ?? 30;
|
||||||
clients.value = cfg.clients ?? {};
|
clients.value = cfg.clients ?? {};
|
||||||
passwordResetByEmail.value = cfg.password_reset_by_email ?? false;
|
passwordResetByEmail.value = cfg.password_reset_by_email ?? false;
|
||||||
|
minPasswordLength.value = cfg.min_password_length ?? 8;
|
||||||
} catch {
|
} catch {
|
||||||
// Keep defaults if the config endpoint is unreachable.
|
// Keep defaults if the config endpoint is unreachable.
|
||||||
} finally {
|
} finally {
|
||||||
@@ -99,6 +105,7 @@ export const useConfigStore = defineStore("config", () => {
|
|||||||
trashRetentionDays,
|
trashRetentionDays,
|
||||||
clients,
|
clients,
|
||||||
passwordResetByEmail,
|
passwordResetByEmail,
|
||||||
|
minPasswordLength,
|
||||||
loaded,
|
loaded,
|
||||||
load,
|
load,
|
||||||
reload,
|
reload,
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ onMounted(() => {
|
|||||||
type="password"
|
type="password"
|
||||||
label="New password"
|
label="New password"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
placeholder="At least 8 characters"
|
:placeholder="`At least ${config.minPasswordLength} characters`"
|
||||||
:error="passwordError"
|
:error="passwordError"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ const loading = ref(false);
|
|||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
error.value = "";
|
error.value = "";
|
||||||
if (password.value.length < 8) {
|
if (password.value.length < config.minPasswordLength) {
|
||||||
error.value = "Password must be at least 8 characters.";
|
error.value = `Password must be at least ${config.minPasswordLength} characters.`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@@ -80,7 +80,7 @@ async function submit() {
|
|||||||
label="Password"
|
label="Password"
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
placeholder="At least 8 characters"
|
:placeholder="`At least ${config.minPasswordLength} characters`"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<p
|
<p
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ const loading = ref(false);
|
|||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
error.value = "";
|
error.value = "";
|
||||||
if (password.value.length < 8) {
|
if (password.value.length < config.minPasswordLength) {
|
||||||
error.value = "Password must be at least 8 characters.";
|
error.value = `Password must be at least ${config.minPasswordLength} characters.`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (password.value !== confirm.value) {
|
if (password.value !== confirm.value) {
|
||||||
@@ -77,7 +77,7 @@ async function submit() {
|
|||||||
label="New password"
|
label="New password"
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
placeholder="At least 8 characters"
|
:placeholder="`At least ${config.minPasswordLength} characters`"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<BaseInput
|
<BaseInput
|
||||||
|
|||||||
+1
-2
@@ -26,7 +26,7 @@ from .ratelimit import (
|
|||||||
sign_in_by_address,
|
sign_in_by_address,
|
||||||
)
|
)
|
||||||
from .security import dummy_verify, generate_token, hash_password, hash_token, verify_password
|
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
|
from .storage import storage_limit, storage_used
|
||||||
|
|
||||||
bp = Blueprint("auth", __name__, url_prefix="/api/auth")
|
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
|
# 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.
|
# no key and reads as 0, the epoch every account started at.
|
||||||
EPOCH_KEY = "epoch"
|
EPOCH_KEY = "epoch"
|
||||||
MIN_PASSWORD_LEN = 8
|
|
||||||
|
|
||||||
# The first account can only be created this long after the server starts (family
|
# 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
|
# 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.
|
# attachment an admin may allow is a megabyte under it.
|
||||||
MAX_BODY_MB = 64
|
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)
|
@dataclass(frozen=True)
|
||||||
class SettingDef:
|
class SettingDef:
|
||||||
@@ -320,6 +324,7 @@ async def get_public_config(db) -> dict:
|
|||||||
"trash_retention_days": await get_setting(db, "trash_retention_days"),
|
"trash_retention_days": await get_setting(db, "trash_retention_days"),
|
||||||
# Whether the sign-in screen offers "Forgot password?" (#5266).
|
# Whether the sign-in screen offers "Forgot password?" (#5266).
|
||||||
"password_reset_by_email": await mail_configured(db),
|
"password_reset_by_email": await mail_configured(db),
|
||||||
|
"min_password_length": MIN_PASSWORD_LEN,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user