Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
86cdb84
feat: app review columns
h1divp Nov 21, 2025
a741d80
feat: basic selection control
AlexanderWangY Nov 21, 2025
4236e4b
chore: refactor into components
AlexanderWangY Nov 21, 2025
08d937b
Feat: Show "coming soon" status when deadline is passed.
h1divp Nov 22, 2025
b752735
Fix: Apply now button disabled if current time is before app open
h1divp Nov 22, 2025
bdd970d
Fix: Make sure button is disabled only for non-applied applicant user.
h1divp Nov 22, 2025
b4ca18f
feat: completed application review start modal
AlexanderWangY Nov 23, 2025
fd9c49f
fix: removed consolelog
h1divp Nov 23, 2025
6a0ee29
fix: reword
h1divp Nov 23, 2025
66e2387
fix: nitpicks
h1divp Nov 23, 2025
4584b04
started on Application Review feature
hieunguyent12 Nov 25, 2025
af4ff4f
Merge pull request #213 from swamphacks/Phoenix/appreview-db-change
AlexanderWangY Nov 25, 2025
e50f263
nit: add handling for zero applications selection
AlexanderWangY Nov 25, 2025
5506246
Merge pull request #215 from swamphacks/feat/app-review-admin
AlexanderWangY Nov 25, 2025
6673638
Merge pull request #214 from swamphacks/hotfix/disable-apply-now-button
AlexanderWangY Nov 25, 2025
efaf5a4
fix: handle negative applications selected
AlexanderWangY Nov 25, 2025
2842e98
Merge pull request #219 from swamphacks/fix/negative-reviewer-apps
AlexanderWangY Nov 25, 2025
6a9069a
temp: application assignment initialized handler
AlexanderWangY Nov 25, 2025
959164d
feat: applications assignment and atomic assignments
AlexanderWangY Nov 25, 2025
1cc4f4c
feat: added reset functionality
AlexanderWangY Nov 25, 2025
a7c7b0d
chore: protect endpoint
AlexanderWangY Nov 28, 2025
ede5782
chore: remove redundant comments
AlexanderWangY Nov 28, 2025
8e5b54a
chore: remove redundant comments
AlexanderWangY Nov 28, 2025
922ca1a
chore: generate web openapi types
AlexanderWangY Nov 28, 2025
8785a28
nit: spacing
AlexanderWangY Nov 28, 2025
287deeb
Merge pull request #220 from swamphacks/feat/application-assigment
AlexanderWangY Nov 28, 2025
2543458
refactor: changed application review page rendering logic
AlexanderWangY Nov 28, 2025
7156236
Merge branch 'dev' into feat/app-review
AlexanderWangY Nov 28, 2025
dcedc76
Merge pull request #221 from swamphacks/feat/app-review
AlexanderWangY Nov 28, 2025
f11b55c
Merge branch 'dev' into feat/application-review
AlexanderWangY Nov 28, 2025
6cdbfaf
Feat: download resume for reviewing; Refactor: add lifetimeSec param …
h1divp Nov 28, 2025
5fc9d83
feat: download resumes with role check
h1divp Nov 28, 2025
e5e0ae3
fix: also allow admins
h1divp Nov 28, 2025
18fa0c0
feat: application navigation logic
AlexanderWangY Nov 29, 2025
bd2762b
Merge pull request #222 from swamphacks/feat/TECH-301-application-rev…
AlexanderWangY Nov 29, 2025
9abbc8b
Merge branch 'dev' into feat/application-review
AlexanderWangY Nov 29, 2025
945c16d
chore: merge resolution
AlexanderWangY Nov 29, 2025
15dceee
feat: application review submissions
AlexanderWangY Nov 29, 2025
8a6b36f
Merge pull request #223 from swamphacks/feat/application-review
AlexanderWangY Nov 29, 2025
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
498 changes: 496 additions & 2 deletions apps/api/docs/docs.go

Large diffs are not rendered by default.

498 changes: 496 additions & 2 deletions apps/api/docs/swagger.json

Large diffs are not rendered by default.

329 changes: 327 additions & 2 deletions apps/api/docs/swagger.yaml

Large diffs are not rendered by default.

16 changes: 15 additions & 1 deletion 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,6 +122,8 @@

// --- Event routes ---
api.Router.Route("/events", func(r chi.Router) {
// r.Post("/{eventId}/application/reset-reviews", api.Handlers.Application.ResetApplicationReviews)
// r.Post("/{eventId}/application/assign-reviewers", api.Handlers.Application.AssignApplicationReviewers)

// Superuser-only
r.With(mw.Auth.RequireAuth, ensureSuperuser).Post("/", api.Handlers.Event.CreateEvent)
Expand Down Expand Up @@ -156,13 +158,25 @@
// Application routes
r.Route("/application", func(r chi.Router) {
r.Use(mw.Auth.RequireAuth)
r.Get("/", api.Handlers.Application.GetApplicationByUserAndEventID)
r.Get("/", api.Handlers.Application.GetMyApplication)
r.Post("/submit", api.Handlers.Application.SubmitApplication)
r.Post("/save", api.Handlers.Application.SaveApplication)
r.Get("/download-resume", api.Handlers.Application.DownloadResume)
r.With(mw.Event.AttachEventRoleToContext()).Get("/{applicationId}/resume", api.Handlers.Application.GetResumePresignedUrl)

// Getting a resume (Staff Only)
r.With(ensureEventStaff).Get("/{applicationId}", api.Handlers.Application.GetApplication)

// For statistics (Staff ONLY)
r.With(ensureEventStaff).Get("/stats", api.Handlers.Application.GetApplicationStatistics)

// For application review (Staff ONLY)
r.With(ensureEventStaff).Get("/assigned", api.Handlers.Application.GetAssignedApplications)
r.With(ensureEventStaff).Post("/{applicationId}/review", api.Handlers.Application.SubmitApplicationReview)

// Review admin routes (For Event Admins only)
r.With(ensureEventAdmin).Post("/reset-reviews", api.Handlers.Application.ResetApplicationReviews)
r.With(ensureEventAdmin).Post("/assign-reviewers", api.Handlers.Application.AssignApplicationReviewers)
})

// Team routes
Expand Down
261 changes: 248 additions & 13 deletions apps/api/internal/api/handlers/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"github.com/swamphacks/core/apps/api/internal/db/repository"
"github.com/swamphacks/core/apps/api/internal/db/sqlc"
"github.com/swamphacks/core/apps/api/internal/services"
"github.com/swamphacks/core/apps/api/internal/web"
)

type ApplicationHandler struct {
Expand All @@ -28,21 +29,21 @@
}
}

// Get Application By User and Event ID
// Get current user's application by event ID
//
// @Summary Get Application By User and Event ID
// @Summary Get Current User's Application by Event ID
// @Description Get the current user's application progress for an event. If this is their first time filling out the application, a new application will be created.
// @Tags Application
// @Accept json
// @Produce json
// @Param eventId path string true "Event ID"
// @Param sh_session cookie string true "The authenticated session token/id"
// @Param sh_session_id cookie string true "The authenticated session token/id"
// @Success 200 {object} sqlc.Application "OK: An application was found"
// @Success 200 {object} map[string]any "OK: An application was found"
// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request."
// @Failure 500 {object} response.ErrorResponse "Server Error: error retrieving application"\
// @Router /events/{eventId}/application [get]
func (h *ApplicationHandler) GetApplicationByUserAndEventID(w http.ResponseWriter, r *http.Request) {
func (h *ApplicationHandler) GetMyApplication(w http.ResponseWriter, r *http.Request) {
eventIdStr := chi.URLParam(r, "eventId")

if eventIdStr == "" {
Expand Down Expand Up @@ -110,7 +111,8 @@
// @Tags Application
// @Accept json
// @Produce json
// @Param formBody formData any true "Submission form data"
// @Param formBody formData any true "Submission form data"
// @Param eventId path string true "Event ID" Format(uuid)
// @Success 200
// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request."
// @Failure 500 {object} response.ErrorResponse "Server Error: error submitting application"
Expand Down Expand Up @@ -182,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 @@ -238,7 +240,8 @@
// @Tags Application
// @Accept json
// @Produce json
// @Param data body any true "Form data"
// @Param data body any true "Form data"
// @Param eventId path string true "Event ID" Format(uuid)
// @Success 200
// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request."
// @Failure 500 {object} response.ErrorResponse "Server Error: error saving application"
Expand Down Expand Up @@ -287,9 +290,10 @@
// @Description This handler creates a presigned S3 URL with GET permission for the user's specific object, which is their uploaded resume. The client can use this URL to download the object.
// @Tags Application
// @Produce json
// @Success 200 {object} string
// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request."
// @Failure 500 {object} response.ErrorResponse "Server Error: error handling download resume request"
// @Param eventId path string true "Event ID" Format(uuid)
// @Success 200 {object} string
// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request."
// @Failure 500 {object} response.ErrorResponse "Server Error: error handling download resume request"
// @Router /events/{eventId}/application/download-resume [get]
func (h *ApplicationHandler) DownloadResume(w http.ResponseWriter, r *http.Request) {
eventIdStr := chi.URLParam(r, "eventId")
Expand All @@ -312,7 +316,7 @@
return
}

request, err := h.appService.DownloadResume(r.Context(), *userId, eventId)
request, err := h.appService.DownloadResume(r.Context(), *userId, eventId, 60)

if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("resume_download_error", "unable to retrieve resume download url"))
Expand All @@ -328,9 +332,10 @@
// @Description This aggregates applications by race, gender, age, majors, and schools. This route is only available to event staff and admins.
// @Tags Application
// @Produce json
// @Success 200 {object} services.ApplicationStatistics
// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request."
// @Failure 500 {object} response.ErrorResponse "Server Error: error getting statistics"
// @Param eventId path string true "Event ID" Format(uuid)
// @Success 200 {object} services.ApplicationStatistics
// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request."
// @Failure 500 {object} response.ErrorResponse "Server Error: error getting statistics"
// @Router /events/{eventId}/application/stats [get]
func (h *ApplicationHandler) GetApplicationStatistics(w http.ResponseWriter, r *http.Request) {
eventIdStr := chi.URLParam(r, "eventId")
Expand All @@ -353,3 +358,233 @@

res.Send(w, http.StatusOK, appStats)
}

// Get an application for a user and event
//
// @Summary Get an application based on a user id and event id.
// @Description Retrieves an application using the user id and event id primary keys and unique constraints. Only accessible by event staff and admins.
// @Tags Application
// @Produce json
// @Param eventId path string true "Event ID"
// @Param applicationId path string true "Application ID (Technically user ID)"
// @Param sh_session cookie string true "The authenticated session token/id"
// @Success 200 {object} sqlc.Application "OK: An application was found"
// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request."
// @Failure 500 {object} response.ErrorResponse "Server Error: error retrieving assigned application"
// @Router /events/{eventId}/application/{applicationId} [get]
func (h *ApplicationHandler) GetApplication(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
}

// So funny story, there is no ID in the application table, this is just an abstracted user_id.
applicationId, err := web.PathParamToUUID(r, "applicationId")
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_application_id", "The application ID is not a valid."))
return
}

application, err := h.appService.GetApplicationByUserAndEventID(r.Context(), sqlc.GetApplicationByUserAndEventIDParams{
UserID: applicationId,
EventID: eventId,
})
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("get_assigned_application_error", "error retrieving assigned application"))
return
}

res.Send(w, http.StatusOK, application)
}

type ReviewRatings struct {
PassionRating int `json:"passion_rating" validate:"required,min=1,max=5"`
ExperienceRating int `json:"experience_rating" validate:"required,min=1,max=5"`
}

// Submit application review
//
// @Summary Submit application review
// @Description Handles ratings submissions from staff during the application review process.
// @Tags Application
// @Produce json
// @Param reviewData body ReviewRatings true "An object containing the passion and experience ratings"
// @Success 201
// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request."
// @Failure 500 {object} response.ErrorResponse "Server Error: error submitting application review"
// @Router /events/{eventId}/application/{applicationId}/review [post]
func (h *ApplicationHandler) SubmitApplicationReview(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
}

applicationId, err := web.PathParamToUUID(r, "applicationId")
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_application_id", "The application ID is not a valid."))
return
}

reviewerId := ctxutils.GetUserIdFromCtx(r.Context())
if reviewerId == nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_user_id", "invalid user id"))
return
}

var reviewData ReviewRatings
if err := json.NewDecoder(r.Body).Decode(&reviewData); err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Failed to parse request body: "+err.Error()))
return
}

validate := validator.New()
if err := validate.Struct(reviewData); err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", err.Error()))
return
}

if err = h.appService.SaveApplicationReview(r.Context(), *reviewerId, applicationId, eventId, reviewData.ExperienceRating, reviewData.PassionRating); err != nil {
res.SendError(w, http.StatusInternalServerError, res.NewError("save_review_error", "Something went wrong while saving the application review."))
return
}

w.WriteHeader(http.StatusCreated)
}

// Get Assigned Application IDs and Progress
//
// @Summary Get Assigned Application IDs and Progress
// @Description Retrieves assigned applications and their review progress for the authenticated reviewer.
// @Tags Application
// @Produce json
// @Param eventId path string true "Event ID"
// @Param sh_session_id cookie string true "The authenticated session token/id"
// @Success 200 {array} services.AssignedApplication "OK: An application was found"
// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request."
// @Failure 500 {object} response.ErrorResponse "Server Error: error retrieving assigned application"
// @Router /events/{eventId}/application/assigned [get]
func (h *ApplicationHandler) GetAssignedApplications(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
}

userId := ctxutils.GetUserIdFromCtx(r.Context())
if userId == nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_user_id", "invalid user id"))
return
}

assignedApps, err := h.appService.GetAssignedApplicationsAndProgress(r.Context(), *userId, eventId)
if err != nil {
res.SendError(w, http.StatusInternalServerError, res.NewError("get_assigned_applications_error", "Something went wrong while retrieving assigned applications."))
return
}

res.Send(w, http.StatusOK, assignedApps)
}

// Assign application to reviewers
//
// @Summary Assign application to reviewers
// @Description Assigns applications for an event to reviewers for the application review process.
// @Tags Application
// @Accept json
// @Param eventId path string true "Event ID" Format(uuid)
// @Param request body []services.ReviewerAssignment true "Reviewer assignmnet payload"
// @Success 201 "Reviewers assigned"
// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request."
// @Failure 500 {object} response.ErrorResponse "Server Error: error assigning reviewers"
// @Router /events/{eventId}/application/assign-reviewers [post]
func (h *ApplicationHandler) AssignApplicationReviewers(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 payload []services.ReviewerAssignment
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Failed to parse request body: "+err.Error()))
return
}

// Process assignments
err = h.appService.AssignReviewers(r.Context(), eventId, payload)
if err != nil {
res.SendError(w, http.StatusInternalServerError, res.NewError("assign_reviewers_error", "Something went wrong while assigning reviewers to applications."))
return
}

w.WriteHeader(http.StatusCreated)
}

// Reset application reviews
//
// @Summary Reset application reviews
// @Description Resets all application reviews for a given event, clearing any existing reviewer assignments.
// @Tags Application
//
// @Param eventId path string true "ID of the event to reset reviews for"
// @Success 200 "Application reviews reset successfully"
// @Failure 400 {object} res.ErrorResponse "Bad request: invalid event ID"
// @Failure 500 {object} res.ErrorResponse "Server error: failed to reset application reviews"
// @Router /events/{eventId}/application/reset-reviews [post]
func (h *ApplicationHandler) ResetApplicationReviews(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
}

err = h.appService.ResetApplicationReviews(r.Context(), eventId)
if err != nil {
res.SendError(w, http.StatusInternalServerError, res.NewError("reset_reviews_error", "Something went wrong while resetting application reviews."))
return
}

w.WriteHeader(http.StatusOK)
}

// Get resume
//
// @Summary Get resume for application review
// @Description This handler creates a presigned S3 URL with GET permission for a specific user's resume as an object. The client can use this URL to download the object temporarily for application review.
// @Tags Application
// @Produce json
// @Param eventId path string true "Event ID" Format(uuid)
// @Param applicationId path string true "The application ID (userId of applicant)" Format(uuid)
// @Success 200 {object} string
// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request."
// @Failure 500 {object} response.ErrorResponse "Server Error: error handling download resume request"
// @Router /events/{eventId}/application/{applicationId}/resume [get]
func (h *ApplicationHandler) GetResumePresignedUrl(w http.ResponseWriter, r *http.Request) {
eventId, err := web.PathParamToUUID(r, "eventId")

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

View workflow job for this annotation

GitHub Actions / API Lint

ineffectual assignment to err (ineffassign)
applicationId, err := web.PathParamToUUID(r, "applicationId")

// Ensure access
userId := ctxutils.GetUserIdFromCtx(r.Context())
eventRole := ctxutils.GetEventRoleFromCtx(r.Context())

if userId == nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_user_id", "the user id of the current user is invalid"))
return
}

if eventRole.Role != sqlc.EventRoleTypeStaff && eventRole.Role != sqlc.EventRoleTypeAdmin && *userId != applicationId {
res.SendError(w, http.StatusForbidden, res.NewError("forbidden", "You are not allowed to see other ppls resumes :("))
return
}

request, err := h.appService.DownloadResume(r.Context(), applicationId, eventId, 600)

if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("resume_download_error", "unable to retrieve resume download url"))
return
}

res.Send(w, http.StatusOK, request.URL)
}
2 changes: 1 addition & 1 deletion apps/api/internal/api/handlers/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func NewAuthHandler(authService *services.AuthService, cfg *config.Config, logge
// @Success 200 {object} middleware.UserContext
// @Failure 401 {object} response.ErrorResponse "Unauthenticated: Requester is not currently authenticated."
// @Failure 500 {object} response.ErrorResponse
// @Router /auth/me [get]
// @Router /auth/me [get] [get]
func (h *AuthHandler) GetMe(w http.ResponseWriter, r *http.Request) {
user, err := h.authService.GetMe(r.Context())
if err != nil {
Expand Down
Loading
Loading