account page render fix

This commit is contained in:
makearmy 2025-09-30 13:02:57 -04:00
parent 996ebe4757
commit 6ea6080b11
4 changed files with 161 additions and 223 deletions

View file

@ -1,15 +1,21 @@
// app/auth/sign-in/page.tsx
import { Suspense } from "react";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import SignIn from "./sign-in";
// Ensure this page renders at runtime (avoids SSG trying to pre-render a client-only page)
export const dynamic = "force-dynamic";
export default async function SignInPage(
props: { searchParams: Promise<Record<string, string | string[] | undefined>> }
) {
const at = (await cookies()).get("ma_at")?.value;
if (at) redirect("/portal");
export default function SignInPage() {
// Wrap the client component (which uses useSearchParams) in Suspense
return (
<Suspense fallback={<div className="p-6">Loading</div>}>
<SignIn />
</Suspense>
);
const sp = await props.searchParams;
const nextParam = Array.isArray(sp.next) ? sp.next[0] : sp.next;
const nextPath = nextParam && nextParam.startsWith("/") ? nextParam : "/portal";
const reauthParam = Array.isArray(sp.reauth) ? sp.reauth[0] : sp.reauth;
const forceParam = Array.isArray(sp.force) ? sp.force[0] : sp.force;
const reauth = reauthParam === "1" || forceParam === "1";
return <SignIn nextPath={nextPath} reauth={reauth} />;
}

View file

@ -2,130 +2,114 @@
"use client";
import { useState, useCallback } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useRouter } from "next/navigation";
type Props = { nextPath?: string };
type Props = { nextPath?: string; reauth?: boolean };
export default function SignIn({ nextPath = "/portal" }: Props) {
export default function SignIn({ nextPath = "/portal", reauth = false }: Props) {
const router = useRouter();
const sp = useSearchParams();
// Respect reauth/force flags from query
const reauth = sp.get("reauth") === "1" || sp.get("force") === "1";
const next = sp.get("next") || nextPath;
const [identifier, setIdentifier] = useState(""); // email OR username
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [err, setErr] = useState<string | null>(null);
const [password, setPassword] = useState("");
const [showPassword, setShow] = useState(false);
const [loading, setLoading] = useState(false);
const [err, setErr] = useState<string | null>(null);
const onSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setErr(null);
setLoading(true);
try {
const res = await fetch("/api/auth/login", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ identifier, password }),
});
const txt = await res.text();
let j: any = null;
try { j = txt ? JSON.parse(txt) : null; } catch {}
if (!res.ok) {
const message = j?.error || j?.message || `Sign-in failed (${res.status})`;
throw new Error(message);
}
// Success → go to intended destination (account page in reauth flow)
router.replace(next);
router.refresh();
} catch (e: any) {
setErr(e?.message || "Unable to sign in.");
} finally {
setLoading(false);
const onSubmit = useCallback(async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setErr(null);
setLoading(true);
try {
const res = await fetch("/api/auth/login", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ identifier, password }),
});
const txt = await res.text();
let j: any = null; try { j = txt ? JSON.parse(txt) : null; } catch {}
if (!res.ok) {
throw new Error(j?.error || j?.message || `Sign-in failed (${res.status})`);
}
},
[identifier, password, next, router]
);
// Land where caller requested
router.replace(nextPath);
router.refresh();
} catch (e: any) {
setErr(e?.message || "Unable to sign in.");
} finally {
setLoading(false);
}
}, [identifier, password, nextPath, router]);
return (
<div className="mx-auto max-w-md rounded-lg border p-6">
{reauth && (
<div className="mb-4 rounded-md border p-3 bg-card">
<div className="font-medium">Please sign in again</div>
<div className="text-sm text-muted-foreground">
For security, we need to confirm its you before accessing account settings.
<div className="mx-auto max-w-md rounded-lg border p-6 space-y-4">
<div>
<h1 className="mb-1 text-2xl font-semibold">
{reauth ? "Re-authenticate" : "Sign In"}
</h1>
<p className="text-sm opacity-70">
{reauth
? "Please sign in again to continue."
: <>Use your email <em>or</em> username with your password.</>}
</p>
</div>
<form className="space-y-4" onSubmit={onSubmit}>
<div className="space-y-1">
<label className="text-sm font-medium">Email or Username</label>
<input
type="text"
autoComplete="username"
className="w-full rounded-md border px-3 py-2"
placeholder="you@example.com or your-handle"
value={identifier}
onChange={(e) => setIdentifier(e.currentTarget.value)}
required
/>
</div>
)}
<h1 className="mb-1 text-2xl font-semibold">Sign In</h1>
<p className="mb-6 text-sm opacity-70">
Use your email <em>or</em> username with your password.
</p>
<form className="space-y-4" onSubmit={onSubmit}>
<div className="space-y-1">
<label className="text-sm font-medium">Email or Username</label>
<input
type="text"
autoComplete="username"
className="w-full rounded-md border px-3 py-2"
placeholder="you@example.com or your-handle"
value={identifier}
onChange={(e) => setIdentifier(e.currentTarget.value)}
required
/>
</div>
<div className="space-y-1">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Password</label>
<button
type="button"
className="text-xs opacity-70 hover:opacity-100"
onClick={() => setShowPassword((s) => !s)}
>
{showPassword ? "Hide" : "Show"}
</button>
</div>
<input
type={showPassword ? "text" : "password"}
autoComplete="current-password"
className="w-full rounded-md border px-3 py-2"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.currentTarget.value)}
required
/>
</div>
{err && (
<div className="rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700">
{err}
<div className="space-y-1">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Password</label>
<button
type="button"
className="text-xs opacity-70 hover:opacity-100"
onClick={() => setShow((s) => !s)}
>
{showPassword ? "Hide" : "Show"}
</button>
</div>
<input
type={showPassword ? "text" : "password"}
autoComplete="current-password"
className="w-full rounded-md border px-3 py-2"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.currentTarget.value)}
required
/>
</div>
)}
<button
type="submit"
disabled={loading}
className="w-full rounded-md bg-black px-3 py-2 text-white disabled:opacity-60"
>
{loading ? "Signing in…" : "Sign In"}
</button>
</form>
{err && (
<div className="rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700">
{err}
</div>
)}
<div className="mt-4 text-center text-sm">
<span className="opacity-70">New here?</span>{" "}
<a className="underline" href={"/auth/sign-up"}>Create an account</a>
</div>
</div>
<button
type="submit"
disabled={loading}
className="w-full rounded-md bg-black px-3 py-2 text-white disabled:opacity-60"
>
{loading ? "Signing in…" : reauth ? "Re-authenticate" : "Sign In"}
</button>
</form>
{!reauth && (
<div className="mt-2 text-center text-sm">
<span className="opacity-70">New here?</span>{" "}
<a className="underline" href="/auth/sign-up">Create an account</a>
</div>
)}
</div>
);
}

View file

@ -1,8 +1,7 @@
// app/portal/account/AccountClient.tsx
// app/portal/account/AccountPanel.tsx
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
type Me = {
id: string;
@ -11,112 +10,53 @@ type Me = {
last_name?: string | null;
email?: string | null;
location?: string | null;
avatar?: { id: string; filename_download?: string } | null;
avatar?: { id: string; filename_download: string } | null;
};
export default function AccountClient() {
export default function AccountPanel() {
const [me, setMe] = useState<Me | null>(null);
const [err, setErr] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [needReauth, setNeedReauth] = useState(false);
const [error, setError] = useState<string | null>(null);
async function load() {
setLoading(true);
setError(null);
setNeedReauth(false);
try {
const res = await fetch("/api/account", {
credentials: "include",
cache: "no-store",
});
if (res.status === 401 || res.status === 403) {
setNeedReauth(true);
setMe(null);
return;
useEffect(() => {
let alive = true;
(async () => {
try {
setErr(null);
const r = await fetch("/api/account", { credentials: "include", cache: "no-store" });
if (!r.ok) throw new Error(`Load failed (${r.status})`);
const j = await r.json();
if (alive) setMe(j);
} catch (e: any) {
if (alive) setErr(e?.message || "Failed to load account");
} finally {
if (alive) setLoading(false);
}
if (!res.ok) {
const j = await res.json().catch(() => ({}));
throw new Error(j?.error || `Failed: ${res.status}`);
}
const j = await res.json();
setMe(j as Me);
} catch (e: any) {
setError(e?.message || "Failed to load account");
} finally {
setLoading(false);
}
}
useEffect(() => { load(); }, []);
if (loading) {
return <div className="p-6 text-sm opacity-70">Loading account</div>;
}
if (needReauth) {
return (
<div className="p-6 max-w-xl">
<div className="rounded-md border p-4">
<h2 className="text-lg font-semibold mb-2">Confirm its you</h2>
<p className="text-sm text-muted-foreground mb-3">
For security, please sign in again before changing account details.
</p>
<Link
href={`/auth/sign-in?reauth=1&force=1&next=${encodeURIComponent("/portal/account")}`}
className="inline-block px-3 py-2 rounded-md border bg-accent text-background"
>
Re-authenticate
</Link>
</div>
</div>
);
}
if (error) {
return (
<div className="p-6 max-w-xl">
<div className="rounded-md border p-4">
<div className="text-red-600 mb-2">Error: {error}</div>
<button
onClick={load}
className="px-3 py-2 rounded-md border hover:bg-muted text-sm"
>
Retry
</button>
</div>
</div>
);
}
})();
return () => { alive = false; };
}, []);
if (loading) return <div className="rounded-md border p-6 text-sm opacity-70">Loading</div>;
if (err) return <div className="rounded-md border p-6 text-red-600">Error: {err}</div>;
if (!me) return null;
return (
<div className="p-6 max-w-2xl space-y-6">
{/* View-only header */}
<section className="rounded-md border p-4">
<h1 className="text-xl font-semibold mb-2">Account</h1>
<p className="text-sm">
<span className="font-medium">Username:</span> {me.username}{" "}
<span className="text-muted-foreground">(usernames cant be changed)</span>
</p>
</section>
<div className="space-y-6">
<div className="rounded-md border p-4">
<h2 className="text-lg font-semibold mb-2">Account</h2>
<div className="grid sm:grid-cols-2 gap-3 text-sm">
<div><div className="opacity-60">Username</div><div className="font-medium">{me.username}</div></div>
<div><div className="opacity-60">First Name</div><div>{me.first_name || "—"}</div></div>
<div><div className="opacity-60">Last Name</div><div>{me.last_name || "—"}</div></div>
<div><div className="opacity-60">Email</div><div>{me.email || "—"}</div></div>
<div><div className="opacity-60">Location</div><div>{me.location || "—"}</div></div>
</div>
<p className="mt-3 text-xs opacity-70">Usernames cant be changed.</p>
</div>
{/* Your existing edit forms can sit here, wired to:
- PATCH /api/account for first_name, last_name, email (optional), location
- POST /api/account/password for password change (asks current password in the form)
- POST /api/account/avatar for avatar upload (to your folder)
None of these fire automatically; only on submit. */}
{/* Example placeholder showing current profile fields */}
<section className="rounded-md border p-4">
<h2 className="text-lg font-semibold mb-3">Profile</h2>
<div className="grid gap-2 text-sm">
<div><span className="text-muted-foreground">First name:</span> {me.first_name || "—"}</div>
<div><span className="text-muted-foreground">Last name:</span> {me.last_name || "—"}</div>
<div><span className="text-muted-foreground">Email:</span> {me.email || "—"}</div>
<div><span className="text-muted-foreground">Location:</span> {me.location || "—"}</div>
</div>
</section>
</div>
{/* Place your edit forms here; they should only call the update APIs on submit,
never during initial render. For password/avatar we can prompt for password
inline or send to /auth/sign-in?reauth=1&next=/portal/account#security. */}
</div>
);
}

View file

@ -1,8 +1,16 @@
// app/portal/account/page.tsx
export const dynamic = "force-dynamic"; // don't cache this page
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import AccountPanel from "./AccountPanel";
import AccountClient from "./AccountClient";
export const dynamic = "force-dynamic";
export default function Page() {
return <AccountClient />;
export default async function AccountPage() {
const at = (await cookies()).get("ma_at")?.value;
if (!at) {
redirect(`/auth/sign-in?next=${encodeURIComponent("/portal/account")}`);
}
// No reauth gating here; the panel will fetch and render profile,
// and only ask for reauth when user submits sensitive changes.
return <AccountPanel />;
}