[object Object]

← back to Govarbitrage

Username-only sign-in (email login retired); admin uses std fleet password

6c0f4d897504b9e43263564633db48062aa11658 · 2026-07-22 19:11:25 -0700 · Steve Abrams

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Files touched

Diff

commit 6c0f4d897504b9e43263564633db48062aa11658
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 22 19:11:25 2026 -0700

    Username-only sign-in (email login retired); admin uses std fleet password
    
    Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
 src/app/api/auth/login/route.ts | 27 +++++++++++++--------------
 src/app/login/page.tsx          |  9 +++------
 2 files changed, 16 insertions(+), 20 deletions(-)

diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts
index 982b43f..7ce10e2 100644
--- a/src/app/api/auth/login/route.ts
+++ b/src/app/api/auth/login/route.ts
@@ -7,15 +7,14 @@ import { rateLimit, clientIp, tooManyRequests } from "@/lib/rate-limit";
 
 export const dynamic = "force-dynamic";
 
-// `email` accepts a full email OR a bare username (resolved against the
-// email local-part, e.g. "admin" → admin@agentabrams.com). Field name kept
-// as `email` for wire compatibility with existing clients.
-const Schema = z.object({ email: z.string().trim().min(1), password: z.string().min(1) });
+// Username-only login (Steve 2026-07-22: "no more email login"). The username
+// is resolved against the unique user whose email local-part matches
+// ("admin" → admin@govarbitrage.local). Full emails are rejected outright.
+const Schema = z.object({ username: z.string().trim().min(1), password: z.string().min(1) });
 
-async function resolveUser(identifier: string) {
-  const id = identifier.toLowerCase();
-  if (id.includes("@")) return prisma.user.findUnique({ where: { email: id } });
-  // Bare username: match the unique user whose email local-part equals it.
+async function resolveUser(username: string) {
+  const id = username.toLowerCase();
+  if (id.includes("@")) return null; // email-style logins are retired
   const matches = await prisma.user.findMany({
     where: { email: { startsWith: `${id}@` } },
     take: 2,
@@ -33,18 +32,18 @@ export async function POST(req: NextRequest) {
   if (!parsed.success) {
     return NextResponse.json({ error: "Username and password required" }, { status: 400 });
   }
-  const { email, password } = parsed.data;
+  const { username, password } = parsed.data;
 
-  // And per targeted identifier (credential-stuffing protection).
-  const emailLimit = rateLimit(`login:email:${email.toLowerCase()}`, 5, 15 * 60_000);
-  if (!emailLimit.allowed) return tooManyRequests(emailLimit);
-  const user = await resolveUser(email);
+  // And per targeted username (credential-stuffing protection).
+  const userLimit = rateLimit(`login:user:${username.toLowerCase()}`, 5, 15 * 60_000);
+  if (!userLimit.allowed) return tooManyRequests(userLimit);
+  const user = await resolveUser(username);
 
   // Constant-ish response whether or not the user exists.
   const ok = user ? verifyPassword(password, user.passwordHash) : false;
   if (!user || !ok) {
     await prisma.auditLog.create({
-      data: { action: "auth.login.failed", entity: "User", meta: { email } },
+      data: { action: "auth.login.failed", entity: "User", meta: { username } },
     });
     return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
   }
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
index cf8b9f4..e1c7ca8 100644
--- a/src/app/login/page.tsx
+++ b/src/app/login/page.tsx
@@ -10,7 +10,7 @@ function LoginForm() {
   const router = useRouter();
   const search = useSearchParams();
   const next = search.get("next") || "/";
-  const [email, setEmail] = useState("");
+  const [username, setUsername] = useState("");
   const [password, setPassword] = useState("");
   const [error, setError] = useState("");
   const [loading, setLoading] = useState(false);
@@ -23,7 +23,7 @@ function LoginForm() {
       const res = await fetch("/api/auth/login", {
         method: "POST",
         headers: { "Content-Type": "application/json" },
-        body: JSON.stringify({ email, password }),
+        body: JSON.stringify({ username, password }),
       });
       if (res.ok) {
         router.push(next);
@@ -48,16 +48,13 @@ function LoginForm() {
       </CardHeader>
       <CardContent>
         <form onSubmit={submit} className="space-y-3">
-          <Input type="text" autoCapitalize="none" autoCorrect="off" placeholder="Username or email" value={email} onChange={(e) => setEmail(e.target.value)} required autoFocus />
+          <Input type="text" autoCapitalize="none" autoCorrect="off" placeholder="Username" value={username} onChange={(e) => setUsername(e.target.value)} required autoFocus />
           <Input type="password" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} required />
           {error && <p className="text-sm text-[var(--negative)]">{error}</p>}
           <Button type="submit" className="w-full" disabled={loading}>
             {loading ? "Signing in…" : "Sign in"}
           </Button>
         </form>
-        <p className="mt-3 text-xs text-muted-foreground">
-          Demo: <code>admin</code> / <code>changeme</code>
-        </p>
       </CardContent>
     </Card>
   );

← 1a64a1d Sign-in accepts bare username (resolved via email local-part  ·  back to Govarbitrage  ·  auctions: accept fleet SSO (aafleet) cookie → one shared log 2c066de →