Files
bellsystems-cp/frontend/src/auth/AuthContext.jsx

134 lines
4.2 KiB
JavaScript

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 (
<AuthContext.Provider value={{ user, login, logout, loading, hasRole, hasPermission }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}