// Package app wires together every subsystem (database, runtime store, // certificate manager, HTTP/TCP/UDP proxying, web UI, REST API) into a // single running vibeproxy process, and exposes the same wiring pieces // individually for CLI subcommands that don't need the full server. package app import ( "context" "database/sql" "fmt" "log/slog" "os" "path/filepath" "github.com/owen/vibeproxy/internal/api" "github.com/owen/vibeproxy/internal/auth" "github.com/owen/vibeproxy/internal/backup" "github.com/owen/vibeproxy/internal/certificates" "github.com/owen/vibeproxy/internal/config" "github.com/owen/vibeproxy/internal/database" "github.com/owen/vibeproxy/internal/logging" "github.com/owen/vibeproxy/internal/proxy" "github.com/owen/vibeproxy/internal/runtime" "github.com/owen/vibeproxy/internal/streams" "github.com/owen/vibeproxy/internal/web" ) type App struct { Cfg config.Config DB *sql.DB Log *slog.Logger Store *runtime.Store Certs *certificates.Manager Streams *streams.Manager } // Open initializes the database, runtime store, and certificate manager. // It does not start any listeners — used by both `serve` and one-shot CLI // commands like `database migrate`. func Open(cfg config.Config, log *slog.Logger) (*App, error) { if err := os.MkdirAll(cfg.DataDir, 0o700); err != nil { return nil, fmt.Errorf("create data dir: %w", err) } db, err := database.Open(cfg.DBPath) if err != nil { return nil, fmt.Errorf("open database: %w", err) } if err := ensureAdminUser(db, log); err != nil { db.Close() return nil, err } certStoragePath := database.GetSetting(db, "cert_storage_path", filepath.Join(cfg.DataDir, "certs")) certMgr, err := certificates.NewManager(db, certStoragePath, log) if err != nil { db.Close() return nil, fmt.Errorf("init certificate manager: %w", err) } if err := certMgr.LoadAll(); err != nil { log.Warn("failed loading certificates", "err", err) } store := runtime.NewStore(db) if err := store.Reload(context.Background()); err != nil { db.Close() return nil, fmt.Errorf("initial config load: %w", err) } streamMgr := streams.NewManager(db, log) return &App{Cfg: cfg, DB: db, Log: log, Store: store, Certs: certMgr, Streams: streamMgr}, nil } func (a *App) Close() error { return a.DB.Close() } // ensureAdminUser creates the single `admin` account with a generated // temporary password on first run, per the "no administrator exists yet" // requirement. func ensureAdminUser(db *sql.DB, log *slog.Logger) error { n, err := database.CountUsers(db) if err != nil { return err } if n > 0 { return nil } password, err := auth.GenerateRandomPassword() if err != nil { return err } hash, err := auth.HashPassword(password) if err != nil { return err } if err := database.CreateUser(db, "admin", hash, true); err != nil { return err } database.InsertAuditLog(db, "system", "admin_created", "admin", "initial administrator account created") fmt.Println("========================================================") fmt.Println(" vibeproxy: created initial administrator account") fmt.Println(" username: admin") fmt.Printf(" password: %s\n", password) fmt.Println(" Change this password after logging in (Settings > Admin Account).") fmt.Println("========================================================") log.Info("created initial admin account", "username", "admin") return nil } // Run starts every listener (HTTP/HTTPS proxy, TCP/UDP streams, admin web // UI + API) and background loops (health checks are started by the // runtime store on reload; certificate renewal, log retention, backups), // blocking until ctx is cancelled, then shutting everything down // gracefully. func (a *App) Run(ctx context.Context) error { if err := a.Streams.Reload(ctx); err != nil { return fmt.Errorf("initial stream load: %w", err) } settings := a.Store.Current().Settings a.Certs.StartRenewalLoop(ctx, settings.ACMERenewalWindowDays, settings.ACMEEmail, settings.ACMEStaging) logging.StartRetentionLoop(ctx, a.DB, a.Log, func() int { return a.Store.Current().Settings.LogRetentionDays }) backup.StartScheduledBackups(ctx, a.DB, a.Log, func() string { return a.Store.Current().Settings.BackupPath }, func() int { return a.Store.Current().Settings.BackupIntervalHours }, func() int { return a.Store.Current().Settings.BackupRetentionCount }, func() bool { return a.Store.Current().Settings.BackupEnabled }, ) proxySrv := proxy.NewServer(proxy.Options{HTTPAddr: a.Cfg.HTTPAddr, HTTPSAddr: a.Cfg.HTTPSAddr}, a.Store, a.Certs, a.DB, a.Log) csrf := auth.NewCSRF() adminMux := web.NewRouter(a.DB, a.Store, a.Certs, a.Streams, csrf, a.Log) apiMux := api.NewRouter(a.DB, a.Store, a.Certs, a.Streams, a.Log) adminMux.Handle("/api/v1/", apiMux) adminMux.Handle("/healthz", healthzHandler()) adminMux.Handle("/readyz", readyzHandler(a.DB)) adminMux.Handle("/metrics", metricsHandler()) adminSrv := newAdminServer(a.Cfg.AdminAddr, adminMux) errCh := make(chan error, 4) go func() { if err := proxySrv.Start(ctx); err != nil { errCh <- err } }() go func() { if err := adminSrv.Start(ctx); err != nil { errCh <- err } }() select { case <-ctx.Done(): case err := <-errCh: a.Log.Error("fatal listener error", "err", err) } a.Streams.StopAll() return nil }