| 1 | # The example board |
| 2 | |
| 3 | A complete app on rastrillo + idear: a shared message board whose roster is |
| 4 | idear's. It is the worked reference `../SKILL.md` points at, and it is part |
| 5 | of the `amadan.net/rastrillo/idear` module rather than a submodule — a |
| 6 | nested module would need a `replace` directive pointing at its own parent, |
| 7 | which is exactly the thing an app must never write. |
| 8 | |
| 9 | ## Run it |
| 10 | |
| 11 | ```sh |
| 12 | BOARD_SEED=1 go run ./example -addr 127.0.0.1:8080 -db /tmp/board.db |
| 13 | ``` |
| 14 | |
| 15 | Then sign in at <http://127.0.0.1:8080/signin> as one of the seeded |
| 16 | accounts, all with the password `demo-password`: |
| 17 | |
| 18 | | Address | Role | What you can do | |
| 19 | | ------------------- | ------ | ------------------------------------------------- | |
| 20 | | `ada@example.test` | Owner | everything, including transferring ownership | |
| 21 | | `kim@example.test` | Admin | invite and manage **Members** only | |
| 22 | | `sam@example.test` | Member | read the roster; post; nothing else | |
| 23 | |
| 24 | Three accounts at three roles, because a role gate you cannot click on is a |
| 25 | role gate nobody checks. Sign in as Kim and the role selector offers Member |
| 26 | and nothing else; sign in as Sam and the members page offers no controls at |
| 27 | all. |
| 28 | |
| 29 | `BOARD_ORIGIN` sets the external origin (it decides the CSRF check and the |
| 30 | cookie attributes); `BOARD_NAME` sets the instance's display name on the |
| 31 | public invitation page. Both default loudly. |
| 32 | |
| 33 | There is no mail server here, so `HandlerConfig.Deliver` is nil and idear |
| 34 | puts the invitation **link itself** in the flash notice shown to the admin |
| 35 | who minted it. Copy it out of the page. A deployed app sets `Deliver` and |
| 36 | keeps the token out of the browser entirely. |
| 37 | |
| 38 | ## What to read |
| 39 | |
| 40 | | File | What it shows | |
| 41 | | -------------- | ------------------------------------------------------------------- | |
| 42 | | `app.go` | the whole wiring, in the order it has to happen, with the reasons | |
| 43 | | `models.go` | the app's own `User`, `BootSchema`, and a seed through the real flows | |
| 44 | | `render.go` | the two idear render callbacks, and `idear.TokenFrom` in `RenderSignup` | |
| 45 | | `handlers.go` | `idear.From(r)`, and an app route gated on Admin | |
| 46 | | `app_test.go` | the whole flow through real HTTP — the thing to copy | |
| 47 | |
| 48 | Four things in here are load-bearing rather than stylistic, and each has a |
| 49 | comment at the site saying so: |
| 50 | |
| 51 | - **`/` is behind `Require`.** Under the password plugin, deactivation is |
| 52 | enforced per request by `Require`, **not** at sign-in — `password.Signin` |
| 53 | runs Lookup → Verify → mint with no idear involvement. An ungated landing |
| 54 | page is a page a removed member can still read. |
| 55 | - **`POST /signup` is wrapped in `rs.CarryToken`.** Without it the |
| 56 | invitation token never reaches admission and every invited signup is |
| 57 | refused. |
| 58 | - **One 404 renderer**, bound once and handed to both `idear.Config.NotFound` |
| 59 | and chi's own `NotFound`. Two of them is a membership oracle, and it is |
| 60 | the one misconfiguration idear cannot detect at runtime. |
| 61 | - **`renderSignup` seeds its hidden `invite` field from |
| 62 | `idear.TokenFrom(r)`.** `password.PageData` has nowhere to carry a token, |
| 63 | so without this a signup that fails validation re-renders a form with an |
| 64 | empty field and the invitee's *second* attempt is refused. If you rewrite |
| 65 | this page — the likeliest thing to do to it — keep that line. |
| 66 | |