From c77ba198e7098fd41c242b9623f637a5f8e9945e Mon Sep 17 00:00:00 2001 From: John | Elite Encoder Date: Fri, 4 Sep 2026 16:11:56 -0400 Subject: [PATCH 1/4] refactor(auth): update authentication routes and UI components for Auth0 integration - Changed login and signup routes from `/login` and `/signup` to `/auth/login` and `/auth/signup`. - Updated middleware to redirect to Auth0 for authentication. - Refactored components to utilize new authentication links and improved user experience. - Added branding and localization for Auth0 login and signup pages. - Introduced new AuthPanel and AuthMediaRing components for better UI handling. This commit enhances the authentication flow by integrating Auth0, ensuring a seamless user experience during sign-in and sign-up processes. --- CLAUDE.md | 2 +- README.md | 3 +- app/(app)/install/page.tsx | 9 +- app/(app)/page.tsx | 16 +- app/(app)/waitlist/page.tsx | 3 +- app/(auth)/layout.tsx | 2 +- app/(auth)/login/page.tsx | 24 ++- app/(auth)/signup/page.tsx | 12 +- app/api/mcp/oauth/callback/route.ts | 4 +- app/authorize/route.ts | 2 +- app/robots.ts | 2 +- app/sitemap.ts | 3 +- branding/auth0/branding.json | 8 + branding/auth0/text-login-en.json | 7 + branding/auth0/text-signup-en.json | 7 + branding/auth0/text-signup-id-en.json | 6 + branding/auth0/text-signup-password-en.json | 6 + branding/auth0/theme.json | 57 ++++++ components/console/ConsolePageHeader.tsx | 21 +- components/console/ConsoleSidebar.tsx | 48 ++--- components/console/LoginPage.tsx | 194 +----------------- components/console/SignInWall.tsx | 13 +- components/console/auth/AuthMediaRing.tsx | 213 ++++++++++++++++++++ components/console/auth/AuthPanel.tsx | 154 ++++++++++++++ lib/console/auth-login.ts | 18 ++ lib/console/email-allowlist.test.ts | 1 + lib/mcp/oauth.ts | 8 +- middleware.ts | 5 +- public/brand/livepeer-icon.svg | 8 + public/brand/livepeer-wordmark.svg | 10 + 30 files changed, 592 insertions(+), 274 deletions(-) create mode 100644 branding/auth0/branding.json create mode 100644 branding/auth0/text-login-en.json create mode 100644 branding/auth0/text-signup-en.json create mode 100644 branding/auth0/text-signup-id-en.json create mode 100644 branding/auth0/text-signup-password-en.json create mode 100644 branding/auth0/theme.json create mode 100644 components/console/auth/AuthMediaRing.tsx create mode 100644 components/console/auth/AuthPanel.tsx create mode 100644 lib/console/auth-login.ts create mode 100644 public/brand/livepeer-icon.svg create mode 100644 public/brand/livepeer-wordmark.svg diff --git a/CLAUDE.md b/CLAUDE.md index 2a48c65..6a80f36 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 redirect 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..c72b197 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,6 @@ 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 | +| `/auth/login` | public | Sign in / sign up (Auth0 Universal Login) | 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..14eb838 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 + // Middleware already sends signed-out requests to Auth0 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..b655c59 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 → Auth0 Universal 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..1d4d515 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 { authLoginHref } 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..ff23931 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 } 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 = params.returnTo ?? "/home"; const session = await auth0.getSession(); if (session) { - if (mcpOauth) { - redirect(MCP_CALLBACK_PATH); - } - redirect("/home"); + redirect(mcpOauth ? MCP_CALLBACK_PATH : "/home"); + } + + // 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..be9922b 100644 --- a/app/(auth)/signup/page.tsx +++ b/app/(auth)/signup/page.tsx @@ -1,12 +1,14 @@ +import type { Metadata } from "next"; import { redirect } from "next/navigation"; import { auth0 } from "@/lib/auth0"; import LoginPage from "@/components/console/LoginPage"; +export const metadata: Metadata = { + title: "Sign up — Livepeer Early Access", +}; + export default async function SignupRoute() { const session = await auth0.getSession(); - if (session) { - redirect("/home"); - } - - return ; + if (session) redirect("/home"); + 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/app/robots.ts b/app/robots.ts index b2ce5f2..d97a4e4 100644 --- a/app/robots.ts +++ b/app/robots.ts @@ -4,7 +4,7 @@ export default function robots(): MetadataRoute.Robots { return { rules: { userAgent: "*", - allow: ["/", "/explore", "/network", "/models", "/login", "/signup"], + allow: ["/", "/explore", "/network", "/models", "/auth/login"], disallow: ["/home", "/install", "/calls", "/usage", "/keys"], }, sitemap: "https://earlyaccess.livepeer.org/sitemap.xml", diff --git a/app/sitemap.ts b/app/sitemap.ts index 19e2660..ea40f5c 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -6,7 +6,6 @@ export default function sitemap(): MetadataRoute.Sitemap { return [ { url: BASE_URL, changeFrequency: "weekly", priority: 1 }, { url: `${BASE_URL}/network`, changeFrequency: "daily", priority: 0.8 }, - { url: `${BASE_URL}/login`, changeFrequency: "yearly", priority: 0.3 }, - { url: `${BASE_URL}/signup`, changeFrequency: "yearly", priority: 0.3 }, + { url: `${BASE_URL}/auth/login`, changeFrequency: "yearly", priority: 0.3 }, ]; } diff --git a/branding/auth0/branding.json b/branding/auth0/branding.json new file mode 100644 index 0000000..a3e37af --- /dev/null +++ b/branding/auth0/branding.json @@ -0,0 +1,8 @@ +{ + "logo_url": "https://earlyaccess.livepeer.org/icon.svg", + "favicon_url": "https://earlyaccess.livepeer.org/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..0c30da3 --- /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://earlyaccess.livepeer.org/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..41319e7 100644 --- a/components/console/ConsolePageHeader.tsx +++ b/components/console/ConsolePageHeader.tsx @@ -1,9 +1,9 @@ "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 } from "@/lib/console/auth-login"; import ScopeChip, { type PageScope } from "@/components/console/ScopeChip"; interface ConsolePageHeaderProps { @@ -34,8 +34,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 starting an Auth0 + * login), 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 +49,7 @@ export default function ConsolePageHeader({ }: ConsolePageHeaderProps) { const { isConnected, isLoading } = useAuth(); const pathname = usePathname() ?? ""; - const isAuthRoute = - pathname.startsWith("/login") || pathname.startsWith("/signup"); + const isAuthRoute = pathname.startsWith("/auth/"); // 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 +83,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 (