Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
111c1c6
feat:waitlist transition queries
h1divp Jan 7, 2026
a129227
fix:forgot repo function, syntax errors
h1divp Jan 7, 2026
eb01e88
fix:protect decisions routes
h1divp Jan 9, 2026
cf5c9fc
feat:handler for transition
h1divp Jan 10, 2026
f9b2632
fix:where clause, user ids returned
h1divp Jan 10, 2026
617609a
chore:updated logic
h1divp Jan 10, 2026
e8ea893
chore:sqlc generate
h1divp Jan 10, 2026
cb2b73a
feat: basic parsing library for swamphacks qrs
AlexanderWangY Jan 10, 2026
21777fb
refactor:confirmation email queuing and sending
h1divp Jan 10, 2026
8cb3dab
Merge pull request #265 from swamphacks/feat/qr-engine
AlexanderWangY Jan 10, 2026
98177ca
feat:queue status transition task, email sending (broken)
h1divp Jan 10, 2026
7662650
fix: syntax & orphaned code, feat: task scheduler, total acceptance c…
h1divp Jan 10, 2026
c7ab15c
fix: missing param
h1divp Jan 10, 2026
c175c6f
fix: unused import, unneeded param
h1divp Jan 10, 2026
7217ee9
fix:route
h1divp Jan 10, 2026
eb62b74
fix:scheduler is only called from bat worker
h1divp Jan 10, 2026
b7f8aa6
fix:function renaming and struct fix
h1divp Jan 10, 2026
56dc920
fix:start scheduler
h1divp Jan 11, 2026
32132c3
fix:queue task
h1divp Jan 11, 2026
499eb39
chore: removed orphaned code
h1divp Jan 11, 2026
61552f9
fix: application service added to bat worker
h1divp Jan 11, 2026
390fc6a
fix:added task queue client for emailing
h1divp Jan 11, 2026
db411a7
fix: set period to 72hr; chore: removed orphaned code
h1divp Jan 11, 2026
06ffcef
Merge pull request #264 from swamphacks/feat/waitlist-transition
AlexanderWangY Jan 11, 2026
3b45e7b
fix: remove extra nil param on bat service init
AlexanderWangY Jan 11, 2026
b8693bd
Merge pull request #266 from swamphacks/fix/mismatch-params
AlexanderWangY Jan 11, 2026
affaf42
feat:waitlist params added to env config; refactor:better names
h1divp Jan 11, 2026
ef63ec4
feat:log warning for missing email or name
h1divp Jan 11, 2026
c16d44d
refactor:rename route
h1divp Jan 11, 2026
740fce9
feat:added route to manually shutdown scheduler
h1divp Jan 11, 2026
b3bf9b7
fix: added param
h1divp Jan 11, 2026
6323416
fix:scheduler is shutdown on bat worker side not api side
h1divp Jan 11, 2026
b249512
fix: make sure only accepted *applicants* are waitlisted on a transition
h1divp Jan 11, 2026
729aad2
fix:check for attendee count not accepted app count
h1divp Jan 11, 2026
1d415d2
fix:var name mismatch
h1divp Jan 11, 2026
2840d6e
chore:better log messages
h1divp Jan 11, 2026
26fa847
Merge pull request #267 from swamphacks/feat/waitlist-transition
h1divp Jan 12, 2026
2e11071
feat:added dev asynqmon to docker compose and caddy config
h1divp Jan 12, 2026
66ed77e
Merge pull request #268 from swamphacks/feat/infra-asynq-web-ui
h1divp Jan 12, 2026
629cc9b
fix:port (unsure)
h1divp Jan 12, 2026
a6ac8fd
Merge pull request #269 from swamphacks/feat/infra-asynq-web-ui
h1divp Jan 12, 2026
8b16d99
fix: added gh workflow
h1divp Jan 12, 2026
3a92d0e
Merge pull request #270 from swamphacks/feat/infra-asynq-web-ui
h1divp Jan 12, 2026
e612879
fix: removed orphaned line
h1divp Jan 12, 2026
80bc235
Merge pull request #271 from swamphacks/feat/infra-asynq-web-ui
h1divp Jan 12, 2026
7be94e0
fix: port
h1divp Jan 12, 2026
46c6edd
Merge pull request #272 from swamphacks/feat/infra-asynq-web-ui
h1divp Jan 12, 2026
43d1fcc
possible port fix?
h1divp Jan 12, 2026
44a4b3b
fix: asynqmon port; feat: gh workflow for caddy deployment
h1divp Jan 12, 2026
9eeef42
Merge pull request #273 from swamphacks/feat/infra-asynq-web-ui
h1divp Jan 12, 2026
7b85123
fix:redis port
h1divp Jan 12, 2026
131f381
Merge pull request #274 from swamphacks/feat/infra-asynq-web-ui
h1divp Jan 12, 2026
6306ab7
feat:prod asynqmon
h1divp Jan 12, 2026
8fa8d4d
feat:prod asynqmon deployment gh workflow
h1divp Jan 12, 2026
f37dda1
Merge pull request #275 from swamphacks/feat/infra-asynq-web-ui
h1divp Jan 12, 2026
ccfde38
hotfix:highlight in red
h1divp Jan 12, 2026
e425159
Merge pull request #276 from swamphacks/hotfix/waitlist-email-change
h1divp Jan 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/deploy-caddy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Deploy Caddy

on:
workflow_dispatch:

permissions:
contents: read
packages: write

jobs:
deploy:
name: Deploy to Production Server
runs-on: ubuntu-latest

steps:
- name: SSH proxy command
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.API_HOST }}
username: root
password: ${{ secrets.API_PASSWORD }}
script: |
cd /root/core/infra
git fetch
git checkout dev
git reset --hard origin/dev
git pull

export INFISICAL_TOKEN=$(infisical login \
--method=universal-auth \
--client-id='${{ secrets.INFISICAL_CLIENT_ID }}' \
--client-secret='${{ secrets.INFISICAL_CLIENT_SECRET }}' \
--silent \
--plain)

infisical export \
--token=$INFISICAL_TOKEN \
--env=dev \
--format=dotenv \
--path="/api" \
--projectId='${{ secrets.INFISICAL_PROJECT_ID }}' \
> ./secrets/.env.dev.api

docker compose -f docker-compose.api.yml pull caddy
docker compose -f docker-compose.api.yml up -d --no-deps --force-recreate caddy
30 changes: 30 additions & 0 deletions .github/workflows/dev-deploy-asynqmon.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Deploy Asynqmon to Development

on:
workflow_dispatch:

permissions:
contents: read
packages: write

jobs:
deploy:
name: Deploy to Development Server
runs-on: ubuntu-latest

steps:
- name: SSH proxy commmand
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.API_HOST}}
username: root
password: ${{ secrets.API_PASSWORD}}
script: |
cd /root/core/infra
git fetch
git checkout dev
git reset --hard origin/dev
git pull

docker compose -f docker-compose.api.yml pull asynqmon-dev
docker compose -f docker-compose.api.yml up -d --no-deps --force-recreate asynqmon-dev
30 changes: 30 additions & 0 deletions .github/workflows/prod-deploy-asynqmon.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Deploy Asynqmon to Production

on:
workflow_dispatch:

permissions:
contents: read
packages: write

jobs:
deploy:
name: Deploy to Production Server
runs-on: ubuntu-latest

steps:
- name: SSH proxy commmand
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.API_HOST}}
username: root
password: ${{ secrets.API_PASSWORD}}
script: |
cd /root/core/infra
git fetch
git checkout dev
git reset --hard origin/dev
git pull

docker compose -f docker-compose.api.yml pull asynqmon
docker compose -f docker-compose.api.yml up -d --no-deps --force-recreate asynqmon
5 changes: 5 additions & 0 deletions apps/api/.env.dev.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ DATABASE_URL_MIGRATION="postgres://postgres:postgres@localhost:5432/coredb"
REDIS_URL="redis://redis:6379"
ALLOWED_ORIGINS="" # URLs in comma seperated list

# Application waitlist
MAX_ACCEPTED_APPLICATIONS=500
ACCEPT_FROM_WAITLIST_COUNT=50
ACCEPT_FROM_WAITLIST_PERIOD="@every 72h"

# For OAuth
AUTH_DISCORD_CLIENT_ID=
AUTH_DISCORD_CLIENT_SECRET=
Expand Down
24 changes: 21 additions & 3 deletions apps/api/cmd/BAT_worker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,19 @@ func main() {
},
)

schedulerLocation, err := time.LoadLocation("America/New_York")
if err != nil {
panic(err)
}
scheduler := asynq.NewScheduler(
redisOpt,
&asynq.SchedulerOpts{
Location: schedulerLocation,
},
)

taskQueueClient := asynq.NewClient(redisOpt)

database := db.NewDB(cfg.DatabaseURL)
defer database.Close()

Expand All @@ -64,16 +77,21 @@ func main() {
applicationRepo := repository.NewApplicationRepository(database)
eventRepo := repository.NewEventRespository(database)
userRepo := repository.NewUserRepository(database)
eventService := services.NewEventService(eventRepo, userRepo, nil, nil, logger)
batRunsRepo := repository.NewBatRunsRepository(database)

sesClient := email.NewSESClient(cfg.AWS.AccessKey, cfg.AWS.AccessKeySecret, cfg.AWS.Region, logger)
emailService := services.NewEmailService(nil, sesClient, logger)
batService := services.NewBatService(applicationRepo, eventRepo, userRepo, batRunsRepo, emailService, txm, nil, logger)
emailService := services.NewEmailService(taskQueueClient, sesClient, 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)

BATWorker := workers.NewBATWorker(batService, logger)
BATWorker := workers.NewBATWorker(batService, applicationService, scheduler, taskQueueClient, logger)

mux := asynq.NewServeMux()
mux.HandleFunc(tasks.TypeCalculateAdmissions, BATWorker.HandleCalculateAdmissionsTask)
mux.HandleFunc(tasks.TypeTransitionWaitlist, BATWorker.HandleTransitionWaitlistTask)
mux.HandleFunc(tasks.TypeScheduleTransitionWaitlist, BATWorker.HandleScheduleTransitionWaitlistTask)
mux.HandleFunc(tasks.TypeShutdownScheduler, BATWorker.HandleShutdownScheduler)

if err := srv.Run(mux); err != nil {
logger.Fatal().Msg("Failed to run BAT worker")
Expand Down
4 changes: 2 additions & 2 deletions apps/api/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ func main() {
eventInterestService := services.NewEventInterestService(eventInterestRepo, logger)
eventService := services.NewEventService(eventRepo, userRepo, r2Client, &cfg.CoreBuckets, logger)
emailService := services.NewEmailService(taskQueueClient, sesClient, logger)
applicationService := services.NewApplicationService(applicationRepo, eventService, emailService, txm, r2Client, &cfg.CoreBuckets, 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, logger)
batService := services.NewBatService(applicationRepo, eventRepo, userRepo, batRunsRepo, emailService, txm, taskQueueClient, nil, logger)

// Injections into handlers
apiHandlers := handlers.NewHandlers(authService, userService, eventInterestService, eventService, emailService, applicationService, teamService, batService, cfg, logger)
Expand Down
1 change: 0 additions & 1 deletion apps/api/cmd/email_worker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ func main() {

mux := asynq.NewServeMux()

mux.HandleFunc(tasks.TypeSendConfirmationEmail, emailWorker.HandleSendConfirmationEmailTask)
mux.HandleFunc(tasks.TypeSendHtmlEmail, emailWorker.HandleSendHtmlEmailTask)

wd, err := os.Getwd()
Expand Down
9 changes: 6 additions & 3 deletions apps/api/internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
fmt.Printf("%v", err)
}

fmt.Fprintln(w, htmlContent)

Check failure on line 77 in apps/api/internal/api/api.go

View workflow job for this annotation

GitHub Actions / API Lint

Error return value of `fmt.Fprintln` is not checked (errcheck)
})

// Health check
Expand Down Expand Up @@ -122,9 +122,6 @@

// --- Event routes ---
api.Router.Route("/events", func(r chi.Router) {
r.Post("/{eventId}/calc-admissions", api.Handlers.Admission.HandleCalculateAdmissionsRequest)
r.Post("/{eventId}/reviews/bat-runs/{runId}/release", api.Handlers.Admission.ReleaseDecisions)

// Superuser-only
r.With(mw.Auth.RequireAuth, ensureSuperuser).Post("/", api.Handlers.Event.CreateEvent)

Expand All @@ -143,6 +140,12 @@
r.With(ensureEventStaff).Get("/overview", api.Handlers.Event.GetEventOverview)

// Admin-only
r.With(ensureEventAdmin).Post("/queue-confirmation-email", api.Handlers.Email.QueueConfirmationEmail)
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)
r.With(ensureEventAdmin).Post("/shutdown-waitlist-scheduler", api.Handlers.Bat.QueueShutdownWaitlistSchedulerTask)
r.With(ensureEventAdmin).Post("/reviews/bat-runs/{runId}/release", api.Handlers.Admission.ReleaseDecisions)
r.With(ensureEventAdmin).Patch("/", api.Handlers.Event.UpdateEventById)
r.With(ensureEventAdmin).Post("/banner", api.Handlers.Event.UploadEventBanner)
r.With(ensureEventAdmin).Delete("/banner", api.Handlers.Event.DeleteBanner)
Expand Down
31 changes: 30 additions & 1 deletion apps/api/internal/api/handlers/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@
return
}

defer resumeFile.Close()

Check failure on line 187 in apps/api/internal/api/handlers/application.go

View workflow job for this annotation

GitHub Actions / API Lint

Error return value of `resumeFile.Close` is not checked (errcheck)

resumeFileBuffer := bytes.NewBuffer(nil)

Expand Down Expand Up @@ -652,7 +652,7 @@
// @Tags Application Event
//
// @Param eventId path string true "ID of the event to join the waitlist for"
// @Success 200 "Acceptance withdrawn joined successfully"
// @Success 200 "Acceptance successful"
// @Failure 400 {object} res.ErrorResponse "Bad request: invalid event ID"
// @Failure 500 {object} res.ErrorResponse "Server error: failed to accept"
// @Router /events/{eventId}/application/accept-acceptance [patch]
Expand All @@ -672,3 +672,32 @@

w.WriteHeader(http.StatusOK)
}

// Transition waitlisted applications
//
// @Summary Sets application status from accepted to rejected
// @Description Transitions all accepted users to waitlist, and accepts 50 from the waitlist.
// @Tags Application Event
//
// @Param eventId path string true "ID of the event to join the waitlist for"
// @Success 200 "Transitioned application statuses successfully"
// @Failure 400 {object} res.ErrorResponse "Bad request: invalid event ID"
// @Failure 500 {object} res.ErrorResponse "Server error: failed to transition application statuses"
// @Router /events/{eventId}/application/transition-waitlisted-applications [patch]
func (h *ApplicationHandler) TransitionWaitlistedApplications(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 a valid."))
return
}

var acceptanceCount uint32 = 50
var acceptanceQuota uint32 = 500
err = h.appService.TransitionWaitlistedApplications(r.Context(), eventId, acceptanceCount, acceptanceQuota)
if err != nil {
res.SendError(w, http.StatusInternalServerError, res.NewError("transition-waitlisted-applications-error", "Something went wrong while transitioning waitlisted applications."))
return
}

w.WriteHeader(http.StatusOK)
}
51 changes: 48 additions & 3 deletions apps/api/internal/api/handlers/bat.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/rs/zerolog"
res "github.com/swamphacks/core/apps/api/internal/api/response"
"github.com/swamphacks/core/apps/api/internal/services"
"github.com/swamphacks/core/apps/api/internal/web"
)

type BatHandler struct {
Expand Down Expand Up @@ -116,10 +117,10 @@ func (h *BatHandler) CheckApplicationReviewsComplete(w http.ResponseWriter, r *h
}
}

// Delete an event
// Delete a run
//
// @Summary Delete an event
// @Description Delete an existing event
// @Summary Delete a run
// @Description Delete an existing BAT run
// @Tags Bat
// @Accept json
// @Produce json
Expand Down Expand Up @@ -151,3 +152,47 @@ func (h *BatHandler) DeleteRunById(w http.ResponseWriter, r *http.Request) {

w.WriteHeader(http.StatusNoContent)
}

// Queue transition waitlist task
//
// @Summary Queues a waitlist transition task
// @Description Queues an asynq task that transitions waitlisted applications, running every 3 days.
// @Tags
//
// @Param eventId path string true "ID of the event to join the waitlist for"
// @Success 200 "Transitioned application statuses successfully"
// @Failure 400 {object} res.ErrorResponse "Bad request: invalid event ID"
// @Failure 500 {object} res.ErrorResponse "Server error: failed to transition application statuses"
// @Router /events/{eventId}/queue-transition-waitlist-task [post]
func (h *BatHandler) QueueScheduleWaitlistTransitionTask(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.QueueScheduleWaitlistTransitionTask(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)
}

// Queue Shutdown scheduler task
//
// @Summary Shutsdown an asynq scheduler
// @Description Shutsdown the scheduler used for the waitlist transition task. Error returned through logs if a scheduler is not active.
// @Tags
//
// @Success 200 "Scheduler shutdown successfully"
// @Failure 500 {object} res.ErrorResponse "Server error: failed to shutdown scheduler"
// @Router /events/{eventId}/queue-transition-waitlist-task [post]
func (h *BatHandler) QueueShutdownWaitlistSchedulerTask(w http.ResponseWriter, r *http.Request) {
err := h.BatService.QueueShutdownWaitlistScheduler()
if err != nil {
res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Failed to shutdown scheduler."))
}

res.Send(w, http.StatusOK, nil)
}
32 changes: 28 additions & 4 deletions apps/api/internal/api/handlers/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"net/http"

"github.com/go-playground/validator/v10"
"github.com/rs/zerolog"
res "github.com/swamphacks/core/apps/api/internal/api/response"
"github.com/swamphacks/core/apps/api/internal/email"
Expand All @@ -27,10 +28,6 @@ type QueueTextEmailRequest struct {
Subject string `json:"subject"`
Body string `json:"body"`
}
type QueueConfirmationEmailRequest struct {
To string `json:"to"`
Name string `json:"name"`
}

// Queue an Email Request
//
Expand Down Expand Up @@ -79,3 +76,30 @@ func (h *EmailHandler) QueueTextEmail(w http.ResponseWriter, r *http.Request) {

w.WriteHeader(http.StatusCreated)
}

type QueueConfirmationEmailFields struct {
Email string `json:"email" validate:"required"`
FirstName string `json:"firstName" validate:"required"`
}

func (h *EmailHandler) QueueConfirmationEmail(w http.ResponseWriter, r *http.Request) {
var req QueueConfirmationEmailFields
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()))
}

err = h.emailService.QueueConfirmationEmail(req.Email, req.FirstName)
if err != nil {
res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Confirmation email could not be queued."))
}

res.Send(w, http.StatusOK, nil)
}
16 changes: 10 additions & 6 deletions apps/api/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type CloudflareConfig struct {
AccessKeySecret string `env:"ACCESS_KEY_SECRET"`
}

// TODO: deprecate.
type SmtpConfig struct {
Username string `env:"USERNAME"`
Password string `env:"PASSWORD"`
Expand All @@ -54,12 +55,15 @@ type CoreBuckets struct {
}

type Config struct {
DatabaseURL string `env:"DATABASE_URL"`
RedisURL string `env:"REDIS_URL"`
Port string `env:"PORT" envDefault:"8080"`
AllowedOriginsString string `env:"ALLOWED_ORIGINS"`
EmailTemplateDirectory string `env:"EMAIL_TEMPLATE_DIRECTORY"`
AllowedOrigins []string ``
DatabaseURL string `env:"DATABASE_URL"`
RedisURL string `env:"REDIS_URL"`
Port string `env:"PORT" envDefault:"8080"`
AllowedOriginsString string `env:"ALLOWED_ORIGINS"`
EmailTemplateDirectory string `env:"EMAIL_TEMPLATE_DIRECTORY"`
AllowedOrigins []string ``
MaxAcceptedApplications uint32 `env:"MAX_ACCEPTED_APPLICATIONS"`
AcceptFromWaitlistCount uint32 `env:"ACCEPT_FROM_WAITLIST_COUNT"`
AcceptFromWaitlistPeriod string `env:"ACCEPT_FROM_WAITLIST_PERIOD"`

Auth AuthConfig `envPrefix:"AUTH_"`
Cookie CookieConfig `envPrefix:"COOKIE_"`
Expand Down
Loading
Loading