TTMT.ManageWebGUI/INTEGRATION_GUIDE_VI.md

25 KiB

Tích Hợp MeshCentral vào Hệ Thống React TypeScript

Tổng Quan

MeshCentral cung cấp WebSocket API để tương tác với các chức năng như:

  • Tạo Device Group (Mesh)
  • Thêm/xóa thiết bị
  • Quản lý người dùng
  • Điều khiển thiết bị

1. KIẾN TRÚC GIAO TIẾP

1.1 WebSocket Communication

MeshCentral sử dụng WebSocket cho real-time communication. UI của bạn sẽ kết nối qua:

ws://meshcentral-server:port/ws

1.2 Flow Tương Tác

React App (Client)
    ↓
WebSocket Connection
    ↓
MeshCentral Server (meshuser.js)
    ↓
Database (MongoDB)
    ↓
Response back to Client

2. SETUP REACT TYPESCRIPT CLIENT

2.1 Tạo MeshCentral Service

// services/meshcentral.service.ts
export interface MeshCentralConfig {
  serverUrl: string;
  username: string;
  password: string;
  domain?: string;
}

export interface DeviceGroup {
  _id: string;
  name: string;
  desc: string;
  mtype: number; // 1: FreeAgent, 2: IntelAMT, 3: Mixed
}

export interface Device {
  _id: string;
  name: string;
  meshid: string;
  host: string;
  state: string; // online, offline, unknown
  rname?: string; // remote name
}

export class MeshCentralService {
  private ws: WebSocket | null = null;
  private messageId = 1;
  private pendingRequests = new Map<number, {
    resolve: (value: any) => void;
    reject: (error: any) => void;
    timeout: NodeJS.Timeout;
  }>();
  private messageHandlers = new Map<string, (data: any) => void>();

  constructor(private config: MeshCentralConfig) {}

  /**
   * Kết nối đến MeshCentral Server
   */
  async connect(): Promise<void> {
    return new Promise((resolve, reject) => {
      try {
        const wsUrl = `${this.config.serverUrl.replace(/^http/, 'ws')}/control.ashx`;
        this.ws = new WebSocket(wsUrl);

        this.ws.onopen = () => {
          console.log('Connected to MeshCentral');
          this.authenticate().then(resolve).catch(reject);
        };

        this.ws.onmessage = (event) => this.handleMessage(event.data);
        this.ws.onerror = (error) => {
          console.error('WebSocket error:', error);
          reject(error);
        };
        this.ws.onclose = () => {
          console.log('Disconnected from MeshCentral');
          this.ws = null;
        };
      } catch (error) {
        reject(error);
      }
    });
  }

  /**
   * Xác thực với server
   */
  private async authenticate(): Promise<void> {
    const authMessage = {
      action: 'authCookie',
      username: this.config.username,
      password: this.config.password,
      domain: this.config.domain || ''
    };

    return this.sendMessage(authMessage);
  }

  /**
   * Gửi message và chờ response
   */
  private async sendMessage(message: any): Promise<any> {
    return new Promise((resolve, reject) => {
      if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
        reject(new Error('WebSocket not connected'));
        return;
      }

      const msgId = this.messageId++;
      message.sessionid = msgId;

      const timeout = setTimeout(() => {
        this.pendingRequests.delete(msgId);
        reject(new Error('Request timeout'));
      }, 30000); // 30 seconds timeout

      this.pendingRequests.set(msgId, { resolve, reject, timeout });

      try {
        this.ws.send(JSON.stringify(message));
      } catch (error) {
        this.pendingRequests.delete(msgId);
        clearTimeout(timeout);
        reject(error);
      }
    });
  }

  /**
   * Xử lý incoming messages
   */
  private handleMessage(data: string): void {
    try {
      const message = JSON.parse(data);

      // Handle response to pending request
      if (message.sessionid && this.pendingRequests.has(message.sessionid)) {
        const { resolve, reject, timeout } = this.pendingRequests.get(message.sessionid)!;
        this.pendingRequests.delete(message.sessionid);
        clearTimeout(timeout);

        if (message.error) {
          reject(new Error(message.error));
        } else {
          resolve(message);
        }
      }

      // Handle event handlers
      if (message.action && this.messageHandlers.has(message.action)) {
        const handler = this.messageHandlers.get(message.action)!;
        handler(message);
      }
    } catch (error) {
      console.error('Error parsing message:', error);
    }
  }

  /**
   * Đăng ký handler cho event
   */
  on(action: string, handler: (data: any) => void): void {
    this.messageHandlers.set(action, handler);
  }

  /**
   * === TẠIDEVICE GROUP ===
   * Tạo một device group (Mesh) mới
   */
  async createDeviceGroup(name: string, desc: string = ''): Promise<DeviceGroup> {
    const response = await this.sendMessage({
      action: 'createmesh',
      meshname: name,
      meshdesc: desc,
      meshtype: 2 // 2 = Intel AMT capable
    });

    if (response.error) {
      throw new Error(`Failed to create device group: ${response.error}`);
    }

    return response.meshid;
  }

  /**
   * Lấy danh sách device groups
   */
  async getDeviceGroups(): Promise<DeviceGroup[]> {
    const response = await this.sendMessage({
      action: 'meshes'
    });

    return response.meshes || [];
  }

  /**
   * === THÊM DEVICE ===
   * Lấy Agent invite link để install trên device
   */
  async getAgentInviteLink(meshId: string, platform: string = 'linux'): Promise<string> {
    const response = await this.sendMessage({
      action: 'getmesh',
      meshid: meshId
    });

    // Agent download URL format:
    // /meshagents?id=<meshid>&installflags=<flags>&exeType=<type>
    const flags = 3; // Windows install flags
    return `/meshagents?id=${meshId}&installflags=${flags}&exeType=${platform}`;
  }

  /**
   * Lấy danh sách devices trong group
   */
  async getDevices(meshId: string): Promise<Device[]> {
    const response = await this.sendMessage({
      action: 'getmesh',
      meshid: meshId
    });

    return response.nodes || [];
  }

  /**
   * === QUẢN LÝ DEVICE ===
   * Vô hiệu hóa/kích hoạt device
   */
  async setDeviceState(deviceId: string, enabled: boolean): Promise<void> {
    await this.sendMessage({
      action: 'changedevice',
      nodeid: deviceId,
      enabled: enabled
    });
  }

  /**
   * Xóa device khỏi group
   */
  async removeDevice(deviceId: string): Promise<void> {
    await this.sendMessage({
      action: 'removenode',
      nodeids: [deviceId]
    });
  }

  /**
   * === QUẢN LÝ NGƯỜI DÙNG ===
   * Tạo user mới
   */
  async createUser(username: string, password: string, email?: string): Promise<any> {
    const response = await this.sendMessage({
      action: 'createuser',
      username: username,
      password: password,
      email: email || ''
    });

    return response;
  }

  /**
   * Gán quyền truy cập device group cho user
   * rights: bitmask của permissions
   * - 1: Edit mesh
   * - 2: Manage users
   * - 4: Manage computers
   * - 8: Remote control
   * - 16: Agent console
   * - 32: Server files
   * - 64: Wake device
   * - 128: Set notes
   * - 256: Remote view only
   */
  async addUserToMesh(meshId: string, username: string, rights: number): Promise<void> {
    await this.sendMessage({
      action: 'editmesh',
      meshid: meshId,
      usernames: [username],
      rights: rights
    });
  }

  /**
   * === ĐIỀU KHIỂN THIẾT BỊ ===
   * Gửi command đến device
   */
  async sendDeviceCommand(deviceId: string, command: string, parameters?: any): Promise<any> {
    return this.sendMessage({
      action: 'runcommand',
      nodeid: deviceId,
      command: command,
      params: parameters || {}
    });
  }

  /**
   * Khởi động lại device
   */
  async rebootDevice(deviceId: string): Promise<void> {
    await this.sendMessage({
      action: 'poweraction',
      nodeid: deviceId,
      actiontype: 1 // 1 = reboot
    });
  }

  /**
   * Tắt device
   */
  async shutdownDevice(deviceId: string): Promise<void> {
    await this.sendMessage({
      action: 'poweraction',
      nodeid: deviceId,
      actiontype: 2 // 2 = shutdown
    });
  }

  /**
   * Disconnect user session từ MeshCentral
   */
  disconnect(): void {
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
    this.pendingRequests.clear();
    this.messageHandlers.clear();
  }
}

2.2 Tạo Custom Hook

// hooks/useMeshCentral.ts
import { useEffect, useState, useCallback, useRef } from 'react';
import { MeshCentralService, DeviceGroup, Device } from '../services/meshcentral.service';

export const useMeshCentral = (config: any) => {
  const serviceRef = useRef<MeshCentralService | null>(null);
  const [connected, setConnected] = useState(false);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [deviceGroups, setDeviceGroups] = useState<DeviceGroup[]>([]);
  const [devices, setDevices] = useState<Device[]>([]);

  // Kết nối
  const connect = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      
      const service = new MeshCentralService(config);
      await service.connect();
      
      serviceRef.current = service;
      setConnected(true);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Connection failed');
      setConnected(false);
    } finally {
      setLoading(false);
    }
  }, [config]);

  // Tạo Device Group
  const createGroup = useCallback(async (name: string, desc: string = '') => {
    try {
      setError(null);
      if (!serviceRef.current) throw new Error('Not connected');
      
      const group = await serviceRef.current.createDeviceGroup(name, desc);
      return group;
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Failed to create group';
      setError(message);
      throw err;
    }
  }, []);

  // Lấy danh sách Device Groups
  const loadDeviceGroups = useCallback(async () => {
    try {
      setError(null);
      if (!serviceRef.current) throw new Error('Not connected');
      
      const groups = await serviceRef.current.getDeviceGroups();
      setDeviceGroups(groups);
      return groups;
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Failed to load groups';
      setError(message);
      return [];
    }
  }, []);

  // Lấy danh sách Devices
  const loadDevices = useCallback(async (meshId: string) => {
    try {
      setError(null);
      if (!serviceRef.current) throw new Error('Not connected');
      
      const devs = await serviceRef.current.getDevices(meshId);
      setDevices(devs);
      return devs;
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Failed to load devices';
      setError(message);
      return [];
    }
  }, []);

  // Cleanup
  useEffect(() => {
    return () => {
      if (serviceRef.current) {
        serviceRef.current.disconnect();
      }
    };
  }, []);

  return {
    service: serviceRef.current,
    connected,
    loading,
    error,
    deviceGroups,
    devices,
    connect,
    createGroup,
    loadDeviceGroups,
    loadDevices
  };
};

3. REACT COMPONENTS

3.1 Device Group Manager

// components/DeviceGroupManager.tsx
import React, { useState, useEffect } from 'react';
import { useMeshCentral } from '../hooks/useMeshCentral';

export const DeviceGroupManager: React.FC = () => {
  const mesh = useMeshCentral({
    serverUrl: 'https://your-meshcentral-server.com',
    username: 'admin',
    password: process.env.REACT_APP_MESH_PASSWORD,
    domain: ''
  });

  const [newGroupName, setNewGroupName] = useState('');
  const [newGroupDesc, setNewGroupDesc] = useState('');
  const [creating, setCreating] = useState(false);

  useEffect(() => {
    // Kết nối khi component mount
    mesh.connect();
  }, []);

  useEffect(() => {
    // Load groups khi kết nối thành công
    if (mesh.connected) {
      mesh.loadDeviceGroups();
    }
  }, [mesh.connected]);

  const handleCreateGroup = async () => {
    if (!newGroupName.trim()) return;

    setCreating(true);
    try {
      await mesh.createGroup(newGroupName, newGroupDesc);
      setNewGroupName('');
      setNewGroupDesc('');
      
      // Reload groups
      await mesh.loadDeviceGroups();
    } finally {
      setCreating(false);
    }
  };

  return (
    <div className="device-group-manager">
      <h2>Device Groups</h2>

      {/* Connection Status */}
      <div className={`status ${mesh.connected ? 'connected' : 'disconnected'}`}>
        {mesh.connected ? '🟢 Connected' : '🔴 Disconnected'}
        {mesh.loading && <span> (Connecting...)</span>}
      </div>

      {/* Error Message */}
      {mesh.error && (
        <div className="error-message">{mesh.error}</div>
      )}

      {/* Create New Group Form */}
      <div className="create-group-form">
        <h3>Create New Group</h3>
        <input
          type="text"
          placeholder="Group name"
          value={newGroupName}
          onChange={(e) => setNewGroupName(e.target.value)}
          disabled={creating || !mesh.connected}
        />
        <input
          type="text"
          placeholder="Description (optional)"
          value={newGroupDesc}
          onChange={(e) => setNewGroupDesc(e.target.value)}
          disabled={creating || !mesh.connected}
        />
        <button 
          onClick={handleCreateGroup}
          disabled={creating || !mesh.connected || !newGroupName.trim()}
        >
          {creating ? 'Creating...' : 'Create Group'}
        </button>
      </div>

      {/* Groups List */}
      <div className="groups-list">
        <h3>Existing Groups ({mesh.deviceGroups.length})</h3>
        {mesh.deviceGroups.length === 0 ? (
          <p>No device groups found</p>
        ) : (
          <ul>
            {mesh.deviceGroups.map((group: any) => (
              <li key={group._id}>
                <strong>{group.name}</strong>
                {group.desc && <p>{group.desc}</p>}
                <small>ID: {group._id}</small>
              </li>
            ))}
          </ul>
        )}
      </div>
    </div>
  );
};

3.2 Device Manager

// components/DeviceManager.tsx
import React, { useState, useEffect } from 'react';
import { useMeshCentral } from '../hooks/useMeshCentral';

interface DeviceManagerProps {
  meshId: string;
}

export const DeviceManager: React.FC<DeviceManagerProps> = ({ meshId }) => {
  const mesh = useMeshCentral({
    serverUrl: 'https://your-meshcentral-server.com',
    username: 'admin',
    password: process.env.REACT_APP_MESH_PASSWORD
  });

  const [agentUrl, setAgentUrl] = useState('');
  const [showAgentUrl, setShowAgentUrl] = useState(false);

  useEffect(() => {
    mesh.connect();
  }, []);

  useEffect(() => {
    if (mesh.connected) {
      mesh.loadDevices(meshId);
    }
  }, [mesh.connected, meshId]);

  const handleGetAgentUrl = async () => {
    try {
      const url = await mesh.service?.getAgentInviteLink(meshId, 'linux');
      setAgentUrl(url || '');
      setShowAgentUrl(true);
    } catch (err) {
      console.error('Error getting agent URL:', err);
    }
  };

  const handleRemoveDevice = async (deviceId: string) => {
    if (!window.confirm('Are you sure?')) return;

    try {
      await mesh.service?.removeDevice(deviceId);
      await mesh.loadDevices(meshId);
    } catch (err) {
      console.error('Error removing device:', err);
    }
  };

  const handleRebootDevice = async (deviceId: string) => {
    if (!window.confirm('Reboot device?')) return;

    try {
      await mesh.service?.rebootDevice(deviceId);
    } catch (err) {
      console.error('Error rebooting device:', err);
    }
  };

  return (
    <div className="device-manager">
      <h2>Devices in Group: {meshId}</h2>

      <button 
        onClick={handleGetAgentUrl}
        disabled={!mesh.connected}
      >
        Get Agent URL
      </button>

      {showAgentUrl && (
        <div className="agent-url">
          <p>Install link:</p>
          <input 
            type="text" 
            value={`${window.location.origin}${agentUrl}`}
            readOnly
          />
          <button onClick={() => {
            navigator.clipboard.writeText(`${window.location.origin}${agentUrl}`);
          }}>
            Copy
          </button>
        </div>
      )}

      <div className="devices-list">
        <h3>Devices ({mesh.devices.length})</h3>
        {mesh.devices.length === 0 ? (
          <p>No devices</p>
        ) : (
          <table>
            <thead>
              <tr>
                <th>Name</th>
                <th>Status</th>
                <th>Actions</th>
              </tr>
            </thead>
            <tbody>
              {mesh.devices.map((device: any) => (
                <tr key={device._id}>
                  <td>{device.name}</td>
                  <td className={`status ${device.conn ? 'online' : 'offline'}`}>
                    {device.conn ? '🟢 Online' : '🔴 Offline'}
                  </td>
                  <td>
                    <button onClick={() => handleRebootDevice(device._id)}>
                      Reboot
                    </button>
                    <button 
                      onClick={() => handleRemoveDevice(device._id)}
                      className="danger"
                    >
                      Remove
                    </button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </div>
    </div>
  );
};

4. AUTHENTICATION

4.1 Methods

MeshCentral hỗ trợ:

  1. Basic Auth (credentials embed in WebSocket message)
{
  action: 'authCookie',
  username: 'admin',
  password: 'password',
  domain: ''
}
  1. Token Auth (nếu MeshCentral cấu hình OAuth/SSO)
{
  action: 'authToken',
  token: 'jwt-token-here'
}

4.2 Secure Setup

// .env.local
REACT_APP_MESH_SERVER=https://your-meshcentral-server.com
REACT_APP_MESH_USER=your-api-user
REACT_APP_MESH_PASSWORD=your-secure-password

// hoặc sử dụng backend proxy
REACT_APP_MESH_API_ENDPOINT=/api/mesh/

5. WEBSOCKET MESSAGE PROTOCOL

5.1 Request Format

{
  action: 'command-name',
  sessionid: number,      // auto-increment per client
  meshid?: string,
  nodeid?: string,
  ...otherParams
}

5.2 Response Format

{
  action: 'command-response',
  sessionid: number,      // matches request
  error?: string,         // if error occurred
  ... response data
}

5.3 Common Actions

Action Purpose Parameters
createmesh Tạo device group meshname, meshdesc, meshtype
meshes Lấy danh sách groups (none)
getmesh Chi tiết group meshid
editmesh Sửa group meshid, name, desc
deletemesh Xóa group meshid, meshname
nodes Lấy danh sách devices meshid
removenode Xóa device nodeids[]
changedevice Sửa device nodeid, name, desc, etc
poweraction Power control nodeid, actiontype (1=reboot, 2=shutdown)
auth* Authentication (see Auth section)

6. ERROR HANDLING

// service error response examples
{
  error: 'Invalid credentials',
  sessionid: 1
}

{
  error: 'Permission denied',
  sessionid: 2
}

{
  error: 'Device not found',
  sessionid: 3
}

Custom Error Handling

private handleMessage(data: string): void {
  try {
    const message = JSON.parse(data);

    if (message.sessionid && this.pendingRequests.has(message.sessionid)) {
      const { resolve, reject, timeout } = this.pendingRequests.get(message.sessionid)!;
      this.pendingRequests.delete(message.sessionid);
      clearTimeout(timeout);

      if (message.error) {
        // Map error codes to user-friendly messages
        const errorMap: Record<string, string> = {
          'Invalid credentials': 'Username or password incorrect',
          'Permission denied': 'You do not have permission for this action',
          'Device not found': 'Device does not exist',
          'Mesh not found': 'Device group does not exist'
        };

        const userMessage = errorMap[message.error] || message.error;
        reject(new Error(userMessage));
      } else {
        resolve(message);
      }
    }
  } catch (error) {
    console.error('Error parsing message:', error);
  }
}

7. BEST PRACTICES

7.1 Connection Management

// Reconnect logic
private async reconnect(): Promise<void> {
  const maxRetries = 3;
  let retries = 0;

  while (retries < maxRetries) {
    try {
      await this.connect();
      return;
    } catch (error) {
      retries++;
      if (retries < maxRetries) {
        await new Promise(resolve => setTimeout(resolve, 1000 * retries));
      }
    }
  }

  throw new Error('Failed to connect after retries');
}

7.2 Event Subscriptions

// Subscribe to device updates
mesh.on('nodechanged', (data) => {
  console.log('Device updated:', data);
  // Reload device list
  mesh.loadDevices(selectedMeshId);
});

// Subscribe to user login
mesh.on('userlogin', (data) => {
  console.log('User logged in:', data.username);
});

7.3 Batch Operations

// Xóa multiple devices
async removeMultipleDevices(deviceIds: string[]): Promise<void> {
  await this.sendMessage({
    action: 'removenode',
    nodeids: deviceIds
  });
}

// Add users to mesh
async addUsersToMesh(meshId: string, users: string[], rights: number): Promise<void> {
  await this.sendMessage({
    action: 'editmesh',
    meshid: meshId,
    usernames: users,
    rights: rights
  });
}

7.4 Permissions Check

// MESHRIGHT constants
const MESHRIGHT_EDITMESH = 0x00000001;
const MESHRIGHT_MANAGEUSERS = 0x00000002;
const MESHRIGHT_MANAGECOMPUTERS = 0x00000004;
const MESHRIGHT_REMOTECONTROL = 0x00000008;

function hasPermission(userRights: number, requiredRight: number): boolean {
  return (userRights & requiredRight) === requiredRight;
}

// Sử dụng
if (hasPermission(userRights, MESHRIGHT_MANAGECOMPUTERS)) {
  // Allow device management
}

8. CONFIGURATION EXAMPLE

8.1 MeshCentral Server config.json

{
  "settings": {
    "cert": "meshcentral",
    "mongodb": "mongodb://127.0.0.1:27017/meshcentral",
    "domain": {
      "": {
        "title": "MeshCentral",
        "title2": "My System",
        "minpasswordlen": 8,
        "https": true
      }
    }
  }
}

8.2 React App config

// config/meshcentral.config.ts
export const meshCentralConfig = {
  production: {
    serverUrl: 'https://meshcentral.example.com',
    wsUrl: 'wss://meshcentral.example.com'
  },
  development: {
    serverUrl: 'http://localhost:8081',
    wsUrl: 'ws://localhost:8081'
  }
};

9. TYPESCRIPT TYPES (Full API)

// Complete types
export interface MeshCentralMessage {
  action: string;
  sessionid?: number;
  error?: string;
}

export interface CreateMeshRequest {
  action: 'createmesh';
  meshname: string;
  meshdesc?: string;
  meshtype?: number;
}

export interface CreateMeshResponse {
  action: 'meshchange';
  mesh: DeviceGroup;
}

export interface GetMeshesResponse {
  action: 'meshes';
  meshes: DeviceGroup[];
}

export interface DeviceChangeEvent {
  action: 'nodechanged';
  node: Device;
}

export interface PowerActionRequest {
  action: 'poweraction';
  nodeid: string;
  actiontype: 1 | 2 | 5; // 1=reboot, 2=shutdown, 5=wake
}

10. TROUBLESHOOTING

Issue: WebSocket Connection Failed

Solution: Check CORS settings in MeshCentral config
Add to config.json:
"https": { "headers": { "Access-Control-Allow-Origin": "*" } }

Issue: Authentication Failed

Solution: Verify credentials and domain
check MeshCentral admin panel for user
ensure domain name matches in request

Issue: Permission Denied

Solution: Check user permissions in MeshCentral
Admin > Users > Select user > Mesh access
Add necessary permissions (edit mesh, manage computers, etc)

Issue: Device Not Responding

Solution: Verify device agent status
1. Check device is online in MeshCentral UI
2. Verify agent has internet connectivity
3. Check firewall/NAT rules allow agent communication

11. EXAMPLES

Complete Example: Create Group and Add Devices

async function setupNewTeam(teamName: string, deviceIds: string[]) {
  const mesh = new MeshCentralService({
    serverUrl: 'https://meshcentral.example.com',
    username: 'admin',
    password: process.env.MESH_PASSWORD
  });

  try {
    // 1. Connect
    await mesh.connect();
    console.log('Connected');

    // 2. Create group
    const group = await mesh.createDeviceGroup(teamName, `Team: ${teamName}`);
    console.log('Group created:', group);

    // 3. Get agent URL
    const agentUrl = await mesh.getAgentInviteLink(group._id, 'windows');
    console.log('Agent URL:', agentUrl);

    // 4. Add users
    const readOnlyRights = 256; // MESHRIGHT_REMOTEVIEWONLY
    const adminRights = 0xFFFFFFFF;
    
    await mesh.addUserToMesh(group._id, 'user@example.com', readOnlyRights);
    console.log('User added');

    // 5. Deploy agents (manual step or via script)
    console.log(`Deploy agents using: ${agentUrl}`);

    return group;
  } finally {
    mesh.disconnect();
  }
}

📚 Tài Liệu Tham Khảo