Fix Stale User Session with Proper fetch Error Handling (#4770)
* 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 * Refactor refreshUser function to handle errors and return structured response. Update AuthProvider to manage user data based on success status. * Remove console error logging from promise catch in System model for cleaner error handling. * change status from 404 to 400 and valid to success * Refactor error handling in AuthProvider's refreshUser logic to remove redundant catch block and streamline user session management on failure. * prettier * reorder clauses - return errors * refactor account for all user modes dev build --------- Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
This commit is contained in:
parent
c76b0708c3
commit
8aa78c2b75
2
.github/workflows/dev-build.yaml
vendored
2
.github/workflows/dev-build.yaml
vendored
@ -6,7 +6,7 @@ concurrency:
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['migrate-to-bcryptjs'] # put your current branch to create a build. Core team only.
|
||||
branches: ['bug-user-session-stale'] # put your current branch to create a build. Core team only.
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'cloud-deployments/*'
|
||||
|
||||
@ -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,37 @@ export function AuthProvider(props) {
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
* On initial mount and whenever the token changes, fetch a new user object
|
||||
* If the user is suspended, (success === false and data === null) logout the user and redirect to the login page
|
||||
* If success is true and data is not null, update the user object in the store (multi-user mode only)
|
||||
* If success is true and data is null, do nothing (single-user mode only) with or without password protection
|
||||
*/
|
||||
useEffect(() => {
|
||||
async function refreshUser() {
|
||||
const { success, user: refreshedUser } = await System.refreshUser();
|
||||
if (success && refreshedUser === null) return;
|
||||
|
||||
if (!success) {
|
||||
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");
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(AUTH_USER, JSON.stringify(refreshedUser));
|
||||
setStore((prev) => ({
|
||||
...prev,
|
||||
user: refreshedUser,
|
||||
}));
|
||||
}
|
||||
if (store.authToken) refreshUser();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [store.authToken]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ store, actions }}>
|
||||
{props.children}
|
||||
|
||||
@ -83,6 +83,22 @@ const System = {
|
||||
return { valid: false, message: e.message };
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Refreshes the user object from the session.
|
||||
* @returns {Promise<{success: boolean, user: Object | null, message: string | null}>}
|
||||
*/
|
||||
refreshUser: () => {
|
||||
return fetch(`${API_BASE}/system/refresh-user`, {
|
||||
headers: baseHeaders(),
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error("Could not refresh user.");
|
||||
return res.json();
|
||||
})
|
||||
.catch((e) => {
|
||||
return { success: false, user: null, message: e.message };
|
||||
});
|
||||
},
|
||||
recoverAccount: async function (username, recoveryCodes) {
|
||||
return await fetch(`${API_BASE}/system/recover-account`, {
|
||||
method: "POST",
|
||||
|
||||
@ -114,6 +114,52 @@ function systemEndpoints(app) {
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Refreshes the user object from the session from a provided token.
|
||||
* This does not refresh the token itself - if that is expired or invalid, the user will be logged out.
|
||||
* This simply keeps the user object in sync with the database over the course of the session.
|
||||
* @returns {Promise<{success: boolean, user: Object | null, message: string | null}>}
|
||||
*/
|
||||
app.get(
|
||||
"/system/refresh-user",
|
||||
[validatedRequest],
|
||||
async (request, response) => {
|
||||
try {
|
||||
if (!multiUserMode(response))
|
||||
return response
|
||||
.status(200)
|
||||
.json({ success: true, user: null, message: null });
|
||||
|
||||
const user = await userFromSession(request, response);
|
||||
if (!user)
|
||||
return response.status(200).json({
|
||||
success: false,
|
||||
user: null,
|
||||
message: "Session expired or invalid.",
|
||||
});
|
||||
|
||||
if (user.suspended)
|
||||
return response.status(200).json({
|
||||
success: false,
|
||||
user: null,
|
||||
message: "User is suspended.",
|
||||
});
|
||||
|
||||
return response.status(200).json({
|
||||
success: true,
|
||||
user: User.filterFields(user),
|
||||
message: null,
|
||||
});
|
||||
} catch (e) {
|
||||
return response.status(500).json({
|
||||
success: false,
|
||||
user: null,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.post("/request-token", async (request, response) => {
|
||||
try {
|
||||
const bcrypt = require("bcryptjs");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user