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

240 lines
9.2 KiB
JavaScript

import {
createUploadRequestAction,
deleteOwnUploadAction,
extendOwnUploadAction,
userLogoutAction,
} from '@/src/lib/actions.js';
import { all, runCleanupIfNeeded } from '@/src/lib/db.js';
import { sharedLinkName } from '@/src/lib/files.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';
import { UploadProgressForm } from '../_components/upload-progress-form.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 uploadRequests = await all(
`SELECT id, note, created_at, expires_at, completed_at, uploaded_original_name
FROM upload_requests
WHERE owner = ?
ORDER BY created_at DESC
LIMIT 200`,
[user.username]
);
const resolvedSearchParams = await searchParams;
const error = readSearchParam(resolvedSearchParams, 'error');
const success = readSearchParam(resolvedSearchParams, 'success');
const totalBytes = uploads.reduce((total, item) => total + (Number(item.size_bytes) || 0), 0);
const nowTs = Date.now();
const requestedTab = readSearchParam(resolvedSearchParams, 'tab');
const activeTab = ['upload', 'files', 'requests'].includes(requestedTab) ? requestedTab : 'upload';
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>
<form className="inline-form" action={userLogoutAction}>
<input type="hidden" name="csrfToken" value={csrfToken} />
<button className="btn secondary" type="submit">
Abmelden
</button>
</form>
</header>
<StatusMessage error={error} success={success} />
<nav className="tabs" aria-label="Dashboard-Bereiche">
<a className={`tab ${activeTab === 'upload' ? 'active' : ''}`} href="/manage/dashboard?tab=upload">
Upload
</a>
<a className={`tab ${activeTab === 'files' ? 'active' : ''}`} href="/manage/dashboard?tab=files">
Dateien
</a>
<a className={`tab ${activeTab === 'requests' ? 'active' : ''}`} href="/manage/dashboard?tab=requests">
Anfragen
</a>
{user.admin ? (
<>
<a className="tab" href="/manage/admin/dashboard?tab=stats">Admin</a>
<a className="tab" href="/manage/admin/files">Dateimanager</a>
<a className="tab" href="/manage/admin/users">Benutzer</a>
<a className="tab" href="/manage/admin/tokens">Upload-Tokens</a>
</>
) : null}
</nav>
<div className="summary-line">
<span>{uploads.length} aktive Uploads</span>
<span>{totalBytes > 0 ? formatBytes(totalBytes) : '0 B'} genutzt</span>
</div>
{activeTab === 'upload' ? (
<section className="panel">
<h2>Neue Datei hochladen</h2>
<UploadProgressForm csrfToken={csrfToken} />
</section>
) : null}
{activeTab === 'requests' ? (
<section className="panel">
<h2>Upload-Anfragen</h2>
<p className="muted">Benachrichtigung erfolgt an deinen Benutzernamen (E-Mail-Adresse).</p>
<form className="form-grid" action={createUploadRequestAction}>
<input type="hidden" name="csrfToken" value={csrfToken} />
<label className="field">
Notiz (optional)
<input className="input" name="note" placeholder="z. B. Bitte die Rechnung als PDF senden" />
</label>
<label className="field">
Gültig in Stunden
<input className="input" name="expiresHours" placeholder="72" />
</label>
<button className="btn" type="submit">
Anfrage erstellen
</button>
</form>
{uploadRequests.length === 0 ? (
<p className="muted">Noch keine Upload-Anfragen.</p>
) : (
<div className="table-wrap">
<table>
<thead>
<tr>
<th>Anfrage</th>
<th>Status</th>
<th>Erstellt</th>
<th>Gültig bis</th>
<th>Ergebnis</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{uploadRequests.map((entry) => {
const requestPath = `/_request/${encodeURIComponent(entry.id)}`;
const isCompleted = Number(entry.completed_at || 0) > 0;
const isExpired = !isCompleted && Number(entry.expires_at || 0) <= nowTs;
return (
<tr key={entry.id}>
<td>
<strong>{entry.id}</strong>
{entry.note ? <div className="muted">{entry.note}</div> : null}
</td>
<td>
<span
className={`badge ${
isCompleted ? 'success' : isExpired ? 'danger' : 'primary'
}`}
>
{isCompleted ? 'Abgeschlossen' : isExpired ? 'Abgelaufen' : 'Offen'}
</span>
</td>
<td>{formatTimestamp(entry.created_at)}</td>
<td>{formatTimestamp(entry.expires_at)}</td>
<td>{entry.uploaded_original_name || '-'}</td>
<td>
<div className="row-actions">
<a className="btn secondary" href={requestPath}>
Öffnen
</a>
<CopyLinkButton path={requestPath} label="Upload-Anfrage" />
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</section>
) : null}
{activeTab === 'files' ? (
<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 shareName = sharedLinkName(item.stored_name);
const sharePath = `/_share/${encodeURIComponent(shareName)}`;
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-form-row">
<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" formAction={extendOwnUploadAction}>
Verlängern
</button>
<button className="btn danger" type="submit" formAction={deleteOwnUploadAction}>
Löschen
</button>
</form>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</section>
) : null}
</main>
);
}