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,1150 @@
|
|
|
1
|
+
import bcrypt from 'bcryptjs';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import { Pool } from 'pg';
|
|
4
|
+
import { DatabaseManager } from '@/infra/database/database.manager.js';
|
|
5
|
+
import { TokenManager } from '@/infra/security/token.manager.js';
|
|
6
|
+
import logger from '@/utils/logger.js';
|
|
7
|
+
import type {
|
|
8
|
+
UserSchema,
|
|
9
|
+
CreateUserResponse,
|
|
10
|
+
CreateSessionResponse,
|
|
11
|
+
VerifyEmailResponse,
|
|
12
|
+
ResetPasswordResponse,
|
|
13
|
+
CreateAdminSessionResponse,
|
|
14
|
+
AuthMetadataSchema,
|
|
15
|
+
OAuthProvidersSchema,
|
|
16
|
+
} from '@insforge/shared-schemas';
|
|
17
|
+
import { OAuthConfigService } from '@/services/auth/oauth-config.service.js';
|
|
18
|
+
import { AuthConfigService } from './auth-config.service.js';
|
|
19
|
+
import { AuthOTPService, OTPPurpose, OTPType } from './auth-otp.service.js';
|
|
20
|
+
import { GoogleOAuthProvider } from '@/providers/oauth/google.provider.js';
|
|
21
|
+
import { GitHubOAuthProvider } from '@/providers/oauth/github.provider.js';
|
|
22
|
+
import { DiscordOAuthProvider } from '@/providers/oauth/discord.provider.js';
|
|
23
|
+
import { LinkedInOAuthProvider } from '@/providers/oauth/linkedin.provider.js';
|
|
24
|
+
import { FacebookOAuthProvider } from '@/providers/oauth/facebook.provider.js';
|
|
25
|
+
import { MicrosoftOAuthProvider } from '@/providers/oauth/microsoft.provider.js';
|
|
26
|
+
import { validatePassword } from '@/utils/validations.js';
|
|
27
|
+
import { getPasswordRequirementsMessage } from '@/utils/utils.js';
|
|
28
|
+
import {
|
|
29
|
+
FacebookUserInfo,
|
|
30
|
+
GitHubUserInfo,
|
|
31
|
+
GoogleUserInfo,
|
|
32
|
+
MicrosoftUserInfo,
|
|
33
|
+
LinkedInUserInfo,
|
|
34
|
+
DiscordUserInfo,
|
|
35
|
+
XUserInfo,
|
|
36
|
+
AppleUserInfo,
|
|
37
|
+
UserRecord,
|
|
38
|
+
OAuthUserData,
|
|
39
|
+
} from '@/types/auth.js';
|
|
40
|
+
import { ADMIN_ID } from '@/utils/constants.js';
|
|
41
|
+
import { getApiBaseUrl } from '@/utils/environment.js';
|
|
42
|
+
import { AppError } from '@/api/middlewares/error.js';
|
|
43
|
+
import { ERROR_CODES } from '@/types/error-constants.js';
|
|
44
|
+
import { EmailService } from '@/services/email/email.service.js';
|
|
45
|
+
import { XOAuthProvider } from '@/providers/oauth/x.provider.js';
|
|
46
|
+
import { AppleOAuthProvider } from '@/providers/oauth/apple.provider.js';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Simplified JWT-based auth service
|
|
50
|
+
* Handles all authentication operations including OAuth
|
|
51
|
+
*/
|
|
52
|
+
export class AuthService {
|
|
53
|
+
private static instance: AuthService;
|
|
54
|
+
private adminEmail: string;
|
|
55
|
+
private adminPassword: string;
|
|
56
|
+
private pool: Pool | null = null;
|
|
57
|
+
private tokenManager: TokenManager;
|
|
58
|
+
|
|
59
|
+
// OAuth provider instances (cached singletons)
|
|
60
|
+
private googleOAuthProvider: GoogleOAuthProvider;
|
|
61
|
+
private githubOAuthProvider: GitHubOAuthProvider;
|
|
62
|
+
private discordOAuthProvider: DiscordOAuthProvider;
|
|
63
|
+
private linkedinOAuthProvider: LinkedInOAuthProvider;
|
|
64
|
+
private facebookOAuthProvider: FacebookOAuthProvider;
|
|
65
|
+
private microsoftOAuthProvider: MicrosoftOAuthProvider;
|
|
66
|
+
private xOAuthProvider: XOAuthProvider;
|
|
67
|
+
private appleOAuthProvider: AppleOAuthProvider;
|
|
68
|
+
|
|
69
|
+
private constructor() {
|
|
70
|
+
this.adminEmail = process.env.ADMIN_EMAIL ?? '';
|
|
71
|
+
this.adminPassword = process.env.ADMIN_PASSWORD ?? '';
|
|
72
|
+
|
|
73
|
+
if (!this.adminEmail || !this.adminPassword) {
|
|
74
|
+
throw new Error('ADMIN_EMAIL and ADMIN_PASSWORD environment variables are required');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Initialize token manager
|
|
78
|
+
this.tokenManager = TokenManager.getInstance();
|
|
79
|
+
|
|
80
|
+
// Initialize OAuth providers (cached singletons)
|
|
81
|
+
this.googleOAuthProvider = GoogleOAuthProvider.getInstance();
|
|
82
|
+
this.githubOAuthProvider = GitHubOAuthProvider.getInstance();
|
|
83
|
+
this.discordOAuthProvider = DiscordOAuthProvider.getInstance();
|
|
84
|
+
this.linkedinOAuthProvider = LinkedInOAuthProvider.getInstance();
|
|
85
|
+
this.facebookOAuthProvider = FacebookOAuthProvider.getInstance();
|
|
86
|
+
this.microsoftOAuthProvider = MicrosoftOAuthProvider.getInstance();
|
|
87
|
+
this.xOAuthProvider = XOAuthProvider.getInstance();
|
|
88
|
+
this.appleOAuthProvider = AppleOAuthProvider.getInstance();
|
|
89
|
+
|
|
90
|
+
logger.info('AuthService initialized');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public static getInstance(): AuthService {
|
|
94
|
+
if (!AuthService.instance) {
|
|
95
|
+
AuthService.instance = new AuthService();
|
|
96
|
+
}
|
|
97
|
+
return AuthService.instance;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private getPool(): Pool {
|
|
101
|
+
if (!this.pool) {
|
|
102
|
+
const dbManager = DatabaseManager.getInstance();
|
|
103
|
+
this.pool = dbManager.getPool();
|
|
104
|
+
}
|
|
105
|
+
return this.pool;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* User registration
|
|
110
|
+
* Otherwise, returns user with access token for immediate login
|
|
111
|
+
*/
|
|
112
|
+
async register(email: string, password: string, name?: string): Promise<CreateUserResponse> {
|
|
113
|
+
// Get email auth configuration and validate password
|
|
114
|
+
const authConfigService = AuthConfigService.getInstance();
|
|
115
|
+
const emailAuthConfig = await authConfigService.getAuthConfig();
|
|
116
|
+
|
|
117
|
+
if (!validatePassword(password, emailAuthConfig)) {
|
|
118
|
+
throw new AppError(
|
|
119
|
+
getPasswordRequirementsMessage(emailAuthConfig),
|
|
120
|
+
400,
|
|
121
|
+
ERROR_CODES.INVALID_INPUT
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const hashedPassword = await bcrypt.hash(password, 10);
|
|
126
|
+
const userId = crypto.randomUUID();
|
|
127
|
+
|
|
128
|
+
const pool = this.getPool();
|
|
129
|
+
const client = await pool.connect();
|
|
130
|
+
try {
|
|
131
|
+
await client.query('BEGIN');
|
|
132
|
+
|
|
133
|
+
await client.query(
|
|
134
|
+
`INSERT INTO _accounts (id, email, password, name, email_verified, created_at, updated_at)
|
|
135
|
+
VALUES ($1, $2, $3, $4, $5, NOW(), NOW())`,
|
|
136
|
+
[userId, email, hashedPassword, name || null, false]
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
await client.query(
|
|
140
|
+
`INSERT INTO users (id, nickname, created_at, updated_at)
|
|
141
|
+
VALUES ($1, $2, NOW(), NOW())`,
|
|
142
|
+
[userId, name || null]
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
await client.query('COMMIT');
|
|
146
|
+
} catch (e) {
|
|
147
|
+
await client.query('ROLLBACK');
|
|
148
|
+
// Postgres unique_violation
|
|
149
|
+
if (e && typeof e === 'object' && 'code' in e && e.code === '23505') {
|
|
150
|
+
throw new AppError('User already exists', 409, ERROR_CODES.ALREADY_EXISTS);
|
|
151
|
+
}
|
|
152
|
+
throw e;
|
|
153
|
+
} finally {
|
|
154
|
+
client.release();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const dbUser = await this.getUserById(userId);
|
|
158
|
+
if (!dbUser) {
|
|
159
|
+
throw new Error('User not found after registration');
|
|
160
|
+
}
|
|
161
|
+
const user = this.transformUserRecordToSchema(dbUser);
|
|
162
|
+
|
|
163
|
+
if (emailAuthConfig.requireEmailVerification) {
|
|
164
|
+
try {
|
|
165
|
+
if (emailAuthConfig.verifyEmailMethod === 'link') {
|
|
166
|
+
await this.sendVerificationEmailWithLink(email);
|
|
167
|
+
} else {
|
|
168
|
+
await this.sendVerificationEmailWithCode(email);
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
logger.warn('Verification email send failed during register', { error });
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
accessToken: null,
|
|
175
|
+
requireEmailVerification: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Email verification not required, provide access token for immediate login
|
|
180
|
+
const accessToken = this.tokenManager.generateToken({
|
|
181
|
+
sub: userId,
|
|
182
|
+
email,
|
|
183
|
+
role: 'authenticated',
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
user,
|
|
188
|
+
accessToken,
|
|
189
|
+
requireEmailVerification: false,
|
|
190
|
+
redirectTo: emailAuthConfig.signInRedirectTo || undefined,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* User login
|
|
196
|
+
*/
|
|
197
|
+
async login(email: string, password: string): Promise<CreateSessionResponse> {
|
|
198
|
+
const dbUser = await this.getUserByEmail(email);
|
|
199
|
+
|
|
200
|
+
if (!dbUser || !dbUser.password) {
|
|
201
|
+
throw new AppError('Invalid credentials', 401, ERROR_CODES.AUTH_UNAUTHORIZED);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const validPassword = await bcrypt.compare(password, dbUser.password);
|
|
205
|
+
if (!validPassword) {
|
|
206
|
+
throw new AppError('Invalid credentials', 401, ERROR_CODES.AUTH_UNAUTHORIZED);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check if email verification is required
|
|
210
|
+
const authConfigService = AuthConfigService.getInstance();
|
|
211
|
+
const emailAuthConfig = await authConfigService.getAuthConfig();
|
|
212
|
+
|
|
213
|
+
if (emailAuthConfig.requireEmailVerification && !dbUser.email_verified) {
|
|
214
|
+
throw new AppError(
|
|
215
|
+
'Email verification required',
|
|
216
|
+
403,
|
|
217
|
+
ERROR_CODES.FORBIDDEN,
|
|
218
|
+
'Please verify your email address before logging in'
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const user = this.transformUserRecordToSchema(dbUser);
|
|
223
|
+
const accessToken = this.tokenManager.generateToken({
|
|
224
|
+
sub: dbUser.id,
|
|
225
|
+
email: dbUser.email,
|
|
226
|
+
role: 'authenticated',
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Include redirect URL if configured
|
|
230
|
+
const response: CreateSessionResponse = {
|
|
231
|
+
user,
|
|
232
|
+
accessToken,
|
|
233
|
+
redirectTo: emailAuthConfig.signInRedirectTo || undefined,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
return response;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Send verification email with numeric OTP code
|
|
241
|
+
* Creates a 6-digit OTP and sends it via email for manual entry
|
|
242
|
+
*/
|
|
243
|
+
async sendVerificationEmailWithCode(email: string): Promise<void> {
|
|
244
|
+
// Check if user exists
|
|
245
|
+
const pool = this.getPool();
|
|
246
|
+
const result = await pool.query('SELECT * FROM _accounts WHERE email = $1', [email]);
|
|
247
|
+
const dbUser = result.rows[0];
|
|
248
|
+
if (!dbUser) {
|
|
249
|
+
// Silently succeed to prevent user enumeration
|
|
250
|
+
logger.info('Verification email requested for non-existent user', { email });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Create numeric OTP code using the OTP service
|
|
255
|
+
const otpService = AuthOTPService.getInstance();
|
|
256
|
+
const { otp: code } = await otpService.createEmailOTP(
|
|
257
|
+
email,
|
|
258
|
+
OTPPurpose.VERIFY_EMAIL,
|
|
259
|
+
OTPType.NUMERIC_CODE
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Send email with verification code
|
|
263
|
+
const emailService = EmailService.getInstance();
|
|
264
|
+
await emailService.sendWithTemplate(email, dbUser.name || 'User', 'email-verification-code', {
|
|
265
|
+
token: code,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Send verification email with clickable link
|
|
271
|
+
* Creates a long cryptographic token and sends it via email as a clickable link
|
|
272
|
+
* The link contains only the token (no email) for better privacy and security
|
|
273
|
+
*/
|
|
274
|
+
async sendVerificationEmailWithLink(email: string): Promise<void> {
|
|
275
|
+
// Check if user exists
|
|
276
|
+
const pool = this.getPool();
|
|
277
|
+
const result = await pool.query('SELECT * FROM _accounts WHERE email = $1', [email]);
|
|
278
|
+
const dbUser = result.rows[0];
|
|
279
|
+
if (!dbUser) {
|
|
280
|
+
// Silently succeed to prevent user enumeration
|
|
281
|
+
logger.info('Verification email requested for non-existent user', { email });
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Create long cryptographic token for clickable verification link
|
|
286
|
+
const otpService = AuthOTPService.getInstance();
|
|
287
|
+
const { otp: token } = await otpService.createEmailOTP(
|
|
288
|
+
email,
|
|
289
|
+
OTPPurpose.VERIFY_EMAIL,
|
|
290
|
+
OTPType.HASH_TOKEN
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// Build verification link URL using backend API endpoint
|
|
294
|
+
const linkUrl = `${getApiBaseUrl()}/auth/verify-email?token=${token}`;
|
|
295
|
+
|
|
296
|
+
// Send email with verification link
|
|
297
|
+
const emailService = EmailService.getInstance();
|
|
298
|
+
await emailService.sendWithTemplate(email, dbUser.name || 'User', 'email-verification-link', {
|
|
299
|
+
link: linkUrl,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Verify email with numeric code
|
|
305
|
+
* Verifies the email OTP code and updates the account in a single transaction
|
|
306
|
+
*/
|
|
307
|
+
async verifyEmailWithCode(email: string, verificationCode: string): Promise<VerifyEmailResponse> {
|
|
308
|
+
const dbManager = DatabaseManager.getInstance();
|
|
309
|
+
const pool = dbManager.getPool();
|
|
310
|
+
const client = await pool.connect();
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
await client.query('BEGIN');
|
|
314
|
+
|
|
315
|
+
// Verify OTP using the OTP service (within the same transaction)
|
|
316
|
+
const otpService = AuthOTPService.getInstance();
|
|
317
|
+
await otpService.verifyEmailOTPWithCode(
|
|
318
|
+
email,
|
|
319
|
+
OTPPurpose.VERIFY_EMAIL,
|
|
320
|
+
verificationCode,
|
|
321
|
+
client
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
// Update account email verification status
|
|
325
|
+
const result = await client.query(
|
|
326
|
+
`UPDATE _accounts
|
|
327
|
+
SET email_verified = true, updated_at = NOW()
|
|
328
|
+
WHERE email = $1
|
|
329
|
+
RETURNING id`,
|
|
330
|
+
[email]
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
if (result.rows.length === 0) {
|
|
334
|
+
throw new Error('User not found');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
await client.query('COMMIT');
|
|
338
|
+
|
|
339
|
+
// Fetch full user record with provider data
|
|
340
|
+
const userId = result.rows[0].id;
|
|
341
|
+
const dbUser = await this.getUserById(userId);
|
|
342
|
+
if (!dbUser) {
|
|
343
|
+
throw new Error('User not found after verification');
|
|
344
|
+
}
|
|
345
|
+
const user = this.transformUserRecordToSchema(dbUser);
|
|
346
|
+
const accessToken = this.tokenManager.generateToken({
|
|
347
|
+
sub: dbUser.id,
|
|
348
|
+
email: dbUser.email,
|
|
349
|
+
role: 'authenticated',
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Get redirect URL from auth config if configured
|
|
353
|
+
const authConfigService = AuthConfigService.getInstance();
|
|
354
|
+
const emailAuthConfig = await authConfigService.getAuthConfig();
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
user,
|
|
358
|
+
accessToken,
|
|
359
|
+
redirectTo: emailAuthConfig.signInRedirectTo || undefined,
|
|
360
|
+
};
|
|
361
|
+
} catch (error) {
|
|
362
|
+
await client.query('ROLLBACK');
|
|
363
|
+
throw error;
|
|
364
|
+
} finally {
|
|
365
|
+
client.release();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Verify email with hash token from clickable link
|
|
371
|
+
* Verifies the token (without needing email), looks up the email, and updates the account
|
|
372
|
+
* This is more secure as the email is not exposed in the URL
|
|
373
|
+
*/
|
|
374
|
+
async verifyEmailWithToken(token: string): Promise<VerifyEmailResponse> {
|
|
375
|
+
const dbManager = DatabaseManager.getInstance();
|
|
376
|
+
const pool = dbManager.getPool();
|
|
377
|
+
const client = await pool.connect();
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
await client.query('BEGIN');
|
|
381
|
+
|
|
382
|
+
// Verify token and get the associated email
|
|
383
|
+
const otpService = AuthOTPService.getInstance();
|
|
384
|
+
const { email } = await otpService.verifyEmailOTPWithToken(
|
|
385
|
+
OTPPurpose.VERIFY_EMAIL,
|
|
386
|
+
token,
|
|
387
|
+
client
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
// Update account email verification status
|
|
391
|
+
const result = await client.query(
|
|
392
|
+
`UPDATE _accounts
|
|
393
|
+
SET email_verified = true, updated_at = NOW()
|
|
394
|
+
WHERE email = $1
|
|
395
|
+
RETURNING id`,
|
|
396
|
+
[email]
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
if (result.rows.length === 0) {
|
|
400
|
+
throw new Error('User not found');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
await client.query('COMMIT');
|
|
404
|
+
|
|
405
|
+
// Fetch full user record with provider data
|
|
406
|
+
const userId = result.rows[0].id;
|
|
407
|
+
const dbUser = await this.getUserById(userId);
|
|
408
|
+
if (!dbUser) {
|
|
409
|
+
throw new Error('User not found after verification');
|
|
410
|
+
}
|
|
411
|
+
const user = this.transformUserRecordToSchema(dbUser);
|
|
412
|
+
const accessToken = this.tokenManager.generateToken({
|
|
413
|
+
sub: dbUser.id,
|
|
414
|
+
email: dbUser.email,
|
|
415
|
+
role: 'authenticated',
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Get redirect URL from auth config if configured
|
|
419
|
+
const authConfigService = AuthConfigService.getInstance();
|
|
420
|
+
const emailAuthConfig = await authConfigService.getAuthConfig();
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
user,
|
|
424
|
+
accessToken,
|
|
425
|
+
redirectTo: emailAuthConfig.signInRedirectTo || undefined,
|
|
426
|
+
};
|
|
427
|
+
} catch (error) {
|
|
428
|
+
await client.query('ROLLBACK');
|
|
429
|
+
throw error;
|
|
430
|
+
} finally {
|
|
431
|
+
client.release();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Send reset password email with numeric OTP code
|
|
437
|
+
* Creates a 6-digit OTP and sends it via email for manual entry
|
|
438
|
+
*/
|
|
439
|
+
async sendResetPasswordEmailWithCode(email: string): Promise<void> {
|
|
440
|
+
// Check if user exists
|
|
441
|
+
const pool = this.getPool();
|
|
442
|
+
const result = await pool.query('SELECT * FROM _accounts WHERE email = $1', [email]);
|
|
443
|
+
const dbUser = result.rows[0];
|
|
444
|
+
if (!dbUser) {
|
|
445
|
+
// Silently succeed to prevent user enumeration
|
|
446
|
+
logger.info('Password reset requested for non-existent user', { email });
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Create numeric OTP code using the OTP service
|
|
451
|
+
const otpService = AuthOTPService.getInstance();
|
|
452
|
+
const { otp: code } = await otpService.createEmailOTP(
|
|
453
|
+
email,
|
|
454
|
+
OTPPurpose.RESET_PASSWORD,
|
|
455
|
+
OTPType.NUMERIC_CODE
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
// Send email with reset password code
|
|
459
|
+
const emailService = EmailService.getInstance();
|
|
460
|
+
await emailService.sendWithTemplate(email, dbUser.name || 'User', 'reset-password-code', {
|
|
461
|
+
token: code,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Send reset password email with clickable link
|
|
467
|
+
* Creates a long cryptographic token and sends it via email as a clickable link
|
|
468
|
+
* The link contains only the token (no email) for better privacy and security
|
|
469
|
+
*/
|
|
470
|
+
async sendResetPasswordEmailWithLink(email: string): Promise<void> {
|
|
471
|
+
// Check if user exists
|
|
472
|
+
const pool = this.getPool();
|
|
473
|
+
const result = await pool.query('SELECT * FROM _accounts WHERE email = $1', [email]);
|
|
474
|
+
const dbUser = result.rows[0];
|
|
475
|
+
if (!dbUser) {
|
|
476
|
+
// Silently succeed to prevent user enumeration
|
|
477
|
+
logger.info('Password reset requested for non-existent user', { email });
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Create long cryptographic token for clickable reset link
|
|
482
|
+
const otpService = AuthOTPService.getInstance();
|
|
483
|
+
const { otp: token } = await otpService.createEmailOTP(
|
|
484
|
+
email,
|
|
485
|
+
OTPPurpose.RESET_PASSWORD,
|
|
486
|
+
OTPType.HASH_TOKEN
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
// Build password reset link URL using backend API endpoint
|
|
490
|
+
const linkUrl = `${getApiBaseUrl()}/auth/reset-password?token=${token}`;
|
|
491
|
+
|
|
492
|
+
// Send email with password reset link
|
|
493
|
+
const emailService = EmailService.getInstance();
|
|
494
|
+
await emailService.sendWithTemplate(email, dbUser.name || 'User', 'reset-password-link', {
|
|
495
|
+
link: linkUrl,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Exchange reset password code for a temporary reset token
|
|
501
|
+
* This separates code verification from password reset for better security
|
|
502
|
+
* The reset token can be used later to reset the password without needing email
|
|
503
|
+
*/
|
|
504
|
+
async exchangeResetPasswordToken(
|
|
505
|
+
email: string,
|
|
506
|
+
verificationCode: string
|
|
507
|
+
): Promise<{ token: string; expiresAt: Date }> {
|
|
508
|
+
const otpService = AuthOTPService.getInstance();
|
|
509
|
+
|
|
510
|
+
// Exchange the numeric verification code for a long-lived reset token
|
|
511
|
+
// All OTP logic (verification, consumption, token generation) is handled by AuthOTPService
|
|
512
|
+
const result = await otpService.exchangeCodeForToken(
|
|
513
|
+
email,
|
|
514
|
+
OTPPurpose.RESET_PASSWORD,
|
|
515
|
+
verificationCode
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
token: result.token,
|
|
520
|
+
expiresAt: result.expiresAt,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Reset password with token
|
|
526
|
+
* Verifies the token (without needing email), looks up the email, and updates the password
|
|
527
|
+
* Both clickable link tokens and code-verified reset tokens use RESET_PASSWORD purpose
|
|
528
|
+
* Note: Does not return access token - user must login again with new password
|
|
529
|
+
*/
|
|
530
|
+
async resetPasswordWithToken(newPassword: string, token: string): Promise<ResetPasswordResponse> {
|
|
531
|
+
// Validate password first before verifying token
|
|
532
|
+
// This allows the user to retry with the same token if password is invalid
|
|
533
|
+
const authConfigService = AuthConfigService.getInstance();
|
|
534
|
+
const emailAuthConfig = await authConfigService.getAuthConfig();
|
|
535
|
+
|
|
536
|
+
if (!validatePassword(newPassword, emailAuthConfig)) {
|
|
537
|
+
throw new AppError(
|
|
538
|
+
getPasswordRequirementsMessage(emailAuthConfig),
|
|
539
|
+
400,
|
|
540
|
+
ERROR_CODES.INVALID_INPUT
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const dbManager = DatabaseManager.getInstance();
|
|
545
|
+
const pool = dbManager.getPool();
|
|
546
|
+
const client = await pool.connect();
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
await client.query('BEGIN');
|
|
550
|
+
|
|
551
|
+
// Verify token and get the associated email
|
|
552
|
+
// Both clickable link tokens and code-verified reset tokens use RESET_PASSWORD purpose
|
|
553
|
+
const otpService = AuthOTPService.getInstance();
|
|
554
|
+
const { email } = await otpService.verifyEmailOTPWithToken(
|
|
555
|
+
OTPPurpose.RESET_PASSWORD,
|
|
556
|
+
token,
|
|
557
|
+
client
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
// Hash the new password
|
|
561
|
+
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
|
562
|
+
|
|
563
|
+
// Update password in the database
|
|
564
|
+
const result = await client.query(
|
|
565
|
+
`UPDATE _accounts
|
|
566
|
+
SET password = $1, updated_at = NOW()
|
|
567
|
+
WHERE email = $2
|
|
568
|
+
RETURNING id`,
|
|
569
|
+
[hashedPassword, email]
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
if (result.rows.length === 0) {
|
|
573
|
+
throw new Error('User not found');
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const userId = result.rows[0].id;
|
|
577
|
+
|
|
578
|
+
await client.query('COMMIT');
|
|
579
|
+
|
|
580
|
+
logger.info('Password reset successfully with token', { userId });
|
|
581
|
+
|
|
582
|
+
return {
|
|
583
|
+
message: 'Password reset successfully. Please login with your new password.',
|
|
584
|
+
};
|
|
585
|
+
} catch (error) {
|
|
586
|
+
await client.query('ROLLBACK');
|
|
587
|
+
throw error;
|
|
588
|
+
} finally {
|
|
589
|
+
client.release();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Admin login (validates against env variables only)
|
|
595
|
+
*/
|
|
596
|
+
adminLogin(email: string, password: string): CreateAdminSessionResponse {
|
|
597
|
+
// Simply validate against environment variables
|
|
598
|
+
if (email !== this.adminEmail || password !== this.adminPassword) {
|
|
599
|
+
throw new AppError('Invalid admin credentials', 401, ERROR_CODES.AUTH_UNAUTHORIZED);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Use a fixed admin ID for the system administrator
|
|
603
|
+
|
|
604
|
+
// Return admin user with JWT token - no database interaction
|
|
605
|
+
const accessToken = this.tokenManager.generateToken({
|
|
606
|
+
sub: ADMIN_ID,
|
|
607
|
+
email,
|
|
608
|
+
role: 'project_admin',
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
user: {
|
|
613
|
+
id: ADMIN_ID,
|
|
614
|
+
email: email,
|
|
615
|
+
name: 'Administrator',
|
|
616
|
+
emailVerified: true,
|
|
617
|
+
createdAt: new Date().toISOString(),
|
|
618
|
+
updatedAt: new Date().toISOString(),
|
|
619
|
+
},
|
|
620
|
+
accessToken,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Admin login with authorization token (validates JWT from external issuer)
|
|
626
|
+
*/
|
|
627
|
+
async adminLoginWithAuthorizationCode(code: string): Promise<CreateAdminSessionResponse> {
|
|
628
|
+
try {
|
|
629
|
+
// Use TokenManager to verify cloud token
|
|
630
|
+
const { payload } = await this.tokenManager.verifyCloudToken(code);
|
|
631
|
+
|
|
632
|
+
// If verification succeeds, extract user info and generate internal token
|
|
633
|
+
const email = payload['email'] || payload['sub'] || 'admin@insforge.local';
|
|
634
|
+
|
|
635
|
+
// Generate internal access token
|
|
636
|
+
const accessToken = this.tokenManager.generateToken({
|
|
637
|
+
sub: ADMIN_ID,
|
|
638
|
+
email: email as string,
|
|
639
|
+
role: 'project_admin',
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
user: {
|
|
644
|
+
id: ADMIN_ID,
|
|
645
|
+
email: email as string,
|
|
646
|
+
name: 'Administrator',
|
|
647
|
+
emailVerified: true,
|
|
648
|
+
createdAt: new Date().toISOString(),
|
|
649
|
+
updatedAt: new Date().toISOString(),
|
|
650
|
+
},
|
|
651
|
+
accessToken,
|
|
652
|
+
};
|
|
653
|
+
} catch (error) {
|
|
654
|
+
logger.error('Admin token verification failed:', error);
|
|
655
|
+
throw new AppError('Invalid admin credentials', 401, ERROR_CODES.AUTH_UNAUTHORIZED);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Find or create third-party user (main OAuth user handler)
|
|
661
|
+
* Adapted from 3-table to 2-table structure
|
|
662
|
+
*/
|
|
663
|
+
async findOrCreateThirdPartyUser(
|
|
664
|
+
provider: string,
|
|
665
|
+
providerId: string,
|
|
666
|
+
email: string,
|
|
667
|
+
userName: string,
|
|
668
|
+
avatarUrl: string,
|
|
669
|
+
identityData:
|
|
670
|
+
| GoogleUserInfo
|
|
671
|
+
| GitHubUserInfo
|
|
672
|
+
| DiscordUserInfo
|
|
673
|
+
| LinkedInUserInfo
|
|
674
|
+
| MicrosoftUserInfo
|
|
675
|
+
| FacebookUserInfo
|
|
676
|
+
| XUserInfo
|
|
677
|
+
| AppleUserInfo
|
|
678
|
+
| Record<string, unknown>
|
|
679
|
+
): Promise<CreateSessionResponse> {
|
|
680
|
+
const pool = this.getPool();
|
|
681
|
+
|
|
682
|
+
// First, try to find existing user by provider ID in _account_providers table
|
|
683
|
+
const accountResult = await pool.query(
|
|
684
|
+
'SELECT * FROM _account_providers WHERE provider = $1 AND provider_account_id = $2',
|
|
685
|
+
[provider, providerId]
|
|
686
|
+
);
|
|
687
|
+
const account = accountResult.rows[0];
|
|
688
|
+
|
|
689
|
+
if (account) {
|
|
690
|
+
// Found existing OAuth user, update last login time
|
|
691
|
+
await pool.query(
|
|
692
|
+
'UPDATE _account_providers SET updated_at = CURRENT_TIMESTAMP WHERE provider = $1 AND provider_account_id = $2',
|
|
693
|
+
[provider, providerId]
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
// Update email_verified to true if not already verified (OAuth login means email is trusted)
|
|
697
|
+
await pool.query(
|
|
698
|
+
'UPDATE _accounts SET email_verified = true WHERE id = $1 AND email_verified = false',
|
|
699
|
+
[account.user_id]
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
const dbUser = await this.getUserById(account.user_id);
|
|
703
|
+
if (!dbUser) {
|
|
704
|
+
throw new Error('User not found after OAuth login');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const user = this.transformUserRecordToSchema(dbUser);
|
|
708
|
+
const accessToken = this.tokenManager.generateToken({
|
|
709
|
+
sub: user.id,
|
|
710
|
+
email: user.email,
|
|
711
|
+
role: 'authenticated',
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
return { user, accessToken };
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// If not found by provider_id, try to find by email in _user table
|
|
718
|
+
const existingUserResult = await pool.query('SELECT * FROM _accounts WHERE email = $1', [
|
|
719
|
+
email,
|
|
720
|
+
]);
|
|
721
|
+
const existingUser = existingUserResult.rows[0];
|
|
722
|
+
|
|
723
|
+
if (existingUser) {
|
|
724
|
+
// Found existing user by email, create _account_providers record to link OAuth
|
|
725
|
+
await pool.query(
|
|
726
|
+
`
|
|
727
|
+
INSERT INTO _account_providers (
|
|
728
|
+
user_id, provider, provider_account_id,
|
|
729
|
+
provider_data, created_at, updated_at
|
|
730
|
+
)
|
|
731
|
+
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
732
|
+
`,
|
|
733
|
+
[existingUser.id, provider, providerId, JSON.stringify(identityData)]
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
// Update email_verified to true (OAuth login means email is trusted)
|
|
737
|
+
await pool.query(
|
|
738
|
+
'UPDATE _accounts SET email_verified = true WHERE id = $1 AND email_verified = false',
|
|
739
|
+
[existingUser.id]
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
// Fetch updated user data with provider information
|
|
743
|
+
const dbUser = await this.getUserById(existingUser.id);
|
|
744
|
+
if (!dbUser) {
|
|
745
|
+
throw new Error('User not found after linking OAuth provider');
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const user = this.transformUserRecordToSchema(dbUser);
|
|
749
|
+
const accessToken = this.tokenManager.generateToken({
|
|
750
|
+
sub: existingUser.id,
|
|
751
|
+
email: existingUser.email,
|
|
752
|
+
role: 'authenticated',
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
return { user, accessToken };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Create new user with OAuth data
|
|
759
|
+
return this.createThirdPartyUser(
|
|
760
|
+
provider,
|
|
761
|
+
userName,
|
|
762
|
+
email,
|
|
763
|
+
providerId,
|
|
764
|
+
identityData,
|
|
765
|
+
avatarUrl
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Create new third-party user
|
|
771
|
+
*/
|
|
772
|
+
private async createThirdPartyUser(
|
|
773
|
+
provider: string,
|
|
774
|
+
userName: string,
|
|
775
|
+
email: string,
|
|
776
|
+
providerId: string,
|
|
777
|
+
identityData:
|
|
778
|
+
| GoogleUserInfo
|
|
779
|
+
| GitHubUserInfo
|
|
780
|
+
| DiscordUserInfo
|
|
781
|
+
| LinkedInUserInfo
|
|
782
|
+
| MicrosoftUserInfo
|
|
783
|
+
| FacebookUserInfo
|
|
784
|
+
| XUserInfo
|
|
785
|
+
| AppleUserInfo
|
|
786
|
+
| Record<string, unknown>,
|
|
787
|
+
avatarUrl: string
|
|
788
|
+
): Promise<CreateSessionResponse> {
|
|
789
|
+
const userId = crypto.randomUUID();
|
|
790
|
+
|
|
791
|
+
const pool = this.getPool();
|
|
792
|
+
const client = await pool.connect();
|
|
793
|
+
|
|
794
|
+
try {
|
|
795
|
+
await client.query('BEGIN');
|
|
796
|
+
|
|
797
|
+
// Create user record (without password for OAuth users)
|
|
798
|
+
await client.query(
|
|
799
|
+
`
|
|
800
|
+
INSERT INTO _accounts (id, email, name, email_verified, created_at, updated_at)
|
|
801
|
+
VALUES ($1, $2, $3, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
802
|
+
`,
|
|
803
|
+
[userId, email, userName]
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
await client.query(
|
|
807
|
+
`
|
|
808
|
+
INSERT INTO users (id, nickname, avatar_url, created_at, updated_at)
|
|
809
|
+
VALUES ($1, $2, $3, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
810
|
+
`,
|
|
811
|
+
[userId, userName, avatarUrl]
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
// Create _account_providers record
|
|
815
|
+
await client.query(
|
|
816
|
+
`
|
|
817
|
+
INSERT INTO _account_providers (
|
|
818
|
+
user_id, provider, provider_account_id,
|
|
819
|
+
provider_data, created_at, updated_at
|
|
820
|
+
)
|
|
821
|
+
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
822
|
+
`,
|
|
823
|
+
[userId, provider, providerId, JSON.stringify({ ...identityData, avatar_url: avatarUrl })]
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
await client.query('COMMIT');
|
|
827
|
+
|
|
828
|
+
const user: UserSchema = {
|
|
829
|
+
id: userId,
|
|
830
|
+
email,
|
|
831
|
+
name: userName,
|
|
832
|
+
emailVerified: true,
|
|
833
|
+
createdAt: new Date().toISOString(),
|
|
834
|
+
updatedAt: new Date().toISOString(),
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
const accessToken = this.tokenManager.generateToken({
|
|
838
|
+
sub: userId,
|
|
839
|
+
email,
|
|
840
|
+
role: 'authenticated',
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
return { user, accessToken };
|
|
844
|
+
} catch (error) {
|
|
845
|
+
await client.query('ROLLBACK');
|
|
846
|
+
throw error;
|
|
847
|
+
} finally {
|
|
848
|
+
client.release();
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async getMetadata(): Promise<AuthMetadataSchema> {
|
|
853
|
+
const oAuthConfigService = OAuthConfigService.getInstance();
|
|
854
|
+
const oAuthConfigs = await oAuthConfigService.getAllConfigs();
|
|
855
|
+
return {
|
|
856
|
+
oauths: oAuthConfigs,
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Generate OAuth authorization URL for any supported provider
|
|
862
|
+
*/
|
|
863
|
+
async generateOAuthUrl(provider: OAuthProvidersSchema, state?: string): Promise<string> {
|
|
864
|
+
switch (provider) {
|
|
865
|
+
case 'google':
|
|
866
|
+
return this.googleOAuthProvider.generateOAuthUrl(state);
|
|
867
|
+
case 'github':
|
|
868
|
+
return this.githubOAuthProvider.generateOAuthUrl(state);
|
|
869
|
+
case 'discord':
|
|
870
|
+
return this.discordOAuthProvider.generateOAuthUrl(state);
|
|
871
|
+
case 'linkedin':
|
|
872
|
+
return this.linkedinOAuthProvider.generateOAuthUrl(state);
|
|
873
|
+
case 'facebook':
|
|
874
|
+
return this.facebookOAuthProvider.generateOAuthUrl(state);
|
|
875
|
+
case 'microsoft':
|
|
876
|
+
return this.microsoftOAuthProvider.generateOAuthUrl(state);
|
|
877
|
+
case 'x':
|
|
878
|
+
return this.xOAuthProvider.generateOAuthUrl(state);
|
|
879
|
+
case 'apple':
|
|
880
|
+
return this.appleOAuthProvider.generateOAuthUrl(state);
|
|
881
|
+
default:
|
|
882
|
+
throw new Error(`OAuth provider ${provider} is not implemented yet.`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Handle OAuth callback for any supported provider
|
|
888
|
+
*/
|
|
889
|
+
async handleOAuthCallback(
|
|
890
|
+
provider: OAuthProvidersSchema,
|
|
891
|
+
payload: { code?: string; token?: string; state?: string }
|
|
892
|
+
): Promise<CreateSessionResponse> {
|
|
893
|
+
let userData: OAuthUserData;
|
|
894
|
+
|
|
895
|
+
switch (provider) {
|
|
896
|
+
case 'google':
|
|
897
|
+
userData = await this.googleOAuthProvider.handleCallback(payload);
|
|
898
|
+
break;
|
|
899
|
+
case 'github':
|
|
900
|
+
userData = await this.githubOAuthProvider.handleCallback(payload);
|
|
901
|
+
break;
|
|
902
|
+
case 'discord':
|
|
903
|
+
userData = await this.discordOAuthProvider.handleCallback(payload);
|
|
904
|
+
break;
|
|
905
|
+
case 'linkedin':
|
|
906
|
+
userData = await this.linkedinOAuthProvider.handleCallback(payload);
|
|
907
|
+
break;
|
|
908
|
+
case 'facebook':
|
|
909
|
+
userData = await this.facebookOAuthProvider.handleCallback(payload);
|
|
910
|
+
break;
|
|
911
|
+
case 'microsoft':
|
|
912
|
+
userData = await this.microsoftOAuthProvider.handleCallback(payload);
|
|
913
|
+
break;
|
|
914
|
+
case 'x':
|
|
915
|
+
userData = await this.xOAuthProvider.handleCallback(payload);
|
|
916
|
+
break;
|
|
917
|
+
case 'apple':
|
|
918
|
+
userData = await this.appleOAuthProvider.handleCallback(payload);
|
|
919
|
+
break;
|
|
920
|
+
default:
|
|
921
|
+
throw new Error(`OAuth provider ${provider} is not implemented yet.`);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return this.findOrCreateThirdPartyUser(
|
|
925
|
+
userData.provider,
|
|
926
|
+
userData.providerId,
|
|
927
|
+
userData.email,
|
|
928
|
+
userData.userName,
|
|
929
|
+
userData.avatarUrl,
|
|
930
|
+
userData.identityData
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Handle shared callback for any supported provider
|
|
936
|
+
* Transforms payload and creates/finds user
|
|
937
|
+
*/
|
|
938
|
+
async handleSharedCallback(
|
|
939
|
+
provider: OAuthProvidersSchema,
|
|
940
|
+
payloadData: Record<string, unknown>
|
|
941
|
+
): Promise<CreateSessionResponse> {
|
|
942
|
+
let userData: OAuthUserData;
|
|
943
|
+
|
|
944
|
+
switch (provider) {
|
|
945
|
+
case 'google':
|
|
946
|
+
userData = this.googleOAuthProvider.handleSharedCallback(payloadData);
|
|
947
|
+
break;
|
|
948
|
+
case 'github':
|
|
949
|
+
userData = this.githubOAuthProvider.handleSharedCallback(payloadData);
|
|
950
|
+
break;
|
|
951
|
+
case 'discord':
|
|
952
|
+
userData = this.discordOAuthProvider.handleSharedCallback(payloadData);
|
|
953
|
+
break;
|
|
954
|
+
case 'linkedin':
|
|
955
|
+
userData = this.linkedinOAuthProvider.handleSharedCallback(payloadData);
|
|
956
|
+
break;
|
|
957
|
+
case 'facebook':
|
|
958
|
+
userData = this.facebookOAuthProvider.handleSharedCallback(payloadData);
|
|
959
|
+
break;
|
|
960
|
+
case 'x':
|
|
961
|
+
userData = this.xOAuthProvider.handleSharedCallback(payloadData);
|
|
962
|
+
break;
|
|
963
|
+
case 'apple':
|
|
964
|
+
userData = this.appleOAuthProvider.handleSharedCallback(payloadData);
|
|
965
|
+
break;
|
|
966
|
+
case 'microsoft':
|
|
967
|
+
default:
|
|
968
|
+
throw new Error(`OAuth provider ${provider} is not supported for shared callback.`);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
return this.findOrCreateThirdPartyUser(
|
|
972
|
+
userData.provider,
|
|
973
|
+
userData.providerId,
|
|
974
|
+
userData.email,
|
|
975
|
+
userData.userName,
|
|
976
|
+
userData.avatarUrl,
|
|
977
|
+
userData.identityData
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Get user by email (helper method for internal use)
|
|
983
|
+
* @private
|
|
984
|
+
*/
|
|
985
|
+
private async getUserByEmail(email: string): Promise<UserRecord | null> {
|
|
986
|
+
const pool = this.getPool();
|
|
987
|
+
const result = await pool.query(
|
|
988
|
+
`
|
|
989
|
+
SELECT
|
|
990
|
+
u.id,
|
|
991
|
+
u.email,
|
|
992
|
+
u.name,
|
|
993
|
+
u.email_verified,
|
|
994
|
+
u.created_at,
|
|
995
|
+
u.updated_at,
|
|
996
|
+
u.password,
|
|
997
|
+
STRING_AGG(a.provider, ',') as providers
|
|
998
|
+
FROM _accounts u
|
|
999
|
+
LEFT JOIN _account_providers a ON u.id = a.user_id
|
|
1000
|
+
WHERE u.email = $1
|
|
1001
|
+
GROUP BY u.id
|
|
1002
|
+
`,
|
|
1003
|
+
[email]
|
|
1004
|
+
);
|
|
1005
|
+
|
|
1006
|
+
return result.rows[0] || null;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Get user by ID (helper method for internal use)
|
|
1011
|
+
* @private
|
|
1012
|
+
*/
|
|
1013
|
+
private async getUserById(userId: string): Promise<UserRecord | null> {
|
|
1014
|
+
const pool = this.getPool();
|
|
1015
|
+
const result = await pool.query(
|
|
1016
|
+
`
|
|
1017
|
+
SELECT
|
|
1018
|
+
u.id,
|
|
1019
|
+
u.email,
|
|
1020
|
+
u.name,
|
|
1021
|
+
u.email_verified,
|
|
1022
|
+
u.created_at,
|
|
1023
|
+
u.updated_at,
|
|
1024
|
+
u.password,
|
|
1025
|
+
STRING_AGG(a.provider, ',') as providers
|
|
1026
|
+
FROM _accounts u
|
|
1027
|
+
LEFT JOIN _account_providers a ON u.id = a.user_id
|
|
1028
|
+
WHERE u.id = $1
|
|
1029
|
+
GROUP BY u.id
|
|
1030
|
+
`,
|
|
1031
|
+
[userId]
|
|
1032
|
+
);
|
|
1033
|
+
|
|
1034
|
+
return result.rows[0] || null;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Transform database user record to API response format (snake_case to camelCase + provider logic)
|
|
1039
|
+
* @private
|
|
1040
|
+
*/
|
|
1041
|
+
private transformUserRecordToSchema(dbUser: UserRecord): UserSchema {
|
|
1042
|
+
const identities = [];
|
|
1043
|
+
const providers: string[] = [];
|
|
1044
|
+
|
|
1045
|
+
// Add social providers if any
|
|
1046
|
+
if (dbUser.providers) {
|
|
1047
|
+
dbUser.providers.split(',').forEach((provider: string) => {
|
|
1048
|
+
identities.push({ provider });
|
|
1049
|
+
providers.push(provider);
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// Add email provider if password exists
|
|
1054
|
+
if (dbUser.password) {
|
|
1055
|
+
identities.push({ provider: 'email' });
|
|
1056
|
+
providers.push('email');
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Use first provider to determine type: 'email' or 'social'
|
|
1060
|
+
const firstProvider = providers[0];
|
|
1061
|
+
const providerType = firstProvider === 'email' ? 'email' : 'social';
|
|
1062
|
+
|
|
1063
|
+
return {
|
|
1064
|
+
id: dbUser.id,
|
|
1065
|
+
email: dbUser.email,
|
|
1066
|
+
name: dbUser.name,
|
|
1067
|
+
emailVerified: dbUser.email_verified,
|
|
1068
|
+
createdAt: dbUser.created_at,
|
|
1069
|
+
updatedAt: dbUser.updated_at,
|
|
1070
|
+
identities: identities,
|
|
1071
|
+
providerType: providerType,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* List users with pagination and search
|
|
1077
|
+
*/
|
|
1078
|
+
async listUsers(
|
|
1079
|
+
limit: number,
|
|
1080
|
+
offset: number,
|
|
1081
|
+
search?: string
|
|
1082
|
+
): Promise<{ users: UserSchema[]; total: number }> {
|
|
1083
|
+
const pool = this.getPool();
|
|
1084
|
+
let query = `
|
|
1085
|
+
SELECT
|
|
1086
|
+
u.id,
|
|
1087
|
+
u.email,
|
|
1088
|
+
u.name,
|
|
1089
|
+
u.email_verified,
|
|
1090
|
+
u.created_at,
|
|
1091
|
+
u.updated_at,
|
|
1092
|
+
u.password,
|
|
1093
|
+
STRING_AGG(a.provider, ',') as providers
|
|
1094
|
+
FROM _accounts u
|
|
1095
|
+
LEFT JOIN _account_providers a ON u.id = a.user_id
|
|
1096
|
+
`;
|
|
1097
|
+
const params: (string | number)[] = [];
|
|
1098
|
+
|
|
1099
|
+
if (search) {
|
|
1100
|
+
query += ' WHERE u.email LIKE $1 OR u.name LIKE $2';
|
|
1101
|
+
params.push(`%${search}%`, `%${search}%`);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
query += ` GROUP BY u.id ORDER BY u.created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
|
|
1105
|
+
params.push(limit, offset);
|
|
1106
|
+
|
|
1107
|
+
const result = await pool.query(query, params);
|
|
1108
|
+
const dbUsers = result.rows as UserRecord[];
|
|
1109
|
+
|
|
1110
|
+
// Transform users
|
|
1111
|
+
const users = dbUsers.map((dbUser) => this.transformUserRecordToSchema(dbUser));
|
|
1112
|
+
|
|
1113
|
+
// Get total count
|
|
1114
|
+
let countQuery = 'SELECT COUNT(*) as count FROM _accounts';
|
|
1115
|
+
const countParams: string[] = [];
|
|
1116
|
+
if (search) {
|
|
1117
|
+
countQuery += ' WHERE email LIKE $1 OR name LIKE $2';
|
|
1118
|
+
countParams.push(`%${search}%`, `%${search}%`);
|
|
1119
|
+
}
|
|
1120
|
+
const countResult = await pool.query(countQuery, countParams);
|
|
1121
|
+
const count = countResult.rows[0].count;
|
|
1122
|
+
|
|
1123
|
+
return {
|
|
1124
|
+
users,
|
|
1125
|
+
total: parseInt(count, 10),
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Get user by ID (returns UserSchema for API)
|
|
1131
|
+
*/
|
|
1132
|
+
async getUserSchemaById(userId: string): Promise<UserSchema | null> {
|
|
1133
|
+
const dbUser = await this.getUserById(userId);
|
|
1134
|
+
if (!dbUser) {
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
return this.transformUserRecordToSchema(dbUser);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* Delete multiple users by IDs
|
|
1142
|
+
*/
|
|
1143
|
+
async deleteUsers(userIds: string[]): Promise<number> {
|
|
1144
|
+
const pool = this.getPool();
|
|
1145
|
+
const placeholders = userIds.map((_, i) => `$${i + 1}`).join(',');
|
|
1146
|
+
const result = await pool.query(`DELETE FROM _accounts WHERE id IN (${placeholders})`, userIds);
|
|
1147
|
+
|
|
1148
|
+
return result.rowCount || 0;
|
|
1149
|
+
}
|
|
1150
|
+
}
|