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:
@@ -0,0 +1,13 @@
|
||||
.PHONY: build run dev sqlc
|
||||
|
||||
build:
|
||||
go build -o bbq .
|
||||
|
||||
run: build
|
||||
./bbq
|
||||
|
||||
dev: build
|
||||
BBQ_FEATURES=auth ./bbq
|
||||
|
||||
sqlc:
|
||||
$(shell go env GOPATH)/bin/sqlc generate
|
||||
@@ -109,7 +109,7 @@ func (s *Server) handleLoginSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
code := generateCode()
|
||||
expiresAt := time.Now().Add(10 * time.Minute)
|
||||
expiresAt := time.Now().UTC().Add(10 * time.Minute)
|
||||
s.q.CreateVerificationCode(r.Context(), db.CreateVerificationCodeParams{
|
||||
Identifier: identifier,
|
||||
Code: code,
|
||||
@@ -177,7 +177,7 @@ func (s *Server) handleVerifyCode(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Create session
|
||||
token := generateSessionToken()
|
||||
expiresAt := time.Now().Add(30 * 24 * time.Hour) // 30 days
|
||||
expiresAt := time.Now().UTC().Add(30 * 24 * time.Hour) // 30 days
|
||||
s.q.CreateSession(r.Context(), db.CreateSessionParams{
|
||||
Token: token,
|
||||
UserID: user.ID,
|
||||
|
||||
+39
-26
@@ -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
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<meta name="twitter:image" content="{{.BaseURL}}/e/{{.Event.Slug}}/og.png">
|
||||
{{end}}
|
||||
|
||||
{{define "admin-bar"}}{{if .IsAdmin}}<div class="admin-bar">ADMIN VIEW — share the guest link: /e/{{.Event.Slug}}</div>{{end}}{{end}}
|
||||
{{define "admin-bar"}}{{if .IsAdmin}}<div class="admin-bar">ADMIN VIEW — <button onclick="navigator.clipboard.writeText(window.location.origin+'/e/{{.Event.Slug}}').then(()=>{this.textContent='Copied!';setTimeout(()=>{this.textContent='Copy guest link'},1500)})" style="background:none;border:none;font-family:inherit;font-size:inherit;cursor:pointer;text-decoration:underline;color:inherit;">Copy guest link</button></div>{{end}}{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="event-header">
|
||||
|
||||
Reference in New Issue
Block a user