Files
web-antrean/stores/permissionStore.ts
T

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 };
});