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/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/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/api/api.go b/apps/api/internal/api/api.go index 610e3553..2a8dc92f 100644 --- a/apps/api/internal/api/api.go +++ b/apps/api/internal/api/api.go @@ -155,6 +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-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 d0639bdf..32947389 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,18 @@ type QueueConfirmationEmailFields struct { FirstName string `json:"firstName" 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) @@ -103,3 +116,49 @@ 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) + 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(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.")) + } + + res.Send(w, http.StatusOK, nil) +} diff --git a/apps/api/internal/config/config.go b/apps/api/internal/config/config.go index fbb74fde..be937a86 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"` } 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 new file mode 100644 index 00000000..cba2022b --- /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/services/bat.go b/apps/api/internal/services/bat.go index c2860e09..63304162 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") } @@ -435,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 23196980..462e8bab 100644 --- a/apps/api/internal/services/email.go +++ b/apps/api/internal/services/email.go @@ -2,26 +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, } } @@ -31,7 +38,10 @@ 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") @@ -41,13 +51,56 @@ func (s *EmailService) QueueConfirmationEmail(recipient string, name string) err return nil } +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" + 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.CoreBuckets.QRCodesBaseUrl, userId.String()) + + subject := "SwampHacks XI – A welcome from our Organizers!" + templateEmailFilepath := cfg.EmailTemplateDirectory + "WelcomeEmail.html" + + type emailTemplateData struct { + Name string + QRPngLink string + } + _, err = s.QueueSendHtmlEmailTask(recipient, subject, emailTemplateData{Name: name, QRPngLink: qrPngLink}, templateEmailFilepath) + + if err != nil { + s.logger.Err(err).Msgf("Failed to send welcome email to recipient with userId %s", userId.String()) + return err + } + + return nil +} + func (s *EmailService) QueueWaitlistAcceptanceEmail(recipient string, name string) error { cfg := config.Load() 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 +110,14 @@ func (s *EmailService) QueueWaitlistAcceptanceEmail(recipient string, name strin return nil } -func (s *EmailService) SendHtmlEmail(recipient string, subject string, name string, templateFilePath string) error { +// 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 template, err := template.ParseFiles(templateFilePath) @@ -65,7 +125,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 +140,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..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 - Name string Subject string + TemplateData interface{} TemplateFilePath string } diff --git a/apps/api/internal/workers/email.go b/apps/api/internal/workers/email.go index a881de80..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.Name, 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 }