Forráskód Böngészése

Add client eviction feature

- Add EvictClient method to DHCP Server
- Add /api/dhcp/leases/evict endpoint to force client IP release
- Add 'Evict' button in Web UI for online clients
- Update table layout to include Action column
- Evicted client will be forced to get a new IP on next DHCP request
CNBUGS AI 1 hónapja
szülő
commit
5bb84c159a
4 módosított fájl, 83 hozzáadás és 1 törlés
  1. 23 0
      internal/dhcp/server.go
  2. 25 0
      internal/web/server.go
  3. 33 0
      web/static/js/app.js
  4. 2 1
      web/templates/index.html

+ 23 - 0
internal/dhcp/server.go

@@ -700,3 +700,26 @@ func newBroadcastUDPConn(host string, port int) (*net.UDPConn, error) {
 
 	return udpConn, nil
 }
+
+// EvictClient removes a client's lease, forcing them to get a new IP
+func (s *Server) EvictClient(mac string) error {
+	s.leaseMutex.Lock()
+	defer s.leaseMutex.Unlock()
+
+	lease, exists := s.leases[mac]
+	if !exists {
+		return fmt.Errorf("lease not found for MAC %s", mac)
+	}
+
+	// Remove from in-memory lease map
+	delete(s.leases, mac)
+	delete(s.usedIPs, lease.IP)
+
+	// Delete from database
+	if err := s.db.Where("mac = ?", mac).Delete(&db.DHCPLease{}).Error; err != nil {
+		return fmt.Errorf("failed to delete lease from database: %v", err)
+	}
+
+	log.Printf("DHCP: Evicted client %s (%s)", mac, lease.IP)
+	return nil
+}

+ 25 - 0
internal/web/server.go

@@ -117,6 +117,7 @@ func (s *Server) setupRoutes() {
 		protected.GET("/dhcp/bindings", s.handleGetBindings)
 		protected.POST("/dhcp/bindings", s.handleCreateBinding)
 		protected.DELETE("/dhcp/bindings/:id", s.handleDeleteBinding)
+		protected.POST("/dhcp/leases/evict", s.handleEvictClient)
 		
 		// DNS
 		protected.GET("/dns/config", s.handleGetDNSConfig)
@@ -282,6 +283,30 @@ func (s *Server) handleDeleteBinding(c *gin.Context) {
 	c.JSON(http.StatusOK, gin.H{"message": "Binding deleted"})
 }
 
+func (s *Server) handleEvictClient(c *gin.Context) {
+	var req struct {
+		MAC string `json:"mac"`
+		IP  string `json:"ip"`
+	}
+	
+	if err := c.ShouldBindJSON(&req); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+		return
+	}
+	
+	if req.MAC == "" {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "MAC is required"})
+		return
+	}
+	
+	if err := s.dhcpServer.EvictClient(req.MAC); err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+		return
+	}
+	
+	c.JSON(http.StatusOK, gin.H{"message": "Client evicted successfully"})
+}
+
 func (s *Server) handleGetRecords(c *gin.Context) {
 	records, err := s.dnsServer.GetDNSRecords()
 	if err != nil {

+ 33 - 0
web/static/js/app.js

@@ -185,6 +185,17 @@ async function loadClients() {
             statusCell.innerHTML = isActive ? '<span class="status-active">● 在线</span>' : '<span class="status-expired">● 已过期</span>';
             row.appendChild(statusCell);
             
+            // Action buttons
+            const actionCell = document.createElement('td');
+            if (isActive) {
+                const evictBtn = document.createElement('button');
+                evictBtn.textContent = '剔除';
+                evictBtn.style.cssText = 'background-color: #f39c12; color: white; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer; margin-right: 5px;';
+                evictBtn.onclick = () => evictClient(lease.MAC, lease.IP);
+                actionCell.appendChild(evictBtn);
+            }
+            row.appendChild(actionCell);
+            
             tbody.appendChild(row);
         });
         
@@ -502,6 +513,28 @@ async function deleteBinding(id) {
     }
 }
 
+async function evictClient(mac, ip) {
+    if (!confirm(`确定要剔除主机 ${mac} (${ip}) 吗?\n\n主机将被迫释放该 IP 并重新获取。`)) return;
+    
+    try {
+        const response = await fetch(`/api/dhcp/leases/evict`, {
+            method: 'POST',
+            headers: { 'X-Session-ID': sessionId, 'Content-Type': 'application/json' },
+            body: JSON.stringify({ mac, ip })
+        });
+        
+        if (response.ok) {
+            alert('主机已剔除,将重新获取 IP');
+            loadClients();
+        } else {
+            const data = await response.json();
+            alert('剔除失败:' + (data.error || '未知错误'));
+        }
+    } catch (error) {
+        alert('剔除失败:' + error.message);
+    }
+}
+
 // Load DNS Config
 async function loadDNSConfig() {
     try {

+ 2 - 1
web/templates/index.html

@@ -182,11 +182,12 @@
                             <th>租约剩余</th>
                             <th>过期时间</th>
                             <th>状态</th>
+                            <th>操作</th>
                         </tr>
                     </thead>
                     <tbody>
                         <tr>
-                            <td colspan="6" style="text-align:center;color:#999;">暂无客户端</td>
+                            <td colspan="7" style="text-align:center;color:#999;">暂无客户端</td>
                         </tr>
                     </tbody>
                 </table>