GitHub Actions
Deploy from github.com/TTS-AGI/TTS-Arena@f49eded
374bcb6
Raw
History Blame Contribute Delete
2.79 kB
/**
* GET /api/auth/callback — HF OAuth redirect target. Verifies state, exchanges
* the code, enforces the account-age gate, upserts the user, starts a session,
* and returns the user to the app. Failures redirect home with an `?auth=`
* reason the client can surface.
*/
import { NextResponse, type NextRequest } from "next/server";
import { cookies } from "next/headers";
import { serverEnv } from "@/server/env";
import {
MIN_ACCOUNT_AGE_DAYS,
ageInDays,
exchangeCode,
fetchOverview,
whoami,
} from "@/server/auth/hf";
import { upsertUser } from "@/server/auth/user";
import { createSession } from "@/server/auth/session";
import { recordLogin } from "@/server/auth/logins";
import { clientIp, userAgent } from "@/server/request-info";
import { errInfo, logErrorEvent } from "@/server/observability/errors";
function home(reason?: string): NextResponse {
const url = new URL(serverEnv.appUrl);
if (reason) url.searchParams.set("auth", reason);
return NextResponse.redirect(url);
}
export async function GET(req: NextRequest) {
const params = req.nextUrl.searchParams;
const code = params.get("code");
const state = params.get("state");
const store = await cookies();
const expectedState = store.get("ttsa_oauth_state")?.value;
store.delete("ttsa_oauth_state");
if (!code || !state || !expectedState || state !== expectedState) {
return home("invalid_state");
}
try {
const accessToken = await exchangeCode(code);
const profile = await whoami(accessToken);
// One overview call: the age gate + the freshest (username-keyed) avatar.
const overview = await fetchOverview(profile.username);
if (!overview.createdAt) return home("age_unverifiable");
if (ageInDays(overview.createdAt) < MIN_ACCOUNT_AGE_DAYS) {
return home("too_new");
}
const user = await upsertUser(
{ ...profile, avatarUrl: overview.avatarUrl ?? profile.avatarUrl },
overview.createdAt,
);
await createSession(user.id);
// Record the login (IP + UA now; the fingerprint arrives client-side via
// /api/auth/fingerprint once the page loads).
await recordLogin({
userId: user.id,
ip: clientIp(req),
userAgent: userAgent(req),
fingerprint: null,
});
return home();
} catch (err) {
const info = errInfo(err);
// Always log to the container output too — if the DB write below is itself
// the failure, the error_events insert won't help, so we must not rely on
// it alone.
console.error("[auth/callback] failed:", info.stack ?? info.message);
void logErrorEvent({
source: "auth_callback",
message: info.message,
stack: info.stack,
route: "/api/auth/callback",
method: "GET",
});
return home("error");
}
}