Skip to content

Commit 21fbc5f

Browse files
committed
feat: make authStorage a single token data source of truth
1 parent 5ad5f88 commit 21fbc5f

File tree

10 files changed

+97
-152
lines changed

10 files changed

+97
-152
lines changed

.changeset/old-houses-obey.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-starter-boilerplate": minor
3+
---
4+
5+
make authStorage a single source of truth for token data

src/context/apiClient/apiClientContextController/interceptors/requestInterceptors.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import axios, { AxiosRequestHeaders, InternalAxiosRequestConfig } from 'axios';
22
import { jwtDecode } from 'jwt-decode';
33

4-
import { RefreshTokenMutationResponse } from 'api/actions/auth/auth.types';
54
import { authStorage } from 'context/auth/authStorage/AuthStorage';
5+
import { RefreshTokenMutationResponse } from 'api/actions/auth/auth.types';
66
import { refreshTokenUrl } from 'api/actions/auth/auth.mutations';
77

88
export const requestSuccessInterceptor = async (
@@ -24,13 +24,13 @@ export const requestSuccessInterceptor = async (
2424

2525
const { exp } = jwtDecode<{ exp: number }>(data.accessToken);
2626

27-
authStorage.accessToken = data.accessToken;
28-
authStorage.expires = exp;
29-
authStorage.refreshToken = data.refreshToken;
27+
authStorage.tokenData = {
28+
accessToken: data.accessToken,
29+
expires: exp,
30+
refreshToken: data.refreshToken,
31+
};
3032
} catch (e) {
31-
authStorage.accessToken = null;
32-
authStorage.expires = null;
33-
authStorage.refreshToken = null;
33+
authStorage.resetTokens();
3434
}
3535

3636
return {

src/context/apiClient/apiClientContextController/interceptors/responseInterceptors.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import axios, { type AxiosError, AxiosResponse } from 'axios';
22
import { jwtDecode } from 'jwt-decode';
33

4-
import { authStorage } from 'context/auth/authStorage/AuthStorage';
54
import { getStandardizedApiError } from 'context/apiClient/apiClientContextController/apiError/apiError';
65
import { ExtendedAxiosRequestConfig } from 'api/types/types';
76
import { RefreshTokenMutationResponse } from 'api/actions/auth/auth.types';
87
import { refreshTokenUrl } from 'api/actions/auth/auth.mutations';
8+
import { authStorage } from 'context/auth/authStorage/AuthStorage';
99

1010
export const responseSuccessInterceptor = (response: AxiosResponse) => response;
1111

@@ -15,9 +15,7 @@ export const responseFailureInterceptor = async (error: AxiosError<unknown>) =>
1515
const originalRequest = error.config as ExtendedAxiosRequestConfig;
1616

1717
if (standarizedError.statusCode === 401 && originalRequest?._retry) {
18-
authStorage.accessToken = null;
19-
authStorage.expires = null;
20-
authStorage.refreshToken = null;
18+
authStorage.resetTokens();
2119

2220
window.location.replace('/login');
2321

@@ -34,15 +32,15 @@ export const responseFailureInterceptor = async (error: AxiosError<unknown>) =>
3432
});
3533
const { exp } = jwtDecode<{ exp: number }>(data.accessToken);
3634

37-
authStorage.accessToken = data.accessToken;
38-
authStorage.expires = exp;
39-
authStorage.refreshToken = data.refreshToken;
35+
authStorage.tokenData = {
36+
accessToken: data.accessToken,
37+
expires: exp,
38+
refreshToken: data.refreshToken,
39+
};
4040

4141
return axios(originalRequest);
4242
} catch {
43-
authStorage.accessToken = null;
44-
authStorage.expires = null;
45-
authStorage.refreshToken = null;
43+
authStorage.resetTokens();
4644
window.location.replace('/login');
4745

4846
return Promise.reject(standarizedError);

src/context/auth/authActionCreators/authActionCreators.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/context/auth/authActionCreators/authActionCreators.types.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.

src/context/auth/authContextController/AuthContextController.tsx

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
1-
import { useCallback, useEffect, useMemo, useReducer } from 'react';
1+
import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react';
22

33
import { useMutation } from 'hooks/useMutation/useMutation';
44
import { useUser } from '../../../hooks/useUser/useUser';
5-
import { resetTokens, setTokens } from '../authActionCreators/authActionCreators';
65
import { AuthContext } from '../authContext/AuthContext';
76
import { AuthContextValue } from '../authContext/AuthContext.types';
8-
import { authReducer } from '../authReducer/authReducer';
9-
import { authStorage } from '../authStorage/AuthStorage';
7+
import { authStorage } from 'context/auth/authStorage/AuthStorage';
108

119
import { AuthContextControllerProps } from './AuthContextController.types';
1210

1311
export const AuthContextController = ({ children }: AuthContextControllerProps) => {
14-
const [state, dispatch] = useReducer(authReducer, {
15-
accessToken: authStorage.accessToken,
16-
refreshToken: authStorage.refreshToken,
17-
expires: authStorage.expires,
18-
});
12+
const authStorageData = useSyncExternalStore(authStorage.subscribe, authStorage.getTokenData);
1913

2014
const {
2115
data: user,
@@ -24,52 +18,44 @@ export const AuthContextController = ({ children }: AuthContextControllerProps)
2418
isError,
2519
resetUser,
2620
} = useUser({
27-
enabled: !!state.accessToken,
21+
enabled: !!authStorageData.accessToken,
2822
});
2923

3024
const { mutateAsync: login, isPending: isAuthenticating } = useMutation('loginMutation', {
3125
onSuccess: (res) => {
32-
dispatch(
33-
setTokens({
34-
accessToken: res.accessToken,
35-
refreshToken: res.refreshToken,
36-
expires: res.expires,
37-
}),
38-
);
26+
authStorage.tokenData = {
27+
accessToken: res.accessToken,
28+
refreshToken: res.refreshToken,
29+
expires: res.expires,
30+
};
3931
},
4032
onError: () => {
41-
dispatch(resetTokens());
33+
authStorage.resetTokens();
4234
resetUser();
4335
},
4436
});
4537

4638
const logout = useCallback(() => {
4739
resetUser();
48-
dispatch(resetTokens());
40+
authStorage.resetTokens();
4941
}, [resetUser]);
5042

5143
useEffect(() => {
5244
if (isError) {
53-
dispatch(resetTokens());
45+
authStorage.resetTokens();
5446
}
5547
}, [isError]);
5648

57-
useEffect(() => {
58-
authStorage.accessToken = state.accessToken;
59-
authStorage.expires = state.expires;
60-
authStorage.refreshToken = state.refreshToken;
61-
}, [state]);
62-
6349
const value: AuthContextValue = useMemo(
6450
() => ({
65-
...state,
51+
...authStorageData,
6652
isAuthenticating: isAuthenticating || isLoadingAndEnabled,
6753
isAuthenticated: isUserSuccess,
6854
login,
6955
logout,
7056
user,
7157
}),
72-
[state, isAuthenticating, isUserSuccess, isLoadingAndEnabled, login, logout, user],
58+
[authStorageData, isAuthenticating, isUserSuccess, isLoadingAndEnabled, login, logout, user],
7359
);
7460

7561
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;

src/context/auth/authReducer/authReducer.ts

Lines changed: 0 additions & 24 deletions
This file was deleted.

src/context/auth/authReducer/authReducer.types.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/context/auth/authStorage/AuthStorage.ts

Lines changed: 57 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,93 @@
1-
import { Storage } from './AuthStorage.types';
1+
import { Storage, TokenData } from './AuthStorage.types';
22

33
const ACCESS_TOKEN_KEY = 'accessToken';
44
const REFRESH_TOKEN_KEY = 'refreshToken';
55
const EXPIRES_KEY = 'expires';
66

7+
const defaultTokenData = {
8+
accessToken: null,
9+
refreshToken: null,
10+
expires: null,
11+
} satisfies TokenData;
712
class AuthStorage {
8-
private _accessToken: string | null = null;
9-
private _refreshToken: string | null = null;
10-
private _expires: number | null = null;
11-
private _storage: Storage | null = null;
13+
private _storage: Storage | null;
14+
private _tokenData: TokenData = defaultTokenData;
15+
private listeners: VoidFunction[] = [];
1216

1317
constructor(_storage: Storage) {
1418
try {
1519
this._storage = _storage;
16-
this.accessToken = _storage.getItem(ACCESS_TOKEN_KEY);
17-
this.refreshToken = _storage.getItem(REFRESH_TOKEN_KEY);
18-
this.expires = Number(_storage.getItem(EXPIRES_KEY));
20+
this._tokenData = {
21+
accessToken: _storage.getItem(ACCESS_TOKEN_KEY),
22+
refreshToken: _storage.getItem(REFRESH_TOKEN_KEY),
23+
expires: Number(_storage.getItem(EXPIRES_KEY)),
24+
};
1925
} catch (error) {
2026
this._storage = null;
21-
this.accessToken = null;
22-
this.refreshToken = null;
23-
this.expires = null;
27+
this._tokenData = defaultTokenData;
2428
}
2529
}
2630

27-
get accessToken(): string | null {
28-
return this._accessToken;
31+
subscribe = (listener: VoidFunction) => {
32+
this.listeners = [...this.listeners, listener];
33+
34+
return () => {
35+
this.listeners = this.listeners.filter((subscriber) => subscriber !== listener);
36+
};
37+
};
38+
39+
notify() {
40+
this.listeners.forEach((listener) => listener());
2941
}
3042

31-
set accessToken(value: string | null) {
32-
this._accessToken = value;
43+
getTokenData = () => {
44+
return this._tokenData;
45+
};
3346

47+
private setStorageValue = (storageKey: string, value: number | string | null) => {
3448
try {
35-
if (typeof value === 'string') {
36-
this._storage?.setItem(ACCESS_TOKEN_KEY, value);
49+
if (value !== null) {
50+
this._storage?.setItem(storageKey, String(value));
3751
} else {
38-
this._storage?.removeItem(ACCESS_TOKEN_KEY);
52+
this._storage?.removeItem(storageKey);
3953
}
4054
} catch (error) {
4155
this._storage?.onError(error);
4256
}
43-
}
57+
};
4458

45-
get refreshToken(): string | null {
46-
return this._refreshToken;
59+
set tokenData(value: TokenData) {
60+
this._tokenData = value;
61+
this.setStorageValue(REFRESH_TOKEN_KEY, value.refreshToken);
62+
this.setStorageValue(ACCESS_TOKEN_KEY, value.accessToken);
63+
this.setStorageValue(EXPIRES_KEY, value.expires);
64+
this.notify();
4765
}
4866

49-
set refreshToken(value: string | null) {
50-
this._refreshToken = value;
51-
52-
try {
53-
if (typeof value === 'string') {
54-
this._storage?.setItem(REFRESH_TOKEN_KEY, value);
55-
} else {
56-
this._storage?.removeItem(REFRESH_TOKEN_KEY);
57-
}
58-
} catch (error) {
59-
this._storage?.onError(error);
60-
}
67+
set accessToken(value: string | null) {
68+
this.tokenData = {
69+
...this._tokenData,
70+
accessToken: value,
71+
};
6172
}
6273

63-
get expires(): number | null {
64-
return this._expires;
74+
set refreshToken(value: string | null) {
75+
this.tokenData = {
76+
...this._tokenData,
77+
refreshToken: value,
78+
};
6579
}
6680

6781
set expires(value: number | null) {
68-
this._expires = value;
69-
70-
try {
71-
if (typeof value === 'number') {
72-
this._storage?.setItem(EXPIRES_KEY, value.toString());
73-
} else {
74-
this._storage?.removeItem(EXPIRES_KEY);
75-
}
76-
} catch (error) {
77-
this._storage?.onError(error);
78-
}
82+
this.tokenData = {
83+
...this._tokenData,
84+
expires: value,
85+
};
7986
}
87+
88+
resetTokens = () => {
89+
this.tokenData = defaultTokenData;
90+
};
8091
}
8192

8293
const storage: Storage = {

src/context/auth/authStorage/AuthStorage.types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,9 @@ export interface Storage<TItem = string | null> {
44
removeItem: (key: string) => void;
55
onError: (error: unknown) => void;
66
}
7+
8+
export type TokenData = {
9+
accessToken: string | null;
10+
refreshToken: string | null;
11+
expires: number | null;
12+
};

0 commit comments

Comments
 (0)