package setup import ( "html" "net/http" "net/url" "git.lohmar.co.uk/cclohmar/NextMyDay/internal/core/web" ) // renderHTML writes the wizard page inside the core shell (SPEC-009 §4). The // door renders no dynamic HTML itself (SPEC-003 §4); the wizard is the hand-off // target for the `setup` workflow. // // F-2: the bootstrap renders secrets and ceremony state — never cache it. func renderHTML(w http.ResponseWriter, status int, page web.Page) error { w.Header().Set("Cache-Control", "no-store") return web.Render(w, status, page) } // writeError writes a terse plain-text error. func writeError(w http.ResponseWriter, status int, msg string) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(status) _, _ = w.Write([]byte(msg + "\n")) } // redirect sends the operator back to the wizard (post/redirect/get). func redirect(w http.ResponseWriter, r *http.Request, path string) { http.Redirect(w, r, path, http.StatusSeeOther) } // wizardPage renders the current step inside the shell. The wizard runs before // any identity exists, so the page is a GUEST screen: no nav (SPEC-009 §4). // Every form carries the signed pre-session CSRF token (A15). func wizardPage(st step, token, domain string) web.Page { c := html.EscapeString(token) return web.Page{ Title: "MyDay setup", Guest: true, CSRF: token, Content: web.Markup(`

MyDay setup

Bring this instance to life: the domain, the top admin, then mail and locale.

` + stepBody(st, c, domain)), } } // totpPage renders the wizard's TOTP step (SPEC-005 §4) once the secret has // been issued: the otpauth URL, its human-readable secret for manual entry, and // the verify form. The wizard is server-rendered and works with no JavaScript, // so the secret is shown as text rather than encoded in a QR image; the page // renders inside the shell (no inline scripts) and the verify form carries the // signed pre-session CSRF token (A15). func totpPage(token, otpauthURL string) web.Page { c := html.EscapeString(token) u := html.EscapeString(otpauthURL) secret := html.EscapeString(secretFromOtpauth(otpauthURL)) return web.Page{ Title: "MyDay setup", Guest: true, CSRF: token, Content: web.Markup(`

MyDay setup

Add the authenticator, then enter a code from it to finish.

` + `

2. Enroll the admin

` + `

A passkey can be added later, at first sign-in.

` + `

Authenticator app

` + `

Open this link in your authenticator app:

` + `

` + u + `

` + `

Or enter this secret by hand:

` + `

` + secret + `

` + `
` + `` + `` + `
`), } } // secretFromOtpauth pulls the shared secret out of an otpauth:// URL for the // manual-entry fallback. It returns "" when the URL carries none — the page then // shows the link alone. The secret is never logged. func secretFromOtpauth(raw string) string { u, err := url.Parse(raw) if err != nil { return "" } return u.Query().Get("secret") } // codeFormPage renders the D-044 first-run page — what `GET /setup` shows while // `setup: true` and a required `setup_code` has not yet been entered. It is // deliberately friendly: the point is that the domain must NOT look "down". // // It is a GUEST screen (no identity, no nav) with the setup-code entry form. // The form submits over POST, so the code never rides the URL (and is never // access-logged — the `code` parameter is redacted even if it ever did, A8). func codeFormPage(token, flash string) web.Page { c := html.EscapeString(token) kind := "" if flash != "" { kind = "error" } return web.Page{ Title: "MyDay is not set up yet", Guest: true, CSRF: token, Flash: flash, FlashKind: kind, Content: web.Markup(`

MyDay is not set up yet

This instance is waiting for its first-run setup. Open the setup wizard with the one-time setup code that MyDay printed to its log when it started.

Where is the code?

It is in MyDay's startup log:

Look for the line setup code.

`), } } // landingPage is the friendly page `GET /` shows while `setup: true` (D-045) — // a pointer to the wizard. It carries no form and no code. func landingPage() web.Page { return web.Page{ Title: "MyDay setup", Guest: true, Content: web.Markup(`

MyDay is ready for setup

The setup wizard is enabled on this instance.

Open the setup wizard

`), } } // lockedPage is what `GET /` and `GET /setup` show when `setup: false` and no // admin exists yet (D-045): the instance is deliberately locked, not claimable. // It is a GUEST screen with no form. func lockedPage() web.Page { return web.Page{ Title: "MyDay is not set up", Guest: true, Content: web.Markup(`

MyDay is not set up

This instance has no administrator yet, and setup is disabled.

To run the first-run setup, set setup: true in config.yaml and restart MyDay. Then open /setup.

`), } } // stepBody renders the section for one step. func stepBody(st step, c, domain string) string { switch st { case stepDomain: return `

1. Domain

` + `
` + `` + `` + `
` case stepAdmin: return `

2. The top admin

` + `
` + `` + `` + `` + `
` case stepEnroll: // Step 2 enrolls TOTP only (D-041): a passkey registered over loopback // would bind RP ID 127.0.0.1 and be dead at the public domain. The // admin adds a passkey at first login, via the nudge (SPEC-006 §8). return `

2. Enroll the admin

` + `

Add an authenticator app. A passkey can be added later, at first sign-in.

` + `

Authenticator app

` + `
` + `` + `
` + `
` + `` + `` + `
` default: // stepSettings return `

3. Mail · locale · timezone

` + `
` + `` + `` + `` + `` + `` + `` + `` + `` + `
` } }