setIsOpen(!isOpen)}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-gray-100 transition-colors"
>
{user.avatarUrl ? (
) : (
{initials}
)}
{displayName}
{isOpen && (
<>
setIsOpen(false)}
/>
{displayName}
{email && (
{email}
)}
{user.username && email !== user.username && (
@{user.username}
)}
{authMethod === 'oauth' ? 'Jira OAuth' : authMethod === 'local' ? 'Lokaal Account' : 'Service Account'}
{(authMethod === 'local' || authMethod === 'oauth') && (
<>
setIsOpen(false)}
className="block px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 transition-colors"
>
Profiel & Instellingen
>
)}
Uitloggen
>
)}
);
}
function AppContent() {
const location = useLocation();
const hasPermission = useAuthStore((state) => state.hasPermission);
const config = useAuthStore((state) => state.config);
// Navigation structure - Logical flow for CMDB setup and data management
// Flow: 1. Setup (Schema Discovery → Configuration → Sync) → 2. Data (Model → Validation) → 3. Application Component → 4. Reports
const appComponentsDropdown: NavDropdown = {
label: 'Application Component',
basePath: '/application',
items: [
{ path: '/app-components', label: 'Dashboard', exact: true, requiredPermission: 'search' },
{ path: '/application/overview', label: 'Overzicht', exact: false, requiredPermission: 'search' },
],
};
const reportsDropdown: NavDropdown = {
label: 'Rapporten',
basePath: '/reports',
items: [
{ path: '/reports', label: 'Overzicht', exact: true, requiredPermission: 'view_reports' },
{ path: '/reports/team-dashboard', label: 'Team-indeling', exact: true, requiredPermission: 'view_reports' },
{ path: '/reports/governance-analysis', label: 'Analyse Regiemodel', exact: true, requiredPermission: 'view_reports' },
{ path: '/reports/technical-debt-heatmap', label: 'Technical Debt Heatmap', exact: true, requiredPermission: 'view_reports' },
{ path: '/reports/lifecycle-pipeline', label: 'Lifecycle Pipeline', exact: true, requiredPermission: 'view_reports' },
{ path: '/reports/data-completeness', label: 'Data Completeness Score', exact: true, requiredPermission: 'view_reports' },
{ path: '/reports/zira-domain-coverage', label: 'ZiRA Domain Coverage', exact: true, requiredPermission: 'view_reports' },
{ path: '/reports/fte-per-zira-domain', label: 'FTE per ZiRA Domain', exact: true, requiredPermission: 'view_reports' },
{ path: '/reports/complexity-dynamics-bubble', label: 'Complexity vs Dynamics Bubble Chart', exact: true, requiredPermission: 'view_reports' },
{ path: '/reports/business-importance-comparison', label: 'Business Importance vs BIA', exact: true, requiredPermission: 'view_reports' },
],
};
const appsDropdown: NavDropdown = {
label: 'Apps',
basePath: '/apps',
items: [
{ path: '/apps/bia-sync', label: 'BIA Sync', exact: true, requiredPermission: 'search' },
{ path: '/apps/fte-calculator', label: 'FTE Calculator', exact: true, requiredPermission: 'search' },
],
};
const settingsDropdown: NavDropdown = {
label: 'Instellingen',
basePath: '/settings',
items: [
{ path: '/settings/data-completeness-config', label: 'Data Completeness Config', exact: true, requiredPermission: 'manage_settings' },
{ path: '/settings/fte-config', label: 'FTE Config', exact: true, requiredPermission: 'manage_settings' },
],
};
const adminDropdown: NavDropdown = {
label: 'Beheer',
basePath: '/admin',
items: [
{ path: '/settings/schema-configuration', label: 'Schema Configuratie & Datamodel', exact: true, requiredPermission: 'manage_settings' },
{ path: '/settings/data-validation', label: 'Data Validatie', exact: true, requiredPermission: 'manage_settings' },
{ path: '/admin/users', label: 'Gebruikers', exact: true, requiredPermission: 'manage_users' },
{ path: '/admin/roles', label: 'Rollen', exact: true, requiredPermission: 'manage_roles' },
{ path: '/admin/debug', label: 'Architecture Debug', exact: true, requiredPermission: 'admin' },
],
};
const isAppComponentsActive = location.pathname.startsWith('/app-components') || location.pathname.startsWith('/application');
const isReportsActive = location.pathname.startsWith('/reports');
// Settings is active for /settings paths EXCEPT admin items (schema-configuration, data-model, data-validation)
const isSettingsActive = location.pathname.startsWith('/settings')
&& !location.pathname.startsWith('/settings/schema-configuration')
&& !location.pathname.startsWith('/settings/data-model')
&& !location.pathname.startsWith('/settings/data-validation');
const isAppsActive = location.pathname.startsWith('/apps');
const isAdminActive = location.pathname.startsWith('/admin') || location.pathname.startsWith('/settings/schema-configuration') || location.pathname.startsWith('/settings/data-model') || location.pathname.startsWith('/settings/data-validation');
return (
{/* Header */}
{config?.appName || 'CMDB Insight'}
{config?.appTagline || 'Management console for Jira Assets'}
{/* Application Component Dropdown */}
{/* Reports Dropdown */}
{/* Apps Dropdown */}
{/* Settings Dropdown - Advanced configuration */}
{/* Admin Dropdown - Setup (Schema Config + Data Model + Data Validation) + Administration */}
{/* Toast Notifications */}
{/* Main content */}
}>
{/* Main Dashboard (Search) */}
} />
{/* Application routes (new structure) - specific routes first, then dynamic */}
} />
} />
} />
{/* Application Component routes */}
} />
{/* Reports routes */}
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
{/* Apps routes */}
} />
} />
{/* Settings routes */}
} />
} />
} />
} />
} />
} />
{/* Legacy redirects for old routes */}
} />
{/* Admin routes */}
} />
} />
} />
{/* Legacy redirects for bookmarks - redirect old paths to new ones */}
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
);
}
function App() {
const { isAuthenticated, checkAuth, fetchConfig, config, user, authMethod, isInitialized, setInitialized, setConfig } = useAuthStore();
const location = useLocation();
useEffect(() => {
// Use singleton pattern to ensure initialization happens only once
// This works across React StrictMode remounts
// Check if already initialized by checking store state
const currentState = useAuthStore.getState();
if (currentState.config && currentState.isInitialized) {
return;
}
// If already initializing, wait for existing promise
if (initializationPromise) {
return;
}
// Create singleton initialization promise
// OPTIMIZATION: Run config and auth checks in parallel instead of sequentially
initializationPromise = (async () => {
try {
const state = useAuthStore.getState();
const defaultConfig = {
appName: 'CMDB Insight',
appTagline: 'Management console for Jira Assets',
appCopyright: `© ${new Date().getFullYear()} Zuyderland Medisch Centrum`,
authMethod: 'local' as const,
oauthEnabled: false,
serviceAccountEnabled: false,
localAuthEnabled: true,
jiraHost: '',
};
// Parallelize API calls - this is the key optimization!
// Instead of waiting for config then auth (sequential), do both at once
await Promise.allSettled([
state.config ? Promise.resolve() : fetchConfig(),
checkAuth(),
]);
// Ensure config is set (use fetched or default)
const stateAfterInit = useAuthStore.getState();
if (!stateAfterInit.config) {
setConfig(defaultConfig);
}
// Ensure isLoading is false
const finalState = useAuthStore.getState();
if (finalState.isLoading) {
const { setLoading } = useAuthStore.getState();
setLoading(false);
}
setInitialized(true);
} catch (error) {
console.error('[App] Initialization error:', error);
// Always mark as initialized to prevent infinite loading
const state = useAuthStore.getState();
if (!state.config) {
setConfig({
appName: 'CMDB Insight',
appTagline: 'Management console for Jira Assets',
appCopyright: `© ${new Date().getFullYear()} Zuyderland Medisch Centrum`,
authMethod: 'local',
oauthEnabled: false,
serviceAccountEnabled: false,
localAuthEnabled: true,
jiraHost: '',
});
}
setInitialized(true);
}
})();
// Reduced timeout since we're optimizing - 1.5 seconds should be plenty
const timeoutId = setTimeout(() => {
const state = useAuthStore.getState();
if (!state.config) {
setConfig({
appName: 'CMDB Insight',
appTagline: 'Management console for Jira Assets',
appCopyright: `© ${new Date().getFullYear()} Zuyderland Medisch Centrum`,
authMethod: 'local',
oauthEnabled: false,
serviceAccountEnabled: false,
localAuthEnabled: true,
jiraHost: '',
});
}
setInitialized(true);
}, 1500);
return () => {
clearTimeout(timeoutId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Empty deps - functions from store are stable
// Auth routes that should render outside the main layout
const isAuthRoute = ['/login', '/forgot-password', '/reset-password', '/accept-invitation'].includes(location.pathname);
// Handle missing config after initialization using useEffect
useEffect(() => {
if (isInitialized && !config) {
setConfig({
appName: 'CMDB Insight',
appTagline: 'Management console for Jira Assets',
appCopyright: `© ${new Date().getFullYear()} Zuyderland Medisch Centrum`,
authMethod: 'local',
oauthEnabled: false,
serviceAccountEnabled: false,
localAuthEnabled: true,
jiraHost: '',
});
}
}, [isInitialized, config, setConfig]);
// Get current config from store (might be updated by useEffect above)
const currentConfig = config || useAuthStore.getState().config;
// If on an auth route, render it directly (no layout) - don't wait for config
if (isAuthRoute) {
return (
}>
} />
} />
} />
} />
);
}
// For non-auth routes, we need config
// Show loading ONLY if we don't have config
// Once initialized and we have config, proceed even if isLoading is true
// (isLoading might be stuck due to StrictMode duplicate calls)
if (!currentConfig) {
return (
);
}
// STRICT AUTHENTICATION CHECK:
// Service accounts are NOT used for application authentication
// They are only for Jira API access (JIRA_SERVICE_ACCOUNT_TOKEN in .env)
// Application authentication ALWAYS requires a real user session (local or OAuth)
// Check if this is a service account user (should never happen, but reject if it does)
const isServiceAccount = user?.accountId === 'service-account' || authMethod === 'service-account';
// Check if user is a real authenticated user (has id, not service account)
const isRealUser = isAuthenticated && user && user.id && !isServiceAccount;
// ALWAYS reject service account users - they are NOT valid for application authentication
if (isServiceAccount) {
return
;
}
// If not authenticated as a real user, redirect to login
if (!isRealUser) {
return
;
}
// Real user authenticated - allow access
// At this point, user is either:
// 1. Authenticated (isAuthenticated === true), OR
// 2. Service account is explicitly allowed (allowServiceAccount === true)
// Show main app
return
;
}
export default App;