Add GitHub OAuth2 flow
Redirect to GitHub authorization, exchange code for token, fetch user profile + email, create KV session. Refs #11
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import { Hono } from "hono";
|
||||
import type { Env } from "../types";
|
||||
import { createSession, setSessionCookie, type Session } from "./session";
|
||||
|
||||
const github = new Hono<{ Bindings: Env }>();
|
||||
|
||||
// ─── GitHub OAuth endpoints ─────────────────────────────────────
|
||||
|
||||
interface GitHubTokenResponse {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
interface GitHubUser {
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
email: string | null;
|
||||
name: string | null;
|
||||
}
|
||||
|
||||
interface GitHubEmail {
|
||||
email: string;
|
||||
primary: boolean;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
// Step 1: Redirect to GitHub authorization
|
||||
github.get("/auth/github", (c) => {
|
||||
const clientId = c.env.GITHUB_CLIENT_ID;
|
||||
if (!clientId) {
|
||||
return c.json({ error: "GitHub OAuth not configured" }, 500);
|
||||
}
|
||||
|
||||
const state = crypto.randomUUID();
|
||||
const params = new URLSearchParams({
|
||||
client_id: clientId,
|
||||
scope: "user:email",
|
||||
state,
|
||||
redirect_uri: `${new URL(c.req.url).origin}/auth/github/callback`,
|
||||
});
|
||||
|
||||
return c.redirect(
|
||||
`https://github.com/login/oauth/authorize?${params.toString()}`,
|
||||
);
|
||||
});
|
||||
|
||||
// Step 2: Handle callback, exchange code for token, create session
|
||||
github.get("/auth/github/callback", async (c) => {
|
||||
const code = c.req.query("code");
|
||||
if (!code) {
|
||||
return c.redirect("/?error=no_code");
|
||||
}
|
||||
|
||||
const clientId = c.env.GITHUB_CLIENT_ID;
|
||||
const clientSecret = c.env.GITHUB_CLIENT_SECRET;
|
||||
if (!clientId || !clientSecret) {
|
||||
return c.json({ error: "GitHub OAuth not configured" }, 500);
|
||||
}
|
||||
|
||||
// Exchange code for access token
|
||||
const tokenRes = await fetch(
|
||||
"https://github.com/login/oauth/access_token",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
code,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!tokenRes.ok) {
|
||||
return c.redirect("/?error=token_exchange_failed");
|
||||
}
|
||||
|
||||
const tokenData = (await tokenRes.json()) as GitHubTokenResponse;
|
||||
if (!tokenData.access_token) {
|
||||
return c.redirect("/?error=no_access_token");
|
||||
}
|
||||
|
||||
// Fetch user profile
|
||||
const userRes = await fetch("https://api.github.com/user", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokenData.access_token}`,
|
||||
Accept: "application/json",
|
||||
"User-Agent": "madcat-provisioner",
|
||||
},
|
||||
});
|
||||
|
||||
if (!userRes.ok) {
|
||||
return c.redirect("/?error=user_fetch_failed");
|
||||
}
|
||||
|
||||
const user = (await userRes.json()) as GitHubUser;
|
||||
|
||||
// If no public email, fetch from emails API
|
||||
let email = user.email;
|
||||
if (!email) {
|
||||
const emailsRes = await fetch("https://api.github.com/user/emails", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokenData.access_token}`,
|
||||
Accept: "application/json",
|
||||
"User-Agent": "madcat-provisioner",
|
||||
},
|
||||
});
|
||||
|
||||
if (emailsRes.ok) {
|
||||
const emails = (await emailsRes.json()) as GitHubEmail[];
|
||||
const primary = emails.find((e) => e.primary && e.verified);
|
||||
email = primary?.email ?? emails[0]?.email ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
return c.redirect("/?error=no_email");
|
||||
}
|
||||
|
||||
// Create session
|
||||
const session: Session = {
|
||||
username: user.login.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 20),
|
||||
email,
|
||||
avatar_url: user.avatar_url,
|
||||
provider: "github",
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const sessionId = await createSession(c.env.INSTANCES, session);
|
||||
const cookie = await setSessionCookie(sessionId, c.env.SESSION_SECRET);
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: "/dashboard",
|
||||
"Set-Cookie": cookie,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export { github };
|
||||
Reference in New Issue
Block a user