mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-20 04:58:29 +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",
|
"openid",
|
||||||
// Integration scope
|
// Integration scope
|
||||||
"integration",
|
"integration",
|
||||||
|
"integration:read",
|
||||||
|
"integration:credentials",
|
||||||
|
"integration:manage",
|
||||||
|
"integration:webhook",
|
||||||
|
// MCP scope
|
||||||
|
"mcp",
|
||||||
|
"mcp:read",
|
||||||
|
"mcp:write",
|
||||||
];
|
];
|
||||||
|
|
||||||
const requestedScopes = Array.isArray(allowedScopes)
|
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
|
// Google-style auth scopes
|
||||||
const authScopes = ["profile", "email", "openid"];
|
const authScopes = ["profile", "email", "openid"];
|
||||||
// Single integration scope
|
// Integration-related scopes
|
||||||
const integrationScopes = ["integration"];
|
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 hasAuthScopes = scopes.some((s) => authScopes.includes(s));
|
||||||
const hasIntegrationScopes = scopes.some((s) =>
|
const hasIntegrationScopes = scopes.some((s) =>
|
||||||
@ -324,6 +333,34 @@ export class OAuth2Service {
|
|||||||
description: "Access your workspace integrations",
|
description: "Access your workspace integrations",
|
||||||
icon: "database",
|
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) => ({
|
return scopes.map((scope) => ({
|
||||||
@ -560,7 +597,6 @@ export class OAuth2Service {
|
|||||||
expiresAt: { gt: new Date() },
|
expiresAt: { gt: new Date() },
|
||||||
userId: tokenPayload.user_id,
|
userId: tokenPayload.user_id,
|
||||||
workspaceId: tokenPayload.workspace_id,
|
workspaceId: tokenPayload.workspace_id,
|
||||||
...(scopes ? { scope: { contains: scopes.join(",") } } : {}),
|
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
client: true,
|
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) {
|
if (!accessToken) {
|
||||||
throw new Error("Invalid or expired token");
|
throw new Error("Invalid or expired token");
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user