diff --git a/scratch/tag_counter.py b/scratch/tag_counter.py new file mode 100644 index 0000000..6cd7934 --- /dev/null +++ b/scratch/tag_counter.py @@ -0,0 +1,22 @@ +import sys + +def count_tags(file_path): + with open(file_path, 'r') as f: + content = f.read() + + div_open = content.count('') + motion_div_open = content.count('') + paren_open = content.count('(') + paren_close = content.count(')') + brace_open = content.count('{') + brace_close = content.count('}') + + print(f"div: open={div_open}, close={div_close}, diff={div_open - div_close}") + print(f"motion.div: open={motion_div_open}, close={motion_div_close}, diff={motion_div_open - motion_div_close}") + print(f"parentheses: open={paren_open}, close={paren_close}, diff={paren_open - paren_close}") + print(f"braces: open={brace_open}, close={brace_close}, diff={brace_open - brace_close}") + +if __name__ == "__main__": + count_tags(sys.argv[1]) diff --git a/src/App.tsx b/src/App.tsx index 3a3436d..97e2256 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,6 +21,7 @@ import { FleetLogin } from './pages/FleetLogin'; import { FleetOperatorDashboard } from './pages/FleetOperatorDashboard'; import { PerspectiveLauncher } from './pages/PerspectiveLauncher'; import { RoleLogin } from './pages/RoleLogin'; +import { HospitalLogin } from './pages/HospitalLogin'; import { ComingSoonPortal } from './pages/ComingSoonPortal'; import { Building2, @@ -43,10 +44,13 @@ const RoleProtectedRoute: React.FC<{ if (!isAuthenticated) return ; - const userRoles = Array.isArray(user?.roles) ? user.roles : []; + const userRoles = Array.isArray(user?.roles) + ? user.roles.map((r: any) => String(r).toUpperCase().replace(/\s+/g, '_')) + : []; const hasAccess = allowedRoles.some(role => userRoles.includes(role)) || userRoles.includes('CURESELECT_ADMIN'); if (!hasAccess) { + console.log('[RBAC] Access Denied:', { required: allowedRoles, current: userRoles }); // Redirect to their respective "home" if they don't have access if (userRoles.includes('FLEET_OPERATOR')) return ; return ; @@ -121,6 +125,7 @@ function AppContent() { } /> } /> + } /> } /> } /> } /> diff --git a/src/api/apiClient.ts b/src/api/apiClient.ts index 262a305..8d2371b 100644 --- a/src/api/apiClient.ts +++ b/src/api/apiClient.ts @@ -25,60 +25,9 @@ export const apiClient = { defaultHeaders['Authorization'] = `Bearer ${authToken}`; } - // --- MOCK BYPASS FOR DEMO SESSIONS --- - if (authToken && ( - authToken.startsWith('mock-') || - authToken.startsWith('dev-token-') || - authToken === 'dev-super-token-2026' - )) { - return new Promise((resolve) => { - setTimeout(() => { - if (endpoint.includes('/v1/incidents')) { - resolve({ - status: 200, - data: [ - { - id: 'INC-MOCK-001', - category: 'MEDICAL', - severity: 'CRITICAL', - status: 'PENDING', - address: 'Sector 7G, Tactical Hub', - notes: 'High-priority mock incident for system validation.', - createdAt: new Date().toISOString(), - gps_lat: 13.0827, - gps_lon: 80.2707, - patients: [{ name: 'Tactical Test', age: 34, gender: 'Male', symptoms: ['None'], triage_code: 'RED' }] - } - ] - }); - } else if (endpoint.includes('/v1/auth/users')) { - resolve({ - status: 200, - data: [ - { id: 'u1', username: 'admin', roles: ['CURESELECT_ADMIN'], status: 'ACTIVE', email: 'admin@teleems.com' }, - { id: 'u2', username: 'fleet_op', roles: ['FLEET_OPERATOR'], status: 'ACTIVE', email: 'fleet@teleems.com' } - ] - }); - } else if (endpoint.includes('/v1/auth/audit-logs')) { - resolve({ - status: 200, - data: { - logs: [ - { id: 'l1', action: 'LOGIN_SUCCESS', createdAt: new Date().toISOString(), ipAddress: '127.0.0.1', user: { username: 'admin' } }, - { id: 'l2', action: 'INCIDENT_VIEW', createdAt: new Date().toISOString(), ipAddress: '127.0.0.1', user: { username: 'admin' } } - ], - total: 2 - } - }); - } else { - resolve({ status: 200, data: [] }); - } - }, 500); - }); - } const url = endpoint.startsWith('http') ? endpoint : `${BASE_URL}${endpoint}`; - + console.log(`[API] ${options.method || 'GET'} ${url}`); try { const response = await fetch(url, { headers: { ...defaultHeaders, ...headers }, @@ -86,8 +35,8 @@ export const apiClient = { }); // Handle session expiration - if (response.status === 401 || response.status === 403) { - console.warn('Unauthorized request detected. Triggering auto-logout...'); + if (response.status === 401 && !url.includes('/auth/login')) { + console.warn('Token expired or invalid. Triggering auto-logout...'); logout(); return null; // Return null as the app will redirect } diff --git a/src/api/auth.ts b/src/api/auth.ts index c6ff7a9..93573c0 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -21,6 +21,10 @@ export const authApi = { return apiClient.post('/v1/auth/mfa/totp/verify', { totp_code: totpCode }, { token }); }, + getProfile: async (token: string): Promise => { + return apiClient.get('/v1/auth/me', { token }); + }, + getAuditLogs: async (token: string, limit = 20, offset = 0) => { return apiClient.get(`/v1/auth/audit-logs?limit=${limit}&offset=${offset}`, { token }); }, diff --git a/src/api/hospital.ts b/src/api/hospital.ts new file mode 100644 index 0000000..e112094 --- /dev/null +++ b/src/api/hospital.ts @@ -0,0 +1,86 @@ +import { apiClient } from './apiClient'; +import { authApi } from './auth'; +import type { LoginResponse } from './types'; + +/** + * Hospital-specific API module. + * Wraps auth endpoints with hospital-context logic. + */ +export const hospitalApi = { + /** + * Authenticate a hospital user via the real auth API. + */ + login: async (username: string, password: string): Promise => { + return authApi.login(username, password); + }, + + /** + * Verify MFA for hospital user. + */ + verifyMfa: async (mfaSessionToken: string, totpCode: string): Promise => { + return authApi.verifyMfa(mfaSessionToken, totpCode); + }, + + /** + * Get hospital profile data from the authenticated user metadata. + */ + getProfile: async (token: string) => { + return apiClient.get('/v1/hospital/ops/profile', { token }); + }, + + /** + * Get departments scoped to a specific hospital. + */ + getDepartments: async (token: string, hospitalId?: string) => { + const endpoint = hospitalId + ? `/v1/hospital/departments?hospitalId=${hospitalId}` + : '/v1/hospital/departments'; + return apiClient.get(endpoint, { token }); + }, + + /** + * Get staff members scoped to a specific hospital. + */ + getStaff: async (token: string, hospitalId?: string) => { + const res = await authApi.getUsers(token); + if (res?.data && hospitalId) { + const filtered = (Array.isArray(res.data) ? res.data : []).filter( + (u: any) => u.hospitalId === hospitalId || u.organisationId === hospitalId + ); + return { ...res, data: filtered }; + } + return res; + }, + + /** + * Create a new department under a hospital. + */ + createDepartment: async (deptData: any, token: string) => { + return authApi.createDepartment(deptData, token); + }, + + /** + * Register a staff member under a hospital. + */ + registerStaff: async (staffData: any, token: string) => { + return authApi.registerUser(staffData, token); + }, + + updateProfile: async (payload: any, token: string) => { + return apiClient.patch('/v1/hospital/ops/profile', payload, { token }); + }, + + /** + * Get incoming operations/dispatches for the hospital. + */ + getIncomingOperations: async (token: string, hospitalId?: string) => { + const url = hospitalId + ? `/v1/hospital/ops/incoming?hospitalId=${hospitalId}` + : '/v1/hospital/ops/incoming'; + return apiClient.get(url, { token }); + }, + + admitPatient: async (patientId: string, departmentId: string, token: string) => { + return apiClient.post(`/v1/hospital/ops/incoming/${patientId}/admit`, { departmentId }, { token }); + }, +}; diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 85e12f9..6960b9c 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -1,4 +1,4 @@ -import React, { Component, ErrorInfo, ReactNode } from 'react'; +import React, { Component, type ErrorInfo, type ReactNode } from 'react'; import { AlertTriangle, RefreshCw } from 'lucide-react'; interface Props { diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 0e0ee57..9e0d649 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -25,7 +25,7 @@ export const Sidebar: React.FC = () => { const parsed = JSON.parse(stored); if (parsed && typeof parsed === 'object') { parsed.roles = Array.isArray(parsed.roles) - ? parsed.roles.map((r: any) => String(r).toUpperCase()) + ? parsed.roles.map((r: any) => String(r).toUpperCase().replace(/\s+/g, '_')) : []; return parsed; } @@ -54,7 +54,7 @@ export const Sidebar: React.FC = () => { const filteredNavItems = useMemo(() => { const userRoles = Array.isArray(user.roles) ? user.roles : []; const adminRoles = ['CURESELECT_ADMIN', 'ADMIN', 'SUPER_ADMIN', 'SUPERADMIN']; - const hasAdminRole = userRoles.some(r => adminRoles.includes(r)); + const hasAdminRole = userRoles.some((r: string) => adminRoles.includes(r)); const filterItems = (items: NavItem[]): NavItem[] => { return items.filter(item => { @@ -66,7 +66,9 @@ export const Sidebar: React.FC = () => { })); }; - return filterItems(NAVIGATION_CONFIG); + const result = filterItems(NAVIGATION_CONFIG); + console.log('[DEBUG] userRoles:', userRoles, 'filteredItems:', result); + return result; }, [user.roles]); const renderNavItem = (item: NavItem, isSubItem = false) => { @@ -79,34 +81,23 @@ export const Sidebar: React.FC = () => {
({ - display: 'flex', - alignItems: 'center', - gap: '12px', - padding: isSubItem ? '8px 20px 8px 48px' : '10px 20px', - textDecoration: 'none', - color: (linkActive || isActive) ? 'var(--accent-cyan)' : 'var(--text-secondary)', - borderLeft: !isSubItem && (linkActive || isActive) ? '3px solid var(--accent-cyan)' : '3px solid transparent', - background: (linkActive || isActive) ? 'rgba(59, 130, 246, 0.08)' : 'transparent', - transition: 'all 0.25s ease', - textShadow: (linkActive || isActive) ? '0 0 10px rgba(59, 130, 246, 0.4)' : 'none', - minWidth: 0, - position: 'relative' - })} + className={({ isActive: linkActive }) => + `${isSubItem ? 'sidebar-sub-item' : 'sidebar-nav-item'} ${(linkActive || isActive) ? 'active' : ''}` + } > {!isSubItem && } - {item.label} {hasSubItems && ( - isParentActive ? : + + {isParentActive ? : } + )} @@ -117,7 +108,7 @@ export const Sidebar: React.FC = () => { animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.3, ease: 'easeInOut' }} - style={{ overflow: 'hidden', background: 'rgba(255,255,255,0.02)' }} + style={{ overflow: 'hidden', background: 'transparent' }} > {item.subItems?.map(sub => renderNavItem(sub, true))} @@ -128,7 +119,7 @@ export const Sidebar: React.FC = () => { }; return ( -