117 lines
4.5 KiB
TypeScript
117 lines
4.5 KiB
TypeScript
import { defineStore } from "pinia";
|
|
import { ref, computed } from "vue";
|
|
import type { HakAksesPayload, HakAksesMenu } from "~/server/utils/schemas/permissionSchema";
|
|
|
|
export const usePermissionStore = defineStore("permission", () => {
|
|
const permissions = ref<HakAksesMenu[]>([]);
|
|
const isLoaded = ref(false);
|
|
const isError = ref(false);
|
|
|
|
/**
|
|
* Load permissions for the current user's role and group
|
|
*/
|
|
const load = async (role: string, group: string = "", username: string = "") => {
|
|
try {
|
|
const config = useRuntimeConfig();
|
|
const useMock = config.public.useMockPermissionApi;
|
|
|
|
let url = "";
|
|
if (useMock) {
|
|
url = `/api/hak-akses`; // Mock endpoint (returns all config)
|
|
} else {
|
|
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
|
|
// Ensure valid origin parsing
|
|
const origin = apiBase.startsWith('http') ? new URL(apiBase).origin : 'http://10.10.123.140:8089';
|
|
// In a real API, passing username might be needed if they support user-level overrides
|
|
url = `${origin}/api/v1/permission?roles=${role}&groups=${group}&username=${username}`;
|
|
}
|
|
|
|
const { data, error } = await useFetch<any>(url);
|
|
|
|
if (error.value) {
|
|
throw new Error(error.value.message || "Failed to fetch permissions");
|
|
}
|
|
|
|
isError.value = false;
|
|
|
|
// Handle Mock vs Real API response differences
|
|
if (useMock && data.value?.success) {
|
|
// Mock returns all permission configs. Find the matching one based on precedence:
|
|
const allConfigs = data.value.data as HakAksesPayload[];
|
|
|
|
// Precedence 0: Exact User Match (Specific Override)
|
|
let matchedConfig = allConfigs.find(c => c.namaTipeUser?.toLowerCase() === username.toLowerCase() || c.namaHakAkses?.toLowerCase() === username.toLowerCase());
|
|
|
|
// Precedence 1: Exact Role Match
|
|
if (!matchedConfig && role) {
|
|
matchedConfig = allConfigs.find(c => c.role?.toLowerCase() === role.toLowerCase() && !c.isGroupBased);
|
|
}
|
|
|
|
// Precedence 2: Group Match
|
|
if (!matchedConfig && group) {
|
|
matchedConfig = allConfigs.find(c => c.isGroupBased && c.group?.toLowerCase() === group.toLowerCase());
|
|
}
|
|
|
|
// If found, use it, else empty (Deny-by-default)
|
|
permissions.value = matchedConfig?.hakAksesMenu || [];
|
|
} else if (!useMock && data.value?.data) {
|
|
// Real API returns { message: "...", data: [ { id, create, read, update, delete, pagename } ] }
|
|
// We need to map Real API schema back to HakAksesMenu schema (contract mapping)
|
|
const realApiData = Array.isArray(data.value.data) ? data.value.data : [];
|
|
permissions.value = realApiData.map((item: any) => ({
|
|
menuKey: item.pagename?.toLowerCase().replace(/\s+/g, '-') || "", // Best effort mapping if API doesn't send menuKey
|
|
name: item.pagename || "Unknown",
|
|
canAccess: item.read === true,
|
|
canView: item.read === true,
|
|
canAdd: item.create === true,
|
|
canEdit: item.update === true,
|
|
canDelete: item.delete === true
|
|
}));
|
|
} else {
|
|
// Unrecognized format -> Deny-by-default
|
|
permissions.value = [];
|
|
}
|
|
|
|
isLoaded.value = true;
|
|
} catch (err) {
|
|
console.error("[permissionStore] Failed to load permissions:", err);
|
|
// Deny-by-default on fetch error
|
|
permissions.value = [];
|
|
isError.value = true;
|
|
isLoaded.value = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Check if a specific menuKey has a specific permission action.
|
|
* If the fetch failed or config is missing, it returns false (Deny-by-default).
|
|
*/
|
|
const can = (menuKey: string, action: keyof HakAksesMenu = 'canAccess'): boolean => {
|
|
// If we haven't loaded yet or there was an error, deny everything
|
|
if (!isLoaded.value || isError.value) return false;
|
|
|
|
// // TODO(security): This check is purely for UX.
|
|
// // True access control must be validated on the backend!
|
|
|
|
// Find the menu configuration
|
|
const menuConfig = permissions.value.find(
|
|
(m) => m.menuKey.toLowerCase() === menuKey.toLowerCase()
|
|
);
|
|
|
|
if (!menuConfig) return false; // Not configured -> deny
|
|
|
|
return menuConfig[action] === true;
|
|
};
|
|
|
|
/**
|
|
* Reset the store (e.g. on logout)
|
|
*/
|
|
const clear = () => {
|
|
permissions.value = [];
|
|
isLoaded.value = false;
|
|
isError.value = false;
|
|
};
|
|
|
|
return { permissions, isLoaded, isError, load, can, clear };
|
|
});
|