From 4ef4146fd7bd11cff937980342816aff44761cd4 Mon Sep 17 00:00:00 2001 From: anishalle Date: Tue, 24 Feb 2026 11:12:14 -0600 Subject: [PATCH 1/5] feat: create migrations --- .../000016_create_application_enabled_setting.down.sql | 1 + .../migrations/000016_create_application_enabled_setting.up.sql | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 cmd/migrate/migrations/000016_create_application_enabled_setting.down.sql create mode 100644 cmd/migrate/migrations/000016_create_application_enabled_setting.up.sql diff --git a/cmd/migrate/migrations/000016_create_application_enabled_setting.down.sql b/cmd/migrate/migrations/000016_create_application_enabled_setting.down.sql new file mode 100644 index 00000000..b643cff4 --- /dev/null +++ b/cmd/migrate/migrations/000016_create_application_enabled_setting.down.sql @@ -0,0 +1 @@ +DELETE FROM settings WHERE key = 'application_enabled'; diff --git a/cmd/migrate/migrations/000016_create_application_enabled_setting.up.sql b/cmd/migrate/migrations/000016_create_application_enabled_setting.up.sql new file mode 100644 index 00000000..7953a0aa --- /dev/null +++ b/cmd/migrate/migrations/000016_create_application_enabled_setting.up.sql @@ -0,0 +1,2 @@ +INSERT INTO settings (key, value) VALUES ('application_enabled', 'true'::jsonb) +ON CONFLICT (key) DO NOTHING; From e8d48adaeb9f9a9118530ad9ec4f144e9f4e26f8 Mon Sep 17 00:00:00 2001 From: anishalle Date: Tue, 24 Feb 2026 11:19:57 -0600 Subject: [PATCH 2/5] fix: more consistent naming scheme --- .../000016_create_application_enabled_setting.down.sql | 1 - .../migrations/000016_create_application_enabled_setting.up.sql | 2 -- 2 files changed, 3 deletions(-) delete mode 100644 cmd/migrate/migrations/000016_create_application_enabled_setting.down.sql delete mode 100644 cmd/migrate/migrations/000016_create_application_enabled_setting.up.sql diff --git a/cmd/migrate/migrations/000016_create_application_enabled_setting.down.sql b/cmd/migrate/migrations/000016_create_application_enabled_setting.down.sql deleted file mode 100644 index b643cff4..00000000 --- a/cmd/migrate/migrations/000016_create_application_enabled_setting.down.sql +++ /dev/null @@ -1 +0,0 @@ -DELETE FROM settings WHERE key = 'application_enabled'; diff --git a/cmd/migrate/migrations/000016_create_application_enabled_setting.up.sql b/cmd/migrate/migrations/000016_create_application_enabled_setting.up.sql deleted file mode 100644 index 7953a0aa..00000000 --- a/cmd/migrate/migrations/000016_create_application_enabled_setting.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -INSERT INTO settings (key, value) VALUES ('application_enabled', 'true'::jsonb) -ON CONFLICT (key) DO NOTHING; From fca02265fb0eff0e41322935b52ef1b37d03eace Mon Sep 17 00:00:00 2001 From: anishalle Date: Tue, 24 Feb 2026 17:00:26 -0600 Subject: [PATCH 3/5] feat: GET endpoint finished, next is POST for SUPERADMIN --- cmd/api/api.go | 1 + cmd/api/applications.go | 227 ++++++++++-------- cmd/api/auth.go | 34 +-- cmd/api/reviews.go | 136 +++++------ cmd/api/scans.go | 98 ++++---- cmd/api/settings.go | 92 +++---- ...eate_applications_enabled_setting.down.sql | 1 + ...create_applications_enabled_setting.up.sql | 2 + docs/docs.go | 55 +++++ internal/store/applications.go | 17 ++ internal/store/mock_store.go | 5 + internal/store/storage.go | 1 + 12 files changed, 391 insertions(+), 278 deletions(-) create mode 100644 cmd/migrate/migrations/000016_create_applications_enabled_setting.down.sql create mode 100644 cmd/migrate/migrations/000016_create_applications_enabled_setting.up.sql diff --git a/cmd/api/api.go b/cmd/api/api.go index 9fb3e599..fa0ac3c5 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -121,6 +121,7 @@ func (app *application) mount() http.Handler { r.Get("/me", app.getOrCreateApplicationHandler) r.Patch("/me", app.updateApplicationHandler) r.Post("/me/submit", app.submitApplicationHandler) + r.Get("/enabled", app.getApplicationsEnabled) }) r.Group(func(r chi.Router) { // TODO clean up this routing diff --git a/cmd/api/applications.go b/cmd/api/applications.go index 33e90f28..2e00b431 100644 --- a/cmd/api/applications.go +++ b/cmd/api/applications.go @@ -55,16 +55,16 @@ type ApplicationWithQuestions struct { // getOrCreateApplicationHandler returns or creates the user's hackathon application // -// @Summary Get or create application -// @Description Returns the authenticated user's hackathon application. If no application exists, creates a new draft application. -// @Tags applications -// @Accept json -// @Produce json -// @Success 200 {object} store.Application -// @Failure 401 {object} object{error=string} -// @Failure 500 {object} object{error=string} -// @Security CookieAuth -// @Router /applications/me [get] +// @Summary Get or create application +// @Description Returns the authenticated user's hackathon application. If no application exists, creates a new draft application. +// @Tags applications +// @Accept json +// @Produce json +// @Success 200 {object} store.Application +// @Failure 401 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /applications/me [get] func (app *application) getOrCreateApplicationHandler(w http.ResponseWriter, r *http.Request) { user := getUserFromContext(r.Context()) if user == nil { @@ -116,19 +116,19 @@ func (app *application) getOrCreateApplicationHandler(w http.ResponseWriter, r * // updateApplicationHandler partially updates the authenticated user's application // -// @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 applications -// @Accept json -// @Produce json -// @Param application body UpdateApplicationPayload true "Fields to update" -// @Success 200 {object} store.Application -// @Failure 400 {object} object{error=string} -// @Failure 401 {object} object{error=string} -// @Failure 404 {object} object{error=string} -// @Failure 409 {object} object{error=string} "Application not in draft status" -// @Security CookieAuth -// @Router /applications/me [patch] +// @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 applications +// @Accept json +// @Produce json +// @Param application body UpdateApplicationPayload true "Fields to update" +// @Success 200 {object} store.Application +// @Failure 400 {object} object{error=string} +// @Failure 401 {object} object{error=string} +// @Failure 404 {object} object{error=string} +// @Failure 409 {object} object{error=string} "Application not in draft status" +// @Security CookieAuth +// @Router /applications/me [patch] func (app *application) updateApplicationHandler(w http.ResponseWriter, r *http.Request) { user := getUserFromContext(r.Context()) if user == nil { @@ -251,17 +251,17 @@ func (app *application) updateApplicationHandler(w http.ResponseWriter, r *http. // submitApplicationHandler submits the authenticated user's application for review // -// @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 applications -// @Produce json -// @Success 200 {object} store.Application -// @Failure 400 {object} object{error=string} "Missing required fields" -// @Failure 401 {object} object{error=string} -// @Failure 404 {object} object{error=string} -// @Failure 409 {object} object{error=string} "Application not in draft status" -// @Security CookieAuth -// @Router /applications/me/submit [post] +// @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 applications +// @Produce json +// @Success 200 {object} store.Application +// @Failure 400 {object} object{error=string} "Missing required fields" +// @Failure 401 {object} object{error=string} +// @Failure 404 {object} object{error=string} +// @Failure 409 {object} object{error=string} "Application not in draft status" +// @Security CookieAuth +// @Router /applications/me/submit [post] func (app *application) submitApplicationHandler(w http.ResponseWriter, r *http.Request) { user := getUserFromContext(r.Context()) if user == nil { @@ -388,16 +388,16 @@ func (app *application) submitApplicationHandler(w http.ResponseWriter, r *http. // getApplicationStatsHandler returns aggregated statistics for all applications // -// @Summary Get application stats (Admin) -// @Description Returns aggregated statistics for all applications -// @Tags admin -// @Produce json -// @Success 200 {object} store.ApplicationStats -// @Failure 401 {object} object{error=string} -// @Failure 403 {object} object{error=string} -// @Failure 500 {object} object{error=string} -// @Security CookieAuth -// @Router /admin/applications/stats [get] +// @Summary Get application stats (Admin) +// @Description Returns aggregated statistics for all applications +// @Tags admin +// @Produce json +// @Success 200 {object} store.ApplicationStats +// @Failure 401 {object} object{error=string} +// @Failure 403 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /admin/applications/stats [get] func (app *application) getApplicationStatsHandler(w http.ResponseWriter, r *http.Request) { stats, err := app.store.Application.GetStats(r.Context()) if err != nil { @@ -412,21 +412,21 @@ func (app *application) getApplicationStatsHandler(w http.ResponseWriter, r *htt // listApplicationsHandler lists all applications with cursor-based pagination // -// @Summary List applications (Admin) -// @Description Lists all applications with cursor-based pagination and optional status filter -// @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" -// @Success 200 {object} store.ApplicationListResult -// @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 /admin/applications [get] +// @Summary List applications (Admin) +// @Description Lists all applications with cursor-based pagination and optional status filter +// @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" +// @Success 200 {object} store.ApplicationListResult +// @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 /admin/applications [get] func (app *application) listApplicationsHandler(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() @@ -502,23 +502,27 @@ type EmailListResponse struct { Count int `json:"count"` } +type ApplicationsEnabledResponse struct { + Enabled bool `json:"enabled"` +} + // setApplicationStatus sets the final status on an application // -// @Summary Set application status (Super Admin) -// @Description Sets the final status (accepted, rejected, or waitlisted) on an application -// @Tags superadmin -// @Accept json -// @Produce json -// @Param applicationID path string true "Application ID" -// @Param status body SetStatusPayload true "New status" -// @Success 200 {object} ApplicationResponse -// @Failure 400 {object} object{error=string} -// @Failure 401 {object} object{error=string} -// @Failure 403 {object} object{error=string} -// @Failure 404 {object} object{error=string} -// @Failure 500 {object} object{error=string} -// @Security CookieAuth -// @Router /superadmin/applications/{applicationID}/status [patch] +// @Summary Set application status (Super Admin) +// @Description Sets the final status (accepted, rejected, or waitlisted) on an application +// @Tags superadmin +// @Accept json +// @Produce json +// @Param applicationID path string true "Application ID" +// @Param status body SetStatusPayload true "New status" +// @Success 200 {object} ApplicationResponse +// @Failure 400 {object} object{error=string} +// @Failure 401 {object} object{error=string} +// @Failure 403 {object} object{error=string} +// @Failure 404 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /superadmin/applications/{applicationID}/status [patch] func (app *application) setApplicationStatus(w http.ResponseWriter, r *http.Request) { applicationID := chi.URLParam(r, "applicationID") if applicationID == "" { @@ -554,18 +558,18 @@ func (app *application) setApplicationStatus(w http.ResponseWriter, r *http.Requ // getApplication returns a single application by ID with embedded questions // -// @Summary Get application by ID (Admin) -// @Description Returns a single application by its ID with embedded short answer questions -// @Tags admin -// @Produce json -// @Param applicationID path string true "Application ID" -// @Success 200 {object} ApplicationWithQuestions -// @Failure 400 {object} object{error=string} -// @Failure 401 {object} object{error=string} -// @Failure 403 {object} object{error=string} -// @Failure 404 {object} object{error=string} -// @Security CookieAuth -// @Router /admin/applications/{applicationID} [get] +// @Summary Get application by ID (Admin) +// @Description Returns a single application by its ID with embedded short answer questions +// @Tags admin +// @Produce json +// @Param applicationID path string true "Application ID" +// @Success 200 {object} ApplicationWithQuestions +// @Failure 400 {object} object{error=string} +// @Failure 401 {object} object{error=string} +// @Failure 403 {object} object{error=string} +// @Failure 404 {object} object{error=string} +// @Security CookieAuth +// @Router /admin/applications/{applicationID} [get] func (app *application) getApplication(w http.ResponseWriter, r *http.Request) { applicationID := chi.URLParam(r, "applicationID") if applicationID == "" { @@ -603,18 +607,18 @@ func (app *application) getApplication(w http.ResponseWriter, r *http.Request) { // getApplicantEmailsByStatusHandler returns applicant emails filtered by status // -// @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 -// @Produce json -// @Param status query string true "Application status (accepted, rejected, or waitlisted)" -// @Success 200 {object} EmailListResponse -// @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/applications/emails [get] +// @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 +// @Produce json +// @Param status query string true "Application status (accepted, rejected, or waitlisted)" +// @Success 200 {object} EmailListResponse +// @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/applications/emails [get] func (app *application) getApplicantEmailsByStatusHandler(w http.ResponseWriter, r *http.Request) { statusStr := r.URL.Query().Get("status") if statusStr == "" { @@ -651,3 +655,30 @@ func (app *application) getApplicantEmailsByStatusHandler(w http.ResponseWriter, } } + +// getApplicationsEnabled returns whether applications are currently open +// +// @Summary Get applications enabled status +// @Description Returns whether the application portal is currently open for submissions +// @Tags applications +// @Produce json +// @Success 200 {object} ApplicationsEnabledResponse +// @Failure 401 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /applications/enabled [get] +func (app *application) getApplicationsEnabled(w http.ResponseWriter, r *http.Request) { + enabled, err := app.store.Application.GetApplicationsEnabled(r.Context()) + if err != nil { + app.internalServerError(w, r, err) + return + } + + response := ApplicationsEnabledResponse{ + Enabled: enabled, + } + + if err = app.jsonResponse(w, http.StatusOK, response); err != nil { + app.internalServerError(w, r, err) + } +} diff --git a/cmd/api/auth.go b/cmd/api/auth.go index 94bfd0ca..0013bbd8 100644 --- a/cmd/api/auth.go +++ b/cmd/api/auth.go @@ -20,14 +20,14 @@ type UserResponse struct { // getCurrentUserHandler returns the authenticated user's profile // -// @Summary Get current user -// @Description Returns the authenticated user's profile -// @Tags auth -// @Accept json -// @Produce json -// @Success 200 {object} UserResponse -// @Failure 401 {object} object{error=string} -// @Router /auth/me [get] +// @Summary Get current user +// @Description Returns the authenticated user's profile +// @Tags auth +// @Accept json +// @Produce json +// @Success 200 {object} UserResponse +// @Failure 401 {object} object{error=string} +// @Router /auth/me [get] func (app *application) getCurrentUserHandler(w http.ResponseWriter, r *http.Request) { user := getUserFromContext(r.Context()) if user == nil { @@ -57,15 +57,15 @@ type CheckEmailResponse struct { // checkEmailAuthMethodHandler checks if an email is registered and returns the auth method // -// @Summary Check email auth method -// @Description Checks if an email is registered and returns the auth method used -// @Tags auth -// @Accept json -// @Produce json -// @Param email query string true "Email address to check" -// @Success 200 {object} CheckEmailResponse -// @Failure 400 {object} object{error=string} -// @Router /auth/check-email [get] +// @Summary Check email auth method +// @Description Checks if an email is registered and returns the auth method used +// @Tags auth +// @Accept json +// @Produce json +// @Param email query string true "Email address to check" +// @Success 200 {object} CheckEmailResponse +// @Failure 400 {object} object{error=string} +// @Router /auth/check-email [get] func (app *application) checkEmailAuthMethodHandler(w http.ResponseWriter, r *http.Request) { email := r.URL.Query().Get("email") if email == "" { diff --git a/cmd/api/reviews.go b/cmd/api/reviews.go index df55b87c..624dc309 100644 --- a/cmd/api/reviews.go +++ b/cmd/api/reviews.go @@ -31,16 +31,16 @@ type NotesListResponse struct { // getPendingReviews returns all pending reviews assigned to the current admin // -// @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 -// @Produce json -// @Success 200 {object} PendingReviewsListResponse -// @Failure 401 {object} object{error=string} -// @Failure 403 {object} object{error=string} -// @Failure 500 {object} object{error=string} -// @Security CookieAuth -// @Router /admin/reviews/pending [get] +// @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 +// @Produce json +// @Success 200 {object} PendingReviewsListResponse +// @Failure 401 {object} object{error=string} +// @Failure 403 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /admin/reviews/pending [get] func (app *application) getPendingReviews(w http.ResponseWriter, r *http.Request) { user := getUserFromContext(r.Context()) @@ -61,16 +61,16 @@ func (app *application) getPendingReviews(w http.ResponseWriter, r *http.Request // getCompletedReviews returns all reviews the current admin has completed // -// @Summary Get completed reviews (Admin) -// @Description Returns all reviews the current admin has completed (voted on), including application details -// @Tags admin -// @Produce json -// @Success 200 {object} CompletedReviewsListResponse -// @Failure 401 {object} object{error=string} -// @Failure 403 {object} object{error=string} -// @Failure 500 {object} object{error=string} -// @Security CookieAuth -// @Router /admin/reviews/completed [get] +// @Summary Get completed reviews (Admin) +// @Description Returns all reviews the current admin has completed (voted on), including application details +// @Tags admin +// @Produce json +// @Success 200 {object} CompletedReviewsListResponse +// @Failure 401 {object} object{error=string} +// @Failure 403 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /admin/reviews/completed [get] func (app *application) getCompletedReviews(w http.ResponseWriter, r *http.Request) { user := getUserFromContext(r.Context()) @@ -91,18 +91,18 @@ func (app *application) getCompletedReviews(w http.ResponseWriter, r *http.Reque // getApplicationNotes returns all reviewer notes for a specific application // -// @Summary Get notes for an application (Admin) -// @Description Returns all reviewer notes for a specific application without exposing votes -// @Tags admin -// @Produce json -// @Param applicationID path string true "Application ID" -// @Success 200 {object} NotesListResponse -// @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 /admin/applications/{applicationID}/notes [get] +// @Summary Get notes for an application (Admin) +// @Description Returns all reviewer notes for a specific application without exposing votes +// @Tags admin +// @Produce json +// @Param applicationID path string true "Application ID" +// @Success 200 {object} NotesListResponse +// @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 /admin/applications/{applicationID}/notes [get] func (app *application) getApplicationNotes(w http.ResponseWriter, r *http.Request) { applicationID := chi.URLParam(r, "applicationID") if applicationID == "" { @@ -127,16 +127,16 @@ func (app *application) getApplicationNotes(w http.ResponseWriter, r *http.Reque // batchAssignReviews assigns submitted applications to admins using workload balancing // -// @Summary Batch assign reviews (SuperAdmin) -// @Description Finds all submitted applications needing more reviews and assigns them to admins using workload balancing -// @Tags superadmin -// @Produce json -// @Success 200 {object} store.BatchAssignmentResult -// @Failure 401 {object} object{error=string} -// @Failure 403 {object} object{error=string} -// @Failure 500 {object} object{error=string} -// @Security CookieAuth -// @Router /superadmin/applications/assign [post] +// @Summary Batch assign reviews (SuperAdmin) +// @Description Finds all submitted applications needing more reviews and assigns them to admins using workload balancing +// @Tags superadmin +// @Produce json +// @Success 200 {object} store.BatchAssignmentResult +// @Failure 401 {object} object{error=string} +// @Failure 403 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /superadmin/applications/assign [post] func (app *application) batchAssignReviews(w http.ResponseWriter, r *http.Request) { reviewsPerApp, err := app.store.Settings.GetReviewsPerApplication(r.Context()) if err != nil { @@ -157,17 +157,17 @@ func (app *application) batchAssignReviews(w http.ResponseWriter, r *http.Reques // getNextReview assigns and returns the next application needing review // -// @Summary Get next review assignment (Admin) -// @Description Automatically assigns the next submitted application needing review to the current admin and returns it -// @Tags admin -// @Produce json -// @Success 200 {object} ReviewResponse -// @Failure 401 {object} object{error=string} -// @Failure 403 {object} object{error=string} -// @Failure 404 {object} object{error=string} "No applications need review" -// @Failure 500 {object} object{error=string} -// @Security CookieAuth -// @Router /admin/reviews/next [get] +// @Summary Get next review assignment (Admin) +// @Description Automatically assigns the next submitted application needing review to the current admin and returns it +// @Tags admin +// @Produce json +// @Success 200 {object} ReviewResponse +// @Failure 401 {object} object{error=string} +// @Failure 403 {object} object{error=string} +// @Failure 404 {object} object{error=string} "No applications need review" +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /admin/reviews/next [get] func (app *application) getNextReview(w http.ResponseWriter, r *http.Request) { user := getUserFromContext(r.Context()) @@ -199,21 +199,21 @@ func (app *application) getNextReview(w http.ResponseWriter, r *http.Request) { // submitVote records the admin's vote on an assigned application review // -// @Summary Submit vote on a review (Admin) -// @Description Records the admin's vote (accept/reject/waitlist) on an assigned application review -// @Tags admin -// @Accept json -// @Produce json -// @Param reviewID path string true "Review ID" -// @Param vote body SubmitVotePayload true "Vote and optional notes" -// @Success 200 {object} ReviewResponse -// @Failure 400 {object} object{error=string} -// @Failure 401 {object} object{error=string} -// @Failure 403 {object} object{error=string} -// @Failure 404 {object} object{error=string} -// @Failure 500 {object} object{error=string} -// @Security CookieAuth -// @Router /admin/reviews/{reviewID} [put] +// @Summary Submit vote on a review (Admin) +// @Description Records the admin's vote (accept/reject/waitlist) on an assigned application review +// @Tags admin +// @Accept json +// @Produce json +// @Param reviewID path string true "Review ID" +// @Param vote body SubmitVotePayload true "Vote and optional notes" +// @Success 200 {object} ReviewResponse +// @Failure 400 {object} object{error=string} +// @Failure 401 {object} object{error=string} +// @Failure 403 {object} object{error=string} +// @Failure 404 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /admin/reviews/{reviewID} [put] func (app *application) submitVote(w http.ResponseWriter, r *http.Request) { reviewID := chi.URLParam(r, "reviewID") if reviewID == "" { diff --git a/cmd/api/scans.go b/cmd/api/scans.go index 561122d0..df2a9b6c 100644 --- a/cmd/api/scans.go +++ b/cmd/api/scans.go @@ -55,20 +55,20 @@ func (app *application) getScanTypesHandler(w http.ResponseWriter, r *http.Reque // createScanHandler records a scan for a user // -// @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 -// @Accept json -// @Produce json -// @Param scan body CreateScanPayload true "Scan to create" -// @Success 201 {object} store.Scan -// @Failure 400 {object} object{error=string} -// @Failure 401 {object} object{error=string} -// @Failure 403 {object} object{error=string} -// @Failure 409 {object} object{error=string} -// @Failure 500 {object} object{error=string} -// @Security CookieAuth -// @Router /admin/scans [post] +// @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 +// @Accept json +// @Produce json +// @Param scan body CreateScanPayload true "Scan to create" +// @Success 201 {object} store.Scan +// @Failure 400 {object} object{error=string} +// @Failure 401 {object} object{error=string} +// @Failure 403 {object} object{error=string} +// @Failure 409 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /admin/scans [post] func (app *application) createScanHandler(w http.ResponseWriter, r *http.Request) { var req CreateScanPayload if err := readJSON(w, r, &req); err != nil { @@ -151,18 +151,18 @@ func (app *application) createScanHandler(w http.ResponseWriter, r *http.Request // getUserScansHandler returns all scan records for a specified user // -// @Summary Get scans for a user (Admin) -// @Description Returns all scan records for the specified user, ordered by most recent first -// @Tags admin -// @Produce json -// @Param userID path string true "User ID" -// @Success 200 {object} ScansResponse -// @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 /admin/scans/user/{userID} [get] +// @Summary Get scans for a user (Admin) +// @Description Returns all scan records for the specified user, ordered by most recent first +// @Tags admin +// @Produce json +// @Param userID path string true "User ID" +// @Success 200 {object} ScansResponse +// @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 /admin/scans/user/{userID} [get] func (app *application) getUserScansHandler(w http.ResponseWriter, r *http.Request) { userID := chi.URLParam(r, "userID") if userID == "" { @@ -183,16 +183,16 @@ func (app *application) getUserScansHandler(w http.ResponseWriter, r *http.Reque // getScanStatsHandler returns aggregate scan counts grouped by scan type // -// @Summary Get scan statistics (Admin) -// @Description Returns aggregate scan counts grouped by scan type -// @Tags admin -// @Produce json -// @Success 200 {object} ScanStatsResponse -// @Failure 401 {object} object{error=string} -// @Failure 403 {object} object{error=string} -// @Failure 500 {object} object{error=string} -// @Security CookieAuth -// @Router /admin/scans/stats [get] +// @Summary Get scan statistics (Admin) +// @Description Returns aggregate scan counts grouped by scan type +// @Tags admin +// @Produce json +// @Success 200 {object} ScanStatsResponse +// @Failure 401 {object} object{error=string} +// @Failure 403 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /admin/scans/stats [get] func (app *application) getScanStatsHandler(w http.ResponseWriter, r *http.Request) { stats, err := app.store.Scans.GetStats(r.Context()) if err != nil { @@ -207,19 +207,19 @@ func (app *application) getScanStatsHandler(w http.ResponseWriter, r *http.Reque // updateScanTypesHandler replaces all scan types with the provided array // -// @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 -// @Accept json -// @Produce json -// @Param scan_types body UpdateScanTypesPayload true "Scan types to set" -// @Success 200 {object} ScanTypesResponse -// @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/settings/scan-types [put] +// @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 +// @Accept json +// @Produce json +// @Param scan_types body UpdateScanTypesPayload true "Scan types to set" +// @Success 200 {object} ScanTypesResponse +// @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/settings/scan-types [put] func (app *application) updateScanTypesHandler(w http.ResponseWriter, r *http.Request) { var req UpdateScanTypesPayload if err := readJSON(w, r, &req); err != nil { diff --git a/cmd/api/settings.go b/cmd/api/settings.go index aa27fb2d..de5e8739 100644 --- a/cmd/api/settings.go +++ b/cmd/api/settings.go @@ -17,16 +17,16 @@ type ShortAnswerQuestionsResponse struct { // getShortAnswerQuestions returns all configurable short answer questions // -// @Summary Get short answer questions (Super Admin) -// @Description Returns all configurable short answer questions for hacker applications -// @Tags superadmin -// @Produce json -// @Success 200 {object} ShortAnswerQuestionsResponse -// @Failure 401 {object} object{error=string} -// @Failure 403 {object} object{error=string} -// @Failure 500 {object} object{error=string} -// @Security CookieAuth -// @Router /superadmin/settings/saquestions [get] +// @Summary Get short answer questions (Super Admin) +// @Description Returns all configurable short answer questions for hacker applications +// @Tags superadmin +// @Produce json +// @Success 200 {object} ShortAnswerQuestionsResponse +// @Failure 401 {object} object{error=string} +// @Failure 403 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /superadmin/settings/saquestions [get] func (app *application) getShortAnswerQuestions(w http.ResponseWriter, r *http.Request) { questions, err := app.store.Settings.GetShortAnswerQuestions(r.Context()) if err != nil { @@ -45,19 +45,19 @@ func (app *application) getShortAnswerQuestions(w http.ResponseWriter, r *http.R // updateShortAnswerQuestions replaces all short answer questions // -// @Summary Update short answer questions (Super Admin) -// @Description Replaces all short answer questions with the provided array -// @Tags superadmin -// @Accept json -// @Produce json -// @Param questions body UpdateShortAnswerQuestionsPayload true "Questions to set" -// @Success 200 {object} ShortAnswerQuestionsResponse -// @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/settings/saquestions [put] +// @Summary Update short answer questions (Super Admin) +// @Description Replaces all short answer questions with the provided array +// @Tags superadmin +// @Accept json +// @Produce json +// @Param questions body UpdateShortAnswerQuestionsPayload true "Questions to set" +// @Success 200 {object} ShortAnswerQuestionsResponse +// @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/settings/saquestions [put] func (app *application) updateShortAnswerQuestions(w http.ResponseWriter, r *http.Request) { var req UpdateShortAnswerQuestionsPayload if err := readJSON(w, r, &req); err != nil { @@ -102,16 +102,16 @@ type ReviewsPerAppResponse struct { // getReviewsPerApp returns the number of reviews required per application // -// @Summary Get reviews per application (Super Admin) -// @Description Returns the number of reviews required per application -// @Tags superadmin -// @Produce json -// @Success 200 {object} ReviewsPerAppResponse -// @Failure 401 {object} object{error=string} -// @Failure 403 {object} object{error=string} -// @Failure 500 {object} object{error=string} -// @Security CookieAuth -// @Router /superadmin/settings/reviews-per-app [get] +// @Summary Get reviews per application (Super Admin) +// @Description Returns the number of reviews required per application +// @Tags superadmin +// @Produce json +// @Success 200 {object} ReviewsPerAppResponse +// @Failure 401 {object} object{error=string} +// @Failure 403 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /superadmin/settings/reviews-per-app [get] func (app *application) getReviewsPerApp(w http.ResponseWriter, r *http.Request) { count, err := app.store.Settings.GetReviewsPerApplication(r.Context()) if err != nil { @@ -130,19 +130,19 @@ func (app *application) getReviewsPerApp(w http.ResponseWriter, r *http.Request) // setReviewsPerApp sets the number of reviews required per application // -// @Summary Set reviews per application (Super Admin) -// @Description Sets the number of reviews required per application -// @Tags superadmin -// @Accept json -// @Produce json -// @Param reviews_per_application body SetReviewsPerAppPayload true "Reviews per application value" -// @Success 200 {object} ReviewsPerAppResponse -// @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/settings/reviews-per-app [post] +// @Summary Set reviews per application (Super Admin) +// @Description Sets the number of reviews required per application +// @Tags superadmin +// @Accept json +// @Produce json +// @Param reviews_per_application body SetReviewsPerAppPayload true "Reviews per application value" +// @Success 200 {object} ReviewsPerAppResponse +// @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/settings/reviews-per-app [post] func (app *application) setReviewsPerApp(w http.ResponseWriter, r *http.Request) { var req SetReviewsPerAppPayload if err := readJSON(w, r, &req); err != nil { diff --git a/cmd/migrate/migrations/000016_create_applications_enabled_setting.down.sql b/cmd/migrate/migrations/000016_create_applications_enabled_setting.down.sql new file mode 100644 index 00000000..00ab13be --- /dev/null +++ b/cmd/migrate/migrations/000016_create_applications_enabled_setting.down.sql @@ -0,0 +1 @@ +DELETE FROM settings WHERE key = 'applications_enabled'; diff --git a/cmd/migrate/migrations/000016_create_applications_enabled_setting.up.sql b/cmd/migrate/migrations/000016_create_applications_enabled_setting.up.sql new file mode 100644 index 00000000..ee3eec9d --- /dev/null +++ b/cmd/migrate/migrations/000016_create_applications_enabled_setting.up.sql @@ -0,0 +1,2 @@ +INSERT INTO settings (key, value) VALUES ('applications_enabled', 'true'::jsonb) +ON CONFLICT (key) DO NOTHING; diff --git a/docs/docs.go b/docs/docs.go index cd056d44..9dadf016 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -907,6 +907,53 @@ const docTemplate = `{ } } }, + "/applications/enabled": { + "get": { + "security": [ + { + "CookieAuth": [] + } + ], + "description": "Returns whether the application portal is currently open for submissions", + "produces": [ + "application/json" + ], + "tags": [ + "applications" + ], + "summary": "Get applications enabled status", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.ApplicationsEnabledResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + }, "/applications/me": { "get": { "security": [ @@ -2021,6 +2068,14 @@ const docTemplate = `{ } } }, + "main.ApplicationsEnabledResponse": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, "main.CheckEmailResponse": { "type": "object", "properties": { diff --git a/internal/store/applications.go b/internal/store/applications.go index 4f1a68c4..4ba5d6ef 100644 --- a/internal/store/applications.go +++ b/internal/store/applications.go @@ -683,3 +683,20 @@ func (s *ApplicationsStore) GetEmailsByStatus(ctx context.Context, status Applic return users, rows.Err() } + +func (s *ApplicationsStore) GetApplicationsEnabled(ctx context.Context) (bool, error) { + ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration) + defer cancel() + + query := ` + SELECT value + FROM settings + WHERE key = 'applications_enabled' + ` + var value bool + err := s.db.QueryRowContext(ctx, query).Scan(&value) + if err != nil { + return false, err // We won't handle err here, (because if the setting doesn't exist, we want it to error instead of defaulting to false) + } + return value, nil +} diff --git a/internal/store/mock_store.go b/internal/store/mock_store.go index f89df53e..37a8b7d3 100644 --- a/internal/store/mock_store.go +++ b/internal/store/mock_store.go @@ -113,6 +113,11 @@ func (m *MockApplicationStore) GetEmailsByStatus(ctx context.Context, status App return args.Get(0).([]UserEmailInfo), args.Error(1) } +func (m *MockApplicationStore) GetApplicationsEnabled(ctx context.Context) (bool, error) { + args := m.Called() + return args.Get(0).(bool), args.Error(1) +} + // mock implementation of the Settings interface type MockSettingsStore struct { mock.Mock diff --git a/internal/store/storage.go b/internal/store/storage.go index 01c1c73a..71e72728 100644 --- a/internal/store/storage.go +++ b/internal/store/storage.go @@ -33,6 +33,7 @@ type Storage struct { GetStats(ctx context.Context) (*ApplicationStats, error) SetStatus(ctx context.Context, id string, status ApplicationStatus) (*Application, error) GetEmailsByStatus(ctx context.Context, status ApplicationStatus) ([]UserEmailInfo, error) + GetApplicationsEnabled(ctx context.Context) (bool, error) } Settings interface { GetShortAnswerQuestions(ctx context.Context) ([]ShortAnswerQuestion, error) From 40c31ea1abba6513836d6983518e546de61dda31 Mon Sep 17 00:00:00 2001 From: anishalle Date: Thu, 5 Mar 2026 14:19:37 -0600 Subject: [PATCH 4/5] feat: add POST for setApplicationsEnabled resolves #31 --- client/web/package-lock.json | 20 ++++- cmd/api/api.go | 1 + cmd/api/applications.go | 41 +++++++++ ...insert_review_assignment_setting.down.sql} | 0 ...7_insert_review_assignment_setting.up.sql} | 0 docs/docs.go | 87 +++++++++++++++++++ internal/store/applications.go | 20 +++++ internal/store/mock_store.go | 5 ++ internal/store/storage.go | 1 + 9 files changed, 174 insertions(+), 1 deletion(-) rename cmd/migrate/migrations/{000016_insert_review_assignment_setting.down.sql => 000017_insert_review_assignment_setting.down.sql} (100%) rename cmd/migrate/migrations/{000016_insert_review_assignment_setting.up.sql => 000017_insert_review_assignment_setting.up.sql} (100%) diff --git a/client/web/package-lock.json b/client/web/package-lock.json index e8088851..5e1627ab 100644 --- a/client/web/package-lock.json +++ b/client/web/package-lock.json @@ -112,6 +112,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -3459,6 +3460,7 @@ "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3469,6 +3471,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3479,6 +3482,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3528,6 +3532,7 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -3766,6 +3771,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3914,6 +3920,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4278,7 +4285,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -4385,6 +4393,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5743,6 +5752,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5773,6 +5783,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -5785,6 +5796,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -6203,6 +6215,7 @@ "resolved": "https://registry.npmjs.org/supertokens-web-js/-/supertokens-web-js-0.16.0.tgz", "integrity": "sha512-wuIdlVJtOsx4ZX0kCyl8lxmmAodXLlMAniZEHyVhsH2fhersh7bMrHpvgN9WoC470HYNC22qpHdlJngvyh/cSA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@simplewebauthn/browser": "^13.0.0", "supertokens-js-override": "0.0.4", @@ -6318,6 +6331,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6386,6 +6400,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6572,6 +6587,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -6663,6 +6679,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6728,6 +6745,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/cmd/api/api.go b/cmd/api/api.go index da9a8e3e..5a75bb44 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -162,6 +162,7 @@ func (app *application) mount() http.Handler { // Application Config r.Get("/settings/saquestions", app.getShortAnswerQuestions) r.Put("/settings/saquestions", app.updateShortAnswerQuestions) + r.Put("/settings/applications-enabled", app.setApplicationsEnabled) // Reviews Config r.Get("/settings/reviews-per-app", app.getReviewsPerApp) diff --git a/cmd/api/applications.go b/cmd/api/applications.go index 2e00b431..9fd2767a 100644 --- a/cmd/api/applications.go +++ b/cmd/api/applications.go @@ -506,6 +506,10 @@ type ApplicationsEnabledResponse struct { Enabled bool `json:"enabled"` } +type SetApplicationsEnabledResponse struct { + Enabled bool `json:"enabled"` //no validate required because bool enforces the only two possible values, which is what we want +} + // setApplicationStatus sets the final status on an application // // @Summary Set application status (Super Admin) @@ -682,3 +686,40 @@ func (app *application) getApplicationsEnabled(w http.ResponseWriter, r *http.Re app.internalServerError(w, r, err) } } + +// setApplicationsEnabled updates whether applications are currently open +// +// @Summary Set applications enabled status +// @Description Sets whether the application portal is currently open for submissions. Requires SuperAdmin privileges. +// @Tags superadmin +// @Produce json +// @Param enabled query bool true "Enable or disable applications" +// @Success 200 {object} SetApplicationsEnabledResponse +// @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/settings/applications-enabled [put] +func (app *application) setApplicationsEnabled(w http.ResponseWriter, r *http.Request) { + enabled, err := strconv.ParseBool(r.URL.Query().Get("enabled")) + + if err != nil { + app.badRequestResponse(w, r, errors.New("enabled must be a boolean value")) + return + } + + enabled, err = app.store.Application.SetApplicationsEnabled(r.Context(), enabled) + if err != nil { + app.internalServerError(w, r, err) + } + + //NOTE: Following existing design pattern of Get response and Set response structs + response := SetApplicationsEnabledResponse{ + Enabled: enabled, + } + + if err = app.jsonResponse(w, http.StatusOK, response); err != nil { + app.internalServerError(w, r, err) + } +} diff --git a/cmd/migrate/migrations/000016_insert_review_assignment_setting.down.sql b/cmd/migrate/migrations/000017_insert_review_assignment_setting.down.sql similarity index 100% rename from cmd/migrate/migrations/000016_insert_review_assignment_setting.down.sql rename to cmd/migrate/migrations/000017_insert_review_assignment_setting.down.sql diff --git a/cmd/migrate/migrations/000016_insert_review_assignment_setting.up.sql b/cmd/migrate/migrations/000017_insert_review_assignment_setting.up.sql similarity index 100% rename from cmd/migrate/migrations/000016_insert_review_assignment_setting.up.sql rename to cmd/migrate/migrations/000017_insert_review_assignment_setting.up.sql diff --git a/docs/docs.go b/docs/docs.go index 90a3258d..1d2de75a 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1557,6 +1557,84 @@ const docTemplate = `{ } } }, + "/superadmin/settings/applications-enabled": { + "put": { + "security": [ + { + "CookieAuth": [] + } + ], + "description": "Sets whether the application portal is currently open for submissions. Requires SuperAdmin privileges.", + "produces": [ + "application/json" + ], + "tags": [ + "superadmin" + ], + "summary": "Set applications enabled status", + "parameters": [ + { + "type": "boolean", + "description": "Enable or disable applications", + "name": "enabled", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.SetApplicationsEnabledResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + }, "/superadmin/settings/review-assignment-toggle": { "get": { "security": [ @@ -2365,6 +2443,15 @@ const docTemplate = `{ } } }, + "main.SetApplicationsEnabledResponse": { + "type": "object", + "properties": { + "enabled": { + "description": "no validate required because bool enforces the only two possible values, which is what we want", + "type": "boolean" + } + } + }, "main.SetReviewAssignmentTogglePayload": { "type": "object", "properties": { diff --git a/internal/store/applications.go b/internal/store/applications.go index 4ba5d6ef..dc0d589e 100644 --- a/internal/store/applications.go +++ b/internal/store/applications.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "strconv" "strings" "time" ) @@ -700,3 +701,22 @@ func (s *ApplicationsStore) GetApplicationsEnabled(ctx context.Context) (bool, e } return value, nil } + +func (s *ApplicationsStore) SetApplicationsEnabled(ctx context.Context, enabled bool) (bool, error) { + ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration) + defer cancel() + + query := ` + UPDATE settings + SET value = $1::jsonb + WHERE key = 'applications_enabled' + RETURNING value` + + var value bool + err := s.db.QueryRowContext(ctx, query, strconv.FormatBool(enabled)).Scan(&value) + if err != nil { + return value, err + } + + return value, nil +} diff --git a/internal/store/mock_store.go b/internal/store/mock_store.go index f0d50571..16b64c99 100644 --- a/internal/store/mock_store.go +++ b/internal/store/mock_store.go @@ -118,6 +118,11 @@ func (m *MockApplicationStore) GetApplicationsEnabled(ctx context.Context) (bool return args.Get(0).(bool), args.Error(1) } +func (m *MockApplicationStore) SetApplicationsEnabled(ctx context.Context, enabled bool) (bool, error) { + args := m.Called() + return args.Get(0).(bool), args.Error(1) +} + // mock implementation of the Settings interface type MockSettingsStore struct { mock.Mock diff --git a/internal/store/storage.go b/internal/store/storage.go index e5485ddc..8aeeffce 100644 --- a/internal/store/storage.go +++ b/internal/store/storage.go @@ -34,6 +34,7 @@ type Storage struct { SetStatus(ctx context.Context, id string, status ApplicationStatus) (*Application, error) GetEmailsByStatus(ctx context.Context, status ApplicationStatus) ([]UserEmailInfo, error) GetApplicationsEnabled(ctx context.Context) (bool, error) + SetApplicationsEnabled(ctx context.Context, enabled bool) (bool, error) } Settings interface { GetShortAnswerQuestions(ctx context.Context) ([]ShortAnswerQuestion, error) From 8db478388e475dbc827c95bb854ed8025a526c12 Mon Sep 17 00:00:00 2001 From: anishalle Date: Tue, 24 Mar 2026 13:08:40 -0500 Subject: [PATCH 5/5] fix: pr review, waiting on refactor from main --- cmd/api/api.go | 2 +- cmd/api/applications.go | 178 +++++--------- cmd/api/resume.go | 74 +++--- cmd/api/settings.go | 68 ++++++ cmd/api/superadmin_users.go | 58 ++--- ...insert_review_assignment_setting.down.sql} | 0 ...3_insert_review_assignment_setting.up.sql} | 0 docs/docs.go | 227 +++++++++++++++++- 8 files changed, 409 insertions(+), 198 deletions(-) rename cmd/migrate/migrations/{000017_insert_review_assignment_setting.down.sql => 000023_insert_review_assignment_setting.down.sql} (100%) rename cmd/migrate/migrations/{000017_insert_review_assignment_setting.up.sql => 000023_insert_review_assignment_setting.up.sql} (100%) diff --git a/cmd/api/api.go b/cmd/api/api.go index bc90a003..4f487cea 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -169,7 +169,6 @@ func (app *application) mount() http.Handler { r.Get("/me", app.getOrCreateApplicationHandler) r.Patch("/me", app.updateApplicationHandler) r.Post("/me/submit", app.submitApplicationHandler) - r.Get("/enabled", app.getApplicationsEnabled) r.Post("/me/resume-upload-url", app.generateResumeUploadURLHandler) r.Delete("/me/resume", app.deleteResumeHandler) }) @@ -183,6 +182,7 @@ func (app *application) mount() http.Handler { r.Route("/applications", func(r chi.Router) { r.Get("/", app.listApplicationsHandler) r.Get("/stats", app.getApplicationStatsHandler) + r.Get("/enabled", app.getApplicationsEnabled) r.Get("/{applicationID}", app.getApplication) r.Get("/{applicationID}/resume-url", app.getResumeDownloadURLHandler) diff --git a/cmd/api/applications.go b/cmd/api/applications.go index 37035eab..0ec53f74 100644 --- a/cmd/api/applications.go +++ b/cmd/api/applications.go @@ -416,22 +416,22 @@ func (app *application) getApplicationStatsHandler(w http.ResponseWriter, r *htt // listApplicationsHandler lists all applications with cursor-based pagination // -// @Summary List applications (Admin) -// @Description Lists all applications with cursor-based pagination and optional status filter -// @Tags admin/applications -// @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} -// @Failure 403 {object} object{error=string} -// @Failure 500 {object} object{error=string} -// @Security CookieAuth -// @Router /admin/applications [get] +// @Summary List applications (Admin) +// @Description Lists all applications with cursor-based pagination and optional status filter +// @Tags admin/applications +// @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} +// @Failure 403 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /admin/applications [get] func (app *application) listApplicationsHandler(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() @@ -542,27 +542,23 @@ type ApplicationsEnabledResponse struct { Enabled bool `json:"enabled"` } -type SetApplicationsEnabledResponse struct { - Enabled bool `json:"enabled"` //no validate required because bool enforces the only two possible values, which is what we want -} - // setApplicationStatus sets the final status on an application // -// @Summary Set application status (Super Admin) -// @Description Sets the final status (accepted, rejected, or waitlisted) on an application -// @Tags superadmin -// @Accept json -// @Produce json -// @Param applicationID path string true "Application ID" -// @Param status body SetStatusPayload true "New status" -// @Success 200 {object} ApplicationResponse -// @Failure 400 {object} object{error=string} -// @Failure 401 {object} object{error=string} -// @Failure 403 {object} object{error=string} -// @Failure 404 {object} object{error=string} -// @Failure 500 {object} object{error=string} -// @Security CookieAuth -// @Router /superadmin/applications/{applicationID}/status [patch] +// @Summary Set application status (Super Admin) +// @Description Sets the final status (accepted, rejected, or waitlisted) on an application +// @Tags superadmin +// @Accept json +// @Produce json +// @Param applicationID path string true "Application ID" +// @Param status body SetStatusPayload true "New status" +// @Success 200 {object} ApplicationResponse +// @Failure 400 {object} object{error=string} +// @Failure 401 {object} object{error=string} +// @Failure 403 {object} object{error=string} +// @Failure 404 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /superadmin/applications/{applicationID}/status [patch] func (app *application) setApplicationStatus(w http.ResponseWriter, r *http.Request) { applicationID := chi.URLParam(r, "applicationID") if applicationID == "" { @@ -598,18 +594,18 @@ func (app *application) setApplicationStatus(w http.ResponseWriter, r *http.Requ // getApplication returns a single application by ID with embedded questions // -// @Summary Get application by ID (Admin) -// @Description Returns a single application by its ID with embedded short answer questions -// @Tags admin -// @Produce json -// @Param applicationID path string true "Application ID" -// @Success 200 {object} ApplicationWithQuestions -// @Failure 400 {object} object{error=string} -// @Failure 401 {object} object{error=string} -// @Failure 403 {object} object{error=string} -// @Failure 404 {object} object{error=string} -// @Security CookieAuth -// @Router /admin/applications/{applicationID} [get] +// @Summary Get application by ID (Admin) +// @Description Returns a single application by its ID with embedded short answer questions +// @Tags admin +// @Produce json +// @Param applicationID path string true "Application ID" +// @Success 200 {object} ApplicationWithQuestions +// @Failure 400 {object} object{error=string} +// @Failure 401 {object} object{error=string} +// @Failure 403 {object} object{error=string} +// @Failure 404 {object} object{error=string} +// @Security CookieAuth +// @Router /admin/applications/{applicationID} [get] func (app *application) getApplication(w http.ResponseWriter, r *http.Request) { applicationID := chi.URLParam(r, "applicationID") if applicationID == "" { @@ -647,18 +643,18 @@ func (app *application) getApplication(w http.ResponseWriter, r *http.Request) { // getApplicantEmailsByStatusHandler returns applicant emails filtered by status // -// @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 -// @Produce json -// @Param status query string true "Application status (accepted, rejected, or waitlisted)" -// @Success 200 {object} EmailListResponse -// @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/applications/emails [get] +// @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 +// @Produce json +// @Param status query string true "Application status (accepted, rejected, or waitlisted)" +// @Success 200 {object} EmailListResponse +// @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/applications/emails [get] func (app *application) getApplicantEmailsByStatusHandler(w http.ResponseWriter, r *http.Request) { statusStr := r.URL.Query().Get("status") if statusStr == "" { @@ -699,67 +695,3 @@ func (app *application) getApplicantEmailsByStatusHandler(w http.ResponseWriter, } } - -// getApplicationsEnabled returns whether applications are currently open -// -// @Summary Get applications enabled status -// @Description Returns whether the application portal is currently open for submissions -// @Tags applications -// @Produce json -// @Success 200 {object} ApplicationsEnabledResponse -// @Failure 401 {object} object{error=string} -// @Failure 500 {object} object{error=string} -// @Security CookieAuth -// @Router /applications/enabled [get] -func (app *application) getApplicationsEnabled(w http.ResponseWriter, r *http.Request) { - enabled, err := app.store.Application.GetApplicationsEnabled(r.Context()) - if err != nil { - app.internalServerError(w, r, err) - return - } - - response := ApplicationsEnabledResponse{ - Enabled: enabled, - } - - if err = app.jsonResponse(w, http.StatusOK, response); err != nil { - app.internalServerError(w, r, err) - } -} - -// setApplicationsEnabled updates whether applications are currently open -// -// @Summary Set applications enabled status -// @Description Sets whether the application portal is currently open for submissions. Requires SuperAdmin privileges. -// @Tags superadmin -// @Produce json -// @Param enabled query bool true "Enable or disable applications" -// @Success 200 {object} SetApplicationsEnabledResponse -// @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/settings/applications-enabled [put] -func (app *application) setApplicationsEnabled(w http.ResponseWriter, r *http.Request) { - enabled, err := strconv.ParseBool(r.URL.Query().Get("enabled")) - - if err != nil { - app.badRequestResponse(w, r, errors.New("enabled must be a boolean value")) - return - } - - enabled, err = app.store.Application.SetApplicationsEnabled(r.Context(), enabled) - if err != nil { - app.internalServerError(w, r, err) - } - - //NOTE: Following existing design pattern of Get response and Set response structs - response := SetApplicationsEnabledResponse{ - Enabled: enabled, - } - - if err = app.jsonResponse(w, http.StatusOK, response); err != nil { - app.internalServerError(w, r, err) - } -} diff --git a/cmd/api/resume.go b/cmd/api/resume.go index 78aa13a4..445e9fb5 100644 --- a/cmd/api/resume.go +++ b/cmd/api/resume.go @@ -24,18 +24,18 @@ type ResumeDownloadURLResponse struct { // generateResumeUploadURLHandler returns a signed upload URL for hacker resume uploads. // -// @Summary Generate resume upload URL -// @Description Generates a signed GCS upload URL for the authenticated user's resume. Application must be in draft status. -// @Tags hackers -// @Produce json -// @Success 200 {object} ResumeUploadURLResponse -// @Failure 401 {object} object{error=string} -// @Failure 404 {object} object{error=string} -// @Failure 409 {object} object{error=string} -// @Failure 500 {object} object{error=string} -// @Failure 503 {object} object{error=string} -// @Security CookieAuth -// @Router /applications/me/resume-upload-url [post] +// @Summary Generate resume upload URL +// @Description Generates a signed GCS upload URL for the authenticated user's resume. Application must be in draft status. +// @Tags hackers +// @Produce json +// @Success 200 {object} ResumeUploadURLResponse +// @Failure 401 {object} object{error=string} +// @Failure 404 {object} object{error=string} +// @Failure 409 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Failure 503 {object} object{error=string} +// @Security CookieAuth +// @Router /applications/me/resume-upload-url [post] func (app *application) generateResumeUploadURLHandler(w http.ResponseWriter, r *http.Request) { user := getUserFromContext(r.Context()) if user == nil { @@ -88,17 +88,17 @@ func (app *application) generateResumeUploadURLHandler(w http.ResponseWriter, r // deleteResumeHandler removes the resume path from the draft application and best-effort deletes from GCS. // -// @Summary Delete resume -// @Description Deletes the resume reference from the authenticated user's draft application and best-effort deletes the object from GCS. -// @Tags hackers -// @Produce json -// @Success 200 {object} store.Application -// @Failure 401 {object} object{error=string} -// @Failure 404 {object} object{error=string} -// @Failure 409 {object} object{error=string} -// @Failure 500 {object} object{error=string} -// @Security CookieAuth -// @Router /applications/me/resume [delete] +// @Summary Delete resume +// @Description Deletes the resume reference from the authenticated user's draft application and best-effort deletes the object from GCS. +// @Tags hackers +// @Produce json +// @Success 200 {object} store.Application +// @Failure 401 {object} object{error=string} +// @Failure 404 {object} object{error=string} +// @Failure 409 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /applications/me/resume [delete] func (app *application) deleteResumeHandler(w http.ResponseWriter, r *http.Request) { user := getUserFromContext(r.Context()) if user == nil { @@ -147,20 +147,20 @@ func (app *application) deleteResumeHandler(w http.ResponseWriter, r *http.Reque // getResumeDownloadURLHandler returns a signed download URL for admin viewing. // -// @Summary Get resume download URL (Admin) -// @Description Generates a signed GCS download URL for an application's resume. -// @Tags admin/applications -// @Produce json -// @Param applicationID path string true "Application ID" -// @Success 200 {object} ResumeDownloadURLResponse -// @Failure 400 {object} object{error=string} -// @Failure 401 {object} object{error=string} -// @Failure 403 {object} object{error=string} -// @Failure 404 {object} object{error=string} -// @Failure 500 {object} object{error=string} -// @Failure 503 {object} object{error=string} -// @Security CookieAuth -// @Router /admin/applications/{applicationID}/resume-url [get] +// @Summary Get resume download URL (Admin) +// @Description Generates a signed GCS download URL for an application's resume. +// @Tags admin/applications +// @Produce json +// @Param applicationID path string true "Application ID" +// @Success 200 {object} ResumeDownloadURLResponse +// @Failure 400 {object} object{error=string} +// @Failure 401 {object} object{error=string} +// @Failure 403 {object} object{error=string} +// @Failure 404 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Failure 503 {object} object{error=string} +// @Security CookieAuth +// @Router /admin/applications/{applicationID}/resume-url [get] func (app *application) getResumeDownloadURLHandler(w http.ResponseWriter, r *http.Request) { applicationID := chi.URLParam(r, "applicationID") if applicationID == "" { diff --git a/cmd/api/settings.go b/cmd/api/settings.go index 47008649..ed8d7448 100644 --- a/cmd/api/settings.go +++ b/cmd/api/settings.go @@ -3,6 +3,7 @@ package main import ( "errors" "net/http" + "strconv" "time" "github.com/hackutd/portal/internal/store" @@ -432,3 +433,70 @@ func (app *application) setHackathonDateRange(w http.ResponseWriter, r *http.Req app.internalServerError(w, r, err) } } + +// getApplicationsEnabled returns whether applications are currently open +// +// @Summary Get applications enabled status +// @Description Returns whether the application portal is currently open for submissions +// @Tags applications +// @Produce json +// @Success 200 {object} ApplicationsEnabledResponse +// @Failure 401 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /applications/enabled [get] +func (app *application) getApplicationsEnabled(w http.ResponseWriter, r *http.Request) { + enabled, err := app.store.Application.GetApplicationsEnabled(r.Context()) + if err != nil { + app.internalServerError(w, r, err) + return + } + + response := ApplicationsEnabledResponse{ + Enabled: enabled, + } + + if err = app.jsonResponse(w, http.StatusOK, response); err != nil { + app.internalServerError(w, r, err) + return + } +} + +// setApplicationsEnabled updates whether applications are currently open +// +// @Summary Set applications enabled status +// @Description Sets whether the application portal is currently open for submissions. Requires SuperAdmin privileges. +// @Tags superadmin +// @Produce json +// @Param enabled query bool true "Enable or disable applications" +// @Success 200 {object} ApplicationsEnabledResponse +// @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/settings/applications-enabled [put] +func (app *application) setApplicationsEnabled(w http.ResponseWriter, r *http.Request) { + enabled, err := strconv.ParseBool(r.URL.Query().Get("enabled")) + + if err != nil { + app.badRequestResponse(w, r, errors.New("enabled must be a boolean value")) + return + } + + enabled, err = app.store.Application.SetApplicationsEnabled(r.Context(), enabled) + if err != nil { + app.internalServerError(w, r, err) + return + } + + //NOTE: Following existing design pattern of Get response and Set response structs + response := ApplicationsEnabledResponse{ + Enabled: enabled, + } + + if err = app.jsonResponse(w, http.StatusOK, response); err != nil { + app.internalServerError(w, r, err) + return + } +} diff --git a/cmd/api/superadmin_users.go b/cmd/api/superadmin_users.go index 995ce281..f0ec88b5 100644 --- a/cmd/api/superadmin_users.go +++ b/cmd/api/superadmin_users.go @@ -24,20 +24,20 @@ type UpdateRoleResponse struct { // searchUsersHandler searches users by email or name // -// @Summary Search users (Super Admin) -// @Description Searches users by email, first name, or last name using trigram matching -// @Tags superadmin/users -// @Produce json -// @Param search query string true "Search query (min 2 chars)" -// @Param limit query int false "Page size (default 20, max 100)" -// @Param offset query int false "Offset (default 0)" -// @Success 200 {object} UserSearchResponse -// @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 [get] +// @Summary Search users (Super Admin) +// @Description Searches users by email, first name, or last name using trigram matching +// @Tags superadmin/users +// @Produce json +// @Param search query string true "Search query (min 2 chars)" +// @Param limit query int false "Page size (default 20, max 100)" +// @Param offset query int false "Offset (default 0)" +// @Success 200 {object} UserSearchResponse +// @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 [get] func (app *application) searchUsersHandler(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() @@ -88,21 +88,21 @@ func (app *application) searchUsersHandler(w http.ResponseWriter, r *http.Reques // updateUserRoleHandler updates a user's role // -// @Summary Update user role (Super Admin) -// @Description Updates the role of a user by their ID -// @Tags superadmin/users -// @Accept json -// @Produce json -// @Param userID path string true "User ID" -// @Param role body UpdateRolePayload true "New role" -// @Success 200 {object} UserResponse -// @Failure 400 {object} object{error=string} -// @Failure 401 {object} object{error=string} -// @Failure 403 {object} object{error=string} -// @Failure 404 {object} object{error=string} -// @Failure 500 {object} object{error=string} -// @Security CookieAuth -// @Router /superadmin/users/{userID}/role [patch] +// @Summary Update user role (Super Admin) +// @Description Updates the role of a user by their ID +// @Tags superadmin/users +// @Accept json +// @Produce json +// @Param userID path string true "User ID" +// @Param role body UpdateRolePayload true "New role" +// @Success 200 {object} UserResponse +// @Failure 400 {object} object{error=string} +// @Failure 401 {object} object{error=string} +// @Failure 403 {object} object{error=string} +// @Failure 404 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /superadmin/users/{userID}/role [patch] func (app *application) updateUserRoleHandler(w http.ResponseWriter, r *http.Request) { userID := chi.URLParam(r, "userID") if userID == "" { diff --git a/cmd/migrate/migrations/000017_insert_review_assignment_setting.down.sql b/cmd/migrate/migrations/000023_insert_review_assignment_setting.down.sql similarity index 100% rename from cmd/migrate/migrations/000017_insert_review_assignment_setting.down.sql rename to cmd/migrate/migrations/000023_insert_review_assignment_setting.down.sql diff --git a/cmd/migrate/migrations/000017_insert_review_assignment_setting.up.sql b/cmd/migrate/migrations/000023_insert_review_assignment_setting.up.sql similarity index 100% rename from cmd/migrate/migrations/000017_insert_review_assignment_setting.up.sql rename to cmd/migrate/migrations/000023_insert_review_assignment_setting.up.sql diff --git a/docs/docs.go b/docs/docs.go index abebf0e2..608da084 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -195,7 +195,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "admin/applications" + "admin" ], "summary": "Get application by ID (Admin)", "parameters": [ @@ -1482,6 +1482,53 @@ const docTemplate = `{ } } }, + "/applications/enabled": { + "get": { + "security": [ + { + "CookieAuth": [] + } + ], + "description": "Returns whether the application portal is currently open for submissions", + "produces": [ + "application/json" + ], + "tags": [ + "applications" + ], + "summary": "Get applications enabled status", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.ApplicationsEnabledResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + }, "/applications/me": { "get": { "security": [ @@ -2060,7 +2107,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "superadmin/applications" + "superadmin" ], "summary": "Get applicant emails by status (Super Admin)", "parameters": [ @@ -2141,7 +2188,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "superadmin/applications" + "superadmin" ], "summary": "Set application status (Super Admin)", "parameters": [ @@ -2290,19 +2337,44 @@ const docTemplate = `{ "CookieAuth": [] } ], - "description": "Generates and sends personalized QR code emails to all accepted hackers. QR encodes the user UUID for check-in scanning.", + "description": "Updates whether users with admin role can create, update, and delete schedule items", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ "superadmin/settings" ], - "summary": "Send QR code emails (Super Admin)", + "summary": "Set admin schedule edit state (Super Admin)", + "parameters": [ + { + "description": "Admin schedule editing enabled state", + "name": "enabled", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.SetAdminScheduleEditTogglePayload" + } + } + ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/main.SendQREmailsResponse" + "$ref": "#/definitions/main.AdminScheduleEditToggleResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } }, "401": { @@ -2369,7 +2441,146 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/main.SetApplicationsEnabledResponse" + "$ref": "#/definitions/main.ApplicationsEnabledResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + }, + "/superadmin/settings/hackathon-date-range": { + "get": { + "security": [ + { + "CookieAuth": [] + } + ], + "description": "Returns configured hackathon start and end dates", + "produces": [ + "application/json" + ], + "tags": [ + "superadmin/settings" + ], + "summary": "Get hackathon date range (Super Admin)", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.HackathonDateRangeResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + }, + "post": { + "security": [ + { + "CookieAuth": [] + } + ], + "description": "Updates configured hackathon start and end dates. Range must be at most 7 days inclusive.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "superadmin/settings" + ], + "summary": "Set hackathon date range (Super Admin)", + "parameters": [ + { + "description": "Hackathon date range", + "name": "range", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.SetHackathonDateRangePayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.HackathonDateRangeResponse" } }, "400": { @@ -3514,7 +3725,7 @@ const docTemplate = `{ "schedule": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/store.ScheduleItem" } } }