insforge 0.3.3 → 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 (635) hide show
  1. package/.claude-plugin/marketplace.json +20 -0
  2. package/.dockerignore +60 -57
  3. package/.env.example +84 -49
  4. package/.github/ISSUE_TEMPLATE/bug_report.yml +36 -83
  5. package/.github/ISSUE_TEMPLATE/config.yml +11 -11
  6. package/.github/ISSUE_TEMPLATE/feature_request.yml +26 -79
  7. package/.github/PULL_REQUEST_TEMPLATE.md +7 -0
  8. package/.github/copilot-instructions.md +146 -146
  9. package/.github/workflows/build-image.yml +66 -65
  10. package/.github/workflows/ci-premerge-check.yml +23 -23
  11. package/.github/workflows/e2e.yml +63 -0
  12. package/.github/workflows/lint-and-format.yml +32 -32
  13. package/.prettierignore +64 -64
  14. package/CHANGELOG.md +44 -3
  15. package/CLAUDE_PLUGIN.md +104 -0
  16. package/CODE_OF_CONDUCT.md +128 -0
  17. package/CONTRIBUTING.md +125 -125
  18. package/Dockerfile +30 -27
  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 -134
  23. package/assets/Dark.svg +23 -23
  24. package/assets/mcpInstallv2.png +0 -0
  25. package/assets/sampleResponse.png +0 -0
  26. package/auth/index.html +13 -0
  27. package/auth/package.json +28 -0
  28. package/auth/public/favicon.ico +0 -0
  29. package/auth/src/App.tsx +33 -0
  30. package/auth/src/components/ErrorCard.tsx +37 -0
  31. package/auth/src/components/Layout.tsx +13 -0
  32. package/auth/src/index.css +19 -0
  33. package/auth/src/lib/broadcastService.ts +117 -0
  34. package/auth/src/lib/utils.ts +11 -0
  35. package/auth/src/main.tsx +22 -0
  36. package/auth/src/pages/ForgotPasswordPage.tsx +11 -0
  37. package/auth/src/pages/ResetPasswordPage.tsx +11 -0
  38. package/auth/src/pages/SignInPage.tsx +60 -0
  39. package/auth/src/pages/SignUpPage.tsx +60 -0
  40. package/auth/src/pages/VerifyEmailPage.tsx +20 -0
  41. package/auth/src/vite-env.d.ts +10 -0
  42. package/auth/tsconfig.json +32 -0
  43. package/auth/tsconfig.node.json +11 -0
  44. package/auth/vite.config.ts +25 -0
  45. package/backend/package.json +78 -75
  46. package/backend/src/api/{middleware → middlewares}/auth.ts +8 -9
  47. package/backend/src/api/middlewares/rate-limiters.ts +127 -0
  48. package/backend/src/api/routes/{ai.ts → ai/index.routes.ts} +22 -26
  49. package/backend/src/api/routes/auth/index.routes.ts +667 -0
  50. package/backend/src/api/routes/auth/oauth.routes.ts +473 -0
  51. package/backend/src/api/routes/{database.advance.ts → database/advance.routes.ts} +128 -65
  52. package/backend/src/api/routes/database/index.routes.ts +90 -0
  53. package/backend/src/api/routes/{database.records.ts → database/records.routes.ts} +26 -12
  54. package/backend/src/api/routes/{database.tables.ts → database/tables.routes.ts} +6 -23
  55. package/backend/src/api/routes/docs/index.routes.ts +75 -0
  56. package/backend/src/api/routes/email/index.routes.ts +35 -0
  57. package/backend/src/api/routes/functions/index.routes.ts +194 -0
  58. package/backend/src/api/routes/{logs.ts → logs/index.routes.ts} +25 -30
  59. package/backend/src/api/routes/{metadata.ts → metadata/index.routes.ts} +33 -31
  60. package/backend/src/api/routes/realtime/channels.routes.ts +81 -0
  61. package/backend/src/api/routes/realtime/index.routes.ts +12 -0
  62. package/backend/src/api/routes/realtime/messages.routes.ts +48 -0
  63. package/backend/src/api/routes/realtime/permissions.routes.ts +19 -0
  64. package/backend/src/api/routes/{secrets.ts → secrets/index.routes.ts} +27 -22
  65. package/backend/src/api/routes/{storage.ts → storage/index.routes.ts} +48 -61
  66. package/backend/src/api/routes/usage/index.routes.ts +91 -0
  67. package/backend/src/infra/config/app.config.ts +51 -0
  68. package/backend/src/infra/database/database.manager.ts +182 -0
  69. package/backend/{migrations → src/infra/database/migrations}/000_create-base-tables.sql +141 -141
  70. package/backend/{migrations → src/infra/database/migrations}/001_create-helper-functions.sql +40 -40
  71. package/backend/{migrations → src/infra/database/migrations}/002_rename-auth-tables.sql +29 -29
  72. package/backend/{migrations → src/infra/database/migrations}/003_create-users-table.sql +55 -55
  73. package/backend/{migrations → src/infra/database/migrations}/004_add-reload-postgrest-func.sql +23 -23
  74. package/backend/{migrations → src/infra/database/migrations}/005_enable-project-admin-modify-users.sql +29 -29
  75. package/backend/{migrations → src/infra/database/migrations}/006_modify-ai-usage-table.sql +24 -24
  76. package/backend/{migrations → src/infra/database/migrations}/007_drop-metadata-table.sql +1 -1
  77. package/backend/{migrations → src/infra/database/migrations}/008_add-system-tables.sql +76 -76
  78. package/backend/{migrations → src/infra/database/migrations}/009_add-function-secrets.sql +23 -23
  79. package/backend/{migrations → src/infra/database/migrations}/010_modify-ai-config-modalities.sql +93 -93
  80. package/backend/{migrations → src/infra/database/migrations}/011_refactor-secrets-table.sql +15 -15
  81. package/backend/{migrations → src/infra/database/migrations}/012_add-storage-uploaded-by.sql +7 -7
  82. package/backend/src/infra/database/migrations/013_create-auth-schema-functions.sql +44 -0
  83. package/backend/src/infra/database/migrations/014_add-updated-at-trigger-user-table.sql +8 -0
  84. package/backend/src/infra/database/migrations/015_create-auth-config-and-email-otp-tables.sql +60 -0
  85. package/backend/src/infra/database/migrations/016_update-auth-config-and-email-otp.sql +24 -0
  86. package/backend/src/infra/database/migrations/017_create-realtime-schema.sql +233 -0
  87. package/backend/src/infra/realtime/realtime.manager.ts +246 -0
  88. package/backend/src/infra/realtime/webhook-sender.ts +82 -0
  89. package/backend/src/{core/secrets/encryption.ts → infra/security/encryption.manager.ts} +3 -2
  90. package/backend/src/infra/security/token.manager.ts +219 -0
  91. package/backend/src/infra/socket/socket.manager.ts +522 -0
  92. package/backend/src/providers/ai/openrouter.provider.ts +380 -0
  93. package/backend/src/providers/email/base.provider.ts +38 -0
  94. package/backend/src/providers/email/cloud.provider.ts +271 -0
  95. package/backend/src/{core/logs/providers → providers/logs}/base.provider.ts +11 -11
  96. package/backend/src/{core/logs/providers → providers/logs}/cloudwatch.provider.ts +61 -38
  97. package/backend/src/providers/logs/local.provider.ts +185 -0
  98. package/backend/src/providers/oauth/apple.provider.ts +266 -0
  99. package/backend/src/providers/oauth/base.provider.ts +29 -0
  100. package/backend/src/providers/oauth/discord.provider.ts +195 -0
  101. package/backend/src/providers/oauth/facebook.provider.ts +194 -0
  102. package/backend/src/providers/oauth/github.provider.ts +208 -0
  103. package/backend/src/providers/oauth/google.provider.ts +249 -0
  104. package/backend/src/providers/oauth/index.ts +8 -0
  105. package/backend/src/providers/oauth/linkedin.provider.ts +240 -0
  106. package/backend/src/providers/oauth/microsoft.provider.ts +169 -0
  107. package/backend/src/providers/oauth/x.provider.ts +202 -0
  108. package/backend/src/providers/storage/base.provider.ts +29 -0
  109. package/backend/src/providers/storage/local.provider.ts +103 -0
  110. package/backend/src/providers/storage/s3.provider.ts +313 -0
  111. package/backend/src/server.ts +317 -288
  112. package/backend/src/{core/ai/config.ts → services/ai/ai-config.service.ts} +19 -24
  113. package/backend/src/services/ai/ai-model.service.ts +60 -0
  114. package/backend/src/{core/ai/usage.ts → services/ai/ai-usage.service.ts} +28 -35
  115. package/backend/src/{core/ai/chat.ts → services/ai/chat-completion.service.ts} +37 -24
  116. package/backend/src/services/ai/helpers.ts +64 -0
  117. package/backend/src/{core/ai/image.ts → services/ai/image-generation.service.ts} +17 -19
  118. package/backend/src/services/ai/index.ts +13 -0
  119. package/backend/src/services/auth/auth-config.service.ts +250 -0
  120. package/backend/src/services/auth/auth-otp.service.ts +424 -0
  121. package/backend/src/services/auth/auth.service.ts +1150 -0
  122. package/backend/src/services/auth/index.ts +4 -0
  123. package/backend/src/{core/auth/oauth.ts → services/auth/oauth-config.service.ts} +106 -52
  124. package/backend/src/{core/database/advance.ts → services/database/database-advance.service.ts} +97 -131
  125. package/backend/src/services/database/database-table.service.ts +802 -0
  126. package/backend/src/services/database/database.service.ts +127 -0
  127. package/backend/src/services/email/email.service.ts +73 -0
  128. package/backend/src/{core/functions/functions.ts → services/functions/function.service.ts} +95 -88
  129. package/backend/src/{core/logs/audit.ts → services/logs/audit.service.ts} +92 -75
  130. package/backend/src/services/logs/log.service.ts +73 -0
  131. package/backend/src/services/realtime/index.ts +3 -0
  132. package/backend/src/services/realtime/realtime-auth.service.ts +104 -0
  133. package/backend/src/services/realtime/realtime-channel.service.ts +237 -0
  134. package/backend/src/services/realtime/realtime-message.service.ts +260 -0
  135. package/backend/src/{core/secrets/secrets.ts → services/secrets/secret.service.ts} +48 -66
  136. package/backend/src/services/storage/storage.service.ts +617 -0
  137. package/backend/src/services/usage/usage.service.ts +149 -0
  138. package/backend/src/types/auth.ts +77 -2
  139. package/backend/src/types/email.ts +8 -0
  140. package/backend/src/types/error-constants.ts +4 -0
  141. package/backend/src/types/logs.ts +0 -29
  142. package/backend/src/types/realtime.ts +18 -0
  143. package/backend/src/{core/socket/types.ts → types/socket.ts} +11 -36
  144. package/backend/src/utils/cookies.ts +35 -0
  145. package/backend/src/utils/environment.ts +9 -3
  146. package/backend/src/utils/logger.ts +20 -2
  147. package/backend/src/utils/s3-config-loader.ts +64 -0
  148. package/backend/src/utils/seed.ts +301 -205
  149. package/backend/src/utils/sql-parser.ts +91 -1
  150. package/backend/src/utils/utils.ts +114 -0
  151. package/backend/src/utils/validations.ts +40 -4
  152. package/backend/tests/README.md +133 -133
  153. package/backend/tests/cleanup-all-test-data.sh +230 -230
  154. package/backend/tests/cloud/test-s3-multitenant.sh +131 -131
  155. package/backend/tests/local/comprehensive-curl-tests.sh +155 -155
  156. package/backend/tests/local/test-ai-config.sh +129 -0
  157. package/backend/tests/local/test-ai-usage.sh +80 -0
  158. package/backend/tests/local/test-auth-router.sh +143 -143
  159. package/backend/tests/local/test-database-router.sh +222 -222
  160. package/backend/tests/local/test-e2e.sh +240 -240
  161. package/backend/tests/local/test-fk-errors.sh +96 -96
  162. package/backend/tests/local/test-functions.sh +123 -0
  163. package/backend/tests/local/test-id-field.sh +200 -200
  164. package/backend/tests/local/test-logs.sh +132 -0
  165. package/backend/tests/local/test-public-bucket.sh +264 -264
  166. package/backend/tests/local/test-secrets.sh +249 -247
  167. package/backend/tests/local/test-serverless-functions.sh.disabled +325 -325
  168. package/backend/tests/local/test-traditional-rest.sh +208 -208
  169. package/backend/tests/manual/README.md +50 -50
  170. package/backend/tests/manual/create-large-table-simple.sql +10 -10
  171. package/backend/tests/manual/seed-large-table.sql +100 -100
  172. package/backend/tests/manual/setup-large-table-extras.sql +33 -33
  173. package/backend/tests/manual/test-bulk-upsert.sh +409 -409
  174. package/backend/tests/manual/test-database-advance.sh +296 -296
  175. package/backend/tests/manual/test-postgrest-stability.sh +191 -191
  176. package/backend/tests/manual/test-rawsql-export-import.sh +411 -411
  177. package/backend/tests/manual/test-rawsql-modes.sh +244 -0
  178. package/backend/tests/manual/test-universal-storage.sh +263 -263
  179. package/backend/tests/manual/test-users.sql +17 -17
  180. package/backend/tests/run-all-tests.sh +139 -139
  181. package/backend/tests/setup.ts +0 -0
  182. package/backend/tests/test-config.sh +338 -302
  183. package/backend/tests/unit/analyze-query.test.ts +697 -0
  184. package/backend/tests/unit/cloud-token.test.ts +48 -0
  185. package/backend/tests/unit/constant.test.ts +8 -0
  186. package/backend/tests/unit/email.test.ts +372 -0
  187. package/backend/tests/unit/environment.test.ts +59 -0
  188. package/backend/tests/unit/helpers.test.ts +63 -0
  189. package/backend/tests/unit/logger.test.ts +22 -0
  190. package/backend/tests/unit/rate-limit.test.ts +154 -0
  191. package/backend/tests/unit/response.test.ts +58 -0
  192. package/backend/tests/unit/sql-parser.test.ts +74 -0
  193. package/backend/tests/unit/uuid.test.ts +21 -0
  194. package/backend/tests/unit/validations.test.ts +80 -0
  195. package/backend/tsconfig.json +22 -22
  196. package/backend/vitest.config.ts +11 -0
  197. package/claude-plugin/.claude-plugin/plugin.json +24 -0
  198. package/claude-plugin/README.md +133 -0
  199. package/claude-plugin/skills/insforge-schema-patterns/SKILL.md +270 -0
  200. package/docker-compose.prod.yml +204 -144
  201. package/docker-compose.yml +232 -167
  202. package/docker-init/db/db-init.sql +97 -125
  203. package/docker-init/db/jwt.sql +5 -5
  204. package/docker-init/db/postgresql.conf +16 -16
  205. package/docker-init/logs/vector.yml +236 -0
  206. package/docs/README.md +44 -0
  207. package/docs/agent-docs/real-time.md +269 -0
  208. package/docs/changelog.mdx +119 -0
  209. package/docs/core-concepts/ai/architecture.mdx +373 -0
  210. package/docs/core-concepts/ai/sdk.mdx +213 -0
  211. package/docs/core-concepts/authentication/architecture.mdx +278 -0
  212. package/docs/core-concepts/authentication/sdk.mdx +414 -0
  213. package/docs/core-concepts/authentication/ui-components/customization.mdx +529 -0
  214. package/docs/core-concepts/authentication/ui-components/nextjs.mdx +221 -0
  215. package/docs/core-concepts/authentication/ui-components/react-router.mdx +184 -0
  216. package/docs/core-concepts/authentication/ui-components/react.mdx +129 -0
  217. package/docs/core-concepts/database/architecture.mdx +256 -0
  218. package/docs/core-concepts/database/sdk.mdx +382 -0
  219. package/docs/core-concepts/email/architecture.mdx +101 -0
  220. package/docs/core-concepts/email/sdk.mdx +53 -0
  221. package/docs/core-concepts/functions/architecture.mdx +105 -0
  222. package/docs/core-concepts/functions/sdk.mdx +184 -0
  223. package/docs/core-concepts/realtime/architecture.mdx +446 -0
  224. package/docs/core-concepts/realtime/sdk.mdx +409 -0
  225. package/docs/core-concepts/storage/architecture.mdx +243 -0
  226. package/docs/core-concepts/storage/sdk.mdx +253 -0
  227. package/docs/deployment/README.md +94 -0
  228. package/docs/deployment/deploy-to-aws-ec2.md +565 -0
  229. package/docs/deployment/deploy-to-azure-virtual-machines.md +313 -0
  230. package/docs/deployment/deploy-to-google-cloud-compute-engine.md +613 -0
  231. package/docs/deployment/deploy-to-render.md +441 -0
  232. package/docs/deprecated/insforge-auth-api.md +214 -214
  233. package/docs/deprecated/insforge-auth-sdk.md +99 -99
  234. package/docs/deprecated/insforge-db-api.md +358 -358
  235. package/docs/deprecated/insforge-db-sdk.md +139 -139
  236. package/docs/deprecated/insforge-debug-sdk.md +156 -156
  237. package/docs/deprecated/insforge-debug.md +64 -64
  238. package/docs/deprecated/insforge-instructions.md +123 -123
  239. package/docs/deprecated/insforge-project.md +117 -117
  240. package/docs/deprecated/insforge-storage-api.md +278 -278
  241. package/docs/deprecated/insforge-storage-sdk.md +158 -158
  242. package/docs/docs.json +232 -0
  243. package/docs/examples/framework-guides/nextjs.mdx +131 -0
  244. package/docs/examples/framework-guides/nuxt.mdx +165 -0
  245. package/docs/examples/framework-guides/react.mdx +165 -0
  246. package/docs/examples/framework-guides/svelte.mdx +153 -0
  247. package/docs/examples/framework-guides/vue.mdx +159 -0
  248. package/docs/examples/overview.mdx +67 -0
  249. package/docs/favicon.svg +19 -0
  250. package/docs/images/changelog/dec-2025/ai-integration.png +0 -0
  251. package/docs/images/changelog/dec-2025/ai-models.webp +0 -0
  252. package/docs/images/changelog/dec-2025/alipay-payment.webp +0 -0
  253. package/docs/images/changelog/dec-2025/apple-login.jpg +0 -0
  254. package/docs/images/changelog/dec-2025/mcp-installer.png +0 -0
  255. package/docs/images/changelog/dec-2025/realtime-module.jpg +0 -0
  256. package/docs/images/changelog/nov-2025/auth-components.webp +0 -0
  257. package/docs/images/changelog/nov-2025/database-metadata.webp +0 -0
  258. package/docs/images/changelog/nov-2025/quickstart-prompts.webp +0 -0
  259. package/docs/images/changelog/nov-2025/sql-editor.webp +0 -0
  260. package/docs/images/changelog/nov-2025/usage-page.webp +0 -0
  261. package/docs/images/changelog/october-2025/csv-upload.webp +0 -0
  262. package/docs/images/changelog/october-2025/logs-feature.webp +0 -0
  263. package/docs/images/changelog/october-2025/oauth-providers.webp +0 -0
  264. package/docs/images/checks-passed.png +0 -0
  265. package/docs/images/dashboard-connect-expanded.png +0 -0
  266. package/docs/images/dashboard-connect.png +0 -0
  267. package/docs/images/hero-dark.png +0 -0
  268. package/docs/images/hero-light.png +0 -0
  269. package/docs/images/icons/ai.svg +4 -0
  270. package/docs/images/icons/auth.svg +1 -0
  271. package/docs/images/icons/database.svg +1 -0
  272. package/docs/images/icons/function.svg +1 -0
  273. package/docs/images/icons/storage.svg +1 -0
  274. package/docs/images/logos/nextjs.svg +4 -0
  275. package/docs/images/logos/nuxt.svg +4 -0
  276. package/docs/images/logos/react.svg +5 -0
  277. package/docs/images/logos/svelte.svg +4 -0
  278. package/docs/images/logos/vue.svg +5 -0
  279. package/docs/images/mcp-install.png +0 -0
  280. package/docs/images/onboarding-mcp.png +0 -0
  281. package/docs/insforge-instructions-sdk.md +89 -407
  282. package/docs/introduction.mdx +45 -0
  283. package/docs/logo/dark.svg +22 -0
  284. package/docs/logo/light.svg +20 -0
  285. package/docs/partnership.mdx +652 -0
  286. package/docs/quickstart.mdx +83 -0
  287. package/docs/showcase/2048-arena.png +0 -0
  288. package/docs/showcase/framegen-cloud.png +0 -0
  289. package/docs/showcase/line-connect-race.png +0 -0
  290. package/docs/showcase/moment-vibe.png +0 -0
  291. package/docs/showcase/national-flags.png +0 -0
  292. package/docs/showcase/pokemon-vibe.png +0 -0
  293. package/docs/showcase/pure-browse-buy.png +0 -0
  294. package/docs/showcase.mdx +52 -0
  295. package/docs/snippets/sdk-installation.mdx +22 -0
  296. package/docs/snippets/service-icons.mdx +27 -0
  297. package/eslint.config.js +10 -3
  298. package/examples/oauth/frontend-oauth-example.html +250 -250
  299. package/examples/response-examples.md +443 -443
  300. package/frontend/components.json +17 -17
  301. package/frontend/package.json +69 -63
  302. package/frontend/src/App.tsx +13 -82
  303. package/frontend/src/assets/icons/checkbox_checked.svg +6 -6
  304. package/frontend/src/assets/icons/checkbox_undetermined.svg +6 -6
  305. package/frontend/src/assets/icons/checked.svg +3 -3
  306. package/frontend/src/assets/icons/connected.svg +3 -0
  307. package/frontend/src/assets/icons/error.svg +3 -3
  308. package/frontend/src/assets/icons/loader.svg +9 -0
  309. package/frontend/src/assets/icons/pencil.svg +4 -4
  310. package/frontend/src/assets/icons/refresh.svg +4 -4
  311. package/frontend/src/assets/icons/step_active.svg +3 -3
  312. package/frontend/src/assets/icons/step_inactive.svg +11 -11
  313. package/frontend/src/assets/icons/warning.svg +3 -3
  314. package/frontend/src/assets/logos/apple.svg +4 -0
  315. package/frontend/src/assets/logos/claude_code.svg +3 -3
  316. package/frontend/src/assets/logos/cline.svg +6 -6
  317. package/frontend/src/assets/logos/cursor.svg +20 -20
  318. package/frontend/src/assets/logos/discord.svg +8 -8
  319. package/frontend/src/assets/logos/facebook.svg +3 -0
  320. package/frontend/src/assets/logos/gemini.svg +19 -19
  321. package/frontend/src/assets/logos/github.svg +5 -5
  322. package/frontend/src/assets/logos/google.svg +13 -13
  323. package/frontend/src/assets/logos/grok.svg +10 -10
  324. package/frontend/src/assets/logos/insforge_dark.svg +15 -15
  325. package/frontend/src/assets/logos/insforge_light.svg +15 -15
  326. package/frontend/src/assets/logos/instagram.svg +2 -0
  327. package/frontend/src/assets/logos/linkedin.svg +3 -0
  328. package/frontend/src/assets/logos/microsoft.svg +1 -0
  329. package/frontend/src/assets/logos/openai.svg +10 -10
  330. package/frontend/src/assets/logos/roo_code.svg +9 -9
  331. package/frontend/src/assets/logos/spotify.svg +17 -0
  332. package/frontend/src/assets/logos/tiktok.svg +6 -0
  333. package/frontend/src/assets/logos/trae.svg +3 -3
  334. package/frontend/src/assets/logos/windsurf.svg +10 -10
  335. package/frontend/src/assets/logos/x.svg +3 -0
  336. package/frontend/src/components/Checkbox.tsx +27 -29
  337. package/frontend/src/components/CodeBlock.tsx +55 -2
  338. package/frontend/src/components/CodeEditor.tsx +92 -0
  339. package/frontend/src/components/ConfirmDialog.tsx +1 -1
  340. package/frontend/src/components/ConnectCTA.tsx +38 -0
  341. package/frontend/src/components/CopyButton.tsx +52 -15
  342. package/frontend/src/components/ErrorState.tsx +1 -2
  343. package/frontend/src/components/FeatureSidebar.tsx +6 -6
  344. package/frontend/src/components/FeatureSidebarItem.tsx +2 -2
  345. package/frontend/src/components/JsonHighlight.tsx +21 -9
  346. package/frontend/src/components/ProjectInfoModal.tsx +128 -0
  347. package/frontend/src/components/PromptDialog.tsx +1 -4
  348. package/frontend/src/components/SearchInput.tsx +1 -2
  349. package/frontend/src/components/Stepper.tsx +53 -0
  350. package/frontend/src/components/ThemeToggle.tsx +3 -3
  351. package/frontend/src/components/datagrid/DataGrid.tsx +25 -32
  352. package/frontend/src/components/datagrid/cell-editors/DateCellEditor.tsx +1 -2
  353. package/frontend/src/components/datagrid/cell-editors/JsonCellEditor.tsx +2 -4
  354. package/frontend/src/components/datagrid/index.ts +23 -0
  355. package/frontend/src/components/index.ts +23 -30
  356. package/frontend/src/components/layout/AppHeader.tsx +131 -91
  357. package/frontend/src/components/layout/AppSidebar.tsx +80 -170
  358. package/frontend/src/components/layout/Layout.tsx +12 -23
  359. package/frontend/src/components/layout/PrimaryMenu.tsx +187 -0
  360. package/frontend/src/components/layout/SecondaryMenu.tsx +70 -0
  361. package/frontend/src/components/layout/index.ts +5 -0
  362. package/frontend/src/components/radix/Tooltip.tsx +24 -13
  363. package/frontend/src/components/radix/index.ts +22 -0
  364. package/frontend/src/features/ai/components/AIConfigCard.tsx +129 -83
  365. package/frontend/src/features/ai/components/AIEmptyState.tsx +12 -7
  366. package/frontend/src/features/ai/components/ModalityFilterSidebar.tsx +101 -0
  367. package/frontend/src/features/ai/components/ModelSelectionDialog.tsx +135 -0
  368. package/frontend/src/features/ai/components/ModelSelectionGrid.tsx +51 -0
  369. package/frontend/src/features/ai/components/SystemPromptDialog.tsx +118 -0
  370. package/frontend/src/features/ai/components/index.ts +6 -0
  371. package/frontend/src/features/ai/helpers.ts +57 -71
  372. package/frontend/src/features/ai/hooks/useAIConfigs.ts +39 -113
  373. package/frontend/src/features/ai/hooks/useAIUsage.ts +0 -2
  374. package/frontend/src/features/ai/pages/AIPage.tsx +166 -0
  375. package/frontend/src/features/ai/services/ai.service.ts +5 -5
  376. package/frontend/src/features/auth/components/AuthPreview.tsx +96 -0
  377. package/frontend/src/features/auth/components/OAuthConfigDialog.tsx +54 -30
  378. package/frontend/src/features/auth/components/UserFormDialog.tsx +13 -6
  379. package/frontend/src/features/auth/components/UsersDataGrid.tsx +50 -14
  380. package/frontend/src/features/auth/components/index.ts +5 -0
  381. package/frontend/src/features/auth/helpers.tsx +208 -0
  382. package/frontend/src/features/auth/hooks/useAnonToken.ts +30 -0
  383. package/frontend/src/features/auth/hooks/useAuthConfig.ts +48 -0
  384. package/frontend/src/features/auth/hooks/useOAuthConfig.ts +14 -10
  385. package/frontend/src/features/auth/hooks/useUsers.ts +43 -5
  386. package/frontend/src/features/auth/index.ts +3 -2
  387. package/frontend/src/features/auth/pages/AuthMethodsPage.tsx +275 -0
  388. package/frontend/src/features/auth/pages/ConfigurationPage.tsx +395 -0
  389. package/frontend/src/features/auth/pages/UsersPage.tsx +257 -0
  390. package/frontend/src/features/auth/services/anonToken.service.ts +11 -0
  391. package/frontend/src/features/auth/services/config.service.ts +19 -0
  392. package/frontend/src/features/auth/services/{oauth.service.ts → oauth-config.service.ts} +4 -4
  393. package/frontend/src/features/auth/services/{auth.service.ts → user.service.ts} +7 -53
  394. package/frontend/src/features/dashboard/components/ConnectionSuccessBanner.tsx +35 -0
  395. package/frontend/src/features/dashboard/components/PromptCard.tsx +21 -0
  396. package/frontend/src/features/dashboard/components/PromptDialog.tsx +103 -0
  397. package/frontend/src/features/dashboard/components/StatsCard.tsx +50 -0
  398. package/frontend/src/features/dashboard/components/index.ts +4 -0
  399. package/frontend/src/features/dashboard/pages/DashboardPage.tsx +212 -0
  400. package/frontend/src/features/dashboard/prompts/ai-chatbot.ts +13 -0
  401. package/frontend/src/features/dashboard/prompts/crm-system.ts +13 -0
  402. package/frontend/src/features/dashboard/prompts/ecommerce-platform.ts +12 -0
  403. package/frontend/src/features/dashboard/prompts/index.ts +31 -0
  404. package/frontend/src/features/dashboard/prompts/instagram-clone.ts +11 -0
  405. package/frontend/src/features/dashboard/prompts/notion-clone.ts +14 -0
  406. package/frontend/src/features/dashboard/prompts/reddit-clone.ts +12 -0
  407. package/frontend/src/features/database/components/DatabaseDataGrid.tsx +48 -17
  408. package/frontend/src/features/database/components/ForeignKeyCell.tsx +15 -34
  409. package/frontend/src/features/database/components/ForeignKeyPopover.tsx +19 -20
  410. package/frontend/src/features/database/components/LinkRecordModal.tsx +120 -125
  411. package/frontend/src/features/database/components/RecordFormDialog.tsx +22 -33
  412. package/frontend/src/features/database/components/RecordFormField.tsx +45 -47
  413. package/frontend/src/features/database/components/SQLModal.tsx +75 -0
  414. package/frontend/src/features/database/components/TableEmptyState.tsx +6 -5
  415. package/frontend/src/features/database/components/TableForm.tsx +28 -19
  416. package/frontend/src/features/database/components/TableFormColumn.tsx +2 -3
  417. package/frontend/src/features/database/components/TableSidebar.tsx +1 -1
  418. package/frontend/src/features/database/components/TablesEmptyState.tsx +48 -0
  419. package/frontend/src/features/database/components/TemplateCard.tsx +37 -0
  420. package/frontend/src/features/database/components/TemplatePreview.tsx +92 -0
  421. package/frontend/src/features/database/components/index.ts +19 -0
  422. package/frontend/src/features/database/constants.ts +28 -2
  423. package/frontend/src/features/database/contexts/SQLEditorContext.tsx +188 -0
  424. package/frontend/src/features/database/helpers.ts +2 -2
  425. package/frontend/src/features/database/hooks/useCSVImport.ts +29 -0
  426. package/frontend/src/features/database/hooks/useDatabase.ts +66 -0
  427. package/frontend/src/features/database/hooks/useRawSQL.ts +55 -0
  428. package/frontend/src/features/database/hooks/useRecords.ts +139 -0
  429. package/frontend/src/features/database/hooks/useTables.ts +135 -0
  430. package/frontend/src/features/database/index.ts +7 -1
  431. package/frontend/src/features/database/pages/FunctionsPage.tsx +203 -0
  432. package/frontend/src/features/database/pages/IndexesPage.tsx +228 -0
  433. package/frontend/src/features/database/pages/PoliciesPage.tsx +237 -0
  434. package/frontend/src/features/database/pages/SQLEditorPage.tsx +382 -0
  435. package/frontend/src/features/database/{page/DatabasePage.tsx → pages/TablesPage.tsx} +168 -209
  436. package/frontend/src/features/database/pages/TemplatesPage.tsx +39 -0
  437. package/frontend/src/features/database/pages/TriggersPage.tsx +230 -0
  438. package/frontend/src/features/database/services/advance.service.ts +40 -0
  439. package/frontend/src/features/database/services/database.service.ts +33 -194
  440. package/frontend/src/features/database/services/record.service.ts +219 -0
  441. package/frontend/src/features/database/services/table.service.ts +58 -0
  442. package/frontend/src/features/database/templates/ai-chatbot.ts +402 -0
  443. package/frontend/src/features/database/templates/crm-system.ts +528 -0
  444. package/frontend/src/features/database/templates/ecommerce-platform.ts +553 -0
  445. package/frontend/src/features/database/templates/index.ts +34 -0
  446. package/frontend/src/features/database/templates/instagram-clone.ts +222 -0
  447. package/frontend/src/features/database/templates/notion-clone.ts +483 -0
  448. package/frontend/src/features/database/templates/reddit-clone.ts +526 -0
  449. package/frontend/src/features/functions/components/FunctionRow.tsx +2 -1
  450. package/frontend/src/features/functions/components/FunctionsSidebar.tsx +1 -1
  451. package/frontend/src/features/functions/components/SecretRow.tsx +1 -1
  452. package/frontend/src/features/functions/components/index.ts +5 -0
  453. package/frontend/src/features/functions/hooks/useFunctions.ts +4 -4
  454. package/frontend/src/features/{secrets → functions}/hooks/useSecrets.ts +5 -5
  455. package/frontend/src/features/functions/pages/FunctionsPage.tsx +148 -0
  456. package/frontend/src/features/functions/{components/SecretsContent.tsx → pages/SecretsPage.tsx} +19 -21
  457. package/frontend/src/features/functions/services/{functions.service.ts → function.service.ts} +2 -2
  458. package/frontend/src/features/{secrets/services/secrets.service.ts → functions/services/secret.service.ts} +2 -2
  459. package/frontend/src/features/login/hooks/usePartnerOrigin.ts +27 -0
  460. package/frontend/src/features/login/pages/CloudLoginPage.tsx +118 -0
  461. package/frontend/src/features/login/{page → pages}/LoginPage.tsx +16 -23
  462. package/frontend/src/features/login/services/partnership.service.ts +65 -0
  463. package/frontend/src/features/logs/components/LogsDataGrid.tsx +89 -0
  464. package/frontend/src/features/logs/components/SeverityBadge.tsx +18 -0
  465. package/frontend/src/features/logs/components/index.ts +2 -0
  466. package/frontend/src/features/logs/helpers.ts +24 -0
  467. package/frontend/src/features/logs/hooks/useAuditLogs.ts +4 -4
  468. package/frontend/src/features/logs/hooks/useLogSources.ts +137 -0
  469. package/frontend/src/features/logs/hooks/useLogs.ts +163 -0
  470. package/frontend/src/features/logs/hooks/useMcpUsage.ts +128 -0
  471. package/frontend/src/features/logs/index.ts +8 -2
  472. package/frontend/src/features/logs/{page → pages}/AuditsPage.tsx +91 -38
  473. package/frontend/src/features/logs/pages/LogsPage.tsx +152 -0
  474. package/frontend/src/features/logs/pages/MCPLogsPage.tsx +84 -0
  475. package/frontend/src/features/logs/services/audit.service.ts +63 -0
  476. package/frontend/src/features/logs/services/log.service.ts +15 -110
  477. package/frontend/src/features/logs/services/usage.service.ts +31 -0
  478. package/frontend/src/features/onboard/components/McpConnectionStatus.tsx +68 -0
  479. package/frontend/src/features/onboard/components/OnboardingModal.tsx +267 -0
  480. package/frontend/src/features/onboard/components/VideoDemoModal.tsx +38 -0
  481. package/frontend/src/features/onboard/components/index.ts +4 -0
  482. package/frontend/src/features/onboard/components/mcp/CursorDeeplinkGenerator.tsx +2 -2
  483. package/frontend/src/features/onboard/components/mcp/{mcp-helper.tsx → helpers.tsx} +8 -8
  484. package/frontend/src/features/onboard/components/mcp/index.ts +2 -3
  485. package/frontend/src/features/onboard/index.ts +13 -3
  486. package/frontend/src/features/realtime/components/ChannelRow.tsx +83 -0
  487. package/frontend/src/features/realtime/components/EditChannelModal.tsx +246 -0
  488. package/frontend/src/features/realtime/components/MessageRow.tsx +85 -0
  489. package/frontend/src/features/realtime/components/RealtimeEmptyState.tsx +30 -0
  490. package/frontend/src/features/realtime/hooks/useRealtime.ts +218 -0
  491. package/frontend/src/features/realtime/index.ts +11 -0
  492. package/frontend/src/features/realtime/pages/RealtimeChannelsPage.tsx +172 -0
  493. package/frontend/src/features/realtime/pages/RealtimeMessagesPage.tsx +211 -0
  494. package/frontend/src/features/realtime/pages/RealtimePermissionsPage.tsx +191 -0
  495. package/frontend/src/features/realtime/services/realtime.service.ts +107 -0
  496. package/frontend/src/features/storage/components/BucketEmptyState.tsx +9 -6
  497. package/frontend/src/features/storage/components/BucketFormDialog.tsx +25 -41
  498. package/frontend/src/features/storage/components/FilePreviewDialog.tsx +20 -8
  499. package/frontend/src/features/storage/components/StorageDataGrid.tsx +4 -3
  500. package/frontend/src/features/storage/components/StorageManager.tsx +23 -34
  501. package/frontend/src/features/storage/components/index.ts +12 -0
  502. package/frontend/src/features/storage/hooks/useStorage.ts +208 -0
  503. package/frontend/src/features/storage/{page → pages}/StoragePage.tsx +41 -143
  504. package/frontend/src/features/storage/services/storage.service.ts +22 -1
  505. package/frontend/src/features/visualizer/components/AuthNode.tsx +72 -56
  506. package/frontend/src/features/visualizer/components/BucketNode.tsx +4 -4
  507. package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +108 -80
  508. package/frontend/src/features/visualizer/components/TableNode.tsx +34 -41
  509. package/frontend/src/features/visualizer/components/VisualizerSkeleton.tsx +12 -4
  510. package/frontend/src/features/visualizer/pages/VisualizerPage.tsx +97 -0
  511. package/frontend/src/index.css +1 -0
  512. package/frontend/src/lib/analytics/posthog.tsx +27 -0
  513. package/frontend/src/lib/contexts/AuthContext.tsx +38 -31
  514. package/frontend/src/lib/contexts/SocketContext.tsx +123 -80
  515. package/frontend/src/{features/metadata → lib}/hooks/useMetadata.ts +1 -1
  516. package/frontend/src/lib/hooks/useToast.tsx +6 -2
  517. package/frontend/src/lib/routing/AppRoutes.tsx +99 -0
  518. package/frontend/src/lib/routing/RequireAuth.tsx +27 -0
  519. package/frontend/src/lib/utils/cloudMessaging.ts +20 -0
  520. package/frontend/src/lib/utils/menuItems.ts +207 -0
  521. package/frontend/src/lib/utils/{validation-schemas.ts → schemaValidations.ts} +10 -5
  522. package/frontend/src/lib/utils/utils.ts +32 -1
  523. package/frontend/src/vite-env.d.ts +1 -0
  524. package/frontend/tsconfig.json +25 -25
  525. package/frontend/tsconfig.node.json +9 -9
  526. package/frontend/vite.config.ts +5 -3
  527. package/functions/deno.json +24 -24
  528. package/functions/server.ts +315 -290
  529. package/functions/worker-template.js +15 -4
  530. package/i18n/README.ar.md +130 -0
  531. package/i18n/README.de.md +130 -0
  532. package/i18n/README.es.md +154 -0
  533. package/i18n/README.fr.md +134 -0
  534. package/i18n/README.hi.md +129 -0
  535. package/i18n/README.ja.md +174 -0
  536. package/i18n/README.ko.md +137 -0
  537. package/i18n/README.pt-BR.md +131 -0
  538. package/i18n/README.ru.md +129 -0
  539. package/i18n/README.zh-CN.md +133 -0
  540. package/openapi/ai.yaml +715 -688
  541. package/openapi/auth.yaml +1244 -563
  542. package/openapi/email.yaml +158 -0
  543. package/openapi/functions.yaml +475 -475
  544. package/openapi/health.yaml +29 -29
  545. package/openapi/logs.yaml +223 -223
  546. package/openapi/metadata.yaml +177 -177
  547. package/openapi/realtime.yaml +699 -0
  548. package/openapi/records.yaml +381 -381
  549. package/openapi/secrets.yaml +370 -370
  550. package/openapi/storage.yaml +875 -875
  551. package/openapi/tables.yaml +463 -463
  552. package/package.json +97 -88
  553. package/shared-schemas/package.json +31 -31
  554. package/shared-schemas/src/ai-api.schema.ts +34 -58
  555. package/shared-schemas/src/ai.schema.ts +63 -54
  556. package/shared-schemas/src/auth-api.schema.ts +352 -193
  557. package/shared-schemas/src/auth.schema.ts +43 -7
  558. package/shared-schemas/src/cloud-events.schema.ts +57 -0
  559. package/shared-schemas/src/database-api.schema.ts +35 -4
  560. package/shared-schemas/src/database.schema.ts +40 -1
  561. package/shared-schemas/src/docs.schema.ts +26 -0
  562. package/shared-schemas/src/email-api.schema.ts +30 -0
  563. package/shared-schemas/src/index.ts +5 -0
  564. package/shared-schemas/src/logs-api.schema.ts +7 -1
  565. package/shared-schemas/src/logs.schema.ts +26 -0
  566. package/shared-schemas/src/metadata.schema.ts +18 -4
  567. package/shared-schemas/src/realtime-api.schema.ts +111 -0
  568. package/shared-schemas/src/realtime.schema.ts +143 -0
  569. package/shared-schemas/tsconfig.json +21 -21
  570. package/tsconfig.json +7 -7
  571. package/zeabur/README.md +13 -0
  572. package/zeabur/template.yml +1032 -0
  573. package/.github/workflows/deploy-aws.yml +0 -130
  574. package/backend/src/api/routes/agent.ts +0 -29
  575. package/backend/src/api/routes/auth.oauth.ts +0 -482
  576. package/backend/src/api/routes/auth.ts +0 -386
  577. package/backend/src/api/routes/docs.ts +0 -66
  578. package/backend/src/api/routes/functions.ts +0 -183
  579. package/backend/src/api/routes/openapi.ts +0 -82
  580. package/backend/src/api/routes/usage.ts +0 -96
  581. package/backend/src/core/ai/client.ts +0 -242
  582. package/backend/src/core/ai/model.ts +0 -117
  583. package/backend/src/core/auth/auth.ts +0 -780
  584. package/backend/src/core/database/manager.ts +0 -178
  585. package/backend/src/core/database/table.ts +0 -772
  586. package/backend/src/core/documentation/agent.ts +0 -689
  587. package/backend/src/core/documentation/openapi.ts +0 -856
  588. package/backend/src/core/logs/analytics.ts +0 -76
  589. package/backend/src/core/logs/providers/localdb.provider.ts +0 -246
  590. package/backend/src/core/socket/socket.ts +0 -388
  591. package/backend/src/core/storage/storage.ts +0 -923
  592. package/backend/src/utils/cloud-token.ts +0 -39
  593. package/backend/src/utils/helpers.ts +0 -49
  594. package/backend/src/utils/uuid.ts +0 -9
  595. package/backend/tests/manual/test-better-auth.sh +0 -303
  596. package/docker-init/db/logs.sql +0 -9
  597. package/frontend/README.md +0 -112
  598. package/frontend/src/components/datagrid/index.tsx +0 -20
  599. package/frontend/src/components/layout/CloudLayout.tsx +0 -95
  600. package/frontend/src/features/ai/components/AIConfigDialog.tsx +0 -76
  601. package/frontend/src/features/ai/components/AIConfigForm.tsx +0 -222
  602. package/frontend/src/features/ai/components/fields/ModalityField.tsx +0 -87
  603. package/frontend/src/features/ai/components/fields/ModelSelectionField.tsx +0 -134
  604. package/frontend/src/features/ai/components/fields/SystemPromptField.tsx +0 -33
  605. package/frontend/src/features/ai/page/AIPage.tsx +0 -178
  606. package/frontend/src/features/auth/components/AddOAuthDialog.tsx +0 -106
  607. package/frontend/src/features/auth/components/AuthMethodTab.tsx +0 -238
  608. package/frontend/src/features/auth/components/UsersTab.tsx +0 -114
  609. package/frontend/src/features/auth/page/AuthenticationPage.tsx +0 -169
  610. package/frontend/src/features/dashboard/page/DashboardPage.tsx +0 -194
  611. package/frontend/src/features/database/hooks/UseLinkModal.tsx +0 -78
  612. package/frontend/src/features/functions/components/FunctionViewer.tsx +0 -46
  613. package/frontend/src/features/functions/components/FunctionsContent.tsx +0 -88
  614. package/frontend/src/features/functions/page/FunctionsPage.tsx +0 -28
  615. package/frontend/src/features/login/components/AuthErrorBoundary.tsx +0 -87
  616. package/frontend/src/features/login/components/PrivateRoute.tsx +0 -24
  617. package/frontend/src/features/login/page/CloudLoginPage.tsx +0 -93
  618. package/frontend/src/features/logs/components/AnalyticsLogsTable.tsx +0 -313
  619. package/frontend/src/features/logs/components/LogsTable.tsx +0 -199
  620. package/frontend/src/features/logs/page/AnalyticsLogsPage.tsx +0 -530
  621. package/frontend/src/features/metadata/index.ts +0 -0
  622. package/frontend/src/features/metadata/page/MetadataPage.tsx +0 -136
  623. package/frontend/src/features/onboard/components/CompletionCard.tsx +0 -41
  624. package/frontend/src/features/onboard/components/OnboardButton.tsx +0 -84
  625. package/frontend/src/features/onboard/components/StepContent.tsx +0 -91
  626. package/frontend/src/features/onboard/components/TestConnectionStep.tsx +0 -53
  627. package/frontend/src/features/onboard/components/mcp/McpInstallation.tsx +0 -144
  628. package/frontend/src/features/onboard/page/OnBoardPage.tsx +0 -104
  629. package/frontend/src/features/onboard/types.ts +0 -8
  630. package/frontend/src/features/visualizer/page/VisualizerPage.tsx +0 -127
  631. package/frontend/src/lib/contexts/OnboardStepContext.tsx +0 -68
  632. package/frontend/src/lib/hooks/useOnboardingCompletion.ts +0 -29
  633. /package/backend/src/api/{middleware → middlewares}/error.ts +0 -0
  634. /package/backend/src/api/{middleware → middlewares}/upload.ts +0 -0
  635. /package/frontend/src/{features/metadata → lib}/services/metadata.service.ts +0 -0
@@ -1,923 +0,0 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import { fileURLToPath } from 'url';
4
- import { DatabaseManager } from '@/core/database/manager.js';
5
- import { StorageRecord, BucketRecord } from '@/types/storage.js';
6
- import {
7
- StorageFileSchema,
8
- UploadStrategyResponse,
9
- DownloadStrategyResponse,
10
- StorageMetadataSchema,
11
- } from '@insforge/shared-schemas';
12
- import {
13
- S3Client,
14
- PutObjectCommand,
15
- GetObjectCommand,
16
- DeleteObjectCommand,
17
- ListObjectsV2Command,
18
- DeleteObjectsCommand,
19
- HeadObjectCommand,
20
- } from '@aws-sdk/client-s3';
21
- import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
22
- import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
23
- import logger from '@/utils/logger.js';
24
- import { ADMIN_ID } from '@/utils/constants';
25
- import { AppError } from '@/api/middleware/error';
26
- import { ERROR_CODES } from '@/types/error-constants';
27
- import { escapeSqlLikePattern, escapeRegexPattern } from '@/utils/validations.js';
28
-
29
- const __filename = fileURLToPath(import.meta.url);
30
- const __dirname = path.dirname(__filename);
31
-
32
- // Storage backend interface
33
- interface StorageBackend {
34
- initialize(): void | Promise<void>;
35
- putObject(bucket: string, key: string, file: Express.Multer.File): Promise<void>;
36
- getObject(bucket: string, key: string): Promise<Buffer | null>;
37
- deleteObject(bucket: string, key: string): Promise<void>;
38
- createBucket(bucket: string): Promise<void>;
39
- deleteBucket(bucket: string): Promise<void>;
40
-
41
- // New methods for presigned URL support
42
- supportsPresignedUrls(): boolean;
43
- getUploadStrategy(
44
- bucket: string,
45
- key: string,
46
- metadata: { contentType?: string; size?: number }
47
- ): Promise<UploadStrategyResponse>;
48
- getDownloadStrategy(
49
- bucket: string,
50
- key: string,
51
- expiresIn?: number,
52
- isPublic?: boolean
53
- ): Promise<DownloadStrategyResponse>;
54
- verifyObjectExists(bucket: string, key: string): Promise<boolean>;
55
- }
56
-
57
- // Local filesystem storage implementation
58
- class LocalStorageBackend implements StorageBackend {
59
- constructor(private baseDir: string) {}
60
-
61
- async initialize(): Promise<void> {
62
- await fs.mkdir(this.baseDir, { recursive: true });
63
- }
64
-
65
- private getFilePath(bucket: string, key: string): string {
66
- return path.join(this.baseDir, bucket, key);
67
- }
68
-
69
- async putObject(bucket: string, key: string, file: Express.Multer.File): Promise<void> {
70
- const filePath = this.getFilePath(bucket, key);
71
- await fs.mkdir(path.dirname(filePath), { recursive: true });
72
- await fs.writeFile(filePath, file.buffer);
73
- }
74
-
75
- async getObject(bucket: string, key: string): Promise<Buffer | null> {
76
- try {
77
- const filePath = this.getFilePath(bucket, key);
78
- return await fs.readFile(filePath);
79
- } catch {
80
- return null;
81
- }
82
- }
83
-
84
- async deleteObject(bucket: string, key: string): Promise<void> {
85
- try {
86
- const filePath = this.getFilePath(bucket, key);
87
- await fs.unlink(filePath);
88
- } catch {
89
- // File might not exist, continue
90
- }
91
- }
92
-
93
- async createBucket(bucket: string): Promise<void> {
94
- const bucketPath = path.join(this.baseDir, bucket);
95
- await fs.mkdir(bucketPath, { recursive: true });
96
- }
97
-
98
- async deleteBucket(bucket: string): Promise<void> {
99
- try {
100
- await fs.rmdir(path.join(this.baseDir, bucket), { recursive: true });
101
- } catch {
102
- // Directory might not exist
103
- }
104
- }
105
-
106
- // Local storage doesn't support presigned URLs
107
- supportsPresignedUrls(): boolean {
108
- return false;
109
- }
110
-
111
- getUploadStrategy(
112
- bucket: string,
113
- key: string,
114
- _metadata: { contentType?: string; size?: number }
115
- ): Promise<UploadStrategyResponse> {
116
- // For local storage, return direct upload strategy with absolute URL
117
- const baseUrl = process.env.API_BASE_URL || 'http://localhost:7130';
118
- return Promise.resolve({
119
- method: 'direct',
120
- uploadUrl: `${baseUrl}/api/storage/buckets/${bucket}/objects/${encodeURIComponent(key)}`,
121
- key,
122
- confirmRequired: false,
123
- });
124
- }
125
-
126
- getDownloadStrategy(
127
- bucket: string,
128
- key: string,
129
- _expiresIn?: number,
130
- _isPublic?: boolean
131
- ): Promise<DownloadStrategyResponse> {
132
- // For local storage, return direct download URL with absolute URL
133
- const baseUrl = process.env.API_BASE_URL || 'http://localhost:7130';
134
- return Promise.resolve({
135
- method: 'direct',
136
- url: `${baseUrl}/api/storage/buckets/${bucket}/objects/${encodeURIComponent(key)}`,
137
- });
138
- }
139
-
140
- async verifyObjectExists(bucket: string, key: string): Promise<boolean> {
141
- // For local storage, check if file exists on disk
142
- try {
143
- const filePath = this.getFilePath(bucket, key);
144
- await fs.access(filePath);
145
- return true;
146
- } catch {
147
- // File doesn't exist
148
- return false;
149
- }
150
- }
151
- }
152
-
153
- // S3 storage implementation
154
- class S3StorageBackend implements StorageBackend {
155
- private s3Client: S3Client | null = null;
156
-
157
- constructor(
158
- private s3Bucket: string,
159
- private appKey: string,
160
- private region: string = 'us-east-2'
161
- ) {}
162
-
163
- initialize(): void {
164
- // On EC2: Use IAM roles attached to the instance for S3 permissions
165
- // The SDK will automatically use the instance's IAM role credentials
166
- // No explicit AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY needed, unless for testing
167
- this.s3Client = new S3Client({
168
- region: this.region,
169
- });
170
- }
171
-
172
- private getS3Key(bucket: string, key: string): string {
173
- return `${this.appKey}/${bucket}/${key}`;
174
- }
175
-
176
- async putObject(bucket: string, key: string, file: Express.Multer.File): Promise<void> {
177
- if (!this.s3Client) {
178
- throw new Error('S3 client not initialized');
179
- }
180
- const s3Key = this.getS3Key(bucket, key);
181
-
182
- const command = new PutObjectCommand({
183
- Bucket: this.s3Bucket,
184
- Key: s3Key,
185
- Body: file.buffer,
186
- ContentType: file.mimetype || 'application/octet-stream',
187
- });
188
-
189
- try {
190
- await this.s3Client.send(command);
191
- } catch (error) {
192
- logger.error('S3 Upload error', {
193
- error: error instanceof Error ? error.message : String(error),
194
- bucket,
195
- key: s3Key,
196
- });
197
- throw error;
198
- }
199
- }
200
-
201
- async getObject(bucket: string, key: string): Promise<Buffer | null> {
202
- if (!this.s3Client) {
203
- throw new Error('S3 client not initialized');
204
- }
205
- try {
206
- const command = new GetObjectCommand({
207
- Bucket: this.s3Bucket,
208
- Key: this.getS3Key(bucket, key),
209
- });
210
- const response = await this.s3Client.send(command);
211
- const chunks: Uint8Array[] = [];
212
- // Type assertion for readable stream
213
- const body = response.Body as AsyncIterable<Uint8Array>;
214
- for await (const chunk of body) {
215
- chunks.push(chunk);
216
- }
217
- return Buffer.concat(chunks);
218
- } catch {
219
- return null;
220
- }
221
- }
222
-
223
- async deleteObject(bucket: string, key: string): Promise<void> {
224
- if (!this.s3Client) {
225
- throw new Error('S3 client not initialized');
226
- }
227
- const command = new DeleteObjectCommand({
228
- Bucket: this.s3Bucket,
229
- Key: this.getS3Key(bucket, key),
230
- });
231
- await this.s3Client.send(command);
232
- }
233
-
234
- async createBucket(_bucket: string): Promise<void> {
235
- // In S3 with multi-tenant, we don't create actual buckets
236
- // We just use folders under the app key
237
- }
238
-
239
- async deleteBucket(bucket: string): Promise<void> {
240
- if (!this.s3Client) {
241
- throw new Error('S3 client not initialized');
242
- }
243
- // List and delete all objects in the "bucket" (folder)
244
- const prefix = `${this.appKey}/${bucket}/`;
245
-
246
- let continuationToken: string | undefined;
247
- do {
248
- const listCommand = new ListObjectsV2Command({
249
- Bucket: this.s3Bucket,
250
- Prefix: prefix,
251
- ContinuationToken: continuationToken,
252
- });
253
- const listResponse = await this.s3Client.send(listCommand);
254
-
255
- if (listResponse.Contents && listResponse.Contents.length > 0) {
256
- const deleteCommand = new DeleteObjectsCommand({
257
- Bucket: this.s3Bucket,
258
- Delete: {
259
- Objects: listResponse.Contents.filter((obj) => obj.Key !== undefined).map((obj) => ({
260
- Key: obj.Key as string,
261
- })),
262
- },
263
- });
264
- await this.s3Client.send(deleteCommand);
265
- }
266
-
267
- continuationToken = listResponse.NextContinuationToken;
268
- } while (continuationToken);
269
- }
270
-
271
- // S3 supports presigned URLs
272
- supportsPresignedUrls(): boolean {
273
- return true;
274
- }
275
-
276
- async getUploadStrategy(
277
- bucket: string,
278
- key: string,
279
- metadata: { contentType?: string; size?: number }
280
- ): Promise<UploadStrategyResponse> {
281
- if (!this.s3Client) {
282
- throw new Error('S3 client not initialized');
283
- }
284
-
285
- const s3Key = this.getS3Key(bucket, key);
286
- const expiresIn = 3600; // 1 hour
287
-
288
- try {
289
- // Generate presigned POST URL for multipart form upload
290
- const { url, fields } = await createPresignedPost(this.s3Client, {
291
- Bucket: this.s3Bucket,
292
- Key: s3Key,
293
- Conditions: [
294
- ['content-length-range', 0, metadata.size || 10485760], // Max 10MB by default
295
- ],
296
- Expires: expiresIn,
297
- });
298
-
299
- return {
300
- method: 'presigned',
301
- uploadUrl: url,
302
- fields,
303
- key,
304
- confirmRequired: true,
305
- confirmUrl: `/api/storage/buckets/${bucket}/objects/${encodeURIComponent(key)}/confirm-upload`,
306
- expiresAt: new Date(Date.now() + expiresIn * 1000),
307
- };
308
- } catch (error) {
309
- logger.error('Failed to generate presigned upload URL', {
310
- error: error instanceof Error ? error.message : String(error),
311
- bucket,
312
- key,
313
- });
314
- throw error;
315
- }
316
- }
317
-
318
- async getDownloadStrategy(
319
- bucket: string,
320
- key: string,
321
- expiresIn: number = 3600,
322
- isPublic: boolean = false
323
- ): Promise<DownloadStrategyResponse> {
324
- if (!this.s3Client) {
325
- throw new Error('S3 client not initialized');
326
- }
327
-
328
- const s3Key = this.getS3Key(bucket, key);
329
-
330
- try {
331
- // Note: isPublic here refers to the application-level setting,
332
- // not the actual S3 bucket policy. In a multi-tenant setup,
333
- // we're using a single S3 bucket with folder-based isolation,
334
- // so we always use presigned URLs for security.
335
- // The "public" setting only affects the URL expiration time.
336
-
337
- // Always generate presigned URL for security in multi-tenant environment
338
- const command = new GetObjectCommand({
339
- Bucket: this.s3Bucket,
340
- Key: s3Key,
341
- });
342
-
343
- // Public files get longer expiration (7 days), private files get shorter (1 hour default)
344
- const actualExpiresIn = isPublic ? 604800 : expiresIn; // 604800 = 7 days
345
- const url = await getSignedUrl(this.s3Client, command, { expiresIn: actualExpiresIn });
346
-
347
- return {
348
- method: 'presigned',
349
- url,
350
- expiresAt: new Date(Date.now() + actualExpiresIn * 1000),
351
- };
352
- } catch (error) {
353
- logger.error('Failed to generate download URL', {
354
- error: error instanceof Error ? error.message : String(error),
355
- bucket,
356
- key,
357
- });
358
- throw error;
359
- }
360
- }
361
-
362
- async verifyObjectExists(bucket: string, key: string): Promise<boolean> {
363
- if (!this.s3Client) {
364
- throw new Error('S3 client not initialized');
365
- }
366
-
367
- const s3Key = this.getS3Key(bucket, key);
368
-
369
- try {
370
- const command = new HeadObjectCommand({
371
- Bucket: this.s3Bucket,
372
- Key: s3Key,
373
- });
374
- await this.s3Client.send(command);
375
- return true;
376
- } catch {
377
- return false;
378
- }
379
- }
380
- }
381
-
382
- export class StorageService {
383
- private static instance: StorageService;
384
- private backend: StorageBackend;
385
-
386
- private constructor() {
387
- const s3Bucket = process.env.AWS_S3_BUCKET;
388
- const appKey = process.env.APP_KEY;
389
-
390
- if (s3Bucket) {
391
- // Use S3 backend
392
- if (!appKey) {
393
- throw new Error('APP_KEY is required when using S3 storage');
394
- }
395
- this.backend = new S3StorageBackend(s3Bucket, appKey, process.env.AWS_REGION || 'us-east-2');
396
- } else {
397
- // Use local filesystem backend
398
- const baseDir = process.env.STORAGE_DIR || path.join(__dirname, '../../data/storage');
399
- this.backend = new LocalStorageBackend(baseDir);
400
- }
401
- }
402
-
403
- static getInstance(): StorageService {
404
- if (!StorageService.instance) {
405
- StorageService.instance = new StorageService();
406
- }
407
- return StorageService.instance;
408
- }
409
-
410
- async initialize(): Promise<void> {
411
- await this.backend.initialize();
412
- }
413
-
414
- private validateBucketName(bucket: string): void {
415
- // Simple validation: alphanumeric, hyphens, underscores
416
- if (!/^[a-zA-Z0-9_-]+$/.test(bucket)) {
417
- throw new Error('Invalid bucket name. Use only letters, numbers, hyphens, and underscores.');
418
- }
419
- }
420
-
421
- private validateKey(key: string): void {
422
- // Prevent directory traversal
423
- if (key.includes('..') || key.startsWith('/')) {
424
- throw new Error('Invalid key. Cannot use ".." or start with "/"');
425
- }
426
- }
427
-
428
- async putObject(
429
- bucket: string,
430
- originalKey: string,
431
- file: Express.Multer.File,
432
- userId?: string
433
- ): Promise<StorageFileSchema> {
434
- this.validateBucketName(bucket);
435
- this.validateKey(originalKey);
436
-
437
- const db = DatabaseManager.getInstance().getDb();
438
-
439
- // Parse filename and extension for potential auto-renaming
440
- const lastDotIndex = originalKey.lastIndexOf('.');
441
- const baseName = lastDotIndex > 0 ? originalKey.substring(0, lastDotIndex) : originalKey;
442
- const extension = lastDotIndex > 0 ? originalKey.substring(lastDotIndex) : '';
443
-
444
- // Use efficient SQL query to find the highest existing counter
445
- // This query finds all files matching the pattern and extracts the counter number
446
- const existingFiles = await db
447
- .prepare(
448
- `
449
- SELECT key FROM _storage
450
- WHERE bucket = ?
451
- AND (key = ? OR key LIKE ?)
452
- `
453
- )
454
- .all(
455
- bucket,
456
- originalKey,
457
- `${escapeSqlLikePattern(baseName)} (%)${escapeSqlLikePattern(extension)}`
458
- );
459
-
460
- let finalKey = originalKey;
461
-
462
- if (existingFiles.length > 0) {
463
- // Extract counter numbers from existing files
464
- let incrementNumber = 0;
465
- // This regex is used to match the counter number in the filename, extract the increment number
466
- const counterRegex = new RegExp(
467
- `^${escapeRegexPattern(baseName)} \\((\\d+)\\)${escapeRegexPattern(extension)}$`
468
- );
469
-
470
- for (const file of existingFiles as { key: string }[]) {
471
- if (file.key === originalKey) {
472
- incrementNumber = Math.max(incrementNumber, 0); // Original file exists, so we need at least (1)
473
- } else {
474
- const match = file.key.match(counterRegex);
475
- if (match) {
476
- incrementNumber = Math.max(incrementNumber, parseInt(match[1], 10));
477
- }
478
- }
479
- }
480
-
481
- // Generate the next available filename
482
- finalKey = `${baseName} (${incrementNumber + 1})${extension}`;
483
- }
484
-
485
- // Save file using backend
486
- await this.backend.putObject(bucket, finalKey, file);
487
-
488
- // Save metadata to database
489
- await db
490
- .prepare(
491
- `
492
- INSERT INTO _storage (bucket, key, size, mime_type, uploaded_by)
493
- VALUES (?, ?, ?, ?, ?)
494
- `
495
- )
496
- .run(
497
- bucket,
498
- finalKey,
499
- file.size,
500
- file.mimetype || null,
501
- userId && userId !== ADMIN_ID ? userId : null
502
- );
503
-
504
- // Get the actual uploaded_at timestamp from database (with alias for camelCase)
505
- const result = (await db
506
- .prepare('SELECT uploaded_at as uploadedAt FROM _storage WHERE bucket = ? AND key = ?')
507
- .get(bucket, finalKey)) as { uploadedAt: string } | undefined;
508
-
509
- if (!result) {
510
- throw new Error(`Failed to retrieve upload timestamp for ${bucket}/${finalKey}`);
511
- }
512
-
513
- return {
514
- bucket,
515
- key: finalKey,
516
- size: file.size,
517
- mimeType: file.mimetype,
518
- uploadedAt: result.uploadedAt,
519
- url: `${process.env.API_BASE_URL || 'http://localhost:7130'}/api/storage/buckets/${bucket}/objects/${encodeURIComponent(finalKey)}`,
520
- };
521
- }
522
-
523
- async getObject(
524
- bucket: string,
525
- key: string
526
- ): Promise<{ file: Buffer; metadata: StorageFileSchema } | null> {
527
- this.validateBucketName(bucket);
528
- this.validateKey(key);
529
-
530
- const db = DatabaseManager.getInstance().getDb();
531
-
532
- const metadata = (await db
533
- .prepare('SELECT * FROM _storage WHERE bucket = ? AND key = ?')
534
- .get(bucket, key)) as StorageRecord | undefined;
535
-
536
- if (!metadata) {
537
- return null;
538
- }
539
-
540
- const file = await this.backend.getObject(bucket, key);
541
- if (!file) {
542
- return null;
543
- }
544
-
545
- return {
546
- file,
547
- metadata: {
548
- key: metadata.key,
549
- bucket: metadata.bucket,
550
- size: metadata.size,
551
- mimeType: metadata.mime_type,
552
- uploadedAt: metadata.uploaded_at,
553
- url: `${process.env.API_BASE_URL || 'http://localhost:7130'}/api/storage/buckets/${bucket}/objects/${encodeURIComponent(key)}`,
554
- },
555
- };
556
- }
557
-
558
- async deleteObject(bucket: string, key: string, userId?: string): Promise<boolean> {
559
- this.validateBucketName(bucket);
560
- this.validateKey(key);
561
-
562
- const db = DatabaseManager.getInstance().getDb();
563
-
564
- // Check permissions if userId is provided
565
- if (userId && userId !== ADMIN_ID) {
566
- const file = (await db
567
- .prepare('SELECT uploaded_by FROM _storage WHERE bucket = ? AND key = ?')
568
- .get(bucket, key)) as { uploaded_by: string | null } | undefined;
569
-
570
- if (!file) {
571
- return false; // File doesn't exist
572
- }
573
-
574
- // Check if user owns the file
575
- if (file.uploaded_by !== userId) {
576
- throw new AppError(
577
- 'Permission denied: You can only delete files you uploaded',
578
- 403,
579
- ERROR_CODES.FORBIDDEN
580
- );
581
- }
582
- }
583
-
584
- // Delete file using backend
585
- await this.backend.deleteObject(bucket, key);
586
-
587
- // Delete from database
588
- const result = await db
589
- .prepare('DELETE FROM _storage WHERE bucket = ? AND key = ?')
590
- .run(bucket, key);
591
-
592
- return result.changes > 0;
593
- }
594
-
595
- async listObjects(
596
- bucket: string,
597
- prefix?: string,
598
- limit: number = 100,
599
- offset: number = 0,
600
- searchQuery?: string
601
- ): Promise<{ objects: StorageFileSchema[]; total: number }> {
602
- this.validateBucketName(bucket);
603
-
604
- const db = DatabaseManager.getInstance().getDb();
605
-
606
- let query = 'SELECT * FROM _storage WHERE bucket = ?';
607
- let countQuery = 'SELECT COUNT(*) as count FROM _storage WHERE bucket = ?';
608
- const params: (string | number)[] = [bucket];
609
-
610
- if (prefix) {
611
- query += ' AND key LIKE ?';
612
- countQuery += ' AND key LIKE ?';
613
- params.push(`${prefix}%`);
614
- }
615
-
616
- // Add search functionality for file names (key field)
617
- if (searchQuery && searchQuery.trim()) {
618
- query += ' AND key LIKE ?';
619
- countQuery += ' AND key LIKE ?';
620
- const searchPattern = `%${searchQuery.trim()}%`;
621
- params.push(searchPattern);
622
- }
623
-
624
- query += ' ORDER BY key LIMIT ? OFFSET ?';
625
- const queryParams = [...params, limit, offset];
626
-
627
- const objects = await db.prepare(query).all(...queryParams);
628
- const total = ((await db.prepare(countQuery).get(...params)) as { count: number }).count;
629
-
630
- return {
631
- objects: objects.map((obj) => ({
632
- ...obj,
633
- mimeType: obj.mime_type,
634
- uploadedAt: obj.uploaded_at,
635
- url: `${process.env.API_BASE_URL || 'http://localhost:7130'}/api/storage/buckets/${bucket}/objects/${encodeURIComponent(obj.key)}`,
636
- })),
637
- total,
638
- };
639
- }
640
-
641
- async isBucketPublic(bucket: string): Promise<boolean> {
642
- const db = DatabaseManager.getInstance().getDb();
643
- const result = (await db
644
- .prepare('SELECT public FROM _storage_buckets WHERE name = ?')
645
- .get(bucket)) as Pick<BucketRecord, 'public'> | undefined;
646
- return result?.public || false;
647
- }
648
-
649
- async updateBucketVisibility(bucket: string, isPublic: boolean): Promise<void> {
650
- const db = DatabaseManager.getInstance().getDb();
651
-
652
- // Check if bucket exists
653
- const bucketExists = await db
654
- .prepare('SELECT name FROM _storage_buckets WHERE name = ?')
655
- .get(bucket);
656
-
657
- if (!bucketExists) {
658
- throw new Error(`Bucket "${bucket}" does not exist`);
659
- }
660
-
661
- // Update bucket visibility in _storage_buckets table
662
- await db
663
- .prepare(
664
- 'UPDATE _storage_buckets SET public = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?'
665
- )
666
- .run(isPublic, bucket);
667
-
668
- // Update storage metadata
669
- // Metadata is now updated on-demand
670
- }
671
-
672
- async listBuckets(): Promise<string[]> {
673
- const db = DatabaseManager.getInstance().getDb();
674
-
675
- // Get all buckets from _storage_buckets table
676
- const buckets = (await db
677
- .prepare('SELECT name FROM _storage_buckets ORDER BY name')
678
- .all()) as Pick<BucketRecord, 'name'>[];
679
-
680
- return buckets.map((b) => b.name);
681
- }
682
-
683
- async createBucket(bucket: string, isPublic: boolean = true): Promise<void> {
684
- this.validateBucketName(bucket);
685
-
686
- const db = DatabaseManager.getInstance().getDb();
687
-
688
- // Check if bucket already exists
689
- const existing = await db
690
- .prepare('SELECT name FROM _storage_buckets WHERE name = ?')
691
- .get(bucket);
692
-
693
- if (existing) {
694
- throw new Error(`Bucket "${bucket}" already exists`);
695
- }
696
-
697
- // Insert bucket into _storage_buckets table
698
- await db
699
- .prepare('INSERT INTO _storage_buckets (name, public) VALUES (?, ?)')
700
- .run(bucket, isPublic);
701
-
702
- // Create bucket using backend
703
- await this.backend.createBucket(bucket);
704
-
705
- // Update storage metadata
706
- // Metadata is now updated on-demand
707
- }
708
-
709
- async deleteBucket(bucket: string): Promise<boolean> {
710
- this.validateBucketName(bucket);
711
-
712
- const db = DatabaseManager.getInstance().getDb();
713
-
714
- // Check if bucket exists
715
- const bucketExists = await db
716
- .prepare('SELECT name FROM _storage_buckets WHERE name = ?')
717
- .get(bucket);
718
-
719
- if (!bucketExists) {
720
- return false;
721
- }
722
-
723
- // Delete bucket using backend (handles all files)
724
- await this.backend.deleteBucket(bucket);
725
-
726
- // Delete from storage table (cascade will handle _storage entries)
727
- await db.prepare('DELETE FROM _storage_buckets WHERE name = ?').run(bucket);
728
-
729
- // Update storage metadata
730
- // Metadata is now updated on-demand
731
-
732
- return true;
733
- }
734
-
735
- // New methods for universal upload/download strategies
736
- private generateUniqueKey(filename: string): string {
737
- const timestamp = Date.now();
738
- const randomStr = Math.random().toString(36).substring(2, 8);
739
- const ext = path.extname(filename);
740
- const baseName = path.basename(filename, ext);
741
- const sanitizedBaseName = baseName.replace(/[^a-zA-Z0-9-_]/g, '-').substring(0, 32);
742
- return `${sanitizedBaseName}-${timestamp}-${randomStr}${ext}`;
743
- }
744
-
745
- async getUploadStrategy(
746
- bucket: string,
747
- metadata: {
748
- filename: string;
749
- contentType?: string;
750
- size?: number;
751
- }
752
- ): Promise<UploadStrategyResponse> {
753
- this.validateBucketName(bucket);
754
-
755
- // Check if bucket exists
756
- const db = DatabaseManager.getInstance().getDb();
757
- const bucketExists = await db
758
- .prepare('SELECT name FROM _storage_buckets WHERE name = ?')
759
- .get(bucket);
760
-
761
- if (!bucketExists) {
762
- throw new Error(`Bucket "${bucket}" does not exist`);
763
- }
764
-
765
- const key = this.generateUniqueKey(metadata.filename);
766
- return this.backend.getUploadStrategy(bucket, key, metadata);
767
- }
768
-
769
- async getDownloadStrategy(
770
- bucket: string,
771
- key: string,
772
- expiresIn?: number
773
- ): Promise<DownloadStrategyResponse> {
774
- this.validateBucketName(bucket);
775
- this.validateKey(key);
776
-
777
- // Check if bucket is public
778
- const isPublic = await this.isBucketPublic(bucket);
779
-
780
- return this.backend.getDownloadStrategy(bucket, key, expiresIn, isPublic);
781
- }
782
-
783
- async confirmUpload(
784
- bucket: string,
785
- key: string,
786
- metadata: {
787
- size: number;
788
- contentType?: string;
789
- etag?: string;
790
- },
791
- userId?: string
792
- ): Promise<StorageFileSchema> {
793
- this.validateBucketName(bucket);
794
- this.validateKey(key);
795
-
796
- // Verify the file exists in storage
797
- const exists = await this.backend.verifyObjectExists(bucket, key);
798
- if (!exists) {
799
- throw new Error(`Upload not found for key "${key}" in bucket "${bucket}"`);
800
- }
801
-
802
- const db = DatabaseManager.getInstance().getDb();
803
-
804
- // Check if already confirmed
805
- const existing = await db
806
- .prepare('SELECT key FROM _storage WHERE bucket = ? AND key = ?')
807
- .get(bucket, key);
808
-
809
- if (existing) {
810
- throw new Error(`File "${key}" already confirmed in bucket "${bucket}"`);
811
- }
812
-
813
- // Save metadata to database
814
- await db
815
- .prepare(
816
- `
817
- INSERT INTO _storage (bucket, key, size, mime_type, uploaded_by)
818
- VALUES (?, ?, ?, ?, ?)
819
- `
820
- )
821
- .run(
822
- bucket,
823
- key,
824
- metadata.size,
825
- metadata.contentType || null,
826
- userId && userId !== ADMIN_ID ? userId : null
827
- );
828
-
829
- // Get the actual uploaded_at timestamp from database
830
- const result = (await db
831
- .prepare('SELECT uploaded_at as uploadedAt FROM _storage WHERE bucket = ? AND key = ?')
832
- .get(bucket, key)) as { uploadedAt: string } | undefined;
833
-
834
- if (!result) {
835
- throw new Error(`Failed to retrieve upload timestamp for ${bucket}/${key}`);
836
- }
837
-
838
- return {
839
- bucket,
840
- key,
841
- size: metadata.size,
842
- mimeType: metadata.contentType,
843
- uploadedAt: result.uploadedAt,
844
- url: `${process.env.API_BASE_URL || 'http://localhost:7130'}/api/storage/buckets/${bucket}/objects/${encodeURIComponent(key)}`,
845
- };
846
- }
847
-
848
- /**
849
- * Get storage metadata
850
- */
851
- async getMetadata(): Promise<StorageMetadataSchema> {
852
- const db = DatabaseManager.getInstance().getDb();
853
- // Get storage buckets from _storage_buckets table
854
- const storageBuckets = (await db
855
- .prepare('SELECT name, public, created_at FROM _storage_buckets ORDER BY name')
856
- .all()) as { name: string; public: boolean; created_at: string }[];
857
-
858
- const bucketsMetadata = storageBuckets.map((b) => ({
859
- name: b.name,
860
- public: b.public,
861
- createdAt: b.created_at,
862
- }));
863
-
864
- // Get object counts for each bucket
865
- const bucketsObjectCountMap = await this.getBucketsObjectCount();
866
- const storageSize = await this.getStorageSizeInGB();
867
-
868
- return {
869
- buckets: bucketsMetadata.map((bucket) => ({
870
- ...bucket,
871
- objectCount: bucketsObjectCountMap.get(bucket.name) ?? 0,
872
- })),
873
- totalSize: storageSize,
874
- };
875
- }
876
-
877
- private async getBucketsObjectCount(): Promise<Map<string, number>> {
878
- const db = DatabaseManager.getInstance().getDb();
879
- try {
880
- // Query to get object count for each bucket
881
- const bucketCounts = (await db
882
- .prepare('SELECT bucket, COUNT(*) as count FROM _storage GROUP BY bucket')
883
- .all()) as { bucket: string; count: number }[];
884
-
885
- // Convert to Map for easy lookup
886
- const countMap = new Map<string, number>();
887
- bucketCounts.forEach((row) => {
888
- countMap.set(row.bucket, row.count);
889
- });
890
-
891
- return countMap;
892
- } catch (error) {
893
- logger.error('Error getting bucket object counts', {
894
- error: error instanceof Error ? error.message : String(error),
895
- });
896
- // Return empty map on error
897
- return new Map<string, number>();
898
- }
899
- }
900
-
901
- private async getStorageSizeInGB(): Promise<number> {
902
- const db = DatabaseManager.getInstance().getDb();
903
- try {
904
- // Query the _storage table to sum all file sizes
905
- const result = (await db
906
- .prepare(
907
- `
908
- SELECT COALESCE(SUM(size), 0) as total_size
909
- FROM _storage
910
- `
911
- )
912
- .get()) as { total_size: number } | null;
913
-
914
- // Convert bytes to GB
915
- return (result?.total_size || 0) / (1024 * 1024 * 1024);
916
- } catch (error) {
917
- logger.error('Error getting storage size', {
918
- error: error instanceof Error ? error.message : String(error),
919
- });
920
- return 0;
921
- }
922
- }
923
- }