feat: add tenant admin role management page and permission service integration
This commit is contained in:
parent
399666e246
commit
04003a2332
@ -69,6 +69,11 @@ const superAdminPlatformMenu: MenuItem[] = [
|
|||||||
{ label: "Roles & Permissions", path: "/platform-roles" },
|
{ label: "Roles & Permissions", path: "/platform-roles" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: ShieldCheck,
|
||||||
|
label: "Tenant Admin Role",
|
||||||
|
path: "/tenant-admin-role",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: Package,
|
icon: Package,
|
||||||
label: "Modules",
|
label: "Modules",
|
||||||
|
|||||||
420
src/pages/superadmin/TenantAdminRole.tsx
Normal file
420
src/pages/superadmin/TenantAdminRole.tsx
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
import { useState, useEffect, type ReactElement } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Layout } from "@/components/layout/Layout";
|
||||||
|
import {
|
||||||
|
PrimaryButton,
|
||||||
|
SecondaryButton,
|
||||||
|
Modal,
|
||||||
|
FormField,
|
||||||
|
FormSelect,
|
||||||
|
StatusBadge,
|
||||||
|
DataTable,
|
||||||
|
Pagination,
|
||||||
|
DeleteConfirmationModal,
|
||||||
|
type Column,
|
||||||
|
} from "@/components/shared";
|
||||||
|
import { roleService } from "@/services/role-service";
|
||||||
|
import { permissionService } from "@/services/permission-service";
|
||||||
|
import type { Role, Permission } from "@/types/role";
|
||||||
|
import { showToast } from "@/utils/toast";
|
||||||
|
import {
|
||||||
|
Shield,
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
Loader2,
|
||||||
|
ShieldAlert,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
// Zod Schema for custom permissions
|
||||||
|
const customPermissionSchema = z.object({
|
||||||
|
resource: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Resource name is required")
|
||||||
|
.regex(
|
||||||
|
/^[a-z0-9_]+$/,
|
||||||
|
"Resource name must be lowercase alphanumeric and can contain underscores"
|
||||||
|
),
|
||||||
|
action: z.string().min(1, "Action is required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
type CustomPermissionFormData = z.infer<typeof customPermissionSchema>;
|
||||||
|
|
||||||
|
export default function TenantAdminRole(): ReactElement {
|
||||||
|
const [role, setRole] = useState<Role | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// All permissions list for tenant_admin
|
||||||
|
const [permissions, setPermissions] = useState<Permission[]>([]);
|
||||||
|
|
||||||
|
// Pagination state for permissions table
|
||||||
|
const [page, setPage] = useState<number>(1);
|
||||||
|
const [limit, setLimit] = useState<number>(10);
|
||||||
|
const [totalItems, setTotalItems] = useState<number>(0);
|
||||||
|
const [totalPages, setTotalPages] = useState<number>(1);
|
||||||
|
|
||||||
|
// Add Custom Permission Modal state
|
||||||
|
const [isAddCustomOpen, setIsAddCustomOpen] = useState<boolean>(false);
|
||||||
|
const [isAddingCustom, setIsAddingCustom] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Delete/Revoke Permission state
|
||||||
|
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||||||
|
const [isDeleting, setIsDeleting] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// React Hook Form setup
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
watch,
|
||||||
|
reset,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<CustomPermissionFormData>({
|
||||||
|
resolver: zodResolver(customPermissionSchema),
|
||||||
|
defaultValues: {
|
||||||
|
resource: "",
|
||||||
|
action: "read",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const loadPermissions = async (roleId: string, pageNum: number, limitNum: number) => {
|
||||||
|
try {
|
||||||
|
const response = await permissionService.getAll(pageNum, limitNum, { role_id: roleId });
|
||||||
|
if (response.success) {
|
||||||
|
setPermissions(response.data);
|
||||||
|
if (response.pagination) {
|
||||||
|
setTotalItems(response.pagination.total);
|
||||||
|
setTotalPages(response.pagination.totalPages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
showToast.error("Failed to load permissions list.");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadTenantAdminRole = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// 1. Fetch roles to find tenant_admin
|
||||||
|
const rolesResponse = await roleService.getAll(1, 100, null, "tenant_admin", null, true);
|
||||||
|
if (!rolesResponse.success || rolesResponse.data.length === 0) {
|
||||||
|
throw new Error("Tenant Admin role not found. Please verify that the system is seeded.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenantAdminSummary = rolesResponse.data.find(
|
||||||
|
(r) => r.code === "tenant_admin"
|
||||||
|
);
|
||||||
|
if (!tenantAdminSummary) {
|
||||||
|
throw new Error("Tenant Admin role not found among tenant roles.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep role metadata
|
||||||
|
setRole(tenantAdminSummary);
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || "Failed to load Tenant Admin role data.");
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadTenantAdminRole();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// When page or limit changes, load updated permissions
|
||||||
|
useEffect(() => {
|
||||||
|
if (role?.id) {
|
||||||
|
loadPermissions(role.id, page, limit);
|
||||||
|
}
|
||||||
|
}, [page, limit, role?.id]);
|
||||||
|
|
||||||
|
// Add custom permission directly via Permission API (Always tenant scoped)
|
||||||
|
const handleAddCustomPermission = async (data: CustomPermissionFormData) => {
|
||||||
|
if (!role) return;
|
||||||
|
try {
|
||||||
|
setIsAddingCustom(true);
|
||||||
|
const payload = {
|
||||||
|
role_id: role.id,
|
||||||
|
resource: data.resource.trim().toLowerCase(),
|
||||||
|
action: data.action.trim().toLowerCase(),
|
||||||
|
scope: "tenant" as const, // Always tenant scoped
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await permissionService.create(payload);
|
||||||
|
if (response.success) {
|
||||||
|
showToast.success("Permission added successfully.");
|
||||||
|
setIsAddCustomOpen(false);
|
||||||
|
reset();
|
||||||
|
await loadPermissions(role.id, page, limit);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
showToast.error(
|
||||||
|
err?.response?.data?.error?.message ||
|
||||||
|
err?.message ||
|
||||||
|
"Failed to add permission."
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsAddingCustom(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Revoke permission directly via Permission API
|
||||||
|
const handleRevokePermission = async () => {
|
||||||
|
if (!deleteTargetId || !role) return;
|
||||||
|
try {
|
||||||
|
setIsDeleting(true);
|
||||||
|
const response = await permissionService.delete(deleteTargetId);
|
||||||
|
if (response.success) {
|
||||||
|
showToast.success("Permission revoked successfully.");
|
||||||
|
setDeleteTargetId(null);
|
||||||
|
await loadPermissions(role.id, page, limit);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
showToast.error(
|
||||||
|
err?.response?.data?.error?.message ||
|
||||||
|
err?.message ||
|
||||||
|
"Failed to revoke permission."
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DataTable column mapping for permissions
|
||||||
|
const columns: Column<Permission>[] = [
|
||||||
|
{
|
||||||
|
key: "resource",
|
||||||
|
label: "Resource",
|
||||||
|
width: "40%",
|
||||||
|
render: (item) => (
|
||||||
|
<code className="bg-slate-50 px-1.5 py-0.5 rounded font-mono text-xs text-slate-700">
|
||||||
|
{item.resource}
|
||||||
|
</code>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "action",
|
||||||
|
label: "Action",
|
||||||
|
width: "25%",
|
||||||
|
render: (item) => (
|
||||||
|
<span className="px-2 py-1 bg-slate-100 text-slate-700 font-mono rounded text-xs">
|
||||||
|
{item.action}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "scope",
|
||||||
|
label: "Scope",
|
||||||
|
width: "20%",
|
||||||
|
render: (item) => (
|
||||||
|
<StatusBadge variant="info">
|
||||||
|
{item.scope || "tenant"}
|
||||||
|
</StatusBadge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "actions",
|
||||||
|
label: "Action",
|
||||||
|
width: "15%",
|
||||||
|
align: "right",
|
||||||
|
render: (item) => (
|
||||||
|
<button
|
||||||
|
onClick={() => item.id && setDeleteTargetId(item.id)}
|
||||||
|
className="p-1.5 text-slate-400 hover:text-[#ef4444] hover:bg-red-50 rounded-md transition-colors"
|
||||||
|
title="Revoke Permission"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Layout
|
||||||
|
currentPage="Tenant Admin Role"
|
||||||
|
pageHeader={{
|
||||||
|
title: "Tenant Admin Permissions",
|
||||||
|
description: "Manage permissions assigned to the system tenant admin role.",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-[400px]">
|
||||||
|
<Loader2 className="w-10 h-10 text-[#0F3CC9] animate-spin mb-4" />
|
||||||
|
<p className="text-slate-500 text-sm">Loading Tenant Admin configuration...</p>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !role) {
|
||||||
|
return (
|
||||||
|
<Layout
|
||||||
|
currentPage="Tenant Admin Role"
|
||||||
|
pageHeader={{
|
||||||
|
title: "Tenant Admin Permissions",
|
||||||
|
description: "Manage permissions assigned to the system tenant admin role.",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="p-6 bg-red-50 border border-red-200 rounded-lg max-w-2xl mx-auto my-8 text-center">
|
||||||
|
<ShieldAlert className="w-12 h-12 text-[#ef4444] mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-bold text-red-900 mb-2">Configuration Error</h3>
|
||||||
|
<p className="text-red-700 text-sm mb-6">{error || "Role not found."}</p>
|
||||||
|
<SecondaryButton onClick={loadTenantAdminRole} className="px-4 py-2">
|
||||||
|
Retry Loading
|
||||||
|
</SecondaryButton>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout
|
||||||
|
currentPage="Tenant Admin Role"
|
||||||
|
pageHeader={{
|
||||||
|
title: "Tenant Admin Permission Control",
|
||||||
|
description: "Configure system-wide administrative permissions inherited by all Tenant Admins.",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
{/* Role Meta Information Card */}
|
||||||
|
<div className="p-6 bg-white border border-[#E5E7EB] rounded-xl shadow-sm flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-slate-100 flex items-center justify-center text-[#0F3CC9]">
|
||||||
|
<Shield className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<h2 className="text-lg font-bold text-slate-900">{role.name}</h2>
|
||||||
|
<StatusBadge variant="info">Tenant Scoped</StatusBadge>
|
||||||
|
{role.is_system && (
|
||||||
|
<span className="px-2 py-0.5 text-[11px] font-medium bg-slate-100 text-slate-600 rounded">
|
||||||
|
System seeded
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-slate-500 mt-1">
|
||||||
|
Role Code: <code className="bg-slate-50 px-1.5 py-0.5 rounded font-mono text-xs text-slate-700">{role.code}</code>
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-slate-400 mt-2">
|
||||||
|
This is a global system template. Changes here immediately affect the permission policies of all active tenants.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Permissions Table Section */}
|
||||||
|
<div className="bg-white border border-[#E5E7EB] rounded-xl shadow-sm overflow-hidden">
|
||||||
|
<div className="p-5 border-b border-[#E5E7EB] flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-md font-bold text-slate-900">Tenant Admin Permissions</h3>
|
||||||
|
<p className="text-xs text-slate-500 mt-0.5">
|
||||||
|
Manage granular, scope-specific system policies or actions assigned to this role.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={() => setIsAddCustomOpen(true)}
|
||||||
|
className="flex items-center gap-1.5 px-3 py-2 text-xs"
|
||||||
|
>
|
||||||
|
<Plus className="w-3.5 h-3.5" />
|
||||||
|
<span>Add Custom Permission</span>
|
||||||
|
</SecondaryButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 flex flex-col gap-4">
|
||||||
|
<DataTable
|
||||||
|
data={permissions}
|
||||||
|
columns={columns}
|
||||||
|
keyExtractor={(item) => item.id || `${item.resource}-${item.action}`}
|
||||||
|
emptyMessage="No permissions configured for this role."
|
||||||
|
/>
|
||||||
|
|
||||||
|
{totalItems > 0 && (
|
||||||
|
<Pagination
|
||||||
|
currentPage={page}
|
||||||
|
totalPages={totalPages}
|
||||||
|
totalItems={totalItems}
|
||||||
|
limit={limit}
|
||||||
|
onPageChange={(p: number) => setPage(p)}
|
||||||
|
onLimitChange={(l: number) => {
|
||||||
|
setLimit(l);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Custom Permission Modal */}
|
||||||
|
<Modal
|
||||||
|
isOpen={isAddCustomOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsAddCustomOpen(false);
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
title="Add Custom Permission"
|
||||||
|
description="Define a granular resource policy mapping directly to the database permission rules."
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={() => {
|
||||||
|
setIsAddCustomOpen(false);
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
disabled={isAddingCustom}
|
||||||
|
className="px-4 py-2 text-sm"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</SecondaryButton>
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={handleSubmit(handleAddCustomPermission)}
|
||||||
|
disabled={isAddingCustom}
|
||||||
|
className="px-4 py-2 text-sm"
|
||||||
|
>
|
||||||
|
{isAddingCustom ? "Adding..." : "Add Permission"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<form className="flex flex-col gap-4" onSubmit={handleSubmit(handleAddCustomPermission)}>
|
||||||
|
<FormField
|
||||||
|
label="Resource Name"
|
||||||
|
required
|
||||||
|
placeholder="e.g. prompt_templates, audit_filters"
|
||||||
|
{...register("resource")}
|
||||||
|
error={errors.resource?.message}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormSelect
|
||||||
|
label="Action"
|
||||||
|
required
|
||||||
|
options={[
|
||||||
|
{ value: "read", label: "Read" },
|
||||||
|
{ value: "create", label: "Create" },
|
||||||
|
{ value: "update", label: "Update" },
|
||||||
|
{ value: "delete", label: "Delete" },
|
||||||
|
{ value: "*", label: "All Access (*)" },
|
||||||
|
]}
|
||||||
|
value={watch("action")}
|
||||||
|
onValueChange={(val) => setValue("action", val, { shouldValidate: true })}
|
||||||
|
error={errors.action?.message}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Revoke confirmation modal */}
|
||||||
|
<DeleteConfirmationModal
|
||||||
|
isOpen={deleteTargetId !== null}
|
||||||
|
onClose={() => setDeleteTargetId(null)}
|
||||||
|
onConfirm={handleRevokePermission}
|
||||||
|
title="Revoke Permission"
|
||||||
|
message="Are you sure you want to revoke this advanced permission? This will take effect immediately across all tenants."
|
||||||
|
isLoading={isDeleting}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -22,6 +22,7 @@ const FailedEmails = lazy(() => import("@/pages/superadmin/FailedEmails"));
|
|||||||
const AIFallbackHistory = lazy(() => import("@/pages/superadmin/AIFallbackHistory"));
|
const AIFallbackHistory = lazy(() => import("@/pages/superadmin/AIFallbackHistory"));
|
||||||
const PlatformUsers = lazy(() => import("@/pages/superadmin/Users"));
|
const PlatformUsers = lazy(() => import("@/pages/superadmin/Users"));
|
||||||
const PlatformRoles = lazy(() => import("@/pages/superadmin/Roles"));
|
const PlatformRoles = lazy(() => import("@/pages/superadmin/Roles"));
|
||||||
|
const TenantAdminRole = lazy(() => import("@/pages/superadmin/TenantAdminRole"));
|
||||||
const StorageBuckets = lazy(() => import("@/pages/superadmin/StorageBuckets"));
|
const StorageBuckets = lazy(() => import("@/pages/superadmin/StorageBuckets"));
|
||||||
|
|
||||||
// Loading fallback component
|
// Loading fallback component
|
||||||
@ -61,6 +62,10 @@ export const superAdminRoutes: RouteConfig[] = [
|
|||||||
path: "/platform-roles",
|
path: "/platform-roles",
|
||||||
element: <LazyRoute component={PlatformRoles} />,
|
element: <LazyRoute component={PlatformRoles} />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/tenant-admin-role",
|
||||||
|
element: <LazyRoute component={TenantAdminRole} />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/tenants",
|
path: "/tenants",
|
||||||
element: <LazyRoute component={Tenants} />,
|
element: <LazyRoute component={Tenants} />,
|
||||||
|
|||||||
103
src/services/permission-service.ts
Normal file
103
src/services/permission-service.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import apiClient from "./api-client";
|
||||||
|
import type { Permission } from "@/types/role";
|
||||||
|
|
||||||
|
export interface PermissionsResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: Permission[];
|
||||||
|
pagination: {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
hasMore: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PermissionActionResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: Permission;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeletePermissionResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const permissionService = {
|
||||||
|
/**
|
||||||
|
* Get all permissions with pagination and filters
|
||||||
|
*/
|
||||||
|
getAll: async (
|
||||||
|
page: number = 1,
|
||||||
|
limit: number = 100,
|
||||||
|
filters: Record<string, any> = {}
|
||||||
|
): Promise<PermissionsResponse> => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("page", String(page));
|
||||||
|
params.append("limit", String(limit));
|
||||||
|
|
||||||
|
// Append filter options
|
||||||
|
Object.entries(filters).forEach(([key, val]) => {
|
||||||
|
if (val !== undefined && val !== null && val !== "") {
|
||||||
|
params.append(key, String(val));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await apiClient.get<PermissionsResponse>(
|
||||||
|
`/permissions?${params.toString()}`
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get permission details by ID
|
||||||
|
*/
|
||||||
|
getById: async (id: string): Promise<PermissionActionResponse> => {
|
||||||
|
const response = await apiClient.get<PermissionActionResponse>(
|
||||||
|
`/permissions/${id}`
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new custom permission
|
||||||
|
*/
|
||||||
|
create: async (data: {
|
||||||
|
role_id: string;
|
||||||
|
resource: string;
|
||||||
|
action: string;
|
||||||
|
scope: "global" | "tenant" | "module" | "platform";
|
||||||
|
conditions?: any;
|
||||||
|
}): Promise<PermissionActionResponse> => {
|
||||||
|
const response = await apiClient.post<PermissionActionResponse>(
|
||||||
|
"/permissions",
|
||||||
|
data
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing permission
|
||||||
|
*/
|
||||||
|
update: async (
|
||||||
|
id: string,
|
||||||
|
data: Partial<Permission>
|
||||||
|
): Promise<PermissionActionResponse> => {
|
||||||
|
const response = await apiClient.put<PermissionActionResponse>(
|
||||||
|
`/permissions/${id}`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete/revoke a permission
|
||||||
|
*/
|
||||||
|
delete: async (id: string): Promise<DeletePermissionResponse> => {
|
||||||
|
const response = await apiClient.delete<DeletePermissionResponse>(
|
||||||
|
`/permissions/${id}`
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -30,8 +30,11 @@ export interface RolesResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Permission {
|
export interface Permission {
|
||||||
|
id?: string;
|
||||||
|
role_id?: string;
|
||||||
resource: string;
|
resource: string;
|
||||||
action: string;
|
action: string;
|
||||||
|
scope?: "global" | "tenant" | "module" | "platform";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateRoleRequest {
|
export interface CreateRoleRequest {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user