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:
189
frontend/src/pages/APIKeys.tsx
Normal file
189
frontend/src/pages/APIKeys.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user