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 { getMetadata } from "~/lib/metadata";
|
||||
|
||||
import { CliAuthFlow } from "./cli-auth-flow";
|
||||
import { DeviceCodeApproval } from "./device-code-approval";
|
||||
import { CliAuthLogin } from "./cli-auth-login";
|
||||
|
||||
export const generateMetadata = getMetadata({
|
||||
title: "Sync with CLI",
|
||||
description: "Link your claudemesh CLI to your account.",
|
||||
title: "Connect CLI",
|
||||
description: "Sign in to connect your claudemesh CLI.",
|
||||
});
|
||||
|
||||
export default async function CliAuthPage({
|
||||
@@ -17,24 +16,24 @@ export default async function CliAuthPage({
|
||||
searchParams: Promise<{ code?: string; port?: string }>;
|
||||
}) {
|
||||
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;
|
||||
|
||||
// Device-code flow: code contains "-" (e.g. "ABCD-EFGH"), no port
|
||||
const isDeviceCode = code && code.includes("-") && !port;
|
||||
|
||||
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)] 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">
|
||||
<CliAuthLogin code={code} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
// Logged in → auto-approve
|
||||
return (
|
||||
<main className="min-h-screen bg-[var(--cm-bg,#0a0a0a)] text-[var(--cm-fg,#fafafa)] antialiased flex items-center justify-center">
|
||||
<DeviceCodeApproval
|
||||
code={code}
|
||||
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 (
|
||||
<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)" }}
|
||||
>
|
||||
<CliAuthFlow
|
||||
|
||||
Reference in New Issue
Block a user