diff --git a/package.json b/package.json index b4b2692..a87d5f5 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "@ai-sdk/google": "^4.0.8", "@ai-sdk/openai": "^4.0.7", "@ai-sdk/openai-compatible": "^3.0.5", - "@auth/prisma-adapter": "^2.7.4", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -60,7 +59,7 @@ "@tanstack/react-table": "^8.20.5", "@vercel/analytics": "^2.0.1", "ai": "^7.0.14", - "bcryptjs": "^3.0.3", + "better-auth": "^1.6.23", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", @@ -69,7 +68,6 @@ "geist": "^1.3.1", "lucide-react": "^1.16.0", "next": "^16.2.6", - "next-auth": "5.0.0-beta.31", "next-themes": "^0.4.4", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fda9630..ea04ff4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,9 +17,6 @@ importers: '@ai-sdk/openai-compatible': specifier: ^3.0.5 version: 3.0.5(zod@4.4.3) - '@auth/prisma-adapter': - specifier: ^2.7.4 - version: 2.11.2(@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3))(typescript@6.0.3)) '@dnd-kit/core': specifier: ^6.3.1 version: 6.3.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -92,9 +89,9 @@ importers: ai: specifier: ^7.0.14 version: 7.0.14(zod@4.4.3) - bcryptjs: - specifier: ^3.0.3 - version: 3.0.3 + better-auth: + specifier: ^1.6.23 + version: 1.6.23(@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3))(typescript@6.0.3))(mysql2@3.15.3)(next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(pg@8.21.0)(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -119,9 +116,6 @@ importers: next: specifier: ^16.2.6 version: 16.2.6(@babel/core@7.29.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - 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.6(react@19.2.6))(react@19.2.6))(react@19.2.6) next-themes: specifier: ^0.4.4 version: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -233,25 +227,6 @@ 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'} @@ -319,6 +294,85 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@better-auth/core@1.6.23': + resolution: {integrity: sha512-beEhOs0uVeOxYOZKUfIEBd/nQV2Bd4/6wyLxZ0OFkn6CMTK2Vi+hXuZLnyPBeB6RdHpebEoJWiHqwHxBIxgPDQ==} + peerDependencies: + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 + '@cloudflare/workers-types': '>=4' + '@opentelemetry/api': ^1.9.0 + better-call: 1.3.7 + jose: ^6.1.0 + kysely: ^0.28.5 || ^0.29.0 + nanostores: ^1.0.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + '@opentelemetry/api': + optional: true + + '@better-auth/drizzle-adapter@1.6.23': + resolution: {integrity: sha512-2+/PTVfIP9E7iz6af8TB3lhnowHUj9ljC66kECmHaFEdUqPgzHoWux9epotKwO7XDg2ui4ttWQ8CMeNFLvQeKQ==} + peerDependencies: + '@better-auth/core': ^1.6.23 + '@better-auth/utils': 0.4.2 + drizzle-orm: ^0.45.2 + peerDependenciesMeta: + drizzle-orm: + optional: true + + '@better-auth/kysely-adapter@1.6.23': + resolution: {integrity: sha512-zbNJsMbG09exfkGyvFqBLLqWoMPAUWjxCuUnEK5AsjbYoZeIjj/QGZgdf4CapVWryKxjA9Q6Jlr6fbiPpC3VAg==} + peerDependencies: + '@better-auth/core': ^1.6.23 + '@better-auth/utils': 0.4.2 + kysely: ^0.28.17 || ^0.29.0 + peerDependenciesMeta: + kysely: + optional: true + + '@better-auth/memory-adapter@1.6.23': + resolution: {integrity: sha512-krIiR0pIVkaKlAzm690n5bcMW4NGbqeMg0HQSD9fz/KcQF/eWLqcq9gG/BhHTj2i/y96qH+W5JWPmaSOS5iTgQ==} + peerDependencies: + '@better-auth/core': ^1.6.23 + '@better-auth/utils': 0.4.2 + + '@better-auth/mongo-adapter@1.6.23': + resolution: {integrity: sha512-7+QdevitGlKBbP6JbiSk5SBnzPsKV/mDrQBGBn8hwByQLeJwqpqbuBPw7ZI8vzUlFfAAnyFiqwP3Eb8mxnp7pA==} + peerDependencies: + '@better-auth/core': ^1.6.23 + '@better-auth/utils': 0.4.2 + mongodb: ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + mongodb: + optional: true + + '@better-auth/prisma-adapter@1.6.23': + resolution: {integrity: sha512-2qSdzidq4tkb1eS5TTqb4Nzg0mdZWm3Qky9SYeXeb8PpVQbC2sxqJhEM5mK7y12uU6I8hc64wO9f7AFVNL+6UQ==} + peerDependencies: + '@better-auth/core': ^1.6.23 + '@better-auth/utils': 0.4.2 + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + '@prisma/client': + optional: true + prisma: + optional: true + + '@better-auth/telemetry@1.6.23': + resolution: {integrity: sha512-/R2Kb+z2BpDOOWwVHqOk+c0VNpuwfCv4Hp5Yr9003WIZPax/zyNraGLB9CFE8qF2gZW8Dsz419k4I8CPrGzpDA==} + peerDependencies: + '@better-auth/core': ^1.6.23 + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 + + '@better-auth/utils@0.4.2': + resolution: {integrity: sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==} + + '@better-fetch/fetch@1.3.1': + resolution: {integrity: sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==} + '@dnd-kit/accessibility@3.1.1': resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} peerDependencies: @@ -817,6 +871,14 @@ packages: cpu: [x64] os: [win32] + '@noble/ciphers@2.2.0': + resolution: {integrity: sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==} + engines: {node: '>= 20.19.0'} + + '@noble/hashes@2.2.0': + resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==} + engines: {node: '>= 20.19.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -833,8 +895,9 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} - '@panva/hkdf@1.2.1': - resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} + '@opentelemetry/semantic-conventions@1.41.1': + resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} + engines: {node: '>=14'} '@prisma/adapter-pg@7.8.0': resolution: {integrity: sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg==} @@ -1884,9 +1947,75 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - bcryptjs@3.0.3: - resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} - hasBin: true + better-auth@1.6.23: + resolution: {integrity: sha512-4vOaRd9UiKGKm9R+ej0jjU1es3MiJIiNc9Qq3VCnYqOZ4/nb5272QqTxWYoDxyUXl5x6A2x2we5KZKQO9teTQQ==} + peerDependencies: + '@lynx-js/react': '*' + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + '@sveltejs/kit': ^2.0.0 + '@tanstack/react-start': ^1.0.0 + '@tanstack/solid-start': ^1.0.0 + better-sqlite3: ^12.0.0 + drizzle-kit: '>=0.31.4' + drizzle-orm: ^0.45.2 + mongodb: ^6.0.0 || ^7.0.0 + mysql2: ^3.0.0 + next: ^14.0.0 || ^15.0.0 || ^16.0.0 + pg: ^8.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + solid-js: ^1.0.0 + svelte: ^4.0.0 || ^5.0.0 + vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 + vue: ^3.0.0 + peerDependenciesMeta: + '@lynx-js/react': + optional: true + '@prisma/client': + optional: true + '@sveltejs/kit': + optional: true + '@tanstack/react-start': + optional: true + '@tanstack/solid-start': + optional: true + better-sqlite3: + optional: true + drizzle-kit: + optional: true + drizzle-orm: + optional: true + mongodb: + optional: true + mysql2: + optional: true + next: + optional: true + pg: + optional: true + prisma: + optional: true + react: + optional: true + react-dom: + optional: true + solid-js: + optional: true + svelte: + optional: true + vitest: + optional: true + vue: + optional: true + + better-call@1.3.7: + resolution: {integrity: sha512-Al51/hjp2SSp6CRTa3F2ptcx4yQVS1xWKoY6jcVXqNYOap6mHFP2jUBn5EwIL4iIed1/Sq4hlQ+Umm6EflZG+w==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true better-result@2.9.2: resolution: {integrity: sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==} @@ -2714,6 +2843,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kysely@0.29.2: + resolution: {integrity: sha512-s6WVJyEZrbm6jhBpiKHsGHyePMrVQKJ85wZCFCr9W4QHv6WTjWIrdvTmO9hDEA3bNK0xkrE2DqrHsXMLWuZpQg==} + engines: {node: '>=22.0.0'} + language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -2862,6 +2995,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanostores@1.4.0: + resolution: {integrity: sha512-i0tloweeudshAEuddpDxcg9Ik6pkPfVsHIgKyf143JrgG7/MOh0+q7BypdLXZPoOP7fOYt1eTcwGkyiVmhJFkA==} + engines: {node: ^20.0.0 || >=22.0.0} + napi-postinstall@0.3.4: resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -2870,22 +3007,6 @@ 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-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: @@ -2920,9 +3041,6 @@ packages: node-releases@2.0.44: resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==} - 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'} @@ -3079,14 +3197,6 @@ packages: resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} engines: {node: '>=12'} - 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'} @@ -3244,6 +3354,9 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rou3@0.7.12: + resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -3277,6 +3390,9 @@ packages: seq-queue@0.0.5: resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + set-cookie-parser@3.1.1: + resolution: {integrity: sha512-vM9SUhjsUYs6UeJUmygc5Ofm5eQGe85riob5ju6XCgFGJI5PLV4nrDAQpQjd+LkFBpAkADn5BQQpZ9EUNkyLuA==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -3641,23 +3757,6 @@ 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@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3))(typescript@6.0.3))': - dependencies: - '@auth/core': 0.41.2 - '@prisma/client': 7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(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 @@ -3758,6 +3857,60 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0)': + dependencies: + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 + '@opentelemetry/semantic-conventions': 1.41.1 + '@standard-schema/spec': 1.1.0 + better-call: 1.3.7(zod@4.4.3) + jose: 6.2.3 + kysely: 0.29.2 + nanostores: 1.4.0 + zod: 4.4.3 + + '@better-auth/drizzle-adapter@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0))(@better-auth/utils@0.4.2)': + dependencies: + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0) + '@better-auth/utils': 0.4.2 + + '@better-auth/kysely-adapter@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(kysely@0.29.2)': + dependencies: + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0) + '@better-auth/utils': 0.4.2 + optionalDependencies: + kysely: 0.29.2 + + '@better-auth/memory-adapter@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0))(@better-auth/utils@0.4.2)': + dependencies: + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0) + '@better-auth/utils': 0.4.2 + + '@better-auth/mongo-adapter@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0))(@better-auth/utils@0.4.2)': + dependencies: + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0) + '@better-auth/utils': 0.4.2 + + '@better-auth/prisma-adapter@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3))(typescript@6.0.3))(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3))': + dependencies: + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0) + '@better-auth/utils': 0.4.2 + optionalDependencies: + '@prisma/client': 7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3))(typescript@6.0.3) + prisma: 7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + + '@better-auth/telemetry@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)': + dependencies: + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0) + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 + + '@better-auth/utils@0.4.2': + dependencies: + '@noble/hashes': 2.2.0 + + '@better-fetch/fetch@1.3.1': {} + '@dnd-kit/accessibility@3.1.1(react@19.2.6)': dependencies: react: 19.2.6 @@ -4130,6 +4283,10 @@ snapshots: '@next/swc-win32-x64-msvc@16.2.6': optional: true + '@noble/ciphers@2.2.0': {} + + '@noble/hashes@2.2.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4144,7 +4301,7 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} - '@panva/hkdf@1.2.1': {} + '@opentelemetry/semantic-conventions@1.41.1': {} '@prisma/adapter-pg@7.8.0': dependencies: @@ -5204,7 +5361,45 @@ snapshots: baseline-browser-mapping@2.10.31: {} - bcryptjs@3.0.3: {} + better-auth@1.6.23(@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3))(typescript@6.0.3))(mysql2@3.15.3)(next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(pg@8.21.0)(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0) + '@better-auth/drizzle-adapter': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0))(@better-auth/utils@0.4.2) + '@better-auth/kysely-adapter': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(kysely@0.29.2) + '@better-auth/memory-adapter': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0))(@better-auth/utils@0.4.2) + '@better-auth/mongo-adapter': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0))(@better-auth/utils@0.4.2) + '@better-auth/prisma-adapter': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3))(typescript@6.0.3))(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3)) + '@better-auth/telemetry': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1) + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 + '@noble/ciphers': 2.2.0 + '@noble/hashes': 2.2.0 + better-call: 1.3.7(zod@4.4.3) + defu: 6.1.7 + jose: 6.2.3 + kysely: 0.29.2 + nanostores: 1.4.0 + zod: 4.4.3 + optionalDependencies: + '@prisma/client': 7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3))(typescript@6.0.3) + mysql2: 3.15.3 + next: 16.2.6(@babel/core@7.29.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + pg: 8.21.0 + prisma: 7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + transitivePeerDependencies: + - '@cloudflare/workers-types' + - '@opentelemetry/api' + + better-call@1.3.7(zod@4.4.3): + dependencies: + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 + rou3: 0.7.12 + set-cookie-parser: 3.1.1 + optionalDependencies: + zod: 4.4.3 better-result@2.9.2: {} @@ -6173,6 +6368,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kysely@0.29.2: {} + language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -6298,16 +6495,12 @@ snapshots: nanoid@3.3.12: {} + nanostores@1.4.0: {} + napi-postinstall@0.3.4: {} 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.6(react@19.2.6))(react@19.2.6))(react@19.2.6): - dependencies: - '@auth/core': 0.41.2 - next: 16.2.6(@babel/core@7.29.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react: 19.2.6 - next-themes@0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: react: 19.2.6 @@ -6346,8 +6539,6 @@ snapshots: node-releases@2.0.44: {} - oauth4webapi@3.8.6: {} - object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -6504,12 +6695,6 @@ snapshots: postgres@3.4.7: {} - 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@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3): @@ -6674,6 +6859,8 @@ snapshots: reusify@1.1.0: {} + rou3@0.7.12: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -6707,6 +6894,8 @@ snapshots: seq-queue@0.0.5: {} + set-cookie-parser@3.1.1: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ae59cf9..034e3f8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,3 +1,4 @@ + generator client { provider = "prisma-client" output = "../src/generated/prisma" @@ -21,7 +22,6 @@ model User { id String @id @default(cuid()) email String @unique name String? - passwordHash String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -29,6 +29,14 @@ model User { aiApiKeyEncrypted String? aiModel String? aiBaseUrl String? + emailVerified Boolean @default(false) + image String? + twoFactorEnabled Boolean? @default(false) + sessions Session[] + accounts Account[] + twofactors TwoFactor[] + + @@map("user") } // ========================================================================= @@ -132,16 +140,16 @@ enum ProductStatus { } model Product { - id String @id @default(cuid()) - hustleId String - name String - description String? - notes String? - status ProductStatus @default(RESEARCH) - coverImageUrl String? - sortOrder Int @default(0) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + hustleId String + name String + description String? + notes String? + status ProductStatus @default(RESEARCH) + coverImageUrl String? + sortOrder Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt hustle Hustle @relation(fields: [hustleId], references: [id], onDelete: Cascade) costItems CostItem[] @@ -222,20 +230,20 @@ model Supplier { // ========================================================================= model InventoryItem { - id String @id @default(cuid()) - hustleId String - name String - sku String? - unit String @default("unit") - quantity Decimal @default(0) @db.Decimal(14, 2) - reorderAt Decimal @default(0) @db.Decimal(14, 2) - unitCost Decimal @default(0) @db.Decimal(14, 2) - notes String? - url String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + hustleId String + name String + sku String? + unit String @default("unit") + quantity Decimal @default(0) @db.Decimal(14, 2) + reorderAt Decimal @default(0) @db.Decimal(14, 2) + unitCost Decimal @default(0) @db.Decimal(14, 2) + notes String? + url String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - hustle Hustle @relation(fields: [hustleId], references: [id], onDelete: Cascade) + hustle Hustle @relation(fields: [hustleId], references: [id], onDelete: Cascade) adjustments InventoryAdjustment[] @@index([hustleId]) @@ -259,17 +267,17 @@ model InventoryAdjustment { // ========================================================================= model Customer { - id String @id @default(cuid()) - hustleId String - name String - phone String? - email String? - city String? - address String? - source String? - notes String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + hustleId String + name String + phone String? + email String? + city String? + address String? + source String? + notes String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt hustle Hustle @relation(fields: [hustleId], references: [id], onDelete: Cascade) orders Order[] @@ -308,26 +316,26 @@ enum PaymentMethod { } model Order { - id String @id @default(cuid()) - hustleId String - customerId String? - orderNumber String - customerName String - status OrderStatus @default(PENDING) - paymentStatus PaymentStatus @default(UNPAID) - paymentMethod PaymentMethod? - shippingCost Decimal @default(0) @db.Decimal(14, 2) - discount Decimal @default(0) @db.Decimal(14, 2) - total Decimal @default(0) @db.Decimal(14, 2) - amountPaid Decimal @default(0) @db.Decimal(14, 2) - customizations Json? - notes String? - dueDate DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - hustle Hustle @relation(fields: [hustleId], references: [id], onDelete: Cascade) - customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull) + id String @id @default(cuid()) + hustleId String + customerId String? + orderNumber String + customerName String + status OrderStatus @default(PENDING) + paymentStatus PaymentStatus @default(UNPAID) + paymentMethod PaymentMethod? + shippingCost Decimal @default(0) @db.Decimal(14, 2) + discount Decimal @default(0) @db.Decimal(14, 2) + total Decimal @default(0) @db.Decimal(14, 2) + amountPaid Decimal @default(0) @db.Decimal(14, 2) + customizations Json? + notes String? + dueDate DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + hustle Hustle @relation(fields: [hustleId], references: [id], onDelete: Cascade) + customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull) items OrderItem[] events OrderStatusEvent[] payments OrderPayment[] @@ -367,13 +375,13 @@ model OrderStatusEvent { } model OrderPayment { - id String @id @default(cuid()) + id String @id @default(cuid()) orderId String - amount Decimal @db.Decimal(14, 2) + amount Decimal @db.Decimal(14, 2) method PaymentMethod reference String? note String? - createdAt DateTime @default(now()) + createdAt DateTime @default(now()) order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) @@ -480,3 +488,66 @@ model ActivityLog { @@index([type]) @@index([createdAt]) } + +model Session { + id String @id + expiresAt DateTime + token String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + ipAddress String? + userAgent String? + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([token]) + @@index([userId]) + @@map("session") +} + +model Account { + id String @id + accountId String + providerId String + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + accessToken String? + refreshToken String? + idToken String? + accessTokenExpiresAt DateTime? + refreshTokenExpiresAt DateTime? + scope String? + password String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([userId]) + @@map("account") +} + +model Verification { + id String @id + identifier String + value String + expiresAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([identifier]) + @@map("verification") +} + +model TwoFactor { + id String @id + secret String + backupCodes String + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + verified Boolean? @default(true) + failedVerificationCount Int? @default(0) + lockedUntil DateTime? + + @@index([secret]) + @@index([userId]) + @@map("twoFactor") +} diff --git a/prisma/seed.ts b/prisma/seed.ts index 6827c2c..fde4c02 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,6 +1,5 @@ import { PrismaClient, type Prisma } from "@/generated/prisma/client"; import { PrismaPg } from "@prisma/adapter-pg"; -import bcrypt from "bcryptjs"; const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); const prisma = new PrismaClient({ adapter }); @@ -22,16 +21,9 @@ async function main() { await prisma.task.deleteMany(); await prisma.researchNote.deleteMany(); await prisma.hustle.deleteMany(); - await prisma.user.deleteMany(); - await prisma.user.create({ - data: { - email: "demo@example.com", - name: "Demo Owner", - passwordHash: await bcrypt.hash("demo12345", 12), - }, - }); - console.log("👤 Created owner: demo@example.com / demo12345"); + // No user is seeded: the owner account is created through the /setup flow + // (better-auth). This keeps any default/guessable credential out of the repo. const hustle = await prisma.hustle.create({ data: { @@ -630,7 +622,7 @@ async function main() { }, }); - console.log("✅ Seed complete. Run pnpm dev and sign in with demo@example.com / demo12345"); + console.log("✅ Seed complete. Run pnpm dev, then create the owner account at /setup."); } main() diff --git a/public/icon.svg b/public/icon.svg index 4a11f0d..648fee1 100644 --- a/public/icon.svg +++ b/public/icon.svg @@ -1,5 +1,5 @@ - + diff --git a/public/manifest.json b/public/manifest.json index 7f491c3..03e55cf 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -2,8 +2,8 @@ "name": "Sidekit", "short_name": "Sidekit", "description": "Operating system for solo founders managing multiple businesses.", - "theme_color": "#6366f1", - "background_color": "#0f172a", + "theme_color": "#0a0a0d", + "background_color": "#0a0a0d", "display": "standalone", "orientation": "portrait", "start_url": "/", diff --git a/src/actions/_helpers.ts b/src/actions/_helpers.ts index 9e58764..5598d8b 100644 --- a/src/actions/_helpers.ts +++ b/src/actions/_helpers.ts @@ -1,9 +1,10 @@ import { revalidatePath } from "next/cache"; -import { auth } from "@/auth"; +import { headers } from "next/headers"; +import { auth } from "@/lib/auth"; import { fail, type ActionResult } from "@/lib/result"; export async function requireUser(): Promise { - const session = await auth(); + const session = await auth.api.getSession({ headers: await headers() }); return session?.user?.id ?? null; } diff --git a/src/actions/auth-actions.ts b/src/actions/auth-actions.ts index 592574f..ce0856c 100644 --- a/src/actions/auth-actions.ts +++ b/src/actions/auth-actions.ts @@ -1,43 +1,70 @@ "use server"; -import { AuthError } from "next-auth"; -import { signIn, signOut } from "@/auth"; -import { authService } from "@/services/auth-service"; -import { fail, ok, type ActionResult } from "@/lib/result"; +import { headers } from "next/headers"; +import { redirect } from "next/navigation"; +import { auth } from "@/lib/auth"; +import { ok, fail, type ActionResult } from "@/lib/result"; import { logError, toErrorMessage } from "@/lib/errors"; import type { ChangePasswordInput, LoginInput, SetupInput } from "@/schemas/auth"; import { requireUser } from "./_helpers"; +// Owner setup (first user only — enforced by the databaseHooks in lib/auth.ts). +// signUpEmail also signs the new user in, so no separate login call is needed. export async function setupOwnerAction(input: SetupInput): Promise> { - return authService.setupOwner(input); + try { + const res = await auth.api.signUpEmail({ + body: { email: input.email.toLowerCase(), password: input.password, name: input.name }, + headers: await headers(), + }); + return ok({ id: res.user.id }); + } catch (err) { + logError("auth-actions.setupOwnerAction", err); + return fail(toErrorMessage(err)); + } } -export async function loginAction(input: LoginInput): Promise> { +// Password sign-in. If the account has 2FA on, better-auth returns a +// twoFactorRedirect instead of a session; the form then routes to /login/2fa. +export async function loginAction(input: LoginInput): Promise> { try { - await signIn("credentials", { - email: input.email, - password: input.password, - redirect: false, + const res = await auth.api.signInEmail({ + body: { email: input.email.toLowerCase(), password: input.password }, + headers: await headers(), }); - return ok(null); - } catch (err) { - if (err instanceof AuthError) { - if (err.type === "CredentialsSignin") return fail("Invalid email or password."); - return fail("Unable to sign in."); + if ((res as { twoFactorRedirect?: boolean })?.twoFactorRedirect) { + return ok({ totpRequired: true }); } + return ok({}); + } catch (err) { logError("auth-actions.loginAction", err); - return fail(toErrorMessage(err)); + return fail("Invalid email or password."); } } export async function logoutAction(): Promise { - await signOut({ redirectTo: "/login" }); + await auth.api.signOut({ headers: await headers() }); + redirect("/login"); } +// Requires the current password (better-auth enforces it) and revokes other +// sessions so a changed password logs out everywhere. export async function changePasswordAction( input: ChangePasswordInput, ): Promise> { const userId = await requireUser(); if (!userId) return fail("Not authenticated."); - return authService.changePassword(userId, input); + try { + await auth.api.changePassword({ + body: { + currentPassword: input.currentPassword, + newPassword: input.newPassword, + revokeOtherSessions: true, + }, + headers: await headers(), + }); + return ok({ id: userId }); + } catch (err) { + logError("auth-actions.changePasswordAction", err); + return fail("Current password is incorrect."); + } } diff --git a/src/app/(app)/account/ai-settings-card.tsx b/src/app/(app)/account/ai-settings-card.tsx index 4ff3965..9647412 100644 --- a/src/app/(app)/account/ai-settings-card.tsx +++ b/src/app/(app)/account/ai-settings-card.tsx @@ -125,7 +125,7 @@ export function AiSettingsCard({ provider, model, baseUrl, hasKey }: Props) { {revealKey ? : } - {errors.apiKey ?

{errors.apiKey.message}

: null} + {errors.apiKey ?

{errors.apiKey.message}

: null}
@@ -138,7 +138,7 @@ export function AiSettingsCard({ provider, model, baseUrl, hasKey }: Props) { {errors.baseUrl ? ( -

{errors.baseUrl.message}

+

{errors.baseUrl.message}

) : null}
) : null} diff --git a/src/app/(app)/account/change-password-form.tsx b/src/app/(app)/account/change-password-form.tsx index e2f36d6..a54da53 100644 --- a/src/app/(app)/account/change-password-form.tsx +++ b/src/app/(app)/account/change-password-form.tsx @@ -50,7 +50,7 @@ export function ChangePasswordForm() { {...register("currentPassword")} /> {errors.currentPassword ? ( -

{errors.currentPassword.message}

+

{errors.currentPassword.message}

) : null}
@@ -63,7 +63,7 @@ export function ChangePasswordForm() { {...register("newPassword")} /> {errors.newPassword ? ( -

{errors.newPassword.message}

+

{errors.newPassword.message}

) : null}
@@ -75,7 +75,7 @@ export function ChangePasswordForm() { {...register("confirmPassword")} /> {errors.confirmPassword ? ( -

{errors.confirmPassword.message}

+

{errors.confirmPassword.message}

) : null}
diff --git a/src/app/(app)/account/page.tsx b/src/app/(app)/account/page.tsx index 835f6cb..bdab54c 100644 --- a/src/app/(app)/account/page.tsx +++ b/src/app/(app)/account/page.tsx @@ -1,22 +1,24 @@ import { redirect } from "next/navigation"; +import { headers } from "next/headers"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { PageHeader } from "@/components/shared/page-header"; -import { auth } from "@/auth"; +import { auth } from "@/lib/auth"; import { authService } from "@/services/auth-service"; import { ChangePasswordForm } from "./change-password-form"; import { AiSettingsCard } from "./ai-settings-card"; +import { TotpCard } from "./totp-card"; import { formatDate } from "@/lib/format"; export const dynamic = "force-dynamic"; export default async function AccountPage() { - const session = await auth(); + const session = await auth.api.getSession({ headers: await headers() }); if (!session?.user?.id) redirect("/login"); const user = await authService.getCurrentUser(session.user.id); if (!user) redirect("/login"); return ( -
+
@@ -39,6 +41,8 @@ export default async function AccountPage() { + + (initialEnabled ? "on" : "off"); + const [password, setPassword] = useState(""); + const [secret, setSecret] = useState(""); + const [code, setCode] = useState(""); + const [backupCodes, setBackupCodes] = useState([]); + const [pending, setPending] = useState(false); + + async function begin() { + setPending(true); + const { data, error } = await authClient.twoFactor.enable({ password }); + setPending(false); + if (error || !data) { + toast.error(error?.message ?? "Failed to start. Check your password."); + return; + } + setPassword(""); + try { + setSecret(new URL(data.totpURI).searchParams.get("secret") ?? ""); + } catch { + setSecret(""); + } + setBackupCodes(data.backupCodes ?? []); + setCode(""); + setStage("enrolling"); + } + + async function confirm() { + setPending(true); + const { error } = await authClient.twoFactor.verifyTotp({ code: code.trim() }); + setPending(false); + if (error) { + toast.error("Invalid code — try again."); + return; + } + setStage("backup"); + toast.success("Two-factor authentication enabled."); + } + + async function disable() { + setPending(true); + const { error } = await authClient.twoFactor.disable({ password }); + setPending(false); + if (error) { + toast.error(error.message ?? "Wrong password."); + return; + } + setPassword(""); + setStage("off"); + toast.success("Two-factor authentication disabled."); + } + + return ( + + + Two-factor authentication + + Require a time-based code from an authenticator app at sign-in. + + + + {stage === "off" && ( +
+
+ + setPassword(e.target.value)} /> +
+ +
+ )} + + {stage === "enrolling" && ( +
+

+ Add this key to Google Authenticator, Aegis, or 1Password, then enter the code. +

+

+ Manual key: {secret} +

+
+ + setCode(e.target.value)} /> +
+
+ + +
+
+ )} + + {stage === "backup" && ( +
+

Save your backup codes

+

+ Each works once if you lose your authenticator. They won’t be shown again. +

+
+ {backupCodes.map((c) => ( + {c} + ))} +
+ +
+ )} + + {stage === "on" && ( +
+

2FA is enabled.

+
+ + setPassword(e.target.value)} /> +
+ +
+ )} +
+
+ ); +} diff --git a/src/app/(app)/financials/page.tsx b/src/app/(app)/financials/page.tsx index e956450..ec32df4 100644 --- a/src/app/(app)/financials/page.tsx +++ b/src/app/(app)/financials/page.tsx @@ -71,7 +71,7 @@ export default async function ConsolidatedFinancialsPage({ const allTransactions = await transactionService.list({ from, to }); return ( -
+
{parseError ? ( -

{parseError}

+

{parseError}

) : parsed ? ( -

{parsed.length} item{parsed.length === 1 ? "" : "s"} ready to import.

+

{parsed.length} item{parsed.length === 1 ? "" : "s"} ready to import.

) : null}
diff --git a/src/app/(app)/hustles/[id]/cost-sheet/cost-item-form-button.tsx b/src/app/(app)/hustles/[id]/cost-sheet/cost-item-form-button.tsx index 6e054ae..a556689 100644 --- a/src/app/(app)/hustles/[id]/cost-sheet/cost-item-form-button.tsx +++ b/src/app/(app)/hustles/[id]/cost-sheet/cost-item-form-button.tsx @@ -132,7 +132,7 @@ export function CostItemFormButton({ hustleId, suppliers, products, item, trigge
- {errors.name ?

{errors.name.message}

: null} + {errors.name ?

{errors.name.message}

: null}
@@ -230,7 +230,7 @@ export function CostItemFormButton({ hustleId, suppliers, products, item, trigge
- {errors.url ?

{errors.url.message}

: null} + {errors.url ?

{errors.url.message}

: null}
} @@ -364,7 +364,7 @@ export function CostSheetBoard({ hustleId, currency, items, suppliers, products onConfirm={() => handleDelete(i.id)} trigger={ e.preventDefault()} > Delete @@ -457,7 +457,7 @@ export function CostSheetBoard({ hustleId, currency, items, suppliers, products = 30 ? "text-emerald-300" : grossMargin >= 10 ? "text-amber-300" : "text-rose-300"}`}> + = 30 ? "text-success" : grossMargin >= 10 ? "text-warning" : "text-destructive"}`}> {percent(grossMargin)} } diff --git a/src/app/(app)/hustles/[id]/cost-sheet/page.tsx b/src/app/(app)/hustles/[id]/cost-sheet/page.tsx index 2e06339..1f58830 100644 --- a/src/app/(app)/hustles/[id]/cost-sheet/page.tsx +++ b/src/app/(app)/hustles/[id]/cost-sheet/page.tsx @@ -32,7 +32,7 @@ export default async function CostSheetPage({ params }: { params: Promise<{ id: })); return ( -
+
- {errors.name ?

{errors.name.message}

: null} + {errors.name ?

{errors.name.message}

: null}
@@ -139,7 +139,7 @@ export function ProductFormButton({ hustleId, item, trigger }: Props) { {errors.coverImageUrl ? ( -

{errors.coverImageUrl.message}

+

{errors.coverImageUrl.message}

) : null}
diff --git a/src/app/(app)/hustles/[id]/customers/[customerId]/page.tsx b/src/app/(app)/hustles/[id]/customers/[customerId]/page.tsx index c267b0e..ea07e2c 100644 --- a/src/app/(app)/hustles/[id]/customers/[customerId]/page.tsx +++ b/src/app/(app)/hustles/[id]/customers/[customerId]/page.tsx @@ -34,7 +34,7 @@ export default async function CustomerDetailPage({ const totalSpent = customer.orders.reduce((s, o) => s + toNumber(o.total), 0); return ( -
+
- {errors.name ?

{errors.name.message}

: null} + {errors.name ?

{errors.name.message}

: null}
@@ -114,7 +114,7 @@ export function CustomerFormButton({ hustleId, customer, trigger }: Props) {
- {errors.email ?

{errors.email.message}

: null} + {errors.email ?

{errors.email.message}

: null}
diff --git a/src/app/(app)/hustles/[id]/customers/customers-table.tsx b/src/app/(app)/hustles/[id]/customers/customers-table.tsx index be3afd9..496a497 100644 --- a/src/app/(app)/hustles/[id]/customers/customers-table.tsx +++ b/src/app/(app)/hustles/[id]/customers/customers-table.tsx @@ -96,7 +96,7 @@ export function CustomersTable({ hustleId, customers }: Props) { onConfirm={() => handleDelete(c.id)} trigger={ e.preventDefault()} > Delete diff --git a/src/app/(app)/hustles/[id]/customers/page.tsx b/src/app/(app)/hustles/[id]/customers/page.tsx index 87d07d2..4bcd433 100644 --- a/src/app/(app)/hustles/[id]/customers/page.tsx +++ b/src/app/(app)/hustles/[id]/customers/page.tsx @@ -11,7 +11,7 @@ export default async function CustomersPage({ params }: { params: Promise<{ id: const customers = await customerService.list(id); return ( -
+
@@ -296,7 +296,7 @@ export function FinancialsBoard({ onConfirm={() => handleDelete(t.id)} trigger={ e.preventDefault()} > Delete diff --git a/src/app/(app)/hustles/[id]/financials/page.tsx b/src/app/(app)/hustles/[id]/financials/page.tsx index 2f075b1..240515e 100644 --- a/src/app/(app)/hustles/[id]/financials/page.tsx +++ b/src/app/(app)/hustles/[id]/financials/page.tsx @@ -44,7 +44,7 @@ export default async function FinancialsPage({ ); return ( -
+
))} - {errors.category ?

{errors.category.message}

: null} + {errors.category ?

{errors.category.message}

: null}
- {errors.amount ?

{errors.amount.message}

: null} + {errors.amount ?

{errors.amount.message}

: null}
diff --git a/src/app/(app)/hustles/[id]/inventory/inventory-adjust-button.tsx b/src/app/(app)/hustles/[id]/inventory/inventory-adjust-button.tsx index a0b366b..fe1cb29 100644 --- a/src/app/(app)/hustles/[id]/inventory/inventory-adjust-button.tsx +++ b/src/app/(app)/hustles/[id]/inventory/inventory-adjust-button.tsx @@ -68,12 +68,12 @@ export function InventoryAdjustButton({ hustleId, itemId, trigger }: Props) {
- {errors.delta ?

{errors.delta.message}

: null} + {errors.delta ?

{errors.delta.message}

: null}
- {errors.reason ?

{errors.reason.message}

: null} + {errors.reason ?

{errors.reason.message}

: null}
diff --git a/src/app/(app)/hustles/[id]/inventory/inventory-table.tsx b/src/app/(app)/hustles/[id]/inventory/inventory-table.tsx index d1006a7..b29f971 100644 --- a/src/app/(app)/hustles/[id]/inventory/inventory-table.tsx +++ b/src/app/(app)/hustles/[id]/inventory/inventory-table.tsx @@ -200,7 +200,7 @@ export function InventoryTable({ hustleId, currency, items }: Props) { onConfirm={() => handleDelete(i.id)} trigger={ e.preventDefault()} > Delete diff --git a/src/app/(app)/hustles/[id]/inventory/page.tsx b/src/app/(app)/hustles/[id]/inventory/page.tsx index 8c99f14..ec708f4 100644 --- a/src/app/(app)/hustles/[id]/inventory/page.tsx +++ b/src/app/(app)/hustles/[id]/inventory/page.tsx @@ -19,7 +19,7 @@ export default async function InventoryPage({ params }: { params: Promise<{ id: ]); return ( -
+
))}
-
+
diff --git a/src/app/(app)/hustles/[id]/orders/[orderId]/order-payment-dialog.tsx b/src/app/(app)/hustles/[id]/orders/[orderId]/order-payment-dialog.tsx index 4c0bdc6..94f27cd 100644 --- a/src/app/(app)/hustles/[id]/orders/[orderId]/order-payment-dialog.tsx +++ b/src/app/(app)/hustles/[id]/orders/[orderId]/order-payment-dialog.tsx @@ -82,7 +82,7 @@ export function OrderPaymentDialog({ hustleId, orderId }: Props) {
- {errors.amount ?

{errors.amount.message}

: null} + {errors.amount ?

{errors.amount.message}

: null}
diff --git a/src/app/(app)/hustles/[id]/orders/[orderId]/page.tsx b/src/app/(app)/hustles/[id]/orders/[orderId]/page.tsx index d11c836..e8f9b3f 100644 --- a/src/app/(app)/hustles/[id]/orders/[orderId]/page.tsx +++ b/src/app/(app)/hustles/[id]/orders/[orderId]/page.tsx @@ -68,7 +68,7 @@ export default async function OrderDetailPage({ const outstanding = Math.max(0, toNumber(order.total) - toNumber(order.amountPaid)); return ( -
+
Customer name (display) {errors.customerName ? ( -

{errors.customerName.message}

+

{errors.customerName.message}

) : null}
@@ -195,7 +195,7 @@ export function OrderForm({ hustleId, customers, order, onDone }: Props) { type="button" variant="ghost" size="icon" - className="text-rose-300" + className="text-destructive" onClick={() => itemsArray.remove(i)} disabled={itemsArray.fields.length <= 1} > @@ -204,7 +204,7 @@ export function OrderForm({ hustleId, customers, order, onDone }: Props) {
))}
- {errors.items ?

{errors.items.message}

: null} + {errors.items ?

{errors.items.message}

: null}
@@ -294,7 +294,7 @@ export function OrderForm({ hustleId, customers, order, onDone }: Props) { type="button" variant="ghost" size="icon" - className="text-rose-300" + className="text-destructive" onClick={() => customsArray.remove(i)} > diff --git a/src/app/(app)/hustles/[id]/orders/page.tsx b/src/app/(app)/hustles/[id]/orders/page.tsx index 177b0f2..9cee26c 100644 --- a/src/app/(app)/hustles/[id]/orders/page.tsx +++ b/src/app/(app)/hustles/[id]/orders/page.tsx @@ -30,7 +30,7 @@ export default async function OrdersPage({ params }: { params: Promise<{ id: str })); return ( -
+
+
@@ -130,8 +130,8 @@ export default async function HustleOverviewPage({ data={monthly} currency={hustle.currency} series={[ - { key: "income", label: "Income", color: "#10b981" }, - { key: "expenses", label: "Expenses", color: "#ef4444" }, + { key: "income", label: "Income", color: "var(--color-success)" }, + { key: "expenses", label: "Expenses", color: "var(--color-destructive)" }, ]} /> @@ -208,7 +208,7 @@ export default async function HustleOverviewPage({ {upcomingTasks.map((t) => (
  • {t.status === "DONE" ? ( - + ) : ( )} @@ -229,9 +229,9 @@ export default async function HustleOverviewPage({
  • {lowStock.length > 0 ? ( - + - + Low stock alerts {lowStock.length} @@ -243,7 +243,7 @@ export default async function HustleOverviewPage({ className="flex items-center justify-between rounded-md border border-border px-3 py-2 text-sm" > {i.name} - + {i.quantity.toString()} {i.unit} diff --git a/src/app/(app)/hustles/[id]/products/page.tsx b/src/app/(app)/hustles/[id]/products/page.tsx index 0367a73..f428ed7 100644 --- a/src/app/(app)/hustles/[id]/products/page.tsx +++ b/src/app/(app)/hustles/[id]/products/page.tsx @@ -25,7 +25,7 @@ export default async function ProductsPage({ params }: { params: Promise<{ id: s })); return ( -
    +
    )} {previewError && ( -

    Image couldn't be previewed - some sites block embedding. It may still display when saved.

    +

    Image couldn't be previewed - some sites block embedding. It may still display when saved.

    )}
    diff --git a/src/app/(app)/hustles/[id]/research/page.tsx b/src/app/(app)/hustles/[id]/research/page.tsx index 253c586..3ae281b 100644 --- a/src/app/(app)/hustles/[id]/research/page.tsx +++ b/src/app/(app)/hustles/[id]/research/page.tsx @@ -20,7 +20,7 @@ export default async function ResearchPage({ params }: { params: Promise<{ id: s })); return ( -
    +
    diff --git a/src/app/(app)/hustles/[id]/research/research-board.tsx b/src/app/(app)/hustles/[id]/research/research-board.tsx index 98727a0..a3f1929 100644 --- a/src/app/(app)/hustles/[id]/research/research-board.tsx +++ b/src/app/(app)/hustles/[id]/research/research-board.tsx @@ -186,7 +186,7 @@ export function ResearchBoard({ hustleId, notes }: Props) {