Files
files/nextjs/app/manage/admin/tokens/page.js
2026-06-18 19:17:51 +00:00

147 lines
5.5 KiB
JavaScript

import {
adminCreateUploadTokenAction,
adminDisableUploadTokenAction,
adminLogoutAction,
} from '@/src/lib/actions.js';
import { all, runCleanupIfNeeded } from '@/src/lib/db.js';
import { formatTimestamp, readSearchParam } from '@/src/lib/format.js';
import { ensureCsrfToken, requireAdminUser } from '@/src/lib/security.js';
import { AdminTabs } from '../../_components/admin-tabs.js';
import { CurlUploadExample } from '../../_components/curl-upload-example.js';
import { StatusMessage } from '../../_components/status-message.js';
export const dynamic = 'force-dynamic';
export default async function AdminUploadTokensPage({ searchParams }) {
await runCleanupIfNeeded();
await requireAdminUser();
const csrfToken = await ensureCsrfToken();
const tokens = await all(
`SELECT id, label, scope_path, created_by, created_at, expires_at, last_used_at, disabled_at
FROM upload_tokens
ORDER BY created_at DESC
LIMIT 500`
);
const resolvedSearchParams = await searchParams;
const error = readSearchParam(resolvedSearchParams, 'error');
const success = readSearchParam(resolvedSearchParams, 'success');
const createdToken = readSearchParam(resolvedSearchParams, 'createdToken');
return (
<main className="page-shell">
<header className="page-header">
<div className="header-main">
<h1>Upload-Tokens</h1>
<p className="muted">Bearer-Tokens für curl-Uploads in feste Zielordner.</p>
</div>
<form className="inline-form" action={adminLogoutAction}>
<input type="hidden" name="csrfToken" value={csrfToken} />
<button className="btn secondary" type="submit">
Abmelden
</button>
</form>
</header>
<StatusMessage error={error} success={success} />
<AdminTabs active="tokens" />
{createdToken ? (
<section className="panel">
<h2>Neuer Token</h2>
<p className="muted">Token jetzt kopieren. Er wird nicht erneut angezeigt.</p>
<pre><code>{createdToken}</code></pre>
<CurlUploadExample path="/_token-upload/unterordner/datei.txt" token={createdToken} fileName="datei.txt" />
</section>
) : null}
<section className="panel">
<h2>Token erstellen</h2>
<form className="form-grid" action={adminCreateUploadTokenAction}>
<input type="hidden" name="csrfToken" value={csrfToken} />
<label className="field">
Name (optional)
<input className="input" name="label" placeholder="z.B. Kunde A" />
</label>
<label className="field">
Zielordner im Dateimanager
<input className="input" name="scopePath" placeholder="z.B. eingang/kunde-a" required />
</label>
<label className="field">
Ablauf in Stunden (optional)
<input className="input" name="expiresHours" placeholder="leer = kein Ablauf" />
</label>
<button className="btn" type="submit">
Token erstellen
</button>
</form>
</section>
<section className="panel">
<h2>Aktive Tokens</h2>
{tokens.length === 0 ? (
<p className="muted">Noch keine Upload-Tokens.</p>
) : (
<div className="table-wrap">
<table>
<thead>
<tr>
<th>Name</th>
<th>Zielordner</th>
<th>Erstellt</th>
<th>Ablauf</th>
<th>Letzte Nutzung</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{tokens.map((token) => {
const disabled = Number(token.disabled_at || 0) > 0;
const expired = Number(token.expires_at || 0) > 0 && Number(token.expires_at) <= Date.now();
return (
<tr key={token.id}>
<td>
<strong>{token.label || `Token ${token.id}`}</strong>
<div className="muted">von {token.created_by || '-'}</div>
</td>
<td className="mono">{token.scope_path}</td>
<td>{formatTimestamp(token.created_at)}</td>
<td>{token.expires_at ? formatTimestamp(token.expires_at) : 'Kein Ablauf'}</td>
<td>{token.last_used_at ? formatTimestamp(token.last_used_at) : '-'}</td>
<td>
<span className={`badge ${disabled || expired ? 'danger' : 'primary'}`}>
{disabled ? 'Deaktiviert' : expired ? 'Abgelaufen' : 'Aktiv'}
</span>
</td>
<td>
{disabled ? null : (
<form className="inline-form" action={adminDisableUploadTokenAction}>
<input type="hidden" name="csrfToken" value={csrfToken} />
<input type="hidden" name="tokenId" value={token.id} />
<button className="btn danger" type="submit">
Deaktivieren
</button>
</form>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</section>
</main>
);
}