insforge 0.3.2 → 1.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +20 -0
- package/.cursor/rules/cursor-rules.mdc +94 -0
- package/.dockerignore +3 -0
- package/.env.example +33 -4
- package/.github/ISSUE_TEMPLATE/bug_report.yml +13 -60
- package/.github/ISSUE_TEMPLATE/config.yml +2 -2
- package/.github/ISSUE_TEMPLATE/feature_request.yml +10 -63
- package/.github/PULL_REQUEST_TEMPLATE.md +7 -0
- package/.github/workflows/build-image.yml +2 -1
- package/.github/workflows/e2e.yml +63 -0
- package/CHANGELOG.md +41 -0
- package/CLAUDE_PLUGIN.md +104 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +1 -1
- package/Dockerfile +4 -1
- package/README.md +66 -18
- package/assets/mcpInstallv2.png +0 -0
- package/assets/sampleResponse.png +0 -0
- package/auth/index.html +13 -0
- package/auth/package.json +28 -0
- package/auth/public/favicon.ico +0 -0
- package/auth/src/App.tsx +33 -0
- package/auth/src/components/ErrorCard.tsx +37 -0
- package/auth/src/components/Layout.tsx +13 -0
- package/auth/src/index.css +19 -0
- package/auth/src/lib/broadcastService.ts +115 -0
- package/auth/src/lib/utils.ts +11 -0
- package/auth/src/main.tsx +22 -0
- package/auth/src/pages/ForgotPasswordPage.tsx +11 -0
- package/auth/src/pages/ResetPasswordPage.tsx +11 -0
- package/auth/src/pages/SignInPage.tsx +57 -0
- package/auth/src/pages/SignUpPage.tsx +57 -0
- package/auth/src/pages/VerifyEmailPage.tsx +20 -0
- package/auth/src/vite-env.d.ts +10 -0
- package/auth/tsconfig.json +32 -0
- package/auth/tsconfig.node.json +11 -0
- package/auth/vite.config.ts +25 -0
- package/backend/package.json +9 -9
- package/backend/src/api/{middleware → middlewares}/auth.ts +8 -9
- package/backend/src/api/middlewares/rate-limiters.ts +127 -0
- package/backend/src/api/routes/{ai.ts → ai/index.routes.ts} +20 -24
- package/backend/src/api/routes/auth/index.routes.ts +570 -0
- package/backend/src/api/routes/auth/oauth.routes.ts +448 -0
- package/backend/src/api/routes/{database.advance.ts → database/advance.routes.ts} +107 -65
- package/backend/src/api/routes/database/index.routes.ts +13 -0
- package/backend/src/api/routes/{database.records.ts → database/records.routes.ts} +22 -8
- package/backend/src/api/routes/{database.tables.ts → database/tables.routes.ts} +20 -23
- package/backend/src/api/routes/docs/index.routes.ts +76 -0
- package/backend/src/api/routes/functions/index.routes.ts +188 -0
- package/backend/src/api/routes/{logs.ts → logs/index.routes.ts} +25 -30
- package/backend/src/api/routes/{metadata.ts → metadata/index.routes.ts} +21 -31
- package/backend/src/api/routes/{secrets.ts → secrets/index.routes.ts} +27 -22
- package/backend/src/api/routes/{storage.ts → storage/index.routes.ts} +34 -53
- package/backend/src/api/routes/usage/index.routes.ts +89 -0
- package/backend/src/infra/config/app.config.ts +51 -0
- package/backend/src/{core/database/manager.ts → infra/database/database.manager.ts} +76 -85
- package/backend/src/infra/database/migrations/013_create-auth-schema-functions.sql +44 -0
- package/backend/src/infra/database/migrations/014_add-updated-at-trigger-user-table.sql +8 -0
- package/backend/src/infra/database/migrations/015_create-auth-config-and-email-otp-tables.sql +60 -0
- package/backend/src/infra/database/migrations/016_update-auth-config-and-email-otp.sql +24 -0
- package/backend/src/{core/secrets/encryption.ts → infra/security/encryption.manager.ts} +3 -2
- package/backend/src/infra/security/token.manager.ts +125 -0
- package/backend/src/{core/socket/socket.ts → infra/socket/socket.manager.ts} +15 -15
- package/backend/src/providers/ai/openrouter.provider.ts +377 -0
- package/backend/src/providers/email/base.provider.ts +41 -0
- package/backend/src/providers/email/cloud.provider.ts +187 -0
- package/backend/src/{core/logs/providers → providers/logs}/base.provider.ts +11 -11
- package/backend/src/{core/logs/providers → providers/logs}/cloudwatch.provider.ts +61 -38
- package/backend/src/providers/logs/local.provider.ts +185 -0
- package/backend/src/providers/oauth/base.provider.ts +29 -0
- package/backend/src/providers/oauth/discord.provider.ts +195 -0
- package/backend/src/providers/oauth/facebook.provider.ts +194 -0
- package/backend/src/providers/oauth/github.provider.ts +208 -0
- package/backend/src/providers/oauth/google.provider.ts +249 -0
- package/backend/src/providers/oauth/index.ts +7 -0
- package/backend/src/providers/oauth/linkedin.provider.ts +240 -0
- package/backend/src/providers/oauth/microsoft.provider.ts +169 -0
- package/backend/src/providers/oauth/x.provider.ts +202 -0
- package/backend/src/providers/storage/base.provider.ts +29 -0
- package/backend/src/providers/storage/local.provider.ts +103 -0
- package/backend/src/providers/storage/s3.provider.ts +313 -0
- package/backend/src/server.ts +70 -74
- package/backend/src/{core/ai/config.ts → services/ai/ai-config.service.ts} +19 -24
- package/backend/src/services/ai/ai-model.service.ts +60 -0
- package/backend/src/{core/ai/usage.ts → services/ai/ai-usage.service.ts} +28 -35
- package/backend/src/{core/ai/chat.ts → services/ai/chat-completion.service.ts} +37 -24
- package/backend/src/services/ai/helpers.ts +64 -0
- package/backend/src/{core/ai/image.ts → services/ai/image-generation.service.ts} +17 -19
- package/backend/src/services/ai/index.ts +13 -0
- package/backend/src/services/auth/auth-config.service.ts +250 -0
- package/backend/src/services/auth/auth-otp.service.ts +424 -0
- package/backend/src/services/auth/auth.service.ts +1136 -0
- package/backend/src/services/auth/index.ts +4 -0
- package/backend/src/{core/auth/oauth.ts → services/auth/oauth-config.service.ts} +106 -52
- package/backend/src/{core/database/advance.ts → services/database/database-advance.service.ts} +97 -131
- package/backend/src/services/database/database-table.service.ts +811 -0
- package/backend/src/services/email/email.service.ts +75 -0
- package/backend/src/{core/functions/functions.ts → services/functions/function.service.ts} +95 -88
- package/backend/src/{core/logs/audit.ts → services/logs/audit.service.ts} +92 -75
- package/backend/src/services/logs/log.service.ts +73 -0
- package/backend/src/{core/secrets/secrets.ts → services/secrets/secret.service.ts} +48 -66
- package/backend/src/services/storage/storage.service.ts +617 -0
- package/backend/src/services/usage/usage.service.ts +149 -0
- package/backend/src/types/auth.ts +66 -2
- package/backend/src/types/email.ts +8 -0
- package/backend/src/types/error-constants.ts +4 -0
- package/backend/src/types/logs.ts +0 -29
- package/backend/src/{core/socket/types.ts → types/socket.ts} +5 -6
- package/backend/src/utils/environment.ts +9 -3
- package/backend/src/utils/logger.ts +20 -2
- package/backend/src/utils/seed.ts +150 -57
- package/backend/src/utils/sql-parser.ts +1 -1
- package/backend/src/utils/utils.ts +114 -0
- package/backend/src/utils/validations.ts +40 -4
- package/backend/tests/local/test-ai-config.sh +129 -0
- package/backend/tests/local/test-ai-usage.sh +80 -0
- package/backend/tests/local/test-auth-router.sh +1 -1
- package/backend/tests/local/test-e2e.sh +1 -1
- package/backend/tests/local/test-functions.sh +123 -0
- package/backend/tests/local/test-logs.sh +132 -0
- package/backend/tests/local/test-public-bucket.sh +3 -3
- package/backend/tests/local/test-secrets.sh +14 -12
- package/backend/tests/local/test-traditional-rest.sh +2 -2
- package/backend/tests/manual/test-rawsql-modes.sh +244 -0
- package/backend/tests/test-config.sh +37 -1
- package/backend/tests/unit/cloud-token.test.ts +48 -0
- package/backend/tests/unit/constant.test.ts +8 -0
- package/backend/tests/unit/email.test.ts +372 -0
- package/backend/tests/unit/environment.test.ts +59 -0
- package/backend/tests/unit/helpers.test.ts +63 -0
- package/backend/tests/unit/logger.test.ts +22 -0
- package/backend/tests/unit/rate-limit.test.ts +154 -0
- package/backend/tests/unit/response.test.ts +58 -0
- package/backend/tests/unit/sql-parser.test.ts +74 -0
- package/backend/tests/unit/uuid.test.ts +21 -0
- package/backend/tests/unit/validations.test.ts +80 -0
- package/backend/tsconfig.json +1 -1
- package/backend/vitest.config.ts +11 -0
- package/claude-plugin/.claude-plugin/plugin.json +24 -0
- package/claude-plugin/README.md +133 -0
- package/claude-plugin/skills/insforge-schema-patterns/SKILL.md +270 -0
- package/docker-compose.prod.yml +60 -4
- package/docker-compose.yml +65 -4
- package/docker-init/db/db-init.sql +6 -34
- package/docker-init/logs/vector.yml +236 -0
- package/docs/README.md +44 -0
- package/docs/changelog.mdx +67 -0
- package/docs/core-concepts/ai/architecture.mdx +373 -0
- package/docs/core-concepts/ai/sdk.mdx +213 -0
- package/docs/core-concepts/authentication/architecture.mdx +278 -0
- package/docs/core-concepts/authentication/sdk.mdx +414 -0
- package/docs/core-concepts/authentication/ui-components/customization.mdx +529 -0
- package/docs/core-concepts/authentication/ui-components/nextjs.mdx +221 -0
- package/docs/core-concepts/authentication/ui-components/react-router.mdx +184 -0
- package/docs/core-concepts/authentication/ui-components/react.mdx +129 -0
- package/docs/core-concepts/database/architecture.mdx +256 -0
- package/docs/core-concepts/database/sdk.mdx +382 -0
- package/docs/core-concepts/functions/architecture.mdx +105 -0
- package/docs/core-concepts/functions/sdk.mdx +184 -0
- package/docs/core-concepts/storage/architecture.mdx +243 -0
- package/docs/core-concepts/storage/sdk.mdx +253 -0
- package/docs/deployment/README.md +94 -0
- package/docs/deployment/deploy-to-aws-ec2.md +565 -0
- package/docs/deployment/deploy-to-azure-virtual-machines.md +313 -0
- package/docs/deployment/deploy-to-google-cloud-compute-engine.md +613 -0
- package/docs/deployment/deploy-to-render.md +441 -0
- package/docs/docs.json +210 -0
- package/docs/examples/framework-guides/nextjs.mdx +131 -0
- package/docs/examples/framework-guides/nuxt.mdx +165 -0
- package/docs/examples/framework-guides/react.mdx +165 -0
- package/docs/examples/framework-guides/svelte.mdx +153 -0
- package/docs/examples/framework-guides/vue.mdx +159 -0
- package/docs/examples/overview.mdx +67 -0
- package/docs/favicon.svg +19 -0
- package/docs/images/changelog/nov-2025/auth-components.webp +0 -0
- package/docs/images/changelog/nov-2025/database-metadata.webp +0 -0
- package/docs/images/changelog/nov-2025/quickstart-prompts.webp +0 -0
- package/docs/images/changelog/nov-2025/sql-editor.webp +0 -0
- package/docs/images/changelog/nov-2025/usage-page.webp +0 -0
- package/docs/images/changelog/october-2025/csv-upload.webp +0 -0
- package/docs/images/changelog/october-2025/logs-feature.webp +0 -0
- package/docs/images/changelog/october-2025/oauth-providers.webp +0 -0
- package/docs/images/checks-passed.png +0 -0
- package/docs/images/dashboard-connect-expanded.png +0 -0
- package/docs/images/dashboard-connect.png +0 -0
- package/docs/images/hero-dark.png +0 -0
- package/docs/images/hero-light.png +0 -0
- package/docs/images/icons/ai.svg +4 -0
- package/docs/images/icons/auth.svg +1 -0
- package/docs/images/icons/database.svg +1 -0
- package/docs/images/icons/function.svg +1 -0
- package/docs/images/icons/storage.svg +1 -0
- package/docs/images/logos/nextjs.svg +4 -0
- package/docs/images/logos/nuxt.svg +4 -0
- package/docs/images/logos/react.svg +5 -0
- package/docs/images/logos/svelte.svg +4 -0
- package/docs/images/logos/vue.svg +5 -0
- package/docs/images/mcp-install.png +0 -0
- package/docs/images/onboarding-mcp.png +0 -0
- package/docs/insforge-instructions-sdk.md +55 -374
- package/docs/introduction.mdx +45 -0
- package/docs/logo/dark.svg +22 -0
- package/docs/logo/light.svg +20 -0
- package/docs/partnership.mdx +647 -0
- package/docs/quickstart.mdx +83 -0
- package/docs/showcase/2048-arena.png +0 -0
- package/docs/showcase/framegen-cloud.png +0 -0
- package/docs/showcase/line-connect-race.png +0 -0
- package/docs/showcase/moment-vibe.png +0 -0
- package/docs/showcase/national-flags.png +0 -0
- package/docs/showcase/pokemon-vibe.png +0 -0
- package/docs/showcase/pure-browse-buy.png +0 -0
- package/docs/showcase.mdx +52 -0
- package/docs/snippets/sdk-installation.mdx +22 -0
- package/docs/snippets/service-icons.mdx +27 -0
- package/eslint.config.js +10 -3
- package/frontend/package.json +10 -4
- package/frontend/src/App.tsx +13 -82
- package/frontend/src/assets/icons/connected.svg +3 -0
- package/frontend/src/assets/icons/loader.svg +9 -0
- package/frontend/src/assets/logos/apple.svg +4 -0
- package/frontend/src/assets/logos/discord.svg +1 -1
- package/frontend/src/assets/logos/facebook.svg +3 -0
- package/frontend/src/assets/logos/instagram.svg +2 -0
- package/frontend/src/assets/logos/linkedin.svg +3 -0
- package/frontend/src/assets/logos/microsoft.svg +1 -0
- package/frontend/src/assets/logos/spotify.svg +17 -0
- package/frontend/src/assets/logos/tiktok.svg +6 -0
- package/frontend/src/assets/logos/x.svg +3 -0
- package/frontend/src/components/Checkbox.tsx +27 -29
- package/frontend/src/components/CodeBlock.tsx +55 -2
- package/frontend/src/components/CodeEditor.tsx +92 -0
- package/frontend/src/components/ConfirmDialog.tsx +1 -1
- package/frontend/src/components/ConnectCTA.tsx +38 -0
- package/frontend/src/components/CopyButton.tsx +52 -15
- package/frontend/src/components/ErrorState.tsx +1 -2
- package/frontend/src/components/FeatureSidebar.tsx +6 -6
- package/frontend/src/components/FeatureSidebarItem.tsx +2 -2
- package/frontend/src/components/JsonHighlight.tsx +21 -9
- package/frontend/src/components/ProjectInfoModal.tsx +128 -0
- package/frontend/src/components/PromptDialog.tsx +1 -4
- package/frontend/src/components/SearchInput.tsx +1 -2
- package/frontend/src/components/Stepper.tsx +53 -0
- package/frontend/src/components/ThemeToggle.tsx +3 -3
- package/frontend/src/components/datagrid/DataGrid.tsx +25 -32
- package/frontend/src/components/datagrid/cell-editors/DateCellEditor.tsx +1 -2
- package/frontend/src/components/datagrid/cell-editors/JsonCellEditor.tsx +2 -4
- package/frontend/src/components/datagrid/index.ts +23 -0
- package/frontend/src/components/index.ts +23 -30
- package/frontend/src/components/layout/AppHeader.tsx +133 -92
- package/frontend/src/components/layout/AppSidebar.tsx +80 -170
- package/frontend/src/components/layout/Layout.tsx +12 -23
- package/frontend/src/components/layout/PrimaryMenu.tsx +187 -0
- package/frontend/src/components/layout/SecondaryMenu.tsx +70 -0
- package/frontend/src/components/layout/index.ts +5 -0
- package/frontend/src/components/radix/Tooltip.tsx +24 -13
- package/frontend/src/components/radix/index.ts +22 -0
- package/frontend/src/features/ai/components/AIConfigCard.tsx +129 -83
- package/frontend/src/features/ai/components/AIEmptyState.tsx +12 -7
- package/frontend/src/features/ai/components/ModalityFilterSidebar.tsx +101 -0
- package/frontend/src/features/ai/components/ModelSelectionDialog.tsx +135 -0
- package/frontend/src/features/ai/components/ModelSelectionGrid.tsx +51 -0
- package/frontend/src/features/ai/components/SystemPromptDialog.tsx +118 -0
- package/frontend/src/features/ai/components/index.ts +6 -0
- package/frontend/src/features/ai/helpers.ts +57 -71
- package/frontend/src/features/ai/hooks/useAIConfigs.ts +39 -113
- package/frontend/src/features/ai/hooks/useAIUsage.ts +0 -2
- package/frontend/src/features/ai/page/AIPage.tsx +67 -79
- package/frontend/src/features/ai/services/ai.service.ts +5 -5
- package/frontend/src/features/auth/components/AuthPreview.tsx +96 -0
- package/frontend/src/features/auth/components/OAuthConfigDialog.tsx +53 -30
- package/frontend/src/features/auth/components/UserFormDialog.tsx +13 -6
- package/frontend/src/features/auth/components/UsersDataGrid.tsx +44 -14
- package/frontend/src/features/auth/components/index.ts +5 -0
- package/frontend/src/features/auth/helpers.tsx +200 -0
- package/frontend/src/features/auth/hooks/useAnonToken.ts +30 -0
- package/frontend/src/features/auth/hooks/useAuthConfig.ts +48 -0
- package/frontend/src/features/auth/hooks/useOAuthConfig.ts +14 -10
- package/frontend/src/features/auth/hooks/useUsers.ts +43 -5
- package/frontend/src/features/auth/index.ts +3 -2
- package/frontend/src/features/auth/page/AuthMethodsPage.tsx +275 -0
- package/frontend/src/features/auth/page/ConfigurationPage.tsx +395 -0
- package/frontend/src/features/auth/page/UsersPage.tsx +285 -0
- package/frontend/src/features/auth/services/anonToken.service.ts +11 -0
- package/frontend/src/features/auth/services/config.service.ts +19 -0
- package/frontend/src/features/auth/services/{oauth.service.ts → oauth-config.service.ts} +4 -4
- package/frontend/src/features/auth/services/{auth.service.ts → user.service.ts} +7 -53
- package/frontend/src/features/dashboard/components/ConnectionSuccessBanner.tsx +35 -0
- package/frontend/src/features/dashboard/components/PromptCard.tsx +21 -0
- package/frontend/src/features/dashboard/components/PromptDialog.tsx +103 -0
- package/frontend/src/features/dashboard/components/StatsCard.tsx +50 -0
- package/frontend/src/features/dashboard/components/index.ts +4 -0
- package/frontend/src/features/dashboard/page/DashboardPage.tsx +187 -169
- package/frontend/src/features/dashboard/prompts/ai-chatbot.ts +13 -0
- package/frontend/src/features/dashboard/prompts/crm-system.ts +13 -0
- package/frontend/src/features/dashboard/prompts/ecommerce-platform.ts +12 -0
- package/frontend/src/features/dashboard/prompts/index.ts +31 -0
- package/frontend/src/features/dashboard/prompts/instagram-clone.ts +11 -0
- package/frontend/src/features/dashboard/prompts/notion-clone.ts +14 -0
- package/frontend/src/features/dashboard/prompts/reddit-clone.ts +12 -0
- package/frontend/src/features/database/components/DatabaseDataGrid.tsx +48 -17
- package/frontend/src/features/database/components/ForeignKeyCell.tsx +15 -34
- package/frontend/src/features/database/components/ForeignKeyPopover.tsx +19 -20
- package/frontend/src/features/database/components/LinkRecordModal.tsx +120 -125
- package/frontend/src/features/database/components/RecordFormDialog.tsx +22 -33
- package/frontend/src/features/database/components/RecordFormField.tsx +45 -47
- package/frontend/src/features/database/components/TableEmptyState.tsx +6 -5
- package/frontend/src/features/database/components/TableForm.tsx +28 -15
- package/frontend/src/features/database/components/TableFormColumn.tsx +2 -3
- package/frontend/src/features/database/components/TableSidebar.tsx +1 -1
- package/frontend/src/features/database/components/TablesEmptyState.tsx +48 -0
- package/frontend/src/features/database/components/TemplateCard.tsx +37 -0
- package/frontend/src/features/database/components/TemplatePreview.tsx +92 -0
- package/frontend/src/features/database/components/index.ts +19 -0
- package/frontend/src/features/database/constants.ts +28 -2
- package/frontend/src/features/database/contexts/SQLEditorContext.tsx +188 -0
- package/frontend/src/features/database/helpers.ts +2 -2
- package/frontend/src/features/database/hooks/useCSVImport.ts +29 -0
- package/frontend/src/features/database/hooks/useFullMetadata.ts +18 -0
- package/frontend/src/features/database/hooks/useRawSQL.ts +55 -0
- package/frontend/src/features/database/hooks/useRecords.ts +139 -0
- package/frontend/src/features/database/hooks/useTables.ts +131 -0
- package/frontend/src/features/database/index.ts +6 -1
- package/frontend/src/features/database/page/FunctionsPage.tsx +211 -0
- package/frontend/src/features/database/page/IndexesPage.tsx +240 -0
- package/frontend/src/features/database/page/PoliciesPage.tsx +248 -0
- package/frontend/src/features/database/page/SQLEditorPage.tsx +382 -0
- package/frontend/src/features/database/page/{DatabasePage.tsx → TablesPage.tsx} +186 -185
- package/frontend/src/features/database/page/TemplatesPage.tsx +39 -0
- package/frontend/src/features/database/page/TriggersPage.tsx +242 -0
- package/frontend/src/features/database/services/advance.service.ts +66 -0
- package/frontend/src/features/database/services/{database.service.ts → record.service.ts} +67 -64
- package/frontend/src/features/database/services/table.service.ts +64 -0
- package/frontend/src/features/database/templates/ai-chatbot.ts +402 -0
- package/frontend/src/features/database/templates/crm-system.ts +528 -0
- package/frontend/src/features/database/templates/ecommerce-platform.ts +553 -0
- package/frontend/src/features/database/templates/index.ts +34 -0
- package/frontend/src/features/database/templates/instagram-clone.ts +222 -0
- package/frontend/src/features/database/templates/notion-clone.ts +483 -0
- package/frontend/src/features/database/templates/reddit-clone.ts +526 -0
- package/frontend/src/features/functions/components/FunctionRow.tsx +2 -1
- package/frontend/src/features/functions/components/FunctionsSidebar.tsx +1 -1
- package/frontend/src/features/functions/components/SecretRow.tsx +1 -1
- package/frontend/src/features/functions/components/index.ts +5 -0
- package/frontend/src/features/functions/hooks/useFunctions.ts +4 -4
- package/frontend/src/features/{secrets → functions}/hooks/useSecrets.ts +5 -5
- package/frontend/src/features/functions/page/FunctionsPage.tsx +160 -17
- package/frontend/src/features/functions/{components/SecretsContent.tsx → page/SecretsPage.tsx} +8 -12
- package/frontend/src/features/functions/services/{functions.service.ts → function.service.ts} +2 -2
- package/frontend/src/features/{secrets/services/secrets.service.ts → functions/services/secret.service.ts} +2 -2
- package/frontend/src/features/login/hooks/usePartnerOrigin.ts +27 -0
- package/frontend/src/features/login/page/CloudLoginPage.tsx +79 -54
- package/frontend/src/features/login/page/LoginPage.tsx +16 -23
- package/frontend/src/features/login/services/partnership.service.ts +65 -0
- package/frontend/src/features/logs/components/LogsDataGrid.tsx +89 -0
- package/frontend/src/features/logs/components/SeverityBadge.tsx +18 -0
- package/frontend/src/features/logs/components/index.ts +2 -0
- package/frontend/src/features/logs/helpers.ts +24 -0
- package/frontend/src/features/logs/hooks/useAuditLogs.ts +4 -4
- package/frontend/src/features/logs/hooks/useLogSources.ts +137 -0
- package/frontend/src/features/logs/hooks/useLogs.ts +163 -0
- package/frontend/src/features/logs/hooks/useMcpUsage.ts +181 -0
- package/frontend/src/features/logs/index.ts +8 -2
- package/frontend/src/features/logs/page/AuditsPage.tsx +91 -38
- package/frontend/src/features/logs/page/LogsPage.tsx +152 -0
- package/frontend/src/features/logs/page/MCPLogsPage.tsx +84 -0
- package/frontend/src/features/logs/services/audit.service.ts +63 -0
- package/frontend/src/features/logs/services/log.service.ts +15 -110
- package/frontend/src/features/logs/services/usage.service.ts +31 -0
- package/frontend/src/features/onboard/components/McpConnectionStatus.tsx +68 -0
- package/frontend/src/features/onboard/components/OnboardingModal.tsx +267 -0
- package/frontend/src/features/onboard/components/VideoDemoModal.tsx +38 -0
- package/frontend/src/features/onboard/components/index.ts +4 -0
- package/frontend/src/features/onboard/components/mcp/CursorDeeplinkGenerator.tsx +2 -2
- package/frontend/src/features/onboard/components/mcp/{mcp-helper.tsx → helpers.tsx} +8 -8
- package/frontend/src/features/onboard/components/mcp/index.ts +2 -3
- package/frontend/src/features/onboard/index.ts +13 -3
- package/frontend/src/features/storage/components/BucketEmptyState.tsx +9 -6
- package/frontend/src/features/storage/components/BucketFormDialog.tsx +25 -41
- package/frontend/src/features/storage/components/FilePreviewDialog.tsx +20 -8
- package/frontend/src/features/storage/components/StorageDataGrid.tsx +4 -3
- package/frontend/src/features/storage/components/StorageManager.tsx +23 -34
- package/frontend/src/features/storage/components/index.ts +12 -0
- package/frontend/src/features/storage/hooks/useStorage.ts +208 -0
- package/frontend/src/features/storage/page/StoragePage.tsx +41 -115
- package/frontend/src/features/storage/services/storage.service.ts +22 -1
- package/frontend/src/features/visualizer/components/AuthNode.tsx +72 -56
- package/frontend/src/features/visualizer/components/BucketNode.tsx +4 -4
- package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +108 -80
- package/frontend/src/features/visualizer/components/TableNode.tsx +34 -41
- package/frontend/src/features/visualizer/components/VisualizerSkeleton.tsx +12 -4
- package/frontend/src/features/visualizer/page/VisualizerPage.tsx +33 -29
- package/frontend/src/index.css +1 -0
- package/frontend/src/lib/analytics/posthog.tsx +27 -0
- package/frontend/src/lib/contexts/AuthContext.tsx +38 -31
- package/frontend/src/lib/contexts/SocketContext.tsx +5 -6
- package/frontend/src/{features/metadata → lib}/hooks/useMetadata.ts +1 -1
- package/frontend/src/lib/hooks/useToast.tsx +6 -2
- package/frontend/src/lib/routing/AppRoutes.tsx +84 -0
- package/frontend/src/lib/routing/RequireAuth.tsx +27 -0
- package/frontend/src/lib/utils/cloudMessaging.ts +20 -0
- package/frontend/src/lib/utils/menuItems.ts +183 -0
- package/frontend/src/lib/utils/{validation-schemas.ts → schemaValidations.ts} +10 -5
- package/frontend/src/lib/utils/utils.ts +19 -1
- package/frontend/src/vite-env.d.ts +1 -0
- package/frontend/vite.config.ts +5 -3
- package/functions/server.ts +28 -3
- package/functions/worker-template.js +15 -4
- package/i18n/README.ar.md +130 -0
- package/i18n/README.de.md +130 -0
- package/i18n/README.es.md +154 -0
- package/i18n/README.fr.md +134 -0
- package/i18n/README.hi.md +129 -0
- package/i18n/README.ja.md +174 -0
- package/i18n/README.ko.md +137 -0
- package/i18n/README.pt-BR.md +131 -0
- package/i18n/README.ru.md +129 -0
- package/i18n/README.zh-CN.md +133 -0
- package/openapi/ai.yaml +31 -4
- package/openapi/auth.yaml +827 -146
- package/package.json +16 -7
- package/shared-schemas/package.json +1 -1
- package/shared-schemas/src/ai-api.schema.ts +34 -58
- package/shared-schemas/src/ai.schema.ts +5 -0
- package/shared-schemas/src/auth-api.schema.ts +154 -8
- package/shared-schemas/src/auth.schema.ts +42 -6
- package/shared-schemas/src/cloud-events.schema.ts +57 -0
- package/shared-schemas/src/database-api.schema.ts +3 -3
- package/shared-schemas/src/database.schema.ts +1 -1
- package/shared-schemas/src/index.ts +1 -0
- package/shared-schemas/src/logs-api.schema.ts +7 -1
- package/shared-schemas/src/logs.schema.ts +26 -0
- package/shared-schemas/src/metadata.schema.ts +9 -4
- package/test-gemini.sh +35 -0
- package/test-usage-admin.sh +57 -0
- package/test-usage.sh +50 -0
- package/zeabur/README.md +13 -0
- package/zeabur/template.yml +1032 -0
- package/.github/workflows/deploy-aws.yml +0 -130
- package/backend/src/api/routes/agent.ts +0 -29
- package/backend/src/api/routes/auth.oauth.ts +0 -482
- package/backend/src/api/routes/auth.ts +0 -386
- package/backend/src/api/routes/docs.ts +0 -66
- package/backend/src/api/routes/functions.ts +0 -183
- package/backend/src/api/routes/openapi.ts +0 -82
- package/backend/src/api/routes/usage.ts +0 -96
- package/backend/src/core/ai/client.ts +0 -242
- package/backend/src/core/ai/model.ts +0 -117
- package/backend/src/core/auth/auth.ts +0 -781
- package/backend/src/core/database/table.ts +0 -772
- package/backend/src/core/documentation/agent.ts +0 -689
- package/backend/src/core/documentation/openapi.ts +0 -856
- package/backend/src/core/logs/analytics.ts +0 -76
- package/backend/src/core/logs/providers/localdb.provider.ts +0 -246
- package/backend/src/core/storage/storage.ts +0 -923
- package/backend/src/utils/cloud-token.ts +0 -39
- package/backend/src/utils/helpers.ts +0 -49
- package/backend/src/utils/uuid.ts +0 -9
- package/backend/tests/manual/test-better-auth.sh +0 -303
- package/docker-init/db/logs.sql +0 -9
- package/frontend/README.md +0 -112
- package/frontend/src/components/datagrid/index.tsx +0 -20
- package/frontend/src/components/layout/CloudLayout.tsx +0 -95
- package/frontend/src/features/ai/components/AIConfigDialog.tsx +0 -76
- package/frontend/src/features/ai/components/AIConfigForm.tsx +0 -222
- package/frontend/src/features/ai/components/fields/ModalityField.tsx +0 -87
- package/frontend/src/features/ai/components/fields/ModelSelectionField.tsx +0 -134
- package/frontend/src/features/ai/components/fields/SystemPromptField.tsx +0 -33
- package/frontend/src/features/auth/components/AddOAuthDialog.tsx +0 -106
- package/frontend/src/features/auth/components/AuthMethodTab.tsx +0 -238
- package/frontend/src/features/auth/components/UsersTab.tsx +0 -114
- package/frontend/src/features/auth/page/AuthenticationPage.tsx +0 -169
- package/frontend/src/features/database/hooks/UseLinkModal.tsx +0 -78
- package/frontend/src/features/functions/components/FunctionViewer.tsx +0 -46
- package/frontend/src/features/functions/components/FunctionsContent.tsx +0 -88
- package/frontend/src/features/login/components/AuthErrorBoundary.tsx +0 -87
- package/frontend/src/features/login/components/PrivateRoute.tsx +0 -24
- package/frontend/src/features/logs/components/AnalyticsLogsTable.tsx +0 -313
- package/frontend/src/features/logs/components/LogsTable.tsx +0 -199
- package/frontend/src/features/logs/page/AnalyticsLogsPage.tsx +0 -530
- package/frontend/src/features/metadata/index.ts +0 -0
- package/frontend/src/features/metadata/page/MetadataPage.tsx +0 -136
- package/frontend/src/features/onboard/components/CompletionCard.tsx +0 -41
- package/frontend/src/features/onboard/components/OnboardButton.tsx +0 -84
- package/frontend/src/features/onboard/components/StepContent.tsx +0 -91
- package/frontend/src/features/onboard/components/TestConnectionStep.tsx +0 -53
- package/frontend/src/features/onboard/components/mcp/McpInstallation.tsx +0 -144
- package/frontend/src/features/onboard/page/OnBoardPage.tsx +0 -104
- package/frontend/src/features/onboard/types.ts +0 -8
- package/frontend/src/lib/contexts/OnboardStepContext.tsx +0 -68
- package/frontend/src/lib/hooks/useOnboardingCompletion.ts +0 -29
- /package/backend/src/api/{middleware → middlewares}/error.ts +0 -0
- /package/backend/src/api/{middleware → middlewares}/upload.ts +0 -0
- /package/backend/{migrations → src/infra/database/migrations}/000_create-base-tables.sql +0 -0
- /package/backend/{migrations → src/infra/database/migrations}/001_create-helper-functions.sql +0 -0
- /package/backend/{migrations → src/infra/database/migrations}/002_rename-auth-tables.sql +0 -0
- /package/backend/{migrations → src/infra/database/migrations}/003_create-users-table.sql +0 -0
- /package/backend/{migrations → src/infra/database/migrations}/004_add-reload-postgrest-func.sql +0 -0
- /package/backend/{migrations → src/infra/database/migrations}/005_enable-project-admin-modify-users.sql +0 -0
- /package/backend/{migrations → src/infra/database/migrations}/006_modify-ai-usage-table.sql +0 -0
- /package/backend/{migrations → src/infra/database/migrations}/007_drop-metadata-table.sql +0 -0
- /package/backend/{migrations → src/infra/database/migrations}/008_add-system-tables.sql +0 -0
- /package/backend/{migrations → src/infra/database/migrations}/009_add-function-secrets.sql +0 -0
- /package/backend/{migrations → src/infra/database/migrations}/010_modify-ai-config-modalities.sql +0 -0
- /package/backend/{migrations → src/infra/database/migrations}/011_refactor-secrets-table.sql +0 -0
- /package/backend/{migrations → src/infra/database/migrations}/012_add-storage-uploaded-by.sql +0 -0
- /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
|
-
}
|