diff --git a/Makefile b/Makefile index 9711355..5a6b184 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,16 @@ # ============================================================== # 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_APP_STAGING := vereinshaus-staging-app - TAR_EXCLUDE := --exclude='.git' \ --exclude='./app/node_modules' \ --exclude='./app/.svelte-kit' \ @@ -21,8 +18,11 @@ TAR_EXCLUDE := --exclude='.git' \ --exclude='./.env' \ --exclude='./.DS_Store' -.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 +HOOKS_SRC := pocketbase/pb_hooks +HOOKS_DST := /volume1/docker/vereinshaus/pocketbase/data/pb_hooks + +.PHONY: help check-ssh start stop restart status logs logs-f logs-app \ + shell-pb pb-admin deploy # ---------------------------------------------------------- # Hilfe @@ -31,17 +31,17 @@ help: @echo "" @echo " vereins.haus — verfügbare Befehle:" @echo "" - @echo " make deploy App bauen + zur DS übertragen + 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 " 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 status Container-Status anzeigen" @echo "" - @echo " make staging-deploy Staging deployen" - @echo " make staging-seed Testdaten einfügen" - @echo " make staging-reset Staging-DB löschen (Neustart)" + @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 "" # ---------------------------------------------------------- @@ -56,14 +56,20 @@ check-ssh: fi # ---------------------------------------------------------- -# DEPLOY (Production) +# DEPLOY # ---------------------------------------------------------- deploy: check-ssh @echo "→ Sync zu DS..." @COPYFILE_DISABLE=1 tar czf - $(TAR_EXCLUDE) . | ssh $(DS_HOST) "tar xzf - -C $(DS_PATH)/" @echo "→ .env auf DS aktualisieren..." @if [ -f .env ]; then \ - cat .env | ssh $(DS_HOST) "cat > $(DS_PATH)/.env"; \ + grep -E "BREVO_KEY" .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 "→ Docker rebuild + restart..." @ssh $(DS_HOST) " \ @@ -72,7 +78,7 @@ deploy: check-ssh $(DOCKER) compose build app && \ $(DOCKER) compose up -d" @echo " ✓ Deploy fertig." - @ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER_APP) --tail=15" + @ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER_APP) --tail=10" # ---------------------------------------------------------- # Container-Steuerung @@ -98,61 +104,19 @@ 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_APP) -f" + @ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER_PB) -f" -# ============================================================== -# STAGING -# ============================================================== +# ---------------------------------------------------------- +# Shell + Admin +# ---------------------------------------------------------- +shell-pb: check-ssh + @ssh -t $(DS_HOST) "$(DOCKER) exec -it $(CONTAINER_PB) sh" -staging-deploy: check-ssh - @echo "→ Sync zu DS (Staging)..." - @COPYFILE_DISABLE=1 tar czf - $(TAR_EXCLUDE) . | ssh $(DS_HOST) "tar xzf - -C $(STAGING_PATH)/" - @echo "→ .env auf DS (Staging)..." - @if [ -f .env ]; then \ - cat .env | ssh $(DS_HOST) "cat > $(STAGING_PATH)/.env"; \ - fi - @echo "→ Docker rebuild + restart (Staging)..." - @ssh $(DS_HOST) " \ - cd $(STAGING_PATH) && \ - $(DOCKER) compose -f docker-compose.staging.yml down && \ - $(DOCKER) compose -f docker-compose.staging.yml build app-staging && \ - $(DOCKER) compose -f docker-compose.staging.yml up -d" - @echo " ✓ Staging bereit." - @echo " App: https://staging.vereins.haus" - -# Löscht die SQLite-DB auf Staging → frischer Start -# Danach: make staging-deploy && make staging-seed -staging-reset: check-ssh staging-stop - @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..." - @if [ -f .env ]; then \ - export $$(grep -v '^#' .env | xargs) && \ - APP_URL=https://staging.vereins.haus node scripts/seed.js; \ - else \ - APP_URL=https://staging.vereins.haus node scripts/seed.js; \ - fi - -staging-logs: check-ssh - @ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER_APP_STAGING) --tail=50" - -staging-status: check-ssh - @ssh $(DS_HOST) "$(DOCKER) ps \ - --filter name=vereinshaus-staging \ - --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'" - -staging-stop: check-ssh - @ssh $(DS_HOST) "cd $(STAGING_PATH) && $(DOCKER) compose -f docker-compose.staging.yml down" - @echo " ✓ Staging gestoppt." +pb-admin: + @echo " PocketBase Admin: https://api.vereins.haus/_/" diff --git a/app/Dockerfile b/app/Dockerfile index bd6f749..7831668 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -1,5 +1,4 @@ FROM node:22-alpine AS builder -RUN apk add --no-cache python3 make g++ WORKDIR /app COPY package*.json ./ RUN npm ci @@ -7,7 +6,6 @@ COPY . . 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 ./ diff --git a/app/package-lock.json b/app/package-lock.json index f27e45c..7d728af 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -8,31 +8,17 @@ "name": "vereinshaus", "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", - "web-push": "^3.6.7" + "pocketbase": "^0.26.9" }, "devDependencies": { "@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", "svelte-check": "^4.4.6", "typescript": "^6.0.2", "vite": "^8.0.7", - "vite-plugin-pwa": "^1.3.0", - "workbox-core": "^7.4.1", - "workbox-precaching": "^7.4.1" + "vite-plugin-pwa": "^1.3.0" } }, "node_modules/@apideck/better-ajv-errors": { @@ -1628,15 +1614,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@event-calendar/core": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@event-calendar/core/-/core-5.7.0.tgz", - "integrity": "sha512-16S9TncV/az52qFjvmB691fMQA8qIo/Iz4kxrvOMLwD46oFzin07aWQQPBcgCFUN+58m9AbFYWnxkvO7OYAldg==", - "license": "MIT", - "dependencies": { - "svelte": "^5.55.4" - } - }, "node_modules/@isaacs/cliui": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", @@ -1651,6 +1628,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1661,6 +1639,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1671,6 +1650,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1691,12 +1671,14 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2532,6 +2514,7 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" @@ -2642,23 +2625,6 @@ "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", @@ -2670,28 +2636,9 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "25.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", - "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" - } - }, - "node_modules/@types/papaparse": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.2.tgz", - "integrity": "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -2703,22 +2650,14 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT" - }, - "node_modules/@types/web-push": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/@types/web-push/-/web-push-3.6.4.tgz", - "integrity": "sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } + "license": "MIT" }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2727,15 +2666,6 @@ "node": ">=0.4.0" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -2757,6 +2687,7 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -2801,18 +2732,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -2860,6 +2779,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -2917,26 +2837,6 @@ "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", @@ -2950,55 +2850,6 @@ "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", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "license": "MIT" - }, "node_modules/brace-expansion": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", @@ -3046,36 +2897,6 @@ "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", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3170,16 +2991,11 @@ "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", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3323,6 +3139,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3336,30 +3153,6 @@ } } }, - "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", @@ -3410,6 +3203,7 @@ "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" @@ -3419,6 +3213,7 @@ "version": "5.8.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "dev": true, "license": "MIT" }, "node_modules/dunder-proto": { @@ -3436,15 +3231,6 @@ "node": ">= 0.4" } }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -3468,15 +3254,6 @@ "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", @@ -3627,12 +3404,14 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, "license": "MIT" }, "node_modules/esrap": { "version": "2.2.9", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.9.tgz", "integrity": "sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -3676,15 +3455,6 @@ "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", @@ -3734,12 +3504,6 @@ } } }, - "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", @@ -3813,12 +3577,6 @@ "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", @@ -3975,12 +3733,6 @@ "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", @@ -4127,76 +3879,6 @@ "node": ">= 0.4" } }, - "node_modules/http_ece": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz", - "integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/ical-generator": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ical-generator/-/ical-generator-10.2.0.tgz", - "integrity": "sha512-XR5FsiDWCsz5MwBwMA/sQqR3A9H240xkXIeXOabV7uNAiieP+TA9rleVvlwPLRXMz+CXME8cGuDd7cdnE5At6w==", - "license": "MIT", - "engines": { - "node": "20 || 22 || >=24" - }, - "peerDependencies": { - "@touch4it/ical-timezones": ">=1.6.0", - "@types/luxon": ">= 1.26.0", - "@types/mocha": ">= 8.2.1", - "dayjs": ">= 1.10.0", - "luxon": ">= 1.26.0", - "moment": ">= 2.29.0", - "moment-timezone": ">= 0.5.33", - "rrule": ">= 2.6.8" - }, - "peerDependenciesMeta": { - "@touch4it/ical-timezones": { - "optional": true - }, - "@types/luxon": { - "optional": true - }, - "@types/mocha": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "dayjs": { - "optional": true - }, - "luxon": { - "optional": true - }, - "moment": { - "optional": true - }, - "moment-timezone": { - "optional": true - }, - "rrule": { - "optional": true - } - } - }, "node_modules/idb": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", @@ -4204,38 +3886,6 @@ "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", @@ -4708,15 +4358,6 @@ "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", @@ -4780,27 +4421,6 @@ "node": ">=0.10.0" } }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -5086,6 +4706,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, "license": "MIT" }, "node_modules/lodash.debounce": { @@ -5116,6 +4737,7 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -5131,24 +4753,6 @@ "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", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "license": "ISC" - }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -5165,15 +4769,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -5184,12 +4779,6 @@ "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", @@ -5214,6 +4803,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -5235,36 +4825,6 @@ "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", @@ -5327,15 +4887,6 @@ ], "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", @@ -5361,12 +4912,6 @@ "dev": true, "license": "BlueOak-1.0.0" }, - "node_modules/papaparse": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", - "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", - "license": "MIT" - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -5476,33 +5021,6 @@ "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", @@ -5516,16 +5034,6 @@ "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", @@ -5536,35 +5044,6 @@ "node": ">=6" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -5799,15 +5278,6 @@ "dev": true, "license": "MIT" }, - "node_modules/rrule": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.8.1.tgz", - "integrity": "sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw==", - "license": "BSD-3-Clause", - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -5841,26 +5311,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "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/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -5896,12 +5346,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -6090,51 +5534,6 @@ "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", @@ -6219,15 +5618,6 @@ "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", @@ -6340,15 +5730,6 @@ "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", @@ -6366,6 +5747,7 @@ "version": "5.55.8", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.8.tgz", "integrity": "sha512-4D6lyrMHmDaZalQOEBMCWCCidyZjSnec14/oPn0k627G6goxcck9xqMwz1tFLlQz+ZFvtTTHfFOlUayuAz0z6Q==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -6417,39 +5799,12 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, "license": "MIT", "dependencies": { "@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", @@ -6539,19 +5894,9 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "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": "*" - } + "dev": true, + "license": "0BSD", + "optional": true }, "node_modules/type-fest": { "version": "0.16.0", @@ -6677,13 +6022,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", - "dev": true, - "license": "MIT" - }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", @@ -6793,12 +6131,6 @@ "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", @@ -6928,25 +6260,6 @@ } } }, - "node_modules/web-push": { - "version": "3.6.7", - "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz", - "integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==", - "license": "MPL-2.0", - "dependencies": { - "asn1.js": "^5.3.0", - "http_ece": "1.2.0", - "https-proxy-agent": "^7.0.0", - "jws": "^4.0.0", - "minimist": "^1.2.5" - }, - "bin": { - "web-push": "src/cli.js" - }, - "engines": { - "node": ">= 16" - } - }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", @@ -7291,12 +6604,6 @@ "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", @@ -7308,6 +6615,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, "license": "MIT" } } diff --git a/app/package.json b/app/package.json index 9f4431d..e0fc5a5 100644 --- a/app/package.json +++ b/app/package.json @@ -15,27 +15,13 @@ "@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", "svelte-check": "^4.4.6", "typescript": "^6.0.2", "vite": "^8.0.7", - "vite-plugin-pwa": "^1.3.0", - "workbox-core": "^7.4.1", - "workbox-precaching": "^7.4.1" + "vite-plugin-pwa": "^1.3.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", - "web-push": "^3.6.7" + "pocketbase": "^0.26.9" } } diff --git a/app/src/app.d.ts b/app/src/app.d.ts deleted file mode 100644 index d437a87..0000000 --- a/app/src/app.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -declare global { - namespace App { - interface Locals { - user: { - id: string; - verein_id: string; - rolle: string | null; - name: string; - email: string; - } | null; - } - } -} - -export {}; diff --git a/app/src/app.html b/app/src/app.html index 7842f62..6aac5ef 100644 --- a/app/src/app.html +++ b/app/src/app.html @@ -4,12 +4,13 @@ - + - + - + + %sveltekit.head% diff --git a/app/src/lib/api.ts b/app/src/lib/api.ts deleted file mode 100644 index 5db04ec..0000000 --- a/app/src/lib/api.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { get } from 'svelte/store'; -import { user } from './user'; -import { goto } from '$app/navigation'; - -function token() { return get(user)?.token ?? ''; } - -function headers(extra: Record = {}): Record { - return { Authorization: `Bearer ${token()}`, ...extra }; -} - -async function handleRes(res: Response): Promise { - 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; -} - -export const api = { - async get(path: string, query: Record = {}): Promise { - const url = new URL('/api' + path, location.origin); - Object.entries(query).forEach(([k, v]) => v !== undefined && url.searchParams.set(k, v)); - return handleRes(await fetch(url.toString(), { headers: headers() })); - }, - - async post(path: string, data?: unknown): Promise { - return handleRes(await fetch('/api' + path, { - method: 'POST', - headers: headers({ 'Content-Type': 'application/json' }), - body: JSON.stringify(data ?? {}), - })); - }, - - async put(path: string, data?: unknown): Promise { - return handleRes(await fetch('/api' + path, { - method: 'PUT', - headers: headers({ 'Content-Type': 'application/json' }), - body: JSON.stringify(data ?? {}), - })); - }, - - async patch(path: string, data?: unknown): Promise { - return handleRes(await fetch('/api' + path, { - method: 'PATCH', - headers: headers({ 'Content-Type': 'application/json' }), - body: JSON.stringify(data ?? {}), - })); - }, - - async del(path: string): Promise { - return handleRes(await fetch('/api' + path, { method: 'DELETE', headers: headers() })); - }, - - async postForm(path: string, form: FormData): Promise { - return handleRes(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; - } -}; diff --git a/app/src/lib/components/Icon.svelte b/app/src/lib/components/Icon.svelte deleted file mode 100644 index 3af0fc2..0000000 --- a/app/src/lib/components/Icon.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/app/src/lib/event-calendar.d.ts b/app/src/lib/event-calendar.d.ts deleted file mode 100644 index 703104d..0000000 --- a/app/src/lib/event-calendar.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -declare module '@event-calendar/core' { - import type { SvelteComponent } from 'svelte'; - - export class Calendar extends SvelteComponent<{ - plugins: any[]; - options: Record; - }> {} - - export const TimeGrid: any; - export const DayGrid: any; - export const List: any; - export const Interaction: any; -} diff --git a/app/src/lib/icons.ts b/app/src/lib/icons.ts deleted file mode 100644 index bf74341..0000000 --- a/app/src/lib/icons.ts +++ /dev/null @@ -1,19 +0,0 @@ -import house from './icons/house.svg?raw'; -import users from './icons/users.svg?raw'; -import calendar from './icons/calendar.svg?raw'; -import currencyEur from './icons/currency-eur.svg?raw'; -import envelope from './icons/envelope.svg?raw'; -import gear from './icons/gear.svg?raw'; -import images from './icons/images.svg?raw'; - -export const icons = { - house, - users, - calendar, - 'currency-eur': currencyEur, - envelope, - gear, - images, -} as const; - -export type IconName = keyof typeof icons; diff --git a/app/src/lib/icons/calendar.svg b/app/src/lib/icons/calendar.svg deleted file mode 100644 index 3b91cc4..0000000 --- a/app/src/lib/icons/calendar.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/src/lib/icons/currency-eur.svg b/app/src/lib/icons/currency-eur.svg deleted file mode 100644 index c23e136..0000000 --- a/app/src/lib/icons/currency-eur.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/src/lib/icons/envelope.svg b/app/src/lib/icons/envelope.svg deleted file mode 100644 index f07a98b..0000000 --- a/app/src/lib/icons/envelope.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/src/lib/icons/gear.svg b/app/src/lib/icons/gear.svg deleted file mode 100644 index 9a20171..0000000 --- a/app/src/lib/icons/gear.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/src/lib/icons/house.svg b/app/src/lib/icons/house.svg deleted file mode 100644 index 6346350..0000000 --- a/app/src/lib/icons/house.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/src/lib/icons/images.svg b/app/src/lib/icons/images.svg deleted file mode 100644 index 48ffc9a..0000000 --- a/app/src/lib/icons/images.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/src/lib/icons/users.svg b/app/src/lib/icons/users.svg deleted file mode 100644 index 7f7b6ca..0000000 --- a/app/src/lib/icons/users.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/src/lib/sepa.ts b/app/src/lib/sepa.ts deleted file mode 100644 index a49ff21..0000000 --- a/app/src/lib/sepa.ts +++ /dev/null @@ -1,109 +0,0 @@ -export interface SepaKopf { - glaeubigerid: string; - vereinIban: string; - vereinBic: string; - vereinName: string; - einzugsdatum: string; // YYYY-MM-DD -} - -export interface SepaPosition { - endToEndId: string; - betrag: number; - mandatsreferenz: string; - mandatsdatum: string; // YYYY-MM-DD - debitorName: string; - debitorIban: string; - debitorBic: string; - verwendungszweck: string; -} - -function x(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"'); -} - -function iban(raw: string): string { - return raw.replace(/\s/g, '').toUpperCase(); -} - -export function generatePain008(kopf: SepaKopf, positionen: SepaPosition[]): string { - if (positionen.length === 0) throw new Error('Keine Positionen für den SEPA-Export.'); - - const now = new Date().toISOString().slice(0, 19); - const msgId = `VH-${Date.now()}`; - const ctrlSum = positionen.reduce((s, p) => s + p.betrag, 0).toFixed(2); - const nbOfTxs = positionen.length; - - const txXml = positionen.map((p) => ` - - ${x(p.endToEndId)} - ${p.betrag.toFixed(2)} - - - ${x(p.mandatsreferenz)} - ${p.mandatsdatum} - - - ${x(kopf.glaeubigerid)} - SEPA - - - ${p.debitorBic ? `${x(p.debitorBic)}` : 'NOTPROVIDED'} - ${x(p.debitorName)} - ${iban(p.debitorIban)} - ${x(p.verwendungszweck.slice(0, 140))} - `).join(''); - - return ` - - - - ${msgId} - ${now} - ${nbOfTxs} - ${ctrlSum} - ${x(kopf.vereinName)} - - - ${msgId}-PI - DD - ${nbOfTxs} - ${ctrlSum} - - SEPA - CORE - RCUR - - ${kopf.einzugsdatum} - ${x(kopf.vereinName)} - ${iban(kopf.vereinIban)} - ${x(kopf.vereinBic)} - - ${x(kopf.glaeubigerid)} - SEPA - ${txXml} - - -`; -} - -export function downloadXml(xml: string, dateiname: string) { - const blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = dateiname; - a.click(); - URL.revokeObjectURL(url); -} - -export function minEinzugsdatum(): string { - // SEPA CORE RCUR: mindestens 2 Bankarbeitstage Vorlaufzeit (vereinfacht: +3 Tage) - const d = new Date(); - d.setDate(d.getDate() + 3); - return d.toISOString().slice(0, 10); -} diff --git a/app/src/lib/server/auth.ts b/app/src/lib/server/auth.ts deleted file mode 100644 index 1bae1ff..0000000 --- a/app/src/lib/server/auth.ts +++ /dev/null @@ -1,52 +0,0 @@ -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 { - return new SignJWT({ ...payload }) - .setProtectedHeader({ alg: 'HS256' }) - .setExpirationTime('30d') - .sign(JWT_SECRET); -} - -export async function verifyJwt(token: string): Promise { - try { - const { payload } = await jwtVerify(token, JWT_SECRET); - return payload as unknown as JwtPayload; - } catch { - return null; - } -} - -export async function hashPassword(password: string): Promise { - return bcrypt.hash(password, 12); -} - -export async function checkPassword(password: string, hash: string): Promise { - 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 { - 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; -} diff --git a/app/src/lib/server/db.ts b/app/src/lib/server/db.ts deleted file mode 100644 index c7919f7..0000000 --- a/app/src/lib/server/db.ts +++ /dev/null @@ -1,219 +0,0 @@ -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>(r: T): T { - const out: Record = {}; - 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>(rs: T[]): T[] { - return rs.map(r => row(r)); -} diff --git a/app/src/lib/styles/theme.css b/app/src/lib/styles/theme.css deleted file mode 100644 index 220bdcb..0000000 --- a/app/src/lib/styles/theme.css +++ /dev/null @@ -1,59 +0,0 @@ -:root { - /* Primärfarbe */ - --c-primary: #1e40af; - --c-primary-dark: #1d3a9e; - --c-primary-light: #e0e7ff; - --c-primary-subtle: #eff6ff; - - /* Text */ - --c-text: #1e293b; - --c-text-secondary: #475569; - --c-text-muted: #64748b; - --c-text-hint: #94a3b8; - - /* Hintergrund & Rahmen */ - --c-border: #e2e8f0; - --c-bg: #f1f5f9; - --c-bg-card: #ffffff; - --c-bg-subtle: #f8fafc; - - /* Dunkel (Theme-Farbe, Header, PWA) */ - --c-dark: #0f172a; - - /* Fehler / Rot */ - --c-error: #dc2626; - --c-error-dark: #b91c1c; - --c-error-light: #fca5a5; - --c-error-bg: #fee2e2; - --c-error-subtle: #fef2f2; - - /* Erfolg / Grün */ - --c-success: #16a34a; - --c-success-light: #86efac; - --c-success-bg: #dcfce7; - - /* Warnung / Gelb-Amber */ - --c-warning: #f59e0b; - --c-warning-light: #fde047; - --c-warning-bg: #fef9c3; - --c-warning-subtle: #fffbeb; - --c-warning-dark: #92400e; - --c-warning-darker: #713f12; - - /* Akzent / Lila (Plan-Badges) */ - --c-accent: #7c3aed; - --c-accent-subtle: #ede9fe; - - /* Primärfarbe – weitere Töne */ - --c-primary-100: #bfdbfe; - --c-primary-200: #c7d2fe; - --c-primary-bg: #f0f9ff; - - /* Erfolg – weitere Töne */ - --c-success-dark: #166534; - --c-success-subtle: #f0fdf4; - - /* Warnung – weitere Töne */ - --c-warning-amber: #854d0e; - --c-warning-pale: #fefce8; -} diff --git a/app/src/lib/types.ts b/app/src/lib/types.ts index 159b98b..07bad1f 100644 --- a/app/src/lib/types.ts +++ b/app/src/lib/types.ts @@ -1,23 +1,18 @@ export type Plan = 'free' | 'starter' | 'wachstum' | 'verband'; export type MitgliedStatus = 'aktiv' | 'passiv' | 'ausgetreten'; -export type EinzugStatus = 'ausstehend' | 'eingezogen' | 'fehlgeschlagen' | 'storniert'; -export type Rhythmus = 'monatlich' | 'quartalsweise' | 'halbjaehrlich' | 'jaehrlich' | 'einmalig'; +export type EinzugStatus = 'ausstehend' | 'eingezogen' | 'fehlgeschlagen'; +export type Rhythmus = 'monatlich' | 'quartalsweise' | 'halbjaehrlich' | 'jaehrlich'; export interface Verein { id: string; name: string; - adresse?: string; - plz?: string; - ort?: string; - bundesland?: string; + adresse: string; + plz: string; + ort: string; + bundesland: string; plan: Plan; + stripe_customer_id?: string; dosb_mitglied: boolean; - email?: string; - telefon?: string; - website?: string; - glaeubigerid?: string; - iban?: string; - bic?: string; } export interface Mitglied { @@ -25,77 +20,26 @@ export interface Mitglied { verein_id: string; vorname: string; nachname: string; - email?: string; + email: string; telefon?: string; geburtsdatum?: string; - eintrittsdatum?: string; + eintrittsdatum: string; austrittsdatum?: string; - strasse?: string; - plz?: string; - ort?: string; + strasse: string; + plz: string; + ort: string; iban?: string; bic?: string; gruppe_ids: string[]; status: MitgliedStatus; notizen?: string; - mandatsreferenz?: string; - mandatsdatum?: string; } -export type Rolle = 'admin' | 'trainer'; - -export interface Neuigkeit { - id: string; - verein_id: string; - autor_id: string; - autor_name?: string; - text?: string; - medien: string[]; - gruppe_ids: string[]; - termin_id?: string; - created: string; -} - -export interface Reaktion { - id: string; - beitrag_id: string; - user_id: string; -} -export type Verfuegbarkeit = 'offen' | 'bestaetigt' | 'abgesagt' | 'vertretung_gesucht'; - export interface Gruppe { id: string; verein_id: string; name: string; beschreibung?: string; - trainer_ids: string[]; -} - -export type OrtTyp = 'halle' | 'platz' | 'gebaeude' | 'sonstiges'; - -export interface Veranstaltungsort { - id: string; - verein_id: string; - name: string; - adresse?: string; - typ?: OrtTyp; - aktiv: boolean; -} - -export interface OrtAusfall { - id: string; - ort_id: string; - von: string; - bis: string; - grund?: string; -} - -export interface Einladung { - id: string; - verein_id: string; - rolle: Rolle; - token: string; - genutzt: boolean; } export interface Beitrag { @@ -109,11 +53,13 @@ export interface Beitrag { export interface Einzug { id: string; + verein_id: string; mitglied_id: string; beitrag_id: string; betrag: number; - faellig_am?: string; + faelligkeitsdatum: string; status: EinzugStatus; + stripe_payment_intent_id?: string; } export interface Termin { @@ -125,11 +71,6 @@ export interface Termin { ende?: string; ort?: string; gruppe_ids: string[]; - durchfuehrender_id?: string; - verfuegbarkeit?: Verfuegbarkeit; - rrule?: string; - serie_id?: string; - ort_id?: string; } export interface Nachricht { @@ -139,5 +80,5 @@ export interface Nachricht { betreff: string; text: string; gruppe_ids: string[]; - gesendet_am?: string; + gesendet_am: string; } diff --git a/app/src/lib/user.ts b/app/src/lib/user.ts deleted file mode 100644 index 42f59a7..0000000 --- a/app/src/lib/user.ts +++ /dev/null @@ -1,34 +0,0 @@ -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(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(); diff --git a/app/src/routes/(app)/+layout.svelte b/app/src/routes/(app)/+layout.svelte index d6eec7c..b435cee 100644 --- a/app/src/routes/(app)/+layout.svelte +++ b/app/src/routes/(app)/+layout.svelte @@ -2,82 +2,34 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; import { onMount } from 'svelte'; - import { user } from '$lib/user'; - import Icon from '$lib/components/Icon.svelte'; - import type { IconName } from '$lib/icons'; + import { pb } from '$lib/pb'; let { children } = $props(); - let zeigInstallBanner = $state(false); - onMount(() => { - if (!$user) { goto('/login'); return; } - if (!$user.verein_id) { goto('/onboarding'); return; } - registerPush(); - - // iOS Safari: Banner anzeigen wenn App noch nicht installiert - const isIos = /iphone|ipad|ipod/i.test(navigator.userAgent); - const isStandalone = window.matchMedia('(display-mode: standalone)').matches - || (navigator as any).standalone === true; - const dismissed = sessionStorage.getItem('install-banner-dismissed'); - if (isIos && !isStandalone && !dismissed) zeigInstallBanner = true; + if (!pb.authStore.isValid) { + goto('/login'); + } }); - 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; - const keyRes = await fetch('/api/push/key'); - const { publicKey } = await keyRes.json(); - if (!publicKey) return; - let sub = await reg.pushManager.getSubscription(); - if (!sub) { - sub = await reg.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array(publicKey) as BufferSource, - }); - } - await fetch('/api/push/subscribe', { - method: 'POST', - 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); - } + function logout() { + pb.authStore.clear(); + goto('/login'); } - function urlBase64ToUint8Array(base64String: string): Uint8Array { - const padding = '='.repeat((4 - (base64String.length % 4)) % 4); - const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); - const raw = atob(base64); - return Uint8Array.from([...raw].map((c) => c.charCodeAt(0))); - } - - const isAdmin = () => !$user?.rolle || $user?.rolle === 'admin'; - - const allNavItems: { href: string; label: string; icon: IconName; adminOnly?: boolean }[] = [ - { href: '/neuigkeiten', label: 'Neuigkeiten', icon: 'images' }, - { href: '/mitglieder', label: 'Mitglieder', icon: 'users' }, - { href: '/termine', label: 'Termine', icon: 'calendar' }, - { href: '/beitraege', label: 'Beiträge', icon: 'currency-eur', adminOnly: true }, - { href: '/nachrichten', label: 'Nachrichten', icon: 'envelope' }, + const navItems = [ + { href: '/', label: 'Übersicht', icon: '⊞' }, + { href: '/mitglieder', label: 'Mitglieder', icon: '👥' }, + { href: '/termine', label: 'Termine', icon: '📅' }, + { href: '/beitraege', label: 'Beiträge', icon: '💶' }, + { href: '/nachrichten', label: 'Nachrichten', icon: '✉️' }, ]; - - const navItems = $derived(allNavItems.filter(i => !i.adminOnly || isAdmin()));
- - - - + +
@@ -90,23 +42,13 @@ href={item.href} class:active={$page.url.pathname === item.href} > - + {item.icon} {item.label} {/each}
-{#if zeigInstallBanner} -
-
- Zum Homescreen hinzufügen - Tippe auf TeilenZum Home-Bildschirm für die Vollbild-App -
- -
-{/if} - diff --git a/app/src/routes/(app)/+page.svelte b/app/src/routes/(app)/+page.svelte index 1074e92..32a9972 100644 --- a/app/src/routes/(app)/+page.svelte +++ b/app/src/routes/(app)/+page.svelte @@ -1,141 +1,72 @@ Übersicht — vereins.haus -{#if !loading && verein} -
- -
-

{verein.name}

- {#if verein.ort} - {verein.ort} - {/if} -
-
+

Willkommen, {vereinsname}

- {#if termine.length > 0} -
-

Nächste Termine

-
    - {#each termine as t (t.id)} -
  • - {t.titel} - {formatTermin(t)} -
  • - {/each} -
- Alle Termine → -
- {:else} -

Noch keine Termine geplant.

- {/if} -{/if} + diff --git a/app/src/routes/(app)/beitraege/+page.svelte b/app/src/routes/(app)/beitraege/+page.svelte index 306fc99..d1494b1 100644 --- a/app/src/routes/(app)/beitraege/+page.svelte +++ b/app/src/routes/(app)/beitraege/+page.svelte @@ -1,531 +1,27 @@ Beiträge — vereins.haus -
+ -{#if !loading && !hatSepa} -
- SEPA-Export ist im Starter-Plan verfügbar (7 €/Monat). - Jetzt upgraden → -
-{:else if sepaFehlt && !loading} -
- SEPA-Einzug nicht möglich: Gläubiger-ID, IBAN und BIC des Vereins fehlen noch in den Einstellungen. -
-{/if} - -{#if loading} -

Laden…

-{:else if beitraege.length === 0} -

Noch keine Beitragsarten — lege die erste an!

-{:else} -
    - {#each beitraege as b (b.id)} -
  • -
    - {b.name} - {#if b.beschreibung} - {b.beschreibung} - {/if} - - {b.betrag.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} - · {rhythmusLabel[b.rhythmus]} - -
    -
    - - - -
    -
  • - {/each} -
-{/if} - - -{#if showForm} - -{/if} - - -{#if sepaFor} - -{/if} +

SEPA-Beitragseinzug — in Entwicklung

diff --git a/app/src/routes/(app)/einstellungen/+page.svelte b/app/src/routes/(app)/einstellungen/+page.svelte deleted file mode 100644 index 6ec5012..0000000 --- a/app/src/routes/(app)/einstellungen/+page.svelte +++ /dev/null @@ -1,570 +0,0 @@ - - -Einstellungen — vereins.haus - -

Einstellungen

- -{#if loading} -

Laden…

-{:else} - - - {@const pi = planInfo[plan] ?? planInfo.free} -
-
-
- {pi.label} - {#if pi.limit} - bis {pi.limit} Mitglieder - {:else} - unbegrenzte Mitglieder - {/if} -
- - {mitgliederAnz} Mitglieder - -
- - {#if limitErreicht} -
- ⚠ Du hast das Limit von 50 Mitgliedern überschritten. Neue Mitglieder können nicht angelegt werden. -
- {/if} - -
    - {#each pi.features as f} -
  • ✓ {f}
  • - {/each} - {#if istFree} -
  • ✗ SEPA-Export Starter
  • -
  • ✗ iCal-Kalender-Abo Starter
  • - {/if} -
- - {#if istFree} - - Auf Starter upgraden – 7 €/Monat → - - {/if} -
- -
{ e.preventDefault(); speichern(); }}> - -
-

Vereinsprofil

- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
- -
-

Anschrift

- -
- - -
- -
-
- - -
-
- - -
-
- -
- - -
-
- -
-

SEPA-Lastschrift

-

- Für den SEPA-XML-Export. Gläubiger-ID beim Finanzamt oder der Bundesbank beantragen. -

- -
- - -
- -
- - -
- -
- - -
-
- -
-

Kalender-Abo

-

- Diese URL in Apple Calendar, Google Calendar oder Outlook eintragen – Termine erscheinen automatisch im Handy-Kalender. -

-
{typeof window !== 'undefined' ? window.location.origin : ''}/api/kalender/{vereinId}
- -

Für iOS: webcal:// statt https:// verwenden – öffnet direkt den Abonnieren-Dialog.

-
- - {#if isAdmin()} -
-

Durchführende

- {#if trainer.length === 0} -

Noch keine Trainer eingeladen.

- {:else} -
    - {#each trainer as t (t.id)} -
  • -
    - {t.name} - {trainerGruppen(t.id)} -
    - -
  • - {/each} -
- {/if} - - - - {#if einladungUrl} -
-

Einladungslink – einmalig verwendbar:

-
{einladungUrl}
- -
- {/if} -
- {/if} - - {#if error} -

{error}

- {/if} - {#if success} -

{success}

- {/if} - - - -
- -
- - -
-

Passwort ändern

-
- - -
-
- - -
-
- - -
- {#if pwError}

{pwError}

{/if} - {#if pwSuccess}

{pwSuccess}

{/if} - -
- -
- - Import / Export - -
- - -{/if} - - diff --git a/app/src/routes/(app)/import-export/+page.svelte b/app/src/routes/(app)/import-export/+page.svelte deleted file mode 100644 index 9fe214e..0000000 --- a/app/src/routes/(app)/import-export/+page.svelte +++ /dev/null @@ -1,429 +0,0 @@ - - -Import / Export — vereins.haus - -
- ← Einstellungen -

Import / Export

-
- -{#if loading} -

Laden…

-{:else} - - -
-

Mitglieder exportieren

-
- - - -
- {#if exportStatus} -

✓ {exportStatus}

- {/if} -
- -
-

Datensicherung

-

Vollständige Sicherungskopie aller Vereinsdaten als JSON – für Archivierung oder Wechsel der Software (DSGVO Art. 20).

- -
- - -
-

Mitglieder importieren

-

CSV-Datei hochladen – die Spalten werden automatisch erkannt und können vor dem Import angepasst werden.

- - {#if importPhase === 'idle'} - - - {:else if importPhase === 'mapping'} -
- {csvRows.length} Zeilen erkannt · Spalten zuordnen: - -
- -
-
- CSV-Spalte - → Vereinshaus-Feld - Vorschau (1. Zeile) -
- {#each csvHeaders as h} -
- {h} - - {csvRows[0]?.[h] ?? ''} -
- {/each} -
- -
-

Vor- und Nachname sind Pflichtfelder.

- -
- - {:else if importPhase === 'done'} -
0}> -

✓ {importResult.ok} Mitglieder importiert

- {#if importResult.fehler.length > 0} -

{importResult.fehler.length} Probleme:

-
    - {#each importResult.fehler as f} -
  • {f}
  • - {/each} -
- {/if} - -
- {/if} -
- -{/if} - - diff --git a/app/src/routes/(app)/mitglieder/+page.svelte b/app/src/routes/(app)/mitglieder/+page.svelte index 881b71e..21cfe58 100644 --- a/app/src/routes/(app)/mitglieder/+page.svelte +++ b/app/src/routes/(app)/mitglieder/+page.svelte @@ -1,174 +1,27 @@ Mitglieder — vereins.haus -
+ - - -{#if loading} -

Laden…

-{:else if filtered.length === 0} -

- {search ? 'Keine Treffer.' : 'Noch keine Mitglieder — lege das erste an!'} -

-{:else} - -{/if} - -+ +

Mitgliederverwaltung — in Entwicklung

diff --git a/app/src/routes/(app)/mitglieder/[id]/+page.svelte b/app/src/routes/(app)/mitglieder/[id]/+page.svelte deleted file mode 100644 index 0d33a1a..0000000 --- a/app/src/routes/(app)/mitglieder/[id]/+page.svelte +++ /dev/null @@ -1,503 +0,0 @@ - - -{vorname} {nachname} — vereins.haus - -
- ← Mitglieder - {#if !loading} - - {/if} -
- -{#if loading} -

Laden…

- -{:else if !editMode} - -
-
{vorname[0]}{nachname[0]}
-

{vorname} {nachname}

- {status} -
- -
-

Kontakt

- {#if email} -
- E-Mail - {email} -
- {/if} - {#if telefon} -
- Telefon - {telefon} -
- {/if} - {#if strasse || plz || ort} -
- Adresse - {[strasse, [plz, ort].filter(Boolean).join(' ')].filter(Boolean).join(', ')} -
- {/if} - {#if !email && !telefon && !strasse} -

Keine Kontaktdaten hinterlegt.

- {/if} -
- -
-

Mitgliedschaft

- {#if eintrittsdatum} -
- Eintritt - {formatDatum(eintrittsdatum)} -
- {/if} - {#if geburtsdatum} -
- Geburtsdatum - {formatDatum(geburtsdatum)} -
- {/if} - {#if austrittsdatum} -
- Austritt - {formatDatum(austrittsdatum)} -
- {/if} - {#if gruppe_ids?.length} -
- Gruppen - {gruppenName(gruppe_ids)} -
- {/if} -
- - {#if iban || bic || mandatsreferenz} -
-

SEPA-Lastschrift

- {#if iban} -
- IBAN - {iban} -
- {/if} - {#if bic} -
- BIC - {bic} -
- {/if} - {#if mandatsreferenz} -
- Mandat - {mandatsreferenz}{mandatsdatum ? ' · ' + formatDatum(mandatsdatum) : ''} -
- {/if} -
- {/if} - - {#if notizen} -
-

Notizen

-

{notizen}

-
- {/if} - - - -{:else} - -
{ e.preventDefault(); speichern(); }}> - -
-

Stammdaten

-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
- -
-

Kontakt

-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
-
- -
-

SEPA-Lastschrift

-
- - -
-
- - -
-
-
- - -
-
- - -
-
-
- - {#if gruppen.length > 0} -
-

Gruppen

-
- {#each gruppen as g (g.id)} - - {/each} -
-
- {/if} - -
-

Notizen

-
- - -
-
- - {#if error} -

{error}

- {/if} - -
- -
-
-{/if} - - -{#if showDelete} - -{/if} - - diff --git a/app/src/routes/(app)/mitglieder/neu/+page.svelte b/app/src/routes/(app)/mitglieder/neu/+page.svelte deleted file mode 100644 index 7830989..0000000 --- a/app/src/routes/(app)/mitglieder/neu/+page.svelte +++ /dev/null @@ -1,257 +0,0 @@ - - -Neues Mitglied — vereins.haus - -
- ← Zurück -

Neues Mitglied

-
- -
{ e.preventDefault(); speichern(); }}> - -
-

Stammdaten

-
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
-
- -
-
- - -
-
-
- -
-

Kontakt

-
- - -
-
- - -
-
- -
-

Adresse

-
- - -
-
-
- - -
-
- - -
-
-
- -
-

SEPA-Lastschrift

-
- - -
-
- - -
-
-
- - -
-
- - -
-
-
- - {#if gruppen.length > 0} -
-

Gruppen

-
- {#each gruppen as g (g.id)} - - {/each} -
-
- {/if} - -
-

Notizen

-
- - -
-
- - {#if error} -

{error}

- {/if} - -
- Abbrechen - -
- -
- - diff --git a/app/src/routes/(app)/nachrichten/+page.svelte b/app/src/routes/(app)/nachrichten/+page.svelte index 33a6a34..6956026 100644 --- a/app/src/routes/(app)/nachrichten/+page.svelte +++ b/app/src/routes/(app)/nachrichten/+page.svelte @@ -1,330 +1,27 @@ Nachrichten — vereins.haus -
+ -{#if sendSuccess} -
{sendSuccess}
-{/if} - -{#if loading} -

Laden…

-{:else if nachrichten.length === 0} -

Noch keine Nachrichten versendet.

-{:else} -
    - {#each nachrichten as n (n.id)} -
  • -
    - {n.betreff} - {formatDatum(n.gesendet_am)} -
    - {gruppenLabel(n.gruppe_ids ?? [])} - {#if n.text} -

    {stripHtml(n.text).slice(0, 120)}{stripHtml(n.text).length > 120 ? '…' : ''}

    - {/if} -
  • - {/each} -
-{/if} - - -{#if showForm} - -{/if} +

Nachrichten & Push-Benachrichtigungen — in Entwicklung

diff --git a/app/src/routes/(app)/neuigkeiten/+page.svelte b/app/src/routes/(app)/neuigkeiten/+page.svelte deleted file mode 100644 index 4ef9432..0000000 --- a/app/src/routes/(app)/neuigkeiten/+page.svelte +++ /dev/null @@ -1,476 +0,0 @@ - - -Neuigkeiten — vereins.haus - -
-

Neuigkeiten

- {#if canPost()} - - {/if} -
- -{#if loading} -

Laden…

-{:else if ladeError} -

{ladeError}

-{:else if neuigkeiten.length === 0} -

Noch keine Beiträge – schreib den ersten!

-{:else} -
- {#each neuigkeiten as n (n.id)} -
-
-
{autorName(n)[0]?.toUpperCase()}
-
- {autorName(n)} - {zeitAgo(n.created)} -
- {#if n.autor_id === userId()} - - {/if} -
- - {#if n.termin_id} -
📅 {terminName(n.termin_id)}
- {/if} - - {#if n.text} -

{n.text}

- {/if} - - {#if n.medien?.length > 0} -
- {#each n.medien as datei (datei)} - {#if isVideo(datei)} - - {:else} - - {/if} - {/each} -
- {/if} - -
- - {#if n.gruppe_ids?.length > 0} - - {n.gruppe_ids.length === 1 - ? (gruppen.find(g => g.id === n.gruppe_ids[0])?.name ?? '') - : `${n.gruppe_ids.length} Gruppen`} - - {/if} -
-
- {/each} -
-{/if} - - -{#if showForm} - -{/if} - - -{#if lightboxUrl} - -{/if} - - diff --git a/app/src/routes/(app)/orte/+page.svelte b/app/src/routes/(app)/orte/+page.svelte deleted file mode 100644 index 71fcab8..0000000 --- a/app/src/routes/(app)/orte/+page.svelte +++ /dev/null @@ -1,345 +0,0 @@ - - -Veranstaltungsorte — vereins.haus - -
-
- ← Termine -

Veranstaltungsorte

-
- -
- -{#if loading} -

Laden…

-{:else if orte.length === 0} -

Noch keine Orte angelegt.

-{:else} -
    - {#each orte as o (o.id)} - {@const gesperrt = istGesperrt(o)} -
  • -
    -
    - {o.name} - {#if !o.aktiv} - Inaktiv - {:else if gesperrt} - Gesperrt - {:else} - Verfügbar - {/if} -
    - {#if o.adresse}{o.adresse}{/if} - {#if o.typ}{typLabel[o.typ]}{/if} - {#if gesperrt} - - Gesperrt bis {formatDatum(gesperrt.bis)}{gesperrt.grund ? ' · ' + gesperrt.grund : ''} - - {/if} -
    -
    - - - -
    -
  • - {/each} -
-{/if} - - -{#if ausfaelle.length > 0} -

Geplante Ausfälle

-
    - {#each ausfaelle as a (a.id)} -
  • -
    - {ortName(a.ort_id)} - {formatDatum(a.von)} – {formatDatum(a.bis)} - {#if a.grund}{a.grund}{/if} -
    - -
  • - {/each} -
-{/if} - - -{#if showOrtForm} - -{/if} - - -{#if showAusfallForm} - -{/if} - - diff --git a/app/src/routes/(app)/termine/+page.svelte b/app/src/routes/(app)/termine/+page.svelte index e872980..4f1f5ed 100644 --- a/app/src/routes/(app)/termine/+page.svelte +++ b/app/src/routes/(app)/termine/+page.svelte @@ -1,716 +1,27 @@ Termine — vereins.haus -
+ -{#if isAdmin() && offene.length > 0} -
- ⚠ {offene.length} {offene.length === 1 ? 'Termin benötigt' : 'Termine benötigen'} eine Bestätigung -
-{/if} - -{#if loading} -

Laden…

-{:else if ansicht !== 'liste'} -
- -
-{:else if termine.length === 0} -

Noch keine Termine geplant.

-{:else} - {#if upcoming.length > 0} -
    - {#each upcoming as t (t.id)} -
  • -
    - {new Date(t.beginn).toLocaleDateString('de-DE', { weekday: 'short' })} - {new Date(t.beginn).getDate()} - {new Date(t.beginn).toLocaleDateString('de-DE', { month: 'short' })} -
    - -
    -
    - {t.titel} - {#if t.serie_id}{/if} -
    - - {formatZeit(t.beginn)}{t.ende ? ' – ' + formatZeit(t.ende) : ''}{t.ort ? ' · ' + t.ort : ''} - - {#if t.ort_id || t.ort} - {ortNameById(t.ort_id) || t.ort} - {/if} - {#if t.gruppe_ids?.length} - {gruppenLabel(t.gruppe_ids)} - {/if} - {#if ortAusfall(t)} - {@const af = ortAusfall(t)!} - ⚠ Ort gesperrt{af.grund ? ': ' + af.grund : ''} - {/if} - - {#if t.durchfuehrender_id} -
    - {#if t.verfuegbarkeit && t.verfuegbarkeit !== 'offen'} - - ● {verfuegbarkeitConfig[t.verfuegbarkeit].label} - - {:else} - ● Offen - {/if} - {#if isAdmin() && userName(t.durchfuehrender_id)} - → {userName(t.durchfuehrender_id)} - {/if} -
    - - {#if istMeinTermin(t)} -
    - - - -
    - {/if} - {/if} -
    - - {#if isAdmin()} -
    - - -
    - {/if} -
  • - {/each} -
- {/if} - - {#if vergangen.length > 0} -
- Vergangene Termine ({vergangen.length}) -
    - {#each vergangen as t (t.id)} -
  • -
    - {new Date(t.beginn).toLocaleDateString('de-DE', { weekday: 'short' })} - {new Date(t.beginn).getDate()} - {new Date(t.beginn).toLocaleDateString('de-DE', { month: 'short' })} -
    -
    - {t.titel} - {formatDatum(t.beginn)}{t.ort ? ' · ' + t.ort : ''} -
    - {#if isAdmin()} -
    - -
    - {/if} -
  • - {/each} -
-
- {/if} -{/if} - - -{#if showForm && isAdmin()} - -{/if} - - -{#if showDelete} - {@const t = termine.find(x => x.id === showDelete)!} - -{/if} +

Terminkalender — in Entwicklung

diff --git a/app/src/routes/(auth)/+layout.svelte b/app/src/routes/(auth)/+layout.svelte index c91c827..b971aac 100644 --- a/app/src/routes/(auth)/+layout.svelte +++ b/app/src/routes/(auth)/+layout.svelte @@ -1,13 +1,12 @@ @@ -32,8 +31,8 @@ :global(body) { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - color: var(--c-text); - background: var(--c-bg-subtle); + color: #1e293b; + background: #f8fafc; line-height: 1.6; -webkit-font-smoothing: antialiased; } diff --git a/app/src/routes/api/auth/login/+server.ts b/app/src/routes/api/auth/login/+server.ts deleted file mode 100644 index 88156db..0000000 --- a/app/src/routes/api/auth/login/+server.ts +++ /dev/null @@ -1,18 +0,0 @@ -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 }); -} diff --git a/app/src/routes/api/auth/me/+server.ts b/app/src/routes/api/auth/me/+server.ts deleted file mode 100644 index 5071e53..0000000 --- a/app/src/routes/api/auth/me/+server.ts +++ /dev/null @@ -1,11 +0,0 @@ -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); -} diff --git a/app/src/routes/api/auth/register/+server.ts b/app/src/routes/api/auth/register/+server.ts deleted file mode 100644 index b82508e..0000000 --- a/app/src/routes/api/auth/register/+server.ts +++ /dev/null @@ -1,24 +0,0 @@ -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 }); -} diff --git a/app/src/routes/api/beitraege/+server.ts b/app/src/routes/api/beitraege/+server.ts deleted file mode 100644 index e894c5d..0000000 --- a/app/src/routes/api/beitraege/+server.ts +++ /dev/null @@ -1,37 +0,0 @@ -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[])); -} - -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), { status: 201 }); -} diff --git a/app/src/routes/api/beitraege/[id]/+server.ts b/app/src/routes/api/beitraege/[id]/+server.ts deleted file mode 100644 index 43b57da..0000000 --- a/app/src/routes/api/beitraege/[id]/+server.ts +++ /dev/null @@ -1,52 +0,0 @@ -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)); -} - -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)); -} - -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 }); -} diff --git a/app/src/routes/api/einladungen/+server.ts b/app/src/routes/api/einladungen/+server.ts deleted file mode 100644 index bfbcce3..0000000 --- a/app/src/routes/api/einladungen/+server.ts +++ /dev/null @@ -1,23 +0,0 @@ -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), { status: 201 }); -} diff --git a/app/src/routes/api/einladungen/[token]/+server.ts b/app/src/routes/api/einladungen/[token]/+server.ts deleted file mode 100644 index 9631d8c..0000000 --- a/app/src/routes/api/einladungen/[token]/+server.ts +++ /dev/null @@ -1,62 +0,0 @@ -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)); -} - -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 | 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 } - ); -} diff --git a/app/src/routes/api/einzuege/+server.ts b/app/src/routes/api/einzuege/+server.ts deleted file mode 100644 index 9acc51e..0000000 --- a/app/src/routes/api/einzuege/+server.ts +++ /dev/null @@ -1,58 +0,0 @@ -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[])); -} - -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[]), { status: 201 }); -} diff --git a/app/src/routes/api/files/[...path]/+server.ts b/app/src/routes/api/files/[...path]/+server.ts deleted file mode 100644 index b886015..0000000 --- a/app/src/routes/api/files/[...path]/+server.ts +++ /dev/null @@ -1,26 +0,0 @@ -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 = { - 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' - } - }); -} diff --git a/app/src/routes/api/gruppen/+server.ts b/app/src/routes/api/gruppen/+server.ts deleted file mode 100644 index 033387b..0000000 --- a/app/src/routes/api/gruppen/+server.ts +++ /dev/null @@ -1,36 +0,0 @@ -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[])); -} - -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), { status: 201 }); -} diff --git a/app/src/routes/api/gruppen/[id]/+server.ts b/app/src/routes/api/gruppen/[id]/+server.ts deleted file mode 100644 index 799fa9c..0000000 --- a/app/src/routes/api/gruppen/[id]/+server.ts +++ /dev/null @@ -1,51 +0,0 @@ -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)); -} - -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)); -} - -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 }); -} diff --git a/app/src/routes/api/kalender/[vereinId]/+server.ts b/app/src/routes/api/kalender/[vereinId]/+server.ts deleted file mode 100644 index 9ab614e..0000000 --- a/app/src/routes/api/kalender/[vereinId]/+server.ts +++ /dev/null @@ -1,53 +0,0 @@ -import ical from 'ical-generator'; -import { getDb } from '$lib/server/db'; - -export async function GET({ params }) { - const { vereinId } = params; - - 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 }); - } - - // 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 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({ - name: verein.name ?? 'vereins.haus', - prodId: '//vereins.haus//Vereinskalender//DE', - timezone: 'Europe/Berlin', - ttl: 60 * 60, // Clients aktualisieren stündlich - }); - - for (const t of termine) { - const start = new Date(t.beginn); - const end = t.ende - ? new Date(t.ende) - : new Date(start.getTime() + 60 * 60 * 1000); // Default: 1 Stunde - - const ev = cal.createEvent({ - start, - end, - summary: t.titel, - location: t.ort ?? undefined, - description: t.beschreibung ?? undefined, - }); - ev.uid(t.id + '@vereins.haus'); - } - - return new Response(cal.toString(), { - headers: { - 'Content-Type': 'text/calendar; charset=utf-8', - 'Content-Disposition': `attachment; filename="${encodeURIComponent(verein.name ?? 'kalender')}.ics"`, - 'Cache-Control': 'no-cache', - }, - }); -} diff --git a/app/src/routes/api/mitglieder/+server.ts b/app/src/routes/api/mitglieder/+server.ts deleted file mode 100644 index 1037048..0000000 --- a/app/src/routes/api/mitglieder/+server.ts +++ /dev/null @@ -1,65 +0,0 @@ -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[])); -} - -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), { status: 201 }); -} diff --git a/app/src/routes/api/mitglieder/[id]/+server.ts b/app/src/routes/api/mitglieder/[id]/+server.ts deleted file mode 100644 index 769b33b..0000000 --- a/app/src/routes/api/mitglieder/[id]/+server.ts +++ /dev/null @@ -1,69 +0,0 @@ -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)); -} - -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)); -} - -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 }); -} diff --git a/app/src/routes/api/nachrichten/+server.ts b/app/src/routes/api/nachrichten/+server.ts deleted file mode 100644 index f4e63a0..0000000 --- a/app/src/routes/api/nachrichten/+server.ts +++ /dev/null @@ -1,39 +0,0 @@ -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[])); -} - -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), { status: 201 }); -} diff --git a/app/src/routes/api/nachrichten/[id]/+server.ts b/app/src/routes/api/nachrichten/[id]/+server.ts deleted file mode 100644 index 40f1678..0000000 --- a/app/src/routes/api/nachrichten/[id]/+server.ts +++ /dev/null @@ -1,13 +0,0 @@ -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 }); -} diff --git a/app/src/routes/api/neuigkeiten/+server.ts b/app/src/routes/api/neuigkeiten/+server.ts deleted file mode 100644 index 9d00bc2..0000000 --- a/app/src/routes/api/neuigkeiten/+server.ts +++ /dev/null @@ -1,68 +0,0 @@ -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[])); -} - -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), { status: 201 }); -} diff --git a/app/src/routes/api/neuigkeiten/[id]/+server.ts b/app/src/routes/api/neuigkeiten/[id]/+server.ts deleted file mode 100644 index c390a91..0000000 --- a/app/src/routes/api/neuigkeiten/[id]/+server.ts +++ /dev/null @@ -1,27 +0,0 @@ -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 | 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 }); -} diff --git a/app/src/routes/api/ort-ausfaelle/+server.ts b/app/src/routes/api/ort-ausfaelle/+server.ts deleted file mode 100644 index 1c7ec83..0000000 --- a/app/src/routes/api/ort-ausfaelle/+server.ts +++ /dev/null @@ -1,58 +0,0 @@ -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[])); -} - -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), { status: 201 }); -} diff --git a/app/src/routes/api/ort-ausfaelle/[id]/+server.ts b/app/src/routes/api/ort-ausfaelle/[id]/+server.ts deleted file mode 100644 index df7e2f4..0000000 --- a/app/src/routes/api/ort-ausfaelle/[id]/+server.ts +++ /dev/null @@ -1,21 +0,0 @@ -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 }); -} diff --git a/app/src/routes/api/orte/+server.ts b/app/src/routes/api/orte/+server.ts deleted file mode 100644 index 73d7452..0000000 --- a/app/src/routes/api/orte/+server.ts +++ /dev/null @@ -1,37 +0,0 @@ -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[])); -} - -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), { status: 201 }); -} diff --git a/app/src/routes/api/orte/[id]/+server.ts b/app/src/routes/api/orte/[id]/+server.ts deleted file mode 100644 index 25d18b9..0000000 --- a/app/src/routes/api/orte/[id]/+server.ts +++ /dev/null @@ -1,52 +0,0 @@ -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)); -} - -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)); -} - -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 }); -} diff --git a/app/src/routes/api/push/key/+server.ts b/app/src/routes/api/push/key/+server.ts deleted file mode 100644 index 7399d91..0000000 --- a/app/src/routes/api/push/key/+server.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { json } from '@sveltejs/kit'; -import { env } from '$env/dynamic/private'; - -export async function GET() { - return json({ publicKey: env.PUBLIC_VAPID_KEY ?? '' }); -} diff --git a/app/src/routes/api/push/senden/+server.ts b/app/src/routes/api/push/senden/+server.ts deleted file mode 100644 index 3e0575b..0000000 --- a/app/src/routes/api/push/senden/+server.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { json } from '@sveltejs/kit'; -import { env } from '$env/dynamic/private'; -import webpush from 'web-push'; -import { requireAuth } from '$lib/server/auth'; -import { getDb } from '$lib/server/db'; - -export async function POST({ request }) { - 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 }); - - const vapidPublic = env.PUBLIC_VAPID_KEY ?? ''; - const vapidPrivate = env.VAPID_PRIVATE_KEY ?? ''; - const vapidSubject = env.VAPID_SUBJECT ?? 'mailto:info@vereins.haus'; - - if (!vapidPublic || !vapidPrivate) { - return json({ error: 'VAPID-Keys nicht konfiguriert.' }, { status: 500 }); - } - - webpush.setVapidDetails(vapidSubject, vapidPublic, vapidPrivate); - - 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 (!items.length) return json({ sent: 0 }); - - const payload = JSON.stringify({ title: titel, body, url }); - let sent = 0; - - await Promise.allSettled( - items.map(async (sub) => { - try { - await webpush.sendNotification( - { endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } }, - payload, - ); - sent++; - } catch (err: unknown) { - // 410 Gone = Subscription abgelaufen → löschen - if ((err as { statusCode?: number }).statusCode === 410) { - db.prepare('DELETE FROM push_subscriptions WHERE id = ?').run(sub.id); - } - } - }), - ); - - return json({ sent }); -} diff --git a/app/src/routes/api/push/subscribe/+server.ts b/app/src/routes/api/push/subscribe/+server.ts deleted file mode 100644 index 34b10dc..0000000 --- a/app/src/routes/api/push/subscribe/+server.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { json } from '@sveltejs/kit'; -import { requireAuth } from '$lib/server/auth'; -import { getDb, newId } from '$lib/server/db'; - -export async function POST({ request }) { - const authUser = await requireAuth(request).catch(() => null); - if (!authUser) return json({ error: 'Nicht authentifiziert.' }, { status: 401 }); - - const { subscription } = await request.json(); - - if (!subscription?.endpoint) { - return json({ error: 'Ungültige Subscription.' }, { status: 400 }); - } - - 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 - 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, - ); - - return json({ success: true }); -} - -export async function DELETE({ request }) { - const authUser = await requireAuth(request).catch(() => null); - if (!authUser) return json({ success: true }); - - const db = getDb(); - db.prepare('DELETE FROM push_subscriptions WHERE user_id = ?').run(authUser.sub); - - return json({ success: true }); -} diff --git a/app/src/routes/api/reaktionen/+server.ts b/app/src/routes/api/reaktionen/+server.ts deleted file mode 100644 index d48116b..0000000 --- a/app/src/routes/api/reaktionen/+server.ts +++ /dev/null @@ -1,58 +0,0 @@ -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[])); -} - -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), { status: 201 }); -} diff --git a/app/src/routes/api/reaktionen/[id]/+server.ts b/app/src/routes/api/reaktionen/[id]/+server.ts deleted file mode 100644 index a2ed16d..0000000 --- a/app/src/routes/api/reaktionen/[id]/+server.ts +++ /dev/null @@ -1,13 +0,0 @@ -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 }); -} diff --git a/app/src/routes/api/termine/+server.ts b/app/src/routes/api/termine/+server.ts deleted file mode 100644 index 86b5543..0000000 --- a/app/src/routes/api/termine/+server.ts +++ /dev/null @@ -1,63 +0,0 @@ -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[])); -} - -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), { status: 201 }); -} diff --git a/app/src/routes/api/termine/[id]/+server.ts b/app/src/routes/api/termine/[id]/+server.ts deleted file mode 100644 index 0af6d50..0000000 --- a/app/src/routes/api/termine/[id]/+server.ts +++ /dev/null @@ -1,83 +0,0 @@ -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)); -} - -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)); -} - -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 }); -} diff --git a/app/src/routes/api/users/+server.ts b/app/src/routes/api/users/+server.ts deleted file mode 100644 index 4f8e7d4..0000000 --- a/app/src/routes/api/users/+server.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { json } from '@sveltejs/kit'; -import { getDb, rows } 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 rolle = url.searchParams.get('rolle'); - - let query = 'SELECT id, verein_id, email, name, rolle, created FROM users WHERE verein_id = ?'; - const params: unknown[] = [u.verein_id]; - - if (rolle) { query += ' AND rolle = ?'; params.push(rolle); } - query += ' ORDER BY name'; - - const users = db.prepare(query).all(...params); - return json(users); -} diff --git a/app/src/routes/api/users/[id]/+server.ts b/app/src/routes/api/users/[id]/+server.ts deleted file mode 100644 index 8a18995..0000000 --- a/app/src/routes/api/users/[id]/+server.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { json, error } from '@sveltejs/kit'; -import { getDb } from '$lib/server/db'; -import { requireAuth, hashPassword } from '$lib/server/auth'; - -export async function GET({ request, params }) { - const u = await requireAuth(request); - const db = getDb(); - const row = db.prepare( - 'SELECT id, verein_id, email, name, rolle, created FROM users WHERE id = ? AND verein_id = ?' - ).get(params.id, u.verein_id); - if (!row) throw error(404, 'User nicht gefunden'); - return json(row); -} - -export async function PUT({ request, params }) { - const u = await requireAuth(request); - const db = getDb(); - const body = await request.json(); - - const existing = db.prepare('SELECT id FROM users WHERE id = ? AND verein_id = ?').get(params.id, u.verein_id); - if (!existing) throw error(404, 'User nicht gefunden'); - - const fields: string[] = []; - const vals: unknown[] = []; - - if (body.name !== undefined) { fields.push('name = ?'); vals.push(body.name); } - if (body.email !== undefined) { fields.push('email = ?'); vals.push(body.email.toLowerCase()); } - if (body.rolle !== undefined) { fields.push('rolle = ?'); vals.push(body.rolle || null); } - if (body.password) { fields.push('password_hash = ?'); vals.push(await hashPassword(body.password)); } - if (!fields.length) throw error(400, 'Keine Felder zum Aktualisieren'); - - fields.push("updated = strftime('%Y-%m-%dT%H:%M:%SZ','now')"); - vals.push(params.id, u.verein_id); - - db.prepare(`UPDATE users SET ${fields.join(', ')} WHERE id = ? AND verein_id = ?`).run(...vals); - const row = db.prepare('SELECT id, verein_id, email, name, rolle, created FROM users WHERE id = ?').get(params.id); - return json(row); -} - -export async function DELETE({ request, params }) { - const u = await requireAuth(request); - if (u.sub === params.id) throw error(400, 'Eigenen Account nicht löschbar'); - const db = getDb(); - const result = db.prepare('DELETE FROM users WHERE id = ? AND verein_id = ?').run(params.id, u.verein_id); - if (result.changes === 0) throw error(404, 'User nicht gefunden'); - return new Response(null, { status: 204 }); -} diff --git a/app/src/routes/api/vereine/+server.ts b/app/src/routes/api/vereine/+server.ts deleted file mode 100644 index 242ab0d..0000000 --- a/app/src/routes/api/vereine/+server.ts +++ /dev/null @@ -1,39 +0,0 @@ -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)); -} - -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)); -} diff --git a/app/src/routes/favicon.ico/+server.ts b/app/src/routes/favicon.ico/+server.ts deleted file mode 100644 index fbf5ba3..0000000 --- a/app/src/routes/favicon.ico/+server.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { redirect } from '@sveltejs/kit'; - -export function GET() { - throw redirect(301, '/favicon.svg'); -} diff --git a/app/src/routes/invite/[token]/+page.svelte b/app/src/routes/invite/[token]/+page.svelte deleted file mode 100644 index e1c36c7..0000000 --- a/app/src/routes/invite/[token]/+page.svelte +++ /dev/null @@ -1,173 +0,0 @@ - - -Einladung — vereins.haus - -
- - -
- {#if loading} -

Prüfe Einladung…

- - {:else if fehler} -
-

Ungültige Einladung

-

{fehler}

- - {:else} -
🎉
-

Du wurdest eingeladen!

-

- Du wirst als {einladung?.rolle === 'trainer' ? 'Trainer' : 'Admin'} - zum Verein „{vereinName}" hinzugefügt. -

- -
{ e.preventDefault(); registrieren(); }}> -
- - -
-
- - -
-
- - -
-
- - -
- - {#if formError} -

{formError}

- {/if} - - -
- {/if} -
-
- - diff --git a/app/src/routes/onboarding/+page.svelte b/app/src/routes/onboarding/+page.svelte deleted file mode 100644 index de2c257..0000000 --- a/app/src/routes/onboarding/+page.svelte +++ /dev/null @@ -1,334 +0,0 @@ - - -Einrichtung — vereins.haus - -
- - -
- -
- {#each [1, 2, 3] as s} - = s}> - {/each} -
- - {#if schritt === 1} - -
-
👋
-

Schön, dass du dabei bist!

-

- vereins.haus hilft deinem Vorstand, Mitglieder, Termine und - Beiträge zu verwalten – übersichtlich, mobil und ohne Excel. -

-

- Die Einrichtung dauert weniger als eine Minute. -

- -
- - {:else if schritt === 2} - -
-

Dein Verein

-

Wie heißt dein Verein? Du kannst alle Angaben später noch ändern.

- -
{ e.preventDefault(); vereinAnlegen(); }}> -
- - -
- -
- - -
- - {#if error} -

{error}

- {/if} - -
- - -
-
-
- - {:else} - -
-
-

Alles bereit!

-

- „{fertigName}" wurde eingerichtet. Du kannst jetzt loslegen. -

- -
    -
  • - 👤 -
    - Mitglieder anlegen - Namen, E-Mail und IBAN für den SEPA-Einzug -
    -
  • -
  • - 💶 -
    - Beitragsarten definieren - Jahresbeitrag, Sonderumlage – mit SEPA-Export -
    -
  • -
  • - 📅 -
    - Termine eintragen - Versammlung, Training, Ausflug -
    -
  • -
- - -
- {/if} -
-
- - diff --git a/app/src/sw.ts b/app/src/sw.ts deleted file mode 100644 index 3144a2a..0000000 --- a/app/src/sw.ts +++ /dev/null @@ -1,45 +0,0 @@ -/// -import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching'; -import { registerRoute } from 'workbox-routing'; -import { NetworkFirst } from 'workbox-strategies'; - -declare const self: ServiceWorkerGlobalScope; - -cleanupOutdatedCaches(); -precacheAndRoute(self.__WB_MANIFEST); - -// PocketBase API: NetworkFirst (offline-fähig aus Cache) -registerRoute( - ({ url }) => url.hostname === 'api.vereins.haus', - new NetworkFirst({ - cacheName: 'pocketbase-api', - networkTimeoutSeconds: 5, - }), -); - -self.addEventListener('push', (event) => { - const data = event.data?.json() ?? {}; - const title = data.title ?? 'vereins.haus'; - const options: NotificationOptions = { - body: data.body ?? '', - icon: '/icons/icon-192.png', - badge: '/icons/icon-192.png', - tag: data.tag ?? 'vereinshaus', - data: { url: data.url ?? '/nachrichten' }, - }; - event.waitUntil(self.registration.showNotification(title, options)); -}); - -self.addEventListener('notificationclick', (event) => { - event.notification.close(); - const url = (event.notification.data as { url: string })?.url ?? '/'; - event.waitUntil( - self.clients - .matchAll({ type: 'window', includeUncontrolled: true }) - .then((list) => { - const existing = list.find((c) => c.url.includes(url)); - if (existing) return existing.focus(); - return self.clients.openWindow(url); - }), - ); -}); diff --git a/app/static/apple-touch-icon.png b/app/static/apple-touch-icon.png deleted file mode 100644 index cb64ca0..0000000 Binary files a/app/static/apple-touch-icon.png and /dev/null differ diff --git a/app/static/favicon.svg b/app/static/favicon.svg deleted file mode 100644 index 1b5e611..0000000 --- a/app/static/favicon.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/app/static/icons/icon-192.png b/app/static/icons/icon-192.png deleted file mode 100644 index 3a02f99..0000000 Binary files a/app/static/icons/icon-192.png and /dev/null differ diff --git a/app/static/icons/icon-512.png b/app/static/icons/icon-512.png deleted file mode 100644 index eb73fb8..0000000 Binary files a/app/static/icons/icon-512.png and /dev/null differ diff --git a/app/static/icons/icon-512.svg b/app/static/icons/icon-512.svg deleted file mode 100644 index 6412f9e..0000000 --- a/app/static/icons/icon-512.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/app/static/logo.svg b/app/static/logo.svg deleted file mode 100644 index 64f479b..0000000 --- a/app/static/logo.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - vereins.haus - Die einfache Vereins-App - diff --git a/app/vite.config.ts b/app/vite.config.ts index 41b6e6c..78f8947 100644 --- a/app/vite.config.ts +++ b/app/vite.config.ts @@ -6,28 +6,34 @@ export default defineConfig({ plugins: [ sveltekit(), VitePWA({ - strategies: 'injectManifest', - srcDir: 'src', - filename: 'sw.ts', registerType: 'autoUpdate', manifest: { name: 'vereins.haus', short_name: 'vereins.haus', description: 'Vereinsverwaltung die einfach funktioniert', - theme_color: '#0F172A', - background_color: '#0F172A', + theme_color: '#1e40af', + background_color: '#f8fafc', display: 'standalone', start_url: '/', icons: [ + { src: '/icons/icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' }, { src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable' }, - { src: '/icons/icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any maskable' }, - { src: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png', purpose: 'any' }, - { src: '/favicon.svg', sizes: 'any', type: 'image/svg+xml', purpose: 'any' } + { src: '/icons/apple-touch-icon.png', sizes: '180x180', type: 'image/png' } ] }, - injectManifest: { + workbox: { globPatterns: ['**/*.{js,css,html,svg,png,ico}'], - }, - }), - ], + runtimeCaching: [ + { + urlPattern: /^https:\/\/api\.vereins\.haus\/.*/i, + handler: 'NetworkFirst', + options: { + cacheName: 'pocketbase-api', + expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 } + } + } + ] + } + }) + ] }); diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml deleted file mode 100644 index 7e4208b..0000000 --- a/docker-compose.staging.yml +++ /dev/null @@ -1,33 +0,0 @@ -version: "3.8" - -services: - app-staging: - build: - context: ./app - dockerfile: Dockerfile - 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} - - BREVO_KEY=${BREVO_KEY} - - BREVO_SENDER=${BREVO_SENDER:-noreply@vereins.haus} - networks: - default: {} - npm_bridge: - ipv4_address: 172.25.0.13 - -networks: - npm_bridge: - external: true - name: nginx-proxy-manager_bridge_net diff --git a/docker-compose.yml b/docker-compose.yml index ee2a79a..bff1658 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,21 @@ version: "3.8" services: + pocketbase: + image: ghcr.io/muchobien/pocketbase:latest + container_name: vereinshaus-pocketbase + restart: unless-stopped + volumes: + - /volume1/docker/vereinshaus/pocketbase/data:/pb_data + - /volume1/docker/vereinshaus/pocketbase/storage:/pb_public + - /volume1/docker/vereinshaus/pocketbase/data/pb_hooks:/pb_hooks + environment: + - TZ=Europe/Berlin + - BREVO_KEY=${BREVO_KEY} + networks: + - default + - npm_bridge + app: build: context: ./app @@ -8,24 +23,13 @@ services: 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} - - BREVO_KEY=${BREVO_KEY} - - BREVO_SENDER=${BREVO_SENDER:-noreply@vereins.haus} networks: - default: {} - npm_bridge: - ipv4_address: 172.25.0.11 + - default + - npm_bridge networks: npm_bridge: diff --git a/pocketbase/pb_hooks/nachrichten.pb.js b/pocketbase/pb_hooks/nachrichten.pb.js deleted file mode 100644 index 6201223..0000000 --- a/pocketbase/pb_hooks/nachrichten.pb.js +++ /dev/null @@ -1,86 +0,0 @@ -onRecordAfterCreateSuccess(function(e) { - if (!e.record) return; - - var key = $os.getenv("BREVO_KEY"); - if (!key) { - console.log("[nachrichten] BREVO_KEY nicht gesetzt – E-Mail übersprungen."); - return; - } - - var vereinId = e.record.getString("verein_id"); - var betreff = e.record.getString("betreff"); - var text = e.record.getString("text"); - var gruppeIds = e.record.getStringSlice("gruppe_ids"); - - // Vereinsname für Absender - var vereinName = "Ihr Verein"; - try { - var verein = $app.findRecordById("vereine", vereinId); - vereinName = verein.getString("name"); - } catch(err) {} - - // Mitglieder-Filter - var filter = 'verein_id = "' + vereinId + '" && status = "aktiv" && email != ""'; - if (gruppeIds && gruppeIds.length > 0) { - var gruppenParts = []; - for (var i = 0; i < gruppeIds.length; i++) { - gruppenParts.push('gruppe_ids ~ "' + gruppeIds[i] + '"'); - } - filter += " && (" + gruppenParts.join(" || ") + ")"; - } - - var mitglieder; - try { - mitglieder = $app.findRecordsByFilter("mitglieder", filter, "nachname", 500, 0); - } catch(err) { - console.error("[nachrichten] Mitglieder laden fehlgeschlagen: " + String(err)); - return; - } - - var empfaenger = []; - for (var j = 0; j < mitglieder.length; j++) { - var m = mitglieder[j]; - var email = m.getString("email"); - if (email) { - empfaenger.push({ - email: email, - name: m.getString("vorname") + " " + m.getString("nachname") - }); - } - } - - if (empfaenger.length === 0) { - console.log("[nachrichten] Keine Empfänger mit E-Mail gefunden."); - return; - } - - var sender = $os.getenv("BREVO_SENDER") || "noreply@vereins.haus"; - var htmlContent = text - ? text.replace(/\n/g, "
") - : "

(Kein Inhalt)

"; - - // In 50er-Batches senden (Brevo-Limit) - var BATCH = 50; - for (var b = 0; b < empfaenger.length; b += BATCH) { - var batch = empfaenger.slice(b, b + BATCH); - var body = JSON.stringify({ - sender: { name: vereinName, email: sender }, - to: [batch[0]], - bcc: batch.slice(1), - subject: betreff, - htmlContent: htmlContent - }); - - try { - $http.send({ - url: "https://api.brevo.com/v3/smtp/email", - method: "POST", - headers: { "api-key": key, "Content-Type": "application/json" }, - body: body - }); - console.log("[nachrichten] Batch " + (b / BATCH + 1) + " gesendet (" + batch.length + " Empfänger)."); - } catch(err) { - console.error("[nachrichten] Brevo Fehler Batch " + (b / BATCH + 1) + ": " + String(err)); - } - } -}, "nachrichten"); diff --git a/pocketbase/pb_migrations/1779215839_created_vereine.js b/pocketbase/pb_migrations/1779215839_created_vereine.js deleted file mode 100644 index b1f4095..0000000 --- a/pocketbase/pb_migrations/1779215839_created_vereine.js +++ /dev/null @@ -1,144 +0,0 @@ -/// -migrate((app) => { - const collection = new Collection({ - "createRule": null, - "deleteRule": null, - "fields": [ - { - "autogeneratePattern": "[a-z0-9]{15}", - "help": "", - "hidden": false, - "id": "text3208210256", - "max": 15, - "min": 15, - "name": "id", - "pattern": "^[a-z0-9]+$", - "presentable": false, - "primaryKey": true, - "required": true, - "system": true, - "type": "text" - }, - { - "autogeneratePattern": "", - "help": "", - "hidden": false, - "id": "text1579384326", - "max": 0, - "min": 0, - "name": "name", - "pattern": "", - "presentable": false, - "primaryKey": false, - "required": true, - "system": false, - "type": "text" - }, - { - "autogeneratePattern": "", - "help": "", - "hidden": false, - "id": "text3525840331", - "max": 0, - "min": 0, - "name": "plz", - "pattern": "", - "presentable": false, - "primaryKey": false, - "required": false, - "system": false, - "type": "text" - }, - { - "autogeneratePattern": "", - "help": "", - "hidden": false, - "id": "text4138466142", - "max": 0, - "min": 0, - "name": "ort", - "pattern": "", - "presentable": false, - "primaryKey": false, - "required": false, - "system": false, - "type": "text" - }, - { - "help": "", - "hidden": false, - "id": "select1497100012", - "maxSelect": 1, - "name": "bundesland", - "presentable": false, - "required": false, - "system": false, - "type": "select", - "values": [ - "BW", - "BY", - "BE", - "BB", - "HB", - "HH", - "HE", - "MV", - "NI", - "NW", - "RP", - "SL", - "SN", - "ST", - "SH", - "TH" - ] - }, - { - "help": "", - "hidden": false, - "id": "select3713686397", - "maxSelect": 1, - "name": "plan", - "presentable": false, - "required": false, - "system": false, - "type": "select", - "values": [ - "free", - "starter", - "wachstum", - "verband" - ] - }, - { - "autogeneratePattern": "", - "help": "", - "hidden": false, - "id": "text1888339527", - "max": 0, - "min": 0, - "name": "stripe_customer_id", - "pattern": "", - "presentable": false, - "primaryKey": false, - "required": false, - "system": false, - "type": "text" - } - ], - "id": "pbc_3589557411", - "indexes": [], - "listRule": "@request.auth.verein_id = id", - "name": "vereine", - "system": false, - "type": "base", - "updateRule": "@request.auth.verein_id = id", - "viewRule": "@request.auth.verein_id = id" - }); - - return app.save(collection); -}, (app) => { - const collection = app.findCollectionByNameOrId("pbc_3589557411"); - - return app.delete(collection); -}) diff --git a/pocketbase/pb_migrations/1779215883_created_gruppen.js b/pocketbase/pb_migrations/1779215883_created_gruppen.js deleted file mode 100644 index 858d446..0000000 --- a/pocketbase/pb_migrations/1779215883_created_gruppen.js +++ /dev/null @@ -1,67 +0,0 @@ -/// -migrate((app) => { - const collection = new Collection({ - "createRule": "@request.auth.verein_id = verein_id", - "deleteRule": "@request.auth.verein_id = verein_id", - "fields": [ - { - "autogeneratePattern": "[a-z0-9]{15}", - "help": "", - "hidden": false, - "id": "text3208210256", - "max": 15, - "min": 15, - "name": "id", - "pattern": "^[a-z0-9]+$", - "presentable": false, - "primaryKey": true, - "required": true, - "system": true, - "type": "text" - }, - { - "cascadeDelete": true, - "collectionId": "pbc_3589557411", - "help": "", - "hidden": false, - "id": "relation145676011", - "maxSelect": 1, - "minSelect": 0, - "name": "verein_id", - "presentable": false, - "required": true, - "system": false, - "type": "relation" - }, - { - "autogeneratePattern": "", - "help": "", - "hidden": false, - "id": "text1579384326", - "max": 0, - "min": 0, - "name": "name", - "pattern": "", - "presentable": false, - "primaryKey": false, - "required": true, - "system": false, - "type": "text" - } - ], - "id": "pbc_3099069179", - "indexes": [], - "listRule": "@request.auth.verein_id = verein_id", - "name": "gruppen", - "system": false, - "type": "base", - "updateRule": "@request.auth.verein_id = verein_id", - "viewRule": "@request.auth.verein_id = verein_id" - }); - - return app.save(collection); -}, (app) => { - const collection = app.findCollectionByNameOrId("pbc_3099069179"); - - return app.delete(collection); -}) diff --git a/pocketbase/pb_migrations/1779215883_updated_users.js b/pocketbase/pb_migrations/1779215883_updated_users.js deleted file mode 100644 index d8f294a..0000000 --- a/pocketbase/pb_migrations/1779215883_updated_users.js +++ /dev/null @@ -1,29 +0,0 @@ -/// -migrate((app) => { - const collection = app.findCollectionByNameOrId("_pb_users_auth_") - - // add field - collection.fields.addAt(10, new Field({ - "cascadeDelete": false, - "collectionId": "pbc_3589557411", - "help": "", - "hidden": false, - "id": "relation145676011", - "maxSelect": 1, - "minSelect": 0, - "name": "verein_id", - "presentable": false, - "required": false, - "system": false, - "type": "relation" - })) - - return app.save(collection) -}, (app) => { - const collection = app.findCollectionByNameOrId("_pb_users_auth_") - - // remove field - collection.fields.removeById("relation145676011") - - return app.save(collection) -}) diff --git a/pocketbase/pb_migrations/1779215901_created_beitraege.js b/pocketbase/pb_migrations/1779215901_created_beitraege.js deleted file mode 100644 index 5ffa960..0000000 --- a/pocketbase/pb_migrations/1779215901_created_beitraege.js +++ /dev/null @@ -1,97 +0,0 @@ -/// -migrate((app) => { - const collection = new Collection({ - "createRule": "@request.auth.verein_id = verein_id", - "deleteRule": "@request.auth.verein_id = verein_id", - "fields": [ - { - "autogeneratePattern": "[a-z0-9]{15}", - "help": "", - "hidden": false, - "id": "text3208210256", - "max": 15, - "min": 15, - "name": "id", - "pattern": "^[a-z0-9]+$", - "presentable": false, - "primaryKey": true, - "required": true, - "system": true, - "type": "text" - }, - { - "cascadeDelete": true, - "collectionId": "pbc_3589557411", - "help": "", - "hidden": false, - "id": "relation145676011", - "maxSelect": 1, - "minSelect": 0, - "name": "verein_id", - "presentable": false, - "required": true, - "system": false, - "type": "relation" - }, - { - "autogeneratePattern": "", - "help": "", - "hidden": false, - "id": "text1579384326", - "max": 0, - "min": 0, - "name": "name", - "pattern": "", - "presentable": false, - "primaryKey": false, - "required": true, - "system": false, - "type": "text" - }, - { - "help": "", - "hidden": false, - "id": "number3107246631", - "max": null, - "min": null, - "name": "betrag", - "onlyInt": false, - "presentable": false, - "required": true, - "system": false, - "type": "number" - }, - { - "help": "", - "hidden": false, - "id": "select917011370", - "maxSelect": 1, - "name": "rhythmus", - "presentable": false, - "required": true, - "system": false, - "type": "select", - "values": [ - "monatlich", - "quartalsweise", - "jaehrlich", - "einmalig" - ] - } - ], - "id": "pbc_3218207135", - "indexes": [], - "listRule": "@request.auth.verein_id = verein_id", - "name": "beitraege", - "system": false, - "type": "base", - "updateRule": "@request.auth.verein_id = verein_id", - "viewRule": "@request.auth.verein_id = verein_id" - }); - - return app.save(collection); -}, (app) => { - const collection = app.findCollectionByNameOrId("pbc_3218207135"); - - return app.delete(collection); -}) diff --git a/pocketbase/pb_migrations/1779215901_created_mitglieder.js b/pocketbase/pb_migrations/1779215901_created_mitglieder.js deleted file mode 100644 index 6dad2e0..0000000 --- a/pocketbase/pb_migrations/1779215901_created_mitglieder.js +++ /dev/null @@ -1,139 +0,0 @@ -/// -migrate((app) => { - const collection = new Collection({ - "createRule": "@request.auth.verein_id = verein_id", - "deleteRule": "@request.auth.verein_id = verein_id", - "fields": [ - { - "autogeneratePattern": "[a-z0-9]{15}", - "help": "", - "hidden": false, - "id": "text3208210256", - "max": 15, - "min": 15, - "name": "id", - "pattern": "^[a-z0-9]+$", - "presentable": false, - "primaryKey": true, - "required": true, - "system": true, - "type": "text" - }, - { - "cascadeDelete": true, - "collectionId": "pbc_3589557411", - "help": "", - "hidden": false, - "id": "relation145676011", - "maxSelect": 1, - "minSelect": 0, - "name": "verein_id", - "presentable": false, - "required": true, - "system": false, - "type": "relation" - }, - { - "autogeneratePattern": "", - "help": "", - "hidden": false, - "id": "text386110805", - "max": 0, - "min": 0, - "name": "vorname", - "pattern": "", - "presentable": false, - "primaryKey": false, - "required": true, - "system": false, - "type": "text" - }, - { - "autogeneratePattern": "", - "help": "", - "hidden": false, - "id": "text3586640595", - "max": 0, - "min": 0, - "name": "nachname", - "pattern": "", - "presentable": false, - "primaryKey": false, - "required": true, - "system": false, - "type": "text" - }, - { - "exceptDomains": null, - "help": "", - "hidden": false, - "id": "email3885137012", - "name": "email", - "onlyDomains": null, - "presentable": false, - "required": false, - "system": false, - "type": "email" - }, - { - "autogeneratePattern": "", - "help": "", - "hidden": false, - "id": "text4208291426", - "max": 0, - "min": 0, - "name": "iban", - "pattern": "", - "presentable": false, - "primaryKey": false, - "required": false, - "system": false, - "type": "text" - }, - { - "cascadeDelete": false, - "collectionId": "pbc_3099069179", - "help": "", - "hidden": false, - "id": "relation1077495665", - "maxSelect": 99, - "minSelect": 0, - "name": "gruppe_ids", - "presentable": false, - "required": false, - "system": false, - "type": "relation" - }, - { - "help": "", - "hidden": false, - "id": "select2063623452", - "maxSelect": 1, - "name": "status", - "presentable": false, - "required": false, - "system": false, - "type": "select", - "values": [ - "aktiv", - "passiv", - "ausgetreten" - ] - } - ], - "id": "pbc_2707111162", - "indexes": [], - "listRule": "@request.auth.verein_id = verein_id", - "name": "mitglieder", - "system": false, - "type": "base", - "updateRule": "@request.auth.verein_id = verein_id", - "viewRule": "@request.auth.verein_id = verein_id" - }); - - return app.save(collection); -}, (app) => { - const collection = app.findCollectionByNameOrId("pbc_2707111162"); - - return app.delete(collection); -}) diff --git a/pocketbase/pb_migrations/1779215948_created_einzuege.js b/pocketbase/pb_migrations/1779215948_created_einzuege.js deleted file mode 100644 index 16c1ad5..0000000 --- a/pocketbase/pb_migrations/1779215948_created_einzuege.js +++ /dev/null @@ -1,123 +0,0 @@ -/// -migrate((app) => { - const collection = new Collection({ - "createRule": "@request.auth.verein_id = mitglied_id.verein_id", - "deleteRule": "@request.auth.verein_id = mitglied_id.verein_id", - "fields": [ - { - "autogeneratePattern": "[a-z0-9]{15}", - "help": "", - "hidden": false, - "id": "text3208210256", - "max": 15, - "min": 15, - "name": "id", - "pattern": "^[a-z0-9]+$", - "presentable": false, - "primaryKey": true, - "required": true, - "system": true, - "type": "text" - }, - { - "cascadeDelete": true, - "collectionId": "pbc_2707111162", - "help": "", - "hidden": false, - "id": "relation3039789658", - "maxSelect": 1, - "minSelect": 0, - "name": "mitglied_id", - "presentable": false, - "required": true, - "system": false, - "type": "relation" - }, - { - "cascadeDelete": false, - "collectionId": "pbc_3218207135", - "help": "", - "hidden": false, - "id": "relation715527895", - "maxSelect": 1, - "minSelect": 0, - "name": "beitrag_id", - "presentable": false, - "required": true, - "system": false, - "type": "relation" - }, - { - "help": "", - "hidden": false, - "id": "number3107246631", - "max": null, - "min": null, - "name": "betrag", - "onlyInt": false, - "presentable": false, - "required": true, - "system": false, - "type": "number" - }, - { - "help": "", - "hidden": false, - "id": "date3314956993", - "max": "", - "min": "", - "name": "faellig_am", - "presentable": false, - "required": false, - "system": false, - "type": "date" - }, - { - "help": "", - "hidden": false, - "id": "select2063623452", - "maxSelect": 1, - "name": "status", - "presentable": false, - "required": false, - "system": false, - "type": "select", - "values": [ - "ausstehend", - "bezahlt", - "fehlgeschlagen", - "storniert" - ] - }, - { - "autogeneratePattern": "", - "help": "", - "hidden": false, - "id": "text4235393406", - "max": 0, - "min": 0, - "name": "stripe_payment_intent_id", - "pattern": "", - "presentable": false, - "primaryKey": false, - "required": false, - "system": false, - "type": "text" - } - ], - "id": "pbc_659326735", - "indexes": [], - "listRule": "@request.auth.verein_id = mitglied_id.verein_id", - "name": "einzuege", - "system": false, - "type": "base", - "updateRule": "@request.auth.verein_id = mitglied_id.verein_id", - "viewRule": "@request.auth.verein_id = mitglied_id.verein_id" - }); - - return app.save(collection); -}, (app) => { - const collection = app.findCollectionByNameOrId("pbc_659326735"); - - return app.delete(collection); -}) diff --git a/pocketbase/pb_migrations/1779215948_created_nachrichten.js b/pocketbase/pb_migrations/1779215948_created_nachrichten.js deleted file mode 100644 index 5343778..0000000 --- a/pocketbase/pb_migrations/1779215948_created_nachrichten.js +++ /dev/null @@ -1,105 +0,0 @@ -/// -migrate((app) => { - const collection = new Collection({ - "createRule": "@request.auth.verein_id = verein_id", - "deleteRule": "@request.auth.verein_id = verein_id", - "fields": [ - { - "autogeneratePattern": "[a-z0-9]{15}", - "help": "", - "hidden": false, - "id": "text3208210256", - "max": 15, - "min": 15, - "name": "id", - "pattern": "^[a-z0-9]+$", - "presentable": false, - "primaryKey": true, - "required": true, - "system": true, - "type": "text" - }, - { - "cascadeDelete": true, - "collectionId": "pbc_3589557411", - "help": "", - "hidden": false, - "id": "relation145676011", - "maxSelect": 1, - "minSelect": 0, - "name": "verein_id", - "presentable": false, - "required": true, - "system": false, - "type": "relation" - }, - { - "autogeneratePattern": "", - "help": "", - "hidden": false, - "id": "text3446813636", - "max": 0, - "min": 0, - "name": "betreff", - "pattern": "", - "presentable": false, - "primaryKey": false, - "required": true, - "system": false, - "type": "text" - }, - { - "convertURLs": false, - "help": "", - "hidden": false, - "id": "editor999008199", - "maxSize": 0, - "name": "text", - "presentable": false, - "required": false, - "system": false, - "type": "editor" - }, - { - "cascadeDelete": false, - "collectionId": "pbc_3099069179", - "help": "", - "hidden": false, - "id": "relation1077495665", - "maxSelect": 99, - "minSelect": 0, - "name": "gruppe_ids", - "presentable": false, - "required": false, - "system": false, - "type": "relation" - }, - { - "help": "", - "hidden": false, - "id": "date2716153632", - "max": "", - "min": "", - "name": "gesendet_am", - "presentable": false, - "required": false, - "system": false, - "type": "date" - } - ], - "id": "pbc_1415911511", - "indexes": [], - "listRule": "@request.auth.verein_id = verein_id", - "name": "nachrichten", - "system": false, - "type": "base", - "updateRule": "@request.auth.verein_id = verein_id", - "viewRule": "@request.auth.verein_id = verein_id" - }); - - return app.save(collection); -}, (app) => { - const collection = app.findCollectionByNameOrId("pbc_1415911511"); - - return app.delete(collection); -}) diff --git a/pocketbase/pb_migrations/1779215948_created_push_subscriptions.js b/pocketbase/pb_migrations/1779215948_created_push_subscriptions.js deleted file mode 100644 index c7998d0..0000000 --- a/pocketbase/pb_migrations/1779215948_created_push_subscriptions.js +++ /dev/null @@ -1,94 +0,0 @@ -/// -migrate((app) => { - const collection = new Collection({ - "createRule": "@request.auth.id != ''", - "deleteRule": "@request.auth.id = user_id", - "fields": [ - { - "autogeneratePattern": "[a-z0-9]{15}", - "help": "", - "hidden": false, - "id": "text3208210256", - "max": 15, - "min": 15, - "name": "id", - "pattern": "^[a-z0-9]+$", - "presentable": false, - "primaryKey": true, - "required": true, - "system": true, - "type": "text" - }, - { - "cascadeDelete": true, - "collectionId": "_pb_users_auth_", - "help": "", - "hidden": false, - "id": "relation2809058197", - "maxSelect": 1, - "minSelect": 0, - "name": "user_id", - "presentable": false, - "required": true, - "system": false, - "type": "relation" - }, - { - "exceptDomains": null, - "help": "", - "hidden": false, - "id": "url3292663675", - "name": "endpoint", - "onlyDomains": null, - "presentable": false, - "required": true, - "system": false, - "type": "url" - }, - { - "autogeneratePattern": "", - "help": "", - "hidden": false, - "id": "text3303707132", - "max": 0, - "min": 0, - "name": "p256dh", - "pattern": "", - "presentable": false, - "primaryKey": false, - "required": true, - "system": false, - "type": "text" - }, - { - "autogeneratePattern": "", - "help": "", - "hidden": false, - "id": "text4175343705", - "max": 0, - "min": 0, - "name": "auth", - "pattern": "", - "presentable": false, - "primaryKey": false, - "required": true, - "system": false, - "type": "text" - } - ], - "id": "pbc_1438754935", - "indexes": [], - "listRule": "@request.auth.id = user_id", - "name": "push_subscriptions", - "system": false, - "type": "base", - "updateRule": "@request.auth.id = user_id", - "viewRule": "@request.auth.id = user_id" - }); - - return app.save(collection); -}, (app) => { - const collection = app.findCollectionByNameOrId("pbc_1438754935"); - - return app.delete(collection); -}) diff --git a/pocketbase/pb_migrations/1779215948_created_termine.js b/pocketbase/pb_migrations/1779215948_created_termine.js deleted file mode 100644 index 6053dbe..0000000 --- a/pocketbase/pb_migrations/1779215948_created_termine.js +++ /dev/null @@ -1,120 +0,0 @@ -/// -migrate((app) => { - const collection = new Collection({ - "createRule": "@request.auth.verein_id = verein_id", - "deleteRule": "@request.auth.verein_id = verein_id", - "fields": [ - { - "autogeneratePattern": "[a-z0-9]{15}", - "help": "", - "hidden": false, - "id": "text3208210256", - "max": 15, - "min": 15, - "name": "id", - "pattern": "^[a-z0-9]+$", - "presentable": false, - "primaryKey": true, - "required": true, - "system": true, - "type": "text" - }, - { - "cascadeDelete": true, - "collectionId": "pbc_3589557411", - "help": "", - "hidden": false, - "id": "relation145676011", - "maxSelect": 1, - "minSelect": 0, - "name": "verein_id", - "presentable": false, - "required": true, - "system": false, - "type": "relation" - }, - { - "autogeneratePattern": "", - "help": "", - "hidden": false, - "id": "text2200468358", - "max": 0, - "min": 0, - "name": "titel", - "pattern": "", - "presentable": false, - "primaryKey": false, - "required": true, - "system": false, - "type": "text" - }, - { - "help": "", - "hidden": false, - "id": "date363840172", - "max": "", - "min": "", - "name": "beginn", - "presentable": false, - "required": true, - "system": false, - "type": "date" - }, - { - "help": "", - "hidden": false, - "id": "date1404831091", - "max": "", - "min": "", - "name": "ende", - "presentable": false, - "required": false, - "system": false, - "type": "date" - }, - { - "autogeneratePattern": "", - "help": "", - "hidden": false, - "id": "text4138466142", - "max": 0, - "min": 0, - "name": "ort", - "pattern": "", - "presentable": false, - "primaryKey": false, - "required": false, - "system": false, - "type": "text" - }, - { - "cascadeDelete": false, - "collectionId": "pbc_3099069179", - "help": "", - "hidden": false, - "id": "relation1077495665", - "maxSelect": 99, - "minSelect": 0, - "name": "gruppe_ids", - "presentable": false, - "required": false, - "system": false, - "type": "relation" - } - ], - "id": "pbc_2279568741", - "indexes": [], - "listRule": "@request.auth.verein_id = verein_id", - "name": "termine", - "system": false, - "type": "base", - "updateRule": "@request.auth.verein_id = verein_id", - "viewRule": "@request.auth.verein_id = verein_id" - }); - - return app.save(collection); -}, (app) => { - const collection = app.findCollectionByNameOrId("pbc_2279568741"); - - return app.delete(collection); -}) diff --git a/pocketbase/pb_migrations/1779230000_align_schema.js b/pocketbase/pb_migrations/1779230000_align_schema.js deleted file mode 100644 index 5499ebe..0000000 --- a/pocketbase/pb_migrations/1779230000_align_schema.js +++ /dev/null @@ -1,194 +0,0 @@ -/// -migrate((app) => { - - // vereine: +adresse, +dosb_mitglied; -stripe_customer_id - { - const c = app.findCollectionByNameOrId("pbc_3589557411") - c.fields.removeById("text1888339527") // stripe_customer_id - c.fields.addAt(2, new Field({ - "type": "text", "id": "text2001000001", "name": "adresse", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - })) - c.fields.addAt(99, new Field({ - "type": "bool", "id": "bool2001000002", "name": "dosb_mitglied", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false - })) - app.save(c) - } - - // gruppen: +beschreibung - { - const c = app.findCollectionByNameOrId("pbc_3099069179") - c.fields.addAt(99, new Field({ - "type": "text", "id": "text2001000003", "name": "beschreibung", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - })) - app.save(c) - } - - // mitglieder: +telefon, geburtsdatum, eintrittsdatum, austrittsdatum, strasse, plz, ort, bic, notizen - { - const c = app.findCollectionByNameOrId("pbc_2707111162") - c.fields.addAt(99, new Field({ - "type": "text", "id": "text2001000010", "name": "telefon", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - })) - c.fields.addAt(99, new Field({ - "type": "date", "id": "date2001000011", "name": "geburtsdatum", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "min": "", "max": "" - })) - c.fields.addAt(99, new Field({ - "type": "date", "id": "date2001000012", "name": "eintrittsdatum", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "min": "", "max": "" - })) - c.fields.addAt(99, new Field({ - "type": "date", "id": "date2001000013", "name": "austrittsdatum", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "min": "", "max": "" - })) - c.fields.addAt(99, new Field({ - "type": "text", "id": "text2001000014", "name": "strasse", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - })) - c.fields.addAt(99, new Field({ - "type": "text", "id": "text2001000015", "name": "plz", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - })) - c.fields.addAt(99, new Field({ - "type": "text", "id": "text2001000016", "name": "ort", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - })) - c.fields.addAt(99, new Field({ - "type": "text", "id": "text2001000017", "name": "bic", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - })) - c.fields.addAt(99, new Field({ - "type": "text", "id": "text2001000018", "name": "notizen", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - })) - app.save(c) - } - - // beitraege: +beschreibung; rhythmus +halbjaehrlich (einmalig bleibt) - { - const c = app.findCollectionByNameOrId("pbc_3218207135") - const rhythmus = c.fields.getById("select917011370") - rhythmus.values = ["monatlich", "quartalsweise", "halbjaehrlich", "jaehrlich", "einmalig"] - c.fields.addAt(99, new Field({ - "type": "text", "id": "text2001000020", "name": "beschreibung", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - })) - app.save(c) - } - - // einzuege: -stripe_payment_intent_id; status bezahlt→eingezogen - { - const c = app.findCollectionByNameOrId("pbc_659326735") - c.fields.removeById("text4235393406") // stripe_payment_intent_id - const status = c.fields.getById("select2063623452") - status.values = ["ausstehend", "eingezogen", "fehlgeschlagen", "storniert"] - app.save(c) - } - - // termine: +beschreibung - { - const c = app.findCollectionByNameOrId("pbc_2279568741") - c.fields.addAt(99, new Field({ - "type": "text", "id": "text2001000030", "name": "beschreibung", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - })) - app.save(c) - } - - // nachrichten: +autor_id (relation zu users) - { - const c = app.findCollectionByNameOrId("pbc_1415911511") - c.fields.addAt(2, new Field({ - "type": "relation", "id": "relation2001000040", "name": "autor_id", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "cascadeDelete": false, "collectionId": "_pb_users_auth_", "maxSelect": 1, "minSelect": 0 - })) - app.save(c) - } - -}, (app) => { - - // vereine rollback - { - const c = app.findCollectionByNameOrId("pbc_3589557411") - c.fields.removeById("text2001000001") - c.fields.removeById("bool2001000002") - c.fields.addAt(99, new Field({ - "type": "text", "id": "text1888339527", "name": "stripe_customer_id", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - })) - app.save(c) - } - - // gruppen rollback - { - const c = app.findCollectionByNameOrId("pbc_3099069179") - c.fields.removeById("text2001000003") - app.save(c) - } - - // mitglieder rollback - { - const c = app.findCollectionByNameOrId("pbc_2707111162") - for (const id of ["text2001000010","date2001000011","date2001000012","date2001000013", - "text2001000014","text2001000015","text2001000016","text2001000017","text2001000018"]) { - c.fields.removeById(id) - } - app.save(c) - } - - // beitraege rollback - { - const c = app.findCollectionByNameOrId("pbc_3218207135") - const rhythmus = c.fields.getById("select917011370") - rhythmus.values = ["monatlich", "quartalsweise", "jaehrlich", "einmalig"] - c.fields.removeById("text2001000020") - app.save(c) - } - - // einzuege rollback - { - const c = app.findCollectionByNameOrId("pbc_659326735") - const status = c.fields.getById("select2063623452") - status.values = ["ausstehend", "bezahlt", "fehlgeschlagen", "storniert"] - c.fields.addAt(99, new Field({ - "type": "text", "id": "text4235393406", "name": "stripe_payment_intent_id", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - })) - app.save(c) - } - - // termine rollback - { - const c = app.findCollectionByNameOrId("pbc_2279568741") - c.fields.removeById("text2001000030") - app.save(c) - } - - // nachrichten rollback - { - const c = app.findCollectionByNameOrId("pbc_1415911511") - c.fields.removeById("relation2001000040") - app.save(c) - } - -}) diff --git a/pocketbase/pb_migrations/1779230100_sepa_fields.js b/pocketbase/pb_migrations/1779230100_sepa_fields.js deleted file mode 100644 index f64ad90..0000000 --- a/pocketbase/pb_migrations/1779230100_sepa_fields.js +++ /dev/null @@ -1,58 +0,0 @@ -/// -migrate((app) => { - - // vereine: +glaeubigerid, +iban, +bic (Vereinskonto für SEPA-Einzug) - { - const c = app.findCollectionByNameOrId("pbc_3589557411") - c.fields.addAt(99, new Field({ - "type": "text", "id": "text2001000050", "name": "glaeubigerid", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - })) - c.fields.addAt(99, new Field({ - "type": "text", "id": "text2001000051", "name": "iban", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - })) - c.fields.addAt(99, new Field({ - "type": "text", "id": "text2001000052", "name": "bic", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - })) - app.save(c) - } - - // mitglieder: +mandatsreferenz, +mandatsdatum (SEPA-Mandat des Mitglieds) - { - const c = app.findCollectionByNameOrId("pbc_2707111162") - c.fields.addAt(99, new Field({ - "type": "text", "id": "text2001000053", "name": "mandatsreferenz", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - })) - c.fields.addAt(99, new Field({ - "type": "date", "id": "date2001000054", "name": "mandatsdatum", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "min": "", "max": "" - })) - app.save(c) - } - -}, (app) => { - - { - const c = app.findCollectionByNameOrId("pbc_3589557411") - c.fields.removeById("text2001000050") - c.fields.removeById("text2001000051") - c.fields.removeById("text2001000052") - app.save(c) - } - - { - const c = app.findCollectionByNameOrId("pbc_2707111162") - c.fields.removeById("text2001000053") - c.fields.removeById("date2001000054") - app.save(c) - } - -}) diff --git a/pocketbase/pb_migrations/1779230200_push_subscriptions_vereinrule.js b/pocketbase/pb_migrations/1779230200_push_subscriptions_vereinrule.js deleted file mode 100644 index 31c9122..0000000 --- a/pocketbase/pb_migrations/1779230200_push_subscriptions_vereinrule.js +++ /dev/null @@ -1,14 +0,0 @@ -/// -// Erlaubt allen Usern desselben Vereins, die Subscriptions ihrer Vereinsmitglieder zu lesen -// (notwendig damit die /api/push/senden Route alle Geräte des Vereins erreicht) -migrate((app) => { - const c = app.findCollectionByNameOrId("pbc_1438754935") // push_subscriptions - c.listRule = '@request.auth.verein_id = user_id.verein_id' - c.viewRule = '@request.auth.id = user_id' - app.save(c) -}, (app) => { - const c = app.findCollectionByNameOrId("pbc_1438754935") - c.listRule = '@request.auth.id = user_id' - c.viewRule = '@request.auth.id = user_id' - app.save(c) -}) diff --git a/pocketbase/pb_migrations/1779230300_vereine_create_rule.js b/pocketbase/pb_migrations/1779230300_vereine_create_rule.js deleted file mode 100644 index 434db4f..0000000 --- a/pocketbase/pb_migrations/1779230300_vereine_create_rule.js +++ /dev/null @@ -1,11 +0,0 @@ -/// -// Erlaubt eingeloggten Nutzern, einen Verein anzulegen (Onboarding) -migrate((app) => { - const c = app.findCollectionByNameOrId("pbc_3589557411") // vereine - c.createRule = "@request.auth.id != ''" - app.save(c) -}, (app) => { - const c = app.findCollectionByNameOrId("pbc_3589557411") - c.createRule = null - app.save(c) -}) diff --git a/pocketbase/pb_migrations/1779230400_verein_kontakt.js b/pocketbase/pb_migrations/1779230400_verein_kontakt.js deleted file mode 100644 index 50ed13c..0000000 --- a/pocketbase/pb_migrations/1779230400_verein_kontakt.js +++ /dev/null @@ -1,26 +0,0 @@ -/// -migrate((app) => { - const c = app.findCollectionByNameOrId("pbc_3589557411") // vereine - c.fields.addAt(99, new Field({ - "type": "email", "id": "email2001000060", "name": "email", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "exceptDomains": null, "onlyDomains": null - })) - c.fields.addAt(99, new Field({ - "type": "text", "id": "text2001000061", "name": "telefon", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - })) - c.fields.addAt(99, new Field({ - "type": "url", "id": "url2001000062", "name": "website", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "exceptDomains": null, "onlyDomains": null - })) - app.save(c) -}, (app) => { - const c = app.findCollectionByNameOrId("pbc_3589557411") - c.fields.removeById("email2001000060") - c.fields.removeById("text2001000061") - c.fields.removeById("url2001000062") - app.save(c) -}) diff --git a/pocketbase/pb_migrations/1779230500_rollen.js b/pocketbase/pb_migrations/1779230500_rollen.js deleted file mode 100644 index a998ab1..0000000 --- a/pocketbase/pb_migrations/1779230500_rollen.js +++ /dev/null @@ -1,92 +0,0 @@ -/// -migrate((app) => { - - // Users: +rolle, listRule für verein-weite Sichtbarkeit - { - const c = app.findCollectionByNameOrId("_pb_users_auth_") - c.fields.addAt(99, new Field({ - "type": "select", "id": "select2001000070", "name": "rolle", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "maxSelect": 1, "values": ["admin", "trainer"] - })) - c.listRule = "@request.auth.verein_id = verein_id" - app.save(c) - } - - // Gruppen: +trainer_ids (welche User betreuen diese Gruppe) - { - const c = app.findCollectionByNameOrId("pbc_3099069179") - c.fields.addAt(99, new Field({ - "type": "relation", "id": "relation2001000071", "name": "trainer_ids", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "cascadeDelete": false, "collectionId": "_pb_users_auth_", - "maxSelect": 99, "minSelect": 0 - })) - app.save(c) - } - - // Einladungen-Collection - { - const c = new Collection({ - "createRule": "@request.auth.verein_id = verein_id", - "deleteRule": "@request.auth.verein_id = verein_id", - "listRule": "@request.auth.verein_id = verein_id", - "viewRule": "", - "updateRule": "@request.auth.verein_id = verein_id", - "fields": [ - { - "autogeneratePattern": "[a-z0-9]{15}", "id": "text3208210256", - "max": 15, "min": 15, "name": "id", "pattern": "^[a-z0-9]+$", - "primaryKey": true, "required": true, "system": true, "type": "text", - "help": "", "hidden": false, "presentable": false - }, - { - "type": "relation", "id": "relation2001000072", "name": "verein_id", - "help": "", "hidden": false, "presentable": false, "required": true, "system": false, - "cascadeDelete": true, "collectionId": "pbc_3589557411", "maxSelect": 1, "minSelect": 0 - }, - { - "type": "select", "id": "select2001000073", "name": "rolle", - "help": "", "hidden": false, "presentable": false, "required": true, "system": false, - "maxSelect": 1, "values": ["admin", "trainer"] - }, - { - "type": "text", "id": "text2001000074", "name": "token", - "help": "", "hidden": false, "presentable": false, "required": true, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - }, - { - "type": "bool", "id": "bool2001000075", "name": "genutzt", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false - } - ], - "id": "pbc_einladungen", - "indexes": ["CREATE UNIQUE INDEX idx_einladungen_token ON einladungen (token)"], - "name": "einladungen", - "system": false, - "type": "base" - }) - app.save(c) - } - -}, (app) => { - - { - const c = app.findCollectionByNameOrId("_pb_users_auth_") - c.fields.removeById("select2001000070") - c.listRule = "" - app.save(c) - } - - { - const c = app.findCollectionByNameOrId("pbc_3099069179") - c.fields.removeById("relation2001000071") - app.save(c) - } - - { - const c = app.findCollectionByNameOrId("pbc_einladungen") - app.delete(c) - } - -}) diff --git a/pocketbase/pb_migrations/1779230600_termin_verfuegbarkeit.js b/pocketbase/pb_migrations/1779230600_termin_verfuegbarkeit.js deleted file mode 100644 index 9f653e2..0000000 --- a/pocketbase/pb_migrations/1779230600_termin_verfuegbarkeit.js +++ /dev/null @@ -1,23 +0,0 @@ -/// -migrate((app) => { - const c = app.findCollectionByNameOrId("pbc_2279568741") // termine - - c.fields.addAt(99, new Field({ - "type": "relation", "id": "relation2001000080", "name": "durchfuehrender_id", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "cascadeDelete": false, "collectionId": "_pb_users_auth_", "maxSelect": 1, "minSelect": 0 - })) - - c.fields.addAt(99, new Field({ - "type": "select", "id": "select2001000081", "name": "verfuegbarkeit", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "maxSelect": 1, "values": ["offen", "bestaetigt", "abgesagt", "vertretung_gesucht"] - })) - - app.save(c) -}, (app) => { - const c = app.findCollectionByNameOrId("pbc_2279568741") - c.fields.removeById("relation2001000080") - c.fields.removeById("select2001000081") - app.save(c) -}) diff --git a/pocketbase/pb_migrations/1779230700_termine_serie.js b/pocketbase/pb_migrations/1779230700_termine_serie.js deleted file mode 100644 index b44688a..0000000 --- a/pocketbase/pb_migrations/1779230700_termine_serie.js +++ /dev/null @@ -1,25 +0,0 @@ -/// -migrate((app) => { - const c = app.findCollectionByNameOrId("pbc_2279568741") // termine - - // rrule – RFC-5545-Wiederholungsregel (nur RRULE-Teil ohne DTSTART) - c.fields.addAt(99, new Field({ - "type": "text", "id": "text2001000090", "name": "rrule", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - })) - - // serie_id – gruppiert alle Termine einer Wiederholungsserie - c.fields.addAt(99, new Field({ - "type": "text", "id": "text2001000091", "name": "serie_id", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - })) - - app.save(c) -}, (app) => { - const c = app.findCollectionByNameOrId("pbc_2279568741") - c.fields.removeById("text2001000090") - c.fields.removeById("text2001000091") - app.save(c) -}) diff --git a/pocketbase/pb_migrations/1779230800_veranstaltungsorte.js b/pocketbase/pb_migrations/1779230800_veranstaltungsorte.js deleted file mode 100644 index 362a0eb..0000000 --- a/pocketbase/pb_migrations/1779230800_veranstaltungsorte.js +++ /dev/null @@ -1,117 +0,0 @@ -/// -migrate((app) => { - - // Veranstaltungsorte - { - const c = new Collection({ - "createRule": "@request.auth.verein_id = verein_id", - "deleteRule": "@request.auth.verein_id = verein_id", - "listRule": "@request.auth.verein_id = verein_id", - "viewRule": "@request.auth.verein_id = verein_id", - "updateRule": "@request.auth.verein_id = verein_id", - "fields": [ - { - "autogeneratePattern": "[a-z0-9]{15}", "id": "text3208210256", - "max": 15, "min": 15, "name": "id", "pattern": "^[a-z0-9]+$", - "primaryKey": true, "required": true, "system": true, "type": "text", - "help": "", "hidden": false, "presentable": false - }, - { - "type": "relation", "id": "relation2001000100", "name": "verein_id", - "help": "", "hidden": false, "presentable": false, "required": true, "system": false, - "cascadeDelete": true, "collectionId": "pbc_3589557411", "maxSelect": 1, "minSelect": 0 - }, - { - "type": "text", "id": "text2001000101", "name": "name", - "help": "", "hidden": false, "presentable": true, "required": true, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - }, - { - "type": "text", "id": "text2001000102", "name": "adresse", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - }, - { - "type": "select", "id": "select2001000103", "name": "typ", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "maxSelect": 1, "values": ["halle", "platz", "gebaeude", "sonstiges"] - }, - { - "type": "bool", "id": "bool2001000104", "name": "aktiv", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false - } - ], - "id": "pbc_veranstaltungsorte", - "indexes": [], - "name": "veranstaltungsorte", - "system": false, - "type": "base" - }) - app.save(c) - } - - // Ort-Ausfälle - { - const c = new Collection({ - "createRule": "@request.auth.verein_id = ort_id.verein_id", - "deleteRule": "@request.auth.verein_id = ort_id.verein_id", - "listRule": "@request.auth.verein_id = ort_id.verein_id", - "viewRule": "@request.auth.verein_id = ort_id.verein_id", - "updateRule": "@request.auth.verein_id = ort_id.verein_id", - "fields": [ - { - "autogeneratePattern": "[a-z0-9]{15}", "id": "text3208210256", - "max": 15, "min": 15, "name": "id", "pattern": "^[a-z0-9]+$", - "primaryKey": true, "required": true, "system": true, "type": "text", - "help": "", "hidden": false, "presentable": false - }, - { - "type": "relation", "id": "relation2001000110", "name": "ort_id", - "help": "", "hidden": false, "presentable": false, "required": true, "system": false, - "cascadeDelete": true, "collectionId": "pbc_veranstaltungsorte", "maxSelect": 1, "minSelect": 0 - }, - { - "type": "date", "id": "date2001000111", "name": "von", - "help": "", "hidden": false, "presentable": false, "required": true, "system": false, - "min": "", "max": "" - }, - { - "type": "date", "id": "date2001000112", "name": "bis", - "help": "", "hidden": false, "presentable": false, "required": true, "system": false, - "min": "", "max": "" - }, - { - "type": "text", "id": "text2001000113", "name": "grund", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "autogeneratePattern": "", "min": 0, "max": 0, "pattern": "" - } - ], - "id": "pbc_ort_ausfaelle", - "indexes": [], - "name": "ort_ausfaelle", - "system": false, - "type": "base" - }) - app.save(c) - } - - // Termine: +ort_id (Relation zu veranstaltungsorte, optional) - { - const c = app.findCollectionByNameOrId("pbc_2279568741") - c.fields.addAt(99, new Field({ - "type": "relation", "id": "relation2001000120", "name": "ort_id", - "help": "", "hidden": false, "presentable": false, "required": false, "system": false, - "cascadeDelete": false, "collectionId": "pbc_veranstaltungsorte", "maxSelect": 1, "minSelect": 0 - })) - app.save(c) - } - -}, (app) => { - { - const c = app.findCollectionByNameOrId("pbc_2279568741") - c.fields.removeById("relation2001000120") - app.save(c) - } - try { app.delete(app.findCollectionByNameOrId("pbc_ort_ausfaelle")) } catch(_) {} - try { app.delete(app.findCollectionByNameOrId("pbc_veranstaltungsorte")) } catch(_) {} -}) diff --git a/pocketbase/pb_migrations/1779230900_neuigkeiten.js b/pocketbase/pb_migrations/1779230900_neuigkeiten.js deleted file mode 100644 index 210d69f..0000000 --- a/pocketbase/pb_migrations/1779230900_neuigkeiten.js +++ /dev/null @@ -1,190 +0,0 @@ -/// -migrate((app) => { - - // neuigkeiten: new Collection() + app.save() – genau wie nachrichten/mitglieder. - // importCollections() kompiliert Rules in PB v0.38 NICHT korrekt → HTTP 400. - const neuigkeiten = new Collection({ - "id": "pbc_1779230901", - "name": "neuigkeiten", - "type": "base", - "system": false, - "listRule": "@request.auth.verein_id = verein_id", - "viewRule": "@request.auth.verein_id = verein_id", - "createRule": "@request.auth.verein_id = verein_id", - "updateRule": "@request.auth.verein_id = verein_id && autor_id = @request.auth.id", - "deleteRule": "@request.auth.verein_id = verein_id && autor_id = @request.auth.id", - "indexes": [], - "fields": [ - { - "autogeneratePattern": "[a-z0-9]{15}", - "hidden": false, - "id": "text3208210256", - "max": 15, - "min": 15, - "name": "id", - "pattern": "^[a-z0-9]+$", - "presentable": false, - "primaryKey": true, - "required": true, - "system": true, - "type": "text" - }, - { - "cascadeDelete": true, - "collectionId": "pbc_3589557411", - "hidden": false, - "id": "relation3100049688", - "maxSelect": 1, - "minSelect": 0, - "name": "verein_id", - "presentable": false, - "required": true, - "system": false, - "type": "relation" - }, - { - "autogeneratePattern": "", - "hidden": false, - "id": "text2300079512", - "max": 0, - "min": 0, - "name": "autor_id", - "pattern": "", - "presentable": false, - "primaryKey": false, - "required": true, - "system": false, - "type": "text" - }, - { - "autogeneratePattern": "", - "hidden": false, - "id": "text1546895374", - "max": 0, - "min": 0, - "name": "autor_name", - "pattern": "", - "presentable": false, - "primaryKey": false, - "required": false, - "system": false, - "type": "text" - }, - { - "autogeneratePattern": "", - "hidden": false, - "id": "text1059453555", - "max": 0, - "min": 0, - "name": "text", - "pattern": "", - "presentable": false, - "primaryKey": false, - "required": false, - "system": false, - "type": "text" - }, - { - "hidden": false, - "id": "file5103562148", - "maxSelect": 10, - "maxSize": 15728640, - "mimeTypes": ["image/jpeg","image/png","image/gif","image/webp","video/mp4","video/quicktime"], - "name": "medien", - "presentable": false, - "required": false, - "system": false, - "type": "file" - }, - { - "cascadeDelete": false, - "collectionId": "pbc_3099069179", - "hidden": false, - "id": "relation3177861730", - "maxSelect": 99, - "minSelect": 0, - "name": "gruppe_ids", - "presentable": false, - "required": false, - "system": false, - "type": "relation" - }, - { - "cascadeDelete": false, - "collectionId": "pbc_2279568741", - "hidden": false, - "id": "relation2143897900", - "maxSelect": 1, - "minSelect": 0, - "name": "termin_id", - "presentable": false, - "required": false, - "system": false, - "type": "relation" - } - ] - }); - app.save(neuigkeiten); - - const neuigkeitenId = app.findCollectionByNameOrId("neuigkeiten").id; - - const reaktionen = new Collection({ - "id": "pbc_1779230902", - "name": "reaktionen", - "type": "base", - "system": false, - "createRule": "@request.auth.id != ''", - "deleteRule": "@request.auth.id = user_id", - "listRule": "@request.auth.verein_id = beitrag_id.verein_id", - "viewRule": "@request.auth.verein_id = beitrag_id.verein_id", - "updateRule": null, - "indexes": ["CREATE UNIQUE INDEX idx_reaktion_unique ON reaktionen (beitrag_id, user_id)"], - "fields": [ - { - "autogeneratePattern": "[a-z0-9]{15}", - "hidden": false, - "id": "text3208210256", - "max": 15, - "min": 15, - "name": "id", - "pattern": "^[a-z0-9]+$", - "presentable": false, - "primaryKey": true, - "required": true, - "system": true, - "type": "text" - }, - { - "cascadeDelete": true, - "collectionId": neuigkeitenId, - "hidden": false, - "id": "relation4196188140", - "maxSelect": 1, - "minSelect": 0, - "name": "beitrag_id", - "presentable": false, - "required": true, - "system": false, - "type": "relation" - }, - { - "cascadeDelete": true, - "collectionId": "_pb_users_auth_", - "hidden": false, - "id": "relation3255232538", - "maxSelect": 1, - "minSelect": 0, - "name": "user_id", - "presentable": false, - "required": true, - "system": false, - "type": "relation" - } - ] - }); - app.save(reaktionen); - -}, (app) => { - try { app.delete(app.findCollectionByNameOrId("reaktionen")) } catch(_) {} - try { app.delete(app.findCollectionByNameOrId("neuigkeiten")) } catch(_) {} -}) diff --git a/scripts/seed.js b/scripts/seed.js deleted file mode 100644 index b3d4ac7..0000000 --- a/scripts/seed.js +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env node -// Seed-Script für vereins.haus (SvelteKit + SQLite, kein PocketBase) -// Verwendung: APP_URL=https://staging.vereins.haus node scripts/seed.js - -const BASE = process.env.APP_URL || 'http://localhost:3000'; - -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)}`); - return json; -} - -async function main() { - console.log(`→ Seed gegen ${BASE}`); - - // 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!', - name: 'Max Vorstand', - }); - const T = auth.token; - const VID = auth.verein_id; - console.log(` ✓ Verein ID: ${VID}`); - console.log(` ✓ User: vorstand@tsv-musterstadt.de`); - - // 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).length} Gruppen`); - - // 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`); - - // 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`); - - // 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`); - - // 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 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']], verfuegbarkeit: 'offen', - rrule: 'FREQ=WEEKLY;BYDAY=TU', - }); - } - for (const t of termine) await req('POST', '/termine', t, T); - console.log(` ✓ ${termine.length} Termine`); - - // 8. Nachricht - console.log('→ Nachricht...'); - await req('POST', '/nachrichten', { - betreff: 'Willkommen in vereins.haus!', - text: 'Hallo und herzlich willkommen! Dies ist eine Beispiel-Nachricht an alle Mitglieder.', - 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}`); -} - -main().catch(e => { console.error('✗ Seed-Fehler:', e.message); process.exit(1); });