Skip to content
Closed
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
9 changes: 6 additions & 3 deletions cmd/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ func (app *application) mount() http.Handler {
}
if len(allowedOrigins) > 0 {
r.Use(cors.Handler(cors.Options{
AllowedOrigins: allowedOrigins,
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: append([]string{"Content-Type", "X-API-Key"}, supertokens.GetAllCORSHeaders()...),
AllowedOrigins: []string{app.config.frontendURL},
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowedHeaders: append([]string{"Content-Type"}, supertokens.GetAllCORSHeaders()...),
AllowCredentials: true,
}))
}
Expand Down Expand Up @@ -252,6 +252,9 @@ func (app *application) mount() http.Handler {
r.Patch("/{userID}/role", app.updateUserRoleHandler)
})

// Users
r.Patch("/users/role", app.batchUpdateRolesHandler)

})
})
})
Expand Down
17 changes: 8 additions & 9 deletions cmd/api/applications.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ type ApplicationWithQuestions struct {
//
// @Summary Get or create application
// @Description Returns the authenticated user's hackathon application. If no application exists, creates a new draft application.
// @Tags hackers
// @Tags applications
// @Accept json
// @Produce json
// @Success 200 {object} store.Application
Expand Down Expand Up @@ -119,7 +119,7 @@ func (app *application) getOrCreateApplicationHandler(w http.ResponseWriter, r *
//
// @Summary Update application
// @Description Partially updates the authenticated user's application. Only fields included in the request body are updated. Application must be in draft status.
// @Tags hackers
// @Tags applications
// @Accept json
// @Produce json
// @Param application body UpdateApplicationPayload true "Fields to update"
Expand Down Expand Up @@ -257,7 +257,7 @@ func (app *application) updateApplicationHandler(w http.ResponseWriter, r *http.
//
// @Summary Submit application
// @Description Submits the authenticated user's application for review. All required fields must be filled and acknowledgments must be accepted. Application must be in draft status.
// @Tags hackers
// @Tags applications
// @Produce json
// @Success 200 {object} store.Application
// @Failure 400 {object} object{error=string} "Missing required fields"
Expand Down Expand Up @@ -394,7 +394,7 @@ func (app *application) submitApplicationHandler(w http.ResponseWriter, r *http.
//
// @Summary Get application stats (Admin)
// @Description Returns aggregated statistics for all applications
// @Tags admin/applications
// @Tags admin
// @Produce json
// @Success 200 {object} store.ApplicationStats
// @Failure 401 {object} object{error=string}
Expand All @@ -418,13 +418,12 @@ func (app *application) getApplicationStatsHandler(w http.ResponseWriter, r *htt
//
// @Summary List applications (Admin)
// @Description Lists all applications with cursor-based pagination and optional status filter
// @Tags admin/applications
// @Tags admin
// @Produce json
// @Param cursor query string false "Pagination cursor"
// @Param status query string false "Filter by status (draft, submitted, accepted, rejected, waitlisted)"
// @Param limit query int false "Page size (default 50, max 100)"
// @Param direction query string false "Pagination direction: forward (default) or backward"
// @Param sort_by query string false "Sort column: created_at (default), accept_votes, reject_votes, waitlist_votes"
// @Success 200 {object} store.ApplicationListResult
// @Failure 400 {object} object{error=string}
// @Failure 401 {object} object{error=string}
Expand Down Expand Up @@ -542,7 +541,7 @@ type EmailListResponse struct {
//
// @Summary Set application status (Super Admin)
// @Description Sets the final status (accepted, rejected, or waitlisted) on an application
// @Tags superadmin/applications
// @Tags superadmin
// @Accept json
// @Produce json
// @Param applicationID path string true "Application ID"
Expand Down Expand Up @@ -592,7 +591,7 @@ func (app *application) setApplicationStatus(w http.ResponseWriter, r *http.Requ
//
// @Summary Get application by ID (Admin)
// @Description Returns a single application by its ID with embedded short answer questions
// @Tags admin/applications
// @Tags admin
// @Produce json
// @Param applicationID path string true "Application ID"
// @Success 200 {object} ApplicationWithQuestions
Expand Down Expand Up @@ -641,7 +640,7 @@ func (app *application) getApplication(w http.ResponseWriter, r *http.Request) {
//
// @Summary Get applicant emails by status (Super Admin)
// @Description Returns a list of applicant emails filtered by application status (accepted, rejected, or waitlisted)
// @Tags superadmin/applications
// @Tags superadmin
// @Produce json
// @Param status query string true "Application status (accepted, rejected, or waitlisted)"
// @Success 200 {object} EmailListResponse
Expand Down
12 changes: 6 additions & 6 deletions cmd/api/reviews.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type AIPercentResponse struct {
//
// @Summary Get pending reviews (Admin)
// @Description Returns all reviews assigned to the current admin that haven't been voted on yet, including application details
// @Tags admin/reviews
// @Tags admin
// @Produce json
// @Success 200 {object} PendingReviewsListResponse
// @Failure 401 {object} object{error=string}
Expand Down Expand Up @@ -71,7 +71,7 @@ func (app *application) getPendingReviews(w http.ResponseWriter, r *http.Request
//
// @Summary Get completed reviews (Admin)
// @Description Returns all reviews the current admin has completed (voted on), including application details
// @Tags admin/reviews
// @Tags admin
// @Produce json
// @Success 200 {object} CompletedReviewsListResponse
// @Failure 401 {object} object{error=string}
Expand Down Expand Up @@ -101,7 +101,7 @@ func (app *application) getCompletedReviews(w http.ResponseWriter, r *http.Reque
//
// @Summary Get notes for an application (Admin)
// @Description Returns all reviewer notes for a specific application without exposing votes
// @Tags admin/applications
// @Tags admin
// @Produce json
// @Param applicationID path string true "Application ID"
// @Success 200 {object} NotesListResponse
Expand Down Expand Up @@ -137,7 +137,7 @@ func (app *application) getApplicationNotes(w http.ResponseWriter, r *http.Reque
//
// @Summary Batch assign reviews (SuperAdmin)
// @Description Finds all submitted applications needing more reviews and assigns them to admins using workload balancing
// @Tags superadmin/applications
// @Tags superadmin
// @Produce json
// @Success 200 {object} store.BatchAssignmentResult
// @Failure 401 {object} object{error=string}
Expand Down Expand Up @@ -167,7 +167,7 @@ func (app *application) batchAssignReviews(w http.ResponseWriter, r *http.Reques
//
// @Summary Get next review assignment (Admin)
// @Description Automatically assigns the next submitted application needing review to the current admin and returns it
// @Tags admin/reviews
// @Tags admin
// @Produce json
// @Success 200 {object} ReviewResponse
// @Failure 401 {object} object{error=string}
Expand Down Expand Up @@ -209,7 +209,7 @@ func (app *application) getNextReview(w http.ResponseWriter, r *http.Request) {
//
// @Summary Submit vote on a review (Admin)
// @Description Records the admin's vote (accept/reject/waitlist) on an assigned application review
// @Tags admin/reviews
// @Tags admin
// @Accept json
// @Produce json
// @Param reviewID path string true "Review ID"
Expand Down
8 changes: 4 additions & 4 deletions cmd/api/scans.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (app *application) getScanTypesHandler(w http.ResponseWriter, r *http.Reque
//
// @Summary Create a scan (Admin)
// @Description Records a scan for a user. Validates scan type exists and is active. Non-check_in scans require the user to have checked in first.
// @Tags admin/scans
// @Tags admin
// @Accept json
// @Produce json
// @Param scan body CreateScanPayload true "Scan to create"
Expand Down Expand Up @@ -157,7 +157,7 @@ func (app *application) createScanHandler(w http.ResponseWriter, r *http.Request
//
// @Summary Get scans for a user (Admin)
// @Description Returns all scan records for the specified user, ordered by most recent first
// @Tags admin/scans
// @Tags admin
// @Produce json
// @Param userID path string true "User ID"
// @Success 200 {object} ScansResponse
Expand Down Expand Up @@ -189,7 +189,7 @@ func (app *application) getUserScansHandler(w http.ResponseWriter, r *http.Reque
//
// @Summary Get scan statistics (Admin)
// @Description Returns aggregate scan counts grouped by scan type
// @Tags admin/scans
// @Tags admin
// @Produce json
// @Success 200 {object} ScanStatsResponse
// @Failure 401 {object} object{error=string}
Expand All @@ -213,7 +213,7 @@ func (app *application) getScanStatsHandler(w http.ResponseWriter, r *http.Reque
//
// @Summary Update scan types (Super Admin)
// @Description Replaces all scan types with the provided array. Must include at least one check_in category type. Names must be unique.
// @Tags superadmin/settings
// @Tags superadmin
// @Accept json
// @Produce json
// @Param scan_types body UpdateScanTypesPayload true "Scan types to set"
Expand Down
8 changes: 4 additions & 4 deletions cmd/api/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type ShortAnswerQuestionsResponse struct {
//
// @Summary Get short answer questions (Super Admin)
// @Description Returns all configurable short answer questions for hacker applications
// @Tags superadmin/settings
// @Tags superadmin
// @Produce json
// @Success 200 {object} ShortAnswerQuestionsResponse
// @Failure 401 {object} object{error=string}
Expand Down Expand Up @@ -48,7 +48,7 @@ func (app *application) getShortAnswerQuestions(w http.ResponseWriter, r *http.R
//
// @Summary Update short answer questions (Super Admin)
// @Description Replaces all short answer questions with the provided array
// @Tags superadmin/settings
// @Tags superadmin
// @Accept json
// @Produce json
// @Param questions body UpdateShortAnswerQuestionsPayload true "Questions to set"
Expand Down Expand Up @@ -105,7 +105,7 @@ type ReviewsPerAppResponse struct {
//
// @Summary Get reviews per application (Super Admin)
// @Description Returns the number of reviews required per application
// @Tags superadmin/settings
// @Tags superadmin
// @Produce json
// @Success 200 {object} ReviewsPerAppResponse
// @Failure 401 {object} object{error=string}
Expand Down Expand Up @@ -133,7 +133,7 @@ func (app *application) getReviewsPerApp(w http.ResponseWriter, r *http.Request)
//
// @Summary Set reviews per application (Super Admin)
// @Description Sets the number of reviews required per application
// @Tags superadmin/settings
// @Tags superadmin
// @Accept json
// @Produce json
// @Param reviews_per_application body SetReviewsPerAppPayload true "Reviews per application value"
Expand Down
73 changes: 73 additions & 0 deletions cmd/api/users.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package main

import (
"errors"
"net/http"

"github.com/hackutd/portal/internal/store"
)

type BatchUpdateRolesPayload struct {
UserIDs []string `json:"user_ids" validate:"required,min=1,max=50,dive,uuid"`
Role store.UserRole `json:"role" validate:"required,oneof=hacker admin"`
}

type BatchUpdateRolesResponse struct {
Users []*store.User `json:"users"`
}

// batchUpdateRolesHandler updates the role for a batch of users
//
// @Summary Batch update user roles (Super Admin)
// @Description Updates the role for up to 50 users. Cannot modify own role.
// @Tags superadmin
// @Accept json
// @Produce json
// @Param payload body BatchUpdateRolesPayload true "User IDs and target role"
// @Success 200 {object} BatchUpdateRolesResponse
// @Failure 400 {object} object{error=string}
// @Failure 401 {object} object{error=string}
// @Failure 403 {object} object{error=string}
// @Failure 500 {object} object{error=string}
// @Security CookieAuth
// @Router /superadmin/users/role [patch]
func (app *application) batchUpdateRolesHandler(w http.ResponseWriter, r *http.Request) {
var req BatchUpdateRolesPayload
if err := readJSON(w, r, &req); err != nil {
app.badRequestResponse(w, r, err)
return
}

if err := Validate.Struct(req); err != nil {
app.badRequestResponse(w, r, err)
return
}

caller := getUserFromContext(r.Context())
if caller == nil {
app.internalServerError(w, r, errors.New("user not in context"))
return
}

for _, id := range req.UserIDs {
if id == caller.ID {
app.badRequestResponse(w, r, errors.New("cannot modify your own role"))
return
}
}

users, err := app.store.Users.BatchUpdateRoles(r.Context(), req.UserIDs, req.Role)
if err != nil {
app.internalServerError(w, r, err)
return
}

if len(users) != len(req.UserIDs) {
app.badRequestResponse(w, r, errors.New("one or more user IDs do not exist"))
return
}

if err := app.jsonResponse(w, http.StatusOK, BatchUpdateRolesResponse{Users: users}); err != nil {
app.internalServerError(w, r, err)
}
}
Loading
Loading