bstein-dev-home/frontend/src/member/useMemberAccessState.js
2026-06-29 13:23:07 -03:00

136 lines
4.0 KiB
JavaScript

import { computed, reactive, watch } from "vue";
import { auth, authFetch } from "../auth";
const state = reactive({
loading: false,
loaded: false,
status: "",
onboardingUrl: "/onboarding",
dashboardUrl: "/apps",
accountUrl: "/account",
accessAllowed: true,
error: "",
});
let watcherStarted = false;
let requestToken = 0;
function normalizeInternalUrl(value, fallback) {
if (typeof value !== "string" || !value.trim()) return fallback;
try {
const url = new URL(value, window.location.origin);
if (url.origin === window.location.origin) return `${url.pathname}${url.search}${url.hash}`;
return value;
} catch {
return value.startsWith("/") ? value : fallback;
}
}
function resetState() {
state.loading = false;
state.loaded = false;
state.status = "";
state.onboardingUrl = "/onboarding";
state.dashboardUrl = "/apps";
state.accountUrl = "/account";
state.accessAllowed = true;
state.error = "";
}
async function refreshMemberState() {
if (!auth.ready || !auth.authenticated) {
resetState();
return;
}
const token = (requestToken += 1);
state.loading = true;
state.error = "";
try {
const resp = await authFetch("/api/account/member-state", {
headers: { Accept: "application/json" },
cache: "no-store",
});
const data = await resp.json().catch(() => ({}));
if (token !== requestToken) return;
if (resp.status === 403) {
state.loaded = true;
state.status = "restricted";
state.accessAllowed = false;
state.onboardingUrl = "/request-access";
state.dashboardUrl = "/apps";
state.accountUrl = "/account";
return;
}
if (!resp.ok) throw new Error(data.error || `status ${resp.status}`);
state.loaded = true;
state.accessAllowed = true;
state.status = data.status || "unknown";
state.onboardingUrl = normalizeInternalUrl(data.onboarding_url, "/onboarding");
state.dashboardUrl = normalizeInternalUrl(data.dashboard_url, "/apps");
state.accountUrl = normalizeInternalUrl(data.account_url, "/account");
} catch (err) {
if (token !== requestToken) return;
state.loaded = true;
state.status = "unknown";
state.accessAllowed = true;
state.error = err?.message || "Member state unavailable";
} finally {
if (token === requestToken) state.loading = false;
}
}
function startWatcher() {
if (watcherStarted) return;
watcherStarted = true;
watch(
() => [auth.ready, auth.authenticated, auth.token],
() => {
refreshMemberState();
},
{ immediate: true },
);
}
function memberActionForState() {
if (!auth.enabled) return null;
if (!auth.ready) {
return { kind: "loading", label: "Checking Session", to: "", secondary: "" };
}
if (!auth.authenticated) {
return { kind: "signed-out", label: "Sign In", to: "", secondary: "/request-access" };
}
if (auth.emailVerified === false) {
return {
kind: "verify",
label: "Continue Verification",
to: normalizeInternalUrl(auth.accountUrl, "/request-access"),
secondary: state.accountUrl,
};
}
if (state.loading && !state.loaded) {
return { kind: "loading", label: "Checking Account", to: "", secondary: state.accountUrl };
}
if (["pending_email_verification", "pending"].includes(state.status)) {
return { kind: "pending", label: "Check Access Status", to: "/request-access", secondary: state.accountUrl };
}
if (["accounts_building", "awaiting_onboarding"].includes(state.status)) {
return { kind: "onboarding", label: "Continue Onboarding", to: state.onboardingUrl, secondary: state.accountUrl };
}
if (state.status === "restricted" || !state.accessAllowed) {
return { kind: "restricted", label: "Account Status", to: "/request-access", secondary: state.accountUrl };
}
return { kind: "ready", label: "Open Services", to: state.dashboardUrl, secondary: state.accountUrl };
}
export function useMemberAccessState() {
startWatcher();
return {
auth,
memberState: state,
memberAction: computed(memberActionForState),
refreshMemberState,
};
}