import { type ActionFunctionArgs, type LoaderFunctionArgs, json, } from "@remix-run/node"; import { requireAuth } from "~/utils/auth-helper"; import crypto from "crypto"; import { prisma } from "~/db.server"; // GET /api/oauth/clients - List OAuth clients for user's workspace export const loader = async ({ request }: LoaderFunctionArgs) => { const user = await requireAuth(request); try { // Get user's workspace const userRecord = await prisma.user.findUnique({ where: { id: user.id }, include: { Workspace: true }, }); if (!userRecord?.Workspace) { return json({ error: "No workspace found" }, { status: 404 }); } const clients = await prisma.oAuthClient.findMany({ where: { workspaceId: userRecord.Workspace.id }, select: { id: true, clientId: true, name: true, description: true, redirectUris: true, allowedScopes: true, requirePkce: true, logoUrl: true, homepageUrl: true, isActive: true, createdAt: true, updatedAt: true, createdBy: { select: { id: true, email: true, name: true, }, }, }, orderBy: { createdAt: "desc" }, }); return json({ clients }); } catch (error) { console.error("Error fetching OAuth clients:", error); return json({ error: "Internal server error" }, { status: 500 }); } }; // POST /api/oauth/clients - Create new OAuth client export const action = async ({ request }: ActionFunctionArgs) => { if (request.method !== "POST") { return json({ error: "Method not allowed" }, { status: 405 }); } const user = await requireAuth(request); try { const body = await request.json(); const { name, description, redirectUris, allowedScopes, requirePkce, logoUrl, homepageUrl, } = body; // Validate required fields if (!name || !redirectUris) { return json( { error: "Name and redirectUris are required" }, { status: 400 }, ); } // Validate scopes const validScopes = [ // Authentication scopes (Google-style) "profile", "email", "openid", // Integration scope "integration", "integration:read", "integration:credentials", "integration:manage", "integration:webhook", // MCP scope "mcp", "mcp:read", "mcp:write", "mcp.read", "mcp.write", ]; const requestedScopes = Array.isArray(allowedScopes) ? allowedScopes : [allowedScopes || "read"]; const invalidScopes = requestedScopes.filter( (scope) => !validScopes.includes(scope), ); if (invalidScopes.length > 0) { return json( { error: `Invalid scopes: ${invalidScopes.join(", ")}. Valid scopes are: ${validScopes.join(", ")}`, }, { status: 400 }, ); } // Get user's workspace const userRecord = await prisma.user.findUnique({ where: { id: user.id }, include: { Workspace: true }, }); if (!userRecord?.Workspace) { return json({ error: "No workspace found" }, { status: 404 }); } if (!userRecord?.admin) { return json({ error: "No access to create OAuth app" }, { status: 404 }); } // Generate client credentials const clientId = crypto.randomUUID(); const clientSecret = crypto.randomBytes(32).toString("hex"); // Create OAuth client const client = await prisma.oAuthClient.create({ data: { clientId, clientSecret, name, description: description || null, redirectUris: Array.isArray(redirectUris) ? redirectUris.join(",") : redirectUris, allowedScopes: requestedScopes.join(","), requirePkce: requirePkce || false, logoUrl: logoUrl || null, homepageUrl: homepageUrl || null, workspaceId: userRecord.Workspace.id, createdById: user.id, }, select: { id: true, clientId: true, clientSecret: true, name: true, description: true, redirectUris: true, allowedScopes: true, requirePkce: true, logoUrl: true, homepageUrl: true, isActive: true, createdAt: true, }, }); return json({ success: true, client, message: "OAuth client created successfully", }); } catch (error) { console.error("Error creating OAuth client:", error); return json({ error: "Internal server error" }, { status: 500 }); } };