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