Introduce lms_personas table, repoint practice persona_id FKs off users, validate persona refs on LMS and exam-prep practice flows, personas.* RBAC permissions, and OpenAPI docs. Co-authored-by: Cursor <cursoragent@cursor.com>
167 lines
6.2 KiB
Go
167 lines
6.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"Yimaru-Backend/internal/domain"
|
|
"errors"
|
|
personasservice "Yimaru-Backend/internal/services/personas"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
)
|
|
|
|
type listPersonasData struct {
|
|
Personas []domain.LmsPersona `json:"personas"`
|
|
TotalCount int64 `json:"total_count"`
|
|
Limit int `json:"limit"`
|
|
Offset int `json:"offset"`
|
|
}
|
|
|
|
// CreatePersona godoc
|
|
// @Summary Create LMS persona catalog entry
|
|
// @Tags personas
|
|
// @Accept json
|
|
// @Param body body domain.CreateLmsPersonaInput true "Persona"
|
|
// @Success 201 {object} domain.Response
|
|
// @Router /api/v1/personas [post]
|
|
func (h *Handler) CreatePersona(c *fiber.Ctx) error {
|
|
var req domain.CreateLmsPersonaInput
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid request body",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Validation failed",
|
|
Error: firstValidationError(valErrs),
|
|
})
|
|
}
|
|
p, err := h.personaSvc.Create(c.Context(), req)
|
|
if err != nil {
|
|
if errors.Is(err, personasservice.ErrNameRequired) {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Validation failed", Error: err.Error()})
|
|
}
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create persona", Error: err.Error()})
|
|
}
|
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
|
Message: "Persona created successfully",
|
|
Data: p,
|
|
Success: true,
|
|
StatusCode: fiber.StatusCreated,
|
|
})
|
|
}
|
|
|
|
// ListPersonas godoc
|
|
// @Summary List LMS personas (catalog for practice assignment)
|
|
// @Tags personas
|
|
// @Param active_only query bool false "When true (default), return only active personas" default(true)
|
|
// @Param limit query int false "Page size"
|
|
// @Param offset query int false "Offset"
|
|
// @Router /api/v1/personas [get]
|
|
func (h *Handler) ListPersonas(c *fiber.Ctx) error {
|
|
activeOnlyStr := strings.ToLower(strings.TrimSpace(c.Query("active_only", "true")))
|
|
activeOnly := activeOnlyStr != "false"
|
|
|
|
limit, err := strconv.Atoi(c.Query("limit", "20"))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid limit", Error: err.Error()})
|
|
}
|
|
offset, err := strconv.Atoi(c.Query("offset", "0"))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid offset", Error: err.Error()})
|
|
}
|
|
items, total, err := h.personaSvc.List(c.Context(), activeOnly, int32(limit), int32(offset))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to list personas", Error: err.Error()})
|
|
}
|
|
return c.JSON(domain.Response{
|
|
Message: "Personas retrieved successfully",
|
|
Data: listPersonasData{
|
|
Personas: items,
|
|
TotalCount: total,
|
|
Limit: limit,
|
|
Offset: offset,
|
|
},
|
|
Success: true,
|
|
StatusCode: fiber.StatusOK,
|
|
})
|
|
}
|
|
|
|
// GetPersona godoc
|
|
// @Summary Get LMS persona by ID
|
|
// @Tags personas
|
|
// @Param id path int true "Persona ID"
|
|
// @Router /api/v1/personas/{id} [get]
|
|
func (h *Handler) GetPersona(c *fiber.Ctx) error {
|
|
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
if err != nil || id <= 0 {
|
|
msg := ""
|
|
if err != nil {
|
|
msg = err.Error()
|
|
}
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid persona id", Error: msg})
|
|
}
|
|
p, err := h.personaSvc.GetByID(c.Context(), id)
|
|
if err != nil {
|
|
if errors.Is(err, domain.ErrPersonaNotFound) {
|
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Persona not found", Error: err.Error()})
|
|
}
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load persona", Error: err.Error()})
|
|
}
|
|
return c.JSON(domain.Response{Message: "Persona retrieved successfully", Data: p, Success: true, StatusCode: fiber.StatusOK})
|
|
}
|
|
|
|
// UpdatePersona godoc
|
|
// @Summary Update LMS persona
|
|
// @Tags personas
|
|
// @Param id path int true "Persona ID"
|
|
// @Param body body domain.UpdateLmsPersonaInput true "Fields to update"
|
|
// @Router /api/v1/personas/{id} [put]
|
|
func (h *Handler) UpdatePersona(c *fiber.Ctx) error {
|
|
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
if err != nil || id <= 0 {
|
|
msg := ""
|
|
if err != nil {
|
|
msg = err.Error()
|
|
}
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid persona id", Error: msg})
|
|
}
|
|
var req domain.UpdateLmsPersonaInput
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
|
}
|
|
p, err := h.personaSvc.Update(c.Context(), id, req)
|
|
if err != nil {
|
|
if errors.Is(err, domain.ErrPersonaNotFound) {
|
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Persona not found", Error: err.Error()})
|
|
}
|
|
if errors.Is(err, personasservice.ErrNameEmptyUpdate) {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Validation failed", Error: err.Error()})
|
|
}
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update persona", Error: err.Error()})
|
|
}
|
|
return c.JSON(domain.Response{Message: "Persona updated successfully", Data: p, Success: true, StatusCode: fiber.StatusOK})
|
|
}
|
|
|
|
// DeletePersona godoc
|
|
// @Summary Delete LMS persona (practices referencing it will have persona_id cleared)
|
|
// @Tags personas
|
|
// @Param id path int true "Persona ID"
|
|
// @Router /api/v1/personas/{id} [delete]
|
|
func (h *Handler) DeletePersona(c *fiber.Ctx) error {
|
|
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
if err != nil || id <= 0 {
|
|
msg := ""
|
|
if err != nil {
|
|
msg = err.Error()
|
|
}
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid persona id", Error: msg})
|
|
}
|
|
if err := h.personaSvc.Delete(c.Context(), id); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete persona", Error: err.Error()})
|
|
}
|
|
return c.JSON(domain.Response{Message: "Persona deleted successfully", Success: true, StatusCode: fiber.StatusOK})
|
|
}
|