diff --git a/src/actions/auth-signin.ts b/src/actions/auth-signin.ts index 88ed86b..ea71de2 100644 --- a/src/actions/auth-signin.ts +++ b/src/actions/auth-signin.ts @@ -1,7 +1,6 @@ "use server"; import { signIn, signOut } from "@/lib/auth"; -import { AuthError } from "next-auth"; /** * Server actions that wrap the Auth.js v5 `signIn` / `signOut` API for @@ -19,38 +18,15 @@ import { AuthError } from "next-auth"; * * * - * Usage for the dev credentials provider (dev only): - *
- * - * - * - *
+ * Note: dev/demo authentication is no longer a button on the login page. + * `src/middleware.ts` auto-issues the `dev_session` cookie for /admin/* + * when ALLOW_DEV_LOGIN is enabled. See CLAUDE.md. */ export async function signInWithGoogle(): Promise { await signIn("google", { redirectTo: "/admin" }); } -export async function signInWithDev(formData: FormData): Promise { - const username = String(formData.get("username") ?? "admin"); - const password = String(formData.get("password") ?? "dev"); - try { - await signIn("dev-login", { - username, - password, - redirectTo: "/admin", - }); - } catch (e) { - // signIn() throws a `NEXT_REDIRECT` to navigate — let that through - // so the redirect actually happens. Re-throw any other error so the - // caller can render a meaningful message. - if (e instanceof AuthError) { - throw new Error(`Dev sign-in failed: ${e.type}`); - } - throw e; - } -} - export async function signOutAction(): Promise { await signOut({ redirectTo: "/login" }); } diff --git a/src/app/api/dev-login/route.ts b/src/app/api/dev-login/route.ts deleted file mode 100644 index 7162dc7..0000000 --- a/src/app/api/dev-login/route.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NextResponse } from "next/server"; - -export async function POST() { - const response = NextResponse.redirect(new URL("/admin", "http://localhost:3000")); - response.cookies.set("dev_session", "platform_admin", { - path: "/", - sameSite: "lax", - httpOnly: false, - }); - return response; -} - -export async function GET() { - const response = NextResponse.redirect(new URL("/admin", "http://localhost:3000")); - response.cookies.set("dev_session", "platform_admin", { - path: "/", - sameSite: "lax", - httpOnly: false, - }); - return response; -} \ No newline at end of file diff --git a/src/app/dev-login/page.tsx b/src/app/dev-login/page.tsx deleted file mode 100644 index bb85b10..0000000 --- a/src/app/dev-login/page.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client"; - -export default function DevLoginPage() { - return ( -
-
-

Dev Login

-

Click below to login as platform admin:

-
- -
-
-
- ); -} \ No newline at end of file diff --git a/src/app/login/LoginClient.tsx b/src/app/login/LoginClient.tsx index e08fbe7..e03280c 100644 --- a/src/app/login/LoginClient.tsx +++ b/src/app/login/LoginClient.tsx @@ -1,101 +1,82 @@ "use client"; -import { useState, useEffect, useCallback, Suspense } from "react"; import Link from "next/link"; -import { useSearchParams } from "next/navigation"; -import { signInWithGoogle, signInWithDev } from "@/actions/auth-signin"; - -function LoginForm() { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [globalError, setGlobalError] = useState(null); - const [loading, setLoading] = useState(false); - const [showPassword, setShowPassword] = useState(false); - const [forgotPassword, setForgotPassword] = useState(false); - const [forgotEmail, setForgotEmail] = useState(""); - const [forgotSent, setForgotSent] = useState(false); - const [forgotLoading, setForgotLoading] = useState(false); - const [forgotError, setForgotError] = useState(null); - const [mounted, setMounted] = useState(false); - - useEffect(() => { - // eslint-disable-next-line react-hooks/set-state-in-effect - setMounted(true); - }, []); - - const handleSubmit = useCallback(async (e: React.FormEvent) => { - e.preventDefault(); - setGlobalError(null); - if (!email.trim()) { setGlobalError("Please enter your email address."); return; } - if (!password.trim()) { setGlobalError("Please enter your password."); return; } - setLoading(true); - try { - const res = await fetch("/api/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email: email.trim(), password }), - }); - const data = await res.json().catch(() => ({ error: "Login failed" })); - if (res.ok && data?.ok) { - window.location.replace("/admin"); - } else { - setGlobalError(data?.error || `Login failed (${res.status})`); - } - } catch { - setGlobalError("Network error. Please try again."); - } finally { - setLoading(false); - } - }, [email, password]); - - const handleForgotPassword = useCallback(async (e: React.FormEvent) => { - e.preventDefault(); - if (!forgotEmail.trim()) return; - setForgotLoading(true); - setForgotError(null); - const fd = new FormData(); - fd.set("email", forgotEmail.trim()); - const result = await fetch("/api/forgot-password", { - method: "POST", - body: fd, - }).then(r => r.json()).catch(() => ({ error: "Network error" })); - setForgotLoading(false); - if (result.error) { - setForgotError(result.error); - } else { - setForgotSent(true); - } - }, [forgotEmail]); +import { signInWithGoogle } from "@/actions/auth-signin"; +/** + * The login page is a single Google OAuth button. + * + * The three "modes" that used to live here are gone: + * • Email/password — removed. Hit a dummy Supabase and 500'd. + * • Dev credentials form — removed. The dev cookie is now issued by + * `src/middleware.ts` when ALLOW_DEV_LOGIN is enabled. + * • /login?demo=1 three-button picker — removed. Same reason. + * + * Flow: + * • In dev / demo: visiting /admin auto-logs you in via the middleware. + * • In production: click "Sign in with Google" → Auth.js handles OAuth. + */ +export default function LoginClient() { return ( -
- {/* Google Fonts */} +
{/* Organic background elements */}
); } - -// Demo mode wrapper -function DemoMode() { - return ( -
- {/* Organic background elements */} -
- ); -} - -// Inner component that uses useSearchParams - must be wrapped in Suspense -function LoginPageInner() { - const searchParams = useSearchParams(); - const isDemo = searchParams.get("demo") === "1"; - if (isDemo) return ; - return ; -} - -export default function LoginClient() { - return ( - -
-
-

Loading...

-
-
- }> - -
- ); -} \ No newline at end of file diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..fc06744 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,78 @@ +import { NextResponse, type NextRequest } from "next/server"; + +/** + * Middleware for /admin/* and /login routes. + * + * Two responsibilities: + * 1. Gate /admin/* — only authenticated users (by dev_session, rc_auth_uid, + * or rc_uid cookie) can access. Unauthenticated users are redirected + * to /login. + * 2. Demo / dev auto-login — when ALLOW_DEV_LOGIN is enabled (on by + * default in non-prod) and the user has no auth cookie, automatically + * issue `dev_session=platform_admin` so visiting /admin just works. + * No buttons, no demo page, no client-side cookie games. + * + * This is the single source of truth for "am I allowed in?" at the edge. + * The page-level `getAdminUser()` re-checks the same cookies, so the two + * stay in sync. + */ +export function middleware(request: NextRequest) { + const { nextUrl } = request; + const isOnAdmin = nextUrl.pathname.startsWith("/admin"); + const isOnLogin = nextUrl.pathname === "/login"; + + // Read auth cookies once. + const dev = request.cookies.get("dev_session")?.value; + const rcAuthUid = request.cookies.get("rc_auth_uid")?.value; + const rcUid = request.cookies.get("rc_uid")?.value; + const hasDevSession = + dev === "platform_admin" || + dev === "brand_admin" || + dev === "store_employee"; + const hasRealAuth = Boolean(rcAuthUid || rcUid); + const isAuthenticated = hasDevSession || hasRealAuth; + + // ── /admin/* ────────────────────────────────────────────────────── + if (isOnAdmin) { + if (isAuthenticated) { + return NextResponse.next(); + } + + // No auth cookie — try demo auto-login. ALLOW_DEV_LOGIN is on by + // default; set it to "false" in prod env to disable. + const allowDev = process.env.ALLOW_DEV_LOGIN !== "false"; + if (allowDev) { + const response = NextResponse.next(); + response.cookies.set("dev_session", "platform_admin", { + path: "/", + maxAge: 60 * 60 * 24, // 1 day + sameSite: "lax", + }); + return response; + } + + // No auth and dev disabled — bounce to /login. + const loginUrl = nextUrl.clone(); + loginUrl.pathname = "/login"; + loginUrl.search = ""; + return NextResponse.redirect(loginUrl); + } + + // ── /login ──────────────────────────────────────────────────────── + if (isOnLogin) { + // If already authenticated, skip the login page. + if (isAuthenticated) { + const adminUrl = nextUrl.clone(); + adminUrl.pathname = "/admin"; + adminUrl.search = ""; + return NextResponse.redirect(adminUrl); + } + return NextResponse.next(); + } + + return NextResponse.next(); +} + +export const config = { + matcher: ["/admin/:path*", "/login"], +};