intermittent commit
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { Navigate, Route, Routes } from "react-router-dom";
|
||||
import { lazy, Suspense, useEffect, useMemo, useState } from "react";
|
||||
import { fetchMe, updateProfile } from "./api/auth";
|
||||
import { fetchSession, fetchShellAuth, updateProfile } from "./api/auth";
|
||||
import { fetchPlatformModules, fetchPlatformStatus } from "./api/platform";
|
||||
import { AUTH_REQUIRED_EVENT, loadApiSettings, saveApiSettings, type AuthRequiredEventDetail } from "./api/client";
|
||||
import type { ApiSettings, AuthInfo, AuthTenant, AuthTenantMembership, AuthUser, LoginResponse, PlatformModuleInfo, PlatformWebModule, UserUiPreferences } from "./types";
|
||||
import type { ApiSettings, AuthInfo, AuthSessionInfo, AuthUpdate, AuthUser, LoginResponse, PlatformModuleInfo, PlatformWebModule, UserUiPreferences } from "./types";
|
||||
import AppShell from "./layout/AppShell";
|
||||
import PublicLandingPage from "./features/auth/PublicLandingPage";
|
||||
import LoginModal from "./features/auth/LoginModal";
|
||||
@@ -47,9 +47,9 @@ export default function App() {
|
||||
saveApiSettings(next);
|
||||
}
|
||||
|
||||
function updateAuth(next: AuthInfo | null, accessToken?: string) {
|
||||
function updateAuth(next: AuthUpdate | null, accessToken?: string) {
|
||||
const nextSettings = accessToken !== undefined ? { ...settings, accessToken } : settings;
|
||||
setAuth(next ? normalizeAuthInfo(next) : null);
|
||||
setAuth((current) => next ? normalizeAuthInfo(mergeAuthPayload(current, next)) : null);
|
||||
if (accessToken !== undefined) {
|
||||
setSettings(nextSettings);
|
||||
saveApiSettings(nextSettings);
|
||||
@@ -117,21 +117,26 @@ export default function App() {
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setCheckingSession(true);
|
||||
fetchMe(settings).
|
||||
then((me) => {if (!cancelled) setAuth(normalizeAuthInfo(me));}).
|
||||
catch(() => {
|
||||
if (!cancelled) {
|
||||
const cleared = { ...settings, accessToken: "" };
|
||||
setSettings(cleared);
|
||||
saveApiSettings(cleared);
|
||||
setAuth(null);
|
||||
setPlatformModules(null);
|
||||
setRemoteWebModules([]);
|
||||
|
||||
async function bootstrapAuth() {
|
||||
try {
|
||||
const shellAuth = await fetchShellAuth(settings);
|
||||
if (!cancelled) setAuth(normalizeAuthInfo(shellAuth));
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
const cleared = { ...settings, accessToken: "" };
|
||||
setSettings(cleared);
|
||||
saveApiSettings(cleared);
|
||||
setAuth(null);
|
||||
setPlatformModules(null);
|
||||
setRemoteWebModules([]);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setCheckingSession(false);
|
||||
}
|
||||
}).
|
||||
finally(() => {
|
||||
if (!cancelled) setCheckingSession(false);
|
||||
});
|
||||
}
|
||||
|
||||
void bootstrapAuth();
|
||||
return () => {cancelled = true;};
|
||||
}, [settings.apiBaseUrl, settings.apiKey]);
|
||||
|
||||
@@ -180,11 +185,25 @@ export default function App() {
|
||||
root.classList.toggle("ui-hide-help-hints", !preferences.show_inline_help_hints);
|
||||
root.classList.toggle("ui-reduce-motion", preferences.reduce_motion);
|
||||
root.classList.toggle("ui-no-sticky-section-sidebars", !preferences.sticky_section_sidebars);
|
||||
if (preferences.theme === "system") {
|
||||
delete root.dataset.theme;
|
||||
} else {
|
||||
root.dataset.theme = preferences.theme;
|
||||
|
||||
const systemDarkQuery = window.matchMedia?.("(prefers-color-scheme: dark)") ?? null;
|
||||
const applyTheme = () => {
|
||||
const resolvedTheme = preferences.theme === "system" ?
|
||||
systemDarkQuery?.matches ? "dark" : "light" :
|
||||
preferences.theme;
|
||||
root.dataset.theme = resolvedTheme;
|
||||
root.dataset.themePreference = preferences.theme;
|
||||
};
|
||||
|
||||
applyTheme();
|
||||
if (preferences.theme !== "system" || !systemDarkQuery) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
systemDarkQuery.addEventListener("change", applyTheme);
|
||||
return () => {
|
||||
systemDarkQuery.removeEventListener("change", applyTheme);
|
||||
};
|
||||
}, [
|
||||
auth?.user.ui_preferences?.compact_tables,
|
||||
auth?.user.ui_preferences?.show_inline_help_hints,
|
||||
@@ -208,8 +227,11 @@ export default function App() {
|
||||
useEffect(() => {
|
||||
if (!auth) return;
|
||||
|
||||
const currentAuth = auth;
|
||||
let cancelled = false;
|
||||
let inFlight = false;
|
||||
let lastRefreshAt = 0;
|
||||
let lastShellRefreshAt = Date.now();
|
||||
|
||||
async function refreshVisibleSession() {
|
||||
if (document.visibilityState === "hidden" || inFlight) return;
|
||||
@@ -219,7 +241,17 @@ export default function App() {
|
||||
inFlight = true;
|
||||
lastRefreshAt = now;
|
||||
try {
|
||||
setAuth(normalizeAuthInfo(await fetchMe(settings)));
|
||||
const sessionInfo = await fetchSession(settings);
|
||||
if (cancelled) return;
|
||||
const shellRefreshDue = now - lastShellRefreshAt >= 60_000;
|
||||
if (!sessionMatchesAuth(sessionInfo, currentAuth) || shellRefreshDue) {
|
||||
const shellAuth = await fetchShellAuth(settings);
|
||||
if (cancelled) return;
|
||||
lastShellRefreshAt = Date.now();
|
||||
setAuth((current) => current && sessionMatchesAuth(sessionInfo, current)
|
||||
? normalizeAuthInfo(mergeAuthPayload(current, shellAuth))
|
||||
: normalizeAuthInfo(shellAuth));
|
||||
}
|
||||
} catch {
|
||||
|
||||
|
||||
@@ -230,6 +262,7 @@ export default function App() {
|
||||
window.addEventListener("focus", refreshVisibleSession);
|
||||
document.addEventListener("visibilitychange", refreshVisibleSession);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.removeEventListener("focus", refreshVisibleSession);
|
||||
document.removeEventListener("visibilitychange", refreshVisibleSession);
|
||||
};
|
||||
@@ -327,18 +360,40 @@ export default function App() {
|
||||
|
||||
}
|
||||
|
||||
type AuthPayload = Partial<AuthInfo> & {
|
||||
principal?: AuthInfo["principal"];
|
||||
user?: Partial<AuthUser> | null;
|
||||
tenant?: AuthTenant | null;
|
||||
active_tenant?: AuthTenant | null;
|
||||
tenants?: AuthTenantMembership[] | null;
|
||||
};
|
||||
type AuthPayload = AuthUpdate;
|
||||
|
||||
function mergeAuthPayload(current: AuthInfo | null, next: AuthPayload): AuthPayload {
|
||||
if (!current) return next;
|
||||
const nextActiveTenant = next.active_tenant ?? next.tenant ?? null;
|
||||
const currentActiveTenant = current.active_tenant ?? current.tenant;
|
||||
const tenantChanged = Boolean(nextActiveTenant && nextActiveTenant.id !== currentActiveTenant.id);
|
||||
return {
|
||||
...current,
|
||||
...next,
|
||||
user: next.user ? { ...current.user, ...next.user } : current.user,
|
||||
tenant: nextActiveTenant ?? current.tenant,
|
||||
active_tenant: nextActiveTenant ?? currentActiveTenant,
|
||||
tenants: next.tenants ?? current.tenants,
|
||||
scopes: next.scopes ?? current.scopes,
|
||||
roles: next.roles ?? (next.roles_loaded === false || tenantChanged ? [] : current.roles),
|
||||
groups: next.groups ?? (next.groups_loaded === false || tenantChanged ? [] : current.groups),
|
||||
principal: next.principal === undefined ? current.principal : next.principal,
|
||||
available_languages: next.available_languages ?? (tenantChanged ? undefined : current.available_languages),
|
||||
enabled_language_codes: next.enabled_language_codes ?? (tenantChanged ? undefined : current.enabled_language_codes),
|
||||
default_language: next.default_language ?? (tenantChanged ? undefined : current.default_language),
|
||||
profile_loaded: next.profile_loaded ?? (tenantChanged ? false : current.profile_loaded),
|
||||
roles_loaded: next.roles_loaded ?? (tenantChanged ? false : current.roles_loaded),
|
||||
groups_loaded: next.groups_loaded ?? (tenantChanged ? false : current.groups_loaded)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAuthInfo(response: AuthPayload): AuthInfo {
|
||||
const principal = response.principal ?? null;
|
||||
const activeTenant = response.active_tenant ?? response.tenant ?? response.tenants?.[0] ?? null;
|
||||
const user = normalizeAuthUser(response.user, principal);
|
||||
const profileLoaded = response.profile_loaded ?? hasFullProfilePayload(response);
|
||||
const rolesLoaded = response.roles_loaded ?? response.roles !== undefined;
|
||||
const groupsLoaded = response.groups_loaded ?? response.groups !== undefined;
|
||||
|
||||
if (!activeTenant) {
|
||||
throw new Error("Authentication response did not include an active tenant.");
|
||||
@@ -356,12 +411,25 @@ function normalizeAuthInfo(response: AuthPayload): AuthInfo {
|
||||
roles: response.roles ?? [],
|
||||
groups: response.groups ?? [],
|
||||
principal,
|
||||
available_languages: response.available_languages ?? [],
|
||||
enabled_language_codes: response.enabled_language_codes ?? activeTenant.enabled_language_codes ?? [],
|
||||
default_language: response.default_language ?? user.preferred_language ?? activeTenant.default_locale ?? "en"
|
||||
available_languages: response.available_languages,
|
||||
enabled_language_codes: profileLoaded ? response.enabled_language_codes ?? activeTenant.enabled_language_codes ?? [] : undefined,
|
||||
default_language: profileLoaded ? response.default_language ?? user.preferred_language ?? activeTenant.default_locale ?? "en" : undefined,
|
||||
profile_loaded: profileLoaded,
|
||||
roles_loaded: rolesLoaded,
|
||||
groups_loaded: groupsLoaded
|
||||
};
|
||||
}
|
||||
|
||||
function hasFullProfilePayload(response: AuthPayload): boolean {
|
||||
return Boolean(
|
||||
response.default_language ||
|
||||
response.available_languages ||
|
||||
response.enabled_language_codes ||
|
||||
response.roles ||
|
||||
response.groups
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeAuthUser(user: Partial<AuthUser> | null | undefined, principal: AuthInfo["principal"]): AuthUser | null {
|
||||
if (user?.id && user.account_id) {
|
||||
return {
|
||||
@@ -396,6 +464,17 @@ function normalizeAuthUser(user: Partial<AuthUser> | null | undefined, principal
|
||||
};
|
||||
}
|
||||
|
||||
function sessionMatchesAuth(sessionInfo: AuthSessionInfo, auth: AuthInfo): boolean {
|
||||
const activeTenant = auth.active_tenant ?? auth.tenant;
|
||||
if (sessionInfo.user.id !== auth.user.id) return false;
|
||||
if (sessionInfo.user.account_id !== auth.user.account_id) return false;
|
||||
if ((sessionInfo.active_tenant ?? sessionInfo.tenant).id !== activeTenant.id) return false;
|
||||
if (auth.principal?.auth_method && sessionInfo.auth_method !== auth.principal.auth_method) return false;
|
||||
if (auth.principal?.session_id && sessionInfo.session_id && auth.principal.session_id !== sessionInfo.session_id) return false;
|
||||
if (auth.principal?.api_key_id && sessionInfo.api_key_id && auth.principal.api_key_id !== sessionInfo.api_key_id) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function normalizeUiPreferences(value: Partial<UserUiPreferences> | null | undefined): UserUiPreferences {
|
||||
const theme = value?.theme === "light" || value?.theme === "dark" || value?.theme === "system" ? value.theme : "system";
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user