import { createContext, useContext, useState, useEffect } from "react"; import api from "../api/client"; const AuthContext = createContext(null); export function AuthProvider({ children }) { const [user, setUser] = useState(() => { const stored = localStorage.getItem("user"); return stored ? JSON.parse(stored) : null; }); const [loading, setLoading] = useState(true); useEffect(() => { const token = localStorage.getItem("access_token"); if (!token) { setUser(null); setLoading(false); return; } try { const payload = JSON.parse(atob(token.split(".")[1])); if (payload.exp * 1000 < Date.now()) { localStorage.removeItem("access_token"); localStorage.removeItem("user"); setUser(null); } } catch { localStorage.removeItem("access_token"); localStorage.removeItem("user"); setUser(null); } setLoading(false); }, []); const login = async (email, password) => { const data = await api.post("/auth/login", { email, password }); localStorage.setItem("access_token", data.access_token); const userInfo = { name: data.name, role: data.role, permissions: data.permissions || null, }; localStorage.setItem("user", JSON.stringify(userInfo)); setUser(userInfo); // Fetch full profile from /staff/me for up-to-date permissions try { const me = await api.get("/staff/me"); if (me.permissions) { const updated = { ...userInfo, permissions: me.permissions }; localStorage.setItem("user", JSON.stringify(updated)); setUser(updated); } } catch { // Non-critical, permissions from login response are used } return data; }; const logout = () => { localStorage.removeItem("access_token"); localStorage.removeItem("user"); setUser(null); }; const hasRole = (...roles) => { if (!user) return false; if (user.role === "sysadmin") return true; return roles.includes(user.role); }; /** * hasPermission(section, action) * * Sections and their action keys: * melodies: view, add, delete, safe_edit, full_edit, archetype_access, settings_access, compose_access * devices: view, add, delete, safe_edit, edit_bells, edit_clock, edit_warranty, full_edit, control * app_users: view, add, delete, safe_edit, full_edit * issues_notes: view, add, delete, edit * mail: view, compose, reply * crm: activity_log * crm_customers: full_access, overview, orders_view, orders_edit, quotations_view, quotations_edit, * comms_view, comms_log, comms_edit, comms_compose, add, delete, * files_view, files_edit, devices_view, devices_edit * crm_orders: view (→ crm_customers.orders_view), edit (→ crm_customers.orders_edit) [derived] * crm_products: view, add, edit * mfg: view_inventory, edit, provision, firmware_view, firmware_edit * api_reference: access * mqtt: access */ const hasPermission = (section, action) => { if (!user) return false; // sysadmin and admin have full access if (user.role === "sysadmin" || user.role === "admin") return true; const perms = user.permissions; if (!perms) return false; // crm_orders is derived from crm_customers if (section === "crm_orders") { const cc = perms.crm_customers; if (!cc) return false; if (cc.full_access) return true; if (action === "view") return !!cc.orders_view; if (action === "edit") return !!cc.orders_edit; return false; } const sectionPerms = perms[section]; if (!sectionPerms) return false; // crm_customers.full_access grants everything in that section if (section === "crm_customers" && sectionPerms.full_access) return true; return !!sectionPerms[action]; }; return ( {children} ); } export function useAuth() { const context = useContext(AuthContext); if (!context) { throw new Error("useAuth must be used within an AuthProvider"); } return context; }