'use client'; import { create } from 'zustand'; import { AdminUser } from '@/types'; import axios from 'axios'; const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'; /** * Every permission key a single IAM position grants. * * A position's own `permissions[]` used to be the whole story. IAM now also * hangs grants off *position types* — the legacy singular `positionType` plus * the newer `positionTypes[]` array, whose entries expose * `positionTypePermissions[].permission.key` (note the extra `permission` * wrapper). Union all three rather than trust IAM to have merged them back into * `permissions[]`. Mirrors collectPermissionKeys() on the API. */ export function positionPermissionKeys(pos: any): string[] { const keys: string[] = (pos?.permissions ?? []) .map((p: any) => p?.key) .filter(Boolean) .map(String); const positionTypes: any[] = [pos?.positionType, ...(pos?.positionTypes ?? [])]; for (const pt of positionTypes) { for (const ptp of pt?.positionTypePermissions ?? []) { const key = ptp?.permission?.key; if (key) keys.push(String(key)); } } return keys; } function mapIamRole(roles: { key?: string }[]): 'ADMIN' | 'AGENT' | 'SUPERVISOR' { const keys = roles.map((r) => r.key ?? ''); if (keys.some((k) => k.includes('admin') || k === 'super_admin' || k === 'organization_admin')) return 'ADMIN'; if (keys.some((k) => k.includes('agent'))) return 'AGENT'; return 'SUPERVISOR'; } interface AuthState { user: AdminUser | null; token: string | null; refreshToken: string | null; isAuthenticated: boolean; /** `identifier` may be an email, phone number or username — always sent as `email`. */ login: (identifier: string, password: string) => Promise; logout: () => void; setUser: (user: AdminUser, token: string) => void; initialize: () => void; hasPermission: (key: string) => boolean; hasPermissionStrict: (key: string) => boolean; } export const useAuthStore = create((set, get) => ({ user: null, token: null, refreshToken: null, isAuthenticated: false, initialize: () => { if (typeof window === 'undefined') return; const token = localStorage.getItem('auth_token'); const userStr = localStorage.getItem('auth_user'); if (token && userStr) { try { const user = JSON.parse(userStr) as AdminUser; // backfill for sessions stored before permissions were added if (!user.permissions) user.permissions = []; if (user.isSuperAdmin === undefined) user.isSuperAdmin = false; if (user.isOrgAdmin === undefined) user.isOrgAdmin = false; set({ user, token, isAuthenticated: true }); } catch { localStorage.removeItem('auth_token'); localStorage.removeItem('auth_refresh_token'); localStorage.removeItem('auth_user'); } } }, login: async (identifier: string, password: string) => { // Step 1: IAM login — returns token + refreshToken only. // The IAM accepts an email, phone number or username in the `email` field. const loginRes = await axios.post(`${API_URL}/v1/auth/login`, { email: identifier, password }); const loginData = loginRes.data?.data ?? loginRes.data; const { token, refreshToken } = loginData; if (!token) throw new Error('No token received from server'); // Step 2: fetch full user from IAM /v1/auth/me — returns session.userInfo // employee is an array here (unlike /auth/me which transforms it to a single object via parseToken) const meRes = await axios.get(`${API_URL}/v1/auth/me`, { headers: { Authorization: `Bearer ${token}` }, }); const iamUser = meRes.data?.data ?? meRes.data; // Role permissions — flat array in data.permissions const rolePerms = (iamUser.permissions ?? []).map((p: any) => String(p.key)); // Position permissions — employee[] is an array here; each position contributes // its own permissions[] plus everything its position type(s) grant. const employeeArr: any[] = Array.isArray(iamUser.employee) ? iamUser.employee : []; const positionPerms = employeeArr.flatMap((emp: any) => (emp.positions ?? []).flatMap((pos: any) => positionPermissionKeys(pos)) ); const permissions = Array.from(new Set([...rolePerms, ...positionPerms])); const isSuperAdmin = iamUser.isSuperAdmin ?? false; const isOrgAdmin = iamUser.isOrganizationAdmin ?? false; // Block individual (passenger) accounts — backoffice requires at least one of: // super admin, org admin, an employee position, or an explicit permission. if (!isSuperAdmin && !isOrgAdmin && employeeArr.length === 0 && permissions.length === 0) { throw new Error('ACCESS_DENIED'); } const user: AdminUser = { id: iamUser.id, email: iamUser.email, fullName: iamUser.name?.en ?? iamUser.name?.am ?? iamUser.email, role: mapIamRole(iamUser.roles ?? []), active: true, permissions, isSuperAdmin, isOrgAdmin, }; localStorage.setItem('auth_token', token); localStorage.setItem('auth_user', JSON.stringify(user)); if (refreshToken) localStorage.setItem('auth_refresh_token', refreshToken); // cookie lets middleware detect auth without reading localStorage document.cookie = `auth_token=${token}; path=/; SameSite=Lax`; set({ user, token, refreshToken: refreshToken ?? null, isAuthenticated: true }); }, logout: () => { localStorage.removeItem('auth_token'); localStorage.removeItem('auth_refresh_token'); localStorage.removeItem('auth_user'); document.cookie = 'auth_token=; path=/; max-age=0'; set({ user: null, token: null, refreshToken: null, isAuthenticated: false }); }, setUser: (user: AdminUser, token: string) => { set({ user, token, isAuthenticated: true }); }, hasPermission: (key: string) => { const { user } = get(); if (!user) return false; if (user.isSuperAdmin || user.isOrgAdmin) return true; return user.permissions.includes(key); }, // No super-admin / org-admin bypass — mirrors PassengerStaffStrict on the API, // so we don't render actions that would 403. hasPermissionStrict: (key: string) => { const { user } = get(); if (!user) return false; return user.permissions.includes(key); }, }));