No One Can Stop Me Now

This commit is contained in:
DigiJ
2026-03-13 23:48:47 -07:00
parent 4d3570781e
commit 1a138a2bd0
428 changed files with 519668 additions and 259 deletions

View 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)
}

View 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)
}

View 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"
}

View 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
}

View 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"})
}

View 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)
}

View 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)
}

View 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()
}
}
}
}

View 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
}

View 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"})
}

View 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()
}
}
}
}

View 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
}

View 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"})
}