287 lines
7.9 KiB
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(¬eLabel)
|
|
app.db.QueryRow(`SELECT value FROM config WHERE key = 'volunteer_note_required'`).Scan(¬eRequired)
|
|
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(¬eRequired)
|
|
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)
|
|
}
|