areweplaying/cmd/web/routes.go
Harry Culpan 01d2cae282 Send GM status update email on every attendance change
When a player's attendance is updated — either via email (awp-cli check)
or via the web toggle — an email is sent to the GM with:
  - who changed, what they changed to, and how (email or the site)
  - a summary count of attending/not attending/no response
  - the full current player status list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 19:07:51 -04:00

179 lines
4.6 KiB
Go

package main
import (
"context"
"fmt"
"log"
"net/http"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/golang-jwt/jwt/v4"
"github.com/hculpan/areweplaying/cmd/web/templates"
"github.com/hculpan/areweplaying/pkg/data"
"github.com/hculpan/areweplaying/pkg/email"
)
func routes(r *chi.Mux) {
r.Post("/save-setup", func(w http.ResponseWriter, r *http.Request) {
saveSetupHandler(w, r)
})
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
comp := templates.MainPage()
comp.Render(context.Background(), w)
})
r.Post("/setup", func(w http.ResponseWriter, r *http.Request) {
setupHandler(w, r)
})
r.Get("/setup", func(w http.ResponseWriter, r *http.Request) {
setupHandler(w, r)
})
r.Post("/send-email", func(w http.ResponseWriter, r *http.Request) {
sendEmailHandler(w, r)
})
r.Get("/login", func(w http.ResponseWriter, r *http.Request) {
if ValidateJWTFromCookie(r, appKey) {
http.Redirect(w, r, "/setup", http.StatusSeeOther)
return
}
comp := templates.Login()
comp.Render(context.Background(), w)
})
r.Get("/send-reminder", func(w http.ResponseWriter, r *http.Request) {
sendReminderHandler(w, r)
})
r.Post("/admin/players/add", func(w http.ResponseWriter, r *http.Request) {
addPlayerHandler(w, r)
})
r.Post("/admin/players/delete", func(w http.ResponseWriter, r *http.Request) {
deletePlayerHandler(w, r)
})
r.Post("/admin/players/edit", func(w http.ResponseWriter, r *http.Request) {
editPlayerHandler(w, r)
})
r.Get("/sessioninfo", func(w http.ResponseWriter, r *http.Request) {
session, dataErr := data.ReadSessionData()
if dataErr != nil {
http.Error(w, dataErr.Error(), http.StatusInternalServerError)
return
}
playerName := r.URL.Query().Get("playerName")
playerAttending := r.URL.Query().Get("playerAttending")
if len(playerName) > 0 && len(playerAttending) > 0 {
if err := updatePlayerAttending(playerName, playerAttending, session); err != nil {
log.Default().Println(err.Error())
}
}
comp := templates.PageBody(session)
comp.Render(context.Background(), w)
})
}
func ValidateJWTFromCookie(r *http.Request, key []byte) bool {
// Retrieve the JWT token from the cookie
cookie, err := r.Cookie("jwt_token")
if err != nil {
// Cookie not found
return false
}
tokenString := cookie.Value
// Parse the token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Validate the algorithm used for the token
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, nil
}
return key, nil
})
if err != nil {
return false
}
// Check if token is valid
if _, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
// Here you can also add more checks on claims if needed
// For example, check the issuer, subject, or expiration
return true
}
return false
}
func GenerateJWTAndStoreInCookie(w http.ResponseWriter, key []byte) error {
// Create a new token object, specifying signing method and the claims you would like it to contain.
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"exp": time.Now().Add(90 * 24 * time.Hour).Unix(), // 90 days expiration
})
// Sign and get the complete encoded token as a string using the secret
tokenString, err := token.SignedString(key)
if err != nil {
return err
}
// Create a cookie
cookie := http.Cookie{
Name: "jwt_token",
Value: tokenString,
Expires: time.Now().Add(90 * 24 * time.Hour),
HttpOnly: true, // HttpOnly if you want to prevent access to the cookie from JavaScript
}
// Set the cookie
http.SetCookie(w, &cookie)
return nil
}
func updatePlayerAttending(name, attending string, session data.Session) error {
for i, p := range session.Players {
if p.Name == name {
var newAttending string
switch strings.ToLower(attending) {
case "yes", "no":
newAttending = strings.ToLower(attending)
case "toggle":
switch session.Players[i].Attending {
case "unknown", "no":
newAttending = "yes"
default:
newAttending = "no"
}
default:
return fmt.Errorf("unrecognized attending indicator %q", attending)
}
session.Players[i].Attending = newAttending
if err := data.PersistSession(session); err != nil {
return err
}
if gmEmail := session.GMEmail(); gmEmail != "" {
subject, body := session.FormatStatusUpdate(name, newAttending, "the site")
e := email.NewEmail([]string{gmEmail}, []string{}, gmEmail, subject, body)
if err := email.SendEmail(e); err != nil {
log.Default().Printf("ERROR sending GM notification: %s", err)
}
}
return nil
}
}
return fmt.Errorf("player %q not found", name)
}