Files
leocrm/frontend/src/api/policies.ts
T

155 lines
4.8 KiB
TypeScript

/**
* Policies API client — ABAC entity policies (attribute-based access
* control) management (UI-Backlog module 10/16).
*
* Backend: /api/v1/policies
* - GET /{entity_type} → list policies for one entity type
* - POST / → create policy (201)
* - PUT /{policy_id} → update policy
* - DELETE /{policy_id} → delete policy (204)
* Permissions: policies:read (list) / policies:write (create, update, delete).
*
* Conditions format (JSONB):
* { operator: 'AND' | 'OR', rules: [{ field, op, value }] }
* Supported ops: eq, neq, in, not_in, gt, gte, lt, lte, contains,
* starts_with, is_null, is_not_null.
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiGet, apiPost, apiPut, apiDelete } from '@/api/client';
export type PrincipalType = 'user' | 'group' | 'role';
export type PolicyEffect = 'allow' | 'deny';
export type ConditionOperator = 'AND' | 'OR';
export interface PolicyRule {
field: string;
op: string;
value: unknown;
}
export interface PolicyConditions {
operator: ConditionOperator;
rules: PolicyRule[];
}
export interface Policy {
id: string;
name: string;
entity_type: string;
principal_type: PrincipalType;
principal_id: string;
effect: PolicyEffect;
conditions: PolicyConditions | null;
priority: number;
tenant_id: string;
enabled: boolean;
created_at: string | null;
updated_at: string | null;
}
export interface PolicyListResponse {
items: Policy[];
total: number;
}
export interface PolicyCreatePayload {
name: string;
entity_type: string;
principal_type: PrincipalType;
principal_id: string;
effect?: PolicyEffect;
conditions?: PolicyConditions | null;
priority?: number;
}
export interface PolicyUpdatePayload {
name?: string;
entity_type?: string;
principal_type?: PrincipalType;
principal_id?: string;
effect?: PolicyEffect;
conditions?: PolicyConditions | null;
priority?: number;
enabled?: boolean;
}
/** Entity types with ABAC field whitelists (backend ABAC_ALLOWED_FIELDS). */
export const POLICY_ENTITY_TYPES = [
'contact',
'file',
'task',
'calendar_event',
'mailbox',
'address',
'contact_folder',
'workflow',
] as const;
export const POLICY_OPS = [
'eq', 'neq', 'in', 'not_in', 'gt', 'gte', 'lt', 'lte',
'contains', 'starts_with', 'is_null', 'is_not_null',
] as const;
/**
* Mirrored from backend policy_service.ABAC_ALLOWED_FIELDS — the fields
* each entity type may reference in policy conditions. The backend skips
* non-whitelisted fields; showing them here prevents invalid input.
* Keep in sync with app/services/policy_service.py.
*/
export const ABAC_ALLOWED_FIELDS: Record<string, string[]> = {
contact: ['status', 'type', 'country', 'tags', 'created_at', 'updated_at', 'owner_id', 'name', 'displayname', 'firstname', 'surname', 'email_1', 'email_2', 'code'],
file: ['status', 'size', 'mime_type', 'created_at'],
task: ['status', 'priority', 'due_date', 'created_at'],
calendar_event: ['status', 'start_time', 'end_time', 'created_at'],
mailbox: ['status', 'created_at'],
address: ['country', 'city', 'created_at', 'updated_at'],
contact_folder: ['name', 'created_at'],
workflow: ['status', 'created_at'],
};
/** Ops where the rule value is ignored (null checks). */
export const NULL_OPS = ['is_null', 'is_not_null'] as const;
// ─── Query hooks ─────────────────────────────────────────────
export function usePolicies(entityType: string, enabled = true) {
return useQuery<PolicyListResponse>({
queryKey: ['policies', entityType],
queryFn: () => apiGet<PolicyListResponse>(`/policies/${entityType}`),
enabled: enabled && !!entityType,
});
}
// ─── Mutation hooks ──────────────────────────────────────────
export function useCreatePolicy() {
const qc = useQueryClient();
return useMutation<Policy, Error, PolicyCreatePayload>({
mutationFn: (data) => apiPost<Policy>('/policies', data),
onSuccess: (policy) => {
qc.invalidateQueries({ queryKey: ['policies', policy.entity_type] });
},
});
}
export function useUpdatePolicy() {
const qc = useQueryClient();
return useMutation<Policy, Error, { id: string; data: PolicyUpdatePayload }>({
mutationFn: ({ id, data }) => apiPut<Policy>(`/policies/${id}`, data),
onSuccess: (policy) => {
qc.invalidateQueries({ queryKey: ['policies', policy.entity_type] });
},
});
}
export function useDeletePolicy() {
const qc = useQueryClient();
return useMutation<void, Error, { id: string; entityType: string }>({
mutationFn: ({ id }) => apiDelete(`/policies/${id}`),
onSuccess: (_, vars) => {
qc.invalidateQueries({ queryKey: ['policies', vars.entityType] });
},
});
}