feat: admin article management, sources, settings, rules, and scheduler
- Add article list, editor, and deletion UI. - Add source management with bias/credibility/active toggles. - Add encrypted API key CRUD UI. - Add automated search rules with query, sources, schedule presets, and per-rule AI model override. - Implement node-cron scheduler and instrumentation hook. - Add admin settings including password change and quota reference.
This commit is contained in:
179
src/app/admin/keys/page.tsx
Normal file
179
src/app/admin/keys/page.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ApiProvider } from '@prisma/client';
|
||||
|
||||
type ApiKeyRecord = {
|
||||
id: string;
|
||||
provider: ApiProvider;
|
||||
label: string | null;
|
||||
isActive: boolean;
|
||||
lastUsedAt: string | null;
|
||||
createdAt: string;
|
||||
maskedKey: string;
|
||||
};
|
||||
|
||||
const providers: ApiProvider[] = ['NEWSAPI', 'GNEWS', 'NEWSDATA', 'KIMI', 'WEBSEARCH', 'BRAVE'];
|
||||
|
||||
export default function ApiKeysPage() {
|
||||
const [keys, setKeys] = useState<ApiKeyRecord[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [form, setForm] = useState({ provider: 'KIMI' as ApiProvider, key: '', label: '' });
|
||||
|
||||
async function fetchKeys() {
|
||||
const res = await fetch('/api/admin/keys');
|
||||
if (res.ok) setKeys(await res.json());
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchKeys();
|
||||
}, []);
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const res = await fetch('/api/admin/keys', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(form),
|
||||
});
|
||||
if (res.ok) {
|
||||
setForm({ provider: 'KIMI', key: '', label: '' });
|
||||
fetchKeys();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
alert(data.error || 'Failed to add key');
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleActive(id: string, current: boolean) {
|
||||
const res = await fetch(`/api/admin/keys/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ isActive: !current }),
|
||||
});
|
||||
if (res.ok) fetchKeys();
|
||||
}
|
||||
|
||||
async function removeKey(id: string) {
|
||||
if (!confirm('Delete this API key?')) return;
|
||||
const res = await fetch(`/api/admin/keys/${id}`, { method: 'DELETE' });
|
||||
if (res.ok) fetchKeys();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-6">API Keys</h1>
|
||||
|
||||
<div className="bg-white dark:bg-neutral-900 rounded-xl shadow-sm border dark:border-neutral-800 p-6 mb-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Add New Key</h2>
|
||||
<form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-4 gap-4 items-end">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Provider</label>
|
||||
<select
|
||||
value={form.provider}
|
||||
onChange={(e) => setForm({ ...form, provider: e.target.value as ApiProvider })}
|
||||
className="w-full px-3 py-2 border rounded-lg dark:bg-neutral-800 dark:border-neutral-700"
|
||||
>
|
||||
{providers.map((p) => (
|
||||
<option key={p} value={p}>
|
||||
{p}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium mb-1">Key</label>
|
||||
<input
|
||||
type="password"
|
||||
value={form.key}
|
||||
onChange={(e) => setForm({ ...form, key: e.target.value })}
|
||||
className="w-full px-3 py-2 border rounded-lg dark:bg-neutral-800 dark:border-neutral-700"
|
||||
placeholder="Paste key here"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Label</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.label}
|
||||
onChange={(e) => setForm({ ...form, label: e.target.value })}
|
||||
className="w-full px-3 py-2 border rounded-lg dark:bg-neutral-800 dark:border-neutral-700"
|
||||
placeholder="Optional"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-4">
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium"
|
||||
>
|
||||
Add Key
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
<div className="bg-white dark:bg-neutral-900 rounded-xl shadow-sm border dark:border-neutral-800 overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-neutral-50 dark:bg-neutral-800">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left">Provider</th>
|
||||
<th className="px-4 py-3 text-left">Label</th>
|
||||
<th className="px-4 py-3 text-left">Status</th>
|
||||
<th className="px-4 py-3 text-left">Added</th>
|
||||
<th className="px-4 py-3 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{keys.map((k) => (
|
||||
<tr key={k.id} className="border-t dark:border-neutral-800">
|
||||
<td className="px-4 py-3 font-medium">{k.provider}</td>
|
||||
<td className="px-4 py-3">{k.label || '-'}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span
|
||||
className={`inline-flex px-2 py-1 rounded-full text-xs font-medium ${
|
||||
k.isActive
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-950 dark:text-green-300'
|
||||
: 'bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-400'
|
||||
}`}
|
||||
>
|
||||
{k.isActive ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-neutral-500 dark:text-neutral-400">
|
||||
{new Date(k.createdAt).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right space-x-2">
|
||||
<button
|
||||
onClick={() => toggleActive(k.id, k.isActive)}
|
||||
className="text-blue-600 hover:underline"
|
||||
>
|
||||
{k.isActive ? 'Disable' : 'Enable'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => removeKey(k.id)}
|
||||
className="text-red-600 hover:underline"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{keys.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-4 py-8 text-center text-neutral-500">
|
||||
No API keys configured yet.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user