feat: improve upload admin workflow
Add users.is_admin SQLite migration for promoted admins. Support curl PUT request uploads, save uploads into admin file-manager paths, default 25 GiB uploads, tabbed UI, and local dev preview.
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
adminDeleteUserAction,
|
||||
adminLogoutAction,
|
||||
adminResetUserAction,
|
||||
adminSetUserAdminAction,
|
||||
} from '@/src/lib/actions.js';
|
||||
import { all, runCleanupIfNeeded } from '@/src/lib/db.js';
|
||||
import { formatTimestamp, readSearchParam } from '@/src/lib/format.js';
|
||||
@@ -14,14 +15,16 @@ export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function AdminUsersPage({ searchParams }) {
|
||||
await runCleanupIfNeeded();
|
||||
await requireAdminUser();
|
||||
const actor = await requireAdminUser();
|
||||
|
||||
const csrfToken = await ensureCsrfToken();
|
||||
const users = await all('SELECT username, created_at FROM users ORDER BY username ASC');
|
||||
const users = await all('SELECT username, created_at, is_admin FROM users ORDER BY username ASC');
|
||||
|
||||
const resolvedSearchParams = await searchParams;
|
||||
const error = readSearchParam(resolvedSearchParams, 'error');
|
||||
const success = readSearchParam(resolvedSearchParams, 'success');
|
||||
const requestedTab = readSearchParam(resolvedSearchParams, 'tab');
|
||||
const activeTab = requestedTab === 'create' ? 'create' : 'users';
|
||||
|
||||
return (
|
||||
<main className="page-shell">
|
||||
@@ -46,6 +49,16 @@ export default async function AdminUsersPage({ searchParams }) {
|
||||
|
||||
<StatusMessage error={error} success={success} />
|
||||
|
||||
<nav className="tabs" aria-label="Benutzer-Bereiche">
|
||||
<a className={`tab ${activeTab === 'users' ? 'active' : ''}`} href="/manage/admin/users?tab=users">
|
||||
Benutzer
|
||||
</a>
|
||||
<a className={`tab ${activeTab === 'create' ? 'active' : ''}`} href="/manage/admin/users?tab=create">
|
||||
Neu
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
{activeTab === 'create' ? (
|
||||
<section className="panel">
|
||||
<h2>Neuen Benutzer anlegen</h2>
|
||||
<form className="form-grid" action={adminCreateUserAction}>
|
||||
@@ -61,12 +74,19 @@ export default async function AdminUsersPage({ searchParams }) {
|
||||
<input className="input" name="password" type="password" autoComplete="new-password" required />
|
||||
</label>
|
||||
|
||||
<label className="check-row">
|
||||
<input type="checkbox" name="isAdmin" value="1" />
|
||||
Adminrechte vergeben
|
||||
</label>
|
||||
|
||||
<button className="btn" type="submit">
|
||||
Benutzer erstellen
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{activeTab === 'users' ? (
|
||||
<section className="panel">
|
||||
<h2>Bestehende Benutzer</h2>
|
||||
{users.length === 0 ? (
|
||||
@@ -77,6 +97,7 @@ export default async function AdminUsersPage({ searchParams }) {
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Benutzername</th>
|
||||
<th>Rolle</th>
|
||||
<th>Erstellt</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
@@ -85,9 +106,27 @@ export default async function AdminUsersPage({ searchParams }) {
|
||||
{users.map((user) => (
|
||||
<tr key={user.username}>
|
||||
<td>{user.username}</td>
|
||||
<td>
|
||||
<span className={`badge ${user.is_admin ? 'primary' : ''}`}>
|
||||
{user.is_admin ? 'Admin' : 'Benutzer'}
|
||||
</span>
|
||||
</td>
|
||||
<td>{formatTimestamp(user.created_at)}</td>
|
||||
<td>
|
||||
<div className="stack-actions">
|
||||
<form className="inline-form" action={adminSetUserAdminAction}>
|
||||
<input type="hidden" name="csrfToken" value={csrfToken} />
|
||||
<input type="hidden" name="username" value={user.username} />
|
||||
<input type="hidden" name="isAdmin" value={user.is_admin ? '0' : '1'} />
|
||||
<button
|
||||
className="btn secondary"
|
||||
type="submit"
|
||||
disabled={actor.username === user.username && Boolean(user.is_admin)}
|
||||
>
|
||||
{user.is_admin ? 'Adminrechte entziehen' : 'Zum Admin machen'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form className="inline-form" action={adminResetUserAction}>
|
||||
<input type="hidden" name="csrfToken" value={csrfToken} />
|
||||
<input type="hidden" name="username" value={user.username} />
|
||||
@@ -106,7 +145,7 @@ export default async function AdminUsersPage({ searchParams }) {
|
||||
<form className="inline-form" action={adminDeleteUserAction}>
|
||||
<input type="hidden" name="csrfToken" value={csrfToken} />
|
||||
<input type="hidden" name="username" value={user.username} />
|
||||
<button className="btn danger" type="submit">
|
||||
<button className="btn danger" type="submit" disabled={actor.username === user.username}>
|
||||
Benutzer löschen
|
||||
</button>
|
||||
</form>
|
||||
@@ -119,6 +158,7 @@ export default async function AdminUsersPage({ searchParams }) {
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
) : null}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user