g1t

syntaqx/g1t

public

Git for AI scale: a forge for thousands of agents working on the same code at once.

g1t/apps/web/app/routes/oauth-authorize.tsx

166 lines5,849 bytes
import { CircleX, KeyRound } from "lucide-react";
import { Form, redirect } from "react-router";

import { decodeOAuthClient, isRegisteredRedirect } from "@g1t/contracts";

import type { Route } from "./+types/oauth-authorize";
import { Button } from "../components/ui";
import { identity } from "../lib/services.server";
import { assertSameOrigin, requireUser } from "../lib/session.server";

export function meta({}: Route.MetaArgs) {
  return [{ title: "Sign in to an application · g1t" }];
}

type Checked =
  | { ok: false; problem: string }
  | {
      ok: true;
      clientId: string;
      clientName: string;
      redirectUri: string;
      codeChallenge: string;
      state: string;
    };

/**
 * Checks an authorization request. A request that names a client or a
 * redirect address we cannot vouch for is never redirected anywhere; the
 * person is told instead.
 */
function check(params: URLSearchParams | FormData): Checked {
  const get = (key: string) => String(params.get(key) ?? "");
  const client = decodeOAuthClient(get("client_id"));
  if (!client) {
    return { ok: false, problem: "This sign-in link names an application g1t does not recognise." };
  }
  const redirectUri = get("redirect_uri") || client.redirectUris[0];
  if (!isRegisteredRedirect(client, redirectUri)) {
    return {
      ok: false,
      problem: "This sign-in link would send you somewhere the application did not register.",
    };
  }
  if (get("response_type") !== "code") {
    return { ok: false, problem: "This sign-in link asks for a kind of access g1t does not offer." };
  }
  if (!get("code_challenge") || get("code_challenge_method") !== "S256") {
    return {
      ok: false,
      problem: "This application did not protect its sign-in with PKCE (S256), which g1t requires.",
    };
  }
  return {
    ok: true,
    clientId: get("client_id"),
    clientName: client.name,
    redirectUri,
    codeChallenge: get("code_challenge"),
    state: get("state"),
  };
}

/** The application's redirect address with the outcome added to it. */
function callback(redirectUri: string, params: Record<string, string>): string {
  const url = new URL(redirectUri);
  for (const [key, value] of Object.entries(params)) {
    if (value) url.searchParams.set(key, value);
  }
  return url.toString();
}

/** Where an application sends a person to approve its sign-in. */
export function loader({ request, context }: Route.LoaderArgs) {
  const user = requireUser(context, request);
  const { searchParams } = new URL(request.url);
  return {
    user,
    request: check(searchParams),
    // Sent back unchanged when the person decides.
    query: Object.fromEntries(searchParams),
  };
}

export async function action({ request, context }: Route.ActionArgs) {
  assertSameOrigin(request);
  const user = requireUser(context, request);
  const form = await request.formData();
  const checked = check(form);
  if (!checked.ok) return null;
  if (form.get("decision") !== "approve") {
    throw redirect(
      callback(checked.redirectUri, { error: "access_denied", state: checked.state }),
    );
  }
  const { code } = await identity.oauthAuthorize(user, {
    clientId: checked.clientId,
    clientName: checked.clientName,
    redirectUri: checked.redirectUri,
    codeChallenge: checked.codeChallenge,
  });
  throw redirect(
    callback(checked.redirectUri, { code, state: checked.state, iss: "https://api.g1t.sh" }),
  );
}

export default function Authorize({ loaderData }: Route.ComponentProps) {
  const { user, request, query } = loaderData;

  if (!request.ok) {
    return (
      <main className="mx-auto max-w-md px-4 py-32 text-center">
        <CircleX size={40} className="mx-auto text-muted" />
        <h1 className="mt-6 text-2xl font-semibold tracking-tight">This link cannot be used</h1>
        <p className="mt-2 text-muted">{request.problem}</p>
        <p className="mt-2 text-sm text-faint">Nothing was given access to your account.</p>
      </main>
    );
  }

  const destination = new URL(request.redirectUri);
  return (
    <main className="mx-auto max-w-md px-4 py-24">
      <KeyRound size={36} className="text-accent" />
      <h1 className="mt-6 text-2xl font-semibold tracking-tight">
        Sign in to {request.clientName}
      </h1>
      <p className="mt-2 text-muted">
        <span className="font-medium text-fg">{request.clientName}</span> wants to act as{" "}
        <span className="font-mono font-medium text-fg">{user.username}</span> on g1t.
      </p>

      <dl className="mt-6 space-y-3 rounded-xl border border-line bg-surface p-4 text-sm">
        <div>
          <dt className="text-xs text-faint">It will be able to</dt>
          <dd className="mt-0.5">
            Read and change what you can: repositories, issues and pull requests.
          </dd>
        </div>
        <div>
          <dt className="text-xs text-faint">You will be sent back to</dt>
          <dd className="mt-0.5 font-mono text-[0.8125rem] break-all">
            {destination.protocol === "https:" || destination.protocol === "http:"
              ? destination.host + destination.pathname
              : request.redirectUri}
          </dd>
        </div>
      </dl>
      <p className="mt-3 text-xs text-faint">
        Approve only if you started this from {request.clientName} yourself. You can
        sign it out again in Settings.
      </p>

      <Form method="post" className="mt-6 flex gap-2">
        {Object.entries(query).map(([name, value]) => (
          <input key={name} type="hidden" name={name} value={value} />
        ))}
        <Button variant="accent" type="submit" name="decision" value="approve">
          Approve
        </Button>
        <Button variant="quiet" type="submit" name="decision" value="deny">
          Deny
        </Button>
      </Form>
    </main>
  );
}