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.
+
+
+
+
+
+
+ );
+}
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";