insforge 1.2.10 → 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 -20
- package/.dockerignore +60 -60
- package/.env.example +83 -77
- package/.github/ISSUE_TEMPLATE/bug_report.yml +36 -36
- package/.github/ISSUE_TEMPLATE/config.yml +11 -11
- package/.github/ISSUE_TEMPLATE/feature_request.yml +26 -26
- package/.github/PULL_REQUEST_TEMPLATE.md +7 -7
- package/.github/copilot-instructions.md +146 -146
- package/.github/workflows/build-image.yml +65 -65
- package/.github/workflows/ci-premerge-check.yml +23 -23
- package/.github/workflows/e2e.yml +63 -63
- package/.github/workflows/lint-and-format.yml +32 -32
- package/.prettierignore +64 -64
- package/CHANGELOG.md +44 -44
- package/CLAUDE_PLUGIN.md +104 -104
- package/CODE_OF_CONDUCT.md +128 -128
- package/CONTRIBUTING.md +125 -125
- package/Dockerfile +30 -30
- package/GITHUB_OAUTH_SETUP.md +49 -49
- package/GOOGLE_OAUTH_SETUP.md +148 -148
- package/LICENSE +201 -201
- package/README.md +182 -182
- package/assets/Dark.svg +23 -23
- package/auth/package.json +28 -28
- package/auth/src/lib/broadcastService.ts +117 -115
- package/auth/src/pages/SignInPage.tsx +60 -57
- package/auth/src/pages/SignUpPage.tsx +60 -57
- package/auth/tsconfig.json +32 -32
- package/auth/tsconfig.node.json +11 -11
- package/backend/package.json +78 -75
- package/backend/src/api/routes/ai/index.routes.ts +3 -3
- package/backend/src/api/routes/auth/index.routes.ts +667 -570
- package/backend/src/api/routes/auth/oauth.routes.ts +473 -448
- package/backend/src/api/routes/database/advance.routes.ts +37 -16
- package/backend/src/api/routes/database/index.routes.ts +78 -1
- package/backend/src/api/routes/database/records.routes.ts +10 -10
- package/backend/src/api/routes/database/tables.routes.ts +0 -14
- package/backend/src/api/routes/docs/index.routes.ts +75 -76
- package/backend/src/api/routes/email/index.routes.ts +35 -0
- package/backend/src/api/routes/functions/index.routes.ts +18 -12
- package/backend/src/api/routes/metadata/index.routes.ts +12 -0
- 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/storage/index.routes.ts +18 -12
- package/backend/src/api/routes/usage/index.routes.ts +6 -4
- package/backend/src/infra/database/database.manager.ts +14 -1
- package/backend/src/infra/database/migrations/000_create-base-tables.sql +141 -141
- package/backend/src/infra/database/migrations/001_create-helper-functions.sql +40 -40
- package/backend/src/infra/database/migrations/002_rename-auth-tables.sql +29 -29
- package/backend/src/infra/database/migrations/003_create-users-table.sql +55 -55
- package/backend/src/infra/database/migrations/004_add-reload-postgrest-func.sql +23 -23
- package/backend/src/infra/database/migrations/005_enable-project-admin-modify-users.sql +29 -29
- package/backend/src/infra/database/migrations/006_modify-ai-usage-table.sql +24 -24
- package/backend/src/infra/database/migrations/007_drop-metadata-table.sql +1 -1
- package/backend/src/infra/database/migrations/008_add-system-tables.sql +76 -76
- package/backend/src/infra/database/migrations/009_add-function-secrets.sql +23 -23
- package/backend/src/infra/database/migrations/010_modify-ai-config-modalities.sql +93 -93
- package/backend/src/infra/database/migrations/011_refactor-secrets-table.sql +15 -15
- package/backend/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 -44
- package/backend/src/infra/database/migrations/014_add-updated-at-trigger-user-table.sql +7 -7
- package/backend/src/infra/database/migrations/015_create-auth-config-and-email-otp-tables.sql +59 -59
- package/backend/src/infra/database/migrations/016_update-auth-config-and-email-otp.sql +24 -24
- 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/infra/security/token.manager.ts +219 -125
- package/backend/src/infra/socket/socket.manager.ts +198 -64
- package/backend/src/providers/ai/openrouter.provider.ts +12 -9
- package/backend/src/providers/email/base.provider.ts +4 -7
- package/backend/src/providers/email/cloud.provider.ts +84 -0
- package/backend/src/providers/oauth/apple.provider.ts +266 -0
- package/backend/src/providers/oauth/index.ts +1 -0
- package/backend/src/server.ts +317 -284
- package/backend/src/services/ai/ai-model.service.ts +5 -5
- package/backend/src/services/ai/chat-completion.service.ts +4 -4
- package/backend/src/services/ai/image-generation.service.ts +3 -3
- package/backend/src/services/auth/auth.service.ts +14 -0
- package/backend/src/services/database/database-table.service.ts +0 -9
- package/backend/src/services/database/database.service.ts +127 -0
- package/backend/src/services/email/email.service.ts +5 -7
- 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/types/auth.ts +11 -0
- package/backend/src/types/realtime.ts +18 -0
- package/backend/src/types/socket.ts +7 -31
- package/backend/src/utils/cookies.ts +35 -0
- package/backend/src/utils/s3-config-loader.ts +64 -0
- package/backend/src/utils/seed.ts +301 -298
- package/backend/src/utils/sql-parser.ts +90 -0
- 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 -129
- package/backend/tests/local/test-ai-usage.sh +80 -80
- 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 -123
- package/backend/tests/local/test-id-field.sh +200 -200
- package/backend/tests/local/test-logs.sh +132 -132
- package/backend/tests/local/test-public-bucket.sh +264 -264
- package/backend/tests/local/test-secrets.sh +249 -249
- 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 -244
- 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 -338
- package/backend/tests/unit/analyze-query.test.ts +697 -0
- package/backend/tsconfig.json +22 -22
- package/claude-plugin/.claude-plugin/plugin.json +24 -24
- package/claude-plugin/README.md +133 -133
- package/claude-plugin/skills/insforge-schema-patterns/SKILL.md +270 -270
- package/docker-compose.prod.yml +204 -200
- package/docker-compose.yml +232 -228
- package/docker-init/db/db-init.sql +97 -97
- package/docker-init/db/jwt.sql +5 -5
- package/docker-init/db/postgresql.conf +16 -16
- package/docker-init/logs/vector.yml +236 -236
- package/docs/README.md +44 -44
- package/docs/agent-docs/real-time.md +269 -0
- package/docs/changelog.mdx +119 -67
- package/docs/core-concepts/ai/architecture.mdx +372 -372
- package/docs/core-concepts/ai/sdk.mdx +213 -213
- package/docs/core-concepts/authentication/architecture.mdx +278 -278
- package/docs/core-concepts/authentication/sdk.mdx +414 -414
- package/docs/core-concepts/authentication/ui-components/customization.mdx +529 -529
- package/docs/core-concepts/authentication/ui-components/nextjs.mdx +221 -221
- package/docs/core-concepts/authentication/ui-components/react-router.mdx +184 -184
- package/docs/core-concepts/authentication/ui-components/react.mdx +129 -129
- package/docs/core-concepts/database/architecture.mdx +255 -255
- package/docs/core-concepts/database/sdk.mdx +382 -382
- 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 -105
- package/docs/core-concepts/functions/sdk.mdx +184 -184
- 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 -243
- package/docs/core-concepts/storage/sdk.mdx +253 -253
- package/docs/deployment/README.md +94 -94
- package/docs/deployment/deploy-to-aws-ec2.md +564 -564
- package/docs/deployment/deploy-to-azure-virtual-machines.md +312 -312
- package/docs/deployment/deploy-to-google-cloud-compute-engine.md +613 -613
- package/docs/deployment/deploy-to-render.md +441 -441
- 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 -210
- package/docs/examples/framework-guides/nextjs.mdx +131 -131
- package/docs/examples/framework-guides/nuxt.mdx +165 -165
- package/docs/examples/framework-guides/react.mdx +165 -165
- package/docs/examples/framework-guides/svelte.mdx +153 -153
- package/docs/examples/framework-guides/vue.mdx +159 -159
- package/docs/examples/overview.mdx +67 -67
- package/docs/favicon.svg +19 -19
- 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/icons/ai.svg +4 -4
- package/docs/images/logos/nextjs.svg +4 -4
- package/docs/images/logos/nuxt.svg +4 -4
- package/docs/images/logos/react.svg +5 -5
- package/docs/images/logos/svelte.svg +4 -4
- package/docs/images/logos/vue.svg +5 -5
- package/docs/insforge-instructions-sdk.md +89 -88
- package/docs/introduction.mdx +45 -45
- package/docs/logo/dark.svg +22 -22
- package/docs/logo/light.svg +20 -20
- package/docs/partnership.mdx +651 -646
- package/docs/quickstart.mdx +82 -82
- package/docs/showcase.mdx +52 -52
- package/docs/snippets/sdk-installation.mdx +21 -21
- package/docs/snippets/service-icons.mdx +27 -27
- 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 -69
- 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 -3
- package/frontend/src/assets/icons/error.svg +3 -3
- package/frontend/src/assets/icons/loader.svg +9 -9
- 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 +3 -3
- 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 -3
- 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 +1 -1
- package/frontend/src/assets/logos/linkedin.svg +3 -3
- 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 +16 -16
- package/frontend/src/assets/logos/tiktok.svg +5 -5
- 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 -3
- package/frontend/src/components/layout/AppHeader.tsx +9 -10
- package/frontend/src/features/auth/components/OAuthConfigDialog.tsx +1 -0
- package/frontend/src/features/auth/components/UsersDataGrid.tsx +6 -0
- package/frontend/src/features/auth/helpers.tsx +8 -0
- package/frontend/src/features/auth/{page → pages}/UsersPage.tsx +0 -28
- package/frontend/src/features/database/components/SQLModal.tsx +75 -0
- package/frontend/src/features/database/components/TableForm.tsx +0 -4
- package/frontend/src/features/database/hooks/useDatabase.ts +66 -0
- package/frontend/src/features/database/hooks/useTables.ts +32 -28
- package/frontend/src/features/database/index.ts +1 -0
- package/frontend/src/features/database/{page → pages}/FunctionsPage.tsx +29 -37
- package/frontend/src/features/database/{page → pages}/IndexesPage.tsx +35 -47
- package/frontend/src/features/database/{page → pages}/PoliciesPage.tsx +43 -54
- package/frontend/src/features/database/{page → pages}/TablesPage.tsx +0 -42
- package/frontend/src/features/database/{page → pages}/TriggersPage.tsx +35 -47
- package/frontend/src/features/database/services/advance.service.ts +0 -26
- package/frontend/src/features/database/services/database.service.ts +55 -0
- package/frontend/src/features/database/services/table.service.ts +0 -6
- package/frontend/src/features/functions/{page → pages}/FunctionsPage.tsx +21 -44
- package/frontend/src/features/functions/{page → pages}/SecretsPage.tsx +11 -9
- package/frontend/src/features/logs/hooks/useMcpUsage.ts +13 -66
- 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/{page → pages}/StoragePage.tsx +1 -29
- package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +3 -3
- package/frontend/src/features/visualizer/{page → pages}/VisualizerPage.tsx +1 -35
- package/frontend/src/lib/contexts/SocketContext.tsx +119 -75
- package/frontend/src/lib/routing/AppRoutes.tsx +35 -20
- package/frontend/src/lib/utils/cloudMessaging.ts +1 -1
- package/frontend/src/lib/utils/menuItems.ts +24 -0
- package/frontend/src/lib/utils/utils.ts +14 -1
- package/frontend/tsconfig.json +25 -25
- package/frontend/tsconfig.node.json +9 -9
- package/functions/deno.json +24 -24
- package/functions/server.ts +315 -315
- package/i18n/README.ar.md +130 -130
- package/i18n/README.de.md +130 -130
- package/i18n/README.es.md +154 -154
- package/i18n/README.fr.md +134 -134
- package/i18n/README.hi.md +129 -129
- package/i18n/README.ja.md +174 -174
- package/i18n/README.ko.md +136 -136
- package/i18n/README.pt-BR.md +131 -131
- package/i18n/README.ru.md +129 -129
- package/i18n/README.zh-CN.md +133 -133
- package/openapi/ai.yaml +715 -715
- package/openapi/auth.yaml +1244 -1244
- 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 -97
- package/shared-schemas/package.json +31 -31
- package/shared-schemas/src/ai.schema.ts +63 -59
- package/shared-schemas/src/auth-api.schema.ts +352 -339
- package/shared-schemas/src/auth.schema.ts +1 -1
- package/shared-schemas/src/database-api.schema.ts +32 -1
- package/shared-schemas/src/database.schema.ts +39 -0
- 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 +4 -0
- package/shared-schemas/src/metadata.schema.ts +9 -0
- 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 -13
- package/zeabur/template.yml +1032 -1032
- package/.cursor/rules/cursor-rules.mdc +0 -94
- package/frontend/src/features/database/hooks/useFullMetadata.ts +0 -18
- package/test-gemini.sh +0 -35
- package/test-usage-admin.sh +0 -57
- package/test-usage.sh +0 -50
- /package/frontend/src/features/ai/{page → pages}/AIPage.tsx +0 -0
- /package/frontend/src/features/auth/{page → pages}/AuthMethodsPage.tsx +0 -0
- /package/frontend/src/features/auth/{page → pages}/ConfigurationPage.tsx +0 -0
- /package/frontend/src/features/dashboard/{page → pages}/DashboardPage.tsx +0 -0
- /package/frontend/src/features/database/{page → pages}/SQLEditorPage.tsx +0 -0
- /package/frontend/src/features/database/{page → pages}/TemplatesPage.tsx +0 -0
- /package/frontend/src/features/login/{page → pages}/CloudLoginPage.tsx +0 -0
- /package/frontend/src/features/login/{page → pages}/LoginPage.tsx +0 -0
- /package/frontend/src/features/logs/{page → pages}/AuditsPage.tsx +0 -0
- /package/frontend/src/features/logs/{page → pages}/LogsPage.tsx +0 -0
- /package/frontend/src/features/logs/{page → pages}/MCPLogsPage.tsx +0 -0
|
@@ -1,570 +1,667 @@
|
|
|
1
|
-
import { Router, Request, Response, NextFunction } from 'express';
|
|
2
|
-
import { AuthService } from '@/services/auth/auth.service.js';
|
|
3
|
-
import { AuthConfigService } from '@/services/auth/auth-config.service.js';
|
|
4
|
-
import { OAuthConfigService } from '@/services/auth/oauth-config.service.js';
|
|
5
|
-
import { AuditService } from '@/services/logs/audit.service.js';
|
|
6
|
-
import { TokenManager } from '@/infra/security/token.manager.js';
|
|
7
|
-
import { AppError } from '@/api/middlewares/error.js';
|
|
8
|
-
import { ERROR_CODES } from '@/types/error-constants.js';
|
|
9
|
-
import { successResponse } from '@/utils/response.js';
|
|
10
|
-
import { AuthRequest, verifyAdmin, verifyToken } from '@/api/middlewares/auth.js';
|
|
11
|
-
import oauthRouter from './oauth.routes.js';
|
|
12
|
-
import { sendEmailOTPLimiter, verifyOTPLimiter } from '@/api/middlewares/rate-limiters.js';
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
type
|
|
31
|
-
type
|
|
32
|
-
type
|
|
33
|
-
type
|
|
34
|
-
type
|
|
35
|
-
|
|
36
|
-
type
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
//
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
:
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
);
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
1
|
+
import { Router, Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { AuthService } from '@/services/auth/auth.service.js';
|
|
3
|
+
import { AuthConfigService } from '@/services/auth/auth-config.service.js';
|
|
4
|
+
import { OAuthConfigService } from '@/services/auth/oauth-config.service.js';
|
|
5
|
+
import { AuditService } from '@/services/logs/audit.service.js';
|
|
6
|
+
import { TokenManager } from '@/infra/security/token.manager.js';
|
|
7
|
+
import { AppError } from '@/api/middlewares/error.js';
|
|
8
|
+
import { ERROR_CODES } from '@/types/error-constants.js';
|
|
9
|
+
import { successResponse } from '@/utils/response.js';
|
|
10
|
+
import { AuthRequest, verifyAdmin, verifyToken } from '@/api/middlewares/auth.js';
|
|
11
|
+
import oauthRouter from './oauth.routes.js';
|
|
12
|
+
import { sendEmailOTPLimiter, verifyOTPLimiter } from '@/api/middlewares/rate-limiters.js';
|
|
13
|
+
import {
|
|
14
|
+
REFRESH_TOKEN_COOKIE_NAME,
|
|
15
|
+
setAuthCookie,
|
|
16
|
+
clearAuthCookie,
|
|
17
|
+
} from '@/utils/cookies.js';
|
|
18
|
+
import {
|
|
19
|
+
userIdSchema,
|
|
20
|
+
createUserRequestSchema,
|
|
21
|
+
createSessionRequestSchema,
|
|
22
|
+
createAdminSessionRequestSchema,
|
|
23
|
+
deleteUsersRequestSchema,
|
|
24
|
+
listUsersRequestSchema,
|
|
25
|
+
sendVerificationEmailRequestSchema,
|
|
26
|
+
verifyEmailRequestSchema,
|
|
27
|
+
sendResetPasswordEmailRequestSchema,
|
|
28
|
+
exchangeResetPasswordTokenRequestSchema,
|
|
29
|
+
resetPasswordRequestSchema,
|
|
30
|
+
type CreateUserResponse,
|
|
31
|
+
type CreateSessionResponse,
|
|
32
|
+
type VerifyEmailResponse,
|
|
33
|
+
type ExchangeResetPasswordTokenResponse,
|
|
34
|
+
type ResetPasswordResponse,
|
|
35
|
+
type CreateAdminSessionResponse,
|
|
36
|
+
type GetCurrentSessionResponse,
|
|
37
|
+
type ListUsersResponse,
|
|
38
|
+
type DeleteUsersResponse,
|
|
39
|
+
type GetPublicAuthConfigResponse,
|
|
40
|
+
exchangeAdminSessionRequestSchema,
|
|
41
|
+
type GetAuthConfigResponse,
|
|
42
|
+
updateAuthConfigRequestSchema,
|
|
43
|
+
} from '@insforge/shared-schemas';
|
|
44
|
+
import { SocketManager } from '@/infra/socket/socket.manager.js';
|
|
45
|
+
import { DataUpdateResourceType, ServerEvents } from '@/types/socket.js';
|
|
46
|
+
import logger from '@/utils/logger.js';
|
|
47
|
+
|
|
48
|
+
const router = Router();
|
|
49
|
+
const authService = AuthService.getInstance();
|
|
50
|
+
const authConfigService = AuthConfigService.getInstance();
|
|
51
|
+
const oAuthConfigService = OAuthConfigService.getInstance();
|
|
52
|
+
const auditService = AuditService.getInstance();
|
|
53
|
+
|
|
54
|
+
// Mount OAuth routes
|
|
55
|
+
router.use('/oauth', oauthRouter);
|
|
56
|
+
|
|
57
|
+
// Public Authentication Configuration Routes
|
|
58
|
+
// GET /api/auth/public-config - Get all public authentication configuration (public endpoint)
|
|
59
|
+
router.get('/public-config', async (req: Request, res: Response, next: NextFunction) => {
|
|
60
|
+
try {
|
|
61
|
+
const [oAuthProviders, authConfigs] = await Promise.all([
|
|
62
|
+
oAuthConfigService.getConfiguredProviders(),
|
|
63
|
+
authConfigService.getPublicAuthConfig(),
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
const response: GetPublicAuthConfigResponse = {
|
|
67
|
+
oAuthProviders,
|
|
68
|
+
...authConfigs,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
successResponse(res, response);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
next(error);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Email Authentication Configuration Routes
|
|
78
|
+
// GET /api/auth/config - Get authentication configurations (admin only)
|
|
79
|
+
router.get('/config', verifyAdmin, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
80
|
+
try {
|
|
81
|
+
const config: GetAuthConfigResponse = await authConfigService.getAuthConfig();
|
|
82
|
+
successResponse(res, config);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
next(error);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// PUT /api/auth/config - Update authentication configurations (admin only)
|
|
89
|
+
router.put('/config', verifyAdmin, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
90
|
+
try {
|
|
91
|
+
const validationResult = updateAuthConfigRequestSchema.safeParse(req.body);
|
|
92
|
+
if (!validationResult.success) {
|
|
93
|
+
throw new AppError(
|
|
94
|
+
validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
|
|
95
|
+
400,
|
|
96
|
+
ERROR_CODES.INVALID_INPUT
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const input = validationResult.data;
|
|
101
|
+
const config: GetAuthConfigResponse = await authConfigService.updateAuthConfig(input);
|
|
102
|
+
|
|
103
|
+
await auditService.log({
|
|
104
|
+
actor: req.user?.email || 'api-key',
|
|
105
|
+
action: 'UPDATE_AUTH_CONFIG',
|
|
106
|
+
module: 'AUTH',
|
|
107
|
+
details: {
|
|
108
|
+
updatedFields: Object.keys(input),
|
|
109
|
+
},
|
|
110
|
+
ip_address: req.ip,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
successResponse(res, config);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
next(error);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// POST /api/auth/users - Create a new user (registration)
|
|
120
|
+
router.post('/users', async (req: Request, res: Response, next: NextFunction) => {
|
|
121
|
+
try {
|
|
122
|
+
const validationResult = createUserRequestSchema.safeParse(req.body);
|
|
123
|
+
if (!validationResult.success) {
|
|
124
|
+
throw new AppError(
|
|
125
|
+
validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
|
|
126
|
+
400,
|
|
127
|
+
ERROR_CODES.INVALID_INPUT
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const { email, password, name } = validationResult.data;
|
|
132
|
+
const result: CreateUserResponse = await authService.register(email, password, name);
|
|
133
|
+
|
|
134
|
+
// Set refresh token in httpOnly cookie and generate CSRF token
|
|
135
|
+
let csrfToken: string | null = null;
|
|
136
|
+
if (result.accessToken && result.user) {
|
|
137
|
+
const tokenManager = TokenManager.getInstance();
|
|
138
|
+
const refreshToken = tokenManager.generateRefreshToken(result.user.id);
|
|
139
|
+
setAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME, refreshToken);
|
|
140
|
+
csrfToken = tokenManager.generateCsrfToken(refreshToken);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const socket = SocketManager.getInstance();
|
|
144
|
+
socket.broadcastToRoom(
|
|
145
|
+
'role:project_admin',
|
|
146
|
+
ServerEvents.DATA_UPDATE,
|
|
147
|
+
{ resource: DataUpdateResourceType.USERS },
|
|
148
|
+
'system'
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
successResponse(res, { ...result, csrfToken });
|
|
152
|
+
} catch (error) {
|
|
153
|
+
next(error);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// POST /api/auth/sessions - Create a new session (login)
|
|
158
|
+
router.post('/sessions', async (req: Request, res: Response, next: NextFunction) => {
|
|
159
|
+
try {
|
|
160
|
+
const validationResult = createSessionRequestSchema.safeParse(req.body);
|
|
161
|
+
if (!validationResult.success) {
|
|
162
|
+
throw new AppError(
|
|
163
|
+
validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
|
|
164
|
+
400,
|
|
165
|
+
ERROR_CODES.INVALID_INPUT
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const { email, password } = validationResult.data;
|
|
170
|
+
const result: CreateSessionResponse = await authService.login(email, password);
|
|
171
|
+
|
|
172
|
+
// Set refresh token in httpOnly cookie and generate CSRF token
|
|
173
|
+
const tokenManager = TokenManager.getInstance();
|
|
174
|
+
const refreshToken = tokenManager.generateRefreshToken(result.user.id);
|
|
175
|
+
setAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME, refreshToken);
|
|
176
|
+
const csrfToken = tokenManager.generateCsrfToken(refreshToken);
|
|
177
|
+
|
|
178
|
+
successResponse(res, { ...result, csrfToken });
|
|
179
|
+
} catch (error) {
|
|
180
|
+
next(error);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// POST /api/auth/refresh - Refresh access token using httpOnly cookie
|
|
185
|
+
// Requires X-CSRF-Token header for CSRF protection
|
|
186
|
+
router.post('/refresh', async (req: Request, res: Response, next: NextFunction) => {
|
|
187
|
+
try {
|
|
188
|
+
const refreshToken = req.cookies?.[REFRESH_TOKEN_COOKIE_NAME];
|
|
189
|
+
|
|
190
|
+
if (!refreshToken) {
|
|
191
|
+
throw new AppError('No refresh token provided', 401, ERROR_CODES.AUTH_UNAUTHORIZED);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const tokenManager = TokenManager.getInstance();
|
|
195
|
+
|
|
196
|
+
// Verify CSRF token by re-computing from refresh token
|
|
197
|
+
const csrfHeader = req.headers['x-csrf-token'] as string | undefined;
|
|
198
|
+
if (!tokenManager.verifyCsrfToken(csrfHeader, refreshToken)) {
|
|
199
|
+
logger.warn('[Auth:Refresh] CSRF token validation failed');
|
|
200
|
+
throw new AppError('Invalid CSRF token', 403, ERROR_CODES.AUTH_UNAUTHORIZED);
|
|
201
|
+
}
|
|
202
|
+
const payload = tokenManager.verifyRefreshToken(refreshToken);
|
|
203
|
+
|
|
204
|
+
// Fetch CURRENT user data from DB (email/role may have changed)
|
|
205
|
+
const user = await authService.getUserSchemaById(payload.sub);
|
|
206
|
+
|
|
207
|
+
if (!user) {
|
|
208
|
+
logger.warn('[Auth:Refresh] User not found for valid refresh token', { userId: payload.sub });
|
|
209
|
+
clearAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME);
|
|
210
|
+
throw new AppError('User not found', 401, ERROR_CODES.AUTH_UNAUTHORIZED);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Generate new access token
|
|
214
|
+
const newAccessToken = tokenManager.generateToken({
|
|
215
|
+
sub: user.id,
|
|
216
|
+
email: user.email,
|
|
217
|
+
role: 'authenticated',
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Generate new refresh token (token rotation for security)
|
|
221
|
+
const newRefreshToken = tokenManager.generateRefreshToken(user.id);
|
|
222
|
+
|
|
223
|
+
setAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME, newRefreshToken);
|
|
224
|
+
const newCsrfToken = tokenManager.generateCsrfToken(newRefreshToken);
|
|
225
|
+
|
|
226
|
+
successResponse(res, {
|
|
227
|
+
accessToken: newAccessToken,
|
|
228
|
+
user: user,
|
|
229
|
+
csrfToken: newCsrfToken,
|
|
230
|
+
});
|
|
231
|
+
} catch (error) {
|
|
232
|
+
// Clear invalid cookie on error
|
|
233
|
+
clearAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME);
|
|
234
|
+
next(error);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// POST /api/auth/logout - Logout and clear refresh token cookie
|
|
239
|
+
router.post('/logout', (_req: Request, res: Response, next: NextFunction) => {
|
|
240
|
+
try {
|
|
241
|
+
clearAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME);
|
|
242
|
+
|
|
243
|
+
successResponse(res, {
|
|
244
|
+
success: true,
|
|
245
|
+
message: 'Logged out successfully',
|
|
246
|
+
});
|
|
247
|
+
} catch (error) {
|
|
248
|
+
next(error);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// POST /api/auth/admin/sessions/exchange - Create admin session
|
|
253
|
+
router.post('/admin/sessions/exchange', async (req: Request, res: Response, next: NextFunction) => {
|
|
254
|
+
try {
|
|
255
|
+
const validationResult = exchangeAdminSessionRequestSchema.safeParse(req.body);
|
|
256
|
+
if (!validationResult.success) {
|
|
257
|
+
throw new AppError(
|
|
258
|
+
validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
|
|
259
|
+
400,
|
|
260
|
+
ERROR_CODES.INVALID_INPUT
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const { code } = validationResult.data;
|
|
265
|
+
const result: CreateAdminSessionResponse =
|
|
266
|
+
await authService.adminLoginWithAuthorizationCode(code);
|
|
267
|
+
|
|
268
|
+
successResponse(res, result);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (error instanceof AppError) {
|
|
271
|
+
next(error);
|
|
272
|
+
} else {
|
|
273
|
+
// Convert other errors (like JWT verification errors) to 400
|
|
274
|
+
next(
|
|
275
|
+
new AppError(
|
|
276
|
+
'Failed to exchange admin session' + (error instanceof Error ? `: ${error.message}` : ''),
|
|
277
|
+
400,
|
|
278
|
+
ERROR_CODES.INVALID_INPUT
|
|
279
|
+
)
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// POST /api/auth/admin/sessions - Create admin session
|
|
286
|
+
router.post('/admin/sessions', (req: Request, res: Response, next: NextFunction) => {
|
|
287
|
+
try {
|
|
288
|
+
const validationResult = createAdminSessionRequestSchema.safeParse(req.body);
|
|
289
|
+
if (!validationResult.success) {
|
|
290
|
+
throw new AppError(
|
|
291
|
+
validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
|
|
292
|
+
400,
|
|
293
|
+
ERROR_CODES.INVALID_INPUT
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const { email, password } = validationResult.data;
|
|
298
|
+
const result: CreateAdminSessionResponse = authService.adminLogin(email, password);
|
|
299
|
+
|
|
300
|
+
successResponse(res, result);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
next(error);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// GET /api/auth/sessions/current - Get current session user
|
|
307
|
+
router.get(
|
|
308
|
+
'/sessions/current',
|
|
309
|
+
verifyToken,
|
|
310
|
+
(req: AuthRequest, res: Response, next: NextFunction) => {
|
|
311
|
+
try {
|
|
312
|
+
if (!req.user) {
|
|
313
|
+
throw new AppError('User not authenticated', 401, ERROR_CODES.AUTH_INVALID_CREDENTIALS);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const response: GetCurrentSessionResponse = {
|
|
317
|
+
user: {
|
|
318
|
+
id: req.user.id,
|
|
319
|
+
email: req.user.email,
|
|
320
|
+
role: req.user.role as 'authenticated' | 'project_admin',
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
successResponse(res, response);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
next(error);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// GET /api/auth/users - List all users (admin only)
|
|
332
|
+
router.get('/users', verifyAdmin, async (req: Request, res: Response, next: NextFunction) => {
|
|
333
|
+
try {
|
|
334
|
+
const queryValidation = listUsersRequestSchema.safeParse(req.query);
|
|
335
|
+
const queryParams = queryValidation.success ? queryValidation.data : req.query;
|
|
336
|
+
const { limit = '10', offset = '0', search } = queryParams || {};
|
|
337
|
+
|
|
338
|
+
const parsedLimit = parseInt(limit as string);
|
|
339
|
+
const parsedOffset = parseInt(offset as string);
|
|
340
|
+
|
|
341
|
+
const { users, total } = await authService.listUsers(
|
|
342
|
+
parsedLimit,
|
|
343
|
+
parsedOffset,
|
|
344
|
+
search as string | undefined
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const response: ListUsersResponse = {
|
|
348
|
+
data: users,
|
|
349
|
+
pagination: {
|
|
350
|
+
offset: parsedOffset,
|
|
351
|
+
limit: parsedLimit,
|
|
352
|
+
total: total,
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
successResponse(res, response);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
next(error);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// GET /api/auth/users/:id - Get specific user (admin only)
|
|
363
|
+
router.get(
|
|
364
|
+
'/users/:userId',
|
|
365
|
+
verifyAdmin,
|
|
366
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
367
|
+
try {
|
|
368
|
+
// Validate userId path parameter directly
|
|
369
|
+
const userIdValidation = userIdSchema.safeParse(req.params.userId);
|
|
370
|
+
if (!userIdValidation.success) {
|
|
371
|
+
throw new AppError('Invalid user ID format', 400, ERROR_CODES.INVALID_INPUT);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const userId = userIdValidation.data;
|
|
375
|
+
const user = await authService.getUserSchemaById(userId);
|
|
376
|
+
|
|
377
|
+
if (!user) {
|
|
378
|
+
throw new AppError('User not found', 404, ERROR_CODES.NOT_FOUND);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
successResponse(res, user);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
next(error);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
// DELETE /api/auth/users - Delete users (batch operation, admin only)
|
|
389
|
+
router.delete(
|
|
390
|
+
'/users',
|
|
391
|
+
verifyAdmin,
|
|
392
|
+
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
393
|
+
try {
|
|
394
|
+
const validationResult = deleteUsersRequestSchema.safeParse(req.body);
|
|
395
|
+
if (!validationResult.success) {
|
|
396
|
+
throw new AppError(
|
|
397
|
+
validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
|
|
398
|
+
400,
|
|
399
|
+
ERROR_CODES.INVALID_INPUT
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const { userIds } = validationResult.data;
|
|
404
|
+
|
|
405
|
+
const deletedCount = await authService.deleteUsers(userIds);
|
|
406
|
+
|
|
407
|
+
// Log audit for user deletion
|
|
408
|
+
await auditService.log({
|
|
409
|
+
actor: req.user?.email || 'api-key',
|
|
410
|
+
action: 'DELETE_USERS',
|
|
411
|
+
module: 'AUTH',
|
|
412
|
+
details: {
|
|
413
|
+
userIds,
|
|
414
|
+
deletedCount,
|
|
415
|
+
},
|
|
416
|
+
ip_address: req.ip,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const response: DeleteUsersResponse = {
|
|
420
|
+
message: 'Users deleted successfully',
|
|
421
|
+
deletedCount,
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
successResponse(res, response);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
next(error);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
// POST /api/auth/tokens/anon - Generate anonymous JWT token (never expires)
|
|
432
|
+
router.post('/tokens/anon', verifyAdmin, (_req: Request, res: Response, next: NextFunction) => {
|
|
433
|
+
try {
|
|
434
|
+
const tokenManager = TokenManager.getInstance();
|
|
435
|
+
const token = tokenManager.generateAnonToken();
|
|
436
|
+
|
|
437
|
+
successResponse(res, {
|
|
438
|
+
accessToken: token,
|
|
439
|
+
message: 'Anonymous token generated successfully (never expires)',
|
|
440
|
+
});
|
|
441
|
+
} catch (error) {
|
|
442
|
+
next(error);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// POST /api/auth/email/send-verification - Send email verification (code or link based on config)
|
|
447
|
+
router.post(
|
|
448
|
+
'/email/send-verification',
|
|
449
|
+
sendEmailOTPLimiter,
|
|
450
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
451
|
+
try {
|
|
452
|
+
const validationResult = sendVerificationEmailRequestSchema.safeParse(req.body);
|
|
453
|
+
if (!validationResult.success) {
|
|
454
|
+
throw new AppError(
|
|
455
|
+
validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
|
|
456
|
+
400,
|
|
457
|
+
ERROR_CODES.INVALID_INPUT
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const { email } = validationResult.data;
|
|
462
|
+
|
|
463
|
+
// Get auth config to determine verification method
|
|
464
|
+
const authConfig = await authConfigService.getAuthConfig();
|
|
465
|
+
const method = authConfig.verifyEmailMethod;
|
|
466
|
+
|
|
467
|
+
// Note: User enumeration is prevented at service layer
|
|
468
|
+
// Service returns gracefully (no error) if user not found
|
|
469
|
+
if (method === 'link') {
|
|
470
|
+
await authService.sendVerificationEmailWithLink(email);
|
|
471
|
+
} else {
|
|
472
|
+
await authService.sendVerificationEmailWithCode(email);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Always return 202 Accepted with generic message
|
|
476
|
+
const message =
|
|
477
|
+
method === 'link'
|
|
478
|
+
? 'If your email is registered, we have sent you a verification link. Please check your inbox.'
|
|
479
|
+
: 'If your email is registered, we have sent you a verification code. Please check your inbox.';
|
|
480
|
+
|
|
481
|
+
successResponse(
|
|
482
|
+
res,
|
|
483
|
+
{
|
|
484
|
+
success: true,
|
|
485
|
+
message,
|
|
486
|
+
},
|
|
487
|
+
202
|
|
488
|
+
);
|
|
489
|
+
} catch (error) {
|
|
490
|
+
next(error);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
// POST /api/auth/email/verify - Verify email with OTP
|
|
496
|
+
// Uses verifyEmailMethod from auth config to determine verification type:
|
|
497
|
+
// - 'code': expects email + 6-digit numeric code
|
|
498
|
+
// - 'link': expects 64-char hex token only
|
|
499
|
+
router.post(
|
|
500
|
+
'/email/verify',
|
|
501
|
+
verifyOTPLimiter,
|
|
502
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
503
|
+
try {
|
|
504
|
+
const validationResult = verifyEmailRequestSchema.safeParse(req.body);
|
|
505
|
+
if (!validationResult.success) {
|
|
506
|
+
throw new AppError(
|
|
507
|
+
validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
|
|
508
|
+
400,
|
|
509
|
+
ERROR_CODES.INVALID_INPUT
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const { email, otp } = validationResult.data;
|
|
514
|
+
|
|
515
|
+
// Get auth config to determine verification method
|
|
516
|
+
const authConfig = await authConfigService.getAuthConfig();
|
|
517
|
+
const method = authConfig.verifyEmailMethod;
|
|
518
|
+
|
|
519
|
+
let result: VerifyEmailResponse;
|
|
520
|
+
|
|
521
|
+
if (method === 'link') {
|
|
522
|
+
// Link verification: otp is 64-char hex token
|
|
523
|
+
result = await authService.verifyEmailWithToken(otp);
|
|
524
|
+
} else {
|
|
525
|
+
// Code verification: requires email + 6-digit code
|
|
526
|
+
if (!email) {
|
|
527
|
+
throw new AppError(
|
|
528
|
+
'Email is required for code verification',
|
|
529
|
+
400,
|
|
530
|
+
ERROR_CODES.INVALID_INPUT
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
result = await authService.verifyEmailWithCode(email, otp);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Set refresh token in httpOnly cookie and generate CSRF token
|
|
537
|
+
const tokenManager = TokenManager.getInstance();
|
|
538
|
+
const refreshToken = tokenManager.generateRefreshToken(result.user.id);
|
|
539
|
+
setAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME, refreshToken);
|
|
540
|
+
const csrfToken = tokenManager.generateCsrfToken(refreshToken);
|
|
541
|
+
successResponse(res, { ...result, csrfToken });
|
|
542
|
+
} catch (error) {
|
|
543
|
+
next(error);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
// POST /api/auth/email/send-reset-password - Send password reset (code or link based on config)
|
|
549
|
+
router.post(
|
|
550
|
+
'/email/send-reset-password',
|
|
551
|
+
sendEmailOTPLimiter,
|
|
552
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
553
|
+
try {
|
|
554
|
+
const validationResult = sendResetPasswordEmailRequestSchema.safeParse(req.body);
|
|
555
|
+
if (!validationResult.success) {
|
|
556
|
+
throw new AppError(
|
|
557
|
+
validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
|
|
558
|
+
400,
|
|
559
|
+
ERROR_CODES.INVALID_INPUT
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const { email } = validationResult.data;
|
|
564
|
+
|
|
565
|
+
// Get auth config to determine reset password method
|
|
566
|
+
const authConfig = await authConfigService.getAuthConfig();
|
|
567
|
+
const method = authConfig.resetPasswordMethod;
|
|
568
|
+
|
|
569
|
+
// Note: User enumeration is prevented at service layer
|
|
570
|
+
// Service returns gracefully (no error) if user not found
|
|
571
|
+
if (method === 'link') {
|
|
572
|
+
await authService.sendResetPasswordEmailWithLink(email);
|
|
573
|
+
} else {
|
|
574
|
+
await authService.sendResetPasswordEmailWithCode(email);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Always return 202 Accepted with generic message
|
|
578
|
+
const message =
|
|
579
|
+
method === 'link'
|
|
580
|
+
? 'If your email is registered, we have sent you a password reset link. Please check your inbox.'
|
|
581
|
+
: 'If your email is registered, we have sent you a password reset code. Please check your inbox.';
|
|
582
|
+
|
|
583
|
+
successResponse(
|
|
584
|
+
res,
|
|
585
|
+
{
|
|
586
|
+
success: true,
|
|
587
|
+
message,
|
|
588
|
+
},
|
|
589
|
+
202
|
|
590
|
+
);
|
|
591
|
+
} catch (error) {
|
|
592
|
+
next(error);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
// POST /api/auth/email/exchange-reset-password-token - Exchange reset password code for reset token
|
|
598
|
+
// Step 1 of two-step password reset flow: verify code → get reset token
|
|
599
|
+
// Only used when resetPasswordMethod is 'code'
|
|
600
|
+
router.post(
|
|
601
|
+
'/email/exchange-reset-password-token',
|
|
602
|
+
verifyOTPLimiter,
|
|
603
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
604
|
+
try {
|
|
605
|
+
const validationResult = exchangeResetPasswordTokenRequestSchema.safeParse(req.body);
|
|
606
|
+
if (!validationResult.success) {
|
|
607
|
+
throw new AppError(
|
|
608
|
+
validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
|
|
609
|
+
400,
|
|
610
|
+
ERROR_CODES.INVALID_INPUT
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const { email, code } = validationResult.data;
|
|
615
|
+
|
|
616
|
+
const result = await authService.exchangeResetPasswordToken(email, code);
|
|
617
|
+
|
|
618
|
+
const response: ExchangeResetPasswordTokenResponse = {
|
|
619
|
+
token: result.token,
|
|
620
|
+
expiresAt: result.expiresAt.toISOString(),
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
successResponse(res, response);
|
|
624
|
+
} catch (error) {
|
|
625
|
+
next(error);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
// POST /api/auth/email/reset-password - Reset password with token
|
|
631
|
+
// Token can be:
|
|
632
|
+
// - Magic link token (from send-reset-password endpoint when method is 'link')
|
|
633
|
+
// - Reset token (from exchange-reset-password-token endpoint after code verification)
|
|
634
|
+
// Both use RESET_PASSWORD purpose and are verified the same way
|
|
635
|
+
// Flow:
|
|
636
|
+
// Code: send-reset-password → exchange-reset-password-token → reset-password (with resetToken)
|
|
637
|
+
// Link: send-reset-password → reset-password (with link token)
|
|
638
|
+
router.post(
|
|
639
|
+
'/email/reset-password',
|
|
640
|
+
verifyOTPLimiter,
|
|
641
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
642
|
+
try {
|
|
643
|
+
const validationResult = resetPasswordRequestSchema.safeParse(req.body);
|
|
644
|
+
if (!validationResult.success) {
|
|
645
|
+
throw new AppError(
|
|
646
|
+
validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
|
|
647
|
+
400,
|
|
648
|
+
ERROR_CODES.INVALID_INPUT
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const { newPassword, otp } = validationResult.data;
|
|
653
|
+
|
|
654
|
+
// Both magic link tokens and code-verified reset tokens use RESET_PASSWORD purpose
|
|
655
|
+
const result: ResetPasswordResponse = await authService.resetPasswordWithToken(
|
|
656
|
+
newPassword,
|
|
657
|
+
otp
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
successResponse(res, result); // Return message with optional redirectTo
|
|
661
|
+
} catch (error) {
|
|
662
|
+
next(error);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
export default router;
|