as-app/src/stores/auth.js
2025-07-02 00:54:30 +02:00

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,
},
};
});