No One Can Stop Me Now
This commit is contained in:
272
services/setec-manager/internal/handlers/autarch.go
Normal file
272
services/setec-manager/internal/handlers/autarch.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"setec-manager/internal/deploy"
|
||||
)
|
||||
|
||||
type autarchStatus struct {
|
||||
Installed bool `json:"installed"`
|
||||
InstallDir string `json:"install_dir"`
|
||||
GitCommit string `json:"git_commit"`
|
||||
VenvReady bool `json:"venv_ready"`
|
||||
PipPackages int `json:"pip_packages"`
|
||||
WebRunning bool `json:"web_running"`
|
||||
WebStatus string `json:"web_status"`
|
||||
DNSRunning bool `json:"dns_running"`
|
||||
DNSStatus string `json:"dns_status"`
|
||||
}
|
||||
|
||||
func (h *Handler) AutarchStatus(w http.ResponseWriter, r *http.Request) {
|
||||
status := h.getAutarchStatus()
|
||||
h.render(w, "autarch.html", status)
|
||||
}
|
||||
|
||||
func (h *Handler) AutarchStatusAPI(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, h.getAutarchStatus())
|
||||
}
|
||||
|
||||
func (h *Handler) getAutarchStatus() autarchStatus {
|
||||
dir := h.Config.Autarch.InstallDir
|
||||
status := autarchStatus{InstallDir: dir}
|
||||
|
||||
// Check if installed
|
||||
if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil {
|
||||
status.Installed = true
|
||||
}
|
||||
|
||||
// Git commit
|
||||
if hash, message, err := deploy.CurrentCommit(dir); err == nil {
|
||||
status.GitCommit = hash + " " + message
|
||||
}
|
||||
|
||||
// Venv
|
||||
status.VenvReady = deploy.VenvExists(dir)
|
||||
|
||||
// Pip packages
|
||||
venvDir := filepath.Join(dir, "venv")
|
||||
if pkgs, err := deploy.ListPackages(venvDir); err == nil {
|
||||
status.PipPackages = len(pkgs)
|
||||
}
|
||||
|
||||
// Web service
|
||||
webActive, _ := deploy.IsActive("autarch-web")
|
||||
status.WebRunning = webActive
|
||||
if webActive {
|
||||
status.WebStatus = "active"
|
||||
} else {
|
||||
status.WebStatus = "inactive"
|
||||
}
|
||||
|
||||
// DNS service
|
||||
dnsActive, _ := deploy.IsActive("autarch-dns")
|
||||
status.DNSRunning = dnsActive
|
||||
if dnsActive {
|
||||
status.DNSStatus = "active"
|
||||
} else {
|
||||
status.DNSStatus = "inactive"
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
func (h *Handler) AutarchInstall(w http.ResponseWriter, r *http.Request) {
|
||||
dir := h.Config.Autarch.InstallDir
|
||||
repo := h.Config.Autarch.GitRepo
|
||||
branch := h.Config.Autarch.GitBranch
|
||||
|
||||
if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil {
|
||||
writeError(w, http.StatusConflict, "AUTARCH already installed at "+dir)
|
||||
return
|
||||
}
|
||||
|
||||
depID, _ := h.DB.CreateDeployment(nil, "autarch_install")
|
||||
var output strings.Builder
|
||||
|
||||
steps := []struct {
|
||||
label string
|
||||
fn func() error
|
||||
}{
|
||||
{"Clone from GitHub", func() error {
|
||||
os.MkdirAll(filepath.Dir(dir), 0755)
|
||||
out, err := deploy.Clone(repo, branch, dir)
|
||||
output.WriteString(out)
|
||||
return err
|
||||
}},
|
||||
{"Create Python venv", func() error {
|
||||
return deploy.CreateVenv(dir)
|
||||
}},
|
||||
{"Upgrade pip", func() error {
|
||||
venvDir := filepath.Join(dir, "venv")
|
||||
deploy.UpgradePip(venvDir)
|
||||
return nil
|
||||
}},
|
||||
{"Install pip packages", func() error {
|
||||
reqFile := filepath.Join(dir, "requirements.txt")
|
||||
if _, err := os.Stat(reqFile); err != nil {
|
||||
return nil
|
||||
}
|
||||
venvDir := filepath.Join(dir, "venv")
|
||||
out, err := deploy.InstallRequirements(venvDir, reqFile)
|
||||
output.WriteString(out)
|
||||
return err
|
||||
}},
|
||||
{"Install npm packages", func() error {
|
||||
out, _ := deploy.NpmInstall(dir)
|
||||
output.WriteString(out)
|
||||
return nil
|
||||
}},
|
||||
{"Set permissions", func() error {
|
||||
exec.Command("chown", "-R", "root:root", dir).Run()
|
||||
exec.Command("chmod", "-R", "755", dir).Run()
|
||||
for _, d := range []string{"data", "data/certs", "data/dns", "results", "dossiers", "models"} {
|
||||
os.MkdirAll(filepath.Join(dir, d), 0755)
|
||||
}
|
||||
confPath := filepath.Join(dir, "autarch_settings.conf")
|
||||
if _, err := os.Stat(confPath); err == nil {
|
||||
exec.Command("chmod", "600", confPath).Run()
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
{"Install systemd units", func() error {
|
||||
h.installAutarchUnits(dir)
|
||||
return nil
|
||||
}},
|
||||
}
|
||||
|
||||
for _, step := range steps {
|
||||
output.WriteString(fmt.Sprintf("\n=== %s ===\n", step.label))
|
||||
if err := step.fn(); err != nil {
|
||||
h.DB.FinishDeployment(depID, "failed", output.String())
|
||||
writeError(w, http.StatusInternalServerError, fmt.Sprintf("%s failed: %v", step.label, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
h.DB.FinishDeployment(depID, "success", output.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "installed"})
|
||||
}
|
||||
|
||||
func (h *Handler) AutarchUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
dir := h.Config.Autarch.InstallDir
|
||||
|
||||
depID, _ := h.DB.CreateDeployment(nil, "autarch_update")
|
||||
var output strings.Builder
|
||||
|
||||
// Git pull
|
||||
out, err := deploy.Pull(dir)
|
||||
output.WriteString(out)
|
||||
if err != nil {
|
||||
h.DB.FinishDeployment(depID, "failed", output.String())
|
||||
writeError(w, http.StatusInternalServerError, "git pull failed")
|
||||
return
|
||||
}
|
||||
|
||||
// Reinstall pip packages
|
||||
reqFile := filepath.Join(dir, "requirements.txt")
|
||||
if _, err := os.Stat(reqFile); err == nil {
|
||||
venvDir := filepath.Join(dir, "venv")
|
||||
pipOut, _ := deploy.InstallRequirements(venvDir, reqFile)
|
||||
output.WriteString(pipOut)
|
||||
}
|
||||
|
||||
// Restart services
|
||||
deploy.Restart("autarch-web")
|
||||
deploy.Restart("autarch-dns")
|
||||
|
||||
h.DB.FinishDeployment(depID, "success", output.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
|
||||
}
|
||||
|
||||
func (h *Handler) AutarchStart(w http.ResponseWriter, r *http.Request) {
|
||||
deploy.Start("autarch-web")
|
||||
deploy.Start("autarch-dns")
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "started"})
|
||||
}
|
||||
|
||||
func (h *Handler) AutarchStop(w http.ResponseWriter, r *http.Request) {
|
||||
deploy.Stop("autarch-web")
|
||||
deploy.Stop("autarch-dns")
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "stopped"})
|
||||
}
|
||||
|
||||
func (h *Handler) AutarchRestart(w http.ResponseWriter, r *http.Request) {
|
||||
deploy.Restart("autarch-web")
|
||||
deploy.Restart("autarch-dns")
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "restarted"})
|
||||
}
|
||||
|
||||
func (h *Handler) AutarchConfig(w http.ResponseWriter, r *http.Request) {
|
||||
confPath := filepath.Join(h.Config.Autarch.InstallDir, "autarch_settings.conf")
|
||||
data, err := os.ReadFile(confPath)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "config not found")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"config": string(data)})
|
||||
}
|
||||
|
||||
func (h *Handler) AutarchConfigUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Config string `json:"config"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid body")
|
||||
return
|
||||
}
|
||||
|
||||
confPath := filepath.Join(h.Config.Autarch.InstallDir, "autarch_settings.conf")
|
||||
if err := os.WriteFile(confPath, []byte(body.Config), 0600); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "saved"})
|
||||
}
|
||||
|
||||
func (h *Handler) AutarchDNSBuild(w http.ResponseWriter, r *http.Request) {
|
||||
dnsDir := filepath.Join(h.Config.Autarch.InstallDir, "services", "dns-server")
|
||||
|
||||
depID, _ := h.DB.CreateDeployment(nil, "dns_build")
|
||||
|
||||
cmd := exec.Command("go", "build", "-o", "autarch-dns", ".")
|
||||
cmd.Dir = dnsDir
|
||||
out, err := cmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
h.DB.FinishDeployment(depID, "failed", string(out))
|
||||
writeError(w, http.StatusInternalServerError, "build failed: "+string(out))
|
||||
return
|
||||
}
|
||||
|
||||
h.DB.FinishDeployment(depID, "success", string(out))
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "built"})
|
||||
}
|
||||
|
||||
func (h *Handler) installAutarchUnits(dir string) {
|
||||
webUnit := deploy.GenerateUnit(deploy.UnitConfig{
|
||||
Name: "autarch-web",
|
||||
Description: "AUTARCH Web Dashboard",
|
||||
ExecStart: filepath.Join(dir, "venv", "bin", "python3") + " " + filepath.Join(dir, "autarch_web.py"),
|
||||
WorkingDirectory: dir,
|
||||
User: "root",
|
||||
Environment: map[string]string{"PYTHONUNBUFFERED": "1"},
|
||||
})
|
||||
|
||||
dnsUnit := deploy.GenerateUnit(deploy.UnitConfig{
|
||||
Name: "autarch-dns",
|
||||
Description: "AUTARCH DNS Server",
|
||||
ExecStart: filepath.Join(dir, "services", "dns-server", "autarch-dns") + " --config " + filepath.Join(dir, "data", "dns", "config.json"),
|
||||
WorkingDirectory: dir,
|
||||
User: "root",
|
||||
})
|
||||
|
||||
deploy.InstallUnit("autarch-web", webUnit)
|
||||
deploy.InstallUnit("autarch-dns", dnsUnit)
|
||||
}
|
||||
146
services/setec-manager/internal/handlers/backups.go
Normal file
146
services/setec-manager/internal/handlers/backups.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (h *Handler) BackupList(w http.ResponseWriter, r *http.Request) {
|
||||
backups, err := h.DB.ListBackups()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if acceptsJSON(r) {
|
||||
writeJSON(w, http.StatusOK, backups)
|
||||
return
|
||||
}
|
||||
h.render(w, "backups.html", backups)
|
||||
}
|
||||
|
||||
func (h *Handler) BackupSite(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramInt(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
site, err := h.DB.GetSite(id)
|
||||
if err != nil || site == nil {
|
||||
writeError(w, http.StatusNotFound, "site not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Create backup directory
|
||||
backupDir := h.Config.Backups.Dir
|
||||
os.MkdirAll(backupDir, 0755)
|
||||
|
||||
timestamp := time.Now().Format("20060102-150405")
|
||||
filename := fmt.Sprintf("site-%s-%s.tar.gz", site.Domain, timestamp)
|
||||
backupPath := filepath.Join(backupDir, filename)
|
||||
|
||||
// Create tar.gz
|
||||
cmd := exec.Command("tar", "-czf", backupPath, "-C", filepath.Dir(site.AppRoot), filepath.Base(site.AppRoot))
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, fmt.Sprintf("backup failed: %s", string(out)))
|
||||
return
|
||||
}
|
||||
|
||||
// Get file size
|
||||
info, _ := os.Stat(backupPath)
|
||||
size := int64(0)
|
||||
if info != nil {
|
||||
size = info.Size()
|
||||
}
|
||||
|
||||
bID, _ := h.DB.CreateBackup(&id, "site", backupPath, size)
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"id": bID,
|
||||
"path": backupPath,
|
||||
"size": size,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) BackupFull(w http.ResponseWriter, r *http.Request) {
|
||||
backupDir := h.Config.Backups.Dir
|
||||
os.MkdirAll(backupDir, 0755)
|
||||
|
||||
timestamp := time.Now().Format("20060102-150405")
|
||||
filename := fmt.Sprintf("full-system-%s.tar.gz", timestamp)
|
||||
backupPath := filepath.Join(backupDir, filename)
|
||||
|
||||
// Backup key directories
|
||||
dirs := []string{
|
||||
h.Config.Nginx.Webroot,
|
||||
"/etc/nginx",
|
||||
"/opt/setec-manager/data",
|
||||
}
|
||||
|
||||
args := []string{"-czf", backupPath}
|
||||
for _, d := range dirs {
|
||||
if _, err := os.Stat(d); err == nil {
|
||||
args = append(args, d)
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command("tar", args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, fmt.Sprintf("backup failed: %s", string(out)))
|
||||
return
|
||||
}
|
||||
|
||||
info, _ := os.Stat(backupPath)
|
||||
size := int64(0)
|
||||
if info != nil {
|
||||
size = info.Size()
|
||||
}
|
||||
|
||||
bID, _ := h.DB.CreateBackup(nil, "full", backupPath, size)
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"id": bID,
|
||||
"path": backupPath,
|
||||
"size": size,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) BackupDelete(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramInt(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
// Get backup info to delete file
|
||||
var filePath string
|
||||
h.DB.Conn().QueryRow(`SELECT file_path FROM backups WHERE id=?`, id).Scan(&filePath)
|
||||
if filePath != "" {
|
||||
os.Remove(filePath)
|
||||
}
|
||||
|
||||
h.DB.DeleteBackup(id)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||
}
|
||||
|
||||
func (h *Handler) BackupDownload(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramInt(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
var filePath string
|
||||
h.DB.Conn().QueryRow(`SELECT file_path FROM backups WHERE id=?`, id).Scan(&filePath)
|
||||
if filePath == "" {
|
||||
writeError(w, http.StatusNotFound, "backup not found")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(filePath)))
|
||||
http.ServeFile(w, r, filePath)
|
||||
}
|
||||
151
services/setec-manager/internal/handlers/dashboard.go
Normal file
151
services/setec-manager/internal/handlers/dashboard.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"setec-manager/internal/deploy"
|
||||
"setec-manager/internal/system"
|
||||
)
|
||||
|
||||
type systemInfo struct {
|
||||
Hostname string `json:"hostname"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
CPUs int `json:"cpus"`
|
||||
Uptime string `json:"uptime"`
|
||||
LoadAvg string `json:"load_avg"`
|
||||
MemTotal string `json:"mem_total"`
|
||||
MemUsed string `json:"mem_used"`
|
||||
MemPercent float64 `json:"mem_percent"`
|
||||
DiskTotal string `json:"disk_total"`
|
||||
DiskUsed string `json:"disk_used"`
|
||||
DiskPercent float64 `json:"disk_percent"`
|
||||
SiteCount int `json:"site_count"`
|
||||
Services []serviceInfo `json:"services"`
|
||||
}
|
||||
|
||||
type serviceInfo struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Running bool `json:"running"`
|
||||
}
|
||||
|
||||
func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
info := h.gatherSystemInfo()
|
||||
h.render(w, "dashboard.html", info)
|
||||
}
|
||||
|
||||
func (h *Handler) SystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, h.gatherSystemInfo())
|
||||
}
|
||||
|
||||
func (h *Handler) gatherSystemInfo() systemInfo {
|
||||
info := systemInfo{
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
CPUs: runtime.NumCPU(),
|
||||
}
|
||||
|
||||
// Hostname — no wrapper, keep exec.Command
|
||||
if out, err := exec.Command("hostname").Output(); err == nil {
|
||||
info.Hostname = strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
// Uptime
|
||||
if ut, err := system.GetUptime(); err == nil {
|
||||
info.Uptime = "up " + ut.HumanReadable
|
||||
}
|
||||
|
||||
// Load average
|
||||
if la, err := system.GetLoadAvg(); err == nil {
|
||||
info.LoadAvg = fmt.Sprintf("%.2f %.2f %.2f", la.Load1, la.Load5, la.Load15)
|
||||
}
|
||||
|
||||
// Memory
|
||||
if mem, err := system.GetMemory(); err == nil {
|
||||
info.MemTotal = mem.Total
|
||||
info.MemUsed = mem.Used
|
||||
if mem.TotalBytes > 0 {
|
||||
info.MemPercent = float64(mem.UsedBytes) / float64(mem.TotalBytes) * 100
|
||||
}
|
||||
}
|
||||
|
||||
// Disk — find the root mount from the disk list
|
||||
if disks, err := system.GetDisk(); err == nil {
|
||||
for _, d := range disks {
|
||||
if d.MountPoint == "/" {
|
||||
info.DiskTotal = d.Size
|
||||
info.DiskUsed = d.Used
|
||||
pct := strings.TrimSuffix(d.UsePercent, "%")
|
||||
info.DiskPercent, _ = strconv.ParseFloat(pct, 64)
|
||||
break
|
||||
}
|
||||
}
|
||||
// If no root mount found but we have disks, use the first one
|
||||
if info.DiskTotal == "" && len(disks) > 0 {
|
||||
d := disks[0]
|
||||
info.DiskTotal = d.Size
|
||||
info.DiskUsed = d.Used
|
||||
pct := strings.TrimSuffix(d.UsePercent, "%")
|
||||
info.DiskPercent, _ = strconv.ParseFloat(pct, 64)
|
||||
}
|
||||
}
|
||||
|
||||
// Site count
|
||||
if sites, err := h.DB.ListSites(); err == nil {
|
||||
info.SiteCount = len(sites)
|
||||
}
|
||||
|
||||
// Services
|
||||
services := []struct{ name, unit string }{
|
||||
{"Nginx", "nginx"},
|
||||
{"AUTARCH Web", "autarch-web"},
|
||||
{"AUTARCH DNS", "autarch-dns"},
|
||||
{"Setec Manager", "setec-manager"},
|
||||
}
|
||||
for _, svc := range services {
|
||||
si := serviceInfo{Name: svc.name}
|
||||
active, err := deploy.IsActive(svc.unit)
|
||||
if err == nil && active {
|
||||
si.Status = "active"
|
||||
si.Running = true
|
||||
} else {
|
||||
si.Status = "inactive"
|
||||
si.Running = false
|
||||
}
|
||||
info.Services = append(info.Services, si)
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
func formatBytes(b float64) string {
|
||||
units := []string{"B", "KB", "MB", "GB", "TB"}
|
||||
i := 0
|
||||
for b >= 1024 && i < len(units)-1 {
|
||||
b /= 1024
|
||||
i++
|
||||
}
|
||||
return strconv.FormatFloat(b, 'f', 1, 64) + " " + units[i]
|
||||
}
|
||||
|
||||
// uptimeSince returns a human-readable duration.
|
||||
func uptimeSince(d time.Duration) string {
|
||||
days := int(d.Hours()) / 24
|
||||
hours := int(d.Hours()) % 24
|
||||
mins := int(d.Minutes()) % 60
|
||||
|
||||
if days > 0 {
|
||||
return strconv.Itoa(days) + "d " + strconv.Itoa(hours) + "h " + strconv.Itoa(mins) + "m"
|
||||
}
|
||||
if hours > 0 {
|
||||
return strconv.Itoa(hours) + "h " + strconv.Itoa(mins) + "m"
|
||||
}
|
||||
return strconv.Itoa(mins) + "m"
|
||||
}
|
||||
184
services/setec-manager/internal/handlers/firewall.go
Normal file
184
services/setec-manager/internal/handlers/firewall.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"setec-manager/internal/system"
|
||||
)
|
||||
|
||||
type firewallRule struct {
|
||||
ID int64 `json:"id"`
|
||||
Direction string `json:"direction"`
|
||||
Protocol string `json:"protocol"`
|
||||
Port string `json:"port"`
|
||||
Source string `json:"source"`
|
||||
Action string `json:"action"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
type firewallStatus struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Rules []firewallRule `json:"rules"`
|
||||
UFWOut string `json:"ufw_output"`
|
||||
}
|
||||
|
||||
func (h *Handler) FirewallList(w http.ResponseWriter, r *http.Request) {
|
||||
status := h.getFirewallStatus()
|
||||
if acceptsJSON(r) {
|
||||
writeJSON(w, http.StatusOK, status)
|
||||
return
|
||||
}
|
||||
h.render(w, "firewall.html", status)
|
||||
}
|
||||
|
||||
func (h *Handler) FirewallStatus(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, h.getFirewallStatus())
|
||||
}
|
||||
|
||||
func (h *Handler) FirewallAddRule(w http.ResponseWriter, r *http.Request) {
|
||||
var rule firewallRule
|
||||
if err := json.NewDecoder(r.Body).Decode(&rule); err != nil {
|
||||
rule.Port = r.FormValue("port")
|
||||
rule.Protocol = r.FormValue("protocol")
|
||||
rule.Source = r.FormValue("source")
|
||||
rule.Action = r.FormValue("action")
|
||||
rule.Comment = r.FormValue("comment")
|
||||
}
|
||||
|
||||
if rule.Port == "" {
|
||||
writeError(w, http.StatusBadRequest, "port is required")
|
||||
return
|
||||
}
|
||||
if rule.Protocol == "" {
|
||||
rule.Protocol = "tcp"
|
||||
}
|
||||
if rule.Action == "" {
|
||||
rule.Action = "allow"
|
||||
}
|
||||
if rule.Source == "" {
|
||||
rule.Source = "any"
|
||||
}
|
||||
|
||||
ufwRule := system.UFWRule{
|
||||
Port: rule.Port,
|
||||
Protocol: rule.Protocol,
|
||||
Source: rule.Source,
|
||||
Action: rule.Action,
|
||||
Comment: rule.Comment,
|
||||
}
|
||||
|
||||
if err := system.FirewallAddRule(ufwRule); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Save to DB
|
||||
h.DB.Conn().Exec(`INSERT INTO firewall_rules (direction, protocol, port, source, action, comment)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`, "in", rule.Protocol, rule.Port, rule.Source, rule.Action, rule.Comment)
|
||||
|
||||
writeJSON(w, http.StatusCreated, map[string]string{"status": "rule added"})
|
||||
}
|
||||
|
||||
func (h *Handler) FirewallDeleteRule(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramInt(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
// Get rule from DB to build delete command
|
||||
var port, protocol, action string
|
||||
err = h.DB.Conn().QueryRow(`SELECT port, protocol, action FROM firewall_rules WHERE id=?`, id).
|
||||
Scan(&port, &protocol, &action)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "rule not found")
|
||||
return
|
||||
}
|
||||
|
||||
system.FirewallDeleteRule(system.UFWRule{
|
||||
Port: port,
|
||||
Protocol: protocol,
|
||||
Action: action,
|
||||
})
|
||||
h.DB.Conn().Exec(`DELETE FROM firewall_rules WHERE id=?`, id)
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "rule deleted"})
|
||||
}
|
||||
|
||||
func (h *Handler) FirewallEnable(w http.ResponseWriter, r *http.Request) {
|
||||
if err := system.FirewallEnable(); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "enabled"})
|
||||
}
|
||||
|
||||
func (h *Handler) FirewallDisable(w http.ResponseWriter, r *http.Request) {
|
||||
if err := system.FirewallDisable(); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "disabled"})
|
||||
}
|
||||
|
||||
func (h *Handler) getFirewallStatus() firewallStatus {
|
||||
status := firewallStatus{}
|
||||
|
||||
enabled, _, raw, _ := system.FirewallStatus()
|
||||
status.UFWOut = raw
|
||||
status.Enabled = enabled
|
||||
|
||||
// Load rules from DB
|
||||
rows, err := h.DB.Conn().Query(`SELECT id, direction, protocol, port, source, action, comment
|
||||
FROM firewall_rules WHERE enabled=TRUE ORDER BY id`)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var rule firewallRule
|
||||
rows.Scan(&rule.ID, &rule.Direction, &rule.Protocol, &rule.Port,
|
||||
&rule.Source, &rule.Action, &rule.Comment)
|
||||
status.Rules = append(status.Rules, rule)
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
func (h *Handler) InstallDefaultFirewall() error {
|
||||
// Set default policies
|
||||
system.FirewallSetDefaults("deny", "allow")
|
||||
|
||||
// Add default rules
|
||||
defaultRules := []system.UFWRule{
|
||||
{Port: "22", Protocol: "tcp", Action: "allow", Comment: "SSH"},
|
||||
{Port: "80", Protocol: "tcp", Action: "allow", Comment: "HTTP"},
|
||||
{Port: "443", Protocol: "tcp", Action: "allow", Comment: "HTTPS"},
|
||||
{Port: "9090", Protocol: "tcp", Action: "allow", Comment: "Setec Manager"},
|
||||
{Port: "8181", Protocol: "tcp", Action: "allow", Comment: "AUTARCH Web"},
|
||||
{Port: "53", Protocol: "", Action: "allow", Comment: "AUTARCH DNS"},
|
||||
}
|
||||
|
||||
for _, rule := range defaultRules {
|
||||
system.FirewallAddRule(rule)
|
||||
}
|
||||
|
||||
// Enable the firewall
|
||||
system.FirewallEnable()
|
||||
|
||||
// Record in DB
|
||||
dbRules := []firewallRule{
|
||||
{Port: "22", Protocol: "tcp", Action: "allow", Comment: "SSH"},
|
||||
{Port: "80", Protocol: "tcp", Action: "allow", Comment: "HTTP"},
|
||||
{Port: "443", Protocol: "tcp", Action: "allow", Comment: "HTTPS"},
|
||||
{Port: "9090", Protocol: "tcp", Action: "allow", Comment: "Setec Manager"},
|
||||
{Port: "8181", Protocol: "tcp", Action: "allow", Comment: "AUTARCH Web"},
|
||||
{Port: "53", Protocol: "tcp", Action: "allow", Comment: "AUTARCH DNS"},
|
||||
}
|
||||
for _, rule := range dbRules {
|
||||
h.DB.Conn().Exec(`INSERT OR IGNORE INTO firewall_rules (direction, protocol, port, source, action, comment)
|
||||
VALUES ('in', ?, ?, 'any', ?, ?)`, rule.Protocol, rule.Port, rule.Action, rule.Comment)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
66
services/setec-manager/internal/handlers/float.go
Normal file
66
services/setec-manager/internal/handlers/float.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (h *Handler) FloatRegister(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.Config.Float.Enabled {
|
||||
writeError(w, http.StatusServiceUnavailable, "Float Mode is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
UserAgent string `json:"user_agent"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&body)
|
||||
|
||||
// Parse TTL
|
||||
ttl, err := time.ParseDuration(h.Config.Float.SessionTTL)
|
||||
if err != nil {
|
||||
ttl = 24 * time.Hour
|
||||
}
|
||||
|
||||
sessionID := uuid.New().String()
|
||||
clientIP := r.RemoteAddr
|
||||
|
||||
if err := h.DB.CreateFloatSession(sessionID, 0, clientIP, body.UserAgent, time.Now().Add(ttl)); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, map[string]string{
|
||||
"session_id": sessionID,
|
||||
"expires_in": h.Config.Float.SessionTTL,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) FloatSessions(w http.ResponseWriter, r *http.Request) {
|
||||
// Clean expired sessions first
|
||||
h.DB.CleanExpiredFloatSessions()
|
||||
|
||||
sessions, err := h.DB.ListFloatSessions()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if acceptsJSON(r) {
|
||||
writeJSON(w, http.StatusOK, sessions)
|
||||
return
|
||||
}
|
||||
h.render(w, "float.html", sessions)
|
||||
}
|
||||
|
||||
func (h *Handler) FloatDisconnect(w http.ResponseWriter, r *http.Request) {
|
||||
id := paramStr(r, "id")
|
||||
if err := h.DB.DeleteFloatSession(id); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "disconnected"})
|
||||
}
|
||||
103
services/setec-manager/internal/handlers/handlers.go
Normal file
103
services/setec-manager/internal/handlers/handlers.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"setec-manager/internal/config"
|
||||
"setec-manager/internal/db"
|
||||
"setec-manager/internal/hosting"
|
||||
"setec-manager/web"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
Config *config.Config
|
||||
DB *db.DB
|
||||
HostingConfigs *hosting.ProviderConfigStore
|
||||
tmpl *template.Template
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, database *db.DB, hostingConfigs *hosting.ProviderConfigStore) *Handler {
|
||||
return &Handler{
|
||||
Config: cfg,
|
||||
DB: database,
|
||||
HostingConfigs: hostingConfigs,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) getTemplates() *template.Template {
|
||||
h.once.Do(func() {
|
||||
funcMap := template.FuncMap{
|
||||
"eq": func(a, b interface{}) bool { return a == b },
|
||||
"ne": func(a, b interface{}) bool { return a != b },
|
||||
"default": func(val, def interface{}) interface{} {
|
||||
if val == nil || val == "" || val == 0 || val == false {
|
||||
return def
|
||||
}
|
||||
return val
|
||||
},
|
||||
}
|
||||
|
||||
var err error
|
||||
h.tmpl, err = template.New("").Funcs(funcMap).ParseFS(web.TemplateFS, "templates/*.html")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse templates: %v", err)
|
||||
}
|
||||
|
||||
// Also parse from the static FS to make sure it's available
|
||||
_ = fs.WalkDir(web.StaticFS, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
return nil
|
||||
})
|
||||
})
|
||||
return h.tmpl
|
||||
}
|
||||
|
||||
type pageData struct {
|
||||
Title string
|
||||
Data interface{}
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
func (h *Handler) render(w http.ResponseWriter, name string, data interface{}) {
|
||||
pd := pageData{
|
||||
Data: data,
|
||||
Config: h.Config,
|
||||
}
|
||||
|
||||
t := h.getTemplates().Lookup(name)
|
||||
if t == nil {
|
||||
http.Error(w, "Template not found: "+name, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := t.Execute(w, pd); err != nil {
|
||||
log.Printf("[template] %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, msg string) {
|
||||
writeJSON(w, status, map[string]string{"error": msg})
|
||||
}
|
||||
|
||||
func paramInt(r *http.Request, name string) (int64, error) {
|
||||
return strconv.ParseInt(chi.URLParam(r, name), 10, 64)
|
||||
}
|
||||
|
||||
func paramStr(r *http.Request, name string) string {
|
||||
return chi.URLParam(r, name)
|
||||
}
|
||||
697
services/setec-manager/internal/handlers/hosting.go
Normal file
697
services/setec-manager/internal/handlers/hosting.go
Normal file
@@ -0,0 +1,697 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"setec-manager/internal/hosting"
|
||||
)
|
||||
|
||||
// providerInfo is the view model sent to the hosting template and JSON responses.
|
||||
type providerInfo struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Connected bool `json:"connected"`
|
||||
HasConfig bool `json:"has_config"`
|
||||
}
|
||||
|
||||
// listProviderInfo builds a summary of every registered provider and its config status.
|
||||
func (h *Handler) listProviderInfo() []providerInfo {
|
||||
names := hosting.List()
|
||||
out := make([]providerInfo, 0, len(names))
|
||||
for _, name := range names {
|
||||
p, ok := hosting.Get(name)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
pi := providerInfo{
|
||||
Name: p.Name(),
|
||||
DisplayName: p.DisplayName(),
|
||||
}
|
||||
if h.HostingConfigs != nil {
|
||||
cfg, err := h.HostingConfigs.Load(name)
|
||||
if err == nil && cfg != nil {
|
||||
pi.HasConfig = true
|
||||
if cfg.APIKey != "" {
|
||||
pi.Connected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
out = append(out, pi)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// getProvider retrieves the provider from the URL and returns it. On error it
|
||||
// writes an HTTP error and returns nil.
|
||||
func (h *Handler) getProvider(w http.ResponseWriter, r *http.Request) hosting.Provider {
|
||||
name := paramStr(r, "provider")
|
||||
if name == "" {
|
||||
writeError(w, http.StatusBadRequest, "missing provider name")
|
||||
return nil
|
||||
}
|
||||
p, ok := hosting.Get(name)
|
||||
if !ok {
|
||||
writeError(w, http.StatusNotFound, "hosting provider "+name+" not registered")
|
||||
return nil
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// configureProvider loads saved credentials for a provider and calls Configure
|
||||
// on it so it is ready for API calls. Returns false if no config is saved.
|
||||
func (h *Handler) configureProvider(p hosting.Provider) bool {
|
||||
if h.HostingConfigs == nil {
|
||||
return false
|
||||
}
|
||||
cfg, err := h.HostingConfigs.Load(p.Name())
|
||||
if err != nil || cfg == nil || cfg.APIKey == "" {
|
||||
return false
|
||||
}
|
||||
if err := p.Configure(*cfg); err != nil {
|
||||
log.Printf("[hosting] configure %s: %v", p.Name(), err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ─── Page Handlers ───────────────────────────────────────────────────────────
|
||||
|
||||
// HostingProviders renders the hosting management page (GET /hosting).
|
||||
func (h *Handler) HostingProviders(w http.ResponseWriter, r *http.Request) {
|
||||
providers := h.listProviderInfo()
|
||||
if acceptsJSON(r) {
|
||||
writeJSON(w, http.StatusOK, providers)
|
||||
return
|
||||
}
|
||||
h.render(w, "hosting.html", map[string]interface{}{
|
||||
"Providers": providers,
|
||||
})
|
||||
}
|
||||
|
||||
// HostingProviderConfig returns the config page/detail for a single provider.
|
||||
func (h *Handler) HostingProviderConfig(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var savedCfg *hosting.ProviderConfig
|
||||
if h.HostingConfigs != nil {
|
||||
savedCfg, _ = h.HostingConfigs.Load(p.Name())
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Provider": providerInfo{Name: p.Name(), DisplayName: p.DisplayName()},
|
||||
"Config": savedCfg,
|
||||
"Providers": h.listProviderInfo(),
|
||||
}
|
||||
|
||||
if acceptsJSON(r) {
|
||||
writeJSON(w, http.StatusOK, data)
|
||||
return
|
||||
}
|
||||
h.render(w, "hosting.html", data)
|
||||
}
|
||||
|
||||
// ─── Configuration ───────────────────────────────────────────────────────────
|
||||
|
||||
// HostingProviderSave saves API credentials and tests the connection.
|
||||
func (h *Handler) HostingProviderSave(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
APIKey string `json:"api_key"`
|
||||
APISecret string `json:"api_secret"`
|
||||
Extra map[string]string `json:"extra"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
|
||||
if body.APIKey == "" {
|
||||
writeError(w, http.StatusBadRequest, "api_key is required")
|
||||
return
|
||||
}
|
||||
|
||||
cfg := hosting.ProviderConfig{
|
||||
APIKey: body.APIKey,
|
||||
APISecret: body.APISecret,
|
||||
Extra: body.Extra,
|
||||
}
|
||||
|
||||
// Configure the provider to validate credentials.
|
||||
if err := p.Configure(cfg); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "configure: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Test the connection.
|
||||
connected := true
|
||||
if err := p.TestConnection(); err != nil {
|
||||
log.Printf("[hosting] test %s failed: %v", p.Name(), err)
|
||||
connected = false
|
||||
}
|
||||
|
||||
// Persist.
|
||||
if h.HostingConfigs != nil {
|
||||
if err := h.HostingConfigs.Save(p.Name(), cfg); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "save config: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"status": "saved",
|
||||
"connected": connected,
|
||||
})
|
||||
}
|
||||
|
||||
// HostingProviderTest tests the connection to a provider without saving.
|
||||
func (h *Handler) HostingProviderTest(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured — save credentials first")
|
||||
return
|
||||
}
|
||||
|
||||
if err := p.TestConnection(); err != nil {
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"connected": false,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"connected": true,
|
||||
})
|
||||
}
|
||||
|
||||
// ─── DNS ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
// HostingDNSList returns DNS records for a domain.
|
||||
func (h *Handler) HostingDNSList(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured")
|
||||
return
|
||||
}
|
||||
|
||||
domain := paramStr(r, "domain")
|
||||
records, err := p.ListDNSRecords(domain)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, records)
|
||||
}
|
||||
|
||||
// HostingDNSUpdate replaces DNS records for a domain.
|
||||
func (h *Handler) HostingDNSUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured")
|
||||
return
|
||||
}
|
||||
|
||||
domain := paramStr(r, "domain")
|
||||
|
||||
var body struct {
|
||||
Records []hosting.DNSRecord `json:"records"`
|
||||
Overwrite bool `json:"overwrite"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
|
||||
if err := p.UpdateDNSRecords(domain, body.Records, body.Overwrite); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
|
||||
}
|
||||
|
||||
// HostingDNSDelete deletes DNS records matching name+type for a domain.
|
||||
func (h *Handler) HostingDNSDelete(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured")
|
||||
return
|
||||
}
|
||||
|
||||
domain := paramStr(r, "domain")
|
||||
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
|
||||
filter := hosting.DNSRecordFilter{Name: body.Name, Type: body.Type}
|
||||
if err := p.DeleteDNSRecord(domain, filter); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||
}
|
||||
|
||||
// HostingDNSReset resets DNS records for a domain to provider defaults.
|
||||
func (h *Handler) HostingDNSReset(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured")
|
||||
return
|
||||
}
|
||||
|
||||
domain := paramStr(r, "domain")
|
||||
|
||||
if err := p.ResetDNSRecords(domain); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "reset"})
|
||||
}
|
||||
|
||||
// ─── Domains ─────────────────────────────────────────────────────────────────
|
||||
|
||||
// HostingDomainsList returns all domains registered with the provider.
|
||||
func (h *Handler) HostingDomainsList(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured")
|
||||
return
|
||||
}
|
||||
|
||||
domains, err := p.ListDomains()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, domains)
|
||||
}
|
||||
|
||||
// HostingDomainsCheck checks availability of a domain across TLDs.
|
||||
func (h *Handler) HostingDomainsCheck(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured")
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Domain string `json:"domain"`
|
||||
TLDs []string `json:"tlds"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
if body.Domain == "" {
|
||||
writeError(w, http.StatusBadRequest, "domain is required")
|
||||
return
|
||||
}
|
||||
|
||||
results, err := p.CheckDomainAvailability(body.Domain, body.TLDs)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, results)
|
||||
}
|
||||
|
||||
// HostingDomainsPurchase purchases a domain.
|
||||
func (h *Handler) HostingDomainsPurchase(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured")
|
||||
return
|
||||
}
|
||||
|
||||
var req hosting.DomainPurchaseRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
if req.Domain == "" {
|
||||
writeError(w, http.StatusBadRequest, "domain is required")
|
||||
return
|
||||
}
|
||||
if req.Years <= 0 {
|
||||
req.Years = 1
|
||||
}
|
||||
|
||||
result, err := p.PurchaseDomain(req)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, result)
|
||||
}
|
||||
|
||||
// HostingDomainNameservers updates nameservers for a domain.
|
||||
func (h *Handler) HostingDomainNameservers(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured")
|
||||
return
|
||||
}
|
||||
|
||||
domain := paramStr(r, "domain")
|
||||
|
||||
var body struct {
|
||||
Nameservers []string `json:"nameservers"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
if len(body.Nameservers) == 0 {
|
||||
writeError(w, http.StatusBadRequest, "nameservers list is empty")
|
||||
return
|
||||
}
|
||||
|
||||
if err := p.SetNameservers(domain, body.Nameservers); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
|
||||
}
|
||||
|
||||
// HostingDomainLock toggles the registrar lock on a domain.
|
||||
func (h *Handler) HostingDomainLock(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured")
|
||||
return
|
||||
}
|
||||
|
||||
domain := paramStr(r, "domain")
|
||||
|
||||
var body struct {
|
||||
Locked bool `json:"locked"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
if body.Locked {
|
||||
err = p.EnableDomainLock(domain)
|
||||
} else {
|
||||
err = p.DisableDomainLock(domain)
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"status": "updated", "locked": body.Locked})
|
||||
}
|
||||
|
||||
// HostingDomainPrivacy toggles privacy protection on a domain.
|
||||
func (h *Handler) HostingDomainPrivacy(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured")
|
||||
return
|
||||
}
|
||||
|
||||
domain := paramStr(r, "domain")
|
||||
|
||||
var body struct {
|
||||
Privacy bool `json:"privacy"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
if body.Privacy {
|
||||
err = p.EnablePrivacyProtection(domain)
|
||||
} else {
|
||||
err = p.DisablePrivacyProtection(domain)
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"status": "updated", "privacy": body.Privacy})
|
||||
}
|
||||
|
||||
// ─── VMs / VPS ───────────────────────────────────────────────────────────────
|
||||
|
||||
// HostingVMsList lists all VMs for a provider.
|
||||
func (h *Handler) HostingVMsList(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured")
|
||||
return
|
||||
}
|
||||
|
||||
vms, err := p.ListVMs()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, vms)
|
||||
}
|
||||
|
||||
// HostingVMGet returns details for a single VM.
|
||||
func (h *Handler) HostingVMGet(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured")
|
||||
return
|
||||
}
|
||||
|
||||
id := paramStr(r, "id")
|
||||
vm, err := p.GetVM(id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if vm == nil {
|
||||
writeError(w, http.StatusNotFound, "VM not found")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, vm)
|
||||
}
|
||||
|
||||
// HostingVMCreate creates a new VM.
|
||||
func (h *Handler) HostingVMCreate(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured")
|
||||
return
|
||||
}
|
||||
|
||||
var req hosting.VMCreateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
if req.Plan == "" {
|
||||
writeError(w, http.StatusBadRequest, "plan is required")
|
||||
return
|
||||
}
|
||||
if req.DataCenterID == "" {
|
||||
writeError(w, http.StatusBadRequest, "data_center_id is required")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := p.CreateVM(req)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, result)
|
||||
}
|
||||
|
||||
// HostingDataCenters lists available data centers.
|
||||
func (h *Handler) HostingDataCenters(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured")
|
||||
return
|
||||
}
|
||||
|
||||
dcs, err := p.ListDataCenters()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dcs)
|
||||
}
|
||||
|
||||
// ─── SSH Keys ────────────────────────────────────────────────────────────────
|
||||
|
||||
// HostingSSHKeys lists SSH keys for the provider account.
|
||||
func (h *Handler) HostingSSHKeys(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured")
|
||||
return
|
||||
}
|
||||
|
||||
keys, err := p.ListSSHKeys()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, keys)
|
||||
}
|
||||
|
||||
// HostingSSHKeyAdd adds an SSH key.
|
||||
func (h *Handler) HostingSSHKeyAdd(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured")
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
PublicKey string `json:"public_key"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
if body.Name == "" || body.PublicKey == "" {
|
||||
writeError(w, http.StatusBadRequest, "name and public_key are required")
|
||||
return
|
||||
}
|
||||
|
||||
key, err := p.AddSSHKey(body.Name, body.PublicKey)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, key)
|
||||
}
|
||||
|
||||
// HostingSSHKeyDelete deletes an SSH key.
|
||||
func (h *Handler) HostingSSHKeyDelete(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured")
|
||||
return
|
||||
}
|
||||
|
||||
id := paramStr(r, "id")
|
||||
if err := p.DeleteSSHKey(id); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||
}
|
||||
|
||||
// ─── Billing ─────────────────────────────────────────────────────────────────
|
||||
|
||||
// HostingSubscriptions lists billing subscriptions.
|
||||
func (h *Handler) HostingSubscriptions(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured")
|
||||
return
|
||||
}
|
||||
|
||||
subs, err := p.ListSubscriptions()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, subs)
|
||||
}
|
||||
|
||||
// HostingCatalog returns the product catalog.
|
||||
func (h *Handler) HostingCatalog(w http.ResponseWriter, r *http.Request) {
|
||||
p := h.getProvider(w, r)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !h.configureProvider(p) {
|
||||
writeError(w, http.StatusBadRequest, "provider not configured")
|
||||
return
|
||||
}
|
||||
|
||||
category := r.URL.Query().Get("category")
|
||||
items, err := p.GetCatalog(category)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, items)
|
||||
}
|
||||
133
services/setec-manager/internal/handlers/logs.go
Normal file
133
services/setec-manager/internal/handlers/logs.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"setec-manager/internal/deploy"
|
||||
)
|
||||
|
||||
func (h *Handler) LogsPage(w http.ResponseWriter, r *http.Request) {
|
||||
h.render(w, "logs.html", nil)
|
||||
}
|
||||
|
||||
func (h *Handler) LogsSystem(w http.ResponseWriter, r *http.Request) {
|
||||
linesStr := r.URL.Query().Get("lines")
|
||||
if linesStr == "" {
|
||||
linesStr = "100"
|
||||
}
|
||||
lines, err := strconv.Atoi(linesStr)
|
||||
if err != nil {
|
||||
lines = 100
|
||||
}
|
||||
|
||||
// deploy.Logs requires a unit name; for system-wide logs we pass an empty
|
||||
// unit and use journalctl directly. However, deploy.Logs always passes -u,
|
||||
// so we use it with a broad scope by requesting the system journal for a
|
||||
// pseudo-unit. Instead, keep using journalctl directly for system-wide logs
|
||||
// since deploy.Logs is unit-scoped.
|
||||
out, err := exec.Command("journalctl", "-n", strconv.Itoa(lines), "--no-pager", "-o", "short-iso").Output()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"logs": string(out)})
|
||||
}
|
||||
|
||||
func (h *Handler) LogsNginx(w http.ResponseWriter, r *http.Request) {
|
||||
logType := r.URL.Query().Get("type")
|
||||
if logType == "" {
|
||||
logType = "access"
|
||||
}
|
||||
|
||||
var logPath string
|
||||
switch logType {
|
||||
case "access":
|
||||
logPath = "/var/log/nginx/access.log"
|
||||
case "error":
|
||||
logPath = "/var/log/nginx/error.log"
|
||||
default:
|
||||
writeError(w, http.StatusBadRequest, "invalid log type")
|
||||
return
|
||||
}
|
||||
|
||||
out, err := exec.Command("tail", "-n", "200", logPath).Output()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"logs": string(out), "type": logType})
|
||||
}
|
||||
|
||||
func (h *Handler) LogsUnit(w http.ResponseWriter, r *http.Request) {
|
||||
unit := r.URL.Query().Get("unit")
|
||||
if unit == "" {
|
||||
writeError(w, http.StatusBadRequest, "unit parameter required")
|
||||
return
|
||||
}
|
||||
linesStr := r.URL.Query().Get("lines")
|
||||
if linesStr == "" {
|
||||
linesStr = "100"
|
||||
}
|
||||
lines, err := strconv.Atoi(linesStr)
|
||||
if err != nil {
|
||||
lines = 100
|
||||
}
|
||||
|
||||
out, err := deploy.Logs(unit, lines)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"logs": out, "unit": unit})
|
||||
}
|
||||
|
||||
func (h *Handler) LogsStream(w http.ResponseWriter, r *http.Request) {
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
writeError(w, http.StatusInternalServerError, "streaming not supported")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
unit := r.URL.Query().Get("unit")
|
||||
if unit == "" {
|
||||
unit = "autarch-web"
|
||||
}
|
||||
|
||||
// SSE live streaming requires journalctl -f which the deploy package does
|
||||
// not support (it only returns a snapshot). Keep inline exec.Command here.
|
||||
cmd := exec.Command("journalctl", "-u", unit, "-f", "-n", "0", "--no-pager", "-o", "short-iso")
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cmd.Start()
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
default:
|
||||
n, err := stdout.Read(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if n > 0 {
|
||||
lines := strings.Split(strings.TrimSpace(string(buf[:n])), "\n")
|
||||
for _, line := range lines {
|
||||
fmt.Fprintf(w, "data: %s\n\n", line)
|
||||
}
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
126
services/setec-manager/internal/handlers/monitor.go
Normal file
126
services/setec-manager/internal/handlers/monitor.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"setec-manager/internal/deploy"
|
||||
"setec-manager/internal/system"
|
||||
)
|
||||
|
||||
func (h *Handler) MonitorPage(w http.ResponseWriter, r *http.Request) {
|
||||
h.render(w, "monitor.html", nil)
|
||||
}
|
||||
|
||||
func (h *Handler) MonitorCPU(w http.ResponseWriter, r *http.Request) {
|
||||
cpu, err := system.GetCPUUsage()
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Build a summary line matching the previous top-style format.
|
||||
sysPct := 0.0
|
||||
userPct := cpu.Overall
|
||||
if len(cpu.Cores) > 0 {
|
||||
// Use aggregate core data for a more accurate breakdown
|
||||
var totalUser, totalSys float64
|
||||
for _, c := range cpu.Cores {
|
||||
totalUser += c.User
|
||||
totalSys += c.System
|
||||
}
|
||||
userPct = totalUser / float64(len(cpu.Cores))
|
||||
sysPct = totalSys / float64(len(cpu.Cores))
|
||||
}
|
||||
cpuLine := fmt.Sprintf("%%Cpu(s): %.1f us, %.1f sy, %.1f id",
|
||||
userPct, sysPct, cpu.Idle)
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"cpu": cpuLine,
|
||||
"overall": cpu.Overall,
|
||||
"idle": cpu.Idle,
|
||||
"cores": cpu.Cores,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) MonitorMemory(w http.ResponseWriter, r *http.Request) {
|
||||
mem, err := system.GetMemory()
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"total": mem.Total,
|
||||
"used": mem.Used,
|
||||
"free": mem.Free,
|
||||
"available": mem.Available,
|
||||
"swap_total": mem.SwapTotal,
|
||||
"swap_used": mem.SwapUsed,
|
||||
"swap_free": mem.SwapFree,
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *Handler) MonitorDisk(w http.ResponseWriter, r *http.Request) {
|
||||
disks, err := system.GetDisk()
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusOK, []interface{}{})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, disks)
|
||||
}
|
||||
|
||||
func (h *Handler) MonitorServices(w http.ResponseWriter, r *http.Request) {
|
||||
services := []string{"nginx", "autarch-web", "autarch-dns", "setec-manager", "ufw"}
|
||||
|
||||
type svcStatus struct {
|
||||
Name string `json:"name"`
|
||||
Active string `json:"active"`
|
||||
Running bool `json:"running"`
|
||||
Memory string `json:"memory"`
|
||||
}
|
||||
|
||||
var statuses []svcStatus
|
||||
for _, svc := range services {
|
||||
ss := svcStatus{Name: svc}
|
||||
active, err := deploy.IsActive(svc)
|
||||
if err == nil && active {
|
||||
ss.Active = "active"
|
||||
ss.Running = true
|
||||
} else {
|
||||
ss.Active = "inactive"
|
||||
ss.Running = false
|
||||
}
|
||||
|
||||
// Get memory usage — no wrapper exists for this property, so use exec
|
||||
if ss.Running {
|
||||
out, err := exec.Command("systemctl", "show", svc, "--property=MemoryCurrent").Output()
|
||||
if err == nil {
|
||||
parts := strings.SplitN(string(out), "=", 2)
|
||||
if len(parts) == 2 {
|
||||
val := strings.TrimSpace(parts[1])
|
||||
if val != "[not set]" && val != "" {
|
||||
bytes := parseUint64(val)
|
||||
ss.Memory = formatBytes(float64(bytes))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statuses = append(statuses, ss)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, statuses)
|
||||
}
|
||||
|
||||
// parseUint64 is a helper that returns 0 on failure.
|
||||
func parseUint64(s string) uint64 {
|
||||
var n uint64
|
||||
fmt.Sscanf(s, "%d", &n)
|
||||
return n
|
||||
}
|
||||
97
services/setec-manager/internal/handlers/nginx.go
Normal file
97
services/setec-manager/internal/handlers/nginx.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"setec-manager/internal/nginx"
|
||||
"setec-manager/internal/system"
|
||||
)
|
||||
|
||||
type nginxStatus struct {
|
||||
Running bool `json:"running"`
|
||||
Status string `json:"status"`
|
||||
ConfigTest string `json:"config_test"`
|
||||
ConfigOK bool `json:"config_ok"`
|
||||
}
|
||||
|
||||
func (h *Handler) NginxStatus(w http.ResponseWriter, r *http.Request) {
|
||||
status := nginxStatus{}
|
||||
status.Status, status.Running = nginx.Status()
|
||||
|
||||
testOut, testErr := nginx.Test()
|
||||
status.ConfigTest = testOut
|
||||
status.ConfigOK = testErr == nil
|
||||
|
||||
if acceptsJSON(r) {
|
||||
writeJSON(w, http.StatusOK, status)
|
||||
return
|
||||
}
|
||||
h.render(w, "nginx.html", status)
|
||||
}
|
||||
|
||||
func (h *Handler) NginxReload(w http.ResponseWriter, r *http.Request) {
|
||||
// Validate config first
|
||||
if _, err := nginx.Test(); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "nginx config test failed — not reloading")
|
||||
return
|
||||
}
|
||||
if err := nginx.Reload(); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "reloaded"})
|
||||
}
|
||||
|
||||
func (h *Handler) NginxRestart(w http.ResponseWriter, r *http.Request) {
|
||||
if err := nginx.Restart(); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "restarted"})
|
||||
}
|
||||
|
||||
func (h *Handler) NginxConfigView(w http.ResponseWriter, r *http.Request) {
|
||||
domain := paramStr(r, "domain")
|
||||
path := filepath.Join(h.Config.Nginx.SitesAvailable, domain)
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "config not found for "+domain)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"domain": domain, "config": string(data)})
|
||||
}
|
||||
|
||||
func (h *Handler) NginxTest(w http.ResponseWriter, r *http.Request) {
|
||||
out, err := nginx.Test()
|
||||
ok := err == nil
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"output": strings.TrimSpace(out),
|
||||
"valid": ok,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) NginxInstallBase(w http.ResponseWriter, r *http.Request) {
|
||||
// Install nginx if not present
|
||||
if _, err := exec.LookPath("nginx"); err != nil {
|
||||
if _, installErr := system.PackageInstall("nginx"); installErr != nil {
|
||||
writeError(w, http.StatusInternalServerError, installErr.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Install snippets
|
||||
if err := nginx.InstallSnippets(h.Config); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure certbot webroot exists
|
||||
os.MkdirAll(h.Config.Nginx.CertbotWebroot, 0755)
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "nginx configured"})
|
||||
}
|
||||
453
services/setec-manager/internal/handlers/sites.go
Normal file
453
services/setec-manager/internal/handlers/sites.go
Normal file
@@ -0,0 +1,453 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"setec-manager/internal/db"
|
||||
"setec-manager/internal/deploy"
|
||||
"setec-manager/internal/nginx"
|
||||
)
|
||||
|
||||
var validDomainRegex = regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`)
|
||||
|
||||
func isValidDomain(domain string) bool {
|
||||
if len(domain) > 253 {
|
||||
return false
|
||||
}
|
||||
if net.ParseIP(domain) != nil {
|
||||
return true
|
||||
}
|
||||
return validDomainRegex.MatchString(domain)
|
||||
}
|
||||
|
||||
func (h *Handler) SiteList(w http.ResponseWriter, r *http.Request) {
|
||||
sites, err := h.DB.ListSites()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Enrich with running status
|
||||
type siteView struct {
|
||||
db.Site
|
||||
Running bool `json:"running"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
var views []siteView
|
||||
for _, s := range sites {
|
||||
sv := siteView{Site: s}
|
||||
if s.AppType != "static" && s.AppPort > 0 {
|
||||
unitName := fmt.Sprintf("app-%s", s.Domain)
|
||||
active, _ := deploy.IsActive(unitName)
|
||||
sv.Running = active
|
||||
if active {
|
||||
sv.Status = "active"
|
||||
} else {
|
||||
sv.Status = "inactive"
|
||||
}
|
||||
} else {
|
||||
sv.Status = "static"
|
||||
sv.Running = s.Enabled
|
||||
}
|
||||
views = append(views, sv)
|
||||
}
|
||||
|
||||
if acceptsJSON(r) {
|
||||
writeJSON(w, http.StatusOK, views)
|
||||
return
|
||||
}
|
||||
h.render(w, "sites.html", views)
|
||||
}
|
||||
|
||||
func (h *Handler) SiteNewForm(w http.ResponseWriter, r *http.Request) {
|
||||
h.render(w, "site_new.html", nil)
|
||||
}
|
||||
|
||||
func (h *Handler) SiteCreate(w http.ResponseWriter, r *http.Request) {
|
||||
var site db.Site
|
||||
if err := json.NewDecoder(r.Body).Decode(&site); err != nil {
|
||||
// Try form values
|
||||
site.Domain = r.FormValue("domain")
|
||||
site.Aliases = r.FormValue("aliases")
|
||||
site.AppType = r.FormValue("app_type")
|
||||
site.AppRoot = r.FormValue("app_root")
|
||||
site.GitRepo = r.FormValue("git_repo")
|
||||
site.GitBranch = r.FormValue("git_branch")
|
||||
site.AppEntry = r.FormValue("app_entry")
|
||||
}
|
||||
|
||||
if site.Domain == "" {
|
||||
writeError(w, http.StatusBadRequest, "domain is required")
|
||||
return
|
||||
}
|
||||
if !isValidDomain(site.Domain) {
|
||||
writeError(w, http.StatusBadRequest, "invalid domain name")
|
||||
return
|
||||
}
|
||||
if site.AppType == "" {
|
||||
site.AppType = "static"
|
||||
}
|
||||
if site.AppRoot == "" {
|
||||
site.AppRoot = filepath.Join(h.Config.Nginx.Webroot, site.Domain)
|
||||
}
|
||||
if site.GitBranch == "" {
|
||||
site.GitBranch = "main"
|
||||
}
|
||||
site.Enabled = true
|
||||
|
||||
// Check for duplicate
|
||||
existing, _ := h.DB.GetSiteByDomain(site.Domain)
|
||||
if existing != nil {
|
||||
writeError(w, http.StatusConflict, "domain already exists")
|
||||
return
|
||||
}
|
||||
|
||||
// Create directory
|
||||
os.MkdirAll(site.AppRoot, 0755)
|
||||
|
||||
// Clone repo if provided
|
||||
if site.GitRepo != "" {
|
||||
depID, _ := h.DB.CreateDeployment(nil, "clone")
|
||||
out, err := deploy.Clone(site.GitRepo, site.GitBranch, site.AppRoot)
|
||||
if err != nil {
|
||||
h.DB.FinishDeployment(depID, "failed", out)
|
||||
writeError(w, http.StatusInternalServerError, "git clone failed: "+out)
|
||||
return
|
||||
}
|
||||
h.DB.FinishDeployment(depID, "success", out)
|
||||
}
|
||||
|
||||
// Save to DB
|
||||
id, err := h.DB.CreateSite(&site)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
site.ID = id
|
||||
|
||||
// Generate nginx config
|
||||
if err := nginx.GenerateConfig(h.Config, &site); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "nginx config: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Enable site
|
||||
nginx.EnableSite(h.Config, site.Domain)
|
||||
nginx.Reload()
|
||||
|
||||
// Generate systemd unit for non-static apps
|
||||
if site.AppType != "static" && site.AppEntry != "" {
|
||||
h.generateAppUnit(&site)
|
||||
}
|
||||
|
||||
if acceptsJSON(r) {
|
||||
writeJSON(w, http.StatusCreated, site)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, fmt.Sprintf("/sites/%d", id), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (h *Handler) SiteDetail(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramInt(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
site, err := h.DB.GetSite(id)
|
||||
if err != nil || site == nil {
|
||||
writeError(w, http.StatusNotFound, "site not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Get deployment history
|
||||
deps, _ := h.DB.ListDeployments(&id, 10)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Site": site,
|
||||
"Deployments": deps,
|
||||
}
|
||||
|
||||
if acceptsJSON(r) {
|
||||
writeJSON(w, http.StatusOK, data)
|
||||
return
|
||||
}
|
||||
h.render(w, "site_detail.html", data)
|
||||
}
|
||||
|
||||
func (h *Handler) SiteUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramInt(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
site, err := h.DB.GetSite(id)
|
||||
if err != nil || site == nil {
|
||||
writeError(w, http.StatusNotFound, "site not found")
|
||||
return
|
||||
}
|
||||
|
||||
var update db.Site
|
||||
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid body")
|
||||
return
|
||||
}
|
||||
|
||||
// Apply updates
|
||||
if update.Domain != "" {
|
||||
site.Domain = update.Domain
|
||||
}
|
||||
site.Aliases = update.Aliases
|
||||
if update.AppType != "" {
|
||||
site.AppType = update.AppType
|
||||
}
|
||||
if update.AppPort > 0 {
|
||||
site.AppPort = update.AppPort
|
||||
}
|
||||
site.AppEntry = update.AppEntry
|
||||
site.GitRepo = update.GitRepo
|
||||
site.GitBranch = update.GitBranch
|
||||
site.SSLEnabled = update.SSLEnabled
|
||||
site.Enabled = update.Enabled
|
||||
|
||||
if err := h.DB.UpdateSite(site); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Regenerate nginx config
|
||||
nginx.GenerateConfig(h.Config, site)
|
||||
nginx.Reload()
|
||||
|
||||
writeJSON(w, http.StatusOK, site)
|
||||
}
|
||||
|
||||
func (h *Handler) SiteDelete(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramInt(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
site, err := h.DB.GetSite(id)
|
||||
if err != nil || site == nil {
|
||||
writeError(w, http.StatusNotFound, "site not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Disable nginx
|
||||
nginx.DisableSite(h.Config, site.Domain)
|
||||
nginx.Reload()
|
||||
|
||||
// Stop, disable, and remove the systemd unit
|
||||
unitName := fmt.Sprintf("app-%s", site.Domain)
|
||||
deploy.RemoveUnit(unitName)
|
||||
|
||||
if err := h.DB.DeleteSite(id); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||
}
|
||||
|
||||
func (h *Handler) SiteDeploy(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramInt(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
site, err := h.DB.GetSite(id)
|
||||
if err != nil || site == nil {
|
||||
writeError(w, http.StatusNotFound, "site not found")
|
||||
return
|
||||
}
|
||||
|
||||
depID, _ := h.DB.CreateDeployment(&id, "deploy")
|
||||
|
||||
var output strings.Builder
|
||||
|
||||
// Git pull
|
||||
if site.GitRepo != "" {
|
||||
out, err := deploy.Pull(site.AppRoot)
|
||||
output.WriteString(out)
|
||||
if err != nil {
|
||||
h.DB.FinishDeployment(depID, "failed", output.String())
|
||||
writeError(w, http.StatusInternalServerError, "git pull failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Reinstall deps based on app type
|
||||
switch site.AppType {
|
||||
case "python", "autarch":
|
||||
venvDir := filepath.Join(site.AppRoot, "venv")
|
||||
reqFile := filepath.Join(site.AppRoot, "requirements.txt")
|
||||
if _, err := os.Stat(reqFile); err == nil {
|
||||
out, _ := deploy.InstallRequirements(venvDir, reqFile)
|
||||
output.WriteString(out)
|
||||
}
|
||||
case "node":
|
||||
out, _ := deploy.NpmInstall(site.AppRoot)
|
||||
output.WriteString(out)
|
||||
}
|
||||
|
||||
// Restart service
|
||||
unitName := fmt.Sprintf("app-%s", site.Domain)
|
||||
deploy.Restart(unitName)
|
||||
|
||||
h.DB.FinishDeployment(depID, "success", output.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deployed"})
|
||||
}
|
||||
|
||||
func (h *Handler) SiteRestart(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := paramInt(r, "id")
|
||||
site, _ := h.DB.GetSite(id)
|
||||
if site == nil {
|
||||
writeError(w, http.StatusNotFound, "site not found")
|
||||
return
|
||||
}
|
||||
unitName := fmt.Sprintf("app-%s", site.Domain)
|
||||
deploy.Restart(unitName)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "restarted"})
|
||||
}
|
||||
|
||||
func (h *Handler) SiteStop(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := paramInt(r, "id")
|
||||
site, _ := h.DB.GetSite(id)
|
||||
if site == nil {
|
||||
writeError(w, http.StatusNotFound, "site not found")
|
||||
return
|
||||
}
|
||||
unitName := fmt.Sprintf("app-%s", site.Domain)
|
||||
deploy.Stop(unitName)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "stopped"})
|
||||
}
|
||||
|
||||
func (h *Handler) SiteStart(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := paramInt(r, "id")
|
||||
site, _ := h.DB.GetSite(id)
|
||||
if site == nil {
|
||||
writeError(w, http.StatusNotFound, "site not found")
|
||||
return
|
||||
}
|
||||
unitName := fmt.Sprintf("app-%s", site.Domain)
|
||||
deploy.Start(unitName)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "started"})
|
||||
}
|
||||
|
||||
func (h *Handler) SiteLogs(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := paramInt(r, "id")
|
||||
site, _ := h.DB.GetSite(id)
|
||||
if site == nil {
|
||||
writeError(w, http.StatusNotFound, "site not found")
|
||||
return
|
||||
}
|
||||
|
||||
unitName := fmt.Sprintf("app-%s", site.Domain)
|
||||
out, _ := deploy.Logs(unitName, 100)
|
||||
|
||||
if acceptsJSON(r) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"logs": out})
|
||||
return
|
||||
}
|
||||
h.render(w, "site_detail.html", map[string]interface{}{
|
||||
"Site": site,
|
||||
"Logs": out,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) SiteLogStream(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := paramInt(r, "id")
|
||||
site, _ := h.DB.GetSite(id)
|
||||
if site == nil {
|
||||
writeError(w, http.StatusNotFound, "site not found")
|
||||
return
|
||||
}
|
||||
|
||||
unitName := fmt.Sprintf("app-%s", site.Domain)
|
||||
streamJournalctl(w, r, unitName)
|
||||
}
|
||||
|
||||
func (h *Handler) generateAppUnit(site *db.Site) {
|
||||
var execStart string
|
||||
|
||||
switch site.AppType {
|
||||
case "python":
|
||||
venvPython := filepath.Join(site.AppRoot, "venv", "bin", "python3")
|
||||
execStart = fmt.Sprintf("%s %s", venvPython, filepath.Join(site.AppRoot, site.AppEntry))
|
||||
case "node":
|
||||
execStart = fmt.Sprintf("/usr/bin/node %s", filepath.Join(site.AppRoot, site.AppEntry))
|
||||
case "autarch":
|
||||
venvPython := filepath.Join(site.AppRoot, "venv", "bin", "python3")
|
||||
execStart = fmt.Sprintf("%s %s", venvPython, filepath.Join(site.AppRoot, "autarch_web.py"))
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
unitName := fmt.Sprintf("app-%s", site.Domain)
|
||||
unitContent := deploy.GenerateUnit(deploy.UnitConfig{
|
||||
Name: unitName,
|
||||
Description: fmt.Sprintf("%s (%s)", site.Domain, site.AppType),
|
||||
ExecStart: execStart,
|
||||
WorkingDirectory: site.AppRoot,
|
||||
User: "root",
|
||||
Environment: map[string]string{"PYTHONUNBUFFERED": "1"},
|
||||
})
|
||||
|
||||
deploy.InstallUnit(unitName, unitContent)
|
||||
deploy.Enable(unitName)
|
||||
}
|
||||
|
||||
func acceptsJSON(r *http.Request) bool {
|
||||
accept := r.Header.Get("Accept")
|
||||
return strings.Contains(accept, "application/json")
|
||||
}
|
||||
|
||||
func streamJournalctl(w http.ResponseWriter, r *http.Request, unit string) {
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
writeError(w, http.StatusInternalServerError, "streaming not supported")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
cmd := exec.Command("journalctl", "-u", unit, "-f", "-n", "50", "--no-pager", "-o", "short-iso")
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cmd.Start()
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
default:
|
||||
n, err := stdout.Read(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if n > 0 {
|
||||
fmt.Fprintf(w, "data: %s\n\n", strings.TrimSpace(string(buf[:n])))
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
143
services/setec-manager/internal/handlers/ssl.go
Normal file
143
services/setec-manager/internal/handlers/ssl.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"setec-manager/internal/acme"
|
||||
)
|
||||
|
||||
type certInfo struct {
|
||||
Domain string `json:"domain"`
|
||||
Issuer string `json:"issuer"`
|
||||
NotBefore time.Time `json:"not_before"`
|
||||
NotAfter time.Time `json:"not_after"`
|
||||
DaysLeft int `json:"days_left"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
}
|
||||
|
||||
func (h *Handler) SSLOverview(w http.ResponseWriter, r *http.Request) {
|
||||
certs := h.listCerts()
|
||||
h.render(w, "ssl.html", certs)
|
||||
}
|
||||
|
||||
func (h *Handler) SSLStatus(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, h.listCerts())
|
||||
}
|
||||
|
||||
func (h *Handler) SSLIssue(w http.ResponseWriter, r *http.Request) {
|
||||
domain := paramStr(r, "domain")
|
||||
if domain == "" {
|
||||
writeError(w, http.StatusBadRequest, "domain required")
|
||||
return
|
||||
}
|
||||
|
||||
client := acme.NewClient(
|
||||
h.Config.ACME.Email,
|
||||
h.Config.ACME.Staging,
|
||||
h.Config.Nginx.CertbotWebroot,
|
||||
h.Config.ACME.AccountDir,
|
||||
)
|
||||
|
||||
info, err := client.Issue(domain)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, fmt.Sprintf("certbot failed: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Update site SSL paths
|
||||
site, _ := h.DB.GetSiteByDomain(domain)
|
||||
if site != nil {
|
||||
site.SSLEnabled = true
|
||||
site.SSLCertPath = info.CertPath
|
||||
site.SSLKeyPath = info.KeyPath
|
||||
h.DB.UpdateSite(site)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "issued", "cert": info.CertPath})
|
||||
}
|
||||
|
||||
func (h *Handler) SSLRenew(w http.ResponseWriter, r *http.Request) {
|
||||
domain := paramStr(r, "domain")
|
||||
|
||||
client := acme.NewClient(
|
||||
h.Config.ACME.Email,
|
||||
h.Config.ACME.Staging,
|
||||
h.Config.Nginx.CertbotWebroot,
|
||||
h.Config.ACME.AccountDir,
|
||||
)
|
||||
|
||||
if err := client.Renew(domain); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, fmt.Sprintf("renewal failed: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "renewed"})
|
||||
}
|
||||
|
||||
func (h *Handler) listCerts() []certInfo {
|
||||
var certs []certInfo
|
||||
|
||||
// First, gather certs from DB-tracked sites
|
||||
sites, _ := h.DB.ListSites()
|
||||
for _, site := range sites {
|
||||
if !site.SSLEnabled || site.SSLCertPath == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
ci := certInfo{
|
||||
Domain: site.Domain,
|
||||
AutoRenew: site.SSLAuto,
|
||||
}
|
||||
|
||||
client := acme.NewClient(
|
||||
h.Config.ACME.Email,
|
||||
h.Config.ACME.Staging,
|
||||
h.Config.Nginx.CertbotWebroot,
|
||||
h.Config.ACME.AccountDir,
|
||||
)
|
||||
|
||||
info, err := client.GetCertInfo(site.Domain)
|
||||
if err == nil {
|
||||
ci.Issuer = info.Issuer
|
||||
ci.NotBefore = info.ExpiresAt.Add(-90 * 24 * time.Hour) // approximate
|
||||
ci.NotAfter = info.ExpiresAt
|
||||
ci.DaysLeft = info.DaysLeft
|
||||
}
|
||||
|
||||
certs = append(certs, ci)
|
||||
}
|
||||
|
||||
// Also check Let's Encrypt certs directory via ACME client
|
||||
client := acme.NewClient(
|
||||
h.Config.ACME.Email,
|
||||
h.Config.ACME.Staging,
|
||||
h.Config.Nginx.CertbotWebroot,
|
||||
h.Config.ACME.AccountDir,
|
||||
)
|
||||
|
||||
leCerts, _ := client.ListCerts()
|
||||
for _, le := range leCerts {
|
||||
// Skip if already found via site
|
||||
found := false
|
||||
for _, c := range certs {
|
||||
if c.Domain == le.Domain {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
|
||||
certs = append(certs, certInfo{
|
||||
Domain: le.Domain,
|
||||
Issuer: le.Issuer,
|
||||
NotAfter: le.ExpiresAt,
|
||||
DaysLeft: le.DaysLeft,
|
||||
})
|
||||
}
|
||||
|
||||
return certs
|
||||
}
|
||||
176
services/setec-manager/internal/handlers/users.go
Normal file
176
services/setec-manager/internal/handlers/users.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"setec-manager/internal/system"
|
||||
)
|
||||
|
||||
// ── System Users ────────────────────────────────────────────────────
|
||||
|
||||
type sysUser struct {
|
||||
Username string `json:"username"`
|
||||
UID string `json:"uid"`
|
||||
HomeDir string `json:"home_dir"`
|
||||
Shell string `json:"shell"`
|
||||
}
|
||||
|
||||
func (h *Handler) UserList(w http.ResponseWriter, r *http.Request) {
|
||||
users := listSystemUsers()
|
||||
if acceptsJSON(r) {
|
||||
writeJSON(w, http.StatusOK, users)
|
||||
return
|
||||
}
|
||||
h.render(w, "users.html", users)
|
||||
}
|
||||
|
||||
func (h *Handler) UserCreate(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Shell string `json:"shell"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
body.Username = r.FormValue("username")
|
||||
body.Password = r.FormValue("password")
|
||||
body.Shell = r.FormValue("shell")
|
||||
}
|
||||
|
||||
if body.Username == "" || body.Password == "" {
|
||||
writeError(w, http.StatusBadRequest, "username and password required")
|
||||
return
|
||||
}
|
||||
if body.Shell == "" {
|
||||
body.Shell = "/bin/bash"
|
||||
}
|
||||
|
||||
if err := system.CreateUser(body.Username, body.Password, body.Shell); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, fmt.Sprintf("create user failed: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, map[string]string{"status": "created", "username": body.Username})
|
||||
}
|
||||
|
||||
func (h *Handler) UserDelete(w http.ResponseWriter, r *http.Request) {
|
||||
id := paramStr(r, "id") // actually username for system users
|
||||
if id == "" {
|
||||
writeError(w, http.StatusBadRequest, "username required")
|
||||
return
|
||||
}
|
||||
|
||||
// Safety check
|
||||
if id == "root" || id == "autarch" {
|
||||
writeError(w, http.StatusForbidden, "cannot delete system accounts")
|
||||
return
|
||||
}
|
||||
|
||||
if err := system.DeleteUser(id); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||
}
|
||||
|
||||
func listSystemUsers() []sysUser {
|
||||
systemUsers, err := system.ListUsers()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var users []sysUser
|
||||
for _, su := range systemUsers {
|
||||
users = append(users, sysUser{
|
||||
Username: su.Username,
|
||||
UID: fmt.Sprintf("%d", su.UID),
|
||||
HomeDir: su.HomeDir,
|
||||
Shell: su.Shell,
|
||||
})
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
// ── Panel Users ─────────────────────────────────────────────────────
|
||||
|
||||
func (h *Handler) PanelUserList(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := h.DB.ListManagerUsers()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if acceptsJSON(r) {
|
||||
writeJSON(w, http.StatusOK, users)
|
||||
return
|
||||
}
|
||||
h.render(w, "users.html", map[string]interface{}{"PanelUsers": users})
|
||||
}
|
||||
|
||||
func (h *Handler) PanelUserCreate(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
body.Username = r.FormValue("username")
|
||||
body.Password = r.FormValue("password")
|
||||
body.Role = r.FormValue("role")
|
||||
}
|
||||
|
||||
if body.Username == "" || body.Password == "" {
|
||||
writeError(w, http.StatusBadRequest, "username and password required")
|
||||
return
|
||||
}
|
||||
if body.Role == "" {
|
||||
body.Role = "admin"
|
||||
}
|
||||
|
||||
id, err := h.DB.CreateManagerUser(body.Username, body.Password, body.Role)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{"id": id, "username": body.Username})
|
||||
}
|
||||
|
||||
func (h *Handler) PanelUserUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramInt(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Password string `json:"password"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&body)
|
||||
|
||||
if body.Password != "" {
|
||||
h.DB.UpdateManagerUserPassword(id, body.Password)
|
||||
}
|
||||
if body.Role != "" {
|
||||
h.DB.UpdateManagerUserRole(id, body.Role)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
|
||||
}
|
||||
|
||||
func (h *Handler) PanelUserDelete(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramInt(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.DB.DeleteManagerUser(id); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||
}
|
||||
Reference in New Issue
Block a user