Signed out of Home is the door; the address check is shape only
Review findings: /signed-out rendered Home's bare page while /signin was the door, and plausibleAddress refused dotless domains and a quoted local part's @.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 files changed,
+44
−16
internal/memoria/auth.go+3 −6internal/memoria/auth_test.go+2 −2internal/memoria/handlers.go+7 −4internal/memoria/home_render.go+17 −3internal/memoria/home_router_test.go+14 −0internal/memoria/templates/signin.html+1 −1
diff --git a/internal/memoria/auth.go b/internal/memoria/auth.go| index 57c1154..3ea2aa1 100644 |
| --- a/internal/memoria/auth.go |
| +++ b/internal/memoria/auth.go |
| @@ -278,13 +278,10 @@ type oauthPage struct { |
| } |
| // plausibleAddress is the shape of an email address and nothing more: |
| -// something, one @, a domain with a dot, no spaces. |
| +// something, an @, something, no spaces. The last @ splits it (a quoted |
| +// local part may hold one), and a dotless domain is still an address. |
| func plausibleAddress(v string) bool { |
| v = strings.TrimSpace(v) |
| at := strings.LastIndexByte(v, '@') |
| - if at <= 0 || at == len(v)-1 || strings.ContainsAny(v, " \t\r\n") || strings.Count(v, "@") != 1 { |
| - return false |
| - } |
| - domain := v[at+1:] |
| - return strings.Contains(domain, ".") && !strings.HasPrefix(domain, ".") && !strings.HasSuffix(domain, ".") |
| + return at > 0 && at < len(v)-1 && !strings.ContainsAny(v, " \t\r\n") |
| } |
diff --git a/internal/memoria/auth_test.go b/internal/memoria/auth_test.go| index 101568f..5993d93 100644 |
| --- a/internal/memoria/auth_test.go |
| +++ b/internal/memoria/auth_test.go |
| @@ -6,8 +6,8 @@ func TestPlausibleAddressIsShapeOnly(t *testing.T) { |
| for v, want := range map[string]bool{ |
| "paul@example.com": true, " paul@example.com ": true, "a@b.co": true, |
| "": false, "paul@": false, "@example.com": false, "paul": false, |
| - "paul@example": false, "paul@.com": false, "paul@example.": false, |
| - "pa ul@example.com": false, "a@b@c.com": false, |
| + "paul@localhost": true, `"a@b"@example.com`: true, |
| + "pa ul@example.com": false, "paul@example.com@": false, |
| } { |
| if got := plausibleAddress(v); got != want { |
| t.Errorf("plausibleAddress(%q) = %v, want %v", v, got, want) |
diff --git a/internal/memoria/handlers.go b/internal/memoria/handlers.go| index bd34e24..164b9d8 100644 |
| --- a/internal/memoria/handlers.go |
| +++ b/internal/memoria/handlers.go |
| @@ -1124,12 +1124,15 @@ func (a *app) deleteDocument(w http.ResponseWriter, r *http.Request) { |
| } |
| // signinPage is the sign-in page: the layout's page (footer, egress claim |
| -// and all) around the suite's door. Exactly one of Door and Sent is set — |
| -// the form, or the check-your-email notice that replaces it. |
| +// and all) around the suite's door. Exactly one of Door, Sent and |
| +// SignedOut is set — |
| +// the form, the check-your-email notice that replaces it, or the page |
| +// Home sign-out lands on. |
| type signinPage struct { |
| page |
| - Door *chrome.SignIn |
| - Sent *chrome.SignInNotice |
| + Door *chrome.SignIn |
| + Sent *chrome.SignInNotice |
| + SignedOut *chrome.SignInNotice |
| } |
| // signinLede is what the door says under its heading on a plain visit. |
diff --git a/internal/memoria/home_render.go b/internal/memoria/home_render.go| index f57f506..c71ace2 100644 |
| --- a/internal/memoria/home_render.go |
| +++ b/internal/memoria/home_render.go |
| @@ -1,9 +1,12 @@ |
| package memoria |
| import ( |
| - "amadan.net/oficina/home/connect" |
| "bytes" |
| "net/http" |
| + |
| + "amadan.net/oficina/estilo.git/chrome" |
| + "amadan.net/oficina/estilo.git/sky" |
| + "amadan.net/oficina/home/connect" |
| ) |
| type homePage struct { |
| @@ -12,9 +15,20 @@ type homePage struct { |
| } |
| func (a *app) renderHome(w http.ResponseWriter, r *http.Request, state, label string, status int) { |
| - p := homePage{page: page{Chunks: a.chunkCount(), ModelOn: a.modelOn()}, Home: connect.Page(state, label)} |
| + pg := page{Chunks: a.chunkCount(), ModelOn: a.modelOn()} |
| + data := connect.Page(state, label) |
| + tree, view := pages["home"], any(homePage{page: pg, Home: data}) |
| + // Signed out of Home is the door: Home's heading, sentence and link, |
| + // on Memoria's card and sky, like the sign-in page it sits beside. |
| + if state == "signedout" { |
| + tree, view = pages["signin"], signinPage{page: pg, SignedOut: &chrome.SignInNotice{ |
| + Icon: assets.Path("static/brand/icon.svg"), App: "Memoria", Sky: sky.For(r.Host), |
| + Heading: data.Title, Body: data.Body, |
| + Link: &chrome.Link{Href: data.Action, Label: data.ActionLabel}, |
| + }} |
| + } |
| var body bytes.Buffer |
| - if err := pages["home"].ExecuteTemplate(&body, "layout", p); err != nil { |
| + if err := tree.ExecuteTemplate(&body, "layout", view); err != nil { |
| http.Error(w, "something went wrong", http.StatusInternalServerError) |
| return |
| } |
diff --git a/internal/memoria/home_router_test.go b/internal/memoria/home_router_test.go| index 1ab5119..fa8b6f4 100644 |
| --- a/internal/memoria/home_router_test.go |
| +++ b/internal/memoria/home_router_test.go |
| @@ -375,3 +375,17 @@ func TestHomePagesStateTheModelSwitch(t *testing.T) { |
| } |
| } |
| } |
| + |
| +// Where Home sign-out lands is the door, like the sign-in page beside it: |
| +// Memoria's card on the sky, with Home's words and link, and the door's |
| +// stylesheets linked so the card is drawn. |
| +func TestSignedOutIsTheDoor(t *testing.T) { |
| + f := newHomeRouterFixture(t) |
| + w := f.do("GET", "/signed-out", "") |
| + b := w.Body.String() |
| + for _, want := range []string{"estilo-door", "estilo-sky-layer", "estilo-signin", "<span>Memoria</span>", "/static/chrome.", "/static/sky."} { |
| + if !strings.Contains(b, want) { |
| + t.Errorf("/signed-out (%d) lacks %q", w.Code, want) |
| + } |
| + } |
| +} |
diff --git a/internal/memoria/templates/signin.html b/internal/memoria/templates/signin.html| index c4d1151..bbf136c 100644 |
| --- a/internal/memoria/templates/signin.html |
| +++ b/internal/memoria/templates/signin.html |
| @@ -5,4 +5,4 @@ |
| The layout's footer, and with it the egress claim, stays under it. */}} |
| {{define "head"}}<link rel="stylesheet" href="{{asset "static/chrome.css"}}"> |
| <link rel="stylesheet" href="{{asset "static/sky.css"}}">{{end}} |
| -{{define "content"}}{{with .Sent}}{{template "estilo-signin-sent" .}}{{else}}{{template "estilo-signin" .Door}}{{end}}{{end}} |
| +{{define "content"}}{{with .Sent}}{{template "estilo-signin-sent" .}}{{else with .SignedOut}}{{template "estilo-signed-out" .}}{{else}}{{template "estilo-signin" .Door}}{{end}}{{end}} |