1
0
forked from baron/baron-sso

userfront 연동이력 맞춤

This commit is contained in:
Lectom C Han
2026-02-03 13:37:24 +09:00
parent e20b61189c
commit 4f3d0759c3
24 changed files with 4092 additions and 175 deletions

View File

@@ -521,6 +521,228 @@ func normalizePhoneForLoginID(phone string) string {
return normalized
}
func buildOidcClaimsFromTraits(traits map[string]any, scopes []string) map[string]any {
claims := map[string]any{}
if traits == nil {
return claims
}
scopeSet := map[string]struct{}{}
for _, scope := range scopes {
scope = strings.TrimSpace(scope)
if scope == "" {
continue
}
scopeSet[scope] = struct{}{}
}
getString := func(key string) string {
raw, ok := traits[key]
if !ok || raw == nil {
return ""
}
switch value := raw.(type) {
case string:
return strings.TrimSpace(value)
default:
return strings.TrimSpace(fmt.Sprint(value))
}
}
displayName := getString("displayname")
if displayName == "" {
displayName = getString("name")
}
if displayName != "" {
claims["name"] = displayName
}
primaryEmail := getString("primary_email")
if primaryEmail == "" {
primaryEmail = getString("email")
}
if primaryEmail != "" {
claims["email"] = primaryEmail
}
if _, ok := scopeSet["profile"]; ok {
profile := map[string]any{}
names := map[string]any{}
for _, key := range []string{
"name",
"displayname",
"preferred_username",
"given_name",
"family_name",
"middle_name",
"nickname",
} {
if value := getString(key); value != "" {
names[key] = value
}
}
if len(names) > 0 {
profile["names"] = names
}
emails := collectEmailList(traits, primaryEmail)
if len(emails) > 0 {
profile["emails"] = emails
}
if len(profile) > 0 {
claims["profile"] = profile
}
for _, key := range []string{
"department",
"affiliationType",
"companyCode",
"displayname",
"team",
"grade",
"familyCompany",
"taxCode",
"familyUniqueKey",
"personal",
} {
if raw, ok := traits[key]; ok && raw != nil {
switch value := raw.(type) {
case string:
if strings.TrimSpace(value) != "" {
claims[key] = strings.TrimSpace(value)
}
default:
claims[key] = value
}
}
}
}
if _, ok := scopeSet["phone"]; ok {
if phone := getString("phone_number"); phone != "" {
claims["phone_number"] = phone
}
}
return claims
}
func collectEmailList(traits map[string]any, primaryEmail string) []string {
emails := make([]string, 0)
seen := make(map[string]struct{})
add := func(value string) {
value = strings.TrimSpace(value)
if value == "" {
return
}
if _, ok := seen[value]; ok {
return
}
seen[value] = struct{}{}
emails = append(emails, value)
}
add(primaryEmail)
for _, key := range []string{"email", "primary_email"} {
if raw, ok := traits[key]; ok {
if value, ok := raw.(string); ok {
add(value)
}
}
}
if raw, ok := traits["emails"]; ok {
switch value := raw.(type) {
case []string:
for _, email := range value {
add(email)
}
case []any:
for _, email := range value {
add(fmt.Sprint(email))
}
}
}
if raw, ok := traits["secondary_emails"]; ok {
switch value := raw.(type) {
case []string:
for _, email := range value {
add(email)
}
case []any:
for _, email := range value {
add(fmt.Sprint(email))
}
}
}
if raw, ok := traits["additional_emails"]; ok {
switch value := raw.(type) {
case []string:
for _, email := range value {
add(email)
}
case []any:
for _, email := range value {
add(fmt.Sprint(email))
}
}
}
return emails
}
func buildIdentityLookupCandidates(loginID string) []string {
seen := make(map[string]struct{})
add := func(value string) {
candidate := strings.TrimSpace(value)
if candidate == "" {
return
}
if _, ok := seen[candidate]; ok {
return
}
seen[candidate] = struct{}{}
}
normalized := strings.TrimSpace(loginID)
add(normalized)
if normalized != "" {
add(strings.ToLower(normalized))
}
if normalized != "" && !strings.Contains(normalized, "@") {
add(normalizePhoneForLoginID(normalized))
}
candidates := make([]string, 0, len(seen))
for candidate := range seen {
candidates = append(candidates, candidate)
}
return candidates
}
func (h *AuthHandler) resolveKratosIdentityID(ctx context.Context, identifiers ...string) (string, error) {
if h.KratosAdmin == nil {
return "", fmt.Errorf("kratos admin unavailable")
}
for _, identifier := range identifiers {
candidate := strings.TrimSpace(identifier)
if candidate == "" {
continue
}
identityID, err := h.KratosAdmin.FindIdentityIDByIdentifier(ctx, candidate)
if err == nil && identityID != "" {
return identityID, nil
}
}
return "", fmt.Errorf("kratos identity not found")
}
func (h *AuthHandler) resolveKratosIdentityIDFromLoginID(ctx context.Context, loginID string) (string, error) {
candidates := buildIdentityLookupCandidates(loginID)
return h.resolveKratosIdentityID(ctx, candidates...)
}
func (h *AuthHandler) getSignupState(key string) (*signupState, error) {
val, err := h.RedisService.Get(key)
if err != nil || val == "" {
@@ -1117,6 +1339,12 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error {
if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"})
}
subject, resolveErr := h.resolveKratosIdentityIDFromLoginID(c.Context(), lookupLoginID)
if resolveErr != nil || subject == "" {
slog.Error("[LoginCode] Failed to resolve kratos identity", "loginID", lookupLoginID, "error", resolveErr)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to resolve user identity"})
}
authInfo.Subject = subject
c.Locals("login_id", lookupLoginID)
setSessionIDLocal(c, authInfo.SessionToken)
@@ -1140,7 +1368,7 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error {
"status": "approved",
"pendingRef": pendingRef,
"provider": h.IdpProvider.Name(),
"subject": authInfo.Subject,
"subject": subject,
"message": "Login approved",
})
}
@@ -1149,7 +1377,7 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error {
"token": authInfo.SessionToken.JWT,
"sessionJwt": authInfo.SessionToken.JWT,
"provider": h.IdpProvider.Name(),
"subject": authInfo.Subject,
"subject": subject,
"message": "Login successful",
})
}
@@ -1226,6 +1454,12 @@ func (h *AuthHandler) VerifyLoginShortCode(c *fiber.Ctx) error {
if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"})
}
subject, resolveErr := h.resolveKratosIdentityIDFromLoginID(c.Context(), payload.LoginID)
if resolveErr != nil || subject == "" {
slog.Error("[LoginShortCode] Failed to resolve kratos identity", "loginID", payload.LoginID, "error", resolveErr)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to resolve user identity"})
}
authInfo.Subject = subject
c.Locals("login_id", payload.LoginID)
setSessionIDLocal(c, authInfo.SessionToken)
@@ -1247,7 +1481,7 @@ func (h *AuthHandler) VerifyLoginShortCode(c *fiber.Ctx) error {
"token": authInfo.SessionToken.JWT,
"sessionJwt": authInfo.SessionToken.JWT,
"provider": h.IdpProvider.Name(),
"subject": authInfo.Subject,
"subject": subject,
"message": "Login approved",
})
}
@@ -1256,7 +1490,7 @@ func (h *AuthHandler) VerifyLoginShortCode(c *fiber.Ctx) error {
"token": authInfo.SessionToken.JWT,
"sessionJwt": authInfo.SessionToken.JWT,
"provider": h.IdpProvider.Name(),
"subject": authInfo.Subject,
"subject": subject,
"message": "Login successful",
})
}
@@ -1311,6 +1545,13 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid credentials"})
}
subject, resolveErr := h.resolveKratosIdentityIDFromLoginID(c.Context(), loginID)
if resolveErr != nil || subject == "" {
slog.Error("Failed to resolve kratos identity after login", "loginID", loginID, "error", resolveErr)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve user identity")
}
authInfo.Subject = subject
ale.Status = fiber.StatusOK
ale.LatencyMs = time.Since(startTime)
ale.SessionJwt = authInfo.SessionToken.JWT
@@ -1320,7 +1561,7 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
// --- OIDC 로그인 흐름 처리 ---
if req.LoginChallenge != "" {
slog.Info("OIDC login flow detected", "challenge", req.LoginChallenge)
acceptResp, err := h.Hydra.AcceptLoginRequest(c.Context(), req.LoginChallenge, authInfo.Subject)
acceptResp, err := h.Hydra.AcceptLoginRequest(c.Context(), req.LoginChallenge, subject)
if err != nil {
slog.Error("failed to accept hydra login request", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to accept OIDC login request")
@@ -2326,12 +2567,21 @@ func (h *AuthHandler) GetMe(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to load user profile"})
}
identityID, resolveErr := h.resolveKratosIdentityID(
c.Context(),
userResponse.Email,
normalizePhoneForLoginID(userResponse.Phone),
)
if resolveErr != nil || identityID == "" {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to resolve user identity"})
}
dept, _ := userResponse.CustomAttributes["department"].(string)
affType, _ := userResponse.CustomAttributes["affiliationType"].(string)
compCode, _ := userResponse.CustomAttributes["companyCode"].(string)
resp := domain.UserProfileResponse{
ID: userResponse.UserID,
ID: identityID,
Email: userResponse.Email,
Name: userResponse.Name,
Phone: h.formatPhoneForDisplay(userResponse.Phone),
@@ -3033,24 +3283,43 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "hydra admin unavailable"})
}
subject, err := h.resolveConsentSubject(c)
if err != nil || subject == "" {
subjects, err := h.resolveConsentSubjects(c)
if err != nil || len(subjects) == 0 {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
}
sessions, err := h.Hydra.ListConsentSessions(c.Context(), subject, "")
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
var sessions []service.HydraConsentSession
var lastErr error
hasSuccess := false
for _, subject := range subjects {
subject = strings.TrimSpace(subject)
if subject == "" {
continue
}
linked, listErr := h.Hydra.ListConsentSessions(c.Context(), subject, "")
if listErr != nil {
lastErr = listErr
continue
}
hasSuccess = true
sessions = append(sessions, linked...)
}
if !hasSuccess && lastErr != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": lastErr.Error()})
}
records := make(map[string]*linkedRpRecord)
for _, session := range sessions {
clientID := strings.TrimSpace(session.Client.ClientID)
client := session.Client
if client.ClientID == "" && session.ConsentRequest != nil {
client = session.ConsentRequest.Client
}
clientID := strings.TrimSpace(client.ClientID)
if clientID == "" {
continue
}
name := strings.TrimSpace(session.Client.ClientName)
name := strings.TrimSpace(client.ClientName)
if name == "" {
name = clientID
}
@@ -3060,11 +3329,13 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
lastAuth = *session.AuthenticatedAt
} else if session.RequestedAt != nil {
lastAuth = *session.RequestedAt
} else if session.HandledAt != nil {
lastAuth = *session.HandledAt
}
scopes := session.GrantedScope
if len(scopes) == 0 && strings.TrimSpace(session.Client.Scope) != "" {
scopes = strings.Fields(session.Client.Scope)
if len(scopes) == 0 && strings.TrimSpace(client.Scope) != "" {
scopes = strings.Fields(client.Scope)
}
existing := records[clientID]
@@ -3073,8 +3344,8 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
linkedRpSummary: linkedRpSummary{
ID: clientID,
Name: name,
Logo: extractHydraClientLogo(session.Client.Metadata),
Status: hydraClientStatus(session.Client.Metadata),
Logo: extractHydraClientLogo(client.Metadata),
Status: hydraClientStatus(client.Metadata),
Scopes: scopes,
},
lastAuth: lastAuth,
@@ -3086,7 +3357,7 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
existing.Name = name
}
if existing.Logo == "" {
existing.Logo = extractHydraClientLogo(session.Client.Metadata)
existing.Logo = extractHydraClientLogo(client.Metadata)
}
existing.Scopes = mergeScopes(existing.Scopes, scopes)
if lastAuth.After(existing.lastAuth) {
@@ -3145,7 +3416,19 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get consent information")
}
acceptResp, err := h.Hydra.AcceptConsentRequest(c.Context(), req.ConsentChallenge, consentRequest)
if consentRequest.Subject == "" {
return fiber.NewError(fiber.StatusInternalServerError, "Consent subject missing")
}
if h.KratosAdmin == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Kratos admin unavailable")
}
identity, err := h.KratosAdmin.GetIdentity(c.Context(), consentRequest.Subject)
if err != nil || identity == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to load identity")
}
sessionClaims := buildOidcClaimsFromTraits(identity.Traits, consentRequest.RequestedScope)
acceptResp, err := h.Hydra.AcceptConsentRequest(c.Context(), req.ConsentChallenge, consentRequest, sessionClaims)
if err != nil {
slog.Error("failed to accept hydra consent request", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to accept consent request")
@@ -3154,6 +3437,30 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
return c.JSON(acceptResp)
}
func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error {
var req struct {
LoginChallenge string `json:"login_challenge"`
}
if err := c.BodyParser(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
if req.LoginChallenge == "" {
return fiber.NewError(fiber.StatusBadRequest, "login_challenge is required")
}
subject, err := h.resolveConsentSubject(c)
if err != nil || subject == "" {
return fiber.NewError(fiber.StatusUnauthorized, "Authentication required")
}
acceptResp, err := h.Hydra.AcceptLoginRequest(c.Context(), req.LoginChallenge, subject)
if err != nil {
slog.Error("failed to accept hydra login request", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to accept OIDC login request")
}
return c.JSON(acceptResp)
}
func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) {
token := h.getBearerToken(c)
@@ -3165,11 +3472,19 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe
if err != nil {
return nil, err
}
identityID, resolveErr := h.resolveKratosIdentityID(
c.Context(),
userResponse.Email,
normalizePhoneForLoginID(userResponse.Phone),
)
if resolveErr != nil || identityID == "" {
return nil, fmt.Errorf("failed to resolve kratos identity for profile")
}
dept, _ := userResponse.CustomAttributes["department"].(string)
affType, _ := userResponse.CustomAttributes["affiliationType"].(string)
compCode, _ := userResponse.CustomAttributes["companyCode"].(string)
return &domain.UserProfileResponse{
ID: userResponse.UserID,
ID: identityID,
Email: userResponse.Email,
Name: userResponse.Name,
Phone: h.formatPhoneForDisplay(userResponse.Phone),
@@ -3197,27 +3512,34 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe
func (h *AuthHandler) resolveConsentSubject(c *fiber.Ctx) (string, error) {
token := h.getBearerToken(c)
if token != "" {
if looksLikeJWT(token) && h.DescopeClient != nil && h.KratosAdmin != nil {
if looksLikeJWT(token) && h.DescopeClient != nil {
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
if err == nil && authorized {
userResponse, loadErr := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID)
if loadErr == nil {
if email := strings.TrimSpace(userResponse.Email); email != "" {
if identityID, err := h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), email); err == nil && identityID != "" {
return identityID, nil
}
}
if phone := strings.TrimSpace(userResponse.Phone); phone != "" {
normalized := normalizePhoneForLoginID(phone)
if identityID, err := h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), normalized); err == nil && identityID != "" {
return identityID, nil
}
identityID, resolveErr := h.resolveKratosIdentityID(
c.Context(),
userResponse.Email,
normalizePhoneForLoginID(userResponse.Phone),
)
if resolveErr == nil {
return identityID, nil
}
}
return userToken.ID, nil
return "", fmt.Errorf("failed to resolve kratos identity for consent subject")
}
}
return h.resolveIdentityID(c, token)
identityID, resolveErr := h.resolveIdentityID(c, token)
if resolveErr == nil && identityID != "" {
return identityID, nil
}
if cookie := c.Get("Cookie"); cookie != "" {
cookieID, _, cookieErr := h.getKratosIdentityWithCookie(cookie)
if cookieErr == nil && cookieID != "" {
return cookieID, nil
}
}
return "", resolveErr
}
cookie := c.Get("Cookie")
if cookie == "" {
@@ -3227,6 +3549,114 @@ func (h *AuthHandler) resolveConsentSubject(c *fiber.Ctx) (string, error) {
return identityID, err
}
func (h *AuthHandler) resolveConsentSubjects(c *fiber.Ctx) ([]string, error) {
token := h.getBearerToken(c)
if token != "" && looksLikeJWT(token) && h.DescopeClient != nil {
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
if err == nil && authorized {
subjects := make([]string, 0, 2)
userResponse, loadErr := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID)
if loadErr == nil {
subjects = appendLoginIDsFromValues(subjects, userResponse.Email, userResponse.Phone)
identityID, resolveErr := h.resolveKratosIdentityID(
c.Context(),
userResponse.Email,
normalizePhoneForLoginID(userResponse.Phone),
)
if resolveErr == nil && identityID != "" {
subjects = append([]string{identityID}, subjects...)
}
}
return uniqueStrings(subjects), nil
}
}
if token != "" {
identityID, traits, err := h.getKratosIdentity(token)
if err == nil && identityID != "" {
subjects := []string{identityID}
subjects = appendLoginIDsFromTraits(subjects, traits)
return uniqueStrings(subjects), nil
}
}
cookie := c.Get("Cookie")
if cookie == "" {
return nil, fmt.Errorf("missing authorization token")
}
identityID, traits, err := h.getKratosIdentityWithCookie(cookie)
if err != nil {
return nil, err
}
subjects := []string{identityID}
subjects = appendLoginIDsFromTraits(subjects, traits)
return uniqueStrings(subjects), nil
}
func uniqueStrings(items []string) []string {
seen := make(map[string]struct{}, len(items))
result := make([]string, 0, len(items))
for _, item := range items {
item = strings.TrimSpace(item)
if item == "" {
continue
}
if _, ok := seen[item]; ok {
continue
}
seen[item] = struct{}{}
result = append(result, item)
}
return result
}
func appendLoginIDsFromValues(subjects []string, email string, phone string) []string {
if strings.TrimSpace(email) != "" {
subjects = append(subjects, strings.TrimSpace(email))
}
if strings.TrimSpace(phone) != "" {
subjects = append(subjects, normalizePhoneForLoginID(phone))
}
return subjects
}
func appendLoginIDsFromTraits(subjects []string, traits map[string]interface{}) []string {
if traits == nil {
return subjects
}
if raw, ok := traits["email"]; ok {
if value, ok := raw.(string); ok && strings.TrimSpace(value) != "" {
subjects = append(subjects, strings.TrimSpace(value))
}
}
if raw, ok := traits["phone"]; ok {
if value, ok := raw.(string); ok && strings.TrimSpace(value) != "" {
subjects = append(subjects, normalizePhoneForLoginID(value))
}
}
if raw, ok := traits["phone_number"]; ok {
if value, ok := raw.(string); ok && strings.TrimSpace(value) != "" {
subjects = append(subjects, normalizePhoneForLoginID(value))
}
}
if raw, ok := traits["phoneNumber"]; ok {
if value, ok := raw.(string); ok && strings.TrimSpace(value) != "" {
subjects = append(subjects, normalizePhoneForLoginID(value))
}
}
if raw, ok := traits["mobile"]; ok {
if value, ok := raw.(string); ok && strings.TrimSpace(value) != "" {
subjects = append(subjects, normalizePhoneForLoginID(value))
}
}
if raw, ok := traits["mobile_number"]; ok {
if value, ok := raw.(string); ok && strings.TrimSpace(value) != "" {
subjects = append(subjects, normalizePhoneForLoginID(value))
}
}
return subjects
}
func isAuthEventType(eventType string) bool {
normalized := strings.ToLower(eventType)
return strings.Contains(normalized, " /api/v1/auth/")
@@ -3662,7 +4092,22 @@ func (h *AuthHandler) resolveIdentityID(c *fiber.Ctx, token string) (string, err
if looksLikeJWT(token) && h.DescopeClient != nil {
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
if err == nil && authorized {
return userToken.ID, nil
if h.KratosAdmin == nil {
return "", fmt.Errorf("kratos admin unavailable")
}
userResponse, loadErr := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID)
if loadErr != nil {
return "", loadErr
}
identityID, resolveErr := h.resolveKratosIdentityID(
c.Context(),
userResponse.Email,
normalizePhoneForLoginID(userResponse.Phone),
)
if resolveErr != nil || identityID == "" {
return "", fmt.Errorf("failed to resolve kratos identity for token")
}
return identityID, nil
}
}
id, _, err := h.getKratosIdentity(token)
@@ -3688,7 +4133,7 @@ func (h *AuthHandler) tryIssueDescopeQrSession(c *fiber.Ctx, token string) (*dom
if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" {
return nil, "", "", fmt.Errorf("descope issue session returned empty token")
}
return authInfo.SessionToken, loginID, userToken.ID, nil
return authInfo.SessionToken, loginID, "", nil
}
func (h *AuthHandler) resolveKratosLoginID(token string) (string, error) {
@@ -4204,16 +4649,24 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error {
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to load current user"})
}
identityID, resolveErr := h.resolveKratosIdentityID(
c.Context(),
currentUser.Email,
normalizePhoneForLoginID(currentUser.Phone),
)
if resolveErr != nil || identityID == "" {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to resolve user identity"})
}
newPhoneStorage := h.formatPhoneForStorage(req.Phone)
oldPhoneStorage := currentUser.Phone
slog.Info("[UpdateMe] Checking changes", "userID", userToken.ID, "oldPhone", oldPhoneStorage, "newPhone", newPhoneStorage, "newName", req.Name)
slog.Info("[UpdateMe] Checking changes", "userID", identityID, "oldPhone", oldPhoneStorage, "newPhone", newPhoneStorage, "newName", req.Name)
// 2. Handle Phone Number Change
if newPhoneStorage != "" && newPhoneStorage != oldPhoneStorage {
// Check verification status in Redis
verifyKey := "verify_update_phone:" + userToken.ID + ":" + newPhoneStorage
verifyKey := "verify_update_phone:" + identityID + ":" + newPhoneStorage
val, _ := h.RedisService.Get(verifyKey)
if val != "verified" {
slog.Warn("[UpdateMe] Phone verification missing", "key", verifyKey)
@@ -4221,7 +4674,7 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error {
}
// Update Phone in Descope and mark as verified
slog.Info("[UpdateMe] Updating phone number", "userID", userToken.ID, "newPhone", newPhoneStorage)
slog.Info("[UpdateMe] Updating phone number", "userID", identityID, "newPhone", newPhoneStorage)
_, err = h.DescopeClient.Management.User().UpdatePhone(c.Context(), userToken.ID, newPhoneStorage, true, false)
if err != nil {
slog.Error("Failed to update phone in Descope", "error", err)
@@ -4250,7 +4703,7 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error {
// 3. Update Name if changed
if req.Name != "" && req.Name != currentUser.Name {
slog.Info("[UpdateMe] Updating display name", "userID", userToken.ID, "newName", req.Name)
slog.Info("[UpdateMe] Updating display name", "userID", identityID, "newName", req.Name)
_, err = h.DescopeClient.Management.User().UpdateDisplayName(c.Context(), userToken.ID, req.Name)
if err != nil {
slog.Error("Failed to update user name", "error", err)
@@ -4260,13 +4713,13 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error {
// 4. Update Custom Attributes (Department)
if req.Department != "" {
slog.Info("[UpdateMe] Updating department", "userID", userToken.ID, "dept", req.Department)
slog.Info("[UpdateMe] Updating department", "userID", identityID, "dept", req.Department)
if _, err := h.DescopeClient.Management.User().UpdateCustomAttribute(c.Context(), userToken.ID, "department", req.Department); err != nil {
slog.Error("Failed to update department", "error", err)
}
}
slog.Info("[UpdateMe] Profile update completed successfully", "userID", userToken.ID)
slog.Info("[UpdateMe] Profile update completed successfully", "userID", identityID)
return c.JSON(fiber.Map{
"status": "success",