insforge 0.3.1
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/.dockerignore +58 -0
- package/.env.example +49 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +83 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +79 -0
- package/.github/copilot-instructions.md +147 -0
- package/.github/workflows/build-image.yml +65 -0
- package/.github/workflows/ci-premerge-check.yml +24 -0
- package/.github/workflows/deploy-aws.yml +130 -0
- package/.github/workflows/lint-and-format.yml +33 -0
- package/.prettierignore +65 -0
- package/.prettierrc +9 -0
- package/CHANGELOG.md +3 -0
- package/CONTRIBUTING.md +126 -0
- package/Dockerfile +27 -0
- package/GITHUB_OAUTH_SETUP.md +49 -0
- package/GOOGLE_OAUTH_SETUP.md +148 -0
- package/LICENSE +201 -0
- package/README.md +134 -0
- package/assets/Dark.svg +23 -0
- package/assets/archDiagram.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/mcpInstallv2.png +0 -0
- package/assets/sampleResponse.png +0 -0
- package/assets/signin.png +0 -0
- package/assets/userflow.png +0 -0
- package/backend/migrations/000_create-base-tables.sql +142 -0
- package/backend/migrations/001_create-helper-functions.sql +41 -0
- package/backend/migrations/002_rename-auth-tables.sql +30 -0
- package/backend/migrations/003_create-users-table.sql +56 -0
- package/backend/migrations/004_add-reload-postgrest-func.sql +24 -0
- package/backend/migrations/005_enable-project-admin-modify-users.sql +30 -0
- package/backend/migrations/006_modify-ai-usage-table.sql +25 -0
- package/backend/migrations/007_drop-metadata-table.sql +2 -0
- package/backend/migrations/008_add-system-tables.sql +77 -0
- package/backend/migrations/009_add-function-secrets.sql +24 -0
- package/backend/migrations/010_modify-ai-config-modalities.sql +93 -0
- package/backend/migrations/011_refactor-secrets-table.sql +15 -0
- package/backend/migrations/012_add-storage-uploaded-by.sql +8 -0
- package/backend/package.json +75 -0
- package/backend/src/api/middleware/auth.ts +240 -0
- package/backend/src/api/middleware/error.ts +231 -0
- package/backend/src/api/middleware/upload.ts +59 -0
- package/backend/src/api/routes/agent.ts +29 -0
- package/backend/src/api/routes/ai.ts +472 -0
- package/backend/src/api/routes/auth.oauth.ts +482 -0
- package/backend/src/api/routes/auth.ts +386 -0
- package/backend/src/api/routes/database.advance.ts +275 -0
- package/backend/src/api/routes/database.records.ts +246 -0
- package/backend/src/api/routes/database.tables.ts +161 -0
- package/backend/src/api/routes/docs.ts +66 -0
- package/backend/src/api/routes/functions.ts +183 -0
- package/backend/src/api/routes/logs.ts +150 -0
- package/backend/src/api/routes/metadata.ts +160 -0
- package/backend/src/api/routes/openapi.ts +82 -0
- package/backend/src/api/routes/secrets.ts +199 -0
- package/backend/src/api/routes/storage.ts +547 -0
- package/backend/src/api/routes/usage.ts +96 -0
- package/backend/src/core/ai/chat.ts +207 -0
- package/backend/src/core/ai/client.ts +242 -0
- package/backend/src/core/ai/config.ts +187 -0
- package/backend/src/core/ai/image.ts +156 -0
- package/backend/src/core/ai/model.ts +117 -0
- package/backend/src/core/ai/usage.ts +290 -0
- package/backend/src/core/auth/auth.ts +781 -0
- package/backend/src/core/auth/oauth.ts +398 -0
- package/backend/src/core/database/advance.ts +1074 -0
- package/backend/src/core/database/manager.ts +178 -0
- package/backend/src/core/database/table.ts +772 -0
- package/backend/src/core/documentation/agent.ts +689 -0
- package/backend/src/core/documentation/openapi.ts +856 -0
- package/backend/src/core/functions/functions.ts +310 -0
- package/backend/src/core/logs/analytics.ts +76 -0
- package/backend/src/core/logs/audit.ts +255 -0
- package/backend/src/core/logs/providers/base.provider.ts +83 -0
- package/backend/src/core/logs/providers/cloudwatch.provider.ts +510 -0
- package/backend/src/core/logs/providers/localdb.provider.ts +246 -0
- package/backend/src/core/secrets/encryption.ts +58 -0
- package/backend/src/core/secrets/secrets.ts +410 -0
- package/backend/src/core/socket/socket.ts +388 -0
- package/backend/src/core/socket/types.ts +79 -0
- package/backend/src/core/storage/storage.ts +923 -0
- package/backend/src/server.ts +288 -0
- package/backend/src/types/ai.ts +46 -0
- package/backend/src/types/auth.ts +90 -0
- package/backend/src/types/database.ts +136 -0
- package/backend/src/types/error-constants.ts +86 -0
- package/backend/src/types/logs.ts +47 -0
- package/backend/src/types/profile.ts +55 -0
- package/backend/src/types/storage.ts +23 -0
- package/backend/src/utils/cloud-token.ts +39 -0
- package/backend/src/utils/constants.ts +1 -0
- package/backend/src/utils/environment.ts +35 -0
- package/backend/src/utils/helpers.ts +49 -0
- package/backend/src/utils/logger.ts +13 -0
- package/backend/src/utils/response.ts +62 -0
- package/backend/src/utils/seed.ts +205 -0
- package/backend/src/utils/sql-parser.ts +63 -0
- package/backend/src/utils/uuid.ts +9 -0
- package/backend/src/utils/validations.ts +129 -0
- package/backend/tests/README.md +134 -0
- package/backend/tests/cleanup-all-test-data.sh +231 -0
- package/backend/tests/cloud/test-s3-multitenant.sh +132 -0
- package/backend/tests/local/comprehensive-curl-tests.sh +156 -0
- package/backend/tests/local/test-auth-router.sh +144 -0
- package/backend/tests/local/test-database-router.sh +222 -0
- package/backend/tests/local/test-e2e.sh +241 -0
- package/backend/tests/local/test-fk-errors.sh +97 -0
- package/backend/tests/local/test-id-field.sh +201 -0
- package/backend/tests/local/test-public-bucket.sh +265 -0
- package/backend/tests/local/test-secrets.sh +248 -0
- package/backend/tests/local/test-serverless-functions.sh.disabled +325 -0
- package/backend/tests/local/test-traditional-rest.sh +209 -0
- package/backend/tests/manual/README.md +51 -0
- package/backend/tests/manual/create-large-table-simple.sql +11 -0
- package/backend/tests/manual/seed-large-table.sql +101 -0
- package/backend/tests/manual/setup-large-table-extras.sql +34 -0
- package/backend/tests/manual/test-better-auth.sh +303 -0
- package/backend/tests/manual/test-bulk-upsert.sh +410 -0
- package/backend/tests/manual/test-database-advance.sh +297 -0
- package/backend/tests/manual/test-postgrest-stability.sh +192 -0
- package/backend/tests/manual/test-rawsql-export-import.sh +412 -0
- package/backend/tests/manual/test-universal-storage.sh +264 -0
- package/backend/tests/manual/test-users.sql +18 -0
- package/backend/tests/run-all-tests.sh +140 -0
- package/backend/tests/setup.ts +22 -0
- package/backend/tests/test-config.sh +303 -0
- package/backend/tsconfig.json +23 -0
- package/backend/tsup.config.ts +18 -0
- package/backend/vitest.config.ts +22 -0
- package/docker-compose.prod.yml +145 -0
- package/docker-compose.yml +167 -0
- package/docker-init/db/db-init.sql +125 -0
- package/docker-init/db/jwt.sql +5 -0
- package/docker-init/db/logs.sql +9 -0
- package/docker-init/db/postgresql.conf +17 -0
- package/docs/deprecated/insforge-auth-api.md +215 -0
- package/docs/deprecated/insforge-auth-sdk.md +100 -0
- package/docs/deprecated/insforge-db-api.md +359 -0
- package/docs/deprecated/insforge-db-sdk.md +140 -0
- package/docs/deprecated/insforge-debug-sdk.md +157 -0
- package/docs/deprecated/insforge-debug.md +65 -0
- package/docs/deprecated/insforge-instructions.md +124 -0
- package/docs/deprecated/insforge-project.md +118 -0
- package/docs/deprecated/insforge-storage-api.md +279 -0
- package/docs/deprecated/insforge-storage-sdk.md +159 -0
- package/docs/insforge-instructions-sdk.md +407 -0
- package/eslint.config.js +317 -0
- package/examples/oauth/frontend-oauth-example.html +251 -0
- package/examples/response-examples.md +444 -0
- package/frontend/README.md +112 -0
- package/frontend/components.json +17 -0
- package/frontend/index.html +13 -0
- package/frontend/package.json +63 -0
- package/frontend/public/favicon.ico +0 -0
- package/frontend/src/App.tsx +106 -0
- package/frontend/src/assets/icons/checkbox_checked.svg +6 -0
- package/frontend/src/assets/icons/checkbox_undetermined.svg +6 -0
- package/frontend/src/assets/icons/checked.svg +3 -0
- package/frontend/src/assets/icons/error.svg +3 -0
- package/frontend/src/assets/icons/pencil.svg +4 -0
- package/frontend/src/assets/icons/refresh.svg +4 -0
- package/frontend/src/assets/icons/step_active.svg +3 -0
- package/frontend/src/assets/icons/step_inactive.svg +11 -0
- package/frontend/src/assets/icons/warning.svg +3 -0
- package/frontend/src/assets/logos/amazon.svg +1 -0
- package/frontend/src/assets/logos/claude_code.svg +3 -0
- package/frontend/src/assets/logos/cline.svg +6 -0
- package/frontend/src/assets/logos/cursor.svg +20 -0
- package/frontend/src/assets/logos/discord.svg +9 -0
- package/frontend/src/assets/logos/gemini.svg +19 -0
- package/frontend/src/assets/logos/github.svg +5 -0
- package/frontend/src/assets/logos/google.svg +13 -0
- package/frontend/src/assets/logos/grok.svg +10 -0
- package/frontend/src/assets/logos/insforge_dark.svg +15 -0
- package/frontend/src/assets/logos/insforge_light.svg +15 -0
- package/frontend/src/assets/logos/openai.svg +10 -0
- package/frontend/src/assets/logos/roo_code.svg +9 -0
- package/frontend/src/assets/logos/trae.svg +3 -0
- package/frontend/src/assets/logos/windsurf.svg +10 -0
- package/frontend/src/components/ButtonWithLoading.tsx +27 -0
- package/frontend/src/components/Checkbox.tsx +61 -0
- package/frontend/src/components/CodeBlock.tsx +32 -0
- package/frontend/src/components/ConfirmDialog.tsx +96 -0
- package/frontend/src/components/CopyButton.tsx +69 -0
- package/frontend/src/components/DeleteActionButton.tsx +42 -0
- package/frontend/src/components/EmptyState.tsx +41 -0
- package/frontend/src/components/ErrorState.tsx +35 -0
- package/frontend/src/components/FeatureSidebar.tsx +126 -0
- package/frontend/src/components/FeatureSidebarItem.tsx +101 -0
- package/frontend/src/components/JsonHighlight.tsx +61 -0
- package/frontend/src/components/LoadingState.tsx +16 -0
- package/frontend/src/components/PaginationControls.tsx +54 -0
- package/frontend/src/components/PromptDialog.tsx +68 -0
- package/frontend/src/components/SearchInput.tsx +90 -0
- package/frontend/src/components/SelectionClearButton.tsx +26 -0
- package/frontend/src/components/Stepper.tsx +139 -0
- package/frontend/src/components/ThemeToggle.tsx +58 -0
- package/frontend/src/components/TypeBadge.tsx +20 -0
- package/frontend/src/components/datagrid/DataGrid.tsx +264 -0
- package/frontend/src/components/datagrid/DefaultCellRenderer.tsx +114 -0
- package/frontend/src/components/datagrid/IdCell.tsx +44 -0
- package/frontend/src/components/datagrid/SortableHeader.tsx +74 -0
- package/frontend/src/components/datagrid/cell-editors/BooleanCellEditor.tsx +54 -0
- package/frontend/src/components/datagrid/cell-editors/DateCellEditor.tsx +483 -0
- package/frontend/src/components/datagrid/cell-editors/JsonCellEditor.tsx +362 -0
- package/frontend/src/components/datagrid/cell-editors/TextCellEditor.tsx +38 -0
- package/frontend/src/components/datagrid/cell-editors/index.ts +14 -0
- package/frontend/src/components/datagrid/cell-editors/types.ts +43 -0
- package/frontend/src/components/datagrid/datagridTypes.tsx +72 -0
- package/frontend/src/components/datagrid/index.tsx +20 -0
- package/frontend/src/components/index.ts +39 -0
- package/frontend/src/components/layout/AppHeader.tsx +146 -0
- package/frontend/src/components/layout/AppSidebar.tsx +190 -0
- package/frontend/src/components/layout/CloudLayout.tsx +95 -0
- package/frontend/src/components/layout/Layout.tsx +43 -0
- package/frontend/src/components/radix/Alert.tsx +45 -0
- package/frontend/src/components/radix/AlertDialog.tsx +115 -0
- package/frontend/src/components/radix/Avatar.tsx +45 -0
- package/frontend/src/components/radix/Badge.tsx +33 -0
- package/frontend/src/components/radix/Button.tsx +50 -0
- package/frontend/src/components/radix/Card.tsx +58 -0
- package/frontend/src/components/radix/Dialog.tsx +98 -0
- package/frontend/src/components/radix/DropdownMenu.tsx +185 -0
- package/frontend/src/components/radix/Form.tsx +167 -0
- package/frontend/src/components/radix/Input.tsx +22 -0
- package/frontend/src/components/radix/Label.tsx +19 -0
- package/frontend/src/components/radix/Popover.tsx +29 -0
- package/frontend/src/components/radix/ScrollArea.tsx +44 -0
- package/frontend/src/components/radix/Select.tsx +151 -0
- package/frontend/src/components/radix/Separator.tsx +26 -0
- package/frontend/src/components/radix/Sheet.tsx +119 -0
- package/frontend/src/components/radix/Skeleton.tsx +7 -0
- package/frontend/src/components/radix/Switch.tsx +29 -0
- package/frontend/src/components/radix/Tabs.tsx +50 -0
- package/frontend/src/components/radix/Textarea.tsx +21 -0
- package/frontend/src/components/radix/Tooltip.tsx +28 -0
- package/frontend/src/features/ai/components/AIConfigCard.tsx +154 -0
- package/frontend/src/features/ai/components/AIConfigDialog.tsx +76 -0
- package/frontend/src/features/ai/components/AIConfigForm.tsx +222 -0
- package/frontend/src/features/ai/components/AIEmptyState.tsx +18 -0
- package/frontend/src/features/ai/components/fields/ModalityField.tsx +87 -0
- package/frontend/src/features/ai/components/fields/ModelSelectionField.tsx +134 -0
- package/frontend/src/features/ai/components/fields/SystemPromptField.tsx +33 -0
- package/frontend/src/features/ai/helpers.ts +155 -0
- package/frontend/src/features/ai/hooks/useAIConfigs.ts +221 -0
- package/frontend/src/features/ai/hooks/useAIUsage.ts +77 -0
- package/frontend/src/features/ai/page/AIPage.tsx +178 -0
- package/frontend/src/features/ai/services/ai.service.ts +148 -0
- package/frontend/src/features/auth/components/AddOAuthDialog.tsx +106 -0
- package/frontend/src/features/auth/components/AuthMethodTab.tsx +238 -0
- package/frontend/src/features/auth/components/OAuthConfigDialog.tsx +303 -0
- package/frontend/src/features/auth/components/OAuthEmptyState.tsx +15 -0
- package/frontend/src/features/auth/components/UserFormDialog.tsx +248 -0
- package/frontend/src/features/auth/components/UsersDataGrid.tsx +183 -0
- package/frontend/src/features/auth/components/UsersTab.tsx +114 -0
- package/frontend/src/features/auth/hooks/useOAuthConfig.ts +129 -0
- package/frontend/src/features/auth/hooks/useUsers.ts +57 -0
- package/frontend/src/features/auth/index.ts +9 -0
- package/frontend/src/features/auth/page/AuthenticationPage.tsx +169 -0
- package/frontend/src/features/auth/services/auth.service.ts +112 -0
- package/frontend/src/features/auth/services/oauth.service.ts +49 -0
- package/frontend/src/features/dashboard/page/DashboardPage.tsx +194 -0
- package/frontend/src/features/database/components/ColumnTypeSelect.tsx +64 -0
- package/frontend/src/features/database/components/DatabaseDataGrid.tsx +282 -0
- package/frontend/src/features/database/components/ForeignKeyCell.tsx +187 -0
- package/frontend/src/features/database/components/ForeignKeyPopover.tsx +378 -0
- package/frontend/src/features/database/components/LinkRecordModal.tsx +288 -0
- package/frontend/src/features/database/components/RecordFormDialog.tsx +164 -0
- package/frontend/src/features/database/components/RecordFormField.tsx +568 -0
- package/frontend/src/features/database/components/TableEmptyState.tsx +21 -0
- package/frontend/src/features/database/components/TableForm.tsx +656 -0
- package/frontend/src/features/database/components/TableFormColumn.tsx +137 -0
- package/frontend/src/features/database/components/TableListSkeleton.tsx +9 -0
- package/frontend/src/features/database/components/TableSidebar.tsx +47 -0
- package/frontend/src/features/database/constants.ts +26 -0
- package/frontend/src/features/database/helpers.ts +125 -0
- package/frontend/src/features/database/hooks/UseLinkModal.tsx +78 -0
- package/frontend/src/features/database/index.ts +12 -0
- package/frontend/src/features/database/page/DatabasePage.tsx +626 -0
- package/frontend/src/features/database/schema.ts +25 -0
- package/frontend/src/features/database/services/database.service.ts +216 -0
- package/frontend/src/features/functions/components/FunctionEmptyState.tsx +15 -0
- package/frontend/src/features/functions/components/FunctionRow.tsx +71 -0
- package/frontend/src/features/functions/components/FunctionViewer.tsx +46 -0
- package/frontend/src/features/functions/components/FunctionsContent.tsx +88 -0
- package/frontend/src/features/functions/components/FunctionsSidebar.tsx +56 -0
- package/frontend/src/features/functions/components/SecretEmptyState.tsx +23 -0
- package/frontend/src/features/functions/components/SecretRow.tsx +68 -0
- package/frontend/src/features/functions/components/SecretsContent.tsx +120 -0
- package/frontend/src/features/functions/hooks/useFunctions.ts +106 -0
- package/frontend/src/features/functions/page/FunctionsPage.tsx +28 -0
- package/frontend/src/features/functions/services/functions.service.ts +48 -0
- package/frontend/src/features/login/components/AuthErrorBoundary.tsx +87 -0
- package/frontend/src/features/login/components/PrivateRoute.tsx +24 -0
- package/frontend/src/features/login/page/CloudLoginPage.tsx +93 -0
- package/frontend/src/features/login/page/LoginPage.tsx +174 -0
- package/frontend/src/features/logs/components/AnalyticsLogsTable.tsx +313 -0
- package/frontend/src/features/logs/components/LogsTable.tsx +199 -0
- package/frontend/src/features/logs/hooks/useAuditLogs.ts +39 -0
- package/frontend/src/features/logs/index.ts +5 -0
- package/frontend/src/features/logs/page/AnalyticsLogsPage.tsx +530 -0
- package/frontend/src/features/logs/page/AuditsPage.tsx +192 -0
- package/frontend/src/features/logs/services/log.service.ts +171 -0
- package/frontend/src/features/metadata/hooks/useMetadata.ts +53 -0
- package/frontend/src/features/metadata/index.ts +0 -0
- package/frontend/src/features/metadata/page/MetadataPage.tsx +136 -0
- package/frontend/src/features/metadata/services/metadata.service.ts +17 -0
- package/frontend/src/features/onboard/components/CompletionCard.tsx +41 -0
- package/frontend/src/features/onboard/components/OnboardButton.tsx +84 -0
- package/frontend/src/features/onboard/components/StepContent.tsx +91 -0
- package/frontend/src/features/onboard/components/TestConnectionStep.tsx +53 -0
- package/frontend/src/features/onboard/components/mcp/CursorDeeplinkGenerator.tsx +35 -0
- package/frontend/src/features/onboard/components/mcp/McpInstallation.tsx +144 -0
- package/frontend/src/features/onboard/components/mcp/index.ts +4 -0
- package/frontend/src/features/onboard/components/mcp/mcp-helper.tsx +98 -0
- package/frontend/src/features/onboard/index.ts +3 -0
- package/frontend/src/features/onboard/page/OnBoardPage.tsx +104 -0
- package/frontend/src/features/onboard/types.ts +8 -0
- package/frontend/src/features/secrets/hooks/useSecrets.ts +139 -0
- package/frontend/src/features/secrets/services/secrets.service.ts +57 -0
- package/frontend/src/features/storage/components/BucketEmptyState.tsx +19 -0
- package/frontend/src/features/storage/components/BucketFormDialog.tsx +194 -0
- package/frontend/src/features/storage/components/BucketListSkeleton.tsx +17 -0
- package/frontend/src/features/storage/components/FilePreviewDialog.tsx +287 -0
- package/frontend/src/features/storage/components/StorageDataGrid.tsx +239 -0
- package/frontend/src/features/storage/components/StorageManager.tsx +236 -0
- package/frontend/src/features/storage/components/StorageSidebar.tsx +44 -0
- package/frontend/src/features/storage/components/UploadToast.tsx +46 -0
- package/frontend/src/features/storage/index.ts +3 -0
- package/frontend/src/features/storage/page/StoragePage.tsx +553 -0
- package/frontend/src/features/storage/services/storage.service.ts +144 -0
- package/frontend/src/features/visualizer/components/AuthNode.tsx +107 -0
- package/frontend/src/features/visualizer/components/BucketNode.tsx +34 -0
- package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +359 -0
- package/frontend/src/features/visualizer/components/TableNode.tsx +152 -0
- package/frontend/src/features/visualizer/components/VisualizerSkeleton.tsx +24 -0
- package/frontend/src/features/visualizer/components/index.ts +5 -0
- package/frontend/src/features/visualizer/page/VisualizerPage.tsx +127 -0
- package/frontend/src/index.css +248 -0
- package/frontend/src/lib/api/client.ts +163 -0
- package/frontend/src/lib/contexts/AuthContext.tsx +157 -0
- package/frontend/src/lib/contexts/OnboardStepContext.tsx +68 -0
- package/frontend/src/lib/contexts/SocketContext.tsx +303 -0
- package/frontend/src/lib/contexts/ThemeContext.tsx +125 -0
- package/frontend/src/lib/hooks/useAuth.ts +4 -0
- package/frontend/src/lib/hooks/useConfirm.ts +55 -0
- package/frontend/src/lib/hooks/useInterval.ts +27 -0
- package/frontend/src/lib/hooks/useMediaQuery.ts +59 -0
- package/frontend/src/lib/hooks/useOnboardingCompletion.ts +29 -0
- package/frontend/src/lib/hooks/usePagination.ts +27 -0
- package/frontend/src/lib/hooks/useTimeout.ts +27 -0
- package/frontend/src/lib/hooks/useToast.tsx +229 -0
- package/frontend/src/lib/utils/constants.ts +38 -0
- package/frontend/src/lib/utils/utils.ts +165 -0
- package/frontend/src/lib/utils/validation-schemas.ts +126 -0
- package/frontend/src/main.tsx +16 -0
- package/frontend/src/rdg.css +194 -0
- package/frontend/src/vite-env.d.ts +12 -0
- package/frontend/tailwind.config.js +97 -0
- package/frontend/tsconfig.json +26 -0
- package/frontend/tsconfig.node.json +10 -0
- package/frontend/vite.config.ts +37 -0
- package/frontend/vitest.config.ts +36 -0
- package/functions/deno.json +25 -0
- package/functions/server.ts +290 -0
- package/functions/worker-template.js +126 -0
- package/openapi/ai.yaml +689 -0
- package/openapi/auth.yaml +563 -0
- package/openapi/functions.yaml +476 -0
- package/openapi/health.yaml +30 -0
- package/openapi/logs.yaml +224 -0
- package/openapi/metadata.yaml +178 -0
- package/openapi/records.yaml +382 -0
- package/openapi/secrets.yaml +371 -0
- package/openapi/storage.yaml +876 -0
- package/openapi/tables.yaml +464 -0
- package/package.json +88 -0
- package/shared-schemas/package.json +31 -0
- package/shared-schemas/src/ai-api.schema.ts +167 -0
- package/shared-schemas/src/ai.schema.ts +54 -0
- package/shared-schemas/src/auth-api.schema.ts +193 -0
- package/shared-schemas/src/auth.schema.ts +94 -0
- package/shared-schemas/src/database-api.schema.ts +259 -0
- package/shared-schemas/src/database.schema.ts +69 -0
- package/shared-schemas/src/functions-api.schema.ts +25 -0
- package/shared-schemas/src/functions.schema.ts +16 -0
- package/shared-schemas/src/index.ts +13 -0
- package/shared-schemas/src/logs-api.schema.ts +49 -0
- package/shared-schemas/src/logs.schema.ts +14 -0
- package/shared-schemas/src/metadata.schema.ts +56 -0
- package/shared-schemas/src/storage-api.schema.ts +65 -0
- package/shared-schemas/src/storage.schema.ts +19 -0
- package/shared-schemas/tsconfig.json +21 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { Router, Request, Response, NextFunction } from 'express';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { verifyAdmin, AuthRequest, verifyUser } from '@/api/middleware/auth.js';
|
|
4
|
+
import { AppError } from '@/api/middleware/error.js';
|
|
5
|
+
import { StorageService } from '@/core/storage/storage.js';
|
|
6
|
+
import { DatabaseManager } from '@/core/database/manager.js';
|
|
7
|
+
import { successResponse } from '@/utils/response.js';
|
|
8
|
+
import { upload, handleUploadError } from '@/api/middleware/upload.js';
|
|
9
|
+
import { ERROR_CODES } from '@/types/error-constants.js';
|
|
10
|
+
import {
|
|
11
|
+
StorageBucketSchema,
|
|
12
|
+
createBucketRequestSchema,
|
|
13
|
+
updateBucketRequestSchema,
|
|
14
|
+
} from '@insforge/shared-schemas';
|
|
15
|
+
import { SocketService } from '@/core/socket/socket';
|
|
16
|
+
import { DataUpdateResourceType, ServerEvents } from '@/core/socket/types';
|
|
17
|
+
import { AuditService } from '@/core/logs/audit.js';
|
|
18
|
+
|
|
19
|
+
const router = Router();
|
|
20
|
+
const auditService = AuditService.getInstance();
|
|
21
|
+
|
|
22
|
+
// Middleware to conditionally apply authentication based on bucket visibility
|
|
23
|
+
const conditionalAuth = async (req: Request, res: Response, next: NextFunction) => {
|
|
24
|
+
// For GET and HEAD requests to download objects, check if bucket is public
|
|
25
|
+
if ((req.method === 'GET' || req.method === 'HEAD') && req.params.bucketName) {
|
|
26
|
+
try {
|
|
27
|
+
const storageService = StorageService.getInstance();
|
|
28
|
+
const isPublic = await storageService.isBucketPublic(req.params.bucketName);
|
|
29
|
+
|
|
30
|
+
if (isPublic) {
|
|
31
|
+
// Public bucket - skip authentication
|
|
32
|
+
return next();
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// If error checking bucket, continue with auth requirement
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// All other cases require authentication
|
|
40
|
+
return verifyUser(req, res, next);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// GET /api/storage/buckets - List all buckets (requires auth)
|
|
44
|
+
router.get('/buckets', verifyAdmin, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
45
|
+
try {
|
|
46
|
+
const db = DatabaseManager.getInstance().getDb();
|
|
47
|
+
|
|
48
|
+
// Get all buckets with their metadata from _storage_buckets table
|
|
49
|
+
const buckets = (await db
|
|
50
|
+
.prepare('SELECT name, public, created_at FROM _storage_buckets ORDER BY name')
|
|
51
|
+
.all()) as StorageBucketSchema[];
|
|
52
|
+
|
|
53
|
+
// Traditional REST: return array directly
|
|
54
|
+
successResponse(res, buckets);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
next(error);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// POST /api/storage/buckets - Create a new bucket (requires auth)
|
|
61
|
+
router.post(
|
|
62
|
+
'/buckets',
|
|
63
|
+
verifyAdmin,
|
|
64
|
+
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
65
|
+
try {
|
|
66
|
+
const validation = createBucketRequestSchema.safeParse(req.body);
|
|
67
|
+
if (!validation.success) {
|
|
68
|
+
throw new AppError(
|
|
69
|
+
validation.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
|
|
70
|
+
400,
|
|
71
|
+
ERROR_CODES.STORAGE_INVALID_PARAMETER,
|
|
72
|
+
'Please check the request body, it must conform with the CreateBucketRequest schema.'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const { bucketName, isPublic } = validation.data;
|
|
76
|
+
|
|
77
|
+
const storageService = StorageService.getInstance();
|
|
78
|
+
await storageService.createBucket(bucketName, isPublic);
|
|
79
|
+
|
|
80
|
+
// Log audit for bucket creation
|
|
81
|
+
await auditService.log({
|
|
82
|
+
actor: req.user?.email || 'api-key',
|
|
83
|
+
action: 'CREATE_BUCKET',
|
|
84
|
+
module: 'STORAGE',
|
|
85
|
+
details: {
|
|
86
|
+
bucketName,
|
|
87
|
+
isPublic,
|
|
88
|
+
},
|
|
89
|
+
ip_address: req.ip,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const socket = SocketService.getInstance();
|
|
93
|
+
socket.broadcastToRoom('role:project_admin', ServerEvents.DATA_UPDATE, {
|
|
94
|
+
resource: DataUpdateResourceType.STORAGE_SCHEMA,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const accessInfo = isPublic
|
|
98
|
+
? 'This is a PUBLIC bucket - objects can be accessed without authentication.'
|
|
99
|
+
: 'This is a PRIVATE bucket - authentication is required to access objects.';
|
|
100
|
+
|
|
101
|
+
successResponse(
|
|
102
|
+
res,
|
|
103
|
+
{
|
|
104
|
+
message: 'Bucket created successfully',
|
|
105
|
+
bucketName,
|
|
106
|
+
isPublic: isPublic,
|
|
107
|
+
nextActions: `${accessInfo} You can use /api/storage/buckets/:bucketName/objects/:objectKey to upload an object to the bucket, and /api/storage/buckets/:bucketName/objects to list the objects in the bucket.`,
|
|
108
|
+
},
|
|
109
|
+
201
|
|
110
|
+
);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error instanceof Error && error.message.includes('already exists')) {
|
|
113
|
+
next(new AppError(error.message, 409, ERROR_CODES.ALREADY_EXISTS));
|
|
114
|
+
} else if (error instanceof Error && error.message.includes('Invalid bucket name')) {
|
|
115
|
+
next(
|
|
116
|
+
new AppError(
|
|
117
|
+
error.message,
|
|
118
|
+
400,
|
|
119
|
+
ERROR_CODES.STORAGE_INVALID_PARAMETER,
|
|
120
|
+
'Please check the bucket name, it must be a valid bucket name'
|
|
121
|
+
)
|
|
122
|
+
);
|
|
123
|
+
} else {
|
|
124
|
+
next(error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// PATCH /api/storage/buckets/:bucketName - Update bucket (requires auth)
|
|
131
|
+
router.patch(
|
|
132
|
+
'/buckets/:bucketName',
|
|
133
|
+
verifyAdmin,
|
|
134
|
+
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
135
|
+
try {
|
|
136
|
+
const { bucketName } = req.params;
|
|
137
|
+
const validation = updateBucketRequestSchema.safeParse(req.body);
|
|
138
|
+
if (!validation.success) {
|
|
139
|
+
throw new AppError(
|
|
140
|
+
validation.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
|
|
141
|
+
400,
|
|
142
|
+
ERROR_CODES.STORAGE_INVALID_PARAMETER,
|
|
143
|
+
'Please check the request body, it must conform with the UpdateBucketRequest schema.'
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const { isPublic } = validation.data;
|
|
147
|
+
|
|
148
|
+
const storageService = StorageService.getInstance();
|
|
149
|
+
await storageService.updateBucketVisibility(bucketName, isPublic);
|
|
150
|
+
|
|
151
|
+
// Log audit for bucket update
|
|
152
|
+
await auditService.log({
|
|
153
|
+
actor: req.user?.email || 'api-key',
|
|
154
|
+
action: 'UPDATE_BUCKET',
|
|
155
|
+
module: 'STORAGE',
|
|
156
|
+
details: {
|
|
157
|
+
bucketName,
|
|
158
|
+
isPublic,
|
|
159
|
+
},
|
|
160
|
+
ip_address: req.ip,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const socket = SocketService.getInstance();
|
|
164
|
+
socket.broadcastToRoom('role:project_admin', ServerEvents.DATA_UPDATE, {
|
|
165
|
+
resource: DataUpdateResourceType.BUCKET_SCHEMA,
|
|
166
|
+
data: {
|
|
167
|
+
name: bucketName,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const accessInfo = isPublic
|
|
172
|
+
? 'Bucket is now PUBLIC - objects can be accessed without authentication.'
|
|
173
|
+
: 'Bucket is now PRIVATE - authentication is required to access objects.';
|
|
174
|
+
|
|
175
|
+
successResponse(
|
|
176
|
+
res,
|
|
177
|
+
{
|
|
178
|
+
message: 'Bucket visibility updated',
|
|
179
|
+
bucket: bucketName,
|
|
180
|
+
isPublic: isPublic,
|
|
181
|
+
nextActions: accessInfo,
|
|
182
|
+
},
|
|
183
|
+
200
|
|
184
|
+
);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error instanceof Error && error.message.includes('does not exist')) {
|
|
187
|
+
next(new AppError(error.message, 404, ERROR_CODES.NOT_FOUND));
|
|
188
|
+
} else {
|
|
189
|
+
next(error);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// GET /api/storage/buckets/:bucketName/objects - List objects in bucket (requires auth)
|
|
196
|
+
router.get(
|
|
197
|
+
'/buckets/:bucketName/objects',
|
|
198
|
+
verifyAdmin,
|
|
199
|
+
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
200
|
+
try {
|
|
201
|
+
const { bucketName } = req.params;
|
|
202
|
+
const prefix = req.query.prefix as string;
|
|
203
|
+
const searchQuery = req.query.search as string;
|
|
204
|
+
const limit = Math.min(parseInt(req.query.limit as string) || 100, 1000);
|
|
205
|
+
const offset = parseInt(req.query.offset as string) || 0;
|
|
206
|
+
|
|
207
|
+
const storageService = StorageService.getInstance();
|
|
208
|
+
const result = await storageService.listObjects(
|
|
209
|
+
bucketName,
|
|
210
|
+
prefix,
|
|
211
|
+
limit,
|
|
212
|
+
offset,
|
|
213
|
+
searchQuery
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
successResponse(
|
|
217
|
+
res,
|
|
218
|
+
{
|
|
219
|
+
data: result.objects,
|
|
220
|
+
pagination: {
|
|
221
|
+
offset: offset,
|
|
222
|
+
limit: limit,
|
|
223
|
+
total: result.total,
|
|
224
|
+
},
|
|
225
|
+
nextActions:
|
|
226
|
+
'You can use PUT /api/storage/buckets/:bucketName/objects/:objectKey to upload with a specific key, or POST /api/storage/buckets/:bucketName/objects to upload with auto-generated key, and GET /api/storage/buckets/:bucketName/objects/:objectKey to download an object.',
|
|
227
|
+
},
|
|
228
|
+
200
|
|
229
|
+
);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
next(error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// PUT /api/storage/buckets/:bucketName/objects/:objectKey - Upload object to bucket (requires auth)
|
|
237
|
+
router.put(
|
|
238
|
+
'/buckets/:bucketName/objects/*',
|
|
239
|
+
verifyUser,
|
|
240
|
+
upload.single('file'),
|
|
241
|
+
handleUploadError,
|
|
242
|
+
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
243
|
+
try {
|
|
244
|
+
const { bucketName } = req.params;
|
|
245
|
+
const objectKey = req.params[0]; // Everything after objects
|
|
246
|
+
|
|
247
|
+
if (!objectKey) {
|
|
248
|
+
throw new AppError('Object key is required', 400, ERROR_CODES.STORAGE_INVALID_PARAMETER);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!req.file) {
|
|
252
|
+
throw new AppError('File is required', 400, ERROR_CODES.STORAGE_INVALID_PARAMETER);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const storageService = StorageService.getInstance();
|
|
256
|
+
const storedFile = await storageService.putObject(
|
|
257
|
+
bucketName,
|
|
258
|
+
objectKey,
|
|
259
|
+
req.file,
|
|
260
|
+
req.user?.id
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
successResponse(res, storedFile, 201);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (error instanceof Error && error.message.includes('already exists')) {
|
|
266
|
+
next(new AppError(error.message, 409, ERROR_CODES.ALREADY_EXISTS));
|
|
267
|
+
} else if (error instanceof Error && error.message.includes('Invalid')) {
|
|
268
|
+
next(new AppError(error.message, 400, ERROR_CODES.STORAGE_INVALID_PARAMETER));
|
|
269
|
+
} else {
|
|
270
|
+
next(error);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// POST /api/storage/buckets/:bucketName/objects - Upload object with server-generated key (requires auth)
|
|
277
|
+
router.post(
|
|
278
|
+
'/buckets/:bucketName/objects',
|
|
279
|
+
verifyUser,
|
|
280
|
+
upload.single('file'),
|
|
281
|
+
handleUploadError,
|
|
282
|
+
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
283
|
+
try {
|
|
284
|
+
const { bucketName } = req.params;
|
|
285
|
+
|
|
286
|
+
if (!req.file) {
|
|
287
|
+
throw new AppError('File is required', 400, ERROR_CODES.STORAGE_INVALID_PARAMETER);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Generate a unique key for the object
|
|
291
|
+
const timestamp = Date.now();
|
|
292
|
+
const randomStr = Math.random().toString(36).substring(2, 8);
|
|
293
|
+
const fileExt = req.file.originalname ? path.extname(req.file.originalname) : '';
|
|
294
|
+
const baseName = req.file.originalname
|
|
295
|
+
? path.basename(req.file.originalname, fileExt)
|
|
296
|
+
: 'file';
|
|
297
|
+
const sanitizedBaseName = baseName.replace(/[^a-zA-Z0-9-_]/g, '-').substring(0, 32);
|
|
298
|
+
const objectKey = `${sanitizedBaseName}-${timestamp}-${randomStr}${fileExt}`;
|
|
299
|
+
|
|
300
|
+
const storageService = StorageService.getInstance();
|
|
301
|
+
const storedFile = await storageService.putObject(
|
|
302
|
+
bucketName,
|
|
303
|
+
objectKey,
|
|
304
|
+
req.file,
|
|
305
|
+
req.user?.id
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
successResponse(res, storedFile, 201);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error instanceof Error && error.message.includes('does not exist')) {
|
|
311
|
+
next(
|
|
312
|
+
new AppError(
|
|
313
|
+
'Bucket does not exist',
|
|
314
|
+
404,
|
|
315
|
+
ERROR_CODES.NOT_FOUND,
|
|
316
|
+
'Create the bucket first using POST /api/storage/buckets'
|
|
317
|
+
)
|
|
318
|
+
);
|
|
319
|
+
} else if (error instanceof Error && error.message.includes('Invalid')) {
|
|
320
|
+
next(new AppError(error.message, 400, ERROR_CODES.STORAGE_INVALID_PARAMETER));
|
|
321
|
+
} else {
|
|
322
|
+
next(error);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// GET /api/storage/buckets/:bucketName/objects/:objectKey - Download object from bucket (conditional auth)
|
|
329
|
+
router.get(
|
|
330
|
+
'/buckets/:bucketName/objects/*',
|
|
331
|
+
conditionalAuth,
|
|
332
|
+
async (req: AuthRequest | Request, res: Response, next: NextFunction) => {
|
|
333
|
+
try {
|
|
334
|
+
const { bucketName } = req.params;
|
|
335
|
+
const objectKey = req.params[0]; // Everything after objects
|
|
336
|
+
|
|
337
|
+
if (!objectKey) {
|
|
338
|
+
throw new AppError('Object key is required', 400, ERROR_CODES.STORAGE_INVALID_PARAMETER);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const storageService = StorageService.getInstance();
|
|
342
|
+
const expiresIn = (await storageService.isBucketPublic(bucketName)) ? 0 : 3600;
|
|
343
|
+
const strategy = await storageService.getDownloadStrategy(
|
|
344
|
+
bucketName,
|
|
345
|
+
objectKey,
|
|
346
|
+
Number(expiresIn)
|
|
347
|
+
);
|
|
348
|
+
if (strategy.method === 'presigned') {
|
|
349
|
+
return res.redirect(strategy.url);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const result = await storageService.getObject(bucketName, objectKey);
|
|
353
|
+
if (!result) {
|
|
354
|
+
throw new AppError('Object not found', 404, ERROR_CODES.NOT_FOUND);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const { file, metadata } = result;
|
|
358
|
+
|
|
359
|
+
// Set appropriate headers
|
|
360
|
+
res.setHeader('Content-Type', metadata.mimeType || 'application/octet-stream');
|
|
361
|
+
res.setHeader('Content-Length', file.length.toString());
|
|
362
|
+
|
|
363
|
+
// Send object content
|
|
364
|
+
res.send(file);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
if (error instanceof Error && error.message.includes('Invalid')) {
|
|
367
|
+
next(new AppError(error.message, 400, ERROR_CODES.STORAGE_INVALID_PARAMETER));
|
|
368
|
+
} else {
|
|
369
|
+
next(error);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// DELETE /api/storage/buckets/:bucketName - Delete entire bucket (requires auth)
|
|
376
|
+
router.delete(
|
|
377
|
+
'/buckets/:bucketName',
|
|
378
|
+
verifyAdmin,
|
|
379
|
+
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
380
|
+
try {
|
|
381
|
+
const { bucketName } = req.params;
|
|
382
|
+
const storageService = StorageService.getInstance();
|
|
383
|
+
const deleted = await storageService.deleteBucket(bucketName);
|
|
384
|
+
|
|
385
|
+
if (!deleted) {
|
|
386
|
+
throw new AppError('Bucket not found or already empty', 404, ERROR_CODES.NOT_FOUND);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Log audit for bucket deletion
|
|
390
|
+
await auditService.log({
|
|
391
|
+
actor: req.user?.email || 'api-key',
|
|
392
|
+
action: 'DELETE_BUCKET',
|
|
393
|
+
module: 'STORAGE',
|
|
394
|
+
details: {
|
|
395
|
+
bucketName,
|
|
396
|
+
},
|
|
397
|
+
ip_address: req.ip,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const socket = SocketService.getInstance();
|
|
401
|
+
socket.broadcastToRoom('role:project_admin', ServerEvents.DATA_UPDATE, {
|
|
402
|
+
resource: DataUpdateResourceType.STORAGE_SCHEMA,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
successResponse(
|
|
406
|
+
res,
|
|
407
|
+
{
|
|
408
|
+
message: 'Bucket deleted successfully',
|
|
409
|
+
nextActions:
|
|
410
|
+
'You can use POST /api/storage/buckets to create a new bucket, and GET /api/storage/buckets/:bucketName/objects to list the objects in the bucket.',
|
|
411
|
+
},
|
|
412
|
+
200
|
|
413
|
+
);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
next(error);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
// DELETE /api/storage/buckets/:bucketName/objects/:objectKey - Delete object from bucket (requires auth)
|
|
421
|
+
router.delete(
|
|
422
|
+
'/buckets/:bucketName/objects/*',
|
|
423
|
+
verifyUser,
|
|
424
|
+
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
425
|
+
try {
|
|
426
|
+
const { bucketName } = req.params;
|
|
427
|
+
const objectKey = req.params[0]; // Everything after objects
|
|
428
|
+
|
|
429
|
+
if (!objectKey) {
|
|
430
|
+
throw new AppError('Object key is required', 400, ERROR_CODES.STORAGE_INVALID_PARAMETER);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Delete specific object
|
|
434
|
+
const storageService = StorageService.getInstance();
|
|
435
|
+
const deleted = await storageService.deleteObject(bucketName, objectKey, req.user?.id);
|
|
436
|
+
|
|
437
|
+
if (!deleted) {
|
|
438
|
+
throw new AppError('Object not found', 404, ERROR_CODES.NOT_FOUND);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
successResponse(res, { message: 'Object deleted successfully' });
|
|
442
|
+
} catch (error) {
|
|
443
|
+
if (error instanceof Error && error.message.includes('Invalid')) {
|
|
444
|
+
next(new AppError(error.message, 400, ERROR_CODES.STORAGE_INVALID_PARAMETER));
|
|
445
|
+
} else {
|
|
446
|
+
next(error);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
// POST /api/storage/buckets/:bucketName/upload-strategy - Get upload strategy (presigned or direct)
|
|
453
|
+
router.post(
|
|
454
|
+
'/buckets/:bucketName/upload-strategy',
|
|
455
|
+
verifyUser,
|
|
456
|
+
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
457
|
+
try {
|
|
458
|
+
const { bucketName } = req.params;
|
|
459
|
+
const { filename, contentType, size } = req.body;
|
|
460
|
+
|
|
461
|
+
if (!filename) {
|
|
462
|
+
throw new AppError('Filename is required', 400, ERROR_CODES.STORAGE_INVALID_PARAMETER);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const storageService = StorageService.getInstance();
|
|
466
|
+
const strategy = await storageService.getUploadStrategy(bucketName, {
|
|
467
|
+
filename,
|
|
468
|
+
contentType,
|
|
469
|
+
size,
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
successResponse(res, strategy);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
if (error instanceof Error && error.message.includes('does not exist')) {
|
|
475
|
+
next(new AppError(error.message, 404, ERROR_CODES.NOT_FOUND));
|
|
476
|
+
} else {
|
|
477
|
+
next(error);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
// POST /api/storage/buckets/:bucketName/objects/:objectKey/confirm-upload - Confirm presigned upload
|
|
484
|
+
router.post(
|
|
485
|
+
'/buckets/:bucketName/objects/:objectKey/confirm-upload',
|
|
486
|
+
verifyUser,
|
|
487
|
+
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
488
|
+
try {
|
|
489
|
+
const { bucketName, objectKey } = req.params;
|
|
490
|
+
const { size, contentType, etag } = req.body;
|
|
491
|
+
|
|
492
|
+
if (!size) {
|
|
493
|
+
throw new AppError('Size is required', 400, ERROR_CODES.STORAGE_INVALID_PARAMETER);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const storageService = StorageService.getInstance();
|
|
497
|
+
const fileInfo = await storageService.confirmUpload(
|
|
498
|
+
bucketName,
|
|
499
|
+
objectKey,
|
|
500
|
+
{
|
|
501
|
+
size,
|
|
502
|
+
contentType,
|
|
503
|
+
etag,
|
|
504
|
+
},
|
|
505
|
+
req.user?.id
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
successResponse(res, fileInfo, 201);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
if (error instanceof Error && error.message.includes('not found')) {
|
|
511
|
+
next(new AppError(error.message, 404, ERROR_CODES.NOT_FOUND));
|
|
512
|
+
} else if (error instanceof Error && error.message.includes('already confirmed')) {
|
|
513
|
+
next(new AppError(error.message, 409, ERROR_CODES.ALREADY_EXISTS));
|
|
514
|
+
} else {
|
|
515
|
+
next(error);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
// POST /api/storage/buckets/:bucketName/objects/:objectKey/download-strategy - Get download URL (presigned or direct)
|
|
522
|
+
router.post(
|
|
523
|
+
'/buckets/:bucketName/objects/:objectKey/download-strategy',
|
|
524
|
+
conditionalAuth,
|
|
525
|
+
async (req: AuthRequest | Request, res: Response, next: NextFunction) => {
|
|
526
|
+
try {
|
|
527
|
+
const { bucketName, objectKey } = req.params;
|
|
528
|
+
const { expiresIn = 3600 } = req.body;
|
|
529
|
+
|
|
530
|
+
const storageService = StorageService.getInstance();
|
|
531
|
+
const strategy = await storageService.getDownloadStrategy(
|
|
532
|
+
bucketName,
|
|
533
|
+
objectKey,
|
|
534
|
+
Number(expiresIn)
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
successResponse(res, strategy);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
if (error instanceof Error && error.message.includes('Invalid')) {
|
|
540
|
+
next(new AppError(error.message, 400, ERROR_CODES.STORAGE_INVALID_PARAMETER));
|
|
541
|
+
} else {
|
|
542
|
+
next(error);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
);
|
|
547
|
+
export { router as storageRouter };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { DatabaseManager } from '@/core/database/manager.js';
|
|
3
|
+
import { verifyApiKey, verifyCloudBackend } from '@/api/middleware/auth.js';
|
|
4
|
+
|
|
5
|
+
export const usageRouter = Router();
|
|
6
|
+
|
|
7
|
+
// Create MCP tool usage record
|
|
8
|
+
usageRouter.post('/mcp', verifyApiKey, async (req, res, next) => {
|
|
9
|
+
try {
|
|
10
|
+
const { tool_name, success = true } = req.body;
|
|
11
|
+
|
|
12
|
+
if (!tool_name) {
|
|
13
|
+
return res.status(400).json({
|
|
14
|
+
error: 'VALIDATION_ERROR',
|
|
15
|
+
message: 'tool_name is required',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const dbManager = DatabaseManager.getInstance();
|
|
20
|
+
const db = dbManager.getDb();
|
|
21
|
+
|
|
22
|
+
// Insert MCP usage record directly
|
|
23
|
+
await db
|
|
24
|
+
.prepare(
|
|
25
|
+
`
|
|
26
|
+
INSERT INTO _mcp_usage (tool_name, success)
|
|
27
|
+
VALUES ($1, $2)
|
|
28
|
+
`
|
|
29
|
+
)
|
|
30
|
+
.run(tool_name, success);
|
|
31
|
+
|
|
32
|
+
res.json({ success: true });
|
|
33
|
+
} catch (error) {
|
|
34
|
+
next(error);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Get usage statistics (called by cloud backend)
|
|
39
|
+
usageRouter.get('/stats', verifyCloudBackend, async (req, res, next) => {
|
|
40
|
+
try {
|
|
41
|
+
const { start_date, end_date } = req.query;
|
|
42
|
+
|
|
43
|
+
if (!start_date || !end_date) {
|
|
44
|
+
return res.status(400).json({
|
|
45
|
+
error: 'VALIDATION_ERROR',
|
|
46
|
+
message: 'start_date and end_date are required',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const dbManager = DatabaseManager.getInstance();
|
|
51
|
+
const db = dbManager.getDb();
|
|
52
|
+
|
|
53
|
+
// Get MCP tool usage count within date range
|
|
54
|
+
const mcpResult = await db
|
|
55
|
+
.prepare(
|
|
56
|
+
`
|
|
57
|
+
SELECT COUNT(*) as count
|
|
58
|
+
FROM _mcp_usage
|
|
59
|
+
WHERE success = true
|
|
60
|
+
AND created_at >= $1
|
|
61
|
+
AND created_at < $2
|
|
62
|
+
`
|
|
63
|
+
)
|
|
64
|
+
.get(new Date(start_date as string), new Date(end_date as string));
|
|
65
|
+
const mcpUsageCount = parseInt(mcpResult?.count || '0');
|
|
66
|
+
|
|
67
|
+
// Get database size (in bytes)
|
|
68
|
+
const dbSizeResult = await db
|
|
69
|
+
.prepare(
|
|
70
|
+
`
|
|
71
|
+
SELECT pg_database_size(current_database()) as size
|
|
72
|
+
`
|
|
73
|
+
)
|
|
74
|
+
.get();
|
|
75
|
+
const databaseSize = parseInt(dbSizeResult?.size || '0');
|
|
76
|
+
|
|
77
|
+
// Get total storage size from _storage table
|
|
78
|
+
const storageResult = await db
|
|
79
|
+
.prepare(
|
|
80
|
+
`
|
|
81
|
+
SELECT COALESCE(SUM(size), 0) as total_size
|
|
82
|
+
FROM _storage
|
|
83
|
+
`
|
|
84
|
+
)
|
|
85
|
+
.get();
|
|
86
|
+
const storageSize = parseInt(storageResult?.total_size || '0');
|
|
87
|
+
|
|
88
|
+
res.json({
|
|
89
|
+
mcp_usage_count: mcpUsageCount,
|
|
90
|
+
database_size_bytes: databaseSize,
|
|
91
|
+
storage_size_bytes: storageSize,
|
|
92
|
+
});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
next(error);
|
|
95
|
+
}
|
|
96
|
+
});
|