package admin import ( "context" "log/slog" "net/http" "strings" ) // requireOwner middleware ensures the request is from the hold owner func (ui *AdminUI) requireOwner(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Get session cookie token, ok := getSessionCookie(r) if !ok { http.Redirect(w, r, "/admin/auth/login", http.StatusFound) return } // Validate session session := ui.getSession(token) if session == nil { clearSessionCookie(w) http.Redirect(w, r, "/admin/auth/login", http.StatusFound) return } // Double-check DID still matches captain.Owner _, captain, err := ui.pds.GetCaptainRecord(r.Context()) if err != nil { slog.Error("Failed to get captain record for admin auth", "error", err) http.Error(w, "Failed to verify ownership", http.StatusInternalServerError) return } if session.DID != captain.Owner { slog.Warn("Admin session DID doesn't match captain owner", "sessionDID", session.DID, "captainOwner", captain.Owner) ui.deleteSession(token) clearSessionCookie(w) http.Error(w, "Access denied: ownership verification failed", http.StatusForbidden) return } // Add session to context for handlers ctx := context.WithValue(r.Context(), adminContextKey{}, session) next.ServeHTTP(w, r.WithContext(ctx)) }) } // getSessionFromContext retrieves the admin session from context func getSessionFromContext(ctx context.Context) *AdminSession { session, ok := ctx.Value(adminContextKey{}).(*AdminSession) if !ok { return nil } return session } // PageData contains common data for all admin pages type PageData struct { Title string ActivePage string User *AdminSession HoldDID string Flash *Flash } // Flash represents a flash message type Flash struct { Category string // "success", "error", "warning", "info" Message string } // newPageData creates PageData with common values func (ui *AdminUI) newPageData(r *http.Request, title, activePage string) PageData { session := getSessionFromContext(r.Context()) flash := getFlash(r, ui) return PageData{ Title: title, ActivePage: activePage, User: session, HoldDID: ui.pds.DID(), Flash: flash, } } // renderTemplate renders a template with the given data. // Layout pages (pages/* except login and error) are rendered via "admin-layout" // which uses {{block}} overrides. Partials and standalone pages execute directly. func (ui *AdminUI) renderTemplate(w http.ResponseWriter, name string, data any) { tmpl, ok := ui.templates[name] if !ok { slog.Error("Template not found", "template", name) http.Error(w, "Internal server error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") // Pages that use the shared layout are rendered via the layout entry point. // login.html and error.html are standalone (different layout). execName := name if strings.HasPrefix(name, "pages/") && name != "pages/login.html" && name != "pages/error.html" { execName = "admin-layout" } if err := tmpl.ExecuteTemplate(w, execName, data); err != nil { slog.Error("Failed to render template", "template", name, "exec", execName, "error", err) http.Error(w, "Internal server error", http.StatusInternalServerError) } }