mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 16:40:56 +00:00
164 lines
6.2 KiB
TypeScript
164 lines
6.2 KiB
TypeScript
'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<void>;
|
|
logout: () => void;
|
|
setUser: (user: AdminUser, token: string) => void;
|
|
initialize: () => void;
|
|
hasPermission: (key: string) => boolean;
|
|
hasPermissionStrict: (key: string) => boolean;
|
|
}
|
|
|
|
export const useAuthStore = create<AuthState>((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);
|
|
},
|
|
}));
|