147 lines
5.5 KiB
JavaScript
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>
|
|
);
|
|
}
|