Before Fine Tune
This commit is contained in:
100
Backend/internal/handlers/api/admin/handler.go
Normal file
100
Backend/internal/handlers/api/admin/handler.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"boostai-backend/internal/config"
|
||||
"boostai-backend/internal/database"
|
||||
"boostai-backend/internal/http/respond"
|
||||
"boostai-backend/internal/seeddata"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
ReseedHeaderName = "X-Admin-Reseed-Secret"
|
||||
ReseedConfirm = "RESEED"
|
||||
)
|
||||
|
||||
type Runner interface {
|
||||
Run(ctx context.Context, mockDataDir string) (seeddata.Summary, error)
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
cfg *config.Config
|
||||
runner runFunc
|
||||
}
|
||||
|
||||
type runFunc func(timeoutCtx context.Context, mockDataDir string) (seeddata.Summary, error)
|
||||
|
||||
type reseedRequest struct {
|
||||
Confirm string `json:"confirm"`
|
||||
}
|
||||
|
||||
type reseedResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Environment string `json:"environment"`
|
||||
TriggeredBy string `json:"triggered_by,omitempty"`
|
||||
TriggeredAt time.Time `json:"triggered_at"`
|
||||
Summary seeddata.Summary `json:"summary"`
|
||||
}
|
||||
|
||||
func NewHandler(db *database.DB, cfg *config.Config) *Handler {
|
||||
return &Handler{
|
||||
cfg: cfg,
|
||||
runner: func(timeoutCtx context.Context, mockDataDir string) (seeddata.Summary, error) {
|
||||
return seeddata.Run(timeoutCtx, db, mockDataDir)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) ReseedDatabase(c *fiber.Ctx) error {
|
||||
if !h.cfg.AdminReseedEnabled {
|
||||
return respond.Error(c, fiber.StatusNotFound, "not_found", "The requested endpoint does not exist")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(h.cfg.AdminReseedSecret) == "" {
|
||||
return respond.Error(c, fiber.StatusServiceUnavailable, "admin_reseed_unavailable", "Admin reseed is not configured")
|
||||
}
|
||||
|
||||
providedSecret := strings.TrimSpace(c.Get(ReseedHeaderName))
|
||||
if subtle.ConstantTimeCompare([]byte(providedSecret), []byte(h.cfg.AdminReseedSecret)) != 1 {
|
||||
return respond.Error(c, fiber.StatusForbidden, "forbidden", "Valid reseed secret required")
|
||||
}
|
||||
|
||||
var req reseedRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Invalid request body")
|
||||
}
|
||||
if strings.TrimSpace(req.Confirm) != ReseedConfirm {
|
||||
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "confirm must equal RESEED")
|
||||
}
|
||||
|
||||
triggeredBy, _ := c.Locals("auth.email").(string)
|
||||
userID, _ := c.Locals("auth.user_id").(int64)
|
||||
startedAt := time.Now().UTC()
|
||||
log.Printf("admin reseed requested environment=%s user_id=%d email=%s ip=%s", h.cfg.Environment, userID, triggeredBy, c.IP())
|
||||
|
||||
timeoutCtx, cancelTimeout := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancelTimeout()
|
||||
|
||||
summary, err := h.runner(timeoutCtx, h.cfg.MockDataDir)
|
||||
if err != nil {
|
||||
log.Printf("admin reseed failed environment=%s user_id=%d email=%s err=%v", h.cfg.Environment, userID, triggeredBy, err)
|
||||
return respond.Error(c, fiber.StatusInternalServerError, "admin_reseed_failed", err.Error())
|
||||
}
|
||||
|
||||
log.Printf("admin reseed completed environment=%s user_id=%d email=%s users=%d assignments=%d student_answers=%d", h.cfg.Environment, userID, triggeredBy, summary.Users, summary.Assignments, summary.StudentAnswers)
|
||||
|
||||
return c.JSON(reseedResponse{
|
||||
OK: true,
|
||||
Environment: h.cfg.Environment,
|
||||
TriggeredBy: triggeredBy,
|
||||
TriggeredAt: startedAt,
|
||||
Summary: summary,
|
||||
})
|
||||
}
|
||||
108
Backend/internal/handlers/api/admin/handler_test.go
Normal file
108
Backend/internal/handlers/api/admin/handler_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"boostai-backend/internal/config"
|
||||
"boostai-backend/internal/seeddata"
|
||||
"boostai-backend/internal/sqlc"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func TestReseedDatabaseRequiresEnableFlag(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := &Handler{cfg: &config.Config{Environment: "production"}}
|
||||
status := performReseedRequest(t, h, map[string]any{"confirm": "RESEED"}, "secret", true)
|
||||
if status != fiber.StatusNotFound {
|
||||
t.Fatalf("expected 404, got %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReseedDatabaseRequiresSecret(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newTestHandler(func(ctx context.Context, mockDataDir string) (seeddata.Summary, error) {
|
||||
return seeddata.Summary{}, nil
|
||||
})
|
||||
status := performReseedRequest(t, h, map[string]any{"confirm": "RESEED"}, "wrong", true)
|
||||
if status != fiber.StatusForbidden {
|
||||
t.Fatalf("expected 403, got %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReseedDatabaseRequiresConfirm(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newTestHandler(func(ctx context.Context, mockDataDir string) (seeddata.Summary, error) {
|
||||
return seeddata.Summary{}, nil
|
||||
})
|
||||
status := performReseedRequest(t, h, map[string]any{"confirm": "nope"}, "secret", true)
|
||||
if status != fiber.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReseedDatabaseReturnsSummary(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newTestHandler(func(ctx context.Context, mockDataDir string) (seeddata.Summary, error) {
|
||||
if mockDataDir != "/app/Mock-Data" {
|
||||
t.Fatalf("expected mock data dir /app/Mock-Data, got %q", mockDataDir)
|
||||
}
|
||||
return seeddata.Summary{Users: 13, Assignments: 8, StudentAnswers: 588}, nil
|
||||
})
|
||||
status := performReseedRequest(t, h, map[string]any{"confirm": "RESEED"}, "secret", true)
|
||||
if status != fiber.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReseedDatabaseSurfacesRunnerError(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newTestHandler(func(ctx context.Context, mockDataDir string) (seeddata.Summary, error) {
|
||||
return seeddata.Summary{}, errors.New("boom")
|
||||
})
|
||||
status := performReseedRequest(t, h, map[string]any{"confirm": "RESEED"}, "secret", true)
|
||||
if status != fiber.StatusInternalServerError {
|
||||
t.Fatalf("expected 500, got %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
func newTestHandler(fn func(context.Context, string) (seeddata.Summary, error)) *Handler {
|
||||
return &Handler{
|
||||
cfg: &config.Config{Environment: "production", AdminReseedEnabled: true, AdminReseedSecret: "secret", MockDataDir: "/app/Mock-Data"},
|
||||
runner: fn,
|
||||
}
|
||||
}
|
||||
|
||||
func performReseedRequest(t *testing.T, handler *Handler, payload map[string]any, secret string, authenticated bool) int {
|
||||
t.Helper()
|
||||
app := fiber.New()
|
||||
app.Post("/internal/admin/reseed", func(c *fiber.Ctx) error {
|
||||
if authenticated {
|
||||
c.Locals("auth.user_id", int64(42))
|
||||
c.Locals("auth.role", sqlc.UserRoleTeacher)
|
||||
c.Locals("auth.email", "teacher@example.com")
|
||||
}
|
||||
return handler.ReseedDatabase(c)
|
||||
})
|
||||
bodyBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/internal/admin/reseed", bytes.NewReader(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if secret != "" {
|
||||
req.Header.Set(ReseedHeaderName, secret)
|
||||
}
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return resp.StatusCode
|
||||
}
|
||||
11
Backend/internal/handlers/api/admin/routes.go
Normal file
11
Backend/internal/handlers/api/admin/routes.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
authmw "boostai-backend/internal/middleware"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func RegisterRoutes(app fiber.Router, auth *authmw.AuthMiddleware, h *Handler) {
|
||||
app.Post("/internal/admin/reseed", auth.RequireTeacher(), h.ReseedDatabase)
|
||||
}
|
||||
Reference in New Issue
Block a user