From 3141ba60f4db96e32551608e71ca669ee920d7a2 Mon Sep 17 00:00:00 2001 From: Elias Bennour Date: Tue, 20 May 2025 01:06:10 +0200 Subject: [PATCH] initial commit --- package-lock.json | 536 +++++++++++++++++- package.json | 18 +- .../20250519211949_init/migration.sql | 91 +++ .../migration.sql | 9 + .../migration.sql | 10 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 88 +++ src/lib/prisma.ts | 22 + src/pages/_app.tsx | 11 +- src/pages/admin/index.tsx | 536 ++++++++++++++++++ src/pages/api/admin/users.ts | 103 ++++ .../admin/users/[userId]/adjust-balance.ts | 146 +++++ .../api/admin/users/[userId]/reset-balance.ts | 110 ++++ .../users/[userId]/set-approval-status.ts | 126 ++++ .../api/admin/users/[userId]/transactions.ts | 103 ++++ src/pages/api/auth/[...nextauth].ts | 102 ++++ src/pages/api/auth/signup.ts | 86 +++ src/pages/api/hello.ts | 13 - src/pages/api/user/balance.ts | 65 +++ src/pages/api/user/increase-balance.ts | 102 ++++ src/pages/api/user/transactions.ts | 85 +++ src/pages/auth/error.tsx | 88 +++ src/pages/auth/signin.tsx | 226 ++++++++ src/pages/auth/signup.tsx | 247 ++++++++ src/pages/dashboard.tsx | 281 +++++++++ src/pages/index.tsx | 179 +++--- src/types/global.d.ts | 12 + src/types/next-auth.d.ts | 41 ++ 28 files changed, 3297 insertions(+), 142 deletions(-) create mode 100644 prisma/migrations/20250519211949_init/migration.sql create mode 100644 prisma/migrations/20250519213914_add_user_timestamps/migration.sql create mode 100644 prisma/migrations/20250519224815_add_user_approval_status/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 src/lib/prisma.ts create mode 100644 src/pages/admin/index.tsx create mode 100644 src/pages/api/admin/users.ts create mode 100644 src/pages/api/admin/users/[userId]/adjust-balance.ts create mode 100644 src/pages/api/admin/users/[userId]/reset-balance.ts create mode 100644 src/pages/api/admin/users/[userId]/set-approval-status.ts create mode 100644 src/pages/api/admin/users/[userId]/transactions.ts create mode 100644 src/pages/api/auth/[...nextauth].ts create mode 100644 src/pages/api/auth/signup.ts delete mode 100644 src/pages/api/hello.ts create mode 100644 src/pages/api/user/balance.ts create mode 100644 src/pages/api/user/increase-balance.ts create mode 100644 src/pages/api/user/transactions.ts create mode 100644 src/pages/auth/error.tsx create mode 100644 src/pages/auth/signin.tsx create mode 100644 src/pages/auth/signup.tsx create mode 100644 src/pages/dashboard.tsx create mode 100644 src/types/global.d.ts create mode 100644 src/types/next-auth.d.ts diff --git a/package-lock.json b/package-lock.json index 063b64a..7bf5931 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,12 @@ "name": "strichliste-fsmni", "version": "0.1.0", "dependencies": { + "@headlessui/react": "^2.2.3", + "@next-auth/prisma-adapter": "^1.0.7", + "@prisma/client": "^6.8.2", + "bcryptjs": "^3.0.2", "next": "15.3.2", + "next-auth": "^4.24.11", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -20,6 +25,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.3.2", + "prisma": "^6.8.2", "tailwindcss": "^4", "typescript": "^5" } @@ -51,6 +57,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/core": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", @@ -225,6 +240,79 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz", + "integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz", + "integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, + "node_modules/@headlessui/react": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.3.tgz", + "integrity": "sha512-hgOJGXPifPlOczIeSwX8OjLWRJ5XdYApZFf7DeCbCrO1PXHkPhNTRrA9ZwJsgAG7SON1i2JcvIreF/kbgtJeaQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.16", + "@react-aria/focus": "^3.20.2", + "@react-aria/interactions": "^3.25.0", + "@tanstack/react-virtual": "^3.13.6", + "use-sync-external-store": "^1.5.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -747,6 +835,16 @@ "@tybys/wasm-util": "^0.9.0" } }, + "node_modules/@next-auth/prisma-adapter": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@next-auth/prisma-adapter/-/prisma-adapter-1.0.7.tgz", + "integrity": "sha512-Cdko4KfcmKjsyHFrWwZ//lfLUbcLqlyFqjd/nYE2m3aZ7tjMNUjpks47iw7NTCnXf+5UWz5Ypyt1dSs1EP5QJw==", + "license": "ISC", + "peerDependencies": { + "@prisma/client": ">=2.26.0 || >=3", + "next-auth": "^4" + } + }, "node_modules/@next/env": { "version": "15.3.2", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.2.tgz", @@ -939,6 +1037,194 @@ "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/@prisma/client": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.8.2.tgz", + "integrity": "sha512-5II+vbyzv4si6Yunwgkj0qT/iY0zyspttoDrL3R4BYgLdp42/d2C8xdi9vqkrYtKt9H32oFIukvyw3Koz5JoDg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.8.2.tgz", + "integrity": "sha512-ZJY1fF4qRBPdLQ/60wxNtX+eu89c3AkYEcP7L3jkp0IPXCNphCYxikTg55kPJLDOG6P0X+QG5tCv6CmsBRZWFQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "jiti": "2.4.2" + } + }, + "node_modules/@prisma/debug": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.8.2.tgz", + "integrity": "sha512-4muBSSUwJJ9BYth5N8tqts8JtiLT8QI/RSAzEogwEfpbYGFo9mYsInsVo8dqXdPO2+Rm5OG5q0qWDDE3nyUbVg==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.8.2.tgz", + "integrity": "sha512-XqAJ//LXjqYRQ1RRabs79KOY4+v6gZOGzbcwDQl0D6n9WBKjV7qdrbd042CwSK0v0lM9MSHsbcFnU2Yn7z8Zlw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.8.2", + "@prisma/engines-version": "6.8.0-43.2060c79ba17c6bb9f5823312b6f6b7f4a845738e", + "@prisma/fetch-engine": "6.8.2", + "@prisma/get-platform": "6.8.2" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.8.0-43.2060c79ba17c6bb9f5823312b6f6b7f4a845738e", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.8.0-43.2060c79ba17c6bb9f5823312b6f6b7f4a845738e.tgz", + "integrity": "sha512-Rkik9lMyHpFNGaLpPF3H5q5TQTkm/aE7DsGM5m92FZTvWQsvmi6Va8On3pWvqLHOt5aPUvFb/FeZTmphI4CPiQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.8.2.tgz", + "integrity": "sha512-lCvikWOgaLOfqXGacEKSNeenvj0n3qR5QvZUOmPE2e1Eh8cMYSobxonCg9rqM6FSdTfbpqp9xwhSAOYfNqSW0g==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.8.2", + "@prisma/engines-version": "6.8.0-43.2060c79ba17c6bb9f5823312b6f6b7f4a845738e", + "@prisma/get-platform": "6.8.2" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.8.2.tgz", + "integrity": "sha512-vXSxyUgX3vm1Q70QwzwkjeYfRryIvKno1SXbIqwSptKwqKzskINnDUcx85oX+ys6ooN2ATGSD0xN2UTfg6Zcow==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.8.2" + } + }, + "node_modules/@react-aria/focus": { + "version": "3.20.3", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.3.tgz", + "integrity": "sha512-rR5uZUMSY4xLHmpK/I8bP1V6vUNHFo33gTvrvNUsAKKqvMfa7R2nu5A6v97dr5g6tVH6xzpdkPsOJCWh90H2cw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.25.1", + "@react-aria/utils": "^3.29.0", + "@react-types/shared": "^3.29.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.1.tgz", + "integrity": "sha512-ntLrlgqkmZupbbjekz3fE/n3eQH2vhncx8gUp0+N+GttKWevx7jos11JUBjnJwb1RSOPgRUFcrluOqBp0VgcfQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.8", + "@react-aria/utils": "^3.29.0", + "@react-stately/flags": "^3.1.1", + "@react-types/shared": "^3.29.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.8", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.8.tgz", + "integrity": "sha512-lQDE/c9uTfBSDOjaZUJS8xP2jCKVk4zjQeIlCH90xaLhHDgbpCdns3xvFpJJujfj3nI4Ll9K7A+ONUBDCASOuw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.29.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.29.0.tgz", + "integrity": "sha512-jSOrZimCuT1iKNVlhjIxDkAhgF7HSp3pqyT6qjg/ZoA0wfqCi/okmrMPiWSAKBnkgX93N8GYTLT3CIEO6WZe9Q==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.8", + "@react-stately/flags": "^3.1.1", + "@react-stately/utils": "^3.10.6", + "@react-types/shared": "^3.29.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/flags": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.1.tgz", + "integrity": "sha512-XPR5gi5LfrPdhxZzdIlJDz/B5cBf63l4q6/AzNqVWFKgd0QqY5LvWJftXkklaIUpKSJkIKQb8dphuZXDtkWNqg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.6.tgz", + "integrity": "sha512-O76ip4InfTTzAJrg8OaZxKU4vvjMDOpfA/PGNOytiXwBbkct2ZeZwaimJ8Bt9W1bj5VsZ81/o/tW4BacbdDOMA==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.29.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.29.1.tgz", + "integrity": "sha512-KtM+cDf2CXoUX439rfEhbnEdAgFZX20UP2A35ypNIawR7/PFFPjQDWyA2EnClCcW/dLWJDEPX2U8+EJff8xqmQ==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1244,6 +1530,33 @@ "tailwindcss": "4.1.7" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.8.tgz", + "integrity": "sha512-meS2AanUg50f3FBSNoAdBSRAh8uS0ue01qm7zrw65KGJtiXB9QXfybqZwkh4uFpRv2iX/eu5tjcH5wqUpwYLPg==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.8", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.8.tgz", + "integrity": "sha512-BT6w89Hqy7YKaWewYzmecXQzcJh6HTBbKYJIIkMaNU49DZ06LoTV3z32DWWEdUsgW6n1xTmwTLs4GtWrZC261w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", @@ -2084,6 +2397,15 @@ "dev": true, "license": "MIT" }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2232,6 +2554,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -2284,6 +2615,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3999,12 +4339,21 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "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", @@ -4394,6 +4743,18 @@ "loose-envify": "cli.js" } }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -4602,6 +4963,38 @@ } } }, + "node_modules/next-auth": { + "version": "4.24.11", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz", + "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==", + "license": "ISC", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@panva/hkdf": "^1.0.2", + "cookie": "^0.7.0", + "jose": "^4.15.5", + "oauth": "^0.9.15", + "openid-client": "^5.4.0", + "preact": "^10.6.3", + "preact-render-to-string": "^5.1.19", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "@auth/core": "0.34.2", + "next": "^12.2.5 || ^13 || ^14 || ^15", + "nodemailer": "^6.6.5", + "react": "^17.0.2 || ^18 || ^19", + "react-dom": "^17.0.2 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@auth/core": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -4630,6 +5023,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4640,6 +5039,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -4753,6 +5161,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oidc-token-hash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz", + "integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, + "node_modules/openid-client": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "license": "MIT", + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4919,6 +5351,28 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/preact": { + "version": "10.26.6", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.6.tgz", + "integrity": "sha512-5SRRBinwpwkaD+OqlBDeITlRgvd8I8QlxHJw9AxSdMNV6O+LodN9nUyYGpSF7sadHjs6RzeFShMexC6DbtWr9g==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", + "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", + "license": "MIT", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4929,6 +5383,38 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", + "license": "MIT" + }, + "node_modules/prisma": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.8.2.tgz", + "integrity": "sha512-JNricTXQxzDtRS7lCGGOB4g5DJ91eg3nozdubXze3LpcMl1oWwcFddrj++Up3jnRE6X/3gB/xz3V+ecBk/eEGA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.8.2", + "@prisma/engines": "6.8.2" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5602,6 +6088,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.7.tgz", @@ -5637,6 +6129,16 @@ "node": ">=18" } }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", @@ -5822,7 +6324,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -5901,6 +6403,24 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6017,14 +6537,10 @@ } }, "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" }, "node_modules/yocto-queue": { "version": "0.1.0", diff --git a/package.json b/package.json index 3053bce..988d296 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,25 @@ "lint": "next lint" }, "dependencies": { + "@headlessui/react": "^2.2.3", + "@next-auth/prisma-adapter": "^1.0.7", + "@prisma/client": "^6.8.2", + "bcryptjs": "^3.0.2", + "next": "15.3.2", + "next-auth": "^4.24.11", "react": "^19.0.0", - "react-dom": "^19.0.0", - "next": "15.3.2" + "react-dom": "^19.0.0" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.3.2", - "@eslint/eslintrc": "^3" + "prisma": "^6.8.2", + "tailwindcss": "^4", + "typescript": "^5" } } diff --git a/prisma/migrations/20250519211949_init/migration.sql b/prisma/migrations/20250519211949_init/migration.sql new file mode 100644 index 0000000..8ba2f68 --- /dev/null +++ b/prisma/migrations/20250519211949_init/migration.sql @@ -0,0 +1,91 @@ +-- CreateTable +CREATE TABLE "Account" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "refresh_token" TEXT, + "access_token" TEXT, + "expires_at" INTEGER, + "token_type" TEXT, + "scope" TEXT, + "id_token" TEXT, + "session_state" TEXT, + + CONSTRAINT "Account_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Session" ( + "id" TEXT NOT NULL, + "sessionToken" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Session_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "name" TEXT, + "email" TEXT, + "emailVerified" TIMESTAMP(3), + "image" TEXT, + "password" TEXT, + "role" TEXT DEFAULT 'user', + "balance" DECIMAL(10,2) DEFAULT 0.00, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VerificationToken" ( + "identifier" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL +); + +-- CreateTable +CREATE TABLE "Transaction" ( + "id" TEXT NOT NULL, + "amount" DECIMAL(10,2) NOT NULL, + "type" TEXT NOT NULL, + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT NOT NULL, + "triggeredByAdminId" TEXT, + + CONSTRAINT "Transaction_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); + +-- CreateIndex +CREATE INDEX "Transaction_triggeredByAdminId_idx" ON "Transaction"("triggeredByAdminId"); + +-- AddForeignKey +ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_triggeredByAdminId_fkey" FOREIGN KEY ("triggeredByAdminId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20250519213914_add_user_timestamps/migration.sql b/prisma/migrations/20250519213914_add_user_timestamps/migration.sql new file mode 100644 index 0000000..cacf93a --- /dev/null +++ b/prisma/migrations/20250519213914_add_user_timestamps/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - Added the required column `updatedAt` to the `User` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; diff --git a/prisma/migrations/20250519224815_add_user_approval_status/migration.sql b/prisma/migrations/20250519224815_add_user_approval_status/migration.sql new file mode 100644 index 0000000..bdd3867 --- /dev/null +++ b/prisma/migrations/20250519224815_add_user_approval_status/migration.sql @@ -0,0 +1,10 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "approvedAt" TIMESTAMP(3), +ADD COLUMN "approvedByAdminId" TEXT, +ADD COLUMN "isApproved" BOOLEAN NOT NULL DEFAULT false; + +-- CreateIndex +CREATE INDEX "Transaction_userId_idx" ON "Transaction"("userId"); + +-- AddForeignKey +ALTER TABLE "User" ADD CONSTRAINT "User_approvedByAdminId_fkey" FOREIGN KEY ("approvedByAdminId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 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/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..8ac7e60 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,88 @@ +// Datei: prisma/schema.prisma + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) +} + +model Session { + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? // Von NextAuth benötigt + image String? + password String? // Für CredentialsProvider + role String? @default("user") + balance Decimal? @default(0.00) @db.Decimal(10, 2) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Felder für den Freigabeprozess + isApproved Boolean @default(false) // Ist der Benutzer vom Admin freigegeben? + approvedAt DateTime? // Wann wurde der Benutzer freigegeben? + approvedByAdminId String? // ID des Admins, der den Benutzer freigegeben hat + approvedByAdmin User? @relation("AdminApprovals", fields: [approvedByAdminId], references: [id], onDelete: SetNull) + + accounts Account[] + sessions Session[] + transactions Transaction[] + triggeredTransactions Transaction[] @relation("AdminTransactions") + approvedUsers User[] @relation("AdminApprovals") // Benutzer, die dieser Admin freigegeben hat +} + +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) +} + +model Transaction { + id String @id @default(cuid()) + amount Decimal @db.Decimal(10, 2) + type String + description String? + createdAt DateTime @default(now()) + + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + triggeredByAdminId String? + triggeredByAdmin User? @relation("AdminTransactions", fields: [triggeredByAdminId], references: [id], onDelete: SetNull) + + @@index([triggeredByAdminId]) + @@index([userId]) // Index für userId in Transaction hinzugefügt +} diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts new file mode 100644 index 0000000..91ecaa5 --- /dev/null +++ b/src/lib/prisma.ts @@ -0,0 +1,22 @@ +import { PrismaClient } from '@prisma/client'; + +declare global { + var prisma: PrismaClient | undefined; +} + +const globalForPrisma = global as typeof globalThis & { prisma?: PrismaClient }; + +let prisma: PrismaClient; + +if (process.env.NODE_ENV === 'production') { + prisma = new PrismaClient(); +} else { + if (!globalForPrisma.prisma) { + globalForPrisma.prisma = new PrismaClient({ + log: ['query', 'info', 'warn', 'error'], + }); + } + prisma = globalForPrisma.prisma; +} + +export default prisma; \ No newline at end of file diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index a7a790f..94c28d4 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,6 +1,11 @@ import "@/styles/globals.css"; -import type { AppProps } from "next/app"; +import type {AppProps} from "next/app"; +import {SessionProvider} from "next-auth/react"; -export default function App({ Component, pageProps }: AppProps) { - return ; +export default function App({Component, pageProps: {session, ...pageProps}}: AppProps) { + return ( + + + + ) } diff --git a/src/pages/admin/index.tsx b/src/pages/admin/index.tsx new file mode 100644 index 0000000..02d9e3d --- /dev/null +++ b/src/pages/admin/index.tsx @@ -0,0 +1,536 @@ +// Datei: pages/admin/index.tsx +import type { NextPage } from 'next'; +import { useSession, signOut } from 'next-auth/react'; +import { useRouter } from 'next/router'; +import { useEffect, useState, Fragment } from 'react'; +import { Dialog, Transition } from '@headlessui/react'; + +// Typen für die Admin-Seite (Frontend) +interface AdminUser { + id: string; + name: string | null; + email: string | null; + role: string | null; + balance: string | null; + createdAt: string; + updatedAt: string; + isApproved: boolean; + approvedAt: string | null; + _count?: { + transactions?: number; + }; +} + +interface AdminUsersApiResponse { + users: AdminUser[]; + totalUsers: number; +} + +interface AdminTransaction { + id: string; + userId: string; + amount: string; + type: string; + description: string | null; + createdAt: string; + triggeredByAdminId: string | null; + triggeredByAdmin?: { + id: string; + name: string | null; + } | null; +} +interface AdminUserTransactionsApiResponse { + user: { + id: string; + name: string | null; + email: string | null; + }; + transactions: AdminTransaction[]; + totalTransactions: number; +} + +interface SetApprovalStatusApiResponse { // Typ für die Antwort der /set-approval-status API + message: string; + userId: string; + isApproved: boolean; + approvedAt: Date | null; // API liefert Date-Objekt, im Frontend ggf. als String behandeln +} + + +const AdminPanelPage: NextPage = () => { + const { data: session, status } = useSession(); + const router = useRouter(); + + const [users, setUsers] = useState([]); + const [isLoadingUsers, setIsLoadingUsers] = useState(true); + const [error, setError] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + + const [selectedUserForAdjustment, setSelectedUserForAdjustment] = useState(null); + const [adjustmentAmount, setAdjustmentAmount] = useState(''); + const [adjustmentAction, setAdjustmentAction] = useState<'increase' | 'decrease'>('increase'); + const [adjustmentReason, setAdjustmentReason] = useState(''); + const [isAdjustModalOpen, setIsAdjustModalOpen] = useState(false); + + const [selectedUserForTransactions, setSelectedUserForTransactions] = useState(null); + const [userTransactions, setUserTransactions] = useState([]); + const [isLoadingUserTransactions, setIsLoadingUserTransactions] = useState(false); + const [isTransactionsModalOpen, setIsTransactionsModalOpen] = useState(false); + + useEffect(() => { + if (status === 'loading') return; + + if (status === 'unauthenticated') { + router.push('/auth/signin'); + return; + } + if (session?.user?.role !== 'admin') { + router.push('/dashboard'); + return; + } + fetchUsers(); + }, [status, session, router]); + + const fetchUsers = async () => { + setIsLoadingUsers(true); + setError(null); + try { + const res = await fetch('/api/admin/users'); + if (!res.ok) { + const errorData = await res.json(); + throw new Error(errorData.message || 'Fehler beim Laden der Benutzerliste'); + } + const data: AdminUsersApiResponse = await res.json(); + const formattedUsers = data.users.map(user => ({ + ...user, + // approvedAt von der API kommt als ISO-String oder null + approvedAt: user.approvedAt ? new Date(user.approvedAt).toLocaleString('de-DE') : null + })); + setUsers(formattedUsers); + } catch (err: unknown) { + if (err instanceof Error) { + setError(err.message); + } else { + setError('Ein unbekannter Fehler beim Laden der Benutzerliste ist aufgetreten.'); + } + console.error("Fehler Benutzerliste:", err); + } finally { + setIsLoadingUsers(false); + } + }; + + const handleResetBalance = async (userId: string) => { + setError(null); + setSuccessMessage(null); + if (!confirm(`Möchtest du den Saldo für Benutzer ${userId} wirklich zurücksetzen?`)) { + return; + } + try { + const response = await fetch(`/api/admin/users/${userId}/reset-balance`, { + method: 'POST', + }); + const data = await response.json(); + if (!response.ok) { + throw new Error(data.message || 'Fehler beim Zurücksetzen des Saldos'); + } + setSuccessMessage(data.message); + await fetchUsers(); + } catch (err: unknown) { + if (err instanceof Error) { + setError(err.message); + } else { + setError('Ein unbekannter Fehler beim Zurücksetzen des Saldos ist aufgetreten.'); + } + console.error("Fehler Saldo zurücksetzen:", err); + } + }; + + const openAdjustModal = (user: AdminUser) => { + setSelectedUserForAdjustment(user); + setAdjustmentAmount(''); + setAdjustmentReason(''); + setAdjustmentAction('increase'); + setIsAdjustModalOpen(true); + }; + + const handleAdjustBalance = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedUserForAdjustment) return; + setError(null); + setSuccessMessage(null); + + const amountNumber = parseFloat(adjustmentAmount); + if (isNaN(amountNumber) || amountNumber <= 0) { + setError("Bitte gib einen gültigen positiven Betrag ein."); + return; + } + + try { + const response = await fetch(`/api/admin/users/${selectedUserForAdjustment.id}/adjust-balance`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + amount: amountNumber, + action: adjustmentAction, + reason: adjustmentReason, + }), + }); + const data = await response.json(); + if (!response.ok) { + throw new Error(data.message || 'Fehler beim Anpassen des Saldos'); + } + setSuccessMessage(data.message); + setIsAdjustModalOpen(false); + await fetchUsers(); + } catch (err: unknown) { + if (err instanceof Error) { + setError(err.message); + } else { + setError('Ein unbekannter Fehler beim Anpassen des Saldos ist aufgetreten.'); + } + console.error("Fehler Saldo anpassen:", err); + } + }; + + const openTransactionsModal = async (user: AdminUser) => { + setSelectedUserForTransactions(user); + setIsTransactionsModalOpen(true); + setIsLoadingUserTransactions(true); + setError(null); + try { + const res = await fetch(`/api/admin/users/${user.id}/transactions`); + if (!res.ok) { + const errorData = await res.json(); + throw new Error(errorData.message || 'Fehler beim Laden der Transaktionen des Benutzers'); + } + const data: AdminUserTransactionsApiResponse = await res.json(); + setUserTransactions(data.transactions); + } catch (err: unknown) { + if (err instanceof Error) { + setError(err.message); + } else { + setError('Ein unbekannter Fehler beim Laden der Transaktionen des Benutzers ist aufgetreten.'); + } + console.error("Fehler Transaktionen des Benutzers:", err); + setUserTransactions([]); + } finally { + setIsLoadingUserTransactions(false); + } + }; + + // NEU: Benutzer Freigabestatus umschalten + const handleToggleApproval = async (userId: string, currentApprovalStatus: boolean) => { + setError(null); + setSuccessMessage(null); + const actionText = currentApprovalStatus ? "sperren (Freigabe widerrufen)" : "freigeben"; + if (!confirm(`Möchtest du den Benutzer ${userId} wirklich ${actionText}?`)) { + return; + } + try { + const response = await fetch(`/api/admin/users/${userId}/set-approval-status`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ isApproved: !currentApprovalStatus }), // Den entgegengesetzten Status senden + }); + const data: SetApprovalStatusApiResponse = await response.json(); // Typ für die Antwort verwenden + if (!response.ok) { + throw new Error(data.message || `Fehler beim ${actionText} des Benutzers.`); + } + setSuccessMessage(data.message); + await fetchUsers(); // Benutzerliste neu laden, um den aktualisierten Status anzuzeigen + } catch (err: unknown) { + if (err instanceof Error) { + setError(err.message); + } else { + setError(`Ein unbekannter Fehler beim ${actionText} des Benutzers ist aufgetreten.`); + } + console.error(`Fehler Benutzer ${actionText}:`, err); + } + }; + + + if (status === 'loading' || (status === 'authenticated' && session?.user?.role !== 'admin' && !isLoadingUsers) ) { + return ( +
+

Laden oder Zugriff wird geprüft...

+
+ ); + } + + if (status === 'unauthenticated') { + return ( +
+

Bitte als Admin anmelden.

+
+ ); + } + + return ( +
+
+
+

Admin Panel

+

Benutzerverwaltung

+
+
+ + +
+
+ + {error &&
{error}
} + {successMessage &&
{successMessage}
} + +
+

Benutzerübersicht

+ {isLoadingUsers ? ( +

Benutzerliste wird geladen...

+ ) : users.length > 0 ? ( +
+ + + + + + + + + + + + + {users.map((user) => ( + + + + + + + + + ))} + +
NameE-MailSaldoRolleStatusAktionen
{user.name || 'N/A'}{user.email || 'N/A'}{user.balance ? `${user.balance} €` : '0.00 €'}{user.role} + {user.isApproved ? ( + + Freigegeben + + ) : ( + + Ausstehend/Gesperrt + + )} + {user.isApproved && user.approvedAt && ( +

am {user.approvedAt}

+ )} +
+ {user.isApproved ? ( + + ) : ( + + )} + + + +
+
+ ) : ( +

Keine Benutzer gefunden.

+ )} +
+ + {/* Modal für Saldo anpassen */} + + setIsAdjustModalOpen(false)}> + +
+ + +
+
+ + + + Saldo anpassen für {selectedUserForAdjustment?.name || selectedUserForAdjustment?.email} + +
+
+ + setAdjustmentAmount(e.target.value)} + required + className="mt-1 block w-full px-3 py-2 border border-gray-400 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm text-gray-900" + /> +
+
+ + +
+
+ +