150 lines
5.5 KiB
JavaScript
150 lines
5.5 KiB
JavaScript
import {
|
|
deleteOwnUploadAction,
|
|
extendOwnUploadAction,
|
|
uploadFileAction,
|
|
userLogoutAction,
|
|
} from '@/src/lib/actions.js';
|
|
import { all, runCleanupIfNeeded } from '@/src/lib/db.js';
|
|
import { formatBytes, formatCountdown, formatTimestamp, readSearchParam } from '@/src/lib/format.js';
|
|
import { ensureCsrfToken, requireAuthenticatedUser } from '@/src/lib/security.js';
|
|
|
|
import { CopyLinkButton } from '../_components/copy-link-button.js';
|
|
import { StatusMessage } from '../_components/status-message.js';
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
export default async function DashboardPage({ searchParams }) {
|
|
await runCleanupIfNeeded();
|
|
|
|
const user = await requireAuthenticatedUser();
|
|
const csrfToken = await ensureCsrfToken();
|
|
const uploads = await all(
|
|
'SELECT id, original_name, stored_name, size_bytes, uploaded_at, expires_at FROM uploads WHERE owner = ? ORDER BY uploaded_at DESC',
|
|
[user.username]
|
|
);
|
|
|
|
const resolvedSearchParams = await searchParams;
|
|
const error = readSearchParam(resolvedSearchParams, 'error');
|
|
const success = readSearchParam(resolvedSearchParams, 'success');
|
|
|
|
return (
|
|
<main className="page-shell">
|
|
<header className="page-header">
|
|
<div className="header-main">
|
|
<h1>Dateiverwaltung</h1>
|
|
<p className="muted">Angemeldet als {user.username}</p>
|
|
</div>
|
|
|
|
<div className="toolbar">
|
|
{user.admin ? (
|
|
<a className="chip primary" href="/manage/admin/dashboard">
|
|
Adminbereich
|
|
</a>
|
|
) : null}
|
|
|
|
<form className="inline-form" action={userLogoutAction}>
|
|
<input type="hidden" name="csrfToken" value={csrfToken} />
|
|
<button className="btn secondary" type="submit">
|
|
Abmelden
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</header>
|
|
|
|
<section className="panel">
|
|
<h2>Neue Datei hochladen</h2>
|
|
<StatusMessage error={error} success={success} />
|
|
|
|
<form className="form-grid" action={uploadFileAction} encType="multipart/form-data">
|
|
<input type="hidden" name="csrfToken" value={csrfToken} />
|
|
|
|
<label className="field">
|
|
Datei
|
|
<input className="input" name="file" type="file" required />
|
|
</label>
|
|
|
|
<label className="field">
|
|
Aufbewahrung in Stunden
|
|
<input className="input" name="retentionHours" placeholder="168" />
|
|
</label>
|
|
|
|
<button className="btn" type="submit">
|
|
Hochladen
|
|
</button>
|
|
</form>
|
|
</section>
|
|
|
|
<section className="panel">
|
|
<h2>Aktuelle Uploads</h2>
|
|
{uploads.length === 0 ? (
|
|
<p className="muted">Noch keine Uploads.</p>
|
|
) : (
|
|
<div className="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Datei</th>
|
|
<th>Größe</th>
|
|
<th>Hochgeladen</th>
|
|
<th>Ablauf</th>
|
|
<th>Aktionen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{uploads.map((item) => {
|
|
const sharePath = `/_share/${encodeURIComponent(item.stored_name)}`;
|
|
return (
|
|
<tr key={item.id}>
|
|
<td>
|
|
<strong>{item.original_name}</strong>
|
|
<div className="muted mono">{item.stored_name}</div>
|
|
</td>
|
|
<td>{formatBytes(item.size_bytes)}</td>
|
|
<td>{formatTimestamp(item.uploaded_at)}</td>
|
|
<td>
|
|
<div>{formatTimestamp(item.expires_at)}</div>
|
|
<div className="muted">Noch {formatCountdown(item.expires_at)}</div>
|
|
</td>
|
|
<td>
|
|
<div className="stack-actions">
|
|
<div className="row-actions">
|
|
<a className="btn secondary" href={sharePath}>
|
|
Download
|
|
</a>
|
|
<CopyLinkButton path={sharePath} label={item.original_name} />
|
|
</div>
|
|
|
|
<form className="inline-form" action={extendOwnUploadAction}>
|
|
<input type="hidden" name="csrfToken" value={csrfToken} />
|
|
<input type="hidden" name="uploadId" value={item.id} />
|
|
<input
|
|
className="input small"
|
|
name="extendHours"
|
|
placeholder="Stunden"
|
|
/>
|
|
<button className="btn" type="submit">
|
|
Verlängern
|
|
</button>
|
|
</form>
|
|
|
|
<form className="inline-form" action={deleteOwnUploadAction}>
|
|
<input type="hidden" name="csrfToken" value={csrfToken} />
|
|
<input type="hidden" name="uploadId" value={item.id} />
|
|
<button className="btn danger" type="submit">
|
|
Löschen
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|