← 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
M src/app/api/auth/login/route.tsM src/app/login/page.tsx
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 →