From e20a6d0830835da86b7ad601a472afd646478810 Mon Sep 17 00:00:00 2001 From: Giorgio Ravera Date: Thu, 12 Mar 2026 19:08:47 +0100 Subject: [PATCH] Improved login json --- backend/routes/login.py | 48 ++++++++++++++++++++++------ frontend/js/login.js | 71 ++++++++++++++++++----------------------- 2 files changed, 70 insertions(+), 49 deletions(-) diff --git a/backend/routes/login.py b/backend/routes/login.py index d454b4a..feda767 100644 --- a/backend/routes/login.py +++ b/backend/routes/login.py @@ -27,9 +27,11 @@ def check_rate_limit(ip: str): if len(attempts) >= int(get_config("login_max_attempts")): raise HTTPException( - status_code=status.HTTP_409_CONFLICT, + status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail={ - "error": "Too many login attempts" + "code": "LOGIN_ERROR", + "status": "failure", + "message": "Too many login attempts" }, ) @@ -56,9 +58,15 @@ def css_login(): return FileResponse(os.path.join(settings.FRONTEND_DIR, "js/session.js")) # --------------------------------------------------------- -# API ENDPOINTS +# Login API # --------------------------------------------------------- -@router.post("/api/login") +@router.post("/api/login", status_code=status.HTTP_200_OK, responses={ + 200: {"description": "Login successful"}, + 401: {"description": "Invalid credentials"}, + 403: {"description": "Account locked"}, # GRGR TBD + 429: {"description": "Too many login attempts"}, + 500: {"description": "Internal server error"}, +}) def api_login(request: Request, data: dict, response: Response): ip = request.client.host check_rate_limit(ip) @@ -71,11 +79,33 @@ def api_login(request: Request, data: dict, response: Response): login_attempts.pop(ip, None) apply_session(response, username=user) - return {"status": "ok"} + response.status_code = status.HTTP_200_OK + return { + "code": "LOGIN_SUCCESS", + "status": "success", + "message": "Login successful", + } + + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail={ + "code": "LOGIN_ERROR", + "status": "failure", + "message": "Invalid credentials", + }, + ) - return {"error": "Wrong credentials"} - -@router.post("/api/logout") +# --------------------------------------------------------- +# Logout API +# --------------------------------------------------------- +@router.post("/api/logout", status_code=status.HTTP_200_OK, responses={ + 200: {"description": "Logout successful"} +}) def api_logout(response: Response): close_session(response) - return {"status": "logged_out"} + response.status_code = status.HTTP_200_OK + return { + "code": "LOGOUT_SUCCESS", + "status": "success", + "message": "Logout successful", + } diff --git a/frontend/js/login.js b/frontend/js/login.js index 0985646..bf26dee 100644 --- a/frontend/js/login.js +++ b/frontend/js/login.js @@ -74,7 +74,7 @@ document.addEventListener('DOMContentLoaded', () => { // Disabilita UI + spinner const originalBtnHTML = btn.innerHTML; - if(btn) { + if(btn) { btn.disabled = true; btn.setAttribute('aria-busy', 'true'); btn.innerHTML = ` @@ -83,7 +83,6 @@ document.addEventListener('DOMContentLoaded', () => { `; } - try { const fetchPromise = fetch('/api/login', { method: 'POST', @@ -93,60 +92,52 @@ document.addEventListener('DOMContentLoaded', () => { signal: inFlightController.signal }); - // error after 5s? // await Promise.race([ fetchPromise, timeout(5000, inFlightController.signal) ]); const res = await fetchPromise; - // Parse JSON resiliente - let data = {}; + // Check content-type to avoid parsing errors + const contentType = res.headers.get("content-type") || ""; + if (!contentType.includes("application/json")) { + const err = new Error(`${res.status}: ${res.statusText}`); + err.status = res.status; + throw err; + } + + // Check JSON + let data; try { data = await res.json(); } catch { - // Se il server non risponde con JSON valido - data = { status: 'error', error: 'Risposta non valida dal server.' }; + throw new Error('Invalid JSON payload'); } - // Gestione HTTP non-OK come errore + // Check JSON errors if (!res.ok) { - // Usa messaggio server se presente, altrimenti status - const errMsg = (typeof data?.error === 'string' && data.error.trim()) - ? data.error.trim() - : `Errore ${res.status} durante il login.`; - throw new Error(errMsg); + const serverMsg = data?.detail?.message?.trim(); + const base = `Error during login`; + const err = new Error(serverMsg ? `${base}: ${serverMsg}` : base); + err.status = res.status; + throw err; } - // Gestione logica dell'API - if (data?.status === 'ok') { - // Redirect alla pagina host - location.assign('/hosts'); - return; - } else { - const msg = (typeof data?.error === 'string' && data.error.trim()) - ? data.error.trim() - : 'Credenziali non valide.'; - // Credenziali errate -> metti focus su username, marca entrambi - showError(msg, { focus: 'username', markUser: true, markPass: true }); - return; - } + // Login Success + // Redirect to main page (hosts) + location.assign('/hosts'); + return; + } catch (err) { - // Se è stato abortito, non mostrare errori - //if (inFlightController?.signal.aborted) return; - // Errori di rete o eccezioni - const msg = err?.message || 'Errore di connessione. Riprova.'; - // Se è un timeout personalizzato, messaggio ad hoc - if (msg === 'Timeout di rete') { - showError('Il server non risponde. Riprova tra poco.', { focus: 'username' }); - } else { - showError(msg, { focus: 'username' }); - } + // Se è stato abortito, non mostrare errori + //if (inFlightController?.signal.aborted) return; + // Errori di rete o eccezioni + showError(err?.message || 'Connection error, please retry.', { focus: 'username', markUser: true, markPass: true }); } finally { // Ripristina bottone if(btn) { - btn.disabled = false; - btn.removeAttribute('aria-busy'); - btn.innerHTML = originalBtnHTML ?? 'Entra'; - } + btn.disabled = false; + btn.removeAttribute('aria-busy'); + btn.innerHTML = originalBtnHTML ?? 'Entra'; + } // controller consumato inFlightController = null; } -- 2.47.3