326 lines
9.6 KiB
JavaScript
326 lines
9.6 KiB
JavaScript
import { secureStore } from "~/storage/memorySecureStore";
|
|
import { STORAGE_KEYS } from "~/storage/storageKeys";
|
|
import jwtDecode from "jwt-decode";
|
|
import { createLogger } from "~/lib/logger";
|
|
import { FEATURE_SCOPES } from "~/lib/logger/scopes";
|
|
import { createAtom } from "~/lib/atomic-zustand";
|
|
|
|
import promiseObject from "~/lib/async/promiseObject";
|
|
import isExpired from "~/lib/time/isExpired";
|
|
|
|
import { registerUser, loginUserToken } from "~/auth/actions";
|
|
|
|
// DEV
|
|
// SecureStore.deleteItemAsync(STORAGE_KEYS.USER_TOKEN);
|
|
// SecureStore.deleteItemAsync(STORAGE_KEYS.AUTH_TOKEN);
|
|
// SecureStore.deleteItemAsync(STORAGE_KEYS.DEV_USER_TOKEN);
|
|
// SecureStore.deleteItemAsync(STORAGE_KEYS.DEV_AUTH_TOKEN);
|
|
// SecureStore.deleteItemAsync(STORAGE_KEYS.ANON_USER_TOKEN);
|
|
// SecureStore.deleteItemAsync(STORAGE_KEYS.ANON_AUTH_TOKEN);
|
|
// SecureStore.getItemAsync(STORAGE_KEYS.USER_TOKEN).then((t) => authLogger.debug("User token", { token: t }));
|
|
|
|
const authLogger = createLogger({
|
|
module: FEATURE_SCOPES.AUTH,
|
|
feature: "store",
|
|
});
|
|
|
|
export default createAtom(({ get, merge, getActions }) => {
|
|
const sessionActions = getActions("session");
|
|
|
|
const navActions = getActions("nav");
|
|
const treeActions = getActions("tree");
|
|
|
|
let loadingPromise;
|
|
let loadingResolve;
|
|
const initLoadingPromise = () => {
|
|
loadingPromise = new Promise((res) => {
|
|
loadingResolve = res;
|
|
});
|
|
};
|
|
const startLoading = () => {
|
|
authLogger.debug("Starting auth loading state");
|
|
merge({
|
|
userToken: null,
|
|
loading: true,
|
|
});
|
|
initLoadingPromise();
|
|
};
|
|
const endLoading = (data = {}) => {
|
|
authLogger.debug("Ending auth loading state", {
|
|
hasUserToken: !!data.userToken,
|
|
});
|
|
merge({
|
|
...data,
|
|
loading: false,
|
|
initialized: true,
|
|
onReloadAuthToken: null,
|
|
});
|
|
loadingResolve();
|
|
loadingPromise = null;
|
|
};
|
|
const isLoading = () => {
|
|
const { loading } = get();
|
|
return loading;
|
|
};
|
|
initLoadingPromise();
|
|
|
|
const loadUserJWT = async (authToken) => {
|
|
try {
|
|
authLogger.info("Attempting to login with auth token");
|
|
const { userToken } = await loginUserToken({ authToken });
|
|
authLogger.info("Successfully obtained user token");
|
|
await secureStore.setItemAsync(STORAGE_KEYS.USER_TOKEN, userToken);
|
|
endLoading({
|
|
userToken,
|
|
});
|
|
sessionActions.loadSessionFromJWT(userToken);
|
|
return { userToken };
|
|
} catch (error) {
|
|
authLogger.error("Failed to load user JWT", { error });
|
|
if (error?.graphQLErrors?.[0]?.extensions.statusCode === 410) {
|
|
authLogger.warn(
|
|
"Auth token expired, clearing tokens and reinitializing",
|
|
);
|
|
await Promise.all([
|
|
secureStore.deleteItemAsync(STORAGE_KEYS.AUTH_TOKEN),
|
|
secureStore.deleteItemAsync(STORAGE_KEYS.USER_TOKEN),
|
|
]);
|
|
return init();
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const init = async () => {
|
|
authLogger.debug("Initializing auth state");
|
|
let { userToken, authToken } = await promiseObject({
|
|
userToken: secureStore.getItemAsync(STORAGE_KEYS.USER_TOKEN),
|
|
authToken: secureStore.getItemAsync(STORAGE_KEYS.AUTH_TOKEN),
|
|
});
|
|
// await delay(5);
|
|
// authLogger.debug("Auth tokens", { userToken, authToken });
|
|
|
|
if (userToken) {
|
|
const jwtData = jwtDecode(userToken);
|
|
// authLogger.debug("JWT data", { jwtData });
|
|
const { exp } = jwtData;
|
|
// authLogger.debug("Token expiration", { isExpired: isExpired(exp) });
|
|
if (isExpired(exp)) {
|
|
authLogger.info("User token expired, clearing token");
|
|
userToken = null;
|
|
} else {
|
|
endLoading({
|
|
userToken,
|
|
});
|
|
sessionActions.loadSessionFromJWT(jwtData);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!authToken) {
|
|
authLogger.info("No auth token found, registering new user");
|
|
const res = await registerUser();
|
|
authLogger.info("Successfully registered new user");
|
|
authToken = res.authToken;
|
|
await secureStore.setItemAsync(STORAGE_KEYS.AUTH_TOKEN, authToken);
|
|
}
|
|
|
|
if (!userToken && authToken) {
|
|
authLogger.info("Auth token present but no user token, loading user JWT");
|
|
loadUserJWT(authToken);
|
|
}
|
|
};
|
|
|
|
const reload = async () => {
|
|
authLogger.info("Reloading auth state");
|
|
|
|
// Check if we're already reloading or in a loading state
|
|
const { isReloading, lastReloadTime } = get();
|
|
const now = Date.now();
|
|
const timeSinceLastReload = now - lastReloadTime;
|
|
const RELOAD_COOLDOWN = 2000; // 2 seconds cooldown
|
|
|
|
if (isReloading) {
|
|
authLogger.info("Auth reload already in progress, skipping");
|
|
return true;
|
|
}
|
|
|
|
if (timeSinceLastReload < RELOAD_COOLDOWN) {
|
|
authLogger.info("Auth reload requested too soon, skipping", {
|
|
timeSinceLastReload,
|
|
cooldown: RELOAD_COOLDOWN,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (isLoading()) {
|
|
authLogger.info("Auth is already loading, waiting for completion");
|
|
await loadingPromise;
|
|
return true;
|
|
}
|
|
|
|
// Set reloading state
|
|
merge({ isReloading: true, lastReloadTime: now });
|
|
|
|
try {
|
|
startLoading();
|
|
|
|
authLogger.debug("Deleting userToken for refresh");
|
|
await secureStore.deleteItemAsync(STORAGE_KEYS.USER_TOKEN);
|
|
|
|
await init();
|
|
return true;
|
|
} catch (error) {
|
|
authLogger.error("Auth reload failed", { error: error.message });
|
|
throw error;
|
|
} finally {
|
|
// Clear reloading state even if there was an error
|
|
merge({ isReloading: false });
|
|
}
|
|
};
|
|
|
|
const onReload = async () => {
|
|
authLogger.info("Handling auth reload");
|
|
const { onReloadAuthToken: authToken } = get();
|
|
|
|
if (authToken) {
|
|
await secureStore.setItemAsync(STORAGE_KEYS.AUTH_TOKEN, authToken);
|
|
await loadUserJWT(authToken);
|
|
} else {
|
|
await init();
|
|
}
|
|
|
|
navActions.setNextNavigation([
|
|
{
|
|
name: "Profile",
|
|
},
|
|
]);
|
|
};
|
|
|
|
const triggerReload = () => {
|
|
treeActions.triggerReload(onReload);
|
|
};
|
|
const confirmLoginRequest = async ({ authTokenJwt, isConnected }) => {
|
|
authLogger.info("Confirming login request", { isConnected });
|
|
if (!isConnected) {
|
|
// backup anon tokens
|
|
const [anonAuthToken, anonUserToken] = await Promise.all([
|
|
secureStore.getItemAsync(STORAGE_KEYS.AUTH_TOKEN),
|
|
secureStore.getItemAsync(STORAGE_KEYS.USER_TOKEN),
|
|
]);
|
|
await Promise.all([
|
|
secureStore.setItemAsync(STORAGE_KEYS.ANON_AUTH_TOKEN, anonAuthToken),
|
|
secureStore.setItemAsync(STORAGE_KEYS.ANON_USER_TOKEN, anonUserToken),
|
|
]);
|
|
}
|
|
merge({ onReloadAuthToken: authTokenJwt });
|
|
triggerReload();
|
|
};
|
|
|
|
const impersonate = async ({ authTokenJwt }) => {
|
|
authLogger.info("Starting impersonation");
|
|
const [anonAuthToken, anonUserToken] = await Promise.all([
|
|
secureStore.getItemAsync(STORAGE_KEYS.AUTH_TOKEN),
|
|
secureStore.getItemAsync(STORAGE_KEYS.USER_TOKEN),
|
|
]);
|
|
await Promise.all([
|
|
secureStore.setItemAsync(STORAGE_KEYS.DEV_AUTH_TOKEN, anonAuthToken),
|
|
secureStore.setItemAsync(STORAGE_KEYS.DEV_USER_TOKEN, anonUserToken),
|
|
]);
|
|
merge({ onReloadAuthToken: authTokenJwt });
|
|
triggerReload();
|
|
};
|
|
|
|
const logout = async () => {
|
|
authLogger.info("Initiating logout");
|
|
const [devAuthToken, devUserToken, anonAuthToken, anonUserToken] =
|
|
await Promise.all([
|
|
secureStore.getItemAsync(STORAGE_KEYS.DEV_AUTH_TOKEN),
|
|
secureStore.getItemAsync(STORAGE_KEYS.DEV_USER_TOKEN),
|
|
secureStore.getItemAsync(STORAGE_KEYS.ANON_AUTH_TOKEN),
|
|
secureStore.getItemAsync(STORAGE_KEYS.ANON_USER_TOKEN),
|
|
]);
|
|
if (devAuthToken && devUserToken) {
|
|
await Promise.all([
|
|
secureStore.setItemAsync(STORAGE_KEYS.AUTH_TOKEN, devAuthToken),
|
|
secureStore.setItemAsync(STORAGE_KEYS.USER_TOKEN, devUserToken),
|
|
secureStore.deleteItemAsync(STORAGE_KEYS.DEV_AUTH_TOKEN),
|
|
secureStore.deleteItemAsync(STORAGE_KEYS.DEV_USER_TOKEN),
|
|
]);
|
|
} else if (anonAuthToken && anonUserToken) {
|
|
await Promise.all([
|
|
secureStore.setItemAsync(STORAGE_KEYS.AUTH_TOKEN, anonAuthToken),
|
|
secureStore.setItemAsync(STORAGE_KEYS.USER_TOKEN, anonUserToken),
|
|
secureStore.deleteItemAsync(STORAGE_KEYS.ANON_AUTH_TOKEN),
|
|
secureStore.deleteItemAsync(STORAGE_KEYS.ANON_USER_TOKEN),
|
|
]);
|
|
} else {
|
|
await Promise.all([
|
|
secureStore.deleteItemAsync(STORAGE_KEYS.AUTH_TOKEN),
|
|
secureStore.deleteItemAsync(STORAGE_KEYS.USER_TOKEN),
|
|
]);
|
|
merge({
|
|
userOffMode: true,
|
|
});
|
|
return;
|
|
}
|
|
|
|
triggerReload();
|
|
};
|
|
|
|
const userOnMode = () => {
|
|
authLogger.info("Enabling user mode");
|
|
merge({
|
|
userOffMode: false,
|
|
});
|
|
triggerReload();
|
|
};
|
|
|
|
const setUserToken = async (userToken) => {
|
|
authLogger.info("Setting user token", {
|
|
hasToken: !!userToken,
|
|
});
|
|
|
|
try {
|
|
// Update secure storage
|
|
await secureStore.setItemAsync(STORAGE_KEYS.USER_TOKEN, userToken);
|
|
|
|
// Update in-memory state
|
|
merge({ userToken });
|
|
|
|
// Update session from JWT
|
|
if (userToken) {
|
|
const jwtData = jwtDecode(userToken);
|
|
sessionActions.loadSessionFromJWT(jwtData);
|
|
}
|
|
|
|
authLogger.debug("User token updated successfully");
|
|
} catch (error) {
|
|
authLogger.error("Failed to set user token", { error: error.message });
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
return {
|
|
default: {
|
|
userToken: null,
|
|
loading: true,
|
|
initialized: false,
|
|
onReload: false,
|
|
onReloadAuthToken: null,
|
|
userOffMode: false,
|
|
isReloading: false,
|
|
lastReloadTime: 0,
|
|
},
|
|
actions: {
|
|
init,
|
|
reload,
|
|
confirmLoginRequest,
|
|
impersonate,
|
|
logout,
|
|
onReload,
|
|
userOnMode,
|
|
setUserToken,
|
|
},
|
|
};
|
|
});
|