oficina / calendar Public

Clone
git clone https://amadan.net/oficina/calendar

Plain git — no account needed to clone.

Download

Download this file

1package calendar
2
3import (
4 "amadan.net/oficina/home/connect"
5 "amadan.net/rastrillo/rastrillo/sessions"
6 "context"
7 "net/http"
8)
9
10// personCtxKey carries the resolved Person down the request. A private
11// key type, so nothing outside this package can set it and pretend to
12// be somebody.
13type personCtxKey struct{}
14
15// requirePerson turns the session's verified ADDRESS into this app's
16// own person ROW, creating it on first sign-in, and puts it on the
17// request.
18//
19// It runs inside the session group, after the auth plugin's own
20// RequireSession, and it is the single seam where an address becomes an
21// id. Every owner-scoped query downstream takes that id — never the
22// address — so a change of address is one row, not a migration across
23// every table.
24//
25// It is a middleware rather than a call in each handler for the reason
26// every such seam is: a handler that forgot it would not fail, it would
27// silently scope to the zero person and show an empty calendar. An
28// empty calendar looks like a new account, not like a bug.
29func (a *App) requirePerson(next http.Handler) http.Handler {
30 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
31 if session, ok := sessions.Current(r); ok && session.Method == connect.HomeMethod {
32 if a.home == nil {
33 a.h.errorPage(w, r, http.StatusNotFound, "")
34 return
35 }
36 p, err := a.homePerson(r)
37 if err != nil {
38 a.h.errorPage(w, r, http.StatusNotFound, "")
39 return
40 }
41 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), personCtxKey{}, p)))
42 return
43 }
44
45 id, ok := viewer(r)
46 if !ok {
47 // RequireSession runs first and would have refused, so
48 // reaching here means the two disagree. Refuse rather than
49 // continue with no viewer.
50 a.logger.Error("calendar: session with no identity reached requirePerson", "path", r.URL.Path)
51 a.h.errorPage(w, r, http.StatusInternalServerError, "")
52 return
53 }
54 if a.home != nil && !a.home.admitted.allows(id.Address) {
55 a.h.errorPage(w, r, http.StatusNotFound, "")
56 return
57 }
58 p, err := a.store.EnsurePersonSetup(r.Context(), id.Address)
59 if err != nil {
60 // No address in the log line: the error is about this
61 // request, and `carlos logs` is platform-held storage we
62 // cannot grep afterwards.
63 a.logger.Error("calendar: ensure person", "err", err)
64 a.h.errorPage(w, r, http.StatusInternalServerError, "")
65 return
66 }
67 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), personCtxKey{}, p)))
68 })
69}
70
71// CurrentPerson is the signed-in person on r. Past requirePerson the
72// bool is always true; it is returned anyway so a handler mounted
73// outside that group cannot read a zero Person and mistake it for
74// somebody.
75func CurrentPerson(r *http.Request) (Person, bool) {
76 p, ok := r.Context().Value(personCtxKey{}).(Person)
77 return p, ok
78}
79