mirror of
https://github.com/tiennm99/llmapikey.git
synced 2026-08-17 02:23:31 +00:00
feat(auth): replace Supabase Auth with app-native GitHub OAuth
Self-contained GitHub OAuth (Arctic) with a stateless HS256 signed-cookie session (jose); Supabase is downgraded to the Postgres host only. - Origin-derived callback (no redirect-uri env); read:user scope; access token read once at callback and discarded (no token storage). - CSRF via single-use state cookie; open-redirect guard on next. - getCurrentGithubIdentity() now reads the session cookie, preserving the numeric provider_id identity contract for admin/dashboard/mint. - Remove @supabase/ssr + @supabase/supabase-js, middleware, and the supabase-dependent rls test; delete lib/supabase clients.
This commit is contained in:
+44
-21
@@ -1,35 +1,58 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
import { createServerAuthClient } from "@/lib/supabase/server-client";
|
||||
import { getGithubOAuth } from "@/lib/auth/github-oauth";
|
||||
import { createSession } from "@/lib/auth/session";
|
||||
import { sanitizeNext } from "@/lib/auth/sanitize-next";
|
||||
|
||||
/**
|
||||
* OAuth callback: exchange the GitHub auth code for a Supabase session cookie,
|
||||
* then redirect to `next` (default /dashboard).
|
||||
* GitHub OAuth callback. Verify CSRF state, exchange the code, read the public
|
||||
* profile once (`id` + `login`), mint the session cookie, then redirect to the
|
||||
* stashed `next`. The access token is never stored.
|
||||
*
|
||||
* @param {Request} request
|
||||
*/
|
||||
export async function GET(request) {
|
||||
const { searchParams, origin } = new URL(request.url);
|
||||
const code = searchParams.get("code");
|
||||
const next = sanitizeNext(searchParams.get("next"));
|
||||
const state = searchParams.get("state");
|
||||
|
||||
if (code) {
|
||||
const supabase = await createServerAuthClient();
|
||||
const { error } = await supabase.auth.exchangeCodeForSession(code);
|
||||
if (!error) {
|
||||
return NextResponse.redirect(`${origin}${next}`);
|
||||
}
|
||||
const cookieStore = await cookies();
|
||||
const storedState = cookieStore.get("oauth_state")?.value;
|
||||
const next = sanitizeNext(cookieStore.get("oauth_next")?.value);
|
||||
|
||||
// Clear the transient cookies regardless of outcome.
|
||||
cookieStore.delete("oauth_state");
|
||||
cookieStore.delete("oauth_next");
|
||||
|
||||
if (!code || !state || !storedState || state !== storedState) {
|
||||
return NextResponse.redirect(`${origin}/?auth_error=1`);
|
||||
}
|
||||
return NextResponse.redirect(`${origin}/?auth_error=1`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only allow same-origin relative paths to avoid open-redirect.
|
||||
*
|
||||
* @param {string | null} next
|
||||
* @returns {string}
|
||||
*/
|
||||
function sanitizeNext(next) {
|
||||
if (next && next.startsWith("/") && !next.startsWith("//")) return next;
|
||||
return "/dashboard";
|
||||
try {
|
||||
const oauth = getGithubOAuth(origin);
|
||||
const tokens = await oauth.validateAuthorizationCode(code);
|
||||
const accessToken = tokens.accessToken();
|
||||
|
||||
const res = await fetch("https://api.github.com/user", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"User-Agent": "llmapikey",
|
||||
Accept: "application/vnd.github+json",
|
||||
},
|
||||
});
|
||||
if (!res.ok) return NextResponse.redirect(`${origin}/?auth_error=1`);
|
||||
|
||||
const profile = await res.json();
|
||||
const githubUserId = String(profile.id);
|
||||
// Anchor on the numeric provider_id, never the mutable login.
|
||||
if (!/^\d+$/.test(githubUserId)) {
|
||||
return NextResponse.redirect(`${origin}/?auth_error=1`);
|
||||
}
|
||||
|
||||
await createSession({ githubUserId, githubUsername: String(profile.login) });
|
||||
return NextResponse.redirect(`${origin}${next}`);
|
||||
} catch {
|
||||
return NextResponse.redirect(`${origin}/?auth_error=1`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user