fix(auth): centralize password and username validation constants (#4120)

Added PASSWORD_MIN_LENGTH and RESERVED_USERNAMES to src/constants.py as the
single source of truth. Previously PASSWORD_MIN_LENGTH was hardcoded as 8 in
four route handlers and all three JS validation paths; RESERVED_USERNAMES was
an inline frozenset duplicated in core/auth.py, routes/assistant_routes.py,
routes/research_routes.py, and src/task_scheduler.py.

Added GET /api/auth/policy (unauthenticated) so the frontend reads the real
values from the server instead of hardcoding them in JS.

Added missing empty-username guard to /setup and admin POST /users. Both
returned a misleading 500/409 on whitespace-only input. /signup already had the
check; this makes all three consistent.
This commit is contained in:
Karl Jussila
2026-06-16 02:52:15 -05:00
committed by GitHub
parent 2b519bf355
commit ee72d71872
12 changed files with 327 additions and 30 deletions
+12 -1
View File
@@ -13,6 +13,7 @@ let modalEl = null;
// the endpoints list can flash a glow on that row. Cleared once the
// animation fires.
let _recentlyAddedEpId = null;
let _authPolicy = { password_min_length: 8, reserved_usernames: [] };
function el(id) { return document.getElementById(id); }
function esc(s) { return uiModule.esc(s); }
@@ -343,6 +344,15 @@ function initSignupToggle() {
}
function initAddUser() {
fetch('/api/auth/policy', { credentials: 'same-origin' })
.then(r => r.ok ? r.json() : null)
.then(policy => {
if (!policy) return;
_authPolicy = policy;
const admPw = el('adm-newPassword');
if (admPw) admPw.placeholder = `Password (min ${policy.password_min_length})`;
})
.catch(() => {});
el('adm-addBtn').addEventListener('click', async () => {
const msg = el('adm-addMsg');
msg.textContent = ''; msg.className = '';
@@ -350,7 +360,8 @@ function initAddUser() {
const password = el('adm-newPassword').value;
const is_admin = el('adm-newIsAdmin').checked;
if (!username) { msg.textContent = 'Username required'; msg.className = 'admin-error'; return; }
if (password.length < 8) { msg.textContent = 'Password must be at least 8 characters'; msg.className = 'admin-error'; return; }
if (password.length < _authPolicy.password_min_length) { msg.textContent = `Password must be at least ${_authPolicy.password_min_length} characters`; msg.className = 'admin-error'; return; }
if (_authPolicy.reserved_usernames.includes(username.toLowerCase())) { msg.textContent = 'This username is reserved'; msg.className = 'admin-error'; return; }
el('adm-addBtn').disabled = true;
try {
const res = await fetch('/api/auth/users', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password, is_admin }) });
+12 -1
View File
@@ -11,6 +11,7 @@ import { isAltGrEvent } from './platform.js';
let initialized = false;
let modalEl = null;
let _authPolicy = { password_min_length: 8 };
function el(id) { return document.getElementById(id); }
function esc(s) { return uiModule.esc(s); }
@@ -2160,6 +2161,16 @@ function initAccount() {
}
}).catch(() => {});
// Update password placeholder and policy from server
fetch('/api/auth/policy', { credentials: 'same-origin' })
.then(r => r.ok ? r.json() : null)
.then(policy => {
if (!policy) return;
_authPolicy = policy;
const pwNew = el('settings-pw-new');
if (pwNew) pwNew.placeholder = `New password (min ${policy.password_min_length})`;
}).catch(() => {});
// Change password
const saveBtn = el('settings-pw-save');
const msgEl = el('settings-pw-msg');
@@ -2170,7 +2181,7 @@ function initAccount() {
const conf = el('settings-pw-confirm').value;
msgEl.style.color = '';
if (!cur || !nw) { msgEl.textContent = 'Fill in all fields'; msgEl.style.color = 'var(--red)'; return; }
if (nw.length < 8) { msgEl.textContent = 'Min 8 characters'; msgEl.style.color = 'var(--red)'; return; }
if (nw.length < _authPolicy.password_min_length) { msgEl.textContent = `Min ${_authPolicy.password_min_length} characters`; msgEl.style.color = 'var(--red)'; return; }
if (nw !== conf) { msgEl.textContent = 'Passwords don\'t match'; msgEl.style.color = 'var(--red)'; return; }
saveBtn.disabled = true;
try {