| 1 | package docs |
| 2 | |
| 3 | import ( |
| 4 | "database/sql" |
| 5 | "encoding/json" |
| 6 | "log/slog" |
| 7 | "net/http" |
| 8 | "strings" |
| 9 | "time" |
| 10 | |
| 11 | "amadan.net/rastrillo/rastrillo" |
| 12 | |
| 13 | // The one import left on rastrillo's pre-move path, on purpose: |
| 14 | // rastrillo/native's server (2a5d07b, and its main) still takes a |
| 15 | // github.com/carlosframework/rastrillo *sessions.Sessions. Both |
| 16 | // sessions packages read and write the same sessions table, so a |
| 17 | // session native sign-in mints is an ordinary session to the app |
| 18 | // (native_exchange_test.go). Move this to |
| 19 | // amadan.net/rastrillo/rastrillo/sessions when native/server moves; |
| 20 | // TestNoPreMoveRastrillo forbids every other import of the old path. |
| 21 | presessions "github.com/carlosframework/rastrillo/sessions" |
| 22 | ) |
| 23 | |
| 24 | // newNativeSessions is the sessions handle rastrillo/native's server |
| 25 | // approves and mints with, over the app's own database. |
| 26 | func newNativeSessions(db *sql.DB, origin string, logger *slog.Logger) (*presessions.Sessions, error) { |
| 27 | return presessions.New(presessions.Config{DB: db, Origin: origin, SigninPath: "/signin", Logger: logger}) |
| 28 | } |
| 29 | |
| 30 | func wantsNative(r *http.Request) bool { |
| 31 | return strings.Contains(r.Header.Get("Accept"), "application/json") |
| 32 | } |
| 33 | |
| 34 | func nativeJSON(w http.ResponseWriter, r *http.Request, status int, value any) { |
| 35 | body, err := json.Marshal(value) |
| 36 | if err != nil { |
| 37 | http.Error(w, rastrillo.T(r, "docs.error.generic"), http.StatusInternalServerError) |
| 38 | return |
| 39 | } |
| 40 | w.Header().Set("Content-Type", "application/json") |
| 41 | w.Header().Set("Cache-Control", "no-store") |
| 42 | w.Header().Add("Vary", "Accept") |
| 43 | _ = writeGuardedBody(w, r, status, body) |
| 44 | } |
| 45 | |
| 46 | func (h *handlers) nativeList(w http.ResponseWriter, r *http.Request) { |
| 47 | actor, err := h.actor(r) |
| 48 | if err != nil { |
| 49 | http.Error(w, rastrillo.T(r, "docs.error.generic"), http.StatusUnauthorized) |
| 50 | return |
| 51 | } |
| 52 | var documents []*Document |
| 53 | if query := strings.TrimSpace(r.URL.Query().Get("q")); query != "" { |
| 54 | documents, err = h.store.SearchFor(r.Context(), actor, query) |
| 55 | } else { |
| 56 | documents, err = h.store.ListFor(r.Context(), actor) |
| 57 | } |
| 58 | if err != nil { |
| 59 | http.Error(w, rastrillo.T(r, "docs.error.generic"), h.internalFailureStatus()) |
| 60 | return |
| 61 | } |
| 62 | type item struct { |
| 63 | Sqid string `json:"sqid"` |
| 64 | Title string `json:"title"` |
| 65 | UpdatedAt time.Time `json:"updated_at"` |
| 66 | Shared bool `json:"shared"` |
| 67 | Thumbnail thumbView `json:"thumbnail"` |
| 68 | } |
| 69 | items := make([]item, 0, len(documents)) |
| 70 | thumbnail := h.thumbnailer(r.Context()) |
| 71 | for _, doc := range documents { |
| 72 | items = append(items, item{doc.Sqid, doc.Title, doc.UpdatedAt, doc.OwnerID != actor.PersonID, thumbnail(doc)}) |
| 73 | } |
| 74 | nativeJSON(w, r, 200, map[string]any{"documents": items, "can_create": h.member(r)}) |
| 75 | } |
| 76 | |
| 77 | // The authentication middleware is rerun against the cookie before every |
| 78 | // stream write. A context captured when the stream opened is not current proof. |
| 79 | func (h *handlers) nativeStreamAllowed(r *http.Request, sqid string) bool { |
| 80 | if h.requireCurrent == nil { |
| 81 | return true |
| 82 | } // isolated handler fixtures |
| 83 | allowed := false |
| 84 | h.requireCurrent(http.HandlerFunc(func(_ http.ResponseWriter, fresh *http.Request) { |
| 85 | actor, err := h.actor(fresh) |
| 86 | if err != nil { |
| 87 | return |
| 88 | } |
| 89 | _, err = h.store.GetFor(fresh.Context(), actor, sqid) |
| 90 | allowed = err == nil |
| 91 | })).ServeHTTP(nativeDiscard{}, r) |
| 92 | return allowed |
| 93 | } |
| 94 | |
| 95 | type nativeDiscard struct{} |
| 96 | |
| 97 | func (nativeDiscard) Header() http.Header { return make(http.Header) } |
| 98 | func (nativeDiscard) Write(p []byte) (int, error) { return len(p), nil } |
| 99 | func (nativeDiscard) WriteHeader(int) {} |
| 100 | |