Feat : add Webauth
All checks were successful
Build and Push Docker Images / docker (push) Successful in 1m22s
All checks were successful
Build and Push Docker Images / docker (push) Successful in 1m22s
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { ShieldCheck, ArrowLeft, RefreshCw, ToggleLeft, ToggleRight, Check, X, Database, Trash2, AlertTriangle } from 'lucide-react'
|
||||
import { getAdminSettings, setAdminSetting, getLoginLogs, getDbInfo, purgeDb } from '../api/client'
|
||||
import { ShieldCheck, ArrowLeft, RefreshCw, ToggleLeft, ToggleRight, Check, X, Database, Trash2, AlertTriangle, Fingerprint, Key } from 'lucide-react'
|
||||
import { getAdminSettings, setAdminSetting, getLoginLogs, getDbInfo, purgeDb, adminGetPasskeys, adminDeletePasskey } from '../api/client'
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
@@ -27,7 +27,7 @@ function ToggleRow({ label, description, enabled, onChange, loading }) {
|
||||
}
|
||||
|
||||
export default function AdminPage({ onBack }) {
|
||||
const [activeTab, setActiveTab] = useState('settings') // 'settings' | 'logs' | 'database'
|
||||
const [activeTab, setActiveTab] = useState('settings') // 'settings' | 'passkeys' | 'logs' | 'database'
|
||||
|
||||
// ─── Settings ────────────────────────────────────────────────────────────
|
||||
const [settings, setSettings] = useState(null)
|
||||
@@ -64,6 +64,55 @@ export default function AdminPage({ onBack }) {
|
||||
}
|
||||
}
|
||||
|
||||
const togglePasskeys = async () => {
|
||||
if (!settings) return
|
||||
const newValue = settings.passkey_enabled === 'true' ? 'false' : 'true'
|
||||
setToggleLoading(true)
|
||||
try {
|
||||
await setAdminSetting('passkey_enabled', newValue)
|
||||
setSettings(prev => ({ ...prev, passkey_enabled: newValue }))
|
||||
} catch (err) {
|
||||
setSettingsError(err.message)
|
||||
} finally {
|
||||
setToggleLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Passkeys admin ──────────────────────────────────────────────────────
|
||||
const [adminPasskeys, setAdminPasskeys] = useState([])
|
||||
const [adminPasskeysLoading, setAdminPasskeysLoading] = useState(false)
|
||||
const [adminPasskeysError, setAdminPasskeysError] = useState(null)
|
||||
const [revokeLoading, setRevokeLoading] = useState(null)
|
||||
|
||||
const loadAdminPasskeys = useCallback(async () => {
|
||||
setAdminPasskeysLoading(true)
|
||||
setAdminPasskeysError(null)
|
||||
try {
|
||||
const data = await adminGetPasskeys()
|
||||
setAdminPasskeys(data)
|
||||
} catch (err) {
|
||||
setAdminPasskeysError(err.message)
|
||||
} finally {
|
||||
setAdminPasskeysLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'passkeys') loadAdminPasskeys()
|
||||
}, [activeTab, loadAdminPasskeys])
|
||||
|
||||
const handleRevokePasskey = async (credentialId) => {
|
||||
setRevokeLoading(credentialId)
|
||||
try {
|
||||
await adminDeletePasskey(credentialId)
|
||||
setAdminPasskeys(prev => prev.filter(p => p.credential_id !== credentialId))
|
||||
} catch (err) {
|
||||
setAdminPasskeysError(err.message)
|
||||
} finally {
|
||||
setRevokeLoading(null)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Login logs ──────────────────────────────────────────────────────────
|
||||
const [logs, setLogs] = useState([])
|
||||
const [logsTotal, setLogsTotal] = useState(0)
|
||||
@@ -196,6 +245,7 @@ export default function AdminPage({ onBack }) {
|
||||
<div className="flex gap-1 mb-8 border-b border-gray-800">
|
||||
{[
|
||||
{ key: 'settings', label: 'Paramètres' },
|
||||
{ key: 'passkeys', label: 'Passkeys' },
|
||||
{ key: 'logs', label: 'Connexions' },
|
||||
{ key: 'database', label: 'Base de données' },
|
||||
].map(tab => (
|
||||
@@ -236,12 +286,95 @@ export default function AdminPage({ onBack }) {
|
||||
onChange={toggleRegistration}
|
||||
loading={toggleLoading}
|
||||
/>
|
||||
<ToggleRow
|
||||
label="Authentification par passkey"
|
||||
description="Autorise la connexion via TouchID / FaceID (WebAuthn)."
|
||||
enabled={settings?.passkey_enabled === 'true'}
|
||||
onChange={togglePasskeys}
|
||||
loading={toggleLoading}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ── Tab: Passkeys ── */}
|
||||
{activeTab === 'passkeys' && (
|
||||
<section className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-gray-300">Passkeys enregistrées</h2>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
Gérez les passkeys (TouchID / FaceID) de tous les utilisateurs.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={loadAdminPasskeys}
|
||||
disabled={adminPasskeysLoading}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs bg-gray-800 hover:bg-gray-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
<RefreshCw size={12} className={adminPasskeysLoading ? 'animate-spin' : ''} />
|
||||
Actualiser
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{adminPasskeysError && (
|
||||
<div className="bg-red-950/40 border border-red-800/50 rounded-lg px-3 py-2 text-xs text-red-300 mb-3">
|
||||
{adminPasskeysError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{adminPasskeysLoading && adminPasskeys.length === 0
|
||||
? <p className="text-xs text-gray-500 py-8 text-center">Chargement…</p>
|
||||
: adminPasskeys.length === 0
|
||||
? (
|
||||
<div className="flex flex-col items-center gap-2 py-10 text-gray-600">
|
||||
<Fingerprint size={28} />
|
||||
<p className="text-xs">Aucune passkey enregistrée.</p>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="overflow-x-auto -mx-2">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-left text-gray-500 border-b border-gray-800">
|
||||
<th className="pb-2 px-2 font-medium">Utilisateur</th>
|
||||
<th className="pb-2 px-2 font-medium">Appareil</th>
|
||||
<th className="pb-2 px-2 font-medium">Enregistrée le</th>
|
||||
<th className="pb-2 px-2 font-medium"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-800/60">
|
||||
{adminPasskeys.map(pk => (
|
||||
<tr key={pk.credential_id} className="hover:bg-gray-800/30 transition-colors">
|
||||
<td className="py-2 px-2 font-mono text-gray-200">{pk.username}</td>
|
||||
<td className="py-2 px-2 text-gray-300 flex items-center gap-1.5">
|
||||
<Key size={11} className="text-indigo-400 flex-shrink-0" />
|
||||
{pk.name}
|
||||
</td>
|
||||
<td className="py-2 px-2 text-gray-500 whitespace-nowrap">
|
||||
{new Date(pk.created_at).toLocaleString('fr-FR')}
|
||||
</td>
|
||||
<td className="py-2 px-2 text-right">
|
||||
<button
|
||||
onClick={() => handleRevokePasskey(pk.credential_id)}
|
||||
disabled={revokeLoading === pk.credential_id}
|
||||
className="px-2 py-1 rounded-md text-xs text-red-400 hover:bg-red-950/50 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{revokeLoading === pk.credential_id ? '…' : 'Révoquer'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ── Tab: Connexions ── */}
|
||||
{activeTab === 'logs' && (
|
||||
<section className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
|
||||
|
||||
Reference in New Issue
Block a user