diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..14f5264 --- /dev/null +++ b/Makefile @@ -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 diff --git a/auth.go b/auth.go index a06c99c..41712d9 100644 --- a/auth.go +++ b/auth.go @@ -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, diff --git a/handlers.go b/handlers.go index 000830c..d74f1ca 100644 --- a/handlers.go +++ b/handlers.go @@ -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 diff --git a/templates/event.html b/templates/event.html index 2b6c795..03aa757 100644 --- a/templates/event.html +++ b/templates/event.html @@ -15,7 +15,7 @@ {{end}} -{{define "admin-bar"}}{{if .IsAdmin}}
{{end}}{{end}} +{{define "admin-bar"}}{{if .IsAdmin}}{{end}}{{end}} {{define "content"}}