Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,17 @@ PYMTHOUSE_ALLOW_INSECURE_HTTP=
# Invite-only Console HTML (/home, /usage, /keys, /calls, /settings).
# Empty = no gate. Comma-separated emails, matched case-insensitively.
# CONSOLE_EMAIL_ALLOWLIST=alice@livepeer.org,bob@studio.com

# Waitlist + canonical users (Postgres migrations 0000-0005).
# Production must use the existing waitlist database. Previews and tests must
# use an isolated database branch; never point them at the production branch.
DATABASE_URL=postgresql://user:password@host/database?sslmode=require
ATTRIBUTION_HASH_SECRET=replace-with-at-least-32-random-bytes
RESEND_API_KEY=re_replace_me
RESEND_NEWSLETTER_SEGMENT_ID=replace-with-resend-segment-id
EMAIL_FROM=Livepeer Waitlist <waitlist@mail.example.com>
EMAIL_REPLY_TO=help@example.com
INTERNAL_OUTBOX_SECRET=replace-with-at-least-32-random-bytes
NEXT_PUBLIC_SITE_URL=http://localhost:3000
# Publishable token; analytics safely no-ops when unset.
NEXT_PUBLIC_POSTHOG_KEY=
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

# next.js
/.next/
/.next-cutover/
/out/

# production
Expand Down
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
pnpm-lock.yaml
.next/
.next-cutover/
9 changes: 6 additions & 3 deletions app/(app)/device/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { redirect } from "next/navigation";

import { auth0 } from "@/lib/auth0";
import { authLoginHref } from "@/lib/console/auth-login";
import { parseDeviceInitiateParams } from "@/lib/console/device-approval";
import DeviceApproveForm, { DevicePageChrome } from "./DeviceApproveForm";

Expand All @@ -18,20 +19,22 @@ export default async function DevicePage({
const params = await searchParams;
const query = new URLSearchParams();
if (params.iss) query.set("iss", params.iss);
if (params.target_link_uri) query.set("target_link_uri", params.target_link_uri);
if (params.target_link_uri)
query.set("target_link_uri", params.target_link_uri);
if (params.login_hint) query.set("login_hint", params.login_hint);
const returnTo = `/device${query.size ? `?${query.toString()}` : ""}`;

const session = await auth0.getSession();
if (!session?.user?.sub) {
redirect(`/auth/login?returnTo=${encodeURIComponent(returnTo)}`);
redirect(authLoginHref({ returnTo }));
}

let parsed;
try {
parsed = parseDeviceInitiateParams(query);
} catch (error) {
const message = error instanceof Error ? error.message : "Invalid device request";
const message =
error instanceof Error ? error.message : "Invalid device request";
return (
<DevicePageChrome>
<p className="text-sm text-red-400">{message}</p>
Expand Down
47 changes: 0 additions & 47 deletions app/(app)/waitlist/page.tsx

This file was deleted.

9 changes: 9 additions & 0 deletions app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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 { syncCanonicalUserBestEffort } from "@/lib/identity/canonical-user";

import type { Metadata } from "next";

Expand All @@ -25,6 +26,14 @@ export default async function LoginRoute({

const session = await auth0.getSession();
if (session) {
const sub = session.user.sub?.trim();
if (sub) {
await syncCanonicalUserBestEffort({
sub,
email: session.user.email?.trim() || undefined,
emailVerified: session.user.email_verified === true,
});
}
redirect(mcpOauth ? MCP_CALLBACK_PATH : returnTo);
}

Expand Down
13 changes: 12 additions & 1 deletion app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { redirect } from "next/navigation";
import { auth0 } from "@/lib/auth0";
import { safeReturnTo } from "@/lib/console/auth-login";
import LoginPage from "@/components/console/LoginPage";
import { syncCanonicalUserBestEffort } from "@/lib/identity/canonical-user";

export const metadata: Metadata = {
title: "Sign up — Livepeer Early Access",
Expand All @@ -16,6 +17,16 @@ export default async function SignupRoute({
const params = await searchParams;
const returnTo = safeReturnTo(params.returnTo);
const session = await auth0.getSession();
if (session) redirect(returnTo);
if (session) {
const sub = session.user.sub?.trim();
if (sub) {
await syncCanonicalUserBestEffort({
sub,
email: session.user.email?.trim() || undefined,
emailVerified: session.user.email_verified === true,
});
}
redirect(returnTo);
}
return <LoginPage mode="signup" returnTo={returnTo} />;
}
68 changes: 68 additions & 0 deletions app/(waitlist)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { Metadata } from "next";
import localFont from "next/font/local";

const waitlistInter = localFont({
src: [
{
path: "../../assets/fonts/waitlist/InterVariable.woff2",
weight: "100 900",
style: "normal",
},
{
path: "../../assets/fonts/waitlist/InterVariable-Italic.woff2",
weight: "100 900",
style: "italic",
},
],
variable: "--font-inter",
});

const waitlistFavorit = localFont({
src: [
{
path: "../../assets/fonts/waitlist/FavoritPro-Light.woff2",
weight: "300",
},
{
path: "../../assets/fonts/waitlist/FavoritPro-Book.woff2",
weight: "350",
},
{
path: "../../assets/fonts/waitlist/FavoritPro-Regular.woff2",
weight: "400",
},
{
path: "../../assets/fonts/waitlist/FavoritPro-Medium.woff2",
weight: "500",
},
{
path: "../../assets/fonts/waitlist/FavoritPro-Bold.woff2",
weight: "700",
},
],
variable: "--font-favorit",
});

const waitlistMono = localFont({
src: "../../assets/fonts/waitlist/FavoritMono-Regular.woff2",
weight: "400",
variable: "--font-mono",
});

export const metadata: Metadata = {
title: "Join the waitlist — Livepeer Early Access",
description:
"Request early access to a faster way to build, run, and scale live video products.",
};

export default function WaitlistLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<div
className={`waitlist-surface ${waitlistInter.variable} ${waitlistFavorit.variable} ${waitlistMono.variable}`}
>
{children}
</div>
);
}
20 changes: 20 additions & 0 deletions app/(waitlist)/waitlist/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { AgentScrollerPage } from "@/components/livepeer-ui/agent-scroller-page";
import {
capabilities,
networkImages,
} from "@/components/livepeer-ui/frozen-content";
import { WaitlistSessionProvider } from "@/components/livepeer-ui/waitlist-session";
import { getCurrentWaitlistSession } from "@/lib/waitlist/current-session";

export default async function WaitlistPage() {
const initialSession = await getCurrentWaitlistSession();

return (
<WaitlistSessionProvider initialSession={initialSession}>
<AgentScrollerPage
capabilities={capabilities}
networkImages={networkImages}
/>
</WaitlistSessionProvider>
);
}
3 changes: 3 additions & 0 deletions app/admin/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import WaitlistLayout from "@/app/(waitlist)/layout";

export default WaitlistLayout;
119 changes: 119 additions & 0 deletions app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { redirect } from "next/navigation";
import { Download } from "lucide-react";

import { LivepeerHeader } from "@/components/livepeer-ui/livepeer-header";
import { buttonVariants } from "@/components/ui/button";
import {
getAdminWaitlistRows,
getAdminWaitlistSummary,
} from "@/lib/waitlist/admin";
import { getAdminSession } from "@/lib/waitlist/admin-auth";
import { cn } from "@/lib/utils";

export const dynamic = "force-dynamic";

export default async function AdminPage() {
const admin = await getAdminSession();
if (!admin) redirect("/waitlist");

const [rows, summary] = await Promise.all([
getAdminWaitlistRows(),
getAdminWaitlistSummary(),
]);

return (
<main className="dark min-h-svh bg-background text-foreground">
<LivepeerHeader
homeHref="/waitlist"
transparent
utility={
<div className="flex items-center">
<a
href="/api/admin/signups.csv"
className={cn(
buttonVariants({ variant: "muted", size: "sm" }),
"h-8 rounded-sm px-3 text-[10px] dark:bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]"
)}
>
<Download className="size-3.5" aria-hidden="true" />
Export CSV
</a>
</div>
}
/>

<section className="w-full px-4 py-10 font-sans sm:px-6 lg:px-10">
<h1 className="font-display text-4xl font-light tracking-[-0.04em]">
Waitlist
</h1>

<dl className="mt-8 grid grid-cols-2 gap-x-6 gap-y-8 md:grid-cols-4">
{[
["Total signups", summary.totalSignups],
["Verified signups", summary.confirmedSignups],
["Total verified referrals", summary.totalVerifiedReferrals],
["Newsletter opt-ins", summary.newsletterSubscribers],
].map(([label, value]) => (
<div key={label}>
<dt className="text-xs text-white/50">{label}</dt>
<dd className="mt-2 text-3xl font-light tracking-[-0.04em] tabular-nums">
{Number(value).toLocaleString()}
</dd>
</div>
))}
</dl>

<div className="mt-8 overflow-x-auto border-y border-white/15">
<table className="w-full min-w-[72rem] text-left text-xs">
<thead className="border-b border-white/15 text-xs text-white/50">
<tr>
<th className="py-3 pr-6 font-medium">Email</th>
<th className="px-3 py-3 font-medium">Status</th>
<th className="px-3 py-3 font-medium">Referred by</th>
<th className="px-3 py-3 text-right font-medium">
Verified referrals
</th>
<th className="px-3 py-3 text-right font-medium">
Pending referrals
</th>
<th className="px-3 py-3 text-right font-medium">Points</th>
<th className="px-3 py-3 font-medium">Newsletter</th>
<th className="py-3 pl-6 font-medium">Joined</th>
</tr>
</thead>
<tbody className="divide-y divide-white/10">
{rows.map((row) => (
<tr key={row.email}>
<td className="py-3 pr-6 font-medium">{row.email}</td>
<td className="px-3 py-3 text-white/65">{row.status}</td>
<td className="max-w-64 truncate px-3 py-3 text-white/65">
{row.referredByEmail ?? "—"}
</td>
<td className="px-3 py-3 text-right tabular-nums">
{row.verifiedReferrals}
</td>
<td className="px-3 py-3 text-right tabular-nums">
{row.pendingReferrals}
</td>
<td className="px-3 py-3 text-right tabular-nums">
{row.points}
</td>
<td className="px-3 py-3 text-white/65">
{row.marketingConsent ? "Subscribed" : "Not subscribed"}
</td>
<td className="py-3 pl-6 text-white/65">
{row.firstSeenAt.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
})}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
</main>
);
}
Loading