1
0
forked from baron/baron-sso

feat: implement CSV bulk user upload functionality

This commit is contained in:
2026-03-04 11:26:37 +09:00
parent db88c7ab1c
commit 7c28bd4867
5 changed files with 437 additions and 0 deletions

View File

@@ -363,6 +363,153 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
return c.Status(fiber.StatusCreated).JSON(response)
}
type bulkUserItem struct {
Email string `json:"email"`
Name string `json:"name"`
Phone string `json:"phone"`
Role string `json:"role"`
CompanyCode string `json:"companyCode"`
Department string `json:"department"`
Metadata map[string]any `json:"metadata"`
}
type bulkUserResult struct {
Email string `json:"email"`
Success bool `json:"success"`
Message string `json:"message,omitempty"`
UserID string `json:"userId,omitempty"`
}
func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
if h.OryProvider == nil || h.KratosAdmin == nil {
return errorJSON(c, fiber.StatusServiceUnavailable, "identity provider not available")
}
var req struct {
Users []bulkUserItem `json:"users"`
}
if err := c.BodyParser(&req); err != nil {
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
}
if len(req.Users) == 0 {
return errorJSON(c, fiber.StatusBadRequest, "no users provided")
}
policy, err := h.OryProvider.GetPasswordPolicy()
if err != nil || policy == nil {
policy = &domain.PasswordPolicy{
MinLength: 12, Number: true, NonAlphanumeric: true,
}
}
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
results := make([]bulkUserResult, 0, len(req.Users))
// Pre-fetch tenant schemas to avoid redundant DB calls
tenantSchemas := make(map[string][]interface{})
for _, item := range req.Users {
email := strings.TrimSpace(item.Email)
if email == "" {
results = append(results, bulkUserResult{Email: "unknown", Success: false, Message: "email is required"})
continue
}
// Role-based access check
if requester != nil && requester.Role == domain.RoleTenantAdmin {
if item.CompanyCode != requester.CompanyCode {
results = append(results, bulkUserResult{Email: email, Success: false, Message: "forbidden: cannot add users to another tenant"})
continue
}
}
// Resolve Schema
var schema []interface{}
if item.CompanyCode != "" {
if s, ok := tenantSchemas[item.CompanyCode]; ok {
schema = s
} else if h.TenantService != nil {
tenant, err := h.TenantService.GetTenantBySlug(c.Context(), item.CompanyCode)
if err == nil && tenant != nil {
if s, ok := tenant.Config["userSchema"].([]interface{}); ok {
tenantSchemas[item.CompanyCode] = s
schema = s
}
}
}
}
// Validation
if schema != nil {
if err := h.validateMetadata(item.Metadata, schema, true); err != nil {
results = append(results, bulkUserResult{Email: email, Success: false, Message: "validation failed: " + err.Error()})
continue
}
}
password, _ := utils.GeneratePasswordWithPolicy(policy)
role := item.Role
if role == "" {
role = "user"
}
attributes := map[string]interface{}{
"department": item.Department,
"affiliationType": "internal",
"companyCode": item.CompanyCode,
"grade": role,
"role": role,
}
// Resolve TenantID
var tenantID string
if item.CompanyCode != "" && h.TenantService != nil {
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), item.CompanyCode); err == nil && tenant != nil {
tenantID = tenant.ID
attributes["tenant_id"] = tenantID
}
}
// Merge metadata
for k, v := range item.Metadata {
if _, exists := attributes[k]; !exists {
attributes[k] = v
}
}
identityID, err := h.OryProvider.CreateUser(&domain.BrokerUser{
Email: email,
Name: item.Name,
PhoneNumber: normalizePhoneNumber(item.Phone),
Attributes: attributes,
}, password)
if err != nil {
results = append(results, bulkUserResult{Email: email, Success: false, Message: err.Error()})
continue
}
// Sync to local DB
if h.UserRepo != nil {
identity, _ := h.KratosAdmin.GetIdentity(c.Context(), identityID)
if identity != nil {
localUser := h.mapToLocalUser(*identity)
_ = h.UserRepo.Update(context.Background(), localUser)
if h.KetoOutboxRepo != nil {
h.syncKetoRole(context.Background(), localUser.ID, role, "", "", localUser.TenantID)
}
}
}
results = append(results, bulkUserResult{Email: email, Success: true, UserID: identityID})
}
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"results": results,
})
}
func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
if h.KratosAdmin == nil {
return errorJSON(c, fiber.StatusServiceUnavailable, "identity provider not available")