// The platform-admin drill-down: merchant -> shop -> camera. // // These exist because the tenant routes cannot serve this screen, and the // reason is structural rather than incidental. Every tenant handler derives // the client from the SESSION - that is what makes cross-tenant access // impossible rather than merely disallowed - and a platform admin has no // client at all. The three workarounds all make it worse: passing a company id // to a tenant route puts a caller-chosen tenant back into the one place this // system refuses to take one, filtering the whole estate in the browser ships // every merchant's data to render one, and signing in as the owner leaves an // audit trail naming the wrong person. // // So the tenant STORE functions are reused with an explicit client id and the // scoping the tenant handlers get from the session is done here instead. package api import "net/http" // clientForAdmin resolves {id} to a merchant that exists. // // A suspended merchant still resolves: "this company is suspended" is // precisely what an admin opens the console to look at, and hiding it would // make the one screen that can fix it the one screen that cannot see it. func (s *Server) clientForAdmin(w http.ResponseWriter, r *http.Request) (ClientDetail, bool) { c, err := s.Store.ClientDetail(r.Context(), r.PathValue("id")) if err != nil { s.serverError(w, "admin client", err) return ClientDetail{}, false } if c.ID == "" { writeErr(w, http.StatusNotFound, "not_found", "No such merchant.") return ClientDetail{}, false } return c, true } // siteForAdmin resolves {site} within that merchant. A site belonging to // somebody else is 404 and not an empty list: the caller asked for a named // thing, and "here are its zero cameras" is a different and wrong answer. func (s *Server) siteForAdmin(w http.ResponseWriter, r *http.Request, clientID string) (string, bool) { id, err := s.Store.AdminSiteID(r.Context(), clientID, r.PathValue("site")) if err != nil { s.serverError(w, "admin site", err) return "", false } if id == "" { writeErr(w, http.StatusNotFound, "not_found", "No such shop for this merchant.") return "", false } return id, true } func (s *Server) handleAdminClient(w http.ResponseWriter, r *http.Request) { c, ok := s.clientForAdmin(w, r) if !ok { return } writeJSON(w, http.StatusOK, c) } func (s *Server) handleAdminClientSites(w http.ResponseWriter, r *http.Request) { c, ok := s.clientForAdmin(w, r) if !ok { return } rows, err := s.Store.SiteHealth(r.Context(), c.ID) if err != nil { s.serverError(w, "admin sites", err) return } if rows == nil { rows = []SiteHealth{} } s.auditAdminRead(r, c.ID, "admin.sites.read", "client", c.ID, len(rows)) writeJSON(w, http.StatusOK, rows) } func (s *Server) handleAdminClientSite(w http.ResponseWriter, r *http.Request) { c, ok := s.clientForAdmin(w, r) if !ok { return } siteID, ok := s.siteForAdmin(w, r, c.ID) if !ok { return } // SiteHealth is the one place that knows what "online" means (three missed // heartbeats, not one) and what cameras_up counts. A second query here // would be a second definition of a working shop, and the two would drift. rows, err := s.Store.SiteHealth(r.Context(), c.ID) if err != nil { s.serverError(w, "admin site", err) return } for _, row := range rows { if row.SiteID == siteID { s.auditAdminRead(r, c.ID, "admin.site.read", "site", siteID, 1) writeJSON(w, http.StatusOK, row) return } } writeErr(w, http.StatusNotFound, "not_found", "No such shop for this merchant.") } func (s *Server) handleAdminSiteCameras(w http.ResponseWriter, r *http.Request) { c, ok := s.clientForAdmin(w, r) if !ok { return } siteID, ok := s.siteForAdmin(w, r, c.ID) if !ok { return } rows, err := s.Store.Cameras(r.Context(), c.ID, siteID) if err != nil { s.serverError(w, "admin cameras", err) return } s.auditAdminRead(r, c.ID, "admin.cameras.read", "site", siteID, len(rows)) writeJSON(w, http.StatusOK, AdminCameras(rows)) } func (s *Server) handleAdminSiteCamera(w http.ResponseWriter, r *http.Request) { c, ok := s.clientForAdmin(w, r) if !ok { return } siteID, ok := s.siteForAdmin(w, r, c.ID) if !ok { return } camID, err := s.Store.AdminCameraID(r.Context(), c.ID, siteID, r.PathValue("camera")) if err != nil { s.serverError(w, "admin camera", err) return } if camID == "" { writeErr(w, http.StatusNotFound, "not_found", "No such camera for this shop.") return } rows, err := s.Store.Cameras(r.Context(), c.ID, siteID) if err != nil { s.serverError(w, "admin camera", err) return } for _, row := range rows { if row.ID == camID { s.auditAdminRead(r, c.ID, "admin.camera.read", "camera", camID, 1) writeJSON(w, http.StatusOK, adminCamera(row)) return } } writeErr(w, http.StatusNotFound, "not_found", "No such camera for this shop.") } func (s *Server) handleAdminMonitoringSummary(w http.ResponseWriter, r *http.Request) { out, err := s.Store.PlatformSummary(r.Context()) if err != nil { s.serverError(w, "platform summary", err) return } // No audit row: this is counts across the platform, naming no merchant and // no person. Logging a header strip that a console refreshes on a timer // would bury the reads that are actually worth finding. writeJSON(w, http.StatusOK, out) } // auditAdminRead records a platform admin reading inside one merchant. // // Below the merchant list, every read is somebody outside a company looking at // that company's estate. "Who looked at my shops" has to be answerable for the // same reason it does for face images, and an admin is exactly the account for // which nothing else in the system would leave a trace. func (s *Server) auditAdminRead(r *http.Request, clientID, action, entity, entityID string, n int) { p := PrincipalFrom(r.Context()) s.Store.Audit(r.Context(), AuditEntry{ ClientID: clientID, ActorID: p.UserID, ActorKind: "admin", Action: action, Entity: entity, EntityID: entityID, Detail: map[string]any{"path": r.URL.Path, "rows": n}, }) }