insforge 1.2.10 → 1.3.0

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 (335) hide show
  1. package/.claude-plugin/marketplace.json +20 -20
  2. package/.dockerignore +60 -60
  3. package/.env.example +83 -77
  4. package/.github/ISSUE_TEMPLATE/bug_report.yml +36 -36
  5. package/.github/ISSUE_TEMPLATE/config.yml +11 -11
  6. package/.github/ISSUE_TEMPLATE/feature_request.yml +26 -26
  7. package/.github/PULL_REQUEST_TEMPLATE.md +7 -7
  8. package/.github/copilot-instructions.md +146 -146
  9. package/.github/workflows/build-image.yml +65 -65
  10. package/.github/workflows/ci-premerge-check.yml +23 -23
  11. package/.github/workflows/e2e.yml +63 -63
  12. package/.github/workflows/lint-and-format.yml +32 -32
  13. package/.prettierignore +64 -64
  14. package/CHANGELOG.md +44 -44
  15. package/CLAUDE_PLUGIN.md +104 -104
  16. package/CODE_OF_CONDUCT.md +128 -128
  17. package/CONTRIBUTING.md +125 -125
  18. package/Dockerfile +30 -30
  19. package/GITHUB_OAUTH_SETUP.md +49 -49
  20. package/GOOGLE_OAUTH_SETUP.md +148 -148
  21. package/LICENSE +201 -201
  22. package/README.md +182 -182
  23. package/assets/Dark.svg +23 -23
  24. package/auth/package.json +28 -28
  25. package/auth/src/lib/broadcastService.ts +117 -115
  26. package/auth/src/pages/SignInPage.tsx +60 -57
  27. package/auth/src/pages/SignUpPage.tsx +60 -57
  28. package/auth/tsconfig.json +32 -32
  29. package/auth/tsconfig.node.json +11 -11
  30. package/backend/package.json +78 -75
  31. package/backend/src/api/routes/ai/index.routes.ts +3 -3
  32. package/backend/src/api/routes/auth/index.routes.ts +667 -570
  33. package/backend/src/api/routes/auth/oauth.routes.ts +473 -448
  34. package/backend/src/api/routes/database/advance.routes.ts +37 -16
  35. package/backend/src/api/routes/database/index.routes.ts +78 -1
  36. package/backend/src/api/routes/database/records.routes.ts +10 -10
  37. package/backend/src/api/routes/database/tables.routes.ts +0 -14
  38. package/backend/src/api/routes/docs/index.routes.ts +75 -76
  39. package/backend/src/api/routes/email/index.routes.ts +35 -0
  40. package/backend/src/api/routes/functions/index.routes.ts +18 -12
  41. package/backend/src/api/routes/metadata/index.routes.ts +12 -0
  42. package/backend/src/api/routes/realtime/channels.routes.ts +81 -0
  43. package/backend/src/api/routes/realtime/index.routes.ts +12 -0
  44. package/backend/src/api/routes/realtime/messages.routes.ts +48 -0
  45. package/backend/src/api/routes/realtime/permissions.routes.ts +19 -0
  46. package/backend/src/api/routes/storage/index.routes.ts +18 -12
  47. package/backend/src/api/routes/usage/index.routes.ts +6 -4
  48. package/backend/src/infra/database/database.manager.ts +14 -1
  49. package/backend/src/infra/database/migrations/000_create-base-tables.sql +141 -141
  50. package/backend/src/infra/database/migrations/001_create-helper-functions.sql +40 -40
  51. package/backend/src/infra/database/migrations/002_rename-auth-tables.sql +29 -29
  52. package/backend/src/infra/database/migrations/003_create-users-table.sql +55 -55
  53. package/backend/src/infra/database/migrations/004_add-reload-postgrest-func.sql +23 -23
  54. package/backend/src/infra/database/migrations/005_enable-project-admin-modify-users.sql +29 -29
  55. package/backend/src/infra/database/migrations/006_modify-ai-usage-table.sql +24 -24
  56. package/backend/src/infra/database/migrations/007_drop-metadata-table.sql +1 -1
  57. package/backend/src/infra/database/migrations/008_add-system-tables.sql +76 -76
  58. package/backend/src/infra/database/migrations/009_add-function-secrets.sql +23 -23
  59. package/backend/src/infra/database/migrations/010_modify-ai-config-modalities.sql +93 -93
  60. package/backend/src/infra/database/migrations/011_refactor-secrets-table.sql +15 -15
  61. package/backend/src/infra/database/migrations/012_add-storage-uploaded-by.sql +7 -7
  62. package/backend/src/infra/database/migrations/013_create-auth-schema-functions.sql +44 -44
  63. package/backend/src/infra/database/migrations/014_add-updated-at-trigger-user-table.sql +7 -7
  64. package/backend/src/infra/database/migrations/015_create-auth-config-and-email-otp-tables.sql +59 -59
  65. package/backend/src/infra/database/migrations/016_update-auth-config-and-email-otp.sql +24 -24
  66. package/backend/src/infra/database/migrations/017_create-realtime-schema.sql +233 -0
  67. package/backend/src/infra/realtime/realtime.manager.ts +246 -0
  68. package/backend/src/infra/realtime/webhook-sender.ts +82 -0
  69. package/backend/src/infra/security/token.manager.ts +219 -125
  70. package/backend/src/infra/socket/socket.manager.ts +198 -64
  71. package/backend/src/providers/ai/openrouter.provider.ts +12 -9
  72. package/backend/src/providers/email/base.provider.ts +4 -7
  73. package/backend/src/providers/email/cloud.provider.ts +84 -0
  74. package/backend/src/providers/oauth/apple.provider.ts +266 -0
  75. package/backend/src/providers/oauth/index.ts +1 -0
  76. package/backend/src/server.ts +317 -284
  77. package/backend/src/services/ai/ai-model.service.ts +5 -5
  78. package/backend/src/services/ai/chat-completion.service.ts +4 -4
  79. package/backend/src/services/ai/image-generation.service.ts +3 -3
  80. package/backend/src/services/auth/auth.service.ts +14 -0
  81. package/backend/src/services/database/database-table.service.ts +0 -9
  82. package/backend/src/services/database/database.service.ts +127 -0
  83. package/backend/src/services/email/email.service.ts +5 -7
  84. package/backend/src/services/realtime/index.ts +3 -0
  85. package/backend/src/services/realtime/realtime-auth.service.ts +104 -0
  86. package/backend/src/services/realtime/realtime-channel.service.ts +237 -0
  87. package/backend/src/services/realtime/realtime-message.service.ts +260 -0
  88. package/backend/src/types/auth.ts +11 -0
  89. package/backend/src/types/realtime.ts +18 -0
  90. package/backend/src/types/socket.ts +7 -31
  91. package/backend/src/utils/cookies.ts +35 -0
  92. package/backend/src/utils/s3-config-loader.ts +64 -0
  93. package/backend/src/utils/seed.ts +301 -298
  94. package/backend/src/utils/sql-parser.ts +90 -0
  95. package/backend/tests/README.md +133 -133
  96. package/backend/tests/cleanup-all-test-data.sh +230 -230
  97. package/backend/tests/cloud/test-s3-multitenant.sh +131 -131
  98. package/backend/tests/local/comprehensive-curl-tests.sh +155 -155
  99. package/backend/tests/local/test-ai-config.sh +129 -129
  100. package/backend/tests/local/test-ai-usage.sh +80 -80
  101. package/backend/tests/local/test-auth-router.sh +143 -143
  102. package/backend/tests/local/test-database-router.sh +222 -222
  103. package/backend/tests/local/test-e2e.sh +240 -240
  104. package/backend/tests/local/test-fk-errors.sh +96 -96
  105. package/backend/tests/local/test-functions.sh +123 -123
  106. package/backend/tests/local/test-id-field.sh +200 -200
  107. package/backend/tests/local/test-logs.sh +132 -132
  108. package/backend/tests/local/test-public-bucket.sh +264 -264
  109. package/backend/tests/local/test-secrets.sh +249 -249
  110. package/backend/tests/local/test-serverless-functions.sh.disabled +325 -325
  111. package/backend/tests/local/test-traditional-rest.sh +208 -208
  112. package/backend/tests/manual/README.md +50 -50
  113. package/backend/tests/manual/create-large-table-simple.sql +10 -10
  114. package/backend/tests/manual/seed-large-table.sql +100 -100
  115. package/backend/tests/manual/setup-large-table-extras.sql +33 -33
  116. package/backend/tests/manual/test-bulk-upsert.sh +409 -409
  117. package/backend/tests/manual/test-database-advance.sh +296 -296
  118. package/backend/tests/manual/test-postgrest-stability.sh +191 -191
  119. package/backend/tests/manual/test-rawsql-export-import.sh +411 -411
  120. package/backend/tests/manual/test-rawsql-modes.sh +244 -244
  121. package/backend/tests/manual/test-universal-storage.sh +263 -263
  122. package/backend/tests/manual/test-users.sql +17 -17
  123. package/backend/tests/run-all-tests.sh +139 -139
  124. package/backend/tests/setup.ts +0 -0
  125. package/backend/tests/test-config.sh +338 -338
  126. package/backend/tests/unit/analyze-query.test.ts +697 -0
  127. package/backend/tsconfig.json +22 -22
  128. package/claude-plugin/.claude-plugin/plugin.json +24 -24
  129. package/claude-plugin/README.md +133 -133
  130. package/claude-plugin/skills/insforge-schema-patterns/SKILL.md +270 -270
  131. package/docker-compose.prod.yml +204 -200
  132. package/docker-compose.yml +232 -228
  133. package/docker-init/db/db-init.sql +97 -97
  134. package/docker-init/db/jwt.sql +5 -5
  135. package/docker-init/db/postgresql.conf +16 -16
  136. package/docker-init/logs/vector.yml +236 -236
  137. package/docs/README.md +44 -44
  138. package/docs/agent-docs/real-time.md +269 -0
  139. package/docs/changelog.mdx +119 -67
  140. package/docs/core-concepts/ai/architecture.mdx +372 -372
  141. package/docs/core-concepts/ai/sdk.mdx +213 -213
  142. package/docs/core-concepts/authentication/architecture.mdx +278 -278
  143. package/docs/core-concepts/authentication/sdk.mdx +414 -414
  144. package/docs/core-concepts/authentication/ui-components/customization.mdx +529 -529
  145. package/docs/core-concepts/authentication/ui-components/nextjs.mdx +221 -221
  146. package/docs/core-concepts/authentication/ui-components/react-router.mdx +184 -184
  147. package/docs/core-concepts/authentication/ui-components/react.mdx +129 -129
  148. package/docs/core-concepts/database/architecture.mdx +255 -255
  149. package/docs/core-concepts/database/sdk.mdx +382 -382
  150. package/docs/core-concepts/email/architecture.mdx +101 -0
  151. package/docs/core-concepts/email/sdk.mdx +53 -0
  152. package/docs/core-concepts/functions/architecture.mdx +105 -105
  153. package/docs/core-concepts/functions/sdk.mdx +184 -184
  154. package/docs/core-concepts/realtime/architecture.mdx +446 -0
  155. package/docs/core-concepts/realtime/sdk.mdx +409 -0
  156. package/docs/core-concepts/storage/architecture.mdx +243 -243
  157. package/docs/core-concepts/storage/sdk.mdx +253 -253
  158. package/docs/deployment/README.md +94 -94
  159. package/docs/deployment/deploy-to-aws-ec2.md +564 -564
  160. package/docs/deployment/deploy-to-azure-virtual-machines.md +312 -312
  161. package/docs/deployment/deploy-to-google-cloud-compute-engine.md +613 -613
  162. package/docs/deployment/deploy-to-render.md +441 -441
  163. package/docs/deprecated/insforge-auth-api.md +214 -214
  164. package/docs/deprecated/insforge-auth-sdk.md +99 -99
  165. package/docs/deprecated/insforge-db-api.md +358 -358
  166. package/docs/deprecated/insforge-db-sdk.md +139 -139
  167. package/docs/deprecated/insforge-debug-sdk.md +156 -156
  168. package/docs/deprecated/insforge-debug.md +64 -64
  169. package/docs/deprecated/insforge-instructions.md +123 -123
  170. package/docs/deprecated/insforge-project.md +117 -117
  171. package/docs/deprecated/insforge-storage-api.md +278 -278
  172. package/docs/deprecated/insforge-storage-sdk.md +158 -158
  173. package/docs/docs.json +232 -210
  174. package/docs/examples/framework-guides/nextjs.mdx +131 -131
  175. package/docs/examples/framework-guides/nuxt.mdx +165 -165
  176. package/docs/examples/framework-guides/react.mdx +165 -165
  177. package/docs/examples/framework-guides/svelte.mdx +153 -153
  178. package/docs/examples/framework-guides/vue.mdx +159 -159
  179. package/docs/examples/overview.mdx +67 -67
  180. package/docs/favicon.svg +19 -19
  181. package/docs/images/changelog/dec-2025/ai-integration.png +0 -0
  182. package/docs/images/changelog/dec-2025/ai-models.webp +0 -0
  183. package/docs/images/changelog/dec-2025/alipay-payment.webp +0 -0
  184. package/docs/images/changelog/dec-2025/apple-login.jpg +0 -0
  185. package/docs/images/changelog/dec-2025/mcp-installer.png +0 -0
  186. package/docs/images/changelog/dec-2025/realtime-module.jpg +0 -0
  187. package/docs/images/icons/ai.svg +4 -4
  188. package/docs/images/logos/nextjs.svg +4 -4
  189. package/docs/images/logos/nuxt.svg +4 -4
  190. package/docs/images/logos/react.svg +5 -5
  191. package/docs/images/logos/svelte.svg +4 -4
  192. package/docs/images/logos/vue.svg +5 -5
  193. package/docs/insforge-instructions-sdk.md +89 -88
  194. package/docs/introduction.mdx +45 -45
  195. package/docs/logo/dark.svg +22 -22
  196. package/docs/logo/light.svg +20 -20
  197. package/docs/partnership.mdx +651 -646
  198. package/docs/quickstart.mdx +82 -82
  199. package/docs/showcase.mdx +52 -52
  200. package/docs/snippets/sdk-installation.mdx +21 -21
  201. package/docs/snippets/service-icons.mdx +27 -27
  202. package/examples/oauth/frontend-oauth-example.html +250 -250
  203. package/examples/response-examples.md +443 -443
  204. package/frontend/components.json +17 -17
  205. package/frontend/package.json +69 -69
  206. package/frontend/src/assets/icons/checkbox_checked.svg +6 -6
  207. package/frontend/src/assets/icons/checkbox_undetermined.svg +6 -6
  208. package/frontend/src/assets/icons/checked.svg +3 -3
  209. package/frontend/src/assets/icons/connected.svg +3 -3
  210. package/frontend/src/assets/icons/error.svg +3 -3
  211. package/frontend/src/assets/icons/loader.svg +9 -9
  212. package/frontend/src/assets/icons/pencil.svg +4 -4
  213. package/frontend/src/assets/icons/refresh.svg +4 -4
  214. package/frontend/src/assets/icons/step_active.svg +3 -3
  215. package/frontend/src/assets/icons/step_inactive.svg +11 -11
  216. package/frontend/src/assets/icons/warning.svg +3 -3
  217. package/frontend/src/assets/logos/apple.svg +3 -3
  218. package/frontend/src/assets/logos/claude_code.svg +3 -3
  219. package/frontend/src/assets/logos/cline.svg +6 -6
  220. package/frontend/src/assets/logos/cursor.svg +20 -20
  221. package/frontend/src/assets/logos/discord.svg +8 -8
  222. package/frontend/src/assets/logos/facebook.svg +3 -3
  223. package/frontend/src/assets/logos/gemini.svg +19 -19
  224. package/frontend/src/assets/logos/github.svg +5 -5
  225. package/frontend/src/assets/logos/google.svg +13 -13
  226. package/frontend/src/assets/logos/grok.svg +10 -10
  227. package/frontend/src/assets/logos/insforge_dark.svg +15 -15
  228. package/frontend/src/assets/logos/insforge_light.svg +15 -15
  229. package/frontend/src/assets/logos/instagram.svg +1 -1
  230. package/frontend/src/assets/logos/linkedin.svg +3 -3
  231. package/frontend/src/assets/logos/openai.svg +10 -10
  232. package/frontend/src/assets/logos/roo_code.svg +9 -9
  233. package/frontend/src/assets/logos/spotify.svg +16 -16
  234. package/frontend/src/assets/logos/tiktok.svg +5 -5
  235. package/frontend/src/assets/logos/trae.svg +3 -3
  236. package/frontend/src/assets/logos/windsurf.svg +10 -10
  237. package/frontend/src/assets/logos/x.svg +3 -3
  238. package/frontend/src/components/layout/AppHeader.tsx +9 -10
  239. package/frontend/src/features/auth/components/OAuthConfigDialog.tsx +1 -0
  240. package/frontend/src/features/auth/components/UsersDataGrid.tsx +6 -0
  241. package/frontend/src/features/auth/helpers.tsx +8 -0
  242. package/frontend/src/features/auth/{page → pages}/UsersPage.tsx +0 -28
  243. package/frontend/src/features/database/components/SQLModal.tsx +75 -0
  244. package/frontend/src/features/database/components/TableForm.tsx +0 -4
  245. package/frontend/src/features/database/hooks/useDatabase.ts +66 -0
  246. package/frontend/src/features/database/hooks/useTables.ts +32 -28
  247. package/frontend/src/features/database/index.ts +1 -0
  248. package/frontend/src/features/database/{page → pages}/FunctionsPage.tsx +29 -37
  249. package/frontend/src/features/database/{page → pages}/IndexesPage.tsx +35 -47
  250. package/frontend/src/features/database/{page → pages}/PoliciesPage.tsx +43 -54
  251. package/frontend/src/features/database/{page → pages}/TablesPage.tsx +0 -42
  252. package/frontend/src/features/database/{page → pages}/TriggersPage.tsx +35 -47
  253. package/frontend/src/features/database/services/advance.service.ts +0 -26
  254. package/frontend/src/features/database/services/database.service.ts +55 -0
  255. package/frontend/src/features/database/services/table.service.ts +0 -6
  256. package/frontend/src/features/functions/{page → pages}/FunctionsPage.tsx +21 -44
  257. package/frontend/src/features/functions/{page → pages}/SecretsPage.tsx +11 -9
  258. package/frontend/src/features/logs/hooks/useMcpUsage.ts +13 -66
  259. package/frontend/src/features/realtime/components/ChannelRow.tsx +83 -0
  260. package/frontend/src/features/realtime/components/EditChannelModal.tsx +246 -0
  261. package/frontend/src/features/realtime/components/MessageRow.tsx +85 -0
  262. package/frontend/src/features/realtime/components/RealtimeEmptyState.tsx +30 -0
  263. package/frontend/src/features/realtime/hooks/useRealtime.ts +218 -0
  264. package/frontend/src/features/realtime/index.ts +11 -0
  265. package/frontend/src/features/realtime/pages/RealtimeChannelsPage.tsx +172 -0
  266. package/frontend/src/features/realtime/pages/RealtimeMessagesPage.tsx +211 -0
  267. package/frontend/src/features/realtime/pages/RealtimePermissionsPage.tsx +191 -0
  268. package/frontend/src/features/realtime/services/realtime.service.ts +107 -0
  269. package/frontend/src/features/storage/{page → pages}/StoragePage.tsx +1 -29
  270. package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +3 -3
  271. package/frontend/src/features/visualizer/{page → pages}/VisualizerPage.tsx +1 -35
  272. package/frontend/src/lib/contexts/SocketContext.tsx +119 -75
  273. package/frontend/src/lib/routing/AppRoutes.tsx +35 -20
  274. package/frontend/src/lib/utils/cloudMessaging.ts +1 -1
  275. package/frontend/src/lib/utils/menuItems.ts +24 -0
  276. package/frontend/src/lib/utils/utils.ts +14 -1
  277. package/frontend/tsconfig.json +25 -25
  278. package/frontend/tsconfig.node.json +9 -9
  279. package/functions/deno.json +24 -24
  280. package/functions/server.ts +315 -315
  281. package/i18n/README.ar.md +130 -130
  282. package/i18n/README.de.md +130 -130
  283. package/i18n/README.es.md +154 -154
  284. package/i18n/README.fr.md +134 -134
  285. package/i18n/README.hi.md +129 -129
  286. package/i18n/README.ja.md +174 -174
  287. package/i18n/README.ko.md +136 -136
  288. package/i18n/README.pt-BR.md +131 -131
  289. package/i18n/README.ru.md +129 -129
  290. package/i18n/README.zh-CN.md +133 -133
  291. package/openapi/ai.yaml +715 -715
  292. package/openapi/auth.yaml +1244 -1244
  293. package/openapi/email.yaml +158 -0
  294. package/openapi/functions.yaml +475 -475
  295. package/openapi/health.yaml +29 -29
  296. package/openapi/logs.yaml +223 -223
  297. package/openapi/metadata.yaml +177 -177
  298. package/openapi/realtime.yaml +699 -0
  299. package/openapi/records.yaml +381 -381
  300. package/openapi/secrets.yaml +370 -370
  301. package/openapi/storage.yaml +875 -875
  302. package/openapi/tables.yaml +463 -463
  303. package/package.json +97 -97
  304. package/shared-schemas/package.json +31 -31
  305. package/shared-schemas/src/ai.schema.ts +63 -59
  306. package/shared-schemas/src/auth-api.schema.ts +352 -339
  307. package/shared-schemas/src/auth.schema.ts +1 -1
  308. package/shared-schemas/src/database-api.schema.ts +32 -1
  309. package/shared-schemas/src/database.schema.ts +39 -0
  310. package/shared-schemas/src/docs.schema.ts +26 -0
  311. package/shared-schemas/src/email-api.schema.ts +30 -0
  312. package/shared-schemas/src/index.ts +4 -0
  313. package/shared-schemas/src/metadata.schema.ts +9 -0
  314. package/shared-schemas/src/realtime-api.schema.ts +111 -0
  315. package/shared-schemas/src/realtime.schema.ts +143 -0
  316. package/shared-schemas/tsconfig.json +21 -21
  317. package/tsconfig.json +7 -7
  318. package/zeabur/README.md +13 -13
  319. package/zeabur/template.yml +1032 -1032
  320. package/.cursor/rules/cursor-rules.mdc +0 -94
  321. package/frontend/src/features/database/hooks/useFullMetadata.ts +0 -18
  322. package/test-gemini.sh +0 -35
  323. package/test-usage-admin.sh +0 -57
  324. package/test-usage.sh +0 -50
  325. /package/frontend/src/features/ai/{page → pages}/AIPage.tsx +0 -0
  326. /package/frontend/src/features/auth/{page → pages}/AuthMethodsPage.tsx +0 -0
  327. /package/frontend/src/features/auth/{page → pages}/ConfigurationPage.tsx +0 -0
  328. /package/frontend/src/features/dashboard/{page → pages}/DashboardPage.tsx +0 -0
  329. /package/frontend/src/features/database/{page → pages}/SQLEditorPage.tsx +0 -0
  330. /package/frontend/src/features/database/{page → pages}/TemplatesPage.tsx +0 -0
  331. /package/frontend/src/features/login/{page → pages}/CloudLoginPage.tsx +0 -0
  332. /package/frontend/src/features/login/{page → pages}/LoginPage.tsx +0 -0
  333. /package/frontend/src/features/logs/{page → pages}/AuditsPage.tsx +0 -0
  334. /package/frontend/src/features/logs/{page → pages}/LogsPage.tsx +0 -0
  335. /package/frontend/src/features/logs/{page → pages}/MCPLogsPage.tsx +0 -0
@@ -1,570 +1,667 @@
1
- import { Router, Request, Response, NextFunction } from 'express';
2
- import { AuthService } from '@/services/auth/auth.service.js';
3
- import { AuthConfigService } from '@/services/auth/auth-config.service.js';
4
- import { OAuthConfigService } from '@/services/auth/oauth-config.service.js';
5
- import { AuditService } from '@/services/logs/audit.service.js';
6
- import { TokenManager } from '@/infra/security/token.manager.js';
7
- import { AppError } from '@/api/middlewares/error.js';
8
- import { ERROR_CODES } from '@/types/error-constants.js';
9
- import { successResponse } from '@/utils/response.js';
10
- import { AuthRequest, verifyAdmin, verifyToken } from '@/api/middlewares/auth.js';
11
- import oauthRouter from './oauth.routes.js';
12
- import { sendEmailOTPLimiter, verifyOTPLimiter } from '@/api/middlewares/rate-limiters.js';
13
- import {
14
- userIdSchema,
15
- createUserRequestSchema,
16
- createSessionRequestSchema,
17
- createAdminSessionRequestSchema,
18
- deleteUsersRequestSchema,
19
- listUsersRequestSchema,
20
- sendVerificationEmailRequestSchema,
21
- verifyEmailRequestSchema,
22
- sendResetPasswordEmailRequestSchema,
23
- exchangeResetPasswordTokenRequestSchema,
24
- resetPasswordRequestSchema,
25
- type CreateUserResponse,
26
- type CreateSessionResponse,
27
- type VerifyEmailResponse,
28
- type ExchangeResetPasswordTokenResponse,
29
- type ResetPasswordResponse,
30
- type CreateAdminSessionResponse,
31
- type GetCurrentSessionResponse,
32
- type ListUsersResponse,
33
- type DeleteUsersResponse,
34
- type GetPublicAuthConfigResponse,
35
- exchangeAdminSessionRequestSchema,
36
- type GetAuthConfigResponse,
37
- updateAuthConfigRequestSchema,
38
- } from '@insforge/shared-schemas';
39
- import { SocketManager } from '@/infra/socket/socket.manager.js';
40
- import { DataUpdateResourceType, ServerEvents } from '@/types/socket.js';
41
-
42
- const router = Router();
43
- const authService = AuthService.getInstance();
44
- const authConfigService = AuthConfigService.getInstance();
45
- const oAuthConfigService = OAuthConfigService.getInstance();
46
- const auditService = AuditService.getInstance();
47
-
48
- // Mount OAuth routes
49
- router.use('/oauth', oauthRouter);
50
-
51
- // Public Authentication Configuration Routes
52
- // GET /api/auth/public-config - Get all public authentication configuration (public endpoint)
53
- router.get('/public-config', async (req: Request, res: Response, next: NextFunction) => {
54
- try {
55
- const [oAuthProviders, authConfigs] = await Promise.all([
56
- oAuthConfigService.getConfiguredProviders(),
57
- authConfigService.getPublicAuthConfig(),
58
- ]);
59
-
60
- const response: GetPublicAuthConfigResponse = {
61
- oAuthProviders,
62
- ...authConfigs,
63
- };
64
-
65
- successResponse(res, response);
66
- } catch (error) {
67
- next(error);
68
- }
69
- });
70
-
71
- // Email Authentication Configuration Routes
72
- // GET /api/auth/config - Get authentication configurations (admin only)
73
- router.get('/config', verifyAdmin, async (req: AuthRequest, res: Response, next: NextFunction) => {
74
- try {
75
- const config: GetAuthConfigResponse = await authConfigService.getAuthConfig();
76
- successResponse(res, config);
77
- } catch (error) {
78
- next(error);
79
- }
80
- });
81
-
82
- // PUT /api/auth/config - Update authentication configurations (admin only)
83
- router.put('/config', verifyAdmin, async (req: AuthRequest, res: Response, next: NextFunction) => {
84
- try {
85
- const validationResult = updateAuthConfigRequestSchema.safeParse(req.body);
86
- if (!validationResult.success) {
87
- throw new AppError(
88
- validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
89
- 400,
90
- ERROR_CODES.INVALID_INPUT
91
- );
92
- }
93
-
94
- const input = validationResult.data;
95
- const config: GetAuthConfigResponse = await authConfigService.updateAuthConfig(input);
96
-
97
- await auditService.log({
98
- actor: req.user?.email || 'api-key',
99
- action: 'UPDATE_AUTH_CONFIG',
100
- module: 'AUTH',
101
- details: {
102
- updatedFields: Object.keys(input),
103
- },
104
- ip_address: req.ip,
105
- });
106
-
107
- successResponse(res, config);
108
- } catch (error) {
109
- next(error);
110
- }
111
- });
112
-
113
- // POST /api/auth/users - Create a new user (registration)
114
- router.post('/users', async (req: Request, res: Response, next: NextFunction) => {
115
- try {
116
- const validationResult = createUserRequestSchema.safeParse(req.body);
117
- if (!validationResult.success) {
118
- throw new AppError(
119
- validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
120
- 400,
121
- ERROR_CODES.INVALID_INPUT
122
- );
123
- }
124
-
125
- const { email, password, name } = validationResult.data;
126
- const result: CreateUserResponse = await authService.register(email, password, name);
127
-
128
- const socket = SocketManager.getInstance();
129
- socket.broadcastToRoom('role:project_admin', ServerEvents.DATA_UPDATE, {
130
- resource: DataUpdateResourceType.USERS,
131
- });
132
-
133
- successResponse(res, result);
134
- } catch (error) {
135
- next(error);
136
- }
137
- });
138
-
139
- // POST /api/auth/sessions - Create a new session (login)
140
- router.post('/sessions', async (req: Request, res: Response, next: NextFunction) => {
141
- try {
142
- const validationResult = createSessionRequestSchema.safeParse(req.body);
143
- if (!validationResult.success) {
144
- throw new AppError(
145
- validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
146
- 400,
147
- ERROR_CODES.INVALID_INPUT
148
- );
149
- }
150
-
151
- const { email, password } = validationResult.data;
152
- const result: CreateSessionResponse = await authService.login(email, password);
153
-
154
- successResponse(res, result);
155
- } catch (error) {
156
- next(error);
157
- }
158
- });
159
-
160
- // POST /api/auth/admin/sessions/exchange - Create admin session
161
- router.post('/admin/sessions/exchange', async (req: Request, res: Response, next: NextFunction) => {
162
- try {
163
- const validationResult = exchangeAdminSessionRequestSchema.safeParse(req.body);
164
- if (!validationResult.success) {
165
- throw new AppError(
166
- validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
167
- 400,
168
- ERROR_CODES.INVALID_INPUT
169
- );
170
- }
171
-
172
- const { code } = validationResult.data;
173
- const result: CreateAdminSessionResponse =
174
- await authService.adminLoginWithAuthorizationCode(code);
175
-
176
- successResponse(res, result);
177
- } catch (error) {
178
- if (error instanceof AppError) {
179
- next(error);
180
- } else {
181
- // Convert other errors (like JWT verification errors) to 400
182
- next(
183
- new AppError(
184
- 'Failed to exchange admin session' + (error instanceof Error ? `: ${error.message}` : ''),
185
- 400,
186
- ERROR_CODES.INVALID_INPUT
187
- )
188
- );
189
- }
190
- }
191
- });
192
-
193
- // POST /api/auth/admin/sessions - Create admin session
194
- router.post('/admin/sessions', (req: Request, res: Response, next: NextFunction) => {
195
- try {
196
- const validationResult = createAdminSessionRequestSchema.safeParse(req.body);
197
- if (!validationResult.success) {
198
- throw new AppError(
199
- validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
200
- 400,
201
- ERROR_CODES.INVALID_INPUT
202
- );
203
- }
204
-
205
- const { email, password } = validationResult.data;
206
- const result: CreateAdminSessionResponse = authService.adminLogin(email, password);
207
-
208
- successResponse(res, result);
209
- } catch (error) {
210
- next(error);
211
- }
212
- });
213
-
214
- // GET /api/auth/sessions/current - Get current session user
215
- router.get(
216
- '/sessions/current',
217
- verifyToken,
218
- (req: AuthRequest, res: Response, next: NextFunction) => {
219
- try {
220
- if (!req.user) {
221
- throw new AppError('User not authenticated', 401, ERROR_CODES.AUTH_INVALID_CREDENTIALS);
222
- }
223
-
224
- const response: GetCurrentSessionResponse = {
225
- user: {
226
- id: req.user.id,
227
- email: req.user.email,
228
- role: req.user.role as 'authenticated' | 'project_admin',
229
- },
230
- };
231
-
232
- successResponse(res, response);
233
- } catch (error) {
234
- next(error);
235
- }
236
- }
237
- );
238
-
239
- // GET /api/auth/users - List all users (admin only)
240
- router.get('/users', verifyAdmin, async (req: Request, res: Response, next: NextFunction) => {
241
- try {
242
- const queryValidation = listUsersRequestSchema.safeParse(req.query);
243
- const queryParams = queryValidation.success ? queryValidation.data : req.query;
244
- const { limit = '10', offset = '0', search } = queryParams || {};
245
-
246
- const parsedLimit = parseInt(limit as string);
247
- const parsedOffset = parseInt(offset as string);
248
-
249
- const { users, total } = await authService.listUsers(
250
- parsedLimit,
251
- parsedOffset,
252
- search as string | undefined
253
- );
254
-
255
- const response: ListUsersResponse = {
256
- data: users,
257
- pagination: {
258
- offset: parsedOffset,
259
- limit: parsedLimit,
260
- total: total,
261
- },
262
- };
263
-
264
- successResponse(res, response);
265
- } catch (error) {
266
- next(error);
267
- }
268
- });
269
-
270
- // GET /api/auth/users/:id - Get specific user (admin only)
271
- router.get(
272
- '/users/:userId',
273
- verifyAdmin,
274
- async (req: Request, res: Response, next: NextFunction) => {
275
- try {
276
- // Validate userId path parameter directly
277
- const userIdValidation = userIdSchema.safeParse(req.params.userId);
278
- if (!userIdValidation.success) {
279
- throw new AppError('Invalid user ID format', 400, ERROR_CODES.INVALID_INPUT);
280
- }
281
-
282
- const userId = userIdValidation.data;
283
- const user = await authService.getUserSchemaById(userId);
284
-
285
- if (!user) {
286
- throw new AppError('User not found', 404, ERROR_CODES.NOT_FOUND);
287
- }
288
-
289
- successResponse(res, user);
290
- } catch (error) {
291
- next(error);
292
- }
293
- }
294
- );
295
-
296
- // DELETE /api/auth/users - Delete users (batch operation, admin only)
297
- router.delete(
298
- '/users',
299
- verifyAdmin,
300
- async (req: AuthRequest, res: Response, next: NextFunction) => {
301
- try {
302
- const validationResult = deleteUsersRequestSchema.safeParse(req.body);
303
- if (!validationResult.success) {
304
- throw new AppError(
305
- validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
306
- 400,
307
- ERROR_CODES.INVALID_INPUT
308
- );
309
- }
310
-
311
- const { userIds } = validationResult.data;
312
-
313
- const deletedCount = await authService.deleteUsers(userIds);
314
-
315
- // Log audit for user deletion
316
- await auditService.log({
317
- actor: req.user?.email || 'api-key',
318
- action: 'DELETE_USERS',
319
- module: 'AUTH',
320
- details: {
321
- userIds,
322
- deletedCount,
323
- },
324
- ip_address: req.ip,
325
- });
326
-
327
- const response: DeleteUsersResponse = {
328
- message: 'Users deleted successfully',
329
- deletedCount,
330
- };
331
-
332
- successResponse(res, response);
333
- } catch (error) {
334
- next(error);
335
- }
336
- }
337
- );
338
-
339
- // POST /api/auth/tokens/anon - Generate anonymous JWT token (never expires)
340
- router.post('/tokens/anon', verifyAdmin, (_req: Request, res: Response, next: NextFunction) => {
341
- try {
342
- const tokenManager = TokenManager.getInstance();
343
- const token = tokenManager.generateAnonToken();
344
-
345
- successResponse(res, {
346
- accessToken: token,
347
- message: 'Anonymous token generated successfully (never expires)',
348
- });
349
- } catch (error) {
350
- next(error);
351
- }
352
- });
353
-
354
- // POST /api/auth/email/send-verification - Send email verification (code or link based on config)
355
- router.post(
356
- '/email/send-verification',
357
- sendEmailOTPLimiter,
358
- async (req: Request, res: Response, next: NextFunction) => {
359
- try {
360
- const validationResult = sendVerificationEmailRequestSchema.safeParse(req.body);
361
- if (!validationResult.success) {
362
- throw new AppError(
363
- validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
364
- 400,
365
- ERROR_CODES.INVALID_INPUT
366
- );
367
- }
368
-
369
- const { email } = validationResult.data;
370
-
371
- // Get auth config to determine verification method
372
- const authConfig = await authConfigService.getAuthConfig();
373
- const method = authConfig.verifyEmailMethod;
374
-
375
- // Note: User enumeration is prevented at service layer
376
- // Service returns gracefully (no error) if user not found
377
- if (method === 'link') {
378
- await authService.sendVerificationEmailWithLink(email);
379
- } else {
380
- await authService.sendVerificationEmailWithCode(email);
381
- }
382
-
383
- // Always return 202 Accepted with generic message
384
- const message =
385
- method === 'link'
386
- ? 'If your email is registered, we have sent you a verification link. Please check your inbox.'
387
- : 'If your email is registered, we have sent you a verification code. Please check your inbox.';
388
-
389
- successResponse(
390
- res,
391
- {
392
- success: true,
393
- message,
394
- },
395
- 202
396
- );
397
- } catch (error) {
398
- next(error);
399
- }
400
- }
401
- );
402
-
403
- // POST /api/auth/email/verify - Verify email with OTP
404
- // Uses verifyEmailMethod from auth config to determine verification type:
405
- // - 'code': expects email + 6-digit numeric code
406
- // - 'link': expects 64-char hex token only
407
- router.post(
408
- '/email/verify',
409
- verifyOTPLimiter,
410
- async (req: Request, res: Response, next: NextFunction) => {
411
- try {
412
- const validationResult = verifyEmailRequestSchema.safeParse(req.body);
413
- if (!validationResult.success) {
414
- throw new AppError(
415
- validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
416
- 400,
417
- ERROR_CODES.INVALID_INPUT
418
- );
419
- }
420
-
421
- const { email, otp } = validationResult.data;
422
-
423
- // Get auth config to determine verification method
424
- const authConfig = await authConfigService.getAuthConfig();
425
- const method = authConfig.verifyEmailMethod;
426
-
427
- let result: VerifyEmailResponse;
428
-
429
- if (method === 'link') {
430
- // Link verification: otp is 64-char hex token
431
- result = await authService.verifyEmailWithToken(otp);
432
- } else {
433
- // Code verification: requires email + 6-digit code
434
- if (!email) {
435
- throw new AppError(
436
- 'Email is required for code verification',
437
- 400,
438
- ERROR_CODES.INVALID_INPUT
439
- );
440
- }
441
- result = await authService.verifyEmailWithCode(email, otp);
442
- }
443
-
444
- successResponse(res, result); // Return session info with optional redirectTo upon successful verification
445
- } catch (error) {
446
- next(error);
447
- }
448
- }
449
- );
450
-
451
- // POST /api/auth/email/send-reset-password - Send password reset (code or link based on config)
452
- router.post(
453
- '/email/send-reset-password',
454
- sendEmailOTPLimiter,
455
- async (req: Request, res: Response, next: NextFunction) => {
456
- try {
457
- const validationResult = sendResetPasswordEmailRequestSchema.safeParse(req.body);
458
- if (!validationResult.success) {
459
- throw new AppError(
460
- validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
461
- 400,
462
- ERROR_CODES.INVALID_INPUT
463
- );
464
- }
465
-
466
- const { email } = validationResult.data;
467
-
468
- // Get auth config to determine reset password method
469
- const authConfig = await authConfigService.getAuthConfig();
470
- const method = authConfig.resetPasswordMethod;
471
-
472
- // Note: User enumeration is prevented at service layer
473
- // Service returns gracefully (no error) if user not found
474
- if (method === 'link') {
475
- await authService.sendResetPasswordEmailWithLink(email);
476
- } else {
477
- await authService.sendResetPasswordEmailWithCode(email);
478
- }
479
-
480
- // Always return 202 Accepted with generic message
481
- const message =
482
- method === 'link'
483
- ? 'If your email is registered, we have sent you a password reset link. Please check your inbox.'
484
- : 'If your email is registered, we have sent you a password reset code. Please check your inbox.';
485
-
486
- successResponse(
487
- res,
488
- {
489
- success: true,
490
- message,
491
- },
492
- 202
493
- );
494
- } catch (error) {
495
- next(error);
496
- }
497
- }
498
- );
499
-
500
- // POST /api/auth/email/exchange-reset-password-token - Exchange reset password code for reset token
501
- // Step 1 of two-step password reset flow: verify code → get reset token
502
- // Only used when resetPasswordMethod is 'code'
503
- router.post(
504
- '/email/exchange-reset-password-token',
505
- verifyOTPLimiter,
506
- async (req: Request, res: Response, next: NextFunction) => {
507
- try {
508
- const validationResult = exchangeResetPasswordTokenRequestSchema.safeParse(req.body);
509
- if (!validationResult.success) {
510
- throw new AppError(
511
- validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
512
- 400,
513
- ERROR_CODES.INVALID_INPUT
514
- );
515
- }
516
-
517
- const { email, code } = validationResult.data;
518
-
519
- const result = await authService.exchangeResetPasswordToken(email, code);
520
-
521
- const response: ExchangeResetPasswordTokenResponse = {
522
- token: result.token,
523
- expiresAt: result.expiresAt.toISOString(),
524
- };
525
-
526
- successResponse(res, response);
527
- } catch (error) {
528
- next(error);
529
- }
530
- }
531
- );
532
-
533
- // POST /api/auth/email/reset-password - Reset password with token
534
- // Token can be:
535
- // - Magic link token (from send-reset-password endpoint when method is 'link')
536
- // - Reset token (from exchange-reset-password-token endpoint after code verification)
537
- // Both use RESET_PASSWORD purpose and are verified the same way
538
- // Flow:
539
- // Code: send-reset-password → exchange-reset-password-token → reset-password (with resetToken)
540
- // Link: send-reset-password reset-password (with link token)
541
- router.post(
542
- '/email/reset-password',
543
- verifyOTPLimiter,
544
- async (req: Request, res: Response, next: NextFunction) => {
545
- try {
546
- const validationResult = resetPasswordRequestSchema.safeParse(req.body);
547
- if (!validationResult.success) {
548
- throw new AppError(
549
- validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
550
- 400,
551
- ERROR_CODES.INVALID_INPUT
552
- );
553
- }
554
-
555
- const { newPassword, otp } = validationResult.data;
556
-
557
- // Both magic link tokens and code-verified reset tokens use RESET_PASSWORD purpose
558
- const result: ResetPasswordResponse = await authService.resetPasswordWithToken(
559
- newPassword,
560
- otp
561
- );
562
-
563
- successResponse(res, result); // Return message with optional redirectTo
564
- } catch (error) {
565
- next(error);
566
- }
567
- }
568
- );
569
-
570
- export default router;
1
+ import { Router, Request, Response, NextFunction } from 'express';
2
+ import { AuthService } from '@/services/auth/auth.service.js';
3
+ import { AuthConfigService } from '@/services/auth/auth-config.service.js';
4
+ import { OAuthConfigService } from '@/services/auth/oauth-config.service.js';
5
+ import { AuditService } from '@/services/logs/audit.service.js';
6
+ import { TokenManager } from '@/infra/security/token.manager.js';
7
+ import { AppError } from '@/api/middlewares/error.js';
8
+ import { ERROR_CODES } from '@/types/error-constants.js';
9
+ import { successResponse } from '@/utils/response.js';
10
+ import { AuthRequest, verifyAdmin, verifyToken } from '@/api/middlewares/auth.js';
11
+ import oauthRouter from './oauth.routes.js';
12
+ import { sendEmailOTPLimiter, verifyOTPLimiter } from '@/api/middlewares/rate-limiters.js';
13
+ import {
14
+ REFRESH_TOKEN_COOKIE_NAME,
15
+ setAuthCookie,
16
+ clearAuthCookie,
17
+ } from '@/utils/cookies.js';
18
+ import {
19
+ userIdSchema,
20
+ createUserRequestSchema,
21
+ createSessionRequestSchema,
22
+ createAdminSessionRequestSchema,
23
+ deleteUsersRequestSchema,
24
+ listUsersRequestSchema,
25
+ sendVerificationEmailRequestSchema,
26
+ verifyEmailRequestSchema,
27
+ sendResetPasswordEmailRequestSchema,
28
+ exchangeResetPasswordTokenRequestSchema,
29
+ resetPasswordRequestSchema,
30
+ type CreateUserResponse,
31
+ type CreateSessionResponse,
32
+ type VerifyEmailResponse,
33
+ type ExchangeResetPasswordTokenResponse,
34
+ type ResetPasswordResponse,
35
+ type CreateAdminSessionResponse,
36
+ type GetCurrentSessionResponse,
37
+ type ListUsersResponse,
38
+ type DeleteUsersResponse,
39
+ type GetPublicAuthConfigResponse,
40
+ exchangeAdminSessionRequestSchema,
41
+ type GetAuthConfigResponse,
42
+ updateAuthConfigRequestSchema,
43
+ } from '@insforge/shared-schemas';
44
+ import { SocketManager } from '@/infra/socket/socket.manager.js';
45
+ import { DataUpdateResourceType, ServerEvents } from '@/types/socket.js';
46
+ import logger from '@/utils/logger.js';
47
+
48
+ const router = Router();
49
+ const authService = AuthService.getInstance();
50
+ const authConfigService = AuthConfigService.getInstance();
51
+ const oAuthConfigService = OAuthConfigService.getInstance();
52
+ const auditService = AuditService.getInstance();
53
+
54
+ // Mount OAuth routes
55
+ router.use('/oauth', oauthRouter);
56
+
57
+ // Public Authentication Configuration Routes
58
+ // GET /api/auth/public-config - Get all public authentication configuration (public endpoint)
59
+ router.get('/public-config', async (req: Request, res: Response, next: NextFunction) => {
60
+ try {
61
+ const [oAuthProviders, authConfigs] = await Promise.all([
62
+ oAuthConfigService.getConfiguredProviders(),
63
+ authConfigService.getPublicAuthConfig(),
64
+ ]);
65
+
66
+ const response: GetPublicAuthConfigResponse = {
67
+ oAuthProviders,
68
+ ...authConfigs,
69
+ };
70
+
71
+ successResponse(res, response);
72
+ } catch (error) {
73
+ next(error);
74
+ }
75
+ });
76
+
77
+ // Email Authentication Configuration Routes
78
+ // GET /api/auth/config - Get authentication configurations (admin only)
79
+ router.get('/config', verifyAdmin, async (req: AuthRequest, res: Response, next: NextFunction) => {
80
+ try {
81
+ const config: GetAuthConfigResponse = await authConfigService.getAuthConfig();
82
+ successResponse(res, config);
83
+ } catch (error) {
84
+ next(error);
85
+ }
86
+ });
87
+
88
+ // PUT /api/auth/config - Update authentication configurations (admin only)
89
+ router.put('/config', verifyAdmin, async (req: AuthRequest, res: Response, next: NextFunction) => {
90
+ try {
91
+ const validationResult = updateAuthConfigRequestSchema.safeParse(req.body);
92
+ if (!validationResult.success) {
93
+ throw new AppError(
94
+ validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
95
+ 400,
96
+ ERROR_CODES.INVALID_INPUT
97
+ );
98
+ }
99
+
100
+ const input = validationResult.data;
101
+ const config: GetAuthConfigResponse = await authConfigService.updateAuthConfig(input);
102
+
103
+ await auditService.log({
104
+ actor: req.user?.email || 'api-key',
105
+ action: 'UPDATE_AUTH_CONFIG',
106
+ module: 'AUTH',
107
+ details: {
108
+ updatedFields: Object.keys(input),
109
+ },
110
+ ip_address: req.ip,
111
+ });
112
+
113
+ successResponse(res, config);
114
+ } catch (error) {
115
+ next(error);
116
+ }
117
+ });
118
+
119
+ // POST /api/auth/users - Create a new user (registration)
120
+ router.post('/users', async (req: Request, res: Response, next: NextFunction) => {
121
+ try {
122
+ const validationResult = createUserRequestSchema.safeParse(req.body);
123
+ if (!validationResult.success) {
124
+ throw new AppError(
125
+ validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
126
+ 400,
127
+ ERROR_CODES.INVALID_INPUT
128
+ );
129
+ }
130
+
131
+ const { email, password, name } = validationResult.data;
132
+ const result: CreateUserResponse = await authService.register(email, password, name);
133
+
134
+ // Set refresh token in httpOnly cookie and generate CSRF token
135
+ let csrfToken: string | null = null;
136
+ if (result.accessToken && result.user) {
137
+ const tokenManager = TokenManager.getInstance();
138
+ const refreshToken = tokenManager.generateRefreshToken(result.user.id);
139
+ setAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME, refreshToken);
140
+ csrfToken = tokenManager.generateCsrfToken(refreshToken);
141
+ }
142
+
143
+ const socket = SocketManager.getInstance();
144
+ socket.broadcastToRoom(
145
+ 'role:project_admin',
146
+ ServerEvents.DATA_UPDATE,
147
+ { resource: DataUpdateResourceType.USERS },
148
+ 'system'
149
+ );
150
+
151
+ successResponse(res, { ...result, csrfToken });
152
+ } catch (error) {
153
+ next(error);
154
+ }
155
+ });
156
+
157
+ // POST /api/auth/sessions - Create a new session (login)
158
+ router.post('/sessions', async (req: Request, res: Response, next: NextFunction) => {
159
+ try {
160
+ const validationResult = createSessionRequestSchema.safeParse(req.body);
161
+ if (!validationResult.success) {
162
+ throw new AppError(
163
+ validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
164
+ 400,
165
+ ERROR_CODES.INVALID_INPUT
166
+ );
167
+ }
168
+
169
+ const { email, password } = validationResult.data;
170
+ const result: CreateSessionResponse = await authService.login(email, password);
171
+
172
+ // Set refresh token in httpOnly cookie and generate CSRF token
173
+ const tokenManager = TokenManager.getInstance();
174
+ const refreshToken = tokenManager.generateRefreshToken(result.user.id);
175
+ setAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME, refreshToken);
176
+ const csrfToken = tokenManager.generateCsrfToken(refreshToken);
177
+
178
+ successResponse(res, { ...result, csrfToken });
179
+ } catch (error) {
180
+ next(error);
181
+ }
182
+ });
183
+
184
+ // POST /api/auth/refresh - Refresh access token using httpOnly cookie
185
+ // Requires X-CSRF-Token header for CSRF protection
186
+ router.post('/refresh', async (req: Request, res: Response, next: NextFunction) => {
187
+ try {
188
+ const refreshToken = req.cookies?.[REFRESH_TOKEN_COOKIE_NAME];
189
+
190
+ if (!refreshToken) {
191
+ throw new AppError('No refresh token provided', 401, ERROR_CODES.AUTH_UNAUTHORIZED);
192
+ }
193
+
194
+ const tokenManager = TokenManager.getInstance();
195
+
196
+ // Verify CSRF token by re-computing from refresh token
197
+ const csrfHeader = req.headers['x-csrf-token'] as string | undefined;
198
+ if (!tokenManager.verifyCsrfToken(csrfHeader, refreshToken)) {
199
+ logger.warn('[Auth:Refresh] CSRF token validation failed');
200
+ throw new AppError('Invalid CSRF token', 403, ERROR_CODES.AUTH_UNAUTHORIZED);
201
+ }
202
+ const payload = tokenManager.verifyRefreshToken(refreshToken);
203
+
204
+ // Fetch CURRENT user data from DB (email/role may have changed)
205
+ const user = await authService.getUserSchemaById(payload.sub);
206
+
207
+ if (!user) {
208
+ logger.warn('[Auth:Refresh] User not found for valid refresh token', { userId: payload.sub });
209
+ clearAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME);
210
+ throw new AppError('User not found', 401, ERROR_CODES.AUTH_UNAUTHORIZED);
211
+ }
212
+
213
+ // Generate new access token
214
+ const newAccessToken = tokenManager.generateToken({
215
+ sub: user.id,
216
+ email: user.email,
217
+ role: 'authenticated',
218
+ });
219
+
220
+ // Generate new refresh token (token rotation for security)
221
+ const newRefreshToken = tokenManager.generateRefreshToken(user.id);
222
+
223
+ setAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME, newRefreshToken);
224
+ const newCsrfToken = tokenManager.generateCsrfToken(newRefreshToken);
225
+
226
+ successResponse(res, {
227
+ accessToken: newAccessToken,
228
+ user: user,
229
+ csrfToken: newCsrfToken,
230
+ });
231
+ } catch (error) {
232
+ // Clear invalid cookie on error
233
+ clearAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME);
234
+ next(error);
235
+ }
236
+ });
237
+
238
+ // POST /api/auth/logout - Logout and clear refresh token cookie
239
+ router.post('/logout', (_req: Request, res: Response, next: NextFunction) => {
240
+ try {
241
+ clearAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME);
242
+
243
+ successResponse(res, {
244
+ success: true,
245
+ message: 'Logged out successfully',
246
+ });
247
+ } catch (error) {
248
+ next(error);
249
+ }
250
+ });
251
+
252
+ // POST /api/auth/admin/sessions/exchange - Create admin session
253
+ router.post('/admin/sessions/exchange', async (req: Request, res: Response, next: NextFunction) => {
254
+ try {
255
+ const validationResult = exchangeAdminSessionRequestSchema.safeParse(req.body);
256
+ if (!validationResult.success) {
257
+ throw new AppError(
258
+ validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
259
+ 400,
260
+ ERROR_CODES.INVALID_INPUT
261
+ );
262
+ }
263
+
264
+ const { code } = validationResult.data;
265
+ const result: CreateAdminSessionResponse =
266
+ await authService.adminLoginWithAuthorizationCode(code);
267
+
268
+ successResponse(res, result);
269
+ } catch (error) {
270
+ if (error instanceof AppError) {
271
+ next(error);
272
+ } else {
273
+ // Convert other errors (like JWT verification errors) to 400
274
+ next(
275
+ new AppError(
276
+ 'Failed to exchange admin session' + (error instanceof Error ? `: ${error.message}` : ''),
277
+ 400,
278
+ ERROR_CODES.INVALID_INPUT
279
+ )
280
+ );
281
+ }
282
+ }
283
+ });
284
+
285
+ // POST /api/auth/admin/sessions - Create admin session
286
+ router.post('/admin/sessions', (req: Request, res: Response, next: NextFunction) => {
287
+ try {
288
+ const validationResult = createAdminSessionRequestSchema.safeParse(req.body);
289
+ if (!validationResult.success) {
290
+ throw new AppError(
291
+ validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
292
+ 400,
293
+ ERROR_CODES.INVALID_INPUT
294
+ );
295
+ }
296
+
297
+ const { email, password } = validationResult.data;
298
+ const result: CreateAdminSessionResponse = authService.adminLogin(email, password);
299
+
300
+ successResponse(res, result);
301
+ } catch (error) {
302
+ next(error);
303
+ }
304
+ });
305
+
306
+ // GET /api/auth/sessions/current - Get current session user
307
+ router.get(
308
+ '/sessions/current',
309
+ verifyToken,
310
+ (req: AuthRequest, res: Response, next: NextFunction) => {
311
+ try {
312
+ if (!req.user) {
313
+ throw new AppError('User not authenticated', 401, ERROR_CODES.AUTH_INVALID_CREDENTIALS);
314
+ }
315
+
316
+ const response: GetCurrentSessionResponse = {
317
+ user: {
318
+ id: req.user.id,
319
+ email: req.user.email,
320
+ role: req.user.role as 'authenticated' | 'project_admin',
321
+ },
322
+ };
323
+
324
+ successResponse(res, response);
325
+ } catch (error) {
326
+ next(error);
327
+ }
328
+ }
329
+ );
330
+
331
+ // GET /api/auth/users - List all users (admin only)
332
+ router.get('/users', verifyAdmin, async (req: Request, res: Response, next: NextFunction) => {
333
+ try {
334
+ const queryValidation = listUsersRequestSchema.safeParse(req.query);
335
+ const queryParams = queryValidation.success ? queryValidation.data : req.query;
336
+ const { limit = '10', offset = '0', search } = queryParams || {};
337
+
338
+ const parsedLimit = parseInt(limit as string);
339
+ const parsedOffset = parseInt(offset as string);
340
+
341
+ const { users, total } = await authService.listUsers(
342
+ parsedLimit,
343
+ parsedOffset,
344
+ search as string | undefined
345
+ );
346
+
347
+ const response: ListUsersResponse = {
348
+ data: users,
349
+ pagination: {
350
+ offset: parsedOffset,
351
+ limit: parsedLimit,
352
+ total: total,
353
+ },
354
+ };
355
+
356
+ successResponse(res, response);
357
+ } catch (error) {
358
+ next(error);
359
+ }
360
+ });
361
+
362
+ // GET /api/auth/users/:id - Get specific user (admin only)
363
+ router.get(
364
+ '/users/:userId',
365
+ verifyAdmin,
366
+ async (req: Request, res: Response, next: NextFunction) => {
367
+ try {
368
+ // Validate userId path parameter directly
369
+ const userIdValidation = userIdSchema.safeParse(req.params.userId);
370
+ if (!userIdValidation.success) {
371
+ throw new AppError('Invalid user ID format', 400, ERROR_CODES.INVALID_INPUT);
372
+ }
373
+
374
+ const userId = userIdValidation.data;
375
+ const user = await authService.getUserSchemaById(userId);
376
+
377
+ if (!user) {
378
+ throw new AppError('User not found', 404, ERROR_CODES.NOT_FOUND);
379
+ }
380
+
381
+ successResponse(res, user);
382
+ } catch (error) {
383
+ next(error);
384
+ }
385
+ }
386
+ );
387
+
388
+ // DELETE /api/auth/users - Delete users (batch operation, admin only)
389
+ router.delete(
390
+ '/users',
391
+ verifyAdmin,
392
+ async (req: AuthRequest, res: Response, next: NextFunction) => {
393
+ try {
394
+ const validationResult = deleteUsersRequestSchema.safeParse(req.body);
395
+ if (!validationResult.success) {
396
+ throw new AppError(
397
+ validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
398
+ 400,
399
+ ERROR_CODES.INVALID_INPUT
400
+ );
401
+ }
402
+
403
+ const { userIds } = validationResult.data;
404
+
405
+ const deletedCount = await authService.deleteUsers(userIds);
406
+
407
+ // Log audit for user deletion
408
+ await auditService.log({
409
+ actor: req.user?.email || 'api-key',
410
+ action: 'DELETE_USERS',
411
+ module: 'AUTH',
412
+ details: {
413
+ userIds,
414
+ deletedCount,
415
+ },
416
+ ip_address: req.ip,
417
+ });
418
+
419
+ const response: DeleteUsersResponse = {
420
+ message: 'Users deleted successfully',
421
+ deletedCount,
422
+ };
423
+
424
+ successResponse(res, response);
425
+ } catch (error) {
426
+ next(error);
427
+ }
428
+ }
429
+ );
430
+
431
+ // POST /api/auth/tokens/anon - Generate anonymous JWT token (never expires)
432
+ router.post('/tokens/anon', verifyAdmin, (_req: Request, res: Response, next: NextFunction) => {
433
+ try {
434
+ const tokenManager = TokenManager.getInstance();
435
+ const token = tokenManager.generateAnonToken();
436
+
437
+ successResponse(res, {
438
+ accessToken: token,
439
+ message: 'Anonymous token generated successfully (never expires)',
440
+ });
441
+ } catch (error) {
442
+ next(error);
443
+ }
444
+ });
445
+
446
+ // POST /api/auth/email/send-verification - Send email verification (code or link based on config)
447
+ router.post(
448
+ '/email/send-verification',
449
+ sendEmailOTPLimiter,
450
+ async (req: Request, res: Response, next: NextFunction) => {
451
+ try {
452
+ const validationResult = sendVerificationEmailRequestSchema.safeParse(req.body);
453
+ if (!validationResult.success) {
454
+ throw new AppError(
455
+ validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
456
+ 400,
457
+ ERROR_CODES.INVALID_INPUT
458
+ );
459
+ }
460
+
461
+ const { email } = validationResult.data;
462
+
463
+ // Get auth config to determine verification method
464
+ const authConfig = await authConfigService.getAuthConfig();
465
+ const method = authConfig.verifyEmailMethod;
466
+
467
+ // Note: User enumeration is prevented at service layer
468
+ // Service returns gracefully (no error) if user not found
469
+ if (method === 'link') {
470
+ await authService.sendVerificationEmailWithLink(email);
471
+ } else {
472
+ await authService.sendVerificationEmailWithCode(email);
473
+ }
474
+
475
+ // Always return 202 Accepted with generic message
476
+ const message =
477
+ method === 'link'
478
+ ? 'If your email is registered, we have sent you a verification link. Please check your inbox.'
479
+ : 'If your email is registered, we have sent you a verification code. Please check your inbox.';
480
+
481
+ successResponse(
482
+ res,
483
+ {
484
+ success: true,
485
+ message,
486
+ },
487
+ 202
488
+ );
489
+ } catch (error) {
490
+ next(error);
491
+ }
492
+ }
493
+ );
494
+
495
+ // POST /api/auth/email/verify - Verify email with OTP
496
+ // Uses verifyEmailMethod from auth config to determine verification type:
497
+ // - 'code': expects email + 6-digit numeric code
498
+ // - 'link': expects 64-char hex token only
499
+ router.post(
500
+ '/email/verify',
501
+ verifyOTPLimiter,
502
+ async (req: Request, res: Response, next: NextFunction) => {
503
+ try {
504
+ const validationResult = verifyEmailRequestSchema.safeParse(req.body);
505
+ if (!validationResult.success) {
506
+ throw new AppError(
507
+ validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
508
+ 400,
509
+ ERROR_CODES.INVALID_INPUT
510
+ );
511
+ }
512
+
513
+ const { email, otp } = validationResult.data;
514
+
515
+ // Get auth config to determine verification method
516
+ const authConfig = await authConfigService.getAuthConfig();
517
+ const method = authConfig.verifyEmailMethod;
518
+
519
+ let result: VerifyEmailResponse;
520
+
521
+ if (method === 'link') {
522
+ // Link verification: otp is 64-char hex token
523
+ result = await authService.verifyEmailWithToken(otp);
524
+ } else {
525
+ // Code verification: requires email + 6-digit code
526
+ if (!email) {
527
+ throw new AppError(
528
+ 'Email is required for code verification',
529
+ 400,
530
+ ERROR_CODES.INVALID_INPUT
531
+ );
532
+ }
533
+ result = await authService.verifyEmailWithCode(email, otp);
534
+ }
535
+
536
+ // Set refresh token in httpOnly cookie and generate CSRF token
537
+ const tokenManager = TokenManager.getInstance();
538
+ const refreshToken = tokenManager.generateRefreshToken(result.user.id);
539
+ setAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME, refreshToken);
540
+ const csrfToken = tokenManager.generateCsrfToken(refreshToken);
541
+ successResponse(res, { ...result, csrfToken });
542
+ } catch (error) {
543
+ next(error);
544
+ }
545
+ }
546
+ );
547
+
548
+ // POST /api/auth/email/send-reset-password - Send password reset (code or link based on config)
549
+ router.post(
550
+ '/email/send-reset-password',
551
+ sendEmailOTPLimiter,
552
+ async (req: Request, res: Response, next: NextFunction) => {
553
+ try {
554
+ const validationResult = sendResetPasswordEmailRequestSchema.safeParse(req.body);
555
+ if (!validationResult.success) {
556
+ throw new AppError(
557
+ validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
558
+ 400,
559
+ ERROR_CODES.INVALID_INPUT
560
+ );
561
+ }
562
+
563
+ const { email } = validationResult.data;
564
+
565
+ // Get auth config to determine reset password method
566
+ const authConfig = await authConfigService.getAuthConfig();
567
+ const method = authConfig.resetPasswordMethod;
568
+
569
+ // Note: User enumeration is prevented at service layer
570
+ // Service returns gracefully (no error) if user not found
571
+ if (method === 'link') {
572
+ await authService.sendResetPasswordEmailWithLink(email);
573
+ } else {
574
+ await authService.sendResetPasswordEmailWithCode(email);
575
+ }
576
+
577
+ // Always return 202 Accepted with generic message
578
+ const message =
579
+ method === 'link'
580
+ ? 'If your email is registered, we have sent you a password reset link. Please check your inbox.'
581
+ : 'If your email is registered, we have sent you a password reset code. Please check your inbox.';
582
+
583
+ successResponse(
584
+ res,
585
+ {
586
+ success: true,
587
+ message,
588
+ },
589
+ 202
590
+ );
591
+ } catch (error) {
592
+ next(error);
593
+ }
594
+ }
595
+ );
596
+
597
+ // POST /api/auth/email/exchange-reset-password-token - Exchange reset password code for reset token
598
+ // Step 1 of two-step password reset flow: verify code → get reset token
599
+ // Only used when resetPasswordMethod is 'code'
600
+ router.post(
601
+ '/email/exchange-reset-password-token',
602
+ verifyOTPLimiter,
603
+ async (req: Request, res: Response, next: NextFunction) => {
604
+ try {
605
+ const validationResult = exchangeResetPasswordTokenRequestSchema.safeParse(req.body);
606
+ if (!validationResult.success) {
607
+ throw new AppError(
608
+ validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
609
+ 400,
610
+ ERROR_CODES.INVALID_INPUT
611
+ );
612
+ }
613
+
614
+ const { email, code } = validationResult.data;
615
+
616
+ const result = await authService.exchangeResetPasswordToken(email, code);
617
+
618
+ const response: ExchangeResetPasswordTokenResponse = {
619
+ token: result.token,
620
+ expiresAt: result.expiresAt.toISOString(),
621
+ };
622
+
623
+ successResponse(res, response);
624
+ } catch (error) {
625
+ next(error);
626
+ }
627
+ }
628
+ );
629
+
630
+ // POST /api/auth/email/reset-password - Reset password with token
631
+ // Token can be:
632
+ // - Magic link token (from send-reset-password endpoint when method is 'link')
633
+ // - Reset token (from exchange-reset-password-token endpoint after code verification)
634
+ // Both use RESET_PASSWORD purpose and are verified the same way
635
+ // Flow:
636
+ // Code: send-reset-password → exchange-reset-password-token → reset-password (with resetToken)
637
+ // Link: send-reset-password → reset-password (with link token)
638
+ router.post(
639
+ '/email/reset-password',
640
+ verifyOTPLimiter,
641
+ async (req: Request, res: Response, next: NextFunction) => {
642
+ try {
643
+ const validationResult = resetPasswordRequestSchema.safeParse(req.body);
644
+ if (!validationResult.success) {
645
+ throw new AppError(
646
+ validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
647
+ 400,
648
+ ERROR_CODES.INVALID_INPUT
649
+ );
650
+ }
651
+
652
+ const { newPassword, otp } = validationResult.data;
653
+
654
+ // Both magic link tokens and code-verified reset tokens use RESET_PASSWORD purpose
655
+ const result: ResetPasswordResponse = await authService.resetPasswordWithToken(
656
+ newPassword,
657
+ otp
658
+ );
659
+
660
+ successResponse(res, result); // Return message with optional redirectTo
661
+ } catch (error) {
662
+ next(error);
663
+ }
664
+ }
665
+ );
666
+
667
+ export default router;