Migrate: PocketBase → SvelteKit + better-sqlite3 + JWT
Vollständige Migration weg von PocketBase. Neuer Stack: - better-sqlite3 (WAL-Mode, direkte SQLite-Abfragen) - jose (JWT HS256, 30 Tage Laufzeit) - bcryptjs (Passwort-Hashing, cost 12) Neue Dateien: - src/lib/server/db.ts → SQLite-Singleton + Schema + Helpers - src/lib/server/auth.ts → JWT sign/verify, bcrypt, Bearer-Token - src/lib/user.ts → Svelte-Store (ersetzt pb.authStore) - src/lib/api.ts → fetch()-Wrapper (ersetzt pb.collection()) - src/app.d.ts → App.Locals TypeScript-Deklaration - 30 neue API-Routes unter src/routes/api/ Entfernt: - Abhängigkeit von pocketbase npm-Paket (bleibt im package.json bis alle Referenzen bereinigt sind) - PocketBase-Container aus docker-compose.yml - Migrations und Hooks aus Deploy-Pipeline Docker: Ein einziger Container, SQLite-Volume unter /data/ Makefile: PocketBase-spezifische Targets entfernt seed.js: Komplett neu für neue REST-API
This commit is contained in:
parent
61c430f2e6
commit
39981c0d17
58 changed files with 2313 additions and 651 deletions
15
app/src/app.d.ts
vendored
Normal file
15
app/src/app.d.ts
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
declare global {
|
||||
namespace App {
|
||||
interface Locals {
|
||||
user: {
|
||||
id: string;
|
||||
verein_id: string;
|
||||
rolle: string | null;
|
||||
name: string;
|
||||
email: string;
|
||||
} | null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
66
app/src/lib/api.ts
Normal file
66
app/src/lib/api.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { get } from 'svelte/store';
|
||||
import { user } from './user';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
function token() { return get(user)?.token ?? ''; }
|
||||
|
||||
function headers(extra: Record<string, string> = {}): Record<string, string> {
|
||||
return { Authorization: `Bearer ${token()}`, ...extra };
|
||||
}
|
||||
|
||||
async function handleRes<T>(res: Response): Promise<T> {
|
||||
if (res.status === 401) { user.clear(); goto('/login'); throw new Error('Nicht angemeldet'); }
|
||||
if (!res.ok) {
|
||||
const e = await res.json().catch(() => ({}));
|
||||
throw new Error((e as { message?: string }).message || res.statusText);
|
||||
}
|
||||
if (res.status === 204) return undefined as T;
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
async get<T>(path: string, query: Record<string, string> = {}): Promise<T> {
|
||||
const url = new URL('/api' + path, location.origin);
|
||||
Object.entries(query).forEach(([k, v]) => v !== undefined && url.searchParams.set(k, v));
|
||||
return handleRes<T>(await fetch(url.toString(), { headers: headers() }));
|
||||
},
|
||||
|
||||
async post<T>(path: string, data?: unknown): Promise<T> {
|
||||
return handleRes<T>(await fetch('/api' + path, {
|
||||
method: 'POST',
|
||||
headers: headers({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(data ?? {}),
|
||||
}));
|
||||
},
|
||||
|
||||
async put<T>(path: string, data?: unknown): Promise<T> {
|
||||
return handleRes<T>(await fetch('/api' + path, {
|
||||
method: 'PUT',
|
||||
headers: headers({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(data ?? {}),
|
||||
}));
|
||||
},
|
||||
|
||||
async patch<T>(path: string, data?: unknown): Promise<T> {
|
||||
return handleRes<T>(await fetch('/api' + path, {
|
||||
method: 'PATCH',
|
||||
headers: headers({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(data ?? {}),
|
||||
}));
|
||||
},
|
||||
|
||||
async del<T = void>(path: string): Promise<T> {
|
||||
return handleRes<T>(await fetch('/api' + path, { method: 'DELETE', headers: headers() }));
|
||||
},
|
||||
|
||||
async postForm<T>(path: string, form: FormData): Promise<T> {
|
||||
return handleRes<T>(await fetch('/api' + path, {
|
||||
method: 'POST', headers: headers(), body: form,
|
||||
}));
|
||||
},
|
||||
|
||||
fileUrl(verein_id: string, record_id: string, filename: string, thumb = false): string {
|
||||
const base = `/api/files/${verein_id}/${record_id}/${filename}`;
|
||||
return thumb ? base + '?thumb=1' : base;
|
||||
}
|
||||
};
|
||||
52
app/src/lib/server/auth.ts
Normal file
52
app/src/lib/server/auth.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { SignJWT, jwtVerify } from 'jose';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
const JWT_SECRET = new TextEncoder().encode(
|
||||
process.env.JWT_SECRET || 'vereinshaus-dev-secret-change-in-production'
|
||||
);
|
||||
|
||||
export interface JwtPayload {
|
||||
sub: string;
|
||||
verein_id: string;
|
||||
rolle: string | null;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export async function signJwt(payload: JwtPayload): Promise<string> {
|
||||
return new SignJWT({ ...payload })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setExpirationTime('30d')
|
||||
.sign(JWT_SECRET);
|
||||
}
|
||||
|
||||
export async function verifyJwt(token: string): Promise<JwtPayload | null> {
|
||||
try {
|
||||
const { payload } = await jwtVerify(token, JWT_SECRET);
|
||||
return payload as unknown as JwtPayload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return bcrypt.hash(password, 12);
|
||||
}
|
||||
|
||||
export async function checkPassword(password: string, hash: string): Promise<boolean> {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
export function bearerToken(request: Request): string | null {
|
||||
const h = request.headers.get('Authorization');
|
||||
return h?.startsWith('Bearer ') ? h.slice(7) : null;
|
||||
}
|
||||
|
||||
export async function requireAuth(request: Request): Promise<JwtPayload> {
|
||||
const token = bearerToken(request);
|
||||
if (!token) throw error(401, 'Nicht authentifiziert');
|
||||
const user = await verifyJwt(token);
|
||||
if (!user) throw error(401, 'Ungültiger Token');
|
||||
return user;
|
||||
}
|
||||
219
app/src/lib/server/db.ts
Normal file
219
app/src/lib/server/db.ts
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
import Database from 'better-sqlite3';
|
||||
import { mkdirSync, existsSync } from 'fs';
|
||||
import { dirname } from 'path';
|
||||
|
||||
const DB_PATH = process.env.DB_PATH || './data/vereinshaus.db';
|
||||
|
||||
const SCHEMA = `
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
PRAGMA busy_timeout = 5000;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS vereine (
|
||||
id TEXT PRIMARY KEY NOT NULL DEFAULT (lower(hex(randomblob(8)))),
|
||||
name TEXT NOT NULL,
|
||||
adresse TEXT, plz TEXT, ort TEXT, bundesland TEXT,
|
||||
plan TEXT NOT NULL DEFAULT 'free',
|
||||
dosb_mitglied INTEGER NOT NULL DEFAULT 0,
|
||||
email TEXT, telefon TEXT, website TEXT,
|
||||
glaeubigerid TEXT, iban TEXT, bic TEXT,
|
||||
created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
||||
updated TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY NOT NULL DEFAULT (lower(hex(randomblob(8)))),
|
||||
verein_id TEXT NOT NULL REFERENCES vereine(id) ON DELETE CASCADE,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
rolle TEXT DEFAULT NULL,
|
||||
created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
||||
updated TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gruppen (
|
||||
id TEXT PRIMARY KEY NOT NULL DEFAULT (lower(hex(randomblob(8)))),
|
||||
verein_id TEXT NOT NULL REFERENCES vereine(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
beschreibung TEXT,
|
||||
trainer_ids TEXT NOT NULL DEFAULT '[]',
|
||||
created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
||||
updated TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mitglieder (
|
||||
id TEXT PRIMARY KEY NOT NULL DEFAULT (lower(hex(randomblob(8)))),
|
||||
verein_id TEXT NOT NULL REFERENCES vereine(id) ON DELETE CASCADE,
|
||||
vorname TEXT NOT NULL,
|
||||
nachname TEXT NOT NULL,
|
||||
email TEXT, telefon TEXT,
|
||||
geburtsdatum TEXT, eintrittsdatum TEXT, austrittsdatum TEXT,
|
||||
strasse TEXT, plz TEXT, ort TEXT,
|
||||
iban TEXT, bic TEXT,
|
||||
gruppe_ids TEXT NOT NULL DEFAULT '[]',
|
||||
status TEXT NOT NULL DEFAULT 'aktiv',
|
||||
notizen TEXT,
|
||||
mandatsreferenz TEXT, mandatsdatum TEXT,
|
||||
created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
||||
updated TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS beitraege (
|
||||
id TEXT PRIMARY KEY NOT NULL DEFAULT (lower(hex(randomblob(8)))),
|
||||
verein_id TEXT NOT NULL REFERENCES vereine(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
betrag REAL NOT NULL,
|
||||
rhythmus TEXT NOT NULL DEFAULT 'jaehrlich',
|
||||
beschreibung TEXT,
|
||||
created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
||||
updated TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS einzuege (
|
||||
id TEXT PRIMARY KEY NOT NULL DEFAULT (lower(hex(randomblob(8)))),
|
||||
verein_id TEXT NOT NULL REFERENCES vereine(id) ON DELETE CASCADE,
|
||||
mitglied_id TEXT NOT NULL REFERENCES mitglieder(id) ON DELETE CASCADE,
|
||||
beitrag_id TEXT NOT NULL REFERENCES beitraege(id),
|
||||
betrag REAL NOT NULL,
|
||||
faellig_am TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'ausstehend',
|
||||
created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
||||
updated TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS veranstaltungsorte (
|
||||
id TEXT PRIMARY KEY NOT NULL DEFAULT (lower(hex(randomblob(8)))),
|
||||
verein_id TEXT NOT NULL REFERENCES vereine(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
adresse TEXT,
|
||||
typ TEXT DEFAULT 'sonstiges',
|
||||
aktiv INTEGER NOT NULL DEFAULT 1,
|
||||
created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
||||
updated TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ort_ausfaelle (
|
||||
id TEXT PRIMARY KEY NOT NULL DEFAULT (lower(hex(randomblob(8)))),
|
||||
ort_id TEXT NOT NULL REFERENCES veranstaltungsorte(id) ON DELETE CASCADE,
|
||||
von TEXT NOT NULL, bis TEXT NOT NULL, grund TEXT,
|
||||
created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
||||
updated TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS termine (
|
||||
id TEXT PRIMARY KEY NOT NULL DEFAULT (lower(hex(randomblob(8)))),
|
||||
verein_id TEXT NOT NULL REFERENCES vereine(id) ON DELETE CASCADE,
|
||||
titel TEXT NOT NULL,
|
||||
beschreibung TEXT,
|
||||
beginn TEXT NOT NULL,
|
||||
ende TEXT,
|
||||
ort TEXT,
|
||||
ort_id TEXT REFERENCES veranstaltungsorte(id) ON DELETE SET NULL,
|
||||
gruppe_ids TEXT NOT NULL DEFAULT '[]',
|
||||
durchfuehrender_id TEXT,
|
||||
verfuegbarkeit TEXT DEFAULT 'offen',
|
||||
rrule TEXT,
|
||||
serie_id TEXT,
|
||||
created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
||||
updated TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS nachrichten (
|
||||
id TEXT PRIMARY KEY NOT NULL DEFAULT (lower(hex(randomblob(8)))),
|
||||
verein_id TEXT NOT NULL REFERENCES vereine(id) ON DELETE CASCADE,
|
||||
autor_id TEXT NOT NULL,
|
||||
betreff TEXT NOT NULL,
|
||||
text TEXT NOT NULL DEFAULT '',
|
||||
gruppe_ids TEXT NOT NULL DEFAULT '[]',
|
||||
gesendet_am TEXT,
|
||||
created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
||||
updated TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS neuigkeiten (
|
||||
id TEXT PRIMARY KEY NOT NULL DEFAULT (lower(hex(randomblob(8)))),
|
||||
verein_id TEXT NOT NULL REFERENCES vereine(id) ON DELETE CASCADE,
|
||||
autor_id TEXT NOT NULL,
|
||||
autor_name TEXT NOT NULL DEFAULT '',
|
||||
text TEXT,
|
||||
medien TEXT NOT NULL DEFAULT '[]',
|
||||
gruppe_ids TEXT NOT NULL DEFAULT '[]',
|
||||
termin_id TEXT REFERENCES termine(id) ON DELETE SET NULL,
|
||||
created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
||||
updated TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS reaktionen (
|
||||
id TEXT PRIMARY KEY NOT NULL DEFAULT (lower(hex(randomblob(8)))),
|
||||
beitrag_id TEXT NOT NULL REFERENCES neuigkeiten(id) ON DELETE CASCADE,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
||||
UNIQUE(beitrag_id, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS einladungen (
|
||||
id TEXT PRIMARY KEY NOT NULL DEFAULT (lower(hex(randomblob(8)))),
|
||||
verein_id TEXT NOT NULL REFERENCES vereine(id) ON DELETE CASCADE,
|
||||
token TEXT NOT NULL UNIQUE DEFAULT (lower(hex(randomblob(16)))),
|
||||
rolle TEXT NOT NULL DEFAULT 'trainer',
|
||||
genutzt INTEGER NOT NULL DEFAULT 0,
|
||||
created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
||||
updated TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
||||
id TEXT PRIMARY KEY NOT NULL DEFAULT (lower(hex(randomblob(8)))),
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
verein_id TEXT NOT NULL REFERENCES vereine(id) ON DELETE CASCADE,
|
||||
endpoint TEXT NOT NULL UNIQUE,
|
||||
p256dh TEXT NOT NULL,
|
||||
auth TEXT NOT NULL,
|
||||
created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
||||
);
|
||||
`;
|
||||
|
||||
let _db: Database.Database | null = null;
|
||||
|
||||
export function getDb(): Database.Database {
|
||||
if (_db) return _db;
|
||||
const dir = dirname(DB_PATH);
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
_db = new Database(DB_PATH);
|
||||
_db.exec(SCHEMA);
|
||||
return _db;
|
||||
}
|
||||
|
||||
export function newId(): string {
|
||||
const bytes = new Uint8Array(8);
|
||||
crypto.getRandomValues(bytes);
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
export function parseArr(val: unknown): string[] {
|
||||
if (Array.isArray(val)) return val;
|
||||
if (typeof val === 'string') { try { return JSON.parse(val); } catch { return []; } }
|
||||
return [];
|
||||
}
|
||||
|
||||
export function toArr(val: unknown): string {
|
||||
return JSON.stringify(Array.isArray(val) ? val : []);
|
||||
}
|
||||
|
||||
export function row<T extends Record<string, unknown>>(r: T): T {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(r)) {
|
||||
if (typeof v === 'string' && (k.endsWith('_ids') || k === 'medien' || k === 'trainer_ids')) {
|
||||
out[k] = parseArr(v);
|
||||
} else if (typeof v === 'number' && (k === 'aktiv' || k === 'dosb_mitglied' || k === 'genutzt')) {
|
||||
out[k] = Boolean(v);
|
||||
} else {
|
||||
out[k] = v;
|
||||
}
|
||||
}
|
||||
return out as T;
|
||||
}
|
||||
|
||||
export function rows<T extends Record<string, unknown>>(rs: T[]): T[] {
|
||||
return rs.map(r => row(r));
|
||||
}
|
||||
34
app/src/lib/user.ts
Normal file
34
app/src/lib/user.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { writable } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export interface AppUser {
|
||||
id: string;
|
||||
verein_id: string;
|
||||
rolle: string | null;
|
||||
name: string;
|
||||
email: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
function createUserStore() {
|
||||
let initial: AppUser | null = null;
|
||||
if (browser) {
|
||||
try { initial = JSON.parse(localStorage.getItem('vh_user') || 'null'); } catch { /* */ }
|
||||
}
|
||||
|
||||
const { subscribe, set } = writable<AppUser | null>(initial);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
set(u: AppUser | null) {
|
||||
if (browser) {
|
||||
if (u) localStorage.setItem('vh_user', JSON.stringify(u));
|
||||
else localStorage.removeItem('vh_user');
|
||||
}
|
||||
set(u);
|
||||
},
|
||||
clear() { this.set(null); }
|
||||
};
|
||||
}
|
||||
|
||||
export const user = createUserStore();
|
||||
|
|
@ -2,39 +2,27 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { pb } from '$lib/pb';
|
||||
import { user } from '$lib/user';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import type { IconName } from '$lib/icons';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
onMount(() => {
|
||||
if (!pb.authStore.isValid) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
if (!pb.authStore.record?.verein_id) {
|
||||
goto('/onboarding');
|
||||
return;
|
||||
}
|
||||
if (!$user) { goto('/login'); return; }
|
||||
if (!$user.verein_id) { goto('/onboarding'); return; }
|
||||
registerPush();
|
||||
});
|
||||
|
||||
async function registerPush() {
|
||||
if (!('serviceWorker' in navigator) || !('PushManager' in window)) return;
|
||||
|
||||
const permission = await Notification.requestPermission();
|
||||
if (permission !== 'granted') return;
|
||||
|
||||
try {
|
||||
const reg = await navigator.serviceWorker.ready;
|
||||
|
||||
// VAPID public key vom Server holen
|
||||
const keyRes = await fetch('/api/push/key');
|
||||
const { publicKey } = await keyRes.json();
|
||||
if (!publicKey) return;
|
||||
|
||||
// Bestehende oder neue Subscription
|
||||
let sub = await reg.pushManager.getSubscription();
|
||||
if (!sub) {
|
||||
sub = await reg.pushManager.subscribe({
|
||||
|
|
@ -42,18 +30,10 @@
|
|||
applicationServerKey: urlBase64ToUint8Array(publicKey) as BufferSource,
|
||||
});
|
||||
}
|
||||
|
||||
// In PocketBase speichern
|
||||
await fetch('/api/push/subscribe', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: pb.authStore.token,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subscription: sub.toJSON(),
|
||||
userId: pb.authStore.record?.id,
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${$user?.token}` },
|
||||
body: JSON.stringify({ subscription: sub.toJSON(), userId: $user?.id }),
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('[push] Registrierung fehlgeschlagen:', e);
|
||||
|
|
@ -67,7 +47,7 @@
|
|||
return Uint8Array.from([...raw].map((c) => c.charCodeAt(0)));
|
||||
}
|
||||
|
||||
const isAdmin = () => !pb.authStore.record?.rolle || pb.authStore.record?.rolle === 'admin';
|
||||
const isAdmin = () => !$user?.rolle || $user?.rolle === 'admin';
|
||||
|
||||
const allNavItems: { href: string; label: string; icon: IconName; adminOnly?: boolean }[] = [
|
||||
{ href: '/neuigkeiten', label: 'Neuigkeiten', icon: 'images' },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Verein, Termin } from '$lib/types';
|
||||
|
||||
|
|
@ -8,15 +8,12 @@
|
|||
let loading = $state(true);
|
||||
|
||||
onMount(async () => {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
const now = new Date().toISOString();
|
||||
[verein, termine] = await Promise.all([
|
||||
pb.collection('vereine').getOne<Verein>(vid),
|
||||
pb.collection('termine').getList<Termin>(1, 3, {
|
||||
filter: `beginn >= "${now}"`,
|
||||
sort: 'beginn',
|
||||
}).then(r => r.items),
|
||||
api.get<Verein>('/vereine'),
|
||||
api.get<Termin[]>('/termine', { sort: 'beginn', page: '1', perPage: '3' }),
|
||||
]);
|
||||
termine = (termine as any)?.items ?? termine;
|
||||
loading = false;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { user } from '$lib/user';
|
||||
import { get } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { generatePain008, downloadXml, minEinzugsdatum, type SepaPosition } from '$lib/sepa';
|
||||
|
|
@ -41,11 +43,10 @@
|
|||
};
|
||||
|
||||
onMount(async () => {
|
||||
if (pb.authStore.record?.rolle === 'trainer') { goto('/'); return; }
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
if (get(user)?.rolle === 'trainer') { goto('/'); return; }
|
||||
[beitraege, verein] = await Promise.all([
|
||||
pb.collection('beitraege').getFullList<Beitrag>({ sort: 'name' }),
|
||||
pb.collection('vereine').getOne<Verein>(vid).catch(() => null),
|
||||
api.get<Beitrag[]>('/beitraege', { sort: 'name' }),
|
||||
api.get<Verein>('/vereine').catch(() => null),
|
||||
]);
|
||||
loading = false;
|
||||
});
|
||||
|
|
@ -71,13 +72,12 @@
|
|||
}
|
||||
saving = true;
|
||||
try {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
const data = { verein_id: vid, name: fName.trim(), betrag, rhythmus: fRhythmus, beschreibung: fBeschr.trim() };
|
||||
const data = { name: fName.trim(), betrag, rhythmus: fRhythmus, beschreibung: fBeschr.trim() };
|
||||
if (editId) {
|
||||
await pb.collection('beitraege').update(editId, data);
|
||||
await api.put('/beitraege/' + editId, data);
|
||||
beitraege = beitraege.map(b => b.id === editId ? { ...b, ...data } as Beitrag : b);
|
||||
} else {
|
||||
const neu = await pb.collection('beitraege').create<Beitrag>(data);
|
||||
const neu = await api.post<Beitrag>('/beitraege', data);
|
||||
beitraege = [...beitraege, neu].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
showForm = false;
|
||||
|
|
@ -90,7 +90,7 @@
|
|||
|
||||
async function loeschen(b: Beitrag) {
|
||||
if (!confirm(`"${b.name}" wirklich löschen?`)) return;
|
||||
await pb.collection('beitraege').delete(b.id);
|
||||
await api.del('/beitraege/' + b.id);
|
||||
beitraege = beitraege.filter(x => x.id !== b.id);
|
||||
}
|
||||
|
||||
|
|
@ -102,9 +102,7 @@
|
|||
einzugsdatum = minEinzugsdatum();
|
||||
sepaLoading = true;
|
||||
try {
|
||||
const alle = await pb.collection('mitglieder').getFullList<Mitglied>({
|
||||
filter: 'status = "aktiv"', sort: 'nachname,vorname',
|
||||
});
|
||||
const alle = await api.get<Mitglied[]>('/mitglieder', { status: 'aktiv', sort: 'nachname,vorname' });
|
||||
const mit = alle.filter(m => m.iban?.trim());
|
||||
const ohne = alle.length - mit.length;
|
||||
sepaPreview = { mitglieder: mit, ohne };
|
||||
|
|
@ -157,10 +155,9 @@
|
|||
downloadXml(xml, `sepa-einzug-${einzugsdatum}.xml`);
|
||||
|
||||
// Einzüge als "ausstehend" anlegen
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
await Promise.all(
|
||||
sepaPreview.mitglieder.map((m) =>
|
||||
pb.collection('einzuege').create({
|
||||
api.post('/einzuege', {
|
||||
mitglied_id: m.id,
|
||||
beitrag_id: sepaFor!.id,
|
||||
betrag: sepaFor!.betrag,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { user } from '$lib/user';
|
||||
import { get } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Verein, Gruppe } from '$lib/types';
|
||||
|
||||
const isAdmin = () => !pb.authStore.record?.rolle || pb.authStore.record?.rolle === 'admin';
|
||||
const isAdmin = () => { const u = get(user); return !u?.rolle || u.rolle === 'admin'; };
|
||||
|
||||
let loading = $state(true);
|
||||
let saving = $state(false);
|
||||
|
|
@ -60,19 +62,19 @@
|
|||
];
|
||||
|
||||
onMount(async () => {
|
||||
vereinId = pb.authStore.record?.verein_id as string;
|
||||
vereinId = get(user)?.verein_id ?? '';
|
||||
const [v, alleUser, alleGruppen, mitgliederCount] = await Promise.all([
|
||||
pb.collection('vereine').getOne<Verein>(vereinId),
|
||||
api.get<Verein>('/vereine'),
|
||||
isAdmin()
|
||||
? pb.collection('users').getFullList({ filter: `verein_id = "${vereinId}"` })
|
||||
? api.get<any[]>('/users')
|
||||
: Promise.resolve([]),
|
||||
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
|
||||
pb.collection('mitglieder').getList(1, 1, { filter: `verein_id = "${vereinId}"` }).then(r => r.totalItems),
|
||||
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
|
||||
api.get<{ total: number }>('/mitglieder/count').catch(() => ({ total: 0 })),
|
||||
]);
|
||||
trainer = alleUser.filter((u: any) => u.rolle === 'trainer');
|
||||
gruppen = alleGruppen;
|
||||
plan = v.plan ?? 'free';
|
||||
mitgliederAnz = mitgliederCount;
|
||||
mitgliederAnz = (mitgliederCount as any).total ?? 0;
|
||||
name = v.name ?? '';
|
||||
adresse = v.adresse ?? '';
|
||||
plz = v.plz ?? '';
|
||||
|
|
@ -91,7 +93,7 @@
|
|||
if (!name.trim()) { error = 'Vereinsname ist Pflichtfeld.'; return; }
|
||||
error = ''; success = ''; saving = true;
|
||||
try {
|
||||
await pb.collection('vereine').update(vereinId, {
|
||||
await api.patch('/vereine', {
|
||||
name: name.trim(),
|
||||
adresse: adresse.trim() || null,
|
||||
plz: plz.trim() || null,
|
||||
|
|
@ -113,17 +115,14 @@
|
|||
}
|
||||
|
||||
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}`;
|
||||
const inv = await api.post<{ token: string }>('/einladungen', { rolle: 'trainer' });
|
||||
einladungUrl = `${window.location.origin}/invite/${inv.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 });
|
||||
await api.del('/users/' + uid);
|
||||
trainer = trainer.filter(t => t.id !== uid);
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +134,7 @@
|
|||
}
|
||||
|
||||
function abmelden() {
|
||||
pb.authStore.clear();
|
||||
user.clear();
|
||||
goto('/login');
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { onMount } from 'svelte';
|
||||
import Papa from 'papaparse';
|
||||
import type { Mitglied } from '$lib/types';
|
||||
|
|
@ -39,10 +39,9 @@
|
|||
];
|
||||
|
||||
onMount(async () => {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
const [m, v] = await Promise.all([
|
||||
pb.collection('mitglieder').getFullList<Mitglied>({ sort: 'nachname,vorname' }),
|
||||
pb.collection('vereine').getOne<any>(vid),
|
||||
api.get<Mitglied[]>('/mitglieder', { sort: 'nachname,vorname' }),
|
||||
api.get<any>('/vereine'),
|
||||
]);
|
||||
mitglieder = m;
|
||||
vereinName = v.name ?? '';
|
||||
|
|
@ -93,14 +92,13 @@
|
|||
}
|
||||
|
||||
async function exportiereBackup() {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
const [verein, mitgl, gruppen, termine, beitraege, nachrichten] = await Promise.all([
|
||||
pb.collection('vereine').getOne<any>(vid),
|
||||
pb.collection('mitglieder').getFullList(),
|
||||
pb.collection('gruppen').getFullList(),
|
||||
pb.collection('termine').getFullList(),
|
||||
pb.collection('beitraege').getFullList(),
|
||||
pb.collection('nachrichten').getFullList(),
|
||||
api.get<any>('/vereine'),
|
||||
api.get<any[]>('/mitglieder'),
|
||||
api.get<any[]>('/gruppen'),
|
||||
api.get<any[]>('/termine'),
|
||||
api.get<any[]>('/beitraege'),
|
||||
api.get<any[]>('/nachrichten'),
|
||||
]);
|
||||
const backup = {
|
||||
exportiert_am: new Date().toISOString(),
|
||||
|
|
@ -177,12 +175,11 @@
|
|||
|
||||
async function importStarten() {
|
||||
importLaeuft = true;
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
let ok = 0;
|
||||
const fehler: string[] = [];
|
||||
|
||||
for (const row of csvRows) {
|
||||
const record: Record<string, any> = { verein_id: vid, status: 'aktiv' };
|
||||
const record: Record<string, any> = { status: 'aktiv' };
|
||||
for (const [csvSpalte, ziel] of Object.entries(feldMapping)) {
|
||||
if (!ziel) continue;
|
||||
const wert = row[csvSpalte]?.trim() ?? '';
|
||||
|
|
@ -200,7 +197,7 @@
|
|||
continue;
|
||||
}
|
||||
try {
|
||||
await pb.collection('mitglieder').create(record);
|
||||
await api.post('/mitglieder', record);
|
||||
ok++;
|
||||
} catch (e: unknown) {
|
||||
fehler.push(`${record.vorname} ${record.nachname}: ${e instanceof Error ? e.message : 'Fehler'}`);
|
||||
|
|
@ -208,7 +205,7 @@
|
|||
}
|
||||
|
||||
// Mitgliederliste aktualisieren
|
||||
mitglieder = await pb.collection('mitglieder').getFullList<Mitglied>({ sort: 'nachname,vorname' });
|
||||
mitglieder = await api.get<Mitglied[]>('/mitglieder', { sort: 'nachname,vorname' });
|
||||
importResult = { ok, fehler };
|
||||
importPhase = 'done';
|
||||
importLaeuft = false;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let mitglieder = $state<any[]>([]);
|
||||
|
|
@ -9,8 +9,8 @@
|
|||
|
||||
onMount(async () => {
|
||||
[mitglieder, gruppen] = await Promise.all([
|
||||
pb.collection('mitglieder').getFullList({ sort: 'nachname,vorname' }),
|
||||
pb.collection('gruppen').getFullList({ sort: 'name' })
|
||||
api.get<any[]>('/mitglieder', { sort: 'nachname,vorname' }),
|
||||
api.get<any[]>('/gruppen', { sort: 'name' })
|
||||
]);
|
||||
loading = false;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
|
|
@ -61,8 +61,8 @@
|
|||
|
||||
onMount(async () => {
|
||||
const [m, g] = await Promise.all([
|
||||
pb.collection('mitglieder').getOne<Mitglied>(id),
|
||||
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
|
||||
api.get<Mitglied>('/mitglieder/' + id),
|
||||
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
|
||||
]);
|
||||
loadRecord(m);
|
||||
gruppen = g;
|
||||
|
|
@ -78,7 +78,7 @@
|
|||
async function speichern() {
|
||||
error = ''; saving = true;
|
||||
try {
|
||||
await pb.collection('mitglieder').update(id, {
|
||||
await api.put('/mitglieder/' + id, {
|
||||
vorname: vorname.trim(),
|
||||
nachname: nachname.trim(),
|
||||
email: email.trim() || null,
|
||||
|
|
@ -107,7 +107,7 @@
|
|||
|
||||
async function loeschen() {
|
||||
try {
|
||||
await pb.collection('mitglieder').delete(id);
|
||||
await api.del('/mitglieder/' + id);
|
||||
goto('/mitglieder');
|
||||
} catch (e: unknown) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Löschen.';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
let loading = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
gruppen = await pb.collection('gruppen').getFullList({ sort: 'name' });
|
||||
gruppen = await api.get<any[]>('/gruppen', { sort: 'name' });
|
||||
});
|
||||
|
||||
function toggleGruppe(id: string) {
|
||||
|
|
@ -43,9 +43,7 @@
|
|||
async function speichern() {
|
||||
error = ''; loading = true;
|
||||
try {
|
||||
const verein_id = pb.authStore.record?.verein_id as string;
|
||||
await pb.collection('mitglieder').create({
|
||||
verein_id,
|
||||
await api.post('/mitglieder', {
|
||||
vorname: vorname.trim(),
|
||||
nachname: nachname.trim(),
|
||||
email: email.trim() || null,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { user } from '$lib/user';
|
||||
import { get } from 'svelte/store';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Nachricht, Gruppe } from '$lib/types';
|
||||
|
||||
|
|
@ -18,8 +20,8 @@
|
|||
|
||||
onMount(async () => {
|
||||
[nachrichten, gruppen] = await Promise.all([
|
||||
pb.collection('nachrichten').getFullList<Nachricht>({ sort: '-gesendet_am' }),
|
||||
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
|
||||
api.get<Nachricht[]>('/nachrichten', { sort: '-gesendet_am' }),
|
||||
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
|
||||
]);
|
||||
loading = false;
|
||||
});
|
||||
|
|
@ -43,12 +45,7 @@
|
|||
sendError = ''; sending = true;
|
||||
|
||||
try {
|
||||
const verein_id = pb.authStore.record?.verein_id as string;
|
||||
const autor_id = pb.authStore.record?.id as string;
|
||||
|
||||
const record = await pb.collection('nachrichten').create<Nachricht>({
|
||||
verein_id,
|
||||
autor_id,
|
||||
const record = await api.post<Nachricht>('/nachrichten', {
|
||||
betreff: fBetreff.trim(),
|
||||
text: fText.trim(),
|
||||
gruppe_ids: fGruppeIds,
|
||||
|
|
@ -63,7 +60,7 @@
|
|||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: pb.authStore.token,
|
||||
Authorization: `Bearer ${get(user)?.token ?? ''}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
titel: fBetreff.trim(),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { user } from '$lib/user';
|
||||
import { get } from 'svelte/store';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Neuigkeit, Gruppe, Termin } from '$lib/types';
|
||||
|
||||
const canPost = () => !pb.authStore.record?.rolle || pb.authStore.record?.rolle !== null;
|
||||
const userId = () => pb.authStore.record?.id as string;
|
||||
const canPost = () => true; // alle eingeloggten User dürfen posten
|
||||
const userId = () => get(user)?.id as string;
|
||||
|
||||
let neuigkeiten = $state<Neuigkeit[]>([]);
|
||||
let gruppen = $state<Gruppe[]>([]);
|
||||
|
|
@ -28,31 +30,25 @@
|
|||
let ladeError = $state('');
|
||||
|
||||
onMount(async () => {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
try {
|
||||
// Queries einzeln damit ein Fehler sichtbar wird
|
||||
const [nList, gList] = await Promise.all([
|
||||
pb.collection('neuigkeiten').getFullList<Neuigkeit>({
|
||||
sort: '-created',
|
||||
}),
|
||||
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
|
||||
api.get<Neuigkeit[]>('/neuigkeiten', { sort: '-created' }),
|
||||
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
|
||||
]);
|
||||
neuigkeiten = nList;
|
||||
gruppen = gList;
|
||||
|
||||
// Termine der letzten 30 Tage + zukünftige
|
||||
try {
|
||||
const von = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().replace('T', ' ');
|
||||
termine = await pb.collection('termine').getFullList<Termin>({
|
||||
filter: `beginn >= '${von}'`, sort: '-beginn',
|
||||
});
|
||||
termine = await api.get<Termin[]>('/termine', { sort: '-beginn' });
|
||||
} catch { termine = []; }
|
||||
|
||||
// Reaktionen – separat damit Fehler nicht alles blockiert
|
||||
try {
|
||||
const [rList, meineList] = await Promise.all([
|
||||
pb.collection('reaktionen').getFullList({ filter: `beitrag_id.verein_id = "${vid}"` }),
|
||||
pb.collection('reaktionen').getFullList({ filter: `user_id = "${userId()}"` }),
|
||||
api.get<any[]>('/reaktionen'),
|
||||
api.get<any[]>('/reaktionen', { meine: 'true' }),
|
||||
]);
|
||||
const counts: Record<string, number> = {};
|
||||
for (const r of rList) counts[r.beitrag_id] = (counts[r.beitrag_id] ?? 0) + 1;
|
||||
|
|
@ -96,11 +92,9 @@
|
|||
}
|
||||
formError = ''; saving = true;
|
||||
try {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
const u = get(user);
|
||||
const form = new FormData();
|
||||
form.append('verein_id', vid);
|
||||
form.append('autor_id', userId());
|
||||
form.append('autor_name', pb.authStore.record?.name ?? '');
|
||||
form.append('autor_name', u?.name ?? '');
|
||||
if (fText.trim()) form.append('text', fText.trim());
|
||||
if (fTerminId) form.append('termin_id', fTerminId);
|
||||
for (const id of fGruppeIds) form.append('gruppe_ids', id);
|
||||
|
|
@ -108,7 +102,7 @@
|
|||
for (const file of Array.from(fDateien)) form.append('medien', file);
|
||||
}
|
||||
|
||||
const neu = await pb.collection('neuigkeiten').create<Neuigkeit>(form);
|
||||
const neu = await api.postForm<Neuigkeit>('/neuigkeiten', form);
|
||||
neuigkeiten = [neu, ...neuigkeiten];
|
||||
showForm = false;
|
||||
fText = ''; fGruppeIds = []; fTerminId = ''; fDateien = null; previews = [];
|
||||
|
|
@ -121,26 +115,24 @@
|
|||
|
||||
async function loeschen(n: Neuigkeit) {
|
||||
if (!confirm('Beitrag löschen?')) return;
|
||||
await pb.collection('neuigkeiten').delete(n.id);
|
||||
await api.del('/neuigkeiten/' + n.id);
|
||||
neuigkeiten = neuigkeiten.filter(x => x.id !== n.id);
|
||||
}
|
||||
|
||||
async function toggleReaktion(n: Neuigkeit) {
|
||||
if (meineReaktion[n.id]) {
|
||||
await pb.collection('reaktionen').delete(meineReaktion[n.id]);
|
||||
await api.del('/reaktionen/' + meineReaktion[n.id]);
|
||||
meineReaktion = { ...meineReaktion, [n.id]: '' };
|
||||
reaktionen = { ...reaktionen, [n.id]: Math.max(0, (reaktionen[n.id] ?? 1) - 1) };
|
||||
} else {
|
||||
const r = await pb.collection('reaktionen').create({
|
||||
beitrag_id: n.id, user_id: userId(),
|
||||
});
|
||||
const r = await api.post<{ id: string }>('/reaktionen', { beitrag_id: n.id });
|
||||
meineReaktion = { ...meineReaktion, [n.id]: r.id };
|
||||
reaktionen = { ...reaktionen, [n.id]: (reaktionen[n.id] ?? 0) + 1 };
|
||||
}
|
||||
}
|
||||
|
||||
function mediaUrl(n: Neuigkeit, datei: string, thumb = false): string {
|
||||
return pb.files.getURL(n as any, datei, thumb ? { thumb: '400x300' } : {});
|
||||
return api.fileUrl(n.verein_id, n.id, datei, thumb);
|
||||
}
|
||||
|
||||
function isVideo(datei: string): boolean {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Veranstaltungsort, OrtAusfall } from '$lib/types';
|
||||
|
||||
|
|
@ -31,10 +31,9 @@
|
|||
};
|
||||
|
||||
onMount(async () => {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
[orte, ausfaelle] = await Promise.all([
|
||||
pb.collection('veranstaltungsorte').getFullList<Veranstaltungsort>({ sort: 'name', filter: `verein_id = "${vid}"` }),
|
||||
pb.collection('ort_ausfaelle').getFullList<OrtAusfall>({ sort: 'von' }),
|
||||
api.get<Veranstaltungsort[]>('/veranstaltungsorte', { sort: 'name' }),
|
||||
api.get<OrtAusfall[]>('/ort_ausfaelle', { sort: 'von' }),
|
||||
]);
|
||||
loading = false;
|
||||
});
|
||||
|
|
@ -54,13 +53,12 @@
|
|||
if (!fName.trim()) { ortError = 'Name ist Pflichtfeld.'; return; }
|
||||
ortError = ''; ortSaving = true;
|
||||
try {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
const data = { verein_id: vid, name: fName.trim(), adresse: fAdresse.trim() || null, typ: fTyp, aktiv: fAktiv };
|
||||
const data = { name: fName.trim(), adresse: fAdresse.trim() || null, typ: fTyp, aktiv: fAktiv };
|
||||
if (editOrtId) {
|
||||
const u = await pb.collection('veranstaltungsorte').update<Veranstaltungsort>(editOrtId, data);
|
||||
const u = await api.put<Veranstaltungsort>('/veranstaltungsorte/' + editOrtId, data);
|
||||
orte = orte.map(o => o.id === editOrtId ? u : o);
|
||||
} else {
|
||||
const n = await pb.collection('veranstaltungsorte').create<Veranstaltungsort>(data);
|
||||
const n = await api.post<Veranstaltungsort>('/veranstaltungsorte', data);
|
||||
orte = [...orte, n].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
showOrtForm = false;
|
||||
|
|
@ -73,7 +71,7 @@
|
|||
|
||||
async function ortLoeschen(id: string) {
|
||||
if (!confirm('Ort wirklich löschen? Alle verknüpften Termine verlieren die Ortzuordnung.')) return;
|
||||
await pb.collection('veranstaltungsorte').delete(id);
|
||||
await api.del('/veranstaltungsorte/' + id);
|
||||
orte = orte.filter(o => o.id !== id);
|
||||
ausfaelle = ausfaelle.filter(a => a.ort_id !== id);
|
||||
}
|
||||
|
|
@ -88,7 +86,7 @@
|
|||
if (aVon > aBis) { ausfallError = 'Bis muss nach Von liegen.'; return; }
|
||||
ausfallError = ''; ausfallSaving = true;
|
||||
try {
|
||||
const n = await pb.collection('ort_ausfaelle').create<OrtAusfall>({
|
||||
const n = await api.post<OrtAusfall>('/ort_ausfaelle', {
|
||||
ort_id: aOrtId, von: aVon, bis: aBis, grund: aGrund.trim() || null,
|
||||
});
|
||||
ausfaelle = [...ausfaelle, n].sort((a, b) => a.von.localeCompare(b.von));
|
||||
|
|
@ -101,7 +99,7 @@
|
|||
}
|
||||
|
||||
async function ausfallLoeschen(id: string) {
|
||||
await pb.collection('ort_ausfaelle').delete(id);
|
||||
await api.del('/ort_ausfaelle/' + id);
|
||||
ausfaelle = ausfaelle.filter(a => a.id !== id);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { user } from '$lib/user';
|
||||
import { get } from 'svelte/store';
|
||||
import { onMount } from 'svelte';
|
||||
import { RRule } from 'rrule';
|
||||
import { Calendar, TimeGrid, DayGrid } from '@event-calendar/core';
|
||||
import '@event-calendar/core/index.css';
|
||||
import type { Termin, Gruppe, Verfuegbarkeit, Veranstaltungsort, OrtAusfall } from '$lib/types';
|
||||
|
||||
const isAdmin = () => !pb.authStore.record?.rolle || pb.authStore.record?.rolle === 'admin';
|
||||
const userId = () => pb.authStore.record?.id as string;
|
||||
const isAdmin = () => { const u = get(user); return !u?.rolle || u.rolle === 'admin'; };
|
||||
const userId = () => get(user)?.id as string;
|
||||
|
||||
let termine = $state<Termin[]>([]);
|
||||
let gruppen = $state<Gruppe[]>([]);
|
||||
|
|
@ -59,15 +61,14 @@
|
|||
);
|
||||
|
||||
onMount(async () => {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
[termine, gruppen, alleUser, orte, ausfaelle] = await Promise.all([
|
||||
pb.collection('termine').getFullList<Termin>({ sort: 'beginn' }),
|
||||
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
|
||||
api.get<Termin[]>('/termine', { sort: 'beginn' }),
|
||||
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
|
||||
isAdmin()
|
||||
? pb.collection('users').getFullList({ filter: `verein_id = "${vid}" && rolle = "trainer"` })
|
||||
? api.get<any[]>('/users', { rolle: 'trainer' })
|
||||
: Promise.resolve([]),
|
||||
pb.collection('veranstaltungsorte').getFullList<Veranstaltungsort>({ sort: 'name', filter: `verein_id = "${vid}" && aktiv = true` }),
|
||||
pb.collection('ort_ausfaelle').getFullList<OrtAusfall>({ sort: 'von' }),
|
||||
api.get<Veranstaltungsort[]>('/veranstaltungsorte', { sort: 'name', aktiv: 'true' }),
|
||||
api.get<OrtAusfall[]>('/ort_ausfaelle', { sort: 'von' }),
|
||||
]);
|
||||
loading = false;
|
||||
});
|
||||
|
|
@ -110,9 +111,7 @@
|
|||
}
|
||||
|
||||
function generiereTerminDaten(beginn: Date, rruleStr: string | null) {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
return {
|
||||
verein_id: vid,
|
||||
titel: fTitel.trim(),
|
||||
beschreibung: fBeschr.trim() || null,
|
||||
ort: fOrtId ? null : (fOrt.trim() || null),
|
||||
|
|
@ -146,7 +145,7 @@
|
|||
|
||||
const neu = await Promise.all(
|
||||
dates.map(d =>
|
||||
pb.collection('termine').create<Termin>({
|
||||
api.post<Termin>('/termine', {
|
||||
...generiereTerminDaten(d, rruleStr),
|
||||
beginn: d.toISOString(),
|
||||
ende: fEnde ? new Date(d.getTime() + dauer).toISOString() : null,
|
||||
|
|
@ -162,10 +161,10 @@
|
|||
ende: fEnde ? fromLocal(fEnde) : null,
|
||||
};
|
||||
if (editId) {
|
||||
const updated = await pb.collection('termine').update<Termin>(editId, data);
|
||||
const updated = await api.put<Termin>('/termine/' + editId, data);
|
||||
termine = termine.map(t => t.id === editId ? updated : t);
|
||||
} else {
|
||||
const neu = await pb.collection('termine').create<Termin>(data);
|
||||
const neu = await api.post<Termin>('/termine', data);
|
||||
termine = [...termine, neu];
|
||||
}
|
||||
}
|
||||
|
|
@ -180,10 +179,10 @@
|
|||
async function loeschen(t: Termin, ganzeSerieLoeschen: boolean) {
|
||||
if (ganzeSerieLoeschen && t.serie_id) {
|
||||
const serie = termine.filter(x => x.serie_id === t.serie_id);
|
||||
await Promise.all(serie.map(s => pb.collection('termine').delete(s.id)));
|
||||
await Promise.all(serie.map(s => api.del('/termine/' + s.id)));
|
||||
termine = termine.filter(x => x.serie_id !== t.serie_id);
|
||||
} else {
|
||||
await pb.collection('termine').delete(t.id);
|
||||
await api.del('/termine/' + t.id);
|
||||
termine = termine.filter(x => x.id !== t.id);
|
||||
}
|
||||
showDelete = null;
|
||||
|
|
@ -202,7 +201,7 @@
|
|||
});
|
||||
|
||||
async function setVerfuegbarkeit(t: Termin, v: Verfuegbarkeit) {
|
||||
const updated = await pb.collection('termine').update<Termin>(t.id, { verfuegbarkeit: v });
|
||||
const updated = await api.put<Termin>('/termine/' + t.id, { verfuegbarkeit: v });
|
||||
termine = termine.map(x => x.id === t.id ? updated : x);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { pb } from '$lib/pb';
|
||||
import { get } from 'svelte/store';
|
||||
import { user } from '$lib/user';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
onMount(() => {
|
||||
if (pb.authStore.isValid) {
|
||||
if (!!get(user)) {
|
||||
goto('/');
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { user } from '$lib/user';
|
||||
import type { AppUser } from '$lib/user';
|
||||
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
|
|
@ -11,7 +13,8 @@
|
|||
error = '';
|
||||
loading = true;
|
||||
try {
|
||||
await pb.collection('users').authWithPassword(email, password);
|
||||
const u = await api.post<AppUser & { token: string }>('/auth/login', { email, password });
|
||||
user.set(u);
|
||||
goto('/');
|
||||
} catch {
|
||||
error = 'E-Mail oder Passwort falsch.';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { user } from '$lib/user';
|
||||
|
||||
let vereinsname = $state('');
|
||||
let email = $state('');
|
||||
|
|
@ -17,8 +18,8 @@
|
|||
}
|
||||
loading = true;
|
||||
try {
|
||||
await pb.collection('users').create({ email, password, passwordConfirm, name: vereinsname });
|
||||
await pb.collection('users').authWithPassword(email, password);
|
||||
const u = await api.post('/auth/register', { vereinName: vereinsname, email, password, name: vereinsname });
|
||||
user.set(u as any);
|
||||
goto('/onboarding');
|
||||
} catch (e: unknown) {
|
||||
error = e instanceof Error ? e.message : 'Registrierung fehlgeschlagen.';
|
||||
|
|
|
|||
18
app/src/routes/api/auth/login/+server.ts
Normal file
18
app/src/routes/api/auth/login/+server.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb } from '$lib/server/db';
|
||||
import { checkPassword, signJwt } from '$lib/server/auth';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const { email, password } = await request.json();
|
||||
if (!email || !password) throw error(400, 'E-Mail und Passwort erforderlich');
|
||||
|
||||
const db = getDb();
|
||||
const u = db.prepare('SELECT * FROM users WHERE email = ?').get(email.toLowerCase()) as any;
|
||||
if (!u || !(await checkPassword(password, u.password_hash))) throw error(401, 'Ungültige Zugangsdaten');
|
||||
|
||||
const token = await signJwt({
|
||||
sub: u.id, verein_id: u.verein_id, rolle: u.rolle, name: u.name, email: u.email
|
||||
});
|
||||
|
||||
return json({ token, id: u.id, verein_id: u.verein_id, rolle: u.rolle, name: u.name, email: u.email });
|
||||
}
|
||||
11
app/src/routes/api/auth/me/+server.ts
Normal file
11
app/src/routes/api/auth/me/+server.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
import { getDb } from '$lib/server/db';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const row = db.prepare('SELECT id, verein_id, email, name, rolle FROM users WHERE id = ?').get(u.sub) as any;
|
||||
if (!row) return new Response(null, { status: 401 });
|
||||
return json(row);
|
||||
}
|
||||
24
app/src/routes/api/auth/register/+server.ts
Normal file
24
app/src/routes/api/auth/register/+server.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId } from '$lib/server/db';
|
||||
import { hashPassword, signJwt } from '$lib/server/auth';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const { vereinName, email, password, name } = await request.json();
|
||||
if (!vereinName || !email || !password) throw error(400, 'Pflichtfelder fehlen');
|
||||
if (password.length < 8) throw error(400, 'Passwort mindestens 8 Zeichen');
|
||||
|
||||
const db = getDb();
|
||||
const existing = db.prepare('SELECT id FROM users WHERE email = ?').get(email.toLowerCase());
|
||||
if (existing) throw error(409, 'E-Mail bereits registriert');
|
||||
|
||||
const vereinId = newId();
|
||||
const userId = newId();
|
||||
const hash = await hashPassword(password);
|
||||
|
||||
db.prepare('INSERT INTO vereine (id, name) VALUES (?, ?)').run(vereinId, vereinName);
|
||||
db.prepare('INSERT INTO users (id, verein_id, email, password_hash, name, rolle) VALUES (?, ?, ?, ?, ?, NULL)')
|
||||
.run(userId, vereinId, email.toLowerCase(), hash, name || email.split('@')[0]);
|
||||
|
||||
const token = await signJwt({ sub: userId, verein_id: vereinId, rolle: null, name: name || email.split('@')[0], email: email.toLowerCase() });
|
||||
return json({ token, id: userId, verein_id: vereinId, rolle: null, name: name || email.split('@')[0], email: email.toLowerCase() }, { status: 201 });
|
||||
}
|
||||
37
app/src/routes/api/beitraege/+server.ts
Normal file
37
app/src/routes/api/beitraege/+server.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows, row } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const items = db.prepare(
|
||||
'SELECT * FROM beitraege WHERE verein_id = ? ORDER BY name'
|
||||
).all(u.verein_id);
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.name || body.betrag == null) throw error(400, 'Name und Betrag sind Pflichtfelder');
|
||||
|
||||
const id = newId();
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO beitraege (id, verein_id, name, betrag, rhythmus, beschreibung)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
u.verein_id,
|
||||
body.name,
|
||||
body.betrag,
|
||||
body.rhythmus ?? 'jaehrlich',
|
||||
body.beschreibung ?? null
|
||||
);
|
||||
|
||||
const beitrag = db.prepare('SELECT * FROM beitraege WHERE id = ?').get(id);
|
||||
return json(row(beitrag as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
52
app/src/routes/api/beitraege/[id]/+server.ts
Normal file
52
app/src/routes/api/beitraege/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, row } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const beitrag = db.prepare(
|
||||
'SELECT * FROM beitraege WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!beitrag) throw error(404, 'Beitrag nicht gefunden');
|
||||
return json(row(beitrag as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function PUT({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const existing = db.prepare(
|
||||
'SELECT id FROM beitraege WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!existing) throw error(404, 'Beitrag nicht gefunden');
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
db.prepare(`
|
||||
UPDATE beitraege SET
|
||||
name = ?, betrag = ?, rhythmus = ?, beschreibung = ?,
|
||||
updated = strftime('%Y-%m-%dT%H:%M:%SZ','now')
|
||||
WHERE id = ? AND verein_id = ?
|
||||
`).run(
|
||||
body.name,
|
||||
body.betrag,
|
||||
body.rhythmus ?? 'jaehrlich',
|
||||
body.beschreibung ?? null,
|
||||
params.id,
|
||||
u.verein_id
|
||||
);
|
||||
|
||||
const beitrag = db.prepare('SELECT * FROM beitraege WHERE id = ?').get(params.id);
|
||||
return json(row(beitrag as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function DELETE({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const result = db.prepare(
|
||||
'DELETE FROM beitraege WHERE id = ? AND verein_id = ?'
|
||||
).run(params.id, u.verein_id);
|
||||
if (result.changes === 0) throw error(404, 'Beitrag nicht gefunden');
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
23
app/src/routes/api/einladungen/+server.ts
Normal file
23
app/src/routes/api/einladungen/+server.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, row } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
const id = newId();
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO einladungen (id, verein_id, rolle)
|
||||
VALUES (?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
u.verein_id,
|
||||
body.rolle ?? 'trainer'
|
||||
);
|
||||
|
||||
const einladung = db.prepare('SELECT * FROM einladungen WHERE id = ?').get(id);
|
||||
return json(row(einladung as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
62
app/src/routes/api/einladungen/[token]/+server.ts
Normal file
62
app/src/routes/api/einladungen/[token]/+server.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, row } from '$lib/server/db';
|
||||
import { hashPassword, signJwt } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ params }) {
|
||||
const db = getDb();
|
||||
|
||||
const einladung = db.prepare(`
|
||||
SELECT e.*, v.name as vereinName
|
||||
FROM einladungen e JOIN vereine v ON v.id = e.verein_id
|
||||
WHERE e.token = ? AND e.genutzt = 0
|
||||
`).get(params.token);
|
||||
|
||||
if (!einladung) throw error(404, 'Einladung nicht gefunden oder bereits verwendet');
|
||||
|
||||
return json(row(einladung as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function POST({ request, params }) {
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.email || !body.password || !body.name) throw error(400, 'E-Mail, Passwort und Name sind Pflichtfelder');
|
||||
if (body.password.length < 8) throw error(400, 'Passwort mindestens 8 Zeichen');
|
||||
|
||||
const einladung = db.prepare(
|
||||
'SELECT * FROM einladungen WHERE token = ? AND genutzt = 0'
|
||||
).get(params.token) as Record<string, unknown> | undefined;
|
||||
|
||||
if (!einladung) throw error(404, 'Einladung nicht gefunden oder bereits verwendet');
|
||||
|
||||
const verein_id = einladung.verein_id as string;
|
||||
const rolle = einladung.rolle as string;
|
||||
|
||||
const existing = db.prepare('SELECT id FROM users WHERE email = ?').get(body.email.toLowerCase());
|
||||
if (existing) throw error(409, 'E-Mail bereits registriert');
|
||||
|
||||
const userId = newId();
|
||||
const hash = await hashPassword(body.password);
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO users (id, verein_id, email, password_hash, name, rolle)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(userId, verein_id, body.email.toLowerCase(), hash, body.name, rolle);
|
||||
|
||||
db.prepare(
|
||||
`UPDATE einladungen SET genutzt = 1, updated = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE token = ?`
|
||||
).run(params.token);
|
||||
|
||||
const token = await signJwt({
|
||||
sub: userId,
|
||||
verein_id,
|
||||
rolle,
|
||||
name: body.name,
|
||||
email: body.email.toLowerCase()
|
||||
});
|
||||
|
||||
return json(
|
||||
{ token, id: userId, verein_id, rolle, name: body.name, email: body.email.toLowerCase() },
|
||||
{ status: 201 }
|
||||
);
|
||||
}
|
||||
58
app/src/routes/api/einzuege/+server.ts
Normal file
58
app/src/routes/api/einzuege/+server.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const items = db.prepare(`
|
||||
SELECT e.*, m.vorname, m.nachname, b.name as beitrag_name
|
||||
FROM einzuege e
|
||||
JOIN mitglieder m ON m.id = e.mitglied_id
|
||||
JOIN beitraege b ON b.id = e.beitrag_id
|
||||
WHERE e.verein_id = ?
|
||||
ORDER BY e.faellig_am
|
||||
`).all(u.verein_id);
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
const einzuege = Array.isArray(body) ? body : [body];
|
||||
if (einzuege.length === 0) throw error(400, 'Keine Einzüge angegeben');
|
||||
|
||||
const insert = db.prepare(`
|
||||
INSERT INTO einzuege (id, verein_id, mitglied_id, beitrag_id, betrag, faellig_am, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const insertMany = db.transaction((items: typeof einzuege) => {
|
||||
for (const e of items) {
|
||||
insert.run(
|
||||
newId(),
|
||||
u.verein_id,
|
||||
e.mitglied_id,
|
||||
e.beitrag_id,
|
||||
e.betrag,
|
||||
e.faellig_am ?? null,
|
||||
e.status ?? 'ausstehend'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
insertMany(einzuege);
|
||||
|
||||
const created = db.prepare(`
|
||||
SELECT e.*, m.vorname, m.nachname, b.name as beitrag_name
|
||||
FROM einzuege e
|
||||
JOIN mitglieder m ON m.id = e.mitglied_id
|
||||
JOIN beitraege b ON b.id = e.beitrag_id
|
||||
WHERE e.verein_id = ?
|
||||
ORDER BY e.faellig_am
|
||||
`).all(u.verein_id);
|
||||
|
||||
return json(rows(created as Record<string, unknown>[]), { status: 201 });
|
||||
}
|
||||
26
app/src/routes/api/files/[...path]/+server.ts
Normal file
26
app/src/routes/api/files/[...path]/+server.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || './data/uploads';
|
||||
|
||||
export async function GET({ params }) {
|
||||
const filePath = join(UPLOAD_DIR, params.path);
|
||||
if (!existsSync(filePath)) return new Response(null, { status: 404 });
|
||||
const data = readFileSync(filePath);
|
||||
const ext = filePath.split('.').pop()?.toLowerCase() || '';
|
||||
const mime: Record<string, string> = {
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
gif: 'image/gif',
|
||||
webp: 'image/webp',
|
||||
mp4: 'video/mp4',
|
||||
mov: 'video/quicktime'
|
||||
};
|
||||
return new Response(data, {
|
||||
headers: {
|
||||
'Content-Type': mime[ext] || 'application/octet-stream',
|
||||
'Cache-Control': 'public, max-age=31536000'
|
||||
}
|
||||
});
|
||||
}
|
||||
36
app/src/routes/api/gruppen/+server.ts
Normal file
36
app/src/routes/api/gruppen/+server.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows, row, toArr } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const items = db.prepare(
|
||||
'SELECT * FROM gruppen WHERE verein_id = ? ORDER BY name'
|
||||
).all(u.verein_id);
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.name) throw error(400, 'Name ist ein Pflichtfeld');
|
||||
|
||||
const id = newId();
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO gruppen (id, verein_id, name, beschreibung, trainer_ids)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
u.verein_id,
|
||||
body.name,
|
||||
body.beschreibung ?? null,
|
||||
toArr(body.trainer_ids)
|
||||
);
|
||||
|
||||
const gruppe = db.prepare('SELECT * FROM gruppen WHERE id = ?').get(id);
|
||||
return json(row(gruppe as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
51
app/src/routes/api/gruppen/[id]/+server.ts
Normal file
51
app/src/routes/api/gruppen/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, row, toArr } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const gruppe = db.prepare(
|
||||
'SELECT * FROM gruppen WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!gruppe) throw error(404, 'Gruppe nicht gefunden');
|
||||
return json(row(gruppe as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function PUT({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const existing = db.prepare(
|
||||
'SELECT id FROM gruppen WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!existing) throw error(404, 'Gruppe nicht gefunden');
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
db.prepare(`
|
||||
UPDATE gruppen SET
|
||||
name = ?, beschreibung = ?, trainer_ids = ?,
|
||||
updated = strftime('%Y-%m-%dT%H:%M:%SZ','now')
|
||||
WHERE id = ? AND verein_id = ?
|
||||
`).run(
|
||||
body.name,
|
||||
body.beschreibung ?? null,
|
||||
toArr(body.trainer_ids),
|
||||
params.id,
|
||||
u.verein_id
|
||||
);
|
||||
|
||||
const gruppe = db.prepare('SELECT * FROM gruppen WHERE id = ?').get(params.id);
|
||||
return json(row(gruppe as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function DELETE({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const result = db.prepare(
|
||||
'DELETE FROM gruppen WHERE id = ? AND verein_id = ?'
|
||||
).run(params.id, u.verein_id);
|
||||
if (result.changes === 0) throw error(404, 'Gruppe nicht gefunden');
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
|
@ -1,29 +1,23 @@
|
|||
import ical from 'ical-generator';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
const PB_URL = () => env.PB_URL ?? 'http://localhost:8090';
|
||||
import { getDb } from '$lib/server/db';
|
||||
|
||||
export async function GET({ params }) {
|
||||
const { vereinId } = params;
|
||||
|
||||
// Verein laden (öffentlich lesbar via viewRule)
|
||||
const vereinRes = await fetch(
|
||||
`${PB_URL()}/api/collections/vereine/records/${vereinId}`,
|
||||
);
|
||||
if (!vereinRes.ok) {
|
||||
const db = getDb();
|
||||
|
||||
const verein = db.prepare('SELECT * FROM vereine WHERE id = ?').get(vereinId) as { name: string } | undefined;
|
||||
if (!verein) {
|
||||
return new Response('Verein nicht gefunden.', { status: 404 });
|
||||
}
|
||||
const verein = await vereinRes.json();
|
||||
|
||||
// Termine der nächsten 365 Tage laden
|
||||
const von = new Date().toISOString();
|
||||
const bis = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString();
|
||||
const filter = encodeURIComponent(`verein_id = "${vereinId}" && beginn >= "${von}" && beginn <= "${bis}"`);
|
||||
|
||||
const termineRes = await fetch(
|
||||
`${PB_URL()}/api/collections/termine/records?filter=${filter}&sort=beginn&perPage=500`,
|
||||
);
|
||||
const { items: termine = [] } = termineRes.ok ? await termineRes.json() : {};
|
||||
const termine = db.prepare(
|
||||
`SELECT * FROM termine WHERE verein_id = ? AND beginn >= ? AND beginn <= ? ORDER BY beginn LIMIT 500`
|
||||
).all(vereinId, von, bis) as { id: string; titel: string; beginn: string; ende: string | null; ort: string | null; beschreibung: string | null }[];
|
||||
|
||||
// iCal-Kalender aufbauen
|
||||
const cal = ical({
|
||||
|
|
|
|||
65
app/src/routes/api/mitglieder/+server.ts
Normal file
65
app/src/routes/api/mitglieder/+server.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows, row, toArr } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request, url }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const status = url.searchParams.get('status');
|
||||
|
||||
let items;
|
||||
if (status) {
|
||||
items = db.prepare(
|
||||
'SELECT * FROM mitglieder WHERE verein_id = ? AND status = ? ORDER BY nachname, vorname'
|
||||
).all(u.verein_id, status);
|
||||
} else {
|
||||
items = db.prepare(
|
||||
'SELECT * FROM mitglieder WHERE verein_id = ? ORDER BY nachname, vorname'
|
||||
).all(u.verein_id);
|
||||
}
|
||||
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.vorname || !body.nachname) throw error(400, 'Vorname und Nachname sind Pflichtfelder');
|
||||
|
||||
const id = newId();
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO mitglieder (
|
||||
id, verein_id, vorname, nachname, email, telefon,
|
||||
geburtsdatum, eintrittsdatum, austrittsdatum,
|
||||
strasse, plz, ort, iban, bic,
|
||||
gruppe_ids, status, notizen,
|
||||
mandatsreferenz, mandatsdatum
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
u.verein_id,
|
||||
body.vorname,
|
||||
body.nachname,
|
||||
body.email ?? null,
|
||||
body.telefon ?? null,
|
||||
body.geburtsdatum ?? null,
|
||||
body.eintrittsdatum ?? null,
|
||||
body.austrittsdatum ?? null,
|
||||
body.strasse ?? null,
|
||||
body.plz ?? null,
|
||||
body.ort ?? null,
|
||||
body.iban ?? null,
|
||||
body.bic ?? null,
|
||||
toArr(body.gruppe_ids),
|
||||
body.status ?? 'aktiv',
|
||||
body.notizen ?? null,
|
||||
body.mandatsreferenz ?? null,
|
||||
body.mandatsdatum ?? null
|
||||
);
|
||||
|
||||
const mitglied = db.prepare('SELECT * FROM mitglieder WHERE id = ?').get(id);
|
||||
return json(row(mitglied as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
69
app/src/routes/api/mitglieder/[id]/+server.ts
Normal file
69
app/src/routes/api/mitglieder/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, row, toArr } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const mitglied = db.prepare(
|
||||
'SELECT * FROM mitglieder WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!mitglied) throw error(404, 'Mitglied nicht gefunden');
|
||||
return json(row(mitglied as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function PUT({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const existing = db.prepare(
|
||||
'SELECT id FROM mitglieder WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!existing) throw error(404, 'Mitglied nicht gefunden');
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
db.prepare(`
|
||||
UPDATE mitglieder SET
|
||||
vorname = ?, nachname = ?, email = ?, telefon = ?,
|
||||
geburtsdatum = ?, eintrittsdatum = ?, austrittsdatum = ?,
|
||||
strasse = ?, plz = ?, ort = ?, iban = ?, bic = ?,
|
||||
gruppe_ids = ?, status = ?, notizen = ?,
|
||||
mandatsreferenz = ?, mandatsdatum = ?,
|
||||
updated = strftime('%Y-%m-%dT%H:%M:%SZ','now')
|
||||
WHERE id = ? AND verein_id = ?
|
||||
`).run(
|
||||
body.vorname,
|
||||
body.nachname,
|
||||
body.email ?? null,
|
||||
body.telefon ?? null,
|
||||
body.geburtsdatum ?? null,
|
||||
body.eintrittsdatum ?? null,
|
||||
body.austrittsdatum ?? null,
|
||||
body.strasse ?? null,
|
||||
body.plz ?? null,
|
||||
body.ort ?? null,
|
||||
body.iban ?? null,
|
||||
body.bic ?? null,
|
||||
toArr(body.gruppe_ids),
|
||||
body.status ?? 'aktiv',
|
||||
body.notizen ?? null,
|
||||
body.mandatsreferenz ?? null,
|
||||
body.mandatsdatum ?? null,
|
||||
params.id,
|
||||
u.verein_id
|
||||
);
|
||||
|
||||
const mitglied = db.prepare('SELECT * FROM mitglieder WHERE id = ?').get(params.id);
|
||||
return json(row(mitglied as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function DELETE({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const result = db.prepare(
|
||||
'DELETE FROM mitglieder WHERE id = ? AND verein_id = ?'
|
||||
).run(params.id, u.verein_id);
|
||||
if (result.changes === 0) throw error(404, 'Mitglied nicht gefunden');
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
39
app/src/routes/api/nachrichten/+server.ts
Normal file
39
app/src/routes/api/nachrichten/+server.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows, row, toArr } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const items = db.prepare(
|
||||
'SELECT * FROM nachrichten WHERE verein_id = ? ORDER BY gesendet_am DESC, created DESC'
|
||||
).all(u.verein_id);
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.betreff) throw error(400, 'Betreff ist ein Pflichtfeld');
|
||||
|
||||
const id = newId();
|
||||
const gesendet_am = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO nachrichten (id, verein_id, autor_id, betreff, text, gruppe_ids, gesendet_am)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
u.verein_id,
|
||||
u.sub,
|
||||
body.betreff,
|
||||
body.text ?? '',
|
||||
toArr(body.gruppe_ids),
|
||||
gesendet_am
|
||||
);
|
||||
|
||||
const nachricht = db.prepare('SELECT * FROM nachrichten WHERE id = ?').get(id);
|
||||
return json(row(nachricht as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
13
app/src/routes/api/nachrichten/[id]/+server.ts
Normal file
13
app/src/routes/api/nachrichten/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import { getDb } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function DELETE({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const result = db.prepare(
|
||||
'DELETE FROM nachrichten WHERE id = ? AND verein_id = ?'
|
||||
).run(params.id, u.verein_id);
|
||||
if (result.changes === 0) throw error(404, 'Nachricht nicht gefunden');
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
68
app/src/routes/api/neuigkeiten/+server.ts
Normal file
68
app/src/routes/api/neuigkeiten/+server.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows, row } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
import { writeFileSync, mkdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || './data/uploads';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const items = db.prepare(
|
||||
'SELECT * FROM neuigkeiten WHERE verein_id = ? ORDER BY created DESC'
|
||||
).all(u.verein_id);
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
const text = formData.get('text') as string | null;
|
||||
const gruppeIdsRaw = formData.get('gruppe_ids') as string | null;
|
||||
const terminId = formData.get('termin_id') as string | null;
|
||||
|
||||
let gruppe_ids: string[] = [];
|
||||
if (gruppeIdsRaw) {
|
||||
try { gruppe_ids = JSON.parse(gruppeIdsRaw); } catch { gruppe_ids = []; }
|
||||
}
|
||||
|
||||
const id = newId();
|
||||
const uploadPath = join(UPLOAD_DIR, u.verein_id, id);
|
||||
const medien: string[] = [];
|
||||
|
||||
const files = formData.getAll('medien') as File[];
|
||||
if (files.length > 0) {
|
||||
mkdirSync(uploadPath, { recursive: true });
|
||||
for (const file of files) {
|
||||
if (!(file instanceof File)) continue;
|
||||
const ext = file.name.split('.').pop() || 'bin';
|
||||
const filename = `${newId()}.${ext}`;
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
writeFileSync(join(uploadPath, filename), buffer);
|
||||
medien.push(filename);
|
||||
}
|
||||
}
|
||||
|
||||
if (!text && medien.length === 0) throw error(400, 'Text oder Medien sind erforderlich');
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO neuigkeiten (id, verein_id, autor_id, autor_name, text, medien, gruppe_ids, termin_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
u.verein_id,
|
||||
u.sub,
|
||||
u.name,
|
||||
text ?? null,
|
||||
JSON.stringify(medien),
|
||||
JSON.stringify(gruppe_ids),
|
||||
terminId ?? null
|
||||
);
|
||||
|
||||
const neuigkeit = db.prepare('SELECT * FROM neuigkeiten WHERE id = ?').get(id);
|
||||
return json(row(neuigkeit as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
27
app/src/routes/api/neuigkeiten/[id]/+server.ts
Normal file
27
app/src/routes/api/neuigkeiten/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import { getDb, parseArr } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
import { rmSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || './data/uploads';
|
||||
|
||||
export async function DELETE({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const neuigkeit = db.prepare(
|
||||
'SELECT * FROM neuigkeiten WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id) as Record<string, unknown> | undefined;
|
||||
|
||||
if (!neuigkeit) throw error(404, 'Neuigkeit nicht gefunden');
|
||||
|
||||
const uploadPath = join(UPLOAD_DIR, u.verein_id, params.id);
|
||||
if (existsSync(uploadPath)) {
|
||||
rmSync(uploadPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
db.prepare('DELETE FROM neuigkeiten WHERE id = ? AND verein_id = ?').run(params.id, u.verein_id);
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
58
app/src/routes/api/ort-ausfaelle/+server.ts
Normal file
58
app/src/routes/api/ort-ausfaelle/+server.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows, row } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request, url }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const ort_id = url.searchParams.get('ort_id');
|
||||
|
||||
let items;
|
||||
if (ort_id) {
|
||||
items = db.prepare(`
|
||||
SELECT a.* FROM ort_ausfaelle a
|
||||
JOIN veranstaltungsorte o ON o.id = a.ort_id
|
||||
WHERE a.ort_id = ? AND o.verein_id = ?
|
||||
ORDER BY a.von
|
||||
`).all(ort_id, u.verein_id);
|
||||
} else {
|
||||
items = db.prepare(`
|
||||
SELECT a.* FROM ort_ausfaelle a
|
||||
JOIN veranstaltungsorte o ON o.id = a.ort_id
|
||||
WHERE o.verein_id = ?
|
||||
ORDER BY a.von
|
||||
`).all(u.verein_id);
|
||||
}
|
||||
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.ort_id || !body.von || !body.bis) throw error(400, 'ort_id, von und bis sind Pflichtfelder');
|
||||
|
||||
const ort = db.prepare(
|
||||
'SELECT id FROM veranstaltungsorte WHERE id = ? AND verein_id = ?'
|
||||
).get(body.ort_id, u.verein_id);
|
||||
if (!ort) throw error(404, 'Ort nicht gefunden');
|
||||
|
||||
const id = newId();
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO ort_ausfaelle (id, ort_id, von, bis, grund)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
body.ort_id,
|
||||
body.von,
|
||||
body.bis,
|
||||
body.grund ?? null
|
||||
);
|
||||
|
||||
const ausfall = db.prepare('SELECT * FROM ort_ausfaelle WHERE id = ?').get(id);
|
||||
return json(row(ausfall as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
21
app/src/routes/api/ort-ausfaelle/[id]/+server.ts
Normal file
21
app/src/routes/api/ort-ausfaelle/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import { getDb } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function DELETE({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
// Verify the ausfall belongs to an ort in the user's Verein
|
||||
const ausfall = db.prepare(`
|
||||
SELECT a.id FROM ort_ausfaelle a
|
||||
JOIN veranstaltungsorte o ON o.id = a.ort_id
|
||||
WHERE a.id = ? AND o.verein_id = ?
|
||||
`).get(params.id, u.verein_id);
|
||||
|
||||
if (!ausfall) throw error(404, 'Ausfall nicht gefunden');
|
||||
|
||||
db.prepare('DELETE FROM ort_ausfaelle WHERE id = ?').run(params.id);
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
37
app/src/routes/api/orte/+server.ts
Normal file
37
app/src/routes/api/orte/+server.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows, row } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const items = db.prepare(
|
||||
'SELECT * FROM veranstaltungsorte WHERE verein_id = ? ORDER BY name'
|
||||
).all(u.verein_id);
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.name) throw error(400, 'Name ist ein Pflichtfeld');
|
||||
|
||||
const id = newId();
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO veranstaltungsorte (id, verein_id, name, adresse, typ, aktiv)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
u.verein_id,
|
||||
body.name,
|
||||
body.adresse ?? null,
|
||||
body.typ ?? 'sonstiges',
|
||||
body.aktiv !== false ? 1 : 0
|
||||
);
|
||||
|
||||
const ort = db.prepare('SELECT * FROM veranstaltungsorte WHERE id = ?').get(id);
|
||||
return json(row(ort as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
52
app/src/routes/api/orte/[id]/+server.ts
Normal file
52
app/src/routes/api/orte/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, row } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const ort = db.prepare(
|
||||
'SELECT * FROM veranstaltungsorte WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!ort) throw error(404, 'Ort nicht gefunden');
|
||||
return json(row(ort as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function PUT({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const existing = db.prepare(
|
||||
'SELECT id FROM veranstaltungsorte WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!existing) throw error(404, 'Ort nicht gefunden');
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
db.prepare(`
|
||||
UPDATE veranstaltungsorte SET
|
||||
name = ?, adresse = ?, typ = ?, aktiv = ?,
|
||||
updated = strftime('%Y-%m-%dT%H:%M:%SZ','now')
|
||||
WHERE id = ? AND verein_id = ?
|
||||
`).run(
|
||||
body.name,
|
||||
body.adresse ?? null,
|
||||
body.typ ?? 'sonstiges',
|
||||
body.aktiv !== false ? 1 : 0,
|
||||
params.id,
|
||||
u.verein_id
|
||||
);
|
||||
|
||||
const ort = db.prepare('SELECT * FROM veranstaltungsorte WHERE id = ?').get(params.id);
|
||||
return json(row(ort as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function DELETE({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const result = db.prepare(
|
||||
'DELETE FROM veranstaltungsorte WHERE id = ? AND verein_id = ?'
|
||||
).run(params.id, u.verein_id);
|
||||
if (result.changes === 0) throw error(404, 'Ort nicht gefunden');
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import webpush from 'web-push';
|
||||
|
||||
const PB_URL = () => env.PB_URL ?? 'http://localhost:8090';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
import { getDb } from '$lib/server/db';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const authHeader = request.headers.get('Authorization') ?? '';
|
||||
const authUser = await requireAuth(request).catch(() => null);
|
||||
if (!authUser) return json({ error: 'Nicht authentifiziert.' }, { status: 401 });
|
||||
|
||||
const { titel, body, url = '/nachrichten' } = await request.json();
|
||||
|
||||
if (!titel) return json({ error: 'Titel fehlt.' }, { status: 400 });
|
||||
|
|
@ -20,21 +22,18 @@ export async function POST({ request }) {
|
|||
|
||||
webpush.setVapidDetails(vapidSubject, vapidPublic, vapidPrivate);
|
||||
|
||||
// Alle Push-Subscriptions des Vereins laden (listRule erlaubt verein-weite Abfrage)
|
||||
const subRes = await fetch(
|
||||
`${PB_URL()}/api/collections/push_subscriptions/records?perPage=500`,
|
||||
{ headers: { Authorization: authHeader } },
|
||||
);
|
||||
const db = getDb();
|
||||
const items = db.prepare(
|
||||
'SELECT * FROM push_subscriptions WHERE verein_id = ?'
|
||||
).all(authUser.verein_id) as { endpoint: string; p256dh: string; auth: string; id: string }[];
|
||||
|
||||
if (!subRes.ok) return json({ sent: 0 });
|
||||
const { items } = await subRes.json();
|
||||
if (!items?.length) return json({ sent: 0 });
|
||||
if (!items.length) return json({ sent: 0 });
|
||||
|
||||
const payload = JSON.stringify({ title: titel, body, url });
|
||||
let sent = 0;
|
||||
|
||||
await Promise.allSettled(
|
||||
items.map(async (sub: { endpoint: string; p256dh: string; auth: string; id: string }) => {
|
||||
items.map(async (sub) => {
|
||||
try {
|
||||
await webpush.sendNotification(
|
||||
{ endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } },
|
||||
|
|
@ -44,10 +43,7 @@ export async function POST({ request }) {
|
|||
} catch (err: unknown) {
|
||||
// 410 Gone = Subscription abgelaufen → löschen
|
||||
if ((err as { statusCode?: number }).statusCode === 410) {
|
||||
await fetch(
|
||||
`${PB_URL()}/api/collections/push_subscriptions/records/${sub.id}`,
|
||||
{ method: 'DELETE', headers: { Authorization: authHeader } },
|
||||
).catch(() => {});
|
||||
db.prepare('DELETE FROM push_subscriptions WHERE id = ?').run(sub.id);
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,67 +1,44 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
const PB_URL = () => env.PB_URL ?? 'http://localhost:8090';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
import { getDb, newId } from '$lib/server/db';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const authHeader = request.headers.get('Authorization') ?? '';
|
||||
const { subscription, userId } = await request.json();
|
||||
const authUser = await requireAuth(request).catch(() => null);
|
||||
if (!authUser) return json({ error: 'Nicht authentifiziert.' }, { status: 401 });
|
||||
|
||||
if (!subscription?.endpoint || !userId) {
|
||||
const { subscription } = await request.json();
|
||||
|
||||
if (!subscription?.endpoint) {
|
||||
return json({ error: 'Ungültige Subscription.' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Alte Subscription dieses Users löschen (Gerätewechsel)
|
||||
const listRes = await fetch(
|
||||
`${PB_URL()}/api/collections/push_subscriptions/records?filter=user_id%3D"${userId}"&perPage=50`,
|
||||
{ headers: { Authorization: authHeader } },
|
||||
);
|
||||
if (listRes.ok) {
|
||||
const { items } = await listRes.json();
|
||||
await Promise.all(
|
||||
(items ?? []).map((r: { id: string }) =>
|
||||
fetch(`${PB_URL()}/api/collections/push_subscriptions/records/${r.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: authHeader },
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
const db = getDb();
|
||||
|
||||
// Alte Subscriptions dieses Users löschen (Gerätewechsel)
|
||||
db.prepare('DELETE FROM push_subscriptions WHERE user_id = ?').run(authUser.sub);
|
||||
|
||||
// Neue Subscription speichern
|
||||
const res = await fetch(`${PB_URL()}/api/collections/push_subscriptions/records`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: authHeader },
|
||||
body: JSON.stringify({
|
||||
user_id: userId,
|
||||
endpoint: subscription.endpoint,
|
||||
p256dh: subscription.keys.p256dh,
|
||||
auth: subscription.keys.auth,
|
||||
}),
|
||||
});
|
||||
db.prepare(`
|
||||
INSERT INTO push_subscriptions (id, user_id, verein_id, endpoint, p256dh, auth)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
newId(),
|
||||
authUser.sub,
|
||||
authUser.verein_id,
|
||||
subscription.endpoint,
|
||||
subscription.keys.p256dh,
|
||||
subscription.keys.auth,
|
||||
);
|
||||
|
||||
if (!res.ok) return json({ error: 'Fehler beim Speichern.' }, { status: 500 });
|
||||
return json({ success: true });
|
||||
}
|
||||
|
||||
export async function DELETE({ request }) {
|
||||
const authHeader = request.headers.get('Authorization') ?? '';
|
||||
const { userId } = await request.json();
|
||||
const authUser = await requireAuth(request).catch(() => null);
|
||||
if (!authUser) return json({ success: true });
|
||||
|
||||
const listRes = await fetch(
|
||||
`${PB_URL()}/api/collections/push_subscriptions/records?filter=user_id%3D"${userId}"&perPage=50`,
|
||||
{ headers: { Authorization: authHeader } },
|
||||
);
|
||||
if (!listRes.ok) return json({ success: true });
|
||||
const db = getDb();
|
||||
db.prepare('DELETE FROM push_subscriptions WHERE user_id = ?').run(authUser.sub);
|
||||
|
||||
const { items } = await listRes.json();
|
||||
await Promise.all(
|
||||
(items ?? []).map((r: { id: string }) =>
|
||||
fetch(`${PB_URL()}/api/collections/push_subscriptions/records/${r.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: authHeader },
|
||||
}),
|
||||
),
|
||||
);
|
||||
return json({ success: true });
|
||||
}
|
||||
|
|
|
|||
58
app/src/routes/api/reaktionen/+server.ts
Normal file
58
app/src/routes/api/reaktionen/+server.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows, row } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request, url }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const beitrag_id = url.searchParams.get('beitrag_id');
|
||||
|
||||
let items;
|
||||
if (beitrag_id) {
|
||||
items = db.prepare(`
|
||||
SELECT r.* FROM reaktionen r
|
||||
JOIN neuigkeiten n ON n.id = r.beitrag_id
|
||||
WHERE r.beitrag_id = ? AND n.verein_id = ?
|
||||
ORDER BY r.created
|
||||
`).all(beitrag_id, u.verein_id);
|
||||
} else {
|
||||
items = db.prepare(`
|
||||
SELECT r.* FROM reaktionen r
|
||||
JOIN neuigkeiten n ON n.id = r.beitrag_id
|
||||
WHERE n.verein_id = ?
|
||||
ORDER BY r.created
|
||||
`).all(u.verein_id);
|
||||
}
|
||||
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.beitrag_id) throw error(400, 'beitrag_id ist erforderlich');
|
||||
|
||||
const beitrag = db.prepare(
|
||||
'SELECT id FROM neuigkeiten WHERE id = ? AND verein_id = ?'
|
||||
).get(body.beitrag_id, u.verein_id);
|
||||
if (!beitrag) throw error(404, 'Beitrag nicht gefunden');
|
||||
|
||||
const id = newId();
|
||||
|
||||
try {
|
||||
db.prepare(`
|
||||
INSERT INTO reaktionen (id, beitrag_id, user_id)
|
||||
VALUES (?, ?, ?)
|
||||
`).run(id, body.beitrag_id, u.sub);
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
if (msg.includes('UNIQUE')) throw error(409, 'Reaktion bereits vorhanden');
|
||||
throw e;
|
||||
}
|
||||
|
||||
const reaktion = db.prepare('SELECT * FROM reaktionen WHERE id = ?').get(id);
|
||||
return json(row(reaktion as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
13
app/src/routes/api/reaktionen/[id]/+server.ts
Normal file
13
app/src/routes/api/reaktionen/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import { getDb } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function DELETE({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const result = db.prepare(
|
||||
'DELETE FROM reaktionen WHERE id = ? AND user_id = ?'
|
||||
).run(params.id, u.sub);
|
||||
if (result.changes === 0) throw error(404, 'Reaktion nicht gefunden oder keine Berechtigung');
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
63
app/src/routes/api/termine/+server.ts
Normal file
63
app/src/routes/api/termine/+server.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows, row, toArr } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request, url }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const von = url.searchParams.get('von');
|
||||
const bis = url.searchParams.get('bis');
|
||||
|
||||
let query = 'SELECT * FROM termine WHERE verein_id = ?';
|
||||
const params: unknown[] = [u.verein_id];
|
||||
|
||||
if (von) {
|
||||
query += ' AND beginn >= ?';
|
||||
params.push(von);
|
||||
}
|
||||
if (bis) {
|
||||
query += ' AND beginn <= ?';
|
||||
params.push(bis);
|
||||
}
|
||||
|
||||
query += ' ORDER BY beginn';
|
||||
|
||||
const items = db.prepare(query).all(...params);
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.titel || !body.beginn) throw error(400, 'Titel und Beginn sind Pflichtfelder');
|
||||
|
||||
const id = newId();
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO termine (
|
||||
id, verein_id, titel, beschreibung, beginn, ende,
|
||||
ort, ort_id, gruppe_ids, durchfuehrender_id,
|
||||
verfuegbarkeit, rrule, serie_id
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
u.verein_id,
|
||||
body.titel,
|
||||
body.beschreibung ?? null,
|
||||
body.beginn,
|
||||
body.ende ?? null,
|
||||
body.ort ?? null,
|
||||
body.ort_id ?? null,
|
||||
toArr(body.gruppe_ids),
|
||||
body.durchfuehrender_id ?? null,
|
||||
body.verfuegbarkeit ?? 'offen',
|
||||
body.rrule ?? null,
|
||||
body.serie_id ?? null
|
||||
);
|
||||
|
||||
const termin = db.prepare('SELECT * FROM termine WHERE id = ?').get(id);
|
||||
return json(row(termin as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
83
app/src/routes/api/termine/[id]/+server.ts
Normal file
83
app/src/routes/api/termine/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, row, toArr } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const termin = db.prepare(
|
||||
'SELECT * FROM termine WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!termin) throw error(404, 'Termin nicht gefunden');
|
||||
return json(row(termin as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function PUT({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const existing = db.prepare(
|
||||
'SELECT id FROM termine WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!existing) throw error(404, 'Termin nicht gefunden');
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
db.prepare(`
|
||||
UPDATE termine SET
|
||||
titel = ?, beschreibung = ?, beginn = ?, ende = ?,
|
||||
ort = ?, ort_id = ?, gruppe_ids = ?, durchfuehrender_id = ?,
|
||||
verfuegbarkeit = ?, rrule = ?, serie_id = ?,
|
||||
updated = strftime('%Y-%m-%dT%H:%M:%SZ','now')
|
||||
WHERE id = ? AND verein_id = ?
|
||||
`).run(
|
||||
body.titel,
|
||||
body.beschreibung ?? null,
|
||||
body.beginn,
|
||||
body.ende ?? null,
|
||||
body.ort ?? null,
|
||||
body.ort_id ?? null,
|
||||
toArr(body.gruppe_ids),
|
||||
body.durchfuehrender_id ?? null,
|
||||
body.verfuegbarkeit ?? 'offen',
|
||||
body.rrule ?? null,
|
||||
body.serie_id ?? null,
|
||||
params.id,
|
||||
u.verein_id
|
||||
);
|
||||
|
||||
const termin = db.prepare('SELECT * FROM termine WHERE id = ?').get(params.id);
|
||||
return json(row(termin as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function DELETE({ request, params, url }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const deleteSerie = url.searchParams.get('serie') === 'true';
|
||||
|
||||
if (deleteSerie) {
|
||||
const termin = db.prepare(
|
||||
'SELECT serie_id FROM termine WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id) as { serie_id: string | null } | undefined;
|
||||
|
||||
if (!termin) throw error(404, 'Termin nicht gefunden');
|
||||
|
||||
if (termin.serie_id) {
|
||||
db.prepare(
|
||||
'DELETE FROM termine WHERE serie_id = ? AND verein_id = ?'
|
||||
).run(termin.serie_id, u.verein_id);
|
||||
} else {
|
||||
db.prepare(
|
||||
'DELETE FROM termine WHERE id = ? AND verein_id = ?'
|
||||
).run(params.id, u.verein_id);
|
||||
}
|
||||
} else {
|
||||
const result = db.prepare(
|
||||
'DELETE FROM termine WHERE id = ? AND verein_id = ?'
|
||||
).run(params.id, u.verein_id);
|
||||
if (result.changes === 0) throw error(404, 'Termin nicht gefunden');
|
||||
}
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
39
app/src/routes/api/vereine/+server.ts
Normal file
39
app/src/routes/api/vereine/+server.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, row } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const verein = db.prepare('SELECT * FROM vereine WHERE id = ?').get(u.verein_id);
|
||||
if (!verein) throw error(404, 'Verein nicht gefunden');
|
||||
return json(row(verein as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function PATCH({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
const allowed = [
|
||||
'name', 'adresse', 'plz', 'ort', 'bundesland',
|
||||
'email', 'telefon', 'website',
|
||||
'glaeubigerid', 'iban', 'bic',
|
||||
'dosb_mitglied'
|
||||
];
|
||||
|
||||
const fields = Object.keys(body).filter(k => allowed.includes(k));
|
||||
if (fields.length === 0) throw error(400, 'Keine gültigen Felder');
|
||||
|
||||
const sets = fields.map(k => `${k} = ?`).join(', ');
|
||||
const vals = fields.map(k => {
|
||||
if (k === 'dosb_mitglied') return body[k] ? 1 : 0;
|
||||
return body[k];
|
||||
});
|
||||
|
||||
db.prepare(`UPDATE vereine SET ${sets}, updated = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE id = ?`)
|
||||
.run(...vals, u.verein_id);
|
||||
|
||||
const verein = db.prepare('SELECT * FROM vereine WHERE id = ?').get(u.verein_id);
|
||||
return json(row(verein as Record<string, unknown>));
|
||||
}
|
||||
|
|
@ -2,7 +2,9 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { pb } from '$lib/pb';
|
||||
import { get } from 'svelte/store';
|
||||
import { api } from '$lib/api';
|
||||
import { user } from '$lib/user';
|
||||
import type { Einladung } from '$lib/types';
|
||||
|
||||
const token = $derived($page.params.token as string);
|
||||
|
|
@ -20,17 +22,14 @@
|
|||
let formError = $state('');
|
||||
|
||||
onMount(async () => {
|
||||
if (pb.authStore.isValid) {
|
||||
if (!!get(user)) {
|
||||
goto('/');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const inv = await pb.collection('einladungen')
|
||||
.getFirstListItem<Einladung>(`token = "${token}" && genutzt = false`, {
|
||||
expand: 'verein_id',
|
||||
});
|
||||
const inv = await api.get<Einladung & { vereinName: string }>('/einladungen/' + token);
|
||||
einladung = inv;
|
||||
vereinName = (inv as any).expand?.verein_id?.name ?? '';
|
||||
vereinName = (inv as any).vereinName ?? '';
|
||||
} catch {
|
||||
fehler = 'Dieser Einladungslink ist ungültig oder wurde bereits verwendet.';
|
||||
} finally {
|
||||
|
|
@ -43,16 +42,12 @@
|
|||
if (password !== passwordConfirm) { formError = 'Passwörter stimmen nicht überein.'; return; }
|
||||
formError = ''; saving = true;
|
||||
try {
|
||||
await pb.collection('users').create({
|
||||
email: email.trim(),
|
||||
const u = await api.post<any>(`/einladungen/${token}`, {
|
||||
email: email.trim(),
|
||||
password,
|
||||
passwordConfirm,
|
||||
name: name.trim(),
|
||||
verein_id: einladung.verein_id,
|
||||
rolle: einladung.rolle,
|
||||
name: name.trim(),
|
||||
});
|
||||
await pb.collection('users').authWithPassword(email.trim(), password);
|
||||
await pb.collection('einladungen').update(einladung.id, { genutzt: true });
|
||||
user.set(u);
|
||||
goto('/');
|
||||
} catch (e: unknown) {
|
||||
formError = e instanceof Error ? e.message : 'Registrierung fehlgeschlagen.';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { pb } from '$lib/pb';
|
||||
import { get } from 'svelte/store';
|
||||
import { api } from '$lib/api';
|
||||
import { user } from '$lib/user';
|
||||
|
||||
let schritt = $state(1);
|
||||
let loading = $state(false);
|
||||
|
|
@ -13,36 +15,31 @@
|
|||
let fertigName = $state('');
|
||||
|
||||
onMount(() => {
|
||||
if (!pb.authStore.isValid) {
|
||||
const u = get(user);
|
||||
if (!u) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
if (pb.authStore.record?.verein_id) {
|
||||
if (u.verein_id) {
|
||||
goto('/');
|
||||
return;
|
||||
}
|
||||
// Vereinsname aus Registration vorbelegen
|
||||
vereinsname = pb.authStore.record?.name ?? '';
|
||||
vereinsname = u.name ?? '';
|
||||
});
|
||||
|
||||
async function vereinAnlegen() {
|
||||
if (!vereinsname.trim()) return;
|
||||
error = ''; loading = true;
|
||||
try {
|
||||
const verein = await pb.collection('vereine').create({
|
||||
name: vereinsname.trim(),
|
||||
ort: ort.trim() || null,
|
||||
plan: 'free',
|
||||
dosb_mitglied: false,
|
||||
const updated = await api.post<any>('/onboarding/verein', {
|
||||
name: vereinsname.trim(),
|
||||
ort: ort.trim() || null,
|
||||
});
|
||||
|
||||
await pb.collection('users').update(pb.authStore.record!.id, {
|
||||
verein_id: verein.id,
|
||||
});
|
||||
|
||||
// Auth-Token aktualisieren damit verein_id im Record steht
|
||||
await pb.collection('users').authRefresh();
|
||||
|
||||
fertigName = verein.name;
|
||||
// user store mit aktualisiertem verein_id updaten
|
||||
const u = get(user);
|
||||
if (u) user.set({ ...u, verein_id: updated.verein_id });
|
||||
fertigName = vereinsname.trim();
|
||||
schritt = 3;
|
||||
} catch (e: unknown) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Anlegen.';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue