2026-03-12 20:51:38 -07:00

259 lines
7.7 KiB
Go

package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
"setec-manager/internal/config"
"setec-manager/internal/db"
"setec-manager/internal/deploy"
"setec-manager/internal/nginx"
"setec-manager/internal/scheduler"
"setec-manager/internal/server"
)
const banner = `
███████╗███████╗████████╗███████╗ ██████╗
██╔════╝██╔════╝╚══██╔══╝██╔════╝██╔════╝
███████╗█████╗ ██║ █████╗ ██║
╚════██║██╔══╝ ██║ ██╔══╝ ██║
███████║███████╗ ██║ ███████╗╚██████╗
╚══════╝╚══════╝ ╚═╝ ╚══════╝ ╚═════╝
A P P M A N A G E R v1.0
darkHal Security Group & Setec Security Labs
`
func main() {
configPath := flag.String("config", "/opt/setec-manager/config.yaml", "Path to config file")
setup := flag.Bool("setup", false, "Run first-time setup")
flag.Parse()
fmt.Print(banner)
// Load config
cfg, err := config.Load(*configPath)
if err != nil {
log.Fatalf("[setec] Failed to load config: %v", err)
}
// Open database
database, err := db.Open(cfg.Database.Path)
if err != nil {
log.Fatalf("[setec] Failed to open database: %v", err)
}
defer database.Close()
// First-time setup
if *setup {
runSetup(cfg, database, *configPath)
return
}
// Check if any admin users exist
count, _ := database.ManagerUserCount()
if count == 0 {
log.Println("[setec] No admin users found. Creating default admin account.")
log.Println("[setec] Username: admin")
log.Println("[setec] Password: autarch")
log.Println("[setec] ** CHANGE THIS IMMEDIATELY **")
database.CreateManagerUser("admin", "autarch", "admin")
}
// Load or create persistent JWT key
dataDir := filepath.Dir(cfg.Database.Path)
jwtKey, err := server.LoadOrCreateJWTKey(dataDir)
if err != nil {
log.Fatalf("[setec] Failed to load JWT key: %v", err)
}
// Create and start server
srv := server.New(cfg, database, jwtKey)
// Start scheduler
sched := scheduler.New(database)
sched.RegisterHandler(scheduler.JobSSLRenew, func(siteID *int64) error {
log.Println("[scheduler] Running SSL renewal")
_, err := exec.Command("certbot", "renew", "--non-interactive").CombinedOutput()
return err
})
sched.RegisterHandler(scheduler.JobCleanup, func(siteID *int64) error {
log.Println("[scheduler] Running cleanup")
return nil
})
sched.RegisterHandler(scheduler.JobBackup, func(siteID *int64) error {
if siteID == nil {
log.Println("[scheduler] Backup job requires a site ID, skipping")
return fmt.Errorf("backup job requires a site ID")
}
site, err := database.GetSite(*siteID)
if err != nil || site == nil {
return fmt.Errorf("backup: site %d not found", *siteID)
}
backupDir := cfg.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)
cmd := exec.Command("tar", "-czf", backupPath, "-C", filepath.Dir(site.AppRoot), filepath.Base(site.AppRoot))
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("backup tar failed: %s: %w", string(out), err)
}
info, _ := os.Stat(backupPath)
size := int64(0)
if info != nil {
size = info.Size()
}
database.CreateBackup(siteID, "site", backupPath, size)
log.Printf("[scheduler] Backup complete for site %s: %s (%d bytes)", site.Domain, backupPath, size)
return nil
})
sched.RegisterHandler(scheduler.JobGitPull, func(siteID *int64) error {
if siteID == nil {
return fmt.Errorf("git_pull job requires a site ID")
}
site, err := database.GetSite(*siteID)
if err != nil || site == nil {
return fmt.Errorf("git_pull: site %d not found", *siteID)
}
if site.GitRepo == "" {
return fmt.Errorf("git_pull: site %s has no git repo configured", site.Domain)
}
output, err := deploy.Pull(site.AppRoot)
if err != nil {
return fmt.Errorf("git_pull %s: %w", site.Domain, err)
}
log.Printf("[scheduler] Git pull for site %s: %s", site.Domain, strings.TrimSpace(output))
return nil
})
sched.RegisterHandler(scheduler.JobRestart, func(siteID *int64) error {
if siteID == nil {
return fmt.Errorf("restart job requires a site ID")
}
site, err := database.GetSite(*siteID)
if err != nil || site == nil {
return fmt.Errorf("restart: site %d not found", *siteID)
}
unitName := fmt.Sprintf("app-%s", site.Domain)
if err := deploy.Restart(unitName); err != nil {
return fmt.Errorf("restart %s: %w", site.Domain, err)
}
log.Printf("[scheduler] Restarted service for site %s (unit: %s)", site.Domain, unitName)
return nil
})
sched.Start()
// Graceful shutdown
done := make(chan os.Signal, 1)
signal.Notify(done, os.Interrupt, syscall.SIGTERM)
go func() {
if err := srv.Start(); err != nil {
log.Fatalf("[setec] Server error: %v", err)
}
}()
log.Printf("[setec] Dashboard: https://%s:%d", cfg.Server.Host, cfg.Server.Port)
<-done
log.Println("[setec] Shutting down...")
sched.Stop()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx)
}
func runSetup(cfg *config.Config, database *db.DB, configPath string) {
log.Println("[setup] Starting first-time setup...")
// Ensure directories exist
dirs := []string{
"/opt/setec-manager/data",
"/opt/setec-manager/data/acme",
"/opt/setec-manager/data/backups",
cfg.Nginx.Webroot,
cfg.Nginx.CertbotWebroot,
cfg.Nginx.SitesAvailable,
cfg.Nginx.SitesEnabled,
}
for _, d := range dirs {
os.MkdirAll(d, 0755)
}
// Install Nginx if needed
log.Println("[setup] Installing nginx...")
execQuiet("apt-get", "update", "-qq")
execQuiet("apt-get", "install", "-y", "nginx", "certbot", "ufw")
// Install nginx snippets
log.Println("[setup] Configuring nginx snippets...")
nginx.InstallSnippets(cfg)
// Create admin user
count, _ := database.ManagerUserCount()
if count == 0 {
log.Println("[setup] Creating default admin user (admin / autarch)")
database.CreateManagerUser("admin", "autarch", "admin")
}
// Save config
cfg.Save(configPath)
// Generate self-signed cert for manager if none exists
if _, err := os.Stat(cfg.Server.Cert); os.IsNotExist(err) {
log.Println("[setup] Generating self-signed TLS cert for manager...")
os.MkdirAll(cfg.ACME.AccountDir, 0755)
execQuiet("openssl", "req", "-x509", "-newkey", "rsa:2048",
"-keyout", cfg.Server.Key, "-out", cfg.Server.Cert,
"-days", "3650", "-nodes",
"-subj", "/CN=setec-manager/O=Setec Security Labs")
}
// Install systemd unit for setec-manager
unit := `[Unit]
Description=Setec App Manager
After=network.target
[Service]
Type=simple
User=root
ExecStart=/opt/setec-manager/setec-manager --config /opt/setec-manager/config.yaml
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
`
os.WriteFile("/etc/systemd/system/setec-manager.service", []byte(unit), 0644)
execQuiet("systemctl", "daemon-reload")
log.Println("[setup] Setup complete!")
log.Println("[setup] Start with: systemctl start setec-manager")
log.Printf("[setup] Dashboard will be at: https://<your-ip>:%d\n", cfg.Server.Port)
}
func execQuiet(name string, args ...string) {
log.Printf("[setup] $ %s %s", name, strings.Join(args, " "))
cmd := exec.Command(name, args...)
out, err := cmd.CombinedOutput()
if err != nil {
log.Printf("[setup] Warning: %v\n%s", err, string(out))
}
}