feat(app): add WGRplane web UI and backend features

- Add Vue 3 frontend with glassmorphism design (Tailwind CSS)
- Add Go backend handlers: auth, webhooks, stats, scheduler, validation
- Add i18n support (EN, ID, ZH)
- Add Swagger docs and API handlers
- Add nftables integration and plugins support
- Remove deprecated go.mod (migrated to wgrplane)
This commit is contained in:
datadunia
2026-05-03 23:20:34 +07:00
parent 3eae539a56
commit 6d8899a078
54 changed files with 7395 additions and 434 deletions
+785
View File
@@ -0,0 +1,785 @@
package main
import (
"encoding/json"
"net/http"
"fmt"
"github.com/gorilla/mux"
"gorm.io/gorm"
qrcode "github.com/skip2/go-qrcode"
)
// Helper wrappers
func respondJSON(w http.ResponseWriter, status int, payload interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(payload)
}
func respondError(w http.ResponseWriter, status int, message string) {
respondJSON(w, status, map[string]string{"error": message})
}
// ListServers lists all registered WireGuard servers.
// @Summary List all servers
// @Description Returns a list of all registered WireGuard servers.
// @Tags servers
// @Accept json
// @Produce json
// @Success 200 {array} Server
// @Failure 500 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/servers [get]
func getServers(w http.ResponseWriter, r *http.Request) {
var servers []Server
if err := db.Find(&servers).Error; err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
respondJSON(w, http.StatusOK, servers)
}
// CreateServer creates a new WireGuard server entry.
// @Summary Create a server
// @Description Creates a new WireGuard server with the provided configuration.
// @Tags servers
// @Accept json
// @Produce json
// @Param server body Server true "Server configuration"
// @Success 201 {object} Server
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/servers [post]
func createServer(w http.ResponseWriter, r *http.Request) {
var s Server
if err := json.NewDecoder(r.Body).Decode(&s); err != nil {
respondError(w, http.StatusBadRequest, "invalid request payload")
return
}
// Input validation
if !ValidatePublicKey(s.PublicKey) {
respondError(w, http.StatusBadRequest, "invalid public key")
return
}
if err := db.Create(&s).Error; err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
respondJSON(w, http.StatusCreated, s)
}
// GetServer retrieves a single WireGuard server by ID.
// @Summary Get a server
// @Description Returns a single WireGuard server by its ID.
// @Tags servers
// @Produce json
// @Param id path string true "Server ID"
// @Success 200 {object} Server
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/servers/{id} [get]
func getServer(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
var s Server
if err := db.First(&s, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
respondError(w, http.StatusNotFound, "server not found")
} else {
respondError(w, http.StatusInternalServerError, err.Error())
}
return
}
respondJSON(w, http.StatusOK, s)
}
// UpdateServer updates an existing WireGuard server.
// @Summary Update a server
// @Description Updates fields of an existing WireGuard server.
// @Tags servers
// @Accept json
// @Produce json
// @Param id path string true "Server ID"
// @Param server body Server true "Updated server fields"
// @Success 200 {object} Server
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/servers/{id} [put]
func updateServer(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
var s Server
if err := db.First(&s, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
respondError(w, http.StatusNotFound, "server not found")
} else {
respondError(w, http.StatusInternalServerError, err.Error())
}
return
}
var updates Server
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
respondError(w, http.StatusBadRequest, "invalid request payload")
return
}
// Input validation for updated fields
if updates.PublicKey != "" && !ValidatePublicKey(updates.PublicKey) {
respondError(w, http.StatusBadRequest, "invalid public key")
return
}
// Apply updates using fields defined in models.go
s.Name = updates.Name
s.Mode = updates.Mode
s.PublicKey = updates.PublicKey
s.Endpoint = updates.Endpoint
if err := db.Save(&s).Error; err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
respondJSON(w, http.StatusOK, s)
}
// DeleteServer removes a WireGuard server and cascades to its peers and webhooks.
// @Summary Delete a server
// @Description Deletes a WireGuard server and cascade-removes its peers and webhooks.
// @Tags servers
// @Produce json
// @Param id path string true "Server ID"
// @Success 204 "No content"
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/servers/{id} [delete]
func deleteServer(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
var s Server
if err := db.First(&s, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
respondError(w, http.StatusNotFound, "server not found")
} else {
respondError(w, http.StatusInternalServerError, err.Error())
}
return
}
if err := db.Delete(&s).Error; err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
respondJSON(w, http.StatusNoContent, nil)
}
// ListServerPeers lists all peers for a given WireGuard server.
// @Summary List server peers
// @Description Returns all peers belonging to a specific WireGuard server.
// @Tags peers
// @Produce json
// @Param id path string true "Server ID"
// @Success 200 {array} Peer
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/servers/{id}/peers [get]
func getServerPeers(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
// Ensure server exists
var s Server
if err := db.First(&s, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
respondError(w, http.StatusNotFound, "server not found")
} else {
respondError(w, http.StatusInternalServerError, err.Error())
}
return
}
var peers []Peer
if err := db.Where("server_id = ?", id).Find(&peers).Error; err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
respondJSON(w, http.StatusOK, peers)
}
// CreateServerPeer creates a new peer for a WireGuard server.
// @Summary Create a peer
// @Description Creates a new WireGuard peer. Applies nftables rules in forward mode or triggers webhooks in standalone mode.
// @Tags peers
// @Accept json
// @Produce json
// @Param id path string true "Server ID"
// @Param peer body Peer true "Peer configuration"
// @Success 201 {object} Peer
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/servers/{id}/peers [post]
func createServerPeer(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
// ensure server exists
var s Server
if err := db.First(&s, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
respondError(w, http.StatusNotFound, "server not found")
} else {
respondError(w, http.StatusInternalServerError, err.Error())
}
return
}
var p Peer
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
respondError(w, http.StatusBadRequest, "invalid request payload")
return
}
// Input validation for peer
if !ValidatePublicKey(p.PublicKey) {
respondError(w, http.StatusBadRequest, "invalid public key")
return
}
if !ValidateIP(p.IP) {
respondError(w, http.StatusBadRequest, "invalid ip address for peer IP")
return
}
// Optional: validate AllowAccess CIDRs if provided
if len(p.AllowAccess) > 0 {
var targets []string
if err := json.Unmarshal(p.AllowAccess, &targets); err == nil {
for _, t := range targets {
if !ValidateCIDR(t) {
respondError(w, http.StatusBadRequest, "invalid CIDR in AllowAccess")
return
}
}
}
}
p.ServerID = s.ID
if err := db.Create(&p).Error; err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
// Trigger mode-specific actions after creation
policyChanges := []string{"peer_created"}
if s.Mode == "forward" {
// Apply nftables rules for the new peer
if len(p.AllowAccess) > 0 {
var targets []string
if err := json.Unmarshal(p.AllowAccess, &targets); err == nil {
for _, t := range targets {
_ = AddAccessRule(p.IP, t)
}
}
}
_ = SetInternetAccess(p.IP, p.AllowInternet)
// Notify policy change
_ = TriggerWebhook("policy_changed", &s, &p, "created", policyChanges)
} else if s.Mode == "standalone" {
// Notify remote via webhooks
_ = TriggerWebhook("peer_created", &s, &p, "created", []string{})
// Also notify policy change
_ = TriggerWebhook("policy_changed", &s, &p, "created", policyChanges)
}
respondJSON(w, http.StatusCreated, p)
}
// UpdatePeer updates an existing WireGuard peer.
// @Summary Update a peer
// @Description Updates a peer and computes diffs to apply incremental nftables changes.
// @Tags peers
// @Accept json
// @Produce json
// @Param id path string true "Peer ID"
// @Param peer body Peer true "Updated peer fields"
// @Success 200 {object} Peer
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/peers/{id} [put]
func updatePeer(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
var p Peer
if err := db.First(&p, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
respondError(w, http.StatusNotFound, "peer not found")
} else {
respondError(w, http.StatusInternalServerError, err.Error())
}
return
}
var updates Peer
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
respondError(w, http.StatusBadRequest, "invalid request payload")
return
}
// Preserve the original peer for diff computation
oldP := p
// Validate updated fields
if updates.PublicKey != "" && !ValidatePublicKey(updates.PublicKey) {
respondError(w, http.StatusBadRequest, "invalid public key")
return
}
if updates.IP != "" && !ValidateIP(updates.IP) {
respondError(w, http.StatusBadRequest, "invalid ip address for peer IP")
return
}
if len(updates.AllowAccess) > 0 {
var targets []string
if err := json.Unmarshal(updates.AllowAccess, &targets); err == nil {
for _, t := range targets {
if !ValidateCIDR(t) {
respondError(w, http.StatusBadRequest, "invalid CIDR in AllowAccess")
return
}
}
}
}
// Build a list of policy-related changes for notification purposes
changes := []string{}
if updates.PublicKey != "" && updates.PublicKey != p.PublicKey {
changes = append(changes, "PublicKey updated")
}
if updates.IP != "" && updates.IP != p.IP {
changes = append(changes, "IP updated")
}
if len(updates.AllowAccess) > 0 && string(updates.AllowAccess) != string(p.AllowAccess) {
changes = append(changes, "AllowAccess updated")
}
if updates.AllowInternet != p.AllowInternet {
changes = append(changes, "AllowInternet updated")
}
// Apply updates based on the Peer model fields
p.PublicKey = updates.PublicKey
p.IP = updates.IP
p.AllowAccess = updates.AllowAccess
p.AllowInternet = updates.AllowInternet
if err := db.Save(&p).Error; err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
// If policy-related fields changed, apply mode-specific actions
if len(changes) > 0 {
var server Server
if err := db.First(&server, "id = ?", p.ServerID).Error; err == nil {
if server.Mode == "forward" {
// nftables-based updates: diff AllowAccess and IP/internet changes
// Compute targets diffs between old and new
var oldTargets []string
if len(oldP.AllowAccess) > 0 {
_ = json.Unmarshal(oldP.AllowAccess, &oldTargets)
}
var newTargets []string
if len(p.AllowAccess) > 0 {
_ = json.Unmarshal(p.AllowAccess, &newTargets)
}
// Determine additions/removals
added := []string{}
for _, t := range newTargets {
found := false
for _, o := range oldTargets {
if o == t { found = true; break }
}
if !found { added = append(added, t) }
}
removed := []string{}
for _, o := range oldTargets {
found := false
for _, t := range newTargets {
if o == t { found = true; break }
}
if !found { removed = append(removed, o) }
}
// Apply removals first for safety
fromIP := oldP.IP
if oldP.IP == "" {
fromIP = p.IP
}
for _, t := range removed {
_ = RemoveAccessRule(fromIP, t)
}
// Apply additions for the current IP
toIP := p.IP
for _, t := range added {
_ = AddAccessRule(toIP, t)
}
// Internet access flag
_ = SetInternetAccess(p.IP, p.AllowInternet)
// Notify policy change
_ = TriggerWebhook("policy_changed", &server, &p, "updated", changes)
} else if server.Mode == "standalone" {
_ = TriggerWebhook("peer_updated", &server, &p, "updated", changes)
// Also notify policy change
_ = TriggerWebhook("policy_changed", &server, &p, "updated", changes)
}
}
}
respondJSON(w, http.StatusOK, p)
}
// DeletePeer removes a WireGuard peer and cleans up nftables rules.
// @Summary Delete a peer
// @Description Deletes a peer, cleans up nftables rules, and triggers webhooks.
// @Tags peers
// @Produce json
// @Param id path string true "Peer ID"
// @Success 204 "No content"
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/peers/{id} [delete]
func deletePeer(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
var p Peer
if err := db.First(&p, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
respondError(w, http.StatusNotFound, "peer not found")
} else {
respondError(w, http.StatusInternalServerError, err.Error())
}
return
}
// If in forward mode, remove any NFTables rules for this peer before deletion
var server Server
if err := db.First(&server, "id = ?", p.ServerID).Error; err == nil {
if server.Mode == "forward" {
var targets []string
if len(p.AllowAccess) > 0 {
_ = json.Unmarshal(p.AllowAccess, &targets)
}
for _, t := range targets {
_ = RemoveAccessRule(p.IP, t)
}
_ = SetInternetAccess(p.IP, false)
// Notify policy change
_ = TriggerWebhook("policy_changed", &server, &p, "deleted", []string{"peer_deleted"})
}
}
if err := db.Delete(&p).Error; err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
// Notify external systems about the deletion according to server mode
if err := db.First(&server, "id = ?", p.ServerID).Error; err == nil {
if server.Mode == "standalone" {
_ = TriggerWebhook("peer_deleted", &server, &p, "deleted", []string{})
// Also notify policy change
_ = TriggerWebhook("policy_changed", &server, &p, "deleted", []string{"peer_deleted"})
}
}
respondJSON(w, http.StatusNoContent, nil)
}
// GetSMTPSettings retrieves the current SMTP configuration.
// @Summary Get SMTP settings
// @Description Returns the current SMTP configuration for email notifications.
// @Tags settings
// @Produce json
// @Success 200 {object} SMTPSettings
// @Failure 500 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/settings/smtp [get]
func getSMTPSettings(w http.ResponseWriter, r *http.Request) {
settings, err := GetSMTPSettings()
if err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
respondJSON(w, http.StatusOK, settings)
}
// SetSMTPSettings saves SMTP configuration for email notifications.
// @Summary Save SMTP settings
// @Description Saves or updates SMTP configuration for email notifications.
// @Tags settings
// @Accept json
// @Produce json
// @Param settings body SMTPSettings true "SMTP configuration"
// @Success 200 {object} SMTPSettings
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/settings/smtp [post]
func setSMTPSettings(w http.ResponseWriter, r *http.Request) {
var s SMTPSettings
if err := json.NewDecoder(r.Body).Decode(&s); err != nil {
respondError(w, http.StatusBadRequest, "invalid request payload")
return
}
if err := SaveSMTPSettings(s); err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
respondJSON(w, http.StatusOK, s)
}
// ListServerWebhooks lists all webhooks for a WireGuard server.
// @Summary List server webhooks
// @Description Returns all webhooks configured for a specific WireGuard server.
// @Tags webhooks
// @Produce json
// @Param id path string true "Server ID"
// @Success 200 {array} Webhook
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/servers/{id}/webhooks [get]
func getServerWebhooks(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
var s Server
if err := db.First(&s, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
respondError(w, http.StatusNotFound, "server not found")
} else {
respondError(w, http.StatusInternalServerError, err.Error())
}
return
}
var webhooks []Webhook
if err := db.Where("server_id = ?", id).Find(&webhooks).Error; err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
respondJSON(w, http.StatusOK, webhooks)
}
// CreateServerWebhook creates a new webhook for a WireGuard server.
// @Summary Create a webhook
// @Description Creates a new webhook configuration for a WireGuard server.
// @Tags webhooks
// @Accept json
// @Produce json
// @Param id path string true "Server ID"
// @Param webhook body Webhook true "Webhook configuration"
// @Success 201 {object} Webhook
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/servers/{id}/webhooks [post]
func createServerWebhook(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
var s Server
if err := db.First(&s, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
respondError(w, http.StatusNotFound, "server not found")
} else {
respondError(w, http.StatusInternalServerError, err.Error())
}
return
}
var wb Webhook
if err := json.NewDecoder(r.Body).Decode(&wb); err != nil {
respondError(w, http.StatusBadRequest, "invalid request payload")
return
}
wb.ServerID = s.ID
if err := db.Create(&wb).Error; err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
respondJSON(w, http.StatusCreated, wb)
}
// DeleteWebhook removes a webhook configuration.
// @Summary Delete a webhook
// @Description Deletes a webhook by its ID.
// @Tags webhooks
// @Produce json
// @Param id path string true "Webhook ID"
// @Success 204 "No content"
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/webhooks/{id} [delete]
func deleteWebhook(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
var wb Webhook
if err := db.First(&wb, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
respondError(w, http.StatusNotFound, "webhook not found")
} else {
respondError(w, http.StatusInternalServerError, err.Error())
}
return
}
if err := db.Delete(&wb).Error; err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
respondJSON(w, http.StatusNoContent, nil)
}
// GetPeerConfig downloads the WireGuard configuration file for a peer.
// @Summary Get peer config
// @Description Downloads the WireGuard .conf file for a specific peer.
// @Tags peers
// @Produce plain
// @Param id path string true "Peer ID"
// @Success 200 {string} string "WireGuard configuration file"
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/peers/{id}/config [get]
func getPeerConfig(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
var p Peer
if err := db.First(&p, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
respondError(w, http.StatusNotFound, "peer not found")
} else {
respondError(w, http.StatusInternalServerError, err.Error())
}
return
}
var s Server
if err := db.First(&s, "id = ?", p.ServerID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
respondError(w, http.StatusNotFound, "server not found for peer")
} else {
respondError(w, http.StatusInternalServerError, err.Error())
}
return
}
conf, err := GeneratePeerConfig(p, s)
if err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=peer-%d.conf", p.ID))
w.Write(conf)
}
// GetPeerQRCode returns a QR code PNG image for the peer configuration.
// @Summary Get peer QR code
// @Description Returns a QR code PNG image of the peer config for mobile import.
// @Tags peers
// @Produce png
// @Param id path string true "Peer ID"
// @Success 200 {file} binary "QR code PNG image"
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/peers/{id}/qrcode [get]
func getPeerQRCode(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
var p Peer
if err := db.First(&p, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
respondError(w, http.StatusNotFound, "peer not found")
} else {
respondError(w, http.StatusInternalServerError, err.Error())
}
return
}
var s Server
if err := db.First(&s, "id = ?", p.ServerID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
respondError(w, http.StatusNotFound, "server not found for peer")
} else {
respondError(w, http.StatusInternalServerError, err.Error())
}
return
}
conf, err := GeneratePeerConfig(p, s)
if err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
png, err := qrcode.Encode(string(conf), qrcode.Medium, 256)
if err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
w.Header().Set("Content-Type", "image/png")
w.Write(png)
}
// GetStats returns global statistics for servers, peers, and webhooks.
// @Summary Get global stats
// @Description Returns global statistics including total servers, peers, and webhooks.
// @Tags stats
// @Produce json
// @Success 200 {object} map[string]int64
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/stats [get]
func getStats(w http.ResponseWriter, r *http.Request) {
var serverCount int64
var peerCount int64
var webhookCount int64
db.Model(&Server{}).Count(&serverCount)
db.Model(&Peer{}).Count(&peerCount)
db.Model(&Webhook{}).Count(&webhookCount)
resp := map[string]interface{}{
"servers": serverCount,
"peers": peerCount,
"webhooks": webhookCount,
}
respondJSON(w, http.StatusOK, resp)
}
// GetServerStats returns per-server statistics.
// @Summary Get server stats
// @Description Returns per-server statistics including peer count and webhook count.
// @Tags stats
// @Produce json
// @Param id path string true "Server ID"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/servers/{id}/stats [get]
func getServerStats(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
var s Server
if err := db.First(&s, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
respondError(w, http.StatusNotFound, "server not found")
} else {
respondError(w, http.StatusInternalServerError, err.Error())
}
return
}
var peers []Peer
var webhooks []Webhook
db.Where("server_id = ?", id).Find(&peers)
db.Where("server_id = ?", id).Find(&webhooks)
resp := map[string]interface{}{
"server": s,
"peers": len(peers),
"webhooks": len(webhooks),
}
respondJSON(w, http.StatusOK, resp)
}