Add attendee cap with required login for capped events
Hosts can optionally cap total attendance (RSVPs + plus-ones) when creating an event. Capped events require guests to log in via the existing email code flow; RSVPs are tied to accounts and deduped per user. The event page shows spots left and replaces the form with a full notice when the cap is reached; the server rejects over-cap RSVPs and edits with 409 as a race backstop. Also threads a safeNext-validated ?next= param through the login and name-setup flow so guests land back on the event after signing in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"log"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -63,39 +64,62 @@ func (s *Server) sessionMiddleware(next http.Handler) http.Handler {
|
||||
func (s *Server) requireAuth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if s.currentUser(r) == nil {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
dest := "/login"
|
||||
if r.Method == http.MethodGet {
|
||||
dest += "?next=" + url.QueryEscape(r.URL.RequestURI())
|
||||
}
|
||||
http.Redirect(w, r, dest, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// safeNext returns next if it is a safe local path, else "".
|
||||
func safeNext(next string) string {
|
||||
if strings.HasPrefix(next, "/") && !strings.HasPrefix(next, "//") && !strings.ContainsAny(next, "\\\r\n") {
|
||||
return next
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// isEmail returns true if the input looks like an email address.
|
||||
func isEmail(s string) bool {
|
||||
return strings.Contains(s, "@")
|
||||
}
|
||||
|
||||
func (s *Server) handleLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||
next := safeNext(r.URL.Query().Get("next"))
|
||||
if s.currentUser(r) != nil {
|
||||
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
||||
dest := "/dashboard"
|
||||
if next != "" {
|
||||
dest = next
|
||||
}
|
||||
http.Redirect(w, r, dest, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
pageTmpl["login"].ExecuteTemplate(w, "layout", map[string]any{
|
||||
"Step": "identify",
|
||||
"Next": next,
|
||||
"AuthEnabled": true,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleLoginSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
next := safeNext(r.FormValue("next"))
|
||||
loginURL := "/login"
|
||||
if next != "" {
|
||||
loginURL += "?next=" + url.QueryEscape(next)
|
||||
}
|
||||
raw := strings.TrimSpace(r.FormValue("identifier"))
|
||||
if raw == "" {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
http.Redirect(w, r, loginURL, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
identifier := strings.ToLower(raw)
|
||||
if !isEmail(identifier) {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
http.Redirect(w, r, loginURL, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -118,11 +142,13 @@ func (s *Server) handleLoginSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
pageTmpl["login"].ExecuteTemplate(w, "layout", map[string]any{
|
||||
"Step": "code",
|
||||
"Identifier": identifier,
|
||||
"Next": next,
|
||||
"AuthEnabled": true,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleVerifyCode(w http.ResponseWriter, r *http.Request) {
|
||||
next := safeNext(r.FormValue("next"))
|
||||
identifier := strings.TrimSpace(r.FormValue("identifier"))
|
||||
code := strings.TrimSpace(r.FormValue("code"))
|
||||
|
||||
@@ -134,6 +160,7 @@ func (s *Server) handleVerifyCode(w http.ResponseWriter, r *http.Request) {
|
||||
pageTmpl["login"].ExecuteTemplate(w, "layout", map[string]any{
|
||||
"Step": "code",
|
||||
"Identifier": identifier,
|
||||
"Next": next,
|
||||
"Error": "Invalid or expired code. Try again.",
|
||||
"AuthEnabled": true,
|
||||
})
|
||||
@@ -174,10 +201,18 @@ func (s *Server) handleVerifyCode(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// If user has no name, send them to set it
|
||||
if user.Name == "" {
|
||||
http.Redirect(w, r, "/account/name", http.StatusSeeOther)
|
||||
dest := "/account/name"
|
||||
if next != "" {
|
||||
dest += "?next=" + url.QueryEscape(next)
|
||||
}
|
||||
http.Redirect(w, r, dest, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if next != "" {
|
||||
http.Redirect(w, r, next, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -215,21 +250,31 @@ func (s *Server) handleNamePage(w http.ResponseWriter, r *http.Request) {
|
||||
user := s.currentUser(r)
|
||||
pageTmpl["name"].ExecuteTemplate(w, "layout", map[string]any{
|
||||
"User": user,
|
||||
"Next": safeNext(r.URL.Query().Get("next")),
|
||||
"AuthEnabled": true,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleNameSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
user := s.currentUser(r)
|
||||
next := safeNext(r.FormValue("next"))
|
||||
name := strings.TrimSpace(r.FormValue("name"))
|
||||
if name == "" {
|
||||
http.Redirect(w, r, "/account/name", http.StatusSeeOther)
|
||||
dest := "/account/name"
|
||||
if next != "" {
|
||||
dest += "?next=" + url.QueryEscape(next)
|
||||
}
|
||||
http.Redirect(w, r, dest, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
s.q.UpdateUserName(r.Context(), db.UpdateUserNameParams{
|
||||
Name: name,
|
||||
ID: user.ID,
|
||||
})
|
||||
if next != "" {
|
||||
http.Redirect(w, r, next, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user