Add a flexible user attribute system that allows admins to define custom fields for users (text, textarea, number, email, url, date, select, multi_select types). - Add Ent schemas for UserAttributeDefinition and UserAttributeValue - Add service layer with validation logic - Add repository layer with batch operations support - Add admin API endpoints for CRUD and reorder operations - Add batch API for loading attribute values for multiple users - Add database migration (018_user_attributes.sql) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
343 lines
10 KiB
Go
343 lines
10 KiB
Go
package admin
|
|
|
|
import (
|
|
"strconv"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// UserAttributeHandler handles user attribute management
|
|
type UserAttributeHandler struct {
|
|
attrService *service.UserAttributeService
|
|
}
|
|
|
|
// NewUserAttributeHandler creates a new handler
|
|
func NewUserAttributeHandler(attrService *service.UserAttributeService) *UserAttributeHandler {
|
|
return &UserAttributeHandler{attrService: attrService}
|
|
}
|
|
|
|
// --- Request/Response DTOs ---
|
|
|
|
// CreateAttributeDefinitionRequest represents create attribute definition request
|
|
type CreateAttributeDefinitionRequest struct {
|
|
Key string `json:"key" binding:"required,min=1,max=100"`
|
|
Name string `json:"name" binding:"required,min=1,max=255"`
|
|
Description string `json:"description"`
|
|
Type string `json:"type" binding:"required"`
|
|
Options []service.UserAttributeOption `json:"options"`
|
|
Required bool `json:"required"`
|
|
Validation service.UserAttributeValidation `json:"validation"`
|
|
Placeholder string `json:"placeholder"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
// UpdateAttributeDefinitionRequest represents update attribute definition request
|
|
type UpdateAttributeDefinitionRequest struct {
|
|
Name *string `json:"name"`
|
|
Description *string `json:"description"`
|
|
Type *string `json:"type"`
|
|
Options *[]service.UserAttributeOption `json:"options"`
|
|
Required *bool `json:"required"`
|
|
Validation *service.UserAttributeValidation `json:"validation"`
|
|
Placeholder *string `json:"placeholder"`
|
|
Enabled *bool `json:"enabled"`
|
|
}
|
|
|
|
// ReorderRequest represents reorder attribute definitions request
|
|
type ReorderRequest struct {
|
|
IDs []int64 `json:"ids" binding:"required"`
|
|
}
|
|
|
|
// UpdateUserAttributesRequest represents update user attributes request
|
|
type UpdateUserAttributesRequest struct {
|
|
Values map[int64]string `json:"values" binding:"required"`
|
|
}
|
|
|
|
// BatchGetUserAttributesRequest represents batch get user attributes request
|
|
type BatchGetUserAttributesRequest struct {
|
|
UserIDs []int64 `json:"user_ids" binding:"required"`
|
|
}
|
|
|
|
// BatchUserAttributesResponse represents batch user attributes response
|
|
type BatchUserAttributesResponse struct {
|
|
// Map of userID -> map of attributeID -> value
|
|
Attributes map[int64]map[int64]string `json:"attributes"`
|
|
}
|
|
|
|
// AttributeDefinitionResponse represents attribute definition response
|
|
type AttributeDefinitionResponse struct {
|
|
ID int64 `json:"id"`
|
|
Key string `json:"key"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Type string `json:"type"`
|
|
Options []service.UserAttributeOption `json:"options"`
|
|
Required bool `json:"required"`
|
|
Validation service.UserAttributeValidation `json:"validation"`
|
|
Placeholder string `json:"placeholder"`
|
|
DisplayOrder int `json:"display_order"`
|
|
Enabled bool `json:"enabled"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// AttributeValueResponse represents attribute value response
|
|
type AttributeValueResponse struct {
|
|
ID int64 `json:"id"`
|
|
UserID int64 `json:"user_id"`
|
|
AttributeID int64 `json:"attribute_id"`
|
|
Value string `json:"value"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// --- Helpers ---
|
|
|
|
func defToResponse(def *service.UserAttributeDefinition) *AttributeDefinitionResponse {
|
|
return &AttributeDefinitionResponse{
|
|
ID: def.ID,
|
|
Key: def.Key,
|
|
Name: def.Name,
|
|
Description: def.Description,
|
|
Type: string(def.Type),
|
|
Options: def.Options,
|
|
Required: def.Required,
|
|
Validation: def.Validation,
|
|
Placeholder: def.Placeholder,
|
|
DisplayOrder: def.DisplayOrder,
|
|
Enabled: def.Enabled,
|
|
CreatedAt: def.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
UpdatedAt: def.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
}
|
|
}
|
|
|
|
func valueToResponse(val *service.UserAttributeValue) *AttributeValueResponse {
|
|
return &AttributeValueResponse{
|
|
ID: val.ID,
|
|
UserID: val.UserID,
|
|
AttributeID: val.AttributeID,
|
|
Value: val.Value,
|
|
CreatedAt: val.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
UpdatedAt: val.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
}
|
|
}
|
|
|
|
// --- Handlers ---
|
|
|
|
// ListDefinitions lists all attribute definitions
|
|
// GET /admin/user-attributes
|
|
func (h *UserAttributeHandler) ListDefinitions(c *gin.Context) {
|
|
enabledOnly := c.Query("enabled") == "true"
|
|
|
|
defs, err := h.attrService.ListDefinitions(c.Request.Context(), enabledOnly)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
out := make([]*AttributeDefinitionResponse, 0, len(defs))
|
|
for i := range defs {
|
|
out = append(out, defToResponse(&defs[i]))
|
|
}
|
|
|
|
response.Success(c, out)
|
|
}
|
|
|
|
// CreateDefinition creates a new attribute definition
|
|
// POST /admin/user-attributes
|
|
func (h *UserAttributeHandler) CreateDefinition(c *gin.Context) {
|
|
var req CreateAttributeDefinitionRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
def, err := h.attrService.CreateDefinition(c.Request.Context(), service.CreateAttributeDefinitionInput{
|
|
Key: req.Key,
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
Type: service.UserAttributeType(req.Type),
|
|
Options: req.Options,
|
|
Required: req.Required,
|
|
Validation: req.Validation,
|
|
Placeholder: req.Placeholder,
|
|
Enabled: req.Enabled,
|
|
})
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, defToResponse(def))
|
|
}
|
|
|
|
// UpdateDefinition updates an attribute definition
|
|
// PUT /admin/user-attributes/:id
|
|
func (h *UserAttributeHandler) UpdateDefinition(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "Invalid attribute ID")
|
|
return
|
|
}
|
|
|
|
var req UpdateAttributeDefinitionRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
input := service.UpdateAttributeDefinitionInput{
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
Options: req.Options,
|
|
Required: req.Required,
|
|
Validation: req.Validation,
|
|
Placeholder: req.Placeholder,
|
|
Enabled: req.Enabled,
|
|
}
|
|
if req.Type != nil {
|
|
t := service.UserAttributeType(*req.Type)
|
|
input.Type = &t
|
|
}
|
|
|
|
def, err := h.attrService.UpdateDefinition(c.Request.Context(), id, input)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, defToResponse(def))
|
|
}
|
|
|
|
// DeleteDefinition deletes an attribute definition
|
|
// DELETE /admin/user-attributes/:id
|
|
func (h *UserAttributeHandler) DeleteDefinition(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "Invalid attribute ID")
|
|
return
|
|
}
|
|
|
|
if err := h.attrService.DeleteDefinition(c.Request.Context(), id); err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{"message": "Attribute definition deleted successfully"})
|
|
}
|
|
|
|
// ReorderDefinitions reorders attribute definitions
|
|
// PUT /admin/user-attributes/reorder
|
|
func (h *UserAttributeHandler) ReorderDefinitions(c *gin.Context) {
|
|
var req ReorderRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Convert IDs array to orders map (position in array = display_order)
|
|
orders := make(map[int64]int, len(req.IDs))
|
|
for i, id := range req.IDs {
|
|
orders[id] = i
|
|
}
|
|
|
|
if err := h.attrService.ReorderDefinitions(c.Request.Context(), orders); err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{"message": "Reorder successful"})
|
|
}
|
|
|
|
// GetUserAttributes gets a user's attribute values
|
|
// GET /admin/users/:id/attributes
|
|
func (h *UserAttributeHandler) GetUserAttributes(c *gin.Context) {
|
|
userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "Invalid user ID")
|
|
return
|
|
}
|
|
|
|
values, err := h.attrService.GetUserAttributes(c.Request.Context(), userID)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
out := make([]*AttributeValueResponse, 0, len(values))
|
|
for i := range values {
|
|
out = append(out, valueToResponse(&values[i]))
|
|
}
|
|
|
|
response.Success(c, out)
|
|
}
|
|
|
|
// UpdateUserAttributes updates a user's attribute values
|
|
// PUT /admin/users/:id/attributes
|
|
func (h *UserAttributeHandler) UpdateUserAttributes(c *gin.Context) {
|
|
userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "Invalid user ID")
|
|
return
|
|
}
|
|
|
|
var req UpdateUserAttributesRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
inputs := make([]service.UpdateUserAttributeInput, 0, len(req.Values))
|
|
for attrID, value := range req.Values {
|
|
inputs = append(inputs, service.UpdateUserAttributeInput{
|
|
AttributeID: attrID,
|
|
Value: value,
|
|
})
|
|
}
|
|
|
|
if err := h.attrService.UpdateUserAttributes(c.Request.Context(), userID, inputs); err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
// Return updated values
|
|
values, err := h.attrService.GetUserAttributes(c.Request.Context(), userID)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
out := make([]*AttributeValueResponse, 0, len(values))
|
|
for i := range values {
|
|
out = append(out, valueToResponse(&values[i]))
|
|
}
|
|
|
|
response.Success(c, out)
|
|
}
|
|
|
|
// GetBatchUserAttributes gets attribute values for multiple users
|
|
// POST /admin/user-attributes/batch
|
|
func (h *UserAttributeHandler) GetBatchUserAttributes(c *gin.Context) {
|
|
var req BatchGetUserAttributesRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
if len(req.UserIDs) == 0 {
|
|
response.Success(c, BatchUserAttributesResponse{Attributes: map[int64]map[int64]string{}})
|
|
return
|
|
}
|
|
|
|
attrs, err := h.attrService.GetBatchUserAttributes(c.Request.Context(), req.UserIDs)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, BatchUserAttributesResponse{Attributes: attrs})
|
|
}
|