From 8e0186f9accc2d2b4f90ac1f60513183f4077a2e Mon Sep 17 00:00:00 2001 From: Marcello Fitton <106866560+angelplusultra@users.noreply.github.com> Date: Wed, 10 Dec 2025 12:40:34 -0800 Subject: [PATCH] Fix stale user permissions in UI by refreshing user data on app load (#4751) * add refresh user functionality * prettier * add eslint disable comment for exhaustive-deps warning in AuthContext to stop nagging about navigate func * remove unused imports and fix typo * handle unsafe parse of undefined for in-session user deleted --------- Co-authored-by: Timothy Carambat --- frontend/src/AuthContext.jsx | 38 +++++++++++++++++++++++++++++++++-- frontend/src/models/system.js | 8 ++++++++ server/endpoints/system.js | 29 ++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/frontend/src/AuthContext.jsx b/frontend/src/AuthContext.jsx index 672241f9..b31a65fc 100644 --- a/frontend/src/AuthContext.jsx +++ b/frontend/src/AuthContext.jsx @@ -1,20 +1,31 @@ -import React, { useState, createContext } from "react"; +import React, { useState, createContext, useEffect } from "react"; import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER, USER_PROMPT_INPUT_MAP, } from "@/utils/constants"; +import System from "./models/system"; +import { useNavigate } from "react-router-dom"; +import { safeJsonParse } from "@/utils/request"; export const AuthContext = createContext(null); export function AuthProvider(props) { const localUser = localStorage.getItem(AUTH_USER); const localAuthToken = localStorage.getItem(AUTH_TOKEN); const [store, setStore] = useState({ - user: localUser ? JSON.parse(localUser) : null, + user: localUser ? safeJsonParse(localUser, null) : null, authToken: localAuthToken ? localAuthToken : null, }); + const navigate = useNavigate(); + + /* NOTE: + * 1. There's no reason for these helper functions to be stateful. They could + * just be regular funcs or methods on a basic object. + * 2. These actions are not being invoked anywhere in the + * codebase, dead code. + */ const [actions] = useState({ updateUser: (user, authToken = "") => { localStorage.setItem(AUTH_USER, JSON.stringify(user)); @@ -30,6 +41,29 @@ export function AuthProvider(props) { }, }); + // On initial mount and whenever the token changes fetch a new user object + useEffect(() => { + if (store.authToken) { + System.refreshUser() + .then(({ user }) => { + localStorage.setItem(AUTH_USER, JSON.stringify(user)); + setStore((prev) => ({ + ...prev, + user, + })); + }) + .catch(() => { + localStorage.removeItem(AUTH_USER); + localStorage.removeItem(AUTH_TOKEN); + localStorage.removeItem(AUTH_TIMESTAMP); + localStorage.removeItem(USER_PROMPT_INPUT_MAP); + setStore({ user: null, authToken: null }); + navigate("/login"); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [store.authToken]); + return ( {props.children} diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js index c0c14f98..bbf6993d 100644 --- a/frontend/src/models/system.js +++ b/frontend/src/models/system.js @@ -83,6 +83,14 @@ const System = { return { valid: false, message: e.message }; }); }, + refreshUser: () => { + return fetch(`${API_BASE}/system/refresh-user`, { headers: baseHeaders() }) + .then((res) => res.json()) + .then((data) => data) + .catch((e) => { + console.log(e); + }); + }, recoverAccount: async function (username, recoveryCodes) { return await fetch(`${API_BASE}/system/recover-account`, { method: "POST", diff --git a/server/endpoints/system.js b/server/endpoints/system.js index f4e9099a..dd6198c8 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -114,6 +114,35 @@ function systemEndpoints(app) { } ); + app.get("/system/refresh-user", [validatedRequest], async (req, res) => { + try { + if (multiUserMode(res)) { + const user = await userFromSession(req, res); + if (!user || user.suspended) { + res.sendStatus(403).end(); + return; + } + + res.status(200).json({ + valid: true, + user: User.filterFields(user), + message: null, + }); + } else { + res.status(500).json({ + success: false, + message: "Multi-User Mode is not enabled.", + }); + } + } catch (e) { + console.log(e); + res.status(400).json({ + success: false, + message: "Failed to retrieve the user from session.", + }); + } + }); + app.post("/request-token", async (request, response) => { try { const bcrypt = require("bcrypt");