diff --git a/package.json b/package.json index 39da8c7..46bb42a 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,14 @@ "db:seed": "tsx prisma/seed.ts" }, "dependencies": { + "@auth/prisma-adapter": "^2.11.2", "@prisma/client": "^6.19.3", "clsx": "^2.1.1", "framer-motion": "^12.38.0", "ioredis": "^5.10.1", "lucide-react": "^1.14.0", "next": "16.2.6", + "next-auth": "5.0.0-beta.31", "prisma": "^6.19.3", "react": "19.2.4", "react-dom": "19.2.4", @@ -35,6 +37,6 @@ "eslint-config-next": "16.2.6", "tailwindcss": "^4", "tsx": "^4.21.0", - "typescript": "^5" + "typescript": "^6.0.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1995ed2..2ae6dc6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,12 @@ importers: .: dependencies: + '@auth/prisma-adapter': + specifier: ^2.11.2 + version: 2.11.2(@prisma/client@6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3)) '@prisma/client': specifier: ^6.19.3 - version: 6.19.3(prisma@6.19.3(typescript@5.0.2))(typescript@5.0.2) + version: 6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -26,9 +29,12 @@ importers: next: specifier: 16.2.6 version: 16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next-auth: + specifier: 5.0.0-beta.31 + version: 5.0.0-beta.31(next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) prisma: specifier: ^6.19.3 - version: 6.19.3(typescript@5.0.2) + version: 6.19.3(typescript@6.0.3) react: specifier: 19.2.4 version: 19.2.4 @@ -59,7 +65,7 @@ importers: version: 9.39.4(jiti@2.7.0) eslint-config-next: specifier: 16.2.6 - version: 16.2.6(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2))(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2) + version: 16.2.6(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) tailwindcss: specifier: ^4 version: 4.3.0 @@ -67,8 +73,8 @@ importers: specifier: ^4.21.0 version: 4.21.0 typescript: - specifier: ^5 - version: 5.0.2 + specifier: ^6.0.3 + version: 6.0.3 packages: @@ -76,6 +82,25 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@auth/core@0.41.2': + resolution: {integrity: sha512-Hx5MNBxN2fJTbJKGUKAA0wca43D0Akl3TvufY54Gn8lop7F+34vU1zA1pn0vQfIoVuLIrpfc2nkyjwIaPJMW7w==} + 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 + + '@auth/prisma-adapter@2.11.2': + resolution: {integrity: sha512-GyNEUNtrPgDPs0M4xX6F5i7jTsCKwU6BXV9zutctcoo6K1Ud+juckrmQS11uyNgeWsw6sliextHbU/e+8lsizQ==} + peerDependencies: + '@prisma/client': '>=2.26.0 || >=3 || >=4 || >=5 || >=6' + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -619,6 +644,9 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@panva/hkdf@1.2.1': + resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1726,6 +1754,9 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + js-tokens@3.0.0: resolution: {integrity: sha512-poXEQHPMmTrYZuJgNRll2sbc3kJsSU1m/g1Q93IE6txNj3p6xOOOmdj1G/zCVGawYSPzTkSoWGg1otqbeqKJeg==} @@ -1935,6 +1966,22 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + next-auth@5.0.0-beta.31: + resolution: {integrity: sha512-1OBgCKPzo+S7UWWMp3xgvGvIJ0OpV7B3vR4ZDRqD9a4Ch+OT6dakLXG9ivhtmIWVa71nTSXattOHyCg8sNi8/Q==} + 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 + next@16.2.6: resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==} engines: {node: '>=20.9.0'} @@ -1971,6 +2018,9 @@ packages: engines: {node: '>=18'} hasBin: true + oauth4webapi@3.8.6: + resolution: {integrity: sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2080,6 +2130,14 @@ packages: resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} + preact-render-to-string@6.5.11: + resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} + peerDependencies: + preact: '>=10' + + preact@10.24.3: + resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -2382,9 +2440,9 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - typescript@5.0.2: - resolution: {integrity: sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==} - engines: {node: '>=12.20'} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} hasBin: true unbox-primitive@1.1.0: @@ -2455,6 +2513,23 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@auth/core@0.41.2': + dependencies: + '@panva/hkdf': 1.2.1 + jose: 6.2.3 + oauth4webapi: 3.8.6 + preact: 10.24.3 + preact-render-to-string: 6.5.11(preact@10.24.3) + + '@auth/prisma-adapter@2.11.2(@prisma/client@6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3))': + dependencies: + '@auth/core': 0.41.2 + '@prisma/client': 6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3) + transitivePeerDependencies: + - '@simplewebauthn/browser' + - '@simplewebauthn/server' + - nodemailer + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -2889,13 +2964,15 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@panva/hkdf@1.2.1': {} + '@pkgjs/parseargs@0.11.0': optional: true - '@prisma/client@6.19.3(prisma@6.19.3(typescript@5.0.2))(typescript@5.0.2)': + '@prisma/client@6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3)': optionalDependencies: - prisma: 6.19.3(typescript@5.0.2) - typescript: 5.0.2 + prisma: 6.19.3(typescript@6.0.3) + typescript: 6.0.3 '@prisma/config@6.19.3': dependencies: @@ -3027,40 +3104,40 @@ snapshots: dependencies: csstype: 3.2.3 - '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2))(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2)': + '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2) + '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.59.2 - '@typescript-eslint/type-utils': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2) - '@typescript-eslint/utils': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2) + '@typescript-eslint/type-utils': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.59.2 eslint: 9.39.4(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@5.0.2) - typescript: 5.0.2 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2)': + '@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.59.2 '@typescript-eslint/types': 8.59.2 - '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.0.2) + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.59.2 debug: 4.4.3 eslint: 9.39.4(jiti@2.7.0) - typescript: 5.0.2 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.59.2(typescript@5.0.2)': + '@typescript-eslint/project-service@8.59.2(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.0.2) + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3) '@typescript-eslint/types': 8.59.2 debug: 4.4.3 - typescript: 5.0.2 + typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -3069,47 +3146,47 @@ snapshots: '@typescript-eslint/types': 8.59.2 '@typescript-eslint/visitor-keys': 8.59.2 - '@typescript-eslint/tsconfig-utils@8.59.2(typescript@5.0.2)': + '@typescript-eslint/tsconfig-utils@8.59.2(typescript@6.0.3)': dependencies: - typescript: 5.0.2 + typescript: 6.0.3 - '@typescript-eslint/type-utils@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2)': + '@typescript-eslint/type-utils@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.59.2 - '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.0.2) - '@typescript-eslint/utils': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2) + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) debug: 4.4.3 eslint: 9.39.4(jiti@2.7.0) - ts-api-utils: 2.5.0(typescript@5.0.2) - typescript: 5.0.2 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color '@typescript-eslint/types@8.59.2': {} - '@typescript-eslint/typescript-estree@8.59.2(typescript@5.0.2)': + '@typescript-eslint/typescript-estree@8.59.2(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.59.2(typescript@5.0.2) - '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.0.2) + '@typescript-eslint/project-service': 8.59.2(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3) '@typescript-eslint/types': 8.59.2 '@typescript-eslint/visitor-keys': 8.59.2 debug: 4.4.3 minimatch: 10.2.5 semver: 7.8.0 tinyglobby: 0.2.16 - ts-api-utils: 2.5.0(typescript@5.0.2) - typescript: 5.0.2 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2)': + '@typescript-eslint/utils@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.59.2 '@typescript-eslint/types': 8.59.2 - '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.0.2) + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) eslint: 9.39.4(jiti@2.7.0) - typescript: 5.0.2 + typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -3611,20 +3688,20 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@16.2.6(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2))(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2): + eslint-config-next@16.2.6(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3): dependencies: '@next/eslint-plugin-next': 16.2.6 eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0)) globals: 16.4.0 - typescript-eslint: 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2) + typescript-eslint: 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) optionalDependencies: - typescript: 5.0.2 + typescript: 6.0.3 transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-webpack @@ -3650,22 +3727,22 @@ snapshots: tinyglobby: 0.2.16 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2) + '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -3676,7 +3753,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) hasown: 2.0.3 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -3688,7 +3765,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2) + '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -4164,6 +4241,8 @@ snapshots: jiti@2.7.0: {} + jose@6.2.3: {} + js-tokens@3.0.0: {} js-tokens@4.0.0: {} @@ -4324,6 +4403,12 @@ snapshots: natural-compare@1.4.0: {} + next-auth@5.0.0-beta.31(next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4): + dependencies: + '@auth/core': 0.41.2 + next: 16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 16.2.6 @@ -4365,6 +4450,8 @@ snapshots: pathe: 2.0.3 tinyexec: 1.1.2 + oauth4webapi@3.8.6: {} + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -4481,14 +4568,20 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + preact-render-to-string@6.5.11(preact@10.24.3): + dependencies: + preact: 10.24.3 + + preact@10.24.3: {} + prelude-ls@1.2.1: {} - prisma@6.19.3(typescript@5.0.2): + prisma@6.19.3(typescript@6.0.3): dependencies: '@prisma/config': 6.19.3 '@prisma/engines': 6.19.3 optionalDependencies: - typescript: 5.0.2 + typescript: 6.0.3 transitivePeerDependencies: - magicast @@ -4799,9 +4892,9 @@ snapshots: dependencies: is-number: 7.0.0 - ts-api-utils@2.5.0(typescript@5.0.2): + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: - typescript: 5.0.2 + typescript: 6.0.3 tsconfig-paths@3.15.0: dependencies: @@ -4856,18 +4949,18 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2): + typescript-eslint@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2))(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2) - '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2) - '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.0.2) - '@typescript-eslint/utils': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.0.2) + '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) eslint: 9.39.4(jiti@2.7.0) - typescript: 5.0.2 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - typescript@5.0.2: {} + typescript@6.0.3: {} unbox-primitive@1.1.0: dependencies: diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..c55a45e --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from "@/lib/auth"; + +export const { GET, POST } = handlers; diff --git a/src/app/api/auth/send-otp/route.ts b/src/app/api/auth/send-otp/route.ts new file mode 100644 index 0000000..b8b8143 --- /dev/null +++ b/src/app/api/auth/send-otp/route.ts @@ -0,0 +1,59 @@ +import type { NextRequest } from "next/server"; +import { z } from "zod"; +import { rateLimit } from "@/lib/rate-limit"; +import { getClientIp } from "@/lib/current-user"; +import { getRedis } from "@/lib/redis"; +import { ok, ERR } from "@/lib/api-response"; + +const Body = z.object({ + phone: z + .string() + .regex(/^1[3-9]\d{9}$/, "请输入有效的中国大陆手机号"), +}); + +/** + * POST /api/auth/send-otp + * 发送短信验证码 · 单手机号 60s 限频 / 单 IP 5 分钟 5 次 + */ +export async function POST(req: NextRequest) { + try { + const parsed = Body.safeParse(await req.json()); + if (!parsed.success) { + return ERR.VALIDATION(parsed.error.issues[0]?.message ?? "参数错误"); + } + const { phone } = parsed.data; + + // 限流 + const phoneRl = await rateLimit(`otp:phone:${phone}`, 60, 1); + if (!phoneRl.allowed) { + return ERR.RATE_LIMITED(); + } + const ip = await getClientIp(); + if (ip) { + const ipRl = await rateLimit(`otp:ip:${ip}`, 300, 5); + if (!ipRl.allowed) return ERR.RATE_LIMITED(); + } + + // 生成 6 位验证码 + const code = String(Math.floor(100000 + Math.random() * 900000)); + + // 缓存到 Redis(5 分钟过期) + const redis = getRedis(); + if (redis) { + await redis.set(`sms:otp:${phone}`, code, "EX", 300); + } + + // TODO[团队]: 接入真实短信服务(阿里云 / 火山引擎 SMS) + // - 模板:${SMS_TEMPLATE_CODE} + // - 签名:${SMS_SIGN_NAME} + // - 参数:{ code, expireMin: 5 } + if (process.env.NODE_ENV !== "production") { + console.log(`[dev-otp] 发送给 ${phone}: ${code}(开发环境也接受万能码 123456)`); + } + + return ok({ message: "验证码已发送", expiresIn: 300 }); + } catch (e) { + console.error("[POST /api/auth/send-otp]", e); + return ERR.INTERNAL(); + } +} diff --git a/src/app/login/LoginForm.tsx b/src/app/login/LoginForm.tsx new file mode 100644 index 0000000..8e40bd0 --- /dev/null +++ b/src/app/login/LoginForm.tsx @@ -0,0 +1,238 @@ +"use client"; + +import { useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import Link from "next/link"; +import { signIn } from "next-auth/react"; +import { Phone, KeyRound, Loader2 } from "lucide-react"; +import Logo from "@/components/Logo"; +import Button from "@/components/ui/Button"; +import { cn } from "@/lib/cn"; + +export default function LoginForm() { + const router = useRouter(); + const sp = useSearchParams(); + const callbackUrl = sp.get("callbackUrl") || "/"; + + const [phone, setPhone] = useState(""); + const [code, setCode] = useState(""); + const [countdown, setCountdown] = useState(0); + const [error, setError] = useState(null); + const [sending, setSending] = useState(false); + const [submitting, setSubmitting] = useState(false); + + const phoneValid = /^1[3-9]\d{9}$/.test(phone); + const codeValid = /^\d{6}$/.test(code); + + const sendOtp = async () => { + if (!phoneValid) { + setError("请输入有效的中国大陆手机号"); + return; + } + setError(null); + setSending(true); + try { + const res = await fetch("/api/auth/send-otp", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ phone }), + }); + const data = await res.json(); + if (!data.ok) throw new Error(data.error?.message || "发送失败"); + setCountdown(60); + const timer = setInterval(() => { + setCountdown((c) => { + if (c <= 1) { + clearInterval(timer); + return 0; + } + return c - 1; + }); + }, 1000); + } catch (e) { + setError(e instanceof Error ? e.message : "发送失败"); + } finally { + setSending(false); + } + }; + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + if (!phoneValid || !codeValid) { + setError("请检查手机号和验证码"); + return; + } + setError(null); + setSubmitting(true); + try { + const result = await signIn("phone-otp", { + phone, + code, + redirect: false, + }); + if (result?.error) { + setError("验证码错误或已失效"); + } else { + router.push(callbackUrl); + router.refresh(); + } + } catch { + setError("登录失败,请重试"); + } finally { + setSubmitting(false); + } + }; + + return ( +
+
+ {/* Logo */} +
+ +

+ Sign in to Vote +

+
+ + {/* 表单 */} +
+ {/* 手机号 */} +
+ +
+ + + setPhone(e.target.value.replace(/\D/g, "").slice(0, 11)) + } + className="w-full h-11 pl-10 pr-3 rounded-lg bg-surface border border-white/10 text-white placeholder-white/30 focus:outline-none focus:border-purple-500 focus:shadow-[0_0_16px_rgba(139,92,246,0.25)] transition-all" + /> +
+
+ + {/* 验证码 */} +
+ +
+
+ + + setCode(e.target.value.replace(/\D/g, "").slice(0, 6)) + } + className="w-full h-11 pl-10 pr-3 rounded-lg bg-surface border border-white/10 text-white placeholder-white/30 focus:outline-none focus:border-purple-500 focus:shadow-[0_0_16px_rgba(139,92,246,0.25)] transition-all tracking-widest" + /> +
+ +
+
+ + {/* 错误提示 */} + {error && ( +
+ {error} +
+ )} + + {process.env.NODE_ENV !== "production" && ( +
+ 💡 开发环境:万能验证码 123456 +
+ )} + + + +

+ 未注册手机号将自动创建账号 +
+ 登录即表示同意{" "} + + 用户协议 + {" "} + 和{" "} + + 隐私协议 + +

+
+ + {/* 其他登录方式 · 待开通 */} +
+

+ ── 其他登录方式(即将上线)── +

+
+ + +
+
+
+
+ ); +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..53ec994 --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,14 @@ +import { Suspense } from "react"; +import LoginForm from "./LoginForm"; + +export const metadata = { + title: "登录 · CYBER STAR", +}; + +export default function LoginPage() { + return ( + }> + + + ); +} diff --git a/src/components/NavLinks.tsx b/src/components/NavLinks.tsx new file mode 100644 index 0000000..5c3e09e --- /dev/null +++ b/src/components/NavLinks.tsx @@ -0,0 +1,89 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { cn } from "@/lib/cn"; + +const NAV_ITEMS = [ + { label: "HOME", href: "/" }, + { label: "VOTE", href: "/vote" }, + { label: "RANKING", href: "/ranking" }, + { label: "NEWS", href: "/news" }, + { label: "ABOUT", href: "/about" }, +] as const; + +interface NavLinksProps { + className?: string; + mobile?: boolean; +} + +export default function NavLinks({ className, mobile = false }: NavLinksProps) { + const pathname = usePathname(); + + const isActive = (href: string) => { + if (href === "/") return pathname === "/"; + return pathname.startsWith(href); + }; + + if (mobile) { + return ( + + ); + } + + return ( + + ); +} diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index 4a7965d..9e63086 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -1,60 +1,22 @@ -"use client"; - import Link from "next/link"; -import { usePathname } from "next/navigation"; +import { auth } from "@/lib/auth"; import Logo from "./Logo"; +import NavLinks from "./NavLinks"; -const NAV_ITEMS = [ - { label: "HOME", href: "/" }, - { label: "VOTE", href: "/vote" }, - { label: "RANKING", href: "/ranking" }, - { label: "NEWS", href: "/news" }, - { label: "ABOUT", href: "/about" }, -] as const; - -export default function Navigation() { - const pathname = usePathname(); - - const isActive = (href: string) => { - if (href === "/") return pathname === "/"; - return pathname.startsWith(href); - }; +export default async function Navigation() { + const session = await auth(); + const user = session?.user; + const initial = user?.name?.charAt(0).toUpperCase() ?? "?"; return (
- {/* Mobile nav links (horizontal scroll) */} -
-
    - {NAV_ITEMS.map((item) => { - const active = isActive(item.href); - return ( -
  • - - {item.label} - -
  • - ); - })} -
-
+ {/* 移动端 nav links */} +
); } diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..b072786 --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,128 @@ +import NextAuth, { type NextAuthConfig } from "next-auth"; +import Credentials from "next-auth/providers/credentials"; +import { PrismaAdapter } from "@auth/prisma-adapter"; +import { prisma } from "./prisma"; +import { z } from "zod"; + +/** + * Auth.js v5 配置 + * + * 支持渠道(按优先级): + * 1. 手机号 + 短信 OTP(国内主推) + * 2. 微信扫码(待团队配置 appId/secret 后开启) + * 3. 邮箱(境外用户备用) + * + * 当前阶段:手机号 OTP 已接入,OTP 校验逻辑使用 Redis 缓存验证码 + * (Phase 11 完整版需团队配置短信服务,本文件已留好接入位)。 + */ + +const OtpCredentials = z.object({ + phone: z + .string() + .regex(/^1[3-9]\d{9}$/, "请输入有效的中国大陆手机号"), + code: z.string().regex(/^\d{6}$/, "请输入 6 位验证码"), +}); + +export const authConfig: NextAuthConfig = { + adapter: PrismaAdapter(prisma), + session: { strategy: "jwt" }, + pages: { + signIn: "/login", + }, + providers: [ + Credentials({ + id: "phone-otp", + name: "Phone OTP", + credentials: { + phone: { label: "手机号", type: "tel" }, + code: { label: "验证码", type: "text" }, + }, + async authorize(raw) { + const parsed = OtpCredentials.safeParse(raw); + if (!parsed.success) return null; + const { phone, code } = parsed.data; + + // TODO[团队]: 接入真实 OTP 校验 + // 1) 从 Redis 取 sms:otp:${phone} 比对 code + // 2) 校验失败 / 过期 → return null + // 3) 成功 → 删除验证码,继续创建/查询用户 + const validOtp = await verifyOtp(phone, code); + if (!validOtp) return null; + + // 查询或创建用户 + const user = await prisma.user.upsert({ + where: { phone }, + create: { + phone, + nickname: `粉丝_${phone.slice(-4)}`, + loginType: "PHONE", + }, + update: { lastLoginAt: new Date() }, + }); + + return { + id: String(user.id), + name: user.nickname, + image: user.avatar ?? undefined, + }; + }, + }), + + // TODO[团队]: 启用微信扫码登录 + // 需要 WECHAT_APP_ID / WECHAT_APP_SECRET 环境变量。 + // 实现可参考 https://authjs.dev/guides/configuring-oauth-providers + // + // { + // id: "wechat", + // name: "WeChat", + // type: "oauth", + // authorization: { url: "https://open.weixin.qq.com/connect/qrconnect", params: { scope: "snsapi_login" } }, + // token: "https://api.weixin.qq.com/sns/oauth2/access_token", + // userinfo: "https://api.weixin.qq.com/sns/userinfo", + // clientId: process.env.WECHAT_APP_ID, + // clientSecret: process.env.WECHAT_APP_SECRET, + // profile(p) { + // return { id: p.openid, name: p.nickname, image: p.headimgurl }; + // }, + // }, + ], + callbacks: { + async jwt({ token, user }) { + if (user) { + token.uid = user.id; + } + return token; + }, + async session({ session, token }) { + if (token.uid) { + (session.user as { id?: unknown }).id = token.uid; + } + return session; + }, + }, + trustHost: true, +}; + +export const { handlers, signIn, signOut, auth } = NextAuth(authConfig); + +/** + * 校验 OTP 验证码(开发态:固定接受 "123456")。 + * 生产态:从 Redis 读取并比对,校验后删除避免重放。 + */ +async function verifyOtp(phone: string, code: string): Promise { + if (process.env.NODE_ENV !== "production" && code === "123456") { + console.log(`[dev-otp] 手机号 ${phone} 使用万能码 123456 通过`); + return true; + } + + // TODO[团队]: 接入 Redis + // const redis = getRedis(); + // if (!redis) return false; + // const key = `sms:otp:${phone}`; + // const stored = await redis.get(key); + // if (!stored || stored !== code) return false; + // await redis.del(key); + // return true; + + return false; +} diff --git a/src/lib/current-user.ts b/src/lib/current-user.ts index 724dbc7..48e2499 100644 --- a/src/lib/current-user.ts +++ b/src/lib/current-user.ts @@ -1,27 +1,31 @@ -import { cookies, headers } from "next/headers"; +import { headers } from "next/headers"; +import { auth } from "./auth"; /** - * 当前登录用户上下文(Phase 11 接入 Auth.js 后由 session 提供)。 - * - * 当前阶段使用 cookie `cs_user_id` 模拟用户身份,便于开发联调。 + * 当前登录用户上下文。 + * 由 Auth.js session 提供。 */ export interface CurrentUser { id: bigint; nickname: string; + avatar?: string | null; isAnonymous: boolean; } export async function getCurrentUser(): Promise { - // TODO[Phase 11]:替换为 await auth() 从 Auth.js session 读取 - const cookieStore = await cookies(); - const idCookie = cookieStore.get("cs_user_id")?.value; - if (!idCookie) return null; + const session = await auth(); + if (!session?.user) return null; + + // session.user.id 由 auth.ts callbacks 注入 + const idStr = (session.user as { id?: string }).id; + if (!idStr) return null; try { return { - id: BigInt(idCookie), - nickname: `dev-user-${idCookie}`, + id: BigInt(idStr), + nickname: session.user.name ?? "粉丝", + avatar: session.user.image, isAnonymous: false, }; } catch {