diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..c34d187 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "isms-dev", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "port": 3000 + } + ] +} diff --git a/AGENTS.md b/AGENTS.md index f87cd61..fc05a5d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,6 +25,12 @@ Multi-Tenant-SaaS für Informationssicherheits-Managementsysteme (ISO/IEC 27001: - Tests: Vitest (Unit), Playwright (E2E). `npm run lint` und `npm run build` müssen vor jedem Commit grün sein. - Sprache: UI und Fachbegriffe Deutsch; Code (Bezeichner, Kommentare) Englisch. +## Offene Härtungspunkte (Iteration 8) + +- RLS scharfschalten: App-Verbindung auf DB-Rolle `isms_app` umstellen und `app.tenant_id` pro Transaktion setzen (Policies existieren bereits, siehe Migration `row_level_security`). +- TOTP-2FA (Schema-Feld `mfa_secret` ist vorbereitet). +- Rollenänderungen wirken erst beim nächsten Login (Permissions liegen im JWT). + ## Entwicklung ```bash diff --git a/docker-compose.yml b/docker-compose.yml index 6af1740..7c7d34b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,9 @@ services: condition: service_started restart: unless-stopped + # Wird in Iteration 4 aktiviert (BullMQ-Scheduler, siehe docs/SPEC.md §4.5) worker: + profiles: ["worker"] build: . command: ["npm", "run", "worker"] env_file: .env diff --git a/messages/de.json b/messages/de.json new file mode 100644 index 0000000..cd521cd --- /dev/null +++ b/messages/de.json @@ -0,0 +1,23 @@ +{ + "common": { + "appName": "ISMS-Tool", + "logout": "Abmelden" + }, + "login": { + "title": "Anmelden", + "subtitle": "Melde dich mit deinem Firmenkonto an.", + "email": "E-Mail-Adresse", + "password": "Passwort", + "tenant": "Organisation (optional)", + "tenantHint": "Nur nötig, wenn deine E-Mail in mehreren Organisationen existiert.", + "submit": "Anmelden", + "error": "Anmeldung fehlgeschlagen. Bitte prüfe E-Mail, Passwort und ggf. die Organisation." + }, + "dashboard": { + "title": "Dashboard", + "welcome": "Willkommen, {name}", + "tenant": "Mandant", + "roles": "Rollen", + "placeholder": "Die Modul-Dashboards (Risiken, Aufgaben, SoA-Erfüllungsgrad) folgen in den nächsten Iterationen." + } +} diff --git a/messages/en.json b/messages/en.json new file mode 100644 index 0000000..3591c35 --- /dev/null +++ b/messages/en.json @@ -0,0 +1,23 @@ +{ + "common": { + "appName": "ISMS Tool", + "logout": "Sign out" + }, + "login": { + "title": "Sign in", + "subtitle": "Sign in with your company account.", + "email": "E-mail address", + "password": "Password", + "tenant": "Organization (optional)", + "tenantHint": "Only needed if your e-mail exists in several organizations.", + "submit": "Sign in", + "error": "Sign-in failed. Please check e-mail, password and organization." + }, + "dashboard": { + "title": "Dashboard", + "welcome": "Welcome, {name}", + "tenant": "Tenant", + "roles": "Roles", + "placeholder": "Module dashboards (risks, tasks, SoA coverage) follow in upcoming iterations." + } +} diff --git a/next.config.ts b/next.config.ts index 566c88e..895f595 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,8 +1,11 @@ import type { NextConfig } from "next"; +import createNextIntlPlugin from "next-intl/plugin"; const nextConfig: NextConfig = { // Standalone-Output für den Docker-Multi-Stage-Build (siehe Dockerfile) output: "standalone", }; -export default nextConfig; +const withNextIntl = createNextIntlPlugin(); + +export default withNextIntl(nextConfig); diff --git a/package-lock.json b/package-lock.json index 18f960c..b9813a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,13 @@ "name": "isms-tool", "version": "0.1.0", "dependencies": { + "@node-rs/argon2": "^2.0.2", "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", "dotenv": "^17.4.2", "next": "16.2.10", + "next-auth": "^5.0.0-beta.31", + "next-intl": "^4.13.1", "react": "19.2.4", "react-dom": "19.2.4", "zod": "^4.4.3" @@ -42,6 +45,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@auth/core": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.2.tgz", + "integrity": "sha512-Hx5MNBxN2fJTbJKGUKAA0wca43D0Akl3TvufY54Gn8lop7F+34vU1zA1pn0vQfIoVuLIrpfc2nkyjwIaPJMW7w==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^7.0.7" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -316,7 +348,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -338,7 +369,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -931,6 +961,36 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.6.tgz", + "integrity": "sha512-H5aexk1Le7T9TPmscacZ+1pR6CTa2n1wq+HDVGXhH8TzUlQQpeXzZs91dRtmFHrbeNbjPFPfQujUqm7MHgVoXQ==", + "license": "MIT" + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.12.tgz", + "integrity": "sha512-YyzzxVgYJ8DELmmkhn0Yr0rUj0dTJFf9Jp628K3S0ysInBWxLVDOS8i3RP91cCp4DMK4WYb4cVMhWA9i4knSJg==", + "license": "MIT", + "dependencies": { + "@formatjs/icu-skeleton-parser": "2.1.10" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.10.tgz", + "integrity": "sha512-XuSva+8ZGawk8VnD5VD6UeH8KarQ/Z022zgjHDoHmlNiAewstXuuzXc0Hk5pGFSdG+nNw5bfJKXqj1ZXHn9yUA==", + "license": "MIT" + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.10.tgz", + "integrity": "sha512-P/IC3qws3jH+1fEs+o0RIFgXKRaQlFehjS5W0FPAqdo6hgzawLl+eD0q0JjheQ3XtoOe5n8WSYfX06KQZI/QJA==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.6" + } + }, "node_modules/@hono/node-server": { "version": "1.19.11", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", @@ -1756,6 +1816,279 @@ "node": ">= 10" } }, + "node_modules/@node-rs/argon2": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2/-/argon2-2.0.2.tgz", + "integrity": "sha512-t64wIsPEtNd4aUPuTAyeL2ubxATCBGmeluaKXEMAFk/8w6AJIVVkeLKMBpgLW6LU2t5cQxT+env/c6jxbtTQBg==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@node-rs/argon2-android-arm-eabi": "2.0.2", + "@node-rs/argon2-android-arm64": "2.0.2", + "@node-rs/argon2-darwin-arm64": "2.0.2", + "@node-rs/argon2-darwin-x64": "2.0.2", + "@node-rs/argon2-freebsd-x64": "2.0.2", + "@node-rs/argon2-linux-arm-gnueabihf": "2.0.2", + "@node-rs/argon2-linux-arm64-gnu": "2.0.2", + "@node-rs/argon2-linux-arm64-musl": "2.0.2", + "@node-rs/argon2-linux-x64-gnu": "2.0.2", + "@node-rs/argon2-linux-x64-musl": "2.0.2", + "@node-rs/argon2-wasm32-wasi": "2.0.2", + "@node-rs/argon2-win32-arm64-msvc": "2.0.2", + "@node-rs/argon2-win32-ia32-msvc": "2.0.2", + "@node-rs/argon2-win32-x64-msvc": "2.0.2" + } + }, + "node_modules/@node-rs/argon2-android-arm-eabi": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-2.0.2.tgz", + "integrity": "sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-android-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-2.0.2.tgz", + "integrity": "sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-darwin-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-2.0.2.tgz", + "integrity": "sha512-3TTNL/7wbcpNju5YcqUrCgXnXUSbD7ogeAKatzBVHsbpjZQbNb1NDxDjqqrWoTt6XL3z9mJUMGwbAk7zQltHtA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-darwin-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-2.0.2.tgz", + "integrity": "sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-freebsd-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-2.0.2.tgz", + "integrity": "sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-arm-gnueabihf": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-2.0.2.tgz", + "integrity": "sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-arm64-gnu": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-2.0.2.tgz", + "integrity": "sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-arm64-musl": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-2.0.2.tgz", + "integrity": "sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-x64-gnu": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-2.0.2.tgz", + "integrity": "sha512-ZM3jrHuJ0dKOhvA80gKJqBpBRmTJTFSo2+xVZR+phQcbAKRlDMSZMFDiKbSTnctkfwNFtjgDdh5g1vaEV04AvA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-x64-musl": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-2.0.2.tgz", + "integrity": "sha512-of5uPqk7oCRF/44a89YlWTEfjsftPywyTULwuFDKyD8QtVZoonrJR6ZWvfFE/6jBT68S0okAkAzzMEdBVWdxWw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-wasm32-wasi": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-2.0.2.tgz", + "integrity": "sha512-U3PzLYKSQYzTERstgtHLd4ZTkOF9co57zTXT77r0cVUsleGZOrd6ut7rHzeWwoJSiHOVxxa0OhG1JVQeB7lLoQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@node-rs/argon2-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@node-rs/argon2-win32-arm64-msvc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-2.0.2.tgz", + "integrity": "sha512-Eisd7/NM0m23ijrGr6xI2iMocdOuyl6gO27gfMfya4C5BODbUSP7ljKJ7LrA0teqZMdYHesRDzx36Js++/vhiQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-win32-ia32-msvc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-2.0.2.tgz", + "integrity": "sha512-GsE2ezwAYwh72f9gIjbGTZOf4HxEksb5M2eCaj+Y5rGYVwAdt7C12Q2e9H5LRYxWcFvLH4m4jiSZpQQ4upnPAQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-win32-x64-msvc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-2.0.2.tgz", + "integrity": "sha512-cJxWXanH4Ew9CfuZ4IAEiafpOBCe97bzoKowHCGk5lG/7kR4WF/eknnBlHW9m8q7t10mKq75kruPLtbSDqgRTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1804,6 +2137,340 @@ "node": ">=12.4.0" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@prisma/adapter-pg": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.8.0.tgz", @@ -2192,6 +2859,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@schummar/icu-type-parser": { + "version": "1.21.5", + "resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz", + "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -2199,6 +2872,222 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.43.tgz", + "integrity": "sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.43.tgz", + "integrity": "sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.43.tgz", + "integrity": "sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.43.tgz", + "integrity": "sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.43.tgz", + "integrity": "sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.43.tgz", + "integrity": "sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.43.tgz", + "integrity": "sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.43.tgz", + "integrity": "sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.43.tgz", + "integrity": "sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.43.tgz", + "integrity": "sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.43.tgz", + "integrity": "sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.43.tgz", + "integrity": "sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -2208,6 +3097,15 @@ "tslib": "^2.8.0" } }, + "node_modules/@swc/types": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.27.tgz", + "integrity": "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tailwindcss/node": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", @@ -2495,7 +3393,6 @@ "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -3976,7 +4873,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -5357,6 +6253,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/icu-minify": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.13.1.tgz", + "integrity": "sha512-nFYW2im0WJ3RUVZwabd71J8QTZRtkK1xxZBY7klg7a6KS/os17LZSj9q1VhbRSnk3S8Mv2I7F1izr/aEJ6cUsw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/icu-messageformat-parser": "^3.4.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5409,6 +6320,16 @@ "node": ">= 0.4" } }, + "node_modules/intl-messageformat": { + "version": "11.2.9", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.9.tgz", + "integrity": "sha512-cGzymZerpDhVXRKjKLgXKda9gI29TU2o88L7gwNMHp3WZVxA/0c5tX52udXbW9JklDApolvMXZG6Dhhdz5eirA==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/fast-memoize": "3.1.6", + "@formatjs/icu-messageformat-parser": "3.5.12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -5587,7 +6508,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5633,7 +6553,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -5889,6 +6808,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6517,6 +7445,15 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/next": { "version": "16.2.10", "resolved": "https://registry.npmjs.org/next/-/next-16.2.10.tgz", @@ -6570,6 +7507,110 @@ } } }, + "node_modules/next-auth": { + "version": "5.0.0-beta.31", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.31.tgz", + "integrity": "sha512-1OBgCKPzo+S7UWWMp3xgvGvIJ0OpV7B3vR4ZDRqD9a4Ch+OT6dakLXG9ivhtmIWVa71nTSXattOHyCg8sNi8/Q==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.2" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0", + "nodemailer": "^7.0.7", + "react": "^18.2.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/next-intl": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.13.1.tgz", + "integrity": "sha512-aS8KTA+nNhSNJJBlIhxgvU135WzoObwzFwav4wTDti/Gmhxqe0fs/Q343igo8Z7HGqPB/xgmoagwySZAlHmIfA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/intl-localematcher": "^0.8.1", + "@parcel/watcher": "^2.4.1", + "@swc/core": "^1.15.2", + "icu-minify": "^4.13.1", + "negotiator": "^1.0.0", + "next-intl-swc-plugin-extractor": "^4.13.1", + "po-parser": "^2.1.1", + "use-intl": "^4.13.1" + }, + "peerDependencies": { + "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/next-intl-swc-plugin-extractor": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.13.1.tgz", + "integrity": "sha512-RhlH2DR1ViEXzcX7G3tDXAvzNrBL2Ph54Hq/q/9oP9eXQV/okh3UQpA/lx2k9U5Ck83CZOSgH0eFTNi5U+zyXw==", + "license": "MIT" + }, + "node_modules/next-intl/node_modules/@swc/core": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.43.tgz", + "integrity": "sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.27" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.43", + "@swc/core-darwin-x64": "1.15.43", + "@swc/core-linux-arm-gnueabihf": "1.15.43", + "@swc/core-linux-arm64-gnu": "1.15.43", + "@swc/core-linux-arm64-musl": "1.15.43", + "@swc/core-linux-ppc64-gnu": "1.15.43", + "@swc/core-linux-s390x-gnu": "1.15.43", + "@swc/core-linux-x64-gnu": "1.15.43", + "@swc/core-linux-x64-musl": "1.15.43", + "@swc/core-win32-arm64-msvc": "1.15.43", + "@swc/core-win32-ia32-msvc": "1.15.43", + "@swc/core-win32-x64-msvc": "1.15.43" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -6598,6 +7639,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-exports-info": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", @@ -6627,6 +7674,15 @@ "node": ">=18" } }, + "node_modules/oauth4webapi": { + "version": "3.8.6", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.6.tgz", + "integrity": "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -7008,6 +8064,12 @@ "pathe": "^2.0.3" } }, + "node_modules/po-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz", + "integrity": "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==", + "license": "MIT" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -7100,6 +8162,25 @@ "node": ">=0.10.0" } }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8353,6 +9434,27 @@ "punycode": "^2.1.0" } }, + "node_modules/use-intl": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.13.1.tgz", + "integrity": "sha512-UU5C3zAC7yVg3m7rq5C8VF5J5jhAfvS19Wi9bPNCB9xB7jQYBsUcrqfdxs4Mxl9XR3x6BDB5K++iAw7/rcm3gg==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "^3.1.0", + "@schummar/icu-type-parser": "1.21.5", + "icu-minify": "^4.13.1", + "intl-messageformat": "^11.1.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, "node_modules/valibot": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", diff --git a/package.json b/package.json index 7e938d3..04d29c9 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,13 @@ "lint": "eslint" }, "dependencies": { + "@node-rs/argon2": "^2.0.2", "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", "dotenv": "^17.4.2", "next": "16.2.10", + "next-auth": "^5.0.0-beta.31", + "next-intl": "^4.13.1", "react": "19.2.4", "react-dom": "19.2.4", "zod": "^4.4.3" diff --git a/prisma/migrations/20260702095800_init/migration.sql b/prisma/migrations/20260702095800_init/migration.sql new file mode 100644 index 0000000..2e56fd5 --- /dev/null +++ b/prisma/migrations/20260702095800_init/migration.sql @@ -0,0 +1,131 @@ +-- CreateExtension +CREATE EXTENSION IF NOT EXISTS "vector"; + +-- CreateEnum +CREATE TYPE "TenantStatus" AS ENUM ('ACTIVE', 'SUSPENDED'); + +-- CreateEnum +CREATE TYPE "UserStatus" AS ENUM ('ACTIVE', 'INVITED', 'LOCKED', 'DEACTIVATED'); + +-- CreateTable +CREATE TABLE "tenants" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "status" "TenantStatus" NOT NULL DEFAULT 'ACTIVE', + "config" JSONB NOT NULL DEFAULT '{}', + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "tenants_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "users" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password_hash" TEXT NOT NULL, + "name" TEXT NOT NULL, + "status" "UserStatus" NOT NULL DEFAULT 'ACTIVE', + "mfa_secret" TEXT, + "is_platform_admin" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "roles" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "key" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "roles_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "permissions" ( + "id" TEXT NOT NULL, + "key" TEXT NOT NULL, + + CONSTRAINT "permissions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "user_roles" ( + "user_id" TEXT NOT NULL, + "role_id" TEXT NOT NULL, + + CONSTRAINT "user_roles_pkey" PRIMARY KEY ("user_id","role_id") +); + +-- CreateTable +CREATE TABLE "role_permissions" ( + "role_id" TEXT NOT NULL, + "permission_id" TEXT NOT NULL, + + CONSTRAINT "role_permissions_pkey" PRIMARY KEY ("role_id","permission_id") +); + +-- CreateTable +CREATE TABLE "audit_logs" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "actor_id" TEXT, + "action" TEXT NOT NULL, + "entity" TEXT NOT NULL, + "entity_id" TEXT, + "before" JSONB, + "after" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "tenants_slug_key" ON "tenants"("slug"); + +-- CreateIndex +CREATE INDEX "users_tenant_id_idx" ON "users"("tenant_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_tenant_id_email_key" ON "users"("tenant_id", "email"); + +-- CreateIndex +CREATE INDEX "roles_tenant_id_idx" ON "roles"("tenant_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "roles_tenant_id_key_key" ON "roles"("tenant_id", "key"); + +-- CreateIndex +CREATE UNIQUE INDEX "permissions_key_key" ON "permissions"("key"); + +-- CreateIndex +CREATE INDEX "audit_logs_tenant_id_entity_entity_id_idx" ON "audit_logs"("tenant_id", "entity", "entity_id"); + +-- CreateIndex +CREATE INDEX "audit_logs_tenant_id_created_at_idx" ON "audit_logs"("tenant_id", "created_at"); + +-- AddForeignKey +ALTER TABLE "users" ADD CONSTRAINT "users_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "roles" ADD CONSTRAINT "roles_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_permission_id_fkey" FOREIGN KEY ("permission_id") REFERENCES "permissions"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20260702095816_row_level_security/migration.sql b/prisma/migrations/20260702095816_row_level_security/migration.sql new file mode 100644 index 0000000..4027740 --- /dev/null +++ b/prisma/migrations/20260702095816_row_level_security/migration.sql @@ -0,0 +1,36 @@ +-- Row Level Security als zweite Verteidigungslinie (docs/SPEC.md §2). +-- Primäre Isolation ist der Tenant-Guard in src/server/db.ts. +-- +-- Aktivierungsstand: Policies sind definiert; die App verbindet sich aktuell +-- noch als Tabellen-Owner (Policies greifen für Owner ohne FORCE nicht). +-- Härtungs-Iteration (SPEC §11 Punkt 8): App-Verbindung auf die Rolle +-- "isms_app" umstellen und app.tenant_id pro Transaktion setzen — dann sind +-- die Policies scharf. FORCE wird bewusst nicht gesetzt, damit Migrationen, +-- Seeds und der mandantenübergreifende Login-Lookup als Owner funktionieren. + +-- Eingeschränkte Anwendungsrolle (für die Härtungs-Iteration) +DO $$ +BEGIN + IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'isms_app') THEN + CREATE ROLE isms_app NOLOGIN NOBYPASSRLS; + END IF; +END +$$; + +GRANT USAGE ON SCHEMA public TO isms_app; +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO isms_app; +ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO isms_app; + +-- RLS auf allen mandanten-gebundenen Tabellen +ALTER TABLE "users" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "roles" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "audit_logs" ENABLE ROW LEVEL SECURITY; + +-- Isolation: Zugriff nur auf Zeilen des in app.tenant_id gesetzten Mandanten +CREATE POLICY tenant_isolation ON "users" + USING ("tenant_id" = current_setting('app.tenant_id', true)); +CREATE POLICY tenant_isolation ON "roles" + USING ("tenant_id" = current_setting('app.tenant_id', true)); +CREATE POLICY tenant_isolation ON "audit_logs" + USING ("tenant_id" = current_setting('app.tenant_id', true)); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..b2a2f93 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,92 @@ +import "dotenv/config"; +import { PrismaClient } from "@prisma/client"; +import { PrismaPg } from "@prisma/adapter-pg"; +import { hash } from "@node-rs/argon2"; +import { PERMISSIONS, ROLE_DEFS } from "../src/server/rbac"; + +/** + * Seed: global permission catalog + demo tenant with roles and users. + * Idempotent (upserts) — safe to run repeatedly. + */ + +const prisma = new PrismaClient({ + adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }), +}); + +const DEMO_PASSWORD = "Demo1234!"; + +async function main() { + // 1. Global permission catalog + for (const key of PERMISSIONS) { + await prisma.permission.upsert({ where: { key }, update: {}, create: { key } }); + } + console.log(`✔ ${PERMISSIONS.length} Permissions`); + + // 2. Demo tenant + const tenant = await prisma.tenant.upsert({ + where: { slug: "demo" }, + update: {}, + create: { name: "Demo GmbH", slug: "demo" }, + }); + + // 3. Roles from blueprints, with permission bundles + for (const [key, def] of Object.entries(ROLE_DEFS)) { + const role = await prisma.role.upsert({ + where: { tenantId_key: { tenantId: tenant.id, key } }, + update: { name: def.name }, + create: { tenantId: tenant.id, key, name: def.name }, + }); + const perms = await prisma.permission.findMany({ + where: { key: { in: [...def.permissions] } }, + }); + for (const p of perms) { + await prisma.rolePermission.upsert({ + where: { roleId_permissionId: { roleId: role.id, permissionId: p.id } }, + update: {}, + create: { roleId: role.id, permissionId: p.id }, + }); + } + } + console.log(`✔ Mandant "demo" mit ${Object.keys(ROLE_DEFS).length} Rollen`); + + // 4. Demo users + const passwordHash = await hash(DEMO_PASSWORD); + const demoUsers: { email: string; name: string; roles: string[] }[] = [ + { email: "admin@demo.example", name: "Anna Admin", roles: ["tenant-admin", "isb"] }, + { email: "auditor@demo.example", name: "Axel Auditor", roles: ["auditor"] }, + { email: "owner@demo.example", name: "Oskar Owner", roles: ["owner"] }, + { email: "user@demo.example", name: "Ulla User", roles: ["user"] }, + ]; + + for (const u of demoUsers) { + const user = await prisma.user.upsert({ + where: { tenantId_email: { tenantId: tenant.id, email: u.email } }, + update: {}, + create: { + tenantId: tenant.id, + email: u.email, + name: u.name, + passwordHash, + }, + }); + const roles = await prisma.role.findMany({ + where: { tenantId: tenant.id, key: { in: u.roles } }, + }); + for (const r of roles) { + await prisma.userRole.upsert({ + where: { userId_roleId: { userId: user.id, roleId: r.id } }, + update: {}, + create: { userId: user.id, roleId: r.id }, + }); + } + } + console.log(`✔ ${demoUsers.length} Demo-Nutzer (Passwort: ${DEMO_PASSWORD})`); +} + +main() + .then(() => prisma.$disconnect()) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..63cd05d --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from "@/server/auth"; + +export const { GET, POST } = handlers; diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 0000000..6423d47 --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,43 @@ +import { getTranslations } from "next-intl/server"; +import { redirect } from "next/navigation"; +import { auth, signOut } from "@/server/auth"; + +export default async function DashboardPage() { + const session = await auth(); + if (!session?.user) redirect("/login"); + + const t = await getTranslations("dashboard"); + const tc = await getTranslations("common"); + + async function logout() { + "use server"; + await signOut({ redirectTo: "/login" }); + } + + return ( +
+
+

{t("title")}

+
+ +
+
+ +
+

{t("welcome", { name: session.user.name ?? "" })}

+
+
{t("tenant")}
+
{session.user.tenantSlug}
+
{t("roles")}
+
{session.user.roles.join(", ")}
+
+

{t("placeholder")}

+
+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..ef0c1e9 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,8 +1,10 @@ @import "tailwindcss"; +/* Helles Basis-Theme; Dark-Mode folgt kontrolliert mit dem shadcn/ui-Theming + (kein automatisches prefers-color-scheme, damit Komponenten konsistent bleiben). */ :root { - --background: #ffffff; - --foreground: #171717; + --background: #f8fafc; + --foreground: #0f172a; } @theme inline { @@ -12,15 +14,8 @@ --font-mono: var(--font-geist-mono); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - body { background: var(--background); color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + font-family: var(--font-sans), Arial, Helvetica, sans-serif; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 976eb90..34c4415 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import { NextIntlClientProvider } from "next-intl"; +import { getLocale } from "next-intl/server"; import "./globals.css"; const geistSans = Geist({ @@ -13,21 +15,26 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "ISMS-Tool", + description: + "Informationssicherheits-Managementsystem für ISO/IEC 27001:2022 und TISAX/VDA-ISA", }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const locale = await getLocale(); + return ( - {children} + + {children} + ); } diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..4060d14 --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,104 @@ +import { getTranslations } from "next-intl/server"; +import { redirect } from "next/navigation"; +import { AuthError } from "next-auth"; +import { auth, signIn } from "@/server/auth"; + +export default async function LoginPage({ + searchParams, +}: { + searchParams: Promise<{ error?: string; callbackUrl?: string }>; +}) { + const t = await getTranslations("login"); + const tc = await getTranslations("common"); + const { error, callbackUrl } = await searchParams; + + // Already signed in → straight to the app + const session = await auth(); + if (session?.user) redirect("/dashboard"); + + async function login(formData: FormData) { + "use server"; + const target = (formData.get("callbackUrl") as string) || "/dashboard"; + try { + await signIn("credentials", { + email: formData.get("email"), + password: formData.get("password"), + tenant: formData.get("tenant"), + redirectTo: target, + }); + } catch (err) { + if (err instanceof AuthError) { + redirect(`/login?error=1${target !== "/dashboard" ? `&callbackUrl=${encodeURIComponent(target)}` : ""}`); + } + throw err; // NEXT_REDIRECT from a successful signIn must propagate + } + } + + return ( +
+
+

{tc("appName")}

+

{t("subtitle")}

+ + {error && ( +

+ {t("error")} +

+ )} + +
+ +
+ + +
+
+ + +
+
+ + +

{t("tenantHint")}

+
+ +
+
+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 3f36f7c..a74cb27 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,65 +1,5 @@ -import Image from "next/image"; +import { redirect } from "next/navigation"; export default function Home() { - return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
-
- - Vercel logomark - Deploy Now - - - Documentation - -
-
-
- ); + redirect("/dashboard"); } diff --git a/src/i18n/request.ts b/src/i18n/request.ts new file mode 100644 index 0000000..d388c0e --- /dev/null +++ b/src/i18n/request.ts @@ -0,0 +1,13 @@ +import { getRequestConfig } from "next-intl/server"; + +/** + * i18n scaffold (SPEC §7): all UI strings come from the message catalog. + * MVP is fixed to "de"; per-user locale (en) is prepared — switch here later. + */ +export default getRequestConfig(async () => { + const locale = "de"; + return { + locale, + messages: (await import(`../../messages/${locale}.json`)).default, + }; +}); diff --git a/src/proxy.ts b/src/proxy.ts new file mode 100644 index 0000000..fec61e6 --- /dev/null +++ b/src/proxy.ts @@ -0,0 +1,34 @@ +import { NextResponse, type NextRequest } from "next/server"; + +/** + * Route gate (Next.js 16 proxy, formerly middleware): redirects anonymous + * visitors to /login. This is a UX-level gate only — authoritative checks + * happen server-side via requireSession()/requirePermission(). + */ + +const PUBLIC_PATHS = ["/login", "/api/auth"]; + +export function proxy(request: NextRequest) { + const { pathname } = request.nextUrl; + + if (PUBLIC_PATHS.some((p) => pathname === p || pathname.startsWith(p + "/"))) { + return NextResponse.next(); + } + + const hasSessionCookie = + request.cookies.has("authjs.session-token") || + request.cookies.has("__Secure-authjs.session-token"); + + if (!hasSessionCookie) { + const loginUrl = new URL("/login", request.url); + if (pathname !== "/") loginUrl.searchParams.set("callbackUrl", pathname); + return NextResponse.redirect(loginUrl); + } + + return NextResponse.next(); +} + +export const config = { + // Everything except static assets + matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|ico)).*)"], +}; diff --git a/src/server/audit.ts b/src/server/audit.ts new file mode 100644 index 0000000..2e5defe --- /dev/null +++ b/src/server/audit.ts @@ -0,0 +1,28 @@ +import { prisma } from "./db"; + +/** + * Audit trail (SPEC §5 AuditLog, §10): every writing action creates an entry. + * Uses the raw client on purpose — audit writes must never be silently + * filtered, and tenantId is passed explicitly by the caller. + */ +export async function writeAuditLog(entry: { + tenantId: string; + actorId?: string; + action: "create" | "update" | "delete" | "login" | "logout" | "export" | "import"; + entity: string; + entityId?: string; + before?: unknown; + after?: unknown; +}) { + await prisma.auditLog.create({ + data: { + tenantId: entry.tenantId, + actorId: entry.actorId, + action: entry.action, + entity: entry.entity, + entityId: entry.entityId, + before: entry.before as object | undefined, + after: entry.after as object | undefined, + }, + }); +} diff --git a/src/server/auth.ts b/src/server/auth.ts new file mode 100644 index 0000000..fd9d2eb --- /dev/null +++ b/src/server/auth.ts @@ -0,0 +1,119 @@ +import NextAuth from "next-auth"; +import Credentials from "next-auth/providers/credentials"; +import { verify } from "@node-rs/argon2"; +import { z } from "zod"; +import { prisma } from "./db"; + +/** + * Auth.js (NextAuth v5) with local credentials (MVP, see docs/SPEC.md §0). + * JWT strategy: tenant context, roles and permissions are resolved at login + * and carried in the token — a role change becomes effective at next login. + * TOTP-2FA and OIDC/SAML are planned extensions (SPEC §13). + */ + +const credentialsSchema = z.object({ + email: z.string().email(), + password: z.string().min(1), + // Optional: disambiguates when the same e-mail exists in several tenants. + tenant: z.string().optional(), +}); + +export class LoginError extends Error {} + +export const { handlers, auth, signIn, signOut } = NextAuth({ + session: { strategy: "jwt" }, + pages: { signIn: "/login" }, + providers: [ + Credentials({ + credentials: { + email: {}, + password: {}, + tenant: {}, + }, + authorize: async (raw) => { + const parsed = credentialsSchema.safeParse(raw); + if (!parsed.success) return null; + const { email, password, tenant } = parsed.data; + + const users = await prisma.user.findMany({ + where: { + email: email.toLowerCase(), + status: "ACTIVE", + tenant: { + status: "ACTIVE", + ...(tenant ? { slug: tenant.toLowerCase() } : {}), + }, + }, + include: { + tenant: true, + userRoles: { + include: { + role: { + include: { + rolePermissions: { include: { permission: true } }, + }, + }, + }, + }, + }, + }); + + // Same e-mail in several tenants and no slug given → cannot decide. + if (users.length !== 1) return null; + const user = users[0]; + + const valid = await verify(user.passwordHash, password); + if (!valid) return null; + + const roles = user.userRoles.map((ur) => ur.role.key); + const permissions = [ + ...new Set( + user.userRoles.flatMap((ur) => + ur.role.rolePermissions.map((rp) => rp.permission.key) + ) + ), + ]; + + return { + id: user.id, + email: user.email, + name: user.name, + tenantId: user.tenantId, + tenantSlug: user.tenant.slug, + roles, + permissions, + isPlatformAdmin: user.isPlatformAdmin, + }; + }, + }), + ], + callbacks: { + jwt({ token, user }) { + if (user) { + token.userId = user.id!; + token.tenantId = user.tenantId; + token.tenantSlug = user.tenantSlug; + token.roles = user.roles; + token.permissions = user.permissions; + token.isPlatformAdmin = user.isPlatformAdmin; + } + return token; + }, + session({ session, token }) { + session.user.id = token.userId; + session.user.tenantId = token.tenantId; + session.user.tenantSlug = token.tenantSlug; + session.user.roles = token.roles; + session.user.permissions = token.permissions; + session.user.isPlatformAdmin = token.isPlatformAdmin; + return session; + }, + }, +}); + +/** Session guard for server components / route handlers. */ +export async function requireSession() { + const session = await auth(); + if (!session?.user) throw new Error("Nicht angemeldet"); + return session; +} diff --git a/src/server/rbac.ts b/src/server/rbac.ts new file mode 100644 index 0000000..b9e0a89 --- /dev/null +++ b/src/server/rbac.ts @@ -0,0 +1,152 @@ +/** + * RBAC catalog: granular permissions bundled into per-tenant roles. + * See docs/SPEC.md §3. Permissions are global rows (no tenantId), + * roles are created per tenant from ROLE_DEFS at tenant provisioning. + */ + +export const PERMISSIONS = [ + // Administration + "tenant:manage", + "user:read", + "user:manage", + // Assets & BIA (one module) + "asset:read", + "asset:write", + "bia:read", + "bia:write", + // Risk + "risk:read", + "risk:write", + "risk:accept", + // Compliance + "soa:read", + "soa:write", + // Measures / tasks + "measure:read", + "measure:write", + // Policies + "policy:read", + "policy:write", + "policy:approve", + // Incidents + "incident:report", + "incident:manage", + // Audits + "audit:read", + "audit:conduct", + // Suppliers + "supplier:read", + "supplier:write", + // Evidence / documents + "evidence:read", + "evidence:write", + // Reporting / management review + "report:read", + "review:manage", + // Chat + "chat:use", +] as const; + +export type Permission = (typeof PERMISSIONS)[number]; + +/** Role blueprints instantiated for every tenant (key → name + permissions). */ +export const ROLE_DEFS: Record< + string, + { name: string; permissions: readonly Permission[] } +> = { + "tenant-admin": { + name: "Mandanten-Admin", + permissions: ["tenant:manage", "user:read", "user:manage", "report:read", "chat:use"], + }, + isb: { + name: "ISB / CISO", + permissions: [ + "user:read", + "asset:read", + "asset:write", + "bia:read", + "bia:write", + "risk:read", + "risk:write", + "risk:accept", + "soa:read", + "soa:write", + "measure:read", + "measure:write", + "policy:read", + "policy:write", + "policy:approve", + "incident:report", + "incident:manage", + "audit:read", + "supplier:read", + "supplier:write", + "evidence:read", + "evidence:write", + "report:read", + "review:manage", + "chat:use", + ], + }, + auditor: { + name: "Auditor", + permissions: [ + "asset:read", + "bia:read", + "risk:read", + "soa:read", + "measure:read", + "policy:read", + "audit:read", + "audit:conduct", + "supplier:read", + "evidence:read", + "report:read", + ], + }, + owner: { + name: "Asset-/Risk-Owner", + permissions: [ + "asset:read", + "asset:write", + "bia:read", + "risk:read", + "risk:write", + "measure:read", + "measure:write", + "policy:read", + "incident:report", + "evidence:read", + "evidence:write", + "chat:use", + ], + }, + user: { + name: "Mitarbeiter", + permissions: ["policy:read", "incident:report", "measure:read", "chat:use"], + }, +}; + +export class ForbiddenError extends Error { + constructor(permission: Permission) { + super(`Fehlende Berechtigung: ${permission}`); + this.name = "ForbiddenError"; + } +} + +/** Server-side permission check — UI hiding is convenience, this is security. */ +export function requirePermission( + session: { user?: { permissions?: string[] } } | null, + permission: Permission +): void { + if (!session?.user?.permissions?.includes(permission)) { + throw new ForbiddenError(permission); + } +} + +export function hasPermission( + session: { user?: { permissions?: string[] } } | null, + permission: Permission +): boolean { + return session?.user?.permissions?.includes(permission) ?? false; +} diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts new file mode 100644 index 0000000..d621b18 --- /dev/null +++ b/src/types/next-auth.d.ts @@ -0,0 +1,35 @@ +import type { DefaultSession } from "next-auth"; + +declare module "next-auth" { + interface Session { + user: { + id: string; + tenantId: string; + tenantSlug: string; + roles: string[]; + permissions: string[]; + isPlatformAdmin: boolean; + } & DefaultSession["user"]; + } + + interface User { + id?: string; + tenantId: string; + tenantSlug: string; + roles: string[]; + permissions: string[]; + isPlatformAdmin: boolean; + } +} + +// next-auth/jwt re-exportiert nur aus @auth/core/jwt — Augmentation muss dorthin +declare module "@auth/core/jwt" { + interface JWT { + userId: string; + tenantId: string; + tenantSlug: string; + roles: string[]; + permissions: string[]; + isPlatformAdmin: boolean; + } +}