Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e059f38
feat: complete webhook and add auto assign commands
SobaSkee Dec 25, 2025
90088cc
feat: create endpoint for discord bot and event listener to add auto …
SobaSkee Dec 27, 2025
541fa02
actual update
SobaSkee Dec 27, 2025
9538b2f
refactor: old implementation reworked to use endpoints from api
SobaSkee Dec 27, 2025
671a2eb
chore: remove old db file
SobaSkee Dec 27, 2025
554027e
chore: remove gemini api key requirement
SobaSkee Dec 27, 2025
28af3e4
fix: revert accounts go sqlc version
SobaSkee Dec 27, 2025
b88cc37
fix: rename hacker_role to role
SobaSkee Dec 27, 2025
e9bf979
fix: refactor code to get event role by discord and event id
SobaSkee Jan 16, 2026
d54565a
Check in scanner (#284)
AlexanderWangY Jan 19, 2026
b68f64b
fix: remove event id tests from intents (#285)
AlexanderWangY Jan 19, 2026
43f0443
fix/parse intents tests (#286)
AlexanderWangY Jan 19, 2026
a6fd23e
chore: change to to be announced (#287)
AlexanderWangY Jan 19, 2026
625bfd3
feat: add event id when calling endpoint
SobaSkee Jan 19, 2026
f9faa87
Merge branch 'dev' into feat/discord-bot-auto-assign-roles
h1divp Jan 19, 2026
39b2d82
fix: sqlc generate, deleted large bin file
h1divp Jan 19, 2026
b15e47a
Merge pull request #260 from swamphacks/feat/discord-bot-auto-assign-…
h1divp Jan 19, 2026
05c13f7
Stanley/basic bot structure (#216)
SobaSkee Jan 20, 2026
f7deaf2
Redemptions (#288)
hugoliu-code Jan 20, 2026
60cbf6e
feat: resume downloader script; fix: add to gitignore (#289)
h1divp Jan 20, 2026
42db4cf
fix: injected services (#290)
AlexanderWangY Jan 20, 2026
1522649
feat: welcome email release (#291)
h1divp Jan 21, 2026
adf256e
qr code fixes (#293)
h1divp Jan 21, 2026
a10695e
feat: change 72 hours to 24 in email (#296)
h1divp Jan 22, 2026
5eface4
Redeemablebugfixes (#297)
hugoliu-code Jan 23, 2026
dc99bf3
feat: production github workflow and docker config for discord bot (#…
h1divp Jan 23, 2026
07dd064
feat: mobile endpoints (#298)
AlexanderWangY Jan 23, 2026
d67f556
fix: remove bad discord docker stuff, fix syntax (#299)
h1divp Jan 23, 2026
f00052e
update rfid (#300)
AlexanderWangY Jan 23, 2026
e9b6b30
feat/update rfid (#301)
AlexanderWangY Jan 23, 2026
3042947
Merge branch 'master' into dev
h1divp Jan 23, 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
17 changes: 16 additions & 1 deletion apps/api/internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,22 @@
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)
})

api.Router.Route("/mobile", func(r chi.Router) {
// This means you have to set a Authorization header with "Key xxx".
r.Use(mw.Auth.RequireMobileAuth)

r.Get("/events/{eventId}/users/{userId}", api.Handlers.Event.GetUserForEvent)
r.Get("/events/{eventId}/users/by-rfid/{rfid}", api.Handlers.Event.GetUserByRFID)
r.Post("/events/{eventId}/checkin", api.Handlers.Admission.HandleEventCheckIn)

r.Get("/events/{eventId}/redeemables", api.Handlers.Redeemables.GetRedeemables)
r.Post("/redeemables/{redeemableId}/users/{userId}", api.Handlers.Redeemables.RedeemRedeemable)
r.Post("/events/{eventId}/users/{userId}/update-rfid", api.Handlers.Event.UpdateUserRFID)
})

// Health check
api.Router.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
api.Logger.Trace().Str("method", r.Method).Str("path", r.URL.Path).Msg("Received ping.")
Expand Down Expand Up @@ -152,6 +165,8 @@
r.With(ensureEventStaff).Get("/users/{userId}", api.Handlers.Event.GetUserForEvent)
// Get user ID by RFID
r.With(ensureEventStaff).Get("/users/by-rfid/{rfid}", api.Handlers.Event.GetUserByRFID)
// Is the user checked in
r.With(ensureEventStaff).Get("/users/{userId}/checked-in-status", api.Handlers.Event.GetCheckedInStatusByIds)

// Admin-only
r.With(ensureEventAdmin).Post("/queue-confirmation-email", api.Handlers.Email.QueueConfirmationEmail)
Expand Down Expand Up @@ -225,7 +240,7 @@
// Update and delete specific redeemable
r.Route("/{redeemableId}", func(r chi.Router) {
r.Patch("/", api.Handlers.Redeemables.UpdateRedeemable)
r.Delete("/", api.Handlers.Redeemables.DeleteRedeemable)
r.With(ensureEventAdmin).Delete("/", api.Handlers.Redeemables.DeleteRedeemable)

r.Route("/users/{userId}", func(r chi.Router) {
r.Post("/", api.Handlers.Redeemables.RedeemRedeemable)
Expand Down
95 changes: 94 additions & 1 deletion apps/api/internal/api/handlers/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"fmt"
"io"
"net/http"
"strconv"
"time"

"github.com/go-chi/chi/v5"
Expand Down Expand Up @@ -315,7 +316,7 @@
if err != nil {
if errors.Is(err, repository.ErrEventRoleNotFound) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(NullableEventRole{

Check failure on line 319 in apps/api/internal/api/handlers/events.go

View workflow job for this annotation

GitHub Actions / API Lint

Error return value of `(*encoding/json.Encoder).Encode` is not checked (errcheck)
UserID: *userId,
EventID: eventId,
Role: nil,
Expand All @@ -328,7 +329,7 @@
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(NullableEventRole{

Check failure on line 332 in apps/api/internal/api/handlers/events.go

View workflow job for this annotation

GitHub Actions / API Lint

Error return value of `(*encoding/json.Encoder).Encode` is not checked (errcheck)
UserID: eventRole.UserID,
EventID: eventRole.EventID,
Role: &eventRole.Role,
Expand Down Expand Up @@ -865,4 +866,96 @@

// Return just the user ID as a simple JSON object
res.Send(w, http.StatusOK, map[string]string{"user_id": user.ID.String()})
}
}

// Get User by RFID
//
// @Summary Retrieves a user's ID by their RFID
// @Description Looks up a user's ID by their RFID code for a specific event. Returns the user ID which can be used for other operations.
// @Tags Event
// @Produce json
// @Param eventId path string true "Event ID" Format(uuid)
// @Param rfid path string true "RFID code (10 digits)"
// @Success 200 {object} map[string]string "OK - Returns user ID"
// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request."
// @Failure 404 {object} response.ErrorResponse "User not found with the provided RFID"
// @Failure 500 {object} response.ErrorResponse "Server Error: error getting user by RFID"
// @Router /events/{eventId}/users/by-rfid/{rfid} [get]
func (h *EventHandler) GetCheckedInStatusByIds(w http.ResponseWriter, r *http.Request) {
eventId, err := web.PathParamToUUID(r, "eventId")
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!"))
return
}

userId, err := web.PathParamToUUID(r, "userId")
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("missing_user_id", "The user ID is missing from the URL!"))
return
}
result, err := h.eventService.GetCheckedInStatusByIds(r.Context(), userId, eventId)
if err != nil {
res.SendError(w, http.StatusNotFound, res.NewError("error", "Something went wrong internally."))
return
}
// Return just the checked in status as a simple JSON object
res.Send(w, http.StatusOK, map[string]string{
"checked_in_status": strconv.FormatBool(result),
})
}

type UpdateRFID struct {
RFID string `json:"rfid"`
}

// UpdateUserRFID
//
// @Summary Updates a user's RFID tag
// @Description Associates a new RFID string with a specific user for the given event. This overwrites any existing RFID association.
// @Tags Event
// @Accept json
// @Produce json
// @Param eventId path string true "Event ID" Format(uuid)
// @Param userId path string true "User ID" Format(uuid)
// @Param body body UpdateRFID true "New RFID data"
// @Success 204 "No Content - RFID updated successfully"
// @Failure 400 {object} response.ErrorResponse "Invalid request body or UUID format"
// @Failure 404 {object} response.ErrorResponse "User or Event not found"
// @Failure 500 {object} response.ErrorResponse "Internal server error"
// @Router /events/{eventId}/users/{userId}/update-rfid [post]
func (h *EventHandler) UpdateUserRFID(w http.ResponseWriter, r *http.Request) {
eventId, err := web.PathParamToUUID(r, "eventId")
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!"))
return
}

userId, err := web.PathParamToUUID(r, "userId")
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("missing_user_id", "The user ID is missing from the URL!"))
return
}

var payload UpdateRFID
err = json.NewDecoder(r.Body).Decode(&payload)
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("body_malformed", "Invalid body"))
return
}
defer r.Body.Close()

Check failure on line 945 in apps/api/internal/api/handlers/events.go

View workflow job for this annotation

GitHub Actions / API Lint

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

if payload.RFID == "" {
res.SendError(w, http.StatusBadRequest, res.NewError("body_malformed", "Invalid body"))
return
}

tempRole := sqlc.EventRoleTypeAttendee

err = h.eventService.UpdateEventRoleByIds(r.Context(), userId, eventId, &tempRole, nil, &payload.RFID)
if err != nil {
res.SendError(w, http.StatusNotFound, res.NewError("error", "Something went wrong internally."))
return
}

w.WriteHeader(http.StatusNoContent)
}
29 changes: 29 additions & 0 deletions apps/api/internal/api/middleware/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"net/http"
"slices"
"strings"
"time"

"github.com/google/uuid"
Expand Down Expand Up @@ -69,6 +70,34 @@ func NewAuthMiddleware(db *db.DB, logger zerolog.Logger, cfg *config.Config) *Au
}
}

func (m *AuthMiddleware) RequireMobileAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
m.logger.Trace().Msg("Incoming mobile request")

auth := r.Header.Get("Authorization")
if auth == "" {
m.logger.Warn().Msg("Authorization header is empty.")
response.SendError(w, http.StatusUnauthorized, response.NewError("no_auth", "You are not authorized"))
return
}

parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 {
m.logger.Warn().Msg("Authorization header is malformed > 2 parts")
response.SendError(w, http.StatusUnauthorized, response.NewError("no_auth", "You are not authorized"))
return
}

if parts[0] != "Key" || parts[1] != m.cfg.MobileAuthKey {
m.logger.Warn().Msg("Authorization header is not a valid value")
response.SendError(w, http.StatusUnauthorized, response.NewError("no_auth", "You are not authorized"))
return
}

next.ServeHTTP(w, r)
})
}

func (m *AuthMiddleware) RequireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
m.logger.Trace().Msg("Checking auth status")
Expand Down
2 changes: 2 additions & 0 deletions apps/api/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ type Config struct {
CoreBuckets CoreBuckets `envPrefix:"CORE_BUCKETS_"`
Smtp SmtpConfig `envPrefix:"SMTP_"`
AWS AWSConfig `envPrefix:"AWS_"`

MobileAuthKey string `env:"MOBILE_AUTH_KEY"`
}

func Load() *Config {
Expand Down
9 changes: 9 additions & 0 deletions apps/api/internal/db/queries/event_roles.sql
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,12 @@ FROM auth.users u
JOIN event_roles er ON u.id = er.user_id
WHERE er.event_id = $1
AND er.rfid = $2;

-- name: GetCheckedInStatusByIds :one
SELECT EXISTS (
SELECT 1
FROM event_roles
WHERE user_id = $1
AND event_id = $2
AND checked_in_at IS NOT NULL
)::bool;
16 changes: 16 additions & 0 deletions apps/api/internal/db/repository/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var (
ErrDuplicateEvent = errors.New("event already exists in database")
ErrNoEventsDeleted = errors.New("no events deleted")
ErrMultipleEventsDeleted = errors.New("multiple events affected by delete query while only expecting one to delete one")
ErrUserEventNotFound = errors.New("the user and event id combination was not found")
ErrUnknown = errors.New("an unkown error was caught")
)

Expand Down Expand Up @@ -206,6 +207,21 @@ func (r *EventRepository) GetEventRoleByDiscordIDAndEventId(ctx context.Context,
return &eventRole, nil
}

func (r *EventRepository) GetCheckedInStatusByUserIdAndEventId(ctx context.Context, userId uuid.UUID, eventId uuid.UUID) (bool, error) {
params := sqlc.GetCheckedInStatusByIdsParams{
UserID: userId,
EventID: eventId,
}

result, err := r.db.Query.GetCheckedInStatusByIds(ctx, params)

if err != nil {
return false, err
}

return result, nil
}

func (r *EventRepository) GetAttendeeUserIdsByEventId(ctx context.Context, eventID uuid.UUID) ([]uuid.UUID, error) {
return r.db.Query.GetAttendeeUserIdsByEventId(ctx, eventID)
}
22 changes: 22 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.

Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ <h2 style="margin:0; font-size:22px; color:#1a1a1a;">Hi {{ .Name }},</h2>
</p>

<p style="margin:0 0 15px 0; font-size:16px;">
You have <strong><span style="color:#CC0000;">72 hours</span></strong> to confirm your spot. If you
You have <strong><span style="color:#CC0000;">24 hours</span></strong> to confirm your spot. If you
don’t,
you’ll be moved back to the
waitlist to make room for others.
Expand Down
10 changes: 10 additions & 0 deletions apps/api/internal/services/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,3 +422,13 @@ func (s *EventService) GetUserInfoForEvent(ctx context.Context, userId, eventId

return result, nil
}

func (s *EventService) GetCheckedInStatusByIds(ctx context.Context, userId uuid.UUID, eventId uuid.UUID) (bool, error) {
result, err := s.eventRepo.GetCheckedInStatusByUserIdAndEventId(ctx, userId, eventId)
if err != nil {
s.logger.Err(err).Msg("Failed to get user by ids")
return false, err
}
return result, nil

}
4 changes: 4 additions & 0 deletions apps/api/internal/services/redeemables.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ func (s *RedeemablesService) UpdateRedeemable(ctx context.Context, redeemableID
}

func (s *RedeemablesService) RedeemRedeemable(ctx context.Context, redeemableID uuid.UUID, userID uuid.UUID) error {
// Need to do a check to see if the user is checked in
// Probably need event service

// CREATE NEW SQL function for getting checked in status
_, err := s.redeemablesRepo.RedeemRedeemable(ctx, redeemableID, userID)

if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions apps/discord-bot/.env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
DISCORD_TOKEN=
API_KEY=
API_URL=
GEMINI_API_KEY=
API_URL=https://api.swamphacks.com
SESSION_COOKIE=
WEBHOOK_URL=
WEBHOOK_PORT=
GEMINI_API_KEY=
PORT=
EVENT_ID=
26 changes: 13 additions & 13 deletions apps/discord-bot/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
FROM python:3.12-slim
# FROM python:3.12-slim

# Install prerequisites for uv
RUN apt-get update && apt-get install -y curl git
# RUN pip install uv

# Install uv by Astral
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
# WORKDIR /app

# Set working directory
WORKDIR /app
# COPY pyproject.toml .
# COPY uv.lock .

# Copy bot files
COPY . .
# # ENV PATH="/root/.local/bin:$PATH"

# Environment variables are handled by docker-compose
ENV PATH="/root/.local/bin:$PATH"
# RUN uv sync --frozen --no-cache --all-extras

# Default command to run bot
CMD ["uv", "run", "main.py"]
# COPY . .

# EXPOSE 3000

# CMD ["uv", "run", "main.py"]
# # CMD ["uv", "run", "api_server.py"]
Loading
Loading