Rollen: Trainer-Einladung, rollenbasierte Navigation und Zugriffskontrolle
This commit is contained in:
parent
7e2e5a643d
commit
59aa3cbcce
6 changed files with 413 additions and 9 deletions
178
app/src/routes/invite/[token]/+page.svelte
Normal file
178
app/src/routes/invite/[token]/+page.svelte
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { pb } from '$lib/pb';
|
||||
import type { Einladung } from '$lib/types';
|
||||
|
||||
const token = $derived($page.params.token as string);
|
||||
|
||||
let einladung = $state<Einladung | null>(null);
|
||||
let vereinName = $state('');
|
||||
let fehler = $state('');
|
||||
let loading = $state(true);
|
||||
|
||||
let name = $state('');
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
let passwordConfirm = $state('');
|
||||
let saving = $state(false);
|
||||
let formError = $state('');
|
||||
|
||||
onMount(async () => {
|
||||
if (pb.authStore.isValid) {
|
||||
goto('/');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const inv = await pb.collection('einladungen')
|
||||
.getFirstListItem<Einladung>(`token = "${token}" && genutzt = false`, {
|
||||
expand: 'verein_id',
|
||||
});
|
||||
einladung = inv;
|
||||
vereinName = (inv as any).expand?.verein_id?.name ?? '';
|
||||
} catch {
|
||||
fehler = 'Dieser Einladungslink ist ungültig oder wurde bereits verwendet.';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
async function registrieren() {
|
||||
if (!einladung) return;
|
||||
if (password !== passwordConfirm) { formError = 'Passwörter stimmen nicht überein.'; return; }
|
||||
formError = ''; saving = true;
|
||||
try {
|
||||
await pb.collection('users').create({
|
||||
email: email.trim(),
|
||||
password,
|
||||
passwordConfirm,
|
||||
name: name.trim(),
|
||||
verein_id: einladung.verein_id,
|
||||
rolle: einladung.rolle,
|
||||
});
|
||||
await pb.collection('users').authWithPassword(email.trim(), password);
|
||||
await pb.collection('einladungen').update(einladung.id, { genutzt: true });
|
||||
goto('/');
|
||||
} catch (e: unknown) {
|
||||
formError = e instanceof Error ? e.message : 'Registrierung fehlgeschlagen.';
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Einladung — vereins.haus</title></svelte:head>
|
||||
|
||||
<div class="shell">
|
||||
<div class="logo">
|
||||
<img src="/favicon.svg" alt="" width="36" height="36" />
|
||||
vereins.haus
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
{#if loading}
|
||||
<p class="hint">Prüfe Einladung…</p>
|
||||
|
||||
{:else if fehler}
|
||||
<div class="icon-ring error">✕</div>
|
||||
<h1>Ungültige Einladung</h1>
|
||||
<p class="beschr">{fehler}</p>
|
||||
|
||||
{:else}
|
||||
<div class="icon-ring">🎉</div>
|
||||
<h1>Du wurdest eingeladen!</h1>
|
||||
<p class="beschr">
|
||||
Du wirst als <strong>{einladung?.rolle === 'trainer' ? 'Trainer' : 'Admin'}</strong>
|
||||
zum Verein <strong>„{vereinName}"</strong> hinzugefügt.
|
||||
</p>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); registrieren(); }}>
|
||||
<div class="field">
|
||||
<label for="name">Dein Name *</label>
|
||||
<input id="name" type="text" bind:value={name} required autocomplete="name" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="email">E-Mail *</label>
|
||||
<input id="email" type="email" bind:value={email} required autocomplete="email" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="pw">Passwort *</label>
|
||||
<input id="pw" type="password" bind:value={password} required minlength="8" autocomplete="new-password" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="pw2">Passwort bestätigen *</label>
|
||||
<input id="pw2" type="password" bind:value={passwordConfirm} required autocomplete="new-password" />
|
||||
</div>
|
||||
|
||||
{#if formError}
|
||||
<p class="error">{formError}</p>
|
||||
{/if}
|
||||
|
||||
<button type="submit" class="btn-primary" disabled={saving || !name || !email || !password}>
|
||||
{saving ? 'Registrieren…' : 'Konto erstellen & beitreten'}
|
||||
</button>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.shell {
|
||||
min-height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem 1rem;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
padding: 2rem 1.5rem;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.icon-ring {
|
||||
width: 3.5rem; height: 3.5rem; border-radius: 50%;
|
||||
background: #e0e7ff; display: flex;
|
||||
align-items: center; justify-content: center;
|
||||
font-size: 1.5rem; margin: 0 auto;
|
||||
}
|
||||
.icon-ring.error { background: #fee2e2; }
|
||||
h1 { font-size: 1.2rem; font-weight: 700; color: #1e293b; text-align: center; margin: 0; }
|
||||
.beschr { font-size: 0.9rem; color: #64748b; text-align: center; line-height: 1.55; margin: 0; }
|
||||
.hint { color: #94a3b8; text-align: center; }
|
||||
.field { display: flex; flex-direction: column; gap: 0.3rem; margin-bottom: 0.5rem; }
|
||||
label { font-size: 0.875rem; font-weight: 500; color: #475569; }
|
||||
input {
|
||||
padding: 0.65rem 0.85rem;
|
||||
border: 1.5px solid #e2e8f0; border-radius: 8px;
|
||||
font-size: 1rem; background: #fff; width: 100%; box-sizing: border-box;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
input:focus { outline: none; border-color: #1e40af; }
|
||||
.error { color: #dc2626; font-size: 0.875rem; margin: 0; }
|
||||
.btn-primary {
|
||||
width: 100%; padding: 0.75rem; background: #1e40af; color: #fff;
|
||||
border: none; border-radius: 8px; font-size: 1rem; font-weight: 600;
|
||||
cursor: pointer; margin-top: 0.25rem; transition: background 0.15s;
|
||||
}
|
||||
.btn-primary:hover:not(:disabled) { background: #1d3a9e; }
|
||||
.btn-primary:disabled { opacity: 0.55; cursor: not-allowed; }
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue