Add comprehensive backend features and mobile UI improvements

Backend:
- Add 2FA authentication with TOTP support
- Add API keys management system
- Add audit logging for security events
- Add file upload/management system
- Add notifications system with preferences
- Add session management
- Add webhooks integration
- Add analytics endpoints
- Add export functionality
- Add password policy enforcement
- Add new database migrations for core tables

Frontend:
- Add module position system (top/bottom sidebar sections)
- Add search and notifications module configuration tabs
- Add mobile logo replacing hamburger menu
- Center page title absolutely when no tabs present
- Align sidebar footer toggles with navigation items
- Add lighter icon color in dark theme for mobile
- Add API keys management page
- Add notifications page with context
- Add admin analytics and audit logs pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-17 22:27:32 +01:00
parent f698aa4d51
commit 8c4a555b88
76 changed files with 9751 additions and 323 deletions

View File

@@ -0,0 +1,189 @@
import { useEffect, useState } from 'react';
import { useTranslation } from '../contexts/LanguageContext';
import { useSidebar } from '../contexts/SidebarContext';
import { apiKeysAPI } from '../api/client';
import type { ApiKeyItem } from '../api/client';
import '../styles/APIKeys.css';
export default function APIKeys() {
const { t } = useTranslation();
const { toggleMobileMenu } = useSidebar();
const [items, setItems] = useState<ApiKeyItem[]>([]);
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const [name, setName] = useState('');
const [createdKey, setCreatedKey] = useState<string | null>(null);
const load = async () => {
setLoading(true);
setError('');
try {
const data = await apiKeysAPI.list();
setItems(data.items || []);
} catch (err: any) {
setError(err?.response?.data?.detail || t.common.error);
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const create = async () => {
setBusy(true);
setError('');
setCreatedKey(null);
try {
const created = await apiKeysAPI.create({ name: name.trim() });
setCreatedKey(created.key);
setName('');
await load();
} catch (err: any) {
setError(err?.response?.data?.detail || t.common.error);
} finally {
setBusy(false);
}
};
const revoke = async (id: string) => {
setBusy(true);
setError('');
try {
await apiKeysAPI.revoke(id);
await load();
} catch (err: any) {
setError(err?.response?.data?.detail || t.common.error);
} finally {
setBusy(false);
}
};
const deleteKey = async (id: string) => {
setBusy(true);
setError('');
try {
await apiKeysAPI.delete(id);
await load();
} catch (err: any) {
setError(err?.response?.data?.detail || t.common.error);
} finally {
setBusy(false);
}
};
const copy = async () => {
if (!createdKey) return;
try {
await navigator.clipboard.writeText(createdKey);
} catch {
// ignore
}
};
return (
<main className="main-content api-keys-root">
<div className="page-tabs-container">
<div className="page-tabs-slider">
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
<span className="material-symbols-outlined">menu</span>
</button>
<div className="page-title-section">
<span className="material-symbols-outlined">vpn_key</span>
<span className="page-title-text">{t.apiKeysPage.title}</span>
</div>
</div>
</div>
<div className="page-content">
{error && <div className="error-message">{error}</div>}
<div className="api-keys-section">
<h3 className="section-title">{t.apiKeysPage.createTitle}</h3>
<p className="api-keys-desc">{t.apiKeysPage.createDesc}</p>
<div className="api-keys-create-row">
<input
className="api-keys-input"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={t.apiKeysPage.namePlaceholder}
disabled={busy}
/>
<button className="btn-primary" onClick={create} disabled={busy || name.trim().length < 1}>
{t.apiKeysPage.createButton}
</button>
</div>
{createdKey && (
<div className="api-keys-created">
<div className="api-keys-created-header">
<span className="badge badge-accent">{t.apiKeysPage.showOnce}</span>
<button className="btn-link" onClick={copy}>{t.apiKeysPage.copy}</button>
</div>
<code className="api-keys-created-key">{createdKey}</code>
</div>
)}
</div>
<div className="api-keys-section">
<h3 className="section-title">{t.apiKeysPage.listTitle}</h3>
{loading ? (
<div className="loading">{t.common.loading}</div>
) : items.length === 0 ? (
<div className="api-keys-empty">{t.apiKeysPage.empty}</div>
) : (
<div className="api-keys-table-card">
<table className="api-keys-table">
<thead>
<tr>
<th>{t.apiKeysPage.name}</th>
<th>{t.apiKeysPage.prefix}</th>
<th>{t.apiKeysPage.status}</th>
<th>{t.apiKeysPage.lastUsed}</th>
<th>{t.apiKeysPage.usage}</th>
<th>{t.apiKeysPage.actions}</th>
</tr>
</thead>
<tbody>
{items.map((k) => (
<tr key={k.id}>
<td>{k.name}</td>
<td className="mono">{k.key_prefix}</td>
<td>
<span className={`badge ${k.is_active ? 'badge-success' : 'badge-muted'}`}>
{k.is_active ? t.settings.enabled : t.settings.disabled}
</span>
</td>
<td className="mono">{k.last_used_at ? new Date(k.last_used_at).toLocaleString() : '—'}</td>
<td className="mono">{k.usage_count}</td>
<td className="api-keys-actions">
{k.is_active ? (
<button className="btn-link" onClick={() => revoke(k.id)} disabled={busy}>
{t.apiKeysPage.revoke}
</button>
) : (
<span className="api-keys-muted">{t.apiKeysPage.revoked}</span>
)}
<button className="btn-link danger" onClick={() => deleteKey(k.id)} disabled={busy}>
{t.common.delete}
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</main>
);
}