From 4fd5832fdc9f9151025eb11104d7c28cbc0b8d8c Mon Sep 17 00:00:00 2001 From: Lorenzo Venerandi Date: Sat, 7 Mar 2026 17:06:29 +0100 Subject: [PATCH] feat: implement authentication modal and update dashboard password handling --- src/routes/api.py | 8 +- src/templates/jinja2/dashboard/index.html | 3 + .../jinja2/dashboard/partials/auth_modal.html | 39 +++++ src/templates/static/css/dashboard.css | 133 ++++++++++++++++++ src/templates/static/js/dashboard.js | 39 ++++- 5 files changed, 213 insertions(+), 9 deletions(-) create mode 100644 src/templates/jinja2/dashboard/partials/auth_modal.html diff --git a/src/routes/api.py b/src/routes/api.py index 9ef8f0f..e830423 100644 --- a/src/routes/api.py +++ b/src/routes/api.py @@ -6,9 +6,10 @@ Migrated from handler.py dashboard API endpoints. All endpoints are prefixed with the secret dashboard path. """ +import hashlib +import hmac import os import secrets -import hmac from fastapi import APIRouter, Request, Response, Query, Cookie from fastapi.responses import JSONResponse, PlainTextResponse @@ -33,7 +34,7 @@ def _no_cache_headers() -> dict: class AuthRequest(BaseModel): - password: str + fingerprint: str def verify_auth(request: Request) -> bool: @@ -45,7 +46,8 @@ def verify_auth(request: Request) -> bool: @router.post("/api/auth") async def authenticate(request: Request, body: AuthRequest): config = request.app.state.config - if hmac.compare_digest(body.password, config.dashboard_password): + expected = hashlib.sha256(config.dashboard_password.encode()).hexdigest() + if hmac.compare_digest(body.fingerprint, expected): token = secrets.token_hex(32) _auth_tokens.add(token) response = JSONResponse(content={"authenticated": True}) diff --git a/src/templates/jinja2/dashboard/index.html b/src/templates/jinja2/dashboard/index.html index 8fba3e7..9a551dc 100644 --- a/src/templates/jinja2/dashboard/index.html +++ b/src/templates/jinja2/dashboard/index.html @@ -205,5 +205,8 @@ {# Raw request modal - Alpine.js #} {% include "dashboard/partials/raw_request_modal.html" %} + {# Auth modal - Alpine.js #} + {% include "dashboard/partials/auth_modal.html" %} + {% endblock %} diff --git a/src/templates/jinja2/dashboard/partials/auth_modal.html b/src/templates/jinja2/dashboard/partials/auth_modal.html new file mode 100644 index 0000000..af7ed8c --- /dev/null +++ b/src/templates/jinja2/dashboard/partials/auth_modal.html @@ -0,0 +1,39 @@ +{# Authentication modal - Alpine.js controlled #} +
+
+
+
+ + + +

Authentication Required

+
+ × +
+
+

Enter the dashboard password to access protected panels.

+
+ +

+
+ +
+
+
diff --git a/src/templates/static/css/dashboard.css b/src/templates/static/css/dashboard.css index 98d1472..3b3c53b 100644 --- a/src/templates/static/css/dashboard.css +++ b/src/templates/static/css/dashboard.css @@ -1168,6 +1168,139 @@ tbody { opacity: 1; } +/* Auth Modal */ +.auth-modal { + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(4px); +} +.auth-modal-content { + background-color: #161b22; + border: 1px solid #30363d; + border-radius: 12px; + width: 400px; + max-width: 90vw; + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.6); + animation: authModalIn 0.2s ease-out; +} +@keyframes authModalIn { + from { opacity: 0; transform: scale(0.95) translateY(-10px); } + to { opacity: 1; transform: scale(1) translateY(0); } +} +.auth-modal-header { + padding: 20px 24px 16px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #30363d; +} +.auth-modal-title { + display: flex; + align-items: center; + gap: 10px; + color: #58a6ff; +} +.auth-modal-title h3 { + margin: 0; + font-size: 16px; + font-weight: 600; + color: #e6edf3; +} +.auth-modal-close { + color: #8b949e; + font-size: 24px; + font-weight: bold; + cursor: pointer; + line-height: 20px; + transition: color 0.2s; +} +.auth-modal-close:hover { + color: #c9d1d9; +} +.auth-modal-body { + padding: 24px; +} +.auth-modal-description { + margin: 0 0 20px; + color: #8b949e; + font-size: 14px; + line-height: 1.5; +} +.auth-modal-input-group { + margin-bottom: 20px; +} +.auth-modal-input { + width: 100%; + padding: 10px 14px; + background: #0d1117; + border: 1px solid #30363d; + border-radius: 6px; + color: #e6edf3; + font-size: 14px; + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; + box-sizing: border-box; +} +.auth-modal-input:focus { + border-color: #58a6ff; + box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.15); +} +.auth-modal-input-error { + border-color: #f85149; +} +.auth-modal-input-error:focus { + border-color: #f85149; + box-shadow: 0 0 0 3px rgba(248, 81, 73, 0.15); +} +.auth-modal-error { + margin: 8px 0 0; + color: #f85149; + font-size: 13px; +} +.auth-modal-footer { + display: flex; + justify-content: flex-end; + gap: 10px; +} +.auth-modal-btn { + padding: 8px 20px; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + border: 1px solid transparent; + transition: all 0.2s; +} +.auth-modal-btn-cancel { + background: transparent; + border-color: #30363d; + color: #8b949e; +} +.auth-modal-btn-cancel:hover { + background: #21262d; + color: #c9d1d9; +} +.auth-modal-btn-submit { + background: #238636; + color: #fff; + border-color: rgba(240, 246, 252, 0.1); +} +.auth-modal-btn-submit:hover { + background: #2ea043; +} +.auth-modal-btn-submit:disabled { + opacity: 0.6; + cursor: not-allowed; +} + /* Attack Types Cell Styling */ .attack-types-cell { max-width: 280px; diff --git a/src/templates/static/js/dashboard.js b/src/templates/static/js/dashboard.js index c1b12d5..719d9dd 100644 --- a/src/templates/static/js/dashboard.js +++ b/src/templates/static/js/dashboard.js @@ -22,6 +22,7 @@ document.addEventListener('alpine:init', () => { // Auth state (UI only — actual security enforced server-side via cookie) authenticated: false, + authModal: { show: false, password: '', error: '', loading: false }, async init() { // Check if already authenticated (cookie-based) @@ -97,24 +98,50 @@ document.addEventListener('alpine:init', () => { if (this.tab === 'admin') this.switchToOverview(); }, - async promptAuth() { - const password = prompt('Enter dashboard password:'); - if (!password) return; + promptAuth() { + this.authModal = { show: true, password: '', error: '', loading: false }; + this.$nextTick(() => { + if (this.$refs.authPasswordInput) this.$refs.authPasswordInput.focus(); + }); + }, + + closeAuthModal() { + this.authModal.show = false; + this.authModal.password = ''; + this.authModal.error = ''; + this.authModal.loading = false; + }, + + async submitAuth() { + const password = this.authModal.password; + if (!password) { + this.authModal.error = 'Please enter a password'; + return; + } + this.authModal.error = ''; + this.authModal.loading = true; try { + const msgBuf = new TextEncoder().encode(password); + const hashBuf = await crypto.subtle.digest('SHA-256', msgBuf); + const fingerprint = Array.from(new Uint8Array(hashBuf)) + .map(b => b.toString(16).padStart(2, '0')).join(''); const resp = await fetch(`${this.dashboardPath}/api/auth`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', - body: JSON.stringify({ password }), + body: JSON.stringify({ fingerprint }), }); if (resp.ok) { this.authenticated = true; + this.closeAuthModal(); this.switchToAdmin(); } else { - alert('Invalid password'); + this.authModal.error = 'Invalid password'; + this.authModal.loading = false; } } catch { - alert('Authentication failed'); + this.authModal.error = 'Authentication failed'; + this.authModal.loading = false; } },