Skip to content

Commit 2fc03f1

Browse files
feat: returns queried user alongside refreshed token (#2813)
Co-authored-by: Dan Ribbens <[email protected]>
1 parent 7927dd4 commit 2fc03f1

File tree

16 files changed

+351
-113
lines changed

16 files changed

+351
-113
lines changed

src/admin/components/utilities/Auth/index.tsx

Lines changed: 76 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import jwtDecode from 'jwt-decode';
33
import { useHistory, useLocation } from 'react-router-dom';
44
import { useModal } from '@faceless-ui/modal';
55
import { useTranslation } from 'react-i18next';
6+
import { toast } from 'react-toastify';
67
import { Permissions, User } from '../../../../auth/types';
78
import { useConfig } from '../Config';
89
import { requests } from '../../../api';
@@ -44,29 +45,56 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
4445

4546
const id = user?.id;
4647

47-
const refreshCookie = useCallback(() => {
48+
const refreshCookie = useCallback((forceRefresh?: boolean) => {
4849
const now = Math.round((new Date()).getTime() / 1000);
4950
const remainingTime = (exp as number || 0) - now;
5051

51-
if (exp && remainingTime < 120) {
52+
if (forceRefresh || (exp && remainingTime < 120)) {
5253
setTimeout(async () => {
53-
const request = await requests.post(`${serverURL}${api}/${userSlug}/refresh-token`, {
54-
headers: {
55-
'Accept-Language': i18n.language,
56-
},
57-
});
58-
59-
if (request.status === 200) {
60-
const json = await request.json();
61-
setUser(json.user);
62-
} else {
63-
setUser(null);
64-
push(`${admin}${logoutInactivityRoute}?redirect=${encodeURIComponent(window.location.pathname)}`);
54+
try {
55+
const request = await requests.post(`${serverURL}${api}/${userSlug}/refresh-token`, {
56+
headers: {
57+
'Accept-Language': i18n.language,
58+
},
59+
});
60+
61+
if (request.status === 200) {
62+
const json = await request.json();
63+
setUser(json.user);
64+
} else {
65+
setUser(null);
66+
push(`${admin}${logoutInactivityRoute}?redirect=${encodeURIComponent(window.location.pathname)}`);
67+
}
68+
} catch (e) {
69+
toast.error(e.message);
6570
}
6671
}, 1000);
6772
}
6873
}, [exp, serverURL, api, userSlug, push, admin, logoutInactivityRoute, i18n]);
6974

75+
const refreshCookieAsync = useCallback(async (skipSetUser?: boolean): Promise<User> => {
76+
try {
77+
const request = await requests.post(`${serverURL}${api}/${userSlug}/refresh-token`, {
78+
headers: {
79+
'Accept-Language': i18n.language,
80+
},
81+
});
82+
83+
if (request.status === 200) {
84+
const json = await request.json();
85+
if (!skipSetUser) setUser(json.user);
86+
return json.user;
87+
}
88+
89+
setUser(null);
90+
push(`${admin}${logoutInactivityRoute}`);
91+
return null;
92+
} catch (e) {
93+
toast.error(`Refreshing token failed: ${e.message}`);
94+
return null;
95+
}
96+
}, [serverURL, api, userSlug, push, admin, logoutInactivityRoute, i18n]);
97+
7098
const setToken = useCallback((token: string) => {
7199
const decoded = jwtDecode<User>(token);
72100
setUser(decoded);
@@ -80,37 +108,47 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
80108
}, [serverURL, api, userSlug]);
81109

82110
const refreshPermissions = useCallback(async () => {
83-
const request = await requests.get(`${serverURL}${api}/access`, {
84-
headers: {
85-
'Accept-Language': i18n.language,
86-
},
87-
});
88-
89-
if (request.status === 200) {
90-
const json: Permissions = await request.json();
91-
setPermissions(json);
92-
} else {
93-
throw new Error(`Fetching permissions failed with status code ${request.status}`);
94-
}
95-
}, [serverURL, api, i18n]);
96-
97-
// On mount, get user and set
98-
useEffect(() => {
99-
const fetchMe = async () => {
100-
const request = await requests.get(`${serverURL}${api}/${userSlug}/me`, {
111+
try {
112+
const request = await requests.get(`${serverURL}${api}/access`, {
101113
headers: {
102114
'Accept-Language': i18n.language,
103115
},
104116
});
105117

106118
if (request.status === 200) {
107-
const json = await request.json();
119+
const json: Permissions = await request.json();
120+
setPermissions(json);
121+
} else {
122+
throw new Error(`Fetching permissions failed with status code ${request.status}`);
123+
}
124+
} catch (e) {
125+
toast.error(`Refreshing permissions failed: ${e.message}`);
126+
}
127+
}, [serverURL, api, i18n]);
108128

109-
setUser(json?.user || null);
129+
// On mount, get user and set
130+
useEffect(() => {
131+
const fetchMe = async () => {
132+
try {
133+
const request = await requests.get(`${serverURL}${api}/${userSlug}/me`, {
134+
headers: {
135+
'Accept-Language': i18n.language,
136+
},
137+
});
138+
139+
if (request.status === 200) {
140+
const json = await request.json();
110141

111-
if (json?.token) {
112-
setToken(json.token);
142+
if (json?.user) {
143+
setUser(json.user);
144+
} else if (json?.token) {
145+
setToken(json.token);
146+
} else {
147+
setUser(null);
148+
}
113149
}
150+
} catch (e) {
151+
toast.error(`Fetching user failed: ${e.message}`);
114152
}
115153
};
116154

@@ -172,8 +210,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
172210
return (
173211
<Context.Provider value={{
174212
user,
213+
setUser,
175214
logOut,
176215
refreshCookie,
216+
refreshCookieAsync,
177217
refreshPermissions,
178218
permissions,
179219
setToken,

src/admin/components/utilities/Auth/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { User, Permissions } from '../../../../auth/types';
22

33
export type AuthContext<T = User> = {
44
user?: T | null
5+
setUser: (user: T) => void
56
logOut: () => void
6-
refreshCookie: () => void
7+
refreshCookie: (forceRefresh?: boolean) => void
8+
refreshCookieAsync: () => Promise<User>
79
setToken: (token: string) => void
810
token?: string
911
refreshPermissions: () => Promise<void>

src/admin/components/views/Account/Default.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useCallback } from 'react';
22
import { Link } from 'react-router-dom';
33
import { useTranslation } from 'react-i18next';
44
import { useConfig } from '../../utilities/Config';
@@ -22,6 +22,7 @@ import Label from '../../forms/Label';
2222
import type { Translation } from '../../../../translations/type';
2323
import { LoadingOverlayToggle } from '../../elements/Loading';
2424
import { formatDate } from '../../../utilities/formatDate';
25+
import { useAuth } from '../../utilities/Auth';
2526

2627
import './index.scss';
2728

@@ -37,7 +38,7 @@ const DefaultAccount: React.FC<Props> = (props) => {
3738
initialState,
3839
isLoading,
3940
action,
40-
onSave,
41+
onSave: onSaveFromProps,
4142
} = props;
4243

4344
const {
@@ -51,13 +52,21 @@ const DefaultAccount: React.FC<Props> = (props) => {
5152
auth,
5253
} = collection;
5354

55+
const { refreshCookieAsync } = useAuth();
5456
const { admin: { dateFormat }, routes: { admin } } = useConfig();
5557
const { t, i18n } = useTranslation('authentication');
5658

5759
const languageOptions = Object.entries(i18n.options.resources).map(([language, resource]) => (
5860
{ label: (resource as Translation).general.thisLanguage, value: language }
5961
));
6062

63+
const onSave = useCallback(async () => {
64+
await refreshCookieAsync();
65+
if (typeof onSaveFromProps === 'function') {
66+
onSaveFromProps();
67+
}
68+
}, [onSaveFromProps, refreshCookieAsync]);
69+
6170
const classes = [
6271
baseClass,
6372
].filter(Boolean).join(' ');
@@ -69,7 +78,6 @@ const DefaultAccount: React.FC<Props> = (props) => {
6978
show={isLoading}
7079
type="withoutNav"
7180
/>
72-
7381
<div className={classes}>
7482
{!isLoading && (
7583
<OperationContext.Provider value="update">
@@ -201,7 +209,6 @@ const DefaultAccount: React.FC<Props> = (props) => {
201209
)}
202210
</React.Fragment>
203211
)}
204-
205212
</ul>
206213
</div>
207214
</div>

src/admin/components/views/Account/index.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect } from 'react';
1+
import React, { useState, useEffect, useRef } from 'react';
22
import { useLocation } from 'react-router-dom';
33
import { useTranslation } from 'react-i18next';
44
import { useConfig } from '../../utilities/Config';
@@ -18,7 +18,8 @@ const AccountView: React.FC = () => {
1818
const locale = useLocale();
1919
const { setStepNav } = useStepNav();
2020
const { user } = useAuth();
21-
const [initialState, setInitialState] = useState<Fields>();
21+
const userRef = useRef(user);
22+
const [internalState, setInternalState] = useState<Fields>();
2223
const { id, preferencesKey, docPermissions, getDocPermissions, slug, getDocPreferences } = useDocumentInfo();
2324
const { getPreference } = usePreferences();
2425

@@ -36,6 +37,7 @@ const AccountView: React.FC = () => {
3637
} = {},
3738
},
3839
} = useConfig();
40+
3941
const { t } = useTranslation('authentication');
4042

4143
const collection = collections.find((coll) => coll.slug === slug);
@@ -63,7 +65,7 @@ const AccountView: React.FC = () => {
6365
getDocPermissions();
6466
const preferences = await getDocPreferences();
6567
const state = await buildStateFromSchema({ fieldSchema: collection.fields, preferences, data: json.doc, user, id, operation: 'update', locale, t });
66-
setInitialState(state);
68+
setInternalState(state);
6769
}, [collection, user, id, t, locale, getDocPermissions, getDocPreferences]);
6870

6971
useEffect(() => {
@@ -75,26 +77,28 @@ const AccountView: React.FC = () => {
7577
}, [setStepNav, t]);
7678

7779
useEffect(() => {
78-
const awaitInitialState = async () => {
80+
const awaitInternalState = async () => {
7981
const preferences = await getDocPreferences();
82+
8083
const state = await buildStateFromSchema({
8184
fieldSchema: fields,
8285
preferences,
8386
data: dataToRender,
8487
operation: 'update',
8588
id,
86-
user,
89+
user: userRef.current,
8790
locale,
8891
t,
8992
});
93+
9094
await getPreference(preferencesKey);
91-
setInitialState(state);
95+
setInternalState(state);
9296
};
9397

94-
if (dataToRender) awaitInitialState();
95-
}, [dataToRender, fields, id, user, locale, preferencesKey, getPreference, t, getDocPreferences]);
98+
if (dataToRender) awaitInternalState();
99+
}, [dataToRender, fields, id, locale, preferencesKey, getPreference, t, getDocPreferences]);
96100

97-
const isLoading = !initialState || !docPermissions || isLoadingData;
101+
const isLoading = !internalState || !docPermissions || isLoadingData;
98102

99103
return (
100104
<RenderCustomComponent
@@ -106,7 +110,7 @@ const AccountView: React.FC = () => {
106110
collection,
107111
permissions: docPermissions,
108112
hasSavePermission,
109-
initialState,
113+
initialState: internalState,
110114
apiURL,
111115
isLoading,
112116
onSave,

src/admin/components/views/collections/Edit/Auth/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ const Auth: React.FC<Props> = (props) => {
7373

7474
return (
7575
<div className={baseClass}>
76-
{ !collection.auth.disableLocalStrategy && (
76+
{!collection.auth.disableLocalStrategy && (
7777
<React.Fragment>
7878
<Email
7979
required

src/admin/components/views/collections/Edit/Default.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { getTranslation } from '../../../../../utilities/getTranslation';
2929
import { SetStepNav } from './SetStepNav';
3030
import { FormLoadingOverlayToggle } from '../../../elements/Loading';
3131
import { formatDate } from '../../../../utilities/formatDate';
32+
import { useAuth } from '../../../utilities/Auth';
3233

3334
import './index.scss';
3435

@@ -38,6 +39,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
3839
const { admin: { dateFormat }, routes: { admin } } = useConfig();
3940
const { publishedDoc } = useDocumentInfo();
4041
const { t, i18n } = useTranslation('general');
42+
const { user, refreshCookieAsync } = useAuth();
4143

4244
const {
4345
collection,
@@ -78,14 +80,18 @@ const DefaultEditView: React.FC<Props> = (props) => {
7880
isEditing && `${baseClass}--is-editing`,
7981
].filter(Boolean).join(' ');
8082

81-
const onSave = useCallback((json) => {
83+
const onSave = useCallback(async (json) => {
84+
if (auth && id === user.id) {
85+
await refreshCookieAsync();
86+
}
87+
8288
if (typeof onSaveFromProps === 'function') {
8389
onSaveFromProps({
8490
...json,
8591
operation: id ? 'update' : 'create',
8692
});
8793
}
88-
}, [id, onSaveFromProps]);
94+
}, [id, onSaveFromProps, auth, user, refreshCookieAsync]);
8995

9096
const operation = isEditing ? 'update' : 'create';
9197

0 commit comments

Comments
 (0)