Turnpike/handle_signup.go

221 lines
6.4 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" {
code, err := app.generateVolunteerKioskCode()
if err == nil {
if err := app.assignKioskCode(vol.ID, code); err == 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() {
// Assign kiosk codes to email-confirmed volunteers that don't have one yet.
vols, _ := app.listVolunteersNeedingKioskCode()
for _, v := range vols {
code, err := app.generateVolunteerKioskCode()
if err != nil {
continue
}
app.assignKioskCode(v.ID, code)
}
// Email all email-confirmed volunteers that now have a kiosk code.
confirmed, _ := queryVolunteers(app.db, `
SELECT `+volunteerSelect+` `+volunteerFrom+`
WHERE v.email_confirmed = 1 AND v.kiosk_code IS NOT NULL AND v.deleted_at IS NULL`)
baseURL := app.resolveBaseURL()
sent := 0
for _, v := range confirmed {
if v.Email == "" || v.KioskCode == nil {
continue
}
kioskLink := fmt.Sprintf("%s/v/%s", baseURL, *v.KioskCode)
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)
}