feat: add /admin/login page (v0.14.0)

Password form at /admin/login that calls POST /api/v1/admin/login and
redirects to /admin on success. Admin dashboard now redirects to
/admin/login instead of /join when unauthenticated. Test guide updated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-04-03 19:35:40 +02:00
parent 5b2947cdbe
commit eab5bb4d1c
3 changed files with 70 additions and 18 deletions

View File

@@ -60,23 +60,7 @@ Please test each step in order and report any errors (console errors, wrong text
## Admin & Host Features ## Admin & Host Features
For these steps you need an admin session. Log in via the admin API (or use the curl command below): For these steps you need an admin session. Go to **http://localhost:5173/admin/login** and enter the admin password (`admin123` for the dev environment). You'll be redirected to the dashboard automatically.
```bash
curl -s -X POST http://localhost:3000/api/v1/admin/login \
-H 'Content-Type: application/json' \
-d '{"password":"admin123"}' | python3 -m json.tool
```
Copy the `jwt` value — you'll need it for the Authorization header in curl commands.
To use the admin dashboard in the browser, paste the token into DevTools console:
```js
localStorage.setItem('token', '<jwt>');
localStorage.setItem('role', 'admin');
```
Then navigate to **http://localhost:5173/admin**.
### Step 10 — Admin Dashboard: Stats & Config ### Step 10 — Admin Dashboard: Stats & Config
1. Go to **http://localhost:5173/admin** 1. Go to **http://localhost:5173/admin**

View File

@@ -47,7 +47,7 @@
const token = getToken(); const token = getToken();
const role = getRole(); const role = getRole();
if (!token || role !== 'admin') { if (!token || role !== 'admin') {
goto('/join'); goto('/admin/login');
return; return;
} }
await reload(); await reload();

View File

@@ -0,0 +1,68 @@
<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 }>('/admin/login', { password });
// Admin sessions have no PIN; pass null so setAuth doesn't overwrite a guest PIN
setAuth(res.jwt, null, '');
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">
<div class="w-full max-w-sm">
<h1 class="mb-2 text-center text-2xl font-bold text-gray-900">Admin-Login</h1>
<p class="mb-6 text-center text-gray-500 text-sm">Nur für Veranstalter</p>
<form onsubmit={(e) => { e.preventDefault(); handleLogin(); }}>
<input
type="password"
bind:value={password}
placeholder="Passwort"
autocomplete="current-password"
class="mb-3 w-full rounded-lg border border-gray-300 px-4 py-3 text-lg focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200"
/>
{#if error}
<p class="mb-3 text-sm text-red-600">{error}</p>
{/if}
<button
type="submit"
disabled={loading || !password}
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"
>
{loading ? 'Wird angemeldet…' : 'Anmelden'}
</button>
</form>
<p class="mt-4 text-center text-sm text-gray-500">
<a href="/join" class="text-blue-600 hover:underline">Zurück zum Event</a>
</p>
</div>
</div>