Turnpike/handle_signup.go

287 lines
7.9 KiB
Go

package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
)
func (app *App) handlePublicSignupConfig(w http.ResponseWriter, r *http.Request) {
var noteLabel, noteRequired string
app.db.QueryRow(`SELECT value FROM config WHERE key = 'volunteer_note_label'`).Scan(&noteLabel)
app.db.QueryRow(`SELECT value FROM config WHERE key = 'volunteer_note_required'`).Scan(&noteRequired)
if noteLabel == "" {
noteLabel = "Additional note"
}
depts, _ := app.listDepartments("")
deptList := []map[string]any{}
for _, d := range depts {
deptList = append(deptList, map[string]any{"id": d.ID, "name": d.Name, "color": d.Color})
}
eventName := app.eventName()
writeJSON(w, map[string]any{
"event_name": eventName,
"departments": deptList,
"volunteer_note_label": noteLabel,
"volunteer_note_required": noteRequired == "true",
})
}
func (app *App) handlePublicSignup(w http.ResponseWriter, r *http.Request) {
var body struct {
PreferredName string `json:"preferred_name"`
TicketName string `json:"ticket_name"`
Email string `json:"email"`
Pronouns string `json:"pronouns"`
Phone string `json:"phone"`
DepartmentID *int `json:"department_id"`
Note string `json:"note"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, "invalid request", http.StatusBadRequest)
return
}
body.PreferredName = strings.TrimSpace(body.PreferredName)
body.Email = strings.TrimSpace(body.Email)
if body.PreferredName == "" || body.Email == "" {
writeError(w, "preferred name and email are required", http.StatusBadRequest)
return
}
var noteRequired string
app.db.QueryRow(`SELECT value FROM config WHERE key = 'volunteer_note_required'`).Scan(&noteRequired)
if noteRequired == "true" && strings.TrimSpace(body.Note) == "" {
writeError(w, "note field is required", http.StatusBadRequest)
return
}
// Don't reveal whether email is already registered
existing, _ := app.getVolunteerByEmail(body.Email)
if existing != nil {
writeJSON(w, map[string]any{"ok": true})
return
}
// Find or create participant by email.
participant, _, err := app.upsertParticipant(body.Email, body.PreferredName)
if err != nil {
writeError(w, "internal error", http.StatusInternalServerError)
return
}
// Update participant's personal details if they signed up with more info.
if body.Phone != "" || body.Pronouns != "" {
app.db.Exec(`UPDATE participants SET
phone = CASE WHEN phone = '' THEN ? ELSE phone END,
pronouns = CASE WHEN pronouns = '' THEN ? ELSE pronouns END,
updated_at = ?
WHERE id = ?`, body.Phone, body.Pronouns, now(), participant.ID)
}
confirmToken, err := generateConfirmationToken()
if err != nil {
writeError(w, "internal error", http.StatusInternalServerError)
return
}
vol := Volunteer{
ParticipantID: &participant.ID,
Name: body.PreferredName,
PreferredName: body.PreferredName,
TicketName: body.TicketName,
Email: body.Email,
Phone: body.Phone,
Pronouns: body.Pronouns,
DepartmentID: body.DepartmentID,
Note: body.Note,
ConfirmationToken: &confirmToken,
}
if _, err := app.createVolunteer(vol); err != nil {
writeError(w, "internal error", http.StatusInternalServerError)
return
}
go func() {
if err := app.sendConfirmationEmail(body.Email, body.PreferredName, confirmToken); err != nil {
log.Printf("confirmation email to %s failed: %v", body.Email, err)
}
}()
writeJSON(w, map[string]any{"ok": true})
}
func (app *App) handleConfirmEmail(w http.ResponseWriter, r *http.Request) {
var body struct {
Token string `json:"token"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Token == "" {
writeError(w, "invalid request", http.StatusBadRequest)
return
}
vol, err := app.getVolunteerByConfirmationToken(body.Token)
if err != nil || vol == nil {
writeJSON(w, map[string]any{"status": "invalid"})
return
}
if vol.EmailConfirmed {
writeJSON(w, map[string]any{"status": "already_confirmed"})
return
}
if err := app.confirmVolunteerEmail(vol.ID); err != nil {
writeError(w, "internal error", http.StatusInternalServerError)
return
}
response := map[string]any{"status": "confirmed"}
var signupsOpen string
app.db.QueryRow(`SELECT value FROM config WHERE key = 'shift_signups_open'`).Scan(&signupsOpen)
if signupsOpen == "true" && vol.ParticipantID != nil {
// Find a ticket with a code, or create/assign one.
tickets, _ := app.listTickets(vol.ParticipantID, "")
var code *string
for _, tk := range tickets {
if tk.Code != nil {
code = tk.Code
break
}
}
if code == nil {
// No coded ticket — find any ticket or create a stub, then generate code.
var ticketID int
if len(tickets) > 0 {
ticketID = tickets[0].ID
} else {
tkName := vol.TicketName
if tkName == "" {
tkName = vol.PreferredName
}
stub, err := app.createTicket(Ticket{
ParticipantID: vol.ParticipantID,
Name: tkName,
Source: "manual",
})
if err == nil {
ticketID = stub.ID
}
}
if ticketID > 0 {
if t, err := app.generateUniqueToken(); err == nil {
app.db.Exec(`UPDATE tickets SET code=?, updated_at=? WHERE id=?`, t, now(), ticketID)
code = &t
}
}
}
if code != nil {
kioskLink := fmt.Sprintf("%s/v/%s", app.resolveBaseURL(), *code)
response["kiosk_link"] = kioskLink
go func() {
if err := app.sendShiftSignupEmail(vol.Email, vol.PreferredName, kioskLink); err != nil {
log.Printf("shift signup email to %s failed: %v", vol.Email, err)
}
}()
}
}
writeJSON(w, response)
}
func (app *App) handleToggleShiftSignups(w http.ResponseWriter, r *http.Request) {
var body struct {
Open bool `json:"open"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, "invalid request", http.StatusBadRequest)
return
}
val := "false"
if body.Open {
val = "true"
}
app.db.Exec(`INSERT OR REPLACE INTO config (key, value) VALUES ('shift_signups_open', ?)`, val)
if body.Open {
go app.openShiftSignups()
}
writeJSON(w, map[string]any{"shift_signups_open": body.Open})
}
func (app *App) openShiftSignups() {
// Generate codes for tickets belonging to confirmed volunteers that have no code yet.
vols, _ := app.listConfirmedVolunteersNeedingCode()
for _, v := range vols {
if v.ParticipantID == nil {
continue
}
// Find any ticket for this participant, or create a stub one.
tickets, _ := app.listTickets(v.ParticipantID, "")
var ticketID int
if len(tickets) > 0 {
ticketID = tickets[0].ID
} else {
tkName := v.TicketName
if tkName == "" {
tkName = v.PreferredName
}
stub, err := app.createTicket(Ticket{
ParticipantID: v.ParticipantID,
Name: tkName,
Source: "manual",
})
if err != nil {
continue
}
ticketID = stub.ID
}
t, err := app.generateUniqueToken()
if err != nil {
continue
}
app.db.Exec(`UPDATE tickets SET code=?, updated_at=? WHERE id=?`, t, now(), ticketID)
}
// Email all confirmed volunteers that now have a ticket with a code.
confirmed, _ := queryVolunteers(app.db, `
SELECT `+volunteerSelect+` `+volunteerFrom+`
WHERE v.email_confirmed = 1 AND v.deleted_at IS NULL AND v.participant_id IS NOT NULL`)
baseURL := app.resolveBaseURL()
sent := 0
for _, v := range confirmed {
if v.ParticipantID == nil || v.Email == "" {
continue
}
tickets, _ := app.listTickets(v.ParticipantID, "")
var code *string
for _, tk := range tickets {
if tk.Code != nil {
code = tk.Code
break
}
}
if code == nil {
continue
}
kioskLink := fmt.Sprintf("%s/v/%s", baseURL, *code)
name := v.PreferredName
if name == "" {
name = v.Name
}
if err := app.sendShiftSignupEmail(v.Email, name, kioskLink); err == nil {
sent++
} else {
log.Printf("shift signup email to %s failed: %v", v.Email, err)
}
}
log.Printf("Shift signups opened: sent %d emails", sent)
}