@wener/console
Version:
Base console UI toolkit
208 lines (184 loc) • 5.16 kB
text/typescript
import { useEffect, useRef, type FC } from 'react';
import { getGlobalStates } from '@wener/utils';
import { createStore } from 'zustand';
import { mutative } from 'zustand-mutative';
import { useNetworkStatus } from '../../utils/NetworkStatus';
import { createStoreSelectorHook } from '../../zustand';
export const AuthStatus = {
Init: 'Init',
Authenticated: 'Authenticated',
Unauthenticated: 'Unauthenticated',
Expired: 'Expired',
Loading: 'Loading',
Error: 'Error',
Locked: 'Locked',
} as const;
type AuthStatusCode = (typeof AuthStatus)[keyof typeof AuthStatus];
interface SetAuthOptions {
accessToken: string;
refreshToken?: string;
expiresIn?: number;
expiresAt?: Date | string;
}
interface AuthStoreState {
status: AuthStatusCode;
accessToken?: string;
refreshToken?: string;
expiresIn?: number;
expiresAt?: Date;
error?: any;
setAuth(o: SetAuthOptions): void;
reset(): void;
}
type AuthStore = ReturnType<typeof createAuthStore>;
function createAuthStore() {
return createStore(
mutative<AuthStoreState>((setState, getState, store) => {
return {
status: AuthStatus.Init,
setAuth(o: SetAuthOptions) {
setState((s) => {
Object.assign(s, {
status: AuthStatus.Authenticated,
...o,
expiresAt: o.expiresAt ? new Date(o.expiresAt) : undefined,
});
});
},
reset() {
setState((s) => {
Object.assign(s, {
status: AuthStatus.Unauthenticated,
accessToken: undefined,
refreshToken: undefined,
expiresIn: undefined,
expiresAt: undefined,
error: undefined,
});
});
},
};
}),
);
}
type AuthSidecarProps = {
store: AuthStore;
actions: {
refresh: (o: { accessToken: string; refreshToken?: string }) => Promise<{
accessToken: string;
refreshToken?: string;
expiresAt: Date | string;
}>;
};
storage?: Storage;
};
function useAuthSidecar({ store, actions: { refresh }, storage = localStorage }: AuthSidecarProps) {
const { online } = useNetworkStatus();
// todo watch storage ?
const checkAuth = async () => {
const accessToken = storage.getItem('accessToken');
const refreshToken = storage.getItem('refreshToken') ?? undefined;
if (accessToken) {
try {
const out = await refresh({ accessToken, refreshToken });
store.getState().setAuth(out);
return true;
} catch (e) {
console.error('Failed to refresh token', e);
}
}
// not authenticated or server error
store.getState().reset();
return false;
};
const authRef = useRef<Promise<boolean>>();
const doAuthCheck = () => {
const state = store.getState();
// only check for init and authenticated
switch (state.status) {
case AuthStatus.Init:
case AuthStatus.Authenticated:
break;
default:
return;
}
// avoid race
let current = authRef.current;
if (current) {
current.then((v) => {
// authed, skip for now, will check for next
if (v) {
authRef.current = undefined;
} else {
// check again
return (current = checkAuth());
}
return v;
});
} else {
authRef.current = checkAuth().then((v) => {
// done
authRef.current = undefined;
return v;
});
}
};
// auth check
useEffect(() => {
if (!online) return;
doAuthCheck();
const timer = setInterval(doAuthCheck, 5 * 60 * 1000);
return () => {
clearInterval(timer);
};
}, [store, online]);
//
useAuthTokenPersist(store, storage);
}
const AuthStoreStateKey = 'AuthStore';
export const AuthSidecar: FC<Omit<AuthSidecarProps, 'store'>> = (props) => {
let store = getAuthStore();
useAuthSidecar({
store,
...props,
});
return null;
};
function useAuthTokenPersist(store: AuthStore, storage: Storage) {
const ref = useRef(storage);
ref.current = storage;
// persist token
useEffect(() => {
return store.subscribe((s) => {
if (s.status !== AuthStatus.Authenticated) {
return;
}
const { accessToken = '', refreshToken } = s;
const storage = ref.current;
if ((storage.getItem('accessToken') ?? '') === accessToken) {
return;
}
accessToken ? storage.setItem('accessToken', accessToken) : deleteItem(storage, 'accessToken');
refreshToken ? storage.setItem('refreshToken', refreshToken) : deleteItem(storage, 'refreshToken');
});
}, [store]);
}
export function getAuthStore() {
return getGlobalStates(AuthStoreStateKey, () => {
return createAuthStore();
});
}
export function getAuthState() {
return getAuthStore().getState();
}
export function getAccessToken() {
return getAuthState().accessToken;
}
function deleteItem(s: any, key: string) {
if ('removeItem' in s) {
s.removeItem(key);
} else {
delete s[key];
}
}
export const useAuthStore = createStoreSelectorHook(getAuthStore);