Feat: add integration credentials API

This commit is contained in:
Manoj K 2025-07-28 10:40:31 +05:30 committed by Harshith Mullapudi
parent 2b42078245
commit 23bf49b4cf
3 changed files with 175 additions and 3 deletions

View File

@ -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)

View File

@ -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 }
);
};

View File

@ -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");
}