mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-11 17:08:27 +00:00
Feat: add integration credentials API
This commit is contained in:
parent
2b42078245
commit
23bf49b4cf
@ -94,6 +94,14 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
"openid",
|
||||
// Integration scope
|
||||
"integration",
|
||||
"integration:read",
|
||||
"integration:credentials",
|
||||
"integration:manage",
|
||||
"integration:webhook",
|
||||
// MCP scope
|
||||
"mcp",
|
||||
"mcp:read",
|
||||
"mcp:write",
|
||||
];
|
||||
|
||||
const requestedScopes = Array.isArray(allowedScopes)
|
||||
|
||||
@ -0,0 +1,114 @@
|
||||
import { type LoaderFunctionArgs, json } from "@remix-run/node";
|
||||
import { z } from "zod";
|
||||
import { authenticateOAuthRequest } from "~/services/apiAuth.server";
|
||||
import { prisma } from "~/db.server";
|
||||
|
||||
// Schema for the integration account ID parameter
|
||||
const ParamsSchema = z.object({
|
||||
id: z.string().min(1, "Integration account ID is required"),
|
||||
});
|
||||
|
||||
/**
|
||||
* API endpoint for OAuth apps to get integration account credentials
|
||||
* GET /api/v1/integration_account/:id/credentials
|
||||
* Authorization: Bearer <oauth_access_token>
|
||||
* Required scope: integration:credentials
|
||||
*/
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
try {
|
||||
// Authenticate OAuth request and verify integration:credentials scope
|
||||
const authResult = await authenticateOAuthRequest(request, ["integration:credentials", "integration"]);
|
||||
|
||||
if (!authResult.success) {
|
||||
return json(
|
||||
{
|
||||
error: "unauthorized",
|
||||
error_description: authResult.error
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate parameters
|
||||
const parseResult = ParamsSchema.safeParse(params);
|
||||
if (!parseResult.success) {
|
||||
return json(
|
||||
{
|
||||
error: "invalid_request",
|
||||
error_description: "Invalid integration account ID"
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { id } = parseResult.data;
|
||||
|
||||
// Get the integration account with proper access control
|
||||
const integrationAccount = await prisma.integrationAccount.findFirst({
|
||||
where: {
|
||||
id,
|
||||
integratedById: authResult.user!.id, // Ensure user owns this integration account
|
||||
isActive: true,
|
||||
deleted: null,
|
||||
},
|
||||
include: {
|
||||
integrationDefinition: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
icon: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!integrationAccount) {
|
||||
return json(
|
||||
{
|
||||
error: "not_found",
|
||||
error_description: "Integration account not found or access denied"
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Extract credentials from integrationConfiguration
|
||||
const credentials = integrationAccount.integrationConfiguration as Record<string, any>;
|
||||
|
||||
// Return the credentials and metadata
|
||||
return json({
|
||||
id: integrationAccount.id,
|
||||
accountId: integrationAccount.accountId,
|
||||
provider: integrationAccount.integrationDefinition.slug,
|
||||
name: integrationAccount.integrationDefinition.name,
|
||||
icon: integrationAccount.integrationDefinition.icon,
|
||||
credentials,
|
||||
settings: integrationAccount.settings,
|
||||
connectedAt: integrationAccount.createdAt,
|
||||
isActive: integrationAccount.isActive,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching integration account credentials:", error);
|
||||
return json(
|
||||
{
|
||||
error: "server_error",
|
||||
error_description: "Internal server error"
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Method not allowed for non-GET requests
|
||||
export const action = async () => {
|
||||
return json(
|
||||
{
|
||||
error: "method_not_allowed",
|
||||
error_description: "Only GET requests are allowed"
|
||||
},
|
||||
{ status: 405 }
|
||||
);
|
||||
};
|
||||
@ -289,8 +289,17 @@ export class OAuth2Service {
|
||||
|
||||
// Google-style auth scopes
|
||||
const authScopes = ["profile", "email", "openid"];
|
||||
// Single integration scope
|
||||
const integrationScopes = ["integration"];
|
||||
// Integration-related scopes
|
||||
const integrationScopes = [
|
||||
"integration",
|
||||
"integration:read",
|
||||
"integration:credentials",
|
||||
"integration:manage",
|
||||
"integration:webhook",
|
||||
];
|
||||
|
||||
// MCP-related scopes
|
||||
const mcpScopes = ["mcp", "mcp:read", "mcp:write"];
|
||||
|
||||
const hasAuthScopes = scopes.some((s) => authScopes.includes(s));
|
||||
const hasIntegrationScopes = scopes.some((s) =>
|
||||
@ -324,6 +333,34 @@ export class OAuth2Service {
|
||||
description: "Access your workspace integrations",
|
||||
icon: "database",
|
||||
},
|
||||
"integration:read": {
|
||||
description: "Read integration metadata and status",
|
||||
icon: "eye",
|
||||
},
|
||||
"integration:credentials": {
|
||||
description: "Access integration account credentials",
|
||||
icon: "key",
|
||||
},
|
||||
"integration:manage": {
|
||||
description: "Create, update, and delete integrations",
|
||||
icon: "settings",
|
||||
},
|
||||
"integration:webhook": {
|
||||
description: "Manage integration webhooks",
|
||||
icon: "webhook",
|
||||
},
|
||||
mcp: {
|
||||
description: "Access MCP endpoints",
|
||||
icon: "mcp",
|
||||
},
|
||||
"mcp:read": {
|
||||
description: "Read MCP endpoints",
|
||||
icon: "eye",
|
||||
},
|
||||
"mcp:write": {
|
||||
description: "Write to MCP endpoints",
|
||||
icon: "pencil",
|
||||
},
|
||||
};
|
||||
|
||||
return scopes.map((scope) => ({
|
||||
@ -560,7 +597,6 @@ export class OAuth2Service {
|
||||
expiresAt: { gt: new Date() },
|
||||
userId: tokenPayload.user_id,
|
||||
workspaceId: tokenPayload.workspace_id,
|
||||
...(scopes ? { scope: { contains: scopes.join(",") } } : {}),
|
||||
},
|
||||
include: {
|
||||
client: true,
|
||||
@ -568,6 +604,20 @@ export class OAuth2Service {
|
||||
},
|
||||
});
|
||||
|
||||
// Validate scopes separately if requested
|
||||
if (scopes && accessToken) {
|
||||
const tokenScopes =
|
||||
accessToken.scope?.split(",").map((s) => s.trim()) || [];
|
||||
|
||||
const hasAllScopes = scopes.some((requiredScope) =>
|
||||
tokenScopes.some((tokenScope) => tokenScope === requiredScope),
|
||||
);
|
||||
|
||||
if (!hasAllScopes) {
|
||||
throw new Error("Insufficient scope");
|
||||
}
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
throw new Error("Invalid or expired token");
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user