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
106
Makefile
106
Makefile
|
|
@ -1,22 +1,18 @@
|
||||||
# ==============================================================
|
# ==============================================================
|
||||||
# VEREINS.HAUS — Makefile
|
# VEREINS.HAUS — Makefile
|
||||||
# Deploy-Strategie: SSH zur DS, Docker Compose
|
# Deploy-Strategie: SSH zur DS, Docker Compose
|
||||||
|
# Stack: SvelteKit + better-sqlite3 (kein PocketBase)
|
||||||
# ==============================================================
|
# ==============================================================
|
||||||
|
|
||||||
DS_HOST := ds
|
DS_HOST := ds
|
||||||
DS_IP := 10.47.11.10
|
DS_IP := 10.47.11.10
|
||||||
DS_SSH_PORT := 4711
|
DS_SSH_PORT := 4711
|
||||||
DS_PATH := /volume1/docker/vereinshaus
|
DS_PATH := /volume1/docker/vereinshaus
|
||||||
CONTAINER_PB := vereinshaus-pocketbase
|
|
||||||
CONTAINER_APP := vereinshaus-app
|
CONTAINER_APP := vereinshaus-app
|
||||||
DOCKER := sudo /usr/local/bin/docker
|
DOCKER := sudo /usr/local/bin/docker
|
||||||
|
|
||||||
STAGING_PATH := /volume1/docker/vereinshaus-staging
|
STAGING_PATH := /volume1/docker/vereinshaus-staging
|
||||||
CONTAINER_PB_STAGING := vereinshaus-staging-pocketbase
|
|
||||||
CONTAINER_APP_STAGING := vereinshaus-staging-app
|
CONTAINER_APP_STAGING := vereinshaus-staging-app
|
||||||
STAGING_PB_URL := http://localhost:8091
|
|
||||||
STAGING_MIGRATIONS := $(STAGING_PATH)/pocketbase/migrations
|
|
||||||
STAGING_HOOKS := $(STAGING_PATH)/pocketbase/data/pb_hooks
|
|
||||||
|
|
||||||
TAR_EXCLUDE := --exclude='.git' \
|
TAR_EXCLUDE := --exclude='.git' \
|
||||||
--exclude='./app/node_modules' \
|
--exclude='./app/node_modules' \
|
||||||
|
|
@ -25,13 +21,8 @@ TAR_EXCLUDE := --exclude='.git' \
|
||||||
--exclude='./.env' \
|
--exclude='./.env' \
|
||||||
--exclude='./.DS_Store'
|
--exclude='./.DS_Store'
|
||||||
|
|
||||||
HOOKS_SRC := pocketbase/pb_hooks
|
.PHONY: help check-ssh start stop restart status logs logs-app logs-f deploy \
|
||||||
HOOKS_DST := /volume1/docker/vereinshaus/pocketbase/data/pb_hooks
|
staging-deploy staging-reset staging-seed staging-logs staging-status staging-stop
|
||||||
MIGRATIONS_SRC := pocketbase/pb_migrations
|
|
||||||
MIGRATIONS_DST := /volume1/docker/vereinshaus/pocketbase/migrations
|
|
||||||
|
|
||||||
.PHONY: help check-ssh start stop restart status logs logs-f logs-app \
|
|
||||||
shell-pb pb-admin deploy
|
|
||||||
|
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
# Hilfe
|
# Hilfe
|
||||||
|
|
@ -41,16 +32,16 @@ help:
|
||||||
@echo " vereins.haus — verfügbare Befehle:"
|
@echo " vereins.haus — verfügbare Befehle:"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " make deploy App bauen + zur DS übertragen + Container neu starten"
|
@echo " make deploy App bauen + zur DS übertragen + Container neu starten"
|
||||||
@echo " make start Alle Container starten"
|
@echo " make start Container starten"
|
||||||
@echo " make stop Alle Container stoppen"
|
@echo " make stop Container stoppen"
|
||||||
@echo " make restart Alle Container neu starten"
|
@echo " make restart Container neu starten"
|
||||||
@echo " make status Container-Status anzeigen"
|
@echo " make status Container-Status anzeigen"
|
||||||
|
@echo " make logs App-Logs (100 Zeilen)"
|
||||||
|
@echo " make logs-f App Live-Log"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " make logs PocketBase-Logs (100 Zeilen)"
|
@echo " make staging-deploy Staging deployen"
|
||||||
@echo " make logs-app App-Logs (100 Zeilen)"
|
@echo " make staging-seed Testdaten einfügen"
|
||||||
@echo " make logs-f PocketBase Live-Log"
|
@echo " make staging-reset Staging-DB löschen (Neustart)"
|
||||||
@echo " make shell-pb Shell in PocketBase-Container"
|
|
||||||
@echo " make pb-admin PocketBase Admin-URL anzeigen"
|
|
||||||
@echo ""
|
@echo ""
|
||||||
|
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
|
|
@ -65,7 +56,7 @@ check-ssh:
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
# DEPLOY
|
# DEPLOY (Production)
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
deploy: check-ssh
|
deploy: check-ssh
|
||||||
@echo "→ Sync zu DS..."
|
@echo "→ Sync zu DS..."
|
||||||
|
|
@ -74,23 +65,6 @@ deploy: check-ssh
|
||||||
@if [ -f .env ]; then \
|
@if [ -f .env ]; then \
|
||||||
cat .env | ssh $(DS_HOST) "cat > $(DS_PATH)/.env"; \
|
cat .env | ssh $(DS_HOST) "cat > $(DS_PATH)/.env"; \
|
||||||
fi
|
fi
|
||||||
@echo "→ PocketBase Hooks synchronisieren..."
|
|
||||||
@if ls $(HOOKS_SRC)/*.pb.js 2>/dev/null | grep -q .; then \
|
|
||||||
for f in $(HOOKS_SRC)/*.pb.js; do \
|
|
||||||
cat "$$f" | ssh $(DS_HOST) "cat > $(HOOKS_DST)/$$(basename $$f)"; \
|
|
||||||
done; \
|
|
||||||
fi
|
|
||||||
@echo "→ PocketBase Migrations synchronisieren (nur neue)..."
|
|
||||||
@ssh $(DS_HOST) "mkdir -p $(MIGRATIONS_DST)"
|
|
||||||
@if ls $(MIGRATIONS_SRC)/*.js 2>/dev/null | grep -q .; then \
|
|
||||||
for f in $(MIGRATIONS_SRC)/*.js; do \
|
|
||||||
fname=$$(basename "$$f"); \
|
|
||||||
if ! ssh $(DS_HOST) "test -f $(MIGRATIONS_DST)/$$fname" 2>/dev/null; then \
|
|
||||||
cat "$$f" | ssh $(DS_HOST) "cat > $(MIGRATIONS_DST)/$$fname"; \
|
|
||||||
echo " ✓ $$fname"; \
|
|
||||||
fi; \
|
|
||||||
done; \
|
|
||||||
fi
|
|
||||||
@echo "→ Docker rebuild + restart..."
|
@echo "→ Docker rebuild + restart..."
|
||||||
@ssh $(DS_HOST) " \
|
@ssh $(DS_HOST) " \
|
||||||
cd $(DS_PATH) && \
|
cd $(DS_PATH) && \
|
||||||
|
|
@ -98,7 +72,7 @@ deploy: check-ssh
|
||||||
$(DOCKER) compose build app && \
|
$(DOCKER) compose build app && \
|
||||||
$(DOCKER) compose up -d"
|
$(DOCKER) compose up -d"
|
||||||
@echo " ✓ Deploy fertig."
|
@echo " ✓ Deploy fertig."
|
||||||
@ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER_APP) --tail=10"
|
@ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER_APP) --tail=15"
|
||||||
|
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
# Container-Steuerung
|
# Container-Steuerung
|
||||||
|
|
@ -124,27 +98,16 @@ status: check-ssh
|
||||||
# Logs
|
# Logs
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
logs: check-ssh
|
logs: check-ssh
|
||||||
@ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER_PB) --tail=100"
|
|
||||||
|
|
||||||
logs-app: check-ssh
|
|
||||||
@ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER_APP) --tail=100"
|
@ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER_APP) --tail=100"
|
||||||
|
|
||||||
|
logs-app: logs
|
||||||
|
|
||||||
logs-f: check-ssh
|
logs-f: check-ssh
|
||||||
@ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER_PB) -f"
|
@ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER_APP) -f"
|
||||||
|
|
||||||
# ----------------------------------------------------------
|
|
||||||
# Shell + Admin
|
|
||||||
# ----------------------------------------------------------
|
|
||||||
shell-pb: check-ssh
|
|
||||||
@ssh -t $(DS_HOST) "$(DOCKER) exec -it $(CONTAINER_PB) sh"
|
|
||||||
|
|
||||||
pb-admin:
|
|
||||||
@echo " PocketBase Admin: https://api.vereins.haus/_/"
|
|
||||||
|
|
||||||
# ==============================================================
|
# ==============================================================
|
||||||
# STAGING
|
# STAGING
|
||||||
# ==============================================================
|
# ==============================================================
|
||||||
.PHONY: staging-deploy staging-reset staging-seed staging-logs staging-status staging-stop
|
|
||||||
|
|
||||||
staging-deploy: check-ssh
|
staging-deploy: check-ssh
|
||||||
@echo "→ Sync zu DS (Staging)..."
|
@echo "→ Sync zu DS (Staging)..."
|
||||||
|
|
@ -153,20 +116,6 @@ staging-deploy: check-ssh
|
||||||
@if [ -f .env ]; then \
|
@if [ -f .env ]; then \
|
||||||
cat .env | ssh $(DS_HOST) "cat > $(STAGING_PATH)/.env"; \
|
cat .env | ssh $(DS_HOST) "cat > $(STAGING_PATH)/.env"; \
|
||||||
fi
|
fi
|
||||||
@echo "→ Hooks synchronisieren (Staging, vollständig)..."
|
|
||||||
@ssh $(DS_HOST) "mkdir -p $(STAGING_HOOKS)"
|
|
||||||
@if ls $(HOOKS_SRC)/*.pb.js 2>/dev/null | grep -q .; then \
|
|
||||||
for f in $(HOOKS_SRC)/*.pb.js; do \
|
|
||||||
cat "$$f" | ssh $(DS_HOST) "cat > $(STAGING_HOOKS)/$$(basename $$f)"; \
|
|
||||||
done; \
|
|
||||||
fi
|
|
||||||
@echo "→ Migrations synchronisieren (Staging, immer aktuell)..."
|
|
||||||
@ssh $(DS_HOST) "mkdir -p $(STAGING_MIGRATIONS)"
|
|
||||||
@if ls $(MIGRATIONS_SRC)/*.js 2>/dev/null | grep -q .; then \
|
|
||||||
for f in $(MIGRATIONS_SRC)/*.js; do \
|
|
||||||
cat "$$f" | ssh $(DS_HOST) "cat > $(STAGING_MIGRATIONS)/$$(basename $$f)"; \
|
|
||||||
done; \
|
|
||||||
fi
|
|
||||||
@echo "→ Docker rebuild + restart (Staging)..."
|
@echo "→ Docker rebuild + restart (Staging)..."
|
||||||
@ssh $(DS_HOST) " \
|
@ssh $(DS_HOST) " \
|
||||||
cd $(STAGING_PATH) && \
|
cd $(STAGING_PATH) && \
|
||||||
|
|
@ -175,30 +124,25 @@ staging-deploy: check-ssh
|
||||||
$(DOCKER) compose -f docker-compose.staging.yml up -d"
|
$(DOCKER) compose -f docker-compose.staging.yml up -d"
|
||||||
@echo " ✓ Staging bereit."
|
@echo " ✓ Staging bereit."
|
||||||
@echo " App: https://staging.vereins.haus"
|
@echo " App: https://staging.vereins.haus"
|
||||||
@echo " PocketBase: https://api-staging.vereins.haus/_/"
|
|
||||||
|
|
||||||
# Löscht alle PB-Daten auf Staging → Migrations laufen frisch durch
|
# Löscht die SQLite-DB auf Staging → frischer Start
|
||||||
# Danach: make staging-deploy && make staging-seed
|
# Danach: make staging-deploy && make staging-seed
|
||||||
staging-reset: check-ssh staging-stop
|
staging-reset: check-ssh staging-stop
|
||||||
@echo "→ PocketBase-Daten auf Staging löschen..."
|
@echo "→ Staging-Daten löschen..."
|
||||||
@ssh $(DS_HOST) "rm -rf \
|
@ssh $(DS_HOST) "rm -f \
|
||||||
$(STAGING_PATH)/pocketbase/data/storage \
|
$(STAGING_PATH)/data/vereinshaus.db \
|
||||||
'$(STAGING_PATH)/pocketbase/data/data.db' \
|
$(STAGING_PATH)/data/vereinshaus.db-wal \
|
||||||
'$(STAGING_PATH)/pocketbase/data/data.db-wal' \
|
$(STAGING_PATH)/data/vereinshaus.db-shm && \
|
||||||
'$(STAGING_PATH)/pocketbase/data/data.db-shm' \
|
rm -rf $(STAGING_PATH)/data/uploads"
|
||||||
$(STAGING_PATH)/pocketbase/migrations"
|
|
||||||
@echo "→ Alle Hooks auf Staging löschen (werden via staging-deploy neu geschrieben)..."
|
|
||||||
@ssh $(DS_HOST) "rm -f $(STAGING_HOOKS)/*.pb.js"
|
|
||||||
@echo " ✓ Reset fertig. Jetzt: make staging-deploy && make staging-seed"
|
@echo " ✓ Reset fertig. Jetzt: make staging-deploy && make staging-seed"
|
||||||
|
|
||||||
staging-seed:
|
staging-seed:
|
||||||
@echo "→ Testdaten in Staging einfügen..."
|
@echo "→ Testdaten in Staging einfügen..."
|
||||||
@echo " Voraussetzung: PB_EMAIL + PB_PASSWORD in .env gesetzt (Staging-Superuser)"
|
|
||||||
@if [ -f .env ]; then \
|
@if [ -f .env ]; then \
|
||||||
export $$(grep -v '^#' .env | xargs) && \
|
export $$(grep -v '^#' .env | xargs) && \
|
||||||
PB_URL=https://api-staging.vereins.haus node scripts/seed.js; \
|
APP_URL=https://staging.vereins.haus node scripts/seed.js; \
|
||||||
else \
|
else \
|
||||||
PB_URL=https://api-staging.vereins.haus node scripts/seed.js; \
|
APP_URL=https://staging.vereins.haus node scripts/seed.js; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
staging-logs: check-ssh
|
staging-logs: check-ssh
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
FROM node:22-alpine AS builder
|
FROM node:22-alpine AS builder
|
||||||
|
RUN apk add --no-cache python3 make g++
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
COPY . .
|
COPY . .
|
||||||
ARG VITE_PB_URL=http://vereinshaus-pocketbase:8090
|
|
||||||
ENV VITE_PB_URL=$VITE_PB_URL
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM node:22-alpine
|
FROM node:22-alpine
|
||||||
|
RUN apk add --no-cache python3 make g++
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /app/build ./build
|
COPY --from=builder /app/build ./build
|
||||||
COPY --from=builder /app/package*.json ./
|
COPY --from=builder /app/package*.json ./
|
||||||
|
|
|
||||||
449
app/package-lock.json
generated
449
app/package-lock.json
generated
|
|
@ -9,7 +9,10 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@event-calendar/core": "^5.7.0",
|
"@event-calendar/core": "^5.7.0",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
|
"better-sqlite3": "^12.10.0",
|
||||||
"ical-generator": "^10.2.0",
|
"ical-generator": "^10.2.0",
|
||||||
|
"jose": "^6.2.3",
|
||||||
"papaparse": "^5.5.3",
|
"papaparse": "^5.5.3",
|
||||||
"pocketbase": "^0.26.9",
|
"pocketbase": "^0.26.9",
|
||||||
"rrule": "^2.8.1",
|
"rrule": "^2.8.1",
|
||||||
|
|
@ -19,6 +22,8 @@
|
||||||
"@sveltejs/adapter-node": "^5.5.4",
|
"@sveltejs/adapter-node": "^5.5.4",
|
||||||
"@sveltejs/kit": "^2.57.0",
|
"@sveltejs/kit": "^2.57.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/papaparse": "^5.5.2",
|
"@types/papaparse": "^5.5.2",
|
||||||
"@types/web-push": "^3.6.4",
|
"@types/web-push": "^3.6.4",
|
||||||
"svelte": "^5.55.2",
|
"svelte": "^5.55.2",
|
||||||
|
|
@ -2637,6 +2642,23 @@
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/bcryptjs": {
|
||||||
|
"version": "2.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
|
||||||
|
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/better-sqlite3": {
|
||||||
|
"version": "7.6.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
||||||
|
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/cookie": {
|
"node_modules/@types/cookie": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||||
|
|
@ -2895,6 +2917,26 @@
|
||||||
"node": "18 || 20 || >=22"
|
"node": "18 || 20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.10.31",
|
"version": "2.10.31",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz",
|
||||||
|
|
@ -2908,6 +2950,49 @@
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bcryptjs": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"bin": {
|
||||||
|
"bcrypt": "bin/bcrypt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/better-sqlite3": {
|
||||||
|
"version": "12.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz",
|
||||||
|
"integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bindings": "^1.5.0",
|
||||||
|
"prebuild-install": "^7.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bindings": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"file-uri-to-path": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bl": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": "^5.5.0",
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"readable-stream": "^3.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bn.js": {
|
"node_modules/bn.js": {
|
||||||
"version": "4.12.3",
|
"version": "4.12.3",
|
||||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||||
|
|
@ -2961,6 +3046,30 @@
|
||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.1.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer-equal-constant-time": {
|
"node_modules/buffer-equal-constant-time": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
|
@ -3061,6 +3170,12 @@
|
||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chownr": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/clsx": {
|
"node_modules/clsx": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
|
|
@ -3221,6 +3336,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decompress-response": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-response": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/deep-extend": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/deepmerge": {
|
"node_modules/deepmerge": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
|
|
@ -3271,7 +3410,6 @@
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
|
|
@ -3330,6 +3468,15 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/end-of-stream": {
|
||||||
|
"version": "1.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||||
|
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-abstract": {
|
"node_modules/es-abstract": {
|
||||||
"version": "1.24.2",
|
"version": "1.24.2",
|
||||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
|
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
|
||||||
|
|
@ -3529,6 +3676,15 @@
|
||||||
"url": "https://github.com/bgub/eta?sponsor=1"
|
"url": "https://github.com/bgub/eta?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expand-template": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||||
|
"license": "(MIT OR WTFPL)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
|
@ -3578,6 +3734,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/file-uri-to-path": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/filelist": {
|
"node_modules/filelist": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
|
||||||
|
|
@ -3651,6 +3813,12 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fs-constants": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||||
|
|
@ -3807,6 +3975,12 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/github-from-package": {
|
||||||
|
"version": "0.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||||
|
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "11.1.0",
|
"version": "11.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
|
||||||
|
|
@ -4030,12 +4204,38 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/inherits": {
|
"node_modules/inherits": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ini": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/internal-slot": {
|
"node_modules/internal-slot": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||||
|
|
@ -4508,6 +4708,15 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jose": {
|
||||||
|
"version": "6.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
|
||||||
|
"integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
|
@ -4922,6 +5131,18 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mimic-response": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimalistic-assert": {
|
"node_modules/minimalistic-assert": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||||
|
|
@ -4963,6 +5184,12 @@
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mkdirp-classic": {
|
||||||
|
"version": "0.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||||
|
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/mri": {
|
"node_modules/mri": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||||
|
|
@ -5008,6 +5235,36 @@
|
||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/napi-build-utils": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/node-abi": {
|
||||||
|
"version": "3.92.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz",
|
||||||
|
"integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^7.3.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-abi/node_modules/semver": {
|
||||||
|
"version": "7.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
|
||||||
|
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.44",
|
"version": "2.0.44",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz",
|
||||||
|
|
@ -5070,6 +5327,15 @@
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/own-keys": {
|
"node_modules/own-keys": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
||||||
|
|
@ -5210,6 +5476,33 @@
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prebuild-install": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||||
|
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.0",
|
||||||
|
"expand-template": "^2.0.3",
|
||||||
|
"github-from-package": "0.0.0",
|
||||||
|
"minimist": "^1.2.3",
|
||||||
|
"mkdirp-classic": "^0.5.3",
|
||||||
|
"napi-build-utils": "^2.0.0",
|
||||||
|
"node-abi": "^3.3.0",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"rc": "^1.2.7",
|
||||||
|
"simple-get": "^4.0.0",
|
||||||
|
"tar-fs": "^2.0.0",
|
||||||
|
"tunnel-agent": "^0.6.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"prebuild-install": "bin.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pretty-bytes": {
|
"node_modules/pretty-bytes": {
|
||||||
"version": "6.1.1",
|
"version": "6.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
|
||||||
|
|
@ -5223,6 +5516,16 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pump": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"end-of-stream": "^1.1.0",
|
||||||
|
"once": "^1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
|
@ -5233,6 +5536,35 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rc": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||||
|
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||||
|
"dependencies": {
|
||||||
|
"deep-extend": "^0.6.0",
|
||||||
|
"ini": "~1.3.0",
|
||||||
|
"minimist": "^1.2.0",
|
||||||
|
"strip-json-comments": "~2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rc": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
|
|
@ -5758,6 +6090,51 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/simple-concat": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/simple-get": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"decompress-response": "^6.0.0",
|
||||||
|
"once": "^1.3.1",
|
||||||
|
"simple-concat": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sirv": {
|
"node_modules/sirv": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
|
||||||
|
|
@ -5842,6 +6219,15 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string.prototype.matchall": {
|
"node_modules/string.prototype.matchall": {
|
||||||
"version": "4.0.12",
|
"version": "4.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
|
||||||
|
|
@ -5954,6 +6340,15 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strip-json-comments": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/supports-preserve-symlinks-flag": {
|
"node_modules/supports-preserve-symlinks-flag": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||||
|
|
@ -6027,6 +6422,34 @@
|
||||||
"@types/estree": "^1.0.6"
|
"@types/estree": "^1.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tar-fs": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chownr": "^1.1.1",
|
||||||
|
"mkdirp-classic": "^0.5.2",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"tar-stream": "^2.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar-stream": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bl": "^4.0.3",
|
||||||
|
"end-of-stream": "^1.4.1",
|
||||||
|
"fs-constants": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/temp-dir": {
|
"node_modules/temp-dir": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
|
||||||
|
|
@ -6118,6 +6541,18 @@
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
|
"node_modules/tunnel-agent": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/type-fest": {
|
"node_modules/type-fest": {
|
||||||
"version": "0.16.0",
|
"version": "0.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz",
|
||||||
|
|
@ -6358,6 +6793,12 @@
|
||||||
"browserslist": ">= 4.21.0"
|
"browserslist": ">= 4.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.13",
|
"version": "8.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
|
||||||
|
|
@ -6850,6 +7291,12 @@
|
||||||
"workbox-core": "7.4.1"
|
"workbox-core": "7.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
"@sveltejs/adapter-node": "^5.5.4",
|
"@sveltejs/adapter-node": "^5.5.4",
|
||||||
"@sveltejs/kit": "^2.57.0",
|
"@sveltejs/kit": "^2.57.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/papaparse": "^5.5.2",
|
"@types/papaparse": "^5.5.2",
|
||||||
"@types/web-push": "^3.6.4",
|
"@types/web-push": "^3.6.4",
|
||||||
"svelte": "^5.55.2",
|
"svelte": "^5.55.2",
|
||||||
|
|
@ -27,7 +29,10 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@event-calendar/core": "^5.7.0",
|
"@event-calendar/core": "^5.7.0",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
|
"better-sqlite3": "^12.10.0",
|
||||||
"ical-generator": "^10.2.0",
|
"ical-generator": "^10.2.0",
|
||||||
|
"jose": "^6.2.3",
|
||||||
"papaparse": "^5.5.3",
|
"papaparse": "^5.5.3",
|
||||||
"pocketbase": "^0.26.9",
|
"pocketbase": "^0.26.9",
|
||||||
"rrule": "^2.8.1",
|
"rrule": "^2.8.1",
|
||||||
|
|
|
||||||
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 { goto } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { pb } from '$lib/pb';
|
import { user } from '$lib/user';
|
||||||
import Icon from '$lib/components/Icon.svelte';
|
import Icon from '$lib/components/Icon.svelte';
|
||||||
import type { IconName } from '$lib/icons';
|
import type { IconName } from '$lib/icons';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!pb.authStore.isValid) {
|
if (!$user) { goto('/login'); return; }
|
||||||
goto('/login');
|
if (!$user.verein_id) { goto('/onboarding'); return; }
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!pb.authStore.record?.verein_id) {
|
|
||||||
goto('/onboarding');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
registerPush();
|
registerPush();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function registerPush() {
|
async function registerPush() {
|
||||||
if (!('serviceWorker' in navigator) || !('PushManager' in window)) return;
|
if (!('serviceWorker' in navigator) || !('PushManager' in window)) return;
|
||||||
|
|
||||||
const permission = await Notification.requestPermission();
|
const permission = await Notification.requestPermission();
|
||||||
if (permission !== 'granted') return;
|
if (permission !== 'granted') return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const reg = await navigator.serviceWorker.ready;
|
const reg = await navigator.serviceWorker.ready;
|
||||||
|
|
||||||
// VAPID public key vom Server holen
|
|
||||||
const keyRes = await fetch('/api/push/key');
|
const keyRes = await fetch('/api/push/key');
|
||||||
const { publicKey } = await keyRes.json();
|
const { publicKey } = await keyRes.json();
|
||||||
if (!publicKey) return;
|
if (!publicKey) return;
|
||||||
|
|
||||||
// Bestehende oder neue Subscription
|
|
||||||
let sub = await reg.pushManager.getSubscription();
|
let sub = await reg.pushManager.getSubscription();
|
||||||
if (!sub) {
|
if (!sub) {
|
||||||
sub = await reg.pushManager.subscribe({
|
sub = await reg.pushManager.subscribe({
|
||||||
|
|
@ -42,18 +30,10 @@
|
||||||
applicationServerKey: urlBase64ToUint8Array(publicKey) as BufferSource,
|
applicationServerKey: urlBase64ToUint8Array(publicKey) as BufferSource,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// In PocketBase speichern
|
|
||||||
await fetch('/api/push/subscribe', {
|
await fetch('/api/push/subscribe', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${$user?.token}` },
|
||||||
'Content-Type': 'application/json',
|
body: JSON.stringify({ subscription: sub.toJSON(), userId: $user?.id }),
|
||||||
Authorization: pb.authStore.token,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
subscription: sub.toJSON(),
|
|
||||||
userId: pb.authStore.record?.id,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[push] Registrierung fehlgeschlagen:', e);
|
console.warn('[push] Registrierung fehlgeschlagen:', e);
|
||||||
|
|
@ -67,7 +47,7 @@
|
||||||
return Uint8Array.from([...raw].map((c) => c.charCodeAt(0)));
|
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 }[] = [
|
const allNavItems: { href: string; label: string; icon: IconName; adminOnly?: boolean }[] = [
|
||||||
{ href: '/neuigkeiten', label: 'Neuigkeiten', icon: 'images' },
|
{ href: '/neuigkeiten', label: 'Neuigkeiten', icon: 'images' },
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { pb } from '$lib/pb';
|
import { api } from '$lib/api';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { Verein, Termin } from '$lib/types';
|
import type { Verein, Termin } from '$lib/types';
|
||||||
|
|
||||||
|
|
@ -8,15 +8,12 @@
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const vid = pb.authStore.record?.verein_id as string;
|
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
[verein, termine] = await Promise.all([
|
[verein, termine] = await Promise.all([
|
||||||
pb.collection('vereine').getOne<Verein>(vid),
|
api.get<Verein>('/vereine'),
|
||||||
pb.collection('termine').getList<Termin>(1, 3, {
|
api.get<Termin[]>('/termine', { sort: 'beginn', page: '1', perPage: '3' }),
|
||||||
filter: `beginn >= "${now}"`,
|
|
||||||
sort: 'beginn',
|
|
||||||
}).then(r => r.items),
|
|
||||||
]);
|
]);
|
||||||
|
termine = (termine as any)?.items ?? termine;
|
||||||
loading = false;
|
loading = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<script lang="ts">
|
<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 { goto } from '$app/navigation';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { generatePain008, downloadXml, minEinzugsdatum, type SepaPosition } from '$lib/sepa';
|
import { generatePain008, downloadXml, minEinzugsdatum, type SepaPosition } from '$lib/sepa';
|
||||||
|
|
@ -41,11 +43,10 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (pb.authStore.record?.rolle === 'trainer') { goto('/'); return; }
|
if (get(user)?.rolle === 'trainer') { goto('/'); return; }
|
||||||
const vid = pb.authStore.record?.verein_id as string;
|
|
||||||
[beitraege, verein] = await Promise.all([
|
[beitraege, verein] = await Promise.all([
|
||||||
pb.collection('beitraege').getFullList<Beitrag>({ sort: 'name' }),
|
api.get<Beitrag[]>('/beitraege', { sort: 'name' }),
|
||||||
pb.collection('vereine').getOne<Verein>(vid).catch(() => null),
|
api.get<Verein>('/vereine').catch(() => null),
|
||||||
]);
|
]);
|
||||||
loading = false;
|
loading = false;
|
||||||
});
|
});
|
||||||
|
|
@ -71,13 +72,12 @@
|
||||||
}
|
}
|
||||||
saving = true;
|
saving = true;
|
||||||
try {
|
try {
|
||||||
const vid = pb.authStore.record?.verein_id as string;
|
const data = { name: fName.trim(), betrag, rhythmus: fRhythmus, beschreibung: fBeschr.trim() };
|
||||||
const data = { verein_id: vid, name: fName.trim(), betrag, rhythmus: fRhythmus, beschreibung: fBeschr.trim() };
|
|
||||||
if (editId) {
|
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);
|
beitraege = beitraege.map(b => b.id === editId ? { ...b, ...data } as Beitrag : b);
|
||||||
} else {
|
} 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));
|
beitraege = [...beitraege, neu].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
}
|
||||||
showForm = false;
|
showForm = false;
|
||||||
|
|
@ -90,7 +90,7 @@
|
||||||
|
|
||||||
async function loeschen(b: Beitrag) {
|
async function loeschen(b: Beitrag) {
|
||||||
if (!confirm(`"${b.name}" wirklich löschen?`)) return;
|
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);
|
beitraege = beitraege.filter(x => x.id !== b.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,9 +102,7 @@
|
||||||
einzugsdatum = minEinzugsdatum();
|
einzugsdatum = minEinzugsdatum();
|
||||||
sepaLoading = true;
|
sepaLoading = true;
|
||||||
try {
|
try {
|
||||||
const alle = await pb.collection('mitglieder').getFullList<Mitglied>({
|
const alle = await api.get<Mitglied[]>('/mitglieder', { status: 'aktiv', sort: 'nachname,vorname' });
|
||||||
filter: 'status = "aktiv"', sort: 'nachname,vorname',
|
|
||||||
});
|
|
||||||
const mit = alle.filter(m => m.iban?.trim());
|
const mit = alle.filter(m => m.iban?.trim());
|
||||||
const ohne = alle.length - mit.length;
|
const ohne = alle.length - mit.length;
|
||||||
sepaPreview = { mitglieder: mit, ohne };
|
sepaPreview = { mitglieder: mit, ohne };
|
||||||
|
|
@ -157,10 +155,9 @@
|
||||||
downloadXml(xml, `sepa-einzug-${einzugsdatum}.xml`);
|
downloadXml(xml, `sepa-einzug-${einzugsdatum}.xml`);
|
||||||
|
|
||||||
// Einzüge als "ausstehend" anlegen
|
// Einzüge als "ausstehend" anlegen
|
||||||
const vid = pb.authStore.record?.verein_id as string;
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
sepaPreview.mitglieder.map((m) =>
|
sepaPreview.mitglieder.map((m) =>
|
||||||
pb.collection('einzuege').create({
|
api.post('/einzuege', {
|
||||||
mitglied_id: m.id,
|
mitglied_id: m.id,
|
||||||
beitrag_id: sepaFor!.id,
|
beitrag_id: sepaFor!.id,
|
||||||
betrag: sepaFor!.betrag,
|
betrag: sepaFor!.betrag,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
<script lang="ts">
|
<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 { goto } from '$app/navigation';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { Verein, Gruppe } from '$lib/types';
|
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 loading = $state(true);
|
||||||
let saving = $state(false);
|
let saving = $state(false);
|
||||||
|
|
@ -60,19 +62,19 @@
|
||||||
];
|
];
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
vereinId = pb.authStore.record?.verein_id as string;
|
vereinId = get(user)?.verein_id ?? '';
|
||||||
const [v, alleUser, alleGruppen, mitgliederCount] = await Promise.all([
|
const [v, alleUser, alleGruppen, mitgliederCount] = await Promise.all([
|
||||||
pb.collection('vereine').getOne<Verein>(vereinId),
|
api.get<Verein>('/vereine'),
|
||||||
isAdmin()
|
isAdmin()
|
||||||
? pb.collection('users').getFullList({ filter: `verein_id = "${vereinId}"` })
|
? api.get<any[]>('/users')
|
||||||
: Promise.resolve([]),
|
: Promise.resolve([]),
|
||||||
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
|
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
|
||||||
pb.collection('mitglieder').getList(1, 1, { filter: `verein_id = "${vereinId}"` }).then(r => r.totalItems),
|
api.get<{ total: number }>('/mitglieder/count').catch(() => ({ total: 0 })),
|
||||||
]);
|
]);
|
||||||
trainer = alleUser.filter((u: any) => u.rolle === 'trainer');
|
trainer = alleUser.filter((u: any) => u.rolle === 'trainer');
|
||||||
gruppen = alleGruppen;
|
gruppen = alleGruppen;
|
||||||
plan = v.plan ?? 'free';
|
plan = v.plan ?? 'free';
|
||||||
mitgliederAnz = mitgliederCount;
|
mitgliederAnz = (mitgliederCount as any).total ?? 0;
|
||||||
name = v.name ?? '';
|
name = v.name ?? '';
|
||||||
adresse = v.adresse ?? '';
|
adresse = v.adresse ?? '';
|
||||||
plz = v.plz ?? '';
|
plz = v.plz ?? '';
|
||||||
|
|
@ -91,7 +93,7 @@
|
||||||
if (!name.trim()) { error = 'Vereinsname ist Pflichtfeld.'; return; }
|
if (!name.trim()) { error = 'Vereinsname ist Pflichtfeld.'; return; }
|
||||||
error = ''; success = ''; saving = true;
|
error = ''; success = ''; saving = true;
|
||||||
try {
|
try {
|
||||||
await pb.collection('vereine').update(vereinId, {
|
await api.patch('/vereine', {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
adresse: adresse.trim() || null,
|
adresse: adresse.trim() || null,
|
||||||
plz: plz.trim() || null,
|
plz: plz.trim() || null,
|
||||||
|
|
@ -113,17 +115,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function trainerEinladen() {
|
async function trainerEinladen() {
|
||||||
const token = crypto.randomUUID().replace(/-/g, '');
|
const inv = await api.post<{ token: string }>('/einladungen', { rolle: 'trainer' });
|
||||||
await pb.collection('einladungen').create({
|
einladungUrl = `${window.location.origin}/invite/${inv.token}`;
|
||||||
verein_id: vereinId, rolle: 'trainer', token, genutzt: false,
|
|
||||||
});
|
|
||||||
einladungUrl = `${window.location.origin}/invite/${token}`;
|
|
||||||
einladungKopiert = false;
|
einladungKopiert = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function trainerEntfernen(uid: string) {
|
async function trainerEntfernen(uid: string) {
|
||||||
if (!confirm('Trainer wirklich entfernen?')) return;
|
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);
|
trainer = trainer.filter(t => t.id !== uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,7 +134,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function abmelden() {
|
function abmelden() {
|
||||||
pb.authStore.clear();
|
user.clear();
|
||||||
goto('/login');
|
goto('/login');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { pb } from '$lib/pb';
|
import { api } from '$lib/api';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import Papa from 'papaparse';
|
import Papa from 'papaparse';
|
||||||
import type { Mitglied } from '$lib/types';
|
import type { Mitglied } from '$lib/types';
|
||||||
|
|
@ -39,10 +39,9 @@
|
||||||
];
|
];
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const vid = pb.authStore.record?.verein_id as string;
|
|
||||||
const [m, v] = await Promise.all([
|
const [m, v] = await Promise.all([
|
||||||
pb.collection('mitglieder').getFullList<Mitglied>({ sort: 'nachname,vorname' }),
|
api.get<Mitglied[]>('/mitglieder', { sort: 'nachname,vorname' }),
|
||||||
pb.collection('vereine').getOne<any>(vid),
|
api.get<any>('/vereine'),
|
||||||
]);
|
]);
|
||||||
mitglieder = m;
|
mitglieder = m;
|
||||||
vereinName = v.name ?? '';
|
vereinName = v.name ?? '';
|
||||||
|
|
@ -93,14 +92,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportiereBackup() {
|
async function exportiereBackup() {
|
||||||
const vid = pb.authStore.record?.verein_id as string;
|
|
||||||
const [verein, mitgl, gruppen, termine, beitraege, nachrichten] = await Promise.all([
|
const [verein, mitgl, gruppen, termine, beitraege, nachrichten] = await Promise.all([
|
||||||
pb.collection('vereine').getOne<any>(vid),
|
api.get<any>('/vereine'),
|
||||||
pb.collection('mitglieder').getFullList(),
|
api.get<any[]>('/mitglieder'),
|
||||||
pb.collection('gruppen').getFullList(),
|
api.get<any[]>('/gruppen'),
|
||||||
pb.collection('termine').getFullList(),
|
api.get<any[]>('/termine'),
|
||||||
pb.collection('beitraege').getFullList(),
|
api.get<any[]>('/beitraege'),
|
||||||
pb.collection('nachrichten').getFullList(),
|
api.get<any[]>('/nachrichten'),
|
||||||
]);
|
]);
|
||||||
const backup = {
|
const backup = {
|
||||||
exportiert_am: new Date().toISOString(),
|
exportiert_am: new Date().toISOString(),
|
||||||
|
|
@ -177,12 +175,11 @@
|
||||||
|
|
||||||
async function importStarten() {
|
async function importStarten() {
|
||||||
importLaeuft = true;
|
importLaeuft = true;
|
||||||
const vid = pb.authStore.record?.verein_id as string;
|
|
||||||
let ok = 0;
|
let ok = 0;
|
||||||
const fehler: string[] = [];
|
const fehler: string[] = [];
|
||||||
|
|
||||||
for (const row of csvRows) {
|
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)) {
|
for (const [csvSpalte, ziel] of Object.entries(feldMapping)) {
|
||||||
if (!ziel) continue;
|
if (!ziel) continue;
|
||||||
const wert = row[csvSpalte]?.trim() ?? '';
|
const wert = row[csvSpalte]?.trim() ?? '';
|
||||||
|
|
@ -200,7 +197,7 @@
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await pb.collection('mitglieder').create(record);
|
await api.post('/mitglieder', record);
|
||||||
ok++;
|
ok++;
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
fehler.push(`${record.vorname} ${record.nachname}: ${e instanceof Error ? e.message : 'Fehler'}`);
|
fehler.push(`${record.vorname} ${record.nachname}: ${e instanceof Error ? e.message : 'Fehler'}`);
|
||||||
|
|
@ -208,7 +205,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mitgliederliste aktualisieren
|
// Mitgliederliste aktualisieren
|
||||||
mitglieder = await pb.collection('mitglieder').getFullList<Mitglied>({ sort: 'nachname,vorname' });
|
mitglieder = await api.get<Mitglied[]>('/mitglieder', { sort: 'nachname,vorname' });
|
||||||
importResult = { ok, fehler };
|
importResult = { ok, fehler };
|
||||||
importPhase = 'done';
|
importPhase = 'done';
|
||||||
importLaeuft = false;
|
importLaeuft = false;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { pb } from '$lib/pb';
|
import { api } from '$lib/api';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
let mitglieder = $state<any[]>([]);
|
let mitglieder = $state<any[]>([]);
|
||||||
|
|
@ -9,8 +9,8 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
[mitglieder, gruppen] = await Promise.all([
|
[mitglieder, gruppen] = await Promise.all([
|
||||||
pb.collection('mitglieder').getFullList({ sort: 'nachname,vorname' }),
|
api.get<any[]>('/mitglieder', { sort: 'nachname,vorname' }),
|
||||||
pb.collection('gruppen').getFullList({ sort: 'name' })
|
api.get<any[]>('/gruppen', { sort: 'name' })
|
||||||
]);
|
]);
|
||||||
loading = false;
|
loading = false;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { pb } from '$lib/pb';
|
import { api } from '$lib/api';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
@ -61,8 +61,8 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const [m, g] = await Promise.all([
|
const [m, g] = await Promise.all([
|
||||||
pb.collection('mitglieder').getOne<Mitglied>(id),
|
api.get<Mitglied>('/mitglieder/' + id),
|
||||||
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
|
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
|
||||||
]);
|
]);
|
||||||
loadRecord(m);
|
loadRecord(m);
|
||||||
gruppen = g;
|
gruppen = g;
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
async function speichern() {
|
async function speichern() {
|
||||||
error = ''; saving = true;
|
error = ''; saving = true;
|
||||||
try {
|
try {
|
||||||
await pb.collection('mitglieder').update(id, {
|
await api.put('/mitglieder/' + id, {
|
||||||
vorname: vorname.trim(),
|
vorname: vorname.trim(),
|
||||||
nachname: nachname.trim(),
|
nachname: nachname.trim(),
|
||||||
email: email.trim() || null,
|
email: email.trim() || null,
|
||||||
|
|
@ -107,7 +107,7 @@
|
||||||
|
|
||||||
async function loeschen() {
|
async function loeschen() {
|
||||||
try {
|
try {
|
||||||
await pb.collection('mitglieder').delete(id);
|
await api.del('/mitglieder/' + id);
|
||||||
goto('/mitglieder');
|
goto('/mitglieder');
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
error = e instanceof Error ? e.message : 'Fehler beim Löschen.';
|
error = e instanceof Error ? e.message : 'Fehler beim Löschen.';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { pb } from '$lib/pb';
|
import { api } from '$lib/api';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
gruppen = await pb.collection('gruppen').getFullList({ sort: 'name' });
|
gruppen = await api.get<any[]>('/gruppen', { sort: 'name' });
|
||||||
});
|
});
|
||||||
|
|
||||||
function toggleGruppe(id: string) {
|
function toggleGruppe(id: string) {
|
||||||
|
|
@ -43,9 +43,7 @@
|
||||||
async function speichern() {
|
async function speichern() {
|
||||||
error = ''; loading = true;
|
error = ''; loading = true;
|
||||||
try {
|
try {
|
||||||
const verein_id = pb.authStore.record?.verein_id as string;
|
await api.post('/mitglieder', {
|
||||||
await pb.collection('mitglieder').create({
|
|
||||||
verein_id,
|
|
||||||
vorname: vorname.trim(),
|
vorname: vorname.trim(),
|
||||||
nachname: nachname.trim(),
|
nachname: nachname.trim(),
|
||||||
email: email.trim() || null,
|
email: email.trim() || null,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<script lang="ts">
|
<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 { onMount } from 'svelte';
|
||||||
import type { Nachricht, Gruppe } from '$lib/types';
|
import type { Nachricht, Gruppe } from '$lib/types';
|
||||||
|
|
||||||
|
|
@ -18,8 +20,8 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
[nachrichten, gruppen] = await Promise.all([
|
[nachrichten, gruppen] = await Promise.all([
|
||||||
pb.collection('nachrichten').getFullList<Nachricht>({ sort: '-gesendet_am' }),
|
api.get<Nachricht[]>('/nachrichten', { sort: '-gesendet_am' }),
|
||||||
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
|
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
|
||||||
]);
|
]);
|
||||||
loading = false;
|
loading = false;
|
||||||
});
|
});
|
||||||
|
|
@ -43,12 +45,7 @@
|
||||||
sendError = ''; sending = true;
|
sendError = ''; sending = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const verein_id = pb.authStore.record?.verein_id as string;
|
const record = await api.post<Nachricht>('/nachrichten', {
|
||||||
const autor_id = pb.authStore.record?.id as string;
|
|
||||||
|
|
||||||
const record = await pb.collection('nachrichten').create<Nachricht>({
|
|
||||||
verein_id,
|
|
||||||
autor_id,
|
|
||||||
betreff: fBetreff.trim(),
|
betreff: fBetreff.trim(),
|
||||||
text: fText.trim(),
|
text: fText.trim(),
|
||||||
gruppe_ids: fGruppeIds,
|
gruppe_ids: fGruppeIds,
|
||||||
|
|
@ -63,7 +60,7 @@
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: pb.authStore.token,
|
Authorization: `Bearer ${get(user)?.token ?? ''}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
titel: fBetreff.trim(),
|
titel: fBetreff.trim(),
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
<script lang="ts">
|
<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 { onMount } from 'svelte';
|
||||||
import type { Neuigkeit, Gruppe, Termin } from '$lib/types';
|
import type { Neuigkeit, Gruppe, Termin } from '$lib/types';
|
||||||
|
|
||||||
const canPost = () => !pb.authStore.record?.rolle || pb.authStore.record?.rolle !== null;
|
const canPost = () => true; // alle eingeloggten User dürfen posten
|
||||||
const userId = () => pb.authStore.record?.id as string;
|
const userId = () => get(user)?.id as string;
|
||||||
|
|
||||||
let neuigkeiten = $state<Neuigkeit[]>([]);
|
let neuigkeiten = $state<Neuigkeit[]>([]);
|
||||||
let gruppen = $state<Gruppe[]>([]);
|
let gruppen = $state<Gruppe[]>([]);
|
||||||
|
|
@ -28,31 +30,25 @@
|
||||||
let ladeError = $state('');
|
let ladeError = $state('');
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const vid = pb.authStore.record?.verein_id as string;
|
|
||||||
try {
|
try {
|
||||||
// Queries einzeln damit ein Fehler sichtbar wird
|
// Queries einzeln damit ein Fehler sichtbar wird
|
||||||
const [nList, gList] = await Promise.all([
|
const [nList, gList] = await Promise.all([
|
||||||
pb.collection('neuigkeiten').getFullList<Neuigkeit>({
|
api.get<Neuigkeit[]>('/neuigkeiten', { sort: '-created' }),
|
||||||
sort: '-created',
|
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
|
||||||
}),
|
|
||||||
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
|
|
||||||
]);
|
]);
|
||||||
neuigkeiten = nList;
|
neuigkeiten = nList;
|
||||||
gruppen = gList;
|
gruppen = gList;
|
||||||
|
|
||||||
// Termine der letzten 30 Tage + zukünftige
|
// Termine der letzten 30 Tage + zukünftige
|
||||||
try {
|
try {
|
||||||
const von = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().replace('T', ' ');
|
termine = await api.get<Termin[]>('/termine', { sort: '-beginn' });
|
||||||
termine = await pb.collection('termine').getFullList<Termin>({
|
|
||||||
filter: `beginn >= '${von}'`, sort: '-beginn',
|
|
||||||
});
|
|
||||||
} catch { termine = []; }
|
} catch { termine = []; }
|
||||||
|
|
||||||
// Reaktionen – separat damit Fehler nicht alles blockiert
|
// Reaktionen – separat damit Fehler nicht alles blockiert
|
||||||
try {
|
try {
|
||||||
const [rList, meineList] = await Promise.all([
|
const [rList, meineList] = await Promise.all([
|
||||||
pb.collection('reaktionen').getFullList({ filter: `beitrag_id.verein_id = "${vid}"` }),
|
api.get<any[]>('/reaktionen'),
|
||||||
pb.collection('reaktionen').getFullList({ filter: `user_id = "${userId()}"` }),
|
api.get<any[]>('/reaktionen', { meine: 'true' }),
|
||||||
]);
|
]);
|
||||||
const counts: Record<string, number> = {};
|
const counts: Record<string, number> = {};
|
||||||
for (const r of rList) counts[r.beitrag_id] = (counts[r.beitrag_id] ?? 0) + 1;
|
for (const r of rList) counts[r.beitrag_id] = (counts[r.beitrag_id] ?? 0) + 1;
|
||||||
|
|
@ -96,11 +92,9 @@
|
||||||
}
|
}
|
||||||
formError = ''; saving = true;
|
formError = ''; saving = true;
|
||||||
try {
|
try {
|
||||||
const vid = pb.authStore.record?.verein_id as string;
|
const u = get(user);
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append('verein_id', vid);
|
form.append('autor_name', u?.name ?? '');
|
||||||
form.append('autor_id', userId());
|
|
||||||
form.append('autor_name', pb.authStore.record?.name ?? '');
|
|
||||||
if (fText.trim()) form.append('text', fText.trim());
|
if (fText.trim()) form.append('text', fText.trim());
|
||||||
if (fTerminId) form.append('termin_id', fTerminId);
|
if (fTerminId) form.append('termin_id', fTerminId);
|
||||||
for (const id of fGruppeIds) form.append('gruppe_ids', id);
|
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);
|
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];
|
neuigkeiten = [neu, ...neuigkeiten];
|
||||||
showForm = false;
|
showForm = false;
|
||||||
fText = ''; fGruppeIds = []; fTerminId = ''; fDateien = null; previews = [];
|
fText = ''; fGruppeIds = []; fTerminId = ''; fDateien = null; previews = [];
|
||||||
|
|
@ -121,26 +115,24 @@
|
||||||
|
|
||||||
async function loeschen(n: Neuigkeit) {
|
async function loeschen(n: Neuigkeit) {
|
||||||
if (!confirm('Beitrag löschen?')) return;
|
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);
|
neuigkeiten = neuigkeiten.filter(x => x.id !== n.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleReaktion(n: Neuigkeit) {
|
async function toggleReaktion(n: Neuigkeit) {
|
||||||
if (meineReaktion[n.id]) {
|
if (meineReaktion[n.id]) {
|
||||||
await pb.collection('reaktionen').delete(meineReaktion[n.id]);
|
await api.del('/reaktionen/' + meineReaktion[n.id]);
|
||||||
meineReaktion = { ...meineReaktion, [n.id]: '' };
|
meineReaktion = { ...meineReaktion, [n.id]: '' };
|
||||||
reaktionen = { ...reaktionen, [n.id]: Math.max(0, (reaktionen[n.id] ?? 1) - 1) };
|
reaktionen = { ...reaktionen, [n.id]: Math.max(0, (reaktionen[n.id] ?? 1) - 1) };
|
||||||
} else {
|
} else {
|
||||||
const r = await pb.collection('reaktionen').create({
|
const r = await api.post<{ id: string }>('/reaktionen', { beitrag_id: n.id });
|
||||||
beitrag_id: n.id, user_id: userId(),
|
|
||||||
});
|
|
||||||
meineReaktion = { ...meineReaktion, [n.id]: r.id };
|
meineReaktion = { ...meineReaktion, [n.id]: r.id };
|
||||||
reaktionen = { ...reaktionen, [n.id]: (reaktionen[n.id] ?? 0) + 1 };
|
reaktionen = { ...reaktionen, [n.id]: (reaktionen[n.id] ?? 0) + 1 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mediaUrl(n: Neuigkeit, datei: string, thumb = false): string {
|
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 {
|
function isVideo(datei: string): boolean {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { pb } from '$lib/pb';
|
import { api } from '$lib/api';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { Veranstaltungsort, OrtAusfall } from '$lib/types';
|
import type { Veranstaltungsort, OrtAusfall } from '$lib/types';
|
||||||
|
|
||||||
|
|
@ -31,10 +31,9 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const vid = pb.authStore.record?.verein_id as string;
|
|
||||||
[orte, ausfaelle] = await Promise.all([
|
[orte, ausfaelle] = await Promise.all([
|
||||||
pb.collection('veranstaltungsorte').getFullList<Veranstaltungsort>({ sort: 'name', filter: `verein_id = "${vid}"` }),
|
api.get<Veranstaltungsort[]>('/veranstaltungsorte', { sort: 'name' }),
|
||||||
pb.collection('ort_ausfaelle').getFullList<OrtAusfall>({ sort: 'von' }),
|
api.get<OrtAusfall[]>('/ort_ausfaelle', { sort: 'von' }),
|
||||||
]);
|
]);
|
||||||
loading = false;
|
loading = false;
|
||||||
});
|
});
|
||||||
|
|
@ -54,13 +53,12 @@
|
||||||
if (!fName.trim()) { ortError = 'Name ist Pflichtfeld.'; return; }
|
if (!fName.trim()) { ortError = 'Name ist Pflichtfeld.'; return; }
|
||||||
ortError = ''; ortSaving = true;
|
ortError = ''; ortSaving = true;
|
||||||
try {
|
try {
|
||||||
const vid = pb.authStore.record?.verein_id as string;
|
const data = { name: fName.trim(), adresse: fAdresse.trim() || null, typ: fTyp, aktiv: fAktiv };
|
||||||
const data = { verein_id: vid, name: fName.trim(), adresse: fAdresse.trim() || null, typ: fTyp, aktiv: fAktiv };
|
|
||||||
if (editOrtId) {
|
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);
|
orte = orte.map(o => o.id === editOrtId ? u : o);
|
||||||
} else {
|
} 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));
|
orte = [...orte, n].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
}
|
||||||
showOrtForm = false;
|
showOrtForm = false;
|
||||||
|
|
@ -73,7 +71,7 @@
|
||||||
|
|
||||||
async function ortLoeschen(id: string) {
|
async function ortLoeschen(id: string) {
|
||||||
if (!confirm('Ort wirklich löschen? Alle verknüpften Termine verlieren die Ortzuordnung.')) return;
|
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);
|
orte = orte.filter(o => o.id !== id);
|
||||||
ausfaelle = ausfaelle.filter(a => a.ort_id !== id);
|
ausfaelle = ausfaelle.filter(a => a.ort_id !== id);
|
||||||
}
|
}
|
||||||
|
|
@ -88,7 +86,7 @@
|
||||||
if (aVon > aBis) { ausfallError = 'Bis muss nach Von liegen.'; return; }
|
if (aVon > aBis) { ausfallError = 'Bis muss nach Von liegen.'; return; }
|
||||||
ausfallError = ''; ausfallSaving = true;
|
ausfallError = ''; ausfallSaving = true;
|
||||||
try {
|
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,
|
ort_id: aOrtId, von: aVon, bis: aBis, grund: aGrund.trim() || null,
|
||||||
});
|
});
|
||||||
ausfaelle = [...ausfaelle, n].sort((a, b) => a.von.localeCompare(b.von));
|
ausfaelle = [...ausfaelle, n].sort((a, b) => a.von.localeCompare(b.von));
|
||||||
|
|
@ -101,7 +99,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ausfallLoeschen(id: string) {
|
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);
|
ausfaelle = ausfaelle.filter(a => a.id !== id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
<script lang="ts">
|
<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 { onMount } from 'svelte';
|
||||||
import { RRule } from 'rrule';
|
import { RRule } from 'rrule';
|
||||||
import { Calendar, TimeGrid, DayGrid } from '@event-calendar/core';
|
import { Calendar, TimeGrid, DayGrid } from '@event-calendar/core';
|
||||||
import '@event-calendar/core/index.css';
|
import '@event-calendar/core/index.css';
|
||||||
import type { Termin, Gruppe, Verfuegbarkeit, Veranstaltungsort, OrtAusfall } from '$lib/types';
|
import type { Termin, Gruppe, Verfuegbarkeit, Veranstaltungsort, OrtAusfall } 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'; };
|
||||||
const userId = () => pb.authStore.record?.id as string;
|
const userId = () => get(user)?.id as string;
|
||||||
|
|
||||||
let termine = $state<Termin[]>([]);
|
let termine = $state<Termin[]>([]);
|
||||||
let gruppen = $state<Gruppe[]>([]);
|
let gruppen = $state<Gruppe[]>([]);
|
||||||
|
|
@ -59,15 +61,14 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const vid = pb.authStore.record?.verein_id as string;
|
|
||||||
[termine, gruppen, alleUser, orte, ausfaelle] = await Promise.all([
|
[termine, gruppen, alleUser, orte, ausfaelle] = await Promise.all([
|
||||||
pb.collection('termine').getFullList<Termin>({ sort: 'beginn' }),
|
api.get<Termin[]>('/termine', { sort: 'beginn' }),
|
||||||
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
|
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
|
||||||
isAdmin()
|
isAdmin()
|
||||||
? pb.collection('users').getFullList({ filter: `verein_id = "${vid}" && rolle = "trainer"` })
|
? api.get<any[]>('/users', { rolle: 'trainer' })
|
||||||
: Promise.resolve([]),
|
: Promise.resolve([]),
|
||||||
pb.collection('veranstaltungsorte').getFullList<Veranstaltungsort>({ sort: 'name', filter: `verein_id = "${vid}" && aktiv = true` }),
|
api.get<Veranstaltungsort[]>('/veranstaltungsorte', { sort: 'name', aktiv: 'true' }),
|
||||||
pb.collection('ort_ausfaelle').getFullList<OrtAusfall>({ sort: 'von' }),
|
api.get<OrtAusfall[]>('/ort_ausfaelle', { sort: 'von' }),
|
||||||
]);
|
]);
|
||||||
loading = false;
|
loading = false;
|
||||||
});
|
});
|
||||||
|
|
@ -110,9 +111,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function generiereTerminDaten(beginn: Date, rruleStr: string | null) {
|
function generiereTerminDaten(beginn: Date, rruleStr: string | null) {
|
||||||
const vid = pb.authStore.record?.verein_id as string;
|
|
||||||
return {
|
return {
|
||||||
verein_id: vid,
|
|
||||||
titel: fTitel.trim(),
|
titel: fTitel.trim(),
|
||||||
beschreibung: fBeschr.trim() || null,
|
beschreibung: fBeschr.trim() || null,
|
||||||
ort: fOrtId ? null : (fOrt.trim() || null),
|
ort: fOrtId ? null : (fOrt.trim() || null),
|
||||||
|
|
@ -146,7 +145,7 @@
|
||||||
|
|
||||||
const neu = await Promise.all(
|
const neu = await Promise.all(
|
||||||
dates.map(d =>
|
dates.map(d =>
|
||||||
pb.collection('termine').create<Termin>({
|
api.post<Termin>('/termine', {
|
||||||
...generiereTerminDaten(d, rruleStr),
|
...generiereTerminDaten(d, rruleStr),
|
||||||
beginn: d.toISOString(),
|
beginn: d.toISOString(),
|
||||||
ende: fEnde ? new Date(d.getTime() + dauer).toISOString() : null,
|
ende: fEnde ? new Date(d.getTime() + dauer).toISOString() : null,
|
||||||
|
|
@ -162,10 +161,10 @@
|
||||||
ende: fEnde ? fromLocal(fEnde) : null,
|
ende: fEnde ? fromLocal(fEnde) : null,
|
||||||
};
|
};
|
||||||
if (editId) {
|
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);
|
termine = termine.map(t => t.id === editId ? updated : t);
|
||||||
} else {
|
} else {
|
||||||
const neu = await pb.collection('termine').create<Termin>(data);
|
const neu = await api.post<Termin>('/termine', data);
|
||||||
termine = [...termine, neu];
|
termine = [...termine, neu];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -180,10 +179,10 @@
|
||||||
async function loeschen(t: Termin, ganzeSerieLoeschen: boolean) {
|
async function loeschen(t: Termin, ganzeSerieLoeschen: boolean) {
|
||||||
if (ganzeSerieLoeschen && t.serie_id) {
|
if (ganzeSerieLoeschen && t.serie_id) {
|
||||||
const serie = termine.filter(x => x.serie_id === 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);
|
termine = termine.filter(x => x.serie_id !== t.serie_id);
|
||||||
} else {
|
} else {
|
||||||
await pb.collection('termine').delete(t.id);
|
await api.del('/termine/' + t.id);
|
||||||
termine = termine.filter(x => x.id !== t.id);
|
termine = termine.filter(x => x.id !== t.id);
|
||||||
}
|
}
|
||||||
showDelete = null;
|
showDelete = null;
|
||||||
|
|
@ -202,7 +201,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
async function setVerfuegbarkeit(t: Termin, v: Verfuegbarkeit) {
|
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);
|
termine = termine.map(x => x.id === t.id ? updated : x);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { pb } from '$lib/pb';
|
import { get } from 'svelte/store';
|
||||||
|
import { user } from '$lib/user';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (pb.authStore.isValid) {
|
if (!!get(user)) {
|
||||||
goto('/');
|
goto('/');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
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 email = $state('');
|
||||||
let password = $state('');
|
let password = $state('');
|
||||||
|
|
@ -11,7 +13,8 @@
|
||||||
error = '';
|
error = '';
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
await pb.collection('users').authWithPassword(email, password);
|
const u = await api.post<AppUser & { token: string }>('/auth/login', { email, password });
|
||||||
|
user.set(u);
|
||||||
goto('/');
|
goto('/');
|
||||||
} catch {
|
} catch {
|
||||||
error = 'E-Mail oder Passwort falsch.';
|
error = 'E-Mail oder Passwort falsch.';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { pb } from '$lib/pb';
|
import { api } from '$lib/api';
|
||||||
|
import { user } from '$lib/user';
|
||||||
|
|
||||||
let vereinsname = $state('');
|
let vereinsname = $state('');
|
||||||
let email = $state('');
|
let email = $state('');
|
||||||
|
|
@ -17,8 +18,8 @@
|
||||||
}
|
}
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
await pb.collection('users').create({ email, password, passwordConfirm, name: vereinsname });
|
const u = await api.post('/auth/register', { vereinName: vereinsname, email, password, name: vereinsname });
|
||||||
await pb.collection('users').authWithPassword(email, password);
|
user.set(u as any);
|
||||||
goto('/onboarding');
|
goto('/onboarding');
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
error = e instanceof Error ? e.message : 'Registrierung fehlgeschlagen.';
|
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 ical from 'ical-generator';
|
||||||
import { env } from '$env/dynamic/private';
|
import { getDb } from '$lib/server/db';
|
||||||
|
|
||||||
const PB_URL = () => env.PB_URL ?? 'http://localhost:8090';
|
|
||||||
|
|
||||||
export async function GET({ params }) {
|
export async function GET({ params }) {
|
||||||
const { vereinId } = params;
|
const { vereinId } = params;
|
||||||
|
|
||||||
// Verein laden (öffentlich lesbar via viewRule)
|
const db = getDb();
|
||||||
const vereinRes = await fetch(
|
|
||||||
`${PB_URL()}/api/collections/vereine/records/${vereinId}`,
|
const verein = db.prepare('SELECT * FROM vereine WHERE id = ?').get(vereinId) as { name: string } | undefined;
|
||||||
);
|
if (!verein) {
|
||||||
if (!vereinRes.ok) {
|
|
||||||
return new Response('Verein nicht gefunden.', { status: 404 });
|
return new Response('Verein nicht gefunden.', { status: 404 });
|
||||||
}
|
}
|
||||||
const verein = await vereinRes.json();
|
|
||||||
|
|
||||||
// Termine der nächsten 365 Tage laden
|
// Termine der nächsten 365 Tage laden
|
||||||
const von = new Date().toISOString();
|
const von = new Date().toISOString();
|
||||||
const bis = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).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(
|
const termine = db.prepare(
|
||||||
`${PB_URL()}/api/collections/termine/records?filter=${filter}&sort=beginn&perPage=500`,
|
`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 }[];
|
||||||
const { items: termine = [] } = termineRes.ok ? await termineRes.json() : {};
|
|
||||||
|
|
||||||
// iCal-Kalender aufbauen
|
// iCal-Kalender aufbauen
|
||||||
const cal = ical({
|
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 { json } from '@sveltejs/kit';
|
||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
import webpush from 'web-push';
|
import webpush from 'web-push';
|
||||||
|
import { requireAuth } from '$lib/server/auth';
|
||||||
const PB_URL = () => env.PB_URL ?? 'http://localhost:8090';
|
import { getDb } from '$lib/server/db';
|
||||||
|
|
||||||
export async function POST({ request }) {
|
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();
|
const { titel, body, url = '/nachrichten' } = await request.json();
|
||||||
|
|
||||||
if (!titel) return json({ error: 'Titel fehlt.' }, { status: 400 });
|
if (!titel) return json({ error: 'Titel fehlt.' }, { status: 400 });
|
||||||
|
|
@ -20,21 +22,18 @@ export async function POST({ request }) {
|
||||||
|
|
||||||
webpush.setVapidDetails(vapidSubject, vapidPublic, vapidPrivate);
|
webpush.setVapidDetails(vapidSubject, vapidPublic, vapidPrivate);
|
||||||
|
|
||||||
// Alle Push-Subscriptions des Vereins laden (listRule erlaubt verein-weite Abfrage)
|
const db = getDb();
|
||||||
const subRes = await fetch(
|
const items = db.prepare(
|
||||||
`${PB_URL()}/api/collections/push_subscriptions/records?perPage=500`,
|
'SELECT * FROM push_subscriptions WHERE verein_id = ?'
|
||||||
{ headers: { Authorization: authHeader } },
|
).all(authUser.verein_id) as { endpoint: string; p256dh: string; auth: string; id: string }[];
|
||||||
);
|
|
||||||
|
|
||||||
if (!subRes.ok) return json({ sent: 0 });
|
if (!items.length) return json({ sent: 0 });
|
||||||
const { items } = await subRes.json();
|
|
||||||
if (!items?.length) return json({ sent: 0 });
|
|
||||||
|
|
||||||
const payload = JSON.stringify({ title: titel, body, url });
|
const payload = JSON.stringify({ title: titel, body, url });
|
||||||
let sent = 0;
|
let sent = 0;
|
||||||
|
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
items.map(async (sub: { endpoint: string; p256dh: string; auth: string; id: string }) => {
|
items.map(async (sub) => {
|
||||||
try {
|
try {
|
||||||
await webpush.sendNotification(
|
await webpush.sendNotification(
|
||||||
{ endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } },
|
{ endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } },
|
||||||
|
|
@ -44,10 +43,7 @@ export async function POST({ request }) {
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
// 410 Gone = Subscription abgelaufen → löschen
|
// 410 Gone = Subscription abgelaufen → löschen
|
||||||
if ((err as { statusCode?: number }).statusCode === 410) {
|
if ((err as { statusCode?: number }).statusCode === 410) {
|
||||||
await fetch(
|
db.prepare('DELETE FROM push_subscriptions WHERE id = ?').run(sub.id);
|
||||||
`${PB_URL()}/api/collections/push_subscriptions/records/${sub.id}`,
|
|
||||||
{ method: 'DELETE', headers: { Authorization: authHeader } },
|
|
||||||
).catch(() => {});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,67 +1,44 @@
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import { env } from '$env/dynamic/private';
|
import { requireAuth } from '$lib/server/auth';
|
||||||
|
import { getDb, newId } from '$lib/server/db';
|
||||||
const PB_URL = () => env.PB_URL ?? 'http://localhost:8090';
|
|
||||||
|
|
||||||
export async function POST({ request }) {
|
export async function POST({ request }) {
|
||||||
const authHeader = request.headers.get('Authorization') ?? '';
|
const authUser = await requireAuth(request).catch(() => null);
|
||||||
const { subscription, userId } = await request.json();
|
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 });
|
return json({ error: 'Ungültige Subscription.' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alte Subscription dieses Users löschen (Gerätewechsel)
|
const db = getDb();
|
||||||
const listRes = await fetch(
|
|
||||||
`${PB_URL()}/api/collections/push_subscriptions/records?filter=user_id%3D"${userId}"&perPage=50`,
|
// Alte Subscriptions dieses Users löschen (Gerätewechsel)
|
||||||
{ headers: { Authorization: authHeader } },
|
db.prepare('DELETE FROM push_subscriptions WHERE user_id = ?').run(authUser.sub);
|
||||||
);
|
|
||||||
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 },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Neue Subscription speichern
|
// Neue Subscription speichern
|
||||||
const res = await fetch(`${PB_URL()}/api/collections/push_subscriptions/records`, {
|
db.prepare(`
|
||||||
method: 'POST',
|
INSERT INTO push_subscriptions (id, user_id, verein_id, endpoint, p256dh, auth)
|
||||||
headers: { 'Content-Type': 'application/json', Authorization: authHeader },
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
body: JSON.stringify({
|
`).run(
|
||||||
user_id: userId,
|
newId(),
|
||||||
endpoint: subscription.endpoint,
|
authUser.sub,
|
||||||
p256dh: subscription.keys.p256dh,
|
authUser.verein_id,
|
||||||
auth: subscription.keys.auth,
|
subscription.endpoint,
|
||||||
}),
|
subscription.keys.p256dh,
|
||||||
});
|
subscription.keys.auth,
|
||||||
|
);
|
||||||
|
|
||||||
if (!res.ok) return json({ error: 'Fehler beim Speichern.' }, { status: 500 });
|
|
||||||
return json({ success: true });
|
return json({ success: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DELETE({ request }) {
|
export async function DELETE({ request }) {
|
||||||
const authHeader = request.headers.get('Authorization') ?? '';
|
const authUser = await requireAuth(request).catch(() => null);
|
||||||
const { userId } = await request.json();
|
if (!authUser) return json({ success: true });
|
||||||
|
|
||||||
const listRes = await fetch(
|
const db = getDb();
|
||||||
`${PB_URL()}/api/collections/push_subscriptions/records?filter=user_id%3D"${userId}"&perPage=50`,
|
db.prepare('DELETE FROM push_subscriptions WHERE user_id = ?').run(authUser.sub);
|
||||||
{ headers: { Authorization: authHeader } },
|
|
||||||
);
|
|
||||||
if (!listRes.ok) return json({ success: true });
|
|
||||||
|
|
||||||
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 });
|
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 { goto } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { onMount } from 'svelte';
|
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';
|
import type { Einladung } from '$lib/types';
|
||||||
|
|
||||||
const token = $derived($page.params.token as string);
|
const token = $derived($page.params.token as string);
|
||||||
|
|
@ -20,17 +22,14 @@
|
||||||
let formError = $state('');
|
let formError = $state('');
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (pb.authStore.isValid) {
|
if (!!get(user)) {
|
||||||
goto('/');
|
goto('/');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const inv = await pb.collection('einladungen')
|
const inv = await api.get<Einladung & { vereinName: string }>('/einladungen/' + token);
|
||||||
.getFirstListItem<Einladung>(`token = "${token}" && genutzt = false`, {
|
|
||||||
expand: 'verein_id',
|
|
||||||
});
|
|
||||||
einladung = inv;
|
einladung = inv;
|
||||||
vereinName = (inv as any).expand?.verein_id?.name ?? '';
|
vereinName = (inv as any).vereinName ?? '';
|
||||||
} catch {
|
} catch {
|
||||||
fehler = 'Dieser Einladungslink ist ungültig oder wurde bereits verwendet.';
|
fehler = 'Dieser Einladungslink ist ungültig oder wurde bereits verwendet.';
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -43,16 +42,12 @@
|
||||||
if (password !== passwordConfirm) { formError = 'Passwörter stimmen nicht überein.'; return; }
|
if (password !== passwordConfirm) { formError = 'Passwörter stimmen nicht überein.'; return; }
|
||||||
formError = ''; saving = true;
|
formError = ''; saving = true;
|
||||||
try {
|
try {
|
||||||
await pb.collection('users').create({
|
const u = await api.post<any>(`/einladungen/${token}`, {
|
||||||
email: email.trim(),
|
email: email.trim(),
|
||||||
password,
|
password,
|
||||||
passwordConfirm,
|
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
verein_id: einladung.verein_id,
|
|
||||||
rolle: einladung.rolle,
|
|
||||||
});
|
});
|
||||||
await pb.collection('users').authWithPassword(email.trim(), password);
|
user.set(u);
|
||||||
await pb.collection('einladungen').update(einladung.id, { genutzt: true });
|
|
||||||
goto('/');
|
goto('/');
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
formError = e instanceof Error ? e.message : 'Registrierung fehlgeschlagen.';
|
formError = e instanceof Error ? e.message : 'Registrierung fehlgeschlagen.';
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { onMount } from 'svelte';
|
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 schritt = $state(1);
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
|
|
@ -13,36 +15,31 @@
|
||||||
let fertigName = $state('');
|
let fertigName = $state('');
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!pb.authStore.isValid) {
|
const u = get(user);
|
||||||
|
if (!u) {
|
||||||
goto('/login');
|
goto('/login');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (pb.authStore.record?.verein_id) {
|
if (u.verein_id) {
|
||||||
goto('/');
|
goto('/');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// Vereinsname aus Registration vorbelegen
|
// Vereinsname aus Registration vorbelegen
|
||||||
vereinsname = pb.authStore.record?.name ?? '';
|
vereinsname = u.name ?? '';
|
||||||
});
|
});
|
||||||
|
|
||||||
async function vereinAnlegen() {
|
async function vereinAnlegen() {
|
||||||
if (!vereinsname.trim()) return;
|
if (!vereinsname.trim()) return;
|
||||||
error = ''; loading = true;
|
error = ''; loading = true;
|
||||||
try {
|
try {
|
||||||
const verein = await pb.collection('vereine').create({
|
const updated = await api.post<any>('/onboarding/verein', {
|
||||||
name: vereinsname.trim(),
|
name: vereinsname.trim(),
|
||||||
ort: ort.trim() || null,
|
ort: ort.trim() || null,
|
||||||
plan: 'free',
|
|
||||||
dosb_mitglied: false,
|
|
||||||
});
|
});
|
||||||
|
// user store mit aktualisiertem verein_id updaten
|
||||||
await pb.collection('users').update(pb.authStore.record!.id, {
|
const u = get(user);
|
||||||
verein_id: verein.id,
|
if (u) user.set({ ...u, verein_id: updated.verein_id });
|
||||||
});
|
fertigName = vereinsname.trim();
|
||||||
|
|
||||||
// Auth-Token aktualisieren damit verein_id im Record steht
|
|
||||||
await pb.collection('users').authRefresh();
|
|
||||||
|
|
||||||
fertigName = verein.name;
|
|
||||||
schritt = 3;
|
schritt = 3;
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
error = e instanceof Error ? e.message : 'Fehler beim Anlegen.';
|
error = e instanceof Error ? e.message : 'Fehler beim Anlegen.';
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,27 @@
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
pocketbase-staging:
|
|
||||||
image: ghcr.io/muchobien/pocketbase:latest
|
|
||||||
container_name: vereinshaus-staging-pocketbase
|
|
||||||
restart: unless-stopped
|
|
||||||
command: ["--migrationsDir=/pb_data/migrations"]
|
|
||||||
volumes:
|
|
||||||
- /volume1/docker/vereinshaus-staging/pocketbase/data:/pb_data
|
|
||||||
- /volume1/docker/vereinshaus-staging/pocketbase/storage:/pb_public
|
|
||||||
- /volume1/docker/vereinshaus-staging/pocketbase/data/pb_hooks:/pb_hooks
|
|
||||||
- /volume1/docker/vereinshaus-staging/pocketbase/migrations:/pb_data/migrations
|
|
||||||
environment:
|
|
||||||
- TZ=Europe/Berlin
|
|
||||||
- BREVO_KEY=${BREVO_KEY}
|
|
||||||
- BREVO_SENDER=${BREVO_SENDER:-noreply@vereins.haus}
|
|
||||||
networks:
|
|
||||||
default: {}
|
|
||||||
npm_bridge:
|
|
||||||
ipv4_address: 172.25.0.14
|
|
||||||
|
|
||||||
app-staging:
|
app-staging:
|
||||||
build:
|
build:
|
||||||
context: ./app
|
context: ./app
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
args:
|
|
||||||
VITE_PB_URL: https://api-staging.vereins.haus
|
|
||||||
image: vereinshaus-staging-app
|
image: vereinshaus-staging-app
|
||||||
container_name: vereinshaus-staging-app
|
container_name: vereinshaus-staging-app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- /volume1/docker/vereinshaus-staging/data:/data
|
||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Berlin
|
- TZ=Europe/Berlin
|
||||||
- HOST=0.0.0.0
|
- HOST=0.0.0.0
|
||||||
- PORT=3000
|
- PORT=3000
|
||||||
|
- DB_PATH=/data/vereinshaus.db
|
||||||
|
- UPLOAD_DIR=/data/uploads
|
||||||
|
- JWT_SECRET=${JWT_SECRET:-staging-secret-change-me}
|
||||||
- PUBLIC_VAPID_KEY=${PUBLIC_VAPID_KEY}
|
- PUBLIC_VAPID_KEY=${PUBLIC_VAPID_KEY}
|
||||||
- VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}
|
- VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}
|
||||||
- VAPID_SUBJECT=${VAPID_SUBJECT:-mailto:info@vereins.haus}
|
- VAPID_SUBJECT=${VAPID_SUBJECT:-mailto:info@vereins.haus}
|
||||||
- PB_URL=http://pocketbase-staging:8090
|
- BREVO_KEY=${BREVO_KEY}
|
||||||
|
- BREVO_SENDER=${BREVO_SENDER:-noreply@vereins.haus}
|
||||||
networks:
|
networks:
|
||||||
default: {}
|
default: {}
|
||||||
npm_bridge:
|
npm_bridge:
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,27 @@
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
pocketbase:
|
|
||||||
image: ghcr.io/muchobien/pocketbase:latest
|
|
||||||
container_name: vereinshaus-pocketbase
|
|
||||||
restart: unless-stopped
|
|
||||||
command: ["--migrationsDir=/pb_data/migrations"]
|
|
||||||
volumes:
|
|
||||||
- /volume1/docker/vereinshaus/pocketbase/data:/pb_data
|
|
||||||
- /volume1/docker/vereinshaus/pocketbase/storage:/pb_public
|
|
||||||
- /volume1/docker/vereinshaus/pocketbase/data/pb_hooks:/pb_hooks
|
|
||||||
- /volume1/docker/vereinshaus/pocketbase/migrations:/pb_data/migrations
|
|
||||||
environment:
|
|
||||||
- TZ=Europe/Berlin
|
|
||||||
- BREVO_KEY=${BREVO_KEY}
|
|
||||||
- BREVO_SENDER=${BREVO_SENDER:-noreply@vereins.haus}
|
|
||||||
networks:
|
|
||||||
default: {}
|
|
||||||
npm_bridge:
|
|
||||||
ipv4_address: 172.25.0.12
|
|
||||||
|
|
||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
context: ./app
|
context: ./app
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
args:
|
|
||||||
VITE_PB_URL: https://api.vereins.haus
|
|
||||||
image: vereinshaus-app
|
image: vereinshaus-app
|
||||||
container_name: vereinshaus-app
|
container_name: vereinshaus-app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- /volume1/docker/vereinshaus/data:/data
|
||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Berlin
|
- TZ=Europe/Berlin
|
||||||
- HOST=0.0.0.0
|
- HOST=0.0.0.0
|
||||||
- PORT=3000
|
- PORT=3000
|
||||||
|
- DB_PATH=/data/vereinshaus.db
|
||||||
|
- UPLOAD_DIR=/data/uploads
|
||||||
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
- PUBLIC_VAPID_KEY=${PUBLIC_VAPID_KEY}
|
- PUBLIC_VAPID_KEY=${PUBLIC_VAPID_KEY}
|
||||||
- VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}
|
- VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}
|
||||||
- VAPID_SUBJECT=${VAPID_SUBJECT:-mailto:info@vereins.haus}
|
- VAPID_SUBJECT=${VAPID_SUBJECT:-mailto:info@vereins.haus}
|
||||||
- PB_URL=http://pocketbase:8090
|
- BREVO_KEY=${BREVO_KEY}
|
||||||
|
- BREVO_SENDER=${BREVO_SENDER:-noreply@vereins.haus}
|
||||||
networks:
|
networks:
|
||||||
default: {}
|
default: {}
|
||||||
npm_bridge:
|
npm_bridge:
|
||||||
|
|
|
||||||
351
scripts/seed.js
351
scripts/seed.js
|
|
@ -1,264 +1,153 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// Testdaten für vereins.haus – Staging
|
// Seed-Script für vereins.haus (SvelteKit + SQLite, kein PocketBase)
|
||||||
// Aufruf: PB_URL=http://localhost:8090 PB_EMAIL=admin@test.de PB_PASSWORD=Test123456! node scripts/seed.js
|
// Verwendung: APP_URL=https://staging.vereins.haus node scripts/seed.js
|
||||||
|
|
||||||
const PB_URL = process.env.PB_URL || 'http://localhost:8090';
|
const BASE = process.env.APP_URL || 'http://localhost:3000';
|
||||||
const PB_EMAIL = process.env.PB_EMAIL || '';
|
|
||||||
const PB_PWD = process.env.PB_PASSWORD || '';
|
|
||||||
|
|
||||||
if (!PB_EMAIL || !PB_PWD) {
|
async function req(method, path, body, token) {
|
||||||
console.error('Fehler: PB_EMAIL und PB_PASSWORD setzen.');
|
const headers = { 'Content-Type': 'application/json' };
|
||||||
process.exit(1);
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||||
}
|
const res = await fetch(`${BASE}/api${path}`, {
|
||||||
|
method, headers, body: body ? JSON.stringify(body) : undefined,
|
||||||
let token = '';
|
|
||||||
|
|
||||||
async function pb(method, path, body) {
|
|
||||||
const res = await fetch(`${PB_URL}/api/${path}`, {
|
|
||||||
method,
|
|
||||||
headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: token } : {}) },
|
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
|
||||||
});
|
});
|
||||||
const json = await res.json().catch(() => ({}));
|
const json = await res.json().catch(() => ({}));
|
||||||
if (!res.ok) throw new Error(`${method} /${path} → ${res.status}: ${JSON.stringify(json)}`);
|
if (!res.ok) throw new Error(`${method} ${path} → ${res.status}: ${JSON.stringify(json)}`);
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function find(collection, filter) {
|
async function main() {
|
||||||
const r = await pb('GET', `collections/${collection}/records?filter=${encodeURIComponent(filter)}&perPage=1`);
|
console.log(`→ Seed gegen ${BASE}`);
|
||||||
return r.items?.[0] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function create(collection, data) {
|
// 1. Verein + Admin registrieren
|
||||||
return pb('POST', `collections/${collection}/records`, data);
|
console.log('→ Registrierung...');
|
||||||
}
|
const auth = await req('POST', '/auth/register', {
|
||||||
|
vereinName: 'TSV Musterstadt 1983 e.V.',
|
||||||
async function update(collection, id, data) {
|
|
||||||
return pb('PATCH', `collections/${collection}/records/${id}`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 1. Admin-Login ──────────────────────────────────────────────────────────
|
|
||||||
console.log('→ Admin-Login…');
|
|
||||||
const auth = await pb('POST', 'collections/_superusers/auth-with-password', {
|
|
||||||
identity: PB_EMAIL, password: PB_PWD,
|
|
||||||
});
|
|
||||||
token = auth.token;
|
|
||||||
console.log(' ✓ Eingeloggt als', PB_EMAIL);
|
|
||||||
|
|
||||||
// ── 2. Verein ────────────────────────────────────────────────────────────────
|
|
||||||
console.log('→ Verein anlegen…');
|
|
||||||
let verein = await find('vereine', 'name = "TSV Musterstadt 1983 e.V."');
|
|
||||||
if (!verein) {
|
|
||||||
verein = await create('vereine', {
|
|
||||||
name: 'TSV Musterstadt 1983 e.V.',
|
|
||||||
adresse: 'Vereinsstraße 12',
|
|
||||||
plz: '80333',
|
|
||||||
ort: 'Musterstadt',
|
|
||||||
bundesland: 'BY',
|
|
||||||
plan: 'starter',
|
|
||||||
dosb_mitglied: true,
|
|
||||||
email: 'info@tsv-musterstadt.de',
|
|
||||||
telefon: '089 123456',
|
|
||||||
website: 'https://tsv-musterstadt.de',
|
|
||||||
glaeubigerid: 'DE98ZZZ09999999999',
|
|
||||||
iban: 'DE89370400440532013000',
|
|
||||||
bic: 'COBADEFFXXX',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log(' ✓ Verein:', verein.name, `(${verein.id})`);
|
|
||||||
|
|
||||||
// ── 3. Admin-User ────────────────────────────────────────────────────────────
|
|
||||||
console.log('→ Admin-User…');
|
|
||||||
let adminUser = await find('users', 'email = "vorstand@tsv-musterstadt.de"');
|
|
||||||
if (!adminUser) {
|
|
||||||
adminUser = await create('users', {
|
|
||||||
email: 'vorstand@tsv-musterstadt.de',
|
email: 'vorstand@tsv-musterstadt.de',
|
||||||
password: 'Test123456!', passwordConfirm: 'Test123456!',
|
password: 'Test123456!',
|
||||||
name: 'Max Mustermann', verein_id: verein.id, rolle: null, // null = admin
|
name: 'Max Vorstand',
|
||||||
emailVisibility: true,
|
|
||||||
});
|
});
|
||||||
}
|
const T = auth.token;
|
||||||
console.log(' ✓ Admin:', adminUser.email);
|
const VID = auth.verein_id;
|
||||||
|
console.log(` ✓ Verein ID: ${VID}`);
|
||||||
|
console.log(` ✓ User: vorstand@tsv-musterstadt.de`);
|
||||||
|
|
||||||
// ── 4. Gruppen ───────────────────────────────────────────────────────────────
|
// 2. Verein-Details ergänzen
|
||||||
console.log('→ Gruppen…');
|
await req('PATCH', '/vereine', {
|
||||||
const gruppenDef = ['Vorstand', 'Aktive Mitglieder', 'Jugend U15', 'Senioren'];
|
adresse: 'Musterstraße 1', plz: '12345', ort: 'Musterstadt',
|
||||||
|
bundesland: 'Bayern', plan: 'starter', dosb_mitglied: true,
|
||||||
|
email: 'info@tsv-musterstadt.de', telefon: '01234 56789',
|
||||||
|
glaeubigerid: 'DE98ZZZ09999999999',
|
||||||
|
iban: 'DE89370400440532013000', bic: 'COBADEFFXXX',
|
||||||
|
}, T);
|
||||||
|
|
||||||
|
// 3. Gruppen
|
||||||
|
console.log('→ Gruppen...');
|
||||||
const gruppen = {};
|
const gruppen = {};
|
||||||
for (const name of gruppenDef) {
|
for (const name of ['Vorstand', 'Aktive Mitglieder', 'Jugend U15', 'Senioren']) {
|
||||||
let g = await find('gruppen', `name = "${name}" && verein_id = "${verein.id}"`);
|
const g = await req('POST', '/gruppen', { name }, T);
|
||||||
if (!g) g = await create('gruppen', { verein_id: verein.id, name });
|
|
||||||
gruppen[name] = g.id;
|
gruppen[name] = g.id;
|
||||||
}
|
}
|
||||||
console.log(' ✓', Object.keys(gruppen).join(', '));
|
console.log(` ✓ ${Object.keys(gruppen).length} Gruppen`);
|
||||||
|
|
||||||
// ── 5. Mitglieder ────────────────────────────────────────────────────────────
|
// 4. Mitglieder
|
||||||
console.log('→ Mitglieder (18)…');
|
console.log('→ Mitglieder (18)...');
|
||||||
const mitgliederDef = [
|
const mitgliederDaten = [
|
||||||
// Vorstand
|
{ vorname: 'Max', nachname: 'Vorstand', email: 'vorstand@tsv-musterstadt.de', status: 'aktiv', gruppe_ids: [gruppen['Vorstand'], gruppen['Aktive Mitglieder']], eintrittsdatum: '2010-01-01' },
|
||||||
{ vorname: 'Max', nachname: 'Mustermann', email: 'max.mustermann@example.de', telefon: '0170 1111111', geburtsdatum: '1975-03-15', eintrittsdatum: '2000-01-01', strasse: 'Hauptstraße 1', plz: '80333', ort: 'Musterstadt', iban: 'DE89370400440532013000', bic: 'COBADEFFXXX', status: 'aktiv', gruppe: 'Vorstand', mandatsreferenz: 'MANDAT-001', mandatsdatum: '2020-01-10' },
|
{ vorname: 'Anna', nachname: 'Schmidt', email: 'anna.schmidt@example.de', status: 'aktiv', gruppe_ids: [gruppen['Aktive Mitglieder']], eintrittsdatum: '2015-03-15', iban: 'DE89370400440532013001' },
|
||||||
{ vorname: 'Sabine', nachname: 'Richter', email: 'sabine.richter@example.de', telefon: '0171 2222222', geburtsdatum: '1980-07-22', eintrittsdatum: '2005-03-01', strasse: 'Birkenweg 5', plz: '80334', ort: 'Musterstadt', iban: 'DE27100777770209299700', bic: 'SSKMDEMMXXX', status: 'aktiv', gruppe: 'Vorstand', mandatsreferenz: 'MANDAT-002', mandatsdatum: '2020-01-10' },
|
{ vorname: 'Thomas', nachname: 'Müller', email: 'thomas.mueller@example.de', status: 'aktiv', gruppe_ids: [gruppen['Aktive Mitglieder'], gruppen['Senioren']], eintrittsdatum: '2012-06-01' },
|
||||||
// Aktive mit IBAN
|
{ vorname: 'Lisa', nachname: 'Weber', email: 'lisa.weber@example.de', status: 'aktiv', gruppe_ids: [gruppen['Jugend U15']], eintrittsdatum: '2022-09-01', geburtsdatum: '2010-04-12' },
|
||||||
{ vorname: 'Anna', nachname: 'Schmidt', email: 'anna.schmidt@example.de', telefon: '0172 3333333', geburtsdatum: '1990-11-05', eintrittsdatum: '2015-09-01', strasse: 'Rosenstraße 8', plz: '80333', ort: 'Musterstadt', iban: 'DE61500105179767440929', bic: 'BELADEBEXXX', status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: 'MANDAT-003', mandatsdatum: '2021-04-15' },
|
{ vorname: 'Klaus', nachname: 'Fischer', email: 'klaus.fischer@example.de', status: 'aktiv', gruppe_ids: [gruppen['Senioren']], eintrittsdatum: '2008-01-15' },
|
||||||
{ vorname: 'Peter', nachname: 'Wagner', email: 'peter.wagner@example.de', telefon: '0173 4444444', geburtsdatum: '1985-02-28', eintrittsdatum: '2010-01-15', strasse: 'Gartenweg 12', plz: '80334', ort: 'Musterstadt', iban: 'DE24500105171911148770', bic: 'BELADEBEXXX', status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: 'MANDAT-004', mandatsdatum: '2021-04-15' },
|
{ vorname: 'Maria', nachname: 'Bauer', email: 'maria.bauer@example.de', status: 'aktiv', gruppe_ids: [gruppen['Aktive Mitglieder']], eintrittsdatum: '2019-02-20' },
|
||||||
{ vorname: 'Thomas', nachname: 'Fischer', email: 'thomas.fischer@example.de', telefon: '0174 5555555', geburtsdatum: '1978-06-10', eintrittsdatum: '2008-04-01', strasse: 'Lindenallee 3', plz: '80335', ort: 'Musterstadt', iban: 'DE12500105176228935005', bic: 'BELADEBEXXX', status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: 'MANDAT-005', mandatsdatum: '2021-04-15' },
|
{ vorname: 'Peter', nachname: 'Wagner', email: 'peter.wagner@example.de', status: 'aktiv', gruppe_ids: [gruppen['Aktive Mitglieder']], eintrittsdatum: '2017-05-10', iban: 'DE89370400440532013002' },
|
||||||
{ vorname: 'Claudia', nachname: 'König', email: 'claudia.koenig@example.de', telefon: '0175 6666666', geburtsdatum: '1992-09-17', eintrittsdatum: '2018-06-01', strasse: 'Feldweg 7', plz: '80333', ort: 'Musterstadt', iban: 'DE67200501001234567890', bic: 'HASPDEHHXXX', status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: 'MANDAT-006', mandatsdatum: '2022-01-20' },
|
{ vorname: 'Julia', nachname: 'Becker', email: 'julia.becker@example.de', status: 'aktiv', gruppe_ids: [gruppen['Jugend U15']], eintrittsdatum: '2023-01-01', geburtsdatum: '2011-07-22' },
|
||||||
{ vorname: 'Michael', nachname: 'Koch', email: 'michael.koch@example.de', telefon: '0176 7777777', geburtsdatum: '1983-04-03', eintrittsdatum: '2012-02-01', strasse: 'Bergstraße 22', plz: '80336', ort: 'Musterstadt', iban: 'DE86200400600526015800', bic: 'COBADEFFXXX', status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: 'MANDAT-007', mandatsdatum: '2021-04-15' },
|
{ vorname: 'Stefan', nachname: 'Hoffmann', email: 'stefan.hoffmann@example.de', status: 'passiv', gruppe_ids: [], eintrittsdatum: '2005-03-01' },
|
||||||
{ vorname: 'Lisa', nachname: 'Zimmermann', email: 'lisa.zimm@example.de', telefon: '0177 8888888', geburtsdatum: '1995-12-25', eintrittsdatum: '2020-10-01', strasse: 'Blumenstraße 4', plz: '80333', ort: 'Musterstadt', iban: 'DE21700519950021267002', bic: 'BYLADEM1AUG', status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: 'MANDAT-008', mandatsdatum: '2022-03-05' },
|
{ vorname: 'Sandra', nachname: 'Koch', email: 'sandra.koch@example.de', status: 'aktiv', gruppe_ids: [gruppen['Senioren']], eintrittsdatum: '2014-11-15' },
|
||||||
{ vorname: 'Petra', nachname: 'Schreiber', email: 'petra.schreiber@example.de', telefon: '0178 9999999', geburtsdatum: '1988-08-14', eintrittsdatum: '2014-07-01', strasse: 'Weinbergweg 9', plz: '80334', ort: 'Musterstadt', iban: 'DE36200400600532013004', bic: 'COBADEFFXXX', status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: 'MANDAT-009', mandatsdatum: '2021-04-15' },
|
{ vorname: 'Michael', nachname: 'Schäfer', email: 'michael.schaefer@example.de', status: 'aktiv', gruppe_ids: [gruppen['Aktive Mitglieder']], eintrittsdatum: '2020-04-01' },
|
||||||
// Aktive ohne IBAN
|
{ vorname: 'Sabine', nachname: 'Zimmermann', email: 'sabine.zimm@example.de', status: 'aktiv', gruppe_ids: [gruppen['Aktive Mitglieder']], eintrittsdatum: '2018-08-20' },
|
||||||
{ vorname: 'Maria', nachname: 'Becker', email: 'maria.becker@example.de', telefon: '0179 1010101', geburtsdatum: '1987-01-30', eintrittsdatum: '2016-03-01', strasse: 'Kirchgasse 6', plz: '80335', ort: 'Musterstadt', iban: null, bic: null, status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: null, mandatsdatum: null },
|
{ vorname: 'Andreas', nachname: 'Braun', email: 'andreas.braun@example.de', status: 'aktiv', gruppe_ids: [gruppen['Senioren']], eintrittsdatum: '2011-01-01' },
|
||||||
{ vorname: 'Julia', nachname: 'Müller', email: 'julia.mueller@example.de', telefon: '0151 1111222', geburtsdatum: '1993-05-19', eintrittsdatum: '2019-01-01', strasse: 'Parkstraße 11', plz: '80333', ort: 'Musterstadt', iban: null, bic: null, status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: null, mandatsdatum: null },
|
{ vorname: 'Monika', nachname: 'Richter', email: 'monika.richter@example.de', status: 'passiv', gruppe_ids: [], eintrittsdatum: '2009-06-15' },
|
||||||
{ vorname: 'Markus', nachname: 'Schäfer', email: 'markus.schaefer@example.de', telefon: '0152 3333444', geburtsdatum: '1991-07-07', eintrittsdatum: '2021-05-01', strasse: 'Schulweg 2', plz: '80336', ort: 'Musterstadt', iban: null, bic: null, status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: null, mandatsdatum: null },
|
{ vorname: 'Tobias', nachname: 'Wolf', email: 'tobias.wolf@example.de', status: 'aktiv', gruppe_ids: [gruppen['Jugend U15']], eintrittsdatum: '2022-01-15', geburtsdatum: '2012-01-30' },
|
||||||
// Jugend
|
{ vorname: 'Eva', nachname: 'Krause', email: 'eva.krause@example.de', status: 'aktiv', gruppe_ids: [gruppen['Aktive Mitglieder']], eintrittsdatum: '2021-03-01' },
|
||||||
{ vorname: 'Lena', nachname: 'Bauer', email: null, telefon: '0160 1234567', geburtsdatum: '2012-03-22', eintrittsdatum: '2023-09-01', strasse: 'Schulstraße 14', plz: '80333', ort: 'Musterstadt', iban: 'DE47200400600128491600', bic: 'COBADEFFXXX', status: 'aktiv', gruppe: 'Jugend U15', mandatsreferenz: 'MANDAT-013', mandatsdatum: '2023-09-01' },
|
{ vorname: 'Markus', nachname: 'Schwarz', email: 'markus.schwarz@example.de', status: 'aktiv', gruppe_ids: [gruppen['Aktive Mitglieder'], gruppen['Vorstand']], eintrittsdatum: '2013-09-01' },
|
||||||
{ vorname: 'Kevin', nachname: 'Hoffmann', email: null, telefon: '0160 7654321', geburtsdatum: '2013-11-08', eintrittsdatum: '2023-09-01', strasse: 'Waldweg 3', plz: '80334', ort: 'Musterstadt', iban: null, bic: null, status: 'aktiv', gruppe: 'Jugend U15', mandatsreferenz: null, mandatsdatum: null },
|
{ vorname: 'Nina', nachname: 'Lange', email: 'nina.lange@example.de', status: 'ausgetreten', gruppe_ids: [], eintrittsdatum: '2016-02-01', austrittsdatum: '2023-12-31' },
|
||||||
{ vorname: 'Emma', nachname: 'Klein', email: null, telefon: '0160 1122334', geburtsdatum: '2011-06-15', eintrittsdatum: '2022-09-01', strasse: 'Seeweg 7', plz: '80335', ort: 'Musterstadt', iban: null, bic: null, status: 'aktiv', gruppe: 'Jugend U15', mandatsreferenz: null, mandatsdatum: null },
|
|
||||||
// Senioren
|
|
||||||
{ vorname: 'Gertrude', nachname: 'Neumann', email: 'g.neumann@example.de', telefon: '089 9876543', geburtsdatum: '1948-04-12', eintrittsdatum: '1990-01-01', strasse: 'Ahornweg 1', plz: '80333', ort: 'Musterstadt', iban: 'DE43500105176118506698', bic: 'BELADEBEXXX', status: 'aktiv', gruppe: 'Senioren', mandatsreferenz: 'MANDAT-016', mandatsdatum: '2019-06-01' },
|
|
||||||
// Passiv / Ausgetreten
|
|
||||||
{ vorname: 'Hans', nachname: 'Schneider', email: 'hans.schneider@example.de', telefon: '0173 0000001', geburtsdatum: '1965-10-20', eintrittsdatum: '1995-04-01', strasse: 'Bergblick 5', plz: '80334', ort: 'Musterstadt', iban: null, bic: null, status: 'passiv', gruppe: 'Senioren', mandatsreferenz: null, mandatsdatum: null },
|
|
||||||
{ vorname: 'Horst', nachname: 'Braun', email: 'horst.braun@example.de', telefon: null, geburtsdatum: '1960-02-14', eintrittsdatum: '1988-01-01', strasse: 'Alte Gasse 3', plz: '80336', ort: 'Musterstadt', iban: null, bic: null, status: 'ausgetreten', gruppe: 'Aktive Mitglieder', mandatsreferenz: null, mandatsdatum: null, austrittsdatum: '2024-12-31' },
|
|
||||||
];
|
];
|
||||||
|
for (const m of mitgliederDaten) await req('POST', '/mitglieder', m, T);
|
||||||
|
console.log(` ✓ 18 Mitglieder`);
|
||||||
|
|
||||||
let mitgliederIds = [];
|
// 5. Veranstaltungsorte
|
||||||
for (const m of mitgliederDef) {
|
console.log('→ Orte...');
|
||||||
let rec = await find('mitglieder', `vorname = "${m.vorname}" && nachname = "${m.nachname}" && verein_id = "${verein.id}"`);
|
|
||||||
if (!rec) {
|
|
||||||
rec = await create('mitglieder', {
|
|
||||||
verein_id: verein.id,
|
|
||||||
vorname: m.vorname,
|
|
||||||
nachname: m.nachname,
|
|
||||||
email: m.email || null,
|
|
||||||
telefon: m.telefon || null,
|
|
||||||
geburtsdatum: m.geburtsdatum,
|
|
||||||
eintrittsdatum: m.eintrittsdatum,
|
|
||||||
austrittsdatum: m.austrittsdatum || null,
|
|
||||||
strasse: m.strasse,
|
|
||||||
plz: m.plz,
|
|
||||||
ort: m.ort,
|
|
||||||
iban: m.iban || null,
|
|
||||||
bic: m.bic || null,
|
|
||||||
mandatsreferenz:m.mandatsreferenz || null,
|
|
||||||
mandatsdatum: m.mandatsdatum || null,
|
|
||||||
status: m.status,
|
|
||||||
gruppe_ids: [gruppen[m.gruppe]].filter(Boolean),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
mitgliederIds.push(rec.id);
|
|
||||||
}
|
|
||||||
console.log(` ✓ ${mitgliederIds.length} Mitglieder`);
|
|
||||||
|
|
||||||
// ── 6. Veranstaltungsorte ────────────────────────────────────────────────────
|
|
||||||
console.log('→ Veranstaltungsorte…');
|
|
||||||
const orteDef = [
|
|
||||||
{ name: 'Turnhalle Grundschule Muster', adresse: 'Schulstraße 1, 80333 Musterstadt', typ: 'halle', aktiv: true },
|
|
||||||
{ name: 'Vereinsheim TSV', adresse: 'Vereinsstraße 12, 80333 Musterstadt', typ: 'gebaeude', aktiv: true },
|
|
||||||
{ name: 'Sportplatz West', adresse: 'Weststraße 99, 80335 Musterstadt', typ: 'platz', aktiv: true },
|
|
||||||
];
|
|
||||||
const orte = {};
|
const orte = {};
|
||||||
for (const o of orteDef) {
|
for (const o of [
|
||||||
let rec = await find('veranstaltungsorte', `name = "${o.name}" && verein_id = "${verein.id}"`);
|
{ name: 'Turnhalle Grundschule Muster', adresse: 'Schulweg 5, 12345 Musterstadt', typ: 'halle', aktiv: true },
|
||||||
if (!rec) rec = await create('veranstaltungsorte', { verein_id: verein.id, ...o });
|
{ name: 'Vereinsheim TSV', adresse: 'Musterstraße 3, 12345 Musterstadt', typ: 'gebaeude', aktiv: true },
|
||||||
orte[o.name] = rec.id;
|
{ name: 'Sportplatz West', adresse: 'Weststraße 10, 12345 Musterstadt', typ: 'platz', aktiv: true },
|
||||||
|
]) {
|
||||||
|
const r = await req('POST', '/orte', o, T);
|
||||||
|
orte[o.name] = r.id;
|
||||||
}
|
}
|
||||||
// Ausfall: Turnhalle gesperrt wegen Schulveranstaltung
|
console.log(` ✓ 3 Orte`);
|
||||||
const turnhalleId = orte['Turnhalle Grundschule Muster'];
|
|
||||||
const existingAusfall = await find('ort_ausfaelle', `ort_id = "${turnhalleId}"`);
|
// 6. Beitragsarten
|
||||||
if (!existingAusfall) {
|
console.log('→ Beitragsarten...');
|
||||||
const nextWeek = new Date(); nextWeek.setDate(nextWeek.getDate() + 7);
|
for (const b of [
|
||||||
const nextWeek2 = new Date(); nextWeek2.setDate(nextWeek2.getDate() + 9);
|
{ name: 'Jahresbeitrag Erwachsene', betrag: 120, rhythmus: 'jaehrlich', beschreibung: 'Normalbeitrag für erwachsene Mitglieder' },
|
||||||
await create('ort_ausfaelle', {
|
{ name: 'Jahresbeitrag Jugend', betrag: 60, rhythmus: 'jaehrlich', beschreibung: 'Ermäßigter Beitrag bis 18 Jahre' },
|
||||||
ort_id: turnhalleId,
|
{ name: 'Aufnahmegebühr', betrag: 25, rhythmus: 'einmalig', beschreibung: 'Einmalige Gebühr bei Eintritt' },
|
||||||
von: nextWeek.toISOString().slice(0,10),
|
{ name: 'Monatsbeitrag Fitness', betrag: 15, rhythmus: 'monatlich', beschreibung: 'Zusatzbeitrag Fitnessraum' },
|
||||||
bis: nextWeek2.toISOString().slice(0,10),
|
]) {
|
||||||
grund: 'Schulveranstaltung – Halle nicht verfügbar',
|
await req('POST', '/beitraege', b, T);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
console.log(' ✓ Orte:', Object.keys(orte).join(', '));
|
console.log(` ✓ 4 Beitragsarten`);
|
||||||
|
|
||||||
// ── 7. Beitragsarten ─────────────────────────────────────────────────────────
|
// 7. Termine
|
||||||
console.log('→ Beitragsarten…');
|
console.log('→ Termine...');
|
||||||
const beitraegeDef = [
|
const now = new Date();
|
||||||
{ name: 'Jahresbeitrag Erwachsene', betrag: 48, rhythmus: 'jaehrlich', beschreibung: 'Für Mitglieder ab 18 Jahren' },
|
const dt = (offsetDays, h = 18, m = 0) => {
|
||||||
{ name: 'Jahresbeitrag Jugend', betrag: 24, rhythmus: 'jaehrlich', beschreibung: 'Für Mitglieder unter 18 Jahren' },
|
const d = new Date(now);
|
||||||
{ name: 'Aufnahmegebühr', betrag: 20, rhythmus: 'einmalig', beschreibung: 'Einmalig bei Vereinseintritt' },
|
d.setDate(d.getDate() + offsetDays);
|
||||||
{ name: 'Monatsbeitrag Fitness', betrag: 15, rhythmus: 'monatlich', beschreibung: 'Für die Fitnessgruppe (optional)' },
|
d.setHours(h, m, 0, 0);
|
||||||
];
|
return d.toISOString().slice(0, 19);
|
||||||
for (const b of beitraegeDef) {
|
|
||||||
const ex = await find('beitraege', `name = "${b.name}" && verein_id = "${verein.id}"`);
|
|
||||||
if (!ex) await create('beitraege', { verein_id: verein.id, ...b });
|
|
||||||
}
|
|
||||||
console.log(' ✓', beitraegeDef.map(b => b.name).join(', '));
|
|
||||||
|
|
||||||
// ── 8. Termine ───────────────────────────────────────────────────────────────
|
|
||||||
console.log('→ Termine…');
|
|
||||||
|
|
||||||
const d = (offsetDays, h = 0, m = 0) => {
|
|
||||||
const dt = new Date();
|
|
||||||
dt.setDate(dt.getDate() + offsetDays);
|
|
||||||
dt.setHours(h, m, 0, 0);
|
|
||||||
return dt.toISOString();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const termineDef = [
|
const termine = [
|
||||||
// Vergangen
|
{ titel: 'Vorstandssitzung', beginn: dt(3, 19), ort_id: orte['Vereinsheim TSV'], gruppe_ids: [gruppen['Vorstand']], verfuegbarkeit: 'bestaetigt' },
|
||||||
{ titel: 'Jahreshauptversammlung 2026', beginn: d(-60, 19, 0), ende: d(-60, 21, 0), ort_id: orte['Vereinsheim TSV'], gruppe_ids: Object.values(gruppen), beschreibung: 'Jahresabrechnung, Vorstandswahl, Planung Sommerfest' },
|
{ titel: 'Vereinsmeisterschaft', beginn: dt(14, 9), ort_id: orte['Sportplatz West'], gruppe_ids: [], beschreibung: 'Jährliche Meisterschaft', verfuegbarkeit: 'bestaetigt' },
|
||||||
{ titel: 'Trainingssession', beginn: d(-14, 18, 0), ende: d(-14, 20, 0), ort_id: orte['Turnhalle Grundschule Muster'], gruppe_ids: [gruppen['Aktive Mitglieder']] },
|
{ titel: 'Jugendtraining', beginn: dt(2, 16), ort_id: orte['Turnhalle Grundschule Muster'], gruppe_ids: [gruppen['Jugend U15']], verfuegbarkeit: 'bestaetigt' },
|
||||||
{ titel: 'Jugendtraining', beginn: d(-7, 16, 0), ende: d(-7, 17, 30), ort_id: orte['Turnhalle Grundschule Muster'], gruppe_ids: [gruppen['Jugend U15']] },
|
{ titel: 'Mitgliederversammlung', beginn: dt(21, 19,30),ort_id: orte['Vereinsheim TSV'], gruppe_ids: [], beschreibung: 'Ordentliche Jahreshauptversammlung', verfuegbarkeit: 'offen' },
|
||||||
// Upcoming
|
{ titel: 'Seniorensport', beginn: dt(7, 10), ort_id: orte['Turnhalle Grundschule Muster'], gruppe_ids: [gruppen['Senioren']], verfuegbarkeit: 'bestaetigt' },
|
||||||
{ titel: 'Vorstandssitzung', beginn: d(3, 19, 30), ende: d(3, 21, 0), ort_id: orte['Vereinsheim TSV'], gruppe_ids: [gruppen['Vorstand']], beschreibung: 'Vorbereitung Sommerfest, Kassenstand Q1' },
|
{ titel: 'Sommerfest', beginn: dt(45, 14), ort_id: orte['Sportplatz West'], gruppe_ids: [], beschreibung: 'Großes Vereinssommerfest', verfuegbarkeit: 'offen' },
|
||||||
{ titel: 'Sommerfest TSV', beginn: d(32, 14, 0), ende: d(32, 22, 0), ort_id: orte['Sportplatz West'], gruppe_ids: Object.values(gruppen), beschreibung: 'Für Mitglieder und Familien – Grillen, Spiele, Live-Musik' },
|
|
||||||
{ titel: 'Auswärtsspiel Musterliga', beginn: d(18, 11, 0), ende: d(18, 13, 0), ort_id: null, gruppe_ids: [gruppen['Aktive Mitglieder']], ort: 'FC Gegner – Stadionstraße 1', beschreibung: 'Hinfahrt 10:00 Uhr am Vereinsheim' },
|
|
||||||
];
|
];
|
||||||
|
// 8 Wochen Dienstags-Training
|
||||||
// Wöchentliches Training als Serie
|
let di = new Date(now);
|
||||||
const serie_id = 'seed-serie-dienstag-2026';
|
di.setDate(di.getDate() + ((2 - di.getDay() + 7) % 7 || 7));
|
||||||
const trainingsTermine = [];
|
di.setHours(18, 0, 0, 0);
|
||||||
for (let i = 1; i <= 8; i++) {
|
for (let i = 0; i < 8; i++) {
|
||||||
// Nächster Dienstag + i Wochen
|
const b = new Date(di); b.setDate(b.getDate() + i * 7);
|
||||||
const dt = new Date();
|
termine.push({
|
||||||
const daysToTue = (2 - dt.getDay() + 7) % 7 || 7;
|
titel: 'Training Aktive', beginn: b.toISOString().slice(0, 19),
|
||||||
dt.setDate(dt.getDate() + daysToTue + (i - 1) * 7);
|
|
||||||
dt.setHours(18, 30, 0, 0);
|
|
||||||
trainingsTermine.push({
|
|
||||||
titel: 'Dienstags-Training',
|
|
||||||
beginn: dt.toISOString(),
|
|
||||||
ende: new Date(dt.getTime() + 90 * 60 * 1000).toISOString(),
|
|
||||||
ort_id: orte['Turnhalle Grundschule Muster'],
|
ort_id: orte['Turnhalle Grundschule Muster'],
|
||||||
gruppe_ids:[gruppen['Aktive Mitglieder']],
|
gruppe_ids: [gruppen['Aktive Mitglieder']], verfuegbarkeit: 'offen',
|
||||||
rrule: 'FREQ=WEEKLY;BYDAY=TU',
|
rrule: 'FREQ=WEEKLY;BYDAY=TU',
|
||||||
serie_id,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
for (const t of termine) await req('POST', '/termine', t, T);
|
||||||
|
console.log(` ✓ ${termine.length} Termine`);
|
||||||
|
|
||||||
const allTermine = [...termineDef, ...trainingsTermine];
|
// 8. Nachricht
|
||||||
let termineAngelegt = 0;
|
console.log('→ Nachricht...');
|
||||||
for (const t of allTermine) {
|
await req('POST', '/nachrichten', {
|
||||||
const ex = await find('termine', `titel = "${t.titel}" && verein_id = "${verein.id}" && beginn = "${t.beginn}"`);
|
betreff: 'Willkommen in vereins.haus!',
|
||||||
if (!ex) {
|
text: '<p>Hallo und herzlich willkommen! Dies ist eine Beispiel-Nachricht an alle Mitglieder.</p>',
|
||||||
await create('termine', { verein_id: verein.id, verfuegbarkeit: 'offen', ...t });
|
gruppe_ids: [],
|
||||||
termineAngelegt++;
|
}, T);
|
||||||
}
|
console.log(` ✓ Nachricht erstellt`);
|
||||||
}
|
|
||||||
console.log(` ✓ ${termineAngelegt} Termine (inkl. ${trainingsTermine.length} in Dienstags-Serie)`);
|
|
||||||
|
|
||||||
// ── 9. Nachricht ─────────────────────────────────────────────────────────────
|
console.log('');
|
||||||
console.log('→ Beispiel-Nachricht…');
|
console.log('✓ Seed abgeschlossen!');
|
||||||
const exMsg = await find('nachrichten', `verein_id = "${verein.id}"`);
|
console.log('');
|
||||||
if (!exMsg) {
|
console.log(` Verein: TSV Musterstadt 1983 e.V.`);
|
||||||
await create('nachrichten', {
|
console.log(` Login: vorstand@tsv-musterstadt.de / Test123456!`);
|
||||||
verein_id: verein.id,
|
console.log(` App: ${BASE}`);
|
||||||
autor_id: adminUser.id,
|
|
||||||
betreff: 'Willkommen bei vereins.haus!',
|
|
||||||
text: 'Liebe Mitglieder,\n\nwir haben unsere Vereinsverwaltung auf vereins.haus umgestellt. Hier findet ihr alle Termine, Nachrichten und Informationen rund um unseren Verein.\n\nBei Fragen wendet euch an den Vorstand.\n\nEuer Vorstand\nTSV Musterstadt 1983 e.V.',
|
|
||||||
gruppe_ids: Object.values(gruppen),
|
|
||||||
gesendet_am: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Zusammenfassung ──────────────────────────────────────────────────────────
|
main().catch(e => { console.error('✗ Seed-Fehler:', e.message); process.exit(1); });
|
||||||
console.log('\n✓ Seed abgeschlossen!\n');
|
|
||||||
console.log(' Verein: ', verein.name);
|
|
||||||
console.log(' Login: vorstand@tsv-musterstadt.de / Test123456!');
|
|
||||||
console.log(' Mitglieder:', mitgliederIds.length);
|
|
||||||
console.log(' PocketBase:', PB_URL);
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue