insforge 0.3.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +20 -0
- package/.dockerignore +60 -57
- package/.env.example +84 -49
- package/.github/ISSUE_TEMPLATE/bug_report.yml +36 -83
- package/.github/ISSUE_TEMPLATE/config.yml +11 -11
- package/.github/ISSUE_TEMPLATE/feature_request.yml +26 -79
- package/.github/PULL_REQUEST_TEMPLATE.md +7 -0
- package/.github/copilot-instructions.md +146 -146
- package/.github/workflows/build-image.yml +66 -65
- package/.github/workflows/ci-premerge-check.yml +23 -23
- package/.github/workflows/e2e.yml +63 -0
- package/.github/workflows/lint-and-format.yml +32 -32
- package/.prettierignore +64 -64
- package/CHANGELOG.md +44 -3
- package/CLAUDE_PLUGIN.md +104 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +125 -125
- package/Dockerfile +30 -27
- package/GITHUB_OAUTH_SETUP.md +49 -49
- package/GOOGLE_OAUTH_SETUP.md +148 -148
- package/LICENSE +201 -201
- package/README.md +182 -134
- package/assets/Dark.svg +23 -23
- 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 +117 -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 +60 -0
- package/auth/src/pages/SignUpPage.tsx +60 -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 +78 -75
- 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} +22 -26
- package/backend/src/api/routes/auth/index.routes.ts +667 -0
- package/backend/src/api/routes/auth/oauth.routes.ts +473 -0
- package/backend/src/api/routes/{database.advance.ts → database/advance.routes.ts} +128 -65
- package/backend/src/api/routes/database/index.routes.ts +90 -0
- package/backend/src/api/routes/{database.records.ts → database/records.routes.ts} +26 -12
- package/backend/src/api/routes/{database.tables.ts → database/tables.routes.ts} +6 -23
- package/backend/src/api/routes/docs/index.routes.ts +75 -0
- package/backend/src/api/routes/email/index.routes.ts +35 -0
- package/backend/src/api/routes/functions/index.routes.ts +194 -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} +33 -31
- package/backend/src/api/routes/realtime/channels.routes.ts +81 -0
- package/backend/src/api/routes/realtime/index.routes.ts +12 -0
- package/backend/src/api/routes/realtime/messages.routes.ts +48 -0
- package/backend/src/api/routes/realtime/permissions.routes.ts +19 -0
- package/backend/src/api/routes/{secrets.ts → secrets/index.routes.ts} +27 -22
- package/backend/src/api/routes/{storage.ts → storage/index.routes.ts} +48 -61
- package/backend/src/api/routes/usage/index.routes.ts +91 -0
- package/backend/src/infra/config/app.config.ts +51 -0
- package/backend/src/infra/database/database.manager.ts +182 -0
- package/backend/{migrations → src/infra/database/migrations}/000_create-base-tables.sql +141 -141
- package/backend/{migrations → src/infra/database/migrations}/001_create-helper-functions.sql +40 -40
- package/backend/{migrations → src/infra/database/migrations}/002_rename-auth-tables.sql +29 -29
- package/backend/{migrations → src/infra/database/migrations}/003_create-users-table.sql +55 -55
- package/backend/{migrations → src/infra/database/migrations}/004_add-reload-postgrest-func.sql +23 -23
- package/backend/{migrations → src/infra/database/migrations}/005_enable-project-admin-modify-users.sql +29 -29
- package/backend/{migrations → src/infra/database/migrations}/006_modify-ai-usage-table.sql +24 -24
- package/backend/{migrations → src/infra/database/migrations}/007_drop-metadata-table.sql +1 -1
- package/backend/{migrations → src/infra/database/migrations}/008_add-system-tables.sql +76 -76
- package/backend/{migrations → src/infra/database/migrations}/009_add-function-secrets.sql +23 -23
- package/backend/{migrations → src/infra/database/migrations}/010_modify-ai-config-modalities.sql +93 -93
- package/backend/{migrations → src/infra/database/migrations}/011_refactor-secrets-table.sql +15 -15
- package/backend/{migrations → src/infra/database/migrations}/012_add-storage-uploaded-by.sql +7 -7
- 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/infra/database/migrations/017_create-realtime-schema.sql +233 -0
- package/backend/src/infra/realtime/realtime.manager.ts +246 -0
- package/backend/src/infra/realtime/webhook-sender.ts +82 -0
- package/backend/src/{core/secrets/encryption.ts → infra/security/encryption.manager.ts} +3 -2
- package/backend/src/infra/security/token.manager.ts +219 -0
- package/backend/src/infra/socket/socket.manager.ts +522 -0
- package/backend/src/providers/ai/openrouter.provider.ts +380 -0
- package/backend/src/providers/email/base.provider.ts +38 -0
- package/backend/src/providers/email/cloud.provider.ts +271 -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/apple.provider.ts +266 -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 +8 -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 +317 -288
- 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 +1150 -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 +802 -0
- package/backend/src/services/database/database.service.ts +127 -0
- package/backend/src/services/email/email.service.ts +73 -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/services/realtime/index.ts +3 -0
- package/backend/src/services/realtime/realtime-auth.service.ts +104 -0
- package/backend/src/services/realtime/realtime-channel.service.ts +237 -0
- package/backend/src/services/realtime/realtime-message.service.ts +260 -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 +77 -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/types/realtime.ts +18 -0
- package/backend/src/{core/socket/types.ts → types/socket.ts} +11 -36
- package/backend/src/utils/cookies.ts +35 -0
- package/backend/src/utils/environment.ts +9 -3
- package/backend/src/utils/logger.ts +20 -2
- package/backend/src/utils/s3-config-loader.ts +64 -0
- package/backend/src/utils/seed.ts +301 -205
- package/backend/src/utils/sql-parser.ts +91 -1
- package/backend/src/utils/utils.ts +114 -0
- package/backend/src/utils/validations.ts +40 -4
- package/backend/tests/README.md +133 -133
- package/backend/tests/cleanup-all-test-data.sh +230 -230
- package/backend/tests/cloud/test-s3-multitenant.sh +131 -131
- package/backend/tests/local/comprehensive-curl-tests.sh +155 -155
- 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 +143 -143
- package/backend/tests/local/test-database-router.sh +222 -222
- package/backend/tests/local/test-e2e.sh +240 -240
- package/backend/tests/local/test-fk-errors.sh +96 -96
- package/backend/tests/local/test-functions.sh +123 -0
- package/backend/tests/local/test-id-field.sh +200 -200
- package/backend/tests/local/test-logs.sh +132 -0
- package/backend/tests/local/test-public-bucket.sh +264 -264
- package/backend/tests/local/test-secrets.sh +249 -247
- package/backend/tests/local/test-serverless-functions.sh.disabled +325 -325
- package/backend/tests/local/test-traditional-rest.sh +208 -208
- package/backend/tests/manual/README.md +50 -50
- package/backend/tests/manual/create-large-table-simple.sql +10 -10
- package/backend/tests/manual/seed-large-table.sql +100 -100
- package/backend/tests/manual/setup-large-table-extras.sql +33 -33
- package/backend/tests/manual/test-bulk-upsert.sh +409 -409
- package/backend/tests/manual/test-database-advance.sh +296 -296
- package/backend/tests/manual/test-postgrest-stability.sh +191 -191
- package/backend/tests/manual/test-rawsql-export-import.sh +411 -411
- package/backend/tests/manual/test-rawsql-modes.sh +244 -0
- package/backend/tests/manual/test-universal-storage.sh +263 -263
- package/backend/tests/manual/test-users.sql +17 -17
- package/backend/tests/run-all-tests.sh +139 -139
- package/backend/tests/setup.ts +0 -0
- package/backend/tests/test-config.sh +338 -302
- package/backend/tests/unit/analyze-query.test.ts +697 -0
- 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 +22 -22
- 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 +204 -144
- package/docker-compose.yml +232 -167
- package/docker-init/db/db-init.sql +97 -125
- package/docker-init/db/jwt.sql +5 -5
- package/docker-init/db/postgresql.conf +16 -16
- package/docker-init/logs/vector.yml +236 -0
- package/docs/README.md +44 -0
- package/docs/agent-docs/real-time.md +269 -0
- package/docs/changelog.mdx +119 -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/email/architecture.mdx +101 -0
- package/docs/core-concepts/email/sdk.mdx +53 -0
- package/docs/core-concepts/functions/architecture.mdx +105 -0
- package/docs/core-concepts/functions/sdk.mdx +184 -0
- package/docs/core-concepts/realtime/architecture.mdx +446 -0
- package/docs/core-concepts/realtime/sdk.mdx +409 -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/deprecated/insforge-auth-api.md +214 -214
- package/docs/deprecated/insforge-auth-sdk.md +99 -99
- package/docs/deprecated/insforge-db-api.md +358 -358
- package/docs/deprecated/insforge-db-sdk.md +139 -139
- package/docs/deprecated/insforge-debug-sdk.md +156 -156
- package/docs/deprecated/insforge-debug.md +64 -64
- package/docs/deprecated/insforge-instructions.md +123 -123
- package/docs/deprecated/insforge-project.md +117 -117
- package/docs/deprecated/insforge-storage-api.md +278 -278
- package/docs/deprecated/insforge-storage-sdk.md +158 -158
- package/docs/docs.json +232 -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/dec-2025/ai-integration.png +0 -0
- package/docs/images/changelog/dec-2025/ai-models.webp +0 -0
- package/docs/images/changelog/dec-2025/alipay-payment.webp +0 -0
- package/docs/images/changelog/dec-2025/apple-login.jpg +0 -0
- package/docs/images/changelog/dec-2025/mcp-installer.png +0 -0
- package/docs/images/changelog/dec-2025/realtime-module.jpg +0 -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 +89 -407
- package/docs/introduction.mdx +45 -0
- package/docs/logo/dark.svg +22 -0
- package/docs/logo/light.svg +20 -0
- package/docs/partnership.mdx +652 -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/examples/oauth/frontend-oauth-example.html +250 -250
- package/examples/response-examples.md +443 -443
- package/frontend/components.json +17 -17
- package/frontend/package.json +69 -63
- package/frontend/src/App.tsx +13 -82
- package/frontend/src/assets/icons/checkbox_checked.svg +6 -6
- package/frontend/src/assets/icons/checkbox_undetermined.svg +6 -6
- package/frontend/src/assets/icons/checked.svg +3 -3
- package/frontend/src/assets/icons/connected.svg +3 -0
- package/frontend/src/assets/icons/error.svg +3 -3
- package/frontend/src/assets/icons/loader.svg +9 -0
- package/frontend/src/assets/icons/pencil.svg +4 -4
- package/frontend/src/assets/icons/refresh.svg +4 -4
- package/frontend/src/assets/icons/step_active.svg +3 -3
- package/frontend/src/assets/icons/step_inactive.svg +11 -11
- package/frontend/src/assets/icons/warning.svg +3 -3
- package/frontend/src/assets/logos/apple.svg +4 -0
- package/frontend/src/assets/logos/claude_code.svg +3 -3
- package/frontend/src/assets/logos/cline.svg +6 -6
- package/frontend/src/assets/logos/cursor.svg +20 -20
- package/frontend/src/assets/logos/discord.svg +8 -8
- package/frontend/src/assets/logos/facebook.svg +3 -0
- package/frontend/src/assets/logos/gemini.svg +19 -19
- package/frontend/src/assets/logos/github.svg +5 -5
- package/frontend/src/assets/logos/google.svg +13 -13
- package/frontend/src/assets/logos/grok.svg +10 -10
- package/frontend/src/assets/logos/insforge_dark.svg +15 -15
- package/frontend/src/assets/logos/insforge_light.svg +15 -15
- 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/openai.svg +10 -10
- package/frontend/src/assets/logos/roo_code.svg +9 -9
- package/frontend/src/assets/logos/spotify.svg +17 -0
- package/frontend/src/assets/logos/tiktok.svg +6 -0
- package/frontend/src/assets/logos/trae.svg +3 -3
- package/frontend/src/assets/logos/windsurf.svg +10 -10
- 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 +131 -91
- 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/pages/AIPage.tsx +166 -0
- 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 +54 -30
- package/frontend/src/features/auth/components/UserFormDialog.tsx +13 -6
- package/frontend/src/features/auth/components/UsersDataGrid.tsx +50 -14
- package/frontend/src/features/auth/components/index.ts +5 -0
- package/frontend/src/features/auth/helpers.tsx +208 -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/pages/AuthMethodsPage.tsx +275 -0
- package/frontend/src/features/auth/pages/ConfigurationPage.tsx +395 -0
- package/frontend/src/features/auth/pages/UsersPage.tsx +257 -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/pages/DashboardPage.tsx +212 -0
- 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/SQLModal.tsx +75 -0
- package/frontend/src/features/database/components/TableEmptyState.tsx +6 -5
- package/frontend/src/features/database/components/TableForm.tsx +28 -19
- 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/useDatabase.ts +66 -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 +135 -0
- package/frontend/src/features/database/index.ts +7 -1
- package/frontend/src/features/database/pages/FunctionsPage.tsx +203 -0
- package/frontend/src/features/database/pages/IndexesPage.tsx +228 -0
- package/frontend/src/features/database/pages/PoliciesPage.tsx +237 -0
- package/frontend/src/features/database/pages/SQLEditorPage.tsx +382 -0
- package/frontend/src/features/database/{page/DatabasePage.tsx → pages/TablesPage.tsx} +168 -209
- package/frontend/src/features/database/pages/TemplatesPage.tsx +39 -0
- package/frontend/src/features/database/pages/TriggersPage.tsx +230 -0
- package/frontend/src/features/database/services/advance.service.ts +40 -0
- package/frontend/src/features/database/services/database.service.ts +33 -194
- package/frontend/src/features/database/services/record.service.ts +219 -0
- package/frontend/src/features/database/services/table.service.ts +58 -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/pages/FunctionsPage.tsx +148 -0
- package/frontend/src/features/functions/{components/SecretsContent.tsx → pages/SecretsPage.tsx} +19 -21
- 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/pages/CloudLoginPage.tsx +118 -0
- package/frontend/src/features/login/{page → pages}/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 +128 -0
- package/frontend/src/features/logs/index.ts +8 -2
- package/frontend/src/features/logs/{page → pages}/AuditsPage.tsx +91 -38
- package/frontend/src/features/logs/pages/LogsPage.tsx +152 -0
- package/frontend/src/features/logs/pages/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/realtime/components/ChannelRow.tsx +83 -0
- package/frontend/src/features/realtime/components/EditChannelModal.tsx +246 -0
- package/frontend/src/features/realtime/components/MessageRow.tsx +85 -0
- package/frontend/src/features/realtime/components/RealtimeEmptyState.tsx +30 -0
- package/frontend/src/features/realtime/hooks/useRealtime.ts +218 -0
- package/frontend/src/features/realtime/index.ts +11 -0
- package/frontend/src/features/realtime/pages/RealtimeChannelsPage.tsx +172 -0
- package/frontend/src/features/realtime/pages/RealtimeMessagesPage.tsx +211 -0
- package/frontend/src/features/realtime/pages/RealtimePermissionsPage.tsx +191 -0
- package/frontend/src/features/realtime/services/realtime.service.ts +107 -0
- 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 → pages}/StoragePage.tsx +41 -143
- 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/pages/VisualizerPage.tsx +97 -0
- 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 +123 -80
- 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 +99 -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 +207 -0
- package/frontend/src/lib/utils/{validation-schemas.ts → schemaValidations.ts} +10 -5
- package/frontend/src/lib/utils/utils.ts +32 -1
- package/frontend/src/vite-env.d.ts +1 -0
- package/frontend/tsconfig.json +25 -25
- package/frontend/tsconfig.node.json +9 -9
- package/frontend/vite.config.ts +5 -3
- package/functions/deno.json +24 -24
- package/functions/server.ts +315 -290
- 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 +715 -688
- package/openapi/auth.yaml +1244 -563
- package/openapi/email.yaml +158 -0
- package/openapi/functions.yaml +475 -475
- package/openapi/health.yaml +29 -29
- package/openapi/logs.yaml +223 -223
- package/openapi/metadata.yaml +177 -177
- package/openapi/realtime.yaml +699 -0
- package/openapi/records.yaml +381 -381
- package/openapi/secrets.yaml +370 -370
- package/openapi/storage.yaml +875 -875
- package/openapi/tables.yaml +463 -463
- package/package.json +97 -88
- package/shared-schemas/package.json +31 -31
- package/shared-schemas/src/ai-api.schema.ts +34 -58
- package/shared-schemas/src/ai.schema.ts +63 -54
- package/shared-schemas/src/auth-api.schema.ts +352 -193
- package/shared-schemas/src/auth.schema.ts +43 -7
- package/shared-schemas/src/cloud-events.schema.ts +57 -0
- package/shared-schemas/src/database-api.schema.ts +35 -4
- package/shared-schemas/src/database.schema.ts +40 -1
- package/shared-schemas/src/docs.schema.ts +26 -0
- package/shared-schemas/src/email-api.schema.ts +30 -0
- package/shared-schemas/src/index.ts +5 -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 +18 -4
- package/shared-schemas/src/realtime-api.schema.ts +111 -0
- package/shared-schemas/src/realtime.schema.ts +143 -0
- package/shared-schemas/tsconfig.json +21 -21
- package/tsconfig.json +7 -7
- 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 -780
- package/backend/src/core/database/manager.ts +0 -178
- 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/socket/socket.ts +0 -388
- 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/ai/page/AIPage.tsx +0 -178
- 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/dashboard/page/DashboardPage.tsx +0 -194
- 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/functions/page/FunctionsPage.tsx +0 -28
- package/frontend/src/features/login/components/AuthErrorBoundary.tsx +0 -87
- package/frontend/src/features/login/components/PrivateRoute.tsx +0 -24
- package/frontend/src/features/login/page/CloudLoginPage.tsx +0 -93
- 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/features/visualizer/page/VisualizerPage.tsx +0 -127
- 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/frontend/src/{features/metadata → lib}/services/metadata.service.ts +0 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { Pool } from 'pg';
|
|
2
|
+
import { DatabaseManager } from '@/infra/database/database.manager.js';
|
|
3
|
+
import { AppError } from '@/api/middlewares/error.js';
|
|
4
|
+
import { ERROR_CODES } from '@/types/error-constants.js';
|
|
5
|
+
import logger from '@/utils/logger.js';
|
|
6
|
+
import type { AuthConfigSchema, UpdateAuthConfigRequest } from '@insforge/shared-schemas';
|
|
7
|
+
|
|
8
|
+
export class AuthConfigService {
|
|
9
|
+
private static instance: AuthConfigService;
|
|
10
|
+
private pool: Pool | null = null;
|
|
11
|
+
|
|
12
|
+
private constructor() {
|
|
13
|
+
logger.info('AuthConfigService initialized');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public static getInstance(): AuthConfigService {
|
|
17
|
+
if (!AuthConfigService.instance) {
|
|
18
|
+
AuthConfigService.instance = new AuthConfigService();
|
|
19
|
+
}
|
|
20
|
+
return AuthConfigService.instance;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private getPool(): Pool {
|
|
24
|
+
if (!this.pool) {
|
|
25
|
+
this.pool = DatabaseManager.getInstance().getPool();
|
|
26
|
+
}
|
|
27
|
+
return this.pool;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get public authentication configuration (safe for public API)
|
|
32
|
+
* Returns all configuration fields except metadata (id, created_at, updated_at)
|
|
33
|
+
*/
|
|
34
|
+
async getPublicAuthConfig() {
|
|
35
|
+
try {
|
|
36
|
+
const result = await this.getPool().query(
|
|
37
|
+
`SELECT
|
|
38
|
+
require_email_verification as "requireEmailVerification",
|
|
39
|
+
password_min_length as "passwordMinLength",
|
|
40
|
+
require_number as "requireNumber",
|
|
41
|
+
require_lowercase as "requireLowercase",
|
|
42
|
+
require_uppercase as "requireUppercase",
|
|
43
|
+
require_special_char as "requireSpecialChar",
|
|
44
|
+
verify_email_method as "verifyEmailMethod",
|
|
45
|
+
reset_password_method as "resetPasswordMethod"
|
|
46
|
+
FROM _auth_configs
|
|
47
|
+
LIMIT 1`
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// If no config exists, return fallback values
|
|
51
|
+
if (!result.rows.length) {
|
|
52
|
+
logger.warn('No auth config found, returning default fallback values');
|
|
53
|
+
return {
|
|
54
|
+
requireEmailVerification: false,
|
|
55
|
+
passwordMinLength: 6,
|
|
56
|
+
requireNumber: false,
|
|
57
|
+
requireLowercase: false,
|
|
58
|
+
requireUppercase: false,
|
|
59
|
+
requireSpecialChar: false,
|
|
60
|
+
verifyEmailMethod: 'code' as const,
|
|
61
|
+
resetPasswordMethod: 'code' as const,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return result.rows[0];
|
|
66
|
+
} catch (error) {
|
|
67
|
+
logger.error('Failed to get public auth config', { error });
|
|
68
|
+
throw new AppError(
|
|
69
|
+
'Failed to get authentication configuration',
|
|
70
|
+
500,
|
|
71
|
+
ERROR_CODES.INTERNAL_ERROR
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get authentication configuration
|
|
78
|
+
* Returns the singleton configuration row with all columns
|
|
79
|
+
*/
|
|
80
|
+
async getAuthConfig(): Promise<AuthConfigSchema> {
|
|
81
|
+
try {
|
|
82
|
+
const result = await this.getPool().query(
|
|
83
|
+
`SELECT
|
|
84
|
+
id,
|
|
85
|
+
require_email_verification as "requireEmailVerification",
|
|
86
|
+
password_min_length as "passwordMinLength",
|
|
87
|
+
require_number as "requireNumber",
|
|
88
|
+
require_lowercase as "requireLowercase",
|
|
89
|
+
require_uppercase as "requireUppercase",
|
|
90
|
+
require_special_char as "requireSpecialChar",
|
|
91
|
+
verify_email_method as "verifyEmailMethod",
|
|
92
|
+
reset_password_method as "resetPasswordMethod",
|
|
93
|
+
sign_in_redirect_to as "signInRedirectTo",
|
|
94
|
+
created_at as "createdAt",
|
|
95
|
+
updated_at as "updatedAt"
|
|
96
|
+
FROM _auth_configs
|
|
97
|
+
LIMIT 1`
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// If no config exists, return fallback values
|
|
101
|
+
if (!result.rows.length) {
|
|
102
|
+
logger.warn('No auth config found, returning default fallback values');
|
|
103
|
+
// Return a config with fallback values and generate a temporary ID
|
|
104
|
+
return {
|
|
105
|
+
id: '00000000-0000-0000-0000-000000000000',
|
|
106
|
+
requireEmailVerification: false,
|
|
107
|
+
passwordMinLength: 6,
|
|
108
|
+
requireNumber: false,
|
|
109
|
+
requireLowercase: false,
|
|
110
|
+
requireUppercase: false,
|
|
111
|
+
requireSpecialChar: false,
|
|
112
|
+
verifyEmailMethod: 'code' as const,
|
|
113
|
+
resetPasswordMethod: 'code' as const,
|
|
114
|
+
signInRedirectTo: null,
|
|
115
|
+
createdAt: new Date().toISOString(),
|
|
116
|
+
updatedAt: new Date().toISOString(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result.rows[0];
|
|
121
|
+
} catch (error) {
|
|
122
|
+
logger.error('Failed to get auth config', { error });
|
|
123
|
+
throw new AppError(
|
|
124
|
+
'Failed to get authentication configuration',
|
|
125
|
+
500,
|
|
126
|
+
ERROR_CODES.INTERNAL_ERROR
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Update authentication configuration
|
|
133
|
+
* Updates the singleton configuration row
|
|
134
|
+
*/
|
|
135
|
+
async updateAuthConfig(input: UpdateAuthConfigRequest): Promise<AuthConfigSchema> {
|
|
136
|
+
const client = await this.getPool().connect();
|
|
137
|
+
try {
|
|
138
|
+
await client.query('BEGIN');
|
|
139
|
+
|
|
140
|
+
// Ensure config exists and lock row to prevent concurrent modifications
|
|
141
|
+
const existingResult = await client.query('SELECT id FROM _auth_configs LIMIT 1 FOR UPDATE');
|
|
142
|
+
|
|
143
|
+
if (!existingResult.rows.length) {
|
|
144
|
+
// Config doesn't exist, rollback and throw error
|
|
145
|
+
// The migration should have created the default config
|
|
146
|
+
await client.query('ROLLBACK');
|
|
147
|
+
throw new AppError(
|
|
148
|
+
'Authentication configuration not found. Please run migrations.',
|
|
149
|
+
500,
|
|
150
|
+
ERROR_CODES.INTERNAL_ERROR
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Build update query
|
|
155
|
+
const updates: string[] = [];
|
|
156
|
+
const values: (string | number | boolean | null)[] = [];
|
|
157
|
+
let paramCount = 1;
|
|
158
|
+
|
|
159
|
+
if (input.requireEmailVerification !== undefined) {
|
|
160
|
+
updates.push(`require_email_verification = $${paramCount++}`);
|
|
161
|
+
values.push(input.requireEmailVerification);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (input.passwordMinLength !== undefined) {
|
|
165
|
+
updates.push(`password_min_length = $${paramCount++}`);
|
|
166
|
+
values.push(input.passwordMinLength);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (input.requireNumber !== undefined) {
|
|
170
|
+
updates.push(`require_number = $${paramCount++}`);
|
|
171
|
+
values.push(input.requireNumber);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (input.requireLowercase !== undefined) {
|
|
175
|
+
updates.push(`require_lowercase = $${paramCount++}`);
|
|
176
|
+
values.push(input.requireLowercase);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (input.requireUppercase !== undefined) {
|
|
180
|
+
updates.push(`require_uppercase = $${paramCount++}`);
|
|
181
|
+
values.push(input.requireUppercase);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (input.requireSpecialChar !== undefined) {
|
|
185
|
+
updates.push(`require_special_char = $${paramCount++}`);
|
|
186
|
+
values.push(input.requireSpecialChar);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (input.verifyEmailMethod !== undefined) {
|
|
190
|
+
updates.push(`verify_email_method = $${paramCount++}`);
|
|
191
|
+
values.push(input.verifyEmailMethod);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (input.resetPasswordMethod !== undefined) {
|
|
195
|
+
updates.push(`reset_password_method = $${paramCount++}`);
|
|
196
|
+
values.push(input.resetPasswordMethod);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (input.signInRedirectTo !== undefined) {
|
|
200
|
+
updates.push(`sign_in_redirect_to = $${paramCount++}`);
|
|
201
|
+
values.push(input.signInRedirectTo);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!updates.length) {
|
|
205
|
+
await client.query('COMMIT');
|
|
206
|
+
// Return current config if no updates
|
|
207
|
+
return await this.getAuthConfig();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Add updated_at to updates
|
|
211
|
+
updates.push('updated_at = NOW()');
|
|
212
|
+
|
|
213
|
+
const result = await client.query(
|
|
214
|
+
`UPDATE _auth_configs
|
|
215
|
+
SET ${updates.join(', ')}
|
|
216
|
+
RETURNING
|
|
217
|
+
id,
|
|
218
|
+
require_email_verification as "requireEmailVerification",
|
|
219
|
+
password_min_length as "passwordMinLength",
|
|
220
|
+
require_number as "requireNumber",
|
|
221
|
+
require_lowercase as "requireLowercase",
|
|
222
|
+
require_uppercase as "requireUppercase",
|
|
223
|
+
require_special_char as "requireSpecialChar",
|
|
224
|
+
verify_email_method as "verifyEmailMethod",
|
|
225
|
+
reset_password_method as "resetPasswordMethod",
|
|
226
|
+
sign_in_redirect_to as "signInRedirectTo",
|
|
227
|
+
created_at as "createdAt",
|
|
228
|
+
updated_at as "updatedAt"`,
|
|
229
|
+
values
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
await client.query('COMMIT');
|
|
233
|
+
logger.info('Auth config updated', { updatedFields: Object.keys(input) });
|
|
234
|
+
return result.rows[0];
|
|
235
|
+
} catch (error) {
|
|
236
|
+
await client.query('ROLLBACK');
|
|
237
|
+
logger.error('Failed to update auth config', { error });
|
|
238
|
+
if (error instanceof AppError) {
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
throw new AppError(
|
|
242
|
+
'Failed to update authentication configuration',
|
|
243
|
+
500,
|
|
244
|
+
ERROR_CODES.INTERNAL_ERROR
|
|
245
|
+
);
|
|
246
|
+
} finally {
|
|
247
|
+
client.release();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { Pool, PoolClient } from 'pg';
|
|
2
|
+
import bcrypt from 'bcryptjs';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { DatabaseManager } from '@/infra/database/database.manager.js';
|
|
5
|
+
import { AppError } from '@/api/middlewares/error.js';
|
|
6
|
+
import { ERROR_CODES } from '@/types/error-constants.js';
|
|
7
|
+
import logger from '@/utils/logger.js';
|
|
8
|
+
import { generateNumericCode, generateSecureToken } from '@/utils/utils.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* OTP purpose types - used to categorize different OTP use cases
|
|
12
|
+
*/
|
|
13
|
+
export enum OTPPurpose {
|
|
14
|
+
VERIFY_EMAIL = 'VERIFY_EMAIL',
|
|
15
|
+
RESET_PASSWORD = 'RESET_PASSWORD',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Token type - determines token format and expiration
|
|
20
|
+
*/
|
|
21
|
+
export enum OTPType {
|
|
22
|
+
NUMERIC_CODE = 'NUMERIC_CODE', // Short 6-digit numeric code for manual entry
|
|
23
|
+
HASH_TOKEN = 'HASH_TOKEN', // Long cryptographic token with hash-based lookup
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Result of OTP creation
|
|
28
|
+
*/
|
|
29
|
+
export interface CreateOTPResult {
|
|
30
|
+
success: boolean;
|
|
31
|
+
otp: string;
|
|
32
|
+
expiresAt: Date;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Result of OTP verification
|
|
37
|
+
*/
|
|
38
|
+
export interface VerifyOTPResult {
|
|
39
|
+
success: boolean;
|
|
40
|
+
email: string;
|
|
41
|
+
purpose: OTPPurpose;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Service for managing email-based one-time passwords (OTPs)
|
|
46
|
+
*
|
|
47
|
+
* Supports two delivery methods:
|
|
48
|
+
* 1. Short numeric codes (6 digits) - displayed in email for manual entry
|
|
49
|
+
* - Stored as bcrypt hash (defense against brute force if DB compromised)
|
|
50
|
+
* - Brute force protection handled by API-level rate limiting
|
|
51
|
+
* 2. Long cryptographic tokens (64 chars) - embedded in clickable links for one-click verification
|
|
52
|
+
* - Stored as SHA-256 hash (high entropy makes bcrypt unnecessary, allows direct lookup)
|
|
53
|
+
*
|
|
54
|
+
* The dual hashing strategy balances security and performance:
|
|
55
|
+
* - NUMERIC_CODE: Low entropy (10^6 combinations) requires slow bcrypt + API rate limiting
|
|
56
|
+
* - HASH_TOKEN: High entropy (2^256 combinations) only needs fast SHA-256
|
|
57
|
+
*/
|
|
58
|
+
export class AuthOTPService {
|
|
59
|
+
private static instance: AuthOTPService;
|
|
60
|
+
private pool: Pool | null = null;
|
|
61
|
+
|
|
62
|
+
// Configuration constants
|
|
63
|
+
private readonly NUMERIC_CODE_LENGTH = 6; // 6 digits = 1 million combinations
|
|
64
|
+
private readonly NUMERIC_CODE_EXPIRY_MINUTES = 15; // 15 minutes expiry for numeric codes
|
|
65
|
+
private readonly HASH_TOKEN_BYTES = 32; // 32 bytes = 64 hex characters = 256 bits entropy
|
|
66
|
+
private readonly HASH_TOKEN_EXPIRY_HOURS = 24; // 24 hours expiry for hash tokens
|
|
67
|
+
private readonly BCRYPT_SALT_ROUNDS = 10; // Salt rounds for numeric codes (2^10 iterations)
|
|
68
|
+
|
|
69
|
+
private constructor() {
|
|
70
|
+
logger.info('AuthOTPService initialized');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public static getInstance(): AuthOTPService {
|
|
74
|
+
if (!AuthOTPService.instance) {
|
|
75
|
+
AuthOTPService.instance = new AuthOTPService();
|
|
76
|
+
}
|
|
77
|
+
return AuthOTPService.instance;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private getPool(): Pool {
|
|
81
|
+
if (!this.pool) {
|
|
82
|
+
this.pool = DatabaseManager.getInstance().getPool();
|
|
83
|
+
}
|
|
84
|
+
return this.pool;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Create or update an email OTP
|
|
89
|
+
* Supports both short numeric codes (for manual entry) and long cryptographic tokens (for clickable links)
|
|
90
|
+
* Uses upsert to ensure only one active token exists per email/purpose combination
|
|
91
|
+
*
|
|
92
|
+
* Hashing strategy:
|
|
93
|
+
* - NUMERIC_CODE: Uses bcrypt (slow hash) due to low entropy (10^6 combinations)
|
|
94
|
+
* - HASH_TOKEN: Uses SHA-256 (fast hash) - high entropy (2^256) makes bcrypt unnecessary
|
|
95
|
+
*
|
|
96
|
+
* @param email - The email address for the token
|
|
97
|
+
* @param purpose - The purpose of the token (e.g., 'VERIFY_EMAIL', 'RESET_PASSWORD')
|
|
98
|
+
* @param otpType - The type of token to generate ('NUMERIC_CODE' or 'HASH_TOKEN')
|
|
99
|
+
* @returns Promise with creation result including the token and expiry time
|
|
100
|
+
*/
|
|
101
|
+
async createEmailOTP(
|
|
102
|
+
email: string,
|
|
103
|
+
purpose: OTPPurpose,
|
|
104
|
+
otpType: OTPType = OTPType.NUMERIC_CODE
|
|
105
|
+
): Promise<CreateOTPResult> {
|
|
106
|
+
try {
|
|
107
|
+
// Generate token based on type
|
|
108
|
+
let otp: string;
|
|
109
|
+
let expiresAt: Date;
|
|
110
|
+
let otpHash: string;
|
|
111
|
+
|
|
112
|
+
if (otpType === OTPType.NUMERIC_CODE) {
|
|
113
|
+
// Generate 6-digit numeric code for manual entry
|
|
114
|
+
otp = generateNumericCode(this.NUMERIC_CODE_LENGTH);
|
|
115
|
+
expiresAt = new Date(Date.now() + this.NUMERIC_CODE_EXPIRY_MINUTES * 60 * 1000);
|
|
116
|
+
// Use bcrypt for low-entropy codes (defense against brute force)
|
|
117
|
+
otpHash = await bcrypt.hash(otp, this.BCRYPT_SALT_ROUNDS);
|
|
118
|
+
} else {
|
|
119
|
+
// Generate cryptographically secure token for hash-based lookup
|
|
120
|
+
otp = generateSecureToken(this.HASH_TOKEN_BYTES);
|
|
121
|
+
expiresAt = new Date(Date.now() + this.HASH_TOKEN_EXPIRY_HOURS * 60 * 60 * 1000);
|
|
122
|
+
// Use SHA-256 for high-entropy tokens (enables direct lookup)
|
|
123
|
+
otpHash = crypto.createHash('sha256').update(otp).digest('hex');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Upsert token record - insert or update if email+purpose combination already exists
|
|
127
|
+
// This ensures only one active token per email/purpose (replaces any existing token)
|
|
128
|
+
await this.getPool().query(
|
|
129
|
+
`INSERT INTO _email_otps (email, purpose, otp_hash, expires_at, consumed_at)
|
|
130
|
+
VALUES ($1, $2, $3, $4, NULL)
|
|
131
|
+
ON CONFLICT (email, purpose)
|
|
132
|
+
DO UPDATE SET
|
|
133
|
+
otp_hash = EXCLUDED.otp_hash,
|
|
134
|
+
expires_at = EXCLUDED.expires_at,
|
|
135
|
+
consumed_at = NULL,
|
|
136
|
+
updated_at = NOW()`,
|
|
137
|
+
[email, purpose, otpHash, expiresAt]
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
logger.info('Email verification token created successfully', {
|
|
141
|
+
purpose,
|
|
142
|
+
otpType,
|
|
143
|
+
expiresAt: expiresAt.toISOString(),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
otp,
|
|
149
|
+
expiresAt,
|
|
150
|
+
};
|
|
151
|
+
} catch (error) {
|
|
152
|
+
logger.error('Failed to create email verification token', { error, purpose, otpType });
|
|
153
|
+
throw new AppError('Failed to create verification token', 500, ERROR_CODES.INTERNAL_ERROR);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Verify a numeric OTP code (6 digits)
|
|
159
|
+
* Looks up by email and verifies the bcrypt-hashed code
|
|
160
|
+
*
|
|
161
|
+
* Brute force protection is handled by API-level rate limiting.
|
|
162
|
+
*
|
|
163
|
+
* @param email - The email address associated with the OTP
|
|
164
|
+
* @param purpose - The purpose of the OTP
|
|
165
|
+
* @param code - The 6-digit numeric code to verify
|
|
166
|
+
* @param externalClient - Optional external database client for transaction support
|
|
167
|
+
* @returns Promise with verification result
|
|
168
|
+
* @throws AppError if verification fails (with generic error message)
|
|
169
|
+
*/
|
|
170
|
+
async verifyEmailOTPWithCode(
|
|
171
|
+
email: string,
|
|
172
|
+
purpose: OTPPurpose,
|
|
173
|
+
code: string,
|
|
174
|
+
externalClient?: PoolClient
|
|
175
|
+
): Promise<VerifyOTPResult> {
|
|
176
|
+
const client = externalClient || (await this.getPool().connect());
|
|
177
|
+
const shouldManageTransaction = !externalClient;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
if (shouldManageTransaction) {
|
|
181
|
+
await client.query('BEGIN');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Lookup by email and lock the row
|
|
185
|
+
const result = await client.query(
|
|
186
|
+
`SELECT id, email, purpose, otp_hash, expires_at, consumed_at
|
|
187
|
+
FROM _email_otps
|
|
188
|
+
WHERE email = $1 AND purpose = $2
|
|
189
|
+
FOR UPDATE`,
|
|
190
|
+
[email, purpose]
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// Check if OTP record exists
|
|
194
|
+
if (result.rows.length === 0) {
|
|
195
|
+
throw new AppError('Invalid or expired verification code', 400, ERROR_CODES.INVALID_INPUT);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const otpRecord = result.rows[0];
|
|
199
|
+
|
|
200
|
+
// Validate OTP record is still usable
|
|
201
|
+
if (new Date() > new Date(otpRecord.expires_at) || otpRecord.consumed_at !== null) {
|
|
202
|
+
throw new AppError('Invalid or expired verification code', 400, ERROR_CODES.INVALID_INPUT);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Verify bcrypt hash
|
|
206
|
+
const isValid = await bcrypt.compare(code, otpRecord.otp_hash);
|
|
207
|
+
|
|
208
|
+
if (!isValid) {
|
|
209
|
+
throw new AppError('Invalid or expired verification code', 400, ERROR_CODES.INVALID_INPUT);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Mark OTP as consumed atomically
|
|
213
|
+
const consume = await client.query(
|
|
214
|
+
`UPDATE _email_otps
|
|
215
|
+
SET consumed_at = NOW(), updated_at = NOW()
|
|
216
|
+
WHERE id = $1 AND consumed_at IS NULL`,
|
|
217
|
+
[otpRecord.id]
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (consume.rowCount !== 1) {
|
|
221
|
+
throw new AppError('Invalid or expired verification code', 400, ERROR_CODES.INVALID_INPUT);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (shouldManageTransaction) {
|
|
225
|
+
await client.query('COMMIT');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
logger.info('Numeric OTP code verified successfully', { purpose });
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
success: true,
|
|
232
|
+
email: otpRecord.email,
|
|
233
|
+
purpose: otpRecord.purpose,
|
|
234
|
+
};
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (shouldManageTransaction) {
|
|
237
|
+
await client.query('ROLLBACK');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (error instanceof AppError) {
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
logger.error('Failed to verify numeric OTP code', { error, purpose });
|
|
245
|
+
throw new AppError('Failed to verify code', 500, ERROR_CODES.INTERNAL_ERROR);
|
|
246
|
+
} finally {
|
|
247
|
+
if (shouldManageTransaction) {
|
|
248
|
+
client.release();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Verify a hash token (64 hex characters)
|
|
255
|
+
* Performs direct lookup using SHA-256 hash without knowing the email
|
|
256
|
+
*
|
|
257
|
+
* @param purpose - The purpose of the OTP
|
|
258
|
+
* @param token - The 64-character hex token to verify
|
|
259
|
+
* @param externalClient - Optional external database client for transaction support
|
|
260
|
+
* @returns Promise with verification result including the associated email
|
|
261
|
+
* @throws AppError if verification fails (with generic error message)
|
|
262
|
+
*/
|
|
263
|
+
async verifyEmailOTPWithToken(
|
|
264
|
+
purpose: OTPPurpose,
|
|
265
|
+
token: string,
|
|
266
|
+
externalClient?: PoolClient
|
|
267
|
+
): Promise<VerifyOTPResult> {
|
|
268
|
+
const client = externalClient || (await this.getPool().connect());
|
|
269
|
+
const shouldManageTransaction = !externalClient;
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
if (shouldManageTransaction) {
|
|
273
|
+
await client.query('BEGIN');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Hash the token and perform direct lookup
|
|
277
|
+
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
|
|
278
|
+
|
|
279
|
+
// Direct lookup by hash - O(1) with index on otp_hash
|
|
280
|
+
const result = await client.query(
|
|
281
|
+
`SELECT id, email, purpose, otp_hash, expires_at, consumed_at
|
|
282
|
+
FROM _email_otps
|
|
283
|
+
WHERE purpose = $1
|
|
284
|
+
AND otp_hash = $2
|
|
285
|
+
AND expires_at > NOW()
|
|
286
|
+
AND consumed_at IS NULL
|
|
287
|
+
FOR UPDATE`,
|
|
288
|
+
[purpose, tokenHash]
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Check if token exists and is valid
|
|
292
|
+
if (result.rows.length === 0) {
|
|
293
|
+
throw new AppError('Invalid or expired verification token', 400, ERROR_CODES.INVALID_INPUT);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const otpRecord = result.rows[0];
|
|
297
|
+
|
|
298
|
+
// Mark OTP as consumed atomically
|
|
299
|
+
const consume = await client.query(
|
|
300
|
+
`UPDATE _email_otps
|
|
301
|
+
SET consumed_at = NOW(), updated_at = NOW()
|
|
302
|
+
WHERE id = $1 AND consumed_at IS NULL`,
|
|
303
|
+
[otpRecord.id]
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
if (consume.rowCount !== 1) {
|
|
307
|
+
throw new AppError('Invalid or expired verification token', 400, ERROR_CODES.INVALID_INPUT);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (shouldManageTransaction) {
|
|
311
|
+
await client.query('COMMIT');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
logger.info('Hash token verified successfully', { purpose });
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
success: true,
|
|
318
|
+
email: otpRecord.email,
|
|
319
|
+
purpose: otpRecord.purpose,
|
|
320
|
+
};
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (shouldManageTransaction) {
|
|
323
|
+
await client.query('ROLLBACK');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (error instanceof AppError) {
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
logger.error('Failed to verify hash token', { error, purpose });
|
|
331
|
+
throw new AppError('Failed to verify token', 500, ERROR_CODES.INTERNAL_ERROR);
|
|
332
|
+
} finally {
|
|
333
|
+
if (shouldManageTransaction) {
|
|
334
|
+
client.release();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Exchange a verified numeric code for a long-lived hash token
|
|
341
|
+
* This is a common pattern in multi-step verification flows:
|
|
342
|
+
* 1. User receives numeric code via email
|
|
343
|
+
* 2. User submits code to verify
|
|
344
|
+
* 3. System issues a long-lived token for subsequent operations
|
|
345
|
+
*
|
|
346
|
+
* The entire exchange happens atomically within a single transaction to ensure:
|
|
347
|
+
* - Numeric code is consumed only if token creation succeeds
|
|
348
|
+
* - No race conditions between verification and token issuance
|
|
349
|
+
*
|
|
350
|
+
* Example use cases:
|
|
351
|
+
* - Password reset: verify code → get reset token → reset password
|
|
352
|
+
* - Email verification: verify code → get session token → auto-login
|
|
353
|
+
*
|
|
354
|
+
* @param email - The email address associated with the code
|
|
355
|
+
* @param purpose - The purpose of the OTP (e.g., RESET_PASSWORD)
|
|
356
|
+
* @param numericCode - The 6-digit numeric code to verify
|
|
357
|
+
* @param externalClient - Optional external database client for broader transaction support
|
|
358
|
+
* @returns Promise with the long-lived token and its expiration
|
|
359
|
+
* @throws AppError if verification fails or token creation fails
|
|
360
|
+
*/
|
|
361
|
+
async exchangeCodeForToken(
|
|
362
|
+
email: string,
|
|
363
|
+
purpose: OTPPurpose,
|
|
364
|
+
numericCode: string,
|
|
365
|
+
externalClient?: PoolClient
|
|
366
|
+
): Promise<{ token: string; expiresAt: Date }> {
|
|
367
|
+
const client = externalClient || (await this.getPool().connect());
|
|
368
|
+
const shouldManageTransaction = !externalClient;
|
|
369
|
+
let transactionActive = false;
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
if (shouldManageTransaction) {
|
|
373
|
+
await client.query('BEGIN');
|
|
374
|
+
transactionActive = true;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Step 1: Verify the numeric code (consumes it atomically)
|
|
378
|
+
await this.verifyEmailOTPWithCode(email, purpose, numericCode, client);
|
|
379
|
+
|
|
380
|
+
// Step 2: Generate a long-lived hash token
|
|
381
|
+
const token = generateSecureToken(this.HASH_TOKEN_BYTES);
|
|
382
|
+
const expiresAt = new Date(Date.now() + this.HASH_TOKEN_EXPIRY_HOURS * 60 * 60 * 1000);
|
|
383
|
+
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
|
|
384
|
+
|
|
385
|
+
// Step 3: Insert the new token (replaces the consumed numeric code)
|
|
386
|
+
// Uses upsert to overwrite the consumed code record with the new token
|
|
387
|
+
await client.query(
|
|
388
|
+
`INSERT INTO _email_otps (email, purpose, otp_hash, expires_at, consumed_at)
|
|
389
|
+
VALUES ($1, $2, $3, $4, NULL)
|
|
390
|
+
ON CONFLICT (email, purpose)
|
|
391
|
+
DO UPDATE SET
|
|
392
|
+
otp_hash = EXCLUDED.otp_hash,
|
|
393
|
+
expires_at = EXCLUDED.expires_at,
|
|
394
|
+
consumed_at = NULL,
|
|
395
|
+
updated_at = NOW()`,
|
|
396
|
+
[email, purpose, tokenHash, expiresAt]
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
if (shouldManageTransaction) {
|
|
400
|
+
await client.query('COMMIT');
|
|
401
|
+
transactionActive = false;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
logger.info('Successfully exchanged numeric code for hash token', { email, purpose });
|
|
405
|
+
|
|
406
|
+
return { token, expiresAt };
|
|
407
|
+
} catch (error) {
|
|
408
|
+
if (shouldManageTransaction && transactionActive) {
|
|
409
|
+
await client.query('ROLLBACK');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (error instanceof AppError) {
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
logger.error('Failed to exchange code for token', { error, email, purpose });
|
|
417
|
+
throw new AppError('Failed to exchange verification code', 500, ERROR_CODES.INTERNAL_ERROR);
|
|
418
|
+
} finally {
|
|
419
|
+
if (shouldManageTransaction) {
|
|
420
|
+
client.release();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|