sign-in/sign-up build fixes

This commit is contained in:
makearmy 2025-09-26 15:40:36 -04:00
parent 514982d009
commit 9f1dffb3b5
2 changed files with 202 additions and 114 deletions

View file

@ -1,80 +1,125 @@
// app/auth/sign-in/page.tsx
import { Suspense } from "react";
import type { Metadata } from "next";
import Link from "next/link";
// UI (shadcn)
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
export const metadata: Metadata = { title: "Sign in" };
// Server Component wrapper — no hooks here
export default function SignInPage() {
return (
<div className="min-h-[60vh] flex items-center justify-center px-4">
<div className="w-full max-w-md space-y-6">
<div className="text-center space-y-2">
<h1 className="text-2xl font-bold">Sign in</h1>
<p className="text-sm text-muted-foreground">
Welcome back. Enter your credentials to continue.
</p>
</div>
{/* Client sub-component wrapped in Suspense so useSearchParams is allowed */}
<Suspense fallback={<div className="text-sm text-muted-foreground">Loading</div>}>
<SignInClient />
</Suspense>
<p className="text-sm text-center text-muted-foreground">
Dont have an account?{" "}
<Link href="/auth/sign-up" className="underline underline-offset-4 hover:opacity-80">
Create one
</Link>
</p>
</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// Client piece that actually uses useSearchParams/router/fetch
// ─────────────────────────────────────────────────────────────
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
export default function SignInPage() {
function SignInClient() {
const router = useRouter();
const search = useSearchParams();
const nextUrl = search.get("next") || "/my/rigs";
const sp = useSearchParams();
const [identity, setIdentity] = useState("");
const [password, setPassword] = useState("");
const [busy, setBusy] = useState(false);
const [idVal, setIdVal] = useState("");
const [pwVal, setPwVal] = useState("");
const [submitting, setSubmitting] = useState(false);
const [err, setErr] = useState<string | null>(null);
async function onSubmit(e: React.FormEvent) {
const next = sp.get("next") || "/my/rigs";
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setBusy(true);
setErr(null);
setSubmitting(true);
try {
// Post both a generic "identity" and the same value under email/username
// so the API route can accept any of them.
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include", // ensure Set-Cookie is honored everywhere
body: JSON.stringify({ identity, password }),
body: JSON.stringify({
identity: idVal,
email: idVal,
username: idVal,
password: pwVal,
}),
});
const j = await res.json().catch(() => null);
if (!res.ok) throw new Error(j?.error || "Login failed");
router.replace(nextUrl);
} catch (err) {
alert((err as Error).message);
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data?.error || "Login failed");
// Cookies (httpOnly) were set server-side; client just navigates.
router.replace(next);
} catch (e: any) {
setErr(e?.message || "Login failed");
} finally {
setBusy(false);
setSubmitting(false);
}
}
return (
<div className="max-w-md mx-auto p-6">
<h1 className="text-xl font-semibold mb-4">Sign in</h1>
<form onSubmit={onSubmit} className="space-y-3">
<div>
<label className="block text-sm mb-1">Username or Email</label>
<input
className="w-full border rounded px-3 py-2 bg-background"
value={identity}
onChange={(e) => setIdentity(e.target.value)}
<form onSubmit={onSubmit} className="space-y-4">
{err && (
<div className="text-sm rounded border border-destructive/30 bg-destructive/10 px-3 py-2 text-destructive">
{err}
</div>
)}
<div className="space-y-2">
<label className="text-sm font-medium">Email or Username</label>
<Input
autoFocus
autoComplete="username"
placeholder="you@example.com or your-username"
value={idVal}
onChange={(e) => setIdVal(e.target.value)}
required
/>
</div>
<div>
<label className="block text-sm mb-1">Password</label>
<input
<div className="space-y-2">
<label className="text-sm font-medium">Password</label>
<Input
type="password"
className="w-full border rounded px-3 py-2 bg-background"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="current-password"
value={pwVal}
onChange={(e) => setPwVal(e.target.value)}
required
/>
</div>
<button
disabled={busy}
className="px-3 py-2 rounded bg-primary text-primary-foreground disabled:opacity-50"
>
{busy ? "Signing in…" : "Sign in"}
</button>
</form>
<p className="text-sm mt-4">
Dont have an account?{" "}
<a
className="underline"
href={`/auth/sign-up?next=${encodeURIComponent(nextUrl)}`}
>
Sign up
</a>
</p>
</div>
<Button type="submit" disabled={submitting} className="w-full">
{submitting ? "Signing in…" : "Sign in"}
</Button>
{/* keep the next param visible to the client sub-component */}
<input type="hidden" name="next" value={next} />
</form>
);
}

View file

@ -1,91 +1,134 @@
// app/app/auth/sign-up/page.tsx
"use client";
// app/auth/sign-up/page.tsx
import { Suspense } from "react";
import type { Metadata } from "next";
import Link from "next/link";
import { useState } from "react";
import { useRouter } from "next/navigation";
// UI (shadcn)
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
export const metadata: Metadata = { title: "Create account" };
export default function SignUpPage() {
const r = useRouter();
const [form, setForm] = useState({ username: "", email: "", password: "", agree: false });
const [busy, setBusy] = useState(false);
const [err, setErr] = useState<string | null>(null);
const canSubmit = form.username.length >= 3 && form.password.length >= 8 && form.agree && !busy;
return (
<div className="min-h-[60vh] flex items-center justify-center px-4">
<div className="w-full max-w-md space-y-6">
<div className="text-center space-y-2">
<h1 className="text-2xl font-bold">Create account</h1>
<p className="text-sm text-muted-foreground">
Pick a username and password. Email is optional (recommended for password reset).
</p>
</div>
async function submit() {
setBusy(true); setErr(null);
<Suspense fallback={<div className="text-sm text-muted-foreground">Loading</div>}>
<SignUpClient />
</Suspense>
<p className="text-sm text-center text-muted-foreground">
Already have an account?{" "}
<Link href="/auth/sign-in" className="underline underline-offset-4 hover:opacity-80">
Sign in
</Link>
</p>
</div>
</div>
);
}
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
function SignUpClient() {
const router = useRouter();
const sp = useSearchParams();
const [username, setUsername] = useState("");
const [email, setEmail] = useState(""); // optional
const [password, setPassword] = useState("");
const [submitting, setSubmitting] = useState(false);
const [err, setErr] = useState<string | null>(null);
const next = sp.get("next") || "/my/rigs";
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setErr(null);
setSubmitting(true);
try {
const res = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: form.username.trim(),
email: form.email.trim() || undefined,
password: form.password,
username,
email: email || undefined,
password,
}),
});
const j = await res.json();
if (!res.ok) throw new Error(j?.error || "Sign up failed");
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data?.error || "Registration failed");
// Auto-login right after register
const login = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ identifier: form.email.trim() || form.username.trim(), password: form.password }),
});
const lj = await login.json();
if (!login.ok) throw new Error(lj?.error || "Auto login failed");
r.replace("/my/rigs"); // or wherever you want to land
// Registration API should already log user in (sets cookies), then redirect
router.replace(next);
} catch (e: any) {
setErr(e?.message || "Error");
setErr(e?.message || "Registration failed");
} finally {
setBusy(false);
setSubmitting(false);
}
}
return (
<div className="max-w-md mx-auto p-6 space-y-4">
<h1 className="text-2xl font-semibold">Create Account</h1>
<form onSubmit={onSubmit} className="space-y-4">
{err && (
<div className="text-sm rounded border border-destructive/30 bg-destructive/10 px-3 py-2 text-destructive">
{err}
</div>
)}
<label className="block">
<div className="text-sm mb-1">Username *</div>
<input className="w-full border rounded px-3 py-2" value={form.username}
onChange={e=>setForm({...form, username:e.target.value})} />
<p className="text-xs text-muted-foreground mt-1">Used for login if you dont provide an email.</p>
<div className="space-y-2">
<label className="text-sm font-medium">Username</label>
<Input
autoFocus
autoComplete="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">
Email <span className="text-muted-foreground font-normal">(optional)</span>
</label>
<label className="block">
<div className="text-sm mb-1">Email (optional)</div>
<input type="email" className="w-full border rounded px-3 py-2" value={form.email}
onChange={e=>setForm({...form, email:e.target.value})} />
<p className="text-xs text-muted-foreground mt-1">
If you skip this, we cant reset your password later.
<Input
type="email"
autoComplete="email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
Without an email, we cant reset your password if you lose it.
</p>
</label>
<label className="block">
<div className="text-sm mb-1">Password *</div>
<input type="password" className="w-full border rounded px-3 py-2" value={form.password}
onChange={e=>setForm({...form, password:e.target.value})} />
</label>
<label className="flex items-start gap-2 text-sm">
<input type="checkbox" className="mt-1" checked={form.agree}
onChange={e=>setForm({...form, agree:e.target.checked})} />
<span>I understand that without an email on file, my account cant be recovered if I lose the password.</span>
</label>
{err && <div className="text-sm text-red-600">{err}</div>}
<button disabled={!canSubmit} onClick={submit}
className="px-3 py-2 rounded bg-black text-white disabled:opacity-50">
{busy ? "Creating..." : "Create account"}
</button>
<div className="text-sm">
Already have an account?{" "}
<a href="/auth/sign-in" className="underline">Sign in</a>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Password</label>
<Input
type="password"
autoComplete="new-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<Button type="submit" disabled={submitting} className="w-full">
{submitting ? "Creating…" : "Create account"}
</Button>
<input type="hidden" name="next" value={next} />
</form>
);
}