feat(web): dedicated CLI auth page with inline login/register
No more redirect to generic /auth/login. The /cli-auth?code=XXXX page now shows auth forms inline (Google, GitHub, email) with device code context — like Anthropic's "Build with Claude" page. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
60
apps/web/src/app/[locale]/cli-auth/cli-auth-login.tsx
Normal file
60
apps/web/src/app/[locale]/cli-auth/cli-auth-login.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { SocialProvider } from "@turbostarter/auth";
|
||||||
|
|
||||||
|
import { authConfig } from "~/config/auth";
|
||||||
|
import { SocialProviders } from "~/modules/auth/form/social-providers";
|
||||||
|
import { RegisterForm } from "~/modules/auth/form/register-form";
|
||||||
|
import { AuthDivider } from "~/modules/auth/layout/divider";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CliAuthLogin({ code }: Props) {
|
||||||
|
const redirectTo = `/cli-auth?code=${encodeURIComponent(code)}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-md space-y-8 p-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center space-y-3">
|
||||||
|
<div
|
||||||
|
className="mx-auto w-14 h-14 rounded-2xl flex items-center justify-center text-2xl font-bold"
|
||||||
|
style={{ background: "var(--cm-accent, #f97316)", color: "#000" }}
|
||||||
|
>
|
||||||
|
cm
|
||||||
|
</div>
|
||||||
|
<h1 className="text-2xl font-bold tracking-tight">
|
||||||
|
Connect to claudemesh CLI
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm" style={{ color: "var(--cm-fg-muted, #888)" }}>
|
||||||
|
Sign in or create an account to connect your terminal session.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Social providers */}
|
||||||
|
<SocialProviders
|
||||||
|
providers={authConfig.providers.oAuth as SocialProvider[]}
|
||||||
|
redirectTo={redirectTo}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AuthDivider />
|
||||||
|
|
||||||
|
{/* Email + password form */}
|
||||||
|
<RegisterForm redirectTo={redirectTo} />
|
||||||
|
|
||||||
|
{/* Device code footer */}
|
||||||
|
<div className="pt-2 text-center">
|
||||||
|
<div
|
||||||
|
className="inline-block rounded-lg px-4 py-2 font-mono text-sm tracking-widest"
|
||||||
|
style={{ background: "var(--cm-bg-muted, #1a1a1a)", color: "var(--cm-fg-muted, #888)" }}
|
||||||
|
>
|
||||||
|
{code}
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-xs" style={{ color: "var(--cm-fg-muted, #666)" }}>
|
||||||
|
Confirm this code matches your terminal
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import { redirect } from "next/navigation";
|
|
||||||
|
|
||||||
import { getSession } from "~/lib/auth/server";
|
import { getSession } from "~/lib/auth/server";
|
||||||
import { getMetadata } from "~/lib/metadata";
|
import { getMetadata } from "~/lib/metadata";
|
||||||
|
|
||||||
import { CliAuthFlow } from "./cli-auth-flow";
|
import { CliAuthFlow } from "./cli-auth-flow";
|
||||||
import { DeviceCodeApproval } from "./device-code-approval";
|
import { DeviceCodeApproval } from "./device-code-approval";
|
||||||
|
import { CliAuthLogin } from "./cli-auth-login";
|
||||||
|
|
||||||
export const generateMetadata = getMetadata({
|
export const generateMetadata = getMetadata({
|
||||||
title: "Sync with CLI",
|
title: "Connect CLI",
|
||||||
description: "Link your claudemesh CLI to your account.",
|
description: "Sign in to connect your claudemesh CLI.",
|
||||||
});
|
});
|
||||||
|
|
||||||
export default async function CliAuthPage({
|
export default async function CliAuthPage({
|
||||||
@@ -17,24 +16,24 @@ export default async function CliAuthPage({
|
|||||||
searchParams: Promise<{ code?: string; port?: string }>;
|
searchParams: Promise<{ code?: string; port?: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { user } = await getSession();
|
const { user } = await getSession();
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
const sp = await searchParams;
|
|
||||||
const qs = new URLSearchParams();
|
|
||||||
if (sp.code) qs.set("code", sp.code);
|
|
||||||
if (sp.port) qs.set("port", sp.port);
|
|
||||||
const returnTo = `/cli-auth${qs.size ? `?${qs}` : ""}`;
|
|
||||||
return redirect(`/auth/login?redirectTo=${encodeURIComponent(returnTo)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { code, port } = await searchParams;
|
const { code, port } = await searchParams;
|
||||||
|
|
||||||
// Device-code flow: code contains "-" (e.g. "ABCD-EFGH"), no port
|
// Device-code flow: code contains "-" (e.g. "ABCD-EFGH"), no port
|
||||||
const isDeviceCode = code && code.includes("-") && !port;
|
const isDeviceCode = code && code.includes("-") && !port;
|
||||||
|
|
||||||
if (isDeviceCode) {
|
if (isDeviceCode) {
|
||||||
|
if (!user) {
|
||||||
|
// NOT logged in → show inline auth form with device code context
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen bg-[var(--cm-bg,#0a0a0a)] text-[var(--cm-fg,#fafafa)] antialiased flex items-center justify-center">
|
||||||
|
<CliAuthLogin code={code} />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logged in → auto-approve
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-[var(--cm-bg)] text-[var(--cm-fg)] antialiased flex items-center justify-center">
|
<main className="min-h-screen bg-[var(--cm-bg,#0a0a0a)] text-[var(--cm-fg,#fafafa)] antialiased flex items-center justify-center">
|
||||||
<DeviceCodeApproval
|
<DeviceCodeApproval
|
||||||
code={code}
|
code={code}
|
||||||
userName={user.name ?? user.email}
|
userName={user.name ?? user.email}
|
||||||
@@ -43,9 +42,19 @@ export default async function CliAuthPage({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy callback flow (port-based)
|
||||||
|
if (!user) {
|
||||||
|
const { redirect } = await import("next/navigation");
|
||||||
|
const qs = new URLSearchParams();
|
||||||
|
if (code) qs.set("code", code);
|
||||||
|
if (port) qs.set("port", port);
|
||||||
|
const returnTo = `/cli-auth${qs.size ? `?${qs}` : ""}`;
|
||||||
|
return redirect(`/auth/login?redirectTo=${encodeURIComponent(returnTo)}`);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
className="min-h-screen bg-[var(--cm-bg)] text-[var(--cm-fg)] antialiased"
|
className="min-h-screen bg-[var(--cm-bg,#0a0a0a)] text-[var(--cm-fg,#fafafa)] antialiased"
|
||||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
>
|
>
|
||||||
<CliAuthFlow
|
<CliAuthFlow
|
||||||
|
|||||||
Reference in New Issue
Block a user