Public Access
refactor(frontend): lint, one session guard, one request helper, one user context
This commit is contained in:
1 parent
111ab1e200
commit
d0ea9e900d
78 files changed
+2138
-624
No files matched your search
@@ -0,0 +1,74 @@
|
||||
import {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {request} from '../utils/api.js';
|
||||
import {SESSION_KEY, hasSession} from '../utils/auth.js';
|
||||
import {ME_URL, REQUEST_TIMEOUT_MS} from '../utils/settings.js';
|
||||
|
||||
const UserContext = createContext(null);
|
||||
// Apart from the user, so a component that only reloads or clears it does not render again on each change.
|
||||
const ActionsContext = createContext(null);
|
||||
|
||||
/**
|
||||
* GET /api/users/me for the whole app. `user` is undefined until the first answer and null without a session.
|
||||
* `reload()` loads it again and resolves to the `request()` result (not ok for a 200 without a body).
|
||||
* `updateUser(fn)` changes it locally after a save, and skips it when the user is gone by then; `setUser(null)`
|
||||
* drops it. A save that replaces settings still loads /me itself (see saveSettings), as it must be fresh.
|
||||
*/
|
||||
export function CurrentUserProvider({children}) {
|
||||
const [user, setUser] = useState(() => (hasSession() ? undefined : null));
|
||||
const latest = useRef(0);
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
const call = ++latest.current;
|
||||
// After a logout, "no user" becomes "loading" (e.g. the import waits for the favorite platform).
|
||||
setUser((current) => current ?? undefined);
|
||||
const response = await request(ME_URL(), {signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)});
|
||||
const result = {...response, ok: response.ok && response.data != null};
|
||||
if (call === latest.current) {
|
||||
// Only a 401 means "no user"; another failure keeps the user shown so far, or ends "loading".
|
||||
if (result.ok) setUser(result.data);
|
||||
else if (result.status === 401) setUser(null);
|
||||
else setUser((current) => current ?? null);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasSession()) reload();
|
||||
// A login, logout or "view as" in another tab.
|
||||
const onStorage = (event) => {
|
||||
if (event.key !== null && event.key !== SESSION_KEY) return;
|
||||
if (hasSession()) reload();
|
||||
else setUser(null);
|
||||
};
|
||||
window.addEventListener('storage', onStorage);
|
||||
return () => window.removeEventListener('storage', onStorage);
|
||||
}, [reload]);
|
||||
|
||||
const updateUser = useCallback((change) => setUser((current) => current && change(current)), []);
|
||||
const actions = useMemo(() => ({reload, setUser, updateUser}), [reload, updateUser]);
|
||||
|
||||
return (
|
||||
<ActionsContext.Provider value={actions}>
|
||||
<UserContext.Provider value={user}>{children}</UserContext.Provider>
|
||||
</ActionsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const NO_ACTIONS = {reload: async () => ({ok: false, status: 0, data: null}), setUser: () => {}, updateUser: () => {}};
|
||||
|
||||
/**
|
||||
* `{reload, setUser, updateUser}`, without the user. Outside a provider (e.g. a test of one component) they do
|
||||
* nothing.
|
||||
*/
|
||||
export function useCurrentUserActions() {
|
||||
return useContext(ActionsContext) ?? NO_ACTIONS;
|
||||
}
|
||||
|
||||
/**
|
||||
* `{user, reload, setUser, updateUser}`; outside a provider there is no user.
|
||||
*/
|
||||
export function useCurrentUserContext() {
|
||||
const user = useContext(UserContext);
|
||||
const actions = useContext(ActionsContext);
|
||||
return {user: actions ? user : null, ...(actions ?? NO_ACTIONS)};
|
||||
}
|
||||
Reference in new issue
Block a user