diff --git a/cmd/api/api.go b/cmd/api/api.go index a11cd78a..bc90a003 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -169,6 +169,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.Post("/me/resume-upload-url", app.generateResumeUploadURLHandler) r.Delete("/me/resume", app.deleteResumeHandler) }) @@ -239,6 +240,7 @@ func (app *application) mount() http.Handler { r.Get("/hackathon-date-range", app.getHackathonDateRange) r.Post("/hackathon-date-range", app.setHackathonDateRange) r.Put("/scan-types", app.updateScanTypesHandler) + r.Put("/applications-enabled", app.setApplicationsEnabled) }) r.Route("/applications", func(r chi.Router) { diff --git a/cmd/api/applications.go b/cmd/api/applications.go index 7418a813..37035eab 100644 --- a/cmd/api/applications.go +++ b/cmd/api/applications.go @@ -56,16 +56,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 hackers -// @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 hackers +// @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 { @@ -117,19 +117,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 hackers -// @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 hackers +// @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 { @@ -255,17 +255,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 hackers -// @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 hackers +// @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 { @@ -392,16 +392,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/applications -// @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/applications +// @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 { @@ -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() @@ -538,11 +538,19 @@ type EmailListResponse struct { Count int `json:"count"` } +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/applications +// @Tags superadmin // @Accept json // @Produce json // @Param applicationID path string true "Application ID" @@ -592,7 +600,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 @@ -641,7 +649,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 @@ -691,3 +699,67 @@ 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/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/scans.go b/cmd/api/scans.go index a5074423..62064c20 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/scans -// @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/scans +// @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 { @@ -155,18 +155,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/scans -// @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/scans +// @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 == "" { @@ -187,16 +187,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/scans -// @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/scans +// @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 { @@ -211,19 +211,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/settings -// @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/settings +// @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 e7a08e0c..47008649 100644 --- a/cmd/api/settings.go +++ b/cmd/api/settings.go @@ -18,16 +18,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/settings -// @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/settings +// @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 { @@ -46,19 +46,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/settings -// @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/settings +// @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 { @@ -103,16 +103,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/settings -// @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/settings +// @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 { @@ -131,19 +131,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/settings -// @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/settings +// @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/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 d4f59ad8..abebf0e2 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2290,44 +2290,19 @@ const docTemplate = `{ "CookieAuth": [] } ], - "description": "Updates whether users with admin role can create, update, and delete schedule items", - "consumes": [ - "application/json" - ], + "description": "Generates and sends personalized QR code emails to all accepted hackers. QR encodes the user UUID for check-in scanning.", "produces": [ "application/json" ], "tags": [ "superadmin/settings" ], - "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" - } - } - ], + "summary": "Send QR code emails (Super Admin)", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/main.AdminScheduleEditToggleResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } + "$ref": "#/definitions/main.SendQREmailsResponse" } }, "401": { @@ -2366,96 +2341,35 @@ const docTemplate = `{ } } }, - "/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": { + "/superadmin/settings/applications-enabled": { + "put": { "security": [ { "CookieAuth": [] } ], - "description": "Updates configured hackathon start and end dates. Range must be at most 7 days inclusive.", - "consumes": [ - "application/json" - ], + "description": "Sets whether the application portal is currently open for submissions. Requires SuperAdmin privileges.", "produces": [ "application/json" ], "tags": [ - "superadmin/settings" + "superadmin" ], - "summary": "Set hackathon date range (Super Admin)", + "summary": "Set applications enabled status", "parameters": [ { - "description": "Hackathon date range", - "name": "range", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/main.SetHackathonDateRangePayload" - } + "type": "boolean", + "description": "Enable or disable applications", + "name": "enabled", + "in": "query", + "required": true } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/main.HackathonDateRangeResponse" + "$ref": "#/definitions/main.SetApplicationsEnabledResponse" } }, "400": { @@ -3382,6 +3296,14 @@ const docTemplate = `{ } } }, + "main.ApplicationsEnabledResponse": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, "main.CheckEmailResponse": { "type": "object", "properties": { @@ -3592,7 +3514,7 @@ const docTemplate = `{ "schedule": { "type": "array", "items": { - "$ref": "#/definitions/store.ScheduleItem" + "type": "string" } } } diff --git a/internal/store/applications.go b/internal/store/applications.go index f310af7c..544258fe 100644 --- a/internal/store/applications.go +++ b/internal/store/applications.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "strconv" "strings" "time" ) @@ -806,3 +807,39 @@ 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 +} + +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 d7e4f8d7..83c1e930 100644 --- a/internal/store/mock_store.go +++ b/internal/store/mock_store.go @@ -129,6 +129,16 @@ 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) +} + +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 302d1acf..d8b7a857 100644 --- a/internal/store/storage.go +++ b/internal/store/storage.go @@ -35,6 +35,8 @@ 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) + SetApplicationsEnabled(ctx context.Context, enabled bool) (bool, error) } Settings interface { GetShortAnswerQuestions(ctx context.Context) ([]ShortAnswerQuestion, error)