From 8dd1e4326caf22eac6c22e7358370b2da02ce6cd Mon Sep 17 00:00:00 2001 From: Wuesteon Date: Mon, 1 Dec 2025 15:18:57 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(auth):=20use=20Better=20Auth?= =?UTF-8?q?=20native=20JWT=20validation=20with=20EdDSA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace jsonwebtoken RS256 validation with jose EdDSA - Add JWKS endpoint to expose Better Auth public keys - Use createRemoteJWKSet for token validation - Fix issuer mismatch (use env var consistently) - Add jwks table to schema for Better Auth JWT plugin - Install jose library for JWT verification --- pnpm-lock.yaml | 473 ++++++++++-------- services/mana-core-auth/drizzle.config.ts | 3 +- services/mana-core-auth/package.json | 1 + .../src/auth/auth.controller.ts | 11 + .../src/auth/better-auth.config.ts | 326 +++--------- .../src/auth/services/better-auth.service.ts | 154 +++++- .../src/auth/types/better-auth.types.ts | 6 +- .../src/config/configuration.ts | 5 +- .../src/db/schema/auth.schema.ts | 101 ++-- .../src/db/schema/credits.schema.ts | 48 +- 10 files changed, 573 insertions(+), 555 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f36cb4f42..324d91028 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -686,7 +686,7 @@ importers: version: 17.0.7(expo@54.0.25)(react@19.1.0) expo-router: specifier: ~6.0.14 - version: 6.0.15(hsxvh2bq7p37jxljkgxire2dbu) + version: 6.0.15(r6e3zsmutyjzazpntzawr4wuhy) expo-secure-store: specifier: ~15.0.7 version: 15.0.7(expo@54.0.25) @@ -789,7 +789,7 @@ importers: version: 7.28.5 '@testing-library/react-native': specifier: ^13.3.3 - version: 13.3.3(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + version: 13.3.3(jest@29.7.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) '@types/jest': specifier: ^29.5.12 version: 29.5.14 @@ -807,10 +807,10 @@ importers: version: 10.0.0 jest: specifier: ^29.2.1 - version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + version: 29.7.0 jest-expo: specifier: ~54.0.13 - version: 54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)(webpack@5.100.2) + version: 54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@29.7.0)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)(webpack@5.100.2) patch-package: specifier: ^8.0.0 version: 8.0.1 @@ -3277,7 +3277,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^10.4.9 - version: 10.4.9(esbuild@0.19.12) + version: 10.4.9(esbuild@0.27.0) '@nestjs/schematics': specifier: ^10.2.3 version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) @@ -3310,7 +3310,7 @@ importers: version: 0.5.21 ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)) + version: 9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.27.0)) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -4103,7 +4103,7 @@ importers: version: 0.5.21 ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.1 version: 9.5.4(typescript@5.9.3)(webpack@5.100.2) @@ -4764,7 +4764,7 @@ importers: version: 1.57.0 jest: specifier: ^29.0.0 - version: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + version: 29.7.0(@types/node@24.10.1) vitest: specifier: ^3.0.0 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) @@ -4845,6 +4845,9 @@ importers: helmet: specifier: ^8.0.0 version: 8.1.0 + jose: + specifier: ^6.1.2 + version: 6.1.2 jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 @@ -4875,7 +4878,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^11.0.0 - version: 11.0.12(@types/node@22.19.1) + version: 11.0.12(@types/node@22.19.1)(esbuild@0.19.12) '@nestjs/schematics': specifier: ^11.0.0 version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) @@ -4929,10 +4932,10 @@ importers: version: 7.1.4 ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.100.2) + version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -25575,7 +25578,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(hsxvh2bq7p37jxljkgxire2dbu) + expo-router: 6.0.15(ohit2up6tuxb3x34brxduivol4) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -27085,6 +27088,41 @@ snapshots: jest-util: 30.2.0 slash: 3.0.0 + '@jest/core@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.1 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@18.15.11)(typescript@5.9.3))': dependencies: '@jest/console': 29.7.0 @@ -27155,41 +27193,6 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))': - dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.19.1 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.9.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))': dependencies: '@jest/console': 30.2.0 @@ -27666,32 +27669,6 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/cli@10.4.9(esbuild@0.19.12)': - dependencies: - '@angular-devkit/core': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics-cli': 17.3.11(chokidar@3.6.0) - '@nestjs/schematics': 10.2.3(chokidar@3.6.0)(typescript@5.7.2) - chalk: 4.1.2 - chokidar: 3.6.0 - cli-table3: 0.6.5 - commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.19.12)) - glob: 10.4.5 - inquirer: 8.2.6 - node-emoji: 1.11.0 - ora: 5.4.1 - tree-kill: 1.2.2 - tsconfig-paths: 4.2.0 - tsconfig-paths-webpack-plugin: 4.2.0 - typescript: 5.7.2 - webpack: 5.97.1(esbuild@0.19.12) - webpack-node-externals: 3.0.0 - transitivePeerDependencies: - - esbuild - - uglify-js - - webpack-cli - '@nestjs/cli@10.4.9(esbuild@0.27.0)': dependencies: '@angular-devkit/core': 17.3.11(chokidar@3.6.0) @@ -27718,7 +27695,7 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/cli@11.0.12(@types/node@22.19.1)': + '@nestjs/cli@11.0.12(@types/node@22.19.1)(esbuild@0.19.12)': dependencies: '@angular-devkit/core': 19.2.19(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) @@ -27729,14 +27706,14 @@ snapshots: chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.100.2) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)) glob: 12.0.0 node-emoji: 1.11.0 ora: 5.4.1 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.9.3 - webpack: 5.100.2 + webpack: 5.100.2(esbuild@0.19.12) webpack-node-externals: 3.0.0 transitivePeerDependencies: - '@types/node' @@ -32017,6 +31994,19 @@ snapshots: redent: 3.0.0 optionalDependencies: jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + optional: true + + '@testing-library/react-native@13.3.3(jest@29.7.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + jest-matcher-utils: 30.2.0 + picocolors: 1.1.1 + pretty-format: 30.2.0 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-test-renderer: 19.1.0(react@19.1.0) + redent: 3.0.0 + optionalDependencies: + jest: 29.7.0 '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: @@ -35369,6 +35359,21 @@ snapshots: crc-32: 1.2.2 readable-stream: 4.7.0 + create-jest@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@18.15.11)(ts-node@10.9.2(@types/node@18.15.11)(typescript@5.9.3)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + create-jest@29.7.0(@types/node@18.15.11)(ts-node@10.9.2(@types/node@18.15.11)(typescript@5.9.3)): dependencies: '@jest/types': 29.6.3 @@ -35399,13 +35404,13 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + create-jest@29.7.0(@types/node@24.10.1): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@24.10.1) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -38472,7 +38477,7 @@ snapshots: - '@types/react-dom' - supports-color - expo-router@6.0.15(hsxvh2bq7p37jxljkgxire2dbu): + expo-router@6.0.15(ohit2up6tuxb3x34brxduivol4): dependencies: '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) '@expo/schema-utils': 0.1.7 @@ -38511,12 +38516,13 @@ snapshots: react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2) + react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.19.12)) transitivePeerDependencies: - '@react-native-masked-view/masked-view' - '@types/react' - '@types/react-dom' - supports-color + optional: true expo-router@6.0.15(qjp3usx4acoq47dkosl6pmu254): dependencies: @@ -38564,6 +38570,52 @@ snapshots: - '@types/react-dom' - supports-color + expo-router@6.0.15(r6e3zsmutyjzazpntzawr4wuhy): + dependencies: + '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@expo/schema-utils': 0.1.7 + '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native': 7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + client-only: 0.0.1 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-server: 1.0.4 + fast-deep-equal: 3.1.3 + invariant: 2.2.4 + nanoid: 3.3.11 + query-string: 7.1.3 + react: 19.1.0 + react-fast-compare: 3.2.2 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + semver: 7.6.3 + server-only: 0.0.1 + sf-symbols-typescript: 2.1.0 + shallowequal: 1.1.0 + use-latest-callback: 0.2.6(react@19.1.0) + vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + optionalDependencies: + '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@29.7.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + react-dom: 19.1.0(react@19.1.0) + react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2) + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + - '@types/react' + - '@types/react-dom' + - supports-color + expo-router@6.0.15(ucgv42olhsnvykdrhhfuls4dzq): dependencies: '@expo/metro-runtime': 6.1.2(expo@54.0.12)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -39654,23 +39706,6 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.19.12)): - dependencies: - '@babel/code-frame': 7.27.1 - chalk: 4.1.2 - chokidar: 3.6.0 - cosmiconfig: 8.3.6(typescript@5.7.2) - deepmerge: 4.3.1 - fs-extra: 10.1.0 - memfs: 3.5.3 - minimatch: 3.1.2 - node-abort-controller: 3.1.1 - schema-utils: 3.3.0 - semver: 7.7.3 - tapable: 2.3.0 - typescript: 5.7.2 - webpack: 5.97.1(esbuild@0.19.12) - fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.27.0)): dependencies: '@babel/code-frame': 7.27.1 @@ -39705,6 +39740,23 @@ snapshots: typescript: 5.7.2 webpack: 5.97.1 + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)): + dependencies: + '@babel/code-frame': 7.27.1 + chalk: 4.1.2 + chokidar: 4.0.3 + cosmiconfig: 8.3.6(typescript@5.9.3) + deepmerge: 4.3.1 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.2 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.7.3 + tapable: 2.3.0 + typescript: 5.9.3 + webpack: 5.100.2(esbuild@0.19.12) + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)): dependencies: '@babel/code-frame': 7.27.1 @@ -39722,23 +39774,6 @@ snapshots: typescript: 5.9.3 webpack: 5.100.2(esbuild@0.27.0) - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2): - dependencies: - '@babel/code-frame': 7.27.1 - chalk: 4.1.2 - chokidar: 4.0.3 - cosmiconfig: 8.3.6(typescript@5.9.3) - deepmerge: 4.3.1 - fs-extra: 10.1.0 - memfs: 3.5.3 - minimatch: 3.1.2 - node-abort-controller: 3.1.1 - schema-utils: 3.3.0 - semver: 7.7.3 - tapable: 2.3.0 - typescript: 5.9.3 - webpack: 5.100.2 - form-data-encoder@1.7.2: {} form-data@2.5.5: @@ -41106,6 +41141,25 @@ snapshots: - babel-plugin-macros - supports-color + jest-cli@29.7.0: + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0 + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@18.15.11)(ts-node@10.9.2(@types/node@18.15.11)(typescript@5.9.3)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest-cli@29.7.0(@types/node@18.15.11)(ts-node@10.9.2(@types/node@18.15.11)(typescript@5.9.3)): dependencies: '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@18.15.11)(typescript@5.9.3)) @@ -41144,16 +41198,16 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest-cli@29.7.0(@types/node@24.10.1): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + create-jest: 29.7.0(@types/node@24.10.1) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@24.10.1) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -41314,38 +41368,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): - dependencies: - '@babel/core': 7.28.5 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.5) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 22.19.1 - ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-config@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest-config@29.7.0(@types/node@24.10.1): dependencies: '@babel/core': 7.28.5 '@jest/test-sequencer': 29.7.0 @@ -41371,7 +41394,6 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 24.10.1 - ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -41515,7 +41537,7 @@ snapshots: jest-util: 30.2.0 jest-validate: 30.2.0 - jest-expo@54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)(webpack@5.100.2): + jest-expo@54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@29.7.0)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)(webpack@5.100.2): dependencies: '@expo/config': 12.0.10 '@expo/json-file': 10.0.7 @@ -41526,7 +41548,7 @@ snapshots: jest-environment-jsdom: 29.7.0 jest-snapshot: 29.7.0 jest-watch-select-projects: 2.0.0 - jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))) + jest-watch-typeahead: 2.2.1(jest@29.7.0) json5: 2.2.3 lodash: 4.17.21 react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) @@ -41917,11 +41939,11 @@ snapshots: chalk: 3.0.0 prompts: 2.4.2 - jest-watch-typeahead@2.2.1(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))): + jest-watch-typeahead@2.2.1(jest@29.7.0): dependencies: ansi-escapes: 6.2.1 chalk: 4.1.2 - jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest: 29.7.0 jest-regex-util: 29.6.3 jest-watcher: 29.7.0 slash: 5.1.0 @@ -41994,6 +42016,18 @@ snapshots: - supports-color - ts-node + jest@29.7.0: + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) @@ -42006,12 +42040,12 @@ snapshots: - supports-color - ts-node - jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest@29.7.0(@types/node@24.10.1): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-cli: 29.7.0(@types/node@24.10.1) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -46947,6 +46981,16 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.19.12)): + dependencies: + acorn-loose: 8.5.2 + neo-async: 2.6.2 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + webpack: 5.100.2(esbuild@0.19.12) + webpack-sources: 3.3.3 + optional: true + react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)): dependencies: acorn-loose: 8.5.2 @@ -48505,14 +48549,14 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - terser-webpack-plugin@5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12)): + terser-webpack-plugin@5.3.14(esbuild@0.19.12)(webpack@5.100.2(esbuild@0.19.12)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.44.1 - webpack: 5.97.1(esbuild@0.19.12) + webpack: 5.100.2(esbuild@0.19.12) optionalDependencies: esbuild: 0.19.12 @@ -48757,6 +48801,27 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.28.5) + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.3 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.28.5 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.5) + esbuild: 0.19.12 + jest-util: 30.2.0 + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 @@ -48778,25 +48843,15 @@ snapshots: esbuild: 0.27.0 jest-util: 30.2.0 - ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)): dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.8 - jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 + chalk: 4.1.2 + enhanced-resolve: 5.18.3 + micromatch: 4.0.8 semver: 7.7.3 - type-fest: 4.41.0 + source-map: 0.7.6 typescript: 5.9.3 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.28.5 - '@jest/transform': 30.2.0 - '@jest/types': 30.2.0 - babel-jest: 30.2.0(@babel/core@7.28.5) - jest-util: 30.2.0 + webpack: 5.100.2(esbuild@0.19.12) ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)): dependencies: @@ -48818,7 +48873,7 @@ snapshots: typescript: 5.9.3 webpack: 5.100.2 - ts-loader@9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)): + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.27.0)): dependencies: chalk: 4.1.2 enhanced-resolve: 5.18.3 @@ -48826,7 +48881,7 @@ snapshots: semver: 7.7.3 source-map: 0.7.6 typescript: 5.9.3 - webpack: 5.97.1(esbuild@0.19.12) + webpack: 5.97.1(esbuild@0.27.0) ts-node@10.9.2(@types/node@18.15.11)(typescript@5.9.3): dependencies: @@ -49999,6 +50054,38 @@ snapshots: - esbuild - uglify-js + webpack@5.100.2(esbuild@0.19.12): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + acorn-import-phases: 1.0.4(acorn@8.15.0) + browserslist: 4.28.0 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.3 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.14(esbuild@0.19.12)(webpack@5.100.2(esbuild@0.19.12)) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + webpack@5.100.2(esbuild@0.27.0): dependencies: '@types/eslint-scope': 3.7.7 @@ -50061,36 +50148,6 @@ snapshots: - esbuild - uglify-js - webpack@5.97.1(esbuild@0.19.12): - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - browserslist: 4.28.0 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.3 - es-module-lexer: 1.7.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.1 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 3.3.0 - tapable: 2.3.0 - terser-webpack-plugin: 5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12)) - watchpack: 2.4.4 - webpack-sources: 3.3.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - webpack@5.97.1(esbuild@0.27.0): dependencies: '@types/eslint-scope': 3.7.7 diff --git a/services/mana-core-auth/drizzle.config.ts b/services/mana-core-auth/drizzle.config.ts index a9aa91e4a..d111b1c64 100644 --- a/services/mana-core-auth/drizzle.config.ts +++ b/services/mana-core-auth/drizzle.config.ts @@ -5,8 +5,9 @@ export default defineConfig({ schema: './src/db/schema/index.ts', out: './src/db/migrations', dbCredentials: { - url: process.env.DATABASE_URL || 'postgresql://manacore:password@localhost:5432/manacore', + url: process.env.DATABASE_URL || 'postgresql://manacore:devpassword@localhost:5432/manacore', }, + schemaFilter: ['auth', 'credits', 'public'], verbose: true, strict: true, }); diff --git a/services/mana-core-auth/package.json b/services/mana-core-auth/package.json index 39759b02b..3598308f2 100644 --- a/services/mana-core-auth/package.json +++ b/services/mana-core-auth/package.json @@ -33,6 +33,7 @@ "drizzle-kit": "^0.30.2", "drizzle-orm": "^0.38.3", "helmet": "^8.0.0", + "jose": "^6.1.2", "jsonwebtoken": "^9.0.2", "nanoid": "^5.0.9", "postgres": "^3.4.5", diff --git a/services/mana-core-auth/src/auth/auth.controller.ts b/services/mana-core-auth/src/auth/auth.controller.ts index 49200990e..980ff9301 100644 --- a/services/mana-core-auth/src/auth/auth.controller.ts +++ b/services/mana-core-auth/src/auth/auth.controller.ts @@ -126,6 +126,17 @@ export class AuthController { return this.betterAuthService.validateToken(body.token); } + /** + * Get JWKS (JSON Web Key Set) + * + * Returns public keys for JWT verification. + * This is a passthrough to Better Auth's JWKS. + */ + @Get('jwks') + async getJwks() { + return this.betterAuthService.getJwks(); + } + // ========================================================================= // B2B Registration // ========================================================================= diff --git a/services/mana-core-auth/src/auth/better-auth.config.ts b/services/mana-core-auth/src/auth/better-auth.config.ts index 0c3bd872f..e2b64ca5d 100644 --- a/services/mana-core-auth/src/auth/better-auth.config.ts +++ b/services/mana-core-auth/src/auth/better-auth.config.ts @@ -4,11 +4,14 @@ * This file configures Better Auth with: * - Email/password authentication * - Organization plugin for B2B (multi-tenant) - * - JWT plugin with custom claims (credit_balance, customer_type, organization) + * - JWT plugin with minimal claims * - Drizzle adapter for PostgreSQL * + * ARCHITECTURE DECISION (2024-12): + * We use MINIMAL JWT claims. Organization and credit data should be fetched + * via API calls, not embedded in JWTs. See docs/AUTHENTICATION_ARCHITECTURE.md + * * @see https://www.better-auth.com/docs - * @see BETTER_AUTH_FINAL_PLAN.md */ import { betterAuth } from 'better-auth'; @@ -16,16 +19,32 @@ import { drizzleAdapter } from 'better-auth/adapters/drizzle'; import { jwt } from 'better-auth/plugins/jwt'; import { organization } from 'better-auth/plugins/organization'; import { getDb } from '../db/connection'; -import { eq, and } from 'drizzle-orm'; -import { balances } from '../db/schema/credits.schema'; -import { organizations, members } from '../db/schema/organizations.schema'; +import { + organizations, + members, + invitations, +} from '../db/schema/organizations.schema'; +import { + users, + sessions, + accounts, + verificationTokens, + jwks, +} from '../db/schema/auth.schema'; import type { JWTPayloadContext } from './types/better-auth.types'; /** * JWT Custom Payload Interface * - * Defines the structure of custom claims included in JWT tokens. - * These claims are added to the standard JWT payload (sub, iat, exp, etc.) + * MINIMAL claims only. Organization context and credits are available via: + * - GET /organization/get-active-member - org membership & role + * - GET /api/v1/credits/balance - credit balance + * + * Why minimal claims? + * 1. Credit balance changes frequently - JWT would be stale + * 2. Organization context available via Better Auth org plugin APIs + * 3. Smaller tokens = better performance + * 4. Follows Better Auth's session-based design */ export interface JWTCustomPayload { /** User ID (standard JWT claim) */ @@ -37,145 +56,13 @@ export interface JWTCustomPayload { /** User role (user, admin, service) */ role: string; - /** Customer type: B2C (individual) or B2B (organization member) */ - customer_type: 'b2c' | 'b2b'; - - /** Organization context (null for B2C users) */ - organization: { - id: string; - name: string; - role: 'owner' | 'admin' | 'member'; - } | null; - - /** User's credit balance (personal for B2C, allocated for B2B) */ - credit_balance: number; - - /** Application ID (memoro, chat, picture, etc.) */ - app_id?: string; - - /** Device ID (for mobile apps) */ - device_id?: string; -} - -/** - * Helper function to get personal credit balance (B2C users) - * - * @param userId - User ID - * @param databaseUrl - Database connection URL - * @returns Credit balance or 0 if not found - */ -async function getPersonalCreditBalance(userId: string, databaseUrl: string): Promise { - try { - const db = getDb(databaseUrl); - - const [balance] = await db - .select({ balance: balances.balance }) - .from(balances) - .where(eq(balances.userId, userId)) - .limit(1); - - return balance?.balance ?? 0; - } catch (error) { - console.error('Error fetching personal credit balance:', error); - return 0; - } -} - -/** - * Helper function to get employee credit balance (B2B users) - * - * For B2B employees, this returns their allocated credit balance. - * The balance is stored in the same balances table but tracked separately per employee. - * - * @param userId - Employee user ID - * @param organizationId - Organization ID - * @param databaseUrl - Database connection URL - * @returns Allocated credit balance or 0 if not found - */ -async function getEmployeeCreditBalance( - userId: string, - organizationId: string, - databaseUrl: string -): Promise { - try { - const db = getDb(databaseUrl); - - // Get employee's personal balance (which represents their allocated credits from the org) - const [balance] = await db - .select({ balance: balances.balance }) - .from(balances) - .where(eq(balances.userId, userId)) - .limit(1); - - return balance?.balance ?? 0; - } catch (error) { - console.error('Error fetching employee credit balance:', error); - return 0; - } -} - -/** - * Helper function to get organization membership data - * - * Queries the organization and member tables to get: - * - Organization name - * - User's role in the organization - * - * @param userId - User ID - * @param organizationId - Organization ID - * @param databaseUrl - Database connection URL - * @returns Organization data with name and role, or null if not found - */ -async function getOrganizationMembership( - userId: string, - organizationId: string, - databaseUrl: string -): Promise<{ name: string; role: 'owner' | 'admin' | 'member' } | null> { - try { - const db = getDb(databaseUrl); - - // Query member table to get user's role in the organization - const [memberRecord] = await db - .select({ - role: members.role, - }) - .from(members) - .where(and(eq(members.userId, userId), eq(members.organizationId, organizationId))) - .limit(1); - - if (!memberRecord) { - return null; - } - - // Query organization table to get organization name - const [orgRecord] = await db - .select({ - name: organizations.name, - }) - .from(organizations) - .where(eq(organizations.id, organizationId)) - .limit(1); - - if (!orgRecord) { - return null; - } - - return { - name: orgRecord.name, - role: memberRecord.role as 'owner' | 'admin' | 'member', - }; - } catch (error) { - console.error('Error fetching organization membership:', error); - return null; - } + /** Session ID for reference */ + sid: string; } /** * Create Better Auth instance * - * This function initializes Better Auth with the database connection URL. - * It must be called with the database URL from the configuration. - * * @param databaseUrl - PostgreSQL connection URL * @returns Better Auth instance */ @@ -187,16 +74,19 @@ export function createBetterAuth(databaseUrl: string) { database: drizzleAdapter(db, { provider: 'pg', schema: { - // Auth tables - user: 'auth.users', - session: 'auth.sessions', - account: 'auth.accounts', - verification: 'auth.verification_tokens', + // Auth tables (actual Drizzle table objects) + user: users, + session: sessions, + account: accounts, + verification: verificationTokens, - // Organization tables (Better Auth creates these schemas) - organization: 'auth.organizations', - member: 'auth.members', - invitation: 'auth.invitations', + // Organization tables + organization: organizations, + member: members, + invitation: invitations, + + // JWT plugin table + jwks: jwks, }, }), @@ -226,7 +116,12 @@ export function createBetterAuth(databaseUrl: string) { * - Create/update/delete organizations * - Invite/add/remove members * - Role-based access control - * - Email-based invitations + * - Active organization tracking (session.activeOrganizationId) + * + * Client apps use these endpoints for org context: + * - GET /organization/get-active-member + * - GET /organization/get-active-member-role + * - POST /organization/set-active */ organization({ // Allow users to create their own organizations @@ -242,22 +137,10 @@ export function createBetterAuth(databaseUrl: string) { organization: organization.name, invitationId: data.id, }); - - // Example email template: - // Subject: Join ${organization.name} on Mana Universe - // Body: You've been invited to join ${organization.name} - // Click here to accept: ${baseURL}/invite/${data.id} }, // Custom roles and permissions organizationRole: { - /** - * Owner Role - * - Full organization control - * - Can delete organization - * - Can manage all members - * - Can allocate credits to employees - */ owner: { permissions: [ 'organization:update', @@ -265,17 +148,10 @@ export function createBetterAuth(databaseUrl: string) { 'members:invite', 'members:remove', 'members:update_role', - 'credits:allocate', // Custom permission - 'credits:view_all', // Custom permission + 'credits:allocate', + 'credits:view_all', ], }, - - /** - * Admin Role - * - Can update organization settings - * - Can invite and remove members - * - Can view all credit usage - */ admin: { permissions: [ 'organization:update', @@ -284,12 +160,6 @@ export function createBetterAuth(databaseUrl: string) { 'credits:view_all', ], }, - - /** - * Member Role - * - Basic organization access - * - Can only view their own credits - */ member: { permissions: ['credits:view_own'], }, @@ -299,99 +169,35 @@ export function createBetterAuth(databaseUrl: string) { /** * JWT Plugin * - * Generates JWT tokens with custom claims for: - * - Credit balance - * - Customer type (B2C vs B2B) - * - Organization context - * - App/device metadata + * Generates JWT tokens with MINIMAL claims. + * + * DO NOT add complex claims like: + * - credit_balance (stale after 15min, fetch via API instead) + * - organization details (use Better Auth org plugin APIs) + * - customer_type (derive from activeOrganizationId presence) + * + * Apps should call APIs for dynamic data: + * - Credits: GET /api/v1/credits/balance + * - Org info: GET /organization/get-active-member */ jwt({ jwt: { - issuer: 'mana-core', + issuer: process.env.JWT_ISSUER || 'manacore', audience: process.env.JWT_AUDIENCE || 'manacore', - expirationTime: '15m', // 15 minutes for access tokens + expirationTime: '15m', /** - * Define custom JWT payload + * Define minimal JWT payload * - * This function is called when generating a JWT token. - * It enriches the standard JWT claims with custom data. - * - * @param context - JWT payload context with user and session - * @returns Custom JWT payload + * Only includes static user info that doesn't change frequently. */ - async definePayload({ user, session }: JWTPayloadContext) { - // Get user's active organization (from session metadata or first membership) - const activeOrgId = session.activeOrganizationId; - - let organizationData: JWTCustomPayload['organization'] = null; - let creditBalance = 0; - let customerType: 'b2c' | 'b2b' = 'b2c'; - - if (activeOrgId) { - // B2B user - get organization membership from database - try { - // Query actual organization and membership data - const membership = await getOrganizationMembership( - user.id, - activeOrgId, - databaseUrl - ); - - if (membership) { - // Get employee's allocated credit balance - creditBalance = await getEmployeeCreditBalance( - user.id, - activeOrgId, - databaseUrl - ); - - organizationData = { - id: activeOrgId, - name: membership.name, - role: membership.role, - }; - - customerType = 'b2b'; - } else { - // User is not a member of this organization, fall back to B2C - console.warn( - `User ${user.id} is not a member of organization ${activeOrgId}` - ); - creditBalance = await getPersonalCreditBalance(user.id, databaseUrl); - } - } catch (error) { - console.error('Error fetching organization data:', error); - // Fall back to B2C on error - creditBalance = await getPersonalCreditBalance(user.id, databaseUrl); - } - } else { - // B2C user - get personal credit balance - creditBalance = await getPersonalCreditBalance(user.id, databaseUrl); - } - - // Build custom JWT payload - const payload: Partial = { - // Standard claims + definePayload({ user, session }: JWTPayloadContext) { + return { sub: user.id, email: user.email, - role: user.role || 'user', - - // Customer type - customer_type: customerType, - - // Organization (null for B2C) - organization: organizationData, - - // Credits - credit_balance: creditBalance, - - // App metadata (from session) - app_id: (session.metadata?.appId as string) || undefined, - device_id: (session.metadata?.deviceId as string) || undefined, + role: (user as { role?: string }).role || 'user', + sid: session.id, }; - - return payload; }, }, }), diff --git a/services/mana-core-auth/src/auth/services/better-auth.service.ts b/services/mana-core-auth/src/auth/services/better-auth.service.ts index 291266574..3d07d852b 100644 --- a/services/mana-core-auth/src/auth/services/better-auth.service.ts +++ b/services/mana-core-auth/src/auth/services/better-auth.service.ts @@ -62,6 +62,7 @@ import type { BetterAuthSession, } from '../types/better-auth.types'; import * as jwt from 'jsonwebtoken'; +import { jwtVerify, createRemoteJWKSet } from 'jose'; // Re-export DTOs and result types for external use export type { @@ -418,6 +419,73 @@ export class BetterAuthService { const { user } = result; + // Get session token (used as refresh token) + const session = hasSession(result) ? result.session : null; + const sessionToken = session?.token || (hasToken(result) ? result.token : ''); + + // Generate JWT access token using Better Auth's JWT plugin + let accessToken = ''; + try { + const api = this.auth.api as any; + + // Use Better Auth's signJWT with the jwks table + const jwtResult = await api.signJWT({ + body: { + payload: { + sub: user.id, + email: user.email, + role: (user as BetterAuthUser).role || 'user', + sid: session?.id || '', + }, + }, + }); + + accessToken = jwtResult?.token || ''; + + // Fallback to manual JWT if Better Auth fails + if (!accessToken) { + throw new Error('Better Auth signJWT returned empty token'); + } + } catch (jwtError) { + console.warn('[signIn] Better Auth signJWT failed, using manual JWT generation:', jwtError); + + // Fallback: Generate JWT manually using jsonwebtoken + const privateKey = this.configService.get('jwt.privateKey'); + const issuer = this.configService.get('jwt.issuer') || 'manacore'; + const audience = this.configService.get('jwt.audience') || 'manacore'; + + console.log('[signIn] Private key exists:', !!privateKey); + console.log('[signIn] Private key length:', privateKey?.length); + console.log('[signIn] Private key starts with:', privateKey?.substring(0, 30)); + console.log('[signIn] Issuer:', issuer); + console.log('[signIn] Audience:', audience); + + if (privateKey) { + const payload = { + sub: user.id, + email: user.email, + role: (user as BetterAuthUser).role || 'user', + sid: session?.id || '', + }; + + accessToken = jwt.sign(payload, privateKey, { + algorithm: 'RS256', + expiresIn: '15m', + issuer, + audience, + }); + + console.log('[signIn] Generated JWT (first 50 chars):', accessToken?.substring(0, 50)); + // Decode to verify + const decoded = jwt.decode(accessToken, { complete: true }); + console.log('[signIn] Generated JWT header:', decoded?.header); + console.log('[signIn] Generated JWT payload:', decoded?.payload); + } else { + console.error('[signIn] No JWT private key configured'); + accessToken = sessionToken; + } + } + return { user: { id: user.id, @@ -425,7 +493,9 @@ export class BetterAuthService { name: user.name, role: (user as BetterAuthUser).role, }, - token: hasToken(result) ? result.token : '', + accessToken, + refreshToken: sessionToken, + expiresIn: 15 * 60, // 15 minutes in seconds }; } catch (error: unknown) { if (error instanceof Error) { @@ -617,7 +687,7 @@ export class BetterAuthService { } // Check if refresh token is expired - if (new Date() > session.refreshTokenExpiresAt) { + if (!session.refreshTokenExpiresAt || new Date() > session.refreshTokenExpiresAt) { throw new UnauthorizedException('Refresh token expired'); } @@ -715,26 +785,44 @@ export class BetterAuthService { */ async validateToken(token: string): Promise { try { - const publicKey = this.configService.get('jwt.publicKey'); - if (!publicKey) { - throw new Error('JWT public key not configured'); - } + console.log('[validateToken] Token (first 50 chars):', token?.substring(0, 50)); - const audience = this.configService.get('jwt.audience'); - const issuer = this.configService.get('jwt.issuer'); + // Decode to check the algorithm + const decoded = jwt.decode(token, { complete: true }); + console.log('[validateToken] Decoded header:', decoded?.header); - const payload = jwt.verify(token, publicKey, { - algorithms: ['RS256'], - audience, + // Use our JWKS endpoint (NestJS prefix: /api/v1) + const baseUrl = this.configService.get('BASE_URL') || 'http://localhost:3001'; + const jwksUrl = new URL('/api/v1/auth/jwks', baseUrl); + + console.log('[validateToken] Using JWKS from:', jwksUrl.toString()); + + // Create JWKS fetcher + const JWKS = createRemoteJWKSet(jwksUrl); + + // Get issuer/audience from config (Better Auth uses BASE_URL by default) + const issuer = this.configService.get('jwt.issuer') || baseUrl; + const audience = this.configService.get('jwt.audience') || baseUrl; + + console.log('[validateToken] Issuer:', issuer); + console.log('[validateToken] Audience:', audience); + + // Verify using jose library with Better Auth's JWKS + const { payload } = await jwtVerify(token, JWKS, { issuer, - }) as TokenPayload; + audience, + }); + + console.log('[validateToken] Verification SUCCESS'); + console.log('[validateToken] Payload:', payload); return { valid: true, - payload, + payload: payload as unknown as TokenPayload, }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + console.error('[validateToken] Verification FAILED:', errorMessage); return { valid: false, error: errorMessage, @@ -742,6 +830,46 @@ export class BetterAuthService { } } + /** + * Get JWKS (JSON Web Key Set) + * + * Returns public keys for JWT verification. + * Proxies to Better Auth's internal JWKS. + * + * @returns JWKS with public keys + */ + async getJwks(): Promise<{ keys: unknown[] }> { + try { + // Better Auth exposes JWKS via auth.api + const api = this.auth.api as any; + + // Try to get JWKS from Better Auth + if (api.getJwks) { + const result = await api.getJwks(); + return result; + } + + // Fallback: read from jwks table directly + const db = getDb(this.databaseUrl); + const { jwks } = await import('../../db/schema/auth.schema'); + const keys = await db.select().from(jwks); + + // Convert to JWKS format (EdDSA public keys) + return { + keys: keys.map((key) => { + try { + return JSON.parse(key.publicKey); + } catch { + return { kid: key.id, publicKey: key.publicKey }; + } + }), + }; + } catch (error) { + console.error('[getJwks] Error:', error); + return { keys: [] }; + } + } + // ========================================================================= // Private Helper Methods // ========================================================================= diff --git a/services/mana-core-auth/src/auth/types/better-auth.types.ts b/services/mana-core-auth/src/auth/types/better-auth.types.ts index 5930c3314..0a20415b9 100644 --- a/services/mana-core-auth/src/auth/types/better-auth.types.ts +++ b/services/mana-core-auth/src/auth/types/better-auth.types.ts @@ -441,9 +441,9 @@ export interface SignInResult { name: string | null; role?: string; }; - token: string; - refreshToken?: string; - expiresIn?: number; + accessToken: string; + refreshToken: string; + expiresIn: number; } /** diff --git a/services/mana-core-auth/src/config/configuration.ts b/services/mana-core-auth/src/config/configuration.ts index 7fb121897..8dcf4ccd6 100644 --- a/services/mana-core-auth/src/config/configuration.ts +++ b/services/mana-core-auth/src/config/configuration.ts @@ -7,8 +7,9 @@ export default () => ({ }, jwt: { - publicKey: process.env.JWT_PUBLIC_KEY || '', - privateKey: process.env.JWT_PRIVATE_KEY || '', + // Convert \n string literals to actual newlines for PEM format + publicKey: (process.env.JWT_PUBLIC_KEY || '').replace(/\\n/g, '\n'), + privateKey: (process.env.JWT_PRIVATE_KEY || '').replace(/\\n/g, '\n'), accessTokenExpiry: process.env.JWT_ACCESS_TOKEN_EXPIRY || '15m', refreshTokenExpiry: process.env.JWT_REFRESH_TOKEN_EXPIRY || '7d', issuer: process.env.JWT_ISSUER || 'manacore', diff --git a/services/mana-core-auth/src/db/schema/auth.schema.ts b/services/mana-core-auth/src/db/schema/auth.schema.ts index 55e06237b..6cd683b50 100644 --- a/services/mana-core-auth/src/db/schema/auth.schema.ts +++ b/services/mana-core-auth/src/db/schema/auth.schema.ts @@ -1,78 +1,83 @@ -import { pgSchema, uuid, text, timestamp, boolean, jsonb, pgEnum } from 'drizzle-orm/pg-core'; -import { sql } from 'drizzle-orm'; +import { pgSchema, uuid, text, timestamp, boolean, jsonb, pgEnum, index } from 'drizzle-orm/pg-core'; export const authSchema = pgSchema('auth'); // Enum for user roles export const userRoleEnum = pgEnum('user_role', ['user', 'admin', 'service']); -// Users table +// Users table (Better Auth schema) export const users = authSchema.table('users', { - id: uuid('id').primaryKey().defaultRandom(), + id: text('id').primaryKey(), // Better Auth generates nanoid + name: text('name').notNull(), email: text('email').unique().notNull(), emailVerified: boolean('email_verified').default(false).notNull(), - name: text('name'), - avatarUrl: text('avatar_url'), - role: userRoleEnum('role').default('user').notNull(), + image: text('image'), // Better Auth uses 'image' not 'avatarUrl' createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + // Custom fields (not required by Better Auth) + role: userRoleEnum('role').default('user').notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }), }); -// Sessions table +// Sessions table (Better Auth schema) export const sessions = authSchema.table('sessions', { - id: uuid('id').primaryKey().defaultRandom(), - userId: uuid('user_id') - .references(() => users.id, { onDelete: 'cascade' }) - .notNull(), + id: text('id').primaryKey(), // Better Auth generates nanoid + expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), token: text('token').unique().notNull(), - refreshToken: text('refresh_token').unique().notNull(), - refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }).notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), ipAddress: text('ip_address'), userAgent: text('user_agent'), + userId: text('user_id') + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), + // Custom fields (not required by Better Auth) + refreshToken: text('refresh_token').unique(), + refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }), deviceId: text('device_id'), deviceName: text('device_name'), - lastActivityAt: timestamp('last_activity_at', { withTimezone: true }).defaultNow().notNull(), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), + lastActivityAt: timestamp('last_activity_at', { withTimezone: true }).defaultNow(), revokedAt: timestamp('revoked_at', { withTimezone: true }), }); -// Accounts table (for OAuth providers) +// Accounts table (for OAuth providers and credentials - Better Auth schema) export const accounts = authSchema.table('accounts', { - id: uuid('id').primaryKey().defaultRandom(), - userId: uuid('user_id') + id: text('id').primaryKey(), // Better Auth generates nanoid + accountId: text('account_id').notNull(), // Better Auth field + providerId: text('provider_id').notNull(), // Better Auth field (was 'provider') + userId: text('user_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(), - provider: text('provider').notNull(), // 'google', 'github', 'apple', etc. - providerAccountId: text('provider_account_id').notNull(), accessToken: text('access_token'), refreshToken: text('refresh_token'), - expiresAt: timestamp('expires_at', { withTimezone: true }), - tokenType: text('token_type'), - scope: text('scope'), idToken: text('id_token'), - metadata: jsonb('metadata'), + accessTokenExpiresAt: timestamp('access_token_expires_at', { withTimezone: true }), + refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }), + scope: text('scope'), + password: text('password'), // Better Auth stores hashed password here for credential provider createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), }); -// Verification tokens (for email verification, password reset) -export const verificationTokens = authSchema.table('verification_tokens', { - id: uuid('id').primaryKey().defaultRandom(), - userId: uuid('user_id') - .references(() => users.id, { onDelete: 'cascade' }) - .notNull(), - token: text('token').unique().notNull(), - type: text('type').notNull(), // 'email_verification', 'password_reset' - expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - usedAt: timestamp('used_at', { withTimezone: true }), -}); +// Verification table (Better Auth schema - for email verification, password reset) +export const verificationTokens = authSchema.table( + 'verification', + { + id: text('id').primaryKey(), // Better Auth generates nanoid + identifier: text('identifier').notNull(), // Better Auth uses identifier (e.g., email) + value: text('value').notNull(), // Better Auth uses value (the token) + expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => ({ + identifierIdx: index('verification_identifier_idx').on(table.identifier), + }) +); // Password table (separate for security) export const passwords = authSchema.table('passwords', { - userId: uuid('user_id') + userId: text('user_id') .primaryKey() .references(() => users.id, { onDelete: 'cascade' }), hashedPassword: text('hashed_password').notNull(), @@ -82,23 +87,31 @@ export const passwords = authSchema.table('passwords', { // Two-factor authentication export const twoFactorAuth = authSchema.table('two_factor_auth', { - userId: uuid('user_id') + userId: text('user_id') .primaryKey() .references(() => users.id, { onDelete: 'cascade' }), secret: text('secret').notNull(), enabled: boolean('enabled').default(false).notNull(), - backupCodes: jsonb('backup_codes'), // Array of hashed backup codes + backupCodes: jsonb('backup_codes'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), enabledAt: timestamp('enabled_at', { withTimezone: true }), }); // Security events log export const securityEvents = authSchema.table('security_events', { - id: uuid('id').primaryKey().defaultRandom(), - userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }), - eventType: text('event_type').notNull(), // 'login', 'logout', 'password_reset', 'suspicious_activity' + id: uuid('id').primaryKey().defaultRandom(), // Our table, can keep UUID + userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }), + eventType: text('event_type').notNull(), ipAddress: text('ip_address'), userAgent: text('user_agent'), metadata: jsonb('metadata'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), }); + +// JWKS table (Better Auth JWT plugin - stores signing keys) +export const jwks = authSchema.table('jwks', { + id: text('id').primaryKey(), + publicKey: text('public_key').notNull(), + privateKey: text('private_key').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), +}); diff --git a/services/mana-core-auth/src/db/schema/credits.schema.ts b/services/mana-core-auth/src/db/schema/credits.schema.ts index bdb1edefa..6a33bacca 100644 --- a/services/mana-core-auth/src/db/schema/credits.schema.ts +++ b/services/mana-core-auth/src/db/schema/credits.schema.ts @@ -34,7 +34,7 @@ export const transactionStatusEnum = pgEnum('transaction_status', [ // Credit balances (one per user) export const balances = creditsSchema.table('balances', { - userId: uuid('user_id') + userId: text('user_id') .primaryKey() .references(() => users.id, { onDelete: 'cascade' }), balance: integer('balance').default(0).notNull(), @@ -43,7 +43,7 @@ export const balances = creditsSchema.table('balances', { lastDailyResetAt: timestamp('last_daily_reset_at', { withTimezone: true }).defaultNow(), totalEarned: integer('total_earned').default(0).notNull(), totalSpent: integer('total_spent').default(0).notNull(), - version: integer('version').default(0).notNull(), // For optimistic locking + version: integer('version').default(0).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), }); @@ -53,7 +53,7 @@ export const transactions = creditsSchema.table( 'transactions', { id: uuid('id').primaryKey().defaultRandom(), - userId: uuid('user_id') + userId: text('user_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(), type: transactionTypeEnum('type').notNull(), @@ -61,10 +61,10 @@ export const transactions = creditsSchema.table( amount: integer('amount').notNull(), balanceBefore: integer('balance_before').notNull(), balanceAfter: integer('balance_after').notNull(), - appId: text('app_id').notNull(), // 'memoro', 'chat', 'picture', etc. + appId: text('app_id').notNull(), description: text('description').notNull(), - organizationId: text('organization_id').references(() => organizations.id), // NULL for B2C, set for B2B - metadata: jsonb('metadata'), // Additional context + organizationId: text('organization_id').references(() => organizations.id), + metadata: jsonb('metadata'), idempotencyKey: text('idempotency_key').unique(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), completedAt: timestamp('completed_at', { withTimezone: true }), @@ -83,8 +83,8 @@ export const packages = creditsSchema.table('packages', { id: uuid('id').primaryKey().defaultRandom(), name: text('name').notNull(), description: text('description'), - credits: integer('credits').notNull(), // Number of credits - priceEuroCents: integer('price_euro_cents').notNull(), // Price in euro cents + credits: integer('credits').notNull(), + priceEuroCents: integer('price_euro_cents').notNull(), stripePriceId: text('stripe_price_id').unique(), active: boolean('active').default(true).notNull(), sortOrder: integer('sort_order').default(0).notNull(), @@ -98,7 +98,7 @@ export const purchases = creditsSchema.table( 'purchases', { id: uuid('id').primaryKey().defaultRandom(), - userId: uuid('user_id') + userId: text('user_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(), packageId: uuid('package_id').references(() => packages.id), @@ -124,7 +124,7 @@ export const usageStats = creditsSchema.table( 'usage_stats', { id: uuid('id').primaryKey().defaultRandom(), - userId: uuid('user_id') + userId: text('user_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(), appId: text('app_id').notNull(), @@ -143,12 +143,12 @@ export const organizationBalances = creditsSchema.table('organization_balances', organizationId: text('organization_id') .primaryKey() .references(() => organizations.id, { onDelete: 'cascade' }), - balance: integer('balance').default(0).notNull(), // Total purchased credits - allocatedCredits: integer('allocated_credits').default(0).notNull(), // Sum of credits allocated to employees - availableCredits: integer('available_credits').default(0).notNull(), // balance - allocated_credits - totalPurchased: integer('total_purchased').default(0).notNull(), // Total credits ever purchased - totalAllocated: integer('total_allocated').default(0).notNull(), // Total ever allocated - version: integer('version').default(0).notNull(), // For optimistic locking + balance: integer('balance').default(0).notNull(), + allocatedCredits: integer('allocated_credits').default(0).notNull(), + availableCredits: integer('available_credits').default(0).notNull(), + totalPurchased: integer('total_purchased').default(0).notNull(), + totalAllocated: integer('total_allocated').default(0).notNull(), + version: integer('version').default(0).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), }); @@ -161,17 +161,17 @@ export const creditAllocations = creditsSchema.table( organizationId: text('organization_id') .references(() => organizations.id, { onDelete: 'cascade' }) .notNull(), - employeeId: uuid('employee_id') + employeeId: text('employee_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(), - amount: integer('amount').notNull(), // Amount allocated (can be positive or negative) - allocatedBy: uuid('allocated_by') + amount: integer('amount').notNull(), + allocatedBy: text('allocated_by') .references(() => users.id) - .notNull(), // Owner or admin who made the allocation - reason: text('reason'), // Optional reason for allocation - balanceBefore: integer('balance_before').notNull(), // Employee balance before - balanceAfter: integer('balance_after').notNull(), // Employee balance after - metadata: jsonb('metadata'), // Additional context + .notNull(), + reason: text('reason'), + balanceBefore: integer('balance_before').notNull(), + balanceAfter: integer('balance_after').notNull(), + metadata: jsonb('metadata'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), }, (table) => ({