From 11bc584dec6891e31484d640a1147025ca15c70f Mon Sep 17 00:00:00 2001 From: makearmy Date: Tue, 30 Sep 2025 10:11:13 -0400 Subject: [PATCH] delay reauth to edit --- app/portal/account/AccountClient.tsx | 240 ++++++++++++--------------- app/portal/account/page.tsx | 53 +++--- 2 files changed, 139 insertions(+), 154 deletions(-) diff --git a/app/portal/account/AccountClient.tsx b/app/portal/account/AccountClient.tsx index e772787d..b731d6a2 100644 --- a/app/portal/account/AccountClient.tsx +++ b/app/portal/account/AccountClient.tsx @@ -10,35 +10,24 @@ type Me = { last_name?: string | null; email?: string | null; location?: string | null; - avatar?: { id: string; filename_download?: string; title?: string } | string | null; + avatar?: { id: string; filename_download?: string } | string | null; }; -export default function AccountClient({ initialUser }: { initialUser: Me }) { - const [user, setUser] = useState(initialUser); - const [saving, setSaving] = useState(false); +export default function AccountClient({ me }: { me: Me }) { + // local edit state; no network until you press Save + const [first, setFirst] = useState(me.first_name ?? ""); + const [last, setLast] = useState(me.last_name ?? ""); + const [email, setEmail] = useState(me.email ?? ""); + const [location, setLocation] = useState(me.location ?? ""); + const [busy, setBusy] = useState(false); const [msg, setMsg] = useState(null); - const [firstName, setFirst] = useState(user.first_name ?? ""); - const [lastName, setLast] = useState(user.last_name ?? ""); - const [email, setEmail] = useState(user.email ?? ""); - const [location, setLocation] = useState(user.location ?? ""); - - const [pwCurr, setPwCurr] = useState(""); - const [pwNew, setPwNew] = useState(""); - const [pwMsg, setPwMsg] = useState(null); - const [uploading, setUploading] = useState(false); - - const avatarThumb = (() => { - // Directus file thumbnails: /assets/{id}?download=... if you expose assets - const id = typeof user.avatar === "object" ? user.avatar?.id : user.avatar; - if (!id) return null; - const base = (process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, ""); - return `${base}/assets/${id}?width=96&height=96&fit=cover`; - })(); + const avatarId = + typeof me.avatar === "string" ? me.avatar : (me.avatar as any)?.id ?? null; async function saveProfile(e: React.FormEvent) { e.preventDefault(); - setSaving(true); + setBusy(true); setMsg(null); try { const res = await fetch("/api/account", { @@ -46,137 +35,118 @@ export default function AccountClient({ initialUser }: { initialUser: Me }) { headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ - first_name: firstName.trim(), - last_name: lastName.trim(), - email: email.trim(), - location: location.trim(), + first_name: first || null, + last_name: last || null, + email: email || null, // email optional per your model + location: location || null, }), }); const j = await res.json().catch(() => ({})); if (!res.ok) throw new Error(j?.error || "Update failed"); - setUser((u) => ({ ...u, ...j })); - setMsg("Profile updated."); - } catch (err: any) { - setMsg(err?.message || "Failed to update."); + setMsg("Saved!"); + } catch (e: any) { + const m = String(e?.message || "Update failed"); + setMsg(m.includes("sign in") ? "Session expired — please sign in again." : m); } finally { - setSaving(false); - } - } - - async function onAvatarChange(e: React.ChangeEvent) { - const file = e.target.files?.[0]; - if (!file) return; - setUploading(true); - setMsg(null); - try { - const fd = new FormData(); - fd.append("file", file, file.name); - const res = await fetch("/api/account/avatar", { method: "POST", body: fd, credentials: "include" }); - const j = await res.json().catch(() => ({})); - if (!res.ok) throw new Error(j?.error || "Upload failed"); - // Refresh our user record - setUser((u) => ({ ...u, avatar: j.avatar })); - setMsg("Avatar updated."); - } catch (err: any) { - setMsg(err?.message || "Failed to upload avatar."); - } finally { - setUploading(false); - e.currentTarget.value = ""; - } - } - - async function changePassword(e: React.FormEvent) { - e.preventDefault(); - setPwMsg(null); - try { - const res = await fetch("/api/account/password", { - method: "POST", - headers: { "Content-Type": "application/json" }, - credentials: "include", - body: JSON.stringify({ current_password: pwCurr, new_password: pwNew }), - }); - const j = await res.json().catch(() => ({})); - if (!res.ok) throw new Error(j?.error || "Password change failed"); - setPwMsg("Password updated."); - setPwCurr(""); - setPwNew(""); - } catch (err: any) { - setPwMsg(err?.message || "Failed to update password."); + setBusy(false); } } return ( -
-
-

Account

+
+
+

Profile

-
- -
- -
-

Usernames can’t be changed.

+
+
+ + +

+ Usernames can’t be changed. +

-
-
-
- - setFirst(e.target.value)} /> +
+
+ {avatarId ? ( + // You already serve files via Directus; swap URL builder if needed + Avatar + ) : ( + No avatar + )}
-
- - setLast(e.target.value)} /> -
-
-
- - setEmail(e.target.value)} /> -
-
- - setLocation(e.target.value)} /> -
- -
-
- {avatarThumb ? avatar : null} -
-
- - - {uploading ?
Uploading…
: null} -
-
- -
- - {msg ? {msg} : null} + +
+
+ +
+
+ + setFirst(e.target.value)} /> +
+
+ + setLast(e.target.value)} /> +
+
+ + setEmail(e.target.value)} /> +
+
+ + setLocation(e.target.value)} /> +
+ +
+ + {msg && {msg}}
-
-

Change Password

-
-
- - setPwCurr(e.target.value)} /> -
-
- - setPwNew(e.target.value)} /> -
-
- - {pwMsg ? {pwMsg} : null} -
-
-

- If this fails, enable password updates for your role or use the email reset flow. -

+
+

Change Password

+ {/* render your ConfirmIdentity + password form here; nothing fires until submit */}
); diff --git a/app/portal/account/page.tsx b/app/portal/account/page.tsx index 09696b6f..9cd048ed 100644 --- a/app/portal/account/page.tsx +++ b/app/portal/account/page.tsx @@ -2,9 +2,7 @@ import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import { dxGET } from "@/lib/directus"; -import AccountClient from "./AccountClient"; - -export const dynamic = "force-dynamic"; +import AccountClient from "./AccountClient"; // client component below type Me = { id: string; @@ -13,29 +11,46 @@ type Me = { last_name?: string | null; email?: string | null; location?: string | null; - avatar?: { id: string; filename_download?: string; title?: string } | string | null; + avatar?: { id: string; filename_download?: string } | string | null; }; +// Next 15 cookies() is async export default async function Page() { const jar = await cookies(); const token = jar.get("ma_at")?.value; - if (!token) redirect("/auth/sign-in?next=/portal/account"); + if (!token) { + redirect(`/auth/sign-in?next=${encodeURIComponent("/portal/account")}`); + } const bearer = `Bearer ${token}`; - const fields = encodeURIComponent([ - "id", - "username", - "first_name", - "last_name", - "email", - "location", - "avatar.id", - "avatar.filename_download", - "avatar.title", - ].join(",")); + // READ-ONLY fields only; no password calls here, ever. + const fields = + "id,username,first_name,last_name,email,location,avatar.id,avatar.filename_download"; - const me = await dxGET<{ data: Me }>(`/users/me?fields=${fields}`, bearer); - const user: Me = (me as any)?.data ?? me; + let me: Me | null = null; + let loadError: string | null = null; - return ; + try { + const res = await dxGET(`/users/me?fields=${encodeURIComponent(fields)}`, bearer); + me = (res?.data ?? res) as Me; + } catch (e: any) { + // If token is stale, push to sign-in; otherwise show a friendly message + const msg = String(e?.message || ""); + if (/401|403|unauth|expired|credential/i.test(msg)) { + redirect(`/auth/sign-in?next=${encodeURIComponent("/portal/account")}`); + } else { + loadError = "Couldn’t load your profile. Please try again."; + } + } + + return ( +
+

Account

+ {loadError ? ( +
{loadError}
+ ) : ( + + )} +
+ ); }