tests
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import React, { useContext, useEffect, useMemo, useReducer } from 'react';
|
||||
import axios from 'axios';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
const storage = typeof window !== 'undefined' ? window.sessionStorage : undefined;
|
||||
|
||||
const STORED_TOKEN = storage?.getItem('papercrate_token') ?? '';
|
||||
let STORED_TENANT = null;
|
||||
|
||||
if (storage) {
|
||||
try {
|
||||
const rawTenant = storage.getItem('papercrate_tenant');
|
||||
if (rawTenant) {
|
||||
STORED_TENANT = JSON.parse(rawTenant);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[app] Failed to parse stored tenant metadata', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (STORED_TOKEN) {
|
||||
api.defaults.headers.common.Authorization = `Bearer ${STORED_TOKEN}`;
|
||||
}
|
||||
|
||||
const initialAppState = {
|
||||
status: STORED_TOKEN ? 'authenticated' : 'logged-out',
|
||||
token: STORED_TOKEN,
|
||||
error: null,
|
||||
isRefreshing: false,
|
||||
tenantSelection: null,
|
||||
tenant: STORED_TENANT,
|
||||
tenants: [],
|
||||
};
|
||||
|
||||
const AppStateContext = React.createContext(null);
|
||||
const AppDispatchContext = React.createContext(null);
|
||||
|
||||
const appStateReducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'LOGIN_REQUEST':
|
||||
return {
|
||||
...state,
|
||||
status: 'authenticating',
|
||||
error: null,
|
||||
tenantSelection: null,
|
||||
tenant: null,
|
||||
tenants: [],
|
||||
};
|
||||
case 'LOGIN_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
status: 'authenticated',
|
||||
token: action.token,
|
||||
error: null,
|
||||
tenantSelection: null,
|
||||
tenant: action.tenant || null,
|
||||
tenants: state.tenants,
|
||||
};
|
||||
case 'LOGIN_FAILURE':
|
||||
return {
|
||||
status: 'logged-out',
|
||||
token: '',
|
||||
error: action.error || null,
|
||||
isRefreshing: false,
|
||||
tenantSelection: null,
|
||||
tenant: null,
|
||||
tenants: [],
|
||||
};
|
||||
case 'TENANT_SELECTION_REQUIRED':
|
||||
return {
|
||||
status: 'selecting-tenant',
|
||||
token: '',
|
||||
error: null,
|
||||
isRefreshing: false,
|
||||
tenantSelection: {
|
||||
selectionToken: action.selectionToken,
|
||||
tenants: action.tenants,
|
||||
},
|
||||
tenant: null,
|
||||
tenants: [],
|
||||
};
|
||||
case 'CLEAR_TENANT_SELECTION':
|
||||
return {
|
||||
status: 'logged-out',
|
||||
token: '',
|
||||
error: null,
|
||||
isRefreshing: false,
|
||||
tenantSelection: null,
|
||||
tenant: null,
|
||||
tenants: [],
|
||||
};
|
||||
case 'LOGOUT_SUCCESS':
|
||||
return {
|
||||
status: 'logged-out',
|
||||
token: '',
|
||||
error: null,
|
||||
isRefreshing: false,
|
||||
tenantSelection: null,
|
||||
tenant: null,
|
||||
tenants: [],
|
||||
};
|
||||
case 'BOOTSTRAP_START':
|
||||
return { ...state, status: 'bootstrapping', error: null };
|
||||
case 'BOOTSTRAP_SUCCESS':
|
||||
return { ...state, status: 'ready', error: null };
|
||||
case 'BOOTSTRAP_FAILURE':
|
||||
return { ...state, status: 'authenticated', error: action.error || null };
|
||||
case 'TOKEN_REFRESH_START':
|
||||
return { ...state, isRefreshing: true, error: null };
|
||||
case 'TOKEN_REFRESH_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
token: action.token,
|
||||
isRefreshing: false,
|
||||
status: state.status === 'logged-out' ? 'authenticated' : state.status,
|
||||
tenantSelection: null,
|
||||
tenant: action.tenant || state.tenant || null,
|
||||
tenants: state.tenants,
|
||||
};
|
||||
case 'TOKEN_REFRESH_FAILURE':
|
||||
return {
|
||||
status: 'logged-out',
|
||||
token: '',
|
||||
error: action.error || null,
|
||||
isRefreshing: false,
|
||||
tenantSelection: null,
|
||||
tenant: null,
|
||||
tenants: [],
|
||||
};
|
||||
case 'LOGOUT':
|
||||
return {
|
||||
status: 'logged-out',
|
||||
token: '',
|
||||
error: null,
|
||||
isRefreshing: false,
|
||||
tenantSelection: null,
|
||||
tenant: null,
|
||||
tenants: [],
|
||||
};
|
||||
case 'RESET_ERROR':
|
||||
return { ...state, error: null };
|
||||
case 'SET_TENANTS':
|
||||
return {
|
||||
...state,
|
||||
tenants: Array.isArray(action.tenants) ? action.tenants : [],
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const AppStateProvider = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(appStateReducer, initialAppState);
|
||||
|
||||
useEffect(() => {
|
||||
const token = state.token ?? '';
|
||||
if (token) {
|
||||
api.defaults.headers.common.Authorization = `Bearer ${token}`;
|
||||
storage?.setItem('papercrate_token', token);
|
||||
} else {
|
||||
delete api.defaults.headers.common.Authorization;
|
||||
storage?.removeItem('papercrate_token');
|
||||
}
|
||||
}, [state.token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.tenant) {
|
||||
try {
|
||||
storage?.setItem('papercrate_tenant', JSON.stringify(state.tenant));
|
||||
} catch (error) {
|
||||
console.warn('[app] Failed to persist tenant info', error);
|
||||
}
|
||||
} else {
|
||||
storage?.removeItem('papercrate_tenant');
|
||||
}
|
||||
}, [state.tenant]);
|
||||
|
||||
useEffect(() => {
|
||||
let abort = false;
|
||||
|
||||
const loadTenants = async () => {
|
||||
if (state.status !== 'authenticated' || !state.token) {
|
||||
dispatch({ type: 'SET_TENANTS', tenants: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await api.get('/auth/tenants');
|
||||
if (!abort) {
|
||||
dispatch({
|
||||
type: 'SET_TENANTS',
|
||||
tenants: Array.isArray(data?.tenants) ? data.tenants : [],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (!abort) {
|
||||
console.warn('Failed to load tenant list', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadTenants();
|
||||
|
||||
return () => {
|
||||
abort = true;
|
||||
};
|
||||
}, [state.status, state.token, dispatch]);
|
||||
|
||||
const stateValue = useMemo(() => state, [state]);
|
||||
|
||||
return (
|
||||
<AppStateContext.Provider value={stateValue}>
|
||||
<AppDispatchContext.Provider value={dispatch}>
|
||||
{children}
|
||||
</AppDispatchContext.Provider>
|
||||
</AppStateContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useAppState = () => {
|
||||
const context = useContext(AppStateContext);
|
||||
if (!context) {
|
||||
throw new Error('useAppState must be used within an AppStateProvider.');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
const useAppDispatch = () => {
|
||||
const context = useContext(AppDispatchContext);
|
||||
if (!context) {
|
||||
throw new Error('useAppDispatch must be used within an AppStateProvider.');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export { api, AppStateProvider, useAppState, useAppDispatch };
|
||||
Reference in New Issue
Block a user