6d8899a078
- 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)
786 lines
27 KiB
Go
786 lines
27 KiB
Go
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)
|
|
}
|