forked from baron/baron-sso
worksmobile 연동 & ory stack 26.2.0으로 업그레이드
This commit is contained in:
@@ -36,6 +36,7 @@ type UserHandler struct {
|
||||
UserRepo repository.UserRepository
|
||||
UserGroupRepo repository.UserGroupRepository
|
||||
AuditRepo domain.AuditRepository
|
||||
Worksmobile service.WorksmobileSyncer
|
||||
}
|
||||
|
||||
func NewUserHandler(kratosAdmin service.KratosAdminService, oryProvider OryProviderAPI, tenantService service.TenantService, ketoService service.KetoService, ketoOutboxRepo repository.KetoOutboxRepository, userRepo repository.UserRepository, userGroupRepo repository.UserGroupRepository, auditRepo domain.AuditRepository) *UserHandler {
|
||||
@@ -51,6 +52,97 @@ func NewUserHandler(kratosAdmin service.KratosAdminService, oryProvider OryProvi
|
||||
}
|
||||
}
|
||||
|
||||
func (h *UserHandler) SetWorksmobileSyncer(syncer service.WorksmobileSyncer) {
|
||||
h.Worksmobile = syncer
|
||||
}
|
||||
|
||||
func mergeUserAppointmentMetadata(metadata map[string]any, appointments []map[string]any, primaryTenantID string, primaryTenantName string, primaryTenantIsOwner *bool) map[string]any {
|
||||
if metadata == nil {
|
||||
metadata = map[string]any{}
|
||||
}
|
||||
if len(appointments) > 0 {
|
||||
values := make([]any, 0, len(appointments))
|
||||
for _, appointment := range appointments {
|
||||
values = append(values, appointment)
|
||||
}
|
||||
metadata["additionalAppointments"] = values
|
||||
}
|
||||
if strings.TrimSpace(primaryTenantID) != "" {
|
||||
metadata["primaryTenantId"] = strings.TrimSpace(primaryTenantID)
|
||||
}
|
||||
if strings.TrimSpace(primaryTenantName) != "" {
|
||||
metadata["primaryTenantName"] = strings.TrimSpace(primaryTenantName)
|
||||
}
|
||||
if primaryTenantIsOwner != nil {
|
||||
metadata["primaryTenantIsOwner"] = *primaryTenantIsOwner
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
func primaryTenantIDFromRequest(primaryTenantID string, metadata map[string]any, appointments []map[string]any) string {
|
||||
if value := strings.TrimSpace(primaryTenantID); value != "" {
|
||||
return value
|
||||
}
|
||||
if value := normalizeMetadataString(metadata["primaryTenantId"]); value != "" {
|
||||
return value
|
||||
}
|
||||
for _, appointment := range appointments {
|
||||
if isPrimary, ok := metadataBoolFromMap(appointment, "isPrimary", "primary"); ok && isPrimary {
|
||||
if value := normalizeMetadataString(appointment["tenantId"]); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(appointments) > 0 {
|
||||
return normalizeMetadataString(appointments[0]["tenantId"])
|
||||
}
|
||||
if raw, ok := metadata["additionalAppointments"].([]any); ok {
|
||||
for _, item := range raw {
|
||||
appointment, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if isPrimary, ok := metadataBoolFromMap(appointment, "isPrimary", "primary"); ok && isPrimary {
|
||||
if value := normalizeMetadataString(appointment["tenantId"]); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(raw) > 0 {
|
||||
if appointment, ok := raw[0].(map[string]any); ok {
|
||||
return normalizeMetadataString(appointment["tenantId"])
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func metadataBoolFromMap(metadata map[string]any, keys ...string) (bool, bool) {
|
||||
for _, key := range keys {
|
||||
value, ok := metadata[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch v := value.(type) {
|
||||
case bool:
|
||||
return v, true
|
||||
case string:
|
||||
normalized := strings.ToLower(strings.TrimSpace(v))
|
||||
if normalized == "true" || normalized == "1" || normalized == "yes" {
|
||||
return true, true
|
||||
}
|
||||
if normalized == "false" || normalized == "0" || normalized == "no" {
|
||||
return false, true
|
||||
}
|
||||
case float64:
|
||||
return v != 0, true
|
||||
case int:
|
||||
return v != 0, true
|
||||
}
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
type userSummary struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
@@ -331,21 +423,26 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Email string `json:"email"`
|
||||
LoginID string `json:"loginId"`
|
||||
Password string `json:"password"`
|
||||
Name string `json:"name"`
|
||||
Phone string `json:"phone"`
|
||||
Role string `json:"role"`
|
||||
CompanyCode string `json:"companyCode"`
|
||||
Department string `json:"department"`
|
||||
Position string `json:"position"`
|
||||
JobTitle string `json:"jobTitle"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
Email string `json:"email"`
|
||||
LoginID string `json:"loginId"`
|
||||
Password string `json:"password"`
|
||||
Name string `json:"name"`
|
||||
Phone string `json:"phone"`
|
||||
Role string `json:"role"`
|
||||
CompanyCode string `json:"companyCode"`
|
||||
Department string `json:"department"`
|
||||
Position string `json:"position"`
|
||||
JobTitle string `json:"jobTitle"`
|
||||
PrimaryTenantID string `json:"primaryTenantId"`
|
||||
PrimaryTenantName string `json:"primaryTenantName"`
|
||||
PrimaryTenantIsOwner *bool `json:"primaryTenantIsOwner"`
|
||||
AdditionalAppointments []map[string]any `json:"additionalAppointments"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
|
||||
}
|
||||
req.Metadata = mergeUserAppointmentMetadata(req.Metadata, req.AdditionalAppointments, req.PrimaryTenantID, req.PrimaryTenantName, req.PrimaryTenantIsOwner)
|
||||
|
||||
email := strings.TrimSpace(req.Email)
|
||||
if email == "" {
|
||||
@@ -411,6 +508,14 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
|
||||
// [Resolve TenantID and Custom Login IDs before Kratos creation]
|
||||
var tenantID string
|
||||
if req.CompanyCode == "" && h.TenantService != nil {
|
||||
if primaryTenantID := primaryTenantIDFromRequest(req.PrimaryTenantID, req.Metadata, req.AdditionalAppointments); primaryTenantID != "" {
|
||||
if tenant, err := h.TenantService.GetTenant(c.Context(), primaryTenantID); err == nil && tenant != nil {
|
||||
tenantID = tenant.ID
|
||||
req.CompanyCode = tenant.Slug
|
||||
}
|
||||
}
|
||||
}
|
||||
if req.CompanyCode != "" && h.TenantService != nil {
|
||||
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), req.CompanyCode); err == nil && tenant != nil {
|
||||
tenantID = tenant.ID
|
||||
@@ -421,6 +526,7 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
loginIDRecords := syncCustomLoginIDs(c.Context(), h.TenantService, attributes, req.Metadata, "")
|
||||
|
||||
attributes["role"] = role
|
||||
attributes["companyCode"] = req.CompanyCode
|
||||
if tenantID != "" {
|
||||
attributes["tenant_id"] = tenantID
|
||||
}
|
||||
@@ -495,6 +601,11 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
if err := h.UserRepo.Update(c.Context(), localUser); err != nil {
|
||||
slog.Error("[UserHandler] Failed to sync new user to local DB", "email", localUser.Email, "error", err)
|
||||
}
|
||||
if h.Worksmobile != nil {
|
||||
if err := h.Worksmobile.EnqueueUserUpsertIfInScope(c.Context(), *localUser); err != nil {
|
||||
slog.Warn("[UserHandler] Failed to enqueue Worksmobile user sync", "userID", localUser.ID, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Update User Login IDs in local DB
|
||||
for i := range loginIDRecords {
|
||||
@@ -796,6 +907,11 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
if err := h.UserRepo.Update(c.Context(), localUser); err != nil {
|
||||
slog.Error("Failed to sync bulk user to local DB", "email", email, "error", err)
|
||||
}
|
||||
if h.Worksmobile != nil {
|
||||
if err := h.Worksmobile.EnqueueUserUpsertIfInScope(c.Context(), *localUser); err != nil {
|
||||
slog.Warn("Failed to enqueue Worksmobile bulk user sync", "userID", localUser.ID, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Update User Login IDs in local DB
|
||||
for i := range loginIDRecords {
|
||||
@@ -1170,6 +1286,11 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
_ = h.UserRepo.Update(c.Context(), localUser)
|
||||
if h.Worksmobile != nil {
|
||||
if err := h.Worksmobile.EnqueueUserUpsertIfInScope(c.Context(), *localUser); err != nil {
|
||||
slog.Warn("Failed to enqueue Worksmobile bulk user update sync", "userID", localUser.ID, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// [Keto Sync]
|
||||
if h.KetoOutboxRepo != nil {
|
||||
@@ -1244,6 +1365,12 @@ func (h *UserHandler) BulkDeleteUsers(c *fiber.Ctx) error {
|
||||
results = append(results, map[string]any{"id": id, "success": false, "message": err.Error()})
|
||||
continue
|
||||
}
|
||||
if h.Worksmobile != nil {
|
||||
localUser := h.mapToLocalUser(*identity)
|
||||
if err := h.Worksmobile.EnqueueUserDeleteIfInScope(c.Context(), *localUser); err != nil {
|
||||
slog.Warn("Failed to enqueue Worksmobile bulk user delete", "userID", id, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Local DB Sync
|
||||
if h.UserRepo != nil {
|
||||
@@ -1298,21 +1425,26 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
var req struct {
|
||||
LoginID *string `json:"loginId"`
|
||||
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"`
|
||||
Position *string `json:"position"`
|
||||
JobTitle *string `json:"jobTitle"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
LoginID *string `json:"loginId"`
|
||||
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"`
|
||||
Position *string `json:"position"`
|
||||
JobTitle *string `json:"jobTitle"`
|
||||
PrimaryTenantID string `json:"primaryTenantId"`
|
||||
PrimaryTenantName string `json:"primaryTenantName"`
|
||||
PrimaryTenantIsOwner *bool `json:"primaryTenantIsOwner"`
|
||||
AdditionalAppointments []map[string]any `json:"additionalAppointments"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
|
||||
}
|
||||
req.Metadata = mergeUserAppointmentMetadata(req.Metadata, req.AdditionalAppointments, req.PrimaryTenantID, req.PrimaryTenantName, req.PrimaryTenantIsOwner)
|
||||
|
||||
// [New] Tenant Admin restriction: Cannot change companyCode
|
||||
if requester != nil && domain.NormalizeRole(requester.Role) == domain.RoleTenantAdmin {
|
||||
@@ -1510,11 +1642,19 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
// [New] Local DB Sync - Sync synchronously to ensure immediate consistency for the caller
|
||||
if h.UserRepo != nil {
|
||||
updatedLocalUser := h.mapToLocalUser(*updated)
|
||||
if req.Status != nil {
|
||||
updatedLocalUser.Status = normalizeStatus(*req.Status)
|
||||
}
|
||||
|
||||
ctx := context.Background() // Use request context if appropriate, but sync must finish
|
||||
if err := h.UserRepo.Update(ctx, updatedLocalUser); err != nil {
|
||||
slog.Error("[UserHandler] Failed to sync updated user to local DB", "userID", updatedLocalUser.ID, "error", err)
|
||||
}
|
||||
if h.Worksmobile != nil {
|
||||
if err := h.Worksmobile.EnqueueUserUpsertIfInScope(ctx, *updatedLocalUser); err != nil {
|
||||
slog.Warn("[UserHandler] Failed to enqueue Worksmobile updated user sync", "userID", updatedLocalUser.ID, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Update User Login IDs in local DB
|
||||
if err := h.UserRepo.UpdateUserLoginIDs(ctx, updatedLocalUser.ID, loginIDRecords); err != nil {
|
||||
@@ -1628,9 +1768,15 @@ func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
|
||||
return errorJSON(c, fiber.StatusForbidden, "cannot delete your own account for safety")
|
||||
}
|
||||
|
||||
var identity *service.KratosIdentity
|
||||
if requester != nil && domain.NormalizeRole(requester.Role) == domain.RoleTenantAdmin || h.Worksmobile != nil {
|
||||
found, err := h.KratosAdmin.GetIdentity(c.Context(), userID)
|
||||
if err == nil {
|
||||
identity = found
|
||||
}
|
||||
}
|
||||
if requester != nil && domain.NormalizeRole(requester.Role) == domain.RoleTenantAdmin {
|
||||
identity, err := h.KratosAdmin.GetIdentity(c.Context(), userID)
|
||||
if err == nil && identity != nil {
|
||||
if identity != nil {
|
||||
compCode := extractTraitString(identity.Traits, "companyCode")
|
||||
if requester.CompanyCode == "" || compCode != requester.CompanyCode {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: cannot delete user in another tenant")
|
||||
@@ -1641,6 +1787,12 @@ func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
|
||||
if err := h.KratosAdmin.DeleteIdentity(c.Context(), userID); err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if h.Worksmobile != nil && identity != nil {
|
||||
localUser := h.mapToLocalUser(*identity)
|
||||
if err := h.Worksmobile.EnqueueUserDeleteIfInScope(c.Context(), *localUser); err != nil {
|
||||
slog.Warn("[UserHandler] Failed to enqueue Worksmobile user delete", "userID", userID, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// [Keto] Cleanup relations via Outbox
|
||||
if h.KetoOutboxRepo != nil {
|
||||
@@ -2041,11 +2193,17 @@ func formatTime(value time.Time) string {
|
||||
|
||||
func normalizeStatus(state string) string {
|
||||
state = strings.ToLower(strings.TrimSpace(state))
|
||||
if state == "inactive" || state == "blocked" || state == "active" {
|
||||
if state == "blocked" {
|
||||
return domain.UserStatusInactive
|
||||
}
|
||||
if state == domain.UserStatusInactive ||
|
||||
state == domain.UserStatusSuspended ||
|
||||
state == domain.UserStatusLeaveOfAbsence ||
|
||||
state == domain.UserStatusActive {
|
||||
return state
|
||||
}
|
||||
if state == "" {
|
||||
return "active"
|
||||
return domain.UserStatusActive
|
||||
}
|
||||
return state
|
||||
}
|
||||
@@ -2056,10 +2214,15 @@ func normalizeKratosState(status *string) string {
|
||||
}
|
||||
value := strings.ToLower(strings.TrimSpace(*status))
|
||||
if value == "blocked" {
|
||||
return "inactive"
|
||||
return domain.UserStatusInactive
|
||||
}
|
||||
if value == "active" || value == "inactive" {
|
||||
return value
|
||||
if value == domain.UserStatusActive {
|
||||
return domain.UserStatusActive
|
||||
}
|
||||
if value == domain.UserStatusInactive ||
|
||||
value == domain.UserStatusSuspended ||
|
||||
value == domain.UserStatusLeaveOfAbsence {
|
||||
return domain.UserStatusInactive
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user