Skip to content
Merged

aaa #360

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2d6a146
feat: implement EmailCampaignRepository with CRUD operations (#351)
williamchiii May 24, 2026
5e1a838
init front page
hieunguyent12 May 25, 2026
e7cb424
started working on info modal and other styling tweaks
hieunguyent12 May 26, 2026
573d99e
teams tab and more changes
hieunguyent12 May 26, 2026
bd7521e
add Sponsors and Partners components with modal functionality
hieunguyent12 May 26, 2026
f699ff2
Adds the email campaign service layer inside the existing email domai…
williamchiii Jun 2, 2026
1a6bda7
feat: add email campaign HTTP handlers (#353)
williamchiii Jun 4, 2026
9c3b287
Applicant Views (Accepted, Rejected, Waitlisted) (#354)
hieunguyent12 Jun 4, 2026
6b62e05
fix pnpm install fail inside docker
hieunguyent12 Jun 4, 2026
022ad9c
fix for pnpm install
hieunguyent12 Jun 4, 2026
6d14fb3
maybe this will work
hieunguyent12 Jun 4, 2026
84cee8e
surely this time it'll work
hieunguyent12 Jun 4, 2026
5c3860e
4th time is the charm
hieunguyent12 Jun 4, 2026
1eda687
aaaaa
hieunguyent12 Jun 4, 2026
1caf182
this should be it
hieunguyent12 Jun 4, 2026
b1667b1
confirm attendance, withdraw applications and small changes to settin…
hieunguyent12 Jun 5, 2026
34f7d56
feat/resume updating for applicants (#356)
williamchiii Jun 7, 2026
b2cbcc3
feat: register email campaign CRUD API and fix enum array handling (#…
williamchiii Jun 11, 2026
ce054ee
Adds unit tests for the email campaign service’s validation and lifec…
williamchiii Jun 21, 2026
59423fd
Hieu/applications review (#359)
hieunguyent12 Jun 22, 2026
ae5088e
added mlh badge and link to code of conduct
hieunguyent12 Jun 22, 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
1 change: 1 addition & 0 deletions apps/api/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.env
tmp
docs/openapi.json
149 changes: 65 additions & 84 deletions apps/api/cmd/BAT_worker/main.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,5 @@
package main

import (
"time"

"github.com/hibiken/asynq"
"github.com/swamphacks/core/apps/api/internal/config"
"github.com/swamphacks/core/apps/api/internal/database"
"github.com/swamphacks/core/apps/api/internal/database/repository"
"github.com/swamphacks/core/apps/api/internal/domains/application"
"github.com/swamphacks/core/apps/api/internal/domains/bat"
"github.com/swamphacks/core/apps/api/internal/domains/email"
"github.com/swamphacks/core/apps/api/internal/domains/hackathon"
"github.com/swamphacks/core/apps/api/internal/emailutils"
"github.com/swamphacks/core/apps/api/internal/logger"
"github.com/swamphacks/core/apps/api/internal/storage"
"github.com/swamphacks/core/apps/api/internal/tasks"
"github.com/swamphacks/core/apps/api/internal/workers"
)

/*
-. .-
_..-'( )`-.._
Expand All @@ -37,70 +19,69 @@ V V V }' `\ /' `{ V V V
*/

func main() {
logger := logger.New()
cfg := config.LoadConfig()

redisOpt, err := asynq.ParseRedisURI(cfg.RedisURL)
if err != nil {
logger.Fatal().Msg("failed to parse REDIS_URL")
}

srv := asynq.NewServer(
redisOpt,
asynq.Config{
Concurrency: 1,
Queues: map[string]int{
"bat": 1,
},
TaskCheckInterval: 10 * time.Second,
DelayedTaskCheckInterval: time.Minute,
HealthCheckInterval: 2 * time.Minute,
JanitorInterval: time.Hour,
JanitorBatchSize: 100,
},
)

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

taskQueueClient := asynq.NewClient(redisOpt)
defer taskQueueClient.Close()

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

txm := database.NewTransactionManager(db)

hackathonRepo := repository.NewHackathonRepository(db)
applicationRepo := repository.NewApplicationRepository(db)
userRepo := repository.NewUserRepository(db)
batRunsRepo := repository.NewBatRunsRepository(db)
eventInterestsRepo := repository.NewEventInterestsRepository(db)

sesClient := emailutils.NewSESClient(cfg.AWS.AccessKey, cfg.AWS.AccessKeySecret, cfg.AWS.Region, logger)
emailService := email.NewEmailService(hackathonRepo, userRepo, taskQueueClient, sesClient, nil, logger, cfg)
batService := bat.NewBatService(applicationRepo, hackathonRepo, userRepo, batRunsRepo, emailService, txm, nil, scheduler, cfg, logger)
applicationService := application.NewService(applicationRepo, userRepo, hackathonRepo, txm, nil, nil, scheduler, emailService, batService, cfg, logger)
hackathonService := hackathon.NewService(hackathonRepo, userRepo, eventInterestsRepo, &storage.R2Client{}, nil, logger)

BATWorker := workers.NewBATWorker(batService, applicationService, hackathonService, scheduler, taskQueueClient, cfg, 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")
}

// logger := logger.New()
// cfg := config.LoadConfig()

// redisOpt, err := asynq.ParseRedisURI(cfg.RedisURL)
// if err != nil {
// logger.Fatal().Msg("failed to parse REDIS_URL")
// }

// srv := asynq.NewServer(
// redisOpt,
// asynq.Config{
// Concurrency: 1,
// Queues: map[string]int{
// "bat": 1,
// },
// TaskCheckInterval: 10 * time.Second,
// DelayedTaskCheckInterval: time.Minute,
// HealthCheckInterval: 2 * time.Minute,
// JanitorInterval: time.Hour,
// JanitorBatchSize: 100,
// },
// )

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

// taskQueueClient := asynq.NewClient(redisOpt)
// defer taskQueueClient.Close()

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

// txm := database.NewTransactionManager(db)

// hackathonRepo := repository.NewHackathonRepository(db)
// applicationRepo := repository.NewApplicationRepository(db)
// userRepo := repository.NewUserRepository(db)
// batRunsRepo := repository.NewBatRunsRepository(db)
// eventInterestsRepo := repository.NewEventInterestsRepository(db)

// sesClient := emailutils.NewSESClient(cfg.AWS.AccessKey, cfg.AWS.AccessKeySecret, cfg.AWS.Region, logger)
// emailService := email.NewEmailService(hackathonRepo, userRepo, taskQueueClient, sesClient, nil, logger, cfg)
// batService := bat.NewBatService(applicationRepo, hackathonRepo, userRepo, batRunsRepo, emailService, txm, nil, scheduler, cfg, logger)
// applicationService := application.NewService(applicationRepo, userRepo, hackathonRepo, txm, nil, nil, scheduler, emailService, batService, cfg, logger)
// hackathonService := hackathon.NewService(hackathonRepo, userRepo, eventInterestsRepo, &storage.R2Client{}, nil, logger)

// BATWorker := workers.NewBATWorker(batService, applicationService, hackathonService, scheduler, taskQueueClient, cfg, 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")
// }
}
2 changes: 1 addition & 1 deletion apps/api/cmd/samples/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func main() {

hackathonRepo := repository.NewHackathonRepository(db)

appOpenTime := time.Date(2026, 5, 26, 19, 13, 20, 0, time.UTC)
appOpenTime := time.Date(2026, 4, 26, 19, 13, 20, 0, time.UTC)
appCloseTime := time.Date(2026, 6, 26, 19, 13, 20, 0, time.UTC)
earlyAppOpenTime := time.Date(2026, 4, 20, 19, 13, 20, 0, time.UTC)
earlyAppCloseTime := time.Date(2026, 4, 26, 19, 13, 20, 0, time.UTC)
Expand Down
1 change: 0 additions & 1 deletion apps/api/docs/openapi.json

This file was deleted.

16 changes: 9 additions & 7 deletions apps/api/internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"github.com/swamphacks/core/apps/api/internal/database/repository"
"github.com/swamphacks/core/apps/api/internal/domains/application"
"github.com/swamphacks/core/apps/api/internal/domains/auth"
"github.com/swamphacks/core/apps/api/internal/domains/bat"
"github.com/swamphacks/core/apps/api/internal/domains/email"
"github.com/swamphacks/core/apps/api/internal/domains/hackathon"
"github.com/swamphacks/core/apps/api/internal/domains/redeemables"
Expand Down Expand Up @@ -80,7 +79,7 @@ func Run() {
}))

humaConfig := huma.DefaultConfig("SwampHacks API", "1.0.0")
humaConfig.DocsRenderer = huma.DocsRendererScalar
humaConfig.DocsRenderer = huma.DocsRendererSwaggerUI
humaConfig.CreateHooks = nil

// TODO: figure out a way to override the default schema name
Expand Down Expand Up @@ -111,14 +110,13 @@ func Run() {
accountRepo := repository.NewAccountRespository(db)
sessionRepo := repository.NewSessionRepository(db)
hackathonRepo := repository.NewHackathonRepository(db)
applicationRepo := repository.NewApplicationRepository(db)
teamRepo := repository.NewTeamRespository(db)
teamMemberRepo := repository.NewTeamMemberRespository(db)
teamJoinRequestRepo := repository.NewTeamJoinRequestRepository(db)
redeemablesRepo := repository.NewRedeemablesRepository(db)
batRunsRepo := repository.NewBatRunsRepository(db)
eventInterestsRepo := repository.NewEventInterestsRepository(db)
workshopRepo := repository.NewWorkshopsRepository(db)
emailCampaignRepo := repository.NewEmailCampaignRepository(db)

mw := mw.NewMiddleware(userRepo, db, logger, config)

Expand All @@ -139,9 +137,13 @@ func Run() {
emailHandler := email.NewHandler(emailService, logger)
email.RegisterRoutes(emailHandler, huma.NewGroup(api, "/email"), mw)

batService := bat.NewBatService(applicationRepo, hackathonRepo, userRepo, batRunsRepo, emailService, txm, taskQueueClient, nil, config, logger)
applicationService := application.NewService(applicationRepo, userRepo, hackathonRepo, txm, r2Client, &config.CoreBuckets, nil, emailService, batService, config, logger)
applicationHandler := application.NewHandler(applicationService, batService, config, logger)
emailCampaignService := email.NewEmailCampaignService(emailCampaignRepo, logger)
emailCampaignHandler := email.NewCampaignHandler(emailCampaignService, logger)
email.RegisterCampaignRoutes(emailCampaignHandler, huma.NewGroup(api, "/email"), mw)

// batService := bat.NewBatService(applicationRepo, hackathonRepo, userRepo, batRunsRepo, emailService, txm, taskQueueClient, nil, config, logger)
applicationService := application.NewService(db, txm, r2Client, &config.CoreBuckets, nil, emailService, config, logger)
applicationHandler := application.NewHandler(applicationService, config, logger)
application.RegisterRoutes(applicationHandler, huma.NewGroup(api, "/application"), mw)

teamService := teams.NewService(teamRepo, teamMemberRepo, teamJoinRequestRepo, hackathonRepo, userRepo, txm, logger)
Expand Down
23 changes: 13 additions & 10 deletions apps/api/internal/api/middleware/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ type UserContext struct {
Rfid *string `json:"rfid"`

CheckedInAt *time.Time `json:"checkedInAt"`

HasSeeNewApplicationStatus *bool `json:"hasSeenNewApplicationStatus"`
}

type SessionContext struct {
Expand Down Expand Up @@ -191,16 +193,17 @@ func (m *AuthMiddleware) RequireAuth(next http.Handler) http.Handler {

// TODO: I don't think we need UserContext here, just return sqlc.User directly
userContext := UserContext{
UserID: user.UserID,
Name: user.Name,
Email: user.Email,
PreferredEmail: user.PreferredEmail,
Image: user.Image,
Onboarded: user.Onboarded,
Role: user.Role,
EmailConsent: user.EmailConsent,
Rfid: user.Rfid,
CheckedInAt: user.CheckedInAt,
UserID: user.UserID,
Name: user.Name,
Email: user.Email,
PreferredEmail: user.PreferredEmail,
Image: user.Image,
Onboarded: user.Onboarded,
Role: user.Role,
EmailConsent: user.EmailConsent,
Rfid: user.Rfid,
CheckedInAt: user.CheckedInAt,
HasSeeNewApplicationStatus: user.HasSeenNewApplicationStatus,
}

sessionContext := SessionContext{
Expand Down
51 changes: 50 additions & 1 deletion apps/api/internal/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package database

import (
"context"
"errors"
"log"

"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/swamphacks/core/apps/api/internal/database/sqlc"
)
Expand All @@ -14,7 +17,16 @@ type DB struct {
}

func NewDB(connStr string) *DB {
pool, err := pgxpool.New(context.Background(), connStr)
poolConfig, err := pgxpool.ParseConfig(connStr)
if err != nil {
log.Fatal(err)
}

poolConfig.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
return registerCustomTypes(ctx, conn)
}

pool, err := pgxpool.NewWithConfig(context.Background(), poolConfig)
if err != nil {
log.Fatal(err)
}
Expand All @@ -29,6 +41,43 @@ func NewDB(connStr string) *DB {
return &db
}

func (d *DB) NewTX(tx pgx.Tx) *DB {
txDB := DB{
Pool: d.Pool,
Query: sqlc.New(tx),
}

return &txDB
}

func (d *DB) Close() {
d.Pool.Close()
}

func registerCustomTypes(ctx context.Context, conn *pgx.Conn) error {
typeNames := []string{
"email_campaign_format",
"email_campaign_status",
"email_recipient_type",
"_email_recipient_type",
}

for _, typeName := range typeNames {
dataType, err := conn.LoadType(ctx, typeName)
if isUndefinedType(err) {
continue
}
if err != nil {
return err
}

conn.TypeMap().RegisterType(dataType)
}

return nil
}

func isUndefinedType(err error) bool {
var pgErr *pgconn.PgError
return errors.As(err, &pgErr) && pgErr.Code == "42704"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- +goose Up
alter table users add has_seen_new_application_status boolean;

-- +goose Down
alter table users drop column has_seen_new_application_status;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- +goose Up
ALTER TYPE application_status
ADD VALUE IF NOT EXISTS 'confirmed';

-- +goose Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-- +goose Up
alter table applications drop column if exists waitlist_join_time;

alter table applications add id uuid not null default gen_random_uuid();

alter table applications drop constraint applications_pkey;

alter table applications add primary key (id);

alter table applications add constraint one_application_per_user unique (user_id, hackathon_id);

-- +goose Down
alter table applications add waitlist_join_time timestamptz;

alter table applications drop constraint one_application_per_user;

alter table applications drop constraint applications_pkey;

alter table applications drop column if exists id;

alter table applications add primary key (user_id);
Loading
Loading