diff --git a/openapi/oauth/providers/user/team.go b/openapi/oauth/providers/user/team.go index 7b91ed49..400b6e5e 100644 --- a/openapi/oauth/providers/user/team.go +++ b/openapi/oauth/providers/user/team.go @@ -291,7 +291,7 @@ func (u *DefaultUser) GetTeamsByMember(ctx context.Context, memberID string) ([] teams, err := u.GetTeams(ctx, model.QueryParam{ Select: u.teamFields, Wheres: []model.QueryWhere{ - {Column: "team_id", Value: teamIDs, Method: "in"}, + {Column: "team_id", Value: teamIDs, OP: "in"}, }, }) if err != nil { diff --git a/openapi/oauth/signing.go b/openapi/oauth/signing.go index e422da2d..00509934 100644 --- a/openapi/oauth/signing.go +++ b/openapi/oauth/signing.go @@ -567,6 +567,9 @@ func (s *Service) SignIDToken(clientID, scope string, expiresIn int, userdata *t } // Add Yao custom fields with namespace + if userdata.YaoUserID != "" { + claims["yao:user_id"] = userdata.YaoUserID + } if userdata.YaoTenantID != "" { claims["yao:tenant_id"] = userdata.YaoTenantID } diff --git a/openapi/oauth/types/oidc.go b/openapi/oauth/types/oidc.go index 64f5c448..9b8d6563 100644 --- a/openapi/oauth/types/oidc.go +++ b/openapi/oauth/types/oidc.go @@ -265,6 +265,9 @@ func MakeOIDCUserInfo(user map[string]interface{}) *OIDCUserInfo { } // Yao custom fields with namespace + if userID, ok := user["yao:user_id"].(string); ok { + userInfo.YaoUserID = userID + } if tenantID, ok := user["yao:tenant_id"].(string); ok { userInfo.YaoTenantID = tenantID } diff --git a/openapi/oauth/types/types.go b/openapi/oauth/types/types.go index 91c48c8b..b8ffc626 100644 --- a/openapi/oauth/types/types.go +++ b/openapi/oauth/types/types.go @@ -690,6 +690,7 @@ type OIDCUserInfo struct { Address *OIDCAddress `json:"address,omitempty"` // Physical mailing address // Additional custom claims with namespace + YaoUserID string `json:"yao:user_id,omitempty"` // Yao user ID (original user ID) YaoTenantID string `json:"yao:tenant_id,omitempty"` // Yao tenant ID YaoTeamID string `json:"yao:team_id,omitempty"` // Yao team ID YaoTeam *OIDCTeamInfo `json:"yao:team,omitempty"` // Yao team info diff --git a/openapi/user/login.go b/openapi/user/login.go index 434541ab..6339151d 100644 --- a/openapi/user/login.go +++ b/openapi/user/login.go @@ -299,6 +299,7 @@ func issueTokens(ctx context.Context, userid string, teamID string, team map[str // Prepare OIDC user info oidcUserInfo := oauthtypes.MakeOIDCUserInfo(user) oidcUserInfo.Sub = subject + oidcUserInfo.YaoUserID = userid // Add original user ID // Prepare extra claims for access token extraClaims := make(map[string]interface{}) diff --git a/openapi/user/profile.go b/openapi/user/profile.go new file mode 100644 index 00000000..d5317f57 --- /dev/null +++ b/openapi/user/profile.go @@ -0,0 +1,152 @@ +package user + +import ( + "context" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/yaoapp/kun/log" + "github.com/yaoapp/yao/openapi/oauth" + oauthtypes "github.com/yaoapp/yao/openapi/oauth/types" + "github.com/yaoapp/yao/openapi/response" +) + +// User Profile Management Handlers + +// GinProfileGet handles GET /profile - Get current user profile +func GinProfileGet(c *gin.Context) { + // Get authorized user info + authInfo := oauth.GetAuthorizedInfo(c) + if authInfo == nil || authInfo.UserID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidClient.Code, + ErrorDescription: "User not authenticated", + } + response.RespondWithError(c, response.StatusUnauthorized, errorResp) + return + } + + // Get user provider + userProvider, err := oauth.OAuth.GetUserProvider() + if err != nil { + log.Error("Failed to get user provider: %v", err) + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Failed to get user provider", + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Get user data with scopes + ctx := c.Request.Context() + if ctx == nil { + ctx = context.Background() + } + + user, err := userProvider.GetUserWithScopes(ctx, authInfo.UserID) + if err != nil { + log.Error("Failed to get user profile: %v", err) + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Failed to retrieve user profile", + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Get Yao client config + yaoClientConfig := GetYaoClientConfig() + + // Get or create subject + subject, err := oauth.OAuth.Subject(yaoClientConfig.ClientID, authInfo.UserID) + if err != nil { + log.Warn("Failed to get user subject: %s", err.Error()) + subject = authInfo.UserID // Fallback to user ID + } + + // Prepare OIDC user info (same format as login response) + oidcUserInfo := oauthtypes.MakeOIDCUserInfo(user) + oidcUserInfo.Sub = subject + oidcUserInfo.YaoUserID = authInfo.UserID + + // Add team context if available from token + if authInfo.TeamID != "" { + // Get team details + team, err := userProvider.GetTeamByMember(ctx, authInfo.TeamID, authInfo.UserID) + if err == nil && team != nil { + // Add team info to OIDC user info + oidcUserInfo.YaoTeamID = authInfo.TeamID + + teamInfo := &oauthtypes.OIDCTeamInfo{} + if teamIDVal := toString(team["team_id"]); teamIDVal != "" { + teamInfo.TeamID = teamIDVal + } + if logo := toString(team["logo"]); logo != "" { + teamInfo.Logo = logo + } + if name := toString(team["name"]); name != "" { + teamInfo.Name = name + } + if description := toString(team["description"]); description != "" { + teamInfo.Description = description + } + if ownerID := toString(team["owner_id"]); ownerID != "" { + teamInfo.OwnerID = ownerID + + // Check if user is owner + if ownerID == authInfo.UserID { + isOwner := true + oidcUserInfo.YaoIsOwner = &isOwner + } + } + oidcUserInfo.YaoTeam = teamInfo + + // Add tenant_id if available from the team + if tenantID := toString(team["tenant_id"]); tenantID != "" { + oidcUserInfo.YaoTenantID = tenantID + } + } + } + + // Add type information + var typeID string + if authInfo.TeamID != "" { + // Team context - try to get team's type first + team, err := userProvider.GetTeamByMember(ctx, authInfo.TeamID, authInfo.UserID) + if err == nil && team != nil { + typeID = toString(team["type_id"]) + } + } + + // Fallback to user's type if no team type + if typeID == "" { + typeID = toString(user["type_id"]) + } + + if typeID != "" { + oidcUserInfo.YaoTypeID = typeID + + // Get type details + typeInfo, err := userProvider.GetType(ctx, typeID) + if err == nil && typeInfo != nil { + typeDetails := &oauthtypes.OIDCTypeInfo{} + if typeIDVal := toString(typeInfo["type_id"]); typeIDVal != "" { + typeDetails.TypeID = typeIDVal + } + if name := toString(typeInfo["name"]); name != "" { + typeDetails.Name = name + } + if locale := toString(typeInfo["locale"]); locale != "" { + typeDetails.Locale = locale + } + oidcUserInfo.YaoType = typeDetails + } + } + + // Convert to map for response + profileData := oidcUserInfo.Map() + + // Return user profile + response.RespondWithSuccess(c, http.StatusOK, profileData) +} diff --git a/openapi/user/team_invitation.go b/openapi/user/team_invitation.go index 0a91a61c..6447ebd0 100644 --- a/openapi/user/team_invitation.go +++ b/openapi/user/team_invitation.go @@ -817,6 +817,7 @@ func teamInvitationGetPublic(ctx context.Context, invitationID, locale string) ( inviter, err := provider.GetUser(ctx, inviterID) if err == nil { inviterInfo = &InviterInfo{ + UserID: inviterID, Name: toString(inviter["name"]), Picture: toString(inviter["picture"]), } diff --git a/openapi/user/types.go b/openapi/user/types.go index 16918ce2..7fada0b3 100644 --- a/openapi/user/types.go +++ b/openapi/user/types.go @@ -392,6 +392,7 @@ type PublicInvitationResponse struct { // InviterInfo represents public information about the person who sent the invitation type InviterInfo struct { + UserID string `json:"user_id"` // Inviter's user ID Name string `json:"name,omitempty"` Picture string `json:"picture"` // Always return, empty string if not set } diff --git a/openapi/user/user.go b/openapi/user/user.go index 89db6c6b..7a07a5f5 100644 --- a/openapi/user/user.go +++ b/openapi/user/user.go @@ -189,8 +189,8 @@ func attachProfile(group *gin.RouterGroup, oauth types.OAuth) { profile := group.Group("/profile") profile.Use(oauth.Guard) - profile.GET("/", placeholder) // Get user profile - profile.PUT("/", placeholder) // Update user profile + profile.GET("/", GinProfileGet) // Get user profile + profile.PUT("/", placeholder) // Update user profile } // User management (CRUD)