diff --git a/admin/src/app/auth/LoginPage.tsx b/admin/src/app/auth/LoginPage.tsx index 030ffab8..89fba62c 100644 --- a/admin/src/app/auth/LoginPage.tsx +++ b/admin/src/app/auth/LoginPage.tsx @@ -1,29 +1,43 @@ -// Admin login. Same endpoint as the dashboard (/auth/login). The page -// is intentionally lean — no marketing chrome, no sky animations. The -// admin app exists to do work, not to convert anyone. +// Admin sign-in. +// +// Clean and in-theme (the same light shadcn surface as the rest of the admin +// app), but distinct from the consumer dashboard's two-pane marketing login: +// a single, focused card with a red accent that marks this as restricted +// admin access. Same /auth/login endpoint as the dashboard. Stack matches the +// dashboard — Tailwind + the shadcn ui primitives + the shared theme tokens. import { useState, type FormEvent } from "react"; import { useNavigate, useLocation } from "react-router-dom"; import { toast } from "sonner"; +import { ShieldCheck, Eye, EyeOff, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { AdminBadge } from "@/components/layout/AdminBadge"; +import { Logo } from "@/components/Logo"; import { EnvPill } from "@/components/layout/EnvPill"; import { login } from "@/lib/api/client/auth"; import { setToken } from "@/lib/auth/storage"; import { APIError } from "@/lib/api/client"; +import { DASHBOARD_URL } from "@/lib/env"; +import { cn } from "@/lib/utils"; + +// Admin accent. red-600 — the same value as the theme's --admin-danger token, +// signalling "restricted / elevated access" without any extra chrome. +const ACCENT = "var(--admin-danger)"; export default function LoginPage() { const nav = useNavigate(); const loc = useLocation(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); async function onSubmit(e: FormEvent) { e.preventDefault(); setSubmitting(true); + setError(null); try { const tok = await login({ email, password }); setToken(tok); @@ -34,76 +48,129 @@ export default function LoginPage() { err instanceof APIError ? err.message : "Sign-in failed. Check your credentials or contact an admin."; + setError(msg); toast.error(msg); } finally { setSubmitting(false); } } + const focusRed = + "focus-visible:border-red-400 focus-visible:ring-red-100"; + return ( -
- {/* Mirror the app-shell stripe so the auth screen is also - visually anchored to the admin surface. */} -
- -
-
-
-
- W -
-
Warmbly
-
- +
+
+ {/* Brand */} +
+ + Warmbly
-
-
- - sign in to control plane + {/* Card — a thin red top edge is the only "admin" flourish. */} +
+
+ +
+
+ + + Admin access + + +
+ +

Sign in

+

+ Restricted to Warmbly staff with admin permissions. +

+ +
+
+ + setEmail(e.target.value)} + onInput={() => error && setError(null)} + required + placeholder="you@warmbly.com" + aria-invalid={Boolean(error)} + className={cn("h-10", focusRed)} + /> +
+ +
+ +
+ setPassword(e.target.value)} + onInput={() => error && setError(null)} + required + placeholder="••••••••••••" + aria-invalid={Boolean(error)} + className={cn("h-10 pr-10", focusRed)} + /> + +
+
+ + {error && ( +

+ + {error} +

+ )} + + +
-

Admin sign in

-

- Restricted to accounts with admin permissions on the Warmbly platform. +

+ + {/* Footer */} +
+

+ Every admin action is logged to{" "} + admin_audit_logs.

- -
-
- - setEmail(e.target.value)} - required - /> -
-
- - setPassword(e.target.value)} - required - /> -
- -
+ + ← Back to the Warmbly dashboard +
- -

- All admin actions are logged to admin_audit_logs. -

); diff --git a/admin/src/components/Logo.tsx b/admin/src/components/Logo.tsx new file mode 100644 index 00000000..b30adc92 --- /dev/null +++ b/admin/src/components/Logo.tsx @@ -0,0 +1,20 @@ +// The Warmbly geometric mark. Single-path, inherits color via currentColor so +// it works on light or dark surfaces. Mirrors the wordmark used on the +// marketing site and dashboard so the admin app shares one brand. + +export function Logo({ className }: { className?: string }) { + return ( + + ); +}