Production readiness hardening and ops tooling
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
const ADMIN_TOKEN_KEY = "admin_token";
|
||||
const ADMIN_PROFILE_KEY = "admin_profile";
|
||||
|
||||
function formatMoney(value) {
|
||||
const number = Number(value || 0);
|
||||
@ -90,11 +91,64 @@ async function adminFetch(path, options = {}) {
|
||||
return payload?.data !== undefined ? payload.data : payload;
|
||||
}
|
||||
|
||||
function hasPermission(profile, permission) {
|
||||
if (!permission) {
|
||||
return true;
|
||||
}
|
||||
const permissions = profile?.permissions;
|
||||
if (!permissions) {
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(permissions)) {
|
||||
return permissions.includes("*") || permissions.includes(permission);
|
||||
}
|
||||
if (permissions.admin === "*" || permissions.admin === true) {
|
||||
return true;
|
||||
}
|
||||
if (permissions[permission] === "*" || permissions[permission] === true) {
|
||||
return true;
|
||||
}
|
||||
const [domain, action] = permission.split(":");
|
||||
const domainValue = permissions[domain];
|
||||
if (domainValue === "*" || domainValue === true) {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(domainValue)) {
|
||||
return domainValue.includes("*") || domainValue.includes(action);
|
||||
}
|
||||
if (domainValue && typeof domainValue === "object") {
|
||||
return domainValue[action] === "*" || domainValue[action] === true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getStoredProfile() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(ADMIN_PROFILE_KEY) || "null");
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function applyPermissionAttributes(profile) {
|
||||
document.querySelectorAll("[data-admin-permission]").forEach((el) => {
|
||||
const allowed = hasPermission(profile, el.getAttribute("data-admin-permission"));
|
||||
el.classList.toggle("hidden", !allowed);
|
||||
if ("disabled" in el) {
|
||||
el.disabled = !allowed;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.AdminUIAPI = {
|
||||
ADMIN_TOKEN_KEY,
|
||||
ADMIN_PROFILE_KEY,
|
||||
setToken: (token) => localStorage.setItem(ADMIN_TOKEN_KEY, token),
|
||||
getToken: () => localStorage.getItem(ADMIN_TOKEN_KEY),
|
||||
clearToken: () => localStorage.removeItem(ADMIN_TOKEN_KEY),
|
||||
clearToken: () => {
|
||||
localStorage.removeItem(ADMIN_TOKEN_KEY);
|
||||
localStorage.removeItem(ADMIN_PROFILE_KEY);
|
||||
},
|
||||
requireToken: () => {
|
||||
const token = localStorage.getItem(ADMIN_TOKEN_KEY);
|
||||
if (!token) {
|
||||
@ -111,9 +165,27 @@ window.AdminUIAPI = {
|
||||
});
|
||||
if (data?.token) {
|
||||
window.AdminUIAPI.setToken(data.token);
|
||||
if (data.user) {
|
||||
localStorage.setItem(ADMIN_PROFILE_KEY, JSON.stringify(data.user));
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
getMe: async () => {
|
||||
const profile = await adminFetch("/admin/me");
|
||||
localStorage.setItem(ADMIN_PROFILE_KEY, JSON.stringify(profile));
|
||||
return profile;
|
||||
},
|
||||
getStoredProfile,
|
||||
hasPermission: (permission, profile) => hasPermission(profile || getStoredProfile(), permission),
|
||||
applyPermissions: async () => {
|
||||
let profile = getStoredProfile();
|
||||
if (!profile?.permissions) {
|
||||
profile = await window.AdminUIAPI.getMe();
|
||||
}
|
||||
applyPermissionAttributes(profile);
|
||||
return profile;
|
||||
},
|
||||
listMerchants: () => adminFetch("/admin/merchants"),
|
||||
listOutlets: (query) => adminFetch("/admin/outlets", { query }),
|
||||
getOutlet: (id) => adminFetch(`/admin/outlets/${id}`),
|
||||
@ -133,6 +205,11 @@ window.AdminUIAPI = {
|
||||
getTerminal: (id) => adminFetch(`/admin/terminals/${id}`),
|
||||
listDevices: (query) => adminFetch("/admin/devices", { query }),
|
||||
getDevice: (id) => adminFetch(`/admin/devices/${id}`),
|
||||
rotateDeviceCredential: (id) =>
|
||||
adminFetch(`/admin/devices/${id}/credentials/rotate`, {
|
||||
method: "POST",
|
||||
body: {}
|
||||
}),
|
||||
getDeviceHeartbeats: (id, query) =>
|
||||
adminFetch(`/admin/devices/${id}/heartbeats`, { query }),
|
||||
getDeviceConfig: (id) => adminFetch(`/admin/devices/${id}/config`),
|
||||
@ -143,7 +220,87 @@ window.AdminUIAPI = {
|
||||
body: payload || {}
|
||||
}),
|
||||
listTransactions: (query) => adminFetch("/admin/transactions", { query }),
|
||||
getDynamicQrExpiryScheduler: () => adminFetch("/admin/transactions/expiry-scheduler"),
|
||||
createSettlementBatches: (payload) =>
|
||||
adminFetch("/admin/settlement-batches", {
|
||||
method: "POST",
|
||||
body: payload || {}
|
||||
}),
|
||||
listSettlementBatches: (query) => adminFetch("/admin/settlement-batches", { query }),
|
||||
getSettlementBatch: (id) => adminFetch(`/admin/settlement-batches/${id}`),
|
||||
listSettlementAdjustments: (query) =>
|
||||
adminFetch("/admin/settlement-adjustments", { query }),
|
||||
createSettlementAdjustmentExportJob: (payload) =>
|
||||
adminFetch("/admin/exports/settlement-adjustments", {
|
||||
method: "POST",
|
||||
body: payload || {}
|
||||
}),
|
||||
getExportJob: (id) => adminFetch(`/admin/exports/${id}`),
|
||||
listExportJobs: (query) => adminFetch("/admin/exports", { query }),
|
||||
downloadExportJob: async (id) => {
|
||||
const token = localStorage.getItem(ADMIN_TOKEN_KEY);
|
||||
if (!token) {
|
||||
throw new Error("ADMIN_AUTH_MISSING");
|
||||
}
|
||||
const response = await fetch(`/admin/exports/${id}/download`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Export download failed: ${response.status}`);
|
||||
}
|
||||
return {
|
||||
blob: await response.blob(),
|
||||
filename:
|
||||
response.headers.get("content-disposition")?.match(/filename="([^"]+)"/)?.[1] ||
|
||||
`${id}.csv`
|
||||
};
|
||||
},
|
||||
getSettlementReconciliationReport: (query) =>
|
||||
adminFetch("/admin/reconciliation/settlement-batches", { query }),
|
||||
markSettlementBatchPaid: (id, payload) =>
|
||||
adminFetch(`/admin/settlement-batches/${id}/mark-paid`, {
|
||||
method: "POST",
|
||||
body: payload || {}
|
||||
}),
|
||||
updateSettlementBatchReference: (id, payload) =>
|
||||
adminFetch(`/admin/settlement-batches/${id}/reference`, {
|
||||
method: "PATCH",
|
||||
body: payload || {}
|
||||
}),
|
||||
recordSettlementBatchAdjustment: (id, payload) =>
|
||||
adminFetch(`/admin/settlement-batches/${id}/adjustments`, {
|
||||
method: "POST",
|
||||
body: payload || {}
|
||||
}),
|
||||
approveSettlementAdjustment: (id, payload) =>
|
||||
adminFetch(`/admin/settlement-adjustments/${id}/approve`, {
|
||||
method: "POST",
|
||||
body: payload || {}
|
||||
}),
|
||||
rejectSettlementAdjustment: (id, payload) =>
|
||||
adminFetch(`/admin/settlement-adjustments/${id}/reject`, {
|
||||
method: "POST",
|
||||
body: payload || {}
|
||||
}),
|
||||
markSettlementBatchFailed: (id, payload) =>
|
||||
adminFetch(`/admin/settlement-batches/${id}/mark-failed`, {
|
||||
method: "POST",
|
||||
body: payload || {}
|
||||
}),
|
||||
cancelSettlementBatch: (id, payload) =>
|
||||
adminFetch(`/admin/settlement-batches/${id}/cancel`, {
|
||||
method: "POST",
|
||||
body: payload || {}
|
||||
}),
|
||||
reprocessSettlementBatch: (id) =>
|
||||
adminFetch(`/admin/settlement-batches/${id}/reprocess`, {
|
||||
method: "POST",
|
||||
body: {}
|
||||
}),
|
||||
getDashboardSummary: () => adminFetch("/admin/dashboard/summary"),
|
||||
getMqttStatus: (query) => adminFetch("/admin/mqtt/status", { query }),
|
||||
listFailedNotifications: (query) => adminFetch("/admin/notifications/failed", { query }),
|
||||
listAuditLogs: (query) => adminFetch("/admin/audit-logs", { query }),
|
||||
formatMoney,
|
||||
formatDateTime
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user