package handlers import ( "encoding/json" "log" "net/http" "strings" "nano-router/clients" "nano-router/config" "nano-router/dhcp" ) func HandleClients(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { fail(w, http.StatusMethodNotAllowed, "method not allowed") return } list, err := clients.GetAll() if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } ok(w, list) } func HandleClientUpdate(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { fail(w, http.StatusMethodNotAllowed, "method not allowed") return } mac := strings.TrimPrefix(r.URL.Path, "/api/clients/update/") if mac == "" { fail(w, http.StatusBadRequest, "mac address required") return } var req struct { Hostname string `json:"hostname"` Blocked bool `json:"blocked"` StaticIP string `json:"static_ip"` Policy string `json:"policy"` // "disabled" | "direct" | "vpn" | "" } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { fail(w, http.StatusBadRequest, "invalid json: "+err.Error()) return } if err := updateClient(mac, req.Hostname, req.Blocked, req.StaticIP, req.Policy); err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } go applyBlockedFirewall() go applyDHCPStaticBindings() ok(w, map[string]string{"message": "updated"}) } // HandleClientPolicyDefault handles GET/POST for the default client routing policy. func HandleClientPolicyDefault(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: cfg, err := config.Load() if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } ok(w, map[string]string{"default": cfg.ClientPolicy.Default}) case http.MethodPost: var req struct { Default string `json:"default"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { fail(w, http.StatusBadRequest, "invalid json: "+err.Error()) return } if req.Default != "disabled" && req.Default != "direct" && req.Default != "vpn" { fail(w, http.StatusBadRequest, "invalid policy: must be disabled, direct, or vpn") return } cfg, err := config.Load() if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } cfg.ClientPolicy.Default = req.Default if err := config.Save(cfg); err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } go applyBlockedFirewall() ok(w, map[string]string{"default": req.Default}) default: fail(w, http.StatusMethodNotAllowed, "method not allowed") } } // HandleClientPolicyApplyAll sets the given policy on every known device. func HandleClientPolicyApplyAll(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { fail(w, http.StatusMethodNotAllowed, "method not allowed") return } var req struct { Policy string `json:"policy"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { fail(w, http.StatusBadRequest, "invalid json: "+err.Error()) return } if req.Policy != "disabled" && req.Policy != "direct" && req.Policy != "vpn" { fail(w, http.StatusBadRequest, "invalid policy: must be disabled, direct, or vpn") return } cfg, err := config.Load() if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } for i := range cfg.KnownDevices { cfg.KnownDevices[i].Policy = req.Policy // Keep Blocked flag consistent: disabled policy means blocked. cfg.KnownDevices[i].Blocked = req.Policy == "disabled" } if err := config.Save(cfg); err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } go applyBlockedFirewall() ok(w, map[string]int{"updated": len(cfg.KnownDevices)}) } func updateClient(mac, hostname string, blocked bool, staticIP, policy string) error { cfg, err := config.Load() if err != nil { return err } found := false for i := range cfg.KnownDevices { if cfg.KnownDevices[i].MAC == mac { cfg.KnownDevices[i].Hostname = hostname cfg.KnownDevices[i].StaticIP = staticIP cfg.KnownDevices[i].Policy = policy // Derive Blocked from policy for backward compatibility. cfg.KnownDevices[i].Blocked = policy == "disabled" found = true break } } if !found { cfg.KnownDevices = append(cfg.KnownDevices, config.KnownDevice{ MAC: mac, Hostname: hostname, StaticIP: staticIP, Policy: policy, Blocked: policy == "disabled", }) } return config.Save(cfg) } func applyDHCPStaticBindings() { if !dhcp.IsInstalled() { return } cfg, err := config.Load() if err != nil { log.Printf("Warning: load config for DHCP static bindings: %v", err) return } var bindings []dhcp.StaticBinding for _, kd := range cfg.KnownDevices { if kd.StaticIP != "" && kd.MAC != "" { bindings = append(bindings, dhcp.StaticBinding{ MAC: kd.MAC, Host: kd.Hostname, IP: kd.StaticIP, }) } } dhcpCfg := &dhcp.Config{ Enabled: cfg.DHCP.Enabled, Pools: make([]dhcp.Pool, len(cfg.DHCP.Pools)), } for i, p := range cfg.DHCP.Pools { dhcpCfg.Pools[i] = dhcp.Pool{ Interface: p.Interface, Enabled: p.Enabled, Subnet: p.Subnet, Netmask: p.Netmask, RangeStart: p.RangeStart, RangeEnd: p.RangeEnd, Router: p.Router, DNS: p.DNS, LeaseTime: p.LeaseTime, } } if err := dhcp.WriteConfigsWithBindings(dhcpCfg, bindings); err != nil { log.Printf("Warning: write dnsmasq config with static bindings: %v", err) return } if dhcpCfg.Enabled { if err := dhcp.ServiceRestart(); err != nil { log.Printf("Warning: restart dnsmasq after static binding update: %v", err) } else { log.Printf("dnsmasq restarted with %d static bindings", len(bindings)) } } }