From 76864e43ac67d326a94f12dc2cdc6a1d0f42093a Mon Sep 17 00:00:00 2001 From: Noel Varghese <91435868+NoelVarghese2006@users.noreply.github.com> Date: Sun, 15 Mar 2026 23:35:27 -0500 Subject: [PATCH 1/8] feat(superadmin): implement hackathon data reset functionality - Add `POST /v1/superadmin/reset-hackathon` endpoint - Create `HackathonStore` for transactional data truncation of applications, scans, and schedule - Implement background GCS file deletion for resume cleanup when applications are reset - Add "Danger Zone" tab to Super Admin settings dialog - Create `ResetHackathonCard` component with granular reset options and confirmation guard --- .../settings/components/SettingsDialog.tsx | 7 +- .../settings/tabs/ResetHackathonCard.tsx | 199 ++++++++++++++++++ cmd/api/api.go | 1 + cmd/api/settings.go | 67 ++++++ docs/docs.go | 101 +++++++++ internal/store/hackathon.go | 77 +++++++ internal/store/mock_store.go | 14 ++ 7 files changed, 464 insertions(+), 2 deletions(-) create mode 100644 client/web/src/pages/superadmin/settings/tabs/ResetHackathonCard.tsx create mode 100644 internal/store/hackathon.go diff --git a/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx b/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx index 590fe7d4..0c38f063 100644 --- a/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx +++ b/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { CalendarRange, UserCog, UsersRound } from "lucide-react"; +import { AlertTriangle, CalendarRange, UserCog, UsersRound } from "lucide-react"; import * as React from "react"; import { Button } from "@/components/ui/button"; @@ -17,15 +17,17 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { cn } from "@/shared/lib/utils"; import ApplicationsTab from "../tabs/ApplicationsTab"; +import { ResetHackathonCard } from "../tabs/ResetHackathonCard"; import ScheduleTab from "../tabs/ScheduleTab"; import { SetAdminTab } from "../tabs/SetAdminTab"; -type SettingsTab = "set-admin" | "applications" | "schedule"; +type SettingsTab = "set-admin" | "applications" | "schedule" | "reset"; const settingsTabs = [ { id: "set-admin" as const, label: "Set Admin", icon: UserCog }, { id: "applications" as const, label: "Applications", icon: UsersRound }, { id: "schedule" as const, label: "Schedule", icon: CalendarRange }, + { id: "reset" as const, label: "Danger Zone", icon: AlertTriangle }, ]; interface SettingsDialogProps { @@ -82,6 +84,7 @@ export function SettingsDialog({ trigger }: SettingsDialogProps) { {activeTab === "set-admin" && } {activeTab === "applications" && } {activeTab === "schedule" && } + {activeTab === "reset" && } diff --git a/client/web/src/pages/superadmin/settings/tabs/ResetHackathonCard.tsx b/client/web/src/pages/superadmin/settings/tabs/ResetHackathonCard.tsx new file mode 100644 index 00000000..fdf3ee7e --- /dev/null +++ b/client/web/src/pages/superadmin/settings/tabs/ResetHackathonCard.tsx @@ -0,0 +1,199 @@ +import { AlertTriangle, Loader2, Trash2 } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { postRequest } from "@/shared/lib/api"; + +export function ResetHackathonCard() { + const [open, setOpen] = useState(false); + const [loading, setLoading] = useState(false); + const [confirmText, setConfirmText] = useState(""); + const [options, setOptions] = useState({ + reset_applications: false, + reset_scans: false, + reset_schedule: false, + reset_settings: false, + }); + + const handleReset = async () => { + if (confirmText !== "RESET HACKATHON") return; + + // Ensure at least one option is selected + if (!Object.values(options).some(Boolean)) { + toast.error("Please select at least one item to reset"); + return; + } + + setLoading(true); + try { + const res = await postRequest<{ success: boolean }>( + "/superadmin/reset-hackathon", + options, + ); + + if (res.error) { + toast.error(res.error); + return; + } + + toast.success("Hackathon data reset successfully"); + setOpen(false); + setConfirmText(""); + setOptions({ + reset_applications: false, + reset_scans: false, + reset_schedule: false, + reset_settings: false, + }); + } catch (err) { + toast.error("An unexpected error occurred"); + } finally { + setLoading(false); + } + }; + + return ( + + + + + Danger Zone + + + Irreversible actions that destroy data. Proceed with caution. + + + + + + + + + + + + Reset Hackathon Data + + + This action cannot be undone. This will permanently delete the + selected data from the database and remove associated files. + + + +
+
+ {[ + { + id: "reset_applications", + label: "Applications", + desc: "Deletes all hacker applications, reviews, and resume files.", + }, + { + id: "reset_scans", + label: "Scans", + desc: "Deletes all check-in, meal, and event scan records.", + }, + { + id: "reset_schedule", + label: "Schedule", + desc: "Deletes all schedule events.", + }, + { + id: "reset_settings", + label: "Settings Stats", + desc: "Resets scan stats and review assignment toggles.", + }, + ].map((item) => ( +
+ + setOptions((prev) => ({ + ...prev, + [item.id]: !!c, + })) + } + className="border-zinc-600 data-[state=checked]:bg-red-600 data-[state=checked]:border-red-600" + /> +
+ +

+ {item.desc} +

+
+
+ ))} +
+ +
+ + setConfirmText(e.target.value)} + placeholder="RESET HACKATHON" + className="bg-zinc-950 border-zinc-800 text-zinc-100 placeholder:text-zinc-600 focus-visible:ring-red-500/20 focus-visible:border-red-500" + /> +
+
+ + + + + +
+
+
+
+ ); +} diff --git a/cmd/api/api.go b/cmd/api/api.go index a11cd78a..2538337a 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -225,6 +225,7 @@ func (app *application) mount() http.Handler { r.Use(app.RequireRoleMiddleware(store.RoleSuperAdmin)) // Super admin routes r.Route("/superadmin", func(r chi.Router) { + r.Post("/reset-hackathon", app.resetHackathonHandler) // Configs r.Route("/settings", func(r chi.Router) { diff --git a/cmd/api/settings.go b/cmd/api/settings.go index 47008649..dfe9dc6c 100644 --- a/cmd/api/settings.go +++ b/cmd/api/settings.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "net/http" "time" @@ -44,6 +45,72 @@ func (app *application) getShortAnswerQuestions(w http.ResponseWriter, r *http.R } } +type ResetHackathonPayload struct { + ResetApplications bool `json:"reset_applications"` + ResetScans bool `json:"reset_scans"` + ResetSchedule bool `json:"reset_schedule"` + ResetSettings bool `json:"reset_settings"` +} + +// resetHackathonHandler resets hackathon data based on options +// +// @Summary Reset hackathon data (Super Admin) +// @Description Resets selected hackathon data (applications, scans, schedule, settings). Operations are performed in a single transaction. +// @Tags superadmin +// @Accept json +// @Produce json +// @Param options body ResetHackathonPayload true "Reset options" +// @Success 200 {object} map[string]any +// @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/reset-hackathon [post] +func (app *application) resetHackathonHandler(w http.ResponseWriter, r *http.Request) { + var req ResetHackathonPayload + if err := readJSON(w, r, &req); err != nil { + app.badRequestResponse(w, r, err) + return + } + + user := getUserFromContext(r.Context()) + if user == nil { + app.internalServerError(w, r, errors.New("user not in context")) + return + } + + resumePaths, err := app.store.Hackathon.Reset(r.Context(), req.ResetApplications, req.ResetScans, req.ResetSchedule, req.ResetSettings) + if err != nil { + app.internalServerError(w, r, err) + return + } + + // Best-effort cleanup of resumes from GCS + if len(resumePaths) > 0 && app.gcsClient != nil { + go func(paths []string) { + for _, path := range paths { + _ = app.gcsClient.DeleteObject(context.Background(), path) + } + }(resumePaths) + } + + app.logger.Infow("hackathon data reset", "user_id", user.ID, "user_email", user.Email, "reset_apps", req.ResetApplications, "reset_scans", req.ResetScans, "reset_schedule", req.ResetSchedule, "reset_settings", req.ResetSettings, "resumes_deleted_count", len(resumePaths)) + + response := map[string]any{ + "success": true, + "reset_applications": req.ResetApplications, + "reset_scans": req.ResetScans, + "reset_schedule": req.ResetSchedule, + "reset_settings": req.ResetSettings, + "resumes_deleted": len(resumePaths), + } + + if err := app.jsonResponse(w, http.StatusOK, response); err != nil { + app.internalServerError(w, r, err) + } +} + // updateShortAnswerQuestions replaces all short answer questions // // @Summary Update short answer questions (Super Admin) diff --git a/docs/docs.go b/docs/docs.go index d4f59ad8..462aadba 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2227,6 +2227,90 @@ const docTemplate = `{ } } }, + "/superadmin/reset-hackathon": { + "post": { + "security": [ + { + "CookieAuth": [] + } + ], + "description": "Resets selected hackathon data (applications, scans, schedule, settings). Operations are performed in a single transaction.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "superadmin" + ], + "summary": "Reset hackathon data (Super Admin)", + "parameters": [ + { + "description": "Reset options", + "name": "options", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.ResetHackathonPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "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/admin-schedule-edit-toggle": { "get": { "security": [ @@ -3502,6 +3586,23 @@ const docTemplate = `{ } } }, + "main.ResetHackathonPayload": { + "type": "object", + "properties": { + "reset_applications": { + "type": "boolean" + }, + "reset_scans": { + "type": "boolean" + }, + "reset_schedule": { + "type": "boolean" + }, + "reset_settings": { + "type": "boolean" + } + } + }, "main.ResumeDownloadURLResponse": { "type": "object", "properties": { diff --git a/internal/store/hackathon.go b/internal/store/hackathon.go new file mode 100644 index 00000000..72a5754e --- /dev/null +++ b/internal/store/hackathon.go @@ -0,0 +1,77 @@ +package store + +import ( + "context" + "database/sql" +) + +type HackathonStore struct { + db *sql.DB +} + +// Reset resets the selected domains of hackathon data in a single transaction. +// Returns a list of resume paths that should be deleted from storage if applications were reset. +func (s *HackathonStore) Reset(ctx context.Context, resetApplications, resetScans, resetSchedule, resetSettings bool) ([]string, error) { + ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration*2) // Longer timeout for bulk operations + defer cancel() + + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + + var resumePaths []string + + if resetApplications { + // Collect resume paths before truncation + rows, err := tx.QueryContext(ctx, "SELECT resume_path FROM applications WHERE resume_path IS NOT NULL") + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var path string + if err := rows.Scan(&path); err != nil { + return nil, err + } + resumePaths = append(resumePaths, path) + } + if err := rows.Err(); err != nil { + return nil, err + } + rows.Close() + + if _, err := tx.ExecContext(ctx, "TRUNCATE TABLE applications CASCADE"); err != nil { + return nil, err + } + } + + if resetScans { + if _, err := tx.ExecContext(ctx, "TRUNCATE TABLE scans"); err != nil { + return nil, err + } + } + + if resetSchedule { + if _, err := tx.ExecContext(ctx, "TRUNCATE TABLE schedule"); err != nil { + return nil, err + } + } + + if resetSettings { + if _, err := tx.ExecContext(ctx, "UPDATE settings SET value = '{}', updated_at = NOW() WHERE key = $1", SettingsKeyScanStats); err != nil { + return nil, err + } + if _, err := tx.ExecContext(ctx, "UPDATE settings SET value = '[]', updated_at = NOW() WHERE key = $1", SettingsKeyReviewAssignmentToggle); err != nil { + return nil, err + } + } + + if err := tx.Commit(); err != nil { + return nil, err + } + + return resumePaths, nil +} diff --git a/internal/store/mock_store.go b/internal/store/mock_store.go index d7e4f8d7..ea5916db 100644 --- a/internal/store/mock_store.go +++ b/internal/store/mock_store.go @@ -211,6 +211,19 @@ func (m *MockSettingsStore) GetScanStats(ctx context.Context) (map[string]int, e return args.Get(0).(map[string]int), args.Error(1) } +// MockHackathonStore is a mock implementation of the Hackathon interface +type MockHackathonStore struct { + mock.Mock +} + +func (m *MockHackathonStore) Reset(ctx context.Context, resetApplications, resetScans, resetSchedule, resetSettings bool) ([]string, error) { + args := m.Called(resetApplications, resetScans, resetSchedule, resetSettings) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]string), args.Error(1) +} + // MockApplicationReviewsStore is a mock implementation of the ApplicationReviews interface type MockApplicationReviewsStore struct { mock.Mock @@ -334,6 +347,7 @@ func NewMockStore() Storage { Users: &MockUsersStore{}, Application: &MockApplicationStore{}, Settings: &MockSettingsStore{}, + Hackathon: &MockHackathonStore{}, ApplicationReviews: &MockApplicationReviewsStore{}, Scans: &MockScansStore{}, Schedule: &MockScheduleStore{}, From 1c0ec14e231565060d6c4e82cfb3251e8a2f01da Mon Sep 17 00:00:00 2001 From: Noel Varghese <91435868+NoelVarghese2006@users.noreply.github.com> Date: Wed, 18 Mar 2026 00:21:12 -0500 Subject: [PATCH 2/8] chore(testing) --- cmd/api/reset_hackathon.go | 87 +++++++++++++++++++++++++++++++++ cmd/api/reset_hackathon_test.go | 87 +++++++++++++++++++++++++++++++++ cmd/api/settings.go | 67 ------------------------- cmd/api/settings_test.go | 12 +++++ docs/docs.go | 26 +++++++++- internal/store/storage.go | 4 ++ 6 files changed, 214 insertions(+), 69 deletions(-) create mode 100644 cmd/api/reset_hackathon.go create mode 100644 cmd/api/reset_hackathon_test.go diff --git a/cmd/api/reset_hackathon.go b/cmd/api/reset_hackathon.go new file mode 100644 index 00000000..d82b22fd --- /dev/null +++ b/cmd/api/reset_hackathon.go @@ -0,0 +1,87 @@ +package main + +import ( + "context" + "errors" + "net/http" +) + +type ResetHackathonPayload struct { + ResetApplications bool `json:"reset_applications"` + ResetScans bool `json:"reset_scans"` + ResetSchedule bool `json:"reset_schedule"` + ResetSettings bool `json:"reset_settings"` +} + +type ResetHackathonResponse struct { + Success bool `json:"success"` + ResetApplications bool `json:"reset_applications"` + ResetScans bool `json:"reset_scans"` + ResetSchedule bool `json:"reset_schedule"` + ResetSettings bool `json:"reset_settings"` + ResumesDeleted int `json:"resumes_deleted"` +} + +// resetHackathonHandler resets hackathon data based on options +// +// @Summary Reset hackathon data (Super Admin) +// @Description Resets selected hackathon data (applications, scans, schedule, settings). Operations are performed in a single transaction. +// @Tags superadmin +// @Accept json +// @Produce json +// @Param options body ResetHackathonPayload true "Reset options" +// @Success 200 {object} ResetHackathonResponse +// @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/reset-hackathon [post] +func (app *application) resetHackathonHandler(w http.ResponseWriter, r *http.Request) { + var req ResetHackathonPayload + if err := readJSON(w, r, &req); err != nil { + app.badRequestResponse(w, r, err) + return + } + + if err := Validate.Struct(req); err != nil { + app.badRequestResponse(w, r, err) + return + } + + user := getUserFromContext(r.Context()) + if user == nil { + app.internalServerError(w, r, errors.New("user not in context")) + return + } + + resumePaths, err := app.store.Hackathon.Reset(r.Context(), req.ResetApplications, req.ResetScans, req.ResetSchedule, req.ResetSettings) + if err != nil { + app.internalServerError(w, r, err) + return + } + + // Best-effort cleanup of resumes from GCS + if len(resumePaths) > 0 && app.gcsClient != nil { + go func(paths []string) { + for _, path := range paths { + _ = app.gcsClient.DeleteObject(context.Background(), path) + } + }(resumePaths) + } + + app.logger.Infow("hackathon data reset", "user_id", user.ID, "user_email", user.Email, "reset_apps", req.ResetApplications, "reset_scans", req.ResetScans, "reset_schedule", req.ResetSchedule, "reset_settings", req.ResetSettings, "resumes_deleted_count", len(resumePaths)) + + response := ResetHackathonResponse{ + Success: true, + ResetApplications: req.ResetApplications, + ResetScans: req.ResetScans, + ResetSchedule: req.ResetSchedule, + ResetSettings: req.ResetSettings, + ResumesDeleted: len(resumePaths), + } + + if err := app.jsonResponse(w, http.StatusOK, response); err != nil { + app.internalServerError(w, r, err) + } +} diff --git a/cmd/api/reset_hackathon_test.go b/cmd/api/reset_hackathon_test.go new file mode 100644 index 00000000..f25dde7a --- /dev/null +++ b/cmd/api/reset_hackathon_test.go @@ -0,0 +1,87 @@ +package main + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "testing" + + "github.com/hackutd/portal/internal/store" + "github.com/stretchr/testify/assert" + +) + +func TestResetHackathon(t *testing.T) { + t.Run("should allow super admin to reset data", func(t *testing.T) { + app := newTestApplication(t) + app.gcsClient = nil // Ensure GCS client is nil to skip file deletion logic + + payload := ResetHackathonPayload{ + ResetApplications: true, + ResetScans: true, + ResetSchedule: true, + ResetSettings: true, + } + + // Mock successful reset + app.store.Hackathon.(*store.MockHackathonStore). + On("Reset", true, true, true, true). + Return([]string{"resume1.pdf", "resume2.pdf"}, nil) + + reqBody, _ := json.Marshal(payload) + req, _ := http.NewRequest(http.MethodPost, "/v1/superadmin/reset-hackathon", bytes.NewBuffer(reqBody)) + req = setUserContext(req, newSuperAdminUser()) + + rr := executeRequest(req, http.HandlerFunc(app.resetHackathonHandler)) + + assert.Equal(t, http.StatusOK, rr.Code) + + var respBody struct { + Data ResetHackathonResponse `json:"data"` + } + err := json.Unmarshal(rr.Body.Bytes(), &respBody) + assert.NoError(t, err) + assert.True(t, respBody.Data.Success) + assert.Equal(t, 2, respBody.Data.ResumesDeleted) + + app.store.Hackathon.(*store.MockHackathonStore).AssertExpectations(t) + }) + + t.Run("should return 500 when transaction fails", func(t *testing.T) { + app := newTestApplication(t) + app.gcsClient = nil + + payload := ResetHackathonPayload{ + ResetApplications: true, + } + + // Simulate partial failure/rollback by returning error from store + app.store.Hackathon.(*store.MockHackathonStore). + On("Reset",true, false, false, false). + Return([]string(nil), errors.New("db transaction failed")) + + reqBody, _ := json.Marshal(payload) + req, _ := http.NewRequest(http.MethodPost, "/v1/superadmin/reset-hackathon", bytes.NewBuffer(reqBody)) + req = setUserContext(req, newSuperAdminUser()) + + rr := executeRequest(req, http.HandlerFunc(app.resetHackathonHandler)) + + assert.Equal(t, http.StatusInternalServerError, rr.Code) + + app.store.Hackathon.(*store.MockHackathonStore).AssertExpectations(t) + }) + + t.Run("should forbid non-super-admin users", func(t *testing.T) { + app := newTestApplication(t) + payload := ResetHackathonPayload{ResetApplications: true} + reqBody, _ := json.Marshal(payload) + + req, _ := http.NewRequest(http.MethodPost, "/v1/superadmin/reset-hackathon", bytes.NewBuffer(reqBody)) + req = setUserContext(req, newAdminUser()) // Admin is not SuperAdmin + + handler := app.RequireRoleMiddleware(store.RoleSuperAdmin)(http.HandlerFunc(app.resetHackathonHandler)) + rr := executeRequest(req, handler) + assert.Equal(t, http.StatusForbidden, rr.Code) + }) +} diff --git a/cmd/api/settings.go b/cmd/api/settings.go index dfe9dc6c..47008649 100644 --- a/cmd/api/settings.go +++ b/cmd/api/settings.go @@ -1,7 +1,6 @@ package main import ( - "context" "errors" "net/http" "time" @@ -45,72 +44,6 @@ func (app *application) getShortAnswerQuestions(w http.ResponseWriter, r *http.R } } -type ResetHackathonPayload struct { - ResetApplications bool `json:"reset_applications"` - ResetScans bool `json:"reset_scans"` - ResetSchedule bool `json:"reset_schedule"` - ResetSettings bool `json:"reset_settings"` -} - -// resetHackathonHandler resets hackathon data based on options -// -// @Summary Reset hackathon data (Super Admin) -// @Description Resets selected hackathon data (applications, scans, schedule, settings). Operations are performed in a single transaction. -// @Tags superadmin -// @Accept json -// @Produce json -// @Param options body ResetHackathonPayload true "Reset options" -// @Success 200 {object} map[string]any -// @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/reset-hackathon [post] -func (app *application) resetHackathonHandler(w http.ResponseWriter, r *http.Request) { - var req ResetHackathonPayload - if err := readJSON(w, r, &req); err != nil { - app.badRequestResponse(w, r, err) - return - } - - user := getUserFromContext(r.Context()) - if user == nil { - app.internalServerError(w, r, errors.New("user not in context")) - return - } - - resumePaths, err := app.store.Hackathon.Reset(r.Context(), req.ResetApplications, req.ResetScans, req.ResetSchedule, req.ResetSettings) - if err != nil { - app.internalServerError(w, r, err) - return - } - - // Best-effort cleanup of resumes from GCS - if len(resumePaths) > 0 && app.gcsClient != nil { - go func(paths []string) { - for _, path := range paths { - _ = app.gcsClient.DeleteObject(context.Background(), path) - } - }(resumePaths) - } - - app.logger.Infow("hackathon data reset", "user_id", user.ID, "user_email", user.Email, "reset_apps", req.ResetApplications, "reset_scans", req.ResetScans, "reset_schedule", req.ResetSchedule, "reset_settings", req.ResetSettings, "resumes_deleted_count", len(resumePaths)) - - response := map[string]any{ - "success": true, - "reset_applications": req.ResetApplications, - "reset_scans": req.ResetScans, - "reset_schedule": req.ResetSchedule, - "reset_settings": req.ResetSettings, - "resumes_deleted": len(resumePaths), - } - - if err := app.jsonResponse(w, http.StatusOK, response); err != nil { - app.internalServerError(w, r, err) - } -} - // updateShortAnswerQuestions replaces all short answer questions // // @Summary Update short answer questions (Super Admin) diff --git a/cmd/api/settings_test.go b/cmd/api/settings_test.go index 4b044844..5a558358 100644 --- a/cmd/api/settings_test.go +++ b/cmd/api/settings_test.go @@ -62,6 +62,7 @@ func TestUpdateShortAnswerQuestions(t *testing.T) { checkResponseCode(t, http.StatusOK, rr.Code) mockSettings.AssertExpectations(t) + app.store.Hackathon.(*store.MockHackathonStore).AssertExpectations(t) }) t.Run("should return 400 for duplicate question IDs", func(t *testing.T) { @@ -70,6 +71,17 @@ func TestUpdateShortAnswerQuestions(t *testing.T) { require.NoError(t, err) req.Header.Set("Content-Type", "application/json") req = setUserContext(req, newSuperAdminUser()) + }) + + t.Run("should return 500 when transaction fails", func(t *testing.T) { + app := newTestApplication(t) + app.gcsClient = nil + + body := `{"questions":[{"id":"q1","question":"A?","required":true,"display_order":0},{"id":"q1","question":"B?","required":false,"display_order":1}]}` + req, err := http.NewRequest(http.MethodPut, "/", strings.NewReader(body)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req = setUserContext(req, newSuperAdminUser()) rr := executeRequest(req, http.HandlerFunc(app.updateShortAnswerQuestions)) checkResponseCode(t, http.StatusBadRequest, rr.Code) diff --git a/docs/docs.go b/docs/docs.go index 462aadba..be474c92 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2260,8 +2260,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/definitions/main.ResetHackathonResponse" } }, "400": { @@ -3603,6 +3602,29 @@ const docTemplate = `{ } } }, + "main.ResetHackathonResponse": { + "type": "object", + "properties": { + "reset_applications": { + "type": "boolean" + }, + "reset_scans": { + "type": "boolean" + }, + "reset_schedule": { + "type": "boolean" + }, + "reset_settings": { + "type": "boolean" + }, + "resumes_deleted": { + "type": "integer" + }, + "success": { + "type": "boolean" + } + } + }, "main.ResumeDownloadURLResponse": { "type": "object", "properties": { diff --git a/internal/store/storage.go b/internal/store/storage.go index 302d1acf..04af6ad7 100644 --- a/internal/store/storage.go +++ b/internal/store/storage.go @@ -51,6 +51,9 @@ type Storage struct { UpdateScanTypes(ctx context.Context, scanTypes []ScanType) error GetScanStats(ctx context.Context) (map[string]int, error) } + Hackathon interface { + Reset(ctx context.Context, resetApplications, resetScans, resetSchedule, resetSettings bool) ([]string, error) + } Scans interface { Create(ctx context.Context, scan *Scan) error GetByUserID(ctx context.Context, userID string) ([]Scan, error) @@ -79,6 +82,7 @@ func NewStorage(db *sql.DB) Storage { Users: &UsersStore{db: db}, Application: &ApplicationsStore{db: db}, Settings: &SettingsStore{db: db}, + Hackathon: &HackathonStore{db: db}, ApplicationReviews: &ApplicationReviewsStore{db: db}, Scans: &ScansStore{db: db}, Schedule: &ScheduleStore{db: db}, From 551ddaed07908d6051e7b43c48708de54833ec7a Mon Sep 17 00:00:00 2001 From: Noel Varghese <91435868+NoelVarghese2006@users.noreply.github.com> Date: Wed, 18 Mar 2026 00:27:19 -0500 Subject: [PATCH 3/8] chore(formatting) --- .../settings/components/SettingsDialog.tsx | 7 ++++++- .../settings/tabs/ResetHackathonCard.tsx | 7 +++---- client/web/vite.config.ts | 15 ++++++++------- cmd/api/reset_hackathon_test.go | 3 +-- cmd/api/settings_test.go | 2 +- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx b/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx index 0c38f063..210699f4 100644 --- a/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx +++ b/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx @@ -1,6 +1,11 @@ "use client"; -import { AlertTriangle, CalendarRange, UserCog, UsersRound } from "lucide-react"; +import { + AlertTriangle, + CalendarRange, + UserCog, + UsersRound, +} from "lucide-react"; import * as React from "react"; import { Button } from "@/components/ui/button"; diff --git a/client/web/src/pages/superadmin/settings/tabs/ResetHackathonCard.tsx b/client/web/src/pages/superadmin/settings/tabs/ResetHackathonCard.tsx index fdf3ee7e..0692a2ed 100644 --- a/client/web/src/pages/superadmin/settings/tabs/ResetHackathonCard.tsx +++ b/client/web/src/pages/superadmin/settings/tabs/ResetHackathonCard.tsx @@ -146,9 +146,7 @@ export function ResetHackathonCard() { > {item.label} -

- {item.desc} -

+

{item.desc}

))} @@ -156,7 +154,8 @@ export function ResetHackathonCard() {
{ // Frontend routes that React Router should handle - const frontendRoutes = ['/auth/callback', '/auth/verify']; + const frontendRoutes = ["/auth/callback", "/auth/verify"]; // Check if this is a frontend route (without OAuth query params) for (const route of frontendRoutes) { if (req.url?.startsWith(route)) { // If it has OAuth params (code, error), it's a backend callback - proxy it // Exception: /auth/callback/google with params should go to frontend - const url = new URL(req.url, 'http://localhost'); - const hasOAuthParams = url.searchParams.has('code') || url.searchParams.has('error'); + const url = new URL(req.url, "http://localhost"); + const hasOAuthParams = + url.searchParams.has("code") || url.searchParams.has("error"); // /auth/callback/google is always a frontend route (handles OAuth response) - if (req.url.startsWith('/auth/callback/google')) { + if (req.url.startsWith("/auth/callback/google")) { return req.url; } @@ -39,7 +40,7 @@ export default defineConfig({ } }, }, - '/v1': { + "/v1": { target: apiTarget, changeOrigin: true, }, diff --git a/cmd/api/reset_hackathon_test.go b/cmd/api/reset_hackathon_test.go index f25dde7a..3b201880 100644 --- a/cmd/api/reset_hackathon_test.go +++ b/cmd/api/reset_hackathon_test.go @@ -9,7 +9,6 @@ import ( "github.com/hackutd/portal/internal/store" "github.com/stretchr/testify/assert" - ) func TestResetHackathon(t *testing.T) { @@ -58,7 +57,7 @@ func TestResetHackathon(t *testing.T) { // Simulate partial failure/rollback by returning error from store app.store.Hackathon.(*store.MockHackathonStore). - On("Reset",true, false, false, false). + On("Reset", true, false, false, false). Return([]string(nil), errors.New("db transaction failed")) reqBody, _ := json.Marshal(payload) diff --git a/cmd/api/settings_test.go b/cmd/api/settings_test.go index 5a558358..9733a4ad 100644 --- a/cmd/api/settings_test.go +++ b/cmd/api/settings_test.go @@ -72,7 +72,7 @@ func TestUpdateShortAnswerQuestions(t *testing.T) { req.Header.Set("Content-Type", "application/json") req = setUserContext(req, newSuperAdminUser()) }) - + t.Run("should return 500 when transaction fails", func(t *testing.T) { app := newTestApplication(t) app.gcsClient = nil From 3907b06a091d29993367644cb8be00a647f0b385 Mon Sep 17 00:00:00 2001 From: Noel Varghese <91435868+NoelVarghese2006@users.noreply.github.com> Date: Wed, 18 Mar 2026 00:34:01 -0500 Subject: [PATCH 4/8] chore(static_errors) --- .../src/pages/superadmin/settings/tabs/ResetHackathonCard.tsx | 2 +- cmd/api/settings_test.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/client/web/src/pages/superadmin/settings/tabs/ResetHackathonCard.tsx b/client/web/src/pages/superadmin/settings/tabs/ResetHackathonCard.tsx index 0692a2ed..84ef6f82 100644 --- a/client/web/src/pages/superadmin/settings/tabs/ResetHackathonCard.tsx +++ b/client/web/src/pages/superadmin/settings/tabs/ResetHackathonCard.tsx @@ -66,7 +66,7 @@ export function ResetHackathonCard() { reset_settings: false, }); } catch (err) { - toast.error("An unexpected error occurred"); + toast.error("An unexpected error occurred" + (err instanceof Error ? `: ${err.message}` : "")); } finally { setLoading(false); } diff --git a/cmd/api/settings_test.go b/cmd/api/settings_test.go index 9733a4ad..e01d93bb 100644 --- a/cmd/api/settings_test.go +++ b/cmd/api/settings_test.go @@ -67,10 +67,14 @@ func TestUpdateShortAnswerQuestions(t *testing.T) { t.Run("should return 400 for duplicate question IDs", func(t *testing.T) { body := `{"questions":[{"id":"q1","question":"A?","required":true,"display_order":0},{"id":"q1","question":"B?","required":false,"display_order":1}]}` + req, err := http.NewRequest(http.MethodPut, "/", strings.NewReader(body)) require.NoError(t, err) req.Header.Set("Content-Type", "application/json") req = setUserContext(req, newSuperAdminUser()) + + rr := executeRequest(req, http.HandlerFunc(app.updateShortAnswerQuestions)) + checkResponseCode(t, http.StatusBadRequest, rr.Code) }) t.Run("should return 500 when transaction fails", func(t *testing.T) { From 2445affaed51e1e62da46d77c6f06400333a2796 Mon Sep 17 00:00:00 2001 From: Noel Varghese <91435868+NoelVarghese2006@users.noreply.github.com> Date: Wed, 18 Mar 2026 00:36:56 -0500 Subject: [PATCH 5/8] chore(prettier) --- .../pages/superadmin/settings/tabs/ResetHackathonCard.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/web/src/pages/superadmin/settings/tabs/ResetHackathonCard.tsx b/client/web/src/pages/superadmin/settings/tabs/ResetHackathonCard.tsx index 84ef6f82..d06feb23 100644 --- a/client/web/src/pages/superadmin/settings/tabs/ResetHackathonCard.tsx +++ b/client/web/src/pages/superadmin/settings/tabs/ResetHackathonCard.tsx @@ -66,7 +66,10 @@ export function ResetHackathonCard() { reset_settings: false, }); } catch (err) { - toast.error("An unexpected error occurred" + (err instanceof Error ? `: ${err.message}` : "")); + toast.error( + "An unexpected error occurred" + + (err instanceof Error ? `: ${err.message}` : ""), + ); } finally { setLoading(false); } From ba2e5232f86094151d2838be21ace5e704b645b5 Mon Sep 17 00:00:00 2001 From: Noel Varghese <91435868+NoelVarghese2006@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:55:25 -0500 Subject: [PATCH 6/8] chore(Update SettingsDialog.tsx) --- .../src/pages/superadmin/settings/components/SettingsDialog.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx b/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx index 370bc35e..2c31d648 100644 --- a/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx +++ b/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx @@ -3,7 +3,6 @@ import { AlertTriangle, CalendarRange, - UserCog, UsersRound, } from "lucide-react"; import { CalendarRange, UsersRound } from "lucide-react"; From 4507957e500ee78eaf7cddb66212aa7ac8c7a18c Mon Sep 17 00:00:00 2001 From: Noel Varghese <91435868+NoelVarghese2006@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:00:12 -0500 Subject: [PATCH 7/8] chore(prettier) --- .../pages/superadmin/settings/components/SettingsDialog.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx b/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx index 2c31d648..e36486ba 100644 --- a/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx +++ b/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx @@ -1,10 +1,6 @@ "use client"; -import { - AlertTriangle, - CalendarRange, - UsersRound, -} from "lucide-react"; +import { AlertTriangle, CalendarRange, UsersRound } from "lucide-react"; import { CalendarRange, UsersRound } from "lucide-react"; import * as React from "react"; From 39a814fa1454b340bca31a282f672d9bd0998ca0 Mon Sep 17 00:00:00 2001 From: Noel Varghese <91435868+NoelVarghese2006@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:03:20 -0500 Subject: [PATCH 8/8] chore(prettier) --- .../src/pages/superadmin/settings/components/SettingsDialog.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx b/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx index e36486ba..290271c0 100644 --- a/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx +++ b/client/web/src/pages/superadmin/settings/components/SettingsDialog.tsx @@ -1,7 +1,6 @@ "use client"; import { AlertTriangle, CalendarRange, UsersRound } from "lucide-react"; -import { CalendarRange, UsersRound } from "lucide-react"; import * as React from "react"; import { Button } from "@/components/ui/button";