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,448 +1,473 @@
1
- import { Router, Request, Response, NextFunction } from 'express';
2
- import { AuthService } from '@/services/auth/auth.service.js';
3
- import { OAuthConfigService } from '@/services/auth/oauth-config.service.js';
4
- import { AuditService } from '@/services/logs/audit.service.js';
5
- import { AppError } from '@/api/middlewares/error.js';
6
- import { ERROR_CODES } from '@/types/error-constants.js';
7
- import { successResponse } from '@/utils/response.js';
8
- import { AuthRequest, verifyAdmin } from '@/api/middlewares/auth.js';
9
- import logger from '@/utils/logger.js';
10
- import jwt from 'jsonwebtoken';
11
- import {
12
- createOAuthConfigRequestSchema,
13
- updateOAuthConfigRequestSchema,
14
- type ListOAuthConfigsResponse,
15
- oAuthProvidersSchema,
16
- } from '@insforge/shared-schemas';
17
- import { isOAuthSharedKeysAvailable } from '@/utils/environment.js';
18
-
19
- const router = Router();
20
- const authService = AuthService.getInstance();
21
- const oAuthConfigService = OAuthConfigService.getInstance();
22
- const auditService = AuditService.getInstance();
23
-
24
- // Helper function to validate JWT_SECRET
25
- const validateJwtSecret = (): string => {
26
- const jwtSecret = process.env.JWT_SECRET;
27
- if (!jwtSecret || jwtSecret.trim() === '') {
28
- throw new AppError(
29
- 'JWT_SECRET environment variable is not configured.',
30
- 500,
31
- ERROR_CODES.INTERNAL_ERROR
32
- );
33
- }
34
- return jwtSecret;
35
- };
36
-
37
- // OAuth Configuration Management Routes (must come before wildcard routes)
38
- // GET /api/auth/oauth/configs - List all OAuth configurations (admin only)
39
- router.get('/configs', verifyAdmin, async (req: AuthRequest, res: Response, next: NextFunction) => {
40
- try {
41
- const configs = await oAuthConfigService.getAllConfigs();
42
- const response: ListOAuthConfigsResponse = {
43
- data: configs,
44
- count: configs.length,
45
- };
46
- successResponse(res, response);
47
- } catch (error) {
48
- logger.error('Failed to list OAuth configurations', { error });
49
- next(error);
50
- }
51
- });
52
-
53
- // GET /api/auth/oauth/:provider/config - Get specific OAuth configuration (admin only)
54
- router.get(
55
- '/:provider/config',
56
- verifyAdmin,
57
- async (req: AuthRequest, res: Response, next: NextFunction) => {
58
- try {
59
- const { provider } = req.params;
60
- const config = await oAuthConfigService.getConfigByProvider(provider);
61
- const clientSecret = await oAuthConfigService.getClientSecretByProvider(provider);
62
-
63
- if (!config) {
64
- throw new AppError(
65
- `OAuth configuration for ${provider} not found`,
66
- 404,
67
- ERROR_CODES.NOT_FOUND
68
- );
69
- }
70
-
71
- successResponse(res, {
72
- ...config,
73
- clientSecret: clientSecret || undefined,
74
- });
75
- } catch (error) {
76
- logger.error('Failed to get OAuth config by provider', {
77
- provider: req.params.provider,
78
- error,
79
- });
80
- next(error);
81
- }
82
- }
83
- );
84
-
85
- // POST /api/auth/oauth/configs - Create new OAuth configuration (admin only)
86
- router.post(
87
- '/configs',
88
- verifyAdmin,
89
- async (req: AuthRequest, res: Response, next: NextFunction) => {
90
- try {
91
- const validationResult = createOAuthConfigRequestSchema.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
-
102
- // Check if using shared keys when not allowed
103
- if (input.useSharedKey && !isOAuthSharedKeysAvailable()) {
104
- throw new AppError(
105
- 'Shared OAuth keys are not enabled in this environment',
106
- 400,
107
- ERROR_CODES.AUTH_OAUTH_CONFIG_ERROR
108
- );
109
- }
110
-
111
- const config = await oAuthConfigService.createConfig(input);
112
-
113
- await auditService.log({
114
- actor: req.user?.email || 'api-key',
115
- action: 'CREATE_OAUTH_CONFIG',
116
- module: 'AUTH',
117
- details: {
118
- provider: input.provider,
119
- useSharedKey: input.useSharedKey || false,
120
- },
121
- ip_address: req.ip,
122
- });
123
-
124
- successResponse(res, config);
125
- } catch (error) {
126
- logger.error('Failed to create OAuth configuration', { error });
127
- next(error);
128
- }
129
- }
130
- );
131
-
132
- // PUT /api/auth/oauth/:provider/config - Update OAuth configuration (admin only)
133
- router.put(
134
- '/:provider/config',
135
- verifyAdmin,
136
- async (req: AuthRequest, res: Response, next: NextFunction) => {
137
- try {
138
- const provider = req.params.provider;
139
- if (!provider || provider.length === 0 || provider.length > 50) {
140
- throw new AppError('Invalid provider name', 400, ERROR_CODES.INVALID_INPUT);
141
- }
142
-
143
- const validationResult = updateOAuthConfigRequestSchema.safeParse(req.body);
144
- if (!validationResult.success) {
145
- throw new AppError(
146
- validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
147
- 400,
148
- ERROR_CODES.INVALID_INPUT
149
- );
150
- }
151
-
152
- const input = validationResult.data;
153
-
154
- // Check if using shared keys when not allowed
155
- if (input.useSharedKey && !isOAuthSharedKeysAvailable()) {
156
- throw new AppError(
157
- 'Shared OAuth keys are not enabled in this environment',
158
- 400,
159
- ERROR_CODES.AUTH_OAUTH_CONFIG_ERROR
160
- );
161
- }
162
-
163
- const config = await oAuthConfigService.updateConfig(provider, input);
164
-
165
- await auditService.log({
166
- actor: req.user?.email || 'api-key',
167
- action: 'UPDATE_OAUTH_CONFIG',
168
- module: 'AUTH',
169
- details: {
170
- provider,
171
- updatedFields: Object.keys(input),
172
- },
173
- ip_address: req.ip,
174
- });
175
-
176
- successResponse(res, config);
177
- } catch (error) {
178
- logger.error('Failed to update OAuth configuration', {
179
- error,
180
- provider: req.params.provider,
181
- });
182
- next(error);
183
- }
184
- }
185
- );
186
-
187
- // DELETE /api/auth/oauth/:provider/config - Delete OAuth configuration (admin only)
188
- router.delete(
189
- '/:provider/config',
190
- verifyAdmin,
191
- async (req: AuthRequest, res: Response, next: NextFunction) => {
192
- try {
193
- const provider = req.params.provider;
194
- if (!provider || provider.length === 0 || provider.length > 50) {
195
- throw new AppError('Invalid provider name', 400, ERROR_CODES.INVALID_INPUT);
196
- }
197
- const deleted = await oAuthConfigService.deleteConfig(provider);
198
-
199
- if (!deleted) {
200
- throw new AppError(
201
- `OAuth configuration for ${provider} not found`,
202
- 404,
203
- ERROR_CODES.NOT_FOUND
204
- );
205
- }
206
-
207
- await auditService.log({
208
- actor: req.user?.email || 'api-key',
209
- action: 'DELETE_OAUTH_CONFIG',
210
- module: 'AUTH',
211
- details: { provider },
212
- ip_address: req.ip,
213
- });
214
-
215
- successResponse(res, {
216
- success: true,
217
- message: `OAuth configuration for ${provider} deleted successfully`,
218
- });
219
- } catch (error) {
220
- logger.error('Failed to delete OAuth configuration', {
221
- error,
222
- provider: req.params.provider,
223
- });
224
- next(error);
225
- }
226
- }
227
- );
228
-
229
- // OAuth Flow Routes
230
- // GET /api/auth/oauth/:provider - Initialize OAuth flow for any supported provider
231
- router.get('/:provider', async (req: Request, res: Response, next: NextFunction) => {
232
- try {
233
- const { provider } = req.params;
234
- const { redirect_uri } = req.query;
235
-
236
- // Validate provider using OAuthProvidersSchema
237
- const providerValidation = oAuthProvidersSchema.safeParse(provider);
238
- if (!providerValidation.success) {
239
- throw new AppError(
240
- `Unsupported OAuth provider: ${provider}. Supported providers: ${oAuthProvidersSchema.options.join(', ')}`,
241
- 400,
242
- ERROR_CODES.INVALID_INPUT
243
- );
244
- }
245
-
246
- const validatedProvider = providerValidation.data;
247
-
248
- if (!redirect_uri) {
249
- throw new AppError('Redirect URI is required', 400, ERROR_CODES.INVALID_INPUT);
250
- }
251
-
252
- const jwtPayload = {
253
- provider: validatedProvider,
254
- redirectUri: redirect_uri ? (redirect_uri as string) : undefined,
255
- createdAt: Date.now(),
256
- };
257
- const jwtSecret = validateJwtSecret();
258
- const state = jwt.sign(jwtPayload, jwtSecret, {
259
- algorithm: 'HS256',
260
- expiresIn: '1h', // Set expiration time for the state token
261
- });
262
-
263
- const authUrl = await authService.generateOAuthUrl(validatedProvider, state);
264
-
265
- successResponse(res, { authUrl });
266
- } catch (error) {
267
- logger.error(`${req.params.provider} OAuth error`, { error });
268
-
269
- // If it's already an AppError, pass it through
270
- if (error instanceof AppError) {
271
- next(error);
272
- return;
273
- }
274
-
275
- // For other errors, return the generic OAuth configuration error
276
- next(
277
- new AppError(
278
- `${req.params.provider} OAuth is not properly configured. Please check your oauth configurations.`,
279
- 500,
280
- ERROR_CODES.AUTH_OAUTH_CONFIG_ERROR
281
- )
282
- );
283
- }
284
- });
285
-
286
- // GET /api/auth/oauth/shared/callback/:state - Shared callback for OAuth providers
287
- router.get('/shared/callback/:state', async (req: Request, res: Response, next: NextFunction) => {
288
- try {
289
- const { state } = req.params;
290
- const { success, error, payload } = req.query;
291
-
292
- if (!state) {
293
- logger.warn('Shared OAuth callback called without state parameter');
294
- throw new AppError('State parameter is required', 400, ERROR_CODES.INVALID_INPUT);
295
- }
296
-
297
- let redirectUri: string;
298
- let provider: string;
299
- try {
300
- const jwtSecret = validateJwtSecret();
301
- const decodedState = jwt.verify(state, jwtSecret) as {
302
- provider: string;
303
- redirectUri: string;
304
- };
305
- redirectUri = decodedState.redirectUri || '';
306
- provider = decodedState.provider || '';
307
- } catch {
308
- logger.warn('Invalid state parameter', { state });
309
- throw new AppError('Invalid state parameter', 400, ERROR_CODES.INVALID_INPUT);
310
- }
311
-
312
- // Validate provider using OAuthProvidersSchema
313
- const providerValidation = oAuthProvidersSchema.safeParse(provider);
314
- if (!providerValidation.success) {
315
- logger.warn('Invalid provider in state', { provider });
316
- throw new AppError(
317
- `Invalid provider in state: ${provider}. Supported providers: ${oAuthProvidersSchema.options.join(', ')}`,
318
- 400,
319
- ERROR_CODES.INVALID_INPUT
320
- );
321
- }
322
- const validatedProvider = providerValidation.data;
323
- if (!redirectUri) {
324
- throw new AppError('redirectUri is required', 400, ERROR_CODES.INVALID_INPUT);
325
- }
326
-
327
- if (success !== 'true') {
328
- const errorMessage = error || 'OAuth Authentication Failed';
329
- logger.warn('Shared OAuth callback failed', { error: errorMessage, provider });
330
- return res.redirect(`${redirectUri}/?error=${encodeURIComponent(String(errorMessage))}`);
331
- }
332
-
333
- if (!payload) {
334
- throw new AppError('No payload provided in callback', 400, ERROR_CODES.INVALID_INPUT);
335
- }
336
-
337
- const payloadData = JSON.parse(
338
- Buffer.from(payload as string, 'base64').toString('utf8')
339
- ) as Record<string, unknown>;
340
-
341
- // Handle shared callback - transforms payload and creates/finds user
342
- const result = await authService.handleSharedCallback(validatedProvider, payloadData);
343
-
344
- const params = new URLSearchParams();
345
- params.set('access_token', result?.accessToken ?? '');
346
- params.set('user_id', result?.user?.id ?? '');
347
- params.set('email', result?.user?.email ?? '');
348
- params.set('name', result?.user?.name ?? '');
349
-
350
- res.redirect(`${redirectUri}?${params.toString()}`);
351
- } catch (error) {
352
- logger.error('Shared OAuth callback error', { error });
353
- next(error);
354
- }
355
- });
356
-
357
- // GET /api/auth/oauth/:provider/callback - OAuth provider callback
358
- router.get('/:provider/callback', async (req: Request, res: Response, next: NextFunction) => {
359
- try {
360
- const { provider } = req.params;
361
- const { code, state, token } = req.query;
362
-
363
- if (!state) {
364
- logger.warn('OAuth callback called without state parameter');
365
- throw new AppError('State parameter is required', 400, ERROR_CODES.INVALID_INPUT);
366
- }
367
-
368
- // Decode redirectUri from state (needed for both success and error paths)
369
- let redirectUri: string;
370
-
371
- try {
372
- const jwtSecret = validateJwtSecret();
373
- const stateData = jwt.verify(state as string, jwtSecret) as {
374
- provider: string;
375
- redirectUri: string;
376
- };
377
- redirectUri = stateData.redirectUri || '';
378
- } catch {
379
- // Invalid state
380
- logger.warn('Invalid state in provider callback', { state });
381
- throw new AppError('Invalid state parameter', 400, ERROR_CODES.INVALID_INPUT);
382
- }
383
-
384
- if (!redirectUri) {
385
- throw new AppError('redirectUri is required', 400, ERROR_CODES.INVALID_INPUT);
386
- }
387
-
388
- try {
389
- // Validate provider using OAuthProvidersSchema
390
- const providerValidation = oAuthProvidersSchema.safeParse(provider);
391
- if (!providerValidation.success) {
392
- throw new AppError(
393
- `Unsupported OAuth provider: ${provider}. Supported providers: ${oAuthProvidersSchema.options.join(', ')}`,
394
- 400,
395
- ERROR_CODES.INVALID_INPUT
396
- );
397
- }
398
-
399
- const validatedProvider = providerValidation.data;
400
-
401
- const result = await authService.handleOAuthCallback(validatedProvider, {
402
- code: code as string | undefined,
403
- token: token as string | undefined,
404
- state: state as string | undefined,
405
- });
406
-
407
- // Construct redirect URL with query parameters
408
- const params = new URLSearchParams();
409
- params.set('access_token', result?.accessToken ?? '');
410
- params.set('user_id', result?.user?.id ?? '');
411
- params.set('email', result?.user?.email ?? '');
412
- params.set('name', result?.user?.name ?? '');
413
-
414
- const finalRedirectUri = `${redirectUri}?${params.toString()}`;
415
-
416
- logger.info('OAuth callback successful, redirecting with token', {
417
- redirectUri: finalRedirectUri,
418
- hasAccessToken: !!result?.accessToken,
419
- hasUserId: !!result?.user?.id,
420
- provider: validatedProvider,
421
- });
422
-
423
- return res.redirect(finalRedirectUri);
424
- } catch (error) {
425
- logger.error('OAuth callback error', {
426
- error: error instanceof Error ? error.message : error,
427
- stack: error instanceof Error ? error.stack : undefined,
428
- provider: req.params.provider,
429
- hasCode: !!req.query.code,
430
- hasState: !!req.query.state,
431
- hasToken: !!req.query.token,
432
- });
433
-
434
- const errorMessage = error instanceof Error ? error.message : 'OAuth Authentication Failed';
435
-
436
- // Redirect with error in URL parameters
437
- const params = new URLSearchParams();
438
- params.set('error', errorMessage);
439
-
440
- return res.redirect(`${redirectUri}?${params.toString()}`);
441
- }
442
- } catch (error) {
443
- logger.error('OAuth callback error', { error });
444
- next(error);
445
- }
446
- });
447
-
448
- export default router;
1
+ import { Router, Request, Response, NextFunction } from 'express';
2
+ import { AuthService } from '@/services/auth/auth.service.js';
3
+ import { OAuthConfigService } from '@/services/auth/oauth-config.service.js';
4
+ import { AuditService } from '@/services/logs/audit.service.js';
5
+ import { TokenManager } from '@/infra/security/token.manager.js';
6
+ import { AppError } from '@/api/middlewares/error.js';
7
+ import { ERROR_CODES } from '@/types/error-constants.js';
8
+ import { successResponse } from '@/utils/response.js';
9
+ import { AuthRequest, verifyAdmin } from '@/api/middlewares/auth.js';
10
+ import { setAuthCookie, REFRESH_TOKEN_COOKIE_NAME } from '@/utils/cookies.js';
11
+ import logger from '@/utils/logger.js';
12
+ import jwt from 'jsonwebtoken';
13
+ import {
14
+ createOAuthConfigRequestSchema,
15
+ updateOAuthConfigRequestSchema,
16
+ type ListOAuthConfigsResponse,
17
+ oAuthProvidersSchema,
18
+ } from '@insforge/shared-schemas';
19
+ import { isOAuthSharedKeysAvailable } from '@/utils/environment.js';
20
+
21
+ const router = Router();
22
+ const authService = AuthService.getInstance();
23
+ const oAuthConfigService = OAuthConfigService.getInstance();
24
+ const auditService = AuditService.getInstance();
25
+
26
+ // Helper function to validate JWT_SECRET
27
+ const validateJwtSecret = (): string => {
28
+ const jwtSecret = process.env.JWT_SECRET;
29
+ if (!jwtSecret || jwtSecret.trim() === '') {
30
+ throw new AppError(
31
+ 'JWT_SECRET environment variable is not configured.',
32
+ 500,
33
+ ERROR_CODES.INTERNAL_ERROR
34
+ );
35
+ }
36
+ return jwtSecret;
37
+ };
38
+
39
+ // OAuth Configuration Management Routes (must come before wildcard routes)
40
+ // GET /api/auth/oauth/configs - List all OAuth configurations (admin only)
41
+ router.get('/configs', verifyAdmin, async (req: AuthRequest, res: Response, next: NextFunction) => {
42
+ try {
43
+ const configs = await oAuthConfigService.getAllConfigs();
44
+ const response: ListOAuthConfigsResponse = {
45
+ data: configs,
46
+ count: configs.length,
47
+ };
48
+ successResponse(res, response);
49
+ } catch (error) {
50
+ logger.error('Failed to list OAuth configurations', { error });
51
+ next(error);
52
+ }
53
+ });
54
+
55
+ // GET /api/auth/oauth/:provider/config - Get specific OAuth configuration (admin only)
56
+ router.get(
57
+ '/:provider/config',
58
+ verifyAdmin,
59
+ async (req: AuthRequest, res: Response, next: NextFunction) => {
60
+ try {
61
+ const { provider } = req.params;
62
+ const config = await oAuthConfigService.getConfigByProvider(provider);
63
+ const clientSecret = await oAuthConfigService.getClientSecretByProvider(provider);
64
+
65
+ if (!config) {
66
+ throw new AppError(
67
+ `OAuth configuration for ${provider} not found`,
68
+ 404,
69
+ ERROR_CODES.NOT_FOUND
70
+ );
71
+ }
72
+
73
+ successResponse(res, {
74
+ ...config,
75
+ clientSecret: clientSecret || undefined,
76
+ });
77
+ } catch (error) {
78
+ logger.error('Failed to get OAuth config by provider', {
79
+ provider: req.params.provider,
80
+ error,
81
+ });
82
+ next(error);
83
+ }
84
+ }
85
+ );
86
+
87
+ // POST /api/auth/oauth/configs - Create new OAuth configuration (admin only)
88
+ router.post(
89
+ '/configs',
90
+ verifyAdmin,
91
+ async (req: AuthRequest, res: Response, next: NextFunction) => {
92
+ try {
93
+ const validationResult = createOAuthConfigRequestSchema.safeParse(req.body);
94
+ if (!validationResult.success) {
95
+ throw new AppError(
96
+ validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
97
+ 400,
98
+ ERROR_CODES.INVALID_INPUT
99
+ );
100
+ }
101
+
102
+ const input = validationResult.data;
103
+
104
+ // Check if using shared keys when not allowed
105
+ if (input.useSharedKey && !isOAuthSharedKeysAvailable()) {
106
+ throw new AppError(
107
+ 'Shared OAuth keys are not enabled in this environment',
108
+ 400,
109
+ ERROR_CODES.AUTH_OAUTH_CONFIG_ERROR
110
+ );
111
+ }
112
+
113
+ const config = await oAuthConfigService.createConfig(input);
114
+
115
+ await auditService.log({
116
+ actor: req.user?.email || 'api-key',
117
+ action: 'CREATE_OAUTH_CONFIG',
118
+ module: 'AUTH',
119
+ details: {
120
+ provider: input.provider,
121
+ useSharedKey: input.useSharedKey || false,
122
+ },
123
+ ip_address: req.ip,
124
+ });
125
+
126
+ successResponse(res, config);
127
+ } catch (error) {
128
+ logger.error('Failed to create OAuth configuration', { error });
129
+ next(error);
130
+ }
131
+ }
132
+ );
133
+
134
+ // PUT /api/auth/oauth/:provider/config - Update OAuth configuration (admin only)
135
+ router.put(
136
+ '/:provider/config',
137
+ verifyAdmin,
138
+ async (req: AuthRequest, res: Response, next: NextFunction) => {
139
+ try {
140
+ const provider = req.params.provider;
141
+ if (!provider || provider.length === 0 || provider.length > 50) {
142
+ throw new AppError('Invalid provider name', 400, ERROR_CODES.INVALID_INPUT);
143
+ }
144
+
145
+ const validationResult = updateOAuthConfigRequestSchema.safeParse(req.body);
146
+ if (!validationResult.success) {
147
+ throw new AppError(
148
+ validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
149
+ 400,
150
+ ERROR_CODES.INVALID_INPUT
151
+ );
152
+ }
153
+
154
+ const input = validationResult.data;
155
+
156
+ // Check if using shared keys when not allowed
157
+ if (input.useSharedKey && !isOAuthSharedKeysAvailable()) {
158
+ throw new AppError(
159
+ 'Shared OAuth keys are not enabled in this environment',
160
+ 400,
161
+ ERROR_CODES.AUTH_OAUTH_CONFIG_ERROR
162
+ );
163
+ }
164
+
165
+ const config = await oAuthConfigService.updateConfig(provider, input);
166
+
167
+ await auditService.log({
168
+ actor: req.user?.email || 'api-key',
169
+ action: 'UPDATE_OAUTH_CONFIG',
170
+ module: 'AUTH',
171
+ details: {
172
+ provider,
173
+ updatedFields: Object.keys(input),
174
+ },
175
+ ip_address: req.ip,
176
+ });
177
+
178
+ successResponse(res, config);
179
+ } catch (error) {
180
+ logger.error('Failed to update OAuth configuration', {
181
+ error,
182
+ provider: req.params.provider,
183
+ });
184
+ next(error);
185
+ }
186
+ }
187
+ );
188
+
189
+ // DELETE /api/auth/oauth/:provider/config - Delete OAuth configuration (admin only)
190
+ router.delete(
191
+ '/:provider/config',
192
+ verifyAdmin,
193
+ async (req: AuthRequest, res: Response, next: NextFunction) => {
194
+ try {
195
+ const provider = req.params.provider;
196
+ if (!provider || provider.length === 0 || provider.length > 50) {
197
+ throw new AppError('Invalid provider name', 400, ERROR_CODES.INVALID_INPUT);
198
+ }
199
+ const deleted = await oAuthConfigService.deleteConfig(provider);
200
+
201
+ if (!deleted) {
202
+ throw new AppError(
203
+ `OAuth configuration for ${provider} not found`,
204
+ 404,
205
+ ERROR_CODES.NOT_FOUND
206
+ );
207
+ }
208
+
209
+ await auditService.log({
210
+ actor: req.user?.email || 'api-key',
211
+ action: 'DELETE_OAUTH_CONFIG',
212
+ module: 'AUTH',
213
+ details: { provider },
214
+ ip_address: req.ip,
215
+ });
216
+
217
+ successResponse(res, {
218
+ success: true,
219
+ message: `OAuth configuration for ${provider} deleted successfully`,
220
+ });
221
+ } catch (error) {
222
+ logger.error('Failed to delete OAuth configuration', {
223
+ error,
224
+ provider: req.params.provider,
225
+ });
226
+ next(error);
227
+ }
228
+ }
229
+ );
230
+
231
+ // OAuth Flow Routes
232
+ // GET /api/auth/oauth/:provider - Initialize OAuth flow for any supported provider
233
+ router.get('/:provider', async (req: Request, res: Response, next: NextFunction) => {
234
+ try {
235
+ const { provider } = req.params;
236
+ const { redirect_uri } = req.query;
237
+
238
+ // Validate provider using OAuthProvidersSchema
239
+ const providerValidation = oAuthProvidersSchema.safeParse(provider);
240
+ if (!providerValidation.success) {
241
+ throw new AppError(
242
+ `Unsupported OAuth provider: ${provider}. Supported providers: ${oAuthProvidersSchema.options.join(', ')}`,
243
+ 400,
244
+ ERROR_CODES.INVALID_INPUT
245
+ );
246
+ }
247
+
248
+ const validatedProvider = providerValidation.data;
249
+
250
+ if (!redirect_uri) {
251
+ throw new AppError('Redirect URI is required', 400, ERROR_CODES.INVALID_INPUT);
252
+ }
253
+
254
+ const jwtPayload = {
255
+ provider: validatedProvider,
256
+ redirectUri: redirect_uri ? (redirect_uri as string) : undefined,
257
+ createdAt: Date.now(),
258
+ };
259
+ const jwtSecret = validateJwtSecret();
260
+ const state = jwt.sign(jwtPayload, jwtSecret, {
261
+ algorithm: 'HS256',
262
+ expiresIn: '1h', // Set expiration time for the state token
263
+ });
264
+
265
+ const authUrl = await authService.generateOAuthUrl(validatedProvider, state);
266
+
267
+ successResponse(res, { authUrl });
268
+ } catch (error) {
269
+ logger.error(`${req.params.provider} OAuth error`, { error });
270
+
271
+ // If it's already an AppError, pass it through
272
+ if (error instanceof AppError) {
273
+ next(error);
274
+ return;
275
+ }
276
+
277
+ // For other errors, return the generic OAuth configuration error
278
+ next(
279
+ new AppError(
280
+ `${req.params.provider} OAuth is not properly configured. Please check your oauth configurations.`,
281
+ 500,
282
+ ERROR_CODES.AUTH_OAUTH_CONFIG_ERROR
283
+ )
284
+ );
285
+ }
286
+ });
287
+
288
+ // GET /api/auth/oauth/shared/callback/:state - Shared callback for OAuth providers
289
+ router.get('/shared/callback/:state', async (req: Request, res: Response, next: NextFunction) => {
290
+ try {
291
+ const { state } = req.params;
292
+ const { success, error, payload } = req.query;
293
+
294
+ if (!state) {
295
+ logger.warn('Shared OAuth callback called without state parameter');
296
+ throw new AppError('State parameter is required', 400, ERROR_CODES.INVALID_INPUT);
297
+ }
298
+
299
+ let redirectUri: string;
300
+ let provider: string;
301
+ try {
302
+ const jwtSecret = validateJwtSecret();
303
+ const decodedState = jwt.verify(state, jwtSecret) as {
304
+ provider: string;
305
+ redirectUri: string;
306
+ };
307
+ redirectUri = decodedState.redirectUri || '';
308
+ provider = decodedState.provider || '';
309
+ } catch {
310
+ logger.warn('Invalid state parameter', { state });
311
+ throw new AppError('Invalid state parameter', 400, ERROR_CODES.INVALID_INPUT);
312
+ }
313
+
314
+ // Validate provider using OAuthProvidersSchema
315
+ const providerValidation = oAuthProvidersSchema.safeParse(provider);
316
+ if (!providerValidation.success) {
317
+ logger.warn('Invalid provider in state', { provider });
318
+ throw new AppError(
319
+ `Invalid provider in state: ${provider}. Supported providers: ${oAuthProvidersSchema.options.join(', ')}`,
320
+ 400,
321
+ ERROR_CODES.INVALID_INPUT
322
+ );
323
+ }
324
+ const validatedProvider = providerValidation.data;
325
+ if (!redirectUri) {
326
+ throw new AppError('redirectUri is required', 400, ERROR_CODES.INVALID_INPUT);
327
+ }
328
+
329
+ if (success !== 'true') {
330
+ const errorMessage = error || 'OAuth Authentication Failed';
331
+ logger.warn('Shared OAuth callback failed', { error: errorMessage, provider });
332
+ return res.redirect(`${redirectUri}/?error=${encodeURIComponent(String(errorMessage))}`);
333
+ }
334
+
335
+ if (!payload) {
336
+ throw new AppError('No payload provided in callback', 400, ERROR_CODES.INVALID_INPUT);
337
+ }
338
+
339
+ const payloadData = JSON.parse(
340
+ Buffer.from(payload as string, 'base64').toString('utf8')
341
+ ) as Record<string, unknown>;
342
+
343
+ // Handle shared callback - transforms payload and creates/finds user
344
+ const result = await authService.handleSharedCallback(validatedProvider, payloadData);
345
+
346
+ // Set refresh token in httpOnly cookie and generate CSRF token
347
+ const tokenManager = TokenManager.getInstance();
348
+ const refreshToken = tokenManager.generateRefreshToken(result.user.id);
349
+ setAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME, refreshToken);
350
+ const csrfToken = tokenManager.generateCsrfToken(refreshToken);
351
+
352
+ const params = new URLSearchParams();
353
+ // TODO: Remove all the parameters, will use PKCE in future
354
+ params.set('access_token', result.accessToken);
355
+ params.set('user_id', result.user.id);
356
+ params.set('email', result.user.email);
357
+ params.set('name', result.user.name);
358
+ params.set('csrf_token', csrfToken);
359
+
360
+ res.redirect(`${redirectUri}?${params.toString()}`);
361
+ } catch (error) {
362
+ logger.error('Shared OAuth callback error', { error });
363
+ next(error);
364
+ }
365
+ });
366
+
367
+ /**
368
+ * Handle OAuth provider callback (shared logic for GET and POST)
369
+ * Most providers use GET, but Apple uses POST with form data
370
+ */
371
+ const handleOAuthCallback = async (req: Request, res: Response, next: NextFunction) => {
372
+ try {
373
+ const { provider } = req.params;
374
+ // Support both query params (GET) and body params (POST for Apple)
375
+ // Use method-based source selection to prevent parameter pollution attacks
376
+ const isPostRequest = req.method === 'POST';
377
+ const code = isPostRequest ? (req.body.code as string) : (req.query.code as string);
378
+ const state = isPostRequest ? (req.body.state as string) : (req.query.state as string);
379
+ const token = isPostRequest ? (req.body.id_token as string) : (req.query.token as string);
380
+
381
+ if (!state) {
382
+ logger.warn('OAuth callback called without state parameter');
383
+ throw new AppError('State parameter is required', 400, ERROR_CODES.INVALID_INPUT);
384
+ }
385
+
386
+ // Decode redirectUri from state (needed for both success and error paths)
387
+ let redirectUri: string;
388
+
389
+ try {
390
+ const jwtSecret = validateJwtSecret();
391
+ const stateData = jwt.verify(state, jwtSecret) as {
392
+ provider: string;
393
+ redirectUri: string;
394
+ };
395
+ redirectUri = stateData.redirectUri || '';
396
+ } catch {
397
+ // Invalid state
398
+ logger.warn('Invalid state in provider callback', { state });
399
+ throw new AppError('Invalid state parameter', 400, ERROR_CODES.INVALID_INPUT);
400
+ }
401
+
402
+ if (!redirectUri) {
403
+ throw new AppError('redirectUri is required', 400, ERROR_CODES.INVALID_INPUT);
404
+ }
405
+
406
+ try {
407
+ // Validate provider using OAuthProvidersSchema
408
+ const providerValidation = oAuthProvidersSchema.safeParse(provider);
409
+ if (!providerValidation.success) {
410
+ throw new AppError(
411
+ `Unsupported OAuth provider: ${provider}. Supported providers: ${oAuthProvidersSchema.options.join(', ')}`,
412
+ 400,
413
+ ERROR_CODES.INVALID_INPUT
414
+ );
415
+ }
416
+
417
+ const validatedProvider = providerValidation.data;
418
+
419
+ const result = await authService.handleOAuthCallback(validatedProvider, {
420
+ code: code || undefined,
421
+ token: token || undefined,
422
+ state: state || undefined,
423
+ });
424
+
425
+ // Set refresh token in httpOnly cookie and generate CSRF token
426
+ const tokenManager = TokenManager.getInstance();
427
+ const refreshToken = tokenManager.generateRefreshToken(result.user.id);
428
+ setAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME, refreshToken);
429
+ const csrfToken = tokenManager.generateCsrfToken(refreshToken);
430
+
431
+ // Construct redirect URL with query parameters
432
+ const params = new URLSearchParams();
433
+ // TODO: Remove all the parameters, will use PKCE in future
434
+ params.set('access_token', result.accessToken);
435
+ params.set('user_id', result.user.id);
436
+ params.set('email', result.user.email);
437
+ params.set('name', result.user.name);
438
+ params.set('csrf_token', csrfToken);
439
+
440
+ const finalRedirectUri = `${redirectUri}?${params.toString()}`;
441
+
442
+ return res.redirect(finalRedirectUri);
443
+ } catch (error) {
444
+ logger.error('OAuth callback error', {
445
+ error: error instanceof Error ? error.message : error,
446
+ stack: error instanceof Error ? error.stack : undefined,
447
+ provider: req.params.provider,
448
+ hasCode: !!code,
449
+ hasState: !!state,
450
+ hasToken: !!token,
451
+ });
452
+
453
+ const errorMessage = error instanceof Error ? error.message : 'OAuth Authentication Failed';
454
+
455
+ // Redirect with error in URL parameters
456
+ const params = new URLSearchParams();
457
+ params.set('error', errorMessage);
458
+
459
+ return res.redirect(`${redirectUri}?${params.toString()}`);
460
+ }
461
+ } catch (error) {
462
+ logger.error('OAuth callback error', { error });
463
+ next(error);
464
+ }
465
+ };
466
+
467
+ // GET /api/auth/oauth/:provider/callback - OAuth provider callback (most providers)
468
+ router.get('/:provider/callback', handleOAuthCallback);
469
+
470
+ // POST /api/auth/oauth/:provider/callback - OAuth provider callback (Apple uses POST with form_post)
471
+ router.post('/:provider/callback', handleOAuthCallback);
472
+
473
+ export default router;