forked from baron/baron-sso
feat: implement multi-identifier architecture (Issue #496)
- Database: Add user_login_ids table for 1:N identifier mapping and remove legacy login_id column - Kratos: Update identity schema to use custom_login_ids array instead of a single id trait - Backend: Implement syncCustomLoginIDs to collect isLoginId fields across tenant schemas - Backend: Add backtracking logic to auto-assign session tenant based on used login identifier - Backend: Add 409 Conflict exception handling for Create/Update operations - AdminFront: Refactor UserDetailPage to a tabbed grid layout (Info, Tenants, Security) - AdminFront: Show '로그인 ID' badge on tenant schema fields used for authentication - UserFront: Remove legacy optional 'Login ID' input from signup flow - Tests: Add multi-identifier repository tests and update handler tests
This commit is contained in:
@@ -287,6 +287,16 @@ func (h *AuthHandler) CheckLoginID(c *fiber.Ctx) error {
|
||||
return errorJSON(c, fiber.StatusServiceUnavailable, "Identity provider unavailable")
|
||||
}
|
||||
|
||||
if !exists && h.UserRepo != nil {
|
||||
// [New] Check local DB for custom login IDs (Plan A)
|
||||
taken, err := h.UserRepo.IsLoginIDTaken(c.Context(), req.LoginID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to check login ID in local DB", "error", err)
|
||||
} else if taken {
|
||||
exists = true
|
||||
}
|
||||
}
|
||||
|
||||
if exists {
|
||||
return c.JSON(fiber.Map{"available": false, "message": "ID already registered"})
|
||||
}
|
||||
@@ -596,27 +606,20 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
|
||||
"grade": "member",
|
||||
}
|
||||
|
||||
if req.LoginID != "" {
|
||||
attributes["id"] = req.LoginID
|
||||
}
|
||||
// Sync all custom login IDs based on tenant schemas
|
||||
loginIDRecords := syncCustomLoginIDs(c.Context(), h.TenantService, attributes, req.Metadata, "")
|
||||
|
||||
// Sync custom field to LoginID if configured
|
||||
if tenantID != nil && h.TenantService != nil {
|
||||
if tenant, err := h.TenantService.GetTenant(c.Context(), *tenantID); err == nil && tenant != nil {
|
||||
if loginIdField, ok := tenant.Config["loginIdField"].(string); ok && loginIdField != "" {
|
||||
syncLoginID(attributes, req.Metadata, *tenantID, loginIdField)
|
||||
// Validate all collected LoginIDs
|
||||
if collectedIDs, ok := attributes["custom_login_ids"].([]string); ok {
|
||||
for _, lid := range collectedIDs {
|
||||
if err := domain.ValidateLoginID(lid, req.Email, normalizedPhone); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "Invalid LoginID ("+lid+"): "+err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finalLoginID := extractTraitString(attributes, "id")
|
||||
if err := domain.ValidateLoginID(finalLoginID, req.Email, normalizedPhone); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
brokerUser := &domain.BrokerUser{
|
||||
Email: req.Email,
|
||||
LoginID: finalLoginID,
|
||||
Name: req.Name,
|
||||
PhoneNumber: normalizedPhone,
|
||||
Attributes: attributes,
|
||||
@@ -629,7 +632,7 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
|
||||
}
|
||||
slog.Error("[Signup] Failed to create user via IDP", "provider", h.IdpProvider.Name(), "error", err)
|
||||
if strings.Contains(err.Error(), "already exists") {
|
||||
return errorJSON(c, fiber.StatusConflict, "User already exists")
|
||||
return errorJSON(c, fiber.StatusConflict, "User or login identifier already exists")
|
||||
}
|
||||
// Include the actual error message in the response for debugging
|
||||
return errorJSON(c, fiber.StatusInternalServerError, fmt.Sprintf("Failed to create user: %v", err))
|
||||
@@ -644,29 +647,47 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
|
||||
// [SoT Policy] Kratos가 SoT이므로 로컬 DB 저장은 비동기 Read-Model 동기화로 처리합니다.
|
||||
// 로컬 DB 저장이 실패하더라도 회원가입 프로세스는 성공으로 간주합니다.
|
||||
localUser := &domain.User{
|
||||
ID: providerID, // Match IDP Subject
|
||||
ID: providerID,
|
||||
Email: req.Email,
|
||||
Name: req.Name,
|
||||
Phone: normalizedPhone,
|
||||
Role: "user",
|
||||
AffiliationType: req.AffiliationType,
|
||||
CompanyCode: companyCode,
|
||||
TenantID: tenantID,
|
||||
Department: req.Department,
|
||||
Role: "user",
|
||||
Status: "active",
|
||||
Metadata: req.Metadata,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if tenantID != nil {
|
||||
localUser.TenantID = tenantID
|
||||
}
|
||||
|
||||
// Merge metadata
|
||||
localUser.Metadata = make(domain.JSONMap)
|
||||
for k, v := range req.Metadata {
|
||||
localUser.Metadata[k] = v
|
||||
}
|
||||
|
||||
if h.UserRepo != nil {
|
||||
go func(u *domain.User) {
|
||||
// 요청 Context가 취소될 수 있으므로 Background Context 사용
|
||||
go func(u *domain.User, ids []domain.UserLoginID) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := h.UserRepo.Create(ctx, u); err != nil {
|
||||
if err := h.UserRepo.Update(ctx, u); err != nil {
|
||||
slog.Error("[Signup] Failed to sync user to Read-Model (Local DB)", "email", u.Email, "error", err)
|
||||
} else {
|
||||
slog.Debug("[Signup] Synced user to Read-Model", "email", u.Email)
|
||||
|
||||
// Update User Login IDs
|
||||
for i := range ids {
|
||||
ids[i].UserID = u.ID
|
||||
}
|
||||
if err := h.UserRepo.UpdateUserLoginIDs(ctx, u.ID, ids); err != nil {
|
||||
slog.Error("[Signup] Failed to update user login IDs", "userID", u.ID, "error", err)
|
||||
}
|
||||
|
||||
// [Keto] Sync user-tenant relationship via Outbox
|
||||
if h.KetoOutboxRepo != nil && u.TenantID != nil {
|
||||
_ = h.KetoOutboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
@@ -678,7 +699,7 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
}
|
||||
}(localUser)
|
||||
}(localUser, loginIDRecords)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
@@ -3182,7 +3203,7 @@ func (h *AuthHandler) ScanQRLogin(c *fiber.Ctx) error {
|
||||
if cookie == "" {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "Missing session token")
|
||||
}
|
||||
_, traits, err := h.getKratosIdentityWithCookie(cookie)
|
||||
_, traits, _, err := h.getKratosIdentityWithCookie(cookie)
|
||||
if err != nil {
|
||||
slog.Warn("[QR] Cookie session invalid", "error", err)
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "Invalid session")
|
||||
@@ -4252,7 +4273,7 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
|
||||
if strings.Contains(path, "/api/v1/auth/oidc/login/accept") {
|
||||
appName = "OIDC 로그인"
|
||||
// 우선 audit details의 client 정보를 사용하고, 없으면 Hydra 조회로 보강
|
||||
if details, err := parseAuditDetails(log.Details); err == nil && details != nil {
|
||||
if details, err := utils.ParseAuditDetails(log.Details); err == nil && details != nil {
|
||||
if name, ok := details["client_name"].(string); ok && strings.TrimSpace(name) != "" {
|
||||
appName = strings.TrimSpace(name)
|
||||
}
|
||||
@@ -5089,6 +5110,15 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe
|
||||
profile.Role = domain.RoleUser
|
||||
}
|
||||
|
||||
// [New] Backtracking Logic for Session Tenant (Plan A)
|
||||
if usedID, ok := profile.Metadata["_used_identifier"].(string); ok && usedID != "" && h.UserRepo != nil {
|
||||
if tid, err := h.UserRepo.FindTenantIDByLoginID(c.Context(), usedID); err == nil && tid != "" {
|
||||
profile.SessionTenantID = &tid
|
||||
slog.Debug("Auto-assigned session tenant via backtracking", "loginID", usedID, "tenantID", tid)
|
||||
}
|
||||
delete(profile.Metadata, "_used_identifier") // Cleanup
|
||||
}
|
||||
|
||||
// Fetch Tenant Metadata if missing
|
||||
if profile.Tenant == nil && profile.TenantID != nil && *profile.TenantID != "" {
|
||||
if tenant, err := h.TenantService.GetTenant(c.Context(), *profile.TenantID); err == nil {
|
||||
@@ -5140,7 +5170,7 @@ func (h *AuthHandler) resolveConsentSubject(c *fiber.Ctx) (string, error) {
|
||||
return identityID, nil
|
||||
}
|
||||
if cookie := c.Get("Cookie"); cookie != "" {
|
||||
cookieID, _, cookieErr := h.getKratosIdentityWithCookie(cookie)
|
||||
cookieID, _, _, cookieErr := h.getKratosIdentityWithCookie(cookie)
|
||||
if cookieErr == nil && cookieID != "" {
|
||||
return cookieID, nil
|
||||
}
|
||||
@@ -5151,14 +5181,14 @@ func (h *AuthHandler) resolveConsentSubject(c *fiber.Ctx) (string, error) {
|
||||
if cookie == "" {
|
||||
return "", fmt.Errorf("missing authorization token")
|
||||
}
|
||||
identityID, _, err := h.getKratosIdentityWithCookie(cookie)
|
||||
identityID, _, _, err := h.getKratosIdentityWithCookie(cookie)
|
||||
return identityID, err
|
||||
}
|
||||
|
||||
func (h *AuthHandler) resolveConsentSubjects(c *fiber.Ctx) ([]string, error) {
|
||||
token := h.getBearerToken(c)
|
||||
if token != "" {
|
||||
identityID, traits, err := h.getKratosIdentity(token)
|
||||
identityID, traits, _, err := h.getKratosIdentity(token)
|
||||
if err == nil && identityID != "" {
|
||||
subjects := []string{identityID}
|
||||
subjects = appendLoginIDsFromTraits(subjects, traits)
|
||||
@@ -5170,7 +5200,7 @@ func (h *AuthHandler) resolveConsentSubjects(c *fiber.Ctx) ([]string, error) {
|
||||
if cookie == "" {
|
||||
return nil, fmt.Errorf("missing authorization token")
|
||||
}
|
||||
identityID, traits, err := h.getKratosIdentityWithCookie(cookie)
|
||||
identityID, traits, _, err := h.getKratosIdentityWithCookie(cookie)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -5250,7 +5280,7 @@ func isAuthEventType(eventType string) bool {
|
||||
|
||||
func extractAuditPath(log domain.AuditLog) string {
|
||||
if log.Details != "" {
|
||||
if payload, err := parseAuditDetails(log.Details); err == nil {
|
||||
if payload, err := utils.ParseAuditDetails(log.Details); err == nil {
|
||||
if path, ok := payload["path"].(string); ok && path != "" {
|
||||
return path
|
||||
}
|
||||
@@ -5263,17 +5293,6 @@ func extractAuditPath(log domain.AuditLog) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func parseAuditDetails(details string) (map[string]any, error) {
|
||||
var payload map[string]any
|
||||
if details == "" {
|
||||
return nil, fmt.Errorf("empty details")
|
||||
}
|
||||
if err := json.Unmarshal([]byte(details), &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func extractRequestBody(details map[string]any) map[string]any {
|
||||
if details == nil {
|
||||
return nil
|
||||
@@ -5290,7 +5309,7 @@ func extractRequestBody(details map[string]any) map[string]any {
|
||||
}
|
||||
|
||||
func shouldSkipAuthTimeline(log domain.AuditLog) bool {
|
||||
details, _ := parseAuditDetails(log.Details)
|
||||
details, _ := utils.ParseAuditDetails(log.Details)
|
||||
path := strings.ToLower(extractAuditPath(log))
|
||||
if path != "" && strings.Contains(path, "/api/v1/auth/enchanted-link/init") {
|
||||
return true
|
||||
@@ -5384,7 +5403,7 @@ func deriveAuthMethod(log domain.AuditLog) string {
|
||||
|
||||
loginID := extractLoginIDFromAuditDetails(log.Details)
|
||||
kind := loginIDKind(loginID)
|
||||
details, _ := parseAuditDetails(log.Details)
|
||||
details, _ := utils.ParseAuditDetails(log.Details)
|
||||
requestBody := extractRequestBody(details)
|
||||
if details != nil {
|
||||
if raw, ok := details["auth_timeline_skip"]; ok {
|
||||
@@ -5603,7 +5622,7 @@ func extractLoginChallengeFromAuditDetails(details string) string {
|
||||
if details == "" {
|
||||
return ""
|
||||
}
|
||||
payload, err := parseAuditDetails(details)
|
||||
payload, err := utils.ParseAuditDetails(details)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
@@ -5750,12 +5769,12 @@ func extractApprovedSessionIDFromAuditDetails(details string) string {
|
||||
}
|
||||
|
||||
func (h *AuthHandler) resolveIdentityID(c *fiber.Ctx, token string) (string, error) {
|
||||
id, _, err := h.getKratosIdentity(token)
|
||||
id, _, _, err := h.getKratosIdentity(token)
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (h *AuthHandler) resolveKratosLoginID(token string) (string, error) {
|
||||
_, traits, err := h.getKratosIdentity(token)
|
||||
_, traits, _, err := h.getKratosIdentity(token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -5943,44 +5962,56 @@ func extractLoginIDFromClaims(claims map[string]any) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (h *AuthHandler) getKratosIdentity(sessionToken string) (string, map[string]interface{}, error) {
|
||||
identityID, traits, _, err := h.getKratosIdentityWithSession(sessionToken)
|
||||
return identityID, traits, err
|
||||
func (h *AuthHandler) getKratosIdentity(sessionToken string) (string, map[string]interface{}, string, error) {
|
||||
identityID, traits, _, usedID, err := h.getKratosIdentityWithSession(sessionToken)
|
||||
return identityID, traits, usedID, err
|
||||
}
|
||||
|
||||
func (h *AuthHandler) getKratosIdentityWithSession(sessionToken string) (string, map[string]interface{}, string, error) {
|
||||
func (h *AuthHandler) getKratosIdentityWithSession(sessionToken string) (string, map[string]interface{}, string, string, error) {
|
||||
kratosURL := strings.TrimRight(os.Getenv("KRATOS_PUBLIC_URL"), "/")
|
||||
if kratosURL == "" {
|
||||
kratosURL = "http://kratos:4433"
|
||||
}
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, kratosURL+"/sessions/whoami", nil)
|
||||
if err != nil {
|
||||
return "", nil, "", err
|
||||
return "", nil, "", "", err
|
||||
}
|
||||
req.Header.Set("X-Session-Token", sessionToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", nil, "", err
|
||||
return "", nil, "", "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 300 {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
||||
return "", nil, "", fmt.Errorf("kratos whoami failed status=%d body=%s", resp.StatusCode, string(body))
|
||||
return "", nil, "", "", fmt.Errorf("kratos whoami failed status=%d body=%s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var result struct {
|
||||
AuthenticatedAt string `json:"authenticated_at"`
|
||||
Identity struct {
|
||||
AuthenticatedAt string `json:"authenticated_at"`
|
||||
AuthenticationMethods []struct {
|
||||
Method string `json:"method"`
|
||||
Identifier string `json:"identifier"`
|
||||
} `json:"authentication_methods"`
|
||||
Identity struct {
|
||||
ID string `json:"id"`
|
||||
Traits map[string]interface{} `json:"traits"`
|
||||
} `json:"identity"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", nil, "", err
|
||||
return "", nil, "", "", err
|
||||
}
|
||||
|
||||
return result.Identity.ID, result.Identity.Traits, result.AuthenticatedAt, nil
|
||||
usedIdentifier := ""
|
||||
for _, m := range result.AuthenticationMethods {
|
||||
if m.Identifier != "" {
|
||||
usedIdentifier = m.Identifier
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result.Identity.ID, result.Identity.Traits, result.AuthenticatedAt, usedIdentifier, nil
|
||||
}
|
||||
|
||||
func (h *AuthHandler) getKratosSessionID(sessionToken string) (string, error) {
|
||||
@@ -6056,44 +6087,56 @@ func (h *AuthHandler) issueKratosSession(ctx context.Context, identityID string)
|
||||
return parsed.SessionToken, nil
|
||||
}
|
||||
|
||||
func (h *AuthHandler) getKratosIdentityWithCookie(cookie string) (string, map[string]interface{}, error) {
|
||||
identityID, traits, _, err := h.getKratosIdentityWithCookieAndSession(cookie)
|
||||
return identityID, traits, err
|
||||
func (h *AuthHandler) getKratosIdentityWithCookie(cookie string) (string, map[string]interface{}, string, error) {
|
||||
identityID, traits, _, usedID, err := h.getKratosIdentityWithCookieAndSession(cookie)
|
||||
return identityID, traits, usedID, err
|
||||
}
|
||||
|
||||
func (h *AuthHandler) getKratosIdentityWithCookieAndSession(cookie string) (string, map[string]interface{}, string, error) {
|
||||
func (h *AuthHandler) getKratosIdentityWithCookieAndSession(cookie string) (string, map[string]interface{}, string, string, error) {
|
||||
kratosURL := strings.TrimRight(os.Getenv("KRATOS_PUBLIC_URL"), "/")
|
||||
if kratosURL == "" {
|
||||
kratosURL = "http://kratos:4433"
|
||||
}
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, kratosURL+"/sessions/whoami", nil)
|
||||
if err != nil {
|
||||
return "", nil, "", err
|
||||
return "", nil, "", "", err
|
||||
}
|
||||
req.Header.Set("Cookie", cookie)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", nil, "", err
|
||||
return "", nil, "", "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 300 {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
||||
return "", nil, "", fmt.Errorf("kratos whoami failed status=%d body=%s", resp.StatusCode, string(body))
|
||||
return "", nil, "", "", fmt.Errorf("kratos whoami failed status=%d body=%s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var result struct {
|
||||
AuthenticatedAt string `json:"authenticated_at"`
|
||||
Identity struct {
|
||||
AuthenticatedAt string `json:"authenticated_at"`
|
||||
AuthenticationMethods []struct {
|
||||
Method string `json:"method"`
|
||||
Identifier string `json:"identifier"`
|
||||
} `json:"authentication_methods"`
|
||||
Identity struct {
|
||||
ID string `json:"id"`
|
||||
Traits map[string]interface{} `json:"traits"`
|
||||
} `json:"identity"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", nil, "", err
|
||||
return "", nil, "", "", err
|
||||
}
|
||||
|
||||
return result.Identity.ID, result.Identity.Traits, result.AuthenticatedAt, nil
|
||||
usedIdentifier := ""
|
||||
for _, m := range result.AuthenticationMethods {
|
||||
if m.Identifier != "" {
|
||||
usedIdentifier = m.Identifier
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result.Identity.ID, result.Identity.Traits, result.AuthenticatedAt, usedIdentifier, nil
|
||||
}
|
||||
|
||||
func (h *AuthHandler) getKratosSessionIDWithCookie(cookie string) (string, error) {
|
||||
@@ -6228,33 +6271,41 @@ func (h *AuthHandler) mapKratosIdentityToProfile(identityID string, traits map[s
|
||||
return profile
|
||||
}
|
||||
|
||||
func (h *AuthHandler) applySessionAuthenticatedAtFromWhoami(profile *domain.UserProfileResponse, authenticatedAt string) *domain.UserProfileResponse {
|
||||
func (h *AuthHandler) applySessionInfoFromWhoami(profile *domain.UserProfileResponse, authenticatedAt, usedIdentifier string) *domain.UserProfileResponse {
|
||||
if profile == nil {
|
||||
return nil
|
||||
}
|
||||
profile.SessionAuthenticatedAt = strings.TrimSpace(authenticatedAt)
|
||||
if usedIdentifier != "" {
|
||||
if profile.Metadata == nil {
|
||||
profile.Metadata = make(map[string]any)
|
||||
}
|
||||
profile.Metadata["_used_identifier"] = usedIdentifier
|
||||
}
|
||||
return profile
|
||||
}
|
||||
|
||||
func (h *AuthHandler) getKratosProfile(sessionToken string) (*domain.UserProfileResponse, error) {
|
||||
identityID, traits, authenticatedAt, err := h.getKratosIdentityWithSession(sessionToken)
|
||||
identityID, traits, authenticatedAt, usedIdentifier, err := h.getKratosIdentityWithSession(sessionToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.applySessionAuthenticatedAtFromWhoami(
|
||||
return h.applySessionInfoFromWhoami(
|
||||
h.mapKratosIdentityToProfile(identityID, traits),
|
||||
authenticatedAt,
|
||||
usedIdentifier,
|
||||
), nil
|
||||
}
|
||||
|
||||
func (h *AuthHandler) getKratosProfileWithCookie(cookie string) (*domain.UserProfileResponse, error) {
|
||||
identityID, traits, authenticatedAt, err := h.getKratosIdentityWithCookieAndSession(cookie)
|
||||
identityID, traits, authenticatedAt, usedIdentifier, err := h.getKratosIdentityWithCookieAndSession(cookie)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.applySessionAuthenticatedAtFromWhoami(
|
||||
return h.applySessionInfoFromWhoami(
|
||||
h.mapKratosIdentityToProfile(identityID, traits),
|
||||
authenticatedAt,
|
||||
usedIdentifier,
|
||||
), nil
|
||||
}
|
||||
|
||||
@@ -6272,13 +6323,13 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error {
|
||||
err error
|
||||
)
|
||||
if token != "" {
|
||||
identityID, traits, err = h.getKratosIdentity(token)
|
||||
identityID, traits, _, err = h.getKratosIdentity(token)
|
||||
} else {
|
||||
cookie := c.Get("Cookie")
|
||||
if cookie == "" {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "Missing authorization token")
|
||||
}
|
||||
identityID, traits, err = h.getKratosIdentityWithCookie(cookie)
|
||||
identityID, traits, _, err = h.getKratosIdentityWithCookie(cookie)
|
||||
}
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "Invalid session")
|
||||
@@ -6331,27 +6382,35 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error {
|
||||
|
||||
// [LoginID Sync based on Tenant Settings]
|
||||
// Perform sync AFTER metadata merge to ensure traits contains current values
|
||||
syncCompCode := extractTraitString(traits, "companyCode")
|
||||
if syncCompCode != "" && h.TenantService != nil {
|
||||
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), syncCompCode); err == nil && tenant != nil {
|
||||
if loginIdField, ok := tenant.Config["loginIdField"].(string); ok && loginIdField != "" {
|
||||
syncLoginID(traits, req.Metadata, tenant.ID, loginIdField)
|
||||
loginIDRecords := syncCustomLoginIDs(c.Context(), h.TenantService, traits, req.Metadata, identityID)
|
||||
|
||||
// Validate all collected LoginIDs
|
||||
userEmail := extractTraitString(traits, "email")
|
||||
userPhone := extractTraitString(traits, "phone_number")
|
||||
if collectedIDs, ok := traits["custom_login_ids"].([]string); ok {
|
||||
for _, lid := range collectedIDs {
|
||||
if err := domain.ValidateLoginID(lid, userEmail, userPhone); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "Invalid LoginID ("+lid+"): "+err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finalLoginID := extractTraitString(traits, "id")
|
||||
userEmail := extractTraitString(traits, "email")
|
||||
userPhone := extractTraitString(traits, "phone")
|
||||
if err := domain.ValidateLoginID(finalLoginID, userEmail, userPhone); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
if err := h.updateKratosIdentity(identityID, traits); err != nil {
|
||||
slog.Error("Failed to update profile in Kratos", "error", err)
|
||||
return errorJSON(c, fiber.StatusInternalServerError, "프로필 업데이트에 실패했습니다.")
|
||||
}
|
||||
|
||||
// [New] Local DB Sync - Sync synchronously to ensure immediate consistency
|
||||
if h.UserRepo != nil {
|
||||
ctx := context.Background()
|
||||
// Also update local User record (read-model)
|
||||
// We can fetch updated identity or just map current traits
|
||||
// Since mapKratosIdentityToProfile is for UI, let's just use UpdateUserLoginIDs first
|
||||
if err := h.UserRepo.UpdateUserLoginIDs(ctx, identityID, loginIDRecords); err != nil {
|
||||
slog.Error("[UpdateMe] Failed to update user login IDs", "userID", identityID, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidate token-based profile cache so refreshed /user/me returns latest traits.
|
||||
if h.RedisService != nil && token != "" {
|
||||
cacheKey := "cache:profile:token:" + token
|
||||
@@ -6396,7 +6455,7 @@ func (h *AuthHandler) ChangeMyPassword(c *fiber.Ctx) error {
|
||||
if cookie == "" {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "Missing authorization token")
|
||||
}
|
||||
_, traits, err := h.getKratosIdentityWithCookie(cookie)
|
||||
_, traits, _, err := h.getKratosIdentityWithCookie(cookie)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "Invalid session")
|
||||
}
|
||||
@@ -6434,7 +6493,7 @@ func (h *AuthHandler) SendUpdateCode(c *fiber.Ctx) error {
|
||||
if cookie == "" {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
userID, _, err = h.getKratosIdentityWithCookie(cookie)
|
||||
userID, _, _, err = h.getKratosIdentityWithCookie(cookie)
|
||||
}
|
||||
if err != nil || userID == "" {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "Invalid session")
|
||||
@@ -6475,7 +6534,7 @@ func (h *AuthHandler) VerifyUpdateCode(c *fiber.Ctx) error {
|
||||
if cookie == "" {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "Invalid session")
|
||||
}
|
||||
userID, _, err = h.getKratosIdentityWithCookie(cookie)
|
||||
userID, _, _, err = h.getKratosIdentityWithCookie(cookie)
|
||||
}
|
||||
if err != nil || userID == "" {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "Invalid session")
|
||||
@@ -6625,7 +6684,7 @@ func (h *AuthHandler) ListRpHistory(c *fiber.Ctx) error {
|
||||
// Logs are DESC (newest first). Iterate in reverse (oldest first) to build state.
|
||||
for i := len(logs) - 1; i >= 0; i-- {
|
||||
log := logs[i]
|
||||
details, _ := parseAuditDetails(log.Details)
|
||||
details, _ := utils.ParseAuditDetails(log.Details)
|
||||
clientID, _ := details["client_id"].(string)
|
||||
if clientID == "" {
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user