package handler import ( "baron-sso-backend/internal/domain" "baron-sso-backend/internal/service" "baron-sso-backend/internal/utils" "strings" "time" "github.com/gofiber/fiber/v2" ) type UserHandler struct { KratosAdmin *service.KratosAdminService OryProvider *service.OryProvider } func NewUserHandler(kratosAdmin *service.KratosAdminService, oryProvider *service.OryProvider) *UserHandler { return &UserHandler{ KratosAdmin: kratosAdmin, OryProvider: oryProvider, } } type userSummary struct { ID string `json:"id"` Email string `json:"email"` Name string `json:"name"` Phone string `json:"phone"` Role string `json:"role"` Status string `json:"status"` CompanyCode string `json:"companyCode"` Department string `json:"department"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` InitialPassword string `json:"initialPassword,omitempty"` } type userListResponse struct { Items []userSummary `json:"items"` Limit int `json:"limit"` Offset int `json:"offset"` Total int64 `json:"total"` } func (h *UserHandler) ListUsers(c *fiber.Ctx) error { if h.KratosAdmin == nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity provider not available"}) } limit := c.QueryInt("limit", 50) offset := c.QueryInt("offset", 0) search := strings.TrimSpace(c.Query("search")) if limit <= 0 { limit = 50 } if offset < 0 { offset = 0 } identities, err := h.KratosAdmin.ListIdentities(c.Context()) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } filtered := make([]service.KratosIdentity, 0, len(identities)) if search == "" { filtered = identities } else { searchLower := strings.ToLower(search) for _, identity := range identities { email := strings.ToLower(extractTraitString(identity.Traits, "email")) name := strings.ToLower(extractTraitString(identity.Traits, "name")) if strings.Contains(email, searchLower) || strings.Contains(name, searchLower) { filtered = append(filtered, identity) } } } total := int64(len(filtered)) if offset > len(filtered) { offset = len(filtered) } end := offset + limit if end > len(filtered) { end = len(filtered) } items := make([]userSummary, 0, end-offset) for _, identity := range filtered[offset:end] { items = append(items, mapIdentitySummary(identity)) } return c.JSON(userListResponse{Items: items, Limit: limit, Offset: offset, Total: total}) } func (h *UserHandler) GetUser(c *fiber.Ctx) error { if h.KratosAdmin == nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity provider not available"}) } userID := strings.TrimSpace(c.Params("id")) if userID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "user id is required"}) } identity, err := h.KratosAdmin.GetIdentity(c.Context(), userID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } if identity == nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "user not found"}) } return c.JSON(mapIdentitySummary(*identity)) } func (h *UserHandler) CreateUser(c *fiber.Ctx) error { if h.OryProvider == nil || h.KratosAdmin == nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity provider not available"}) } var req struct { Email string `json:"email"` Password string `json:"password"` Name string `json:"name"` Phone string `json:"phone"` Role string `json:"role"` CompanyCode string `json:"companyCode"` Department string `json:"department"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } email := strings.TrimSpace(req.Email) if email == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "email is required"}) } name := strings.TrimSpace(req.Name) if name == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name is required"}) } password := strings.TrimSpace(req.Password) policy, err := h.OryProvider.GetPasswordPolicy() if err != nil || policy == nil { policy = &domain.PasswordPolicy{ MinLength: 12, Lowercase: true, Uppercase: false, Number: true, NonAlphanumeric: true, MinCharacterTypes: 0, } } generatedPassword := "" if password == "" { generated, genErr := utils.GeneratePasswordWithPolicy(policy) if genErr != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to generate password"}) } password = generated generatedPassword = generated } else { if err := utils.ValidatePasswordWithPolicy(policy, password); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } } role := strings.TrimSpace(req.Role) if role == "" { role = "user" } attributes := map[string]interface{}{ "department": req.Department, "affiliationType": "internal", "companyCode": req.CompanyCode, "grade": role, } brokerUser := &domain.BrokerUser{ Email: email, Name: name, PhoneNumber: normalizePhoneNumber(req.Phone), Attributes: attributes, } identityID, err := h.OryProvider.CreateUser(brokerUser, password) if err != nil { if strings.Contains(err.Error(), "already exists") { return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "email already exists"}) } return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } identity, err := h.KratosAdmin.GetIdentity(c.Context(), identityID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } if identity == nil { return c.Status(fiber.StatusCreated).JSON(fiber.Map{"id": identityID, "initialPassword": generatedPassword}) } response := mapIdentitySummary(*identity) if generatedPassword != "" { response.InitialPassword = generatedPassword } return c.Status(fiber.StatusCreated).JSON(response) } func (h *UserHandler) UpdateUser(c *fiber.Ctx) error { if h.KratosAdmin == nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity provider not available"}) } userID := strings.TrimSpace(c.Params("id")) if userID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "user id is required"}) } identity, err := h.KratosAdmin.GetIdentity(c.Context(), userID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } if identity == nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "user not found"}) } var req struct { Password *string `json:"password"` Name *string `json:"name"` Phone *string `json:"phone"` Role *string `json:"role"` Status *string `json:"status"` CompanyCode *string `json:"companyCode"` Department *string `json:"department"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } traits := identity.Traits if traits == nil { traits = map[string]interface{}{} } if req.Name != nil { traits["name"] = strings.TrimSpace(*req.Name) } if req.Phone != nil { traits["phone_number"] = normalizePhoneNumber(strings.TrimSpace(*req.Phone)) } if req.CompanyCode != nil { traits["companyCode"] = strings.TrimSpace(*req.CompanyCode) } if req.Department != nil { traits["department"] = strings.TrimSpace(*req.Department) } if req.Role != nil { role := strings.TrimSpace(*req.Role) if role == "" { role = "user" } traits["grade"] = role } state := normalizeKratosState(req.Status) updated, err := h.KratosAdmin.UpdateIdentity(c.Context(), userID, traits, state) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } if req.Password != nil && *req.Password != "" { if err := h.KratosAdmin.UpdateIdentityPassword(c.Context(), userID, *req.Password); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } } return c.JSON(mapIdentitySummary(*updated)) } func (h *UserHandler) DeleteUser(c *fiber.Ctx) error { if h.KratosAdmin == nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity provider not available"}) } userID := strings.TrimSpace(c.Params("id")) if userID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "user id is required"}) } if err := h.KratosAdmin.DeleteIdentity(c.Context(), userID); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.SendStatus(fiber.StatusNoContent) } func mapIdentitySummary(identity service.KratosIdentity) userSummary { traits := identity.Traits role := extractTraitString(traits, "grade") if role == "" { role = "user" } return userSummary{ ID: identity.ID, Email: extractTraitString(traits, "email"), Name: extractTraitString(traits, "name"), Phone: extractTraitString(traits, "phone_number"), Role: role, Status: normalizeStatus(identity.State), CompanyCode: extractTraitString(traits, "companyCode"), Department: extractTraitString(traits, "department"), CreatedAt: formatTime(identity.CreatedAt), UpdatedAt: formatTime(identity.UpdatedAt), } } func extractTraitString(traits map[string]interface{}, key string) string { if traits == nil { return "" } if raw, ok := traits[key]; ok { if value, ok := raw.(string); ok { return value } } return "" } func formatTime(value time.Time) string { if value.IsZero() { return "" } return value.Format(time.RFC3339) } func normalizeStatus(state string) string { state = strings.ToLower(strings.TrimSpace(state)) if state == "inactive" || state == "blocked" || state == "active" { return state } if state == "" { return "active" } return state } func normalizeKratosState(status *string) string { if status == nil { return "" } value := strings.ToLower(strings.TrimSpace(*status)) if value == "blocked" { return "inactive" } if value == "active" || value == "inactive" { return value } return "" } func normalizePhoneNumber(phone string) string { normalized := strings.ReplaceAll(phone, "-", "") normalized = strings.ReplaceAll(normalized, " ", "") if normalized == "" { return "" } if strings.HasPrefix(normalized, "010") { return "+82" + normalized[1:] } if strings.HasPrefix(normalized, "82") { return "+" + normalized } return normalized }