core/apps/webapp/app/routes/oauth.authorize.tsx
Harshith Mullapudi cdb6788ef0 Feat: add MCP logs
2025-08-24 12:22:12 +05:30

357 lines
12 KiB
TypeScript

import {
type ActionFunctionArgs,
type LoaderFunctionArgs,
redirect,
} from "@remix-run/node";
import { Form, useLoaderData } from "@remix-run/react";
import { getUser, requireWorkpace } from "~/services/session.server";
import {
oauth2Service,
OAuth2Errors,
type OAuth2AuthorizeRequest,
} from "~/services/oauth2.server";
import { Button } from "~/components/ui/button";
import { Card, CardContent } from "~/components/ui/card";
import Logo from "~/components/logo/logo";
import {
AlignLeft,
Pen,
User,
Mail,
Shield,
Database,
LoaderCircle,
ArrowRightLeft,
} from "lucide-react";
import { useState } from "react";
import { getIconForAuthorise } from "~/components/icon-utils";
export const loader = async ({ request }: LoaderFunctionArgs) => {
// Check if user is authenticated
const user = await getUser(request);
if (!user) {
// Redirect to login with return URL
const url = new URL(request.url);
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("redirectTo", url.pathname + url.search);
return redirect(loginUrl.toString());
}
const url = new URL(request.url);
let scopeParam = url.searchParams.get("scope") || "mcp";
// If scope is present, normalize it to comma-separated format
// Handle both space-separated (from URL encoding) and comma-separated scopes
if (scopeParam) {
// First, try splitting by spaces (common in OAuth2 URLs)
let scopes = scopeParam.split(/\s+/).filter((s) => s.length > 0);
// If no spaces found, try splitting by commas
if (scopes.length === 1) {
scopes = scopeParam
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
}
scopeParam = scopes.join(",");
} else {
throw new Error("Scope is not found");
}
const params: OAuth2AuthorizeRequest = {
client_id: url.searchParams.get("client_id") || "",
redirect_uri: url.searchParams.get("redirect_uri") || "",
response_type: url.searchParams.get("response_type") || "",
scope: scopeParam,
state: url.searchParams.get("state") || undefined,
code_challenge: url.searchParams.get("code_challenge") || undefined,
code_challenge_method:
url.searchParams.get("code_challenge_method") || undefined,
};
// Validate required parameters
if (!params.client_id || !params.redirect_uri || !params.response_type) {
return redirect(
`${params.redirect_uri}?error=${OAuth2Errors.INVALID_REQUEST}&error_description=Missing required parameters${params.state ? `&state=${params.state}` : ""}`,
);
}
// Only support authorization code flow
if (params.response_type !== "code") {
return redirect(
`${params.redirect_uri}?error=${OAuth2Errors.UNSUPPORTED_RESPONSE_TYPE}&error_description=Only authorization code flow is supported${params.state ? `&state=${params.state}` : ""}`,
);
}
try {
// Validate client
const client = await oauth2Service.validateClient(params.client_id);
// Validate redirect URI
if (!oauth2Service.validateRedirectUri(client, params.redirect_uri)) {
return redirect(
`${params.redirect_uri}?error=${OAuth2Errors.INVALID_REQUEST}&error_description=Invalid redirect URI${params.state ? `&state=${params.state}` : ""}`,
);
}
// Validate scopes
if (!oauth2Service.validateScopes(client, params.scope || "")) {
return redirect(
`${params.redirect_uri}?error=${OAuth2Errors.INVALID_SCOPE}&error_description=Invalid scope${params.state ? `&state=${params.state}` : ""}`,
);
}
return {
user,
client,
params,
};
} catch (error) {
return redirect(
`${params.redirect_uri}?error=${OAuth2Errors.INVALID_CLIENT}&error_description=Invalid client${params.state ? `&state=${params.state}` : ""}`,
);
}
};
export const action = async ({ request }: ActionFunctionArgs) => {
const user = await getUser(request);
const workspace = await requireWorkpace(request);
if (!user) {
return redirect("/login");
}
const formData = await request.formData();
const action = formData.get("action");
const params: OAuth2AuthorizeRequest = {
client_id: formData.get("client_id") as string,
redirect_uri: formData.get("redirect_uri") as string,
response_type: formData.get("response_type") as string,
scope: (formData.get("scope") as string) || undefined,
state: (formData.get("state") as string) || undefined,
code_challenge: (formData.get("code_challenge") as string) || undefined,
code_challenge_method:
(formData.get("code_challenge_method") as string) || undefined,
};
if (action === "deny") {
return redirect(
`${params.redirect_uri}?error=${OAuth2Errors.ACCESS_DENIED}&error_description=User denied access${params.state ? `&state=${params.state}` : ""}`,
);
}
if (action === "allow") {
try {
// Validate client again
const client = await oauth2Service.validateClient(params.client_id);
if (!oauth2Service.validateRedirectUri(client, params.redirect_uri)) {
return redirect(
`${params.redirect_uri}?error=${OAuth2Errors.INVALID_REQUEST}&error_description=Invalid redirect URI${params.state ? `&state=${params.state}` : ""}`,
);
}
// Create authorization code
const authCode = await oauth2Service.createAuthorizationCode({
clientId: params.client_id,
userId: user.id,
redirectUri: params.redirect_uri,
scope: params.scope,
state: params.state,
codeChallenge: params.code_challenge,
codeChallengeMethod: params.code_challenge_method,
workspaceId: workspace.id,
});
// Redirect back to client with authorization code
const redirectUrl = new URL(params.redirect_uri);
redirectUrl.searchParams.set("code", authCode);
if (params.state) {
redirectUrl.searchParams.set("state", params.state);
}
return redirect(redirectUrl.toString());
} catch (error) {
return redirect(
`${params.redirect_uri}?error=${OAuth2Errors.SERVER_ERROR}&error_description=Failed to create authorization code${params.state ? `&state=${params.state}` : ""}`,
);
}
}
return redirect(
`${params.redirect_uri}?error=${OAuth2Errors.INVALID_REQUEST}&error_description=Invalid action${params.state ? `&state=${params.state}` : ""}`,
);
};
export default function OAuthAuthorize() {
const { user, client, params } = useLoaderData<typeof loader>();
const [isRedirecting, setIsRedirecting] = useState(false);
const getScopeIcon = (scope: string) => {
switch (scope) {
case "profile":
return <User size={16} />;
case "email":
return <Mail size={16} />;
case "openid":
return <Shield size={16} />;
case "integration":
return <Database size={16} />;
case "read":
return <Pen size={16} />;
case "write":
return <Pen size={16} />;
default:
return <AlignLeft size={16} />;
}
};
const getScopeDescription = (scope: string) => {
switch (scope) {
case "profile":
return "View your basic profile information";
case "email":
return "View your email address";
case "openid":
return "Verify your identity using OpenID Connect";
case "integration":
return "Access and manage your workspace integrations";
case "read":
return "Read access to your account";
case "write":
return "Write access to your account";
case "mcp":
return "Access to memory and integrations";
default:
return `Access to ${scope}`;
}
};
return (
<div className="bg-background-2 flex min-h-screen items-center justify-center">
<Card className="bg-background-3 shadow-1 w-full max-w-md rounded-lg p-5">
<CardContent>
<div className="flex items-center justify-center gap-4">
{getIconForAuthorise(client.name, 40, client.logoUrl)}
<ArrowRightLeft size={16} />
<Logo width={40} height={40} />
</div>
<div className="mt-4 space-y-4">
<div className="flex items-center justify-center space-x-3 text-center">
<div>
<p className="text-lg font-normal">
{client.name} is requesting access
</p>
<p className="text-muted-foreground text-sm">
Authenticating with your {user.name} account
</p>
</div>
</div>
<p className="text-muted-foreground mb-2 text-sm">Permissions</p>
<ul className="text-muted-foreground text-sm">
{params.scope?.split(",").map((scope, index, arr) => {
const trimmedScope = scope.trim();
const isFirst = index === 0;
const isLast = index === arr.length - 1;
return (
<li
key={index}
className={`flex items-center gap-2 border-x border-t border-gray-300 p-2 ${isLast ? "border-b" : ""} ${isFirst ? "rounded-tl-md rounded-tr-md" : ""} ${isLast ? "rounded-br-md rounded-bl-md" : ""} `}
>
<div>{getScopeIcon(trimmedScope)}</div>
<div>{getScopeDescription(trimmedScope)}</div>
</li>
);
})}
</ul>
{isRedirecting ? (
<div className="flex flex-col items-center justify-center py-8">
<LoaderCircle className="text-primary mb-2 h-4 w-4 animate-spin" />
<span className="text-muted-foreground text-sm">
Redirecting to the page... (Close this page if it doesn't
redirect in 5 seconds)
</span>
</div>
) : (
<Form
method="post"
className="space-y-3"
onSubmit={(e) => {
// Only show loading if allow is clicked
const form = e.target as HTMLFormElement;
const allowBtn = form.querySelector(
'button[name="action"][value="allow"]',
);
if ((e.nativeEvent as SubmitEvent).submitter === allowBtn) {
setIsRedirecting(true);
}
}}
>
<input
type="hidden"
name="client_id"
value={params.client_id}
/>
<input
type="hidden"
name="redirect_uri"
value={params.redirect_uri}
/>
<input
type="hidden"
name="response_type"
value={params.response_type}
/>
{params.scope && (
<input type="hidden" name="scope" value={params.scope} />
)}
{params.state && (
<input type="hidden" name="state" value={params.state} />
)}
{params.code_challenge && (
<input
type="hidden"
name="code_challenge"
value={params.code_challenge}
/>
)}
{params.code_challenge_method && (
<input
type="hidden"
name="code_challenge_method"
value={params.code_challenge_method}
/>
)}
<div className="flex justify-end space-x-3">
<Button
type="submit"
name="action"
value="deny"
size="lg"
variant="secondary"
>
Deny
</Button>
<Button
type="submit"
name="action"
value="allow"
size="lg"
className="shadow-none"
>
Allow Access
</Button>
</div>
</Form>
)}
</div>
</CardContent>
</Card>
</div>
);
}