package main import ( "context" "crypto/subtle" "log/slog" "net/http" "os" "path/filepath" "time" "github.com/jackc/pgx/v5/pgxpool" ) type config struct { databaseURL string adminUser string adminPass string addr string adminAddr string storageDir string secureCookies bool } func loadConfig() config { c := config{ databaseURL: os.Getenv("DATABASE_URL"), adminUser: env("ADMIN_USER", "admin"), adminPass: os.Getenv("ADMIN_PASSWORD"), addr: env("ADDR", ":8080"), adminAddr: env("ADMIN_ADDR", "127.0.0.1:8081"), storageDir: env("STORAGE_DIR", "./storage"), secureCookies: env("SECURE_COOKIES", "true") != "false", } if c.databaseURL == "" { fatal("DATABASE_URL is not set") } // An admin panel that silently opens is worse than one that won't boot. if c.adminPass == "" { fatal("ADMIN_PASSWORD is not set") } return c } func env(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } func fatal(msg string, args ...any) { slog.Error(msg, args...) os.Exit(1) } type app struct { cfg config pool *pgxpool.Pool } func main() { slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil))) cfg := loadConfig() ctx := context.Background() pool, err := pgxpool.New(ctx, cfg.databaseURL) if err != nil { fatal("database connect", "error", err) } defer pool.Close() // Wait for Postgres rather than crash-looping past a healthcheck that hasn't gone green yet. for i := 0; ; i++ { pingCtx, cancel := context.WithTimeout(ctx, 3*time.Second) err = pool.Ping(pingCtx) cancel() if err == nil { break } if i == 10 { fatal("database unreachable", "error", err) } time.Sleep(time.Second) } if err := migrate(ctx, pool); err != nil { fatal("migrations", "error", err) } if err := sweep(ctx, pool); err != nil { fatal("startup sweep", "error", err) } for _, dir := range []string{"audio", "tmp"} { if err := os.MkdirAll(filepath.Join(cfg.storageDir, dir), 0o755); err != nil { fatal("storage dir", "error", err, "dir", dir) } } a := &app{cfg: cfg, pool: pool} // ponytail: two listeners, one process. Admin is loopback-only — reach it over an SSH tunnel // or the reverse proxy. A separate binary would need its own deploy and would race the // startup migrations; it buys nothing else. go func() { slog.Info("admin listening", "ctx", "startup", "addr", cfg.adminAddr) err := http.ListenAndServe(cfg.adminAddr, a.requireAdmin(a.adminMux())) fatal("admin listener", "error", err) }() slog.Info("listening", "ctx", "startup", "addr", cfg.addr) fatal("listener", "error", http.ListenAndServe(cfg.addr, a.memberMux())) } func (a *app) memberMux() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { if err := a.pool.Ping(r.Context()); err != nil { http.Error(w, "db down", http.StatusServiceUnavailable) return } w.Write([]byte("ok")) }) return mux } func (a *app) adminMux() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("GET /admin", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("levyraati admin")) }) return mux } // ponytail: Basic Auth, no admin session, no admin row. Ceiling: one admin, no logout // (close the browser). Add a cookie session if a second admin ever needs one. // // No bcrypt: hashing protects stored passwords against a database leak, and this one lives in the // env file next to the Postgres password already. The constant-time compare is the part that matters. func (a *app) requireAdmin(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { u, p, ok := r.BasicAuth() userOK := subtle.ConstantTimeCompare([]byte(u), []byte(a.cfg.adminUser)) == 1 passOK := subtle.ConstantTimeCompare([]byte(p), []byte(a.cfg.adminPass)) == 1 if !ok || !userOK || !passOK { w.Header().Set("WWW-Authenticate", `Basic realm="levyraati admin"`) http.Error(w, "unauthorized", http.StatusUnauthorized) return } next.ServeHTTP(w, r) }) }