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 <rambat1010@gmail.com>
This commit is contained in:
Marcello Fitton 2025-12-10 12:40:34 -08:00 committed by GitHub
parent b620ca40ce
commit 8e0186f9ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 73 additions and 2 deletions

View File

@ -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 (
<AuthContext.Provider value={{ store, actions }}>
{props.children}

View File

@ -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",

View File

@ -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");