Gate admin routes behind auth and add copy guest link button

When auth is enabled, admin pages require the logged-in user to be
the event owner — unauthorized visitors get redirected to the guest
view, and admin actions return 403. Also adds a copy-to-clipboard
button in the admin bar and a Makefile for common commands.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 09:06:16 -04:00
parent a0c4b28d1e
commit d68a6629ac
4 changed files with 55 additions and 29 deletions
+39 -26
View File
@@ -454,17 +454,42 @@ func (s *Server) handleSSE(w http.ResponseWriter, r *http.Request) {
// --- Admin ---
func (s *Server) handleAdmin(w http.ResponseWriter, r *http.Request) {
// authorizeAdmin checks the admin token and (when auth is enabled) that the
// logged-in user owns the event. Returns the event on success, nil on failure.
// For page loads (isPage=true) it redirects to the guest view; for actions it
// returns 403.
func (s *Server) authorizeAdmin(w http.ResponseWriter, r *http.Request, isPage bool) *db.Event {
slug := chi.URLParam(r, "slug")
token := chi.URLParam(r, "token")
event, err := s.q.GetEventBySlug(r.Context(), slug)
if err != nil || event.AdminToken != token {
http.NotFound(w, r)
return nil
}
if s.features.Auth {
user := s.currentUser(r)
if user == nil || !event.UserID.Valid || user.ID != event.UserID.Int64 {
if isPage {
http.Redirect(w, r, "/e/"+slug, http.StatusSeeOther)
} else {
http.Error(w, "Forbidden", http.StatusForbidden)
}
return nil
}
}
return &event
}
func (s *Server) handleAdmin(w http.ResponseWriter, r *http.Request) {
event := s.authorizeAdmin(w, r, true)
if event == nil {
return
}
data, err := s.loadEventPage(r, slug, true)
data, err := s.loadEventPage(r, event.Slug, true)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
@@ -473,12 +498,8 @@ func (s *Server) handleAdmin(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleUpdateDescription(w http.ResponseWriter, r *http.Request) {
slug := chi.URLParam(r, "slug")
token := chi.URLParam(r, "token")
event, err := s.q.GetEventBySlug(r.Context(), slug)
if err != nil || event.AdminToken != token {
http.NotFound(w, r)
event := s.authorizeAdmin(w, r, false)
if event == nil {
return
}
@@ -491,16 +512,12 @@ func (s *Server) handleUpdateDescription(w http.ResponseWriter, r *http.Request)
ID: event.ID,
})
http.Redirect(w, r, fmt.Sprintf("/e/%s/admin/%s", slug, token), http.StatusSeeOther)
http.Redirect(w, r, fmt.Sprintf("/e/%s/admin/%s", event.Slug, event.AdminToken), http.StatusSeeOther)
}
func (s *Server) handleCreateSlot(w http.ResponseWriter, r *http.Request) {
slug := chi.URLParam(r, "slug")
token := chi.URLParam(r, "token")
event, err := s.q.GetEventBySlug(r.Context(), slug)
if err != nil || event.AdminToken != token {
http.NotFound(w, r)
event := s.authorizeAdmin(w, r, false)
if event == nil {
return
}
@@ -527,7 +544,7 @@ func (s *Server) handleCreateSlot(w http.ResponseWriter, r *http.Request) {
return
}
_, err = s.q.CreateSlot(r.Context(), db.CreateSlotParams{
_, err := s.q.CreateSlot(r.Context(), db.CreateSlotParams{
EventID: event.ID, Name: name, Emoji: emoji, MaxClaims: mc, SortOrder: 999,
})
if err != nil {
@@ -535,9 +552,9 @@ func (s *Server) handleCreateSlot(w http.ResponseWriter, r *http.Request) {
return
}
s.notify(slug)
s.notify(event.Slug)
data, err := s.loadEventPage(r, slug, true)
data, err := s.loadEventPage(r, event.Slug, true)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
@@ -546,12 +563,8 @@ func (s *Server) handleCreateSlot(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleDeleteSlot(w http.ResponseWriter, r *http.Request) {
slug := chi.URLParam(r, "slug")
token := chi.URLParam(r, "token")
event, err := s.q.GetEventBySlug(r.Context(), slug)
if err != nil || event.AdminToken != token {
http.NotFound(w, r)
event := s.authorizeAdmin(w, r, false)
if event == nil {
return
}
@@ -562,9 +575,9 @@ func (s *Server) handleDeleteSlot(w http.ResponseWriter, r *http.Request) {
}
s.q.DeleteSlot(r.Context(), slotID)
s.notify(slug)
s.notify(event.Slug)
data, err := s.loadEventPage(r, slug, true)
data, err := s.loadEventPage(r, event.Slug, true)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return