feat: returns queried user alongside refreshed token (#2813)
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -73,7 +73,7 @@ const Auth: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{ !collection.auth.disableLocalStrategy && (
|
||||
{!collection.auth.disableLocalStrategy && (
|
||||
<React.Fragment>
|
||||
<Email
|
||||
required
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user