Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion apps/api/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,10 @@ func main() {
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)
discordService := services.NewDiscordService(eventRepo, logger)

// Injections into handlers
apiHandlers := handlers.NewHandlers(authService, userService, eventInterestService, eventService, emailService, applicationService, teamService, batService, cfg, logger)
apiHandlers := handlers.NewHandlers(authService, userService, eventInterestService, eventService, emailService, applicationService, teamService, batService, discordService, cfg, logger)

api := api.NewAPI(&logger, apiHandlers, mw)

Expand Down
7 changes: 7 additions & 0 deletions apps/api/internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ func (api *API) setupRoutes(mw *mw.Middleware) {
r.Post("/join/{requestId}/reject", api.Handlers.Teams.RejectTeamJoinRequest)
})

// --- Discord routes (for Discord bot) ---
api.Router.Route("/discord", func(r chi.Router) {
r.Use(mw.Auth.RequireAuth)
r.Get("/event/{event_id}/attendees", api.Handlers.Discord.GetEventAttendeesWithDiscord)
})

// --- Event routes ---
api.Router.Route("/events", func(r chi.Router) {
// Superuser-only
Expand All @@ -136,6 +142,7 @@ func (api *API) setupRoutes(mw *mw.Middleware) {

r.Get("/", api.Handlers.Event.GetEventByID)
r.Get("/role", api.Handlers.Event.GetEventRole)
r.Get("/discord/{discordId}", api.Handlers.Discord.GetUserEventRoleByDiscordIDAndEventId)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a later refactor, we should probably move this to GET /discord/account/{discordId}/ under the Discord Routes section


r.With(ensureEventStaff).Get("/overview", api.Handlers.Event.GetEventOverview)

Expand Down
111 changes: 111 additions & 0 deletions apps/api/internal/api/handlers/discord.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package handlers
import (
"encoding/json"
"net/http"

"github.com/go-chi/chi/v5"
"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/services"
)

type DiscordHandler struct {
discordService *services.DiscordService
logger zerolog.Logger
}

func NewDiscordHandler(discordService *services.DiscordService, logger zerolog.Logger) *DiscordHandler {
return &DiscordHandler{
discordService: discordService,
logger: logger.With().Str("handler", "DiscordHandler").Str("component", "discord").Logger(),
}
}

// GetEventAttendeesWithDiscord
//
// @Summary Get event attendees with Discord IDs
// @Description Get all attendees for an event who have Discord accounts linked
// @Tags Discord
// @Param event_id path string true "Event ID (UUID)"
// @Success 200 {array} sqlc.GetEventAttendeesWithDiscordRow "List of attendees with Discord IDs"
// @Failure 400 {object} response.ErrorResponse "Invalid event ID"
// @Failure 500 {object} response.ErrorResponse "Internal server error"
// @Router /discord/event/{event_id}/attendees [get]
func (h *DiscordHandler) GetEventAttendeesWithDiscord(w http.ResponseWriter, r *http.Request) {
eventIDStr := chi.URLParam(r, "event_id")
if eventIDStr == "" {
res.SendError(w, http.StatusBadRequest, res.NewError("bad_request", "event_id is required"))
return
}

eventID, err := uuid.Parse(eventIDStr)
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("bad_request", "invalid event ID format"))
return
}

attendees, err := h.discordService.GetEventAttendeesWithDiscord(r.Context(), eventID)
if err != nil {
h.logger.Err(err).Msg("failed to get event attendees with discord")
res.SendError(w, http.StatusInternalServerError, res.NewError("internal_error", "Failed to get attendees"))
return
}

w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(attendees); err != nil {
h.logger.Err(err).Msg("failed to encode response")
res.SendError(w, http.StatusInternalServerError, res.NewError("internal_error", "Failed to encode response"))
return
}
}

// GetUserEventRoleByDiscordIDAndEventId
//
// @Summary Get user event role by Discord ID and Event ID
// @Description Get the event role for a user based on their Discord account ID and a specific event ID
// @Tags Discord
// @Param eventId path string true "Event ID (UUID)"
// @Param discordId path string true "Discord account ID"
// @Success 200 {object} map[string]interface{} "role"
// @Failure 400 {object} response.ErrorResponse "Invalid event ID or discord ID"
// @Failure 404 {object} response.ErrorResponse "User or role not found"
// @Failure 500 {object} response.ErrorResponse "Internal server error"
// @Router /events/{eventId}/discord/{discordId} [get]
func (h *DiscordHandler) GetUserEventRoleByDiscordIDAndEventId(w http.ResponseWriter, r *http.Request) {
eventIDStr := chi.URLParam(r, "eventId")
if eventIDStr == "" {
res.SendError(w, http.StatusBadRequest, res.NewError("bad_request", "eventId is required"))
return
}

eventID, err := uuid.Parse(eventIDStr)
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("bad_request", "invalid event ID format"))
return
}

discordID := chi.URLParam(r, "discordId")
if discordID == "" {
res.SendError(w, http.StatusBadRequest, res.NewError("bad_request", "discordId is required"))
return
}

role, err := h.discordService.GetUserEventRoleByDiscordIDAndEventId(r.Context(), discordID, eventID)
if err != nil {
if err == services.ErrNoEventRole {
res.SendError(w, http.StatusNotFound, res.NewError("not_found", err.Error()))
return
}
h.logger.Err(err).Msg("failed to get user event role")
res.SendError(w, http.StatusInternalServerError, res.NewError("internal_error", "Failed to get user role"))
return
}

response := map[string]interface{}{
"role": role,
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
3 changes: 3 additions & 0 deletions apps/api/internal/api/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Handlers struct {
Teams *TeamHandler
Admission *AdmissionHandler
Bat *BatHandler
Discord *DiscordHandler
}

func NewHandlers(
Expand All @@ -27,6 +28,7 @@ func NewHandlers(
appService *services.ApplicationService,
teamService *services.TeamService,
batService *services.BatService,
discordService *services.DiscordService,
cfg *config.Config,
logger zerolog.Logger,
) *Handlers {
Expand All @@ -40,5 +42,6 @@ func NewHandlers(
Teams: NewTeamHandler(teamService, logger),
Admission: NewAdmissionHandler(batService, logger),
Bat: NewBatHandler(batService, logger),
Discord: NewDiscordHandler(discordService, logger),
}
}
5 changes: 5 additions & 0 deletions apps/api/internal/db/queries/accounts.sql
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,8 @@ WHERE provider_id = $1 AND account_id = $2;
-- name: DeleteAccount :exec
DELETE FROM auth.accounts
WHERE provider_id = $1 AND account_id = $2;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this and its repo function isn't being used anywhere it should be removed, but since we are low on time im okay with doing that in a refactor later

-- name: GetUserIDByDiscordAccountID :one
SELECT user_id
FROM auth.accounts
WHERE provider_id = 'discord' AND account_id = $1;
20 changes: 20 additions & 0 deletions apps/api/internal/db/queries/event_roles.sql
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@ UPDATE event_roles
SET role = $3
WHERE event_id = $1 AND user_id = $2;

-- name: GetEventAttendeesWithDiscord :many
SELECT
a.account_id as discord_id,
u.id as user_id,
u.name,
u.email
FROM auth.users u
JOIN event_roles er ON u.id = er.user_id
JOIN auth.accounts a ON u.id = a.user_id
WHERE er.event_id = $1
AND er.role = 'attendee'
AND a.provider_id = 'discord';

-- name: GetEventRoleByDiscordIDAndEventId :one
SELECT er.event_id, er.role
FROM event_roles er
JOIN auth.accounts a ON er.user_id = a.user_id
WHERE a.provider_id = 'discord'
AND a.account_id = $1
AND er.event_id = $2;
-- name: UpdateEventRoleByIds :exec
UPDATE event_roles
SET
Expand Down
17 changes: 17 additions & 0 deletions apps/api/internal/db/repository/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ package repository

import (
"context"
"errors"

"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/swamphacks/core/apps/api/internal/db"
"github.com/swamphacks/core/apps/api/internal/db/sqlc"
)

var (
ErrAccountNotFound = errors.New("account not found")
)

type AccountRepository struct {
db *db.DB
}
Expand Down Expand Up @@ -38,3 +44,14 @@ func (r *AccountRepository) GetByProviderAndAccountID(ctx context.Context, param
account, err := r.db.Query.GetByProviderAndAccountID(ctx, params)
return &account, err
}

func (r *AccountRepository) GetUserIDByDiscordAccountID(ctx context.Context, discordAccountID string) (*uuid.UUID, error) {
userID, err := r.db.Query.GetUserIDByDiscordAccountID(ctx, discordAccountID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrAccountNotFound
}
return nil, err
}
return &userID, nil
}
25 changes: 25 additions & 0 deletions apps/api/internal/db/repository/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,28 @@ func (r *EventRepository) GetApplicationStatuses(ctx context.Context, eventId uu
func (r *EventRepository) GetSubmissionTimes(ctx context.Context, eventId uuid.UUID) ([]sqlc.GetSubmissionTimesRow, error) {
return r.db.Query.GetSubmissionTimes(ctx, eventId)
}

func (r *EventRepository) GetEventAttendeesWithDiscord(ctx context.Context, eventId uuid.UUID) (*[]sqlc.GetEventAttendeesWithDiscordRow, error) {
attendees, err := r.db.Query.GetEventAttendeesWithDiscord(ctx, eventId)
if err != nil {
return nil, err
}
return &attendees, nil
}

func (r *EventRepository) GetEventRoleByDiscordIDAndEventId(ctx context.Context, discordID string, eventID uuid.UUID) (*sqlc.GetEventRoleByDiscordIDAndEventIdRow, error) {
params := sqlc.GetEventRoleByDiscordIDAndEventIdParams{
AccountID: discordID,
EventID: eventID,
}

eventRole, err := r.db.Query.GetEventRoleByDiscordIDAndEventId(ctx, params)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrEventRoleNotFound
}
return nil, err
}

return &eventRole, nil
}
13 changes: 13 additions & 0 deletions apps/api/internal/db/sqlc/accounts.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 72 additions & 0 deletions apps/api/internal/db/sqlc/event_roles.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading