feat: login écran + clé API utilisateur + affichage nom connecté
- Route POST /api/auth/validate (backend) : valide la clé contre Imago
- AppState supporte clé dynamique (X-Api-Key header utilisateur)
- Écran de login overlay avec champ clé API + validation
- Nom d'utilisateur affiché dans le header (👤 bruno) avec bouton logout
- Stockage clé + nom dans localStorage (persiste entre sessions)
- Tous les appels API incluent X-Api-Key du frontend
This commit is contained in:
+159
-4
@@ -118,6 +118,49 @@
|
||||
}
|
||||
.toast.success { border-left: 3px solid var(--green); }
|
||||
.toast.error { border-left: 3px solid var(--red); }
|
||||
|
||||
/* ─── Login Overlay ─── */
|
||||
.login-overlay {
|
||||
position: fixed; inset: 0; z-index: 500;
|
||||
background: var(--bg);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.login-box {
|
||||
background: var(--bg-card); border: 1px solid var(--border);
|
||||
border-radius: 12px; padding: 32px;
|
||||
max-width: 420px; width: 90%;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.login-box h2 { font-size: 22px; margin-bottom: 4px; color: var(--accent); }
|
||||
.login-box .sub { font-size: 13px; color: var(--fg-dim); margin-bottom: 20px; }
|
||||
.login-box label { display: block; font-size: 13px; margin-bottom: 6px; color: var(--fg-dim); }
|
||||
.login-box input[type="password"] {
|
||||
width: 100%; padding: 10px 14px; border-radius: var(--radius);
|
||||
background: var(--bg); border: 1px solid var(--border);
|
||||
color: var(--fg); font-size: 14px; outline: none; margin-bottom: 12px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.login-box input:focus { border-color: var(--accent); }
|
||||
.login-box .login-error { color: var(--red); font-size: 13px; margin-bottom: 10px; display: none; }
|
||||
.login-box .btn-full { width: 100%; justify-content: center; padding: 10px; font-size: 14px; }
|
||||
.login-box .login-hint { font-size: 11px; color: var(--fg-dim); margin-top: 14px; text-align: center; }
|
||||
.login-box .login-hint a { color: var(--accent); }
|
||||
|
||||
/* ─── User Info ─── */
|
||||
.user-info {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
font-size: 13px; color: var(--fg);
|
||||
background: var(--bg-card); border: 1px solid var(--border);
|
||||
border-radius: var(--radius); padding: 5px 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.user-info .user-avatar { font-size: 16px; }
|
||||
.user-info .user-name { font-weight: 600; }
|
||||
.user-info .btn-logout {
|
||||
background: none; border: none; color: var(--fg-dim); cursor: pointer;
|
||||
font-size: 14px; padding: 2px 4px; margin-left: 4px;
|
||||
}
|
||||
.user-info .btn-logout:hover { color: var(--red); }
|
||||
.toast.info { border-left: 3px solid var(--accent); }
|
||||
.toast-msg { flex: 1; }
|
||||
.toast-close {
|
||||
@@ -243,6 +286,22 @@
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ─── Login Overlay ─── -->
|
||||
<div class="login-overlay" id="login-overlay">
|
||||
<div class="login-box">
|
||||
<h2>🔐 Connexion Imago</h2>
|
||||
<div class="sub">Entrez votre clé API pour accéder au dashboard</div>
|
||||
<label for="api-key-input">Clé API</label>
|
||||
<input type="password" id="api-key-input" placeholder="Pastez votre clé API Imago…" autocomplete="off" onkeydown="if(event.key==='Enter')connect()">
|
||||
<div class="login-error" id="login-error"></div>
|
||||
<button class="btn accent btn-full" id="login-btn" onclick="connect()">🔗 Se connecter</button>
|
||||
<div class="login-hint">
|
||||
Créez une clé dans <a href="http://dev.lab.home:3001" target="_blank">l'admin Imago</a>
|
||||
(scopes requis : images:read, images:write)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── Header ─── -->
|
||||
<div class="header">
|
||||
<div class="logo">🖼️ Imago<span>Dashboard</span></div>
|
||||
@@ -250,6 +309,11 @@
|
||||
<span class="ws-dot off" id="ws-dot"></span>
|
||||
<span id="ws-label">Déconnecté</span>
|
||||
</div>
|
||||
<div class="user-info" id="user-info" style="display:none">
|
||||
<span class="user-avatar">👤</span>
|
||||
<span class="user-name" id="user-name"></span>
|
||||
<button class="btn-logout" onclick="logout()" title="Déconnexion">⏻</button>
|
||||
</div>
|
||||
<div class="search-bar">
|
||||
<input type="text" id="search" placeholder="Rechercher par texte, tag, description…" autocomplete="off" enterkeyhint="search">
|
||||
<button class="btn" id="search-btn">🔍</button>
|
||||
@@ -302,9 +366,20 @@
|
||||
let currentPage = 1;
|
||||
let currentQuery = '';
|
||||
let ws = null;
|
||||
let apiKey = null;
|
||||
let userName = null;
|
||||
|
||||
// ─── Auth Helpers ───
|
||||
function getAuthHeaders() {
|
||||
const h = {};
|
||||
if (apiKey) h['X-Api-Key'] = apiKey;
|
||||
return h;
|
||||
}
|
||||
|
||||
// ─── API Helpers ───
|
||||
async function api(path, opts = {}) {
|
||||
if (!opts.headers) opts.headers = {};
|
||||
Object.assign(opts.headers, getAuthHeaders());
|
||||
const r = await fetch(path, opts);
|
||||
if (!r.ok) {
|
||||
const body = await r.text();
|
||||
@@ -338,6 +413,69 @@ function showToast(msg, type = 'info', duration = 4000) {
|
||||
if (duration > 0) setTimeout(() => { if (toast.parentElement) toast.remove(); }, duration);
|
||||
}
|
||||
|
||||
// ─── Auth ───
|
||||
async function connect() {
|
||||
const input = document.getElementById('api-key-input');
|
||||
const errorEl = document.getElementById('login-error');
|
||||
const btn = document.getElementById('login-btn');
|
||||
const key = input.value.trim();
|
||||
|
||||
if (!key) { errorEl.style.display = 'block'; errorEl.textContent = 'Veuillez entrer une clé API'; return; }
|
||||
|
||||
errorEl.style.display = 'none';
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ Validation…';
|
||||
|
||||
try {
|
||||
const r = await fetch('/api/auth/validate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ api_key: key })
|
||||
});
|
||||
const data = await r.json();
|
||||
|
||||
if (data.valid) {
|
||||
apiKey = key;
|
||||
userName = data.name || 'Utilisateur';
|
||||
localStorage.setItem('imago_api_key', key);
|
||||
localStorage.setItem('imago_user_name', userName);
|
||||
document.getElementById('login-overlay').style.display = 'none';
|
||||
document.getElementById('user-info').style.display = 'flex';
|
||||
document.getElementById('user-name').textContent = userName;
|
||||
showToast(`Connecté en tant que ${userName} ✓`, 'success');
|
||||
initApp();
|
||||
} else {
|
||||
errorEl.style.display = 'block';
|
||||
errorEl.textContent = data.error || 'Clé API invalide';
|
||||
}
|
||||
} catch(e) {
|
||||
errorEl.style.display = 'block';
|
||||
errorEl.textContent = `Erreur : ${e.message}`;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🔗 Se connecter';
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
if (ws) { ws.close(); ws = null; }
|
||||
localStorage.removeItem('imago_api_key');
|
||||
localStorage.removeItem('imago_user_name');
|
||||
apiKey = null;
|
||||
userName = null;
|
||||
document.getElementById('login-overlay').style.display = 'flex';
|
||||
document.getElementById('user-info').style.display = 'none';
|
||||
document.getElementById('api-key-input').value = '';
|
||||
document.getElementById('gallery').innerHTML = '';
|
||||
document.getElementById('pagination').innerHTML = '';
|
||||
document.getElementById('stat-images').textContent = '…';
|
||||
document.getElementById('stat-storage').textContent = '…';
|
||||
document.getElementById('stat-tags').textContent = '…';
|
||||
document.getElementById('stat-uptime').textContent = '●';
|
||||
document.getElementById('stat-uptime').style.color = 'var(--fg-dim)';
|
||||
showToast('Déconnecté', 'info', 2000);
|
||||
}
|
||||
|
||||
// ─── Upload ───
|
||||
async function uploadFiles(files) {
|
||||
if (!files || files.length === 0) return;
|
||||
@@ -359,7 +497,7 @@ async function uploadFiles(files) {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
|
||||
const r = await fetch('/api/upload', { method: 'POST', body: form });
|
||||
const r = await fetch('/api/upload', { method: 'POST', body: form, headers: getAuthHeaders() });
|
||||
const data = await r.json();
|
||||
|
||||
if (r.ok) {
|
||||
@@ -613,9 +751,26 @@ function escHtml(s) {
|
||||
}
|
||||
|
||||
// ─── Init ───
|
||||
loadImages();
|
||||
loadStats();
|
||||
connectWS();
|
||||
function initApp() {
|
||||
loadImages();
|
||||
loadStats();
|
||||
connectWS();
|
||||
}
|
||||
|
||||
// Check localStorage for existing session
|
||||
(function checkAuth() {
|
||||
const savedKey = localStorage.getItem('imago_api_key');
|
||||
const savedName = localStorage.getItem('imago_user_name');
|
||||
if (savedKey) {
|
||||
apiKey = savedKey;
|
||||
userName = savedName || 'Utilisateur';
|
||||
document.getElementById('login-overlay').style.display = 'none';
|
||||
document.getElementById('user-info').style.display = 'flex';
|
||||
document.getElementById('user-name').textContent = userName;
|
||||
initApp();
|
||||
}
|
||||
})();
|
||||
|
||||
setInterval(loadStats, 30000);
|
||||
|
||||
// Keyboard shortcut: Ctrl+U to focus drop zone
|
||||
|
||||
Reference in New Issue
Block a user