insforge 1.3.0 → 1.4.8
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/CHANGELOG.md +2 -0
- package/auth/package.json +5 -3
- package/auth/src/lib/broadcastService.ts +115 -117
- package/auth/src/lib/insforge.ts +8 -0
- package/auth/src/main.tsx +2 -4
- package/auth/src/pages/SignInPage.tsx +60 -60
- package/auth/src/pages/SignUpPage.tsx +60 -60
- package/auth/src/pages/VerifyEmailPage.tsx +18 -0
- package/auth/tsconfig.json +2 -1
- package/backend/package.json +10 -6
- package/backend/src/api/middlewares/rate-limiters.ts +127 -127
- package/backend/src/api/routes/ai/index.routes.ts +475 -468
- package/backend/src/api/routes/auth/index.routes.ts +85 -32
- package/backend/src/api/routes/auth/oauth.routes.ts +11 -6
- package/backend/src/api/routes/database/index.routes.ts +2 -0
- package/backend/src/api/routes/database/records.routes.ts +39 -175
- package/backend/src/api/routes/database/rpc.routes.ts +69 -0
- package/backend/src/api/routes/deployments/index.routes.ts +192 -0
- package/backend/src/api/routes/docs/index.routes.ts +3 -2
- package/backend/src/api/routes/email/index.routes.ts +35 -35
- package/backend/src/api/routes/functions/index.routes.ts +3 -3
- package/backend/src/api/routes/metadata/index.routes.ts +26 -0
- package/backend/src/api/routes/webhooks/index.routes.ts +109 -0
- package/backend/src/infra/database/database.manager.ts +0 -10
- package/backend/src/infra/database/migrations/018_schema-rework.sql +441 -0
- package/backend/src/infra/database/migrations/019_create-deployments-table.sql +36 -0
- package/backend/src/infra/database/migrations/020_add-audio-modality.sql +11 -0
- package/backend/src/infra/database/migrations/bootstrap/bootstrap-migrations.js +103 -0
- package/backend/src/infra/security/token.manager.ts +1 -4
- package/backend/src/providers/ai/openrouter.provider.ts +12 -3
- package/backend/src/providers/database/base.provider.ts +39 -0
- package/backend/src/providers/database/cloud.provider.ts +159 -0
- package/backend/src/providers/deployments/vercel.provider.ts +516 -0
- package/backend/src/server.ts +19 -7
- package/backend/src/services/ai/ai-config.service.ts +6 -6
- package/backend/src/services/ai/ai-model.service.ts +60 -60
- package/backend/src/services/ai/ai-usage.service.ts +7 -7
- package/backend/src/services/ai/chat-completion.service.ts +415 -220
- package/backend/src/services/ai/helpers.ts +64 -64
- package/backend/src/services/ai/index.ts +13 -13
- package/backend/src/services/auth/auth-config.service.ts +4 -4
- package/backend/src/services/auth/auth-otp.service.ts +6 -6
- package/backend/src/services/auth/auth.service.ts +134 -74
- package/backend/src/services/auth/index.ts +4 -4
- package/backend/src/services/auth/oauth-config.service.ts +12 -12
- package/backend/src/services/database/database-advance.service.ts +19 -55
- package/backend/src/services/database/database-table.service.ts +38 -85
- package/backend/src/services/database/postgrest-proxy.service.ts +165 -0
- package/backend/src/services/deployments/deployment.service.ts +693 -0
- package/backend/src/services/functions/function.service.ts +61 -41
- package/backend/src/services/logs/audit.service.ts +10 -10
- package/backend/src/services/secrets/secret.service.ts +101 -27
- package/backend/src/services/storage/storage.service.ts +30 -30
- package/backend/src/services/usage/usage.service.ts +6 -6
- package/backend/src/types/ai.ts +8 -0
- package/backend/src/types/auth.ts +5 -1
- package/backend/src/types/database.ts +2 -0
- package/backend/src/types/deployments.ts +33 -0
- package/backend/src/types/storage.ts +1 -1
- package/backend/src/types/webhooks.ts +45 -0
- package/backend/src/utils/cookies.ts +34 -35
- package/backend/src/utils/environment.ts +0 -14
- package/backend/src/utils/s3-config-loader.ts +64 -64
- package/backend/src/utils/seed.ts +334 -301
- package/backend/src/utils/sql-parser.ts +126 -0
- package/backend/src/utils/utils.ts +114 -114
- package/backend/src/utils/validations.ts +10 -10
- package/backend/tests/local/test-rpc.sh +141 -0
- package/backend/tests/local/test-secrets.sh +1 -1
- package/backend/tests/manual/test-ai-model-plugins.sh +258 -0
- package/backend/tests/manual/test-rawsql-modes.sh +24 -24
- package/backend/tests/unit/database-advance.test.ts +326 -0
- package/backend/tests/unit/helpers.test.ts +2 -2
- package/claude-plugin/skills/insforge-schema-patterns/SKILL.md +13 -10
- package/docker-compose.prod.yml +1 -1
- package/docker-compose.yml +1 -1
- package/docs/agent-docs/deployment.md +79 -0
- package/docs/changelog.mdx +165 -72
- package/docs/core-concepts/ai/architecture.mdx +1 -23
- package/docs/core-concepts/ai/sdk.mdx +26 -1
- package/docs/core-concepts/authentication/architecture.mdx +6 -8
- package/docs/core-concepts/authentication/sdk.mdx +387 -91
- package/docs/core-concepts/authentication/ui-components/customization.mdx +460 -256
- package/docs/core-concepts/authentication/ui-components/nextjs.mdx +50 -24
- package/docs/core-concepts/authentication/ui-components/react-router.mdx +18 -19
- package/docs/core-concepts/authentication/ui-components/react.mdx +26 -19
- package/docs/core-concepts/database/architecture.mdx +58 -21
- package/docs/core-concepts/database/pgvector.mdx +138 -0
- package/docs/core-concepts/database/sdk.mdx +17 -17
- package/docs/core-concepts/deployments/architecture.mdx +152 -0
- package/docs/core-concepts/email/architecture.mdx +4 -2
- package/docs/core-concepts/functions/architecture.mdx +1 -1
- package/docs/core-concepts/functions/sdk.mdx +0 -1
- package/docs/core-concepts/realtime/architecture.mdx +1 -1
- package/docs/core-concepts/storage/architecture.mdx +1 -1
- package/docs/core-concepts/storage/sdk.mdx +25 -25
- package/docs/docs.json +14 -6
- package/docs/favicon.png +0 -0
- package/docs/favicon.svg +3 -18
- package/docs/images/changelog/dec-2025/apple-oauth.mp4 +0 -0
- package/docs/images/changelog/dec-2025/moreModels.png +0 -0
- package/docs/images/changelog/dec-2025/multi-region.webp +0 -0
- package/docs/images/changelog/dec-2025/postgres-connection.webp +0 -0
- package/docs/images/changelog/dec-2025/realtime2.png +0 -0
- package/docs/images/mcp-setup/CC-MCP-1.mp4 +0 -0
- package/docs/images/mcp-setup/CC-MCP-2.mp4 +0 -0
- package/docs/images/mcp-setup/Cursor-MCP-1.mp4 +0 -0
- package/docs/images/mcp-setup/Cursor-MCP-2.mp4 +0 -0
- package/docs/images/mcp-setup/Cursor-MCP-3.mp4 +0 -0
- package/docs/images/mcp-setup/claude-code-connect.png +0 -0
- package/docs/images/mcp-setup/cline-1.png +0 -0
- package/docs/images/mcp-setup/cline-2.png +0 -0
- package/docs/images/mcp-setup/cline-3.png +0 -0
- package/docs/images/mcp-setup/connect-project.png +0 -0
- package/docs/images/mcp-setup/copilot-1.png +0 -0
- package/docs/images/mcp-setup/copilot-2.png +0 -0
- package/docs/images/mcp-setup/copilot-3.png +0 -0
- package/docs/images/mcp-setup/mcp-json-1.png +0 -0
- package/docs/images/mcp-setup/mcp-json-2.png +0 -0
- package/docs/images/mcp-setup/qoder-1.png +0 -0
- package/docs/images/mcp-setup/qoder-2.png +0 -0
- package/docs/images/mcp-setup/roocode-1.png +0 -0
- package/docs/images/mcp-setup/roocode-2.png +0 -0
- package/docs/images/mcp-setup/trae-1.png +0 -0
- package/docs/images/mcp-setup/trae-2.png +0 -0
- package/docs/images/mcp-setup/trae-3.png +0 -0
- package/docs/images/mcp-setup/trae-4.png +0 -0
- package/docs/images/mcp-setup/trae-5.png +0 -0
- package/docs/images/mcp-setup/windsurf-1.png +0 -0
- package/docs/images/mcp-setup/windsurf-2.png +0 -0
- package/docs/insforge-instructions-sdk.md +7 -3
- package/docs/introduction.mdx +9 -8
- package/docs/mcp-setup.mdx +332 -0
- package/docs/oauth-server.mdx +563 -0
- package/docs/partnership.mdx +79 -10
- package/docs/quickstart.mdx +1 -1
- package/docs/vscode-extension.mdx +74 -0
- package/eslint.config.js +1 -0
- package/examples/response-examples.md +1 -1
- package/frontend/package.json +1 -1
- package/frontend/src/App.tsx +8 -3
- package/frontend/src/assets/logos/antigravity.svg +1 -0
- package/frontend/src/assets/logos/copilot.svg +10 -0
- package/frontend/src/assets/logos/deepseek.svg +139 -0
- package/frontend/src/assets/logos/kiro.svg +9 -0
- package/frontend/src/assets/logos/qoder.svg +4 -0
- package/frontend/src/assets/logos/qwen.svg +15 -0
- package/frontend/src/components/CodeBlock.tsx +2 -2
- package/frontend/src/components/ConnectCTA.tsx +3 -2
- package/frontend/src/components/datagrid/DataGrid.tsx +90 -62
- package/frontend/src/components/datagrid/datagridTypes.tsx +2 -1
- package/frontend/src/components/datagrid/index.ts +1 -1
- package/frontend/src/components/index.ts +0 -1
- package/frontend/src/components/layout/AppHeader.tsx +4 -27
- package/frontend/src/components/layout/AppSidebar.tsx +85 -100
- package/frontend/src/components/layout/Layout.tsx +34 -32
- package/frontend/src/components/layout/PrimaryMenu.tsx +12 -4
- package/frontend/src/components/radix/Select.tsx +151 -151
- package/frontend/src/features/ai/components/AIConfigCard.tsx +200 -200
- package/frontend/src/features/ai/components/AIEmptyState.tsx +23 -23
- package/frontend/src/features/ai/components/ModalityFilterSidebar.tsx +102 -101
- package/frontend/src/features/ai/components/ModelSelectionDialog.tsx +135 -135
- package/frontend/src/features/ai/components/ModelSelectionGrid.tsx +51 -51
- package/frontend/src/features/ai/components/SystemPromptDialog.tsx +118 -118
- package/frontend/src/features/ai/components/index.ts +6 -6
- package/frontend/src/features/ai/helpers.ts +147 -141
- package/frontend/src/features/ai/pages/AIPage.tsx +166 -166
- package/frontend/src/features/auth/components/AuthPreview.tsx +96 -96
- package/frontend/src/features/auth/components/UsersDataGrid.tsx +55 -31
- package/frontend/src/features/auth/components/index.ts +5 -5
- package/frontend/src/features/auth/pages/AuthMethodsPage.tsx +275 -275
- package/frontend/src/features/dashboard/pages/DashboardPage.tsx +1 -1
- package/frontend/src/features/database/components/DatabaseDataGrid.tsx +0 -2
- package/frontend/src/features/database/components/ForeignKeyCell.tsx +38 -11
- package/frontend/src/features/database/components/ForeignKeyPopover.tsx +18 -8
- package/frontend/src/features/database/components/LinkRecordModal.tsx +61 -13
- package/frontend/src/features/database/components/RecordFormField.tsx +1 -1
- package/frontend/src/features/database/components/TableSidebar.tsx +0 -3
- package/frontend/src/features/database/components/TablesEmptyState.tsx +1 -1
- package/frontend/src/features/database/components/TemplatePreview.tsx +1 -2
- package/frontend/src/features/database/constants.ts +16 -28
- package/frontend/src/features/database/hooks/useCSVImport.ts +3 -2
- package/frontend/src/features/database/hooks/useRawSQL.ts +3 -2
- package/frontend/src/features/database/hooks/useTables.ts +5 -7
- package/frontend/src/features/database/pages/FunctionsPage.tsx +0 -5
- package/frontend/src/features/database/pages/IndexesPage.tsx +0 -5
- package/frontend/src/features/database/pages/PoliciesPage.tsx +0 -5
- package/frontend/src/features/database/pages/SQLEditorPage.tsx +2 -2
- package/frontend/src/features/database/pages/TriggersPage.tsx +0 -5
- package/frontend/src/features/database/services/advance.service.ts +1 -15
- package/frontend/src/features/database/services/record.service.ts +4 -20
- package/frontend/src/features/database/services/table.service.ts +1 -4
- package/frontend/src/features/database/templates/ai-chatbot.ts +6 -6
- package/frontend/src/features/database/templates/ecommerce-platform.ts +2 -2
- package/frontend/src/features/database/templates/instagram-clone.ts +10 -10
- package/frontend/src/features/database/templates/notion-clone.ts +8 -8
- package/frontend/src/features/database/templates/reddit-clone.ts +10 -10
- package/frontend/src/features/deployments/components/DeploymentRow.tsx +93 -0
- package/frontend/src/features/deployments/components/DeploymentsEmptyState.tsx +15 -0
- package/frontend/src/features/deployments/hooks/useDeployments.ts +157 -0
- package/frontend/src/features/deployments/pages/DeploymentsPage.tsx +318 -0
- package/frontend/src/features/deployments/services/deployments.service.ts +63 -0
- package/frontend/src/features/functions/components/FunctionRow.tsx +72 -72
- package/frontend/src/features/functions/components/FunctionsSidebar.tsx +56 -56
- package/frontend/src/features/functions/components/SecretRow.tsx +3 -3
- package/frontend/src/features/functions/components/index.ts +5 -5
- package/frontend/src/features/functions/hooks/useFunctions.ts +5 -4
- package/frontend/src/features/functions/hooks/useSecrets.ts +6 -9
- package/frontend/src/features/functions/pages/SecretsPage.tsx +118 -118
- package/frontend/src/features/functions/services/function.service.ts +8 -25
- package/frontend/src/features/functions/services/secret.service.ts +23 -41
- package/frontend/src/features/login/pages/CloudLoginPage.tsx +125 -118
- package/frontend/src/features/logs/components/LogDetailPanel.tsx +41 -0
- package/frontend/src/features/logs/components/LogsDataGrid.tsx +32 -1
- package/frontend/src/features/logs/components/index.ts +1 -0
- package/frontend/src/features/logs/pages/LogsPage.tsx +36 -6
- package/frontend/src/features/onboard/components/ApiCredentialsSection.tsx +59 -0
- package/frontend/src/features/onboard/components/ConnectionStringSection.tsx +180 -0
- package/frontend/src/features/onboard/components/McpConnectionSection.tsx +159 -0
- package/frontend/src/features/onboard/components/OnboardingController.tsx +68 -0
- package/frontend/src/features/onboard/components/OnboardingModal.tsx +121 -267
- package/frontend/src/features/onboard/components/ShowPasswordButton.tsx +21 -0
- package/frontend/src/features/onboard/components/index.ts +9 -4
- package/frontend/src/features/onboard/components/mcp/CursorDeeplinkGenerator.tsx +1 -1
- package/frontend/src/features/onboard/components/mcp/QoderDeeplinkGenerator.tsx +36 -0
- package/frontend/src/features/onboard/components/mcp/helpers.tsx +123 -98
- package/frontend/src/features/onboard/components/mcp/index.ts +4 -3
- package/frontend/src/features/onboard/index.ts +17 -13
- package/frontend/src/features/settings/pages/SettingsPage.tsx +349 -0
- package/frontend/src/features/visualizer/components/AuthNode.tsx +4 -4
- package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +21 -8
- package/frontend/src/features/visualizer/pages/VisualizerPage.tsx +10 -1
- package/frontend/src/index.css +249 -249
- package/frontend/src/lib/contexts/ModalContext.tsx +35 -0
- package/frontend/src/lib/hooks/useMetadata.ts +45 -1
- package/frontend/src/lib/hooks/useModal.tsx +2 -0
- package/frontend/src/lib/routing/AppRoutes.tsx +103 -99
- package/frontend/src/lib/services/metadata.service.ts +20 -3
- package/frontend/src/lib/utils/menuItems.ts +223 -207
- package/frontend/src/lib/utils/utils.ts +196 -196
- package/functions/server.ts +315 -315
- package/functions/worker-template.js +1 -1
- package/openapi/ai.yaml +115 -5
- package/openapi/auth.yaml +97 -17
- package/openapi/logs.yaml +0 -2
- package/openapi/metadata.yaml +0 -2
- package/openapi/records.yaml +21 -21
- package/openapi/tables.yaml +1 -2
- package/package.json +1 -1
- package/shared-schemas/package.json +1 -1
- package/shared-schemas/src/ai-api.schema.ts +251 -143
- package/shared-schemas/src/ai.schema.ts +63 -63
- package/shared-schemas/src/auth-api.schema.ts +34 -6
- package/shared-schemas/src/auth.schema.ts +17 -10
- package/shared-schemas/src/cloud-events.schema.ts +26 -0
- package/shared-schemas/src/deployments-api.schema.ts +55 -0
- package/shared-schemas/src/deployments.schema.ts +30 -0
- package/shared-schemas/src/docs.schema.ts +8 -2
- package/shared-schemas/src/email-api.schema.ts +30 -30
- package/shared-schemas/src/functions-api.schema.ts +13 -4
- package/shared-schemas/src/functions.schema.ts +1 -1
- package/shared-schemas/src/index.ts +22 -18
- package/shared-schemas/src/metadata.schema.ts +30 -4
- package/shared-schemas/src/secrets-api.schema.ts +44 -0
- package/shared-schemas/src/secrets.schema.ts +15 -0
- package/zeabur/README.md +13 -0
- package/zeabur/template.yml +20 -51
- package/backend/src/types/profile.ts +0 -55
- package/frontend/src/components/ProjectInfoModal.tsx +0 -128
|
@@ -10,11 +10,7 @@ import { successResponse } from '@/utils/response.js';
|
|
|
10
10
|
import { AuthRequest, verifyAdmin, verifyToken } from '@/api/middlewares/auth.js';
|
|
11
11
|
import oauthRouter from './oauth.routes.js';
|
|
12
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';
|
|
13
|
+
import { REFRESH_TOKEN_COOKIE_NAME, setAuthCookie, clearAuthCookie } from '@/utils/cookies.js';
|
|
18
14
|
import {
|
|
19
15
|
userIdSchema,
|
|
20
16
|
createUserRequestSchema,
|
|
@@ -27,6 +23,7 @@ import {
|
|
|
27
23
|
sendResetPasswordEmailRequestSchema,
|
|
28
24
|
exchangeResetPasswordTokenRequestSchema,
|
|
29
25
|
resetPasswordRequestSchema,
|
|
26
|
+
updateProfileRequestSchema,
|
|
30
27
|
type CreateUserResponse,
|
|
31
28
|
type CreateSessionResponse,
|
|
32
29
|
type VerifyEmailResponse,
|
|
@@ -34,6 +31,7 @@ import {
|
|
|
34
31
|
type ResetPasswordResponse,
|
|
35
32
|
type CreateAdminSessionResponse,
|
|
36
33
|
type GetCurrentSessionResponse,
|
|
34
|
+
type GetProfileResponse,
|
|
37
35
|
type ListUsersResponse,
|
|
38
36
|
type DeleteUsersResponse,
|
|
39
37
|
type GetPublicAuthConfigResponse,
|
|
@@ -74,6 +72,60 @@ router.get('/public-config', async (req: Request, res: Response, next: NextFunct
|
|
|
74
72
|
}
|
|
75
73
|
});
|
|
76
74
|
|
|
75
|
+
// PATCH /api/auth/profiles/current - Update current user's profile (authenticated)
|
|
76
|
+
router.patch(
|
|
77
|
+
'/profiles/current',
|
|
78
|
+
verifyToken,
|
|
79
|
+
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
80
|
+
try {
|
|
81
|
+
if (!req.user) {
|
|
82
|
+
throw new AppError('User not authenticated', 401, ERROR_CODES.AUTH_INVALID_CREDENTIALS);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const validationResult = updateProfileRequestSchema.safeParse(req.body);
|
|
86
|
+
if (!validationResult.success) {
|
|
87
|
+
throw new AppError(
|
|
88
|
+
validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
|
|
89
|
+
400,
|
|
90
|
+
ERROR_CODES.INVALID_INPUT
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { profile } = validationResult.data;
|
|
95
|
+
const result = await authService.updateProfile(req.user.id, profile);
|
|
96
|
+
|
|
97
|
+
const response: GetProfileResponse = result;
|
|
98
|
+
|
|
99
|
+
successResponse(res, response);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
next(error);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// GET /api/auth/profiles/:userId - Get user profile by ID (public endpoint)
|
|
107
|
+
router.get('/profiles/:userId', async (req: Request, res: Response, next: NextFunction) => {
|
|
108
|
+
try {
|
|
109
|
+
const userIdValidation = userIdSchema.safeParse(req.params.userId);
|
|
110
|
+
if (!userIdValidation.success) {
|
|
111
|
+
throw new AppError('Invalid user ID format', 400, ERROR_CODES.INVALID_INPUT);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const userId = userIdValidation.data;
|
|
115
|
+
const userProfile = await authService.getProfileById(userId);
|
|
116
|
+
|
|
117
|
+
if (!userProfile) {
|
|
118
|
+
throw new AppError('User not found', 404, ERROR_CODES.NOT_FOUND);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const response: GetProfileResponse = userProfile;
|
|
122
|
+
|
|
123
|
+
successResponse(res, response);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
next(error);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
77
129
|
// Email Authentication Configuration Routes
|
|
78
130
|
// GET /api/auth/config - Get authentication configurations (admin only)
|
|
79
131
|
router.get('/config', verifyAdmin, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
@@ -128,16 +180,15 @@ router.post('/users', async (req: Request, res: Response, next: NextFunction) =>
|
|
|
128
180
|
);
|
|
129
181
|
}
|
|
130
182
|
|
|
131
|
-
const { email, password, name } = validationResult.data;
|
|
132
|
-
const result: CreateUserResponse = await authService.register(email, password, name);
|
|
183
|
+
const { email, password, name, options } = validationResult.data;
|
|
184
|
+
const result: CreateUserResponse = await authService.register(email, password, name, options);
|
|
133
185
|
|
|
134
186
|
// Set refresh token in httpOnly cookie and generate CSRF token
|
|
135
|
-
let csrfToken: string | null = null;
|
|
136
187
|
if (result.accessToken && result.user) {
|
|
137
188
|
const tokenManager = TokenManager.getInstance();
|
|
138
189
|
const refreshToken = tokenManager.generateRefreshToken(result.user.id);
|
|
139
|
-
setAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME, refreshToken);
|
|
140
|
-
csrfToken = tokenManager.generateCsrfToken(refreshToken);
|
|
190
|
+
setAuthCookie(req, res, REFRESH_TOKEN_COOKIE_NAME, refreshToken);
|
|
191
|
+
result.csrfToken = tokenManager.generateCsrfToken(refreshToken);
|
|
141
192
|
}
|
|
142
193
|
|
|
143
194
|
const socket = SocketManager.getInstance();
|
|
@@ -148,7 +199,7 @@ router.post('/users', async (req: Request, res: Response, next: NextFunction) =>
|
|
|
148
199
|
'system'
|
|
149
200
|
);
|
|
150
201
|
|
|
151
|
-
successResponse(res,
|
|
202
|
+
successResponse(res, result);
|
|
152
203
|
} catch (error) {
|
|
153
204
|
next(error);
|
|
154
205
|
}
|
|
@@ -172,10 +223,10 @@ router.post('/sessions', async (req: Request, res: Response, next: NextFunction)
|
|
|
172
223
|
// Set refresh token in httpOnly cookie and generate CSRF token
|
|
173
224
|
const tokenManager = TokenManager.getInstance();
|
|
174
225
|
const refreshToken = tokenManager.generateRefreshToken(result.user.id);
|
|
175
|
-
setAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME, refreshToken);
|
|
176
|
-
|
|
226
|
+
setAuthCookie(req, res, REFRESH_TOKEN_COOKIE_NAME, refreshToken);
|
|
227
|
+
result.csrfToken = tokenManager.generateCsrfToken(refreshToken);
|
|
177
228
|
|
|
178
|
-
successResponse(res,
|
|
229
|
+
successResponse(res, result);
|
|
179
230
|
} catch (error) {
|
|
180
231
|
next(error);
|
|
181
232
|
}
|
|
@@ -206,7 +257,7 @@ router.post('/refresh', async (req: Request, res: Response, next: NextFunction)
|
|
|
206
257
|
|
|
207
258
|
if (!user) {
|
|
208
259
|
logger.warn('[Auth:Refresh] User not found for valid refresh token', { userId: payload.sub });
|
|
209
|
-
clearAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME);
|
|
260
|
+
clearAuthCookie(req, res, REFRESH_TOKEN_COOKIE_NAME);
|
|
210
261
|
throw new AppError('User not found', 401, ERROR_CODES.AUTH_UNAUTHORIZED);
|
|
211
262
|
}
|
|
212
263
|
|
|
@@ -220,7 +271,7 @@ router.post('/refresh', async (req: Request, res: Response, next: NextFunction)
|
|
|
220
271
|
// Generate new refresh token (token rotation for security)
|
|
221
272
|
const newRefreshToken = tokenManager.generateRefreshToken(user.id);
|
|
222
273
|
|
|
223
|
-
setAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME, newRefreshToken);
|
|
274
|
+
setAuthCookie(req, res, REFRESH_TOKEN_COOKIE_NAME, newRefreshToken);
|
|
224
275
|
const newCsrfToken = tokenManager.generateCsrfToken(newRefreshToken);
|
|
225
276
|
|
|
226
277
|
successResponse(res, {
|
|
@@ -230,15 +281,15 @@ router.post('/refresh', async (req: Request, res: Response, next: NextFunction)
|
|
|
230
281
|
});
|
|
231
282
|
} catch (error) {
|
|
232
283
|
// Clear invalid cookie on error
|
|
233
|
-
clearAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME);
|
|
284
|
+
clearAuthCookie(req, res, REFRESH_TOKEN_COOKIE_NAME);
|
|
234
285
|
next(error);
|
|
235
286
|
}
|
|
236
287
|
});
|
|
237
288
|
|
|
238
289
|
// POST /api/auth/logout - Logout and clear refresh token cookie
|
|
239
|
-
router.post('/logout', (
|
|
290
|
+
router.post('/logout', (req: Request, res: Response, next: NextFunction) => {
|
|
240
291
|
try {
|
|
241
|
-
clearAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME);
|
|
292
|
+
clearAuthCookie(req, res, REFRESH_TOKEN_COOKIE_NAME);
|
|
242
293
|
|
|
243
294
|
successResponse(res, {
|
|
244
295
|
success: true,
|
|
@@ -307,18 +358,19 @@ router.post('/admin/sessions', (req: Request, res: Response, next: NextFunction)
|
|
|
307
358
|
router.get(
|
|
308
359
|
'/sessions/current',
|
|
309
360
|
verifyToken,
|
|
310
|
-
(req: AuthRequest, res: Response, next: NextFunction) => {
|
|
361
|
+
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
311
362
|
try {
|
|
312
363
|
if (!req.user) {
|
|
313
364
|
throw new AppError('User not authenticated', 401, ERROR_CODES.AUTH_INVALID_CREDENTIALS);
|
|
314
365
|
}
|
|
315
366
|
|
|
367
|
+
const user = await authService.getUserSchemaById(req.user.id);
|
|
368
|
+
if (!user) {
|
|
369
|
+
throw new AppError('User not found', 401, ERROR_CODES.AUTH_INVALID_CREDENTIALS);
|
|
370
|
+
}
|
|
371
|
+
|
|
316
372
|
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
|
-
},
|
|
373
|
+
user,
|
|
322
374
|
};
|
|
323
375
|
|
|
324
376
|
successResponse(res, response);
|
|
@@ -359,7 +411,7 @@ router.get('/users', verifyAdmin, async (req: Request, res: Response, next: Next
|
|
|
359
411
|
}
|
|
360
412
|
});
|
|
361
413
|
|
|
362
|
-
// GET /api/auth/users/:
|
|
414
|
+
// GET /api/auth/users/:userId - Get specific user (admin only)
|
|
363
415
|
router.get(
|
|
364
416
|
'/users/:userId',
|
|
365
417
|
verifyAdmin,
|
|
@@ -375,7 +427,7 @@ router.get(
|
|
|
375
427
|
const user = await authService.getUserSchemaById(userId);
|
|
376
428
|
|
|
377
429
|
if (!user) {
|
|
378
|
-
throw new AppError('User not
|
|
430
|
+
throw new AppError('User does not exist', 404, ERROR_CODES.NOT_FOUND);
|
|
379
431
|
}
|
|
380
432
|
|
|
381
433
|
successResponse(res, user);
|
|
@@ -458,7 +510,7 @@ router.post(
|
|
|
458
510
|
);
|
|
459
511
|
}
|
|
460
512
|
|
|
461
|
-
const { email } = validationResult.data;
|
|
513
|
+
const { email, options } = validationResult.data;
|
|
462
514
|
|
|
463
515
|
// Get auth config to determine verification method
|
|
464
516
|
const authConfig = await authConfigService.getAuthConfig();
|
|
@@ -467,7 +519,8 @@ router.post(
|
|
|
467
519
|
// Note: User enumeration is prevented at service layer
|
|
468
520
|
// Service returns gracefully (no error) if user not found
|
|
469
521
|
if (method === 'link') {
|
|
470
|
-
|
|
522
|
+
const redirectTo = authConfig.signInRedirectTo || options?.emailRedirectTo;
|
|
523
|
+
await authService.sendVerificationEmailWithLink(email, redirectTo);
|
|
471
524
|
} else {
|
|
472
525
|
await authService.sendVerificationEmailWithCode(email);
|
|
473
526
|
}
|
|
@@ -536,9 +589,9 @@ router.post(
|
|
|
536
589
|
// Set refresh token in httpOnly cookie and generate CSRF token
|
|
537
590
|
const tokenManager = TokenManager.getInstance();
|
|
538
591
|
const refreshToken = tokenManager.generateRefreshToken(result.user.id);
|
|
539
|
-
setAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME, refreshToken);
|
|
540
|
-
|
|
541
|
-
successResponse(res,
|
|
592
|
+
setAuthCookie(req, res, REFRESH_TOKEN_COOKIE_NAME, refreshToken);
|
|
593
|
+
result.csrfToken = tokenManager.generateCsrfToken(refreshToken);
|
|
594
|
+
successResponse(res, result);
|
|
542
595
|
} catch (error) {
|
|
543
596
|
next(error);
|
|
544
597
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Router, Request, Response, NextFunction } from 'express';
|
|
2
2
|
import { AuthService } from '@/services/auth/auth.service.js';
|
|
3
3
|
import { OAuthConfigService } from '@/services/auth/oauth-config.service.js';
|
|
4
|
+
import { AuthConfigService } from '@/services/auth/auth-config.service.js';
|
|
4
5
|
import { AuditService } from '@/services/logs/audit.service.js';
|
|
5
6
|
import { TokenManager } from '@/infra/security/token.manager.js';
|
|
6
7
|
import { AppError } from '@/api/middlewares/error.js';
|
|
@@ -20,6 +21,7 @@ import { isOAuthSharedKeysAvailable } from '@/utils/environment.js';
|
|
|
20
21
|
|
|
21
22
|
const router = Router();
|
|
22
23
|
const authService = AuthService.getInstance();
|
|
24
|
+
const authConfigService = AuthConfigService.getInstance();
|
|
23
25
|
const oAuthConfigService = OAuthConfigService.getInstance();
|
|
24
26
|
const auditService = AuditService.getInstance();
|
|
25
27
|
|
|
@@ -246,14 +248,17 @@ router.get('/:provider', async (req: Request, res: Response, next: NextFunction)
|
|
|
246
248
|
}
|
|
247
249
|
|
|
248
250
|
const validatedProvider = providerValidation.data;
|
|
251
|
+
const authConfig = await authConfigService.getAuthConfig();
|
|
249
252
|
|
|
250
|
-
|
|
253
|
+
const redirectUri = authConfig.signInRedirectTo || redirect_uri;
|
|
254
|
+
|
|
255
|
+
if (!redirectUri) {
|
|
251
256
|
throw new AppError('Redirect URI is required', 400, ERROR_CODES.INVALID_INPUT);
|
|
252
257
|
}
|
|
253
258
|
|
|
254
259
|
const jwtPayload = {
|
|
255
260
|
provider: validatedProvider,
|
|
256
|
-
redirectUri
|
|
261
|
+
redirectUri,
|
|
257
262
|
createdAt: Date.now(),
|
|
258
263
|
};
|
|
259
264
|
const jwtSecret = validateJwtSecret();
|
|
@@ -346,7 +351,7 @@ router.get('/shared/callback/:state', async (req: Request, res: Response, next:
|
|
|
346
351
|
// Set refresh token in httpOnly cookie and generate CSRF token
|
|
347
352
|
const tokenManager = TokenManager.getInstance();
|
|
348
353
|
const refreshToken = tokenManager.generateRefreshToken(result.user.id);
|
|
349
|
-
setAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME, refreshToken);
|
|
354
|
+
setAuthCookie(req, res, REFRESH_TOKEN_COOKIE_NAME, refreshToken);
|
|
350
355
|
const csrfToken = tokenManager.generateCsrfToken(refreshToken);
|
|
351
356
|
|
|
352
357
|
const params = new URLSearchParams();
|
|
@@ -354,7 +359,7 @@ router.get('/shared/callback/:state', async (req: Request, res: Response, next:
|
|
|
354
359
|
params.set('access_token', result.accessToken);
|
|
355
360
|
params.set('user_id', result.user.id);
|
|
356
361
|
params.set('email', result.user.email);
|
|
357
|
-
params.set('name', result
|
|
362
|
+
params.set('name', String(result?.user?.profile?.name));
|
|
358
363
|
params.set('csrf_token', csrfToken);
|
|
359
364
|
|
|
360
365
|
res.redirect(`${redirectUri}?${params.toString()}`);
|
|
@@ -425,7 +430,7 @@ const handleOAuthCallback = async (req: Request, res: Response, next: NextFuncti
|
|
|
425
430
|
// Set refresh token in httpOnly cookie and generate CSRF token
|
|
426
431
|
const tokenManager = TokenManager.getInstance();
|
|
427
432
|
const refreshToken = tokenManager.generateRefreshToken(result.user.id);
|
|
428
|
-
setAuthCookie(res, REFRESH_TOKEN_COOKIE_NAME, refreshToken);
|
|
433
|
+
setAuthCookie(req, res, REFRESH_TOKEN_COOKIE_NAME, refreshToken);
|
|
429
434
|
const csrfToken = tokenManager.generateCsrfToken(refreshToken);
|
|
430
435
|
|
|
431
436
|
// Construct redirect URL with query parameters
|
|
@@ -434,7 +439,7 @@ const handleOAuthCallback = async (req: Request, res: Response, next: NextFuncti
|
|
|
434
439
|
params.set('access_token', result.accessToken);
|
|
435
440
|
params.set('user_id', result.user.id);
|
|
436
441
|
params.set('email', result.user.email);
|
|
437
|
-
params.set('name', result
|
|
442
|
+
params.set('name', String(result?.user?.profile?.name));
|
|
438
443
|
params.set('csrf_token', csrfToken);
|
|
439
444
|
|
|
440
445
|
const finalRedirectUri = `${redirectUri}?${params.toString()}`;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Router, Response, NextFunction } from 'express';
|
|
2
2
|
import { databaseTablesRouter } from './tables.routes.js';
|
|
3
3
|
import { databaseRecordsRouter } from './records.routes.js';
|
|
4
|
+
import { databaseRpcRouter } from './rpc.routes.js';
|
|
4
5
|
import databaseAdvanceRouter from './advance.routes.js';
|
|
5
6
|
import { DatabaseService } from '@/services/database/database.service.js';
|
|
6
7
|
import { verifyAdmin, AuthRequest } from '@/api/middlewares/auth.js';
|
|
@@ -13,6 +14,7 @@ const databaseService = DatabaseService.getInstance();
|
|
|
13
14
|
// Mount database sub-routes
|
|
14
15
|
router.use('/tables', databaseTablesRouter);
|
|
15
16
|
router.use('/records', databaseRecordsRouter);
|
|
17
|
+
router.use('/rpc', databaseRpcRouter);
|
|
16
18
|
router.use('/advance', databaseAdvanceRouter);
|
|
17
19
|
|
|
18
20
|
/**
|
|
@@ -1,80 +1,41 @@
|
|
|
1
1
|
import { Router, Response, NextFunction } from 'express';
|
|
2
2
|
import axios from 'axios';
|
|
3
|
-
import http from 'http';
|
|
4
|
-
import https from 'https';
|
|
5
3
|
import { AuthRequest, extractApiKey } from '@/api/middlewares/auth.js';
|
|
6
4
|
import { DatabaseManager } from '@/infra/database/database.manager.js';
|
|
7
|
-
import { TokenManager } from '@/infra/security/token.manager.js';
|
|
8
5
|
import { AppError } from '@/api/middlewares/error.js';
|
|
9
6
|
import { ERROR_CODES } from '@/types/error-constants.js';
|
|
10
7
|
import { validateTableName } from '@/utils/validations.js';
|
|
11
8
|
import { DatabaseRecord } from '@/types/database.js';
|
|
12
9
|
import { successResponse } from '@/utils/response.js';
|
|
13
|
-
import logger from '@/utils/logger.js';
|
|
14
|
-
import { SecretService } from '@/services/secrets/secret.service.js';
|
|
15
10
|
import { SocketManager } from '@/infra/socket/socket.manager.js';
|
|
16
11
|
import { DataUpdateResourceType, ServerEvents } from '@/types/socket.js';
|
|
17
12
|
import { DatabaseResourceUpdate } from '@/utils/sql-parser.js';
|
|
13
|
+
import { PostgrestProxyService } from '@/services/database/postgrest-proxy.service.js';
|
|
18
14
|
|
|
19
15
|
const router = Router();
|
|
20
|
-
const
|
|
21
|
-
const postgrestUrl = process.env.POSTGREST_BASE_URL || 'http://localhost:5430';
|
|
16
|
+
const proxyService = PostgrestProxyService.getInstance();
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const httpsAgent = new https.Agent({
|
|
34
|
-
keepAlive: true,
|
|
35
|
-
keepAliveMsecs: 5000,
|
|
36
|
-
maxSockets: 20,
|
|
37
|
-
maxFreeSockets: 5,
|
|
38
|
-
timeout: 10000,
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// Create axios instance with optimized configuration for PostgREST
|
|
42
|
-
const postgrestAxios = axios.create({
|
|
43
|
-
httpAgent,
|
|
44
|
-
httpsAgent,
|
|
45
|
-
timeout: 10000, // Increased timeout for stability
|
|
46
|
-
maxRedirects: 0,
|
|
47
|
-
// Additional connection stability options
|
|
48
|
-
headers: {
|
|
49
|
-
Connection: 'keep-alive',
|
|
50
|
-
'Keep-Alive': 'timeout=5, max=10',
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// Generate admin token once and reuse
|
|
55
|
-
// If user request with api key, this token should be added automatically.
|
|
56
|
-
const tokenManager = TokenManager.getInstance();
|
|
57
|
-
const adminToken = tokenManager.generateAdminToken();
|
|
58
|
-
|
|
59
|
-
// anonymous users can access the database, postgREST does not require authentication, however we seed to unwrap session token for better auth, thus
|
|
60
|
-
// we need to verify user token below.
|
|
61
|
-
// router.use(verifyUserOrApiKey);
|
|
18
|
+
/**
|
|
19
|
+
* Helper to handle PostgREST proxy errors
|
|
20
|
+
*/
|
|
21
|
+
function handleProxyError(error: unknown, res: Response, next: NextFunction) {
|
|
22
|
+
if (axios.isAxiosError(error) && error.response) {
|
|
23
|
+
res.status(error.response.status).json(error.response.data);
|
|
24
|
+
} else {
|
|
25
|
+
next(error);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
62
28
|
|
|
63
29
|
/**
|
|
64
|
-
* Forward database requests to PostgREST
|
|
30
|
+
* Forward database table requests to PostgREST
|
|
65
31
|
*/
|
|
66
32
|
const forwardToPostgrest = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
67
33
|
const { tableName } = req.params;
|
|
68
34
|
const wildcardPath = req.params[0] || '';
|
|
69
|
-
|
|
70
|
-
// Build the target URL early so it's available in error handling
|
|
71
|
-
const targetPath = wildcardPath ? `/${tableName}/${wildcardPath}` : `/${tableName}`;
|
|
72
|
-
const targetUrl = `${postgrestUrl}${targetPath}`;
|
|
35
|
+
const path = wildcardPath ? `/${tableName}/${wildcardPath}` : `/${tableName}`;
|
|
73
36
|
|
|
74
37
|
try {
|
|
75
|
-
// Validate table name
|
|
76
|
-
const method = req.method.toUpperCase();
|
|
77
|
-
|
|
38
|
+
// Validate table name
|
|
78
39
|
try {
|
|
79
40
|
validateTableName(tableName);
|
|
80
41
|
} catch (error) {
|
|
@@ -84,11 +45,14 @@ const forwardToPostgrest = async (req: AuthRequest, res: Response, next: NextFun
|
|
|
84
45
|
throw new AppError('Invalid table name', 400, ERROR_CODES.INVALID_INPUT);
|
|
85
46
|
}
|
|
86
47
|
|
|
87
|
-
// Process request body for POST/PATCH/PUT
|
|
88
|
-
|
|
48
|
+
// Process request body for POST/PATCH/PUT (filter empty values based on column types)
|
|
49
|
+
const method = req.method.toUpperCase();
|
|
50
|
+
let body = req.body;
|
|
51
|
+
|
|
52
|
+
if (['POST', 'PATCH', 'PUT'].includes(method) && body && typeof body === 'object') {
|
|
89
53
|
const columnTypeMap = await DatabaseManager.getColumnTypeMap(tableName);
|
|
90
|
-
if (Array.isArray(
|
|
91
|
-
|
|
54
|
+
if (Array.isArray(body)) {
|
|
55
|
+
body = body.map((item) => {
|
|
92
56
|
if (item && typeof item === 'object') {
|
|
93
57
|
const filtered: DatabaseRecord = {};
|
|
94
58
|
for (const key in item) {
|
|
@@ -102,7 +66,6 @@ const forwardToPostgrest = async (req: AuthRequest, res: Response, next: NextFun
|
|
|
102
66
|
return item;
|
|
103
67
|
});
|
|
104
68
|
} else {
|
|
105
|
-
const body = req.body as DatabaseRecord;
|
|
106
69
|
for (const key in body) {
|
|
107
70
|
if (columnTypeMap[key] === 'uuid' && body[key] === '') {
|
|
108
71
|
delete body[key];
|
|
@@ -111,100 +74,31 @@ const forwardToPostgrest = async (req: AuthRequest, res: Response, next: NextFun
|
|
|
111
74
|
}
|
|
112
75
|
}
|
|
113
76
|
|
|
114
|
-
// Forward
|
|
115
|
-
const
|
|
116
|
-
method: string;
|
|
117
|
-
url: string;
|
|
118
|
-
params: unknown;
|
|
119
|
-
headers: Record<string, string | string[] | undefined>;
|
|
120
|
-
data?: unknown;
|
|
121
|
-
} = {
|
|
77
|
+
// Forward to PostgREST via service
|
|
78
|
+
const result = await proxyService.forward({
|
|
122
79
|
method: req.method,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
headers:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
// Check for API key using shared logic
|
|
133
|
-
const apiKey = extractApiKey(req);
|
|
134
|
-
|
|
135
|
-
// If we have an API key, verify it and use admin token for PostgREST
|
|
136
|
-
if (apiKey) {
|
|
137
|
-
const isValid = await secretService.verifyApiKey(apiKey);
|
|
138
|
-
if (isValid) {
|
|
139
|
-
axiosConfig.headers.authorization = `Bearer ${adminToken}`;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Add body for methods that support it
|
|
144
|
-
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
|
|
145
|
-
axiosConfig.data = req.body;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Enhanced retry logic with improved error handling
|
|
149
|
-
let response;
|
|
150
|
-
let lastError;
|
|
151
|
-
const maxRetries = 3; // Increased retries for connection resets
|
|
152
|
-
|
|
153
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
154
|
-
try {
|
|
155
|
-
response = await postgrestAxios(axiosConfig);
|
|
156
|
-
break; // Success, exit retry loop
|
|
157
|
-
} catch (error) {
|
|
158
|
-
lastError = error;
|
|
159
|
-
|
|
160
|
-
// Retry on network errors (ECONNRESET, ECONNREFUSED, timeout) but not HTTP errors
|
|
161
|
-
const shouldRetry = axios.isAxiosError(error) && !error.response && attempt < maxRetries;
|
|
162
|
-
|
|
163
|
-
if (shouldRetry) {
|
|
164
|
-
logger.warn(`PostgREST request failed, retrying (attempt ${attempt}/${maxRetries})`, {
|
|
165
|
-
url: targetUrl,
|
|
166
|
-
errorCode: error.code,
|
|
167
|
-
message: error.message,
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// Enhanced exponential backoff: 200ms, 500ms, 1000ms
|
|
171
|
-
const backoffDelay = Math.min(200 * Math.pow(2.5, attempt - 1), 1000);
|
|
172
|
-
await new Promise((resolve) => setTimeout(resolve, backoffDelay));
|
|
173
|
-
} else {
|
|
174
|
-
throw error; // Don't retry on HTTP errors or last attempt
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (!response) {
|
|
180
|
-
throw lastError || new Error('Failed to get response from PostgREST');
|
|
181
|
-
}
|
|
80
|
+
path,
|
|
81
|
+
query: req.query as Record<string, unknown>,
|
|
82
|
+
headers: req.headers as Record<string, string | string[] | undefined>,
|
|
83
|
+
body: ['POST', 'PUT', 'PATCH'].includes(req.method) ? body : undefined,
|
|
84
|
+
apiKey: extractApiKey(req) ?? undefined,
|
|
85
|
+
});
|
|
182
86
|
|
|
183
87
|
// Forward response headers
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (
|
|
187
|
-
keyLower !== 'content-length' &&
|
|
188
|
-
keyLower !== 'transfer-encoding' &&
|
|
189
|
-
keyLower !== 'connection' &&
|
|
190
|
-
keyLower !== 'content-encoding'
|
|
191
|
-
) {
|
|
192
|
-
res.setHeader(key, value);
|
|
193
|
-
}
|
|
194
|
-
});
|
|
88
|
+
const headers = PostgrestProxyService.filterHeaders(result.headers);
|
|
89
|
+
Object.entries(headers).forEach(([key, value]) => res.setHeader(key, value));
|
|
195
90
|
|
|
196
91
|
// Handle empty responses
|
|
197
|
-
let responseData =
|
|
92
|
+
let responseData = result.data;
|
|
198
93
|
if (
|
|
199
|
-
|
|
200
|
-
(typeof
|
|
94
|
+
result.data === undefined ||
|
|
95
|
+
(typeof result.data === 'string' && result.data.trim() === '')
|
|
201
96
|
) {
|
|
202
97
|
responseData = [];
|
|
203
98
|
}
|
|
204
99
|
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
if (mutationMethods.includes(req.method.toUpperCase())) {
|
|
100
|
+
// Broadcast socket events for mutations
|
|
101
|
+
if (['POST', 'DELETE'].includes(method)) {
|
|
208
102
|
const socket = SocketManager.getInstance();
|
|
209
103
|
socket.broadcastToRoom(
|
|
210
104
|
'role:project_admin',
|
|
@@ -217,39 +111,9 @@ const forwardToPostgrest = async (req: AuthRequest, res: Response, next: NextFun
|
|
|
217
111
|
);
|
|
218
112
|
}
|
|
219
113
|
|
|
220
|
-
successResponse(res, responseData,
|
|
114
|
+
successResponse(res, responseData, result.status);
|
|
221
115
|
} catch (error) {
|
|
222
|
-
|
|
223
|
-
// Log more detailed error information
|
|
224
|
-
logger.error('PostgREST request failed', {
|
|
225
|
-
url: targetUrl,
|
|
226
|
-
method: req.method,
|
|
227
|
-
error: {
|
|
228
|
-
code: error.code,
|
|
229
|
-
message: error.message,
|
|
230
|
-
response: error.response?.data,
|
|
231
|
-
responseStatus: error.response?.status,
|
|
232
|
-
},
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// Forward PostgREST errors
|
|
236
|
-
if (error.response) {
|
|
237
|
-
res.status(error.response.status).json(error.response.data);
|
|
238
|
-
} else {
|
|
239
|
-
// Network error - connection refused, DNS failure, etc.
|
|
240
|
-
const errorMessage =
|
|
241
|
-
error.code === 'ECONNREFUSED'
|
|
242
|
-
? 'PostgREST connection refused'
|
|
243
|
-
: error.code === 'ENOTFOUND'
|
|
244
|
-
? 'PostgREST service not found'
|
|
245
|
-
: 'Database service unavailable';
|
|
246
|
-
|
|
247
|
-
next(new AppError(errorMessage, 503, ERROR_CODES.INTERNAL_ERROR));
|
|
248
|
-
}
|
|
249
|
-
} else {
|
|
250
|
-
logger.error('Unexpected error in database route', { error });
|
|
251
|
-
next(error);
|
|
252
|
-
}
|
|
116
|
+
handleProxyError(error, res, next);
|
|
253
117
|
}
|
|
254
118
|
};
|
|
255
119
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Router, Response, NextFunction } from 'express';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { AuthRequest, extractApiKey } from '@/api/middlewares/auth.js';
|
|
4
|
+
import { AppError } from '@/api/middlewares/error.js';
|
|
5
|
+
import { ERROR_CODES } from '@/types/error-constants.js';
|
|
6
|
+
import { validateFunctionName } from '@/utils/validations.js';
|
|
7
|
+
import { successResponse } from '@/utils/response.js';
|
|
8
|
+
import { PostgrestProxyService } from '@/services/database/postgrest-proxy.service.js';
|
|
9
|
+
|
|
10
|
+
const router = Router();
|
|
11
|
+
const proxyService = PostgrestProxyService.getInstance();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Helper to handle PostgREST proxy errors
|
|
15
|
+
*/
|
|
16
|
+
function handleProxyError(error: unknown, res: Response, next: NextFunction) {
|
|
17
|
+
if (axios.isAxiosError(error) && error.response) {
|
|
18
|
+
res.status(error.response.status).json(error.response.data);
|
|
19
|
+
} else {
|
|
20
|
+
next(error);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Forward RPC calls to PostgREST
|
|
26
|
+
*/
|
|
27
|
+
const forwardRpcToPostgrest = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
28
|
+
const { functionName } = req.params;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Validate function name
|
|
32
|
+
try {
|
|
33
|
+
validateFunctionName(functionName);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error instanceof AppError) {
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
throw new AppError(`Invalid function name: ${functionName}`, 400, ERROR_CODES.INVALID_INPUT);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const result = await proxyService.forward({
|
|
42
|
+
method: req.method,
|
|
43
|
+
path: `/rpc/${functionName}`,
|
|
44
|
+
query: req.query as Record<string, unknown>,
|
|
45
|
+
headers: req.headers as Record<string, string | string[] | undefined>,
|
|
46
|
+
body: req.body,
|
|
47
|
+
apiKey: extractApiKey(req) ?? undefined,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const headers = PostgrestProxyService.filterHeaders(result.headers);
|
|
51
|
+
Object.entries(headers).forEach(([key, value]) => res.setHeader(key, value));
|
|
52
|
+
|
|
53
|
+
let responseData = result.data;
|
|
54
|
+
if (
|
|
55
|
+
result.data === undefined ||
|
|
56
|
+
(typeof result.data === 'string' && result.data.trim() === '')
|
|
57
|
+
) {
|
|
58
|
+
responseData = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
successResponse(res, responseData, result.status);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
handleProxyError(error, res, next);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
router.all('/:functionName', forwardRpcToPostgrest);
|
|
68
|
+
|
|
69
|
+
export { router as databaseRpcRouter };
|