// Command board is a complete, working app on rastrillo + idear: a // shared message board whose roster — who is in this instance, at what // rank, and who may change that — is idear's. // // It is the worked reference SKILL.md points at. Read app.go for the // wiring, models.go for the app's own identity row and the seed, and // app_test.go for the whole flow driven through real HTTP. // // Run it: // // BOARD_SEED=1 go run ./example -addr 127.0.0.1:8080 -db /tmp/board.db // // then sign in at http://127.0.0.1:8080/signin as ada@example.test // (Owner), kim@example.test (Admin) or sam@example.test (Member), all // with the password "demo-password". package main import ( "context" "log/slog" "net/url" "os" "github.com/carlosframework/rastrillo" "github.com/carlosframework/rastrillo/db" ) func main() { logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) origin := os.Getenv("BOARD_ORIGIN") if origin == "" { origin = "http://localhost:8080" // Loud on purpose: origin decides the Secure/__Host- cookie // attributes and the CSRF same-origin check, so a silent // default in a real deployment means http-grade cookies on an // https app. logger.Warn("BOARD_ORIGIN not set; defaulting", "origin", origin) } // The instance's display name, shown on the PUBLIC invitation // page. Set it, always: idear falls back to the request's Host // header, which the client supplies. site := os.Getenv("BOARD_NAME") if site == "" { site = hostOf(origin) } // Resolve, not Run: this app opens its own database handle through // db.Open — a *gorm.DB over the split reader/writer pool — so // Options.DBPath must be blanked before Serve, or Serve opens a // second connection to the same file. opts, err := rastrillo.Resolve(rastrillo.Options{DBPath: "board.db", Logger: logger}) if err != nil { logger.Error("resolve activation", "err", err) os.Exit(1) } d, err := db.Open(opts.DBPath, logger) if err != nil { logger.Error("open database", "err", err) os.Exit(1) } defer d.Close() a, err := newApp(d, origin, site, logger) if err != nil { logger.Error("build app", "err", err) os.Exit(1) } if os.Getenv("BOARD_SEED") == "1" { if err := Seed(context.Background(), d.G, a.roster); err != nil { logger.Error("seed", "err", err) os.Exit(1) } } opts.Mux = a.mux opts.DBPath = "" if err := rastrillo.Serve(opts); err != nil { logger.Error("serve failed", "err", err) os.Exit(1) } } // hostOf is the host half of an absolute origin, used as the instance // name when BOARD_NAME is unset. It is NOT the Host header: it comes // from the app's own configured origin, so it is a name the operator // chose rather than one a visitor sent. func hostOf(origin string) string { u, err := url.Parse(origin) if err != nil || u.Host == "" { return origin } return u.Host }