import { type ActionFunctionArgs, type LoaderFunctionArgs, redirect, } from "@remix-run/node"; import { Form, useLoaderData } from "@remix-run/react"; import { getUser } 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 { Arrows } from "~/components/icons"; import Logo from "~/components/logo/logo"; import { AlignLeft, LayoutGrid, Pen } from "lucide-react"; 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") || undefined; // If scope is present, remove spaces after commas (e.g., "read, write" -> "read,write") if (scopeParam) { scopeParam = scopeParam .split(",") .map((s) => s.trim()) .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}` : ""}`, ); } 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); 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, }); // 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(); const getIcon = (scope: string) => { if (scope === "read") { return ; } return ; }; return (
{client.logoUrl ? ( {client.name} ) : ( )}

{client.name} is requesting access

Authenticating with your {user.name} workspace

Permissions

    {params.scope?.split(" ").map((scope, index, arr) => { const isFirst = index === 0; const isLast = index === arr.length - 1; return (
  • {getIcon(scope)}
    {scope.charAt(0).toUpperCase() + scope.slice(1)} access to your workspace
  • ); })}
{params.scope && ( )} {params.state && ( )} {params.code_challenge && ( )} {params.code_challenge_method && ( )}
); }