| 1 | // Command board is a complete, working app on rastrillo + idear: a |
| 2 | // shared message board whose roster — who is in this instance, at what |
| 3 | // rank, and who may change that — is idear's. |
| 4 | // |
| 5 | // It is the worked reference SKILL.md points at. Read app.go for the |
| 6 | // wiring, models.go for the app's own identity row and the seed, and |
| 7 | // app_test.go for the whole flow driven through real HTTP. |
| 8 | // |
| 9 | // Run it: |
| 10 | // |
| 11 | // BOARD_SEED=1 go run ./example -addr 127.0.0.1:8080 -db /tmp/board.db |
| 12 | // |
| 13 | // then sign in at http://127.0.0.1:8080/signin as ada@example.test |
| 14 | // (Owner), kim@example.test (Admin) or sam@example.test (Member), all |
| 15 | // with the password "demo-password". |
| 16 | package main |
| 17 | |
| 18 | import ( |
| 19 | "context" |
| 20 | "log/slog" |
| 21 | "net/url" |
| 22 | "os" |
| 23 | |
| 24 | "github.com/carlosframework/rastrillo" |
| 25 | "github.com/carlosframework/rastrillo/db" |
| 26 | ) |
| 27 | |
| 28 | func main() { |
| 29 | logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) |
| 30 | |
| 31 | origin := os.Getenv("BOARD_ORIGIN") |
| 32 | if origin == "" { |
| 33 | origin = "http://localhost:8080" |
| 34 | // Loud on purpose: origin decides the Secure/__Host- cookie |
| 35 | // attributes and the CSRF same-origin check, so a silent |
| 36 | // default in a real deployment means http-grade cookies on an |
| 37 | // https app. |
| 38 | logger.Warn("BOARD_ORIGIN not set; defaulting", "origin", origin) |
| 39 | } |
| 40 | |
| 41 | // The instance's display name, shown on the PUBLIC invitation |
| 42 | // page. Set it, always: idear falls back to the request's Host |
| 43 | // header, which the client supplies. |
| 44 | site := os.Getenv("BOARD_NAME") |
| 45 | if site == "" { |
| 46 | site = hostOf(origin) |
| 47 | } |
| 48 | |
| 49 | // Resolve, not Run: this app opens its own database handle through |
| 50 | // db.Open — a *gorm.DB over the split reader/writer pool — so |
| 51 | // Options.DBPath must be blanked before Serve, or Serve opens a |
| 52 | // second connection to the same file. |
| 53 | opts, err := rastrillo.Resolve(rastrillo.Options{DBPath: "board.db", Logger: logger}) |
| 54 | if err != nil { |
| 55 | logger.Error("resolve activation", "err", err) |
| 56 | os.Exit(1) |
| 57 | } |
| 58 | |
| 59 | d, err := db.Open(opts.DBPath, logger) |
| 60 | if err != nil { |
| 61 | logger.Error("open database", "err", err) |
| 62 | os.Exit(1) |
| 63 | } |
| 64 | defer d.Close() |
| 65 | |
| 66 | a, err := newApp(d, origin, site, logger) |
| 67 | if err != nil { |
| 68 | logger.Error("build app", "err", err) |
| 69 | os.Exit(1) |
| 70 | } |
| 71 | |
| 72 | if os.Getenv("BOARD_SEED") == "1" { |
| 73 | if err := Seed(context.Background(), d.G, a.roster); err != nil { |
| 74 | logger.Error("seed", "err", err) |
| 75 | os.Exit(1) |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | opts.Mux = a.mux |
| 80 | opts.DBPath = "" |
| 81 | if err := rastrillo.Serve(opts); err != nil { |
| 82 | logger.Error("serve failed", "err", err) |
| 83 | os.Exit(1) |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | // hostOf is the host half of an absolute origin, used as the instance |
| 88 | // name when BOARD_NAME is unset. It is NOT the Host header: it comes |
| 89 | // from the app's own configured origin, so it is a name the operator |
| 90 | // chose rather than one a visitor sent. |
| 91 | func hostOf(origin string) string { |
| 92 | u, err := url.Parse(origin) |
| 93 | if err != nil || u.Host == "" { |
| 94 | return origin |
| 95 | } |
| 96 | return u.Host |
| 97 | } |
| 98 | |