feat: implement authentication modal and update dashboard password handling

This commit is contained in:
Lorenzo Venerandi
2026-03-07 17:06:29 +01:00
parent 513a5beccd
commit 4fd5832fdc
5 changed files with 213 additions and 9 deletions

View File

@@ -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})

View File

@@ -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" %}
</div>
{% endblock %}

View File

@@ -0,0 +1,39 @@
{# Authentication modal - Alpine.js controlled #}
<div class="auth-modal"
x-show="authModal.show"
x-cloak
@click.self="closeAuthModal()"
@keydown.escape.window="authModal.show && closeAuthModal()"
>
<div class="auth-modal-content">
<div class="auth-modal-header">
<div class="auth-modal-title">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="20" height="20" fill="currentColor">
<path d="M4 4a4 4 0 0 1 8 0v2h.25c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 12.25 15h-8.5A1.75 1.75 0 0 1 2 13.25v-5.5C2 6.784 2.784 6 3.75 6H4Zm8.25 3.5h-8.5a.25.25 0 0 0-.25.25v5.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25ZM10.5 6V4a2.5 2.5 0 1 0-5 0v2Z"/>
</svg>
<h3>Authentication Required</h3>
</div>
<span class="auth-modal-close" @click="closeAuthModal()">&times;</span>
</div>
<form class="auth-modal-body" @submit.prevent="submitAuth()">
<p class="auth-modal-description">Enter the dashboard password to access protected panels.</p>
<div class="auth-modal-input-group">
<input type="password"
class="auth-modal-input"
:class="{ 'auth-modal-input-error': authModal.error }"
x-model="authModal.password"
x-ref="authPasswordInput"
placeholder="Password"
autocomplete="off" />
<p class="auth-modal-error" x-show="authModal.error" x-text="authModal.error" x-cloak></p>
</div>
<div class="auth-modal-footer">
<button type="button" class="auth-modal-btn auth-modal-btn-cancel" @click="closeAuthModal()">Cancel</button>
<button type="submit" class="auth-modal-btn auth-modal-btn-submit" :disabled="authModal.loading">
<span x-show="!authModal.loading">Unlock</span>
<span x-show="authModal.loading" x-cloak>Verifying...</span>
</button>
</div>
</form>
</div>
</div>

View File

@@ -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;

View File

@@ -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;
}
},