diff --git a/CLAUDE.md b/CLAUDE.md index 2a48c65..bd30031 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,7 +32,7 @@ app/ │ ├── home, calls, usage, keys, settings # auth-gated │ ├── device, waitlist # device-approval + early-access │ └── error.tsx -├── (auth)/ # /login, /signup — no sidebar +├── (auth)/ # /login, /signup — branded pages; hand off to Auth0; no sidebar ├── api/ │ ├── pymthouse/ # BFF: account-usage, keys, plans, subscribe, wallet, invoices │ ├── mcp/ # Streamable HTTP MCP resource server diff --git a/README.md b/README.md index d4f14a0..8a6477b 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ lib/ | `/apps/[id]` | public | App detail + playground | | `/orgs/[slug]` | public | Organization's published apps | | `/network` | public | Network stats (sidebar: "Stats") | -| `/login` | public | Sign in | -| `/signup` | public | Sign up | +| `/login` | public | Sign in (hands off to Auth0) | +| `/signup` | public | Sign up (hands off to Auth0) | See `CLAUDE.md` for console conventions (KPI rows, tables, motion tokens, color rules). diff --git a/app/(app)/install/page.tsx b/app/(app)/install/page.tsx index 5a8ef47..811c1d7 100644 --- a/app/(app)/install/page.tsx +++ b/app/(app)/install/page.tsx @@ -9,8 +9,8 @@ import { type PointerEvent as ReactPointerEvent, type ReactNode, } from "react"; -import { useRouter } from "next/navigation"; import { useAuth } from "@/components/console/AuthContext"; +import { AUTH_SIGNIN_HREF } from "@/lib/console/auth-login"; import CopyButton from "@/components/console/CopyButton"; import HarnessLogo from "@/components/console/HarnessLogo"; import SectionHeader from "@/components/console/SectionHeader"; @@ -545,16 +545,15 @@ function McpServerUrl() { export default function InstallPage() { const { isConnected, isLoading } = useAuth(); - const router = useRouter(); // Middleware already sends signed-out requests to /login before this page // is served (see middleware.ts). This client-side fallback only fires if // the session lapses while the console is open. useEffect(() => { if (!isLoading && !isConnected) { - router.replace("/login"); + window.location.replace(AUTH_SIGNIN_HREF); } - }, [isLoading, isConnected, router]); + }, [isLoading, isConnected]); if (isLoading) return null; diff --git a/app/(app)/page.tsx b/app/(app)/page.tsx index 06ed226..b9d3374 100644 --- a/app/(app)/page.tsx +++ b/app/(app)/page.tsx @@ -1,25 +1,25 @@ "use client"; import { useEffect } from "react"; -import { useRouter } from "next/navigation"; import { useAuth } from "@/components/console/AuthContext"; +import { AUTH_SIGNIN_HREF } from "@/lib/console/auth-login"; // Root `/`: // - signed in → redirect to /home (the console default) -// - signed out → redirect to /login (the console default entry point) -// Explore is still reachable at /explore for anyone who lands there directly; -// it just isn't the default landing any more. +// - signed out → /login export default function RootPage() { const { isConnected, isLoading, user } = useAuth(); - const router = useRouter(); const signedIn = isConnected && !!user; useEffect(() => { if (isLoading) return; - router.replace(signedIn ? "/home" : "/login"); - }, [isLoading, signedIn, router]); + if (signedIn) { + window.location.replace("/home"); + return; + } + window.location.replace(AUTH_SIGNIN_HREF); + }, [isLoading, signedIn]); - // Nothing renders here — `/` is a pure redirect in both auth states. return null; } diff --git a/app/(app)/waitlist/page.tsx b/app/(app)/waitlist/page.tsx index 969e838..b02a543 100644 --- a/app/(app)/waitlist/page.tsx +++ b/app/(app)/waitlist/page.tsx @@ -1,6 +1,7 @@ import { auth0 } from "@/lib/auth0"; import ConsolePageHeader from "@/components/console/ConsolePageHeader"; import SectionHeader from "@/components/console/SectionHeader"; +import { consoleSignInHref } from "@/lib/console/auth-login"; import { isEmailAllowlisted } from "@/lib/console/email-allowlist"; export const dynamic = "force-dynamic"; @@ -27,7 +28,7 @@ export default async function WaitlistPage() {

{email}

) : (

- + Sign in {" "} to join the waitlist with your account email. diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx index 7834ef9..b10eb3b 100644 --- a/app/(auth)/layout.tsx +++ b/app/(auth)/layout.tsx @@ -13,7 +13,7 @@ export default function ConsoleAuthLayout({ }) { return ( -

+
{children}
diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 6302b46..9e07de7 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -1,27 +1,37 @@ import { redirect } from "next/navigation"; - import { auth0 } from "@/lib/auth0"; +import { authLoginHref, safeReturnTo } from "@/lib/console/auth-login"; import LoginPage from "@/components/console/LoginPage"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Sign in — Livepeer Early Access", +}; + const MCP_CALLBACK_PATH = "/api/mcp/oauth/callback"; export default async function LoginRoute({ - searchParams + searchParams, }: { searchParams: Promise<{ mcp_oauth?: string; + returnTo?: string; }>; }) { const params = await searchParams; const mcpOauth = params.mcp_oauth === "1"; + const returnTo = safeReturnTo(params.returnTo); const session = await auth0.getSession(); if (session) { - if (mcpOauth) { - redirect(MCP_CALLBACK_PATH); - } - redirect("/home"); + redirect(mcpOauth ? MCP_CALLBACK_PATH : returnTo); + } + + // MCP flow must go directly to Auth0 — no interactive UI step. + if (mcpOauth) { + redirect(authLoginHref({ returnTo: MCP_CALLBACK_PATH })); } - return ; + return ; } diff --git a/app/(auth)/signup/page.tsx b/app/(auth)/signup/page.tsx index eb72ea9..b1dc73f 100644 --- a/app/(auth)/signup/page.tsx +++ b/app/(auth)/signup/page.tsx @@ -1,12 +1,21 @@ +import type { Metadata } from "next"; import { redirect } from "next/navigation"; import { auth0 } from "@/lib/auth0"; +import { safeReturnTo } from "@/lib/console/auth-login"; import LoginPage from "@/components/console/LoginPage"; -export default async function SignupRoute() { - const session = await auth0.getSession(); - if (session) { - redirect("/home"); - } +export const metadata: Metadata = { + title: "Sign up — Livepeer Early Access", +}; - return ; +export default async function SignupRoute({ + searchParams, +}: { + searchParams: Promise<{ returnTo?: string }>; +}) { + const params = await searchParams; + const returnTo = safeReturnTo(params.returnTo); + const session = await auth0.getSession(); + if (session) redirect(returnTo); + return ; } diff --git a/app/api/mcp/oauth/callback/route.ts b/app/api/mcp/oauth/callback/route.ts index af03995..426ce46 100644 --- a/app/api/mcp/oauth/callback/route.ts +++ b/app/api/mcp/oauth/callback/route.ts @@ -25,8 +25,8 @@ export async function GET(req: NextRequest) { const session = await auth0.getSession(); const sub = session?.user?.sub?.trim(); if (!session || !sub) { - const login = new URL("/login", origin); - login.searchParams.set("mcp_oauth", "1"); + const login = new URL("/auth/login", origin); + login.searchParams.set("returnTo", "/api/mcp/oauth/callback"); return NextResponse.redirect(login); } diff --git a/app/authorize/route.ts b/app/authorize/route.ts index 0e84e39..3c75be9 100644 --- a/app/authorize/route.ts +++ b/app/authorize/route.ts @@ -88,7 +88,7 @@ export async function GET(req: NextRequest) { return json(req, 503, { error: "temporarily_unavailable" }); } - const login = consoleLoginUrl(req, nonce); + const login = consoleLoginUrl(req); const response = NextResponse.redirect(login, 302); response.cookies.set(PKCE_COOKIE, pending, pkceCookieOptions()); for (const [k, v] of Object.entries(corsHeaders(req))) { diff --git a/branding/auth0/branding.json b/branding/auth0/branding.json new file mode 100644 index 0000000..9525777 --- /dev/null +++ b/branding/auth0/branding.json @@ -0,0 +1,8 @@ +{ + "logo_url": "https://livepeer-console.vercel.app/icon.svg", + "favicon_url": "https://livepeer-console.vercel.app/icon.svg", + "colors": { + "primary": "#111111", + "page_background": "#FFFFFF" + } +} diff --git a/branding/auth0/text-login-en.json b/branding/auth0/text-login-en.json new file mode 100644 index 0000000..91fa662 --- /dev/null +++ b/branding/auth0/text-login-en.json @@ -0,0 +1,7 @@ +{ + "login": { + "title": "Log in to Livepeer Early Access", + "description": " ", + "buttonText": "Sign in" + } +} diff --git a/branding/auth0/text-signup-en.json b/branding/auth0/text-signup-en.json new file mode 100644 index 0000000..1247d8e --- /dev/null +++ b/branding/auth0/text-signup-en.json @@ -0,0 +1,7 @@ +{ + "signup": { + "title": "Get started with Livepeer", + "description": " ", + "buttonText": "Sign up" + } +} diff --git a/branding/auth0/text-signup-id-en.json b/branding/auth0/text-signup-id-en.json new file mode 100644 index 0000000..a427cc9 --- /dev/null +++ b/branding/auth0/text-signup-id-en.json @@ -0,0 +1,6 @@ +{ + "signup-id": { + "title": "Get started with Livepeer", + "description": " " + } +} diff --git a/branding/auth0/text-signup-password-en.json b/branding/auth0/text-signup-password-en.json new file mode 100644 index 0000000..20e91eb --- /dev/null +++ b/branding/auth0/text-signup-password-en.json @@ -0,0 +1,6 @@ +{ + "signup-password": { + "title": "Get started with Livepeer", + "description": " " + } +} diff --git a/branding/auth0/theme.json b/branding/auth0/theme.json new file mode 100644 index 0000000..5732ed2 --- /dev/null +++ b/branding/auth0/theme.json @@ -0,0 +1,57 @@ +{ + "displayName": "Livepeer Console", + "colors": { + "primary_button": "#111111", + "primary_button_label": "#FFFFFF", + "secondary_button_border": "#E4E4E4", + "secondary_button_label": "#111111", + "base_focus_color": "#40BF86", + "base_hover_color": "#F5F5F5", + "links_focused_components": "#40BF86", + "header": "#111111", + "body_text": "#111111", + "widget_background": "#FFFFFF", + "widget_border": "#E4E4E4", + "input_labels_placeholders": "#9CA3AF", + "input_filled_text": "#111111", + "input_border": "#E4E4E4", + "input_background": "#FFFFFF", + "icons": "#9CA3AF", + "error": "#EF4444", + "success": "#40BF86" + }, + "fonts": { + "font_url": "https://rsms.me/inter/font-files/InterVariable.woff2", + "links_style": "normal", + "reference_text_size": 16, + "title": { "size": 112.5, "bold": false }, + "subtitle": { "size": 87.5, "bold": false }, + "body_text": { "size": 87.5, "bold": false }, + "buttons_text": { "size": 87.5, "bold": false }, + "input_labels": { "size": 87.5, "bold": false }, + "links": { "size": 75, "bold": false } + }, + "borders": { + "button_border_weight": 0, + "buttons_style": "rounded", + "button_border_radius": 4, + "input_border_weight": 1, + "inputs_style": "rounded", + "input_border_radius": 4, + "widget_corner_radius": 4, + "widget_border_weight": 1, + "show_widget_shadow": true + }, + "widget": { + "logo_position": "center", + "logo_url": "https://livepeer-console.vercel.app/icon.svg", + "logo_height": 36, + "header_text_alignment": "center", + "social_buttons_layout": "bottom" + }, + "page_background": { + "background_color": "#FFFFFF", + "background_image_url": "", + "page_layout": "center" + } +} diff --git a/components/console/ConsolePageHeader.tsx b/components/console/ConsolePageHeader.tsx index 1ba5583..dcd2796 100644 --- a/components/console/ConsolePageHeader.tsx +++ b/components/console/ConsolePageHeader.tsx @@ -1,9 +1,13 @@ "use client"; import type { ReactNode } from "react"; -import Link from "next/link"; import { usePathname } from "next/navigation"; import { useAuth } from "@/components/console/AuthContext"; +import { + AUTH_SIGNIN_HREF, + AUTH_SIGNUP_HREF, + isConsoleAuthPath, +} from "@/lib/console/auth-login"; import ScopeChip, { type PageScope } from "@/components/console/ScopeChip"; interface ConsolePageHeaderProps { @@ -34,8 +38,8 @@ interface ConsolePageHeaderProps { * and right-aligned actions. Information density comes from the content; this * bar is pure chrome and stays out of the way. * - * When the user is signed out (and not already on the `/login` - * auth route), a `Sign in` / `Sign up` pair is appended to the right + * When the user is signed out (and not already on /login or /signup), + * a `Sign in` / `Sign up` pair is appended to the right * actions cluster — mirrors the design prototype's `PageHead` injection * (`auth.authed === false && !auth.isAuthRoute`). A faint divider sits * between the page's own actions and the auth CTAs when both exist. @@ -49,8 +53,7 @@ export default function ConsolePageHeader({ }: ConsolePageHeaderProps) { const { isConnected, isLoading } = useAuth(); const pathname = usePathname() ?? ""; - const isAuthRoute = - pathname.startsWith("/login") || pathname.startsWith("/signup"); + const isAuthRoute = isConsoleAuthPath(pathname); // Hide auth CTAs while auth state is still resolving (one frame on first // paint) to avoid flashing them in for connected users. const showAuthCTAs = !isLoading && !isConnected && !isAuthRoute; @@ -84,18 +87,18 @@ export default function ConsolePageHeader({ )} {showAuthCTAs && ( <> - Sign in - - + Sign up - + )}
diff --git a/components/console/ConsoleSidebar.tsx b/components/console/ConsoleSidebar.tsx index ab5839c..6a66a36 100644 --- a/components/console/ConsoleSidebar.tsx +++ b/components/console/ConsoleSidebar.tsx @@ -2,11 +2,12 @@ import { useState } from "react"; import Link from "next/link"; -import { usePathname, useRouter } from "next/navigation"; +import { usePathname } from "next/navigation"; import { EllipsisVertical } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { LivepeerLockup } from "@/components/design-system/LivepeerLogo"; import { PORTAL_NAV_ITEMS } from "@/lib/constants"; +import { AUTH_SIGNIN_HREF, AUTH_SIGNUP_HREF } from "@/lib/console/auth-login"; import { useAuth, type ConsoleUser } from "@/components/console/AuthContext"; import Drawer from "@/components/design-system/Drawer"; import NavLink from "@/components/console/NavLink"; @@ -278,7 +279,6 @@ function SidebarNav({ } function SignedOutSidebarContent({ onNavigate }: { onNavigate?: () => void }) { - const router = useRouter(); const publicItems = PORTAL_NAV_ITEMS.filter((i) => i.zone === "network"); return ( @@ -319,26 +319,20 @@ function SignedOutSidebarContent({ onNavigate }: { onNavigate?: () => void }) { No credit card. Spin up in 30 seconds with an API key.

- - +
@@ -377,18 +371,12 @@ function SidebarContent({ onNavigate }: { onNavigate?: () => void }) { function MobileSidebarContent({ onNavigate }: { onNavigate: () => void }) { const { isConnected, isLoading, user, disconnect } = useAuth(); - const router = useRouter(); const pathname = usePathname(); const isSignedOut = !isLoading && !isConnected; const items = PORTAL_NAV_ITEMS.filter((i) => isSignedOut ? i.zone === "network" : i.zone !== "network" ); - const navigateTo = (href: string) => { - router.push(href); - onNavigate(); - }; - return (