114 lines · 4.6 KB
import { env } from "cloudflare:workers";
import { z } from "zod";
import { first, run } from "./db";
import { randomToken } from "./crypto";
import { pendingLoginCookie, requiresSecondFactor } from "./two-factor";
import { createSessionCookie } from "./auth";
import { accountNameTaken, createUser } from "./accounts";

const STATE_TTL_SECONDS = 600;
const GITHUB_USER_AGENT = "stig-forge";
const MAX_NAME_SUFFIX = 1000;

const tokenResponse = z.object({ access_token: z.string() });
const githubUser = z.object({ id: z.number(), login: z.string(), name: z.string().nullable() });
const githubEmails = z.array(z.object({ email: z.string(), primary: z.boolean(), verified: z.boolean() }));

function callbackUrl(request: Request): string {
  return new URL("/auth/github/callback", request.url).toString();
}

export async function startGithubLogin(request: Request): Promise<Response> {
  if (!env.GITHUB_CLIENT_ID) return new Response("GitHub login is not configured", { status: 404 });
  const state = randomToken("oauth");
  await env.KV.put(`oauth:${state}`, "1", { expirationTtl: STATE_TTL_SECONDS });
  const url = new URL("https://github.com/login/oauth/authorize");
  url.searchParams.set("client_id", env.GITHUB_CLIENT_ID);
  url.searchParams.set("redirect_uri", callbackUrl(request));
  url.searchParams.set("scope", "read:user user:email");
  url.searchParams.set("state", state);
  return Response.redirect(url.toString(), 302);
}

async function uniqueName(login: string): Promise<string> {
  const base = login.replace(/[^A-Za-z0-9-]/g, "-").slice(0, 32) || "user";
  if (!(await accountNameTaken(base))) return base;
  for (let suffix = 2; suffix < MAX_NAME_SUFFIX; suffix++) {
    const candidate = `${base}-${suffix}`;
    if (!(await accountNameTaken(candidate))) return candidate;
  }
  throw new Error("Could not allocate a username");