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
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { Pool } from 'pg';
|
|
3
|
+
import { DatabaseManager } from '@/infra/database/database.manager.js';
|
|
4
|
+
import { StorageRecord } from '@/types/storage.js';
|
|
5
|
+
import {
|
|
6
|
+
StorageBucketSchema,
|
|
7
|
+
StorageFileSchema,
|
|
8
|
+
StorageMetadataSchema,
|
|
9
|
+
} from '@insforge/shared-schemas';
|
|
10
|
+
import { StorageProvider } from '@/providers/storage/base.provider.js';
|
|
11
|
+
import { LocalStorageProvider } from '@/providers/storage/local.provider.js';
|
|
12
|
+
import { S3StorageProvider } from '@/providers/storage/s3.provider.js';
|
|
13
|
+
import logger from '@/utils/logger.js';
|
|
14
|
+
import { ADMIN_ID } from '@/utils/constants.js';
|
|
15
|
+
import { AppError } from '@/api/middlewares/error.js';
|
|
16
|
+
import { ERROR_CODES } from '@/types/error-constants.js';
|
|
17
|
+
import { escapeSqlLikePattern, escapeRegexPattern } from '@/utils/validations.js';
|
|
18
|
+
import { getApiBaseUrl } from '@/utils/environment.js';
|
|
19
|
+
|
|
20
|
+
const DEFAULT_LIST_LIMIT = 100;
|
|
21
|
+
const GIGABYTE_IN_BYTES = 1024 * 1024 * 1024;
|
|
22
|
+
const PUBLIC_BUCKET_EXPIRY = 0; // Public buckets don't expire
|
|
23
|
+
const PRIVATE_BUCKET_EXPIRY = 3600; // Private buckets expire in 1 hour
|
|
24
|
+
|
|
25
|
+
export class StorageService {
|
|
26
|
+
private static instance: StorageService;
|
|
27
|
+
private provider: StorageProvider;
|
|
28
|
+
private pool: Pool | null = null;
|
|
29
|
+
|
|
30
|
+
private constructor() {
|
|
31
|
+
const s3Bucket = process.env.AWS_S3_BUCKET;
|
|
32
|
+
const appKey = process.env.APP_KEY || 'local';
|
|
33
|
+
|
|
34
|
+
if (s3Bucket) {
|
|
35
|
+
// Use S3 backend
|
|
36
|
+
this.provider = new S3StorageProvider(
|
|
37
|
+
s3Bucket,
|
|
38
|
+
appKey,
|
|
39
|
+
process.env.AWS_REGION || 'us-east-2'
|
|
40
|
+
);
|
|
41
|
+
} else {
|
|
42
|
+
// Use local filesystem backend
|
|
43
|
+
const baseDir = process.env.STORAGE_DIR || path.resolve(process.cwd(), 'insforge-storage');
|
|
44
|
+
this.provider = new LocalStorageProvider(baseDir);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private getPool(): Pool {
|
|
49
|
+
if (!this.pool) {
|
|
50
|
+
this.pool = DatabaseManager.getInstance().getPool();
|
|
51
|
+
}
|
|
52
|
+
return this.pool;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static getInstance(): StorageService {
|
|
56
|
+
if (!StorageService.instance) {
|
|
57
|
+
StorageService.instance = new StorageService();
|
|
58
|
+
}
|
|
59
|
+
return StorageService.instance;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async initialize(): Promise<void> {
|
|
63
|
+
await this.provider.initialize();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private validateBucketName(bucket: string): void {
|
|
67
|
+
// Simple validation: alphanumeric, hyphens, underscores
|
|
68
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(bucket)) {
|
|
69
|
+
throw new Error('Invalid bucket name. Use only letters, numbers, hyphens, and underscores.');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private validateKey(key: string): void {
|
|
74
|
+
// Prevent directory traversal
|
|
75
|
+
if (key.includes('..') || key.startsWith('/')) {
|
|
76
|
+
throw new Error('Invalid key. Cannot use ".." or start with "/"');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Generate a unique object key with timestamp and random string
|
|
82
|
+
* @param originalFilename - The original filename from the upload
|
|
83
|
+
* @returns Generated unique key
|
|
84
|
+
*/
|
|
85
|
+
generateObjectKey(originalFilename: string): string {
|
|
86
|
+
const timestamp = Date.now();
|
|
87
|
+
const randomStr = Math.random().toString(36).substring(2, 8);
|
|
88
|
+
const fileExt = originalFilename ? path.extname(originalFilename) : '';
|
|
89
|
+
const baseName = originalFilename ? path.basename(originalFilename, fileExt) : 'file';
|
|
90
|
+
const sanitizedBaseName = baseName.replace(/[^a-zA-Z0-9-_]/g, '-').substring(0, 32);
|
|
91
|
+
const objectKey = `${sanitizedBaseName}-${timestamp}-${randomStr}${fileExt}`;
|
|
92
|
+
|
|
93
|
+
return objectKey;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Generate the next available key for a file, using (1), (2), (3) pattern if duplicates exist
|
|
98
|
+
* @param bucket - The bucket name
|
|
99
|
+
* @param originalKey - The original filename
|
|
100
|
+
* @returns The next available key
|
|
101
|
+
*/
|
|
102
|
+
private async generateNextAvailableKey(bucket: string, originalKey: string): Promise<string> {
|
|
103
|
+
// Parse filename and extension for potential auto-renaming
|
|
104
|
+
const lastDotIndex = originalKey.lastIndexOf('.');
|
|
105
|
+
const baseName = lastDotIndex > 0 ? originalKey.substring(0, lastDotIndex) : originalKey;
|
|
106
|
+
const extension = lastDotIndex > 0 ? originalKey.substring(lastDotIndex) : '';
|
|
107
|
+
|
|
108
|
+
// Use efficient SQL query to find the highest existing counter
|
|
109
|
+
// This query finds all files matching the pattern and extracts the counter number
|
|
110
|
+
const result = await this.getPool().query(
|
|
111
|
+
`
|
|
112
|
+
SELECT key FROM _storage
|
|
113
|
+
WHERE bucket = $1
|
|
114
|
+
AND (key = $2 OR key LIKE $3)
|
|
115
|
+
`,
|
|
116
|
+
[
|
|
117
|
+
bucket,
|
|
118
|
+
originalKey,
|
|
119
|
+
`${escapeSqlLikePattern(baseName)} (%)${escapeSqlLikePattern(extension)}`,
|
|
120
|
+
]
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const existingFiles = result.rows;
|
|
124
|
+
let finalKey = originalKey;
|
|
125
|
+
|
|
126
|
+
if (existingFiles.length) {
|
|
127
|
+
// Extract counter numbers from existing files
|
|
128
|
+
let incrementNumber = 0;
|
|
129
|
+
// This regex is used to match the counter number in the filename, extract the increment number
|
|
130
|
+
const counterRegex = new RegExp(
|
|
131
|
+
`^${escapeRegexPattern(baseName)} \\((\\d+)\\)${escapeRegexPattern(extension)}$`
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
for (const file of existingFiles as { key: string }[]) {
|
|
135
|
+
if (file.key === originalKey) {
|
|
136
|
+
incrementNumber = Math.max(incrementNumber, 0); // Original file exists, so we need at least (1)
|
|
137
|
+
} else {
|
|
138
|
+
const match = file.key.match(counterRegex);
|
|
139
|
+
if (match) {
|
|
140
|
+
incrementNumber = Math.max(incrementNumber, parseInt(match[1], 10));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Generate the next available filename
|
|
146
|
+
finalKey = `${baseName} (${incrementNumber + 1})${extension}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return finalKey;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async putObject(
|
|
153
|
+
bucket: string,
|
|
154
|
+
originalKey: string,
|
|
155
|
+
file: Express.Multer.File,
|
|
156
|
+
userId?: string
|
|
157
|
+
): Promise<StorageFileSchema> {
|
|
158
|
+
this.validateBucketName(bucket);
|
|
159
|
+
this.validateKey(originalKey);
|
|
160
|
+
|
|
161
|
+
// Generate next available key using (1), (2), (3) pattern if duplicates exist
|
|
162
|
+
const finalKey = await this.generateNextAvailableKey(bucket, originalKey);
|
|
163
|
+
|
|
164
|
+
// Save file using backend
|
|
165
|
+
await this.provider.putObject(bucket, finalKey, file);
|
|
166
|
+
|
|
167
|
+
const client = await this.getPool().connect();
|
|
168
|
+
try {
|
|
169
|
+
// Save metadata to database and return the timestamp in one operation
|
|
170
|
+
const result = await client.query(
|
|
171
|
+
`
|
|
172
|
+
INSERT INTO _storage (bucket, key, size, mime_type, uploaded_by)
|
|
173
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
174
|
+
RETURNING uploaded_at as "uploadedAt"
|
|
175
|
+
`,
|
|
176
|
+
[
|
|
177
|
+
bucket,
|
|
178
|
+
finalKey,
|
|
179
|
+
file.size,
|
|
180
|
+
file.mimetype || null,
|
|
181
|
+
userId && userId !== ADMIN_ID ? userId : null,
|
|
182
|
+
]
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
if (!result.rows[0]) {
|
|
186
|
+
throw new Error(`Failed to retrieve upload timestamp for ${bucket}/${finalKey}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
bucket,
|
|
191
|
+
key: finalKey,
|
|
192
|
+
size: file.size,
|
|
193
|
+
mimeType: file.mimetype,
|
|
194
|
+
uploadedAt: result.rows[0].uploadedAt,
|
|
195
|
+
url: `${getApiBaseUrl()}/api/storage/buckets/${bucket}/objects/${encodeURIComponent(finalKey)}`,
|
|
196
|
+
};
|
|
197
|
+
} finally {
|
|
198
|
+
client.release();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async getObject(
|
|
203
|
+
bucket: string,
|
|
204
|
+
key: string
|
|
205
|
+
): Promise<{ file: Buffer; metadata: StorageFileSchema } | null> {
|
|
206
|
+
this.validateBucketName(bucket);
|
|
207
|
+
this.validateKey(key);
|
|
208
|
+
|
|
209
|
+
const result = await this.getPool().query(
|
|
210
|
+
'SELECT * FROM _storage WHERE bucket = $1 AND key = $2',
|
|
211
|
+
[bucket, key]
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const metadata = result.rows[0] as StorageRecord | undefined;
|
|
215
|
+
|
|
216
|
+
if (!metadata) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const file = await this.provider.getObject(bucket, key);
|
|
221
|
+
if (!file) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
file,
|
|
227
|
+
metadata: {
|
|
228
|
+
key: metadata.key,
|
|
229
|
+
bucket: metadata.bucket,
|
|
230
|
+
size: metadata.size,
|
|
231
|
+
mimeType: metadata.mime_type,
|
|
232
|
+
uploadedAt: metadata.uploaded_at,
|
|
233
|
+
url: `${getApiBaseUrl()}/api/storage/buckets/${bucket}/objects/${encodeURIComponent(key)}`,
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async deleteObject(
|
|
239
|
+
bucket: string,
|
|
240
|
+
key: string,
|
|
241
|
+
userId?: string,
|
|
242
|
+
isAdmin?: boolean
|
|
243
|
+
): Promise<boolean> {
|
|
244
|
+
this.validateBucketName(bucket);
|
|
245
|
+
this.validateKey(key);
|
|
246
|
+
|
|
247
|
+
const client = await this.getPool().connect();
|
|
248
|
+
try {
|
|
249
|
+
// Check permissions
|
|
250
|
+
if (!isAdmin) {
|
|
251
|
+
const fileResult = await client.query(
|
|
252
|
+
'SELECT uploaded_by FROM _storage WHERE bucket = $1 AND key = $2',
|
|
253
|
+
[bucket, key]
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const file = fileResult.rows[0] as { uploaded_by: string | null } | undefined;
|
|
257
|
+
|
|
258
|
+
if (!file) {
|
|
259
|
+
return false; // File doesn't exist
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check if user owns the file
|
|
263
|
+
if (userId && file.uploaded_by !== userId) {
|
|
264
|
+
throw new AppError(
|
|
265
|
+
'Permission denied: You can only delete files you uploaded',
|
|
266
|
+
403,
|
|
267
|
+
ERROR_CODES.FORBIDDEN
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Delete file using backend
|
|
273
|
+
await this.provider.deleteObject(bucket, key);
|
|
274
|
+
|
|
275
|
+
// Delete from database
|
|
276
|
+
const result = await client.query('DELETE FROM _storage WHERE bucket = $1 AND key = $2', [
|
|
277
|
+
bucket,
|
|
278
|
+
key,
|
|
279
|
+
]);
|
|
280
|
+
|
|
281
|
+
return result.rowCount !== null && result.rowCount > 0;
|
|
282
|
+
} finally {
|
|
283
|
+
client.release();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async listObjects(
|
|
288
|
+
bucket: string,
|
|
289
|
+
prefix?: string,
|
|
290
|
+
limit: number = DEFAULT_LIST_LIMIT,
|
|
291
|
+
offset: number = 0,
|
|
292
|
+
searchQuery?: string
|
|
293
|
+
): Promise<{ objects: StorageFileSchema[]; total: number }> {
|
|
294
|
+
this.validateBucketName(bucket);
|
|
295
|
+
|
|
296
|
+
const client = await this.getPool().connect();
|
|
297
|
+
try {
|
|
298
|
+
let query = 'SELECT * FROM _storage WHERE bucket = $1';
|
|
299
|
+
let countQuery = 'SELECT COUNT(*) as count FROM _storage WHERE bucket = $1';
|
|
300
|
+
const params: (string | number)[] = [bucket];
|
|
301
|
+
let paramIndex = 2;
|
|
302
|
+
|
|
303
|
+
if (prefix) {
|
|
304
|
+
query += ` AND key LIKE $${paramIndex}`;
|
|
305
|
+
countQuery += ` AND key LIKE $${paramIndex}`;
|
|
306
|
+
params.push(`${prefix}%`);
|
|
307
|
+
paramIndex++;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Add search functionality for file names (key field)
|
|
311
|
+
if (searchQuery && searchQuery.trim()) {
|
|
312
|
+
query += ` AND key LIKE $${paramIndex}`;
|
|
313
|
+
countQuery += ` AND key LIKE $${paramIndex}`;
|
|
314
|
+
const searchPattern = `%${searchQuery.trim()}%`;
|
|
315
|
+
params.push(searchPattern);
|
|
316
|
+
paramIndex++;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
query += ` ORDER BY key LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
|
|
320
|
+
const queryParams = [...params, limit, offset];
|
|
321
|
+
|
|
322
|
+
const objectsResult = await client.query(query, queryParams);
|
|
323
|
+
const totalResult = await client.query(countQuery, params);
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
objects: objectsResult.rows.map((obj) => ({
|
|
327
|
+
...obj,
|
|
328
|
+
mimeType: obj.mime_type,
|
|
329
|
+
uploadedAt: obj.uploaded_at,
|
|
330
|
+
url: `${getApiBaseUrl()}/api/storage/buckets/${bucket}/objects/${encodeURIComponent(obj.key)}`,
|
|
331
|
+
})),
|
|
332
|
+
total: parseInt(totalResult.rows[0].count, 10),
|
|
333
|
+
};
|
|
334
|
+
} finally {
|
|
335
|
+
client.release();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async isBucketPublic(bucket: string): Promise<boolean> {
|
|
340
|
+
const result = await this.getPool().query(
|
|
341
|
+
'SELECT public FROM _storage_buckets WHERE name = $1',
|
|
342
|
+
[bucket]
|
|
343
|
+
);
|
|
344
|
+
return result.rows[0]?.public || false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async updateBucketVisibility(bucket: string, isPublic: boolean): Promise<void> {
|
|
348
|
+
const client = await this.getPool().connect();
|
|
349
|
+
try {
|
|
350
|
+
// Check if bucket exists
|
|
351
|
+
const bucketResult = await client.query('SELECT name FROM _storage_buckets WHERE name = $1', [
|
|
352
|
+
bucket,
|
|
353
|
+
]);
|
|
354
|
+
|
|
355
|
+
if (!bucketResult.rows[0]) {
|
|
356
|
+
throw new Error(`Bucket "${bucket}" does not exist`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Update bucket visibility in _storage_buckets table
|
|
360
|
+
await client.query(
|
|
361
|
+
'UPDATE _storage_buckets SET public = $1, updated_at = CURRENT_TIMESTAMP WHERE name = $2',
|
|
362
|
+
[isPublic, bucket]
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// Update storage metadata
|
|
366
|
+
// Metadata is now updated on-demand
|
|
367
|
+
} finally {
|
|
368
|
+
client.release();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async listBuckets(): Promise<StorageBucketSchema[]> {
|
|
373
|
+
// Get all buckets with their metadata from _storage_buckets table
|
|
374
|
+
const result = await this.getPool().query(
|
|
375
|
+
'SELECT name, public, created_at as "createdAt" FROM _storage_buckets ORDER BY name'
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
return result.rows as StorageBucketSchema[];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async createBucket(bucket: string, isPublic: boolean = true): Promise<void> {
|
|
382
|
+
this.validateBucketName(bucket);
|
|
383
|
+
|
|
384
|
+
const client = await this.getPool().connect();
|
|
385
|
+
try {
|
|
386
|
+
// Check if bucket already exists
|
|
387
|
+
const existing = await client.query('SELECT name FROM _storage_buckets WHERE name = $1', [
|
|
388
|
+
bucket,
|
|
389
|
+
]);
|
|
390
|
+
|
|
391
|
+
if (existing.rows[0]) {
|
|
392
|
+
throw new Error(`Bucket "${bucket}" already exists`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Insert bucket into _storage_buckets table
|
|
396
|
+
await client.query('INSERT INTO _storage_buckets (name, public) VALUES ($1, $2)', [
|
|
397
|
+
bucket,
|
|
398
|
+
isPublic,
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
// Create bucket using backend
|
|
402
|
+
await this.provider.createBucket(bucket);
|
|
403
|
+
|
|
404
|
+
// Update storage metadata
|
|
405
|
+
// Metadata is now updated on-demand
|
|
406
|
+
} finally {
|
|
407
|
+
client.release();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async deleteBucket(bucket: string): Promise<boolean> {
|
|
412
|
+
this.validateBucketName(bucket);
|
|
413
|
+
|
|
414
|
+
const client = await this.getPool().connect();
|
|
415
|
+
try {
|
|
416
|
+
// Check if bucket exists
|
|
417
|
+
const bucketResult = await client.query('SELECT name FROM _storage_buckets WHERE name = $1', [
|
|
418
|
+
bucket,
|
|
419
|
+
]);
|
|
420
|
+
|
|
421
|
+
if (!bucketResult.rows[0]) {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Delete bucket using backend (handles all files)
|
|
426
|
+
await this.provider.deleteBucket(bucket);
|
|
427
|
+
|
|
428
|
+
// Delete from storage table (cascade will handle _storage entries)
|
|
429
|
+
await client.query('DELETE FROM _storage_buckets WHERE name = $1', [bucket]);
|
|
430
|
+
|
|
431
|
+
// Update storage metadata
|
|
432
|
+
// Metadata is now updated on-demand
|
|
433
|
+
|
|
434
|
+
return true;
|
|
435
|
+
} finally {
|
|
436
|
+
client.release();
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// New methods for universal upload/download strategies
|
|
441
|
+
async getUploadStrategy(
|
|
442
|
+
bucket: string,
|
|
443
|
+
metadata: {
|
|
444
|
+
filename: string;
|
|
445
|
+
contentType?: string;
|
|
446
|
+
size?: number;
|
|
447
|
+
}
|
|
448
|
+
) {
|
|
449
|
+
this.validateBucketName(bucket);
|
|
450
|
+
|
|
451
|
+
const client = await this.getPool().connect();
|
|
452
|
+
try {
|
|
453
|
+
// Check if bucket exists
|
|
454
|
+
const bucketResult = await client.query('SELECT name FROM _storage_buckets WHERE name = $1', [
|
|
455
|
+
bucket,
|
|
456
|
+
]);
|
|
457
|
+
|
|
458
|
+
if (!bucketResult.rows[0]) {
|
|
459
|
+
throw new Error(`Bucket "${bucket}" does not exist`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Generate next available key using (1), (2), (3) pattern if duplicates exist
|
|
463
|
+
const key = await this.generateNextAvailableKey(bucket, metadata.filename);
|
|
464
|
+
return this.provider.getUploadStrategy(bucket, key, metadata);
|
|
465
|
+
} finally {
|
|
466
|
+
client.release();
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async getDownloadStrategy(bucket: string, key: string) {
|
|
471
|
+
this.validateBucketName(bucket);
|
|
472
|
+
this.validateKey(key);
|
|
473
|
+
|
|
474
|
+
// Check if bucket is public
|
|
475
|
+
const isPublic = await this.isBucketPublic(bucket);
|
|
476
|
+
|
|
477
|
+
// Auto-calculate expiry based on bucket visibility if not provided
|
|
478
|
+
const expiresIn = isPublic ? PUBLIC_BUCKET_EXPIRY : PRIVATE_BUCKET_EXPIRY;
|
|
479
|
+
|
|
480
|
+
return this.provider.getDownloadStrategy(bucket, key, expiresIn, isPublic);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async confirmUpload(
|
|
484
|
+
bucket: string,
|
|
485
|
+
key: string,
|
|
486
|
+
metadata: {
|
|
487
|
+
size: number;
|
|
488
|
+
contentType?: string;
|
|
489
|
+
etag?: string;
|
|
490
|
+
},
|
|
491
|
+
userId?: string
|
|
492
|
+
): Promise<StorageFileSchema> {
|
|
493
|
+
this.validateBucketName(bucket);
|
|
494
|
+
this.validateKey(key);
|
|
495
|
+
|
|
496
|
+
// Verify the file exists in storage
|
|
497
|
+
const exists = await this.provider.verifyObjectExists(bucket, key);
|
|
498
|
+
if (!exists) {
|
|
499
|
+
throw new Error(`Upload not found for key "${key}" in bucket "${bucket}"`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const client = await this.getPool().connect();
|
|
503
|
+
try {
|
|
504
|
+
// Check if already confirmed
|
|
505
|
+
const existingResult = await client.query(
|
|
506
|
+
'SELECT key FROM _storage WHERE bucket = $1 AND key = $2',
|
|
507
|
+
[bucket, key]
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
if (existingResult.rows[0]) {
|
|
511
|
+
throw new Error(`File "${key}" already confirmed in bucket "${bucket}"`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Save metadata to database and return the timestamp in one operation
|
|
515
|
+
const result = await client.query(
|
|
516
|
+
`
|
|
517
|
+
INSERT INTO _storage (bucket, key, size, mime_type, uploaded_by)
|
|
518
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
519
|
+
RETURNING uploaded_at as "uploadedAt"
|
|
520
|
+
`,
|
|
521
|
+
[
|
|
522
|
+
bucket,
|
|
523
|
+
key,
|
|
524
|
+
metadata.size,
|
|
525
|
+
metadata.contentType || null,
|
|
526
|
+
userId && userId !== ADMIN_ID ? userId : null,
|
|
527
|
+
]
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
if (!result.rows[0]) {
|
|
531
|
+
throw new Error(`Failed to retrieve upload timestamp for ${bucket}/${key}`);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
bucket,
|
|
536
|
+
key,
|
|
537
|
+
size: metadata.size,
|
|
538
|
+
mimeType: metadata.contentType,
|
|
539
|
+
uploadedAt: result.rows[0].uploadedAt,
|
|
540
|
+
url: `${getApiBaseUrl()}/api/storage/buckets/${bucket}/objects/${encodeURIComponent(key)}`,
|
|
541
|
+
};
|
|
542
|
+
} finally {
|
|
543
|
+
client.release();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Get storage metadata
|
|
549
|
+
*/
|
|
550
|
+
async getMetadata(): Promise<StorageMetadataSchema> {
|
|
551
|
+
// Get storage buckets from _storage_buckets table
|
|
552
|
+
const result = await this.getPool().query(
|
|
553
|
+
'SELECT name, public, created_at as "createdAt" FROM _storage_buckets ORDER BY name'
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
const storageBuckets = result.rows as StorageBucketSchema[];
|
|
557
|
+
|
|
558
|
+
// Get object counts for each bucket
|
|
559
|
+
const bucketsObjectCountMap = await this.getBucketsObjectCount();
|
|
560
|
+
const storageSize = await this.getStorageSizeInGB();
|
|
561
|
+
|
|
562
|
+
return {
|
|
563
|
+
buckets: storageBuckets.map((bucket) => ({
|
|
564
|
+
...bucket,
|
|
565
|
+
objectCount: bucketsObjectCountMap.get(bucket.name) ?? 0,
|
|
566
|
+
})),
|
|
567
|
+
totalSizeInGB: storageSize,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private async getBucketsObjectCount(): Promise<Map<string, number>> {
|
|
572
|
+
try {
|
|
573
|
+
// Query to get object count for each bucket
|
|
574
|
+
const result = await this.getPool().query(
|
|
575
|
+
'SELECT bucket, COUNT(*) as count FROM _storage GROUP BY bucket'
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
const bucketCounts = result.rows as { bucket: string; count: string }[];
|
|
579
|
+
|
|
580
|
+
// Convert to Map for easy lookup
|
|
581
|
+
const countMap = new Map<string, number>();
|
|
582
|
+
bucketCounts.forEach((row) => {
|
|
583
|
+
countMap.set(row.bucket, parseInt(row.count, 10));
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
return countMap;
|
|
587
|
+
} catch (error) {
|
|
588
|
+
logger.error('Error getting bucket object counts', {
|
|
589
|
+
error: error instanceof Error ? error.message : String(error),
|
|
590
|
+
});
|
|
591
|
+
// Return empty map on error
|
|
592
|
+
return new Map<string, number>();
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private async getStorageSizeInGB(): Promise<number> {
|
|
597
|
+
try {
|
|
598
|
+
// Query the _storage table to sum all file sizes
|
|
599
|
+
const result = await this.getPool().query(
|
|
600
|
+
`
|
|
601
|
+
SELECT COALESCE(SUM(size), 0) as total_size
|
|
602
|
+
FROM _storage
|
|
603
|
+
`
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
const totalSize = result.rows[0]?.total_size || 0;
|
|
607
|
+
|
|
608
|
+
// Convert bytes to GB
|
|
609
|
+
return Number(totalSize) / GIGABYTE_IN_BYTES;
|
|
610
|
+
} catch (error) {
|
|
611
|
+
logger.error('Error getting storage size', {
|
|
612
|
+
error: error instanceof Error ? error.message : String(error),
|
|
613
|
+
});
|
|
614
|
+
return 0;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|