spread

https://git.tonybtw.com/spread.git git://git.tonybtw.com/spread.git
3,587 bytes raw
1
// Command spread is the self-hostable server. It always serves the public,
2
// stateless demo (/, /api/analyze). When DATABASE_URL is set it also mounts the
3
// Postgres-backed application: accounts, sessions, /login, /admin, /app.
4
//
5
// Excluded from the Vercel deploy (see .vercelignore); there the api/ function
6
// serves the endpoint and the static files are served by Vercel's CDN.
7
package main
8
9
import (
10
	"context"
11
	"embed"
12
	"fmt"
13
	"log"
14
	"net/http"
15
	"os"
16
17
	"spread/internal/app"
18
	"spread/internal/spread"
19
)
20
21
//go:embed index.html app.js example-bom.csv assets
22
var static embed.FS
23
24
func main() {
25
	if len(os.Args) > 1 {
26
		switch os.Args[1] {
27
		case "init-admin":
28
			if err := runInitAdmin(os.Args[2:]); err != nil {
29
				log.Fatalf("init-admin: %v", err)
30
			}
31
			return
32
		case "create-customer":
33
			if err := runCreateCustomer(os.Args[2:]); err != nil {
34
				log.Fatalf("create-customer: %v", err)
35
			}
36
			return
37
		}
38
	}
39
40
	addr := ":" + envOr("PORT", "8137")
41
	mux := http.NewServeMux()
42
	mux.HandleFunc("/api/analyze", spread.Handler)
43
44
	if dsn := os.Getenv("DATABASE_URL"); dsn != "" {
45
		a, err := app.Open(context.Background(), app.Config{
46
			DSN:          dsn,
47
			SecureCookie: os.Getenv("SPREAD_SECURE_COOKIES") == "1",
48
			BaseURL:      envOr("SPREAD_BASE_URL", "http://localhost"+addr),
49
			Mailer:       app.NewMailerFromEnv(),
50
		})
51
		switch {
52
		case err == nil:
53
			defer a.Close()
54
			a.Routes(mux)
55
			log.Printf("accounts enabled: /login, /admin, /app (Postgres)")
56
		case os.Getenv("SPREAD_DB_OPTIONAL") == "1":
57
			// Dev convenience: run the public demo even if Postgres is down.
58
			log.Printf("WARNING: database unavailable (%v) — serving public demo only", err)
59
		default:
60
			log.Fatalf("database: %v", err)
61
		}
62
	} else {
63
		log.Printf("no DATABASE_URL — serving public demo only")
64
	}
65
66
	mux.Handle("/", http.FileServer(http.FS(static)))
67
	log.Printf("spread listening on http://localhost%s", addr)
68
	log.Fatal(http.ListenAndServe(addr, mux))
69
}
70
71
// runInitAdmin creates or resets the first admin user under the internal
72
// subscriber. Password is read from SPREAD_ADMIN_PASSWORD.
73
func runInitAdmin(args []string) error {
74
	if len(args) < 1 {
75
		return fmt.Errorf("usage: DATABASE_URL=... SPREAD_ADMIN_PASSWORD=... spread init-admin <email>")
76
	}
77
	dsn := os.Getenv("DATABASE_URL")
78
	if dsn == "" {
79
		return fmt.Errorf("DATABASE_URL must be set")
80
	}
81
	pw := os.Getenv("SPREAD_ADMIN_PASSWORD")
82
	if pw == "" {
83
		return fmt.Errorf("SPREAD_ADMIN_PASSWORD must be set")
84
	}
85
	if err := app.OpenAndInitAdmin(context.Background(), dsn, args[0], pw); err != nil {
86
		return err
87
	}
88
	log.Printf("admin user %q ready under the internal subscriber", args[0])
89
	return nil
90
}
91
92
// runCreateCustomer creates a customer user under a (possibly new) subscriber.
93
// Password is read from SPREAD_USER_PASSWORD.
94
func runCreateCustomer(args []string) error {
95
	if len(args) < 1 {
96
		return fmt.Errorf("usage: DATABASE_URL=... SPREAD_USER_PASSWORD=... spread create-customer <email> [subscriber-name]")
97
	}
98
	dsn := os.Getenv("DATABASE_URL")
99
	if dsn == "" {
100
		return fmt.Errorf("DATABASE_URL must be set")
101
	}
102
	pw := os.Getenv("SPREAD_USER_PASSWORD")
103
	if pw == "" {
104
		return fmt.Errorf("SPREAD_USER_PASSWORD must be set")
105
	}
106
	email := args[0]
107
	subscriber := "Customer"
108
	if len(args) > 1 {
109
		subscriber = args[1]
110
	}
111
	if err := app.OpenAndCreateCustomer(context.Background(), dsn, email, subscriber, pw); err != nil {
112
		return err
113
	}
114
	log.Printf("customer %q ready under subscriber %q", email, subscriber)
115
	return nil
116
}
117
118
func envOr(key, def string) string {
119
	if v := os.Getenv(key); v != "" {
120
		return v
121
	}
122
	return def
123
}