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
108
Makefile
108
Makefile
|
|
@ -1,22 +1,18 @@
|
|||
# ==============================================================
|
||||
# VEREINS.HAUS — Makefile
|
||||
# Deploy-Strategie: SSH zur DS, Docker Compose
|
||||
# Stack: SvelteKit + better-sqlite3 (kein PocketBase)
|
||||
# ==============================================================
|
||||
|
||||
DS_HOST := ds
|
||||
DS_IP := 10.47.11.10
|
||||
DS_SSH_PORT := 4711
|
||||
DS_PATH := /volume1/docker/vereinshaus
|
||||
CONTAINER_PB := vereinshaus-pocketbase
|
||||
CONTAINER_APP := vereinshaus-app
|
||||
DOCKER := sudo /usr/local/bin/docker
|
||||
|
||||
STAGING_PATH := /volume1/docker/vereinshaus-staging
|
||||
CONTAINER_PB_STAGING := vereinshaus-staging-pocketbase
|
||||
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
|
||||
CONTAINER_APP_STAGING := vereinshaus-staging-app
|
||||
|
||||
TAR_EXCLUDE := --exclude='.git' \
|
||||
--exclude='./app/node_modules' \
|
||||
|
|
@ -25,13 +21,8 @@ TAR_EXCLUDE := --exclude='.git' \
|
|||
--exclude='./.env' \
|
||||
--exclude='./.DS_Store'
|
||||
|
||||
HOOKS_SRC := pocketbase/pb_hooks
|
||||
HOOKS_DST := /volume1/docker/vereinshaus/pocketbase/data/pb_hooks
|
||||
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
|
||||
.PHONY: help check-ssh start stop restart status logs logs-app logs-f deploy \
|
||||
staging-deploy staging-reset staging-seed staging-logs staging-status staging-stop
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Hilfe
|
||||
|
|
@ -41,16 +32,16 @@ help:
|
|||
@echo " vereins.haus — verfügbare Befehle:"
|
||||
@echo ""
|
||||
@echo " make deploy App bauen + zur DS übertragen + Container neu starten"
|
||||
@echo " make start Alle Container starten"
|
||||
@echo " make stop Alle Container stoppen"
|
||||
@echo " make restart Alle Container neu starten"
|
||||
@echo " make start Container starten"
|
||||
@echo " make stop Container stoppen"
|
||||
@echo " make restart Container neu starten"
|
||||
@echo " make status Container-Status anzeigen"
|
||||
@echo " make logs App-Logs (100 Zeilen)"
|
||||
@echo " make logs-f App Live-Log"
|
||||
@echo ""
|
||||
@echo " make logs PocketBase-Logs (100 Zeilen)"
|
||||
@echo " make logs-app App-Logs (100 Zeilen)"
|
||||
@echo " make logs-f PocketBase Live-Log"
|
||||
@echo " make shell-pb Shell in PocketBase-Container"
|
||||
@echo " make pb-admin PocketBase Admin-URL anzeigen"
|
||||
@echo " make staging-deploy Staging deployen"
|
||||
@echo " make staging-seed Testdaten einfügen"
|
||||
@echo " make staging-reset Staging-DB löschen (Neustart)"
|
||||
@echo ""
|
||||
|
||||
# ----------------------------------------------------------
|
||||
|
|
@ -65,7 +56,7 @@ check-ssh:
|
|||
fi
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# DEPLOY
|
||||
# DEPLOY (Production)
|
||||
# ----------------------------------------------------------
|
||||
deploy: check-ssh
|
||||
@echo "→ Sync zu DS..."
|
||||
|
|
@ -74,23 +65,6 @@ deploy: check-ssh
|
|||
@if [ -f .env ]; then \
|
||||
cat .env | ssh $(DS_HOST) "cat > $(DS_PATH)/.env"; \
|
||||
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..."
|
||||
@ssh $(DS_HOST) " \
|
||||
cd $(DS_PATH) && \
|
||||
|
|
@ -98,7 +72,7 @@ deploy: check-ssh
|
|||
$(DOCKER) compose build app && \
|
||||
$(DOCKER) compose up -d"
|
||||
@echo " ✓ Deploy fertig."
|
||||
@ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER_APP) --tail=10"
|
||||
@ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER_APP) --tail=15"
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Container-Steuerung
|
||||
|
|
@ -124,27 +98,16 @@ status: check-ssh
|
|||
# Logs
|
||||
# ----------------------------------------------------------
|
||||
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"
|
||||
|
||||
logs-app: logs
|
||||
|
||||
logs-f: check-ssh
|
||||
@ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER_PB) -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/_/"
|
||||
@ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER_APP) -f"
|
||||
|
||||
# ==============================================================
|
||||
# STAGING
|
||||
# ==============================================================
|
||||
.PHONY: staging-deploy staging-reset staging-seed staging-logs staging-status staging-stop
|
||||
|
||||
staging-deploy: check-ssh
|
||||
@echo "→ Sync zu DS (Staging)..."
|
||||
|
|
@ -153,20 +116,6 @@ staging-deploy: check-ssh
|
|||
@if [ -f .env ]; then \
|
||||
cat .env | ssh $(DS_HOST) "cat > $(STAGING_PATH)/.env"; \
|
||||
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)..."
|
||||
@ssh $(DS_HOST) " \
|
||||
cd $(STAGING_PATH) && \
|
||||
|
|
@ -175,30 +124,25 @@ staging-deploy: check-ssh
|
|||
$(DOCKER) compose -f docker-compose.staging.yml up -d"
|
||||
@echo " ✓ Staging bereit."
|
||||
@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
|
||||
staging-reset: check-ssh staging-stop
|
||||
@echo "→ PocketBase-Daten auf Staging löschen..."
|
||||
@ssh $(DS_HOST) "rm -rf \
|
||||
$(STAGING_PATH)/pocketbase/data/storage \
|
||||
'$(STAGING_PATH)/pocketbase/data/data.db' \
|
||||
'$(STAGING_PATH)/pocketbase/data/data.db-wal' \
|
||||
'$(STAGING_PATH)/pocketbase/data/data.db-shm' \
|
||||
$(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 "→ Staging-Daten löschen..."
|
||||
@ssh $(DS_HOST) "rm -f \
|
||||
$(STAGING_PATH)/data/vereinshaus.db \
|
||||
$(STAGING_PATH)/data/vereinshaus.db-wal \
|
||||
$(STAGING_PATH)/data/vereinshaus.db-shm && \
|
||||
rm -rf $(STAGING_PATH)/data/uploads"
|
||||
@echo " ✓ Reset fertig. Jetzt: make staging-deploy && make staging-seed"
|
||||
|
||||
staging-seed:
|
||||
@echo "→ Testdaten in Staging einfügen..."
|
||||
@echo " Voraussetzung: PB_EMAIL + PB_PASSWORD in .env gesetzt (Staging-Superuser)"
|
||||
@if [ -f .env ]; then \
|
||||
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 \
|
||||
PB_URL=https://api-staging.vereins.haus node scripts/seed.js; \
|
||||
APP_URL=https://staging.vereins.haus node scripts/seed.js; \
|
||||
fi
|
||||
|
||||
staging-logs: check-ssh
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
FROM node:22-alpine AS builder
|
||||
RUN apk add --no-cache python3 make g++
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
ARG VITE_PB_URL=http://vereinshaus-pocketbase:8090
|
||||
ENV VITE_PB_URL=$VITE_PB_URL
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22-alpine
|
||||
RUN apk add --no-cache python3 make g++
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/build ./build
|
||||
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",
|
||||
"dependencies": {
|
||||
"@event-calendar/core": "^5.7.0",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^12.10.0",
|
||||
"ical-generator": "^10.2.0",
|
||||
"jose": "^6.2.3",
|
||||
"papaparse": "^5.5.3",
|
||||
"pocketbase": "^0.26.9",
|
||||
"rrule": "^2.8.1",
|
||||
|
|
@ -19,6 +22,8 @@
|
|||
"@sveltejs/adapter-node": "^5.5.4",
|
||||
"@sveltejs/kit": "^2.57.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/web-push": "^3.6.4",
|
||||
"svelte": "^5.55.2",
|
||||
|
|
@ -2637,6 +2642,23 @@
|
|||
"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": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
|
|
@ -2895,6 +2917,26 @@
|
|||
"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": {
|
||||
"version": "2.10.31",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz",
|
||||
|
|
@ -2908,6 +2950,49 @@
|
|||
"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": {
|
||||
"version": "4.12.3",
|
||||
"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_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": {
|
||||
"version": "1.0.1",
|
||||
"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/"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "2.1.1",
|
||||
"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": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
|
|
@ -3271,7 +3410,6 @@
|
|||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
|
|
@ -3330,6 +3468,15 @@
|
|||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.24.2",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "3.1.3",
|
||||
"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": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
|
||||
|
|
@ -3651,6 +3813,12 @@
|
|||
"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": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||
|
|
@ -3807,6 +3975,12 @@
|
|||
"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": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
|
||||
|
|
@ -4030,12 +4204,38 @@
|
|||
"dev": true,
|
||||
"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": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||
|
|
@ -4508,6 +4708,15 @@
|
|||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
|
@ -4922,6 +5131,18 @@
|
|||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
|
|
@ -4963,6 +5184,12 @@
|
|||
"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": {
|
||||
"version": "1.2.0",
|
||||
"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_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": {
|
||||
"version": "2.0.44",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz",
|
||||
|
|
@ -5070,6 +5327,15 @@
|
|||
],
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
||||
|
|
@ -5210,6 +5476,33 @@
|
|||
"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": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
|
||||
|
|
@ -5223,6 +5516,16 @@
|
|||
"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": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
|
@ -5233,6 +5536,35 @@
|
|||
"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": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
|
|
@ -5758,6 +6090,51 @@
|
|||
"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": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
|
||||
|
|
@ -5842,6 +6219,15 @@
|
|||
"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": {
|
||||
"version": "4.0.12",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
|
||||
|
|
@ -5954,6 +6340,15 @@
|
|||
"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": {
|
||||
"version": "1.0.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
|
||||
|
|
@ -6118,6 +6541,18 @@
|
|||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"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": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz",
|
||||
|
|
@ -6358,6 +6793,12 @@
|
|||
"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": {
|
||||
"version": "8.0.13",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
|
||||
|
|
@ -6850,6 +7291,12 @@
|
|||
"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": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
"@sveltejs/adapter-node": "^5.5.4",
|
||||
"@sveltejs/kit": "^2.57.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/web-push": "^3.6.4",
|
||||
"svelte": "^5.55.2",
|
||||
|
|
@ -27,7 +29,10 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@event-calendar/core": "^5.7.0",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^12.10.0",
|
||||
"ical-generator": "^10.2.0",
|
||||
"jose": "^6.2.3",
|
||||
"papaparse": "^5.5.3",
|
||||
"pocketbase": "^0.26.9",
|
||||
"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 { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { pb } from '$lib/pb';
|
||||
import { user } from '$lib/user';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import type { IconName } from '$lib/icons';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
onMount(() => {
|
||||
if (!pb.authStore.isValid) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
if (!pb.authStore.record?.verein_id) {
|
||||
goto('/onboarding');
|
||||
return;
|
||||
}
|
||||
if (!$user) { goto('/login'); return; }
|
||||
if (!$user.verein_id) { goto('/onboarding'); return; }
|
||||
registerPush();
|
||||
});
|
||||
|
||||
async function registerPush() {
|
||||
if (!('serviceWorker' in navigator) || !('PushManager' in window)) return;
|
||||
|
||||
const permission = await Notification.requestPermission();
|
||||
if (permission !== 'granted') return;
|
||||
|
||||
try {
|
||||
const reg = await navigator.serviceWorker.ready;
|
||||
|
||||
// VAPID public key vom Server holen
|
||||
const keyRes = await fetch('/api/push/key');
|
||||
const { publicKey } = await keyRes.json();
|
||||
if (!publicKey) return;
|
||||
|
||||
// Bestehende oder neue Subscription
|
||||
let sub = await reg.pushManager.getSubscription();
|
||||
if (!sub) {
|
||||
sub = await reg.pushManager.subscribe({
|
||||
|
|
@ -42,18 +30,10 @@
|
|||
applicationServerKey: urlBase64ToUint8Array(publicKey) as BufferSource,
|
||||
});
|
||||
}
|
||||
|
||||
// In PocketBase speichern
|
||||
await fetch('/api/push/subscribe', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: pb.authStore.token,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subscription: sub.toJSON(),
|
||||
userId: pb.authStore.record?.id,
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${$user?.token}` },
|
||||
body: JSON.stringify({ subscription: sub.toJSON(), userId: $user?.id }),
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('[push] Registrierung fehlgeschlagen:', e);
|
||||
|
|
@ -67,7 +47,7 @@
|
|||
return Uint8Array.from([...raw].map((c) => c.charCodeAt(0)));
|
||||
}
|
||||
|
||||
const isAdmin = () => !pb.authStore.record?.rolle || pb.authStore.record?.rolle === 'admin';
|
||||
const isAdmin = () => !$user?.rolle || $user?.rolle === 'admin';
|
||||
|
||||
const allNavItems: { href: string; label: string; icon: IconName; adminOnly?: boolean }[] = [
|
||||
{ href: '/neuigkeiten', label: 'Neuigkeiten', icon: 'images' },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Verein, Termin } from '$lib/types';
|
||||
|
||||
|
|
@ -8,15 +8,12 @@
|
|||
let loading = $state(true);
|
||||
|
||||
onMount(async () => {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
const now = new Date().toISOString();
|
||||
[verein, termine] = await Promise.all([
|
||||
pb.collection('vereine').getOne<Verein>(vid),
|
||||
pb.collection('termine').getList<Termin>(1, 3, {
|
||||
filter: `beginn >= "${now}"`,
|
||||
sort: 'beginn',
|
||||
}).then(r => r.items),
|
||||
api.get<Verein>('/vereine'),
|
||||
api.get<Termin[]>('/termine', { sort: 'beginn', page: '1', perPage: '3' }),
|
||||
]);
|
||||
termine = (termine as any)?.items ?? termine;
|
||||
loading = false;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { user } from '$lib/user';
|
||||
import { get } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { generatePain008, downloadXml, minEinzugsdatum, type SepaPosition } from '$lib/sepa';
|
||||
|
|
@ -41,11 +43,10 @@
|
|||
};
|
||||
|
||||
onMount(async () => {
|
||||
if (pb.authStore.record?.rolle === 'trainer') { goto('/'); return; }
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
if (get(user)?.rolle === 'trainer') { goto('/'); return; }
|
||||
[beitraege, verein] = await Promise.all([
|
||||
pb.collection('beitraege').getFullList<Beitrag>({ sort: 'name' }),
|
||||
pb.collection('vereine').getOne<Verein>(vid).catch(() => null),
|
||||
api.get<Beitrag[]>('/beitraege', { sort: 'name' }),
|
||||
api.get<Verein>('/vereine').catch(() => null),
|
||||
]);
|
||||
loading = false;
|
||||
});
|
||||
|
|
@ -71,13 +72,12 @@
|
|||
}
|
||||
saving = true;
|
||||
try {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
const data = { verein_id: vid, name: fName.trim(), betrag, rhythmus: fRhythmus, beschreibung: fBeschr.trim() };
|
||||
const data = { name: fName.trim(), betrag, rhythmus: fRhythmus, beschreibung: fBeschr.trim() };
|
||||
if (editId) {
|
||||
await pb.collection('beitraege').update(editId, data);
|
||||
await api.put('/beitraege/' + editId, data);
|
||||
beitraege = beitraege.map(b => b.id === editId ? { ...b, ...data } as Beitrag : b);
|
||||
} else {
|
||||
const neu = await pb.collection('beitraege').create<Beitrag>(data);
|
||||
const neu = await api.post<Beitrag>('/beitraege', data);
|
||||
beitraege = [...beitraege, neu].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
showForm = false;
|
||||
|
|
@ -90,7 +90,7 @@
|
|||
|
||||
async function loeschen(b: Beitrag) {
|
||||
if (!confirm(`"${b.name}" wirklich löschen?`)) return;
|
||||
await pb.collection('beitraege').delete(b.id);
|
||||
await api.del('/beitraege/' + b.id);
|
||||
beitraege = beitraege.filter(x => x.id !== b.id);
|
||||
}
|
||||
|
||||
|
|
@ -102,9 +102,7 @@
|
|||
einzugsdatum = minEinzugsdatum();
|
||||
sepaLoading = true;
|
||||
try {
|
||||
const alle = await pb.collection('mitglieder').getFullList<Mitglied>({
|
||||
filter: 'status = "aktiv"', sort: 'nachname,vorname',
|
||||
});
|
||||
const alle = await api.get<Mitglied[]>('/mitglieder', { status: 'aktiv', sort: 'nachname,vorname' });
|
||||
const mit = alle.filter(m => m.iban?.trim());
|
||||
const ohne = alle.length - mit.length;
|
||||
sepaPreview = { mitglieder: mit, ohne };
|
||||
|
|
@ -157,10 +155,9 @@
|
|||
downloadXml(xml, `sepa-einzug-${einzugsdatum}.xml`);
|
||||
|
||||
// Einzüge als "ausstehend" anlegen
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
await Promise.all(
|
||||
sepaPreview.mitglieder.map((m) =>
|
||||
pb.collection('einzuege').create({
|
||||
api.post('/einzuege', {
|
||||
mitglied_id: m.id,
|
||||
beitrag_id: sepaFor!.id,
|
||||
betrag: sepaFor!.betrag,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { user } from '$lib/user';
|
||||
import { get } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Verein, Gruppe } from '$lib/types';
|
||||
|
||||
const isAdmin = () => !pb.authStore.record?.rolle || pb.authStore.record?.rolle === 'admin';
|
||||
const isAdmin = () => { const u = get(user); return !u?.rolle || u.rolle === 'admin'; };
|
||||
|
||||
let loading = $state(true);
|
||||
let saving = $state(false);
|
||||
|
|
@ -60,19 +62,19 @@
|
|||
];
|
||||
|
||||
onMount(async () => {
|
||||
vereinId = pb.authStore.record?.verein_id as string;
|
||||
vereinId = get(user)?.verein_id ?? '';
|
||||
const [v, alleUser, alleGruppen, mitgliederCount] = await Promise.all([
|
||||
pb.collection('vereine').getOne<Verein>(vereinId),
|
||||
api.get<Verein>('/vereine'),
|
||||
isAdmin()
|
||||
? pb.collection('users').getFullList({ filter: `verein_id = "${vereinId}"` })
|
||||
? api.get<any[]>('/users')
|
||||
: Promise.resolve([]),
|
||||
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
|
||||
pb.collection('mitglieder').getList(1, 1, { filter: `verein_id = "${vereinId}"` }).then(r => r.totalItems),
|
||||
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
|
||||
api.get<{ total: number }>('/mitglieder/count').catch(() => ({ total: 0 })),
|
||||
]);
|
||||
trainer = alleUser.filter((u: any) => u.rolle === 'trainer');
|
||||
gruppen = alleGruppen;
|
||||
plan = v.plan ?? 'free';
|
||||
mitgliederAnz = mitgliederCount;
|
||||
mitgliederAnz = (mitgliederCount as any).total ?? 0;
|
||||
name = v.name ?? '';
|
||||
adresse = v.adresse ?? '';
|
||||
plz = v.plz ?? '';
|
||||
|
|
@ -91,7 +93,7 @@
|
|||
if (!name.trim()) { error = 'Vereinsname ist Pflichtfeld.'; return; }
|
||||
error = ''; success = ''; saving = true;
|
||||
try {
|
||||
await pb.collection('vereine').update(vereinId, {
|
||||
await api.patch('/vereine', {
|
||||
name: name.trim(),
|
||||
adresse: adresse.trim() || null,
|
||||
plz: plz.trim() || null,
|
||||
|
|
@ -113,17 +115,14 @@
|
|||
}
|
||||
|
||||
async function trainerEinladen() {
|
||||
const token = crypto.randomUUID().replace(/-/g, '');
|
||||
await pb.collection('einladungen').create({
|
||||
verein_id: vereinId, rolle: 'trainer', token, genutzt: false,
|
||||
});
|
||||
einladungUrl = `${window.location.origin}/invite/${token}`;
|
||||
const inv = await api.post<{ token: string }>('/einladungen', { rolle: 'trainer' });
|
||||
einladungUrl = `${window.location.origin}/invite/${inv.token}`;
|
||||
einladungKopiert = false;
|
||||
}
|
||||
|
||||
async function trainerEntfernen(uid: string) {
|
||||
if (!confirm('Trainer wirklich entfernen?')) return;
|
||||
await pb.collection('users').update(uid, { rolle: null, verein_id: null });
|
||||
await api.del('/users/' + uid);
|
||||
trainer = trainer.filter(t => t.id !== uid);
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +134,7 @@
|
|||
}
|
||||
|
||||
function abmelden() {
|
||||
pb.authStore.clear();
|
||||
user.clear();
|
||||
goto('/login');
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { onMount } from 'svelte';
|
||||
import Papa from 'papaparse';
|
||||
import type { Mitglied } from '$lib/types';
|
||||
|
|
@ -39,10 +39,9 @@
|
|||
];
|
||||
|
||||
onMount(async () => {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
const [m, v] = await Promise.all([
|
||||
pb.collection('mitglieder').getFullList<Mitglied>({ sort: 'nachname,vorname' }),
|
||||
pb.collection('vereine').getOne<any>(vid),
|
||||
api.get<Mitglied[]>('/mitglieder', { sort: 'nachname,vorname' }),
|
||||
api.get<any>('/vereine'),
|
||||
]);
|
||||
mitglieder = m;
|
||||
vereinName = v.name ?? '';
|
||||
|
|
@ -93,14 +92,13 @@
|
|||
}
|
||||
|
||||
async function exportiereBackup() {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
const [verein, mitgl, gruppen, termine, beitraege, nachrichten] = await Promise.all([
|
||||
pb.collection('vereine').getOne<any>(vid),
|
||||
pb.collection('mitglieder').getFullList(),
|
||||
pb.collection('gruppen').getFullList(),
|
||||
pb.collection('termine').getFullList(),
|
||||
pb.collection('beitraege').getFullList(),
|
||||
pb.collection('nachrichten').getFullList(),
|
||||
api.get<any>('/vereine'),
|
||||
api.get<any[]>('/mitglieder'),
|
||||
api.get<any[]>('/gruppen'),
|
||||
api.get<any[]>('/termine'),
|
||||
api.get<any[]>('/beitraege'),
|
||||
api.get<any[]>('/nachrichten'),
|
||||
]);
|
||||
const backup = {
|
||||
exportiert_am: new Date().toISOString(),
|
||||
|
|
@ -177,12 +175,11 @@
|
|||
|
||||
async function importStarten() {
|
||||
importLaeuft = true;
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
let ok = 0;
|
||||
const fehler: string[] = [];
|
||||
|
||||
for (const row of csvRows) {
|
||||
const record: Record<string, any> = { verein_id: vid, status: 'aktiv' };
|
||||
const record: Record<string, any> = { status: 'aktiv' };
|
||||
for (const [csvSpalte, ziel] of Object.entries(feldMapping)) {
|
||||
if (!ziel) continue;
|
||||
const wert = row[csvSpalte]?.trim() ?? '';
|
||||
|
|
@ -200,7 +197,7 @@
|
|||
continue;
|
||||
}
|
||||
try {
|
||||
await pb.collection('mitglieder').create(record);
|
||||
await api.post('/mitglieder', record);
|
||||
ok++;
|
||||
} catch (e: unknown) {
|
||||
fehler.push(`${record.vorname} ${record.nachname}: ${e instanceof Error ? e.message : 'Fehler'}`);
|
||||
|
|
@ -208,7 +205,7 @@
|
|||
}
|
||||
|
||||
// Mitgliederliste aktualisieren
|
||||
mitglieder = await pb.collection('mitglieder').getFullList<Mitglied>({ sort: 'nachname,vorname' });
|
||||
mitglieder = await api.get<Mitglied[]>('/mitglieder', { sort: 'nachname,vorname' });
|
||||
importResult = { ok, fehler };
|
||||
importPhase = 'done';
|
||||
importLaeuft = false;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let mitglieder = $state<any[]>([]);
|
||||
|
|
@ -9,8 +9,8 @@
|
|||
|
||||
onMount(async () => {
|
||||
[mitglieder, gruppen] = await Promise.all([
|
||||
pb.collection('mitglieder').getFullList({ sort: 'nachname,vorname' }),
|
||||
pb.collection('gruppen').getFullList({ sort: 'name' })
|
||||
api.get<any[]>('/mitglieder', { sort: 'nachname,vorname' }),
|
||||
api.get<any[]>('/gruppen', { sort: 'name' })
|
||||
]);
|
||||
loading = false;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
|
|
@ -61,8 +61,8 @@
|
|||
|
||||
onMount(async () => {
|
||||
const [m, g] = await Promise.all([
|
||||
pb.collection('mitglieder').getOne<Mitglied>(id),
|
||||
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
|
||||
api.get<Mitglied>('/mitglieder/' + id),
|
||||
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
|
||||
]);
|
||||
loadRecord(m);
|
||||
gruppen = g;
|
||||
|
|
@ -78,7 +78,7 @@
|
|||
async function speichern() {
|
||||
error = ''; saving = true;
|
||||
try {
|
||||
await pb.collection('mitglieder').update(id, {
|
||||
await api.put('/mitglieder/' + id, {
|
||||
vorname: vorname.trim(),
|
||||
nachname: nachname.trim(),
|
||||
email: email.trim() || null,
|
||||
|
|
@ -107,7 +107,7 @@
|
|||
|
||||
async function loeschen() {
|
||||
try {
|
||||
await pb.collection('mitglieder').delete(id);
|
||||
await api.del('/mitglieder/' + id);
|
||||
goto('/mitglieder');
|
||||
} catch (e: unknown) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Löschen.';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
let loading = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
gruppen = await pb.collection('gruppen').getFullList({ sort: 'name' });
|
||||
gruppen = await api.get<any[]>('/gruppen', { sort: 'name' });
|
||||
});
|
||||
|
||||
function toggleGruppe(id: string) {
|
||||
|
|
@ -43,9 +43,7 @@
|
|||
async function speichern() {
|
||||
error = ''; loading = true;
|
||||
try {
|
||||
const verein_id = pb.authStore.record?.verein_id as string;
|
||||
await pb.collection('mitglieder').create({
|
||||
verein_id,
|
||||
await api.post('/mitglieder', {
|
||||
vorname: vorname.trim(),
|
||||
nachname: nachname.trim(),
|
||||
email: email.trim() || null,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { user } from '$lib/user';
|
||||
import { get } from 'svelte/store';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Nachricht, Gruppe } from '$lib/types';
|
||||
|
||||
|
|
@ -18,8 +20,8 @@
|
|||
|
||||
onMount(async () => {
|
||||
[nachrichten, gruppen] = await Promise.all([
|
||||
pb.collection('nachrichten').getFullList<Nachricht>({ sort: '-gesendet_am' }),
|
||||
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
|
||||
api.get<Nachricht[]>('/nachrichten', { sort: '-gesendet_am' }),
|
||||
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
|
||||
]);
|
||||
loading = false;
|
||||
});
|
||||
|
|
@ -43,12 +45,7 @@
|
|||
sendError = ''; sending = true;
|
||||
|
||||
try {
|
||||
const verein_id = pb.authStore.record?.verein_id as string;
|
||||
const autor_id = pb.authStore.record?.id as string;
|
||||
|
||||
const record = await pb.collection('nachrichten').create<Nachricht>({
|
||||
verein_id,
|
||||
autor_id,
|
||||
const record = await api.post<Nachricht>('/nachrichten', {
|
||||
betreff: fBetreff.trim(),
|
||||
text: fText.trim(),
|
||||
gruppe_ids: fGruppeIds,
|
||||
|
|
@ -63,7 +60,7 @@
|
|||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: pb.authStore.token,
|
||||
Authorization: `Bearer ${get(user)?.token ?? ''}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
titel: fBetreff.trim(),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { user } from '$lib/user';
|
||||
import { get } from 'svelte/store';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Neuigkeit, Gruppe, Termin } from '$lib/types';
|
||||
|
||||
const canPost = () => !pb.authStore.record?.rolle || pb.authStore.record?.rolle !== null;
|
||||
const userId = () => pb.authStore.record?.id as string;
|
||||
const canPost = () => true; // alle eingeloggten User dürfen posten
|
||||
const userId = () => get(user)?.id as string;
|
||||
|
||||
let neuigkeiten = $state<Neuigkeit[]>([]);
|
||||
let gruppen = $state<Gruppe[]>([]);
|
||||
|
|
@ -28,31 +30,25 @@
|
|||
let ladeError = $state('');
|
||||
|
||||
onMount(async () => {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
try {
|
||||
// Queries einzeln damit ein Fehler sichtbar wird
|
||||
const [nList, gList] = await Promise.all([
|
||||
pb.collection('neuigkeiten').getFullList<Neuigkeit>({
|
||||
sort: '-created',
|
||||
}),
|
||||
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
|
||||
api.get<Neuigkeit[]>('/neuigkeiten', { sort: '-created' }),
|
||||
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
|
||||
]);
|
||||
neuigkeiten = nList;
|
||||
gruppen = gList;
|
||||
|
||||
// Termine der letzten 30 Tage + zukünftige
|
||||
try {
|
||||
const von = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().replace('T', ' ');
|
||||
termine = await pb.collection('termine').getFullList<Termin>({
|
||||
filter: `beginn >= '${von}'`, sort: '-beginn',
|
||||
});
|
||||
termine = await api.get<Termin[]>('/termine', { sort: '-beginn' });
|
||||
} catch { termine = []; }
|
||||
|
||||
// Reaktionen – separat damit Fehler nicht alles blockiert
|
||||
try {
|
||||
const [rList, meineList] = await Promise.all([
|
||||
pb.collection('reaktionen').getFullList({ filter: `beitrag_id.verein_id = "${vid}"` }),
|
||||
pb.collection('reaktionen').getFullList({ filter: `user_id = "${userId()}"` }),
|
||||
api.get<any[]>('/reaktionen'),
|
||||
api.get<any[]>('/reaktionen', { meine: 'true' }),
|
||||
]);
|
||||
const counts: Record<string, number> = {};
|
||||
for (const r of rList) counts[r.beitrag_id] = (counts[r.beitrag_id] ?? 0) + 1;
|
||||
|
|
@ -96,11 +92,9 @@
|
|||
}
|
||||
formError = ''; saving = true;
|
||||
try {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
const u = get(user);
|
||||
const form = new FormData();
|
||||
form.append('verein_id', vid);
|
||||
form.append('autor_id', userId());
|
||||
form.append('autor_name', pb.authStore.record?.name ?? '');
|
||||
form.append('autor_name', u?.name ?? '');
|
||||
if (fText.trim()) form.append('text', fText.trim());
|
||||
if (fTerminId) form.append('termin_id', fTerminId);
|
||||
for (const id of fGruppeIds) form.append('gruppe_ids', id);
|
||||
|
|
@ -108,7 +102,7 @@
|
|||
for (const file of Array.from(fDateien)) form.append('medien', file);
|
||||
}
|
||||
|
||||
const neu = await pb.collection('neuigkeiten').create<Neuigkeit>(form);
|
||||
const neu = await api.postForm<Neuigkeit>('/neuigkeiten', form);
|
||||
neuigkeiten = [neu, ...neuigkeiten];
|
||||
showForm = false;
|
||||
fText = ''; fGruppeIds = []; fTerminId = ''; fDateien = null; previews = [];
|
||||
|
|
@ -121,26 +115,24 @@
|
|||
|
||||
async function loeschen(n: Neuigkeit) {
|
||||
if (!confirm('Beitrag löschen?')) return;
|
||||
await pb.collection('neuigkeiten').delete(n.id);
|
||||
await api.del('/neuigkeiten/' + n.id);
|
||||
neuigkeiten = neuigkeiten.filter(x => x.id !== n.id);
|
||||
}
|
||||
|
||||
async function toggleReaktion(n: Neuigkeit) {
|
||||
if (meineReaktion[n.id]) {
|
||||
await pb.collection('reaktionen').delete(meineReaktion[n.id]);
|
||||
await api.del('/reaktionen/' + meineReaktion[n.id]);
|
||||
meineReaktion = { ...meineReaktion, [n.id]: '' };
|
||||
reaktionen = { ...reaktionen, [n.id]: Math.max(0, (reaktionen[n.id] ?? 1) - 1) };
|
||||
} else {
|
||||
const r = await pb.collection('reaktionen').create({
|
||||
beitrag_id: n.id, user_id: userId(),
|
||||
});
|
||||
const r = await api.post<{ id: string }>('/reaktionen', { beitrag_id: n.id });
|
||||
meineReaktion = { ...meineReaktion, [n.id]: r.id };
|
||||
reaktionen = { ...reaktionen, [n.id]: (reaktionen[n.id] ?? 0) + 1 };
|
||||
}
|
||||
}
|
||||
|
||||
function mediaUrl(n: Neuigkeit, datei: string, thumb = false): string {
|
||||
return pb.files.getURL(n as any, datei, thumb ? { thumb: '400x300' } : {});
|
||||
return api.fileUrl(n.verein_id, n.id, datei, thumb);
|
||||
}
|
||||
|
||||
function isVideo(datei: string): boolean {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Veranstaltungsort, OrtAusfall } from '$lib/types';
|
||||
|
||||
|
|
@ -31,10 +31,9 @@
|
|||
};
|
||||
|
||||
onMount(async () => {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
[orte, ausfaelle] = await Promise.all([
|
||||
pb.collection('veranstaltungsorte').getFullList<Veranstaltungsort>({ sort: 'name', filter: `verein_id = "${vid}"` }),
|
||||
pb.collection('ort_ausfaelle').getFullList<OrtAusfall>({ sort: 'von' }),
|
||||
api.get<Veranstaltungsort[]>('/veranstaltungsorte', { sort: 'name' }),
|
||||
api.get<OrtAusfall[]>('/ort_ausfaelle', { sort: 'von' }),
|
||||
]);
|
||||
loading = false;
|
||||
});
|
||||
|
|
@ -54,13 +53,12 @@
|
|||
if (!fName.trim()) { ortError = 'Name ist Pflichtfeld.'; return; }
|
||||
ortError = ''; ortSaving = true;
|
||||
try {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
const data = { verein_id: vid, name: fName.trim(), adresse: fAdresse.trim() || null, typ: fTyp, aktiv: fAktiv };
|
||||
const data = { name: fName.trim(), adresse: fAdresse.trim() || null, typ: fTyp, aktiv: fAktiv };
|
||||
if (editOrtId) {
|
||||
const u = await pb.collection('veranstaltungsorte').update<Veranstaltungsort>(editOrtId, data);
|
||||
const u = await api.put<Veranstaltungsort>('/veranstaltungsorte/' + editOrtId, data);
|
||||
orte = orte.map(o => o.id === editOrtId ? u : o);
|
||||
} else {
|
||||
const n = await pb.collection('veranstaltungsorte').create<Veranstaltungsort>(data);
|
||||
const n = await api.post<Veranstaltungsort>('/veranstaltungsorte', data);
|
||||
orte = [...orte, n].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
showOrtForm = false;
|
||||
|
|
@ -73,7 +71,7 @@
|
|||
|
||||
async function ortLoeschen(id: string) {
|
||||
if (!confirm('Ort wirklich löschen? Alle verknüpften Termine verlieren die Ortzuordnung.')) return;
|
||||
await pb.collection('veranstaltungsorte').delete(id);
|
||||
await api.del('/veranstaltungsorte/' + id);
|
||||
orte = orte.filter(o => o.id !== id);
|
||||
ausfaelle = ausfaelle.filter(a => a.ort_id !== id);
|
||||
}
|
||||
|
|
@ -88,7 +86,7 @@
|
|||
if (aVon > aBis) { ausfallError = 'Bis muss nach Von liegen.'; return; }
|
||||
ausfallError = ''; ausfallSaving = true;
|
||||
try {
|
||||
const n = await pb.collection('ort_ausfaelle').create<OrtAusfall>({
|
||||
const n = await api.post<OrtAusfall>('/ort_ausfaelle', {
|
||||
ort_id: aOrtId, von: aVon, bis: aBis, grund: aGrund.trim() || null,
|
||||
});
|
||||
ausfaelle = [...ausfaelle, n].sort((a, b) => a.von.localeCompare(b.von));
|
||||
|
|
@ -101,7 +99,7 @@
|
|||
}
|
||||
|
||||
async function ausfallLoeschen(id: string) {
|
||||
await pb.collection('ort_ausfaelle').delete(id);
|
||||
await api.del('/ort_ausfaelle/' + id);
|
||||
ausfaelle = ausfaelle.filter(a => a.id !== id);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { user } from '$lib/user';
|
||||
import { get } from 'svelte/store';
|
||||
import { onMount } from 'svelte';
|
||||
import { RRule } from 'rrule';
|
||||
import { Calendar, TimeGrid, DayGrid } from '@event-calendar/core';
|
||||
import '@event-calendar/core/index.css';
|
||||
import type { Termin, Gruppe, Verfuegbarkeit, Veranstaltungsort, OrtAusfall } from '$lib/types';
|
||||
|
||||
const isAdmin = () => !pb.authStore.record?.rolle || pb.authStore.record?.rolle === 'admin';
|
||||
const userId = () => pb.authStore.record?.id as string;
|
||||
const isAdmin = () => { const u = get(user); return !u?.rolle || u.rolle === 'admin'; };
|
||||
const userId = () => get(user)?.id as string;
|
||||
|
||||
let termine = $state<Termin[]>([]);
|
||||
let gruppen = $state<Gruppe[]>([]);
|
||||
|
|
@ -59,15 +61,14 @@
|
|||
);
|
||||
|
||||
onMount(async () => {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
[termine, gruppen, alleUser, orte, ausfaelle] = await Promise.all([
|
||||
pb.collection('termine').getFullList<Termin>({ sort: 'beginn' }),
|
||||
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
|
||||
api.get<Termin[]>('/termine', { sort: 'beginn' }),
|
||||
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
|
||||
isAdmin()
|
||||
? pb.collection('users').getFullList({ filter: `verein_id = "${vid}" && rolle = "trainer"` })
|
||||
? api.get<any[]>('/users', { rolle: 'trainer' })
|
||||
: Promise.resolve([]),
|
||||
pb.collection('veranstaltungsorte').getFullList<Veranstaltungsort>({ sort: 'name', filter: `verein_id = "${vid}" && aktiv = true` }),
|
||||
pb.collection('ort_ausfaelle').getFullList<OrtAusfall>({ sort: 'von' }),
|
||||
api.get<Veranstaltungsort[]>('/veranstaltungsorte', { sort: 'name', aktiv: 'true' }),
|
||||
api.get<OrtAusfall[]>('/ort_ausfaelle', { sort: 'von' }),
|
||||
]);
|
||||
loading = false;
|
||||
});
|
||||
|
|
@ -110,9 +111,7 @@
|
|||
}
|
||||
|
||||
function generiereTerminDaten(beginn: Date, rruleStr: string | null) {
|
||||
const vid = pb.authStore.record?.verein_id as string;
|
||||
return {
|
||||
verein_id: vid,
|
||||
titel: fTitel.trim(),
|
||||
beschreibung: fBeschr.trim() || null,
|
||||
ort: fOrtId ? null : (fOrt.trim() || null),
|
||||
|
|
@ -146,7 +145,7 @@
|
|||
|
||||
const neu = await Promise.all(
|
||||
dates.map(d =>
|
||||
pb.collection('termine').create<Termin>({
|
||||
api.post<Termin>('/termine', {
|
||||
...generiereTerminDaten(d, rruleStr),
|
||||
beginn: d.toISOString(),
|
||||
ende: fEnde ? new Date(d.getTime() + dauer).toISOString() : null,
|
||||
|
|
@ -162,10 +161,10 @@
|
|||
ende: fEnde ? fromLocal(fEnde) : null,
|
||||
};
|
||||
if (editId) {
|
||||
const updated = await pb.collection('termine').update<Termin>(editId, data);
|
||||
const updated = await api.put<Termin>('/termine/' + editId, data);
|
||||
termine = termine.map(t => t.id === editId ? updated : t);
|
||||
} else {
|
||||
const neu = await pb.collection('termine').create<Termin>(data);
|
||||
const neu = await api.post<Termin>('/termine', data);
|
||||
termine = [...termine, neu];
|
||||
}
|
||||
}
|
||||
|
|
@ -180,10 +179,10 @@
|
|||
async function loeschen(t: Termin, ganzeSerieLoeschen: boolean) {
|
||||
if (ganzeSerieLoeschen && t.serie_id) {
|
||||
const serie = termine.filter(x => x.serie_id === t.serie_id);
|
||||
await Promise.all(serie.map(s => pb.collection('termine').delete(s.id)));
|
||||
await Promise.all(serie.map(s => api.del('/termine/' + s.id)));
|
||||
termine = termine.filter(x => x.serie_id !== t.serie_id);
|
||||
} else {
|
||||
await pb.collection('termine').delete(t.id);
|
||||
await api.del('/termine/' + t.id);
|
||||
termine = termine.filter(x => x.id !== t.id);
|
||||
}
|
||||
showDelete = null;
|
||||
|
|
@ -202,7 +201,7 @@
|
|||
});
|
||||
|
||||
async function setVerfuegbarkeit(t: Termin, v: Verfuegbarkeit) {
|
||||
const updated = await pb.collection('termine').update<Termin>(t.id, { verfuegbarkeit: v });
|
||||
const updated = await api.put<Termin>('/termine/' + t.id, { verfuegbarkeit: v });
|
||||
termine = termine.map(x => x.id === t.id ? updated : x);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { pb } from '$lib/pb';
|
||||
import { get } from 'svelte/store';
|
||||
import { user } from '$lib/user';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
onMount(() => {
|
||||
if (pb.authStore.isValid) {
|
||||
if (!!get(user)) {
|
||||
goto('/');
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { user } from '$lib/user';
|
||||
import type { AppUser } from '$lib/user';
|
||||
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
|
|
@ -11,7 +13,8 @@
|
|||
error = '';
|
||||
loading = true;
|
||||
try {
|
||||
await pb.collection('users').authWithPassword(email, password);
|
||||
const u = await api.post<AppUser & { token: string }>('/auth/login', { email, password });
|
||||
user.set(u);
|
||||
goto('/');
|
||||
} catch {
|
||||
error = 'E-Mail oder Passwort falsch.';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { pb } from '$lib/pb';
|
||||
import { api } from '$lib/api';
|
||||
import { user } from '$lib/user';
|
||||
|
||||
let vereinsname = $state('');
|
||||
let email = $state('');
|
||||
|
|
@ -17,8 +18,8 @@
|
|||
}
|
||||
loading = true;
|
||||
try {
|
||||
await pb.collection('users').create({ email, password, passwordConfirm, name: vereinsname });
|
||||
await pb.collection('users').authWithPassword(email, password);
|
||||
const u = await api.post('/auth/register', { vereinName: vereinsname, email, password, name: vereinsname });
|
||||
user.set(u as any);
|
||||
goto('/onboarding');
|
||||
} catch (e: unknown) {
|
||||
error = e instanceof Error ? e.message : 'Registrierung fehlgeschlagen.';
|
||||
|
|
|
|||
18
app/src/routes/api/auth/login/+server.ts
Normal file
18
app/src/routes/api/auth/login/+server.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb } from '$lib/server/db';
|
||||
import { checkPassword, signJwt } from '$lib/server/auth';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const { email, password } = await request.json();
|
||||
if (!email || !password) throw error(400, 'E-Mail und Passwort erforderlich');
|
||||
|
||||
const db = getDb();
|
||||
const u = db.prepare('SELECT * FROM users WHERE email = ?').get(email.toLowerCase()) as any;
|
||||
if (!u || !(await checkPassword(password, u.password_hash))) throw error(401, 'Ungültige Zugangsdaten');
|
||||
|
||||
const token = await signJwt({
|
||||
sub: u.id, verein_id: u.verein_id, rolle: u.rolle, name: u.name, email: u.email
|
||||
});
|
||||
|
||||
return json({ token, id: u.id, verein_id: u.verein_id, rolle: u.rolle, name: u.name, email: u.email });
|
||||
}
|
||||
11
app/src/routes/api/auth/me/+server.ts
Normal file
11
app/src/routes/api/auth/me/+server.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
import { getDb } from '$lib/server/db';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const row = db.prepare('SELECT id, verein_id, email, name, rolle FROM users WHERE id = ?').get(u.sub) as any;
|
||||
if (!row) return new Response(null, { status: 401 });
|
||||
return json(row);
|
||||
}
|
||||
24
app/src/routes/api/auth/register/+server.ts
Normal file
24
app/src/routes/api/auth/register/+server.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId } from '$lib/server/db';
|
||||
import { hashPassword, signJwt } from '$lib/server/auth';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const { vereinName, email, password, name } = await request.json();
|
||||
if (!vereinName || !email || !password) throw error(400, 'Pflichtfelder fehlen');
|
||||
if (password.length < 8) throw error(400, 'Passwort mindestens 8 Zeichen');
|
||||
|
||||
const db = getDb();
|
||||
const existing = db.prepare('SELECT id FROM users WHERE email = ?').get(email.toLowerCase());
|
||||
if (existing) throw error(409, 'E-Mail bereits registriert');
|
||||
|
||||
const vereinId = newId();
|
||||
const userId = newId();
|
||||
const hash = await hashPassword(password);
|
||||
|
||||
db.prepare('INSERT INTO vereine (id, name) VALUES (?, ?)').run(vereinId, vereinName);
|
||||
db.prepare('INSERT INTO users (id, verein_id, email, password_hash, name, rolle) VALUES (?, ?, ?, ?, ?, NULL)')
|
||||
.run(userId, vereinId, email.toLowerCase(), hash, name || email.split('@')[0]);
|
||||
|
||||
const token = await signJwt({ sub: userId, verein_id: vereinId, rolle: null, name: name || email.split('@')[0], email: email.toLowerCase() });
|
||||
return json({ token, id: userId, verein_id: vereinId, rolle: null, name: name || email.split('@')[0], email: email.toLowerCase() }, { status: 201 });
|
||||
}
|
||||
37
app/src/routes/api/beitraege/+server.ts
Normal file
37
app/src/routes/api/beitraege/+server.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows, row } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const items = db.prepare(
|
||||
'SELECT * FROM beitraege WHERE verein_id = ? ORDER BY name'
|
||||
).all(u.verein_id);
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.name || body.betrag == null) throw error(400, 'Name und Betrag sind Pflichtfelder');
|
||||
|
||||
const id = newId();
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO beitraege (id, verein_id, name, betrag, rhythmus, beschreibung)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
u.verein_id,
|
||||
body.name,
|
||||
body.betrag,
|
||||
body.rhythmus ?? 'jaehrlich',
|
||||
body.beschreibung ?? null
|
||||
);
|
||||
|
||||
const beitrag = db.prepare('SELECT * FROM beitraege WHERE id = ?').get(id);
|
||||
return json(row(beitrag as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
52
app/src/routes/api/beitraege/[id]/+server.ts
Normal file
52
app/src/routes/api/beitraege/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, row } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const beitrag = db.prepare(
|
||||
'SELECT * FROM beitraege WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!beitrag) throw error(404, 'Beitrag nicht gefunden');
|
||||
return json(row(beitrag as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function PUT({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const existing = db.prepare(
|
||||
'SELECT id FROM beitraege WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!existing) throw error(404, 'Beitrag nicht gefunden');
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
db.prepare(`
|
||||
UPDATE beitraege SET
|
||||
name = ?, betrag = ?, rhythmus = ?, beschreibung = ?,
|
||||
updated = strftime('%Y-%m-%dT%H:%M:%SZ','now')
|
||||
WHERE id = ? AND verein_id = ?
|
||||
`).run(
|
||||
body.name,
|
||||
body.betrag,
|
||||
body.rhythmus ?? 'jaehrlich',
|
||||
body.beschreibung ?? null,
|
||||
params.id,
|
||||
u.verein_id
|
||||
);
|
||||
|
||||
const beitrag = db.prepare('SELECT * FROM beitraege WHERE id = ?').get(params.id);
|
||||
return json(row(beitrag as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function DELETE({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const result = db.prepare(
|
||||
'DELETE FROM beitraege WHERE id = ? AND verein_id = ?'
|
||||
).run(params.id, u.verein_id);
|
||||
if (result.changes === 0) throw error(404, 'Beitrag nicht gefunden');
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
23
app/src/routes/api/einladungen/+server.ts
Normal file
23
app/src/routes/api/einladungen/+server.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, row } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
const id = newId();
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO einladungen (id, verein_id, rolle)
|
||||
VALUES (?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
u.verein_id,
|
||||
body.rolle ?? 'trainer'
|
||||
);
|
||||
|
||||
const einladung = db.prepare('SELECT * FROM einladungen WHERE id = ?').get(id);
|
||||
return json(row(einladung as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
62
app/src/routes/api/einladungen/[token]/+server.ts
Normal file
62
app/src/routes/api/einladungen/[token]/+server.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, row } from '$lib/server/db';
|
||||
import { hashPassword, signJwt } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ params }) {
|
||||
const db = getDb();
|
||||
|
||||
const einladung = db.prepare(`
|
||||
SELECT e.*, v.name as vereinName
|
||||
FROM einladungen e JOIN vereine v ON v.id = e.verein_id
|
||||
WHERE e.token = ? AND e.genutzt = 0
|
||||
`).get(params.token);
|
||||
|
||||
if (!einladung) throw error(404, 'Einladung nicht gefunden oder bereits verwendet');
|
||||
|
||||
return json(row(einladung as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function POST({ request, params }) {
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.email || !body.password || !body.name) throw error(400, 'E-Mail, Passwort und Name sind Pflichtfelder');
|
||||
if (body.password.length < 8) throw error(400, 'Passwort mindestens 8 Zeichen');
|
||||
|
||||
const einladung = db.prepare(
|
||||
'SELECT * FROM einladungen WHERE token = ? AND genutzt = 0'
|
||||
).get(params.token) as Record<string, unknown> | undefined;
|
||||
|
||||
if (!einladung) throw error(404, 'Einladung nicht gefunden oder bereits verwendet');
|
||||
|
||||
const verein_id = einladung.verein_id as string;
|
||||
const rolle = einladung.rolle as string;
|
||||
|
||||
const existing = db.prepare('SELECT id FROM users WHERE email = ?').get(body.email.toLowerCase());
|
||||
if (existing) throw error(409, 'E-Mail bereits registriert');
|
||||
|
||||
const userId = newId();
|
||||
const hash = await hashPassword(body.password);
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO users (id, verein_id, email, password_hash, name, rolle)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(userId, verein_id, body.email.toLowerCase(), hash, body.name, rolle);
|
||||
|
||||
db.prepare(
|
||||
`UPDATE einladungen SET genutzt = 1, updated = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE token = ?`
|
||||
).run(params.token);
|
||||
|
||||
const token = await signJwt({
|
||||
sub: userId,
|
||||
verein_id,
|
||||
rolle,
|
||||
name: body.name,
|
||||
email: body.email.toLowerCase()
|
||||
});
|
||||
|
||||
return json(
|
||||
{ token, id: userId, verein_id, rolle, name: body.name, email: body.email.toLowerCase() },
|
||||
{ status: 201 }
|
||||
);
|
||||
}
|
||||
58
app/src/routes/api/einzuege/+server.ts
Normal file
58
app/src/routes/api/einzuege/+server.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const items = db.prepare(`
|
||||
SELECT e.*, m.vorname, m.nachname, b.name as beitrag_name
|
||||
FROM einzuege e
|
||||
JOIN mitglieder m ON m.id = e.mitglied_id
|
||||
JOIN beitraege b ON b.id = e.beitrag_id
|
||||
WHERE e.verein_id = ?
|
||||
ORDER BY e.faellig_am
|
||||
`).all(u.verein_id);
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
const einzuege = Array.isArray(body) ? body : [body];
|
||||
if (einzuege.length === 0) throw error(400, 'Keine Einzüge angegeben');
|
||||
|
||||
const insert = db.prepare(`
|
||||
INSERT INTO einzuege (id, verein_id, mitglied_id, beitrag_id, betrag, faellig_am, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const insertMany = db.transaction((items: typeof einzuege) => {
|
||||
for (const e of items) {
|
||||
insert.run(
|
||||
newId(),
|
||||
u.verein_id,
|
||||
e.mitglied_id,
|
||||
e.beitrag_id,
|
||||
e.betrag,
|
||||
e.faellig_am ?? null,
|
||||
e.status ?? 'ausstehend'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
insertMany(einzuege);
|
||||
|
||||
const created = db.prepare(`
|
||||
SELECT e.*, m.vorname, m.nachname, b.name as beitrag_name
|
||||
FROM einzuege e
|
||||
JOIN mitglieder m ON m.id = e.mitglied_id
|
||||
JOIN beitraege b ON b.id = e.beitrag_id
|
||||
WHERE e.verein_id = ?
|
||||
ORDER BY e.faellig_am
|
||||
`).all(u.verein_id);
|
||||
|
||||
return json(rows(created as Record<string, unknown>[]), { status: 201 });
|
||||
}
|
||||
26
app/src/routes/api/files/[...path]/+server.ts
Normal file
26
app/src/routes/api/files/[...path]/+server.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || './data/uploads';
|
||||
|
||||
export async function GET({ params }) {
|
||||
const filePath = join(UPLOAD_DIR, params.path);
|
||||
if (!existsSync(filePath)) return new Response(null, { status: 404 });
|
||||
const data = readFileSync(filePath);
|
||||
const ext = filePath.split('.').pop()?.toLowerCase() || '';
|
||||
const mime: Record<string, string> = {
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
gif: 'image/gif',
|
||||
webp: 'image/webp',
|
||||
mp4: 'video/mp4',
|
||||
mov: 'video/quicktime'
|
||||
};
|
||||
return new Response(data, {
|
||||
headers: {
|
||||
'Content-Type': mime[ext] || 'application/octet-stream',
|
||||
'Cache-Control': 'public, max-age=31536000'
|
||||
}
|
||||
});
|
||||
}
|
||||
36
app/src/routes/api/gruppen/+server.ts
Normal file
36
app/src/routes/api/gruppen/+server.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows, row, toArr } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const items = db.prepare(
|
||||
'SELECT * FROM gruppen WHERE verein_id = ? ORDER BY name'
|
||||
).all(u.verein_id);
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.name) throw error(400, 'Name ist ein Pflichtfeld');
|
||||
|
||||
const id = newId();
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO gruppen (id, verein_id, name, beschreibung, trainer_ids)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
u.verein_id,
|
||||
body.name,
|
||||
body.beschreibung ?? null,
|
||||
toArr(body.trainer_ids)
|
||||
);
|
||||
|
||||
const gruppe = db.prepare('SELECT * FROM gruppen WHERE id = ?').get(id);
|
||||
return json(row(gruppe as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
51
app/src/routes/api/gruppen/[id]/+server.ts
Normal file
51
app/src/routes/api/gruppen/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, row, toArr } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const gruppe = db.prepare(
|
||||
'SELECT * FROM gruppen WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!gruppe) throw error(404, 'Gruppe nicht gefunden');
|
||||
return json(row(gruppe as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function PUT({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const existing = db.prepare(
|
||||
'SELECT id FROM gruppen WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!existing) throw error(404, 'Gruppe nicht gefunden');
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
db.prepare(`
|
||||
UPDATE gruppen SET
|
||||
name = ?, beschreibung = ?, trainer_ids = ?,
|
||||
updated = strftime('%Y-%m-%dT%H:%M:%SZ','now')
|
||||
WHERE id = ? AND verein_id = ?
|
||||
`).run(
|
||||
body.name,
|
||||
body.beschreibung ?? null,
|
||||
toArr(body.trainer_ids),
|
||||
params.id,
|
||||
u.verein_id
|
||||
);
|
||||
|
||||
const gruppe = db.prepare('SELECT * FROM gruppen WHERE id = ?').get(params.id);
|
||||
return json(row(gruppe as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function DELETE({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const result = db.prepare(
|
||||
'DELETE FROM gruppen WHERE id = ? AND verein_id = ?'
|
||||
).run(params.id, u.verein_id);
|
||||
if (result.changes === 0) throw error(404, 'Gruppe nicht gefunden');
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
|
@ -1,29 +1,23 @@
|
|||
import ical from 'ical-generator';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
const PB_URL = () => env.PB_URL ?? 'http://localhost:8090';
|
||||
import { getDb } from '$lib/server/db';
|
||||
|
||||
export async function GET({ params }) {
|
||||
const { vereinId } = params;
|
||||
|
||||
// Verein laden (öffentlich lesbar via viewRule)
|
||||
const vereinRes = await fetch(
|
||||
`${PB_URL()}/api/collections/vereine/records/${vereinId}`,
|
||||
);
|
||||
if (!vereinRes.ok) {
|
||||
const db = getDb();
|
||||
|
||||
const verein = db.prepare('SELECT * FROM vereine WHERE id = ?').get(vereinId) as { name: string } | undefined;
|
||||
if (!verein) {
|
||||
return new Response('Verein nicht gefunden.', { status: 404 });
|
||||
}
|
||||
const verein = await vereinRes.json();
|
||||
|
||||
// Termine der nächsten 365 Tage laden
|
||||
const von = new Date().toISOString();
|
||||
const bis = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString();
|
||||
const filter = encodeURIComponent(`verein_id = "${vereinId}" && beginn >= "${von}" && beginn <= "${bis}"`);
|
||||
|
||||
const termineRes = await fetch(
|
||||
`${PB_URL()}/api/collections/termine/records?filter=${filter}&sort=beginn&perPage=500`,
|
||||
);
|
||||
const { items: termine = [] } = termineRes.ok ? await termineRes.json() : {};
|
||||
const termine = db.prepare(
|
||||
`SELECT * FROM termine WHERE verein_id = ? AND beginn >= ? AND beginn <= ? ORDER BY beginn LIMIT 500`
|
||||
).all(vereinId, von, bis) as { id: string; titel: string; beginn: string; ende: string | null; ort: string | null; beschreibung: string | null }[];
|
||||
|
||||
// iCal-Kalender aufbauen
|
||||
const cal = ical({
|
||||
|
|
|
|||
65
app/src/routes/api/mitglieder/+server.ts
Normal file
65
app/src/routes/api/mitglieder/+server.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows, row, toArr } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request, url }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const status = url.searchParams.get('status');
|
||||
|
||||
let items;
|
||||
if (status) {
|
||||
items = db.prepare(
|
||||
'SELECT * FROM mitglieder WHERE verein_id = ? AND status = ? ORDER BY nachname, vorname'
|
||||
).all(u.verein_id, status);
|
||||
} else {
|
||||
items = db.prepare(
|
||||
'SELECT * FROM mitglieder WHERE verein_id = ? ORDER BY nachname, vorname'
|
||||
).all(u.verein_id);
|
||||
}
|
||||
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.vorname || !body.nachname) throw error(400, 'Vorname und Nachname sind Pflichtfelder');
|
||||
|
||||
const id = newId();
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO mitglieder (
|
||||
id, verein_id, vorname, nachname, email, telefon,
|
||||
geburtsdatum, eintrittsdatum, austrittsdatum,
|
||||
strasse, plz, ort, iban, bic,
|
||||
gruppe_ids, status, notizen,
|
||||
mandatsreferenz, mandatsdatum
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
u.verein_id,
|
||||
body.vorname,
|
||||
body.nachname,
|
||||
body.email ?? null,
|
||||
body.telefon ?? null,
|
||||
body.geburtsdatum ?? null,
|
||||
body.eintrittsdatum ?? null,
|
||||
body.austrittsdatum ?? null,
|
||||
body.strasse ?? null,
|
||||
body.plz ?? null,
|
||||
body.ort ?? null,
|
||||
body.iban ?? null,
|
||||
body.bic ?? null,
|
||||
toArr(body.gruppe_ids),
|
||||
body.status ?? 'aktiv',
|
||||
body.notizen ?? null,
|
||||
body.mandatsreferenz ?? null,
|
||||
body.mandatsdatum ?? null
|
||||
);
|
||||
|
||||
const mitglied = db.prepare('SELECT * FROM mitglieder WHERE id = ?').get(id);
|
||||
return json(row(mitglied as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
69
app/src/routes/api/mitglieder/[id]/+server.ts
Normal file
69
app/src/routes/api/mitglieder/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, row, toArr } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const mitglied = db.prepare(
|
||||
'SELECT * FROM mitglieder WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!mitglied) throw error(404, 'Mitglied nicht gefunden');
|
||||
return json(row(mitglied as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function PUT({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const existing = db.prepare(
|
||||
'SELECT id FROM mitglieder WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!existing) throw error(404, 'Mitglied nicht gefunden');
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
db.prepare(`
|
||||
UPDATE mitglieder SET
|
||||
vorname = ?, nachname = ?, email = ?, telefon = ?,
|
||||
geburtsdatum = ?, eintrittsdatum = ?, austrittsdatum = ?,
|
||||
strasse = ?, plz = ?, ort = ?, iban = ?, bic = ?,
|
||||
gruppe_ids = ?, status = ?, notizen = ?,
|
||||
mandatsreferenz = ?, mandatsdatum = ?,
|
||||
updated = strftime('%Y-%m-%dT%H:%M:%SZ','now')
|
||||
WHERE id = ? AND verein_id = ?
|
||||
`).run(
|
||||
body.vorname,
|
||||
body.nachname,
|
||||
body.email ?? null,
|
||||
body.telefon ?? null,
|
||||
body.geburtsdatum ?? null,
|
||||
body.eintrittsdatum ?? null,
|
||||
body.austrittsdatum ?? null,
|
||||
body.strasse ?? null,
|
||||
body.plz ?? null,
|
||||
body.ort ?? null,
|
||||
body.iban ?? null,
|
||||
body.bic ?? null,
|
||||
toArr(body.gruppe_ids),
|
||||
body.status ?? 'aktiv',
|
||||
body.notizen ?? null,
|
||||
body.mandatsreferenz ?? null,
|
||||
body.mandatsdatum ?? null,
|
||||
params.id,
|
||||
u.verein_id
|
||||
);
|
||||
|
||||
const mitglied = db.prepare('SELECT * FROM mitglieder WHERE id = ?').get(params.id);
|
||||
return json(row(mitglied as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function DELETE({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const result = db.prepare(
|
||||
'DELETE FROM mitglieder WHERE id = ? AND verein_id = ?'
|
||||
).run(params.id, u.verein_id);
|
||||
if (result.changes === 0) throw error(404, 'Mitglied nicht gefunden');
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
39
app/src/routes/api/nachrichten/+server.ts
Normal file
39
app/src/routes/api/nachrichten/+server.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows, row, toArr } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const items = db.prepare(
|
||||
'SELECT * FROM nachrichten WHERE verein_id = ? ORDER BY gesendet_am DESC, created DESC'
|
||||
).all(u.verein_id);
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.betreff) throw error(400, 'Betreff ist ein Pflichtfeld');
|
||||
|
||||
const id = newId();
|
||||
const gesendet_am = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO nachrichten (id, verein_id, autor_id, betreff, text, gruppe_ids, gesendet_am)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
u.verein_id,
|
||||
u.sub,
|
||||
body.betreff,
|
||||
body.text ?? '',
|
||||
toArr(body.gruppe_ids),
|
||||
gesendet_am
|
||||
);
|
||||
|
||||
const nachricht = db.prepare('SELECT * FROM nachrichten WHERE id = ?').get(id);
|
||||
return json(row(nachricht as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
13
app/src/routes/api/nachrichten/[id]/+server.ts
Normal file
13
app/src/routes/api/nachrichten/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import { getDb } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function DELETE({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const result = db.prepare(
|
||||
'DELETE FROM nachrichten WHERE id = ? AND verein_id = ?'
|
||||
).run(params.id, u.verein_id);
|
||||
if (result.changes === 0) throw error(404, 'Nachricht nicht gefunden');
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
68
app/src/routes/api/neuigkeiten/+server.ts
Normal file
68
app/src/routes/api/neuigkeiten/+server.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows, row } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
import { writeFileSync, mkdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || './data/uploads';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const items = db.prepare(
|
||||
'SELECT * FROM neuigkeiten WHERE verein_id = ? ORDER BY created DESC'
|
||||
).all(u.verein_id);
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
const text = formData.get('text') as string | null;
|
||||
const gruppeIdsRaw = formData.get('gruppe_ids') as string | null;
|
||||
const terminId = formData.get('termin_id') as string | null;
|
||||
|
||||
let gruppe_ids: string[] = [];
|
||||
if (gruppeIdsRaw) {
|
||||
try { gruppe_ids = JSON.parse(gruppeIdsRaw); } catch { gruppe_ids = []; }
|
||||
}
|
||||
|
||||
const id = newId();
|
||||
const uploadPath = join(UPLOAD_DIR, u.verein_id, id);
|
||||
const medien: string[] = [];
|
||||
|
||||
const files = formData.getAll('medien') as File[];
|
||||
if (files.length > 0) {
|
||||
mkdirSync(uploadPath, { recursive: true });
|
||||
for (const file of files) {
|
||||
if (!(file instanceof File)) continue;
|
||||
const ext = file.name.split('.').pop() || 'bin';
|
||||
const filename = `${newId()}.${ext}`;
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
writeFileSync(join(uploadPath, filename), buffer);
|
||||
medien.push(filename);
|
||||
}
|
||||
}
|
||||
|
||||
if (!text && medien.length === 0) throw error(400, 'Text oder Medien sind erforderlich');
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO neuigkeiten (id, verein_id, autor_id, autor_name, text, medien, gruppe_ids, termin_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
u.verein_id,
|
||||
u.sub,
|
||||
u.name,
|
||||
text ?? null,
|
||||
JSON.stringify(medien),
|
||||
JSON.stringify(gruppe_ids),
|
||||
terminId ?? null
|
||||
);
|
||||
|
||||
const neuigkeit = db.prepare('SELECT * FROM neuigkeiten WHERE id = ?').get(id);
|
||||
return json(row(neuigkeit as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
27
app/src/routes/api/neuigkeiten/[id]/+server.ts
Normal file
27
app/src/routes/api/neuigkeiten/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import { getDb, parseArr } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
import { rmSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || './data/uploads';
|
||||
|
||||
export async function DELETE({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const neuigkeit = db.prepare(
|
||||
'SELECT * FROM neuigkeiten WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id) as Record<string, unknown> | undefined;
|
||||
|
||||
if (!neuigkeit) throw error(404, 'Neuigkeit nicht gefunden');
|
||||
|
||||
const uploadPath = join(UPLOAD_DIR, u.verein_id, params.id);
|
||||
if (existsSync(uploadPath)) {
|
||||
rmSync(uploadPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
db.prepare('DELETE FROM neuigkeiten WHERE id = ? AND verein_id = ?').run(params.id, u.verein_id);
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
58
app/src/routes/api/ort-ausfaelle/+server.ts
Normal file
58
app/src/routes/api/ort-ausfaelle/+server.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows, row } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request, url }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const ort_id = url.searchParams.get('ort_id');
|
||||
|
||||
let items;
|
||||
if (ort_id) {
|
||||
items = db.prepare(`
|
||||
SELECT a.* FROM ort_ausfaelle a
|
||||
JOIN veranstaltungsorte o ON o.id = a.ort_id
|
||||
WHERE a.ort_id = ? AND o.verein_id = ?
|
||||
ORDER BY a.von
|
||||
`).all(ort_id, u.verein_id);
|
||||
} else {
|
||||
items = db.prepare(`
|
||||
SELECT a.* FROM ort_ausfaelle a
|
||||
JOIN veranstaltungsorte o ON o.id = a.ort_id
|
||||
WHERE o.verein_id = ?
|
||||
ORDER BY a.von
|
||||
`).all(u.verein_id);
|
||||
}
|
||||
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.ort_id || !body.von || !body.bis) throw error(400, 'ort_id, von und bis sind Pflichtfelder');
|
||||
|
||||
const ort = db.prepare(
|
||||
'SELECT id FROM veranstaltungsorte WHERE id = ? AND verein_id = ?'
|
||||
).get(body.ort_id, u.verein_id);
|
||||
if (!ort) throw error(404, 'Ort nicht gefunden');
|
||||
|
||||
const id = newId();
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO ort_ausfaelle (id, ort_id, von, bis, grund)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
body.ort_id,
|
||||
body.von,
|
||||
body.bis,
|
||||
body.grund ?? null
|
||||
);
|
||||
|
||||
const ausfall = db.prepare('SELECT * FROM ort_ausfaelle WHERE id = ?').get(id);
|
||||
return json(row(ausfall as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
21
app/src/routes/api/ort-ausfaelle/[id]/+server.ts
Normal file
21
app/src/routes/api/ort-ausfaelle/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import { getDb } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function DELETE({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
// Verify the ausfall belongs to an ort in the user's Verein
|
||||
const ausfall = db.prepare(`
|
||||
SELECT a.id FROM ort_ausfaelle a
|
||||
JOIN veranstaltungsorte o ON o.id = a.ort_id
|
||||
WHERE a.id = ? AND o.verein_id = ?
|
||||
`).get(params.id, u.verein_id);
|
||||
|
||||
if (!ausfall) throw error(404, 'Ausfall nicht gefunden');
|
||||
|
||||
db.prepare('DELETE FROM ort_ausfaelle WHERE id = ?').run(params.id);
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
37
app/src/routes/api/orte/+server.ts
Normal file
37
app/src/routes/api/orte/+server.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows, row } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const items = db.prepare(
|
||||
'SELECT * FROM veranstaltungsorte WHERE verein_id = ? ORDER BY name'
|
||||
).all(u.verein_id);
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.name) throw error(400, 'Name ist ein Pflichtfeld');
|
||||
|
||||
const id = newId();
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO veranstaltungsorte (id, verein_id, name, adresse, typ, aktiv)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
u.verein_id,
|
||||
body.name,
|
||||
body.adresse ?? null,
|
||||
body.typ ?? 'sonstiges',
|
||||
body.aktiv !== false ? 1 : 0
|
||||
);
|
||||
|
||||
const ort = db.prepare('SELECT * FROM veranstaltungsorte WHERE id = ?').get(id);
|
||||
return json(row(ort as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
52
app/src/routes/api/orte/[id]/+server.ts
Normal file
52
app/src/routes/api/orte/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, row } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const ort = db.prepare(
|
||||
'SELECT * FROM veranstaltungsorte WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!ort) throw error(404, 'Ort nicht gefunden');
|
||||
return json(row(ort as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function PUT({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const existing = db.prepare(
|
||||
'SELECT id FROM veranstaltungsorte WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!existing) throw error(404, 'Ort nicht gefunden');
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
db.prepare(`
|
||||
UPDATE veranstaltungsorte SET
|
||||
name = ?, adresse = ?, typ = ?, aktiv = ?,
|
||||
updated = strftime('%Y-%m-%dT%H:%M:%SZ','now')
|
||||
WHERE id = ? AND verein_id = ?
|
||||
`).run(
|
||||
body.name,
|
||||
body.adresse ?? null,
|
||||
body.typ ?? 'sonstiges',
|
||||
body.aktiv !== false ? 1 : 0,
|
||||
params.id,
|
||||
u.verein_id
|
||||
);
|
||||
|
||||
const ort = db.prepare('SELECT * FROM veranstaltungsorte WHERE id = ?').get(params.id);
|
||||
return json(row(ort as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function DELETE({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const result = db.prepare(
|
||||
'DELETE FROM veranstaltungsorte WHERE id = ? AND verein_id = ?'
|
||||
).run(params.id, u.verein_id);
|
||||
if (result.changes === 0) throw error(404, 'Ort nicht gefunden');
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import webpush from 'web-push';
|
||||
|
||||
const PB_URL = () => env.PB_URL ?? 'http://localhost:8090';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
import { getDb } from '$lib/server/db';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const authHeader = request.headers.get('Authorization') ?? '';
|
||||
const authUser = await requireAuth(request).catch(() => null);
|
||||
if (!authUser) return json({ error: 'Nicht authentifiziert.' }, { status: 401 });
|
||||
|
||||
const { titel, body, url = '/nachrichten' } = await request.json();
|
||||
|
||||
if (!titel) return json({ error: 'Titel fehlt.' }, { status: 400 });
|
||||
|
|
@ -20,21 +22,18 @@ export async function POST({ request }) {
|
|||
|
||||
webpush.setVapidDetails(vapidSubject, vapidPublic, vapidPrivate);
|
||||
|
||||
// Alle Push-Subscriptions des Vereins laden (listRule erlaubt verein-weite Abfrage)
|
||||
const subRes = await fetch(
|
||||
`${PB_URL()}/api/collections/push_subscriptions/records?perPage=500`,
|
||||
{ headers: { Authorization: authHeader } },
|
||||
);
|
||||
const db = getDb();
|
||||
const items = db.prepare(
|
||||
'SELECT * FROM push_subscriptions WHERE verein_id = ?'
|
||||
).all(authUser.verein_id) as { endpoint: string; p256dh: string; auth: string; id: string }[];
|
||||
|
||||
if (!subRes.ok) return json({ sent: 0 });
|
||||
const { items } = await subRes.json();
|
||||
if (!items?.length) return json({ sent: 0 });
|
||||
if (!items.length) return json({ sent: 0 });
|
||||
|
||||
const payload = JSON.stringify({ title: titel, body, url });
|
||||
let sent = 0;
|
||||
|
||||
await Promise.allSettled(
|
||||
items.map(async (sub: { endpoint: string; p256dh: string; auth: string; id: string }) => {
|
||||
items.map(async (sub) => {
|
||||
try {
|
||||
await webpush.sendNotification(
|
||||
{ endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } },
|
||||
|
|
@ -44,10 +43,7 @@ export async function POST({ request }) {
|
|||
} catch (err: unknown) {
|
||||
// 410 Gone = Subscription abgelaufen → löschen
|
||||
if ((err as { statusCode?: number }).statusCode === 410) {
|
||||
await fetch(
|
||||
`${PB_URL()}/api/collections/push_subscriptions/records/${sub.id}`,
|
||||
{ method: 'DELETE', headers: { Authorization: authHeader } },
|
||||
).catch(() => {});
|
||||
db.prepare('DELETE FROM push_subscriptions WHERE id = ?').run(sub.id);
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,67 +1,44 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
const PB_URL = () => env.PB_URL ?? 'http://localhost:8090';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
import { getDb, newId } from '$lib/server/db';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const authHeader = request.headers.get('Authorization') ?? '';
|
||||
const { subscription, userId } = await request.json();
|
||||
const authUser = await requireAuth(request).catch(() => null);
|
||||
if (!authUser) return json({ error: 'Nicht authentifiziert.' }, { status: 401 });
|
||||
|
||||
if (!subscription?.endpoint || !userId) {
|
||||
const { subscription } = await request.json();
|
||||
|
||||
if (!subscription?.endpoint) {
|
||||
return json({ error: 'Ungültige Subscription.' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Alte Subscription dieses Users löschen (Gerätewechsel)
|
||||
const listRes = await fetch(
|
||||
`${PB_URL()}/api/collections/push_subscriptions/records?filter=user_id%3D"${userId}"&perPage=50`,
|
||||
{ headers: { Authorization: authHeader } },
|
||||
);
|
||||
if (listRes.ok) {
|
||||
const { items } = await listRes.json();
|
||||
await Promise.all(
|
||||
(items ?? []).map((r: { id: string }) =>
|
||||
fetch(`${PB_URL()}/api/collections/push_subscriptions/records/${r.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: authHeader },
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
const db = getDb();
|
||||
|
||||
// Alte Subscriptions dieses Users löschen (Gerätewechsel)
|
||||
db.prepare('DELETE FROM push_subscriptions WHERE user_id = ?').run(authUser.sub);
|
||||
|
||||
// Neue Subscription speichern
|
||||
const res = await fetch(`${PB_URL()}/api/collections/push_subscriptions/records`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: authHeader },
|
||||
body: JSON.stringify({
|
||||
user_id: userId,
|
||||
endpoint: subscription.endpoint,
|
||||
p256dh: subscription.keys.p256dh,
|
||||
auth: subscription.keys.auth,
|
||||
}),
|
||||
});
|
||||
db.prepare(`
|
||||
INSERT INTO push_subscriptions (id, user_id, verein_id, endpoint, p256dh, auth)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
newId(),
|
||||
authUser.sub,
|
||||
authUser.verein_id,
|
||||
subscription.endpoint,
|
||||
subscription.keys.p256dh,
|
||||
subscription.keys.auth,
|
||||
);
|
||||
|
||||
if (!res.ok) return json({ error: 'Fehler beim Speichern.' }, { status: 500 });
|
||||
return json({ success: true });
|
||||
}
|
||||
|
||||
export async function DELETE({ request }) {
|
||||
const authHeader = request.headers.get('Authorization') ?? '';
|
||||
const { userId } = await request.json();
|
||||
const authUser = await requireAuth(request).catch(() => null);
|
||||
if (!authUser) return json({ success: true });
|
||||
|
||||
const listRes = await fetch(
|
||||
`${PB_URL()}/api/collections/push_subscriptions/records?filter=user_id%3D"${userId}"&perPage=50`,
|
||||
{ headers: { Authorization: authHeader } },
|
||||
);
|
||||
if (!listRes.ok) return json({ success: true });
|
||||
const db = getDb();
|
||||
db.prepare('DELETE FROM push_subscriptions WHERE user_id = ?').run(authUser.sub);
|
||||
|
||||
const { items } = await listRes.json();
|
||||
await Promise.all(
|
||||
(items ?? []).map((r: { id: string }) =>
|
||||
fetch(`${PB_URL()}/api/collections/push_subscriptions/records/${r.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: authHeader },
|
||||
}),
|
||||
),
|
||||
);
|
||||
return json({ success: true });
|
||||
}
|
||||
|
|
|
|||
58
app/src/routes/api/reaktionen/+server.ts
Normal file
58
app/src/routes/api/reaktionen/+server.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows, row } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request, url }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const beitrag_id = url.searchParams.get('beitrag_id');
|
||||
|
||||
let items;
|
||||
if (beitrag_id) {
|
||||
items = db.prepare(`
|
||||
SELECT r.* FROM reaktionen r
|
||||
JOIN neuigkeiten n ON n.id = r.beitrag_id
|
||||
WHERE r.beitrag_id = ? AND n.verein_id = ?
|
||||
ORDER BY r.created
|
||||
`).all(beitrag_id, u.verein_id);
|
||||
} else {
|
||||
items = db.prepare(`
|
||||
SELECT r.* FROM reaktionen r
|
||||
JOIN neuigkeiten n ON n.id = r.beitrag_id
|
||||
WHERE n.verein_id = ?
|
||||
ORDER BY r.created
|
||||
`).all(u.verein_id);
|
||||
}
|
||||
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.beitrag_id) throw error(400, 'beitrag_id ist erforderlich');
|
||||
|
||||
const beitrag = db.prepare(
|
||||
'SELECT id FROM neuigkeiten WHERE id = ? AND verein_id = ?'
|
||||
).get(body.beitrag_id, u.verein_id);
|
||||
if (!beitrag) throw error(404, 'Beitrag nicht gefunden');
|
||||
|
||||
const id = newId();
|
||||
|
||||
try {
|
||||
db.prepare(`
|
||||
INSERT INTO reaktionen (id, beitrag_id, user_id)
|
||||
VALUES (?, ?, ?)
|
||||
`).run(id, body.beitrag_id, u.sub);
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
if (msg.includes('UNIQUE')) throw error(409, 'Reaktion bereits vorhanden');
|
||||
throw e;
|
||||
}
|
||||
|
||||
const reaktion = db.prepare('SELECT * FROM reaktionen WHERE id = ?').get(id);
|
||||
return json(row(reaktion as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
13
app/src/routes/api/reaktionen/[id]/+server.ts
Normal file
13
app/src/routes/api/reaktionen/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import { getDb } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function DELETE({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const result = db.prepare(
|
||||
'DELETE FROM reaktionen WHERE id = ? AND user_id = ?'
|
||||
).run(params.id, u.sub);
|
||||
if (result.changes === 0) throw error(404, 'Reaktion nicht gefunden oder keine Berechtigung');
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
63
app/src/routes/api/termine/+server.ts
Normal file
63
app/src/routes/api/termine/+server.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, newId, rows, row, toArr } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request, url }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const von = url.searchParams.get('von');
|
||||
const bis = url.searchParams.get('bis');
|
||||
|
||||
let query = 'SELECT * FROM termine WHERE verein_id = ?';
|
||||
const params: unknown[] = [u.verein_id];
|
||||
|
||||
if (von) {
|
||||
query += ' AND beginn >= ?';
|
||||
params.push(von);
|
||||
}
|
||||
if (bis) {
|
||||
query += ' AND beginn <= ?';
|
||||
params.push(bis);
|
||||
}
|
||||
|
||||
query += ' ORDER BY beginn';
|
||||
|
||||
const items = db.prepare(query).all(...params);
|
||||
return json(rows(items as Record<string, unknown>[]));
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.titel || !body.beginn) throw error(400, 'Titel und Beginn sind Pflichtfelder');
|
||||
|
||||
const id = newId();
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO termine (
|
||||
id, verein_id, titel, beschreibung, beginn, ende,
|
||||
ort, ort_id, gruppe_ids, durchfuehrender_id,
|
||||
verfuegbarkeit, rrule, serie_id
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
u.verein_id,
|
||||
body.titel,
|
||||
body.beschreibung ?? null,
|
||||
body.beginn,
|
||||
body.ende ?? null,
|
||||
body.ort ?? null,
|
||||
body.ort_id ?? null,
|
||||
toArr(body.gruppe_ids),
|
||||
body.durchfuehrender_id ?? null,
|
||||
body.verfuegbarkeit ?? 'offen',
|
||||
body.rrule ?? null,
|
||||
body.serie_id ?? null
|
||||
);
|
||||
|
||||
const termin = db.prepare('SELECT * FROM termine WHERE id = ?').get(id);
|
||||
return json(row(termin as Record<string, unknown>), { status: 201 });
|
||||
}
|
||||
83
app/src/routes/api/termine/[id]/+server.ts
Normal file
83
app/src/routes/api/termine/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, row, toArr } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const termin = db.prepare(
|
||||
'SELECT * FROM termine WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!termin) throw error(404, 'Termin nicht gefunden');
|
||||
return json(row(termin as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function PUT({ request, params }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const existing = db.prepare(
|
||||
'SELECT id FROM termine WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id);
|
||||
if (!existing) throw error(404, 'Termin nicht gefunden');
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
db.prepare(`
|
||||
UPDATE termine SET
|
||||
titel = ?, beschreibung = ?, beginn = ?, ende = ?,
|
||||
ort = ?, ort_id = ?, gruppe_ids = ?, durchfuehrender_id = ?,
|
||||
verfuegbarkeit = ?, rrule = ?, serie_id = ?,
|
||||
updated = strftime('%Y-%m-%dT%H:%M:%SZ','now')
|
||||
WHERE id = ? AND verein_id = ?
|
||||
`).run(
|
||||
body.titel,
|
||||
body.beschreibung ?? null,
|
||||
body.beginn,
|
||||
body.ende ?? null,
|
||||
body.ort ?? null,
|
||||
body.ort_id ?? null,
|
||||
toArr(body.gruppe_ids),
|
||||
body.durchfuehrender_id ?? null,
|
||||
body.verfuegbarkeit ?? 'offen',
|
||||
body.rrule ?? null,
|
||||
body.serie_id ?? null,
|
||||
params.id,
|
||||
u.verein_id
|
||||
);
|
||||
|
||||
const termin = db.prepare('SELECT * FROM termine WHERE id = ?').get(params.id);
|
||||
return json(row(termin as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function DELETE({ request, params, url }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
|
||||
const deleteSerie = url.searchParams.get('serie') === 'true';
|
||||
|
||||
if (deleteSerie) {
|
||||
const termin = db.prepare(
|
||||
'SELECT serie_id FROM termine WHERE id = ? AND verein_id = ?'
|
||||
).get(params.id, u.verein_id) as { serie_id: string | null } | undefined;
|
||||
|
||||
if (!termin) throw error(404, 'Termin nicht gefunden');
|
||||
|
||||
if (termin.serie_id) {
|
||||
db.prepare(
|
||||
'DELETE FROM termine WHERE serie_id = ? AND verein_id = ?'
|
||||
).run(termin.serie_id, u.verein_id);
|
||||
} else {
|
||||
db.prepare(
|
||||
'DELETE FROM termine WHERE id = ? AND verein_id = ?'
|
||||
).run(params.id, u.verein_id);
|
||||
}
|
||||
} else {
|
||||
const result = db.prepare(
|
||||
'DELETE FROM termine WHERE id = ? AND verein_id = ?'
|
||||
).run(params.id, u.verein_id);
|
||||
if (result.changes === 0) throw error(404, 'Termin nicht gefunden');
|
||||
}
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
39
app/src/routes/api/vereine/+server.ts
Normal file
39
app/src/routes/api/vereine/+server.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb, row } from '$lib/server/db';
|
||||
import { requireAuth } from '$lib/server/auth';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const verein = db.prepare('SELECT * FROM vereine WHERE id = ?').get(u.verein_id);
|
||||
if (!verein) throw error(404, 'Verein nicht gefunden');
|
||||
return json(row(verein as Record<string, unknown>));
|
||||
}
|
||||
|
||||
export async function PATCH({ request }) {
|
||||
const u = await requireAuth(request);
|
||||
const db = getDb();
|
||||
const body = await request.json();
|
||||
|
||||
const allowed = [
|
||||
'name', 'adresse', 'plz', 'ort', 'bundesland',
|
||||
'email', 'telefon', 'website',
|
||||
'glaeubigerid', 'iban', 'bic',
|
||||
'dosb_mitglied'
|
||||
];
|
||||
|
||||
const fields = Object.keys(body).filter(k => allowed.includes(k));
|
||||
if (fields.length === 0) throw error(400, 'Keine gültigen Felder');
|
||||
|
||||
const sets = fields.map(k => `${k} = ?`).join(', ');
|
||||
const vals = fields.map(k => {
|
||||
if (k === 'dosb_mitglied') return body[k] ? 1 : 0;
|
||||
return body[k];
|
||||
});
|
||||
|
||||
db.prepare(`UPDATE vereine SET ${sets}, updated = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE id = ?`)
|
||||
.run(...vals, u.verein_id);
|
||||
|
||||
const verein = db.prepare('SELECT * FROM vereine WHERE id = ?').get(u.verein_id);
|
||||
return json(row(verein as Record<string, unknown>));
|
||||
}
|
||||
|
|
@ -2,7 +2,9 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { pb } from '$lib/pb';
|
||||
import { get } from 'svelte/store';
|
||||
import { api } from '$lib/api';
|
||||
import { user } from '$lib/user';
|
||||
import type { Einladung } from '$lib/types';
|
||||
|
||||
const token = $derived($page.params.token as string);
|
||||
|
|
@ -20,17 +22,14 @@
|
|||
let formError = $state('');
|
||||
|
||||
onMount(async () => {
|
||||
if (pb.authStore.isValid) {
|
||||
if (!!get(user)) {
|
||||
goto('/');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const inv = await pb.collection('einladungen')
|
||||
.getFirstListItem<Einladung>(`token = "${token}" && genutzt = false`, {
|
||||
expand: 'verein_id',
|
||||
});
|
||||
const inv = await api.get<Einladung & { vereinName: string }>('/einladungen/' + token);
|
||||
einladung = inv;
|
||||
vereinName = (inv as any).expand?.verein_id?.name ?? '';
|
||||
vereinName = (inv as any).vereinName ?? '';
|
||||
} catch {
|
||||
fehler = 'Dieser Einladungslink ist ungültig oder wurde bereits verwendet.';
|
||||
} finally {
|
||||
|
|
@ -43,16 +42,12 @@
|
|||
if (password !== passwordConfirm) { formError = 'Passwörter stimmen nicht überein.'; return; }
|
||||
formError = ''; saving = true;
|
||||
try {
|
||||
await pb.collection('users').create({
|
||||
const u = await api.post<any>(`/einladungen/${token}`, {
|
||||
email: email.trim(),
|
||||
password,
|
||||
passwordConfirm,
|
||||
name: name.trim(),
|
||||
verein_id: einladung.verein_id,
|
||||
rolle: einladung.rolle,
|
||||
});
|
||||
await pb.collection('users').authWithPassword(email.trim(), password);
|
||||
await pb.collection('einladungen').update(einladung.id, { genutzt: true });
|
||||
user.set(u);
|
||||
goto('/');
|
||||
} catch (e: unknown) {
|
||||
formError = e instanceof Error ? e.message : 'Registrierung fehlgeschlagen.';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { pb } from '$lib/pb';
|
||||
import { get } from 'svelte/store';
|
||||
import { api } from '$lib/api';
|
||||
import { user } from '$lib/user';
|
||||
|
||||
let schritt = $state(1);
|
||||
let loading = $state(false);
|
||||
|
|
@ -13,36 +15,31 @@
|
|||
let fertigName = $state('');
|
||||
|
||||
onMount(() => {
|
||||
if (!pb.authStore.isValid) {
|
||||
const u = get(user);
|
||||
if (!u) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
if (pb.authStore.record?.verein_id) {
|
||||
if (u.verein_id) {
|
||||
goto('/');
|
||||
return;
|
||||
}
|
||||
// Vereinsname aus Registration vorbelegen
|
||||
vereinsname = pb.authStore.record?.name ?? '';
|
||||
vereinsname = u.name ?? '';
|
||||
});
|
||||
|
||||
async function vereinAnlegen() {
|
||||
if (!vereinsname.trim()) return;
|
||||
error = ''; loading = true;
|
||||
try {
|
||||
const verein = await pb.collection('vereine').create({
|
||||
const updated = await api.post<any>('/onboarding/verein', {
|
||||
name: vereinsname.trim(),
|
||||
ort: ort.trim() || null,
|
||||
plan: 'free',
|
||||
dosb_mitglied: false,
|
||||
});
|
||||
|
||||
await pb.collection('users').update(pb.authStore.record!.id, {
|
||||
verein_id: verein.id,
|
||||
});
|
||||
|
||||
// Auth-Token aktualisieren damit verein_id im Record steht
|
||||
await pb.collection('users').authRefresh();
|
||||
|
||||
fertigName = verein.name;
|
||||
// user store mit aktualisiertem verein_id updaten
|
||||
const u = get(user);
|
||||
if (u) user.set({ ...u, verein_id: updated.verein_id });
|
||||
fertigName = vereinsname.trim();
|
||||
schritt = 3;
|
||||
} catch (e: unknown) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Anlegen.';
|
||||
|
|
|
|||
|
|
@ -1,42 +1,27 @@
|
|||
version: "3.8"
|
||||
|
||||
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:
|
||||
build:
|
||||
context: ./app
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
VITE_PB_URL: https://api-staging.vereins.haus
|
||||
image: vereinshaus-staging-app
|
||||
container_name: vereinshaus-staging-app
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /volume1/docker/vereinshaus-staging/data:/data
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
- HOST=0.0.0.0
|
||||
- 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}
|
||||
- VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}
|
||||
- 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:
|
||||
default: {}
|
||||
npm_bridge:
|
||||
|
|
|
|||
|
|
@ -1,42 +1,27 @@
|
|||
version: "3.8"
|
||||
|
||||
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:
|
||||
build:
|
||||
context: ./app
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
VITE_PB_URL: https://api.vereins.haus
|
||||
image: vereinshaus-app
|
||||
container_name: vereinshaus-app
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /volume1/docker/vereinshaus/data:/data
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
- HOST=0.0.0.0
|
||||
- PORT=3000
|
||||
- DB_PATH=/data/vereinshaus.db
|
||||
- UPLOAD_DIR=/data/uploads
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- PUBLIC_VAPID_KEY=${PUBLIC_VAPID_KEY}
|
||||
- VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}
|
||||
- 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:
|
||||
default: {}
|
||||
npm_bridge:
|
||||
|
|
|
|||
365
scripts/seed.js
365
scripts/seed.js
|
|
@ -1,264 +1,153 @@
|
|||
#!/usr/bin/env node
|
||||
// Testdaten für vereins.haus – Staging
|
||||
// Aufruf: PB_URL=http://localhost:8090 PB_EMAIL=admin@test.de PB_PASSWORD=Test123456! node scripts/seed.js
|
||||
// Seed-Script für vereins.haus (SvelteKit + SQLite, kein PocketBase)
|
||||
// Verwendung: APP_URL=https://staging.vereins.haus node scripts/seed.js
|
||||
|
||||
const PB_URL = process.env.PB_URL || 'http://localhost:8090';
|
||||
const PB_EMAIL = process.env.PB_EMAIL || '';
|
||||
const PB_PWD = process.env.PB_PASSWORD || '';
|
||||
const BASE = process.env.APP_URL || 'http://localhost:3000';
|
||||
|
||||
if (!PB_EMAIL || !PB_PWD) {
|
||||
console.error('Fehler: PB_EMAIL und PB_PASSWORD setzen.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
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,
|
||||
async function req(method, path, body, token) {
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
const res = await fetch(`${BASE}/api${path}`, {
|
||||
method, headers, body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
async function find(collection, filter) {
|
||||
const r = await pb('GET', `collections/${collection}/records?filter=${encodeURIComponent(filter)}&perPage=1`);
|
||||
return r.items?.[0] ?? null;
|
||||
}
|
||||
async function main() {
|
||||
console.log(`→ Seed gegen ${BASE}`);
|
||||
|
||||
async function create(collection, data) {
|
||||
return pb('POST', `collections/${collection}/records`, data);
|
||||
}
|
||||
|
||||
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', {
|
||||
// 1. Verein + Admin registrieren
|
||||
console.log('→ Registrierung...');
|
||||
const auth = await req('POST', '/auth/register', {
|
||||
vereinName: 'TSV Musterstadt 1983 e.V.',
|
||||
email: 'vorstand@tsv-musterstadt.de',
|
||||
password: 'Test123456!', passwordConfirm: 'Test123456!',
|
||||
name: 'Max Mustermann', verein_id: verein.id, rolle: null, // null = admin
|
||||
emailVisibility: true,
|
||||
password: 'Test123456!',
|
||||
name: 'Max Vorstand',
|
||||
});
|
||||
}
|
||||
console.log(' ✓ Admin:', adminUser.email);
|
||||
const T = auth.token;
|
||||
const VID = auth.verein_id;
|
||||
console.log(` ✓ Verein ID: ${VID}`);
|
||||
console.log(` ✓ User: vorstand@tsv-musterstadt.de`);
|
||||
|
||||
// ── 4. Gruppen ───────────────────────────────────────────────────────────────
|
||||
console.log('→ Gruppen…');
|
||||
const gruppenDef = ['Vorstand', 'Aktive Mitglieder', 'Jugend U15', 'Senioren'];
|
||||
const gruppen = {};
|
||||
for (const name of gruppenDef) {
|
||||
let g = await find('gruppen', `name = "${name}" && verein_id = "${verein.id}"`);
|
||||
if (!g) g = await create('gruppen', { verein_id: verein.id, name });
|
||||
// 2. Verein-Details ergänzen
|
||||
await req('PATCH', '/vereine', {
|
||||
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 = {};
|
||||
for (const name of ['Vorstand', 'Aktive Mitglieder', 'Jugend U15', 'Senioren']) {
|
||||
const g = await req('POST', '/gruppen', { name }, T);
|
||||
gruppen[name] = g.id;
|
||||
}
|
||||
console.log(' ✓', Object.keys(gruppen).join(', '));
|
||||
|
||||
// ── 5. Mitglieder ────────────────────────────────────────────────────────────
|
||||
console.log('→ Mitglieder (18)…');
|
||||
const mitgliederDef = [
|
||||
// Vorstand
|
||||
{ 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: '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' },
|
||||
// Aktive mit IBAN
|
||||
{ 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: '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: '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: '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: '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: '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: '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' },
|
||||
// Aktive ohne IBAN
|
||||
{ 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: '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: '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 },
|
||||
// Jugend
|
||||
{ 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: '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: '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' },
|
||||
];
|
||||
|
||||
let mitgliederIds = [];
|
||||
for (const m of mitgliederDef) {
|
||||
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`);
|
||||
console.log(` ✓ ${Object.keys(gruppen).length} Gruppen`);
|
||||
|
||||
// ── 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 = {};
|
||||
for (const o of orteDef) {
|
||||
let rec = await find('veranstaltungsorte', `name = "${o.name}" && verein_id = "${verein.id}"`);
|
||||
if (!rec) rec = await create('veranstaltungsorte', { verein_id: verein.id, ...o });
|
||||
orte[o.name] = rec.id;
|
||||
}
|
||||
// Ausfall: Turnhalle gesperrt wegen Schulveranstaltung
|
||||
const turnhalleId = orte['Turnhalle Grundschule Muster'];
|
||||
const existingAusfall = await find('ort_ausfaelle', `ort_id = "${turnhalleId}"`);
|
||||
if (!existingAusfall) {
|
||||
const nextWeek = new Date(); nextWeek.setDate(nextWeek.getDate() + 7);
|
||||
const nextWeek2 = new Date(); nextWeek2.setDate(nextWeek2.getDate() + 9);
|
||||
await create('ort_ausfaelle', {
|
||||
ort_id: turnhalleId,
|
||||
von: nextWeek.toISOString().slice(0,10),
|
||||
bis: nextWeek2.toISOString().slice(0,10),
|
||||
grund: 'Schulveranstaltung – Halle nicht verfügbar',
|
||||
});
|
||||
}
|
||||
console.log(' ✓ Orte:', Object.keys(orte).join(', '));
|
||||
// 4. Mitglieder
|
||||
console.log('→ Mitglieder (18)...');
|
||||
const mitgliederDaten = [
|
||||
{ vorname: 'Max', nachname: 'Vorstand', email: 'vorstand@tsv-musterstadt.de', status: 'aktiv', gruppe_ids: [gruppen['Vorstand'], gruppen['Aktive Mitglieder']], eintrittsdatum: '2010-01-01' },
|
||||
{ vorname: 'Anna', nachname: 'Schmidt', email: 'anna.schmidt@example.de', status: 'aktiv', gruppe_ids: [gruppen['Aktive Mitglieder']], eintrittsdatum: '2015-03-15', iban: 'DE89370400440532013001' },
|
||||
{ vorname: 'Thomas', nachname: 'Müller', email: 'thomas.mueller@example.de', status: 'aktiv', gruppe_ids: [gruppen['Aktive Mitglieder'], gruppen['Senioren']], eintrittsdatum: '2012-06-01' },
|
||||
{ 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: 'Klaus', nachname: 'Fischer', email: 'klaus.fischer@example.de', status: 'aktiv', gruppe_ids: [gruppen['Senioren']], eintrittsdatum: '2008-01-15' },
|
||||
{ vorname: 'Maria', nachname: 'Bauer', email: 'maria.bauer@example.de', status: 'aktiv', gruppe_ids: [gruppen['Aktive Mitglieder']], eintrittsdatum: '2019-02-20' },
|
||||
{ vorname: 'Peter', nachname: 'Wagner', email: 'peter.wagner@example.de', status: 'aktiv', gruppe_ids: [gruppen['Aktive Mitglieder']], eintrittsdatum: '2017-05-10', iban: 'DE89370400440532013002' },
|
||||
{ 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: 'Stefan', nachname: 'Hoffmann', email: 'stefan.hoffmann@example.de', status: 'passiv', gruppe_ids: [], eintrittsdatum: '2005-03-01' },
|
||||
{ vorname: 'Sandra', nachname: 'Koch', email: 'sandra.koch@example.de', status: 'aktiv', gruppe_ids: [gruppen['Senioren']], eintrittsdatum: '2014-11-15' },
|
||||
{ vorname: 'Michael', nachname: 'Schäfer', email: 'michael.schaefer@example.de', status: 'aktiv', gruppe_ids: [gruppen['Aktive Mitglieder']], eintrittsdatum: '2020-04-01' },
|
||||
{ vorname: 'Sabine', nachname: 'Zimmermann', email: 'sabine.zimm@example.de', status: 'aktiv', gruppe_ids: [gruppen['Aktive Mitglieder']], eintrittsdatum: '2018-08-20' },
|
||||
{ vorname: 'Andreas', nachname: 'Braun', email: 'andreas.braun@example.de', status: 'aktiv', gruppe_ids: [gruppen['Senioren']], eintrittsdatum: '2011-01-01' },
|
||||
{ vorname: 'Monika', nachname: 'Richter', email: 'monika.richter@example.de', status: 'passiv', gruppe_ids: [], eintrittsdatum: '2009-06-15' },
|
||||
{ vorname: 'Tobias', nachname: 'Wolf', email: 'tobias.wolf@example.de', status: 'aktiv', gruppe_ids: [gruppen['Jugend U15']], eintrittsdatum: '2022-01-15', geburtsdatum: '2012-01-30' },
|
||||
{ vorname: 'Eva', nachname: 'Krause', email: 'eva.krause@example.de', status: 'aktiv', gruppe_ids: [gruppen['Aktive Mitglieder']], eintrittsdatum: '2021-03-01' },
|
||||
{ vorname: 'Markus', nachname: 'Schwarz', email: 'markus.schwarz@example.de', status: 'aktiv', gruppe_ids: [gruppen['Aktive Mitglieder'], gruppen['Vorstand']], eintrittsdatum: '2013-09-01' },
|
||||
{ vorname: 'Nina', nachname: 'Lange', email: 'nina.lange@example.de', status: 'ausgetreten', gruppe_ids: [], eintrittsdatum: '2016-02-01', austrittsdatum: '2023-12-31' },
|
||||
];
|
||||
for (const m of mitgliederDaten) await req('POST', '/mitglieder', m, T);
|
||||
console.log(` ✓ 18 Mitglieder`);
|
||||
|
||||
// ── 7. Beitragsarten ─────────────────────────────────────────────────────────
|
||||
console.log('→ Beitragsarten…');
|
||||
const beitraegeDef = [
|
||||
{ name: 'Jahresbeitrag Erwachsene', betrag: 48, rhythmus: 'jaehrlich', beschreibung: 'Für Mitglieder ab 18 Jahren' },
|
||||
{ name: 'Jahresbeitrag Jugend', betrag: 24, rhythmus: 'jaehrlich', beschreibung: 'Für Mitglieder unter 18 Jahren' },
|
||||
{ name: 'Aufnahmegebühr', betrag: 20, rhythmus: 'einmalig', beschreibung: 'Einmalig bei Vereinseintritt' },
|
||||
{ name: 'Monatsbeitrag Fitness', betrag: 15, rhythmus: 'monatlich', beschreibung: 'Für die Fitnessgruppe (optional)' },
|
||||
];
|
||||
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(', '));
|
||||
// 5. Veranstaltungsorte
|
||||
console.log('→ Orte...');
|
||||
const orte = {};
|
||||
for (const o of [
|
||||
{ name: 'Turnhalle Grundschule Muster', adresse: 'Schulweg 5, 12345 Musterstadt', typ: 'halle', aktiv: true },
|
||||
{ name: 'Vereinsheim TSV', adresse: 'Musterstraße 3, 12345 Musterstadt', typ: 'gebaeude', aktiv: true },
|
||||
{ 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;
|
||||
}
|
||||
console.log(` ✓ 3 Orte`);
|
||||
|
||||
// ── 8. Termine ───────────────────────────────────────────────────────────────
|
||||
console.log('→ Termine…');
|
||||
// 6. Beitragsarten
|
||||
console.log('→ Beitragsarten...');
|
||||
for (const b of [
|
||||
{ name: 'Jahresbeitrag Erwachsene', betrag: 120, rhythmus: 'jaehrlich', beschreibung: 'Normalbeitrag für erwachsene Mitglieder' },
|
||||
{ name: 'Jahresbeitrag Jugend', betrag: 60, rhythmus: 'jaehrlich', beschreibung: 'Ermäßigter Beitrag bis 18 Jahre' },
|
||||
{ name: 'Aufnahmegebühr', betrag: 25, rhythmus: 'einmalig', beschreibung: 'Einmalige Gebühr bei Eintritt' },
|
||||
{ name: 'Monatsbeitrag Fitness', betrag: 15, rhythmus: 'monatlich', beschreibung: 'Zusatzbeitrag Fitnessraum' },
|
||||
]) {
|
||||
await req('POST', '/beitraege', b, T);
|
||||
}
|
||||
console.log(` ✓ 4 Beitragsarten`);
|
||||
|
||||
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();
|
||||
};
|
||||
// 7. Termine
|
||||
console.log('→ Termine...');
|
||||
const now = new Date();
|
||||
const dt = (offsetDays, h = 18, m = 0) => {
|
||||
const d = new Date(now);
|
||||
d.setDate(d.getDate() + offsetDays);
|
||||
d.setHours(h, m, 0, 0);
|
||||
return d.toISOString().slice(0, 19);
|
||||
};
|
||||
|
||||
const termineDef = [
|
||||
// Vergangen
|
||||
{ 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: '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: d(-7, 16, 0), ende: d(-7, 17, 30), ort_id: orte['Turnhalle Grundschule Muster'], gruppe_ids: [gruppen['Jugend U15']] },
|
||||
// Upcoming
|
||||
{ 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 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' },
|
||||
];
|
||||
|
||||
// Wöchentliches Training als Serie
|
||||
const serie_id = 'seed-serie-dienstag-2026';
|
||||
const trainingsTermine = [];
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
// Nächster Dienstag + i Wochen
|
||||
const dt = new Date();
|
||||
const daysToTue = (2 - dt.getDay() + 7) % 7 || 7;
|
||||
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(),
|
||||
const termine = [
|
||||
{ titel: 'Vorstandssitzung', beginn: dt(3, 19), ort_id: orte['Vereinsheim TSV'], gruppe_ids: [gruppen['Vorstand']], verfuegbarkeit: 'bestaetigt' },
|
||||
{ titel: 'Vereinsmeisterschaft', beginn: dt(14, 9), ort_id: orte['Sportplatz West'], gruppe_ids: [], beschreibung: 'Jährliche Meisterschaft', verfuegbarkeit: 'bestaetigt' },
|
||||
{ titel: 'Jugendtraining', beginn: dt(2, 16), ort_id: orte['Turnhalle Grundschule Muster'], gruppe_ids: [gruppen['Jugend U15']], verfuegbarkeit: 'bestaetigt' },
|
||||
{ titel: 'Mitgliederversammlung', beginn: dt(21, 19,30),ort_id: orte['Vereinsheim TSV'], gruppe_ids: [], beschreibung: 'Ordentliche Jahreshauptversammlung', verfuegbarkeit: 'offen' },
|
||||
{ titel: 'Seniorensport', beginn: dt(7, 10), ort_id: orte['Turnhalle Grundschule Muster'], gruppe_ids: [gruppen['Senioren']], verfuegbarkeit: 'bestaetigt' },
|
||||
{ titel: 'Sommerfest', beginn: dt(45, 14), ort_id: orte['Sportplatz West'], gruppe_ids: [], beschreibung: 'Großes Vereinssommerfest', verfuegbarkeit: 'offen' },
|
||||
];
|
||||
// 8 Wochen Dienstags-Training
|
||||
let di = new Date(now);
|
||||
di.setDate(di.getDate() + ((2 - di.getDay() + 7) % 7 || 7));
|
||||
di.setHours(18, 0, 0, 0);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const b = new Date(di); b.setDate(b.getDate() + i * 7);
|
||||
termine.push({
|
||||
titel: 'Training Aktive', beginn: b.toISOString().slice(0, 19),
|
||||
ort_id: orte['Turnhalle Grundschule Muster'],
|
||||
gruppe_ids:[gruppen['Aktive Mitglieder']],
|
||||
gruppe_ids: [gruppen['Aktive Mitglieder']], verfuegbarkeit: 'offen',
|
||||
rrule: 'FREQ=WEEKLY;BYDAY=TU',
|
||||
serie_id,
|
||||
});
|
||||
}
|
||||
|
||||
const allTermine = [...termineDef, ...trainingsTermine];
|
||||
let termineAngelegt = 0;
|
||||
for (const t of allTermine) {
|
||||
const ex = await find('termine', `titel = "${t.titel}" && verein_id = "${verein.id}" && beginn = "${t.beginn}"`);
|
||||
if (!ex) {
|
||||
await create('termine', { verein_id: verein.id, verfuegbarkeit: 'offen', ...t });
|
||||
termineAngelegt++;
|
||||
}
|
||||
}
|
||||
console.log(` ✓ ${termineAngelegt} Termine (inkl. ${trainingsTermine.length} in Dienstags-Serie)`);
|
||||
for (const t of termine) await req('POST', '/termine', t, T);
|
||||
console.log(` ✓ ${termine.length} Termine`);
|
||||
|
||||
// ── 9. Nachricht ─────────────────────────────────────────────────────────────
|
||||
console.log('→ Beispiel-Nachricht…');
|
||||
const exMsg = await find('nachrichten', `verein_id = "${verein.id}"`);
|
||||
if (!exMsg) {
|
||||
await create('nachrichten', {
|
||||
verein_id: verein.id,
|
||||
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(),
|
||||
});
|
||||
// 8. Nachricht
|
||||
console.log('→ Nachricht...');
|
||||
await req('POST', '/nachrichten', {
|
||||
betreff: 'Willkommen in vereins.haus!',
|
||||
text: '<p>Hallo und herzlich willkommen! Dies ist eine Beispiel-Nachricht an alle Mitglieder.</p>',
|
||||
gruppe_ids: [],
|
||||
}, T);
|
||||
console.log(` ✓ Nachricht erstellt`);
|
||||
|
||||
console.log('');
|
||||
console.log('✓ Seed abgeschlossen!');
|
||||
console.log('');
|
||||
console.log(` Verein: TSV Musterstadt 1983 e.V.`);
|
||||
console.log(` Login: vorstand@tsv-musterstadt.de / Test123456!`);
|
||||
console.log(` App: ${BASE}`);
|
||||
}
|
||||
|
||||
// ── Zusammenfassung ──────────────────────────────────────────────────────────
|
||||
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);
|
||||
main().catch(e => { console.error('✗ Seed-Fehler:', e.message); process.exit(1); });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue