Before Fine Tune

This commit is contained in:
MangoPig
2026-05-26 13:43:09 +01:00
parent 4f79137d89
commit f29aff25f5
35 changed files with 6953 additions and 142 deletions

View File

@@ -0,0 +1,338 @@
package reseed
import (
"context"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"fmt"
"html"
"log"
"strings"
"time"
"boostai-backend/internal/config"
"boostai-backend/internal/database"
"boostai-backend/internal/seeddata"
"github.com/gofiber/fiber/v2"
)
const reseedCookieName = "boostai_reseed_auth"
type runFunc func(ctx context.Context, mockDataDir string) (seeddata.Summary, error)
type Handler struct {
cfg *config.Config
runner runFunc
}
type pageData struct {
Authorized bool
Environment string
MockDataDir string
Error string
Success string
Summary *seeddata.Summary
}
func NewHandler(db *database.DB, cfg *config.Config) *Handler {
return &Handler{
cfg: cfg,
runner: func(ctx context.Context, mockDataDir string) (seeddata.Summary, error) {
return seeddata.Run(ctx, db, mockDataDir)
},
}
}
func (h *Handler) Page(c *fiber.Ctx) error {
if !h.cfg.AdminReseedEnabled {
return fiber.ErrNotFound
}
return h.renderPage(c, pageData{
Authorized: h.isAuthorized(c),
Environment: h.cfg.Environment,
MockDataDir: h.cfg.MockDataDir,
})
}
func (h *Handler) Login(c *fiber.Ctx) error {
if !h.cfg.AdminReseedEnabled {
return fiber.ErrNotFound
}
password := strings.TrimSpace(c.FormValue("password"))
if subtle.ConstantTimeCompare([]byte(password), []byte(h.cfg.ReseedPagePassword)) != 1 {
return h.renderPage(c.Status(fiber.StatusUnauthorized), pageData{
Authorized: false,
Environment: h.cfg.Environment,
MockDataDir: h.cfg.MockDataDir,
Error: "Invalid password",
})
}
h.setAuthCookie(c)
return c.Redirect("/reseed", fiber.StatusSeeOther)
}
func (h *Handler) Run(c *fiber.Ctx) error {
if !h.cfg.AdminReseedEnabled {
return fiber.ErrNotFound
}
if !h.isAuthorized(c) {
return h.renderPage(c.Status(fiber.StatusUnauthorized), pageData{
Authorized: false,
Environment: h.cfg.Environment,
MockDataDir: h.cfg.MockDataDir,
Error: "Please unlock the reseed page first",
})
}
startedAt := time.Now().UTC()
log.Printf("browser reseed requested environment=%s ip=%s", h.cfg.Environment, c.IP())
timeoutCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
summary, err := h.runner(timeoutCtx, h.cfg.MockDataDir)
if err != nil {
log.Printf("browser reseed failed environment=%s ip=%s err=%v", h.cfg.Environment, c.IP(), err)
return h.renderPage(c.Status(fiber.StatusInternalServerError), pageData{
Authorized: true,
Environment: h.cfg.Environment,
MockDataDir: h.cfg.MockDataDir,
Error: err.Error(),
})
}
log.Printf("browser reseed completed environment=%s ip=%s users=%d assignments=%d student_answers=%d", h.cfg.Environment, c.IP(), summary.Users, summary.Assignments, summary.StudentAnswers)
return h.renderPage(c, pageData{
Authorized: true,
Environment: h.cfg.Environment,
MockDataDir: h.cfg.MockDataDir,
Success: fmt.Sprintf("Reseed completed at %s UTC", startedAt.Format("2006-01-02 15:04:05")),
Summary: &summary,
})
}
func (h *Handler) Logout(c *fiber.Ctx) error {
if !h.cfg.AdminReseedEnabled {
return fiber.ErrNotFound
}
h.clearAuthCookie(c)
return c.Redirect("/reseed", fiber.StatusSeeOther)
}
func (h *Handler) isAuthorized(c *fiber.Ctx) bool {
provided := strings.TrimSpace(c.Cookies(reseedCookieName))
expected := h.authCookieValue()
if provided == "" || expected == "" {
return false
}
return subtle.ConstantTimeCompare([]byte(provided), []byte(expected)) == 1
}
func (h *Handler) setAuthCookie(c *fiber.Ctx) {
c.Cookie(&fiber.Cookie{
Name: reseedCookieName,
Value: h.authCookieValue(),
HTTPOnly: true,
Secure: h.cfg.IsProduction(),
SameSite: fiber.CookieSameSiteLaxMode,
Path: "/",
Expires: time.Now().UTC().Add(12 * time.Hour),
})
}
func (h *Handler) clearAuthCookie(c *fiber.Ctx) {
c.Cookie(&fiber.Cookie{
Name: reseedCookieName,
Value: "",
HTTPOnly: true,
Secure: h.cfg.IsProduction(),
SameSite: fiber.CookieSameSiteLaxMode,
Path: "/",
Expires: time.Unix(0, 0),
MaxAge: -1,
})
}
func (h *Handler) authCookieValue() string {
sum := sha256.Sum256([]byte(h.cfg.JWTSecret + "|" + h.cfg.ReseedPagePassword))
return hex.EncodeToString(sum[:])
}
func (h *Handler) renderPage(c *fiber.Ctx, data pageData) error {
content := reseedPageHTML(data)
c.Type("html", "utf-8")
return c.SendString(content)
}
func reseedPageHTML(data pageData) string {
var statusHTML strings.Builder
if data.Error != "" {
statusHTML.WriteString(`<div class="notice notice-error">` + html.EscapeString(data.Error) + `</div>`)
}
if data.Success != "" {
statusHTML.WriteString(`<div class="notice notice-success">` + html.EscapeString(data.Success) + `</div>`)
}
if data.Summary != nil {
statusHTML.WriteString(`<pre class="summary">`)
statusHTML.WriteString(html.EscapeString(fmt.Sprintf(
"users: %d\nclassrooms: %d\nquestions: %d\ntags: %d\nassignments: %d\nassignment_links: %d\nstudent_answers: %d\nmock_data_dir: %s",
data.Summary.Users,
data.Summary.Classrooms,
data.Summary.Questions,
data.Summary.Tags,
data.Summary.Assignments,
data.Summary.AssignmentLinks,
data.Summary.StudentAnswers,
data.Summary.MockDataDir,
)))
statusHTML.WriteString(`</pre>`)
}
var body strings.Builder
if data.Authorized {
body.WriteString(`
<div class="card">
<h2>Reseed database</h2>
<p>This will clear seeded app data and repopulate it from Mock-Data.</p>
<form method="post" action="/reseed/run">
<button class="danger" type="submit">Reseed now</button>
</form>
<form method="post" action="/reseed/logout">
<button type="submit">Lock page</button>
</form>
</div>
`)
} else {
body.WriteString(`
<div class="card">
<h2>Unlock reseed</h2>
<form method="post" action="/reseed/login">
<label for="password">Password</label>
<input id="password" name="password" type="password" autocomplete="current-password" required />
<button type="submit">Unlock</button>
</form>
</div>
`)
}
return fmt.Sprintf(`<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>BoostAI Reseed</title>
<style>
:root { color-scheme: dark; }
body {
margin: 0;
font-family: Inter, Arial, sans-serif;
background: #0f172a;
color: #e2e8f0;
}
main {
max-width: 720px;
margin: 48px auto;
padding: 0 20px 48px;
}
h1, h2 { margin-top: 0; }
.card {
background: #111827;
border: 1px solid #334155;
border-radius: 16px;
padding: 24px;
margin-bottom: 20px;
box-shadow: 0 12px 30px rgba(0,0,0,0.25);
}
.meta {
display: grid;
grid-template-columns: 180px 1fr;
gap: 8px 12px;
font-size: 14px;
margin-bottom: 20px;
}
.meta strong { color: #93c5fd; }
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
}
input {
width: 100%%;
box-sizing: border-box;
padding: 12px 14px;
border-radius: 10px;
border: 1px solid #475569;
background: #0f172a;
color: #e2e8f0;
margin-bottom: 14px;
}
button {
padding: 12px 16px;
border-radius: 10px;
border: 0;
cursor: pointer;
font-weight: 700;
background: #38bdf8;
color: #082f49;
margin-right: 12px;
margin-bottom: 12px;
}
button.danger {
background: #f87171;
color: #450a0a;
}
.notice {
padding: 14px 16px;
border-radius: 12px;
margin-bottom: 16px;
font-weight: 600;
}
.notice-error {
background: rgba(220, 38, 38, 0.18);
border: 1px solid rgba(248, 113, 113, 0.45);
color: #fecaca;
}
.notice-success {
background: rgba(22, 163, 74, 0.18);
border: 1px solid rgba(74, 222, 128, 0.45);
color: #bbf7d0;
}
.summary {
background: #020617;
border: 1px solid #334155;
border-radius: 12px;
padding: 16px;
overflow: auto;
}
</style>
</head>
<body>
<main>
<div class="card">
<h1>BoostAI reseed</h1>
<div class="meta">
<strong>Environment</strong><span>%s</span>
<strong>Mock data path</strong><span>%s</span>
<strong>Mode</strong><span>Browser-protected destructive reseed</span>
</div>
%s
</div>
%s
</main>
</body>
</html>`,
html.EscapeString(data.Environment),
html.EscapeString(data.MockDataDir),
statusHTML.String(),
body.String(),
)
}

View File

@@ -0,0 +1,137 @@
package reseed
import (
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"boostai-backend/internal/config"
"boostai-backend/internal/seeddata"
"github.com/gofiber/fiber/v2"
)
func TestPageRequiresEnableFlag(t *testing.T) {
t.Parallel()
h := &Handler{cfg: &config.Config{Environment: "production"}}
status, _ := performRequest(t, h, http.MethodGet, "/reseed", "", "")
if status != fiber.StatusNotFound {
t.Fatalf("expected 404, got %d", status)
}
}
func TestLoginSetsCookieAndRedirects(t *testing.T) {
t.Parallel()
h := newTestHandler(func(ctx context.Context, mockDataDir string) (seeddata.Summary, error) {
return seeddata.Summary{}, nil
})
status, resp := performRequest(t, h, http.MethodPost, "/reseed/login", "password=1588", "")
if status != fiber.StatusSeeOther {
t.Fatalf("expected 303, got %d", status)
}
if location := resp.Header.Get("Location"); location != "/reseed" {
t.Fatalf("expected redirect to /reseed, got %q", location)
}
if cookie := resp.Header.Get("Set-Cookie"); !strings.Contains(cookie, reseedCookieName+"=") {
t.Fatalf("expected auth cookie, got %q", cookie)
}
}
func TestRunRequiresAuthCookie(t *testing.T) {
t.Parallel()
h := newTestHandler(func(ctx context.Context, mockDataDir string) (seeddata.Summary, error) {
return seeddata.Summary{}, nil
})
status, body := performRequestBody(t, h, http.MethodPost, "/reseed/run", "", "")
if status != fiber.StatusUnauthorized {
t.Fatalf("expected 401, got %d", status)
}
if !strings.Contains(body, "Please unlock the reseed page first") {
t.Fatalf("expected unlock message, got %q", body)
}
}
func TestRunExecutesReseed(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, MockDataDir: mockDataDir}, nil
})
status, body := performRequestBody(t, h, http.MethodPost, "/reseed/run", "", reseedCookieName+"="+h.authCookieValue())
if status != fiber.StatusOK {
t.Fatalf("expected 200, got %d", status)
}
if !strings.Contains(body, "Reseed completed") || !strings.Contains(body, "student_answers: 588") {
t.Fatalf("expected success summary, got %q", body)
}
}
func TestRunSurfacesRunnerError(t *testing.T) {
t.Parallel()
h := newTestHandler(func(ctx context.Context, mockDataDir string) (seeddata.Summary, error) {
return seeddata.Summary{}, errors.New("boom")
})
status, body := performRequestBody(t, h, http.MethodPost, "/reseed/run", "", reseedCookieName+"="+h.authCookieValue())
if status != fiber.StatusInternalServerError {
t.Fatalf("expected 500, got %d", status)
}
if !strings.Contains(body, "boom") {
t.Fatalf("expected error body, got %q", body)
}
}
func newTestHandler(fn func(context.Context, string) (seeddata.Summary, error)) *Handler {
return &Handler{
cfg: &config.Config{
Environment: "production",
AdminReseedEnabled: true,
MockDataDir: "/app/Mock-Data",
JWTSecret: "jwt-secret",
ReseedPagePassword: "1588",
},
runner: fn,
}
}
func performRequest(t *testing.T, handler *Handler, method, path, formBody, cookieHeader string) (int, *http.Response) {
t.Helper()
app := testApp(handler)
req := httptest.NewRequest(method, path, strings.NewReader(formBody))
if formBody != "" {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
if cookieHeader != "" {
req.Header.Set("Cookie", cookieHeader)
}
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
return resp.StatusCode, resp
}
func performRequestBody(t *testing.T, handler *Handler, method, path, formBody, cookieHeader string) (int, string) {
t.Helper()
status, resp := performRequest(t, handler, method, path, formBody, cookieHeader)
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("ReadAll: %v", err)
}
return status, string(bodyBytes)
}
func testApp(handler *Handler) *fiber.App {
app := fiber.New()
app.Get("/reseed", handler.Page)
app.Post("/reseed/login", handler.Login)
app.Post("/reseed/run", handler.Run)
app.Post("/reseed/logout", handler.Logout)
return app
}