// Command spread is the self-hostable server. It always serves the public, // stateless demo (/, /api/analyze). When DATABASE_URL is set it also mounts the // Postgres-backed application: accounts, sessions, /login, /admin, /app. // // Excluded from the Vercel deploy (see .vercelignore); there the api/ function // serves the endpoint and the static files are served by Vercel's CDN. package main import ( "context" "embed" "fmt" "log" "net/http" "os" "spread/internal/app" "spread/internal/spread" ) //go:embed index.html app.js example-bom.csv assets var static embed.FS func main() { if len(os.Args) > 1 { switch os.Args[1] { case "init-admin": if err := runInitAdmin(os.Args[2:]); err != nil { log.Fatalf("init-admin: %v", err) } return case "create-customer": if err := runCreateCustomer(os.Args[2:]); err != nil { log.Fatalf("create-customer: %v", err) } return } } addr := ":" + envOr("PORT", "8137") mux := http.NewServeMux() mux.HandleFunc("/api/analyze", spread.Handler) if dsn := os.Getenv("DATABASE_URL"); dsn != "" { a, err := app.Open(context.Background(), app.Config{ DSN: dsn, SecureCookie: os.Getenv("SPREAD_SECURE_COOKIES") == "1", BaseURL: envOr("SPREAD_BASE_URL", "http://localhost"+addr), Mailer: app.NewMailerFromEnv(), }) switch { case err == nil: defer a.Close() a.Routes(mux) log.Printf("accounts enabled: /login, /admin, /app (Postgres)") case os.Getenv("SPREAD_DB_OPTIONAL") == "1": // Dev convenience: run the public demo even if Postgres is down. log.Printf("WARNING: database unavailable (%v) — serving public demo only", err) default: log.Fatalf("database: %v", err) } } else { log.Printf("no DATABASE_URL — serving public demo only") } mux.Handle("/", http.FileServer(http.FS(static))) log.Printf("spread listening on http://localhost%s", addr) log.Fatal(http.ListenAndServe(addr, mux)) } // runInitAdmin creates or resets the first admin user under the internal // subscriber. Password is read from SPREAD_ADMIN_PASSWORD. func runInitAdmin(args []string) error { if len(args) < 1 { return fmt.Errorf("usage: DATABASE_URL=... SPREAD_ADMIN_PASSWORD=... spread init-admin ") } dsn := os.Getenv("DATABASE_URL") if dsn == "" { return fmt.Errorf("DATABASE_URL must be set") } pw := os.Getenv("SPREAD_ADMIN_PASSWORD") if pw == "" { return fmt.Errorf("SPREAD_ADMIN_PASSWORD must be set") } if err := app.OpenAndInitAdmin(context.Background(), dsn, args[0], pw); err != nil { return err } log.Printf("admin user %q ready under the internal subscriber", args[0]) return nil } // runCreateCustomer creates a customer user under a (possibly new) subscriber. // Password is read from SPREAD_USER_PASSWORD. func runCreateCustomer(args []string) error { if len(args) < 1 { return fmt.Errorf("usage: DATABASE_URL=... SPREAD_USER_PASSWORD=... spread create-customer [subscriber-name]") } dsn := os.Getenv("DATABASE_URL") if dsn == "" { return fmt.Errorf("DATABASE_URL must be set") } pw := os.Getenv("SPREAD_USER_PASSWORD") if pw == "" { return fmt.Errorf("SPREAD_USER_PASSWORD must be set") } email := args[0] subscriber := "Customer" if len(args) > 1 { subscriber = args[1] } if err := app.OpenAndCreateCustomer(context.Background(), dsn, email, subscriber, pw); err != nil { return err } log.Printf("customer %q ready under subscriber %q", email, subscriber) return nil } func envOr(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def }