From 26c5a06030fb3b0d6439b79fc0601440f5afe573 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Tue, 20 Jan 2026 21:56:29 -0500 Subject: [PATCH 1/8] feat: QueueWelcomeEmail api route, handler and service --- apps/api/internal/api/api.go | 1 + apps/api/internal/api/handlers/email.go | 36 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/apps/api/internal/api/api.go b/apps/api/internal/api/api.go index 610e3553..e7570289 100644 --- a/apps/api/internal/api/api.go +++ b/apps/api/internal/api/api.go @@ -155,6 +155,7 @@ func (api *API) setupRoutes(mw *mw.Middleware) { // Admin-only r.With(ensureEventAdmin).Post("/queue-confirmation-email", api.Handlers.Email.QueueConfirmationEmail) + r.With(ensureEventAdmin).Post("/queue-hacker-emails", api.Handlers.Email.QueueWelcomeEmail) r.With(ensureEventAdmin).Post("/calc-admissions", api.Handlers.Admission.HandleCalculateAdmissionsRequest) r.With(ensureEventAdmin).Patch("/transition-waitlisted-applications", api.Handlers.Application.TransitionWaitlistedApplications) r.With(ensureEventAdmin).Post("/begin-waitlist-transition", api.Handlers.Bat.QueueScheduleWaitlistTransitionTask) diff --git a/apps/api/internal/api/handlers/email.go b/apps/api/internal/api/handlers/email.go index d0639bdf..9cead7d7 100644 --- a/apps/api/internal/api/handlers/email.go +++ b/apps/api/internal/api/handlers/email.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/go-playground/validator/v10" + "github.com/google/uuid" "github.com/rs/zerolog" res "github.com/swamphacks/core/apps/api/internal/api/response" "github.com/swamphacks/core/apps/api/internal/email" @@ -82,6 +83,12 @@ type QueueConfirmationEmailFields struct { FirstName string `json:"firstName" validate:"required"` } +type QueueWelcomeEmailFields struct { + Email string `json:"email" validate:"required"` + FirstName string `json:"firstName" validate:"required"` + UserId string `json:userId validate:"required"` +} + func (h *EmailHandler) QueueConfirmationEmail(w http.ResponseWriter, r *http.Request) { var req QueueConfirmationEmailFields decoder := json.NewDecoder(r.Body) @@ -103,3 +110,32 @@ func (h *EmailHandler) QueueConfirmationEmail(w http.ResponseWriter, r *http.Req res.Send(w, http.StatusOK, nil) } + +func (h *EmailHandler) QueueWelcomeEmail(w http.ResponseWriter, r *http.Request) { + var req QueueWelcomeEmailFields + decoder := json.NewDecoder(r.Body) + decoder.DisallowUnknownFields() + err := decoder.Decode(&req) + if err != nil { + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Could not parse request body")) + return + } + + validate := validator.New() + if err := validate.Struct(req); err != nil { + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", err.Error())) + } + + parsedUserId, err := uuid.Parse(req.UserId) + if err != nil { + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "userId must be of type uuid")) + return + } + + err = h.emailService.QueueWelcomeEmail(req.Email, req.FirstName, parsedUserId) + if err != nil { + res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Hacker email could not be queued.")) + } + + res.Send(w, http.StatusOK, nil) +} From 29c8f76158f38152bd12cdd24951a2081b6bf6a2 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Wed, 21 Jan 2026 01:17:42 -0500 Subject: [PATCH 2/8] refactor: pass template data in struct --- .../email/templates/WelcomeEmail.html | 174 ++++++++++++++++++ apps/api/internal/email/templates/qrcode.jpg | Bin 0 -> 4245 bytes apps/api/internal/services/bat.go | 10 +- apps/api/internal/services/email.go | 44 ++++- apps/api/internal/tasks/email.go | 2 +- apps/api/internal/workers/email.go | 2 +- 6 files changed, 218 insertions(+), 14 deletions(-) create mode 100644 apps/api/internal/email/templates/WelcomeEmail.html create mode 100644 apps/api/internal/email/templates/qrcode.jpg diff --git a/apps/api/internal/email/templates/WelcomeEmail.html b/apps/api/internal/email/templates/WelcomeEmail.html new file mode 100644 index 00000000..2b207385 --- /dev/null +++ b/apps/api/internal/email/templates/WelcomeEmail.html @@ -0,0 +1,174 @@ + + + + + + + Welcome to SwampHacks XI! + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ SwampHacks XI Banner +
+

Hello {{ .Name }},

+
+

+ Our names are Elle and Rob, we’re SwampHacks XI’s organizers and on behalf of the University of Florida, + we’re so excited to welcome you to 36 hours of hell hacking! (Though after a couple hours of + debugging + at 3 AM, you might not notice the difference.) +

+ +

+ With SwampHacks at the end of the week, we wanted to share some important information and reminders: +

+ +
+

+ Check out our Hacker Guide! This is + essential—your full access to venue, schedule, meals, tracks, challenges, submission, judging, FAQs, + and emergency contacts depends on reviewing this guide before arrival. +

+
+ +

Before the Event

+
    +
  1. We understand that things happen. If you are no longer able to attend, please fill out this + form so that we can offer your spot to someone that can attend.
  2. +
  3. During the event we’ll be using our Discord server to update you when food, workshops, events occur + and of any important information (project submission details, judging info, etc.). Make sure + you have + joined our Discord server (you want to join so badd 🌀)! +
  4. +
  5. Still looking for a team? Check out our #🔍│looking-for-a-team + channel in Discord. If you can't find + a team ahead of our event, we’re going to have a hacker meet up (in Little Hall, LIT 113) right after + the opening ceremony to help you find a team!
  6. +
  7. Visiting from outside of Gainesville? Newell Hall will be open 24/7 for those of you + visiting us + this weekend. The 3rd and 4th floors of Newell will be designated “quiet spaces” starting when Little + Hall closes at 10 PM to breakfast at 8 AM. In alignment with our commitment to an equitable experience + for all, non-UF attendees will be given priority access to our overnight venue. That being said, if + you’re visiting us, please make sure to bring along toiletries and such (see our Hacker guide for our + recommended list and details regarding access to showers, or click here).
  8. +
  9. Feel free to post that you are attending SwampHacks XI with our attached graphic (special thanks to + our marketing team). +
+ +

Check-in & Day Of

+
    +
  1. To attend, you must check in (see QR code below) by 6:30 pm at LIT 113 or fill out our + late-arrival + form by 6 pm, Friday + to ensure + that we don’t give your spot away to those in the in-person waitlist. At check-in, you will present + your check-in QR code found below and receive the badge you will use to guarantee you access to venue, + food, and events. The QR code can also be found in the portal.
  2. + +
    +
    + + Check-in QR Code +
    +
    + +
  3. Last thing, opening ceremony begins at 6:30 PM in Carleton Auditorium, right across from our + check-in room. If you’ve confirmed that you’re arriving late, go straight to Carleton for the opening + ceremony. Right before the opening ceremony, we will be taking note of who’s in our waitlist queue, + accepting those who we can, and temporarily closing the check-in room. Check-in will re-open in LIT + 113 for confirmed late arrivals and for people we accepted off the waitlist after the ceremony + concludes.
  4. +
+ +

+ Have questions for us? Feel free to reach out to contact@swamphacks.com or ask in #🪪│ask-staff + for a quicker response. +

+ +

+ We look forward to meeting you and seeing what you’ve got! +

+ +

+ Best regards,

+

+ Elle and Robert
+ Organizers, SwampHacks XI
+ Department of Computer & Information Science & Engineering
+ University of Florida +

+
+ + Discord + + + Instagram + + + LinkedIn + +
+
+ + + \ No newline at end of file diff --git a/apps/api/internal/email/templates/qrcode.jpg b/apps/api/internal/email/templates/qrcode.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a7a5112aa014671535c8072f05a38e5b32d2c058 GIT binary patch literal 4245 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 XY}U6Kj{OJx-2gddhq0}-3A*_|C<7Do literal 0 HcmV?d00001 diff --git a/apps/api/internal/services/bat.go b/apps/api/internal/services/bat.go index c2860e09..bc99b21f 100644 --- a/apps/api/internal/services/bat.go +++ b/apps/api/internal/services/bat.go @@ -149,7 +149,10 @@ func (s *BatService) SendDecisionEmails(ctx context.Context, batRun sqlc.BatRun) if !ok { return ErrFailedToGetContactEmail } - taskInfo, err := s.emailService.QueueSendHtmlEmailTask(contactEmail, acceptedEmailSubject, emailInfo.Name, accepetedEmailTemplatePath) + type emailTemplateData struct { + Name string + } + taskInfo, err := s.emailService.QueueSendHtmlEmailTask(contactEmail, acceptedEmailSubject, emailTemplateData{Name: emailInfo.Name}, accepetedEmailTemplatePath) s.logger.Info().Str("TaskID", taskInfo.ID).Str("Task Queue", taskInfo.Queue).Str("Task Type", taskInfo.Type).Msg("Queued acceptance email") } @@ -163,7 +166,10 @@ func (s *BatService) SendDecisionEmails(ctx context.Context, batRun sqlc.BatRun) if !ok { return ErrFailedToGetContactEmail } - taskInfo, err := s.emailService.QueueSendHtmlEmailTask(contactEmail, rejectedEmailSubject, emailInfo.Name, rejectedEmailTemplatePath) + type emailTemplateData struct { + Name string + } + taskInfo, err := s.emailService.QueueSendHtmlEmailTask(contactEmail, rejectedEmailSubject, emailTemplateData{emailInfo.Name}, rejectedEmailTemplatePath) s.logger.Info().Str("TaskID", taskInfo.ID).Str("Task Queue", taskInfo.Queue).Str("Task Type", taskInfo.Type).Msg("Queued rejection email") } diff --git a/apps/api/internal/services/email.go b/apps/api/internal/services/email.go index 23196980..8564eef8 100644 --- a/apps/api/internal/services/email.go +++ b/apps/api/internal/services/email.go @@ -4,6 +4,7 @@ import ( "bytes" "html/template" + "github.com/google/uuid" "github.com/hibiken/asynq" "github.com/rs/zerolog" "github.com/swamphacks/core/apps/api/internal/config" @@ -31,7 +32,30 @@ func (s *EmailService) QueueConfirmationEmail(recipient string, name string) err subject := "SwampHacks XI: we received your application!" templateEmailFilepath := cfg.EmailTemplateDirectory + "ConfirmationEmail.html" - _, err := s.QueueSendHtmlEmailTask(recipient, subject, name, templateEmailFilepath) + type emailTemplateData struct { + Name string + } + _, err := s.QueueSendHtmlEmailTask(recipient, subject, emailTemplateData{Name: name}, templateEmailFilepath) + + if err != nil { + s.logger.Err(err).Msg("Failed to send confirmation email to recipient") + return err + } + + return nil +} + +func (s *EmailService) QueueWelcomeEmail(recipient string, name string, userId uuid.UUID) error { + // TODO + cfg := config.Load() + + subject := "SwampHacks XI – A welcome from our Organizers!" + templateEmailFilepath := cfg.EmailTemplateDirectory + "WelcomeEmail.html" + + type emailTemplateData struct { + Name string + } + _, err := s.QueueSendHtmlEmailTask(recipient, subject, emailTemplateData{Name: name}, templateEmailFilepath) if err != nil { s.logger.Err(err).Msg("Failed to send confirmation email to recipient") @@ -47,7 +71,10 @@ func (s *EmailService) QueueWaitlistAcceptanceEmail(recipient string, name strin subject := "Congratulations! You're in – confirm in 72 hours to keep your spot in SwampHacks XI" templateEmailFilepath := cfg.EmailTemplateDirectory + "WaitlistAcceptanceEmail.html" - _, err := s.QueueSendHtmlEmailTask(recipient, subject, name, templateEmailFilepath) + type emailTemplateData struct { + Name string + } + _, err := s.QueueSendHtmlEmailTask(recipient, subject, emailTemplateData{Name: name}, templateEmailFilepath) if err != nil { s.logger.Err(err).Msg("Failed to send waitlist acceptance email to recipient") @@ -57,7 +84,8 @@ func (s *EmailService) QueueWaitlistAcceptanceEmail(recipient string, name strin return nil } -func (s *EmailService) SendHtmlEmail(recipient string, subject string, name string, templateFilePath string) error { +// TODO: refactor name string to data struct of any type. template.Execute using this struct +func (s *EmailService) SendHtmlEmail(recipient string, subject string, templateData interface{}, templateFilePath string) error { var body bytes.Buffer template, err := template.ParseFiles(templateFilePath) @@ -65,7 +93,7 @@ func (s *EmailService) SendHtmlEmail(recipient string, subject string, name stri s.logger.Err(err).Msg("Failed to parse email template for recipient") } - err = template.Execute(&body, struct{ Name string }{Name: name}) + err = template.Execute(&body, templateData) if err != nil { s.logger.Err(err).Msg("Failed to inject template variables for recipient '%s'.") } @@ -80,19 +108,15 @@ func (s *EmailService) SendHtmlEmail(recipient string, subject string, name stri return nil } -// TODO: refactor other queue functions to use a similar naming scheme -func (s *EmailService) QueueSendHtmlEmailTask(to string, subject string, name string, templateFilePath string) (*asynq.TaskInfo, error) { +func (s *EmailService) QueueSendHtmlEmailTask(to string, subject string, templateData interface{}, templateFilePath string) (*asynq.TaskInfo, error) { if len(to) == 0 { s.logger.Warn().Msgf("No recipient email found for email being sent from template '%s'", templateFilePath) } - if len(name) == 0 { - s.logger.Warn().Msgf("No recipient name found for email being sent from template '%s'", templateFilePath) - } task, err := tasks.NewTaskSendHtmlEmail(tasks.SendHtmlEmailPayload{ To: to, Subject: subject, - Name: name, + TemplateData: templateData, TemplateFilePath: templateFilePath, }) diff --git a/apps/api/internal/tasks/email.go b/apps/api/internal/tasks/email.go index 08acda63..09c3c06f 100644 --- a/apps/api/internal/tasks/email.go +++ b/apps/api/internal/tasks/email.go @@ -19,7 +19,7 @@ type SendTextEmailPayload struct { type SendHtmlEmailPayload struct { To string - Name string + Data interface{} Subject string TemplateFilePath string } diff --git a/apps/api/internal/workers/email.go b/apps/api/internal/workers/email.go index a881de80..7f5e5f14 100644 --- a/apps/api/internal/workers/email.go +++ b/apps/api/internal/workers/email.go @@ -30,7 +30,7 @@ func (w *EmailWorker) HandleSendHtmlEmailTask(ctx context.Context, t *asynq.Task return fmt.Errorf("HandleSendHtmlEmailTask: json.Unmarshal failed: %v: %w", err, asynq.SkipRetry) } - if err := w.emailService.SendHtmlEmail(p.To, p.Subject, p.Name, p.TemplateFilePath); err != nil { + if err := w.emailService.SendHtmlEmail(p.To, p.Subject, p.Data, p.TemplateFilePath); err != nil { w.logger.Err(err).Msg("Failed to send ConfirmationEmail from worker") return err } From fef0b00f0d87647938b5d0f0580a245fb9593949 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Wed, 21 Jan 2026 01:36:19 -0500 Subject: [PATCH 3/8] fix: property names --- apps/api/internal/api/handlers/email.go | 35 ++++++++++++++++++++----- apps/api/internal/tasks/email.go | 2 +- apps/api/internal/workers/email.go | 2 +- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/apps/api/internal/api/handlers/email.go b/apps/api/internal/api/handlers/email.go index 9cead7d7..da1ad75f 100644 --- a/apps/api/internal/api/handlers/email.go +++ b/apps/api/internal/api/handlers/email.go @@ -83,12 +83,18 @@ type QueueConfirmationEmailFields struct { FirstName string `json:"firstName" validate:"required"` } -type QueueWelcomeEmailFields struct { - Email string `json:"email" validate:"required"` - FirstName string `json:"firstName" validate:"required"` - UserId string `json:userId validate:"required"` -} - +// Queue a Confirmation Email +// +// @Summary Queue a Confirmation Email Request +// @Description Push a Confirmation Email request to the task queue +// @Tags Email +// @Accept json +// @Produce json +// @Param request body QueueConfirmationEmailFields true "Email data" +// @Success 201 {object} string "OK: Email request queued" +// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request. The email request is potentially invalid." +// @Failure 500 {object} response.ErrorResponse "Server Error: The server went kaput while queueing email sending" +// @Router /email/queue [post] func (h *EmailHandler) QueueConfirmationEmail(w http.ResponseWriter, r *http.Request) { var req QueueConfirmationEmailFields decoder := json.NewDecoder(r.Body) @@ -111,6 +117,23 @@ func (h *EmailHandler) QueueConfirmationEmail(w http.ResponseWriter, r *http.Req res.Send(w, http.StatusOK, nil) } +type QueueWelcomeEmailFields struct { + Email string `json:"email" validate:"required"` + FirstName string `json:"firstName" validate:"required"` + UserId string `json:userId validate:"required"` +} + +// Queue a Welcome Email +// +// @Summary Queue a Welcome Email +// @Description Push an Welcome Email request to the task queue +// @Tags Email +// @Accept json +// @Produce json +// @Param request body QueueConfirmationEmailFields true "Email data" +// @Success 201 {object} string "OK: Email request queued" +// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request. The email request is potentially invalid." +// @Failure 500 {object} response.ErrorResponse "Server Error: The server went kaput while queueing email sending" func (h *EmailHandler) QueueWelcomeEmail(w http.ResponseWriter, r *http.Request) { var req QueueWelcomeEmailFields decoder := json.NewDecoder(r.Body) diff --git a/apps/api/internal/tasks/email.go b/apps/api/internal/tasks/email.go index 09c3c06f..1de132c7 100644 --- a/apps/api/internal/tasks/email.go +++ b/apps/api/internal/tasks/email.go @@ -19,8 +19,8 @@ type SendTextEmailPayload struct { type SendHtmlEmailPayload struct { To string - Data interface{} Subject string + TemplateData interface{} TemplateFilePath string } diff --git a/apps/api/internal/workers/email.go b/apps/api/internal/workers/email.go index 7f5e5f14..77a6478f 100644 --- a/apps/api/internal/workers/email.go +++ b/apps/api/internal/workers/email.go @@ -30,7 +30,7 @@ func (w *EmailWorker) HandleSendHtmlEmailTask(ctx context.Context, t *asynq.Task return fmt.Errorf("HandleSendHtmlEmailTask: json.Unmarshal failed: %v: %w", err, asynq.SkipRetry) } - if err := w.emailService.SendHtmlEmail(p.To, p.Subject, p.Data, p.TemplateFilePath); err != nil { + if err := w.emailService.SendHtmlEmail(p.To, p.Subject, p.TemplateData, p.TemplateFilePath); err != nil { w.logger.Err(err).Msg("Failed to send ConfirmationEmail from worker") return err } From 5e0afbc07440fb0db07f9b87bc1e2cbc517cea10 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Wed, 21 Jan 2026 02:58:42 -0500 Subject: [PATCH 4/8] feat: qr image generation, upload, link generation, and welcome email queue --- apps/api/.env.dev.example | 3 ++ apps/api/go.mod | 1 + apps/api/go.sum | 2 + apps/api/internal/config/config.go | 3 ++ .../email/templates/WelcomeEmail.html | 3 +- apps/api/internal/services/email.go | 40 +++++++++++++++---- 6 files changed, 42 insertions(+), 10 deletions(-) diff --git a/apps/api/.env.dev.example b/apps/api/.env.dev.example index 66b5994b..71119c07 100644 --- a/apps/api/.env.dev.example +++ b/apps/api/.env.dev.example @@ -13,6 +13,9 @@ AUTH_DISCORD_CLIENT_ID= AUTH_DISCORD_CLIENT_SECRET= AUTH_DISCORD_REDIRECT_URI="http://localhost:8080/auth/callback" +# CF +CORE_BUCKETS_USER_QRCODES_BASE_URL= + # For cookies COOKIE_DOMAIN=localhost COOKIE_SECURE=false diff --git a/apps/api/go.mod b/apps/api/go.mod index f36f77a1..de09374e 100644 --- a/apps/api/go.mod +++ b/apps/api/go.mod @@ -63,6 +63,7 @@ require ( github.com/mattn/go-isatty v0.0.19 // indirect github.com/redis/go-redis/v9 v9.7.0 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/spf13/cast v1.7.0 // indirect github.com/sv-tools/openapi v0.4.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect diff --git a/apps/api/go.sum b/apps/api/go.sum index 207cfa61..60638828 100644 --- a/apps/api/go.sum +++ b/apps/api/go.sum @@ -133,6 +133,8 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/apps/api/internal/config/config.go b/apps/api/internal/config/config.go index fbb74fde..0ea801c8 100644 --- a/apps/api/internal/config/config.go +++ b/apps/api/internal/config/config.go @@ -48,9 +48,11 @@ type AWSConfig struct { type CoreBuckets struct { Avatars string `env:"USER_AVATARS" envDefault:"core-user-avatars-dev"` + QRCodes string `env:"USER_QRCODES" envDefault:"core-user-qrcodes-dev"` ApplicationResumes string `env:"APPLICATION_RESUMES" envDefault:"core-application-resumes-dev"` EventAssets string `env:"EVENT_ASSETS" envDefault:"core-event-assets-dev"` AvatarsBaseUrl string `env:"USER_AVATARS_BASE_URL"` + QRCodesBaseUrl string `env:"USER_QRCODES_BASE_URL"` EventAssetsBaseUrl string `env:"EVENT_ASSETS_BASE_URL"` } @@ -64,6 +66,7 @@ type Config struct { MaxAcceptedApplications uint32 `env:"MAX_ACCEPTED_APPLICATIONS"` AcceptFromWaitlistCount uint32 `env:"ACCEPT_FROM_WAITLIST_COUNT"` AcceptFromWaitlistPeriod string `env:"ACCEPT_FROM_WAITLIST_PERIOD"` + QRCodesEndpoint string `env:"QRCODES_ENDPOINT"` Auth AuthConfig `envPrefix:"AUTH_"` Cookie CookieConfig `envPrefix:"COOKIE_"` diff --git a/apps/api/internal/email/templates/WelcomeEmail.html b/apps/api/internal/email/templates/WelcomeEmail.html index 2b207385..b88f35f9 100644 --- a/apps/api/internal/email/templates/WelcomeEmail.html +++ b/apps/api/internal/email/templates/WelcomeEmail.html @@ -106,8 +106,7 @@

Check-in & Day Of

- - Check-in QR Code
diff --git a/apps/api/internal/services/email.go b/apps/api/internal/services/email.go index 8564eef8..dfe9f165 100644 --- a/apps/api/internal/services/email.go +++ b/apps/api/internal/services/email.go @@ -2,27 +2,33 @@ package services import ( "bytes" + "context" + "fmt" "html/template" "github.com/google/uuid" "github.com/hibiken/asynq" "github.com/rs/zerolog" + "github.com/skip2/go-qrcode" "github.com/swamphacks/core/apps/api/internal/config" "github.com/swamphacks/core/apps/api/internal/email" + "github.com/swamphacks/core/apps/api/internal/storage" "github.com/swamphacks/core/apps/api/internal/tasks" ) type EmailService struct { logger zerolog.Logger - SESClient *email.SESClient taskQueue *asynq.Client + SESClient *email.SESClient + storage storage.Storage } -func NewEmailService(taskQueue *asynq.Client, SESClient *email.SESClient, logger zerolog.Logger) *EmailService { +func NewEmailService(taskQueue *asynq.Client, SESClient *email.SESClient, storage storage.Storage, logger zerolog.Logger) *EmailService { return &EmailService{ logger: logger.With().Str("service", "EmailService").Str("component", "email").Logger(), taskQueue: taskQueue, SESClient: SESClient, + storage: storage, } } @@ -45,20 +51,32 @@ func (s *EmailService) QueueConfirmationEmail(recipient string, name string) err return nil } -func (s *EmailService) QueueWelcomeEmail(recipient string, name string, userId uuid.UUID) error { - // TODO +func (s *EmailService) QueueWelcomeEmail(ctx context.Context, recipient string, name string, userId uuid.UUID) error { cfg := config.Load() + qrString := fmt.Sprintf("IDENT::%s", userId) + qrPng, err := qrcode.Encode(qrString, qrcode.Medium, 256) + if err != nil { + s.logger.Err(err).Msg("Failed to generate QR code png") + return err + } + + contentType := "image/png" + err = s.storage.Store(ctx, cfg.CoreBuckets.QRCodes, userId.String(), qrPng, &contentType) + + qrPngLink := fmt.Sprintf("%s/%s", cfg.QRCodesEndpoint, userId.String()) + subject := "SwampHacks XI – A welcome from our Organizers!" templateEmailFilepath := cfg.EmailTemplateDirectory + "WelcomeEmail.html" type emailTemplateData struct { - Name string + Name string + QRPngLink string } - _, err := s.QueueSendHtmlEmailTask(recipient, subject, emailTemplateData{Name: name}, templateEmailFilepath) + _, err = s.QueueSendHtmlEmailTask(recipient, subject, emailTemplateData{Name: name, QRPngLink: qrPngLink}, templateEmailFilepath) if err != nil { - s.logger.Err(err).Msg("Failed to send confirmation email to recipient") + s.logger.Err(err).Msgf("Failed to send welcome email to recipient with userId %s", userId.String()) return err } @@ -84,7 +102,13 @@ func (s *EmailService) QueueWaitlistAcceptanceEmail(recipient string, name strin return nil } -// TODO: refactor name string to data struct of any type. template.Execute using this struct +// SendHtmlEmail +// +// templateData: a struct holding the data which should replace {{}} tags inside of an html template. +// For example, if an email template uses the tag {{ .Name }}, then the templateData struct would look like +// type templateData struct { +// Name string +// } func (s *EmailService) SendHtmlEmail(recipient string, subject string, templateData interface{}, templateFilePath string) error { var body bytes.Buffer From 11e25160acf8c4a4574900503d259e3bc39aa4a9 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Wed, 21 Jan 2026 04:58:59 -0500 Subject: [PATCH 5/8] feat: get slice of attendee userIds, added route to send all welcome emails; fixes: various --- apps/api/cmd/BAT_worker/main.go | 2 +- apps/api/cmd/api/main.go | 2 +- apps/api/cmd/email_worker/main.go | 2 +- apps/api/internal/api/api.go | 3 +- apps/api/internal/api/handlers/bat.go | 26 ++++++++++++++++ apps/api/internal/api/handlers/email.go | 2 +- apps/api/internal/config/config.go | 1 - apps/api/internal/db/queries/event_roles.sql | 7 ++++- apps/api/internal/db/repository/events.go | 6 +++- apps/api/internal/db/sqlc/event_roles.sql.go | 26 ++++++++++++++++ .../email/templates/WelcomeEmail.html | 3 +- apps/api/internal/email/templates/qrcode.jpg | Bin 4245 -> 0 bytes apps/api/internal/services/bat.go | 29 ++++++++++++++++++ apps/api/internal/services/email.go | 10 +++++- 14 files changed, 109 insertions(+), 10 deletions(-) delete mode 100644 apps/api/internal/email/templates/qrcode.jpg diff --git a/apps/api/cmd/BAT_worker/main.go b/apps/api/cmd/BAT_worker/main.go index 3ebc57ef..e745ce78 100644 --- a/apps/api/cmd/BAT_worker/main.go +++ b/apps/api/cmd/BAT_worker/main.go @@ -82,7 +82,7 @@ func main() { batRunsRepo := repository.NewBatRunsRepository(database) sesClient := email.NewSESClient(cfg.AWS.AccessKey, cfg.AWS.AccessKeySecret, cfg.AWS.Region, logger) - emailService := services.NewEmailService(taskQueueClient, sesClient, logger) + emailService := services.NewEmailService(taskQueueClient, sesClient, nil, logger) batService := services.NewBatService(applicationRepo, eventRepo, userRepo, batRunsRepo, emailService, txm, nil, scheduler, logger) applicationService := services.NewApplicationService(applicationRepo, userRepo, eventService, emailService, txm, nil, nil, scheduler, logger) diff --git a/apps/api/cmd/api/main.go b/apps/api/cmd/api/main.go index 73973301..1e427e1f 100644 --- a/apps/api/cmd/api/main.go +++ b/apps/api/cmd/api/main.go @@ -89,7 +89,7 @@ func main() { userService := services.NewUserService(userRepo, logger) eventInterestService := services.NewEventInterestService(eventInterestRepo, logger) eventService := services.NewEventService(eventRepo, userRepo, r2Client, &cfg.CoreBuckets, logger) - emailService := services.NewEmailService(taskQueueClient, sesClient, logger) + emailService := services.NewEmailService(taskQueueClient, sesClient, r2Client, logger) applicationService := services.NewApplicationService(applicationRepo, userRepo, eventService, emailService, txm, r2Client, &cfg.CoreBuckets, nil, logger) teamService := services.NewTeamService(teamRepo, teamMemberRepo, teamJoinRequestRepo, eventRepo, txm, logger) batService := services.NewBatService(applicationRepo, eventRepo, userRepo, batRunsRepo, emailService, txm, taskQueueClient, nil, logger) diff --git a/apps/api/cmd/email_worker/main.go b/apps/api/cmd/email_worker/main.go index d03da5bb..6ee0ae17 100644 --- a/apps/api/cmd/email_worker/main.go +++ b/apps/api/cmd/email_worker/main.go @@ -41,7 +41,7 @@ func main() { // Create ses client sesClient := email.NewSESClient(cfg.AWS.AccessKey, cfg.AWS.AccessKeySecret, cfg.AWS.Region, logger) - emailService := services.NewEmailService(nil, sesClient, logger) + emailService := services.NewEmailService(nil, sesClient, nil, logger) emailWorker := workers.NewEmailWorker(emailService, logger) mux := asynq.NewServeMux() diff --git a/apps/api/internal/api/api.go b/apps/api/internal/api/api.go index e7570289..2a8dc92f 100644 --- a/apps/api/internal/api/api.go +++ b/apps/api/internal/api/api.go @@ -155,7 +155,8 @@ func (api *API) setupRoutes(mw *mw.Middleware) { // Admin-only r.With(ensureEventAdmin).Post("/queue-confirmation-email", api.Handlers.Email.QueueConfirmationEmail) - r.With(ensureEventAdmin).Post("/queue-hacker-emails", api.Handlers.Email.QueueWelcomeEmail) + r.With(ensureEventAdmin).Post("/queue-welcome-email", api.Handlers.Email.QueueWelcomeEmail) + r.With(ensureEventAdmin).Post("/send-welcome-emails", api.Handlers.Bat.SendWelcomeEmails) r.With(ensureEventAdmin).Post("/calc-admissions", api.Handlers.Admission.HandleCalculateAdmissionsRequest) r.With(ensureEventAdmin).Patch("/transition-waitlisted-applications", api.Handlers.Application.TransitionWaitlistedApplications) r.With(ensureEventAdmin).Post("/begin-waitlist-transition", api.Handlers.Bat.QueueScheduleWaitlistTransitionTask) diff --git a/apps/api/internal/api/handlers/bat.go b/apps/api/internal/api/handlers/bat.go index 915b9d4b..fe4ee26b 100644 --- a/apps/api/internal/api/handlers/bat.go +++ b/apps/api/internal/api/handlers/bat.go @@ -197,3 +197,29 @@ func (h *BatHandler) QueueShutdownWaitlistSchedulerTask(w http.ResponseWriter, r res.Send(w, http.StatusOK, nil) } + +// Send welcome emails +// +// @Summary Sends welcome emails to attendees +// @Description +// @Tags +// +// @Param eventId path string true "ID of the event" +// @Success 200 "Welcome emails began to queue successfully" +// @Failure 400 {object} res.ErrorResponse "Bad request: invalid event ID" +// @Failure 500 {object} res.ErrorResponse "Server error: failed to begin queuing welcome emails" +// @Router /events/{eventId}/send-welcome-emails [post] +func (h *BatHandler) SendWelcomeEmails(w http.ResponseWriter, r *http.Request) { + eventId, err := web.PathParamToUUID(r, "eventId") + if err != nil { + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) + return + } + + err = h.BatService.SendWelcomeEmailToAttendees(r.Context(), eventId) + if err != nil { + res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Failed to create ScheduleWaitlistTransition task.")) + } + + res.Send(w, http.StatusCreated, nil) +} diff --git a/apps/api/internal/api/handlers/email.go b/apps/api/internal/api/handlers/email.go index da1ad75f..32947389 100644 --- a/apps/api/internal/api/handlers/email.go +++ b/apps/api/internal/api/handlers/email.go @@ -155,7 +155,7 @@ func (h *EmailHandler) QueueWelcomeEmail(w http.ResponseWriter, r *http.Request) return } - err = h.emailService.QueueWelcomeEmail(req.Email, req.FirstName, parsedUserId) + err = h.emailService.QueueWelcomeEmail(r.Context(), req.Email, req.FirstName, parsedUserId) if err != nil { res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Hacker email could not be queued.")) } diff --git a/apps/api/internal/config/config.go b/apps/api/internal/config/config.go index 0ea801c8..be937a86 100644 --- a/apps/api/internal/config/config.go +++ b/apps/api/internal/config/config.go @@ -66,7 +66,6 @@ type Config struct { MaxAcceptedApplications uint32 `env:"MAX_ACCEPTED_APPLICATIONS"` AcceptFromWaitlistCount uint32 `env:"ACCEPT_FROM_WAITLIST_COUNT"` AcceptFromWaitlistPeriod string `env:"ACCEPT_FROM_WAITLIST_PERIOD"` - QRCodesEndpoint string `env:"QRCODES_ENDPOINT"` Auth AuthConfig `envPrefix:"AUTH_"` Cookie CookieConfig `envPrefix:"COOKIE_"` diff --git a/apps/api/internal/db/queries/event_roles.sql b/apps/api/internal/db/queries/event_roles.sql index 12b8bf59..a68e0594 100644 --- a/apps/api/internal/db/queries/event_roles.sql +++ b/apps/api/internal/db/queries/event_roles.sql @@ -60,9 +60,14 @@ SELECT COUNT(*) FROM event_roles AS er WHERE er.event_id = @event_id::uuid AND er.role = 'attendee'; +-- name: GetAttendeeUserIdsByEventId :many +SELECT er.user_id FROM event_roles AS er +WHERE er.event_id = @event_id::uuid + AND er.role = 'attendee'; + -- name: GetUserByRFID :one SELECT u.* FROM auth.users u JOIN event_roles er ON u.id = er.user_id WHERE er.event_id = $1 - AND er.rfid = $2; \ No newline at end of file + AND er.rfid = $2; diff --git a/apps/api/internal/db/repository/events.go b/apps/api/internal/db/repository/events.go index 43a4a005..86ed536e 100644 --- a/apps/api/internal/db/repository/events.go +++ b/apps/api/internal/db/repository/events.go @@ -204,4 +204,8 @@ func (r *EventRepository) GetEventRoleByDiscordIDAndEventId(ctx context.Context, } return &eventRole, nil -} \ No newline at end of file +} + +func (r *EventRepository) GetAttendeeUserIdsByEventId(ctx context.Context, eventID uuid.UUID) ([]uuid.UUID, error) { + return r.db.Query.GetAttendeeUserIdsByEventId(ctx, eventID) +} diff --git a/apps/api/internal/db/sqlc/event_roles.sql.go b/apps/api/internal/db/sqlc/event_roles.sql.go index b32d9789..32c99a23 100644 --- a/apps/api/internal/db/sqlc/event_roles.sql.go +++ b/apps/api/internal/db/sqlc/event_roles.sql.go @@ -42,6 +42,32 @@ func (q *Queries) GetAttendeeCountByEventId(ctx context.Context, eventID uuid.UU return count, err } +const getAttendeeUserIdsByEventId = `-- name: GetAttendeeUserIdsByEventId :many +SELECT er.user_id FROM event_roles AS er +WHERE er.event_id = $1::uuid + AND er.role = 'attendee' +` + +func (q *Queries) GetAttendeeUserIdsByEventId(ctx context.Context, eventID uuid.UUID) ([]uuid.UUID, error) { + rows, err := q.db.Query(ctx, getAttendeeUserIdsByEventId, eventID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []uuid.UUID{} + for rows.Next() { + var user_id uuid.UUID + if err := rows.Scan(&user_id); err != nil { + return nil, err + } + items = append(items, user_id) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getEventAttendeesWithDiscord = `-- name: GetEventAttendeesWithDiscord :many SELECT a.account_id as discord_id, diff --git a/apps/api/internal/email/templates/WelcomeEmail.html b/apps/api/internal/email/templates/WelcomeEmail.html index b88f35f9..cba2022b 100644 --- a/apps/api/internal/email/templates/WelcomeEmail.html +++ b/apps/api/internal/email/templates/WelcomeEmail.html @@ -101,7 +101,8 @@

Check-in & Day Of

to ensure that we don’t give your spot away to those in the in-person waitlist. At check-in, you will present your check-in QR code found below and receive the badge you will use to guarantee you access to venue, - food, and events. The QR code can also be found in the portal. + food, and events. The QR code can also be found in the portal.
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 XY}U6Kj{OJx-2gddhq0}-3A*_|C<7Do diff --git a/apps/api/internal/services/bat.go b/apps/api/internal/services/bat.go index bc99b21f..63304162 100644 --- a/apps/api/internal/services/bat.go +++ b/apps/api/internal/services/bat.go @@ -441,3 +441,32 @@ func (s *BatService) QueueShutdownWaitlistScheduler() error { return nil } + +func (s *BatService) SendWelcomeEmailToAttendees(ctx context.Context, eventId uuid.UUID) error { + attendees, err := s.eventRepo.GetAttendeeUserIdsByEventId(ctx, eventId) + if err != nil { + s.logger.Err(err).Msg("Could not get attendee user ids") + return err + } + + s.logger.Info().Msgf("Sending welcome emails to %v attendees", len(attendees)) + + for _, userId := range attendees { + contactInfo, err := s.userRepo.GetUserEmailInfoById(ctx, userId) + if err != nil { + s.logger.Err(err).Msgf("Could not get contact info for user with id %s", userId) + return err + } + contactEmail, ok := contactInfo.ContactEmail.(string) + if !ok { + return ErrFailedToGetContactEmail + } + + err = s.emailService.QueueWelcomeEmail(ctx, contactEmail, contactInfo.Name, userId) + if err != nil { + s.logger.Err(err).Msgf("Could not queue welcome email for user with id %s", userId) + return err + } + } + return nil +} diff --git a/apps/api/internal/services/email.go b/apps/api/internal/services/email.go index dfe9f165..462e8bab 100644 --- a/apps/api/internal/services/email.go +++ b/apps/api/internal/services/email.go @@ -62,9 +62,17 @@ func (s *EmailService) QueueWelcomeEmail(ctx context.Context, recipient string, } contentType := "image/png" + if s.storage == nil { + s.logger.Err(err).Msg("A R2 client must be connected for this function to run") + return err + } err = s.storage.Store(ctx, cfg.CoreBuckets.QRCodes, userId.String(), qrPng, &contentType) + if err != nil { + s.logger.Err(err).Msg("Failed to upload QR code to R2") + return err + } - qrPngLink := fmt.Sprintf("%s/%s", cfg.QRCodesEndpoint, userId.String()) + qrPngLink := fmt.Sprintf("%s/%s", cfg.CoreBuckets.QRCodesBaseUrl, userId.String()) subject := "SwampHacks XI – A welcome from our Organizers!" templateEmailFilepath := cfg.EmailTemplateDirectory + "WelcomeEmail.html" From 7b098d17639834c917424af8e3e04b12d247aeaa Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Wed, 21 Jan 2026 05:29:49 -0500 Subject: [PATCH 6/8] fix: error msg, email template --- apps/api/internal/api/handlers/bat.go | 2 +- apps/api/internal/email/templates/WelcomeEmail.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/internal/api/handlers/bat.go b/apps/api/internal/api/handlers/bat.go index fe4ee26b..8f403b6f 100644 --- a/apps/api/internal/api/handlers/bat.go +++ b/apps/api/internal/api/handlers/bat.go @@ -218,7 +218,7 @@ func (h *BatHandler) SendWelcomeEmails(w http.ResponseWriter, r *http.Request) { err = h.BatService.SendWelcomeEmailToAttendees(r.Context(), eventId) if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Failed to create ScheduleWaitlistTransition task.")) + res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Failed to send welcome emails.")) } res.Send(w, http.StatusCreated, nil) diff --git a/apps/api/internal/email/templates/WelcomeEmail.html b/apps/api/internal/email/templates/WelcomeEmail.html index cba2022b..0b851a4d 100644 --- a/apps/api/internal/email/templates/WelcomeEmail.html +++ b/apps/api/internal/email/templates/WelcomeEmail.html @@ -102,7 +102,7 @@

Check-in & Day Of

that we don’t give your spot away to those in the in-person waitlist. At check-in, you will present your check-in QR code found below and receive the badge you will use to guarantee you access to venue, food, and events. The QR code can also be found in the portal. + style="color:#1155cc; text-decoration:underline;">portal.
Date: Wed, 21 Jan 2026 11:31:27 -0500 Subject: [PATCH 7/8] feat: err log on empty contact email and continue loop --- apps/api/internal/services/bat.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/api/internal/services/bat.go b/apps/api/internal/services/bat.go index 63304162..4d97b402 100644 --- a/apps/api/internal/services/bat.go +++ b/apps/api/internal/services/bat.go @@ -461,6 +461,10 @@ func (s *BatService) SendWelcomeEmailToAttendees(ctx context.Context, eventId uu if !ok { return ErrFailedToGetContactEmail } + if contactEmail == "" { + s.logger.Err(err).Msgf("empty contact email found for user with id %s", userId) + continue + } err = s.emailService.QueueWelcomeEmail(ctx, contactEmail, contactInfo.Name, userId) if err != nil { From a4a88d2d6c28a66af4e3f2baa2cff6773b52eec4 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Wed, 21 Jan 2026 11:32:13 -0500 Subject: [PATCH 8/8] fix: dont break loop --- apps/api/internal/services/bat.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/api/internal/services/bat.go b/apps/api/internal/services/bat.go index 4d97b402..81801cb8 100644 --- a/apps/api/internal/services/bat.go +++ b/apps/api/internal/services/bat.go @@ -459,7 +459,8 @@ func (s *BatService) SendWelcomeEmailToAttendees(ctx context.Context, eventId uu } contactEmail, ok := contactInfo.ContactEmail.(string) if !ok { - return ErrFailedToGetContactEmail + s.logger.Err(err).Msgf("could got convert id %s", userId) + continue } if contactEmail == "" { s.logger.Err(err).Msgf("empty contact email found for user with id %s", userId)