Refactor user registration process to include default team creation
- Introduce a new function `registerUserWithTeam` that handles user registration and default team creation with rollback on failure. - Update `GinEntryRegister` and `LoginThirdParty` functions to utilize the new registration method, ensuring consistency in user and team creation. - Remove redundant error handling for user provider retrieval in `GinEntryRegister`. - Enhance logging for user registration and team creation failures.
This commit is contained in:
parent
fa664b7db7
commit
16a96642f5
2 changed files with 104 additions and 32 deletions
|
|
@ -764,17 +764,6 @@ func GinEntryRegister(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user provider
|
|
||||||
userProvider, err := oauth.OAuth.GetUserProvider()
|
|
||||||
if err != nil {
|
|
||||||
errorResp := &response.ErrorResponse{
|
|
||||||
Code: response.ErrServerError.Code,
|
|
||||||
ErrorDescription: "Failed to get user provider: " + err.Error(),
|
|
||||||
}
|
|
||||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate name if not provided
|
// Generate name if not provided
|
||||||
name := req.Name
|
name := req.Name
|
||||||
if name == "" {
|
if name == "" {
|
||||||
|
|
@ -821,18 +810,17 @@ func GinEntryRegister(c *gin.Context) {
|
||||||
userData["status"] = "active"
|
userData["status"] = "active"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create user
|
// Create user and default team (with rollback on team creation failure)
|
||||||
userID, err := userProvider.CreateUser(ctx, userData)
|
userID, err := registerUserWithTeam(ctx, userData, req.Locale)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Failed to create user: %v", err)
|
log.Error("Failed to register user: %v", err)
|
||||||
errorResp := &response.ErrorResponse{
|
errorResp := &response.ErrorResponse{
|
||||||
Code: response.ErrServerError.Code,
|
Code: response.ErrServerError.Code,
|
||||||
ErrorDescription: "Failed to create user: " + err.Error(),
|
ErrorDescription: err.Error(),
|
||||||
}
|
}
|
||||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("User registered successfully: %s (user_id: %s)", usernameStr, userID)
|
log.Info("User registered successfully: %s (user_id: %s)", usernameStr, userID)
|
||||||
|
|
||||||
// If auto_login is false and invite not required, return success without tokens
|
// If auto_login is false and invite not required, return success without tokens
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yaoapp/gou/session"
|
"github.com/yaoapp/gou/session"
|
||||||
"github.com/yaoapp/kun/log"
|
"github.com/yaoapp/kun/log"
|
||||||
|
"github.com/yaoapp/kun/maps"
|
||||||
"github.com/yaoapp/yao/agent/assistant"
|
"github.com/yaoapp/yao/agent/assistant"
|
||||||
"github.com/yaoapp/yao/kb"
|
"github.com/yaoapp/yao/kb"
|
||||||
kbapi "github.com/yaoapp/yao/kb/api"
|
kbapi "github.com/yaoapp/yao/kb/api"
|
||||||
|
|
@ -25,6 +26,71 @@ import (
|
||||||
// kbCollectionCreating tracks collections currently being created to avoid duplicate creation
|
// kbCollectionCreating tracks collections currently being created to avoid duplicate creation
|
||||||
var kbCollectionCreating sync.Map
|
var kbCollectionCreating sync.Map
|
||||||
|
|
||||||
|
// registerUserWithTeam creates a new user and automatically creates a default team.
|
||||||
|
// If team creation fails, the user is rolled back (deleted) to ensure data consistency.
|
||||||
|
// This is the single entry point for all user registration paths (email/mobile, OAuth third-party, etc.).
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - ctx: context for database operations
|
||||||
|
// - userData: user fields to pass to CreateUser (name, email, status, role_id, type_id, etc.)
|
||||||
|
// - locale: user's locale for determining default team name (e.g. "zh-cn", "en")
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - userID: the created user's ID
|
||||||
|
// - error: non-nil if user creation or team creation failed (user is rolled back on team failure)
|
||||||
|
func registerUserWithTeam(ctx context.Context, userData map[string]interface{}, locale string) (string, error) {
|
||||||
|
userProvider, err := oauth.OAuth.GetUserProvider()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get user provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
userID, err := userProvider.CreateUser(ctx, userData)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-create a default team for the new user
|
||||||
|
// Use "<DisplayName>'s Team" / "<DisplayName>的团队" format
|
||||||
|
// Priority: given_name > name (given_name is more natural as display name)
|
||||||
|
userName := ""
|
||||||
|
if v, ok := userData["given_name"].(string); ok && v != "" {
|
||||||
|
userName = v
|
||||||
|
} else if v, ok := userData["name"].(string); ok && v != "" {
|
||||||
|
userName = v
|
||||||
|
}
|
||||||
|
var defaultTeamName string
|
||||||
|
if strings.HasPrefix(strings.ToLower(locale), "zh") {
|
||||||
|
if userName != "" {
|
||||||
|
defaultTeamName = userName + "的团队"
|
||||||
|
} else {
|
||||||
|
defaultTeamName = "我的团队"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if userName != "" {
|
||||||
|
defaultTeamName = userName + "'s Team"
|
||||||
|
} else {
|
||||||
|
defaultTeamName = "My Team"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
teamData := maps.MapStrAny{
|
||||||
|
"name": defaultTeamName,
|
||||||
|
"locale": locale,
|
||||||
|
}
|
||||||
|
defaultTeamID, err := teamCreate(ctx, userID, teamData)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to create default team for user %s: %v", userID, err)
|
||||||
|
// Rollback: delete the created user since a team is required
|
||||||
|
if delErr := userProvider.DeleteUser(ctx, userID); delErr != nil {
|
||||||
|
log.Error("Failed to rollback user %s after team creation failure: %v", userID, delErr)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("registration failed: unable to initialize team: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("User registered: %s, default team: %s", userID, defaultTeamID)
|
||||||
|
return userID, nil
|
||||||
|
}
|
||||||
|
|
||||||
// getCaptcha is the handler for get captcha image for entry (login/register)
|
// getCaptcha is the handler for get captcha image for entry (login/register)
|
||||||
func getCaptcha(c *gin.Context) {
|
func getCaptcha(c *gin.Context) {
|
||||||
var option captcha.Option = captcha.NewOption()
|
var option captcha.Option = captcha.NewOption()
|
||||||
|
|
@ -70,19 +136,17 @@ func LoginThirdParty(providerID string, userinfo *oauthtypes.OIDCUserInfo, login
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user exists
|
// Auto register user if not exists
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
userProvider, err := oauth.OAuth.GetUserProvider()
|
userProvider, err := oauth.OAuth.GetUserProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto register user if not exists
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
var userID string
|
var userID string
|
||||||
|
|
||||||
// Auto register user if not exists
|
|
||||||
if provider.Register != nil && provider.Register.Auto {
|
if provider.Register != nil && provider.Register.Auto {
|
||||||
userID, err = userProvider.GetOAuthUserID(ctx, providerID, userinfo.Sub)
|
userID, err = userProvider.GetOAuthUserID(ctx, providerID, userinfo.Sub)
|
||||||
if err != nil && err.Error() == user.ErrOAuthAccountNotFound {
|
if err != nil && err.Error() == user.ErrOAuthAccountNotFound {
|
||||||
|
|
@ -103,18 +167,22 @@ func LoginThirdParty(providerID string, userinfo *oauthtypes.OIDCUserInfo, login
|
||||||
"status": status,
|
"status": status,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto register user
|
// Register user with default team (with rollback on failure)
|
||||||
userID, err = userProvider.CreateUser(ctx, userData)
|
userID, err = registerUserWithTeam(ctx, userData, locale)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create OAuth account
|
// Create OAuth account link
|
||||||
userData = userinfo.Map()
|
oauthData := userinfo.Map()
|
||||||
userData["provider"] = providerID
|
oauthData["provider"] = providerID
|
||||||
_, err = userProvider.CreateOAuthAccount(ctx, userID, userData)
|
_, err = userProvider.CreateOAuthAccount(ctx, userID, oauthData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
// Rollback: delete user and team if OAuth account creation fails
|
||||||
|
if delErr := userProvider.DeleteUser(ctx, userID); delErr != nil {
|
||||||
|
log.Error("Failed to rollback user %s after OAuth account creation failure: %v", userID, delErr)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to create OAuth account: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -246,8 +314,23 @@ func LoginByUserID(userid string, loginCtx *LoginContext) (*LoginResponse, error
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If user has teams, return team selection status with temporary access token
|
// If user has exactly one team, auto-select it and skip team selection page
|
||||||
if numTeams > 0 {
|
if numTeams == 1 {
|
||||||
|
teams, err := getUserTeams(ctx, userid)
|
||||||
|
if err == nil && len(teams) == 1 {
|
||||||
|
teamID := ""
|
||||||
|
if v, ok := teams[0]["team_id"].(string); ok {
|
||||||
|
teamID = v
|
||||||
|
}
|
||||||
|
if teamID != "" {
|
||||||
|
return LoginByTeamID(userid, teamID, loginCtx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fall through to team selection if we couldn't auto-select
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user has multiple teams, return team selection status with temporary access token
|
||||||
|
if numTeams > 1 {
|
||||||
// Sign temporary access token for Team Selection
|
// Sign temporary access token for Team Selection
|
||||||
var teamSelectionExpire int = 10 * 60 // 10 minutes
|
var teamSelectionExpire int = 10 * 60 // 10 minutes
|
||||||
|
|
||||||
|
|
@ -328,8 +411,9 @@ func LoginByTeamID(userid string, teamID string, loginCtx *LoginContext) (*Login
|
||||||
log.Warn("Failed to store user fingerprint: %s", err.Error())
|
log.Warn("Failed to store user fingerprint: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle personal account (no team)
|
// Handle personal account (no team) - deprecated, all users should use teams
|
||||||
if teamID == "" || teamID == "personal" {
|
if teamID == "" || teamID == "personal" {
|
||||||
|
log.Warn("Personal account login is deprecated. User %s should select a team.", userid)
|
||||||
resp, err := issueTokens(ctx, &IssueTokensParams{
|
resp, err := issueTokens(ctx, &IssueTokensParams{
|
||||||
UserID: userid,
|
UserID: userid,
|
||||||
TeamID: "",
|
TeamID: "",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue