236 lines
6.0 KiB
JavaScript
236 lines
6.0 KiB
JavaScript
import React, { useContext, useEffect, useMemo, useReducer } from 'react';
|
|
import api from '../lib/api';
|
|
|
|
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 };
|