feat(frontend): email+password auth, /signin + /signup pages, marketing consolidation, onboarding workspace naming + full data persistence

This commit is contained in:
2026-06-06 20:28:38 -07:00
parent b33a85c8dc
commit 2714f8cdf3
49 changed files with 2753 additions and 6503 deletions

View File

@@ -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";