feat(frontend): email+password auth, /signin + /signup pages, marketing consolidation, onboarding workspace naming + full data persistence
This commit is contained in:
@@ -15,13 +15,19 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import {
|
||||
exchangeCodeForToken, getAuthenticatedUser, persistGithubIntegration,
|
||||
exchangeCodeForToken,
|
||||
getAuthenticatedUser,
|
||||
persistGithubIntegration,
|
||||
isGithubOauthConfigured,
|
||||
} from "@/lib/integrations/github";
|
||||
|
||||
const STATE_COOKIE = "gh_oauth_state";
|
||||
|
||||
function bounce(origin: string, returnTo: string, params: Record<string, string>): NextResponse {
|
||||
function bounce(
|
||||
origin: string,
|
||||
returnTo: string,
|
||||
params: Record<string, string>,
|
||||
): NextResponse {
|
||||
const dest = new URL(returnTo.startsWith("/") ? returnTo : "/", origin);
|
||||
for (const [k, v] of Object.entries(params)) dest.searchParams.set(k, v);
|
||||
const res = NextResponse.redirect(dest);
|
||||
@@ -35,40 +41,56 @@ export async function GET(req: Request) {
|
||||
const origin = (process.env.NEXTAUTH_URL ?? url.origin).replace(/\/$/, "");
|
||||
|
||||
// Recover the original target from the state cookie *before* any error path.
|
||||
const cookieState = req.headers.get("cookie")
|
||||
?.split(";").map(c => c.trim())
|
||||
.find(c => c.startsWith(`${STATE_COOKIE}=`))
|
||||
?.split("=")[1] ?? "";
|
||||
const [storedState, storedReturnTo = "/"] = decodeURIComponent(cookieState).split(":");
|
||||
const cookieState =
|
||||
req.headers
|
||||
.get("cookie")
|
||||
?.split(";")
|
||||
.map((c) => c.trim())
|
||||
.find((c) => c.startsWith(`${STATE_COOKIE}=`))
|
||||
?.split("=")[1] ?? "";
|
||||
const [storedState, storedReturnTo = "/"] =
|
||||
decodeURIComponent(cookieState).split(":");
|
||||
|
||||
if (!isGithubOauthConfigured()) {
|
||||
return bounce(origin, storedReturnTo, { gh_error: "GitHub OAuth not configured" });
|
||||
return bounce(origin, storedReturnTo, {
|
||||
gh_error: "GitHub OAuth not configured",
|
||||
});
|
||||
}
|
||||
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return bounce(origin, "/auth", { gh_error: "Sign in first" });
|
||||
return bounce(origin, "/signin", { gh_error: "Sign in first" });
|
||||
}
|
||||
|
||||
const code = url.searchParams.get("code");
|
||||
const state = url.searchParams.get("state");
|
||||
const errParam = url.searchParams.get("error_description") ?? url.searchParams.get("error");
|
||||
const errParam =
|
||||
url.searchParams.get("error_description") ?? url.searchParams.get("error");
|
||||
|
||||
if (errParam) {
|
||||
return bounce(origin, storedReturnTo, { gh_error: errParam });
|
||||
}
|
||||
if (!code || !state) {
|
||||
return bounce(origin, storedReturnTo, { gh_error: "Missing code or state" });
|
||||
return bounce(origin, storedReturnTo, {
|
||||
gh_error: "Missing code or state",
|
||||
});
|
||||
}
|
||||
if (!storedState || storedState !== state) {
|
||||
return bounce(origin, storedReturnTo, { gh_error: "State mismatch (try again)" });
|
||||
return bounce(origin, storedReturnTo, {
|
||||
gh_error: "State mismatch (try again)",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const callbackUrl = `${origin}/api/integrations/github/callback`;
|
||||
const tok = await exchangeCodeForToken(code, callbackUrl);
|
||||
const me = await getAuthenticatedUser(tok.accessToken);
|
||||
await persistGithubIntegration(session.user.email, me.login, tok.accessToken, tok.scope);
|
||||
await persistGithubIntegration(
|
||||
session.user.email,
|
||||
me.login,
|
||||
tok.accessToken,
|
||||
tok.scope,
|
||||
);
|
||||
return bounce(origin, storedReturnTo, { gh_connected: me.login });
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : "GitHub connect failed";
|
||||
|
||||
Reference in New Issue
Block a user