feat: returns queried user alongside refreshed token (#2813)

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
This commit is contained in:
Jacob Fletcher
2023-07-17 09:35:34 -04:00
committed by GitHub
parent 7927dd485a
commit 2fc03f196e
16 changed files with 350 additions and 112 deletions

View File

@@ -3,6 +3,7 @@ import jwtDecode from 'jwt-decode';
import { useHistory, useLocation } from 'react-router-dom';
import { useModal } from '@faceless-ui/modal';
import { useTranslation } from 'react-i18next';
import { toast } from 'react-toastify';
import { Permissions, User } from '../../../../auth/types';
import { useConfig } from '../Config';
import { requests } from '../../../api';
@@ -44,29 +45,56 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const id = user?.id;
const refreshCookie = useCallback(() => {
const refreshCookie = useCallback((forceRefresh?: boolean) => {
const now = Math.round((new Date()).getTime() / 1000);
const remainingTime = (exp as number || 0) - now;
if (exp && remainingTime < 120) {
if (forceRefresh || (exp && remainingTime < 120)) {
setTimeout(async () => {
const request = await requests.post(`${serverURL}${api}/${userSlug}/refresh-token`, {
headers: {
'Accept-Language': i18n.language,
},
});
try {
const request = await requests.post(`${serverURL}${api}/${userSlug}/refresh-token`, {
headers: {
'Accept-Language': i18n.language,
},
});
if (request.status === 200) {
const json = await request.json();
setUser(json.user);
} else {
setUser(null);
push(`${admin}${logoutInactivityRoute}?redirect=${encodeURIComponent(window.location.pathname)}`);
if (request.status === 200) {
const json = await request.json();
setUser(json.user);
} else {
setUser(null);
push(`${admin}${logoutInactivityRoute}?redirect=${encodeURIComponent(window.location.pathname)}`);
}
} catch (e) {
toast.error(e.message);
}
}, 1000);
}
}, [exp, serverURL, api, userSlug, push, admin, logoutInactivityRoute, i18n]);
const refreshCookieAsync = useCallback(async (skipSetUser?: boolean): Promise<User> => {
try {
const request = await requests.post(`${serverURL}${api}/${userSlug}/refresh-token`, {
headers: {
'Accept-Language': i18n.language,
},
});
if (request.status === 200) {
const json = await request.json();
if (!skipSetUser) setUser(json.user);
return json.user;
}
setUser(null);
push(`${admin}${logoutInactivityRoute}`);
return null;
} catch (e) {
toast.error(`Refreshing token failed: ${e.message}`);
return null;
}
}, [serverURL, api, userSlug, push, admin, logoutInactivityRoute, i18n]);
const setToken = useCallback((token: string) => {
const decoded = jwtDecode<User>(token);
setUser(decoded);
@@ -80,37 +108,47 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}, [serverURL, api, userSlug]);
const refreshPermissions = useCallback(async () => {
const request = await requests.get(`${serverURL}${api}/access`, {
headers: {
'Accept-Language': i18n.language,
},
});
if (request.status === 200) {
const json: Permissions = await request.json();
setPermissions(json);
} else {
throw new Error(`Fetching permissions failed with status code ${request.status}`);
}
}, [serverURL, api, i18n]);
// On mount, get user and set
useEffect(() => {
const fetchMe = async () => {
const request = await requests.get(`${serverURL}${api}/${userSlug}/me`, {
try {
const request = await requests.get(`${serverURL}${api}/access`, {
headers: {
'Accept-Language': i18n.language,
},
});
if (request.status === 200) {
const json = await request.json();
const json: Permissions = await request.json();
setPermissions(json);
} else {
throw new Error(`Fetching permissions failed with status code ${request.status}`);
}
} catch (e) {
toast.error(`Refreshing permissions failed: ${e.message}`);
}
}, [serverURL, api, i18n]);
setUser(json?.user || null);
// On mount, get user and set
useEffect(() => {
const fetchMe = async () => {
try {
const request = await requests.get(`${serverURL}${api}/${userSlug}/me`, {
headers: {
'Accept-Language': i18n.language,
},
});
if (json?.token) {
setToken(json.token);
if (request.status === 200) {
const json = await request.json();
if (json?.user) {
setUser(json.user);
} else if (json?.token) {
setToken(json.token);
} else {
setUser(null);
}
}
} catch (e) {
toast.error(`Fetching user failed: ${e.message}`);
}
};
@@ -172,8 +210,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
return (
<Context.Provider value={{
user,
setUser,
logOut,
refreshCookie,
refreshCookieAsync,
refreshPermissions,
permissions,
setToken,

View File

@@ -2,8 +2,10 @@ import { User, Permissions } from '../../../../auth/types';
export type AuthContext<T = User> = {
user?: T | null
setUser: (user: T) => void
logOut: () => void
refreshCookie: () => void
refreshCookie: (forceRefresh?: boolean) => void
refreshCookieAsync: () => Promise<User>
setToken: (token: string) => void
token?: string
refreshPermissions: () => Promise<void>

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config';
@@ -22,6 +22,7 @@ import Label from '../../forms/Label';
import type { Translation } from '../../../../translations/type';
import { LoadingOverlayToggle } from '../../elements/Loading';
import { formatDate } from '../../../utilities/formatDate';
import { useAuth } from '../../utilities/Auth';
import './index.scss';
@@ -37,7 +38,7 @@ const DefaultAccount: React.FC<Props> = (props) => {
initialState,
isLoading,
action,
onSave,
onSave: onSaveFromProps,
} = props;
const {
@@ -51,6 +52,7 @@ const DefaultAccount: React.FC<Props> = (props) => {
auth,
} = collection;
const { refreshCookieAsync } = useAuth();
const { admin: { dateFormat }, routes: { admin } } = useConfig();
const { t, i18n } = useTranslation('authentication');
@@ -58,6 +60,13 @@ const DefaultAccount: React.FC<Props> = (props) => {
{ label: (resource as Translation).general.thisLanguage, value: language }
));
const onSave = useCallback(async () => {
await refreshCookieAsync();
if (typeof onSaveFromProps === 'function') {
onSaveFromProps();
}
}, [onSaveFromProps, refreshCookieAsync]);
const classes = [
baseClass,
].filter(Boolean).join(' ');
@@ -69,7 +78,6 @@ const DefaultAccount: React.FC<Props> = (props) => {
show={isLoading}
type="withoutNav"
/>
<div className={classes}>
{!isLoading && (
<OperationContext.Provider value="update">
@@ -201,7 +209,6 @@ const DefaultAccount: React.FC<Props> = (props) => {
)}
</React.Fragment>
)}
</ul>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config';
@@ -18,7 +18,8 @@ const AccountView: React.FC = () => {
const locale = useLocale();
const { setStepNav } = useStepNav();
const { user } = useAuth();
const [initialState, setInitialState] = useState<Fields>();
const userRef = useRef(user);
const [internalState, setInternalState] = useState<Fields>();
const { id, preferencesKey, docPermissions, getDocPermissions, slug, getDocPreferences } = useDocumentInfo();
const { getPreference } = usePreferences();
@@ -36,6 +37,7 @@ const AccountView: React.FC = () => {
} = {},
},
} = useConfig();
const { t } = useTranslation('authentication');
const collection = collections.find((coll) => coll.slug === slug);
@@ -63,7 +65,7 @@ const AccountView: React.FC = () => {
getDocPermissions();
const preferences = await getDocPreferences();
const state = await buildStateFromSchema({ fieldSchema: collection.fields, preferences, data: json.doc, user, id, operation: 'update', locale, t });
setInitialState(state);
setInternalState(state);
}, [collection, user, id, t, locale, getDocPermissions, getDocPreferences]);
useEffect(() => {
@@ -75,26 +77,28 @@ const AccountView: React.FC = () => {
}, [setStepNav, t]);
useEffect(() => {
const awaitInitialState = async () => {
const awaitInternalState = async () => {
const preferences = await getDocPreferences();
const state = await buildStateFromSchema({
fieldSchema: fields,
preferences,
data: dataToRender,
operation: 'update',
id,
user,
user: userRef.current,
locale,
t,
});
await getPreference(preferencesKey);
setInitialState(state);
setInternalState(state);
};
if (dataToRender) awaitInitialState();
}, [dataToRender, fields, id, user, locale, preferencesKey, getPreference, t, getDocPreferences]);
if (dataToRender) awaitInternalState();
}, [dataToRender, fields, id, locale, preferencesKey, getPreference, t, getDocPreferences]);
const isLoading = !initialState || !docPermissions || isLoadingData;
const isLoading = !internalState || !docPermissions || isLoadingData;
return (
<RenderCustomComponent
@@ -106,7 +110,7 @@ const AccountView: React.FC = () => {
collection,
permissions: docPermissions,
hasSavePermission,
initialState,
initialState: internalState,
apiURL,
isLoading,
onSave,

View File

@@ -73,7 +73,7 @@ const Auth: React.FC<Props> = (props) => {
return (
<div className={baseClass}>
{ !collection.auth.disableLocalStrategy && (
{!collection.auth.disableLocalStrategy && (
<React.Fragment>
<Email
required

View File

@@ -29,6 +29,7 @@ import { getTranslation } from '../../../../../utilities/getTranslation';
import { SetStepNav } from './SetStepNav';
import { FormLoadingOverlayToggle } from '../../../elements/Loading';
import { formatDate } from '../../../../utilities/formatDate';
import { useAuth } from '../../../utilities/Auth';
import './index.scss';
@@ -38,6 +39,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
const { admin: { dateFormat }, routes: { admin } } = useConfig();
const { publishedDoc } = useDocumentInfo();
const { t, i18n } = useTranslation('general');
const { user, refreshCookieAsync } = useAuth();
const {
collection,
@@ -78,14 +80,18 @@ const DefaultEditView: React.FC<Props> = (props) => {
isEditing && `${baseClass}--is-editing`,
].filter(Boolean).join(' ');
const onSave = useCallback((json) => {
const onSave = useCallback(async (json) => {
if (auth && id === user.id) {
await refreshCookieAsync();
}
if (typeof onSaveFromProps === 'function') {
onSaveFromProps({
...json,
operation: id ? 'update' : 'create',
});
}
}, [id, onSaveFromProps]);
}, [id, onSaveFromProps, auth, user, refreshCookieAsync]);
const operation = isEditing ? 'update' : 'create';

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Redirect, useRouteMatch, useHistory, useLocation } from 'react-router-dom';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { Redirect, useRouteMatch, useHistory } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../../utilities/Config';
import { useAuth } from '../../../utilities/Auth';
@@ -37,11 +37,11 @@ const EditView: React.FC<IndexProps> = (props) => {
const locale = useLocale();
const { serverURL, routes: { admin, api } } = useConfig();
const { params: { id } = {} } = useRouteMatch<Record<string, string>>();
const { state: locationState } = useLocation();
const history = useHistory();
const [internalState, setInternalState] = useState<Fields>();
const [updatedAt, setUpdatedAt] = useState<string>();
const { user } = useAuth();
const userRef = useRef(user);
const { getVersions, getDocPermissions, docPermissions, getDocPreferences } = useDocumentInfo();
const { t } = useTranslation('general');
@@ -50,6 +50,24 @@ const EditView: React.FC<IndexProps> = (props) => {
{ initialParams: { 'fallback-locale': 'null', depth: 0, draft: 'true' }, initialData: null },
);
const buildState = useCallback(async (doc, overrides?: Partial<Parameters<typeof buildStateFromSchema>[0]>) => {
const preferences = await getDocPreferences();
const state = await buildStateFromSchema({
fieldSchema: overrides.fieldSchema,
preferences,
data: doc || {},
user: userRef.current,
id,
operation: 'update',
locale,
t,
...overrides,
});
setInternalState(state);
}, [getDocPreferences, id, locale, t]);
const onSave = useCallback(async (json: {
doc
}) => {
@@ -59,24 +77,25 @@ const EditView: React.FC<IndexProps> = (props) => {
if (!isEditing) {
setRedirect(`${admin}/collections/${collection.slug}/${json?.doc?.id}`);
} else {
const preferences = await getDocPreferences();
const state = await buildStateFromSchema({ fieldSchema: collection.fields, preferences, data: json.doc, user, id, operation: 'update', locale, t });
setInternalState(state);
buildState(json.doc, {
fieldSchema: collection.fields,
});
}
}, [admin, collection.fields, collection.slug, getDocPreferences, getDocPermissions, getVersions, id, isEditing, locale, t, user]);
const dataToRender = (locationState as Record<string, unknown>)?.data || data;
}, [admin, getVersions, isEditing, buildState, getDocPermissions, collection]);
useEffect(() => {
const awaitInternalState = async () => {
setUpdatedAt(dataToRender?.updatedAt);
const preferences = await getDocPreferences();
const state = await buildStateFromSchema({ fieldSchema: fields, preferences, data: dataToRender || {}, user, operation: isEditing ? 'update' : 'create', id, locale, t });
setInternalState(state);
};
if (fields && (isEditing ? data : true)) {
const awaitInternalState = async () => {
setUpdatedAt(data?.updatedAt);
buildState(data, {
operation: isEditing ? 'update' : 'create',
fieldSchema: fields,
});
};
if (!isEditing || dataToRender) awaitInternalState();
}, [dataToRender, fields, isEditing, id, user, locale, t, getDocPreferences]);
awaitInternalState();
}
}, [isEditing, data, buildState, fields]);
useEffect(() => {
if (redirect) {
@@ -103,7 +122,7 @@ const EditView: React.FC<IndexProps> = (props) => {
componentProps={{
id,
isLoading,
data: dataToRender,
data,
collection,
permissions: docPermissions,
isEditing,
@@ -112,7 +131,7 @@ const EditView: React.FC<IndexProps> = (props) => {
hasSavePermission,
apiURL,
action,
updatedAt: updatedAt || dataToRender?.updatedAt,
updatedAt: updatedAt || data?.updatedAt,
}}
/>
</EditDepthContext.Provider>