156 lines
4.7 KiB
Go
156 lines
4.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"Yimaru-Backend/internal/domain"
|
|
"crypto/subtle"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
)
|
|
|
|
type resetAndReseedReq struct {
|
|
Confirm string `json:"confirm"`
|
|
}
|
|
|
|
// ResetAndReseedDatabase godoc
|
|
// @Summary Reset and reseed database
|
|
// @Description Dangerous operation: truncates all public tables (except schema_migrations) and reseeds from db/data SQL files.
|
|
// @Tags internal
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param X-Seed-Reset-Token header string true "Reset token configured in DB_RESET_RESEED_TOKEN"
|
|
// @Param body body resetAndReseedReq true "Confirmation payload"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 403 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/internal/db/reset-reseed [post]
|
|
func (h *Handler) ResetAndReseedDatabase(c *fiber.Ctx) error {
|
|
if h.Cfg == nil || !h.Cfg.DBResetReseedEnabled {
|
|
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
|
Message: "Operation is disabled",
|
|
Error: "DB_RESET_RESEED_ENABLED must be set to true",
|
|
})
|
|
}
|
|
|
|
var req resetAndReseedReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid request body",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
if strings.TrimSpace(req.Confirm) != "RESET_AND_RESEED" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Confirmation required",
|
|
Error: `set confirm to "RESET_AND_RESEED"`,
|
|
})
|
|
}
|
|
|
|
expectedToken := strings.TrimSpace(h.Cfg.DBResetReseedToken)
|
|
providedToken := strings.TrimSpace(c.Get("X-Seed-Reset-Token"))
|
|
if expectedToken == "" || subtle.ConstantTimeCompare([]byte(providedToken), []byte(expectedToken)) != 1 {
|
|
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
|
Message: "Invalid reset token",
|
|
Error: "missing or invalid X-Seed-Reset-Token",
|
|
})
|
|
}
|
|
|
|
seedDir := strings.TrimSpace(h.Cfg.DBSeedDir)
|
|
if seedDir == "" {
|
|
seedDir = "db/data"
|
|
}
|
|
seedFiles, err := filepath.Glob(filepath.Join(seedDir, "*.sql"))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to resolve seed files",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
if len(seedFiles) == 0 {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "No seed files found",
|
|
Error: fmt.Sprintf("no .sql files found in %s", seedDir),
|
|
})
|
|
}
|
|
sort.Strings(seedFiles)
|
|
|
|
var sqlBuilder strings.Builder
|
|
sqlBuilder.WriteString("BEGIN;\n")
|
|
sqlBuilder.WriteString(`
|
|
DO $$
|
|
DECLARE
|
|
truncate_stmt text;
|
|
BEGIN
|
|
SELECT 'TRUNCATE TABLE ' || string_agg(format('%I.%I', schemaname, tablename), ', ')
|
|
|| ' RESTART IDENTITY CASCADE'
|
|
INTO truncate_stmt
|
|
FROM pg_tables
|
|
WHERE schemaname = 'public'
|
|
AND tablename <> 'schema_migrations';
|
|
|
|
IF truncate_stmt IS NOT NULL THEN
|
|
EXECUTE truncate_stmt;
|
|
END IF;
|
|
END $$;
|
|
`)
|
|
|
|
for _, file := range seedFiles {
|
|
content, readErr := os.ReadFile(file)
|
|
if readErr != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to read seed file",
|
|
Error: fmt.Sprintf("%s: %v", file, readErr),
|
|
})
|
|
}
|
|
sqlBuilder.WriteString("\n-- seed file: ")
|
|
sqlBuilder.WriteString(file)
|
|
sqlBuilder.WriteString("\n")
|
|
sqlBuilder.Write(content)
|
|
sqlBuilder.WriteString("\n")
|
|
}
|
|
sqlBuilder.WriteString("COMMIT;")
|
|
|
|
if _, err := h.analyticsDB.ExecRaw(c.Context(), sqlBuilder.String()); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to reset and reseed database",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
// Ensure permission definitions and default assignments are in sync after reseed.
|
|
if err := h.rbacSvc.SeedPermissions(c.Context()); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Database reseeded but RBAC permission sync failed",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
if err := h.rbacSvc.SeedDefaultRolePermissions(c.Context()); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Database reseeded but RBAC role default sync failed",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
if err := h.rbacSvc.Reload(c.Context()); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Database reseeded but RBAC cache reload failed",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Database reset and reseed completed successfully",
|
|
Data: map[string]interface{}{
|
|
"seed_dir": seedDir,
|
|
"seed_files": seedFiles,
|
|
"seeded_count": len(seedFiles),
|
|
},
|
|
Success: true,
|
|
StatusCode: fiber.StatusOK,
|
|
})
|
|
}
|