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
|
|
@ -42,11 +42,22 @@ export interface Mitglied {
|
||||||
mandatsdatum?: string;
|
mandatsdatum?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Rolle = 'admin' | 'trainer';
|
||||||
|
|
||||||
export interface Gruppe {
|
export interface Gruppe {
|
||||||
id: string;
|
id: string;
|
||||||
verein_id: string;
|
verein_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
beschreibung?: string;
|
beschreibung?: string;
|
||||||
|
trainer_ids: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Einladung {
|
||||||
|
id: string;
|
||||||
|
verein_id: string;
|
||||||
|
rolle: Rolle;
|
||||||
|
token: string;
|
||||||
|
genutzt: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Beitrag {
|
export interface Beitrag {
|
||||||
|
|
|
||||||
|
|
@ -67,14 +67,18 @@
|
||||||
return Uint8Array.from([...raw].map((c) => c.charCodeAt(0)));
|
return Uint8Array.from([...raw].map((c) => c.charCodeAt(0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
const navItems: { href: string; label: string; icon: IconName }[] = [
|
const isAdmin = () => !pb.authStore.record?.rolle || pb.authStore.record?.rolle === 'admin';
|
||||||
{ href: '/', label: 'Übersicht', icon: 'house' },
|
|
||||||
{ href: '/mitglieder', label: 'Mitglieder', icon: 'users' },
|
const allNavItems: { href: string; label: string; icon: IconName; adminOnly?: boolean }[] = [
|
||||||
{ href: '/termine', label: 'Termine', icon: 'calendar' },
|
{ href: '/', label: 'Übersicht', icon: 'house' },
|
||||||
{ href: '/beitraege', label: 'Beiträge', icon: 'currency-eur' },
|
{ href: '/mitglieder', label: 'Mitglieder', icon: 'users' },
|
||||||
{ href: '/nachrichten', label: 'Nachrichten', icon: 'envelope' },
|
{ href: '/termine', label: 'Termine', icon: 'calendar' },
|
||||||
{ href: '/einstellungen', label: 'Einstellungen', icon: 'gear' },
|
{ href: '/beitraege', label: 'Beiträge', icon: 'currency-eur', adminOnly: true },
|
||||||
|
{ href: '/nachrichten', label: 'Nachrichten', icon: 'envelope' },
|
||||||
|
{ href: '/einstellungen', label: 'Einstellungen', icon: 'gear' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const navItems = $derived(allNavItems.filter(i => !i.adminOnly || isAdmin()));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { pb } from '$lib/pb';
|
import { pb } from '$lib/pb';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { generatePain008, downloadXml, minEinzugsdatum, type SepaPosition } from '$lib/sepa';
|
import { generatePain008, downloadXml, minEinzugsdatum, type SepaPosition } from '$lib/sepa';
|
||||||
import type { Beitrag, Mitglied, Verein } from '$lib/types';
|
import type { Beitrag, Mitglied, Verein } from '$lib/types';
|
||||||
|
|
@ -35,6 +36,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
if (pb.authStore.record?.rolle === 'trainer') { goto('/'); return; }
|
||||||
const vid = pb.authStore.record?.verein_id as string;
|
const vid = pb.authStore.record?.verein_id as string;
|
||||||
[beitraege, verein] = await Promise.all([
|
[beitraege, verein] = await Promise.all([
|
||||||
pb.collection('beitraege').getFullList<Beitrag>({ sort: 'name' }),
|
pb.collection('beitraege').getFullList<Beitrag>({ sort: 'name' }),
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
import { pb } from '$lib/pb';
|
import { pb } from '$lib/pb';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { Verein } from '$lib/types';
|
import type { Verein, Gruppe } from '$lib/types';
|
||||||
|
|
||||||
|
const isAdmin = () => !pb.authStore.record?.rolle || pb.authStore.record?.rolle === 'admin';
|
||||||
|
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let saving = $state(false);
|
let saving = $state(false);
|
||||||
|
|
@ -24,6 +26,12 @@
|
||||||
let iban = $state('');
|
let iban = $state('');
|
||||||
let bic = $state('');
|
let bic = $state('');
|
||||||
|
|
||||||
|
// Trainer
|
||||||
|
let trainer = $state<any[]>([]);
|
||||||
|
let gruppen = $state<Gruppe[]>([]);
|
||||||
|
let einladungUrl = $state('');
|
||||||
|
let einladungKopiert = $state(false);
|
||||||
|
|
||||||
let vereinId = '';
|
let vereinId = '';
|
||||||
|
|
||||||
const bundeslaender = [
|
const bundeslaender = [
|
||||||
|
|
@ -38,7 +46,15 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
vereinId = pb.authStore.record?.verein_id as string;
|
vereinId = pb.authStore.record?.verein_id as string;
|
||||||
const v = await pb.collection('vereine').getOne<Verein>(vereinId);
|
const [v, alleUser, alleGruppen] = await Promise.all([
|
||||||
|
pb.collection('vereine').getOne<Verein>(vereinId),
|
||||||
|
isAdmin()
|
||||||
|
? pb.collection('users').getFullList({ filter: `verein_id = "${vereinId}"` })
|
||||||
|
: Promise.resolve([]),
|
||||||
|
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
|
||||||
|
]);
|
||||||
|
trainer = alleUser.filter((u: any) => u.rolle === 'trainer');
|
||||||
|
gruppen = alleGruppen;
|
||||||
name = v.name ?? '';
|
name = v.name ?? '';
|
||||||
adresse = v.adresse ?? '';
|
adresse = v.adresse ?? '';
|
||||||
plz = v.plz ?? '';
|
plz = v.plz ?? '';
|
||||||
|
|
@ -78,6 +94,28 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function trainerEinladen() {
|
||||||
|
const token = crypto.randomUUID().replace(/-/g, '');
|
||||||
|
await pb.collection('einladungen').create({
|
||||||
|
verein_id: vereinId, rolle: 'trainer', token, genutzt: false,
|
||||||
|
});
|
||||||
|
einladungUrl = `${window.location.origin}/invite/${token}`;
|
||||||
|
einladungKopiert = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function trainerEntfernen(uid: string) {
|
||||||
|
if (!confirm('Trainer wirklich entfernen?')) return;
|
||||||
|
await pb.collection('users').update(uid, { rolle: null, verein_id: null });
|
||||||
|
trainer = trainer.filter(t => t.id !== uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
function trainerGruppen(uid: string): string {
|
||||||
|
return gruppen
|
||||||
|
.filter(g => (g.trainer_ids ?? []).includes(uid))
|
||||||
|
.map(g => g.name)
|
||||||
|
.join(', ') || '—';
|
||||||
|
}
|
||||||
|
|
||||||
function abmelden() {
|
function abmelden() {
|
||||||
pb.authStore.clear();
|
pb.authStore.clear();
|
||||||
goto('/login');
|
goto('/login');
|
||||||
|
|
@ -168,6 +206,44 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{#if isAdmin()}
|
||||||
|
<section>
|
||||||
|
<h2>Trainer</h2>
|
||||||
|
{#if trainer.length === 0}
|
||||||
|
<p class="sepa-hint">Noch keine Trainer eingeladen.</p>
|
||||||
|
{:else}
|
||||||
|
<ul class="trainer-liste">
|
||||||
|
{#each trainer as t (t.id)}
|
||||||
|
<li>
|
||||||
|
<div class="trainer-info">
|
||||||
|
<span class="trainer-name">{t.name}</span>
|
||||||
|
<span class="trainer-gruppen">{trainerGruppen(t.id)}</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-remove" onclick={() => trainerEntfernen(t.id)}>Entfernen</button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button type="button" class="btn-einladen" onclick={trainerEinladen}>
|
||||||
|
+ Trainer einladen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if einladungUrl}
|
||||||
|
<div class="invite-box">
|
||||||
|
<p class="invite-label">Einladungslink – einmalig verwendbar:</p>
|
||||||
|
<div class="invite-url">{einladungUrl}</div>
|
||||||
|
<button type="button" class="btn-kopieren" onclick={async () => {
|
||||||
|
await navigator.clipboard.writeText(einladungUrl);
|
||||||
|
einladungKopiert = true;
|
||||||
|
}}>
|
||||||
|
{einladungKopiert ? 'Kopiert ✓' : 'Link kopieren'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<p class="error">{error}</p>
|
<p class="error">{error}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -252,6 +328,47 @@
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.trainer-liste {
|
||||||
|
list-style: none; padding: 0; margin: 0 0 0.75rem;
|
||||||
|
border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden;
|
||||||
|
}
|
||||||
|
.trainer-liste li {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
gap: 0.75rem; padding: 0.7rem 0.85rem;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
}
|
||||||
|
.trainer-liste li:last-child { border-bottom: none; }
|
||||||
|
.trainer-info { display: flex; flex-direction: column; gap: 0.1rem; }
|
||||||
|
.trainer-name { font-size: 0.9rem; font-weight: 600; color: #1e293b; }
|
||||||
|
.trainer-gruppen { font-size: 0.75rem; color: #94a3b8; }
|
||||||
|
.btn-remove {
|
||||||
|
padding: 0.3rem 0.65rem; background: none;
|
||||||
|
border: 1px solid #fca5a5; border-radius: 6px;
|
||||||
|
font-size: 0.78rem; color: #dc2626; cursor: pointer; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.btn-einladen {
|
||||||
|
width: 100%; padding: 0.65rem; background: none;
|
||||||
|
border: 1.5px dashed #e2e8f0; border-radius: 8px;
|
||||||
|
font-size: 0.9rem; color: #1e40af; cursor: pointer;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
.btn-einladen:hover { border-color: #1e40af; }
|
||||||
|
.invite-box {
|
||||||
|
margin-top: 0.75rem; padding: 0.85rem;
|
||||||
|
background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px;
|
||||||
|
display: flex; flex-direction: column; gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.invite-label { font-size: 0.78rem; color: #64748b; margin: 0; }
|
||||||
|
.invite-url {
|
||||||
|
font-size: 0.78rem; font-family: monospace;
|
||||||
|
color: #1e293b; word-break: break-all;
|
||||||
|
}
|
||||||
|
.btn-kopieren {
|
||||||
|
padding: 0.4rem 0.75rem; background: #1e40af; color: #fff;
|
||||||
|
border: none; border-radius: 6px; font-size: 0.82rem;
|
||||||
|
cursor: pointer; align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-logout {
|
.btn-logout {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
|
|
|
||||||
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>
|
||||||
92
pocketbase/pb_migrations/1779230500_rollen.js
Normal file
92
pocketbase/pb_migrations/1779230500_rollen.js
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
|
||||||
|
// Users: +rolle, listRule für verein-weite Sichtbarkeit
|
||||||
|
{
|
||||||
|
const c = app.findCollectionByNameOrId("_pb_users_auth_")
|
||||||
|
c.fields.addAt(99, new Field({
|
||||||
|
"type": "select", "id": "select2001000070", "name": "rolle",
|
||||||
|
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||||
|
"maxSelect": 1, "values": ["admin", "trainer"]
|
||||||
|
}))
|
||||||
|
c.listRule = "@request.auth.verein_id = verein_id"
|
||||||
|
app.save(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gruppen: +trainer_ids (welche User betreuen diese Gruppe)
|
||||||
|
{
|
||||||
|
const c = app.findCollectionByNameOrId("pbc_3099069179")
|
||||||
|
c.fields.addAt(99, new Field({
|
||||||
|
"type": "relation", "id": "relation2001000071", "name": "trainer_ids",
|
||||||
|
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||||
|
"cascadeDelete": false, "collectionId": "_pb_users_auth_",
|
||||||
|
"maxSelect": 99, "minSelect": 0
|
||||||
|
}))
|
||||||
|
app.save(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Einladungen-Collection
|
||||||
|
{
|
||||||
|
const c = new Collection({
|
||||||
|
"createRule": "@request.auth.verein_id = verein_id",
|
||||||
|
"deleteRule": "@request.auth.verein_id = verein_id",
|
||||||
|
"listRule": "@request.auth.verein_id = verein_id",
|
||||||
|
"viewRule": "",
|
||||||
|
"updateRule": "@request.auth.verein_id = verein_id",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "[a-z0-9]{15}", "id": "text3208210256",
|
||||||
|
"max": 15, "min": 15, "name": "id", "pattern": "^[a-z0-9]+$",
|
||||||
|
"primaryKey": true, "required": true, "system": true, "type": "text",
|
||||||
|
"help": "", "hidden": false, "presentable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "relation", "id": "relation2001000072", "name": "verein_id",
|
||||||
|
"help": "", "hidden": false, "presentable": false, "required": true, "system": false,
|
||||||
|
"cascadeDelete": true, "collectionId": "pbc_3589557411", "maxSelect": 1, "minSelect": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select", "id": "select2001000073", "name": "rolle",
|
||||||
|
"help": "", "hidden": false, "presentable": false, "required": true, "system": false,
|
||||||
|
"maxSelect": 1, "values": ["admin", "trainer"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text", "id": "text2001000074", "name": "token",
|
||||||
|
"help": "", "hidden": false, "presentable": false, "required": true, "system": false,
|
||||||
|
"autogeneratePattern": "", "min": 0, "max": 0, "pattern": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bool", "id": "bool2001000075", "name": "genutzt",
|
||||||
|
"help": "", "hidden": false, "presentable": false, "required": false, "system": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "pbc_einladungen",
|
||||||
|
"indexes": ["CREATE UNIQUE INDEX idx_einladungen_token ON einladungen (token)"],
|
||||||
|
"name": "einladungen",
|
||||||
|
"system": false,
|
||||||
|
"type": "base"
|
||||||
|
})
|
||||||
|
app.save(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
}, (app) => {
|
||||||
|
|
||||||
|
{
|
||||||
|
const c = app.findCollectionByNameOrId("_pb_users_auth_")
|
||||||
|
c.fields.removeById("select2001000070")
|
||||||
|
c.listRule = ""
|
||||||
|
app.save(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const c = app.findCollectionByNameOrId("pbc_3099069179")
|
||||||
|
c.fields.removeById("relation2001000071")
|
||||||
|
app.save(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const c = app.findCollectionByNameOrId("pbc_einladungen")
|
||||||
|
app.delete(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue