| 1 | // Command movement wires the app: resolve the platform's activation |
| 2 | // argv, open the database, build the app, serve. |
| 3 | package main |
| 4 | |
| 5 | import ( |
| 6 | "log/slog" |
| 7 | "os" |
| 8 | |
| 9 | "github.com/carlosframework/rastrillo" |
| 10 | "github.com/carlosframework/rastrillo/db" |
| 11 | |
| 12 | movement "amadan.net/carlos/movement/internal/movement" |
| 13 | ) |
| 14 | |
| 15 | func main() { |
| 16 | logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) |
| 17 | |
| 18 | // Resolve, not Run: this app opens its own database handle via |
| 19 | // db.Open — a *gorm.DB with a split reader/writer pool, not the |
| 20 | // bare *sql.DB Options.Router would hand back — so Options.DBPath |
| 21 | // must be empty again by the Serve call below, or Serve would open |
| 22 | // a second, unused connection to the same file. |
| 23 | opts, err := rastrillo.Resolve(rastrillo.Options{DBPath: "movement.db", Logger: logger}) |
| 24 | if err != nil { |
| 25 | logger.Error("resolve activation", "err", err) |
| 26 | os.Exit(1) |
| 27 | } |
| 28 | |
| 29 | d, err := db.Open(opts.DBPath, logger) |
| 30 | if err != nil { |
| 31 | logger.Error("open database", "err", err) |
| 32 | os.Exit(1) |
| 33 | } |
| 34 | defer d.Close() |
| 35 | |
| 36 | // The scaffold defaulted a missing origin to localhost and warned. This |
| 37 | // app refuses instead. The origin is the base of every emailed link, the |
| 38 | // CSRF yardstick and the cookie's host, so a default that boots happily |
| 39 | // is a canary quietly mailing people into production. LoadEnv also |
| 40 | // refuses an unset instance key, and refuses to run without SMTP unless |
| 41 | // the origin is loopback — because rastrillo's mail fallback logs the |
| 42 | // confirmation token rather than sending it. |
| 43 | env, err := movement.LoadEnv() |
| 44 | if err != nil { |
| 45 | logger.Error("configuration", "err", err) |
| 46 | os.Exit(1) |
| 47 | } |
| 48 | movement.SetLogger(logger) |
| 49 | |
| 50 | mux, err := movement.App(d, env) |
| 51 | if err != nil { |
| 52 | logger.Error("build app", "err", err) |
| 53 | os.Exit(1) |
| 54 | } |
| 55 | |
| 56 | opts.Mux = mux |
| 57 | // rastrillo's default policy is form-action 'self', which a browser |
| 58 | // applies to the whole redirect chain a form submission starts — so the |
| 59 | // app's own CSP was refusing its own 303 to the sign-in provider. |
| 60 | opts.CSP = movement.CSP() |
| 61 | opts.DBPath = "" |
| 62 | // The same page a handler's own error path renders (render.go's |
| 63 | // ErrorPage — wire it to Ctx.ErrorPage too as the app grows |
| 64 | // handlers), so a panic and a handled failure look identical. |
| 65 | opts.ErrorPage = movement.ErrorPage |
| 66 | if err := rastrillo.Serve(opts); err != nil { |
| 67 | logger.Error("serve failed", "err", err) |
| 68 | os.Exit(1) |
| 69 | } |
| 70 | } |
| 71 | |