1
0
forked from baron/baron-sso

내정보 페이지 사용성개선, adminFront user 정보 연동.

This commit is contained in:
Lectom C Han
2026-01-30 13:42:41 +09:00
parent 1cb5115f2a
commit 35552943d7
29 changed files with 1586 additions and 472 deletions

View File

@@ -2,21 +2,24 @@ package handler
import (
"baron-sso-backend/internal/domain"
"errors"
"baron-sso-backend/internal/service"
"baron-sso-backend/internal/utils"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type UserHandler struct {
DB *gorm.DB
KratosAdmin *service.KratosAdminService
OryProvider *service.OryProvider
}
func NewUserHandler(db *gorm.DB) *UserHandler {
return &UserHandler{DB: db}
func NewUserHandler(kratosAdmin *service.KratosAdminService, oryProvider *service.OryProvider) *UserHandler {
return &UserHandler{
KratosAdmin: kratosAdmin,
OryProvider: oryProvider,
}
}
type userSummary struct {
@@ -30,6 +33,7 @@ type userSummary struct {
Department string `json:"department"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
InitialPassword string `json:"initialPassword,omitempty"`
}
type userListResponse struct {
@@ -40,8 +44,8 @@ type userListResponse struct {
}
func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
if h.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
if h.KratosAdmin == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity provider not available"})
}
limit := c.QueryInt("limit", 50)
@@ -55,33 +59,45 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
offset = 0
}
query := h.DB.Model(&domain.User{})
if search != "" {
like := "%" + search + "%"
query = query.Where("email ILIKE ? OR name ILIKE ?", like, like)
}
var total int64
if err := query.Count(&total).Error; err != nil {
identities, err := h.KratosAdmin.ListIdentities(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
var users []domain.User
if err := query.Order("created_at desc").Limit(limit).Offset(offset).Find(&users).Error; 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)
}
}
}
items := make([]userSummary, 0, len(users))
for _, u := range users {
items = append(items, mapUserSummary(u))
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.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
if h.KratosAdmin == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity provider not available"})
}
userID := strings.TrimSpace(c.Params("id"))
@@ -89,20 +105,20 @@ func (h *UserHandler) GetUser(c *fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "user id is required"})
}
var user domain.User
if err := h.DB.First(&user, "id = ?", userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
}
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(mapUserSummary(user))
return c.JSON(mapIdentitySummary(*identity))
}
func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
if h.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
if h.OryProvider == nil || h.KratosAdmin == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity provider not available"})
}
var req struct {
@@ -122,52 +138,83 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
if email == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "email is required"})
}
password := req.Password
if password == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "password is required"})
}
name := strings.TrimSpace(req.Name)
if name == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name is required"})
}
// Check duplicates
var exists domain.User
if err := h.DB.Where("email = ?", email).First(&exists).Error; err == nil {
return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "email already exists"})
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,
}
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
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 {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to hash password"})
}
user := domain.User{
Email: email,
PasswordHash: string(hashedPassword),
Name: name,
Phone: req.Phone,
Role: req.Role, // default "user" handled by GORM if empty, but struct default usually works on zero value? GORM default tag works for zero value.
CompanyCode: req.CompanyCode,
Department: req.Department,
Status: "active",
AffiliationType: "internal", // Defaulting for now
}
if user.Role == "" {
user.Role = "user"
}
if err := h.DB.Create(&user).Error; 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()})
}
return c.Status(fiber.StatusCreated).JSON(mapUserSummary(user))
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.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
if h.KratosAdmin == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity provider not available"})
}
userID := strings.TrimSpace(c.Params("id"))
@@ -175,13 +222,13 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "user id is required"})
}
var user domain.User
if err := h.DB.First(&user, "id = ?", userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
}
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"`
@@ -196,46 +243,48 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
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 {
user.Name = strings.TrimSpace(*req.Name)
traits["name"] = strings.TrimSpace(*req.Name)
}
if req.Phone != nil {
user.Phone = strings.TrimSpace(*req.Phone)
}
if req.Role != nil {
user.Role = strings.TrimSpace(*req.Role)
}
if req.Status != nil {
status := strings.ToLower(strings.TrimSpace(*req.Status))
if status == "active" || status == "inactive" || status == "blocked" {
user.Status = status
}
traits["phone_number"] = normalizePhoneNumber(strings.TrimSpace(*req.Phone))
}
if req.CompanyCode != nil {
user.CompanyCode = strings.TrimSpace(*req.CompanyCode)
traits["companyCode"] = strings.TrimSpace(*req.CompanyCode)
}
if req.Department != nil {
user.Department = strings.TrimSpace(*req.Department)
traits["department"] = strings.TrimSpace(*req.Department)
}
if req.Password != nil && *req.Password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to hash password"})
if req.Role != nil {
role := strings.TrimSpace(*req.Role)
if role == "" {
role = "user"
}
user.PasswordHash = string(hashedPassword)
traits["grade"] = role
}
if err := h.DB.Save(&user).Error; err != nil {
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()})
}
return c.JSON(mapUserSummary(user))
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.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
if h.KratosAdmin == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity provider not available"})
}
userID := strings.TrimSpace(c.Params("id"))
@@ -243,25 +292,88 @@ func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "user id is required"})
}
// Soft delete
if err := h.DB.Delete(&domain.User{}, "id = ?", userID).Error; err != nil {
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 mapUserSummary(u domain.User) userSummary {
func mapIdentitySummary(identity service.KratosIdentity) userSummary {
traits := identity.Traits
role := extractTraitString(traits, "grade")
if role == "" {
role = "user"
}
return userSummary{
ID: u.ID,
Email: u.Email,
Name: u.Name,
Phone: u.Phone,
Role: u.Role,
Status: u.Status,
CompanyCode: u.CompanyCode,
Department: u.Department,
CreatedAt: u.CreatedAt.Format(time.RFC3339),
UpdatedAt: u.UpdatedAt.Format(time.RFC3339),
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
}