A comprehensive role-based E2E audit (guest/host/admin, across browser sessions) surfaced one critical and several smaller issues; this addresses them and hardens the tests that missed them. Critical - The client upload pipeline was fully broken: the IndexedDB v1->v2 upgrade opened a *new* transaction inside the upgrade callback, which throws during a version-change transaction and aborted the whole upgrade, leaving the queue object store uncreated -- so no UI upload ever fired. Reuse the version-change transaction the callback provides, and bump the DB to v3 with a contains() guard so installs already corrupted by the shipped bug self-heal on next load. Re-enabled the previously-fixme'd UI upload E2E test. High / Medium - Event lock is uploads-only again: likes, comments and browsing stay open while the event is locked (USER_JOURNEYS 9.3 / FEATURES) -- it was wrongly freezing social interaction. Updated the event-lock spec accordingly. - get_original now excludes soft-deleted and ban-hidden uploads, and direct /media/originals/** serving is blocked, so a hidden user's originals can no longer be pulled by UUID (all originals go through the checked alias). - The upload handler reads the file field with an early-abort size cap chosen from the declared content-type, instead of buffering the entire body before the size check. Low - unban_user mirrors the ban role guard (a host can no longer unban a host/admin banned by an admin). - reset_user_pin's UPDATE is event-scoped. - Admin login returns and stores a real identity (user_id + display name) instead of a blank session. - The host user list no longer renders target-actions (ban/promote/demote/PIN) on the caller's own row, where the backend always rejected them. - /diashow gains a client-side auth guard like the other protected routes. - The join page shows the event name via a new public GET /api/v1/event. Verified: backend cargo build clean, frontend svelte-check 0 errors, full Playwright E2E suite 144 passed / 1 skipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
76 lines
2.6 KiB
Svelte
76 lines
2.6 KiB
Svelte
<script lang="ts">
|
|
import { goto } from '$app/navigation';
|
|
import { api, ApiError } from '$lib/api';
|
|
import { setAuth, getRole } from '$lib/auth';
|
|
import { browser } from '$app/environment';
|
|
|
|
// Already logged in as admin → go straight to dashboard
|
|
if (browser && getRole() === 'admin') {
|
|
goto('/admin');
|
|
}
|
|
|
|
let password = $state('');
|
|
let error = $state('');
|
|
let loading = $state(false);
|
|
|
|
async function handleLogin() {
|
|
if (!password) return;
|
|
loading = true;
|
|
error = '';
|
|
try {
|
|
const res = await api.post<{ jwt: string; user_id: string; display_name: string }>(
|
|
'/admin/login',
|
|
{ password }
|
|
);
|
|
// Admin sessions have no PIN; pass null so setAuth doesn't overwrite a guest PIN.
|
|
// Persist the real user id + name so the admin has an identity (own-post
|
|
// affordances on the feed, a name on the Account page rather than "Unbekannt").
|
|
setAuth(res.jwt, null, res.user_id, res.display_name);
|
|
goto('/admin');
|
|
} catch (e) {
|
|
if (e instanceof ApiError) {
|
|
error = e.message;
|
|
} else {
|
|
error = 'Ein Fehler ist aufgetreten.';
|
|
}
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="flex min-h-screen items-center justify-center bg-gray-50 px-4 dark:bg-gray-950">
|
|
<div class="w-full max-w-sm">
|
|
<h1 class="mb-2 text-center text-2xl font-bold text-gray-900 dark:text-gray-100">Admin-Login</h1>
|
|
<p class="mb-6 text-center text-gray-500 text-sm dark:text-gray-400">Nur für Veranstalter</p>
|
|
|
|
<form onsubmit={(e) => { e.preventDefault(); handleLogin(); }}>
|
|
<input
|
|
type="password"
|
|
bind:value={password}
|
|
placeholder="Passwort"
|
|
autocomplete="current-password"
|
|
data-testid="admin-password-input"
|
|
class="mb-3 w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-lg text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
|
/>
|
|
|
|
{#if error}
|
|
<p class="mb-3 text-sm text-red-600 dark:text-red-400" data-testid="admin-login-error">{error}</p>
|
|
{/if}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={loading || !password}
|
|
data-testid="admin-login-submit"
|
|
class="w-full rounded-lg bg-blue-600 px-4 py-3 text-lg font-medium text-white transition hover:bg-blue-700 disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-400"
|
|
>
|
|
{loading ? 'Wird angemeldet…' : 'Anmelden'}
|
|
</button>
|
|
</form>
|
|
|
|
<p class="mt-4 text-center text-sm text-gray-500 dark:text-gray-400">
|
|
<a href="/join" class="text-blue-600 hover:underline dark:text-blue-400">Zurück zum Event</a>
|
|
</p>
|
|
</div>
|
|
</div>
|