From 460ad7994f013ed5f09d107cc7816d59152658cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aaron=20D=C3=B6tsch?= <aaron@fsmpi.rwth-aachen.de> Date: Thu, 29 Aug 2024 01:24:30 +0200 Subject: [PATCH] update --- .example.env | 2 + package-lock.json | 984 ++++++++---------- package.json | 7 +- src/app.d.ts | 31 +- src/app.pcss | 4 + src/hooks.server.ts | 55 +- src/lib/components/MailInput.svelte | 1 + src/lib/components/MainLayout.svelte | 14 +- src/lib/components/SortableTable.svelte | 45 + .../components/SortableTableHeadCell.svelte | 43 + src/lib/components/SupervisorLayout.svelte | 39 + src/lib/components/TutorLayout.svelte | 41 + src/lib/i18n/de.ts | 23 +- src/lib/i18n/en.ts | 23 +- src/lib/i18n/i18n.ts | 1 + src/lib/mail.ts | 17 +- src/lib/server/auth.ts | 111 ++ .../server/database/entities/Config.entity.ts | 1 + .../database/entities/RallyStation.entity.ts | 3 + .../entities/RallyStationSupervisor.entity.ts | 4 + .../server/database/entities/Tutor.entity.ts | 26 +- .../database/entities/TutorTraining.entity.ts | 4 +- .../database/entities/Tutorial.entity.ts | 9 + src/lib/server/database/migrations/0_init.sql | 7 +- src/lib/server/mail.ts | 6 +- src/routes/(non-admin)/+layout.svelte | 2 +- src/routes/(non-admin)/impressum/+page.svelte | 4 +- src/routes/(non-admin)/login/+page.server.ts | 2 +- src/routes/(non-admin)/tutor/+page.server.ts | 2 +- src/routes/(non-admin)/tutor/+page.svelte | 8 +- .../tutor/register/+page.server.ts | 31 +- .../(non-admin)/tutor/register/+page.svelte | 222 ++-- src/routes/+layout.server.ts | 2 + src/routes/+layout.ts | 1 + src/routes/admin/setup/+page.server.ts | 6 + src/routes/admin/templates/+page.server.ts | 3 +- src/routes/admin/templates/+page.svelte | 48 +- .../admin/tutor/[id=number]/+page.server.ts | 78 +- .../admin/tutor/[id=number]/+page.svelte | 11 +- src/routes/admin/tutor/mail/+page.server.ts | 3 +- src/routes/admin/tutor/training/+page.svelte | 4 +- .../training/[id=number]/+page.server.ts | 4 + .../tutor/training/[id=number]/+page.svelte | 11 +- .../admin/tutor/training/new/+page.server.ts | 5 +- .../admin/tutor/training/new/+page.svelte | 11 +- src/routes/intern/+page.server.ts | 52 + src/routes/intern/+page.svelte | 115 ++ src/routes/intern/rallye/+layout.server.ts | 17 + src/routes/intern/rallye/+layout.svelte | 10 + src/routes/intern/rallye/+page.svelte | 3 + src/routes/intern/tutor/+layout.server.ts | 30 + src/routes/intern/tutor/+layout.svelte | 10 + src/routes/intern/tutor/+page.server.ts | 0 src/routes/intern/tutor/+page.svelte | 33 + .../intern/tutor/settings/+page.server.ts | 53 + src/routes/intern/tutor/settings/+page.svelte | 114 ++ 56 files changed, 1597 insertions(+), 799 deletions(-) create mode 100644 src/lib/components/SortableTable.svelte create mode 100644 src/lib/components/SortableTableHeadCell.svelte create mode 100644 src/lib/components/SupervisorLayout.svelte create mode 100644 src/lib/components/TutorLayout.svelte create mode 100644 src/lib/server/auth.ts create mode 100644 src/routes/intern/+page.server.ts create mode 100644 src/routes/intern/+page.svelte create mode 100644 src/routes/intern/rallye/+layout.server.ts create mode 100644 src/routes/intern/rallye/+layout.svelte create mode 100644 src/routes/intern/rallye/+page.svelte create mode 100644 src/routes/intern/tutor/+layout.server.ts create mode 100644 src/routes/intern/tutor/+layout.svelte create mode 100644 src/routes/intern/tutor/+page.server.ts create mode 100644 src/routes/intern/tutor/+page.svelte create mode 100644 src/routes/intern/tutor/settings/+page.server.ts create mode 100644 src/routes/intern/tutor/settings/+page.svelte diff --git a/.example.env b/.example.env index b26bc52..90655e6 100644 --- a/.example.env +++ b/.example.env @@ -14,3 +14,5 @@ REQUIRED_GROUP=esag MAIL_HOST=mail.example.com MAIL_PORT=25 MAIL_FROM=esag@example.com + +DOMAIN=example.com # The domain of the application diff --git a/package-lock.json b/package-lock.json index 5406a78..59a2e42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@auth/core": "^0.34.2", "@auth/sveltekit": "^1.4.2", + "bcrypt": "^5.1.1", "canvas": "^2.11.2", "classnames": "^2.5.1", "feiertagejs": "^1.4.0", @@ -30,7 +31,8 @@ "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-node": "^5.0.1", "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@sveltejs/vite-plugin-svelte": "^4.0.0-next.6", + "@types/bcrypt": "^5.0.2", "@types/eslint": "^9.6.0", "@types/geojson": "^7946.0.14", "@types/intl": "^1.2.2", @@ -53,8 +55,7 @@ "tailwindcss": "^3.3.6", "tslib": "^2.4.1", "typescript": "^5.0.0", - "vite": "^5.0.3", - "vite-plugin-watch-and-run": "^1.6.0" + "vite": "^5.0.3" } }, "node_modules/@alloc/quick-lru": { @@ -570,9 +571,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz", - "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -643,37 +644,6 @@ "concat-map": "0.0.1" } }, - "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.12.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -688,9 +658,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", - "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==", + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz", + "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==", "dev": true, "license": "MIT", "engines": { @@ -708,28 +678,28 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.5.tgz", - "integrity": "sha512-8GrTWmoFhm5BsMZOTHeGD2/0FLKLQQHvO/ZmQga4tKempYRLz8aqJGqXVuQgisnMObq2YZ2SgkwctN1LOOxcqA==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.7.tgz", + "integrity": "sha512-yDzVT/Lm101nQ5TCVeK65LtdN7Tj4Qpr9RTXJ2vPFLqtLxwOrpoxAHAJI8J3yYWUc40J0BDBheaitK5SJmno2g==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.5" + "@floating-ui/utils": "^0.2.7" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.8.tgz", - "integrity": "sha512-kx62rP19VZ767Q653wsP1XZCGIirkE09E0QUGNYTM/ttbbQHqcGPdSfWFxUyyNLc/W6aoJRBajOSXhP6GXjC0Q==", + "version": "1.6.10", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.10.tgz", + "integrity": "sha512-fskgCFv8J8OamCmyun8MfjB1Olfn+uZKjOKZ0vhYF3gRmEUXcGOjxWL8bBr7i4kIuPZ2KD2S3EUIOxnjC8kl2A==", "license": "MIT", "dependencies": { "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.5" + "@floating-ui/utils": "^0.2.7" } }, "node_modules/@floating-ui/utils": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.5.tgz", - "integrity": "sha512-sTcG+QZ6fdEUObICavU+aB3Mp8HY4n14wYHdxK4fXjPmv3PXZZeY5RaguJmGyeH/CJQhX3fqKUtS4qc1LoHwhQ==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.7.tgz", + "integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==", "license": "MIT" }, "node_modules/@humanwhocodes/module-importer": { @@ -761,9 +731,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.4.tgz", - "integrity": "sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", "cpu": [ "arm64" ], @@ -773,23 +743,19 @@ "darwin" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.2" + "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.4.tgz", - "integrity": "sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", "cpu": [ "x64" ], @@ -799,23 +765,19 @@ "darwin" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.2" + "@img/sharp-libvips-darwin-x64": "1.0.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz", - "integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", "cpu": [ "arm64" ], @@ -824,20 +786,14 @@ "os": [ "darwin" ], - "engines": { - "macos": ">=11", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz", - "integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", "cpu": [ "x64" ], @@ -846,20 +802,14 @@ "os": [ "darwin" ], - "engines": { - "macos": ">=10.13", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz", - "integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", "cpu": [ "arm" ], @@ -868,20 +818,14 @@ "os": [ "linux" ], - "engines": { - "glibc": ">=2.28", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz", - "integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", "cpu": [ "arm64" ], @@ -890,20 +834,14 @@ "os": [ "linux" ], - "engines": { - "glibc": ">=2.26", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz", - "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", "cpu": [ "s390x" ], @@ -912,20 +850,14 @@ "os": [ "linux" ], - "engines": { - "glibc": ">=2.28", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz", - "integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", "cpu": [ "x64" ], @@ -934,20 +866,14 @@ "os": [ "linux" ], - "engines": { - "glibc": ">=2.26", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz", - "integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", "cpu": [ "arm64" ], @@ -956,20 +882,14 @@ "os": [ "linux" ], - "engines": { - "musl": ">=1.2.2", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz", - "integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", "cpu": [ "x64" ], @@ -978,20 +898,14 @@ "os": [ "linux" ], - "engines": { - "musl": ">=1.2.2", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.4.tgz", - "integrity": "sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", "cpu": [ "arm" ], @@ -1001,23 +915,19 @@ "linux" ], "engines": { - "glibc": ">=2.28", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.2" + "@img/sharp-libvips-linux-arm": "1.0.5" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.4.tgz", - "integrity": "sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", "cpu": [ "arm64" ], @@ -1027,23 +937,19 @@ "linux" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.2" + "@img/sharp-libvips-linux-arm64": "1.0.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.4.tgz", - "integrity": "sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", "cpu": [ "s390x" ], @@ -1053,23 +959,19 @@ "linux" ], "engines": { - "glibc": ">=2.31", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.2" + "@img/sharp-libvips-linux-s390x": "1.0.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.4.tgz", - "integrity": "sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", "cpu": [ "x64" ], @@ -1079,23 +981,19 @@ "linux" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.2" + "@img/sharp-libvips-linux-x64": "1.0.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.4.tgz", - "integrity": "sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", "cpu": [ "arm64" ], @@ -1105,23 +1003,19 @@ "linux" ], "engines": { - "musl": ">=1.2.2", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2" + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.4.tgz", - "integrity": "sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", "cpu": [ "x64" ], @@ -1131,45 +1025,38 @@ "linux" ], "engines": { - "musl": ">=1.2.2", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.2" + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.4.tgz", - "integrity": "sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", "cpu": [ "wasm32" ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.1.1" + "@emnapi/runtime": "^1.2.0" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.4.tgz", - "integrity": "sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", "cpu": [ "ia32" ], @@ -1179,19 +1066,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.4.tgz", - "integrity": "sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", "cpu": [ "x64" ], @@ -1201,10 +1085,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -1302,19 +1183,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@kitql/helpers": { - "version": "0.8.9", - "resolved": "https://registry.npmjs.org/@kitql/helpers/-/helpers-0.8.9.tgz", - "integrity": "sha512-uDBFBvCYUT4UaZZKv7gJejQvbrOp4YyI1S0Z92DPiMbyLq0DPDXz3Lt2ZqUZKlQrinBX+W1TO6w0RudEX6Q6WA==", - "dev": true, - "license": "MIT", - "dependencies": { - "esm-env": "^1.0.0" - }, - "engines": { - "node": "^16.14 || >=18" - } - }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -1501,9 +1369,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz", - "integrity": "sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.0.tgz", + "integrity": "sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==", "cpu": [ "arm" ], @@ -1515,9 +1383,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.20.0.tgz", - "integrity": "sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.0.tgz", + "integrity": "sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==", "cpu": [ "arm64" ], @@ -1529,9 +1397,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.20.0.tgz", - "integrity": "sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.0.tgz", + "integrity": "sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==", "cpu": [ "arm64" ], @@ -1543,9 +1411,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.20.0.tgz", - "integrity": "sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.0.tgz", + "integrity": "sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==", "cpu": [ "x64" ], @@ -1557,9 +1425,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.20.0.tgz", - "integrity": "sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.0.tgz", + "integrity": "sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==", "cpu": [ "arm" ], @@ -1571,9 +1439,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.20.0.tgz", - "integrity": "sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.0.tgz", + "integrity": "sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==", "cpu": [ "arm" ], @@ -1585,9 +1453,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.20.0.tgz", - "integrity": "sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.0.tgz", + "integrity": "sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==", "cpu": [ "arm64" ], @@ -1599,9 +1467,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.20.0.tgz", - "integrity": "sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.0.tgz", + "integrity": "sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==", "cpu": [ "arm64" ], @@ -1613,9 +1481,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.20.0.tgz", - "integrity": "sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.0.tgz", + "integrity": "sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==", "cpu": [ "ppc64" ], @@ -1627,9 +1495,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.20.0.tgz", - "integrity": "sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.0.tgz", + "integrity": "sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==", "cpu": [ "riscv64" ], @@ -1641,9 +1509,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.20.0.tgz", - "integrity": "sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.0.tgz", + "integrity": "sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==", "cpu": [ "s390x" ], @@ -1655,9 +1523,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz", - "integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.0.tgz", + "integrity": "sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==", "cpu": [ "x64" ], @@ -1669,9 +1537,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz", - "integrity": "sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.0.tgz", + "integrity": "sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==", "cpu": [ "x64" ], @@ -1683,9 +1551,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.20.0.tgz", - "integrity": "sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.0.tgz", + "integrity": "sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==", "cpu": [ "arm64" ], @@ -1697,9 +1565,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.20.0.tgz", - "integrity": "sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.0.tgz", + "integrity": "sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==", "cpu": [ "ia32" ], @@ -1711,9 +1579,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.20.0.tgz", - "integrity": "sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.0.tgz", + "integrity": "sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==", "cpu": [ "x64" ], @@ -1725,9 +1593,9 @@ ] }, "node_modules/@sveltejs/adapter-auto": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.2.2.tgz", - "integrity": "sha512-Mso5xPCA8zgcKrv+QioVlqMZkyUQ5MjDJiEPuG/Z7cV/5tmwV7LmcVWk5tZ+H0NCOV1x12AsoSpt/CwFwuVXMA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.2.4.tgz", + "integrity": "sha512-a64AKYbfTUrVwU0xslzv1Jf3M8bj0IwhptaXmhgIkjXspBXhD0od9JiItQHchijpLMGdEDcYBlvqySkEawv6mQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1738,9 +1606,9 @@ } }, "node_modules/@sveltejs/adapter-node": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.0.tgz", - "integrity": "sha512-HVZoei2078XSyPmvdTHE03VXDUD0ytTvMuMHMQP0j6zX4nPDpCcKrgvU7baEblMeCCMdM/shQvstFxOJPQKlUQ==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.2.tgz", + "integrity": "sha512-BCX4zP0cf86TXpmvLQTnnT/tp7P12UMezf+5LwljP1MJC1fFzn9XOXpAHQCyP+pyHGy2K7p5gY0LyLcZFAL02w==", "dev": true, "license": "MIT", "dependencies": { @@ -1754,9 +1622,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.5.20", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.20.tgz", - "integrity": "sha512-47rJ5BoYwURE/Rp7FNMLp3NzdbWC9DQ/PmKd0mebxT2D/PrPxZxcLImcD3zsWdX2iS6oJk8ITJbO/N2lWnnUqA==", + "version": "2.5.24", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.24.tgz", + "integrity": "sha512-Nr2oxsCsDfEkdS/zzQQQbsPYTbu692Qs3/iE3L7VHzCVjG2+WujF9oMUozWI7GuX98KxYSoPMlAsfmDLSg44hQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1781,52 +1649,61 @@ "node": ">=18.13" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3" } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.1.tgz", - "integrity": "sha512-rimpFEAboBBHIlzISibg94iP09k/KYdHgVhJlcsTfn7KMBhc70jFX/GRWkRdFCc2fdnk+4+Bdfej23cMDnJS6A==", + "version": "4.0.0-next.6", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.0-next.6.tgz", + "integrity": "sha512-7+bEFN5F9pthG6nOEHNz9yioHxNXK6yl+0GnTy9WOfxN/SvPykkH/Hs6MqTGjo47a9G2q3QXQnzuxG5WXNX4Tg==", "dev": true, "license": "MIT", "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^2.1.0", - "debug": "^4.3.4", + "@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0", + "debug": "^4.3.6", "deepmerge": "^4.3.1", "kleur": "^4.1.5", - "magic-string": "^0.30.10", - "svelte-hmr": "^0.16.0", + "magic-string": "^0.30.11", "vitefu": "^0.2.5" }, "engines": { - "node": "^18.0.0 || >=20" + "node": "^18.0.0 || ^20.0.0 || >=22" }, "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.0", + "svelte": "^5.0.0-next.96 || ^5.0.0", "vite": "^5.0.0" } }, "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz", - "integrity": "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==", + "version": "3.0.0-next.3", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.0-next.3.tgz", + "integrity": "sha512-kuGJ2CZ5lAw3gKF8Kw0AfKtUJWbwdlDHY14K413B0MCyrzvQvsKTorwmwZcky0+QqY6RnVIZ/5FttB9bQmkLXg==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.4" + "debug": "^4.3.5" }, "engines": { - "node": "^18.0.0 || >=20" + "node": "^18.0.0 || ^20.0.0 || >=22" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.0", + "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0", + "svelte": "^5.0.0-next.96 || ^5.0.0", "vite": "^5.0.0" } }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "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", @@ -1907,13 +1784,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", - "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", + "version": "22.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", + "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.13.0" + "undici-types": "~6.19.2" } }, "node_modules/@types/node-cron": { @@ -1958,17 +1835,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.1.tgz", - "integrity": "sha512-5g3Y7GDFsJAnY4Yhvk8sZtFfV6YNF2caLzjrRPUBzewjPCaj0yokePB4LJSobyCzGMzjZZYFbwuzbfDHlimXbQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.2.0.tgz", + "integrity": "sha512-02tJIs655em7fvt9gps/+4k4OsKULYGtLBPJfOsmOq1+3cdClYiF0+d6mHu6qDnTcg88wJBkcPLpQhq7FyDz0A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.0.1", - "@typescript-eslint/type-utils": "8.0.1", - "@typescript-eslint/utils": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/type-utils": "8.2.0", + "@typescript-eslint/utils": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1992,16 +1869,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.1.tgz", - "integrity": "sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.2.0.tgz", + "integrity": "sha512-j3Di+o0lHgPrb7FxL3fdEy6LJ/j2NE8u+AP/5cQ9SKb+JLH6V6UHDqJ+e0hXBkHP1wn1YDFjYCS9LBQsZDlDEg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/typescript-estree": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", "debug": "^4.3.4" }, "engines": { @@ -2021,14 +1898,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.1.tgz", - "integrity": "sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.2.0.tgz", + "integrity": "sha512-OFn80B38yD6WwpoHU2Tz/fTz7CgFqInllBoC3WP+/jLbTb4gGPTy9HBSTsbDWkMdN55XlVU0mMDYAtgvlUspGw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1" + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2039,14 +1916,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.1.tgz", - "integrity": "sha512-+/UT25MWvXeDX9YaHv1IS6KI1fiuTto43WprE7pgSMswHbn1Jm9GEM4Txp+X74ifOWV8emu2AWcbLhpJAvD5Ng==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.2.0.tgz", + "integrity": "sha512-g1CfXGFMQdT5S+0PSO0fvGXUaiSkl73U1n9LTK5aRAFnPlJ8dLKkXr4AaLFvPedW8lVDoMgLLE3JN98ZZfsj0w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.0.1", - "@typescript-eslint/utils": "8.0.1", + "@typescript-eslint/typescript-estree": "8.2.0", + "@typescript-eslint/utils": "8.2.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2064,9 +1941,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.1.tgz", - "integrity": "sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.2.0.tgz", + "integrity": "sha512-6a9QSK396YqmiBKPkJtxsgZZZVjYQ6wQ/TlI0C65z7vInaETuC6HAHD98AGLC8DyIPqHytvNuS8bBVvNLKyqvQ==", "dev": true, "license": "MIT", "engines": { @@ -2078,14 +1955,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.1.tgz", - "integrity": "sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.2.0.tgz", + "integrity": "sha512-kiG4EDUT4dImplOsbh47B1QnNmXSoUqOjWDvCJw/o8LgfD0yr7k2uy54D5Wm0j4t71Ge1NkynGhpWdS0dEIAUA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2107,16 +1984,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.1.tgz", - "integrity": "sha512-CBFR0G0sCt0+fzfnKaciu9IBsKvEKYwN9UZ+eeogK1fYHg4Qxk1yf/wLQkLXlq8wbU2dFlgAesxt8Gi76E8RTA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.2.0.tgz", + "integrity": "sha512-O46eaYKDlV3TvAVDNcoDzd5N550ckSe8G4phko++OCSC1dYIb9LTc3HDGYdWqWIAT5qDUKphO6sd9RrpIJJPfg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1" + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/typescript-estree": "8.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2130,13 +2007,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.1.tgz", - "integrity": "sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.2.0.tgz", + "integrity": "sha512-sbgsPMW9yLvS7IhCi8IpuK1oBmtbWUNP+hBdwl/I9nzqVsszGnNGti5r9dUtF5RLivHUFFIdRvLiTsPhzSyJ3Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.0.1", + "@typescript-eslint/types": "8.2.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2441,6 +2318,20 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2569,9 +2460,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001649", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001649.tgz", - "integrity": "sha512-fJegqZZ0ZX8HOWr6rcafGr72+xcgJKI9oWfDW5DrD7ExUtgZC7a7R7ZYmZqplh7XDocFdGeIFn7roAxhOeYrPQ==", + "version": "1.0.30001653", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz", + "integrity": "sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw==", "dev": true, "funding": [ { @@ -2994,9 +2885,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz", - "integrity": "sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==", + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", + "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==", "dev": true, "license": "ISC" }, @@ -3228,17 +3119,17 @@ } }, "node_modules/eslint": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", - "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz", + "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", - "@eslint/config-array": "^0.17.1", + "@eslint/config-array": "^0.18.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.8.0", + "@eslint/js": "9.9.1", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", @@ -3277,6 +3168,14 @@ }, "funding": { "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-compat-utils": { @@ -3370,20 +3269,10 @@ } } }, - "node_modules/eslint-plugin-svelte/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3391,7 +3280,7 @@ "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -3421,23 +3310,6 @@ "concat-map": "0.0.1" } }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", - "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", @@ -3451,24 +3323,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.12.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3490,18 +3344,31 @@ "license": "MIT" }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -3766,9 +3633,9 @@ } }, "node_modules/foreground-child": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", - "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -4251,9 +4118,9 @@ } }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -4431,9 +4298,9 @@ } }, "node_modules/is-core-module": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", - "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -4703,9 +4570,9 @@ } }, "node_modules/jose": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.6.3.tgz", - "integrity": "sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.7.0.tgz", + "integrity": "sha512-3P9qfTYDVnNn642LCAqIKbTGb9a1TBxZ9ti5zEVEr48aDdflgRjhspWFb6WM4PzAfFbGMJYC4+803v8riCRAKw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -4955,9 +4822,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -5154,6 +5021,12 @@ "dev": true, "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, "node_modules/node-cron": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", @@ -5474,9 +5347,9 @@ } }, "node_modules/oauth4webapi": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.11.1.tgz", - "integrity": "sha512-aNzOnL98bL6izG97zgnZs1PFEyO4WDVRhz2Pd066NPak44w5ESLRCYmJIyey8avSBPOMtBjhF3ZDDm7bIb7UOg==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.12.0.tgz", + "integrity": "sha512-WFmcHzhFtq2Ar91crpGQZUD8DS0SG7Zti1AgbansUAfdpIsoRXE+hcMNi8MW6bGNNObWis0x8BZRl6K+FR4oQg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -5778,9 +5651,9 @@ } }, "node_modules/postcss": { - "version": "8.4.40", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", - "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", "funding": [ { "type": "opencollective", @@ -5954,9 +5827,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz", - "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -6250,9 +6123,9 @@ } }, "node_modules/rollup": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz", - "integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.0.tgz", + "integrity": "sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6266,22 +6139,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.20.0", - "@rollup/rollup-android-arm64": "4.20.0", - "@rollup/rollup-darwin-arm64": "4.20.0", - "@rollup/rollup-darwin-x64": "4.20.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.20.0", - "@rollup/rollup-linux-arm-musleabihf": "4.20.0", - "@rollup/rollup-linux-arm64-gnu": "4.20.0", - "@rollup/rollup-linux-arm64-musl": "4.20.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.20.0", - "@rollup/rollup-linux-riscv64-gnu": "4.20.0", - "@rollup/rollup-linux-s390x-gnu": "4.20.0", - "@rollup/rollup-linux-x64-gnu": "4.20.0", - "@rollup/rollup-linux-x64-musl": "4.20.0", - "@rollup/rollup-win32-arm64-msvc": "4.20.0", - "@rollup/rollup-win32-ia32-msvc": "4.20.0", - "@rollup/rollup-win32-x64-msvc": "4.20.0", + "@rollup/rollup-android-arm-eabi": "4.21.0", + "@rollup/rollup-android-arm64": "4.21.0", + "@rollup/rollup-darwin-arm64": "4.21.0", + "@rollup/rollup-darwin-x64": "4.21.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.0", + "@rollup/rollup-linux-arm-musleabihf": "4.21.0", + "@rollup/rollup-linux-arm64-gnu": "4.21.0", + "@rollup/rollup-linux-arm64-musl": "4.21.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.0", + "@rollup/rollup-linux-riscv64-gnu": "4.21.0", + "@rollup/rollup-linux-s390x-gnu": "4.21.0", + "@rollup/rollup-linux-x64-gnu": "4.21.0", + "@rollup/rollup-linux-x64-musl": "4.21.0", + "@rollup/rollup-win32-arm64-msvc": "4.21.0", + "@rollup/rollup-win32-ia32-msvc": "4.21.0", + "@rollup/rollup-win32-x64-msvc": "4.21.0", "fsevents": "~2.3.2" } }, @@ -6510,43 +6383,42 @@ } }, "node_modules/sharp": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.4.tgz", - "integrity": "sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", - "semver": "^7.6.0" + "semver": "^7.6.3" }, "engines": { - "libvips": ">=8.15.2", "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.4", - "@img/sharp-darwin-x64": "0.33.4", - "@img/sharp-libvips-darwin-arm64": "1.0.2", - "@img/sharp-libvips-darwin-x64": "1.0.2", - "@img/sharp-libvips-linux-arm": "1.0.2", - "@img/sharp-libvips-linux-arm64": "1.0.2", - "@img/sharp-libvips-linux-s390x": "1.0.2", - "@img/sharp-libvips-linux-x64": "1.0.2", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", - "@img/sharp-libvips-linuxmusl-x64": "1.0.2", - "@img/sharp-linux-arm": "0.33.4", - "@img/sharp-linux-arm64": "0.33.4", - "@img/sharp-linux-s390x": "0.33.4", - "@img/sharp-linux-x64": "0.33.4", - "@img/sharp-linuxmusl-arm64": "0.33.4", - "@img/sharp-linuxmusl-x64": "0.33.4", - "@img/sharp-wasm32": "0.33.4", - "@img/sharp-win32-ia32": "0.33.4", - "@img/sharp-win32-x64": "0.33.4" + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" } }, "node_modules/shebang-command": { @@ -6737,9 +6609,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", - "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", "dev": true, "license": "CC0-1.0" }, @@ -6997,9 +6869,9 @@ } }, "node_modules/svelte": { - "version": "5.0.0-next.210", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.0.0-next.210.tgz", - "integrity": "sha512-6QZpzr31weKDyAKifOdXJHK9oEeBE2Z/z/h1IX4tmJRuWPE/2Wc7Lzpfxl+0irS19GZH6V5YZnZLNTRJKjGzfg==", + "version": "5.0.0-next.238", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.0.0-next.238.tgz", + "integrity": "sha512-fCPNBqQA/MYadkI58LOV3kpYHP5zOsMSjjPagc63Z5LXRkdZe/TKOJbVtLK9Gp4+Shf3qTaxBCp/BR1845Rd/A==", "dev": true, "license": "MIT", "dependencies": { @@ -7022,9 +6894,9 @@ } }, "node_modules/svelte-check": { - "version": "3.8.5", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.8.5.tgz", - "integrity": "sha512-3OGGgr9+bJ/+1nbPgsvulkLC48xBsqsgtc8Wam281H4G9F5v3mYGa2bHRsPuwHC5brKl4AxJH95QF73kmfihGQ==", + "version": "3.8.6", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.8.6.tgz", + "integrity": "sha512-ij0u4Lw/sOTREP13BdWZjiXD/BlHE6/e2e34XzmVmsp5IN4kVa3PWP65NM32JAgwjZlwBg/+JtiNV1MM8khu0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7070,17 +6942,39 @@ } } }, - "node_modules/svelte-hmr": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz", - "integrity": "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==", + "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, - "license": "ISC", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, "engines": { - "node": "^12.20 || ^14.13.1 || >= 16" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "peerDependencies": { - "svelte": "^3.19.0 || ^4.0.0" + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/svelte-preprocess": { @@ -7248,9 +7142,9 @@ } }, "node_modules/tailwind-merge": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.4.0.tgz", - "integrity": "sha512-49AwoOQNKdqKPd9CViyH5wJoSKsCDjUlzL8DxuGp3P1FsGY36NJDAa18jLZcaHAUUuTj+JB8IAo8zWgBNvBF7A==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz", + "integrity": "sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==", "license": "MIT", "funding": { "type": "github", @@ -7258,9 +7152,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz", - "integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==", + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", + "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -7350,6 +7244,18 @@ "url": "https://github.com/sponsors/antonk52" } }, + "node_modules/tailwindcss/node_modules/yaml": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -7475,9 +7381,9 @@ "license": "Apache-2.0" }, "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "devOptional": true, "license": "0BSD" }, @@ -7608,9 +7514,9 @@ } }, "node_modules/undici-types": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", - "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true, "license": "MIT" }, @@ -7688,15 +7594,15 @@ } }, "node_modules/vite": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz", - "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", + "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.39", - "rollup": "^4.13.0" + "postcss": "^8.4.41", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -7715,6 +7621,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -7732,6 +7639,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -7743,34 +7653,6 @@ } } }, - "node_modules/vite-plugin-watch-and-run": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/vite-plugin-watch-and-run/-/vite-plugin-watch-and-run-1.7.0.tgz", - "integrity": "sha512-f6TUUxDvOeFPMJ1/NDK8N1y/65w8h4jPZGsuOOYVnaK4lkutN95rTNAunsr0fcgTVo1BRUMxDTY7iFlsGuiCig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@kitql/helpers": "0.8.9", - "micromatch": "4.0.5" - }, - "engines": { - "node": "^16.14 || >=18" - } - }, - "node_modules/vite-plugin-watch-and-run/node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/vitefu": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", @@ -8000,15 +7882,13 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", - "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, "engines": { - "node": ">= 14" + "node": ">= 6" } }, "node_modules/yocto-queue": { diff --git a/package.json b/package.json index ba59698..72f7c27 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-node": "^5.0.1", "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@sveltejs/vite-plugin-svelte": "^4.0.0-next.6", + "@types/bcrypt": "^5.0.2", "@types/eslint": "^9.6.0", "@types/geojson": "^7946.0.14", "@types/intl": "^1.2.2", @@ -37,13 +38,13 @@ "tailwindcss": "^3.3.6", "tslib": "^2.4.1", "typescript": "^5.0.0", - "vite": "^5.0.3", - "vite-plugin-watch-and-run": "^1.6.0" + "vite": "^5.0.3" }, "type": "module", "dependencies": { "@auth/core": "^0.34.2", "@auth/sveltekit": "^1.4.2", + "bcrypt": "^5.1.1", "canvas": "^2.11.2", "classnames": "^2.5.1", "feiertagejs": "^1.4.0", diff --git a/src/app.d.ts b/src/app.d.ts index 9f62f91..1dc6b05 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -2,6 +2,8 @@ // for information about these interfaces import type { Locale } from "$lib/i18n/i18n"; +import type { RallyStationSupervisor } from "$lib/server/database/entities/RallyStationSupervisor.entity"; +import type { Tutor } from "$lib/server/database/entities/Tutor.entity"; import type { User } from "$lib/server/database/entities/User.entity"; declare global { @@ -10,6 +12,8 @@ declare global { interface Locals { locale: Locale; user: User; + tutor: Tutor; + supervisor: RallyStationSupervisor; } // interface PageData {} // interface PageState {} @@ -17,14 +21,29 @@ declare global { } } +type AdminUser = { + type: "admin", + name: string, + userId: string, + email: string, + groups: string[], +}; +type TutorUser = { + type: "tutor", + name: string, + userId: number, + email: string, +}; +type RallyUser = { + type: "rally", + name: string, + userId: number, + email: string, +}; + declare module "@auth/core/types" { interface Session { - user: { - name: string, - userId: string, - email: string, - groups: string[], - }; + user: AdminUser | TutorUser | RallyUser; } } diff --git a/src/app.pcss b/src/app.pcss index 1a7b7cf..af35794 100644 --- a/src/app.pcss +++ b/src/app.pcss @@ -2,3 +2,7 @@ @tailwind base; @tailwind components; @tailwind utilities; + +.dark { + color-scheme: dark; +} diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 6a3fe7b..b1fdb31 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,10 +1,7 @@ -import { KEYCLOAK_ID, KEYCLOAK_ISSUER, KEYCLOAK_SECRET, KEYCLOAK_SCOPES, REQUIRED_GROUP } from '$env/static/private'; import { locales, type Locale, defaultLocale } from '$lib/i18n/i18n'; -import { User } from '$lib/server/database/entities/User.entity'; +import { handle as handleAuthentication, handleAuthorization } from '$lib/server/auth'; import { sendTrainingMails } from '$lib/server/mail'; -import { SvelteKitAuth } from '@auth/sveltekit'; -import KeycloakProvider from '@auth/sveltekit/providers/keycloak'; -import { error, redirect, type Handle, type RequestEvent } from '@sveltejs/kit'; +import type { Handle, RequestEvent } from '@sveltejs/kit'; import { sequence } from '@sveltejs/kit/hooks'; import cron from "node-cron"; @@ -27,60 +24,12 @@ function detectLocale(event: RequestEvent): Locale { return defaultLocale; } -const auth = SvelteKitAuth({ - providers: [ - KeycloakProvider({ - clientId: KEYCLOAK_ID, - clientSecret: KEYCLOAK_SECRET, - issuer: KEYCLOAK_ISSUER, - profile(profile){ - return { - name: profile.name ?? profile.preferred_username, - userId: profile.sub, - email: profile.email, - groups: profile.groups, - }; - }, - authorization: { - params: { - scope: KEYCLOAK_SCOPES, - }, - }, - }), - ], - callbacks: { - jwt({token, user}){ - if(user) return {...token, user}; - else return token; - }, - session({session, token}){ - if(token?.user) session.user = token.user; - return session; - } - } -}); -const handleAuthentication: Handle = async ({event, resolve})=>{ - return auth.handle({event, resolve}); -}; - const handleLocale: Handle = ({event, resolve})=>{ const locale = detectLocale(event); event.locals.locale = locale; return resolve(event, { transformPageChunk: ({html})=>html.replace("%lang%", locale) }); } -const handleAuthorization: Handle = async ({event, resolve})=>{ - if(event.url.pathname.startsWith("/admin")){ - const session = await event.locals.auth(); - if(!session) redirect(303, "/"); - if(REQUIRED_GROUP && !session.user.groups.includes(REQUIRED_GROUP)) error(403, `Missing required group ${REQUIRED_GROUP}`); - const user = await User.getById(session.user.userId); - if(!user) redirect(303, "/"); - event.locals.user = user; - } - return resolve(event); -}; - export const handle = sequence( handleAuthentication, handleAuthorization, diff --git a/src/lib/components/MailInput.svelte b/src/lib/components/MailInput.svelte index d339c6b..0fd79d3 100644 --- a/src/lib/components/MailInput.svelte +++ b/src/lib/components/MailInput.svelte @@ -56,6 +56,7 @@ dietaryRestriction: "vegan, Eiweißallergie", training: trainings[0], sentTrainingMail: false, + password: "password123", }); let [mailHtml, compilationError]: [string, string|null] = $derived.by(()=>{ diff --git a/src/lib/components/MainLayout.svelte b/src/lib/components/MainLayout.svelte index 7fa7e2c..3b4b918 100644 --- a/src/lib/components/MainLayout.svelte +++ b/src/lib/components/MainLayout.svelte @@ -1,4 +1,5 @@ <script lang="ts"> + import { version } from "$app/environment"; import { page } from "$app/stores"; import { L, LL, locale, locales, setLocale, type Translation } from "$lib/i18n/i18n"; import { signIn } from "@auth/sveltekit/client"; @@ -6,7 +7,7 @@ import { ChevronDownOutline } from "flowbite-svelte-icons"; import type { Snippet } from "svelte"; - let { headerLinks, children }: {headerLinks: string[], children: Snippet} = $props(); + let { headerLinks, children, domain }: {headerLinks: string[], children: Snippet, domain: string} = $props(); function processActiveUrl(url: string) { return url; @@ -26,7 +27,7 @@ {/if} {/each} <meta property="og:type" content="website"> - <meta property="og:url" content="https://esa.fsmpi.rwth-aachen.de"> <!-- TODO set correct url --> + <meta property="og:url" content="https://{domain}"> <meta property="og:site_name" content="Erstiarbeit der Fachschaft I/1"> <!-- TODO i18n --> </svelte:head> @@ -35,11 +36,6 @@ <NavBrand href="/">{$LL.Navbar.Branding()}</NavBrand> <NavHamburger /> <NavUl {activeUrl}> - <!--<NavLi href="/information">{$LL.Navbar.Information()}</NavLi> - <NavLi href="/rabatte">{$LL.Navbar.Discounts()}</NavLi> - <NavLi href="/eswe">{$LL.Navbar.ESWE()}</NavLi> - <NavLi href="/flyer">{$LL.Navbar.Flyer()}</NavLi> - <NavLi href="/tutor">{$LL.Navbar.Tutor()}</NavLi>--> {#each headerLinks as link} <NavLi href={link}>{$LL.Navbar.Links[link as keyof Translation["Navbar"]["Links"]]()}</NavLi> {/each} @@ -56,14 +52,14 @@ </Navbar> <div class="mb-auto"> - <div class="container relative mx-auto px-4 max-w-5xl mb-10 mt-4"> + <div class="container relative mx-auto px-4 xs:px-8 sm:px-12 lg:px-14 max-w-5xl mb-10 mt-4"> {@render children()} </div> </div> <Footer footerType="socialmedia"> <div class="sm:flex sm:items-center sm:justify-between"> - <FooterCopyright by={$LL.Footer.Branding()} copyrightMessage="" year={2024} /> + <FooterCopyright by={$LL.Footer.Branding()} copyrightMessage="" year={new Date(parseInt(version)).getFullYear()} /> <FooterLinkGroup class="flex mt-4 sm:justify-center sm:mt-0" ulClass="flex flex-wrap items-center mt-3 text-sm text-gray-500 dark:text-gray-400 sm:mt-0"> <FooterLink href="/impressum">{$LL.Footer.Legal()}</FooterLink> <FooterLink href="/datenschutz">{$LL.Footer.PrivacyPolicy()}</FooterLink> diff --git a/src/lib/components/SortableTable.svelte b/src/lib/components/SortableTable.svelte new file mode 100644 index 0000000..074766c --- /dev/null +++ b/src/lib/components/SortableTable.svelte @@ -0,0 +1,45 @@ +<script lang="ts"> + import { Table, TableBody } from "flowbite-svelte"; + import { setContext, type Snippet } from "svelte"; + import { writable } from "svelte/store"; + + type T = $$Generic; + type ComponentProps = { + items: T[]; + row: Snippet<[{item: T, index: number}]>; + children: Snippet; + } & Record<string, any>; + let { items, row, children, ...restProps }: ComponentProps = $props(); + + const sorting = writable({sorted: items, sortDirection: 1, sorter: null}); + setContext("sorting", sorting); +</script> + +<style> + div :global(.sortable) { + cursor: pointer; + position: relative; + } + div :global(.sortable::after) { + content: ""; + position: absolute; + padding-left: .7rem; + } + div :global(.sorting-asc::after) { + content: "▲"; + } + div :global(.sorting-desc::after) { + content: "▼"; + } +</style> + +<div> + <Table {...restProps}> + {@render children()} + <TableBody> + {#each $sorting.sorted as item, index} + {@render row({item, index})} + {/each} + </TableBody> + </Table> +</div> diff --git a/src/lib/components/SortableTableHeadCell.svelte b/src/lib/components/SortableTableHeadCell.svelte new file mode 100644 index 0000000..08cffc0 --- /dev/null +++ b/src/lib/components/SortableTableHeadCell.svelte @@ -0,0 +1,43 @@ +<script lang="ts"> + import { TableHeadCell } from "flowbite-svelte"; + import { getContext, onMount, type Snippet } from "svelte"; + import type { Writable } from "svelte/store"; + import { twMerge } from "tailwind-merge"; + + type T = $$Generic; + const sorting = getContext("sorting") as Writable<{sorted: T[], sortDirection: 1|-1, sorter?: TableHeadCell}>; + + let { sort, children, padding="px-6 py-3", defaultDirection="asc", default: def }: { sort: (a: T, b: T)=>number, children: Snippet, padding?: string, defaultDirection?: "asc"|"desc", default?: boolean } = $props(); + let self: TableHeadCell|undefined = $state(); + + if(def){ + // update directly for SSR + sorting.update(({sorted}) => { + let dir = (defaultDirection === "asc" ? 1 : -1) as 1 | -1; + return {sorted: sorted.sort((a,b)=>dir*sort(a,b)), sortDirection: dir, sorter: self}; + }); + // update on initialization afte "self" has been initialized + onMount(()=>{ + sorting.update(({sorted}) => { + let dir = (defaultDirection === "asc" ? 1 : -1) as 1 | -1; + return {sorted: sorted.sort((a,b)=>dir*sort(a,b)), sortDirection: dir, sorter: self}; + }); + }); + } + + function onclick(){ + sorting.update(({sorted, sortDirection, sorter}) => { + if(sorter === self){ + return {sorted: sorted.sort((a,b)=>-sortDirection*sort(a,b)), sortDirection: -sortDirection as 1 | -1, sorter}; + } + let dir = (defaultDirection === "asc" ? 1 : -1) as 1 | -1; + return {sorted: sorted.sort((a,b)=>dir*sort(a,b)), sortDirection: dir, sorter: self}; + }); + } +</script> + +<TableHeadCell bind:this={self} padding=""> + <button {onclick} class={twMerge(padding, "sortable w-full text-left", $sorting.sorter === self && `sorting-${$sorting.sortDirection === 1 ? "asc": "desc"}`)}> + {@render children()} + </button> +</TableHeadCell> diff --git a/src/lib/components/SupervisorLayout.svelte b/src/lib/components/SupervisorLayout.svelte new file mode 100644 index 0000000..2a8225c --- /dev/null +++ b/src/lib/components/SupervisorLayout.svelte @@ -0,0 +1,39 @@ +<script lang="ts"> + import { page } from "$app/stores"; + import LL, { locales, setLocale } from "$lib/i18n/i18n"; + import { DarkMode, Dropdown, DropdownItem, Navbar, NavBrand, NavHamburger, NavLi, NavUl } from "flowbite-svelte"; + import { ChevronDownOutline } from "flowbite-svelte-icons"; + import type { Snippet } from "svelte"; + + let { children }: { children: Snippet } = $props(); + + function processActiveUrl(url: string) { + return url; + } + + const activeUrl = $derived(processActiveUrl($page.url.pathname)); +</script> + +<div class="h-screen flex flex-col"> + <Navbar> + <NavBrand href="/intern/tutor">{$LL.Navbar.Branding()}</NavBrand> + <NavHamburger /> + <NavUl {activeUrl}> + <NavLi class="cursor-pointer"> + {$LL.Navbar.Language()}<ChevronDownOutline class="w-4 h-4 ms-1 text-primary-800 dark:text-white inline" /> + </NavLi> + <Dropdown class="w-36 z-20"> + {#each locales as locale} + <DropdownItem on:click={()=>setLocale(locale)}>{$LL.Navbar.Languages[locale]()}</DropdownItem> + {/each} + </Dropdown> + <li><DarkMode btnClass="block py-2 pr-4 pl-3 md:p-0 rounded text-gray-700 hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-primary-700 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent cursor-pointer w-full" /></li> + </NavUl> + </Navbar> + + <div class="mb-auto"> + <div class="container relative mx-auto px-4 max-w-5xl mb-10 mt-4"> + {@render children()} + </div> + </div> +</div> diff --git a/src/lib/components/TutorLayout.svelte b/src/lib/components/TutorLayout.svelte new file mode 100644 index 0000000..8c3de46 --- /dev/null +++ b/src/lib/components/TutorLayout.svelte @@ -0,0 +1,41 @@ +<script lang="ts"> + import { page } from "$app/stores"; + import LL, { locales, setLocale } from "$lib/i18n/i18n"; + import { DarkMode, Dropdown, DropdownItem, Navbar, NavBrand, NavHamburger, NavLi, NavUl } from "flowbite-svelte"; + import { ChevronDownOutline } from "flowbite-svelte-icons"; + import type { Snippet } from "svelte"; + + let { children }: { children: Snippet } = $props(); + + function processActiveUrl(url: string) { + return url; + } + + const activeUrl = $derived(processActiveUrl($page.url.pathname)); +</script> + +<div class="h-screen flex flex-col"> + <Navbar> + <NavBrand href="/intern/tutor">{$LL.Navbar.Branding()}</NavBrand> + <NavHamburger /> + <NavUl {activeUrl}> + <NavLi href="/intern/tutor">Übersicht</NavLi> <!-- TODO i18n --> + <NavLi href="/intern/tutor/settings">Einstellungen</NavLi> <!-- TODO i18n --> + <NavLi class="cursor-pointer"> + {$LL.Navbar.Language()}<ChevronDownOutline class="w-4 h-4 ms-1 text-primary-800 dark:text-white inline" /> + </NavLi> + <Dropdown class="w-36 z-20"> + {#each locales as locale} + <DropdownItem on:click={()=>setLocale(locale)}>{$LL.Navbar.Languages[locale]()}</DropdownItem> + {/each} + </Dropdown> + <li><DarkMode btnClass="block py-2 pr-4 pl-3 md:p-0 rounded text-gray-700 hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-primary-700 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent cursor-pointer w-full" /></li> + </NavUl> + </Navbar> + + <div class="mb-auto"> + <div class="container relative mx-auto px-4 max-w-5xl mb-10 mt-4"> + {@render children()} + </div> + </div> +</div> diff --git a/src/lib/i18n/de.ts b/src/lib/i18n/de.ts index a378ede..79ce0a1 100644 --- a/src/lib/i18n/de.ts +++ b/src/lib/i18n/de.ts @@ -34,6 +34,9 @@ export default { "/admin/tutor/training/new": "Neue Tutschulung erstellen", "/admin/uploads": "Hochgeladene Bilder", "/admin/user": "Berechtigungen verwalten", + "/intern": "Login", + "/intern/rallye": "", // TODO + "/intern/tutor": "", // TODO }, Navbar: { Branding: "Fachschaft I/1", @@ -127,7 +130,10 @@ export default { Email: "RWTH-Mail", Phone: "Telefonnummer", PhoneHelper: "Für kurzfristige Kommunikation v.a. während der Erstiwoche wollen wir dich eventuell anrufen", - Address: "Adresse", + FullAddress: "Adresse", + Address: "Straße und Hausnummer", + Zip: "Postleitzahl", + City: "Ort", Gender: "Geschlecht", ShirtSize: "T-Shirt-Größe", DietaryRestriction: "Essgewohnheiten und Unverträglichkeiten", @@ -137,6 +143,8 @@ export default { Degree: "Abschluss", DegreeHelper: "Der Abschluss, den du gerade anstrebst", Training: "Schulung", + TrainingOption: "{date|dateWithWeekday} ({language})", + TrainingOptionFull: "{date|dateWithWeekday} ({language}) - voll", AlreadyTrained: "Bereits geschult", CoTutorWish: "Wunsch-Co-Tutor (optional)", CoTutorWishHelper: "Ihr müsst beide das gleiche Fach studieren. In der Informatik muss ein:e Mentor:in dabei sein.", @@ -250,4 +258,17 @@ export default { ], Footer: "Sofern nicht anders angegeben, ist die männliche Form in diesem Text nicht geschlechterspezifisch gemeint, sondern wurde aus Gründen der Lesbarkeit gewählt.", }, + Internal: { + Settigs: { + Headline: "Einstellungen", + ProfileHeadline: "Profil", + PasswordHeadline: "Passwort", + Save: "Speichern", + OldPassword: "Altes Passwort", + NewPassword: "Neues Passwort", + NewPasswordRepeat: "Neues Passwort wiederholen", + PasswordMismatch: "Die Passwörter stimmen nicht überein", + PasswordTooShort: "Das Passwort muss mindestens 8 Zeichen lang sein", + }, + }, } diff --git a/src/lib/i18n/en.ts b/src/lib/i18n/en.ts index b308da7..7607c87 100644 --- a/src/lib/i18n/en.ts +++ b/src/lib/i18n/en.ts @@ -36,6 +36,9 @@ export default { "/admin/tutor/training/new": "Create new tutor training", "/admin/uploads": "Uploaded photos", "/admin/user": "Manage Permissions", + "/intern": "Login", + "/intern/rallye": "", // TODO + "/intern/tutor": "", // TODO }, Navbar: { Branding: "Student Council I/1", @@ -130,7 +133,10 @@ export default { Email: "RWTH-Mail", Phone: "Phone Number", PhoneHelper: "For short-term communication, especially during the freshers' week, we may want to call you", - Address: "Address", + FullAddress: "Address", + Address: "Street and House Number", + Zip: "Zip Code", + City: "City", Gender: "Gender", ShirtSize: "T-Shirt Size", DietaryRestriction: "Dietary Restrictions", @@ -140,6 +146,8 @@ export default { Degree: "Degree", DegreeHelper: "The degree you are currently pursuing", Training: "Training", + TrainingOption: "{date|dateWithWeekday} ({language})", + TrainingOptionFull: "{date|dateWithWeekday} ({language}) - full", AlreadyTrained: "Already trained", CoTutorWish: "Co-Tutor Wish (optional)", CoTutorWishHelper: "You both have to study the same subject. In computer science, one of you has to be a mentor.", @@ -253,4 +261,17 @@ export default { ], Footer: "Sofern nicht anders angegeben, ist die männliche Form in diesem Text nicht geschlechterspezifisch gemeint, sondern wurde aus Gründen der Lesbarkeit gewählt.", }, + Internal: { + Settigs: { + Headline: "Settings", + ProfileHeadline: "Profile", + PasswordHeadline: "Password", + Save: "Save", + OldPassword: "Old password", + NewPassword: "New password", + NewPasswordRepeat: "Repeat new password", + PasswordMismatch: "The passwords do not match", + PasswordTooShort: "The password must be at least 8 characters long", + }, + }, } satisfies Translation<LocalizedString>; diff --git a/src/lib/i18n/i18n.ts b/src/lib/i18n/i18n.ts index 4b7a019..e864b70 100644 --- a/src/lib/i18n/i18n.ts +++ b/src/lib/i18n/i18n.ts @@ -141,6 +141,7 @@ function generateLObject(obj: typeof de, locale: Locale): Translation { const formatterBuilders: Record<string, (lang: Locale)=>(arg: any)=>unknown> = { dateLong: (lang: Locale)=>new Intl.DateTimeFormat(lang, {year: "numeric", month: "long", day: "2-digit"}).format, dateRangeLong: (lang: Locale)=>(value: [number|Date, number|Date])=>new Intl.DateTimeFormat(lang, {year: "numeric", month: "long", day: "2-digit"}).formatRange(value[0], value[1]), + dateWithWeekday: (lang: Locale)=>new Intl.DateTimeFormat(lang, {weekday: "short", year: "numeric", month: "long", day: "2-digit"}).format, sanitize: ()=>(input: string)=>input.replace(/[^a-zA-Z0-9_ ()äöüÄÖÜß-]/g, "-"), lowercase: ()=>(input: string)=>input.toLowerCase(), } diff --git a/src/lib/mail.ts b/src/lib/mail.ts index 4ca86b6..fd0fea3 100644 --- a/src/lib/mail.ts +++ b/src/lib/mail.ts @@ -72,7 +72,7 @@ export const types = ["tutor"] as const; export type TemplateType = typeof types[number]; export const configProps = ["currentSemester", "fresherWeekStart", "fresherWeekEnd", "rallyDate", "trainingsStart", "trainingsEnd"] as const satisfies (keyof Config)[]; -export type PartialConfig = Pick<Config, typeof configProps[number]>; +export type PartialConfig = Pick<Config, typeof configProps[number]> & { domain: string }; export type Variable = { name: string; description: string; @@ -105,6 +105,11 @@ export const variables: Record<TemplateType, Variable[]> = { description: "Telefonnummer des Empfängers", replacement: t=>t.phone, }, + { + name: "passwort", + description: "Passwort des Empfängers (nur bei Registrierungsmail)", + replacement: t=>t.password, + }, { name: "studiengang", description: "Studiengang des Empfängers", @@ -188,6 +193,16 @@ export const variables: Record<TemplateType, Variable[]> = { description: "Datum der Rallye", replacement: (t, l, c)=>new Date(c.rallyDate), }, + { + name: "tutorlogin", + description: "Anmeldelink für das Tutoren-Portal", + replacement: (t, l, c)=>`https://${c.domain}/intern#tutor`, + }, + { + name: "rallyelogin", + description: "Anmeldelink für das Rallye-Stationsbetreuer-Portal", + replacement: (t, l, c)=>`https://${c.domain}/intern#rallye`, + }, ], }; diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts new file mode 100644 index 0000000..0c5c679 --- /dev/null +++ b/src/lib/server/auth.ts @@ -0,0 +1,111 @@ +import { KEYCLOAK_ID, KEYCLOAK_ISSUER, KEYCLOAK_SCOPES, KEYCLOAK_SECRET, REQUIRED_GROUP } from "$env/static/private"; +import { SvelteKitAuth } from "@auth/sveltekit"; +import Credentials from "@auth/sveltekit/providers/credentials"; +import KeycloakProvider from "@auth/sveltekit/providers/keycloak"; +import { RallyStationSupervisor } from "./database/entities/RallyStationSupervisor.entity"; +import { Tutor } from "./database/entities/Tutor.entity"; +import bcrypt from "bcrypt"; +import { error, redirect, type Handle } from "@sveltejs/kit"; +import { User } from "./database/entities/User.entity"; + +export const { handle, signIn, signOut } = SvelteKitAuth({ + providers: [ + KeycloakProvider({ + clientId: KEYCLOAK_ID, + clientSecret: KEYCLOAK_SECRET, + issuer: KEYCLOAK_ISSUER, + profile(profile){ + return { + type: "admin", + name: profile.name ?? profile.preferred_username, + userId: profile.sub, + email: profile.email, + groups: profile.groups, + }; + }, + authorization: { + params: { + scope: KEYCLOAK_SCOPES, + }, + }, + }), + Credentials({ + id: "tutor", + name: "Tutor:in", + authorize: async ({email, password})=>{ + if(!email || !password || typeof email !== "string" || typeof password !== "string") return null; + const tutor = await Tutor.getByEmail(email); + if(!tutor) return null; + const passwordCorrect = await bcrypt.compare(password, tutor.password).catch(()=>false); + if(!passwordCorrect) return null; + return { type: "tutor", userId: tutor.id, email: tutor.email, name: tutor.firstname + " " + tutor.lastname }; + }, + credentials: { + email: { + label: "Email-Adresse", + type: "email", + }, + password: { + label: "Passwort", + type: "password", + }, + }, + }), + Credentials({ + id: "rally", + name: "Stationsbetreuer:in", + authorize: async ({email, password})=>{ + if(!email || !password || typeof email !== "string" || typeof password !== "string") return null; + const supervisor = await RallyStationSupervisor.getByEmail(email); + if(!supervisor) return null; + const passwordCorrect = await bcrypt.compare(password, supervisor.password).catch(()=>false); + if(!passwordCorrect) return null; + return { type: "rally", userId: supervisor.id, email: supervisor.email, name: supervisor.firstname + " " + supervisor.lastname }; + }, + credentials: { + email: { + label: "Email-Adresse", + type: "email", + }, + password: { + label: "Passwort", + type: "password", + }, + }, + }), + ], + callbacks: { + jwt({token, user}){ + if(user) return {...token, user}; + else return token; + }, + session({session, token}){ + if(token?.user) session.user = token.user; + return session; + } + }, +}); + +export const handleAuthorization: Handle = async ({event, resolve})=>{ + if(event.url.pathname.startsWith("/admin/") || event.url.pathname === "/admin"){ + const session = await event.locals.auth(); + if(!session || session.user.type !== "admin") redirect(303, "/"); + if(REQUIRED_GROUP && !session.user.groups.includes(REQUIRED_GROUP)) error(403, `Missing required group ${REQUIRED_GROUP}`); + const user = await User.getById(session.user.userId); + if(!user) redirect(303, "/"); + event.locals.user = user; + }else if(event.url.pathname.startsWith("/intern/tutor/") || event.url.pathname === "/intern/tutor"){ + const session = await event.locals.auth(); + if(!session || session.user.type !== "tutor") redirect(303, "/intern#tutor"); + const tutor = await Tutor.getById(session.user.userId); + if(!tutor) redirect(303, "/intern"); + event.locals.tutor = tutor; + }else if(event.url.pathname.startsWith("/intern/rallye/") || event.url.pathname === "/intern/rallye"){ + const session = await event.locals.auth(); + if(!session || session.user.type !== "rally") redirect(303, "/intern#rallye"); + const supervisor = await RallyStationSupervisor.getById(session.user.userId); + if(!supervisor) redirect(303, "/intern"); + event.locals.supervisor = supervisor; + } + return resolve(event); +}; diff --git a/src/lib/server/database/entities/Config.entity.ts b/src/lib/server/database/entities/Config.entity.ts index 33868f2..1200f27 100644 --- a/src/lib/server/database/entities/Config.entity.ts +++ b/src/lib/server/database/entities/Config.entity.ts @@ -35,6 +35,7 @@ export class Config { mailTemplates!: { tutorRegistered: MailTemplate; trainingInformation: MailTemplate; + resetPassword: MailTemplate; }; trainingMailReminderDays!: number; diff --git a/src/lib/server/database/entities/RallyStation.entity.ts b/src/lib/server/database/entities/RallyStation.entity.ts index 2a58a54..1e99da3 100644 --- a/src/lib/server/database/entities/RallyStation.entity.ts +++ b/src/lib/server/database/entities/RallyStation.entity.ts @@ -18,6 +18,9 @@ export class RallyStation { static async getAll(): Promise<RallyStation[]> { return (await sql`SELECT * FROM rally_stations`).map(row => RallyStation.parse(row)!); } + static async getBySupervisor(supervisorId: number): Promise<RallyStation[]> { + return (await sql`SELECT * FROM rally_stations WHERE id IN (SELECT station FROM rally_station_assignments WHERE supervisor=${supervisorId})`).map(row => RallyStation.parse(row)!); + } static async update(rallyStation: RallyStation): Promise<void> { await sql`UPDATE rally_stations SET ${sql(rallyStation)} WHERE id=${rallyStation.id}`; } diff --git a/src/lib/server/database/entities/RallyStationSupervisor.entity.ts b/src/lib/server/database/entities/RallyStationSupervisor.entity.ts index 0f24f65..5466f9c 100644 --- a/src/lib/server/database/entities/RallyStationSupervisor.entity.ts +++ b/src/lib/server/database/entities/RallyStationSupervisor.entity.ts @@ -12,6 +12,7 @@ export class RallyStationSupervisor { afternoon!: boolean; coSupervisorWish!: string; notes!: string; + password!: string; static parse(supervisor: any){ if(!supervisor) return undefined; @@ -22,6 +23,9 @@ export class RallyStationSupervisor { static async getById(id: number){ return RallyStationSupervisor.parse((await sql`SELECT * FROM rally_station_supervisors WHERE id=${id}`)[0]); } + static async getByEmail(email: string){ + return RallyStationSupervisor.parse((await sql`SELECT * FROM rally_station_supervisors WHERE email=${email}`)[0]); + } static async getAll(){ return (await sql`SELECT * FROM rally_station_supervisors`).map(RallyStationSupervisor.parse); } diff --git a/src/lib/server/database/entities/Tutor.entity.ts b/src/lib/server/database/entities/Tutor.entity.ts index 03f6a67..73b8ce3 100644 --- a/src/lib/server/database/entities/Tutor.entity.ts +++ b/src/lib/server/database/entities/Tutor.entity.ts @@ -11,7 +11,7 @@ export class Tutor { id!: number; firstname!: string; lastname!: string; - nickname?: string; + nickname?: string|null; birthday!: string; email!: string; phone!: string; @@ -19,17 +19,18 @@ export class Tutor { gender!: keyof typeof Gender; shirtSize!: string; degree!: keyof typeof Degree; - dietaryRestriction?: string; + dietaryRestriction?: string|null; studyProgram!: StudyProgram; training?: TutorTraining|null; sentTrainingMail!: boolean; trained!: boolean; coTutorWish!: string; mentor!: boolean; - tutorial?: Tutorial; + tutorial?: Tutorial|null; notes!: string; - number?: string; - former?: FormerTutor; + number?: string|null; + password!: string; + former?: FormerTutor|null; static parse(tutor: any){ if(!tutor) return undefined; @@ -41,12 +42,17 @@ export class Tutor { return t; } - // TODO include tutorial in getById and getAll + // TODO include tutorial in getById, getByEmail and getAll static async getById(id: number){ const row = (await sql`SELECT row_to_json(tutors.*) AS tutor, row_to_json(tutor_trainings.*) AS training, row_to_json(study_programs.*) AS study_program, row_to_json(former_tutors.*) AS former FROM tutors LEFT JOIN tutor_trainings ON tutors.training=tutor_trainings.id LEFT JOIN study_programs ON tutors.study_program=study_programs.id LEFT JOIN former_tutors ON tutors.email=former_tutors.email WHERE tutors.id=${id}`)[0]; if(!row) return undefined; return Tutor.parse(Object.assign(row.tutor, { training: row.training, studyProgram: row.studyProgram })); } + static async getByEmail(email: string){ + const row = (await sql`SELECT row_to_json(tutors.*) AS tutor, row_to_json(tutor_trainings.*) AS training, row_to_json(study_programs.*) AS study_program, row_to_json(former_tutors.*) AS former FROM tutors LEFT JOIN tutor_trainings ON tutors.training=tutor_trainings.id LEFT JOIN study_programs ON tutors.study_program=study_programs.id LEFT JOIN former_tutors ON tutors.email=former_tutors.email WHERE tutors.email=${email}`)[0]; + if(!row) return undefined; + return Tutor.parse(Object.assign(row.tutor, { training: row.training, studyProgram: row.studyProgram })); + } static async getAll(){ const rows = await sql`SELECT row_to_json(tutors.*) AS tutor, row_to_json(tutor_trainings.*) AS training, row_to_json(study_programs.*) AS study_program, row_to_json(former_tutors.*) AS former FROM tutors LEFT JOIN tutor_trainings ON tutors.training=tutor_trainings.id LEFT JOIN study_programs ON tutors.study_program=study_programs.id LEFT JOIN former_tutors ON tutors.email=former_tutors.email`; return rows.map(row => Tutor.parse(Object.assign(row.tutor, { training: row.training, studyProgram: row.studyProgram }))!); @@ -54,7 +60,11 @@ export class Tutor { static async create(tutor: Omit<Tutor, "id"|"studyProgram"|"training"|"tutorial"|"former">&{studyProgram: number, training?: number|null}){ return (await sql`INSERT INTO tutors ${sql(tutor)} RETURNING id`)[0].id as number; } - static async update(tutor: PartialExcept<Omit<Tutor, "studyProgram"|"training"|"former"> & {studyProgram: number, training?: number}, "id">){ - return sql`UPDATE tutors SET ${sql({...tutor, tutorial: tutor.tutorial?.id})} WHERE id=${tutor.id}`; + static async update(tutor: PartialExcept<Omit<Tutor, "studyProgram"|"training"|"former"> & {studyProgram: number, training: number|null}, "id">){ + let t = {...tutor} as any; + if(t.tutorial) t.tutorial = t.tutorial.id; + else if(t.tutorial === null) t.tutorial = null; + return sql`UPDATE tutors SET ${sql({...t})} WHERE id=${tutor.id}`; } } +// rename sent_trainings_mail to sent_training_mail diff --git a/src/lib/server/database/entities/TutorTraining.entity.ts b/src/lib/server/database/entities/TutorTraining.entity.ts index 5a5c9f8..9f7aafc 100644 --- a/src/lib/server/database/entities/TutorTraining.entity.ts +++ b/src/lib/server/database/entities/TutorTraining.entity.ts @@ -1,3 +1,4 @@ +import type { Locale } from "$lib/i18n/i18n"; import { sql } from "../db"; import { Tutor } from "./Tutor.entity"; @@ -5,6 +6,7 @@ export class TutorTraining { id!: number; date!: string; location!: string; + language!: Locale; maxParticipants!: number; internal!: boolean; notes!: string; @@ -20,7 +22,7 @@ export class TutorTraining { } static async getById(id: number): Promise<TutorTraining|undefined> { - return TutorTraining.parse((await sql`SELECT tutor_trainings.*, array_agg(json_build_object('tutor', row_to_json(tutors.*), 'studyProgram', row_to_json(study_programs.*))) AS participants FROM tutor_trainings LEFT JOIN tutors ON tutor_trainings.id=tutors.training JOIN study_programs ON tutors.study_program=study_programs.id WHERE tutor_trainings.id=${id} GROUP BY tutor_trainings.id`)[0]); + return TutorTraining.parse((await sql`SELECT tutor_trainings.*, array_agg(json_build_object('tutor', row_to_json(tutors.*), 'studyProgram', row_to_json(study_programs.*))) AS participants FROM tutor_trainings LEFT JOIN tutors ON tutor_trainings.id=tutors.training LEFT JOIN study_programs ON tutors.study_program=study_programs.id WHERE tutor_trainings.id=${id} GROUP BY tutor_trainings.id`)[0]); } static async getAll(): Promise<TutorTraining[]> { return (await sql`SELECT tutor_trainings.*, array_agg(json_build_object('tutor', row_to_json(tutors.*), 'studyProgram', row_to_json(study_programs.*))) AS participants FROM tutor_trainings LEFT JOIN tutors ON tutor_trainings.id=tutors.training LEFT JOIN study_programs ON tutors.study_program=study_programs.id GROUP BY tutor_trainings.id`).map(row=>TutorTraining.parse(row)!); diff --git a/src/lib/server/database/entities/Tutorial.entity.ts b/src/lib/server/database/entities/Tutorial.entity.ts index 2e9d7ef..5825cff 100644 --- a/src/lib/server/database/entities/Tutorial.entity.ts +++ b/src/lib/server/database/entities/Tutorial.entity.ts @@ -26,6 +26,15 @@ export class Tutorial { const rows = await sql`SELECT row_to_json(tutorials.*) AS tutorial, row_to_json(study_programs.*) AS study_program, json_agg(row_to_json(tutors.*)) AS tutors FROM tutorials LEFT JOIN study_programs ON tutorials.study_program=study_programs.id LEFT JOIN tutors ON tutorials.id=tutors.tutorial GROUP BY tutorials.id, study_programs.id`; return rows.map(row => Tutorial.parse(Object.assign(row.tutorial, { studyProgram: row.studyProgram, tutors: row.tutors }))!); } + static async getByStudyProgram(id: number): Promise<Tutorial[]> { + const rows = await sql`SELECT row_to_json(tutorials.*) AS tutorial, row_to_json(study_programs.*) AS study_program, json_agg(row_to_json(tutors.*)) AS tutors FROM tutorials LEFT JOIN study_programs ON tutorials.study_program=study_programs.id LEFT JOIN tutors ON tutorials.id=tutors.tutorial WHERE study_program=${id} GROUP BY tutorials.id, study_programs.id`; + return rows.map(row => Tutorial.parse(Object.assign(row.tutorial, { studyProgram: row.studyProgram, tutors: row.tutors }))!); + } + static async getByTutor(id: number): Promise<Tutorial|undefined> { + const row = (await sql`SELECT row_to_json(tutorials.*) AS tutorial, row_to_json(study_programs.*) AS study_program, json_agg(row_to_json(tutors.*)) AS tutors FROM tutorials LEFT JOIN study_programs ON tutorials.study_program=study_programs.id LEFT JOIN tutors ON tutorials.id=tutors.tutorial WHERE tutors.id=${id} GROUP BY tutorials.id, study_programs.id`)[0]; + if(!row) return; + return Tutorial.parse(Object.assign(row.tutorial, { studyProgram: row.studyProgram, tutors: row.tutors })); + } static async create(tutorial: Omit<Tutorial, "id"|"studyProgram"|"tutors"> & {studyProgram: number}, tutors: number[] = []): Promise<number> { return sql.begin(async sql=>{ const id = (await sql<{id: number}[]>`INSERT INTO tutorials ${sql(tutorial)} RETURNING id`)[0].id; diff --git a/src/lib/server/database/migrations/0_init.sql b/src/lib/server/database/migrations/0_init.sql index 4fa987a..fee5408 100644 --- a/src/lib/server/database/migrations/0_init.sql +++ b/src/lib/server/database/migrations/0_init.sql @@ -48,6 +48,7 @@ CREATE TABLE IF NOT EXISTS tutor_trainings ( "id" SERIAL PRIMARY KEY, "date" TEXT NOT NULL, "location" TEXT NOT NULL, + "language" TEXT NOT NULL, "max_participants" INT NOT NULL DEFAULT 0, "internal" BOOLEAN NOT NULL DEFAULT FALSE, "notes" TEXT NOT NULL DEFAULT '' @@ -74,7 +75,8 @@ CREATE TABLE IF NOT EXISTS tutors ( "mentor" BOOLEAN NOT NULL DEFAULT FALSE, "tutorial" INT DEFAULT NULL REFERENCES tutorials(id), "number" TEXT, - "notes" TEXT NOT NULL DEFAULT '' + "notes" TEXT NOT NULL DEFAULT '', + "password" TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS former_tutors ( @@ -100,7 +102,8 @@ CREATE TABLE IF NOT EXISTS rally_station_supervisors ( "morning" BOOLEAN NOT NULL DEFAULT FALSE, "afternoon" BOOLEAN NOT NULL DEFAULT FALSE, "co_supervisor_wish" TEXT NOT NULL DEFAULT '', - "notes" TEXT NOT NULL DEFAULT '' + "notes" TEXT NOT NULL DEFAULT '', + "password" TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS rally_stations ( diff --git a/src/lib/server/mail.ts b/src/lib/server/mail.ts index 7ed3838..a9ff313 100644 --- a/src/lib/server/mail.ts +++ b/src/lib/server/mail.ts @@ -6,7 +6,7 @@ import { compileTemplate, parseTemplate, renderTemplate, subjectFormatter, type import { Config } from "./database/entities/Config.entity"; import ical from "ical-generator"; import type SMTPTransport from "nodemailer/lib/smtp-transport"; -import type { TutorTraining } from "./database/entities/TutorTraining.entity"; +import { DOMAIN } from "$env/static/private"; const transporter = nodemailer.createTransport({ host: env.MAIL_HOST, @@ -124,9 +124,9 @@ export function sendMail({ template, tutors, attachments = [], from, icalEvent } const subjectParts = parseTemplate(template.type, template.subject); const config = Config.get(); return Promise.all(tutors.map(tutor => { - const compiled = compileTemplate(template.type, textParts, tutor, config); + const compiled = compileTemplate(template.type, textParts, tutor, {...config, domain: DOMAIN}); const rendered = renderTemplate(compiled); - const subject = compileTemplate(template.type, subjectParts, tutor, config, subjectFormatter); + const subject = compileTemplate(template.type, subjectParts, tutor, {...config, domain: DOMAIN}, subjectFormatter); return transporter.sendMail({ to: tutor.email, from: from || env.MAIL_FROM, diff --git a/src/routes/(non-admin)/+layout.svelte b/src/routes/(non-admin)/+layout.svelte index e1faa1e..527bc8a 100644 --- a/src/routes/(non-admin)/+layout.svelte +++ b/src/routes/(non-admin)/+layout.svelte @@ -4,6 +4,6 @@ let { data, children } = $props(); </script> -<MainLayout headerLinks={data.headerLinks as string[]}> +<MainLayout headerLinks={data.headerLinks as string[]} domain={data.domain}> {@render children()} </MainLayout> diff --git a/src/routes/(non-admin)/impressum/+page.svelte b/src/routes/(non-admin)/impressum/+page.svelte index 9ec9bde..4be8b2c 100644 --- a/src/routes/(non-admin)/impressum/+page.svelte +++ b/src/routes/(non-admin)/impressum/+page.svelte @@ -1,6 +1,8 @@ <script lang="ts"> import { LL } from "$lib/i18n/i18n"; import { A, Blockquote, Heading, P } from "flowbite-svelte"; + + let { data } = $props(); </script> <Heading tag="h1" customSize="text-4xl font-bold" class="mb-4 text-center">{$LL.Legal.Headline()}</Heading> @@ -19,7 +21,7 @@ <P class="mb-4"> {$LL.Legal.EmailGeneral()}: <A href="mailto:fs@fsmpi.rwth-aachen.de">fs@fsmpi.rwth-aachen.de</A><br> {$LL.Legal.EmailESA()}: <A href="mailto:esa@fsmpi.rwth-aachen.de">esa@fsmpi.rwth-aachen.de</A><br> - {$LL.Legal.Internet()}: <A href="https://esa.fsmpi.rwth-aachen.de/">https://esa.fsmpi.rwth-aachen.de/</A> + {$LL.Legal.Internet()}: <A href="https://{data.domain}/">https://{data.domain}/</A> </P> <P class="mb-4">{$LL.Legal.LiabilityNote()}</P> diff --git a/src/routes/(non-admin)/login/+page.server.ts b/src/routes/(non-admin)/login/+page.server.ts index 6baa8cf..10e7e6c 100644 --- a/src/routes/(non-admin)/login/+page.server.ts +++ b/src/routes/(non-admin)/login/+page.server.ts @@ -5,7 +5,7 @@ import { error, redirect } from "@sveltejs/kit"; export const load = async (event)=>{ const session = await event.locals.auth(); - if(!session) redirect(303, "/"); + if(session?.user.type !== "admin") redirect(303, "/"); if(REQUIRED_GROUP && !session.user.groups.includes(REQUIRED_GROUP)) error(403, `Missing required group ${REQUIRED_GROUP}`); await User.getOrCreate(session.user.userId, session.user.name); if(Config.get()) redirect(303, "/admin"); diff --git a/src/routes/(non-admin)/tutor/+page.server.ts b/src/routes/(non-admin)/tutor/+page.server.ts index bb61ac8..6c4b550 100644 --- a/src/routes/(non-admin)/tutor/+page.server.ts +++ b/src/routes/(non-admin)/tutor/+page.server.ts @@ -9,7 +9,7 @@ export const load: PageServerLoad = async () => { fresherWeekStart: config?.fresherWeekStart, fresherWeekEnd: config?.fresherWeekEnd, registrationOpen: config?.tutorRegistrationOpen ?? false, - dates: tutorTrainings.filter(t=>!t.internal).map(t=>t.date), + trainings: tutorTrainings.filter(t=>!t.internal).map(t=>({date: t.date, language: t.language})), semester: config?.currentSemester, trainingsStart: config?.trainingsStart, trainingsEnd: config?.trainingsEnd, diff --git a/src/routes/(non-admin)/tutor/+page.svelte b/src/routes/(non-admin)/tutor/+page.svelte index 4f0f45c..0f79a42 100644 --- a/src/routes/(non-admin)/tutor/+page.svelte +++ b/src/routes/(non-admin)/tutor/+page.svelte @@ -26,10 +26,10 @@ {$LL.Tutor.WhenContent({fresherWeek: [new Date(data.fresherWeekStart), new Date(data.fresherWeekEnd)], start: data.trainingsStart, end: data.trainingsEnd})} </P> <List tag="ul" class="mb-6"> - {#each data.dates as date} - <Li class="text-gray-900 dark:text-white">{dateFormatter.format(new Date(date))}</Li> + {#each data.trainings as training} + <Li class="text-gray-900 dark:text-white">{dateFormatter.format(new Date(training.date))} ({$LL.Navbar.Languages[training.language]()})</Li> {/each} - {#if data.dates.length === 0} + {#if data.trainings.length === 0} <Li class="text-gray-900 dark:text-white">{$LL.Tutor.NoDates()}</Li> {/if} </List> @@ -44,7 +44,7 @@ <div class="flex mb-6 justify-center"> {#if data.registrationOpen} - <Button href="/tutor/register" size="xl"> + <Button href="/tutor/register/" size="xl"> <svg aria-hidden="true" class="mr-2 -ml-1 w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg> {$LL.Tutor.RegistrationButton()} </Button> diff --git a/src/routes/(non-admin)/tutor/register/+page.server.ts b/src/routes/(non-admin)/tutor/register/+page.server.ts index 5a2baf8..4ab632c 100644 --- a/src/routes/(non-admin)/tutor/register/+page.server.ts +++ b/src/routes/(non-admin)/tutor/register/+page.server.ts @@ -6,7 +6,8 @@ import { StudyProgram } from "$lib/server/database/entities/StudyProgram.entity. import { TutorTraining } from "$lib/server/database/entities/TutorTraining.entity.js"; import Gender from "$lib/genders.js"; import Degree from "$lib/degrees.js"; -import { sendTutorRegisteredMail } from "$lib/server/mail.js"; +import { sendTrainingMail, sendTutorRegisteredMail } from "$lib/server/mail.js"; +import bcrypt from "bcrypt"; export const load: PageServerLoad = async ()=>{ const config = Config.get(); @@ -14,7 +15,7 @@ export const load: PageServerLoad = async ()=>{ const missingTutorCounts = await StudyProgram.getMissingTutors(); const tutorTrainings = await TutorTraining.getAll(); return { - tutorTrainings: tutorTrainings.filter(t=>!t.internal).map(t=>({id: t.id, date: t.date, location: t.location, isFull: t.participants.length>=t.maxParticipants})), + tutorTrainings: tutorTrainings.filter(t=>!t.internal).map(t=>({id: t.id, date: t.date, language: t.language, location: t.location, isFull: t.participants.length>=t.maxParticipants})), registrationOpen: config.tutorRegistrationOpen, shirtSizes: config.shirtSizes, fresherWeekStart: config.fresherWeekStart, @@ -34,6 +35,8 @@ export const actions = { const phone = data.get("phone"); const shirtSize = data.get("shirt-size"); const address = data.get("address"); + const zip = data.get("zip"); + const city = data.get("city"); const gender = data.get("gender"); const dietaryRestriction = data.get("dietary-restriction"); const studyProgramId = Number(data.get("study-program")); @@ -74,7 +77,7 @@ export const actions = { if(typeof degree !== "string" || !(degree in Degree)){ throw error(422, "Ungültiger Abschluss"); } - if(typeof firstname !== "string" || typeof lastname !== "string" || typeof nickname !== "string" || typeof phone !== "string" || typeof address !== "string" || typeof coTutorWish !== "string"){ + if(typeof firstname !== "string" || typeof lastname !== "string" || typeof nickname !== "string" || typeof phone !== "string" || typeof address !== "string" || typeof zip !== "string" || typeof city !== "string" || typeof coTutorWish !== "string"){ throw error(422, "Ungültige Eingabe"); } if(typeof trainingString !== "string"){ @@ -92,8 +95,10 @@ export const actions = { if(trainingId !== null && !training){ throw error(422, "Ungültige Schulung"); } - const waitlist = ((await StudyProgram.getMissingTutors())[studyProgramId] || 1) <= 0; + const waitlist = ((await StudyProgram.getMissingTutors())[studyProgramId] ?? 1) <= 0; const birthdate = new Date(birthday).toISOString().slice(0, 10); + const password = Math.random().toString(36).slice(2); + const passwordHash = await bcrypt.hash(password, 10); const tutorId = await Tutor.create({ firstname, lastname, @@ -102,7 +107,7 @@ export const actions = { email, phone, shirtSize, - address, + address: `${address}, ${zip} ${city}`, gender: gender as keyof typeof Gender, studyProgram: studyProgramId, degree: degree as keyof typeof Degree, @@ -113,9 +118,10 @@ export const actions = { trained: false, dietaryRestriction, sentTrainingMail: false, + password: passwordHash, // TODO add waitlist }); - sendTutorRegisteredMail({ // no await, doesnt need to block response + const mockTutor: Tutor = { id: tutorId, firstname, lastname, @@ -135,6 +141,19 @@ export const actions = { trained: false, dietaryRestriction, sentTrainingMail: false, + password, + }; + sendTutorRegisteredMail(mockTutor).then(()=>{ // no await, doesnt need to block response + if(training){ + const { trainingMailReminderDays } = Config.get(); + const trainingDate = Date.parse(training.date); + const now = Date.now(); + if(trainingDate < now) return; + const daysUntilTraining = (trainingDate - now) / 1000 / 60 / 60 / 24; + if(daysUntilTraining <= trainingMailReminderDays){ + return sendTrainingMail(mockTutor); + } + } }).catch(err=>{ console.error(err); }); diff --git a/src/routes/(non-admin)/tutor/register/+page.svelte b/src/routes/(non-admin)/tutor/register/+page.svelte index 75cc83d..4005b61 100644 --- a/src/routes/(non-admin)/tutor/register/+page.svelte +++ b/src/routes/(non-admin)/tutor/register/+page.svelte @@ -53,127 +53,145 @@ <Heading tag="h1" customSize="text-4xl font-bold" class="mb-6 text-center">{$LL.Tutor.SignUp.Headline()}</Heading> {#if data.registrationOpen} - {#if success} - {#if waitlist} - <!-- TODO i18n and proper message --> - <Alert border color="yellow" class="mb-6">Du wurdest zur Warteliste hinzugefügt</Alert> - {:else} - <Alert border color="green" class="mb-6">{$LL.Tutor.SignUp.SignedUpSuccessfully()}</Alert> - {/if} - {/if} - {#if error?.message} - <Alert border color="red" class="mb-6">{error.message}</Alert> - {/if} - - <form method="post" use:enhance={({formElement, formData, cancel})=>{ - if(!validateBirthday(formData.get("birthday")) || validateEmail(formData.get("email")) >= 0) { - addMessage({ type: "error", text: $LL.Tutor.SignUp.FormValidationErrors.InvalidForm() }); - return cancel(); +{#if success} +{#if waitlist} +<!-- TODO i18n and proper message --> +<Alert border color="yellow" class="mb-6">Du wurdest zur Warteliste hinzugefügt</Alert> +{:else} +<Alert border color="green" class="mb-6">{$LL.Tutor.SignUp.SignedUpSuccessfully()}</Alert> +{/if} +{/if} +{#if error?.message} +<Alert border color="red" class="mb-6">{error.message}</Alert> +{/if} + +<form method="post" use:enhance={({formElement, formData, cancel})=>{ + if(!validateBirthday(formData.get("birthday")) || validateEmail(formData.get("email")) >= 0) { + addMessage({ type: "error", text: $LL.Tutor.SignUp.FormValidationErrors.InvalidForm() }); + return cancel(); + } + return ({result})=>{ + if(result.type === "success" && result.data?.success) { + waitlist = !!result.data.waitlist; + success = true; + error = undefined; + formElement.reset(); + window.scrollTo({top: 0, behavior: "smooth"}); + }else if(result.type === "error"){ + error = result.error; + window.scrollTo({top: 0, behavior: "smooth"}); } - return ({result})=>{ - if(result.type === "success" && result.data?.success) { - waitlist = !!result.data.waitlist; - success = true; - error = undefined; - formElement.reset(); - window.scrollTo({top: 0, behavior: "smooth"}); - }else if(result.type === "error"){ - error = result.error; - window.scrollTo({top: 0, behavior: "smooth"}); - } - }; - }}> - <Label class="mb-2"> + }; +}}> + <div class="lg:flex md:gap-2"> + <Label class="mb-2 w-full"> {$LL.Tutor.SignUp.FirstName()} <Input type="text" name="firstname" required /> </Label> - <Label class="mb-2"> + <Label class="mb-2 w-full"> {$LL.Tutor.SignUp.LastName()} <Input type="text" name="lastname" required /> </Label> - <Label class="mb-2"> - {$LL.Tutor.SignUp.NickName()} - <Input type="text" name="nickname" /> - </Label> - <Label class="mb-2"> - {$LL.Tutor.SignUp.BirthDate()} - <div> - <Input type="date" name="birthday" required on:change={(e)=>validateBirthday(e.target?.value)} /> - {#if birthdayError} - <Helper color="red">{$LL.Tutor.SignUp.InvalidDate()}</Helper> - {:else if ageError} - <Helper color="red">{$LL.Tutor.SignUp.NotOldEnough()}</Helper> - {/if} - </div> - </Label> - <Label class="mb-2"> - {$LL.Tutor.SignUp.Email()} - <Input type="email" name="email" required color={emailError>=0?"red":"base"} on:blur={e=>validateEmail(e.target?.value)} /> - {#if emailError>=0} - <Helper color="red">{$LL.Tutor.SignUp.FormValidationErrors.InvalidEmail[emailError]()}</Helper> + </div> + <Label class="mb-2"> + {$LL.Tutor.SignUp.NickName()} + <Input type="text" name="nickname" /> + </Label> + <Label class="mb-2"> + {$LL.Tutor.SignUp.BirthDate()} + <div> + <Input type="date" name="birthday" required on:change={(e)=>validateBirthday((e.target! as HTMLInputElement).value)} /> + {#if birthdayError} + <Helper color="red">{$LL.Tutor.SignUp.InvalidDate()}</Helper> + {:else if ageError} + <Helper color="red">{$LL.Tutor.SignUp.NotOldEnough()}</Helper> {/if} - </Label> - <Label class="mb-2"> - {$LL.Tutor.SignUp.Phone()} - <Input type="tel" name="phone" required /> - <Helper>{$LL.Tutor.SignUp.PhoneHelper()}</Helper> - </Label> - <Label class="mb-2"> + </div> + </Label> + <Label class="mb-2"> + {$LL.Tutor.SignUp.Email()} + <Input type="email" name="email" required color={emailError>=0?"red":"base"} on:blur={e=>validateEmail((e.target! as HTMLInputElement).value)} /> + {#if emailError>=0} + <Helper color="red">{$LL.Tutor.SignUp.FormValidationErrors.InvalidEmail[emailError]()}</Helper> + {/if} + </Label> + <Label class="mb-2"> + {$LL.Tutor.SignUp.Phone()} + <Input type="tel" name="phone" required /> + <Helper>{$LL.Tutor.SignUp.PhoneHelper()}</Helper> + </Label> + <div class="md:flex md:gap-2"> + <Label class="mb-2 md:w-full"> {$LL.Tutor.SignUp.Address()} - <Textarea name="address" required /> - </Label> - <Label class="mb-2"> - {$LL.Tutor.SignUp.Gender()} - <Select name="gender" required placeholder={$LL.Tutor.SignUp.PleaseChoose()} items={Object.entries(Gender).map(([value, name])=>({value, name: name[$locale]}))} /> - </Label> - <Label class="mb-2"> - {$LL.Tutor.SignUp.ShirtSize()} - <Select name="shirt-size" required placeholder={$LL.Tutor.SignUp.PleaseChoose()} items={data.shirtSizes.map(x=>({name:x,value:x}))} /> - </Label> - <Label class="mb-2"> - {$LL.Tutor.SignUp.DietaryRestriction()} - <Input type="text" name="dietary-restriction" /> - <Helper>{$LL.Tutor.SignUp.DietaryRestrictionHelper()}</Helper> + <Input name="address" required /> </Label> - <Label class="mb-2"> + <div class="xs:flex xs:gap-2 w-full"> + <Label class="mb-2 w-full xs:w-40"> + {$LL.Tutor.SignUp.Zip()} + <Input name="zip" required /> + </Label> + <Label class="mb-2 w-full md:w-full"> + {$LL.Tutor.SignUp.City()} + <Input name="city" required /> + </Label> + </div> + </div> + <Label class="mb-2"> + {$LL.Tutor.SignUp.Gender()} + <Select name="gender" required placeholder={$LL.Tutor.SignUp.PleaseChoose()} items={Object.entries(Gender).map(([value, name])=>({value, name: name[$locale]}))} /> + </Label> + <Label class="mb-2"> + {$LL.Tutor.SignUp.ShirtSize()} + <Select name="shirt-size" required placeholder={$LL.Tutor.SignUp.PleaseChoose()} items={data.shirtSizes.map(x=>({name:x,value:x}))} /> + </Label> + <Label class="mb-2"> + {$LL.Tutor.SignUp.DietaryRestriction()} + <Input type="text" name="dietary-restriction" /> + <Helper>{$LL.Tutor.SignUp.DietaryRestrictionHelper()}</Helper> + </Label> + <div class="lg:flex lg:gap-2"> + <Label class="mb-2 w-full"> {$LL.Tutor.SignUp.StudyProgram()} <Select name="study-program" required placeholder={$LL.Tutor.SignUp.PleaseChoose()} items={data.studyPrograms.map(x=>({name: x.name[$locale], value: x.id}))} bind:value={studyProgramId} /> {#if studyProgram?.hasWaitlist} <Helper color="red">{$LL.Tutor.SignUp.StudyProgramWaitlist({studyProgram: studyProgram!.name[$locale]??""})}</Helper> {/if} </Label> - <Label class="mb-2"> + <Label class="mb-2 w-full"> {$LL.Tutor.SignUp.Degree()} <Select name="degree" required placeholder={$LL.Tutor.SignUp.PleaseChoose()} items={Object.entries(Degree).map(([value, name])=>({value, name: name[$locale]}))} /> <Helper>{$LL.Tutor.SignUp.DegreeHelper()}</Helper> </Label> - <Label class="mb-2"> - {$LL.Tutor.SignUp.Training()} - <Select name="training" required placeholder={$LL.Tutor.SignUp.PleaseChoose()}> - {#each data.tutorTrainings as t} - <!--TODO i18n for "voll"--> - <option value={t.date} disabled={!isTrainingInFuture(t.date) || t.isFull}>{dateFormatter.format(new Date(t.date))}{t.isFull ? " (voll)" : ""}</option> - {/each} - <option value="-">{$LL.Tutor.SignUp.AlreadyTrained()}</option> - </Select> - </Label> - <Label class="mb-2"> - {$LL.Tutor.SignUp.CoTutorWish()} - <Input name="co-tutor" type="text" /> - <Helper>{$LL.Tutor.SignUp.CoTutorWishHelper()}</Helper> - </Label> - <Checkbox class="mb-2" name="mentor">{$LL.Tutor.SignUp.IsMentor()}</Checkbox> - <Checkbox class="mb-2" required> - <LocalizedText key="Tutor.SignUp.ReadPrivacyPolicy"> - <A class="mx-1" href="/datenschutz" target="_blank">{$LL.Tutor.SignUp.PrivacyPolicy()}</A> - </LocalizedText> - </Checkbox> - <Button type="submit" class="mb-2">{$LL.Tutor.SignUp.Submit()}</Button> - </form> -{:else} - <Alert border color="red"> - <LocalizedText key="Tutor.SignUp.SignUpClosed"> - <A href="mailto:esa@fsmpi.rwth-aachen.de">esa@fsmpi.rwth-aachen.de</A> + </div> + <Label class="mb-2"> + {$LL.Tutor.SignUp.Training()} + <Select name="training" required placeholder={$LL.Tutor.SignUp.PleaseChoose()}> + {#each data.tutorTrainings as t} + {@const date = new Date(t.date)} + {@const language = $LL.Navbar.Languages[t.language]()} + {@const translationFunction = t.isFull ? $LL.Tutor.SignUp.TrainingOptionFull : $LL.Tutor.SignUp.TrainingOption} + <option value={t.date} disabled={!isTrainingInFuture(t.date) || t.isFull}>{translationFunction({date, language})}</option> + {/each} + <option value="-">{$LL.Tutor.SignUp.AlreadyTrained()}</option> + </Select> + </Label> + <Label class="mb-2"> + {$LL.Tutor.SignUp.CoTutorWish()} + <Input name="co-tutor" type="text" /> + <Helper>{$LL.Tutor.SignUp.CoTutorWishHelper()}</Helper> + </Label> + <Checkbox class="mb-2" name="mentor">{$LL.Tutor.SignUp.IsMentor()}</Checkbox> + <Checkbox class="mb-2" required> + <LocalizedText key="Tutor.SignUp.ReadPrivacyPolicy"> + <A class="mx-1" href="/datenschutz" target="_blank">{$LL.Tutor.SignUp.PrivacyPolicy()}</A> </LocalizedText> - </Alert> + </Checkbox> + <Button type="submit" class="mb-2">{$LL.Tutor.SignUp.Submit()}</Button> +</form> +{:else} +<Alert border color="red"> + <LocalizedText key="Tutor.SignUp.SignUpClosed"> + <A href="mailto:esa@fsmpi.rwth-aachen.de">esa@fsmpi.rwth-aachen.de</A> + </LocalizedText> +</Alert> {/if} diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index a3c1bd3..123b1b1 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,3 +1,4 @@ +import { DOMAIN } from "$env/static/private"; import { Config } from "$lib/server/database/entities/Config.entity"; export const load = (event)=>{ @@ -6,5 +7,6 @@ export const load = (event)=>{ return { locale, semester, + domain: DOMAIN, }; } diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 6c327fa..7e2f692 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -6,5 +6,6 @@ export const load = (event)=>{ return { locale, semester: event.data.semester, + domain: event.data.domain, }; }; diff --git a/src/routes/admin/setup/+page.server.ts b/src/routes/admin/setup/+page.server.ts index 6d0c9e5..2bdc292 100644 --- a/src/routes/admin/setup/+page.server.ts +++ b/src/routes/admin/setup/+page.server.ts @@ -110,6 +110,12 @@ export const actions = { text: { de: "", en: "" }, type: "tutor", }, + resetPassword: { + replyTo: "-", + subject: { de: "", en: "" }, + text: { de: "", en: "" }, + type: "tutor", + }, }, trainingMailReminderDays: 7, }); diff --git a/src/routes/admin/templates/+page.server.ts b/src/routes/admin/templates/+page.server.ts index 9bd9ab2..7ea0657 100644 --- a/src/routes/admin/templates/+page.server.ts +++ b/src/routes/admin/templates/+page.server.ts @@ -7,6 +7,7 @@ import { pick, type Localized } from '$lib/utils'; import { error } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; import { Permission } from '$lib/perms'; +import { DOMAIN } from '$env/static/private'; export const load = (async () => { const config = Config.get(); @@ -17,7 +18,7 @@ export const load = (async () => { trainingMailReminderDays: config.trainingMailReminderDays, studyPrograms: studyPrograms.map(sp => ({ id: sp.id, name: sp.name })), trainings: trainings.map(t => ({ id: t.id, date: t.date, location: t.location })), - config: pick(config, configProps as unknown as (keyof Config)[]), + config: Object.assign(pick(config, configProps), { domain: DOMAIN }), }; }) satisfies PageServerLoad; diff --git a/src/routes/admin/templates/+page.svelte b/src/routes/admin/templates/+page.svelte index d63f358..829078c 100644 --- a/src/routes/admin/templates/+page.svelte +++ b/src/routes/admin/templates/+page.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import { enhance } from "$app/forms"; - import { invalidateAll } from "$app/navigation"; + import { goto, invalidateAll } from "$app/navigation"; + import { page } from "$app/stores"; import MailInput from "$lib/components/MailInput.svelte"; import { addMessage } from "$lib/messages"; import { Permission } from "$lib/perms.js"; @@ -10,6 +11,21 @@ let templates = $state(data.templates); let canEdit = data.user.permissions.has(Permission.UPDATE_MAIL_TEMPLATES); + + let tabNames = [ + "tutorRegistered", + "trainingReminder", + "resetPassword", + ]; + let selectedTab = $page.url.hash.substring(1) || tabNames[0]; + let openTabs = $state(tabNames.map(name => name === selectedTab)); + if(openTabs.indexOf(true) === -1) openTabs[0] = true; + $effect(()=>{ + const index = openTabs.indexOf(true); + if(index !== -1){ + goto(`#${tabNames[index]}`, {replaceState: true}); + } + }) </script> <Breadcrumb class="mb-4"> @@ -18,7 +34,7 @@ </Breadcrumb> <Tabs> - <TabItem title="Tutoranmeldung" open> + <TabItem title="Tutoranmeldung" bind:open={openTabs[0]}> <P class="mb-2">Die Email, die versendet wird, wenn sich jemand als Tutor anmeldet.</P> <form method="post" action="?/updateTutorRegistered" use:enhance={()=>({result})=>{ if(result.type === "success"){ @@ -45,7 +61,7 @@ /> </form> </TabItem> - <TabItem title="Schulungserinnerung"> + <TabItem title="Schulungserinnerung" bind:open={openTabs[1]}> <P class="mb-2">Die Mail, die versendet wird, um Tutoren an ihre Schulung zu erinnern.</P> <form method="post" action="?/updateTrainingInformation" use:enhance={()=>({result})=>{ if(result.type === "success"){ @@ -74,5 +90,31 @@ config={data.config} type={templates.trainingInformation.type} /> + </form> + </TabItem> + <TabItem title="Passwort zurücksetzen" bind:open={openTabs[2]}> + <P class="mb-2">Die Email, die versendet wird, wenn jemand sein Passwort zurücksetzen möchte.</P> + <form method="post" action="?/updateResetPassword" use:enhance={()=>({result})=>{ + if(result.type === "success"){ + addMessage({type: "success", text: "Änderungen gespeichert"}); + invalidateAll(); + }else{ + addMessage({type: "error", text: "Fehler beim Speichern"}); + console.error(result); + } + }}> + <MailInput + bind:subject={templates.resetPassword.subject} + bind:text={templates.resetPassword.text} + bind:replyTo={templates.resetPassword.replyTo} + studyPrograms={data.studyPrograms} + trainings={data.trainings} + buttonText="Speichern" + buttonType="submit" + disabled={!canEdit} + config={data.config} + type={templates.resetPassword.type} + /> + </form> </TabItem> </Tabs> diff --git a/src/routes/admin/tutor/[id=number]/+page.server.ts b/src/routes/admin/tutor/[id=number]/+page.server.ts index 71bfa6a..9e52266 100644 --- a/src/routes/admin/tutor/[id=number]/+page.server.ts +++ b/src/routes/admin/tutor/[id=number]/+page.server.ts @@ -1,3 +1,5 @@ +import Degree from "$lib/degrees"; +import Gender from "$lib/genders.js"; import { Config } from "$lib/server/database/entities/Config.entity.js"; import { StudyProgram } from "$lib/server/database/entities/StudyProgram.entity.js"; import { Tutor } from "$lib/server/database/entities/Tutor.entity.js"; @@ -21,32 +23,51 @@ export const load = async (request) => { export const actions = { default: async (request) => { - const data = await request.request.formData(); - const firstname = data.get("firstname") as string ?? undefined; - const lastname = data.get("lastname") as string ?? undefined; - const nickname = data.get("nickname") || ""; - // TODO update birthday format - const birthday = Date.parse(`${data.get("birthday-year")}-${data.get("birthday-month")}-${data.get("birthday-day")}`); - const email = data.get("email") ?? undefined; - const phone = data.get("phone") ?? undefined; - const shirtSize = data.get("shirtSize") ?? undefined; - const address = data.get("address") ?? undefined; - const gender = data.get("gender") ?? undefined; - const studyProgram = data.get("studyProgram") ?? undefined; - const degree = data.get("degree") ?? undefined; - const training = data.get("training") ?? undefined; - const trained = !!data.get("trained") ?? undefined; - const mentor = !!data.get("mentor") ?? undefined; - const coTutorWish = data.get("coTutorWish") || ""; - const comment = data.get("comment") || ""; const tutorId = parseInt(request.params.id); - // validation not necessary, because the frontend only allows valid values and this is - // the admin panel. i think we can trust admins to not craft invalid requests by hand. - - // TODO update in database + const tutor = await Tutor.getById(tutorId); + if(!tutor) error(404, "Not Found"); + const data = await request.request.formData(); + const { shirtSizes } = Config.get(); + const firstname = data.get("firstname"); + const lastname = data.get("lastname"); + const nickname = data.get("nickname"); + const birthday = data.get("birthday"); + const email = data.get("email"); + const phone = data.get("phone"); + const shirtSize = data.get("shirtSize"); + const address = data.get("address"); + const gender = data.get("gender"); + const studyProgramString = data.get("studyProgram"); + const degree = data.get("degree"); + const trainingId = data.get("training"); + const trained = data.get("trained") === "on"; + const mentor = data.get("mentor") === "on"; + const coTutorWish = data.get("coTutorWish"); + const notes = data.get("notes"); + if(!firstname || typeof firstname !== "string") error(400, "Invalid firstname"); + if(!lastname || typeof lastname !== "string") error(400, "Invalid lastname"); + if(nickname !== null && typeof nickname !== "string") error(400, "Invalid nickname"); + if(!birthday || typeof birthday !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(birthday)) error(400, "Invalid birthday"); + if(isNaN(Date.parse(birthday))) error(400, "Invalid birthday"); + if(!email || typeof email !== "string" || !email.includes("@") || !(email.endsWith("@rwth-aachen.de") || email.endsWith(".rwth-aachen.de"))) error(400, "Invalid email"); + if(!phone || typeof phone !== "string") error(400, "Invalid phone"); + if(!shirtSize || typeof shirtSize !== "string" || !shirtSizes.includes(shirtSize)) error(400, "Invalid shirtSize"); + if(!address || typeof address !== "string") error(400, "Invalid address"); + if(typeof gender !== "string" || !(gender in Gender)) error(400, "Invalid gender"); + if(typeof studyProgramString !== "string" || !/^[1-9]+\d*$/.test(studyProgramString)) error(400, "Invalid studyProgram"); + const studyProgramId = Number(studyProgramString); + const studyProgram = await StudyProgram.getById(studyProgramId); + if(!studyProgram) error(400, "Invalid studyProgram"); + if(typeof degree !== "string" || !(degree in Degree)) error(400, "Invalid degree"); + if(!trainingId || typeof trainingId !== "string" || !/^[1-9]+\d*$|^-$/.test(trainingId)) error(400, "Invalid training"); + const training = trainingId === "-" ? null : await TutorTraining.getById(Number(trainingId)); + if(trainingId !== "-" && !training) error(400, "Invalid training"); + if(typeof coTutorWish !== "string") error(400, "Invalid coTutorWish"); + if(typeof notes !== "string") error(400, "Invalid comment"); + const sentTrainingMail = tutor.sentTrainingMail && tutor.training?.id === training?.id; // eslint-disable-next-line no-unused-vars const updatedTutor = await Tutor.update({ - id: tutorId, + id: tutor.id, firstname, lastname, nickname, @@ -55,14 +76,15 @@ export const actions = { phone, shirtSize, address, - gender, - studyProgram, - degree, - training, + gender: gender as keyof typeof Gender, + studyProgram: studyProgram.id, + degree: degree as keyof typeof Degree, + training: training?.id ?? null, trained, mentor, coTutorWish, - comment, + notes, + sentTrainingMail, }); }, }; diff --git a/src/routes/admin/tutor/[id=number]/+page.svelte b/src/routes/admin/tutor/[id=number]/+page.svelte index 1250f1f..666bc10 100644 --- a/src/routes/admin/tutor/[id=number]/+page.svelte +++ b/src/routes/admin/tutor/[id=number]/+page.svelte @@ -4,12 +4,11 @@ import Gender from "$lib/genders"; import Degree from "$lib/degrees"; import { enhance } from "$app/forms"; - import { invalidateAll } from "$app/navigation"; import { addMessage } from "$lib/messages.js"; - export let data; + let { data } = $props(); - $: dateFormatter = new Intl.DateTimeFormat($locale, { day: "2-digit", month: "2-digit", year: "numeric" }); + let dateFormatter = $derived(new Intl.DateTimeFormat($locale, { day: "2-digit", month: "2-digit", year: "numeric" })); </script> <Breadcrumb class="mb-4"> @@ -18,10 +17,10 @@ <BreadcrumbItem href="/admin/tutor/{data.tutor.id}">{data.tutor.firstname} {data.tutor.lastname}</BreadcrumbItem> </Breadcrumb> -<form method="post" use:enhance={()=>({result})=>{ +<form method="post" use:enhance={()=>({result, update})=>{ if(result.type === "success"){ addMessage({type: "success", text: "Tutor gespeichert"}); - invalidateAll(); + update({ reset: false }); }else{ addMessage({type: "error", text: "Fehler beim Speichern"}); console.error(result); @@ -60,7 +59,7 @@ <Select name="gender" value={data.tutor.gender} items={Object.entries(Gender).map(([value, name])=>({value, name: name[$locale]}))} /> </Label> <Label class="mb-2"> - Shirt-Größe + T-Shirt-Größe <Select name="shirtSize" value={data.tutor.shirtSize} items={data.shirtSizes.map(s=>({value:s,name:s}))} /> </Label> <Label class="mb-2"> diff --git a/src/routes/admin/tutor/mail/+page.server.ts b/src/routes/admin/tutor/mail/+page.server.ts index ed6c250..a535726 100644 --- a/src/routes/admin/tutor/mail/+page.server.ts +++ b/src/routes/admin/tutor/mail/+page.server.ts @@ -1,3 +1,4 @@ +import { DOMAIN } from '$env/static/private'; import { configProps } from '$lib/mail'; import { Permission } from '$lib/perms'; import { Config } from '$lib/server/database/entities/Config.entity'; @@ -18,6 +19,6 @@ export const load = (async event => { tutors, degrees, trainings, - config: pick(config, configProps as unknown as (keyof Config)[]), + config: Object.assign(pick(config, configProps), { domain: DOMAIN }), }; }) satisfies PageServerLoad; diff --git a/src/routes/admin/tutor/training/+page.svelte b/src/routes/admin/tutor/training/+page.svelte index a8a75c0..5e80e64 100644 --- a/src/routes/admin/tutor/training/+page.svelte +++ b/src/routes/admin/tutor/training/+page.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import { locale } from "$lib/i18n/i18n"; + import LL, { locale } from "$lib/i18n/i18n"; import { Permission } from "$lib/perms"; import { Breadcrumb, BreadcrumbItem, Button, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte"; @@ -16,6 +16,7 @@ <TableHead> <TableHeadCell>Datum</TableHeadCell> <TableHeadCell>Ort</TableHeadCell> + <TableHeadCell>Sprache</TableHeadCell> <TableHeadCell>Anmeldungen</TableHeadCell> <TableHeadCell>Anmerkung</TableHeadCell> {#if data.user.permissions.has(Permission.EDIT_TRAININGS)} @@ -27,6 +28,7 @@ <TableBodyRow class={training.internal ? "text-gray-700 dark:text-gray-300" : null}> <TableBodyCell>{new Intl.DateTimeFormat($locale, {year: "numeric", month: "2-digit", day: "2-digit"}).format(Date.parse(training.date))}</TableBodyCell> <TableBodyCell>{training.location}</TableBodyCell> + <TableBodyCell>{$LL.Navbar.Languages[training.language]()}</TableBodyCell> <TableBodyCell>{training.participants} / {training.maxParticipants}</TableBodyCell> <TableBodyCell>{training.notes}</TableBodyCell> {#if data.user.permissions.has(Permission.EDIT_TRAININGS)} diff --git a/src/routes/admin/tutor/training/[id=number]/+page.server.ts b/src/routes/admin/tutor/training/[id=number]/+page.server.ts index a5fdb99..7b556af 100644 --- a/src/routes/admin/tutor/training/[id=number]/+page.server.ts +++ b/src/routes/admin/tutor/training/[id=number]/+page.server.ts @@ -1,6 +1,7 @@ import { error } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; import { TutorTraining } from '$lib/server/database/entities/TutorTraining.entity'; +import { locales, type Locale } from '$lib/i18n/i18n'; export const load = (async event => { const id = Number(event.params.id); @@ -21,6 +22,7 @@ export const actions = { const data = await event.request.formData(); const dateStr = data.get("date"); const location = data.get("location"); + const language = data.get("language"); const maxParticipantsStr = data.get("maxParticipants"); const notes = data.get("notes"); const internal = data.get("internal") === "on"; @@ -28,6 +30,7 @@ export const actions = { const dateNum = Date.parse(dateStr); if(isNaN(dateNum)) error(400, 'Invalid date'); if(typeof location !== "string" || location.length < 1) error(400, 'Invalid location'); + if(typeof language !== "string" || !locales.includes(language as any)) error(400, 'Invalid language'); if(typeof maxParticipantsStr !== "string" || !/^\d+$/.test(maxParticipantsStr)) error(400, 'Invalid maxParticipants'); const maxParticipants = Number(maxParticipantsStr); if(!Number.isInteger(maxParticipants) || maxParticipants < 1) error(400, 'Invalid maxParticipants'); @@ -35,6 +38,7 @@ export const actions = { await TutorTraining.update(id, { date: dateStr, location, + language: language as Locale, maxParticipants, notes, internal, diff --git a/src/routes/admin/tutor/training/[id=number]/+page.svelte b/src/routes/admin/tutor/training/[id=number]/+page.svelte index 5645767..af4108c 100644 --- a/src/routes/admin/tutor/training/[id=number]/+page.svelte +++ b/src/routes/admin/tutor/training/[id=number]/+page.svelte @@ -1,8 +1,9 @@ <script lang="ts"> import { enhance } from '$app/forms'; import { goto } from '$app/navigation'; + import LL, { locales } from '$lib/i18n/i18n.js'; import { addMessage } from '$lib/messages.js'; - import { Label, Textarea, Checkbox, Button, Input } from 'flowbite-svelte'; + import { Label, Textarea, Checkbox, Button, Input, Select } from 'flowbite-svelte'; let { data } = $props(); </script> @@ -24,6 +25,14 @@ Ort <Input type="text" name="location" value={data.training.location} required /> </Label> + <Label class="mb-2"> + Sprache + <Select name="language" value={data.training.language} required> + {#each locales as locale} + <option value={locale}>{$LL.Navbar.Languages[locale]()}</option> + {/each} + </Select> + </Label> <Label class="mb-2"> Maximale Teilnehmer <Input type="number" name="maxParticipants" min="1" value={data.training.maxParticipants} required /> diff --git a/src/routes/admin/tutor/training/new/+page.server.ts b/src/routes/admin/tutor/training/new/+page.server.ts index 356790e..289d07e 100644 --- a/src/routes/admin/tutor/training/new/+page.server.ts +++ b/src/routes/admin/tutor/training/new/+page.server.ts @@ -1,3 +1,4 @@ +import { locales, type Locale } from '$lib/i18n/i18n.js'; import { Permission } from '$lib/perms'; import { TutorTraining } from '$lib/server/database/entities/TutorTraining.entity.js'; import { error } from '@sveltejs/kit'; @@ -8,17 +9,19 @@ export const actions = { const data = await event.request.formData(); const date = data.get('date'); const location = data.get('location'); + const language = data.get('language'); const maxParticipantsRaw = data.get('maxParticipants'); const notes = data.get('notes'); const internal = data.get('internal'); if(typeof date !== "string" || !date.match(/^\d{4}-\d{2}-\d{2}$/) || isNaN(Date.parse(date))) error(400, "Invalid date"); if(!location || typeof location !== "string") error(400, "Invalid location"); + if(typeof language !== "string" || !locales.includes(language as any)) error(400, "Invalid language"); if(!maxParticipantsRaw || typeof maxParticipantsRaw !== "string") error(400, "Invalid maxParticipants"); const maxParticipants = Number(maxParticipantsRaw); if(!Number.isInteger(maxParticipants) || maxParticipants < 0) error(400, "Invalid maxParticipants"); if(typeof notes !== "string") error(400, "Invalid notes"); if(internal !== null && internal !== "on") error(400, "Invalid internal"); - const id = await TutorTraining.create({ date, location, maxParticipants, notes, internal: internal === "on" }); + const id = await TutorTraining.create({ date, location, language: language as Locale, maxParticipants, notes, internal: internal === "on" }); return { id }; }, }; diff --git a/src/routes/admin/tutor/training/new/+page.svelte b/src/routes/admin/tutor/training/new/+page.svelte index 8058a46..9b5cef1 100644 --- a/src/routes/admin/tutor/training/new/+page.svelte +++ b/src/routes/admin/tutor/training/new/+page.svelte @@ -1,8 +1,9 @@ <script lang="ts"> import { enhance } from "$app/forms"; import { goto } from "$app/navigation"; + import LL, { locales } from "$lib/i18n/i18n"; import { addMessage } from "$lib/messages"; - import { Breadcrumb, BreadcrumbItem, Button, Checkbox, Input, Label, Textarea } from "flowbite-svelte"; + import { Breadcrumb, BreadcrumbItem, Button, Checkbox, Input, Label, Select, Textarea } from "flowbite-svelte"; </script> <Breadcrumb class="mb-4"> @@ -29,6 +30,14 @@ Ort <Input type="text" name="location" required /> </Label> + <Label class="mb-2"> + Sprache + <Select name="language" required> + {#each locales as locale} + <option value={locale}>{$LL.Navbar.Languages[locale]()}</option> + {/each} + </Select> + </Label> <Label class="mb-2"> Maximale Teilnehmer <Input type="number" name="maxParticipants" min="1" required /> diff --git a/src/routes/intern/+page.server.ts b/src/routes/intern/+page.server.ts new file mode 100644 index 0000000..e385ed4 --- /dev/null +++ b/src/routes/intern/+page.server.ts @@ -0,0 +1,52 @@ +import { signIn } from '$lib/server/auth.js'; +import { Config } from '$lib/server/database/entities/Config.entity.js'; +import { RallyStationSupervisor } from '$lib/server/database/entities/RallyStationSupervisor.entity.js'; +import { Tutor } from '$lib/server/database/entities/Tutor.entity.js'; +import { sendMail } from '$lib/server/mail.js'; +import { CredentialsSignin } from '@auth/sveltekit'; +import { error } from '@sveltejs/kit'; +import bcrypt from 'bcrypt'; + +export const actions = { + login: async event => { + try { + return await signIn(event); + } catch(e) { + if(e instanceof CredentialsSignin){ + error(401, "Invalid credentials"); + }else{ + throw e; + } + } + }, + resetTutor: async event => { + const data = await event.request.formData(); + const email = data.get("email"); + if(!email || typeof email !== "string") error(400, "Invalid email"); + const tutor = await Tutor.getByEmail(email); + const newPassword = Math.random().toString(36).slice(2); + const hash = await bcrypt.hash(newPassword, 10); + const { mailTemplates: { resetPassword: template } } = Config.get(); + if(!tutor) return; + Tutor.update({ id: tutor.id, password: hash }); + sendMail({ + template, + tutors: [{...tutor, password: newPassword}], + }); + }, + resetRally: async event => { + const data = await event.request.formData(); + const email = data.get("email"); + if(!email || typeof email !== "string") error(400, "Invalid email"); + const supervisor = await RallyStationSupervisor.getByEmail(email); + const newPassword = Math.random().toString(36).slice(2); + const hash = await bcrypt.hash(newPassword, 10); + const { mailTemplates: { resetPassword: template } } = Config.get(); + if(!supervisor) return; + RallyStationSupervisor.update(supervisor.id, { password: hash }); + sendMail({ + template, + tutors: [{...supervisor, password: newPassword}], // TODO fix + }); + }, +}; diff --git a/src/routes/intern/+page.svelte b/src/routes/intern/+page.svelte new file mode 100644 index 0000000..d02a7f6 --- /dev/null +++ b/src/routes/intern/+page.svelte @@ -0,0 +1,115 @@ +<script lang="ts"> + import { enhance } from "$app/forms"; + import { goto } from "$app/navigation"; + import { page } from "$app/stores"; + import { Alert, Button, DarkMode, Input, Span, TabItem, Tabs } from "flowbite-svelte"; + + let tabNames = [ + "tutor", + "rallye", + ]; + let selectedTab = $page.url.hash.substring(1) || tabNames[0]; + let openTabs = $state(tabNames.map(name => name === selectedTab)); + if(openTabs.indexOf(true) === -1) openTabs[0] = true; + $effect(()=>{ + const index = openTabs.indexOf(true); + if(index !== -1){ + error = null; + forgotPassword = false; + resetPassword = false; + goto(`#${tabNames[index]}`, {replaceState: true}); + } + }); + + let error: string|null = $state(null); + let resetPassword = $state(false); + let forgotPassword = $state(false); +</script> + +<DarkMode /> + +<!-- +As of 2024-08-24 this does not work as intended, because NextAuth is not exactly following their spec. +The signIn function checks for the "credentials" id and not the "tutor" or "rally" id. +A merge request was created to get this issue fixed: https://github.com/nextauthjs/next-auth/pull/11662 +--> +<div class="container relative mx-auto px-4 max-w-5xl mb-10 mt-4"> + <Tabs> + <TabItem title="Tutor:innen-Login" bind:open={openTabs[0]}> + {#if error} + <Alert color="red" class="mb-4">{error}</Alert> + {/if} + {#if resetPassword} + <Alert color="green" class="mb-4">Du hast per E-Mail ein neues Passwort erhalten.</Alert> + {/if} + {#if forgotPassword} + <form method="post" action="?/resetTutor" use:enhance={()=>({result, update})=>{ + if(result.type === "success"){ + resetPassword = true; + update(); + }else if(result.type === "error"){ + error = result.error.message; + } + }}> + <Input type="email" name="email" placeholder="E-Mail" required class="mb-2" /> + <Button type="submit">Passwort zurücksetzen</Button> + </form> + {:else} + <form method="post" action="?/login" use:enhance={()=>({result, update})=>{ + if(result.type === "success"){ + update(); + }else if(result.type === "error"){ + error = result.error.message; + }else if(result.type === "redirect"){ + goto(result.location); + } + }}> + <input type="hidden" name="providerId" value="tutor" /> + <input type="hidden" name="redirectTo" value="/intern/tutor" /> + <Input type="email" name="email" placeholder="E-Mail" required class="mb-2" /> + <Input type="password" name="password" placeholder="Passwort" required class="mb-2" /> + <Button type="submit">Anmelden</Button> + <button class="block" onclick={()=>forgotPassword=true}><Span underline class="text-blue-600 dark:text-blue-500">Passwort vergessen</Span></button> + </form> + {/if} + </TabItem> + <TabItem title="Stationsbetreuer:innen-Login" bind:open={openTabs[1]}> + {#if error} + <Alert color="red" class="mb-4">{error}</Alert> + {/if} + {#if resetPassword} + <Alert color="green" class="mb-4">Du hast per E-Mail ein neues Passwort erhalten.</Alert> + {/if} + {#if forgotPassword} + <form method="post" action="?/resetRally" use:enhance={()=>({result, update})=>{ + if(result.type === "success"){ + resetPassword = true; + update(); + }else if(result.type === "error"){ + error = result.error.message; + } + }}> + <Input type="email" name="email" placeholder="E-Mail" required class="mb-2" /> + <Button type="submit">Passwort zurücksetzen</Button> + </form> + {:else} + <form method="post" action="?/login" use:enhance={()=>({result, update})=>{ + if(result.type === "success"){ + update(); + }else if(result.type === "error"){ + error = result.error.message; + }else if(result.type === "redirect"){ + goto(result.location); + } + }}> + <input type="hidden" name="providerId" value="rally" /> + <input type="hidden" name="redirectTo" value="/intern/rallye" /> + <Input type="email" name="email" placeholder="E-Mail" required class="mb-2" /> + <Input type="password" name="password" placeholder="Passwort" required class="mb-2" /> + <Button type="submit">Anmelden</Button> + <button class="block" onclick={()=>forgotPassword=true}><Span underline class="text-blue-600 dark:text-blue-500">Passwort vergessen</Span></button> + </form> + {/if} + </TabItem> + </Tabs> +</div> diff --git a/src/routes/intern/rallye/+layout.server.ts b/src/routes/intern/rallye/+layout.server.ts new file mode 100644 index 0000000..0a7ad2a --- /dev/null +++ b/src/routes/intern/rallye/+layout.server.ts @@ -0,0 +1,17 @@ +import { RallyStation } from '$lib/server/database/entities/RallyStation.entity.js'; +import { RallyStationSupervisor } from '$lib/server/database/entities/RallyStationSupervisor.entity.js'; +import { error } from '@sveltejs/kit'; + +export const load = async (event) => { + const supervisor = await RallyStationSupervisor.getById(event.locals.supervisor.id); + if(!supervisor) error(404, "Supervisor not found"); + const stations = await RallyStation.getBySupervisor(supervisor.id); + return { + supervisor: { + ...supervisor, + notes: undefined, + password: undefined, + }, + stations: stations.map(s => ({...s})), + }; +}; diff --git a/src/routes/intern/rallye/+layout.svelte b/src/routes/intern/rallye/+layout.svelte new file mode 100644 index 0000000..302ef1b --- /dev/null +++ b/src/routes/intern/rallye/+layout.svelte @@ -0,0 +1,10 @@ +<script lang="ts"> + import SupervisorLayout from "$lib/components/SupervisorLayout.svelte"; + import type { Snippet } from "svelte"; + + let { children }: { children: Snippet } = $props(); +</script> + +<SupervisorLayout> + {@render children()} +</SupervisorLayout> diff --git a/src/routes/intern/rallye/+page.svelte b/src/routes/intern/rallye/+page.svelte new file mode 100644 index 0000000..fae9f48 --- /dev/null +++ b/src/routes/intern/rallye/+page.svelte @@ -0,0 +1,3 @@ +<script lang="ts"> + let { data } = $props(); +</script> diff --git a/src/routes/intern/tutor/+layout.server.ts b/src/routes/intern/tutor/+layout.server.ts new file mode 100644 index 0000000..5148609 --- /dev/null +++ b/src/routes/intern/tutor/+layout.server.ts @@ -0,0 +1,30 @@ +import { Tutor } from '$lib/server/database/entities/Tutor.entity.js'; +import { error } from '@sveltejs/kit'; + +export const load = async (event) => { + const tutor = await Tutor.getById(event.locals.tutor.id); + if(!tutor) error(404, "Tutor not found"); + return { + tutor: { + ...tutor, + studyProgram: { + id: tutor.studyProgram.id, + name: tutor.studyProgram.name + }, + training: tutor.training ? { + id: tutor.training.id, + date: tutor.training.date, + location: tutor.training.location, + language: tutor.training.language + } : null, + tutorial: tutor.tutorial ? { + id: tutor.tutorial.id, + name: tutor.tutorial.name, + tutors: tutor.tutorial.tutors.map(t=>({firstname: t.firstname, lastname: t.lastname})), // TODO include email? + } : null, + former: undefined, + password: undefined, + notes: undefined, + }, + }; +}; diff --git a/src/routes/intern/tutor/+layout.svelte b/src/routes/intern/tutor/+layout.svelte new file mode 100644 index 0000000..7f9c52d --- /dev/null +++ b/src/routes/intern/tutor/+layout.svelte @@ -0,0 +1,10 @@ +<script lang="ts"> + import TutorLayout from "$lib/components/TutorLayout.svelte"; + import type { Snippet } from "svelte"; + + let { children }: { children: Snippet } = $props(); +</script> + +<TutorLayout> + {@render children()} +</TutorLayout> diff --git a/src/routes/intern/tutor/+page.server.ts b/src/routes/intern/tutor/+page.server.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/intern/tutor/+page.svelte b/src/routes/intern/tutor/+page.svelte new file mode 100644 index 0000000..eea990d --- /dev/null +++ b/src/routes/intern/tutor/+page.svelte @@ -0,0 +1,33 @@ +<script lang="ts"> + import SortableTable from "$lib/components/SortableTable.svelte"; + import SortableTableHeadCell from "$lib/components/SortableTableHeadCell.svelte"; + import { Button, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte"; + + let { data } = $props(); + + let items = [ + { id: 1, name: "John", age: 25 }, + { id: 2, name: "Jane", age: 22 }, + { id: 3, name: "Alice", age: 30 }, + { id: 4, name: "Bob", age: 27 }, + { id: 5, name: "Charlie", age: 23 }, + { id: 6, name: "David", age: 28 }, + { id: 7, name: "Eve", age: 24 }, + { id: 8, name: "Frank", age: 29 }, + ]; +</script> + +<SortableTable {items}> + <TableHead> + <SortableTableHeadCell sort={(a,b)=>a.name.localeCompare(b.name)}>Name</SortableTableHeadCell> + <SortableTableHeadCell sort={(a,b)=>a.age-b.age} defaultDirection="desc" default>Age</SortableTableHeadCell> + <TableHeadCell>Actions</TableHeadCell> + </TableHead> + {#snippet row({item})} + <TableBodyRow> + <TableBodyCell>{item.name}</TableBodyCell> + <TableBodyCell>{item.age}</TableBodyCell> + <TableBodyCell><Button href="/{item.id}">Edit</Button></TableBodyCell> + </TableBodyRow> + {/snippet} +</SortableTable> diff --git a/src/routes/intern/tutor/settings/+page.server.ts b/src/routes/intern/tutor/settings/+page.server.ts new file mode 100644 index 0000000..6cd053e --- /dev/null +++ b/src/routes/intern/tutor/settings/+page.server.ts @@ -0,0 +1,53 @@ +import { Tutor } from '$lib/server/database/entities/Tutor.entity.js'; +import { TutorTraining } from '$lib/server/database/entities/TutorTraining.entity.js'; +import { error } from '@sveltejs/kit'; +import bcrypt from "bcrypt"; + +export const load = async ()=>{ + const trainings = await TutorTraining.getAll(); + return { + trainings: trainings.map(t=>({id: t.id, date: t.date, location: t.location, language: t.language, isFull: t.participants.length >= t.maxParticipants})), + }; +}; + +export const actions = { + settings: async event=>{ + const data = await event.request.formData(); + const phone = data.get("phone"); + const address = data.get("address"); + const dietaryRestriction = data.get("dietaryRestriction"); + const trainingString = data.get("training"); + const coTutorWish = data.get("CoTutorWish") ?? ""; + if(!phone || typeof phone !== 'string') return error(400, 'Invalid phone'); + if(!address || typeof address !== 'string') return error(400, 'Invalid address'); + if(dietaryRestriction !== null && typeof dietaryRestriction !== 'string') return error(400, 'Invalid dietary restriction'); + if(typeof trainingString !== "string" || !/^[1-9]\d*$|^-$/.test(trainingString)) return error(400, 'Invalid training'); + if(typeof coTutorWish !== 'string') return error(400, 'Invalid Co-Tutor wish'); + const training = trainingString === "-" ? null : await TutorTraining.getById(Number(trainingString)); + if(training === undefined) return error(400, 'Invalid training'); + const sentTrainingMail = event.locals.tutor.sentTrainingMail && event.locals.tutor.training?.id === training?.id; + await Tutor.update({ + id: event.locals.tutor.id, + phone, + address, + dietaryRestriction, + training: training?.id ?? null, + coTutorWish, + sentTrainingMail, + }); + }, + password: async event=>{ + const data = await event.request.formData(); + const oldPassword = data.get('oldPassword'); + const newPassword = data.get('newPassword'); + const newPasswordRepeat = data.get('newPasswordRepeat'); + if(!oldPassword || !newPassword || !newPasswordRepeat) return error(400, 'Please fill out all fields'); + if(typeof oldPassword !== 'string' || typeof newPassword !== 'string' || typeof newPasswordRepeat !== 'string') return error(400, 'Invalid input'); + if(newPassword !== newPasswordRepeat) return error(400, 'Passwords do not match'); + if(newPassword.length < 8) return error(400, 'Password too short'); + const match = await bcrypt.compare(oldPassword, event.locals.tutor.password).catch(()=>false); + if(!match) return error(400, 'Wrong password'); + const hash = await bcrypt.hash(newPassword, 10); + await Tutor.update({ id: event.locals.tutor.id, password: hash }); + }, +}; diff --git a/src/routes/intern/tutor/settings/+page.svelte b/src/routes/intern/tutor/settings/+page.svelte new file mode 100644 index 0000000..b81522e --- /dev/null +++ b/src/routes/intern/tutor/settings/+page.svelte @@ -0,0 +1,114 @@ +<script lang="ts"> + import { enhance } from "$app/forms"; + import LL from "$lib/i18n/i18n.js"; + import { addMessage } from "$lib/messages"; + import { A, Button, Heading, Helper, Input, Label, Select, Textarea } from "flowbite-svelte"; + + let { data } = $props(); + + let profileDisabled = $state(false); + let passwordDisabled = $state(false); + + let passwordTooShort = $state(false); + let passwordsDontMatch = $state(false); +</script> + +<Heading tag="h1" customSize="text-4xl font-bold" class="mb-4">{$LL.Internal.Settigs.Headline()}</Heading> + +<Heading tag="h2" customSize="text-2xl font-bold" class="mb-4">{$LL.Internal.Settigs.ProfileHeadline()}</Heading> + +<form method="post" action="?/settings" use:enhance={()=>{ + profileDisabled = true; + return ({result, update})=>{ + profileDisabled = false; + if(result.type === "success"){ + addMessage({ type: "success", text: "Profil gespeichert." }); + update({ reset: false }); + }else if(result.type === "error"){ + addMessage({ type: "error", text: `Fehler beim Speichern des Profils: ${result.error.message}` }); + console.error(result); + } + }; +}}> + <Label class="mb-2"> + {$LL.Tutor.SignUp.Phone()} + <Input type="tel" name="phone" value={data.tutor.phone} required disabled={profileDisabled} /> + <Helper>{$LL.Tutor.SignUp.PhoneHelper()}</Helper> + </Label> + <Label class="mb-2"> + {$LL.Tutor.SignUp.FullAddress()} + <Input name="address" value={data.tutor.address} required disabled={profileDisabled} /> + </Label> + <Label class="mb-2"> + {$LL.Tutor.SignUp.DietaryRestriction()} + <Input type="text" name="dietaryRestriction" value={data.tutor.dietaryRestriction} disabled={profileDisabled} /> + </Label> + <Label class="mb-2"> + {$LL.Tutor.SignUp.Training()} + <Select name="training" value={data.tutor.trained || !data.tutor.training ? "-" : data.tutor.training.id} required disabled={profileDisabled || data.tutor.trained || (data.tutor.training && Date.parse(data.tutor.training.date) - Date.now() < 1000*60*60*24*3)}> + <option value="-">{$LL.Tutor.SignUp.AlreadyTrained()}</option> + {#each data.trainings as training} + {@const date = new Date(training.date)} + {@const language = $LL.Navbar.Languages[training.language]()} + {@const translationFunction = training.isFull ? $LL.Tutor.SignUp.TrainingOptionFull : $LL.Tutor.SignUp.TrainingOption} + <option value={training.id} disabled={training.isFull || Date.parse(training.date) - Date.now() < 1000*60*60*24}>{translationFunction({date, language})}</option> + {/each} + </Select> + <Helper>Bis zu drei Tage vor deiner Schulung kannst du dich noch ummelden. Solltest du kurzfristig nicht an deiner Schulung teilnehmen können, schreib uns eine Mail an <A href="mailto:esa@fsmpi.rwth-aachen.de">esa@fsmpi.rwth-aachen.de</A>.</Helper> <!-- TODO i18n --> + </Label> + <Label class="mb-2"> + {$LL.Tutor.SignUp.CoTutorWish()} + <Input type="text" name="coTutorWish" value={data.tutor.coTutorWish} disabled={profileDisabled} /> + </Label> + <Button type="submit" class="mb-4" disabled={profileDisabled}>{$LL.Internal.Settigs.Save()}</Button> +</form> + +<Heading tag="h2" customSize="text-2xl font-bold" class="mb-4">{$LL.Internal.Settigs.PasswordHeadline()}</Heading> + +<form method="post" action="?/password" use:enhance={({formData, cancel})=>{ + if(passwordTooShort){ + cancel(); + }else if(formData.get("newPassword") !== formData.get("newPasswordRepeat")){ + cancel(); + passwordsDontMatch = true; + }else{ + passwordsDontMatch = false; + passwordDisabled = true; + return ({result, update})=>{ + passwordDisabled = false; + if(result.type === "success"){ + addMessage({ type: "success", text: "Passwort geändert." }); + update(); + }else if(result.type === "error"){ + addMessage({ type: "error", text: `Fehler beim Ändern des Passworts: ${result.error.message}` }); + console.error(result); + } + }; + } +}}> + <Label class="mb-2"> + {$LL.Internal.Settigs.OldPassword()} + <Input type="password" name="oldPassword" required disabled={passwordDisabled} /> + </Label> + <Label class="mb-2"> + {$LL.Internal.Settigs.NewPassword()} + <Input type="password" name="newPassword" required disabled={passwordDisabled} on:input={e=>passwordTooShort=(e.target! as HTMLInputElement).value.length<8} /> + {#if passwordTooShort} + <Helper color="red">{$LL.Internal.Settigs.PasswordTooShort()}</Helper> + {/if} + </Label> + <Label class="mb-2"> + {$LL.Internal.Settigs.NewPasswordRepeat()} + <Input type="password" name="newPasswordRepeat" required disabled={passwordDisabled} /> + {#if passwordsDontMatch} + <Helper color="red" class="mb-2">{$LL.Internal.Settigs.PasswordMismatch()}</Helper> + {/if} + </Label> + <Button type="submit" class="mb-4" disabled={passwordDisabled}>{$LL.Internal.Settigs.Save()}</Button> +</form> + +<!-- +<Heading tag="h2" customSize="text-2xl font-bold" class="mb-4">Account</Heading> + +TODO delete account option? +--> -- GitLab