diff --git a/.env.sample b/.env.sample index ed43d10..efbb162 100644 --- a/.env.sample +++ b/.env.sample @@ -15,6 +15,11 @@ DATABASE_URL="postgresql://postgres:password@localhost:5432/twelvecash" # Secret value for signing JWTs JWT_SECRET="abc123" +# Better Auth secret (min 32 chars, high entropy) +# Generate with: openssl rand -base64 32 +BETTER_AUTH_SECRET="your_secret_here_at_least_32_chars" +BETTER_AUTH_URL="http://localhost:3000" + # App URL (for MDK success/cancel redirects) # In production, set this to your domain (e.g., https://twelve.cash) NEXT_PUBLIC_APP_URL="http://localhost:3000" diff --git a/package.json b/package.json index 706881c..12a5cca 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@trpc/server": "^11.0.0-rc.446", "axios": "^1.6.2", "bech32": "^2.0.0", + "better-auth": "^1.4.18", "cookie": "^0.6.0", "jsonwebtoken": "^9.0.2", "next": "^15.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a1e5788..5290a23 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 0.1.84 '@moneydevkit/nextjs': specifier: ^0.18.0 - version: 0.18.0(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(next@15.5.12(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 0.18.0(@opentelemetry/api@1.9.1)(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@prisma/client': specifier: ^5.17.0 version: 5.22.0(prisma@5.22.0) @@ -41,6 +41,9 @@ importers: bech32: specifier: ^2.0.0 version: 2.0.0 + better-auth: + specifier: ^1.4.18 + version: 1.6.5(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0(prisma@5.22.0))(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(prisma@5.22.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) cookie: specifier: ^0.6.0 version: 0.6.0 @@ -49,7 +52,7 @@ importers: version: 9.0.3 next: specifier: ^15.1.0 - version: 15.5.12(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) nostr-tools: specifier: ^2.7.1 version: 2.23.0(typescript@5.9.3) @@ -318,6 +321,83 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@better-auth/core@1.6.5': + resolution: {integrity: sha512-T3u4rVsJcMWShG2qfQUlU1HdkQGLYX0+lcR48QV2Cp2kpBOLOTYdt+p6zZtGm2Omx/ReEouRQyKy7pYtahRQuA==} + peerDependencies: + '@better-auth/utils': 0.4.0 + '@better-fetch/fetch': 1.1.21 + '@cloudflare/workers-types': '>=4' + '@opentelemetry/api': ^1.9.0 + better-call: 1.3.5 + jose: ^6.1.0 + kysely: ^0.28.5 + nanostores: ^1.0.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + '@better-auth/drizzle-adapter@1.6.5': + resolution: {integrity: sha512-9YjPW35+h66D+QA+YqEJ9pFP97ClLFR+QrTPZojkeP0PTYqpW0ErBK3p1pwRTJG88yK+o3Y4yOwoacMTBxz0jQ==} + peerDependencies: + '@better-auth/core': ^1.6.5 + '@better-auth/utils': 0.4.0 + drizzle-orm: ^0.45.2 + peerDependenciesMeta: + drizzle-orm: + optional: true + + '@better-auth/kysely-adapter@1.6.5': + resolution: {integrity: sha512-kbevd70qzKNR3ZHF7q6/e0XXYRCXanLB2rvmTd3T8WbNEd9kYMqKjgTGNxL1ri5N+PEDUK6zfHx/HrvaEOfoHw==} + peerDependencies: + '@better-auth/core': ^1.6.5 + '@better-auth/utils': 0.4.0 + kysely: ^0.28.14 + peerDependenciesMeta: + kysely: + optional: true + + '@better-auth/memory-adapter@1.6.5': + resolution: {integrity: sha512-5qFUpSdQi+RwHSmNyHMSsJIrFjed8d/ASS61L2xyW7sjBLTIuR7JcgS6hif5cQbtPeq+Qz+Wct5q8oKw33qyqQ==} + peerDependencies: + '@better-auth/core': ^1.6.5 + '@better-auth/utils': 0.4.0 + + '@better-auth/mongo-adapter@1.6.5': + resolution: {integrity: sha512-HvOUFTiSEFSGTzL/vE3FntTwQiZ79O/V+QcsCimR+65Bj3tOqdFaC1G2Yd1dQ9l2YHNXA9SNBrGekbk66RzJMw==} + peerDependencies: + '@better-auth/core': ^1.6.5 + '@better-auth/utils': 0.4.0 + mongodb: ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + mongodb: + optional: true + + '@better-auth/prisma-adapter@1.6.5': + resolution: {integrity: sha512-d7PUO5XoimYYDEG/DoYVbOSbyVYJBDuZgvY9pjf8INccBTCD1BzcyEJ9NQil4huXWj4fcNaGOt2FG0OI8NtWOA==} + peerDependencies: + '@better-auth/core': ^1.6.5 + '@better-auth/utils': 0.4.0 + '@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.5': + resolution: {integrity: sha512-Ag3CjAP+tLretKPq+pYdU/gU4pFIcey/AoNQzw671wV5JQZXrMitS65INi8j8QuYfol2xgQrht5KVlcxGrkhHQ==} + peerDependencies: + '@better-auth/core': ^1.6.5 + '@better-auth/utils': 0.4.0 + '@better-fetch/fetch': 1.1.21 + + '@better-auth/utils@0.4.0': + resolution: {integrity: sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA==} + + '@better-fetch/fetch@1.1.21': + resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} + '@bitcoin-design/bitcoin-icons-react@0.1.10': resolution: {integrity: sha512-f7GSutKHa4EK4LWI/phnGCJsN8fzFbVAVQ4F1MYxiza34LVmXmbgHUmdP/BR8ZeQSIbZLt19inpJZDBtQvYe4Q==} peerDependencies: @@ -788,6 +868,14 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/semantic-conventions@1.40.0': + resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} + engines: {node: '>=14'} + '@orpc/client@1.13.6': resolution: {integrity: sha512-M6lYM6fJUFp9GR+It/qglYTeXwspb6sGj46xXWHqHS6iDVquqju0bdYuLOfHx8CGJcUSzi0aKUcqMXiGJhBG3w==} @@ -1726,6 +1814,76 @@ packages: bech32@2.0.0: resolution: {integrity: sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==} + better-auth@1.6.5: + resolution: {integrity: sha512-rSt8JtJOJK0MqPShXINCmM6DV30GsDvnCTlIxQIzP9OpUx/umA40nUc4ALZHQyqAPbw1ib/a549kIWw/WyxxKA==} + 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.5: + resolution: {integrity: sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1959,6 +2117,9 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -2796,6 +2957,9 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + jose@6.2.2: + resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + js-cookie@2.2.1: resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} @@ -2866,6 +3030,10 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + kysely@0.28.16: + resolution: {integrity: sha512-3i5pmOiZvMDj00qhrIVbH0AnioVTx22DMP7Vn5At4yJO46iy+FM8Y/g61ltenLVSo3fiO8h8Q3QOFgf/gQ72ww==} + engines: {node: '>=20.0.0'} + language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -3011,6 +3179,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanostores@1.3.0: + resolution: {integrity: sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA==} + 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} @@ -3442,6 +3614,9 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true + rou3@0.7.12: + resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} + rtl-css-js@1.16.1: resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} @@ -3486,6 +3661,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-cookie-parser@3.1.0: + resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -3997,6 +4175,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + snapshots: '@adobe/css-tools@4.4.4': {} @@ -4196,6 +4377,61 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@better-auth/core@1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0)': + dependencies: + '@better-auth/utils': 0.4.0 + '@better-fetch/fetch': 1.1.21 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.40.0 + '@standard-schema/spec': 1.1.0 + better-call: 1.3.5(zod@4.3.6) + jose: 6.2.2 + kysely: 0.28.16 + nanostores: 1.3.0 + zod: 4.3.6 + + '@better-auth/drizzle-adapter@1.6.5(@better-auth/core@1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)': + dependencies: + '@better-auth/core': 1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0) + '@better-auth/utils': 0.4.0 + + '@better-auth/kysely-adapter@1.6.5(@better-auth/core@1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(kysely@0.28.16)': + dependencies: + '@better-auth/core': 1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0) + '@better-auth/utils': 0.4.0 + optionalDependencies: + kysely: 0.28.16 + + '@better-auth/memory-adapter@1.6.5(@better-auth/core@1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)': + dependencies: + '@better-auth/core': 1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0) + '@better-auth/utils': 0.4.0 + + '@better-auth/mongo-adapter@1.6.5(@better-auth/core@1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)': + dependencies: + '@better-auth/core': 1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0) + '@better-auth/utils': 0.4.0 + + '@better-auth/prisma-adapter@1.6.5(@better-auth/core@1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@prisma/client@5.22.0(prisma@5.22.0))(prisma@5.22.0)': + dependencies: + '@better-auth/core': 1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0) + '@better-auth/utils': 0.4.0 + optionalDependencies: + '@prisma/client': 5.22.0(prisma@5.22.0) + prisma: 5.22.0 + + '@better-auth/telemetry@1.6.5(@better-auth/core@1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)': + dependencies: + '@better-auth/core': 1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0) + '@better-auth/utils': 0.4.0 + '@better-fetch/fetch': 1.1.21 + + '@better-auth/utils@0.4.0': + dependencies: + '@noble/hashes': 2.0.1 + + '@better-fetch/fetch@1.1.21': {} + '@bitcoin-design/bitcoin-icons-react@0.1.10(react@19.2.4)': dependencies: react: 19.2.4 @@ -4579,12 +4815,12 @@ snapshots: '@orpc/contract': 1.3.0 zod: 3.25.76 - '@moneydevkit/core@0.18.0(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@moneydevkit/core@0.18.0(@opentelemetry/api@1.9.1)(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@hookform/resolvers': 5.4.0(react-hook-form@7.71.1(react@19.2.4)) '@moneydevkit/api-contract': 0.1.33 '@moneydevkit/lightning-js': 0.1.84 - '@orpc/client': 1.13.6 + '@orpc/client': 1.13.6(@opentelemetry/api@1.9.1) '@orpc/contract': 1.3.0 '@orpc/server': 1.3.0(ws@8.21.0) '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -4655,13 +4891,13 @@ snapshots: '@moneydevkit/lightning-js-win32-ia32-msvc': 0.1.84 '@moneydevkit/lightning-js-win32-x64-msvc': 0.1.84 - '@moneydevkit/nextjs@0.18.0(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(next@15.5.12(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@moneydevkit/nextjs@0.18.0(@opentelemetry/api@1.9.1)(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@hookform/resolvers': 5.4.0(react-hook-form@7.71.1(react@19.2.4)) '@moneydevkit/api-contract': 0.1.33 - '@moneydevkit/core': 0.18.0(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@moneydevkit/core': 0.18.0(@opentelemetry/api@1.9.1)(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@moneydevkit/lightning-js': 0.1.84 - '@orpc/client': 1.13.6 + '@orpc/client': 1.13.6(@opentelemetry/api@1.9.1) '@orpc/contract': 1.3.0 '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -4672,7 +4908,7 @@ snapshots: class-variance-authority: 0.7.1 clsx: 2.1.1 lucide-react: 0.511.0(react@19.2.4) - next: 15.5.12(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) qrcode.react: 4.2.0(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -4747,12 +4983,16 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} - '@orpc/client@1.13.6': + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/semantic-conventions@1.40.0': {} + + '@orpc/client@1.13.6(@opentelemetry/api@1.9.1)': dependencies: - '@orpc/shared': 1.13.6 - '@orpc/standard-server': 1.13.6 - '@orpc/standard-server-fetch': 1.13.6 - '@orpc/standard-server-peer': 1.13.6 + '@orpc/shared': 1.13.6(@opentelemetry/api@1.9.1) + '@orpc/standard-server': 1.13.6(@opentelemetry/api@1.9.1) + '@orpc/standard-server-fetch': 1.13.6(@opentelemetry/api@1.9.1) + '@orpc/standard-server-peer': 1.13.6(@opentelemetry/api@1.9.1) transitivePeerDependencies: - '@opentelemetry/api' @@ -4781,20 +5021,22 @@ snapshots: optionalDependencies: ws: 8.21.0 - '@orpc/shared@1.13.6': + '@orpc/shared@1.13.6(@opentelemetry/api@1.9.1)': dependencies: radash: 12.1.1 type-fest: 5.6.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 '@orpc/shared@1.3.0': dependencies: radash: 12.1.1 type-fest: 4.41.0 - '@orpc/standard-server-fetch@1.13.6': + '@orpc/standard-server-fetch@1.13.6(@opentelemetry/api@1.9.1)': dependencies: - '@orpc/shared': 1.13.6 - '@orpc/standard-server': 1.13.6 + '@orpc/shared': 1.13.6(@opentelemetry/api@1.9.1) + '@orpc/standard-server': 1.13.6(@opentelemetry/api@1.9.1) transitivePeerDependencies: - '@opentelemetry/api' @@ -4808,10 +5050,10 @@ snapshots: '@orpc/shared': 1.3.0 '@orpc/standard-server': 1.3.0 - '@orpc/standard-server-peer@1.13.6': + '@orpc/standard-server-peer@1.13.6(@opentelemetry/api@1.9.1)': dependencies: - '@orpc/shared': 1.13.6 - '@orpc/standard-server': 1.13.6 + '@orpc/shared': 1.13.6(@opentelemetry/api@1.9.1) + '@orpc/standard-server': 1.13.6(@opentelemetry/api@1.9.1) transitivePeerDependencies: - '@opentelemetry/api' @@ -4820,9 +5062,9 @@ snapshots: '@orpc/shared': 1.3.0 '@orpc/standard-server': 1.3.0 - '@orpc/standard-server@1.13.6': + '@orpc/standard-server@1.13.6(@opentelemetry/api@1.9.1)': dependencies: - '@orpc/shared': 1.13.6 + '@orpc/shared': 1.13.6(@opentelemetry/api@1.9.1) transitivePeerDependencies: - '@opentelemetry/api' @@ -5746,6 +5988,44 @@ snapshots: bech32@2.0.0: {} + better-auth@1.6.5(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0(prisma@5.22.0))(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(prisma@5.22.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@better-auth/core': 1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0) + '@better-auth/drizzle-adapter': 1.6.5(@better-auth/core@1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0) + '@better-auth/kysely-adapter': 1.6.5(@better-auth/core@1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(kysely@0.28.16) + '@better-auth/memory-adapter': 1.6.5(@better-auth/core@1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0) + '@better-auth/mongo-adapter': 1.6.5(@better-auth/core@1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0) + '@better-auth/prisma-adapter': 1.6.5(@better-auth/core@1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@prisma/client@5.22.0(prisma@5.22.0))(prisma@5.22.0) + '@better-auth/telemetry': 1.6.5(@better-auth/core@1.6.5(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@3.25.76))(jose@6.2.2)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21) + '@better-auth/utils': 0.4.0 + '@better-fetch/fetch': 1.1.21 + '@noble/ciphers': 2.1.1 + '@noble/hashes': 2.0.1 + better-call: 1.3.5(zod@4.3.6) + defu: 6.1.7 + jose: 6.2.2 + kysely: 0.28.16 + nanostores: 1.3.0 + zod: 4.3.6 + optionalDependencies: + '@prisma/client': 5.22.0(prisma@5.22.0) + next: 15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + prisma: 5.22.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + transitivePeerDependencies: + - '@cloudflare/workers-types' + - '@opentelemetry/api' + + better-call@1.3.5(zod@4.3.6): + dependencies: + '@better-auth/utils': 0.4.0 + '@better-fetch/fetch': 1.1.21 + rou3: 0.7.12 + set-cookie-parser: 3.1.0 + optionalDependencies: + zod: 4.3.6 + binary-extensions@2.3.0: {} brace-expansion@1.1.12: @@ -5971,6 +6251,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + defu@6.1.7: {} + delayed-stream@1.0.0: {} dequal@2.0.3: {} @@ -7150,6 +7432,8 @@ snapshots: jiti@1.21.7: {} + jose@6.2.2: {} + js-cookie@2.2.1: {} js-tokens@4.0.0: {} @@ -7249,6 +7533,8 @@ snapshots: kleur@3.0.3: {} + kysely@0.28.16: {} + language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -7374,11 +7660,13 @@ snapshots: nanoid@3.3.11: {} + nanostores@1.3.0: {} + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} - next@15.5.12(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 15.5.12 '@swc/helpers': 0.5.15 @@ -7396,6 +7684,7 @@ snapshots: '@next/swc-linux-x64-musl': 15.5.12 '@next/swc-win32-arm64-msvc': 15.5.12 '@next/swc-win32-x64-msvc': 15.5.12 + '@opentelemetry/api': 1.9.1 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -7793,6 +8082,8 @@ snapshots: dependencies: glob: 7.2.3 + rou3@0.7.12: {} + rtl-css-js@1.16.1: dependencies: '@babel/runtime': 7.28.6 @@ -7836,6 +8127,8 @@ snapshots: semver@7.7.4: {} + set-cookie-parser@3.1.0: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -8447,3 +8740,5 @@ snapshots: yocto-queue@0.1.0: {} zod@3.25.76: {} + + zod@4.3.6: {} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e6a02e1..088a075 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,6 +20,60 @@ model User { apiKey String @default(uuid()) payCode PayCode[] Invoice Invoice[] + // Better Auth fields + name String? + email String? @unique + emailVerified Boolean @default(false) + image String? + sessions Session[] + accounts Account[] +} + +// Better Auth: Session model +model Session { + id String @id @default(uuid()) + expiresAt DateTime + token String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + ipAddress String? + userAgent String? + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("session") +} + +// Better Auth: Account model (for OAuth/email providers) +model Account { + id String @id @default(uuid()) + 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 + + @@map("account") +} + +// Better Auth: Verification tokens (email verification, password reset) +model Verification { + id String @id @default(uuid()) + identifier String + value String + expiresAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("verification") } model UserAuth { diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts new file mode 100644 index 0000000..5b67b06 --- /dev/null +++ b/src/app/api/auth/[...all]/route.ts @@ -0,0 +1,4 @@ +import { auth } from "@/lib/auth"; +import { toNextJsHandler } from "better-auth/next-js"; + +export const { GET, POST } = toNextJsHandler(auth); diff --git a/src/app/auth/email/page.tsx b/src/app/auth/email/page.tsx new file mode 100644 index 0000000..5f72fda --- /dev/null +++ b/src/app/auth/email/page.tsx @@ -0,0 +1,99 @@ +"use client"; + +import Link from "next/link"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { signIn } from "@/lib/auth-client"; +import Button from "@/app/components/Button"; + +export default function EmailLogin() { + const router = useRouter(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + setLoading(true); + + try { + const result = await signIn.email({ + email, + password, + }); + + if (result.error) { + setError(result.error.message || "Failed to sign in"); + setLoading(false); + return; + } + + router.push("/account"); + router.refresh(); + } catch (err) { + setError("An unexpected error occurred"); + setLoading(false); + } + }; + + return ( +
+

Sign in with Email

+ +
+
+ + setEmail(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="you@example.com" + required + /> +
+ +
+ + setPassword(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="••••••••" + required + /> +
+ + {error && ( +

{error}

+ )} + + +
+ +

+ Don't have an account?{" "} + + Sign up + +

+ +

+ + ← Back to all sign in options + +

+
+ ); +} diff --git a/src/app/auth/page.tsx b/src/app/auth/page.tsx index 113df3b..109df87 100644 --- a/src/app/auth/page.tsx +++ b/src/app/auth/page.tsx @@ -1,3 +1,4 @@ +import Link from "next/link"; import Button from "../components/Button"; import getUserServer from "../components/getUserServer"; @@ -12,9 +13,17 @@ export default async function Auth() { } return (
-

Login with your Nostr key so that you can keep track of your user names.

- - +

Sign in to keep track of your pay codes.

+ +
+ + + +
+ +

+ New here? Create an account +

); } diff --git a/src/app/auth/signup/page.tsx b/src/app/auth/signup/page.tsx new file mode 100644 index 0000000..30d800b --- /dev/null +++ b/src/app/auth/signup/page.tsx @@ -0,0 +1,143 @@ +"use client"; + +import Link from "next/link"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { signUp } from "@/lib/auth-client"; +import Button from "@/app/components/Button"; + +export default function SignUp() { + const router = useRouter(); + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + + if (password !== confirmPassword) { + setError("Passwords do not match"); + return; + } + + if (password.length < 8) { + setError("Password must be at least 8 characters"); + return; + } + + setLoading(true); + + try { + const result = await signUp.email({ + email, + password, + name, + }); + + if (result.error) { + setError(result.error.message || "Failed to create account"); + setLoading(false); + return; + } + + router.push("/account"); + router.refresh(); + } catch (err) { + setError("An unexpected error occurred"); + setLoading(false); + } + }; + + return ( +
+

Create an Account

+ +
+
+ + setName(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="Your name" + /> +
+ +
+ + setEmail(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="you@example.com" + required + /> +
+ +
+ + setPassword(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="••••••••" + required + minLength={8} + /> +
+ +
+ + setConfirmPassword(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="••••••••" + required + /> +
+ + {error && ( +

{error}

+ )} + + +
+ +

+ Already have an account?{" "} + + Sign in + +

+ +

+ + ← Back to all sign in options + +

+
+ ); +} diff --git a/src/app/components/Button.tsx b/src/app/components/Button.tsx index c231ddd..32279c9 100644 --- a/src/app/components/Button.tsx +++ b/src/app/components/Button.tsx @@ -11,6 +11,7 @@ interface ButtonProps { active?: boolean; id?: string; className?: string; + type?: "button" | "submit" | "reset"; } export default function Button(props:ButtonProps){ @@ -35,6 +36,7 @@ export default function Button(props:ButtonProps){ className={className} disabled={props.disabled} onClick={props.onClick} + type={props.type || "button"} > {props.children || "Click Here"} diff --git a/src/app/components/ClientUserProvider.tsx b/src/app/components/ClientUserProvider.tsx index 52c6d7c..c2ec752 100644 --- a/src/app/components/ClientUserProvider.tsx +++ b/src/app/components/ClientUserProvider.tsx @@ -1,11 +1,11 @@ "use client"; -import { TokenUser } from "@/server/api/trpc"; +import type { AppUser } from "@/lib/current-user"; import React, { createContext, useContext, useState, useEffect } from "react"; interface UserContextType { - user?: TokenUser; - setUser: React.Dispatch>; + user?: AppUser; + setUser: React.Dispatch>; } const UserContext = createContext(undefined); @@ -15,9 +15,9 @@ export default function ClientUserProvider({ initialUser, }: { children: React.ReactNode; - initialUser: TokenUser | undefined; + initialUser: AppUser | undefined; }) { - const [user, setUser] = useState(initialUser); + const [user, setUser] = useState(initialUser); useEffect(() => { if (initialUser) { diff --git a/src/app/components/LogoutButton.tsx b/src/app/components/LogoutButton.tsx index fad4943..0066636 100644 --- a/src/app/components/LogoutButton.tsx +++ b/src/app/components/LogoutButton.tsx @@ -1,23 +1,23 @@ "use client"; import { useRouter } from "next/navigation"; -import { useUser } from "./ClientUserProvider"; // Adjust the import path as necessary +import { useUser } from "./ClientUserProvider"; import Button from "./Button"; import { api } from "@/trpc/react"; +import { signOut } from "@/lib/auth-client"; export default function LogoutButton() { const router = useRouter(); const { setUser } = useUser(); - const logout = api.user.logout.useMutation({ - onSuccess: () => { - console.debug("logged out"); - setUser(undefined); - router.push(`/`); - }, - onError: () => { - console.error("Failed to log in"); - }, - }); + const logout = api.user.logout.useMutation(); - return ; + const handleLogout = async () => { + await signOut().catch(() => undefined); + await logout.mutateAsync().catch(() => undefined); + setUser(undefined); + router.push(`/`); + router.refresh(); + }; + + return ; } diff --git a/src/app/components/UserProvider.tsx b/src/app/components/UserProvider.tsx index 8adde86..73a4858 100644 --- a/src/app/components/UserProvider.tsx +++ b/src/app/components/UserProvider.tsx @@ -1,7 +1,5 @@ -import jwt from "jsonwebtoken"; -import { TokenUser } from "@/server/api/trpc"; import { headers } from "next/headers"; -import { parse } from "cookie"; +import { getCurrentUser } from "@/lib/current-user"; import ClientUserProvider from "./ClientUserProvider"; export default async function UserProvider({ @@ -9,12 +7,7 @@ export default async function UserProvider({ }: { children: React.ReactNode; }) { - const cookieHeader = (await headers()).get("cookie") || ""; - const cookies = cookieHeader ? parse(cookieHeader) : {}; - const accessToken = cookies["access-token"]; - const user = accessToken - ? (jwt.verify(accessToken, process.env.JWT_SECRET ?? "") as TokenUser) - : undefined; + const user = await getCurrentUser(await headers()); return {children}; } diff --git a/src/app/components/getUserServer.tsx b/src/app/components/getUserServer.tsx index 25db217..78f6dae 100644 --- a/src/app/components/getUserServer.tsx +++ b/src/app/components/getUserServer.tsx @@ -1,15 +1,7 @@ -import { parse } from "cookie"; import { headers } from "next/headers"; -import jwt from "jsonwebtoken"; -import { TokenUser } from "@/server/api/trpc"; +import { getCurrentUser } from "@/lib/current-user"; // Get the user on server components export default async function getUser() { - const cookieHeader = (await headers()).get("cookie") || ""; - const cookies = cookieHeader ? parse(cookieHeader) : {}; - const accessToken = cookies["access-token"]; - const user = accessToken - ? (jwt.verify(accessToken, process.env.JWT_SECRET ?? "") as TokenUser) - : undefined; - return user; + return getCurrentUser(await headers()); } diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts new file mode 100644 index 0000000..15f5c0c --- /dev/null +++ b/src/lib/auth-client.ts @@ -0,0 +1,13 @@ +import { createAuthClient } from "better-auth/react"; + +export const authClient = createAuthClient({ + // Base URL is same domain, so no need to specify +}); + +export const { + signIn, + signUp, + signOut, + useSession, + getSession, +} = authClient; diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..2bf1834 --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,33 @@ +import { betterAuth } from "better-auth"; +import { prismaAdapter } from "better-auth/adapters/prisma"; +import { db } from "@/server/db"; + +export const auth = betterAuth({ + database: prismaAdapter(db, { + provider: "postgresql", + }), + emailAndPassword: { + enabled: true, + // Require email verification before account is active + requireEmailVerification: false, // Set to true when email sending is configured + }, + session: { + // Session expires in 7 days + expiresIn: 60 * 60 * 24 * 7, + // Refresh session when it's less than 1 day from expiring + updateAge: 60 * 60 * 24, + cookieCache: { + enabled: true, + maxAge: 60 * 5, // 5 minutes + }, + }, + // Enable account linking so users can link email to existing Nostr accounts + account: { + accountLinking: { + enabled: true, + trustedProviders: ["email"], + }, + }, +}); + +export type Session = typeof auth.$Infer.Session; diff --git a/src/lib/current-user.ts b/src/lib/current-user.ts new file mode 100644 index 0000000..b127011 --- /dev/null +++ b/src/lib/current-user.ts @@ -0,0 +1,58 @@ +import jwt from "jsonwebtoken"; +import { parse } from "cookie"; +import { auth } from "@/lib/auth"; +import { db } from "@/server/db"; + +export interface AppUser { + id: string; + createdAt: Date; + updatedAt: Date; + lastLogin: Date | null; + nostrPublicKey: string | null; + lnNodePublicKey: string | null; + apiKey: string; + name?: string | null; + email?: string | null; + emailVerified?: boolean; + image?: string | null; +} + +async function getBetterAuthUser(headers: Headers): Promise { + const session = await auth.api.getSession({ headers }); + const sessionUser = session?.user; + + if (!sessionUser?.id) { + return undefined; + } + + const user = await db.user.findUnique({ + where: { id: sessionUser.id }, + }); + + return user ?? undefined; +} + +function getLegacyJwtUser(headers: Headers): AppUser | undefined { + const cookieHeader = headers.get("cookie") || ""; + const cookies = cookieHeader ? parse(cookieHeader) : {}; + const accessToken = cookies["access-token"]; + + if (!accessToken || !process.env.JWT_SECRET) { + return undefined; + } + + try { + return jwt.verify(accessToken, process.env.JWT_SECRET) as AppUser; + } catch { + return undefined; + } +} + +export async function getCurrentUser(headers: Headers): Promise { + const betterAuthUser = await getBetterAuthUser(headers); + if (betterAuthUser) { + return betterAuthUser; + } + + return getLegacyJwtUser(headers); +} diff --git a/src/server/api/routers/user.ts b/src/server/api/routers/user.ts index a731592..2e2ffea 100644 --- a/src/server/api/routers/user.ts +++ b/src/server/api/routers/user.ts @@ -22,7 +22,7 @@ export const userRouter = createTRPCRouter({ logout: protectedProcedure.mutation(async ({ ctx }) => { ctx.resHeaders?.resHeaders.set( "Set-Cookie", - `access-token=; Path=/; HttpOnly; SameSite=Strict; Expires ${new Date(0)}` + `access-token=; Path=/; HttpOnly; SameSite=Strict; Expires=${new Date(0).toUTCString()}` ); return { result: "Success" }; }), diff --git a/src/server/api/trpc.ts b/src/server/api/trpc.ts index de26fab..0566fe5 100644 --- a/src/server/api/trpc.ts +++ b/src/server/api/trpc.ts @@ -9,8 +9,8 @@ import { TRPCError, initTRPC } from "@trpc/server"; import superjson from "superjson"; import { ZodError } from "zod"; -import jwt from "jsonwebtoken"; +import { getCurrentUser, type AppUser } from "@/lib/current-user"; import { db } from "@/server/db"; /** @@ -26,29 +26,14 @@ import { db } from "@/server/db"; * @see https://trpc.io/docs/server/context */ import { FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch"; -import { parse } from "cookie"; -export interface TokenUser { - id: string; - nostrPublicKey: string | null; - lnNodePublicKey: string | null; - apiKey: string; - createdAt: Date; - updatedAt: Date; - lastLogin: Date | null; -} +export type TokenUser = AppUser; export const createTRPCContext = async (opts: { headers: Headers; resHeaders?: FetchCreateContextFnOptions; }) => { - const cookieHeader = opts.headers.get("cookie"); - const cookies = cookieHeader ? parse(cookieHeader) : {}; - const accessToken = cookies["access-token"]; - - const user = accessToken - ? (jwt.verify(accessToken, process.env.JWT_SECRET ?? "") as TokenUser) - : undefined; + const user = await getCurrentUser(opts.headers); return { db,