Skip to content

softwelve/docs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 

Repository files navigation

Enterprise Portal Systems - Technical Documentation

Complete Developer Guide for Building Workflow Engines, Role-Based Access Control, IT Service Management, and Asset Management Systems


Table of Contents

  1. Overview
  2. Technology Stack
  3. Visual Workflow Builder
  4. Role-Based Access Control (RBAC)
  5. IT Service Management (ITSM)
  6. Asset/Inventory Management
  7. System Integration Patterns
  8. Best Practices

Overview

This documentation outlines how to build enterprise-grade systems for:

  • Visual Workflow Builder: Drag-and-drop interface for designing approval and fulfillment workflows
  • Role-Based Access Control (RBAC): Hierarchical permission system with inheritance and caching
  • IT Service Management (ITSM): Dynamic forms, requests, and automated permission management
  • Asset/Inventory Management: Type-based inventory with state machine lifecycle

Key Architectural Principles

graph TD
    classDef frontend fill:#0ea5e9,stroke:#0284c7,stroke-width:2px,color:#fff
    classDef api fill:#64748b,stroke:#475569,stroke-width:2px,color:#fff
    classDef logic fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
    classDef data fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff

    subgraph "Frontend Layer"
        UI[React/Next.js UI]:::frontend
        WorkflowEditor[Visual Workflow Editor]:::frontend
        FormRenderer[Dynamic Form Renderer]:::frontend
    end

    subgraph "API Layer"
        API[REST/GraphQL API]:::api
        Middleware[Permission Middleware]:::api
        Cache[Permission Cache]:::api
    end

    subgraph "Business Logic"
        RBAC[RBAC Engine]:::logic
        Workflow[Workflow Engine]:::logic
        Jobs[Background Job Queue]:::logic
    end

    subgraph "Data Layer"
        DB[("MongoDB/PostgreSQL")]:::data
        Collections[Data Collections]:::data
    end

    UI --> API
    WorkflowEditor --> API
    FormRenderer --> API
    API --> Middleware
    Middleware --> Cache
    Middleware --> RBAC
    API --> Workflow
    API --> Jobs
    RBAC --> Collections
    Collections --> DB
Loading

Technology Stack

Component Recommended Technologies Purpose
Frontend React, Next.js, TypeScript UI and Server-Side Rendering
Workflow Editor ReactFlow, React-DnD Visual drag-and-drop workflow design
Backend Node.js, Payload CMS API and CMS functionality
Database MongoDB, PostgreSQL Data persistence
Authentication Azure AD, MSAL.js, NextAuth.js SSO and identity management
Job Queue Bull, Redis, Payload Jobs Background task processing
Caching In-memory cache, Redis Permission caching
Styling Tailwind CSS, shadcn/ui Responsive and modern UI
Validation Zod Runtime type validation

Visual Workflow Builder

Purpose

Enable non-technical users to define complex approval and fulfillment workflows through a visual interface.

Architecture

graph LR
    classDef startNode fill:#0ea5e9,stroke:#0284c7,stroke-width:2px,color:#fff
    classDef approvalNode fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
    classDef fulfillmentNode fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff
    classDef configNode fill:#64748b,stroke:#475569,stroke-width:1px,color:#fff

    subgraph "Workflow Canvas"
        Start[Start Node]:::startNode --> A1[Approval Step]:::approvalNode
        A1 --> A2[Approval Step]:::approvalNode
        A2 --> F1[Fulfillment Step]:::fulfillmentNode
    end

    subgraph "Node Configuration"
        Config[Step Properties Panel]:::configNode
        Roles[Role Assignment]:::configNode
        Rules[Approval Rules]:::configNode
    end

    subgraph "Data Storage"
        JSON[JSON Graph Structure]:::configNode
    end
Loading

Core Concepts

1. Node Types

Node Type Purpose Configuration Options
Submitted Entry point (auto-created) Read-only, non-deletable
Approval Requires approval before proceeding Approver roles, min approvals, delegation
Fulfillment Final action/completion step Fulfiller roles

2. Data Structure

// Workflow stored as JSON
interface WorkflowValue {
  version: number;
  nodes: StepNode[];
  edges: Edge[];
}

interface StepNode {
  id: string;
  type: "submitted" | "approval" | "fulfillment";
  data: StepData;
  position: { x: number; y: number };
  deletable?: boolean;
}

interface StepData {
  label: string;
  // For approval nodes
  approvalType?: "role" | "lineManager" | "departmentHead";
  approverRole1?: string;
  approverRole2?: string;
  // ... up to N approver roles
  minApprovals?: number;
  allowDelegate?: boolean;
  delegateRole?: string;
  // For fulfillment nodes
  fulfillerRole1?: string;
  fulfillerRole2?: string;
  // ... up to N fulfiller roles
}

interface Edge {
  id: string;
  source: string; // Source node ID
  target: string; // Target node ID
}

3. Workflow Templates

Pre-built templates accelerate workflow creation:

Template Description Use Case
Single Approval Submit → Approval → Fulfillment Simple requests
Two-Step Approval Submit → Approval 1 → Approval 2 → Fulfillment Hierarchical approval
Parallel Approval Submit → [Approval A, Approval B] → Fulfillment Multi-department sign-off

Implementation Steps

flowchart TD
    classDef step fill:#0ea5e9,stroke:#0284c7,stroke-width:2px,color:#fff
    classDef decision fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
    classDef storage fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff

    A[Initialize Canvas]:::step --> B[Load ReactFlow]:::step
    B --> C[Setup Node Types]:::step
    C --> D[Create Node Components]:::step
    D --> E[Handle Drag & Drop]:::step
    E --> F[Manage Edges/Connections]:::step
    F --> G[Persist to Database]:::storage

    G --> H{On Form Submit}:::decision
    H --> I[Serialize to JSON]:::step
    I --> J[Store in Database]:::storage
Loading

Step 1: Initialize Workflow Canvas

// Initialize with ReactFlow
const [nodes, setNodes, onNodesChange] = useNodesState<StepData>(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

// Ensure "Submitted" node always exists
const ensureSubmitted = (workflow: WorkflowValue): WorkflowValue => {
  const hasSubmitted = workflow.nodes.some((n) => n.id === "submitted");
  if (!hasSubmitted) {
    const submitted: StepNode = {
      id: "submitted",
      type: "submitted",
      data: { label: "Submitted" },
      position: { x: 50, y: 50 },
      draggable: false,
      deletable: false,
    };
    return { ...workflow, nodes: [submitted, ...workflow.nodes] };
  }
  return workflow;
};

Step 2: Define Custom Node Components

// Base node component with handles
const BaseNode = ({ label, color }: { label: string; color: string }) => (
  <div style={{ borderColor: color }} className="node-container">
    <Handle type="target" position={Position.Left} />
    {label}
    <Handle type="source" position={Position.Right} />
  </div>
);

// Register node types
const nodeTypes: NodeTypes = {
  submitted: (props) => <BaseNode label="Submitted" color="#0ea5e9" {...props} />,
  approval: (props) => <BaseNode label={props.data?.label || "Approval"} color="#f59e0b" {...props} />,
  fulfillment: (props) => <BaseNode label={props.data?.label || "Fulfillment"} color="#22c55e" {...props} />,
};

Step 3: Handle Drag-and-Drop

// Drop handler for palette items
const onDrop = (event: React.DragEvent) => {
  event.preventDefault();
  const type = event.dataTransfer.getData("application/workflow-node");

  const position = reactFlowInstance.screenToFlowPosition({
    x: event.clientX,
    y: event.clientY,
  });

  const newNode: StepNode = {
    id: `${type}-${generateId()}`,
    type: type as "approval" | "fulfillment",
    position,
    data: {
      label: type === "approval" ? "Approval" : "Fulfillment",
      approvalType: type === "approval" ? "role" : undefined,
    },
  };

  setNodes((nodes) => [...nodes, newNode]);
};

Step 4: Role Assignment

// Multi-role selection component
const MultiRoleSelect = ({
  value,
  options,
  onChange
}: {
  value: string[];
  options: Role[];
  onChange: (roles: string[]) => void;
}) => {
  const toggle = (slug: string) => {
    const next = value.includes(slug)
      ? value.filter(v => v !== slug)
      : [...value, slug];
    onChange(next);
  };

  return (
    <div className="multi-select">
      {options.map(role => (
        <label key={role.id}>
          <input
            type="checkbox"
            checked={value.includes(role.slug)}
            onChange={() => toggle(role.slug)}
          />
          {role.name}
        </label>
      ))}
    </div>
  );
};

// Store roles as approverRole1, approverRole2, etc.
const writeApproverRoles = (data: StepData, selected: string[]): StepData => {
  const next = { ...data };
  // Clear existing
  Object.keys(next).filter(k => k.startsWith('approverRole')).forEach(k => delete next[k]);
  // Add new
  selected.forEach((slug, idx) => {
    next[`approverRole${idx + 1}`] = slug;
  });
  return next;
};

Step 5: Persist Workflow

// Serialize and save workflow
const saveWorkflow = async (formId: string, workflow: WorkflowValue) => {
  const safeNodes = workflow.nodes.map((n) => ({
    id: n.id,
    type: n.type,
    data: sanitizeNodeData(n.data),
    position: { x: n.position.x, y: n.position.y },
  }));

  const safeEdges = workflow.edges.map((e) => ({
    id: e.id,
    source: e.source,
    target: e.target,
  }));

  await fetch(`/api/forms/${formId}`, {
    method: "PATCH",
    body: JSON.stringify({
      workflowBuilder: {
        version: 1,
        nodes: safeNodes,
        edges: safeEdges,
      },
    }),
  });
};

Role-Based Access Control (RBAC)

Purpose

Implement a flexible, hierarchical permission system that supports:

  • Role inheritance
  • Group ancestry
  • Action implications
  • Dynamic permission creation
  • Performance-optimized caching

Architecture Overview

graph TB
    classDef source fill:#64748b,stroke:#475569,stroke-width:2px,color:#fff
    classDef engine fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
    classDef agg fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff
    classDef enforcement fill:#0ea5e9,stroke:#0284c7,stroke-width:2px,color:#fff

    subgraph "Permission Sources"
        DP[Direct Permissions]:::source
        Roles[Role Assignments]:::source
        Groups[Group Memberships]:::source
    end

    subgraph "Resolution Engine"
        RI[Role Inheritance Chain]:::engine
        GA[Group Ancestry Chain]:::engine
        AGG[Permission Aggregator]:::agg
    end

    subgraph "Enforcement"
        Cache[Permission Cache]:::enforcement
        MW[Middleware]:::enforcement
        AC[Access Control]:::enforcement
    end

    DP --> AGG
    Roles --> RI --> AGG
    Groups --> GA --> AGG
    AGG --> Cache
    Cache --> MW
    Cache --> AC
Loading

Core Data Models

1. Permission Resources

Resources define what can be protected. They form a hierarchy.

interface PermissionResource {
  id: string;
  name: string; // "IT Service Management"
  slug: string; // "itsm"
  parent?: PermissionResource; // Parent resource for hierarchy
  description?: string;
}

// Example resource hierarchy:
// system (root)
// ├── itsm
// │   ├── itsm-access (category)
// │   │   ├── access-card-form (form)
// │   │   └── visitor-pass-form (form)
// │   └── itsm-legal (category)
// └── documents

2. Permissions

Permissions combine resources with actions.

interface Permission {
  id: string;
  name: string; // "ITSM Access - Manage"
  slug: string; // "itsm-access.manage"
  resource: PermissionResource;
  action: Action;
  category: "system" | "users" | "content" | "itsm" | "reports" | "settings";
  level: "basic" | "standard" | "advanced" | "admin" | "super";
  isSystem?: boolean; // Cannot be deleted
  status: "active" | "inactive" | "deprecated";
}

type Action =
  | "create"
  | "read"
  | "update"
  | "delete" // CRUD
  | "manage"
  | "admin" // Meta-actions
  | "approve"
  | "fulfill"
  | "delegate"; // Workflow-specific

3. Roles

Roles are named permission sets with inheritance.

interface EmployeeRole {
  id: string;
  name: string; // "IT Manager"
  slug: string; // "it-manager"
  permissions: Permission[];
  inheritsFrom?: EmployeeRole; // Parent role
  level: number; // 1-100, higher = more authority
  isSystem?: boolean;
  status: "active" | "inactive";
}

4. Groups

Groups organize users and can have permissions/roles.

interface EmployeeGroup {
  id: string;
  name: string; // "IT Department"
  slug: string; // "it-department"
  roles: EmployeeRole[];
  permissions: Permission[]; // Direct group permissions
  parentGroup?: EmployeeGroup;
}

Action Implications

Higher-level actions automatically grant lower-level actions:

graph TD
    classDef admin fill:#ef4444,stroke:#dc2626,stroke-width:2px,color:#fff
    classDef manage fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
    classDef workflow fill:#a855f7,stroke:#9333ea,stroke-width:2px,color:#fff
    classDef crud fill:#64748b,stroke:#475569,stroke-width:1px,color:#fff

    Admin[admin]:::admin --> Manage[manage]:::manage
    Admin --> Approve[approve]:::workflow
    Admin --> Fulfill[fulfill]:::workflow
    Admin --> Delegate[delegate]:::workflow

    Manage --> Create[create]:::crud
    Manage --> Read[read]:::crud
    Manage --> Update[update]:::crud
    Manage --> Delete[delete]:::crud
Loading
function actionImplies(possessed: string, required: string): boolean {
  const p = possessed.toLowerCase();
  const r = required.toLowerCase();

  if (p === r) return true;
  if (p === "admin") return true; // Admin implies all
  if (p === "manage") {
    return ["create", "read", "update", "delete"].includes(r);
  }

  return false;
}

Important

manage does NOT imply approve or fulfill. These workflow actions must be granted explicitly.

Permission Resolution Algorithm

async function resolveEffectivePermissions(userId: string): Promise<{
  permissions: string[];
  isSuperadmin: boolean;
}> {
  const permissions = new Set<string>();
  let isSuperadmin = false;

  const user = await getUser(userId);

  // 1. Direct custom permissions
  for (const perm of user.customPermissions) {
    permissions.add(perm.slug);
  }

  // 2. Roles (with inheritance)
  const allRoles = await resolveRoleInheritance(user.roles);
  for (const role of allRoles) {
    if (role.slug === "superadmin") isSuperadmin = true;
    for (const perm of role.permissions) {
      permissions.add(perm.slug);
    }
  }

  // 3. Groups (with ancestry)
  const allGroups = await resolveGroupAncestry(user.groups);
  for (const group of allGroups) {
    // Group's direct permissions
    for (const perm of group.permissions) {
      permissions.add(perm.slug);
    }
    // Group's roles
    const groupRoles = await resolveRoleInheritance(group.roles);
    for (const role of groupRoles) {
      if (role.slug === "superadmin") isSuperadmin = true;
      for (const perm of role.permissions) {
        permissions.add(perm.slug);
      }
    }
  }

  return { permissions: Array.from(permissions), isSuperadmin };
}

Role Inheritance Traversal

async function resolveRoleInheritance(roleIds: string[]): Promise<Role[]> {
  const visited = new Set<string>();
  const stack = [...roleIds];
  const roles: Role[] = [];

  while (stack.length > 0) {
    const id = stack.pop()!;
    if (visited.has(id)) continue; // Prevent cycles
    visited.add(id);

    const role = await getRoleById(id);
    if (!role) continue;

    roles.push(role);

    if (role.inheritsFrom && !visited.has(role.inheritsFrom.id)) {
      stack.push(role.inheritsFrom.id);
    }
  }

  return roles;
}

Permission Caching

Performance optimization with in-memory caching:

const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes

interface CachedPermissions {
  permissions: string[];
  isSuperadmin: boolean;
  timestamp: number;
}

const permissionCache = new Map<string, CachedPermissions>();

async function getEffectivePermissions(
  userId: string,
): Promise<CachedPermissions> {
  const cached = permissionCache.get(userId);

  if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
    return cached;
  }

  const resolved = await resolveEffectivePermissions(userId);
  const cacheEntry: CachedPermissions = {
    ...resolved,
    timestamp: Date.now(),
  };

  permissionCache.set(userId, cacheEntry);
  return cacheEntry;
}

function invalidateUserPermissions(userId: string): void {
  permissionCache.delete(userId);
}

function invalidateAllPermissions(): void {
  permissionCache.clear();
}

Permission Checking Mechanisms

1. Server-Side Middleware

// API route protection
export async function checkPermission(
  request: Request,
  requiredPermission?: string,
  options?: { requireSuperadmin?: boolean },
): Promise<{ hasPermission: boolean; user: User | null }> {
  const user = await getCurrentUser(request);
  if (!user) return { hasPermission: false, user: null };

  const { permissions, isSuperadmin } = await getEffectivePermissions(user.id);

  // Superadmin bypass
  if (isSuperadmin) {
    return { hasPermission: true, user };
  }

  if (options?.requireSuperadmin) {
    return { hasPermission: false, user };
  }

  if (!requiredPermission) {
    return { hasPermission: true, user };
  }

  // Check with action implications
  const [resource, action] = requiredPermission.split(".");
  const hasPermission = permissions.some((perm) => {
    const [permResource, permAction] = perm.split(".");
    if (permResource !== resource) return false;
    return actionImplies(permAction, action);
  });

  return { hasPermission, user };
}

2. Collection Access Control

// Payload CMS collection access hooks
export const ITSMForms: CollectionConfig = {
  slug: "itsm-forms",
  access: {
    read: async ({ req }) => {
      const { isSuperadmin, permissions } = await getEffectivePermissions(
        req.user.id,
      );
      if (isSuperadmin) return true;

      // Return where clause for filtered access
      return buildFormsReadWhere(permissions);
    },
    create: async ({ req, data }) => {
      return await allowFormCreate(req, data);
    },
    update: async ({ req, id }) => {
      return await allowFormUpdateOrDelete(req, id);
    },
    delete: async ({ req, id }) => {
      return await allowFormUpdateOrDelete(req, id);
    },
  },
};

3. Client-Side Hook

// React hook for permission checks
function usePermissions() {
  const [permissions, setPermissions] = useState<string[]>([]);
  const [isSuperadmin, setIsSuperadmin] = useState(false);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchPermissions()
      .then(({ permissions, isSuperadmin }) => {
        setPermissions(permissions);
        setIsSuperadmin(isSuperadmin);
      })
      .finally(() => setLoading(false));
  }, []);

  const hasPermission = useCallback((required: string): boolean => {
    if (isSuperadmin) return true;

    const [resource, action] = required.split('.');
    return permissions.some(perm => {
      const [permResource, permAction] = perm.split('.');
      if (permResource !== resource) return false;
      return actionImplies(permAction, action);
    });
  }, [permissions, isSuperadmin]);

  return { hasPermission, isSuperadmin, loading, permissions };
}

// Usage
function AdminButton() {
  const { hasPermission, loading } = usePermissions();

  if (loading) return <Spinner />;
  if (!hasPermission('documents.create')) return null;

  return <button>Create Document</button>;
}

IT Service Management (ITSM)

Purpose

Enable organizations to manage service requests through configurable forms, workflows, and automated permission management.

Architecture

graph TB
    classDef user fill:#0ea5e9,stroke:#0284c7,stroke-width:2px,color:#fff
    classDef admin fill:#64748b,stroke:#475569,stroke-width:2px,color:#fff
    classDef process fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
    classDef auto fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff

    subgraph "End User"
        Dashboard[ITSM Dashboard]:::user
        Submit[Submit Request]:::user
    end

    subgraph "Admin"
        CatAdmin[Category Management]:::admin
        FormAdmin[Form Builder]:::admin
        WorkflowAdmin[Workflow Designer]:::admin
    end

    subgraph "Processing"
        Requests[Request Queue]:::process
        Approvers[Approver Assignment]:::process
        Fulfillers[Fulfiller Assignment]:::process
    end

    subgraph "Automation"
        PermJobs[Permission Jobs]:::auto
        RoleGen[Role Generation]:::auto
    end

    Dashboard --> Submit
    Submit --> Requests
    CatAdmin --> FormAdmin
    FormAdmin --> WorkflowAdmin
    FormAdmin --> PermJobs
    PermJobs --> RoleGen
    Requests --> Approvers
    Approvers --> Fulfillers
Loading

Resource Hierarchy

graph TD
    classDef root fill:#0ea5e9,stroke:#0284c7,stroke-width:2px,color:#fff
    classDef cat fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
    classDef form fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff

    ITSM[itsm<br/>Root]:::root --> Cat1[itsm-access<br/>Category]:::cat
    ITSM --> Cat2[itsm-legal<br/>Category]:::cat
    ITSM --> Cat3[itsm-transport<br/>Category]:::cat

    Cat1 --> Form1[access-card-form<br/>Form]:::form
    Cat1 --> Form2[visitor-pass-form<br/>Form]:::form

    Cat2 --> Form3[contract-review-form<br/>Form]:::form

    Cat3 --> Form4[vehicle-request-form<br/>Form]:::form
Loading

Data Models

1. ITSM Categories

interface ITSMCategory {
  id: string;
  title: string; // "Access Management"
  slug: string; // "access"
  description?: string;
  icon?: string; // Lucide icon name
  enabled: boolean;
  forms?: ITSMForm[]; // Related forms
}

2. ITSM Forms

interface ITSMForm {
  id: string;
  title: string;
  slug: string;
  description?: string;
  category: ITSMCategory;
  icon?: string;
  formType: "itsm" | "equipment";
  inventoryType?: InventoryType; // For equipment forms
  enabled: boolean;
  removed: boolean;

  // SLA Configuration
  slaTime?: {
    enabled: boolean;
    duration: number;
    unit: "hours" | "days";
  };

  // Form fields (dynamic)
  fields: FormField[];

  // Workflow definition
  workflowBuilder: WorkflowValue;
}

interface FormField {
  name: string;
  type:
    | "text"
    | "textarea"
    | "select"
    | "checkbox"
    | "date"
    | "file"
    | "relationship";
  label: string;
  required?: boolean;
  options?: { label: string; value: string }[];
  visibility?: ConditionalVisibility;
}

interface ConditionalVisibility {
  enabled: boolean;
  logic: "and" | "or";
  rules: Array<{
    field: string;
    operator: "equals" | "notEquals" | "contains" | "isEmpty";
    value: string;
  }>;
}

3. ITSM Requests

interface ITSMRequest {
  id: string;
  form: ITSMForm;
  requester: Employee;
  status: RequestStatus;
  priority: "low" | "medium" | "high" | "urgent";

  // Form data submitted
  data: Record<string, any>;

  // Workflow state
  currentStep: string; // Current node ID in workflow
  approvals: Approval[];
  fulfillments: Fulfillment[];

  // SLA tracking
  slaDueAt?: Date;
  slaBreached?: boolean;

  // Timestamps
  createdAt: Date;
  updatedAt: Date;
  completedAt?: Date;
}

type RequestStatus =
  | "pending_approval"
  | "approved"
  | "rejected"
  | "pending_fulfillment"
  | "fulfilled"
  | "cancelled";

interface Approval {
  step: string;
  approver: Employee;
  action: "approved" | "rejected";
  comments?: string;
  timestamp: Date;
}

Dynamic Permission Creation

When ITSM categories and forms are created, the system automatically generates permissions:

sequenceDiagram
    autonumber
    participant Admin
    participant Collection
    participant Hook
    participant JobQueue
    participant Database

    Note over Admin,Database: Dynamic Permission Creation Flow

    Admin->>Collection: Create Category/Form
    Collection->>Hook: Trigger afterChange
    Hook->>JobQueue: Queue permission job
    rect rgb(241, 245, 249)
        Note right of JobQueue: Background Processing
        JobQueue->>Database: Create PermissionResource
        JobQueue->>Database: Create Permissions (CRUD + manage/admin)
        JobQueue->>Database: Create Roles (manager, approver, fulfiller)
        JobQueue->>Database: Assign permissions to roles
    end
Loading

Permission Job Handler

async function handleCategoryPermissionCreation(
  categorySlug: string,
  categoryTitle: string,
) {
  const resourceSlug = `itsm-${categorySlug}`;

  // 1. Create permission resource
  const resource = await upsertResource({
    slug: resourceSlug,
    name: categoryTitle,
    parent: "itsm", // Parent is root ITSM
  });

  // 2. Create permissions
  const actions = [
    "create",
    "read",
    "update",
    "delete",
    "manage",
    "approve",
    "fulfill",
    "admin",
  ];
  for (const action of actions) {
    await upsertPermission({
      slug: `${resourceSlug}.${action}`,
      name: `${categoryTitle} - ${capitalize(action)}`,
      resource: resource.id,
      action,
      category: "itsm",
    });
  }

  // 3. Create roles
  const roleConfigs = [
    { suffix: "manager", label: "Manager", perms: ["manage"] },
    { suffix: "approver", label: "Approver", perms: ["read", "approve"] },
    { suffix: "fulfiller", label: "Fulfiller", perms: ["read", "fulfill"] },
    { suffix: "admin", label: "Admin", perms: ["admin"] },
  ];

  for (const config of roleConfigs) {
    const roleSlug = `${resourceSlug}-${config.suffix}`;
    const permissions = config.perms.map((p) =>
      getPermissionId(`${resourceSlug}.${p}`),
    );

    await upsertRole({
      slug: roleSlug,
      name: `${categoryTitle} ${config.label}`,
      permissions,
    });
  }
}

Access Control Rules

flowchart TD
    classDef startNode fill:#64748b,stroke:#475569,stroke-width:2px,color:#fff
    classDef decision fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
    classDef allow fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff
    classDef deny fill:#ef4444,stroke:#dc2626,stroke-width:2px,color:#fff

    Start([User Action]):::startNode --> CheckSuper{Superadmin?}:::decision

    CheckSuper -->|Yes| Allow[✓ Allow]:::allow
    CheckSuper -->|No| CheckAction{Action Type?}:::decision

    CheckAction -->|Read Categories| ReadCat{Any itsm-* permission?}:::decision
    CheckAction -->|Create Category| CreateCat{itsm.manage or admin?}:::decision
    CheckAction -->|Update Category| UpdateCat{Category manage/admin?}:::decision

    CheckAction -->|Create Request| CreateReq{Authenticated user?}:::decision
    CheckAction -->|Update Request| UpdateReq{approve/fulfill/manage?}:::decision

    ReadCat -->|Yes| Allow
    ReadCat -->|No| Deny[✗ Deny]:::deny

    CreateCat -->|Yes| Allow
    CreateCat -->|No| Deny

    UpdateCat -->|Yes| Allow
    UpdateCat -->|No| Deny

    CreateReq -->|Yes| Allow
    CreateReq -->|No| Deny

    UpdateReq -->|Yes| Allow
    UpdateReq -->|No| Deny
Loading

Asset/Inventory Management

Purpose

Track physical assets with:

  • Dynamic asset types
  • State machine lifecycle
  • Blueprint annotations
  • Request integration

Architecture

graph TB
    classDef config fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
    classDef asset fill:#0ea5e9,stroke:#0284c7,stroke-width:2px,color:#fff
    classDef integration fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff

    subgraph "Configuration"
        Types[Inventory Types]:::config
        Templates[Blueprint Templates]:::config
    end

    subgraph "Assets"
        Items[Inventory Items]:::asset
        History[Item History]:::asset
    end

    subgraph "Integration"
        Requests[ITSM Requests]:::integration
        Assignment[Asset Assignment]:::integration
    end

    Types --> Items
    Templates --> Items
    Items --> History
    Requests --> Assignment
    Assignment --> Items
Loading

Data Models

1. Inventory Types

Define types of assets (cars, laptops, equipment):

interface InventoryType {
  id: string;
  name: string; // "Company Vehicle"
  slug: string; // "company-vehicle"
  identifierLabel: string; // "License Plate"
  blueprintTemplate?: Media; // Default blueprint image
  description?: string;
  validationRules?: {
    pattern?: string; // Regex for identifier
    minLength?: number;
    maxLength?: number;
    format?: "uppercase" | "lowercase";
  };
}

2. Inventory Items

Individual asset instances:

interface InventoryItem {
  id: string;
  type: InventoryType;
  identifier: string; // "ABC-1234" (license plate)
  status: InventoryStatus;
  blueprint?: Media; // Item-specific blueprint

  // Flexible metadata
  metadata?: Record<string, any>;

  // Current assignment
  assignedTo?: Employee;
  assignedRequest?: ITSMRequest;

  // Blueprint annotations
  comments: BlueprintComment[];

  // History
  history: HistoryEntry[];

  notes?: string;
  createdAt: Date;
  updatedAt: Date;
}

type InventoryStatus =
  | "available"
  | "occupied"
  | "under_review"
  | "under_repair"
  | "reserved"
  | "retired";

interface BlueprintComment {
  id: string;
  position: { x: number; y: number }; // Position on blueprint
  text: string;
  status: "open" | "resolved";
  createdBy: Employee;
  resolvedBy?: Employee;
  createdAt: Date;
  resolvedAt?: Date;
}

Status Lifecycle

stateDiagram-v2
    classDef state fill:#0ea5e9,stroke:#0284c7,stroke-width:2px,color:#fff
    classDef process fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
    classDef final fill:#ef4444,stroke:#dc2626,stroke-width:2px,color:#fff

    [*] --> Available: Create Item

    Available --> Occupied: Assign to Request
    Available --> UnderRepair: Found Broken
    Available --> Reserved: Reserve
    Available --> Retired: Retire

    Occupied --> UnderReview: Report Issue / Return

    UnderReview --> UnderRepair: Needs Repair
    UnderReview --> Reserved: Hold
    UnderReview --> Retired: Unusable
    UnderReview --> Available: Passed Review

    UnderRepair --> Reserved: Repaired, Hold
    UnderRepair --> Retired: Repair Failed
    UnderRepair --> Available: Repair Complete

    Reserved --> Retired: Cancel & Scrap
    Reserved --> Available: Release

    Retired --> Available: Restore to Service
Loading

Status Transition Rules

Current Status Allowed Transitions Notes
Available Under Repair, Reserved, Retired "Occupied" happens via assignment
Occupied Under Review Can only move to review
Under Review Under Repair, Reserved, Retired, Available Triage phase
Under Repair Reserved, Retired, Available Maintenance phase
Reserved Retired, Available Held for future use
Retired Available Can restore to service

Action Permissions by Status

Action Allowed Status Permission Required
Add Comments Under Review inventory-{type}.reviewer
Resolve Comments Under Repair inventory-{type}.maintainer
Change Status Any inventory-{type}.update
Delete Item Any inventory-{type}.admin

Start Over (Reset) Logic

When an item returns to "Available" status:

async function handleStatusChange(
  item: InventoryItem,
  newStatus: InventoryStatus,
) {
  if (newStatus === "available") {
    // 1. Snapshot current state
    const snapshot = {
      comments: item.comments,
      assignedRequest: item.assignedRequest,
      previousStatus: item.status,
      timestamp: new Date(),
    };

    // 2. Save to history
    await addToHistory(item.id, snapshot);

    // 3. Clear current assignment and comments
    await updateItem(item.id, {
      status: "available",
      comments: [],
      assignedTo: null,
      assignedRequest: null,
    });
  }
}

System Integration Patterns

1. ITSM + Workflow Integration

When a request is submitted, the workflow engine processes it:

async function processRequest(request: ITSMRequest) {
  const workflow = request.form.workflowBuilder;
  const currentNode = findNode(workflow, request.currentStep);

  if (!currentNode) return;

  switch (currentNode.type) {
    case "submitted":
      // Move to first approval/fulfillment node
      const nextEdge = workflow.edges.find((e) => e.source === currentNode.id);
      if (nextEdge) {
        await updateRequest(request.id, {
          currentStep: nextEdge.target,
          status: determineStatus(findNode(workflow, nextEdge.target)),
        });
        await notifyAssignees(request, nextEdge.target);
      }
      break;

    case "approval":
      // Check if all required approvals are met
      const approvalNode = currentNode.data;
      const minApprovals = approvalNode.minApprovals || 1;
      const stepApprovals = request.approvals.filter(
        (a) => a.step === currentNode.id,
      );

      if (stepApprovals.length >= minApprovals) {
        await moveToNextStep(request, workflow, currentNode.id);
      }
      break;

    case "fulfillment":
      // Mark as pending fulfillment
      await updateRequest(request.id, { status: "pending_fulfillment" });
      await notifyFulfillers(request, currentNode);
      break;
  }
}

2. ITSM + Inventory Integration

Equipment forms can assign inventory items:

async function handleEquipmentRequest(request: ITSMRequest) {
  if (request.form.formType !== "equipment") return;

  const inventoryType = request.form.inventoryType;

  // Find available item
  const availableItem = await findAvailableItem(inventoryType.id);

  if (availableItem) {
    // Assign item to request
    await updateItem(availableItem.id, {
      status: "occupied",
      assignedTo: request.requester,
      assignedRequest: request.id,
    });

    // Update request with assigned item
    await updateRequest(request.id, {
      assignedInventory: availableItem.id,
    });
  }
}

3. Dynamic Permission Propagation

When roles/permissions change, invalidate caches:

// In role update hook
async function afterRoleUpdate(role: Role) {
  // Find all users with this role
  const usersWithRole = await findUsersWithRole(role.id);

  // Invalidate their permission caches
  for (const user of usersWithRole) {
    invalidateUserPermissions(user.id);
  }

  // Also check groups with this role
  const groupsWithRole = await findGroupsWithRole(role.id);
  for (const group of groupsWithRole) {
    const groupMembers = await getGroupMembers(group.id);
    for (const member of groupMembers) {
      invalidateUserPermissions(member.id);
    }
  }
}

Best Practices

Security

Practice Description
Principle of Least Privilege Grant minimum permissions needed
Use Roles Over Direct Permissions Easier to manage and audit
Superadmin Separation Limit superadmin accounts, audit quarterly
Cache Invalidation Clear cache when permissions change
Server-Side Enforcement Never trust client-side permission checks alone

Performance

Practice Description
Permission Caching 5-minute TTL for permission resolution
Batch Queries Resolve roles/groups in parallel
Shallow Hierarchies Limit role/group nesting to 3-4 levels
Index Key Fields Index slug, status, user identifiers
Background Jobs Use job queues for permission creation

User Experience

Practice Description
Optimistic UI Visibility Show elements when cache is cold
Clear Error Messages Explain why access was denied
Self-Service Let users see their own permissions
Admin Tools Provide permission debugging for admins

Development

Practice Description
TypeScript Use type safety for permission slugs
Document Permissions Add descriptions to all permissions
Version Control Track permission changes in migrations
Test Multiple Roles Verify inheritance works correctly

Example Scenarios

Scenario 1: Category Manager

Setup:

  • User: john@company.com
  • Role: itsm-access-manager
  • Permissions: itsm-access.manage, itsm-access.approve

Capabilities:

  • ✅ View "Access" category in admin
  • ✅ Create/edit forms under "Access" category
  • ✅ Approve requests for "Access" forms
  • ✅ See all requests for "Access" forms
  • ❌ Cannot create new categories
  • ❌ Cannot access other categories

Scenario 2: Form Approver

Setup:

  • User: sarah@company.com
  • Permission: access-card-form.approve

Capabilities:

  • ✅ See requests for "Access Card" form
  • ✅ Approve/reject "Access Card" requests
  • ❌ Cannot see form in admin panel
  • ❌ Cannot edit the form
  • ❌ Cannot fulfill requests

Scenario 3: ITSM Administrator

Setup:

  • User: admin@company.com
  • Permission: itsm.admin

Capabilities:

  • ✅ Full access to all ITSM categories
  • ✅ Create/edit/delete categories and forms
  • ✅ See and manage all ITSM requests
  • ❌ Cannot access system collections (need system.manage)

Scenario 4: Inventory Maintainer

Setup:

  • User: tech@company.com
  • Role: inventory-vehicle-maintainer
  • Permissions: inventory-vehicle.read, inventory-vehicle.maintainer

Capabilities:

  • ✅ View vehicle inventory items
  • ✅ Resolve blueprint comments (mark repairs complete)
  • ❌ Cannot add new comments (need reviewer)
  • ❌ Cannot delete items (need admin)

Conclusion

Building enterprise portal systems requires:

  1. Visual Workflow Builder: ReactFlow-based editor storing workflows as JSON graphs
  2. RBAC System: Hierarchical permissions with inheritance, implications, and caching
  3. ITSM Module: Dynamic forms with automatic permission/role generation
  4. Asset Management: Type-based inventory with state machine lifecycle

The architecture emphasizes:

  • Flexibility: Dynamic configuration without code changes
  • Security: Layered permission checking with server-side enforcement
  • Performance: Caching and background job processing
  • Maintainability: Clear separation of concerns and documented patterns

Document Version: 1.0
Last Updated: January 2026
Status: ✅ Complete

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors