initial commit

This commit is contained in:
RhenCloud 2026-07-24 21:48:50 +08:00
commit 512d4b01d5
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
55 changed files with 6430 additions and 0 deletions

82
src/oauth-routes.ts Normal file
View file

@ -0,0 +1,82 @@
import { Hono } from "hono";
import { getOAuthURL, handleOAuthCallback } from "./github-oauth";
import { removeToken } from "./token-store";
import type { Env } from "./types";
interface PendingState {
redirectTo: string;
expiresAt: number;
}
function generateRandomHex(length: number): string {
const bytes = new Uint8Array(length);
crypto.getRandomValues(bytes);
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
export function createOAuthRoutes(): Hono<{ Bindings: Env }> {
const app = new Hono<{ Bindings: Env }>();
app.get("/github", async (c) => {
const redirectTo = c.req.query("redirect") ?? "/";
const state = generateRandomHex(16);
const pending: PendingState = {
redirectTo,
expiresAt: Date.now() + 10 * 60 * 1000,
};
await c.env.KV.put(`state:${state}`, JSON.stringify(pending), {
expirationTtl: 600,
});
const url = getOAuthURL(c.env.GITHUB_CLIENT_ID ?? "", state);
return c.redirect(url);
});
app.get("/github/callback", async (c) => {
const code = c.req.query("code");
const state = c.req.query("state");
if (!code || !state) {
return c.json({ error: "Missing code or state" }, 400);
}
const raw = await c.env.KV.get(`state:${state}`, "json");
if (!raw) {
return c.json({ error: "Invalid or expired state" }, 400);
}
const pending = raw as PendingState;
if (Date.now() > pending.expiresAt) {
await c.env.KV.delete(`state:${state}`);
return c.json({ error: "Invalid or expired state" }, 400);
}
await c.env.KV.delete(`state:${state}`);
const result = await handleOAuthCallback(
c.env.GITHUB_CLIENT_ID ?? "",
c.env.GITHUB_CLIENT_SECRET ?? "",
code,
state,
c.env.KV,
);
if (!result) {
return c.json({ error: "OAuth failed" }, 400);
}
return c.json({
userId: result.userId,
login: result.login,
redirectTo: pending.redirectTo,
});
});
app.delete("/token/:userId", async (c) => {
await removeToken(c.env.KV, c.req.param("userId"));
return c.json({ ok: true });
});
return app;
}