From 6ea6080b11c6968e67631d846ad0409d99fcd774 Mon Sep 17 00:00:00 2001 From: makearmy Date: Tue, 30 Sep 2025 13:02:57 -0400 Subject: [PATCH] account page render fix --- app/auth/sign-in/page.tsx | 26 ++-- app/auth/sign-in/sign-in.tsx | 206 ++++++++++++--------------- app/portal/account/AccountClient.tsx | 136 +++++------------- app/portal/account/page.tsx | 16 ++- 4 files changed, 161 insertions(+), 223 deletions(-) diff --git a/app/auth/sign-in/page.tsx b/app/auth/sign-in/page.tsx index 016c1157..2a49065e 100644 --- a/app/auth/sign-in/page.tsx +++ b/app/auth/sign-in/page.tsx @@ -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> } +) { + 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 ( - Loading…}> - - - ); + 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 ; } diff --git a/app/auth/sign-in/sign-in.tsx b/app/auth/sign-in/sign-in.tsx index 24587fe5..77f05a02 100644 --- a/app/auth/sign-in/sign-in.tsx +++ b/app/auth/sign-in/sign-in.tsx @@ -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(null); + const [password, setPassword] = useState(""); + const [showPassword, setShow] = useState(false); + const [loading, setLoading] = useState(false); + const [err, setErr] = useState(null); - const onSubmit = useCallback( - async (e: React.FormEvent) => { - 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) => { + 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 ( -
- {reauth && ( -
-
Please sign in again
-
- For security, we need to confirm it’s you before accessing account settings. +
+
+

+ {reauth ? "Re-authenticate" : "Sign In"} +

+

+ {reauth + ? "Please sign in again to continue." + : <>Use your email or username with your password.} +

+ +
+
+ + setIdentifier(e.currentTarget.value)} + required + />
- )} -

Sign In

-

- Use your email or username with your password. -

- - -
- - setIdentifier(e.currentTarget.value)} - required - /> -
- -
-
- - -
- setPassword(e.currentTarget.value)} - required - /> -
- - {err && ( -
- {err} +
+
+ + +
+ setPassword(e.currentTarget.value)} + required + />
- )} - - + {err && ( +
+ {err} +
+ )} -
- New here?{" "} - Create an account -
-
+ + + + {!reauth && ( +
+ New here?{" "} + Create an account +
+ )} +
); } diff --git a/app/portal/account/AccountClient.tsx b/app/portal/account/AccountClient.tsx index 5364e1a8..e9d19b28 100644 --- a/app/portal/account/AccountClient.tsx +++ b/app/portal/account/AccountClient.tsx @@ -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(null); + const [err, setErr] = useState(null); const [loading, setLoading] = useState(true); - const [needReauth, setNeedReauth] = useState(false); - const [error, setError] = useState(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
Loading account…
; - } - - if (needReauth) { - return ( -
-
-

Confirm it’s you

-

- For security, please sign in again before changing account details. -

- - Re-authenticate - -
-
- ); - } - - if (error) { - return ( -
-
-
Error: {error}
- -
-
- ); - } + })(); + return () => { alive = false; }; + }, []); + if (loading) return
Loading…
; + if (err) return
Error: {err}
; if (!me) return null; return ( -
- {/* View-only header */} -
-

Account

-

- Username: {me.username}{" "} - (usernames can’t be changed) -

-
+
+
+

Account

+
+
Username
{me.username}
+
First Name
{me.first_name || "—"}
+
Last Name
{me.last_name || "—"}
+
Email
{me.email || "—"}
+
Location
{me.location || "—"}
+
+

Usernames can’t be changed.

+
- {/* 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 */} -
-

Profile

-
-
First name: {me.first_name || "—"}
-
Last name: {me.last_name || "—"}
-
Email: {me.email || "—"}
-
Location: {me.location || "—"}
-
-
-
+ {/* 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. */} +
); } diff --git a/app/portal/account/page.tsx b/app/portal/account/page.tsx index 030347d8..5760385b 100644 --- a/app/portal/account/page.tsx +++ b/app/portal/account/page.tsx @@ -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 ; +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 ; }