forked from baron/baron-sso
feat: implement CSV bulk user upload functionality
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user