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!
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+ 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
+
+ - 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.
+ - 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 🌀)!
+
+ - 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!
+ - 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).
+ - Feel free to post that you are attending SwampHacks XI with our attached graphic (special thanks to
+ our marketing team).
+
+
+ Check-in & Day Of
+
+ - 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.
+
+
+
+
+ 
+
+
+
+ - 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.
+
+
+
+ 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
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+
+
+
\ 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
-
-
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.
?{$5z-(SD$dY1b+*K^LfpL3tjIiLH
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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)