insforge 0.3.3 → 1.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (507) hide show
  1. package/.claude-plugin/marketplace.json +20 -0
  2. package/.cursor/rules/cursor-rules.mdc +94 -0
  3. package/.dockerignore +3 -0
  4. package/.env.example +33 -4
  5. package/.github/ISSUE_TEMPLATE/bug_report.yml +13 -60
  6. package/.github/ISSUE_TEMPLATE/config.yml +2 -2
  7. package/.github/ISSUE_TEMPLATE/feature_request.yml +10 -63
  8. package/.github/PULL_REQUEST_TEMPLATE.md +7 -0
  9. package/.github/workflows/build-image.yml +2 -1
  10. package/.github/workflows/e2e.yml +63 -0
  11. package/CHANGELOG.md +41 -0
  12. package/CLAUDE_PLUGIN.md +104 -0
  13. package/CODE_OF_CONDUCT.md +128 -0
  14. package/CONTRIBUTING.md +1 -1
  15. package/Dockerfile +4 -1
  16. package/README.md +66 -18
  17. package/assets/mcpInstallv2.png +0 -0
  18. package/assets/sampleResponse.png +0 -0
  19. package/auth/index.html +13 -0
  20. package/auth/package.json +28 -0
  21. package/auth/public/favicon.ico +0 -0
  22. package/auth/src/App.tsx +33 -0
  23. package/auth/src/components/ErrorCard.tsx +37 -0
  24. package/auth/src/components/Layout.tsx +13 -0
  25. package/auth/src/index.css +19 -0
  26. package/auth/src/lib/broadcastService.ts +115 -0
  27. package/auth/src/lib/utils.ts +11 -0
  28. package/auth/src/main.tsx +22 -0
  29. package/auth/src/pages/ForgotPasswordPage.tsx +11 -0
  30. package/auth/src/pages/ResetPasswordPage.tsx +11 -0
  31. package/auth/src/pages/SignInPage.tsx +57 -0
  32. package/auth/src/pages/SignUpPage.tsx +57 -0
  33. package/auth/src/pages/VerifyEmailPage.tsx +20 -0
  34. package/auth/src/vite-env.d.ts +10 -0
  35. package/auth/tsconfig.json +32 -0
  36. package/auth/tsconfig.node.json +11 -0
  37. package/auth/vite.config.ts +25 -0
  38. package/backend/package.json +9 -9
  39. package/backend/src/api/{middleware → middlewares}/auth.ts +8 -9
  40. package/backend/src/api/middlewares/rate-limiters.ts +127 -0
  41. package/backend/src/api/routes/{ai.ts → ai/index.routes.ts} +20 -24
  42. package/backend/src/api/routes/auth/index.routes.ts +570 -0
  43. package/backend/src/api/routes/auth/oauth.routes.ts +448 -0
  44. package/backend/src/api/routes/{database.advance.ts → database/advance.routes.ts} +107 -65
  45. package/backend/src/api/routes/database/index.routes.ts +13 -0
  46. package/backend/src/api/routes/{database.records.ts → database/records.routes.ts} +22 -8
  47. package/backend/src/api/routes/{database.tables.ts → database/tables.routes.ts} +20 -23
  48. package/backend/src/api/routes/docs/index.routes.ts +76 -0
  49. package/backend/src/api/routes/functions/index.routes.ts +188 -0
  50. package/backend/src/api/routes/{logs.ts → logs/index.routes.ts} +25 -30
  51. package/backend/src/api/routes/{metadata.ts → metadata/index.routes.ts} +21 -31
  52. package/backend/src/api/routes/{secrets.ts → secrets/index.routes.ts} +27 -22
  53. package/backend/src/api/routes/{storage.ts → storage/index.routes.ts} +34 -53
  54. package/backend/src/api/routes/usage/index.routes.ts +89 -0
  55. package/backend/src/infra/config/app.config.ts +51 -0
  56. package/backend/src/{core/database/manager.ts → infra/database/database.manager.ts} +76 -85
  57. package/backend/src/infra/database/migrations/013_create-auth-schema-functions.sql +44 -0
  58. package/backend/src/infra/database/migrations/014_add-updated-at-trigger-user-table.sql +8 -0
  59. package/backend/src/infra/database/migrations/015_create-auth-config-and-email-otp-tables.sql +60 -0
  60. package/backend/src/infra/database/migrations/016_update-auth-config-and-email-otp.sql +24 -0
  61. package/backend/src/{core/secrets/encryption.ts → infra/security/encryption.manager.ts} +3 -2
  62. package/backend/src/infra/security/token.manager.ts +125 -0
  63. package/backend/src/{core/socket/socket.ts → infra/socket/socket.manager.ts} +15 -15
  64. package/backend/src/providers/ai/openrouter.provider.ts +377 -0
  65. package/backend/src/providers/email/base.provider.ts +41 -0
  66. package/backend/src/providers/email/cloud.provider.ts +187 -0
  67. package/backend/src/{core/logs/providers → providers/logs}/base.provider.ts +11 -11
  68. package/backend/src/{core/logs/providers → providers/logs}/cloudwatch.provider.ts +61 -38
  69. package/backend/src/providers/logs/local.provider.ts +185 -0
  70. package/backend/src/providers/oauth/base.provider.ts +29 -0
  71. package/backend/src/providers/oauth/discord.provider.ts +195 -0
  72. package/backend/src/providers/oauth/facebook.provider.ts +194 -0
  73. package/backend/src/providers/oauth/github.provider.ts +208 -0
  74. package/backend/src/providers/oauth/google.provider.ts +249 -0
  75. package/backend/src/providers/oauth/index.ts +7 -0
  76. package/backend/src/providers/oauth/linkedin.provider.ts +240 -0
  77. package/backend/src/providers/oauth/microsoft.provider.ts +169 -0
  78. package/backend/src/providers/oauth/x.provider.ts +202 -0
  79. package/backend/src/providers/storage/base.provider.ts +29 -0
  80. package/backend/src/providers/storage/local.provider.ts +103 -0
  81. package/backend/src/providers/storage/s3.provider.ts +313 -0
  82. package/backend/src/server.ts +70 -74
  83. package/backend/src/{core/ai/config.ts → services/ai/ai-config.service.ts} +19 -24
  84. package/backend/src/services/ai/ai-model.service.ts +60 -0
  85. package/backend/src/{core/ai/usage.ts → services/ai/ai-usage.service.ts} +28 -35
  86. package/backend/src/{core/ai/chat.ts → services/ai/chat-completion.service.ts} +37 -24
  87. package/backend/src/services/ai/helpers.ts +64 -0
  88. package/backend/src/{core/ai/image.ts → services/ai/image-generation.service.ts} +17 -19
  89. package/backend/src/services/ai/index.ts +13 -0
  90. package/backend/src/services/auth/auth-config.service.ts +250 -0
  91. package/backend/src/services/auth/auth-otp.service.ts +424 -0
  92. package/backend/src/services/auth/auth.service.ts +1136 -0
  93. package/backend/src/services/auth/index.ts +4 -0
  94. package/backend/src/{core/auth/oauth.ts → services/auth/oauth-config.service.ts} +106 -52
  95. package/backend/src/{core/database/advance.ts → services/database/database-advance.service.ts} +97 -131
  96. package/backend/src/services/database/database-table.service.ts +811 -0
  97. package/backend/src/services/email/email.service.ts +75 -0
  98. package/backend/src/{core/functions/functions.ts → services/functions/function.service.ts} +95 -88
  99. package/backend/src/{core/logs/audit.ts → services/logs/audit.service.ts} +92 -75
  100. package/backend/src/services/logs/log.service.ts +73 -0
  101. package/backend/src/{core/secrets/secrets.ts → services/secrets/secret.service.ts} +48 -66
  102. package/backend/src/services/storage/storage.service.ts +617 -0
  103. package/backend/src/services/usage/usage.service.ts +149 -0
  104. package/backend/src/types/auth.ts +66 -2
  105. package/backend/src/types/email.ts +8 -0
  106. package/backend/src/types/error-constants.ts +4 -0
  107. package/backend/src/types/logs.ts +0 -29
  108. package/backend/src/{core/socket/types.ts → types/socket.ts} +5 -6
  109. package/backend/src/utils/environment.ts +9 -3
  110. package/backend/src/utils/logger.ts +20 -2
  111. package/backend/src/utils/seed.ts +150 -57
  112. package/backend/src/utils/sql-parser.ts +1 -1
  113. package/backend/src/utils/utils.ts +114 -0
  114. package/backend/src/utils/validations.ts +40 -4
  115. package/backend/tests/local/test-ai-config.sh +129 -0
  116. package/backend/tests/local/test-ai-usage.sh +80 -0
  117. package/backend/tests/local/test-auth-router.sh +1 -1
  118. package/backend/tests/local/test-e2e.sh +1 -1
  119. package/backend/tests/local/test-functions.sh +123 -0
  120. package/backend/tests/local/test-logs.sh +132 -0
  121. package/backend/tests/local/test-public-bucket.sh +3 -3
  122. package/backend/tests/local/test-secrets.sh +14 -12
  123. package/backend/tests/local/test-traditional-rest.sh +2 -2
  124. package/backend/tests/manual/test-rawsql-modes.sh +244 -0
  125. package/backend/tests/test-config.sh +37 -1
  126. package/backend/tests/unit/cloud-token.test.ts +48 -0
  127. package/backend/tests/unit/constant.test.ts +8 -0
  128. package/backend/tests/unit/email.test.ts +372 -0
  129. package/backend/tests/unit/environment.test.ts +59 -0
  130. package/backend/tests/unit/helpers.test.ts +63 -0
  131. package/backend/tests/unit/logger.test.ts +22 -0
  132. package/backend/tests/unit/rate-limit.test.ts +154 -0
  133. package/backend/tests/unit/response.test.ts +58 -0
  134. package/backend/tests/unit/sql-parser.test.ts +74 -0
  135. package/backend/tests/unit/uuid.test.ts +21 -0
  136. package/backend/tests/unit/validations.test.ts +80 -0
  137. package/backend/tsconfig.json +1 -1
  138. package/backend/vitest.config.ts +11 -0
  139. package/claude-plugin/.claude-plugin/plugin.json +24 -0
  140. package/claude-plugin/README.md +133 -0
  141. package/claude-plugin/skills/insforge-schema-patterns/SKILL.md +270 -0
  142. package/docker-compose.prod.yml +60 -4
  143. package/docker-compose.yml +65 -4
  144. package/docker-init/db/db-init.sql +6 -34
  145. package/docker-init/logs/vector.yml +236 -0
  146. package/docs/README.md +44 -0
  147. package/docs/changelog.mdx +67 -0
  148. package/docs/core-concepts/ai/architecture.mdx +373 -0
  149. package/docs/core-concepts/ai/sdk.mdx +213 -0
  150. package/docs/core-concepts/authentication/architecture.mdx +278 -0
  151. package/docs/core-concepts/authentication/sdk.mdx +414 -0
  152. package/docs/core-concepts/authentication/ui-components/customization.mdx +529 -0
  153. package/docs/core-concepts/authentication/ui-components/nextjs.mdx +221 -0
  154. package/docs/core-concepts/authentication/ui-components/react-router.mdx +184 -0
  155. package/docs/core-concepts/authentication/ui-components/react.mdx +129 -0
  156. package/docs/core-concepts/database/architecture.mdx +256 -0
  157. package/docs/core-concepts/database/sdk.mdx +382 -0
  158. package/docs/core-concepts/functions/architecture.mdx +105 -0
  159. package/docs/core-concepts/functions/sdk.mdx +184 -0
  160. package/docs/core-concepts/storage/architecture.mdx +243 -0
  161. package/docs/core-concepts/storage/sdk.mdx +253 -0
  162. package/docs/deployment/README.md +94 -0
  163. package/docs/deployment/deploy-to-aws-ec2.md +565 -0
  164. package/docs/deployment/deploy-to-azure-virtual-machines.md +313 -0
  165. package/docs/deployment/deploy-to-google-cloud-compute-engine.md +613 -0
  166. package/docs/deployment/deploy-to-render.md +441 -0
  167. package/docs/docs.json +210 -0
  168. package/docs/examples/framework-guides/nextjs.mdx +131 -0
  169. package/docs/examples/framework-guides/nuxt.mdx +165 -0
  170. package/docs/examples/framework-guides/react.mdx +165 -0
  171. package/docs/examples/framework-guides/svelte.mdx +153 -0
  172. package/docs/examples/framework-guides/vue.mdx +159 -0
  173. package/docs/examples/overview.mdx +67 -0
  174. package/docs/favicon.svg +19 -0
  175. package/docs/images/changelog/nov-2025/auth-components.webp +0 -0
  176. package/docs/images/changelog/nov-2025/database-metadata.webp +0 -0
  177. package/docs/images/changelog/nov-2025/quickstart-prompts.webp +0 -0
  178. package/docs/images/changelog/nov-2025/sql-editor.webp +0 -0
  179. package/docs/images/changelog/nov-2025/usage-page.webp +0 -0
  180. package/docs/images/changelog/october-2025/csv-upload.webp +0 -0
  181. package/docs/images/changelog/october-2025/logs-feature.webp +0 -0
  182. package/docs/images/changelog/october-2025/oauth-providers.webp +0 -0
  183. package/docs/images/checks-passed.png +0 -0
  184. package/docs/images/dashboard-connect-expanded.png +0 -0
  185. package/docs/images/dashboard-connect.png +0 -0
  186. package/docs/images/hero-dark.png +0 -0
  187. package/docs/images/hero-light.png +0 -0
  188. package/docs/images/icons/ai.svg +4 -0
  189. package/docs/images/icons/auth.svg +1 -0
  190. package/docs/images/icons/database.svg +1 -0
  191. package/docs/images/icons/function.svg +1 -0
  192. package/docs/images/icons/storage.svg +1 -0
  193. package/docs/images/logos/nextjs.svg +4 -0
  194. package/docs/images/logos/nuxt.svg +4 -0
  195. package/docs/images/logos/react.svg +5 -0
  196. package/docs/images/logos/svelte.svg +4 -0
  197. package/docs/images/logos/vue.svg +5 -0
  198. package/docs/images/mcp-install.png +0 -0
  199. package/docs/images/onboarding-mcp.png +0 -0
  200. package/docs/insforge-instructions-sdk.md +55 -374
  201. package/docs/introduction.mdx +45 -0
  202. package/docs/logo/dark.svg +22 -0
  203. package/docs/logo/light.svg +20 -0
  204. package/docs/partnership.mdx +647 -0
  205. package/docs/quickstart.mdx +83 -0
  206. package/docs/showcase/2048-arena.png +0 -0
  207. package/docs/showcase/framegen-cloud.png +0 -0
  208. package/docs/showcase/line-connect-race.png +0 -0
  209. package/docs/showcase/moment-vibe.png +0 -0
  210. package/docs/showcase/national-flags.png +0 -0
  211. package/docs/showcase/pokemon-vibe.png +0 -0
  212. package/docs/showcase/pure-browse-buy.png +0 -0
  213. package/docs/showcase.mdx +52 -0
  214. package/docs/snippets/sdk-installation.mdx +22 -0
  215. package/docs/snippets/service-icons.mdx +27 -0
  216. package/eslint.config.js +10 -3
  217. package/frontend/package.json +10 -4
  218. package/frontend/src/App.tsx +13 -82
  219. package/frontend/src/assets/icons/connected.svg +3 -0
  220. package/frontend/src/assets/icons/loader.svg +9 -0
  221. package/frontend/src/assets/logos/apple.svg +4 -0
  222. package/frontend/src/assets/logos/discord.svg +1 -1
  223. package/frontend/src/assets/logos/facebook.svg +3 -0
  224. package/frontend/src/assets/logos/instagram.svg +2 -0
  225. package/frontend/src/assets/logos/linkedin.svg +3 -0
  226. package/frontend/src/assets/logos/microsoft.svg +1 -0
  227. package/frontend/src/assets/logos/spotify.svg +17 -0
  228. package/frontend/src/assets/logos/tiktok.svg +6 -0
  229. package/frontend/src/assets/logos/x.svg +3 -0
  230. package/frontend/src/components/Checkbox.tsx +27 -29
  231. package/frontend/src/components/CodeBlock.tsx +55 -2
  232. package/frontend/src/components/CodeEditor.tsx +92 -0
  233. package/frontend/src/components/ConfirmDialog.tsx +1 -1
  234. package/frontend/src/components/ConnectCTA.tsx +38 -0
  235. package/frontend/src/components/CopyButton.tsx +52 -15
  236. package/frontend/src/components/ErrorState.tsx +1 -2
  237. package/frontend/src/components/FeatureSidebar.tsx +6 -6
  238. package/frontend/src/components/FeatureSidebarItem.tsx +2 -2
  239. package/frontend/src/components/JsonHighlight.tsx +21 -9
  240. package/frontend/src/components/ProjectInfoModal.tsx +128 -0
  241. package/frontend/src/components/PromptDialog.tsx +1 -4
  242. package/frontend/src/components/SearchInput.tsx +1 -2
  243. package/frontend/src/components/Stepper.tsx +53 -0
  244. package/frontend/src/components/ThemeToggle.tsx +3 -3
  245. package/frontend/src/components/datagrid/DataGrid.tsx +25 -32
  246. package/frontend/src/components/datagrid/cell-editors/DateCellEditor.tsx +1 -2
  247. package/frontend/src/components/datagrid/cell-editors/JsonCellEditor.tsx +2 -4
  248. package/frontend/src/components/datagrid/index.ts +23 -0
  249. package/frontend/src/components/index.ts +23 -30
  250. package/frontend/src/components/layout/AppHeader.tsx +133 -92
  251. package/frontend/src/components/layout/AppSidebar.tsx +80 -170
  252. package/frontend/src/components/layout/Layout.tsx +12 -23
  253. package/frontend/src/components/layout/PrimaryMenu.tsx +187 -0
  254. package/frontend/src/components/layout/SecondaryMenu.tsx +70 -0
  255. package/frontend/src/components/layout/index.ts +5 -0
  256. package/frontend/src/components/radix/Tooltip.tsx +24 -13
  257. package/frontend/src/components/radix/index.ts +22 -0
  258. package/frontend/src/features/ai/components/AIConfigCard.tsx +129 -83
  259. package/frontend/src/features/ai/components/AIEmptyState.tsx +12 -7
  260. package/frontend/src/features/ai/components/ModalityFilterSidebar.tsx +101 -0
  261. package/frontend/src/features/ai/components/ModelSelectionDialog.tsx +135 -0
  262. package/frontend/src/features/ai/components/ModelSelectionGrid.tsx +51 -0
  263. package/frontend/src/features/ai/components/SystemPromptDialog.tsx +118 -0
  264. package/frontend/src/features/ai/components/index.ts +6 -0
  265. package/frontend/src/features/ai/helpers.ts +57 -71
  266. package/frontend/src/features/ai/hooks/useAIConfigs.ts +39 -113
  267. package/frontend/src/features/ai/hooks/useAIUsage.ts +0 -2
  268. package/frontend/src/features/ai/page/AIPage.tsx +67 -79
  269. package/frontend/src/features/ai/services/ai.service.ts +5 -5
  270. package/frontend/src/features/auth/components/AuthPreview.tsx +96 -0
  271. package/frontend/src/features/auth/components/OAuthConfigDialog.tsx +53 -30
  272. package/frontend/src/features/auth/components/UserFormDialog.tsx +13 -6
  273. package/frontend/src/features/auth/components/UsersDataGrid.tsx +44 -14
  274. package/frontend/src/features/auth/components/index.ts +5 -0
  275. package/frontend/src/features/auth/helpers.tsx +200 -0
  276. package/frontend/src/features/auth/hooks/useAnonToken.ts +30 -0
  277. package/frontend/src/features/auth/hooks/useAuthConfig.ts +48 -0
  278. package/frontend/src/features/auth/hooks/useOAuthConfig.ts +14 -10
  279. package/frontend/src/features/auth/hooks/useUsers.ts +43 -5
  280. package/frontend/src/features/auth/index.ts +3 -2
  281. package/frontend/src/features/auth/page/AuthMethodsPage.tsx +275 -0
  282. package/frontend/src/features/auth/page/ConfigurationPage.tsx +395 -0
  283. package/frontend/src/features/auth/page/UsersPage.tsx +285 -0
  284. package/frontend/src/features/auth/services/anonToken.service.ts +11 -0
  285. package/frontend/src/features/auth/services/config.service.ts +19 -0
  286. package/frontend/src/features/auth/services/{oauth.service.ts → oauth-config.service.ts} +4 -4
  287. package/frontend/src/features/auth/services/{auth.service.ts → user.service.ts} +7 -53
  288. package/frontend/src/features/dashboard/components/ConnectionSuccessBanner.tsx +35 -0
  289. package/frontend/src/features/dashboard/components/PromptCard.tsx +21 -0
  290. package/frontend/src/features/dashboard/components/PromptDialog.tsx +103 -0
  291. package/frontend/src/features/dashboard/components/StatsCard.tsx +50 -0
  292. package/frontend/src/features/dashboard/components/index.ts +4 -0
  293. package/frontend/src/features/dashboard/page/DashboardPage.tsx +187 -169
  294. package/frontend/src/features/dashboard/prompts/ai-chatbot.ts +13 -0
  295. package/frontend/src/features/dashboard/prompts/crm-system.ts +13 -0
  296. package/frontend/src/features/dashboard/prompts/ecommerce-platform.ts +12 -0
  297. package/frontend/src/features/dashboard/prompts/index.ts +31 -0
  298. package/frontend/src/features/dashboard/prompts/instagram-clone.ts +11 -0
  299. package/frontend/src/features/dashboard/prompts/notion-clone.ts +14 -0
  300. package/frontend/src/features/dashboard/prompts/reddit-clone.ts +12 -0
  301. package/frontend/src/features/database/components/DatabaseDataGrid.tsx +48 -17
  302. package/frontend/src/features/database/components/ForeignKeyCell.tsx +15 -34
  303. package/frontend/src/features/database/components/ForeignKeyPopover.tsx +19 -20
  304. package/frontend/src/features/database/components/LinkRecordModal.tsx +120 -125
  305. package/frontend/src/features/database/components/RecordFormDialog.tsx +22 -33
  306. package/frontend/src/features/database/components/RecordFormField.tsx +45 -47
  307. package/frontend/src/features/database/components/TableEmptyState.tsx +6 -5
  308. package/frontend/src/features/database/components/TableForm.tsx +28 -15
  309. package/frontend/src/features/database/components/TableFormColumn.tsx +2 -3
  310. package/frontend/src/features/database/components/TableSidebar.tsx +1 -1
  311. package/frontend/src/features/database/components/TablesEmptyState.tsx +48 -0
  312. package/frontend/src/features/database/components/TemplateCard.tsx +37 -0
  313. package/frontend/src/features/database/components/TemplatePreview.tsx +92 -0
  314. package/frontend/src/features/database/components/index.ts +19 -0
  315. package/frontend/src/features/database/constants.ts +28 -2
  316. package/frontend/src/features/database/contexts/SQLEditorContext.tsx +188 -0
  317. package/frontend/src/features/database/helpers.ts +2 -2
  318. package/frontend/src/features/database/hooks/useCSVImport.ts +29 -0
  319. package/frontend/src/features/database/hooks/useFullMetadata.ts +18 -0
  320. package/frontend/src/features/database/hooks/useRawSQL.ts +55 -0
  321. package/frontend/src/features/database/hooks/useRecords.ts +139 -0
  322. package/frontend/src/features/database/hooks/useTables.ts +131 -0
  323. package/frontend/src/features/database/index.ts +6 -1
  324. package/frontend/src/features/database/page/FunctionsPage.tsx +211 -0
  325. package/frontend/src/features/database/page/IndexesPage.tsx +240 -0
  326. package/frontend/src/features/database/page/PoliciesPage.tsx +248 -0
  327. package/frontend/src/features/database/page/SQLEditorPage.tsx +382 -0
  328. package/frontend/src/features/database/page/{DatabasePage.tsx → TablesPage.tsx} +186 -185
  329. package/frontend/src/features/database/page/TemplatesPage.tsx +39 -0
  330. package/frontend/src/features/database/page/TriggersPage.tsx +242 -0
  331. package/frontend/src/features/database/services/advance.service.ts +66 -0
  332. package/frontend/src/features/database/services/{database.service.ts → record.service.ts} +67 -64
  333. package/frontend/src/features/database/services/table.service.ts +64 -0
  334. package/frontend/src/features/database/templates/ai-chatbot.ts +402 -0
  335. package/frontend/src/features/database/templates/crm-system.ts +528 -0
  336. package/frontend/src/features/database/templates/ecommerce-platform.ts +553 -0
  337. package/frontend/src/features/database/templates/index.ts +34 -0
  338. package/frontend/src/features/database/templates/instagram-clone.ts +222 -0
  339. package/frontend/src/features/database/templates/notion-clone.ts +483 -0
  340. package/frontend/src/features/database/templates/reddit-clone.ts +526 -0
  341. package/frontend/src/features/functions/components/FunctionRow.tsx +2 -1
  342. package/frontend/src/features/functions/components/FunctionsSidebar.tsx +1 -1
  343. package/frontend/src/features/functions/components/SecretRow.tsx +1 -1
  344. package/frontend/src/features/functions/components/index.ts +5 -0
  345. package/frontend/src/features/functions/hooks/useFunctions.ts +4 -4
  346. package/frontend/src/features/{secrets → functions}/hooks/useSecrets.ts +5 -5
  347. package/frontend/src/features/functions/page/FunctionsPage.tsx +160 -17
  348. package/frontend/src/features/functions/{components/SecretsContent.tsx → page/SecretsPage.tsx} +8 -12
  349. package/frontend/src/features/functions/services/{functions.service.ts → function.service.ts} +2 -2
  350. package/frontend/src/features/{secrets/services/secrets.service.ts → functions/services/secret.service.ts} +2 -2
  351. package/frontend/src/features/login/hooks/usePartnerOrigin.ts +27 -0
  352. package/frontend/src/features/login/page/CloudLoginPage.tsx +79 -54
  353. package/frontend/src/features/login/page/LoginPage.tsx +16 -23
  354. package/frontend/src/features/login/services/partnership.service.ts +65 -0
  355. package/frontend/src/features/logs/components/LogsDataGrid.tsx +89 -0
  356. package/frontend/src/features/logs/components/SeverityBadge.tsx +18 -0
  357. package/frontend/src/features/logs/components/index.ts +2 -0
  358. package/frontend/src/features/logs/helpers.ts +24 -0
  359. package/frontend/src/features/logs/hooks/useAuditLogs.ts +4 -4
  360. package/frontend/src/features/logs/hooks/useLogSources.ts +137 -0
  361. package/frontend/src/features/logs/hooks/useLogs.ts +163 -0
  362. package/frontend/src/features/logs/hooks/useMcpUsage.ts +181 -0
  363. package/frontend/src/features/logs/index.ts +8 -2
  364. package/frontend/src/features/logs/page/AuditsPage.tsx +91 -38
  365. package/frontend/src/features/logs/page/LogsPage.tsx +152 -0
  366. package/frontend/src/features/logs/page/MCPLogsPage.tsx +84 -0
  367. package/frontend/src/features/logs/services/audit.service.ts +63 -0
  368. package/frontend/src/features/logs/services/log.service.ts +15 -110
  369. package/frontend/src/features/logs/services/usage.service.ts +31 -0
  370. package/frontend/src/features/onboard/components/McpConnectionStatus.tsx +68 -0
  371. package/frontend/src/features/onboard/components/OnboardingModal.tsx +267 -0
  372. package/frontend/src/features/onboard/components/VideoDemoModal.tsx +38 -0
  373. package/frontend/src/features/onboard/components/index.ts +4 -0
  374. package/frontend/src/features/onboard/components/mcp/CursorDeeplinkGenerator.tsx +2 -2
  375. package/frontend/src/features/onboard/components/mcp/{mcp-helper.tsx → helpers.tsx} +8 -8
  376. package/frontend/src/features/onboard/components/mcp/index.ts +2 -3
  377. package/frontend/src/features/onboard/index.ts +13 -3
  378. package/frontend/src/features/storage/components/BucketEmptyState.tsx +9 -6
  379. package/frontend/src/features/storage/components/BucketFormDialog.tsx +25 -41
  380. package/frontend/src/features/storage/components/FilePreviewDialog.tsx +20 -8
  381. package/frontend/src/features/storage/components/StorageDataGrid.tsx +4 -3
  382. package/frontend/src/features/storage/components/StorageManager.tsx +23 -34
  383. package/frontend/src/features/storage/components/index.ts +12 -0
  384. package/frontend/src/features/storage/hooks/useStorage.ts +208 -0
  385. package/frontend/src/features/storage/page/StoragePage.tsx +41 -115
  386. package/frontend/src/features/storage/services/storage.service.ts +22 -1
  387. package/frontend/src/features/visualizer/components/AuthNode.tsx +72 -56
  388. package/frontend/src/features/visualizer/components/BucketNode.tsx +4 -4
  389. package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +108 -80
  390. package/frontend/src/features/visualizer/components/TableNode.tsx +34 -41
  391. package/frontend/src/features/visualizer/components/VisualizerSkeleton.tsx +12 -4
  392. package/frontend/src/features/visualizer/page/VisualizerPage.tsx +33 -29
  393. package/frontend/src/index.css +1 -0
  394. package/frontend/src/lib/analytics/posthog.tsx +27 -0
  395. package/frontend/src/lib/contexts/AuthContext.tsx +38 -31
  396. package/frontend/src/lib/contexts/SocketContext.tsx +5 -6
  397. package/frontend/src/{features/metadata → lib}/hooks/useMetadata.ts +1 -1
  398. package/frontend/src/lib/hooks/useToast.tsx +6 -2
  399. package/frontend/src/lib/routing/AppRoutes.tsx +84 -0
  400. package/frontend/src/lib/routing/RequireAuth.tsx +27 -0
  401. package/frontend/src/lib/utils/cloudMessaging.ts +20 -0
  402. package/frontend/src/lib/utils/menuItems.ts +183 -0
  403. package/frontend/src/lib/utils/{validation-schemas.ts → schemaValidations.ts} +10 -5
  404. package/frontend/src/lib/utils/utils.ts +19 -1
  405. package/frontend/src/vite-env.d.ts +1 -0
  406. package/frontend/vite.config.ts +5 -3
  407. package/functions/server.ts +28 -3
  408. package/functions/worker-template.js +15 -4
  409. package/i18n/README.ar.md +130 -0
  410. package/i18n/README.de.md +130 -0
  411. package/i18n/README.es.md +154 -0
  412. package/i18n/README.fr.md +134 -0
  413. package/i18n/README.hi.md +129 -0
  414. package/i18n/README.ja.md +174 -0
  415. package/i18n/README.ko.md +137 -0
  416. package/i18n/README.pt-BR.md +131 -0
  417. package/i18n/README.ru.md +129 -0
  418. package/i18n/README.zh-CN.md +133 -0
  419. package/openapi/ai.yaml +31 -4
  420. package/openapi/auth.yaml +827 -146
  421. package/package.json +16 -7
  422. package/shared-schemas/package.json +1 -1
  423. package/shared-schemas/src/ai-api.schema.ts +34 -58
  424. package/shared-schemas/src/ai.schema.ts +5 -0
  425. package/shared-schemas/src/auth-api.schema.ts +154 -8
  426. package/shared-schemas/src/auth.schema.ts +42 -6
  427. package/shared-schemas/src/cloud-events.schema.ts +57 -0
  428. package/shared-schemas/src/database-api.schema.ts +3 -3
  429. package/shared-schemas/src/database.schema.ts +1 -1
  430. package/shared-schemas/src/index.ts +1 -0
  431. package/shared-schemas/src/logs-api.schema.ts +7 -1
  432. package/shared-schemas/src/logs.schema.ts +26 -0
  433. package/shared-schemas/src/metadata.schema.ts +9 -4
  434. package/test-gemini.sh +35 -0
  435. package/test-usage-admin.sh +57 -0
  436. package/test-usage.sh +50 -0
  437. package/zeabur/README.md +13 -0
  438. package/zeabur/template.yml +1032 -0
  439. package/.github/workflows/deploy-aws.yml +0 -130
  440. package/backend/src/api/routes/agent.ts +0 -29
  441. package/backend/src/api/routes/auth.oauth.ts +0 -482
  442. package/backend/src/api/routes/auth.ts +0 -386
  443. package/backend/src/api/routes/docs.ts +0 -66
  444. package/backend/src/api/routes/functions.ts +0 -183
  445. package/backend/src/api/routes/openapi.ts +0 -82
  446. package/backend/src/api/routes/usage.ts +0 -96
  447. package/backend/src/core/ai/client.ts +0 -242
  448. package/backend/src/core/ai/model.ts +0 -117
  449. package/backend/src/core/auth/auth.ts +0 -780
  450. package/backend/src/core/database/table.ts +0 -772
  451. package/backend/src/core/documentation/agent.ts +0 -689
  452. package/backend/src/core/documentation/openapi.ts +0 -856
  453. package/backend/src/core/logs/analytics.ts +0 -76
  454. package/backend/src/core/logs/providers/localdb.provider.ts +0 -246
  455. package/backend/src/core/storage/storage.ts +0 -923
  456. package/backend/src/utils/cloud-token.ts +0 -39
  457. package/backend/src/utils/helpers.ts +0 -49
  458. package/backend/src/utils/uuid.ts +0 -9
  459. package/backend/tests/manual/test-better-auth.sh +0 -303
  460. package/docker-init/db/logs.sql +0 -9
  461. package/frontend/README.md +0 -112
  462. package/frontend/src/components/datagrid/index.tsx +0 -20
  463. package/frontend/src/components/layout/CloudLayout.tsx +0 -95
  464. package/frontend/src/features/ai/components/AIConfigDialog.tsx +0 -76
  465. package/frontend/src/features/ai/components/AIConfigForm.tsx +0 -222
  466. package/frontend/src/features/ai/components/fields/ModalityField.tsx +0 -87
  467. package/frontend/src/features/ai/components/fields/ModelSelectionField.tsx +0 -134
  468. package/frontend/src/features/ai/components/fields/SystemPromptField.tsx +0 -33
  469. package/frontend/src/features/auth/components/AddOAuthDialog.tsx +0 -106
  470. package/frontend/src/features/auth/components/AuthMethodTab.tsx +0 -238
  471. package/frontend/src/features/auth/components/UsersTab.tsx +0 -114
  472. package/frontend/src/features/auth/page/AuthenticationPage.tsx +0 -169
  473. package/frontend/src/features/database/hooks/UseLinkModal.tsx +0 -78
  474. package/frontend/src/features/functions/components/FunctionViewer.tsx +0 -46
  475. package/frontend/src/features/functions/components/FunctionsContent.tsx +0 -88
  476. package/frontend/src/features/login/components/AuthErrorBoundary.tsx +0 -87
  477. package/frontend/src/features/login/components/PrivateRoute.tsx +0 -24
  478. package/frontend/src/features/logs/components/AnalyticsLogsTable.tsx +0 -313
  479. package/frontend/src/features/logs/components/LogsTable.tsx +0 -199
  480. package/frontend/src/features/logs/page/AnalyticsLogsPage.tsx +0 -530
  481. package/frontend/src/features/metadata/index.ts +0 -0
  482. package/frontend/src/features/metadata/page/MetadataPage.tsx +0 -136
  483. package/frontend/src/features/onboard/components/CompletionCard.tsx +0 -41
  484. package/frontend/src/features/onboard/components/OnboardButton.tsx +0 -84
  485. package/frontend/src/features/onboard/components/StepContent.tsx +0 -91
  486. package/frontend/src/features/onboard/components/TestConnectionStep.tsx +0 -53
  487. package/frontend/src/features/onboard/components/mcp/McpInstallation.tsx +0 -144
  488. package/frontend/src/features/onboard/page/OnBoardPage.tsx +0 -104
  489. package/frontend/src/features/onboard/types.ts +0 -8
  490. package/frontend/src/lib/contexts/OnboardStepContext.tsx +0 -68
  491. package/frontend/src/lib/hooks/useOnboardingCompletion.ts +0 -29
  492. /package/backend/src/api/{middleware → middlewares}/error.ts +0 -0
  493. /package/backend/src/api/{middleware → middlewares}/upload.ts +0 -0
  494. /package/backend/{migrations → src/infra/database/migrations}/000_create-base-tables.sql +0 -0
  495. /package/backend/{migrations → src/infra/database/migrations}/001_create-helper-functions.sql +0 -0
  496. /package/backend/{migrations → src/infra/database/migrations}/002_rename-auth-tables.sql +0 -0
  497. /package/backend/{migrations → src/infra/database/migrations}/003_create-users-table.sql +0 -0
  498. /package/backend/{migrations → src/infra/database/migrations}/004_add-reload-postgrest-func.sql +0 -0
  499. /package/backend/{migrations → src/infra/database/migrations}/005_enable-project-admin-modify-users.sql +0 -0
  500. /package/backend/{migrations → src/infra/database/migrations}/006_modify-ai-usage-table.sql +0 -0
  501. /package/backend/{migrations → src/infra/database/migrations}/007_drop-metadata-table.sql +0 -0
  502. /package/backend/{migrations → src/infra/database/migrations}/008_add-system-tables.sql +0 -0
  503. /package/backend/{migrations → src/infra/database/migrations}/009_add-function-secrets.sql +0 -0
  504. /package/backend/{migrations → src/infra/database/migrations}/010_modify-ai-config-modalities.sql +0 -0
  505. /package/backend/{migrations → src/infra/database/migrations}/011_refactor-secrets-table.sql +0 -0
  506. /package/backend/{migrations → src/infra/database/migrations}/012_add-storage-uploaded-by.sql +0 -0
  507. /package/frontend/src/{features/metadata → lib}/services/metadata.service.ts +0 -0
@@ -0,0 +1,1136 @@
1
+ import bcrypt from 'bcryptjs';
2
+ import crypto from 'crypto';
3
+ import { Pool } from 'pg';
4
+ import { DatabaseManager } from '@/infra/database/database.manager.js';
5
+ import { TokenManager } from '@/infra/security/token.manager.js';
6
+ import logger from '@/utils/logger.js';
7
+ import type {
8
+ UserSchema,
9
+ CreateUserResponse,
10
+ CreateSessionResponse,
11
+ VerifyEmailResponse,
12
+ ResetPasswordResponse,
13
+ CreateAdminSessionResponse,
14
+ AuthMetadataSchema,
15
+ OAuthProvidersSchema,
16
+ } from '@insforge/shared-schemas';
17
+ import { OAuthConfigService } from '@/services/auth/oauth-config.service.js';
18
+ import { AuthConfigService } from './auth-config.service.js';
19
+ import { AuthOTPService, OTPPurpose, OTPType } from './auth-otp.service.js';
20
+ import { GoogleOAuthProvider } from '@/providers/oauth/google.provider.js';
21
+ import { GitHubOAuthProvider } from '@/providers/oauth/github.provider.js';
22
+ import { DiscordOAuthProvider } from '@/providers/oauth/discord.provider.js';
23
+ import { LinkedInOAuthProvider } from '@/providers/oauth/linkedin.provider.js';
24
+ import { FacebookOAuthProvider } from '@/providers/oauth/facebook.provider.js';
25
+ import { MicrosoftOAuthProvider } from '@/providers/oauth/microsoft.provider.js';
26
+ import { validatePassword } from '@/utils/validations.js';
27
+ import { getPasswordRequirementsMessage } from '@/utils/utils.js';
28
+ import {
29
+ FacebookUserInfo,
30
+ GitHubUserInfo,
31
+ GoogleUserInfo,
32
+ MicrosoftUserInfo,
33
+ LinkedInUserInfo,
34
+ DiscordUserInfo,
35
+ XUserInfo,
36
+ UserRecord,
37
+ OAuthUserData,
38
+ } from '@/types/auth.js';
39
+ import { ADMIN_ID } from '@/utils/constants.js';
40
+ import { getApiBaseUrl } from '@/utils/environment.js';
41
+ import { AppError } from '@/api/middlewares/error.js';
42
+ import { ERROR_CODES } from '@/types/error-constants.js';
43
+ import { EmailService } from '@/services/email/email.service.js';
44
+ import { XOAuthProvider } from '@/providers/oauth/x.provider.js';
45
+
46
+ /**
47
+ * Simplified JWT-based auth service
48
+ * Handles all authentication operations including OAuth
49
+ */
50
+ export class AuthService {
51
+ private static instance: AuthService;
52
+ private adminEmail: string;
53
+ private adminPassword: string;
54
+ private pool: Pool | null = null;
55
+ private tokenManager: TokenManager;
56
+
57
+ // OAuth provider instances (cached singletons)
58
+ private googleOAuthProvider: GoogleOAuthProvider;
59
+ private githubOAuthProvider: GitHubOAuthProvider;
60
+ private discordOAuthProvider: DiscordOAuthProvider;
61
+ private linkedinOAuthProvider: LinkedInOAuthProvider;
62
+ private facebookOAuthProvider: FacebookOAuthProvider;
63
+ private microsoftOAuthProvider: MicrosoftOAuthProvider;
64
+ private xOAuthProvider: XOAuthProvider;
65
+
66
+ private constructor() {
67
+ this.adminEmail = process.env.ADMIN_EMAIL ?? '';
68
+ this.adminPassword = process.env.ADMIN_PASSWORD ?? '';
69
+
70
+ if (!this.adminEmail || !this.adminPassword) {
71
+ throw new Error('ADMIN_EMAIL and ADMIN_PASSWORD environment variables are required');
72
+ }
73
+
74
+ // Initialize token manager
75
+ this.tokenManager = TokenManager.getInstance();
76
+
77
+ // Initialize OAuth providers (cached singletons)
78
+ this.googleOAuthProvider = GoogleOAuthProvider.getInstance();
79
+ this.githubOAuthProvider = GitHubOAuthProvider.getInstance();
80
+ this.discordOAuthProvider = DiscordOAuthProvider.getInstance();
81
+ this.linkedinOAuthProvider = LinkedInOAuthProvider.getInstance();
82
+ this.facebookOAuthProvider = FacebookOAuthProvider.getInstance();
83
+ this.microsoftOAuthProvider = MicrosoftOAuthProvider.getInstance();
84
+ this.xOAuthProvider = XOAuthProvider.getInstance();
85
+
86
+ logger.info('AuthService initialized');
87
+ }
88
+
89
+ public static getInstance(): AuthService {
90
+ if (!AuthService.instance) {
91
+ AuthService.instance = new AuthService();
92
+ }
93
+ return AuthService.instance;
94
+ }
95
+
96
+ private getPool(): Pool {
97
+ if (!this.pool) {
98
+ const dbManager = DatabaseManager.getInstance();
99
+ this.pool = dbManager.getPool();
100
+ }
101
+ return this.pool;
102
+ }
103
+
104
+ /**
105
+ * User registration
106
+ * Otherwise, returns user with access token for immediate login
107
+ */
108
+ async register(email: string, password: string, name?: string): Promise<CreateUserResponse> {
109
+ // Get email auth configuration and validate password
110
+ const authConfigService = AuthConfigService.getInstance();
111
+ const emailAuthConfig = await authConfigService.getAuthConfig();
112
+
113
+ if (!validatePassword(password, emailAuthConfig)) {
114
+ throw new AppError(
115
+ getPasswordRequirementsMessage(emailAuthConfig),
116
+ 400,
117
+ ERROR_CODES.INVALID_INPUT
118
+ );
119
+ }
120
+
121
+ const hashedPassword = await bcrypt.hash(password, 10);
122
+ const userId = crypto.randomUUID();
123
+
124
+ const pool = this.getPool();
125
+ const client = await pool.connect();
126
+ try {
127
+ await client.query('BEGIN');
128
+
129
+ await client.query(
130
+ `INSERT INTO _accounts (id, email, password, name, email_verified, created_at, updated_at)
131
+ VALUES ($1, $2, $3, $4, $5, NOW(), NOW())`,
132
+ [userId, email, hashedPassword, name || null, false]
133
+ );
134
+
135
+ await client.query(
136
+ `INSERT INTO users (id, nickname, created_at, updated_at)
137
+ VALUES ($1, $2, NOW(), NOW())`,
138
+ [userId, name || null]
139
+ );
140
+
141
+ await client.query('COMMIT');
142
+ } catch (e) {
143
+ await client.query('ROLLBACK');
144
+ // Postgres unique_violation
145
+ if (e && typeof e === 'object' && 'code' in e && e.code === '23505') {
146
+ throw new AppError('User already exists', 409, ERROR_CODES.ALREADY_EXISTS);
147
+ }
148
+ throw e;
149
+ } finally {
150
+ client.release();
151
+ }
152
+
153
+ const dbUser = await this.getUserById(userId);
154
+ if (!dbUser) {
155
+ throw new Error('User not found after registration');
156
+ }
157
+ const user = this.transformUserRecordToSchema(dbUser);
158
+
159
+ if (emailAuthConfig.requireEmailVerification) {
160
+ try {
161
+ if (emailAuthConfig.verifyEmailMethod === 'link') {
162
+ await this.sendVerificationEmailWithLink(email);
163
+ } else {
164
+ await this.sendVerificationEmailWithCode(email);
165
+ }
166
+ } catch (error) {
167
+ logger.warn('Verification email send failed during register', { error });
168
+ }
169
+ return {
170
+ accessToken: null,
171
+ requireEmailVerification: true,
172
+ };
173
+ }
174
+
175
+ // Email verification not required, provide access token for immediate login
176
+ const accessToken = this.tokenManager.generateToken({
177
+ sub: userId,
178
+ email,
179
+ role: 'authenticated',
180
+ });
181
+
182
+ return {
183
+ user,
184
+ accessToken,
185
+ requireEmailVerification: false,
186
+ redirectTo: emailAuthConfig.signInRedirectTo || undefined,
187
+ };
188
+ }
189
+
190
+ /**
191
+ * User login
192
+ */
193
+ async login(email: string, password: string): Promise<CreateSessionResponse> {
194
+ const dbUser = await this.getUserByEmail(email);
195
+
196
+ if (!dbUser || !dbUser.password) {
197
+ throw new AppError('Invalid credentials', 401, ERROR_CODES.AUTH_UNAUTHORIZED);
198
+ }
199
+
200
+ const validPassword = await bcrypt.compare(password, dbUser.password);
201
+ if (!validPassword) {
202
+ throw new AppError('Invalid credentials', 401, ERROR_CODES.AUTH_UNAUTHORIZED);
203
+ }
204
+
205
+ // Check if email verification is required
206
+ const authConfigService = AuthConfigService.getInstance();
207
+ const emailAuthConfig = await authConfigService.getAuthConfig();
208
+
209
+ if (emailAuthConfig.requireEmailVerification && !dbUser.email_verified) {
210
+ throw new AppError(
211
+ 'Email verification required',
212
+ 403,
213
+ ERROR_CODES.FORBIDDEN,
214
+ 'Please verify your email address before logging in'
215
+ );
216
+ }
217
+
218
+ const user = this.transformUserRecordToSchema(dbUser);
219
+ const accessToken = this.tokenManager.generateToken({
220
+ sub: dbUser.id,
221
+ email: dbUser.email,
222
+ role: 'authenticated',
223
+ });
224
+
225
+ // Include redirect URL if configured
226
+ const response: CreateSessionResponse = {
227
+ user,
228
+ accessToken,
229
+ redirectTo: emailAuthConfig.signInRedirectTo || undefined,
230
+ };
231
+
232
+ return response;
233
+ }
234
+
235
+ /**
236
+ * Send verification email with numeric OTP code
237
+ * Creates a 6-digit OTP and sends it via email for manual entry
238
+ */
239
+ async sendVerificationEmailWithCode(email: string): Promise<void> {
240
+ // Check if user exists
241
+ const pool = this.getPool();
242
+ const result = await pool.query('SELECT * FROM _accounts WHERE email = $1', [email]);
243
+ const dbUser = result.rows[0];
244
+ if (!dbUser) {
245
+ // Silently succeed to prevent user enumeration
246
+ logger.info('Verification email requested for non-existent user', { email });
247
+ return;
248
+ }
249
+
250
+ // Create numeric OTP code using the OTP service
251
+ const otpService = AuthOTPService.getInstance();
252
+ const { otp: code } = await otpService.createEmailOTP(
253
+ email,
254
+ OTPPurpose.VERIFY_EMAIL,
255
+ OTPType.NUMERIC_CODE
256
+ );
257
+
258
+ // Send email with verification code
259
+ const emailService = EmailService.getInstance();
260
+ await emailService.sendWithTemplate(email, dbUser.name || 'User', 'email-verification-code', {
261
+ token: code,
262
+ });
263
+ }
264
+
265
+ /**
266
+ * Send verification email with clickable link
267
+ * Creates a long cryptographic token and sends it via email as a clickable link
268
+ * The link contains only the token (no email) for better privacy and security
269
+ */
270
+ async sendVerificationEmailWithLink(email: string): Promise<void> {
271
+ // Check if user exists
272
+ const pool = this.getPool();
273
+ const result = await pool.query('SELECT * FROM _accounts WHERE email = $1', [email]);
274
+ const dbUser = result.rows[0];
275
+ if (!dbUser) {
276
+ // Silently succeed to prevent user enumeration
277
+ logger.info('Verification email requested for non-existent user', { email });
278
+ return;
279
+ }
280
+
281
+ // Create long cryptographic token for clickable verification link
282
+ const otpService = AuthOTPService.getInstance();
283
+ const { otp: token } = await otpService.createEmailOTP(
284
+ email,
285
+ OTPPurpose.VERIFY_EMAIL,
286
+ OTPType.HASH_TOKEN
287
+ );
288
+
289
+ // Build verification link URL using backend API endpoint
290
+ const linkUrl = `${getApiBaseUrl()}/auth/verify-email?token=${token}`;
291
+
292
+ // Send email with verification link
293
+ const emailService = EmailService.getInstance();
294
+ await emailService.sendWithTemplate(email, dbUser.name || 'User', 'email-verification-link', {
295
+ link: linkUrl,
296
+ });
297
+ }
298
+
299
+ /**
300
+ * Verify email with numeric code
301
+ * Verifies the email OTP code and updates the account in a single transaction
302
+ */
303
+ async verifyEmailWithCode(email: string, verificationCode: string): Promise<VerifyEmailResponse> {
304
+ const dbManager = DatabaseManager.getInstance();
305
+ const pool = dbManager.getPool();
306
+ const client = await pool.connect();
307
+
308
+ try {
309
+ await client.query('BEGIN');
310
+
311
+ // Verify OTP using the OTP service (within the same transaction)
312
+ const otpService = AuthOTPService.getInstance();
313
+ await otpService.verifyEmailOTPWithCode(
314
+ email,
315
+ OTPPurpose.VERIFY_EMAIL,
316
+ verificationCode,
317
+ client
318
+ );
319
+
320
+ // Update account email verification status
321
+ const result = await client.query(
322
+ `UPDATE _accounts
323
+ SET email_verified = true, updated_at = NOW()
324
+ WHERE email = $1
325
+ RETURNING id`,
326
+ [email]
327
+ );
328
+
329
+ if (result.rows.length === 0) {
330
+ throw new Error('User not found');
331
+ }
332
+
333
+ await client.query('COMMIT');
334
+
335
+ // Fetch full user record with provider data
336
+ const userId = result.rows[0].id;
337
+ const dbUser = await this.getUserById(userId);
338
+ if (!dbUser) {
339
+ throw new Error('User not found after verification');
340
+ }
341
+ const user = this.transformUserRecordToSchema(dbUser);
342
+ const accessToken = this.tokenManager.generateToken({
343
+ sub: dbUser.id,
344
+ email: dbUser.email,
345
+ role: 'authenticated',
346
+ });
347
+
348
+ // Get redirect URL from auth config if configured
349
+ const authConfigService = AuthConfigService.getInstance();
350
+ const emailAuthConfig = await authConfigService.getAuthConfig();
351
+
352
+ return {
353
+ user,
354
+ accessToken,
355
+ redirectTo: emailAuthConfig.signInRedirectTo || undefined,
356
+ };
357
+ } catch (error) {
358
+ await client.query('ROLLBACK');
359
+ throw error;
360
+ } finally {
361
+ client.release();
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Verify email with hash token from clickable link
367
+ * Verifies the token (without needing email), looks up the email, and updates the account
368
+ * This is more secure as the email is not exposed in the URL
369
+ */
370
+ async verifyEmailWithToken(token: string): Promise<VerifyEmailResponse> {
371
+ const dbManager = DatabaseManager.getInstance();
372
+ const pool = dbManager.getPool();
373
+ const client = await pool.connect();
374
+
375
+ try {
376
+ await client.query('BEGIN');
377
+
378
+ // Verify token and get the associated email
379
+ const otpService = AuthOTPService.getInstance();
380
+ const { email } = await otpService.verifyEmailOTPWithToken(
381
+ OTPPurpose.VERIFY_EMAIL,
382
+ token,
383
+ client
384
+ );
385
+
386
+ // Update account email verification status
387
+ const result = await client.query(
388
+ `UPDATE _accounts
389
+ SET email_verified = true, updated_at = NOW()
390
+ WHERE email = $1
391
+ RETURNING id`,
392
+ [email]
393
+ );
394
+
395
+ if (result.rows.length === 0) {
396
+ throw new Error('User not found');
397
+ }
398
+
399
+ await client.query('COMMIT');
400
+
401
+ // Fetch full user record with provider data
402
+ const userId = result.rows[0].id;
403
+ const dbUser = await this.getUserById(userId);
404
+ if (!dbUser) {
405
+ throw new Error('User not found after verification');
406
+ }
407
+ const user = this.transformUserRecordToSchema(dbUser);
408
+ const accessToken = this.tokenManager.generateToken({
409
+ sub: dbUser.id,
410
+ email: dbUser.email,
411
+ role: 'authenticated',
412
+ });
413
+
414
+ // Get redirect URL from auth config if configured
415
+ const authConfigService = AuthConfigService.getInstance();
416
+ const emailAuthConfig = await authConfigService.getAuthConfig();
417
+
418
+ return {
419
+ user,
420
+ accessToken,
421
+ redirectTo: emailAuthConfig.signInRedirectTo || undefined,
422
+ };
423
+ } catch (error) {
424
+ await client.query('ROLLBACK');
425
+ throw error;
426
+ } finally {
427
+ client.release();
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Send reset password email with numeric OTP code
433
+ * Creates a 6-digit OTP and sends it via email for manual entry
434
+ */
435
+ async sendResetPasswordEmailWithCode(email: string): Promise<void> {
436
+ // Check if user exists
437
+ const pool = this.getPool();
438
+ const result = await pool.query('SELECT * FROM _accounts WHERE email = $1', [email]);
439
+ const dbUser = result.rows[0];
440
+ if (!dbUser) {
441
+ // Silently succeed to prevent user enumeration
442
+ logger.info('Password reset requested for non-existent user', { email });
443
+ return;
444
+ }
445
+
446
+ // Create numeric OTP code using the OTP service
447
+ const otpService = AuthOTPService.getInstance();
448
+ const { otp: code } = await otpService.createEmailOTP(
449
+ email,
450
+ OTPPurpose.RESET_PASSWORD,
451
+ OTPType.NUMERIC_CODE
452
+ );
453
+
454
+ // Send email with reset password code
455
+ const emailService = EmailService.getInstance();
456
+ await emailService.sendWithTemplate(email, dbUser.name || 'User', 'reset-password-code', {
457
+ token: code,
458
+ });
459
+ }
460
+
461
+ /**
462
+ * Send reset password email with clickable link
463
+ * Creates a long cryptographic token and sends it via email as a clickable link
464
+ * The link contains only the token (no email) for better privacy and security
465
+ */
466
+ async sendResetPasswordEmailWithLink(email: string): Promise<void> {
467
+ // Check if user exists
468
+ const pool = this.getPool();
469
+ const result = await pool.query('SELECT * FROM _accounts WHERE email = $1', [email]);
470
+ const dbUser = result.rows[0];
471
+ if (!dbUser) {
472
+ // Silently succeed to prevent user enumeration
473
+ logger.info('Password reset requested for non-existent user', { email });
474
+ return;
475
+ }
476
+
477
+ // Create long cryptographic token for clickable reset link
478
+ const otpService = AuthOTPService.getInstance();
479
+ const { otp: token } = await otpService.createEmailOTP(
480
+ email,
481
+ OTPPurpose.RESET_PASSWORD,
482
+ OTPType.HASH_TOKEN
483
+ );
484
+
485
+ // Build password reset link URL using backend API endpoint
486
+ const linkUrl = `${getApiBaseUrl()}/auth/reset-password?token=${token}`;
487
+
488
+ // Send email with password reset link
489
+ const emailService = EmailService.getInstance();
490
+ await emailService.sendWithTemplate(email, dbUser.name || 'User', 'reset-password-link', {
491
+ link: linkUrl,
492
+ });
493
+ }
494
+
495
+ /**
496
+ * Exchange reset password code for a temporary reset token
497
+ * This separates code verification from password reset for better security
498
+ * The reset token can be used later to reset the password without needing email
499
+ */
500
+ async exchangeResetPasswordToken(
501
+ email: string,
502
+ verificationCode: string
503
+ ): Promise<{ token: string; expiresAt: Date }> {
504
+ const otpService = AuthOTPService.getInstance();
505
+
506
+ // Exchange the numeric verification code for a long-lived reset token
507
+ // All OTP logic (verification, consumption, token generation) is handled by AuthOTPService
508
+ const result = await otpService.exchangeCodeForToken(
509
+ email,
510
+ OTPPurpose.RESET_PASSWORD,
511
+ verificationCode
512
+ );
513
+
514
+ return {
515
+ token: result.token,
516
+ expiresAt: result.expiresAt,
517
+ };
518
+ }
519
+
520
+ /**
521
+ * Reset password with token
522
+ * Verifies the token (without needing email), looks up the email, and updates the password
523
+ * Both clickable link tokens and code-verified reset tokens use RESET_PASSWORD purpose
524
+ * Note: Does not return access token - user must login again with new password
525
+ */
526
+ async resetPasswordWithToken(newPassword: string, token: string): Promise<ResetPasswordResponse> {
527
+ // Validate password first before verifying token
528
+ // This allows the user to retry with the same token if password is invalid
529
+ const authConfigService = AuthConfigService.getInstance();
530
+ const emailAuthConfig = await authConfigService.getAuthConfig();
531
+
532
+ if (!validatePassword(newPassword, emailAuthConfig)) {
533
+ throw new AppError(
534
+ getPasswordRequirementsMessage(emailAuthConfig),
535
+ 400,
536
+ ERROR_CODES.INVALID_INPUT
537
+ );
538
+ }
539
+
540
+ const dbManager = DatabaseManager.getInstance();
541
+ const pool = dbManager.getPool();
542
+ const client = await pool.connect();
543
+
544
+ try {
545
+ await client.query('BEGIN');
546
+
547
+ // Verify token and get the associated email
548
+ // Both clickable link tokens and code-verified reset tokens use RESET_PASSWORD purpose
549
+ const otpService = AuthOTPService.getInstance();
550
+ const { email } = await otpService.verifyEmailOTPWithToken(
551
+ OTPPurpose.RESET_PASSWORD,
552
+ token,
553
+ client
554
+ );
555
+
556
+ // Hash the new password
557
+ const hashedPassword = await bcrypt.hash(newPassword, 10);
558
+
559
+ // Update password in the database
560
+ const result = await client.query(
561
+ `UPDATE _accounts
562
+ SET password = $1, updated_at = NOW()
563
+ WHERE email = $2
564
+ RETURNING id`,
565
+ [hashedPassword, email]
566
+ );
567
+
568
+ if (result.rows.length === 0) {
569
+ throw new Error('User not found');
570
+ }
571
+
572
+ const userId = result.rows[0].id;
573
+
574
+ await client.query('COMMIT');
575
+
576
+ logger.info('Password reset successfully with token', { userId });
577
+
578
+ return {
579
+ message: 'Password reset successfully. Please login with your new password.',
580
+ };
581
+ } catch (error) {
582
+ await client.query('ROLLBACK');
583
+ throw error;
584
+ } finally {
585
+ client.release();
586
+ }
587
+ }
588
+
589
+ /**
590
+ * Admin login (validates against env variables only)
591
+ */
592
+ adminLogin(email: string, password: string): CreateAdminSessionResponse {
593
+ // Simply validate against environment variables
594
+ if (email !== this.adminEmail || password !== this.adminPassword) {
595
+ throw new AppError('Invalid admin credentials', 401, ERROR_CODES.AUTH_UNAUTHORIZED);
596
+ }
597
+
598
+ // Use a fixed admin ID for the system administrator
599
+
600
+ // Return admin user with JWT token - no database interaction
601
+ const accessToken = this.tokenManager.generateToken({
602
+ sub: ADMIN_ID,
603
+ email,
604
+ role: 'project_admin',
605
+ });
606
+
607
+ return {
608
+ user: {
609
+ id: ADMIN_ID,
610
+ email: email,
611
+ name: 'Administrator',
612
+ emailVerified: true,
613
+ createdAt: new Date().toISOString(),
614
+ updatedAt: new Date().toISOString(),
615
+ },
616
+ accessToken,
617
+ };
618
+ }
619
+
620
+ /**
621
+ * Admin login with authorization token (validates JWT from external issuer)
622
+ */
623
+ async adminLoginWithAuthorizationCode(code: string): Promise<CreateAdminSessionResponse> {
624
+ try {
625
+ // Use TokenManager to verify cloud token
626
+ const { payload } = await this.tokenManager.verifyCloudToken(code);
627
+
628
+ // If verification succeeds, extract user info and generate internal token
629
+ const email = payload['email'] || payload['sub'] || 'admin@insforge.local';
630
+
631
+ // Generate internal access token
632
+ const accessToken = this.tokenManager.generateToken({
633
+ sub: ADMIN_ID,
634
+ email: email as string,
635
+ role: 'project_admin',
636
+ });
637
+
638
+ return {
639
+ user: {
640
+ id: ADMIN_ID,
641
+ email: email as string,
642
+ name: 'Administrator',
643
+ emailVerified: true,
644
+ createdAt: new Date().toISOString(),
645
+ updatedAt: new Date().toISOString(),
646
+ },
647
+ accessToken,
648
+ };
649
+ } catch (error) {
650
+ logger.error('Admin token verification failed:', error);
651
+ throw new AppError('Invalid admin credentials', 401, ERROR_CODES.AUTH_UNAUTHORIZED);
652
+ }
653
+ }
654
+
655
+ /**
656
+ * Find or create third-party user (main OAuth user handler)
657
+ * Adapted from 3-table to 2-table structure
658
+ */
659
+ async findOrCreateThirdPartyUser(
660
+ provider: string,
661
+ providerId: string,
662
+ email: string,
663
+ userName: string,
664
+ avatarUrl: string,
665
+ identityData:
666
+ | GoogleUserInfo
667
+ | GitHubUserInfo
668
+ | DiscordUserInfo
669
+ | LinkedInUserInfo
670
+ | MicrosoftUserInfo
671
+ | FacebookUserInfo
672
+ | XUserInfo
673
+ | Record<string, unknown>
674
+ ): Promise<CreateSessionResponse> {
675
+ const pool = this.getPool();
676
+
677
+ // First, try to find existing user by provider ID in _account_providers table
678
+ const accountResult = await pool.query(
679
+ 'SELECT * FROM _account_providers WHERE provider = $1 AND provider_account_id = $2',
680
+ [provider, providerId]
681
+ );
682
+ const account = accountResult.rows[0];
683
+
684
+ if (account) {
685
+ // Found existing OAuth user, update last login time
686
+ await pool.query(
687
+ 'UPDATE _account_providers SET updated_at = CURRENT_TIMESTAMP WHERE provider = $1 AND provider_account_id = $2',
688
+ [provider, providerId]
689
+ );
690
+
691
+ // Update email_verified to true if not already verified (OAuth login means email is trusted)
692
+ await pool.query(
693
+ 'UPDATE _accounts SET email_verified = true WHERE id = $1 AND email_verified = false',
694
+ [account.user_id]
695
+ );
696
+
697
+ const dbUser = await this.getUserById(account.user_id);
698
+ if (!dbUser) {
699
+ throw new Error('User not found after OAuth login');
700
+ }
701
+
702
+ const user = this.transformUserRecordToSchema(dbUser);
703
+ const accessToken = this.tokenManager.generateToken({
704
+ sub: user.id,
705
+ email: user.email,
706
+ role: 'authenticated',
707
+ });
708
+
709
+ return { user, accessToken };
710
+ }
711
+
712
+ // If not found by provider_id, try to find by email in _user table
713
+ const existingUserResult = await pool.query('SELECT * FROM _accounts WHERE email = $1', [
714
+ email,
715
+ ]);
716
+ const existingUser = existingUserResult.rows[0];
717
+
718
+ if (existingUser) {
719
+ // Found existing user by email, create _account_providers record to link OAuth
720
+ await pool.query(
721
+ `
722
+ INSERT INTO _account_providers (
723
+ user_id, provider, provider_account_id,
724
+ provider_data, created_at, updated_at
725
+ )
726
+ VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
727
+ `,
728
+ [existingUser.id, provider, providerId, JSON.stringify(identityData)]
729
+ );
730
+
731
+ // Update email_verified to true (OAuth login means email is trusted)
732
+ await pool.query(
733
+ 'UPDATE _accounts SET email_verified = true WHERE id = $1 AND email_verified = false',
734
+ [existingUser.id]
735
+ );
736
+
737
+ // Fetch updated user data with provider information
738
+ const dbUser = await this.getUserById(existingUser.id);
739
+ if (!dbUser) {
740
+ throw new Error('User not found after linking OAuth provider');
741
+ }
742
+
743
+ const user = this.transformUserRecordToSchema(dbUser);
744
+ const accessToken = this.tokenManager.generateToken({
745
+ sub: existingUser.id,
746
+ email: existingUser.email,
747
+ role: 'authenticated',
748
+ });
749
+
750
+ return { user, accessToken };
751
+ }
752
+
753
+ // Create new user with OAuth data
754
+ return this.createThirdPartyUser(
755
+ provider,
756
+ userName,
757
+ email,
758
+ providerId,
759
+ identityData,
760
+ avatarUrl
761
+ );
762
+ }
763
+
764
+ /**
765
+ * Create new third-party user
766
+ */
767
+ private async createThirdPartyUser(
768
+ provider: string,
769
+ userName: string,
770
+ email: string,
771
+ providerId: string,
772
+ identityData:
773
+ | GoogleUserInfo
774
+ | GitHubUserInfo
775
+ | DiscordUserInfo
776
+ | LinkedInUserInfo
777
+ | MicrosoftUserInfo
778
+ | FacebookUserInfo
779
+ | XUserInfo
780
+ | Record<string, unknown>,
781
+ avatarUrl: string
782
+ ): Promise<CreateSessionResponse> {
783
+ const userId = crypto.randomUUID();
784
+
785
+ const pool = this.getPool();
786
+ const client = await pool.connect();
787
+
788
+ try {
789
+ await client.query('BEGIN');
790
+
791
+ // Create user record (without password for OAuth users)
792
+ await client.query(
793
+ `
794
+ INSERT INTO _accounts (id, email, name, email_verified, created_at, updated_at)
795
+ VALUES ($1, $2, $3, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
796
+ `,
797
+ [userId, email, userName]
798
+ );
799
+
800
+ await client.query(
801
+ `
802
+ INSERT INTO users (id, nickname, avatar_url, created_at, updated_at)
803
+ VALUES ($1, $2, $3, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
804
+ `,
805
+ [userId, userName, avatarUrl]
806
+ );
807
+
808
+ // Create _account_providers record
809
+ await client.query(
810
+ `
811
+ INSERT INTO _account_providers (
812
+ user_id, provider, provider_account_id,
813
+ provider_data, created_at, updated_at
814
+ )
815
+ VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
816
+ `,
817
+ [userId, provider, providerId, JSON.stringify({ ...identityData, avatar_url: avatarUrl })]
818
+ );
819
+
820
+ await client.query('COMMIT');
821
+
822
+ const user: UserSchema = {
823
+ id: userId,
824
+ email,
825
+ name: userName,
826
+ emailVerified: true,
827
+ createdAt: new Date().toISOString(),
828
+ updatedAt: new Date().toISOString(),
829
+ };
830
+
831
+ const accessToken = this.tokenManager.generateToken({
832
+ sub: userId,
833
+ email,
834
+ role: 'authenticated',
835
+ });
836
+
837
+ return { user, accessToken };
838
+ } catch (error) {
839
+ await client.query('ROLLBACK');
840
+ throw error;
841
+ } finally {
842
+ client.release();
843
+ }
844
+ }
845
+
846
+ async getMetadata(): Promise<AuthMetadataSchema> {
847
+ const oAuthConfigService = OAuthConfigService.getInstance();
848
+ const oAuthConfigs = await oAuthConfigService.getAllConfigs();
849
+ return {
850
+ oauths: oAuthConfigs,
851
+ };
852
+ }
853
+
854
+ /**
855
+ * Generate OAuth authorization URL for any supported provider
856
+ */
857
+ async generateOAuthUrl(provider: OAuthProvidersSchema, state?: string): Promise<string> {
858
+ switch (provider) {
859
+ case 'google':
860
+ return this.googleOAuthProvider.generateOAuthUrl(state);
861
+ case 'github':
862
+ return this.githubOAuthProvider.generateOAuthUrl(state);
863
+ case 'discord':
864
+ return this.discordOAuthProvider.generateOAuthUrl(state);
865
+ case 'linkedin':
866
+ return this.linkedinOAuthProvider.generateOAuthUrl(state);
867
+ case 'facebook':
868
+ return this.facebookOAuthProvider.generateOAuthUrl(state);
869
+ case 'microsoft':
870
+ return this.microsoftOAuthProvider.generateOAuthUrl(state);
871
+ case 'x':
872
+ return this.xOAuthProvider.generateOAuthUrl(state);
873
+ default:
874
+ throw new Error(`OAuth provider ${provider} is not implemented yet.`);
875
+ }
876
+ }
877
+
878
+ /**
879
+ * Handle OAuth callback for any supported provider
880
+ */
881
+ async handleOAuthCallback(
882
+ provider: OAuthProvidersSchema,
883
+ payload: { code?: string; token?: string; state?: string }
884
+ ): Promise<CreateSessionResponse> {
885
+ let userData: OAuthUserData;
886
+
887
+ switch (provider) {
888
+ case 'google':
889
+ userData = await this.googleOAuthProvider.handleCallback(payload);
890
+ break;
891
+ case 'github':
892
+ userData = await this.githubOAuthProvider.handleCallback(payload);
893
+ break;
894
+ case 'discord':
895
+ userData = await this.discordOAuthProvider.handleCallback(payload);
896
+ break;
897
+ case 'linkedin':
898
+ userData = await this.linkedinOAuthProvider.handleCallback(payload);
899
+ break;
900
+ case 'facebook':
901
+ userData = await this.facebookOAuthProvider.handleCallback(payload);
902
+ break;
903
+ case 'microsoft':
904
+ userData = await this.microsoftOAuthProvider.handleCallback(payload);
905
+ break;
906
+ case 'x':
907
+ userData = await this.xOAuthProvider.handleCallback(payload);
908
+ break;
909
+ default:
910
+ throw new Error(`OAuth provider ${provider} is not implemented yet.`);
911
+ }
912
+
913
+ return this.findOrCreateThirdPartyUser(
914
+ userData.provider,
915
+ userData.providerId,
916
+ userData.email,
917
+ userData.userName,
918
+ userData.avatarUrl,
919
+ userData.identityData
920
+ );
921
+ }
922
+
923
+ /**
924
+ * Handle shared callback for any supported provider
925
+ * Transforms payload and creates/finds user
926
+ */
927
+ async handleSharedCallback(
928
+ provider: OAuthProvidersSchema,
929
+ payloadData: Record<string, unknown>
930
+ ): Promise<CreateSessionResponse> {
931
+ let userData: OAuthUserData;
932
+
933
+ switch (provider) {
934
+ case 'google':
935
+ userData = this.googleOAuthProvider.handleSharedCallback(payloadData);
936
+ break;
937
+ case 'github':
938
+ userData = this.githubOAuthProvider.handleSharedCallback(payloadData);
939
+ break;
940
+ case 'discord':
941
+ userData = this.discordOAuthProvider.handleSharedCallback(payloadData);
942
+ break;
943
+ case 'linkedin':
944
+ userData = this.linkedinOAuthProvider.handleSharedCallback(payloadData);
945
+ break;
946
+ case 'facebook':
947
+ userData = this.facebookOAuthProvider.handleSharedCallback(payloadData);
948
+ break;
949
+ case 'x':
950
+ userData = this.xOAuthProvider.handleSharedCallback(payloadData);
951
+ break;
952
+ case 'microsoft':
953
+ default:
954
+ throw new Error(`OAuth provider ${provider} is not supported for shared callback.`);
955
+ }
956
+
957
+ return this.findOrCreateThirdPartyUser(
958
+ userData.provider,
959
+ userData.providerId,
960
+ userData.email,
961
+ userData.userName,
962
+ userData.avatarUrl,
963
+ userData.identityData
964
+ );
965
+ }
966
+
967
+ /**
968
+ * Get user by email (helper method for internal use)
969
+ * @private
970
+ */
971
+ private async getUserByEmail(email: string): Promise<UserRecord | null> {
972
+ const pool = this.getPool();
973
+ const result = await pool.query(
974
+ `
975
+ SELECT
976
+ u.id,
977
+ u.email,
978
+ u.name,
979
+ u.email_verified,
980
+ u.created_at,
981
+ u.updated_at,
982
+ u.password,
983
+ STRING_AGG(a.provider, ',') as providers
984
+ FROM _accounts u
985
+ LEFT JOIN _account_providers a ON u.id = a.user_id
986
+ WHERE u.email = $1
987
+ GROUP BY u.id
988
+ `,
989
+ [email]
990
+ );
991
+
992
+ return result.rows[0] || null;
993
+ }
994
+
995
+ /**
996
+ * Get user by ID (helper method for internal use)
997
+ * @private
998
+ */
999
+ private async getUserById(userId: string): Promise<UserRecord | null> {
1000
+ const pool = this.getPool();
1001
+ const result = await pool.query(
1002
+ `
1003
+ SELECT
1004
+ u.id,
1005
+ u.email,
1006
+ u.name,
1007
+ u.email_verified,
1008
+ u.created_at,
1009
+ u.updated_at,
1010
+ u.password,
1011
+ STRING_AGG(a.provider, ',') as providers
1012
+ FROM _accounts u
1013
+ LEFT JOIN _account_providers a ON u.id = a.user_id
1014
+ WHERE u.id = $1
1015
+ GROUP BY u.id
1016
+ `,
1017
+ [userId]
1018
+ );
1019
+
1020
+ return result.rows[0] || null;
1021
+ }
1022
+
1023
+ /**
1024
+ * Transform database user record to API response format (snake_case to camelCase + provider logic)
1025
+ * @private
1026
+ */
1027
+ private transformUserRecordToSchema(dbUser: UserRecord): UserSchema {
1028
+ const identities = [];
1029
+ const providers: string[] = [];
1030
+
1031
+ // Add social providers if any
1032
+ if (dbUser.providers) {
1033
+ dbUser.providers.split(',').forEach((provider: string) => {
1034
+ identities.push({ provider });
1035
+ providers.push(provider);
1036
+ });
1037
+ }
1038
+
1039
+ // Add email provider if password exists
1040
+ if (dbUser.password) {
1041
+ identities.push({ provider: 'email' });
1042
+ providers.push('email');
1043
+ }
1044
+
1045
+ // Use first provider to determine type: 'email' or 'social'
1046
+ const firstProvider = providers[0];
1047
+ const providerType = firstProvider === 'email' ? 'email' : 'social';
1048
+
1049
+ return {
1050
+ id: dbUser.id,
1051
+ email: dbUser.email,
1052
+ name: dbUser.name,
1053
+ emailVerified: dbUser.email_verified,
1054
+ createdAt: dbUser.created_at,
1055
+ updatedAt: dbUser.updated_at,
1056
+ identities: identities,
1057
+ providerType: providerType,
1058
+ };
1059
+ }
1060
+
1061
+ /**
1062
+ * List users with pagination and search
1063
+ */
1064
+ async listUsers(
1065
+ limit: number,
1066
+ offset: number,
1067
+ search?: string
1068
+ ): Promise<{ users: UserSchema[]; total: number }> {
1069
+ const pool = this.getPool();
1070
+ let query = `
1071
+ SELECT
1072
+ u.id,
1073
+ u.email,
1074
+ u.name,
1075
+ u.email_verified,
1076
+ u.created_at,
1077
+ u.updated_at,
1078
+ u.password,
1079
+ STRING_AGG(a.provider, ',') as providers
1080
+ FROM _accounts u
1081
+ LEFT JOIN _account_providers a ON u.id = a.user_id
1082
+ `;
1083
+ const params: (string | number)[] = [];
1084
+
1085
+ if (search) {
1086
+ query += ' WHERE u.email LIKE $1 OR u.name LIKE $2';
1087
+ params.push(`%${search}%`, `%${search}%`);
1088
+ }
1089
+
1090
+ query += ` GROUP BY u.id ORDER BY u.created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
1091
+ params.push(limit, offset);
1092
+
1093
+ const result = await pool.query(query, params);
1094
+ const dbUsers = result.rows as UserRecord[];
1095
+
1096
+ // Transform users
1097
+ const users = dbUsers.map((dbUser) => this.transformUserRecordToSchema(dbUser));
1098
+
1099
+ // Get total count
1100
+ let countQuery = 'SELECT COUNT(*) as count FROM _accounts';
1101
+ const countParams: string[] = [];
1102
+ if (search) {
1103
+ countQuery += ' WHERE email LIKE $1 OR name LIKE $2';
1104
+ countParams.push(`%${search}%`, `%${search}%`);
1105
+ }
1106
+ const countResult = await pool.query(countQuery, countParams);
1107
+ const count = countResult.rows[0].count;
1108
+
1109
+ return {
1110
+ users,
1111
+ total: parseInt(count, 10),
1112
+ };
1113
+ }
1114
+
1115
+ /**
1116
+ * Get user by ID (returns UserSchema for API)
1117
+ */
1118
+ async getUserSchemaById(userId: string): Promise<UserSchema | null> {
1119
+ const dbUser = await this.getUserById(userId);
1120
+ if (!dbUser) {
1121
+ return null;
1122
+ }
1123
+ return this.transformUserRecordToSchema(dbUser);
1124
+ }
1125
+
1126
+ /**
1127
+ * Delete multiple users by IDs
1128
+ */
1129
+ async deleteUsers(userIds: string[]): Promise<number> {
1130
+ const pool = this.getPool();
1131
+ const placeholders = userIds.map((_, i) => `$${i + 1}`).join(',');
1132
+ const result = await pool.query(`DELETE FROM _accounts WHERE id IN (${placeholders})`, userIds);
1133
+
1134
+ return result.rowCount || 0;
1135
+ }
1136
+ }