- Introduce `YaoAuthSource` and `OAuthEmail` fields in various structures to capture authentication source and OAuth email during user login and registration processes. - Update `SignIDToken`, `GetInfo`, and `Map` functions to include new claims for Yao authentication source. - Modify login context to preserve authentication source and OAuth email across different user flows. - Enhance token issuance to include authentication source and OAuth email in claims for better tracking and user context. - Ensure proper handling of OAuth email for third-party logins without affecting user profile email. This change improves the user experience by providing clearer context on authentication methods used during login and registration processes.
1055 lines
30 KiB
Go
1055 lines
30 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/yaoapp/gou/model"
|
|
"github.com/yaoapp/gou/process"
|
|
"github.com/yaoapp/kun/exception"
|
|
"github.com/yaoapp/kun/log"
|
|
"github.com/yaoapp/kun/maps"
|
|
"github.com/yaoapp/yao/openapi/oauth"
|
|
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
|
"github.com/yaoapp/yao/openapi/oauth/providers/user"
|
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
|
"github.com/yaoapp/yao/openapi/response"
|
|
"github.com/yaoapp/yao/openapi/utils"
|
|
)
|
|
|
|
// Team Management Handlers
|
|
|
|
// GinTeamConfig handles GET /teams/config - Get team configuration (public)
|
|
func GinTeamConfig(c *gin.Context) {
|
|
locale := c.Query("locale")
|
|
if locale == "" {
|
|
locale = "en" // default locale
|
|
}
|
|
|
|
// Clean locale: remove whitespace and special characters
|
|
locale = strings.TrimSpace(locale)
|
|
locale = strings.Trim(locale, "?&=")
|
|
|
|
config := GetTeamConfigPublic(locale)
|
|
if config == nil {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Team configuration not found",
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
return
|
|
}
|
|
|
|
response.RespondWithSuccess(c, http.StatusOK, config)
|
|
}
|
|
|
|
// GinTeamList handles GET /teams - Get user teams (all teams where user is a member)
|
|
func GinTeamList(c *gin.Context) {
|
|
// Get authorized user info
|
|
authInfo := authorized.GetInfo(c)
|
|
if authInfo == nil || authInfo.UserID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidClient.Code,
|
|
ErrorDescription: "User not authenticated",
|
|
}
|
|
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
|
return
|
|
}
|
|
|
|
// Call business logic to get user teams with roles
|
|
teams, err := getUserTeams(c.Request.Context(), authInfo.UserID)
|
|
if err != nil {
|
|
log.Error("Failed to get user teams: %v", err)
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to retrieve teams",
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
return
|
|
}
|
|
|
|
// Return teams list directly (no pagination)
|
|
response.RespondWithSuccess(c, http.StatusOK, teams)
|
|
}
|
|
|
|
// GinTeamGet handles GET /teams/:id - Get user team details
|
|
func GinTeamGet(c *gin.Context) {
|
|
// Get authorized user info
|
|
authInfo := authorized.GetInfo(c)
|
|
if authInfo == nil || authInfo.UserID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidClient.Code,
|
|
ErrorDescription: "User not authenticated",
|
|
}
|
|
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
|
return
|
|
}
|
|
|
|
teamID := c.Param("id")
|
|
if teamID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Team ID is required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Get user provider instance
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
log.Error("Failed to get user provider: %v", err)
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to initialize user provider",
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
return
|
|
}
|
|
|
|
// Get team details
|
|
teamData, err := provider.GetTeamDetail(c.Request.Context(), teamID)
|
|
if err != nil {
|
|
log.Error("Failed to get team details: %v", err)
|
|
// Check if it's a "team not found" error
|
|
if err.Error() == "team not found" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Team not found",
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
} else {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to retrieve team details",
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
}
|
|
return
|
|
}
|
|
|
|
// // Check if user owns this team
|
|
// ownerID := utils.ToString(teamData["owner_id"])
|
|
// if ownerID != authInfo.UserID {
|
|
// errorResp := &response.ErrorResponse{
|
|
// Code: response.ErrAccessDenied.Code,
|
|
// ErrorDescription: "Access denied: you don't own this team",
|
|
// }
|
|
// response.RespondWithError(c, response.StatusForbidden, errorResp)
|
|
// return
|
|
// }
|
|
|
|
// Convert to response format
|
|
team := mapToTeamDetailResponse(teamData)
|
|
response.RespondWithSuccess(c, http.StatusOK, team)
|
|
}
|
|
|
|
// GinTeamCreate handles POST /teams - Create user team
|
|
func GinTeamCreate(c *gin.Context) {
|
|
// Get authorized user info
|
|
authInfo := authorized.GetInfo(c)
|
|
if authInfo.Constraints.OwnerOnly {
|
|
if authInfo == nil || authInfo.UserID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidClient.Code,
|
|
ErrorDescription: "User not authenticated",
|
|
}
|
|
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Parse request body
|
|
var req CreateTeamRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Invalid request body: " + err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Prepare team data
|
|
teamData := authInfo.WithCreateScope(maps.MapStrAny{
|
|
"name": req.Name,
|
|
"description": req.Description,
|
|
})
|
|
|
|
// Add logo if provided
|
|
if req.Logo != "" {
|
|
teamData["logo"] = req.Logo
|
|
}
|
|
|
|
// Add settings if provided
|
|
if req.Settings != nil {
|
|
teamData["settings"] = req.Settings
|
|
}
|
|
|
|
// Call business logic
|
|
teamID, err := teamCreate(c.Request.Context(), authInfo.UserID, teamData)
|
|
if err != nil {
|
|
log.Error("Failed to create team: %v", err)
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to create team",
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
return
|
|
}
|
|
|
|
// Get the created team details
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
log.Error("Failed to get user provider: %v", err)
|
|
// Return basic response if we can't get details
|
|
response.RespondWithSuccess(c, http.StatusCreated, gin.H{"team_id": teamID})
|
|
return
|
|
}
|
|
|
|
createdTeam, err := provider.GetTeamDetail(c.Request.Context(), teamID)
|
|
if err != nil {
|
|
log.Error("Failed to get created team details: %v", err)
|
|
// Return basic response if we can't get details
|
|
response.RespondWithSuccess(c, http.StatusCreated, gin.H{"team_id": teamID})
|
|
return
|
|
}
|
|
|
|
// Convert to response format
|
|
team := mapToTeamDetailResponse(createdTeam)
|
|
response.RespondWithSuccess(c, http.StatusCreated, team)
|
|
}
|
|
|
|
// GinTeamUpdate handles PUT /teams/:id - Update user team
|
|
func GinTeamUpdate(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
|
|
}
|
|
|
|
teamID := c.Param("id")
|
|
if teamID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Team ID is required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Parse request body
|
|
var req UpdateTeamRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Invalid request body: " + err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Prepare update data
|
|
updateData := maps.MapStrAny{}
|
|
|
|
if req.Name != "" {
|
|
updateData["name"] = req.Name
|
|
}
|
|
if req.Description != "" {
|
|
updateData["description"] = req.Description
|
|
}
|
|
if req.Logo != "" {
|
|
updateData["logo"] = req.Logo
|
|
}
|
|
if req.Settings != nil {
|
|
updateData["settings"] = req.Settings
|
|
}
|
|
|
|
// Call business logic
|
|
err := teamUpdate(c.Request.Context(), authInfo.UserID, teamID, updateData)
|
|
if err != nil {
|
|
log.Error("Failed to update team: %v", err)
|
|
// Check error type for appropriate response
|
|
if strings.Contains(err.Error(), "not found") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Team not found",
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
} else if strings.Contains(err.Error(), "access denied") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrAccessDenied.Code,
|
|
ErrorDescription: err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
|
} else {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to update team",
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Get updated team details
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
log.Error("Failed to get user provider: %v", err)
|
|
response.RespondWithSuccess(c, http.StatusOK, gin.H{"message": "Team updated successfully"})
|
|
return
|
|
}
|
|
|
|
updatedTeam, err := provider.GetTeamDetail(c.Request.Context(), teamID)
|
|
if err != nil {
|
|
log.Error("Failed to get updated team details: %v", err)
|
|
response.RespondWithSuccess(c, http.StatusOK, gin.H{"message": "Team updated successfully"})
|
|
return
|
|
}
|
|
|
|
// Convert to response format
|
|
team := mapToTeamDetailResponse(updatedTeam)
|
|
response.RespondWithSuccess(c, http.StatusOK, team)
|
|
}
|
|
|
|
// GinTeamCurrent handles GET /teams/current - Get current team
|
|
func GinTeamCurrent(c *gin.Context) {
|
|
// Get authorized user info
|
|
authInfo := authorized.GetInfo(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 current team ID (from token or first owner team)
|
|
teamID, err := getCurrentTeamID(c.Request.Context(), authInfo.TeamID, authInfo.UserID)
|
|
if err != nil {
|
|
log.Error("Failed to get current team ID: %v", err)
|
|
|
|
// Return 404 if user has no team
|
|
if err.Error() == "no owner team found for user" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "No owner team found for user",
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
return
|
|
}
|
|
|
|
// Return 500 for other errors
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to get current team ID",
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
return
|
|
}
|
|
|
|
// Get team details
|
|
team, err := teamGet(c.Request.Context(), authInfo.UserID, teamID)
|
|
if err != nil {
|
|
log.Error("Failed to get team details: %v", err)
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to get team details",
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
return
|
|
}
|
|
|
|
response.RespondWithSuccess(c, http.StatusOK, team)
|
|
}
|
|
|
|
// GinTeamSelection handles POST /teams/select - Select a team and issue tokens with team_id
|
|
func GinTeamSelection(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
|
|
}
|
|
|
|
// Verify the current token has team_selection scope
|
|
if authInfo.Scope != ScopeTeamSelection {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrAccessDenied.Code,
|
|
ErrorDescription: "Invalid scope: team_selection scope required",
|
|
}
|
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
|
return
|
|
}
|
|
|
|
// Parse request body
|
|
var req TeamSelectionRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Invalid request body: " + err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
|
|
// Prepare login context with full device/platform information
|
|
loginCtx := makeLoginContext(c)
|
|
|
|
// Preserve Remember Me state from temporary token
|
|
loginCtx.RememberMe = authInfo.RememberMe
|
|
loginCtx.AuthSource = authInfo.AuthSource // Preserve auth source from login
|
|
loginCtx.OAuthEmail = authInfo.OAuthEmail // Preserve OAuth email from login
|
|
|
|
// Login with selected team
|
|
loginResponse, err := LoginByTeamID(authInfo.UserID, req.TeamID, loginCtx)
|
|
if err != nil {
|
|
log.Error("Failed to login with team: %v", err)
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to login with team: " + err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
return
|
|
}
|
|
|
|
// Revoke the current temporary token (read from header)
|
|
currentToken := oauth.OAuth.GetAccessToken(c)
|
|
if currentToken != "" {
|
|
if err := oauth.OAuth.Revoke(ctx, currentToken, "access_token"); err != nil {
|
|
// Log the error but don't fail the request
|
|
log.Warn("Failed to revoke temporary token: %v", err)
|
|
}
|
|
}
|
|
|
|
// Send secure cookies (access token, refresh token, and session ID)
|
|
SendLoginCookies(c, loginResponse, "")
|
|
|
|
// Return the new tokens in response body
|
|
response.RespondWithSuccess(c, http.StatusOK, loginResponse)
|
|
}
|
|
|
|
// GinTeamDelete handles DELETE /teams/:id - Delete user team
|
|
func GinTeamDelete(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
|
|
}
|
|
|
|
teamID := c.Param("id")
|
|
if teamID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Team ID is required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Call business logic
|
|
err := teamDelete(c.Request.Context(), authInfo.UserID, teamID)
|
|
if err != nil {
|
|
log.Error("Failed to delete team: %v", err)
|
|
// Check error type for appropriate response
|
|
if strings.Contains(err.Error(), "not found") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Team not found",
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
} else if strings.Contains(err.Error(), "access denied") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrAccessDenied.Code,
|
|
ErrorDescription: err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
|
} else {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to delete team",
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
}
|
|
return
|
|
}
|
|
|
|
response.RespondWithSuccess(c, http.StatusOK, gin.H{"message": "Team deleted successfully"})
|
|
}
|
|
|
|
// Yao Process Handlers (for Yao application calls)
|
|
|
|
// ProcessTeamList user.team.list Team list processor
|
|
// Args[0] map: Query parameters {"status": "active", "name": "search", "page": 1, "pagesize": 20}
|
|
// Return: map: Paginated team list
|
|
func ProcessTeamList(process *process.Process) interface{} {
|
|
process.ValidateArgNums(1)
|
|
|
|
// Get user_id from session
|
|
userIDStr := GetUserIDFromSession(process)
|
|
|
|
// Parse query parameters
|
|
queryMap := process.ArgsMap(0)
|
|
|
|
// Build query parameters
|
|
param := model.QueryParam{}
|
|
|
|
// Add filters
|
|
if status, ok := queryMap["status"].(string); ok && status != "" {
|
|
param.Wheres = append(param.Wheres, model.QueryWhere{
|
|
Column: "status",
|
|
Value: status,
|
|
})
|
|
}
|
|
|
|
if name, ok := queryMap["name"].(string); ok && name != "" {
|
|
param.Wheres = append(param.Wheres, model.QueryWhere{
|
|
Column: "name",
|
|
Value: "%" + name + "%",
|
|
OP: "like",
|
|
})
|
|
}
|
|
|
|
// Parse pagination
|
|
page := 1
|
|
pagesize := 20
|
|
|
|
if p, ok := queryMap["page"]; ok {
|
|
if pageInt, ok := p.(int); ok && pageInt > 0 {
|
|
page = pageInt
|
|
}
|
|
}
|
|
|
|
if ps, ok := queryMap["pagesize"]; ok {
|
|
if pagesizeInt, ok := ps.(int); ok && pagesizeInt > 0 && pagesizeInt <= 100 {
|
|
pagesize = pagesizeInt
|
|
}
|
|
}
|
|
|
|
// Get context
|
|
ctx := process.Context
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
// Call business logic
|
|
result, err := teamList(ctx, userIDStr, param, page, pagesize)
|
|
if err != nil {
|
|
exception.New("failed to list teams: %s", 500, err.Error()).Throw()
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// ProcessTeamGet user.team.get Team get processor
|
|
// Args[0] string: team_id
|
|
// Return: map: Team details
|
|
func ProcessTeamGet(process *process.Process) interface{} {
|
|
process.ValidateArgNums(1)
|
|
|
|
// Get user_id from session
|
|
userIDStr := GetUserIDFromSession(process)
|
|
|
|
teamID := process.ArgsString(0)
|
|
if teamID == "" {
|
|
exception.New("team_id is required", 400).Throw()
|
|
}
|
|
|
|
// Get context
|
|
ctx := process.Context
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
// Call business logic
|
|
result, err := teamGet(ctx, userIDStr, teamID)
|
|
if err != nil {
|
|
exception.New("failed to get team: %s", 500, err.Error()).Throw()
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// ProcessTeamCreate user.team.create Team create processor
|
|
// Args[0] map: Team data {"name": "Team Name", "description": "Description", "settings": {...}}
|
|
// Return: map: {"team_id": "created_team_id"}
|
|
func ProcessTeamCreate(process *process.Process) interface{} {
|
|
process.ValidateArgNums(1)
|
|
|
|
// Get user_id from session
|
|
userIDStr := GetUserIDFromSession(process)
|
|
|
|
teamData := maps.MapStrAny(process.ArgsMap(0))
|
|
|
|
// Validate required fields
|
|
if _, ok := teamData["name"]; !ok {
|
|
exception.New("name is required", 400).Throw()
|
|
}
|
|
|
|
// Get context
|
|
ctx := process.Context
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
// Call business logic
|
|
teamID, err := teamCreate(ctx, userIDStr, teamData)
|
|
if err != nil {
|
|
exception.New("failed to create team: %s", 500, err.Error()).Throw()
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"team_id": teamID,
|
|
}
|
|
}
|
|
|
|
// ProcessTeamUpdate user.team.update Team update processor
|
|
// Args[0] string: team_id
|
|
// Args[1] map: Update data {"name": "New Name", "description": "New Description", "settings": {...}}
|
|
// Return: map: {"message": "success"}
|
|
func ProcessTeamUpdate(process *process.Process) interface{} {
|
|
process.ValidateArgNums(2)
|
|
|
|
// Get user_id from session
|
|
userIDStr := GetUserIDFromSession(process)
|
|
|
|
teamID := process.ArgsString(0)
|
|
updateData := maps.MapStrAny(process.ArgsMap(1))
|
|
|
|
if teamID == "" {
|
|
exception.New("team_id is required", 400).Throw()
|
|
}
|
|
|
|
// Get context
|
|
ctx := process.Context
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
// Call business logic
|
|
err := teamUpdate(ctx, userIDStr, teamID, updateData)
|
|
if err != nil {
|
|
exception.New("failed to update team: %s", 500, err.Error()).Throw()
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"message": "success",
|
|
}
|
|
}
|
|
|
|
// ProcessTeamDelete user.team.delete Team delete processor
|
|
// Args[0] string: team_id
|
|
// Return: map: {"message": "success"}
|
|
func ProcessTeamDelete(process *process.Process) interface{} {
|
|
process.ValidateArgNums(1)
|
|
|
|
// Get user_id from session
|
|
userIDStr := GetUserIDFromSession(process)
|
|
|
|
teamID := process.ArgsString(0)
|
|
if teamID == "" {
|
|
exception.New("team_id is required", 400).Throw()
|
|
}
|
|
|
|
// Get context
|
|
ctx := process.Context
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
// Call business logic
|
|
err := teamDelete(ctx, userIDStr, teamID)
|
|
if err != nil {
|
|
exception.New("failed to delete team: %s", 500, err.Error()).Throw()
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"message": "success",
|
|
}
|
|
}
|
|
|
|
// Private Business Logic Functions (internal use only)
|
|
|
|
// teamList handles the business logic for listing user teams
|
|
func teamList(ctx context.Context, userID string, param model.QueryParam, page, pagesize int) (maps.MapStr, error) {
|
|
// Get user provider instance
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
// Add owner filter to query parameters
|
|
param.Wheres = append(param.Wheres, model.QueryWhere{
|
|
Column: "owner_id",
|
|
Value: userID,
|
|
})
|
|
|
|
// Set default ordering if not provided
|
|
if len(param.Orders) == 0 {
|
|
param.Orders = []model.QueryOrder{
|
|
{Column: "created_at", Option: "desc"},
|
|
}
|
|
}
|
|
|
|
// Get paginated teams
|
|
result, err := provider.PaginateTeams(ctx, param, page, pagesize)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve teams: %w", err)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// getCurrentTeamID resolves the current team ID for a user
|
|
// If teamID is provided, it returns it directly
|
|
// If teamID is empty, it gets the first owner team for the user
|
|
func getCurrentTeamID(ctx context.Context, teamID, userID string) (string, error) {
|
|
// If team ID is already provided, return it
|
|
if teamID != "" {
|
|
return teamID, nil
|
|
}
|
|
|
|
// Get owner teams for the user
|
|
teams, err := getOwnerTeams(ctx, userID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get owner teams: %w", err)
|
|
}
|
|
|
|
// Check if user has any owner teams
|
|
if len(teams) == 0 {
|
|
return "", fmt.Errorf("no owner team found for user")
|
|
}
|
|
|
|
// Return the first team ID
|
|
if teamIDVal, ok := teams[0]["team_id"].(string); ok {
|
|
return teamIDVal, nil
|
|
}
|
|
|
|
return "", fmt.Errorf("invalid team_id format")
|
|
}
|
|
|
|
// teamGet handles the business logic for getting a specific user team
|
|
func teamGet(ctx context.Context, userID, teamID string) (maps.MapStrAny, error) {
|
|
// Get user provider instance
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
// Get team details
|
|
teamData, err := provider.GetTeamDetail(ctx, teamID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve team details: %w", err)
|
|
}
|
|
|
|
// Validate if user is a member of the team
|
|
exists, err := provider.MemberExists(ctx, teamID, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve team member record: %w", err)
|
|
}
|
|
|
|
if !exists {
|
|
return nil, fmt.Errorf("user is not a member of this team")
|
|
}
|
|
|
|
return teamData, nil
|
|
}
|
|
|
|
// teamCreate handles the business logic for creating a user team
|
|
func teamCreate(ctx context.Context, userID string, teamData maps.MapStrAny) (string, error) {
|
|
// Get user provider instance
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
// Set owner and default values
|
|
teamData["owner_id"] = userID
|
|
teamData["status"] = "active"
|
|
teamData["is_verified"] = false
|
|
teamData["created_at"] = time.Now()
|
|
teamData["updated_at"] = time.Now()
|
|
|
|
// Get team config for setting defaults
|
|
locale := ""
|
|
if localeVal, ok := teamData["locale"].(string); ok && localeVal != "" {
|
|
locale = strings.TrimSpace(strings.ToLower(localeVal))
|
|
}
|
|
|
|
// Fallback: try common locale variations or use "en" as final fallback
|
|
// This ensures we always get a valid config even if locale is invalid
|
|
teamConfig := GetTeamConfig(locale)
|
|
if teamConfig == nil {
|
|
// Try fallback locales in order
|
|
fallbackLocales := []string{"en", "zh-cn"}
|
|
for _, fallback := range fallbackLocales {
|
|
teamConfig = GetTeamConfig(fallback)
|
|
if teamConfig != nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set default type_id from team config if not provided
|
|
if _, hasType := teamData["type_id"]; !hasType {
|
|
// Apply default type from config if available
|
|
if teamConfig != nil && teamConfig.Type != "" {
|
|
teamData["type_id"] = teamConfig.Type
|
|
}
|
|
}
|
|
|
|
// Set default role_id from team config if not provided
|
|
if _, hasRole := teamData["role_id"]; !hasRole {
|
|
// Apply default role from config if available
|
|
if teamConfig != nil && teamConfig.Role != "" {
|
|
teamData["role_id"] = teamConfig.Role
|
|
}
|
|
}
|
|
|
|
// Clean up: remove locale from team data as it's not stored in database
|
|
delete(teamData, "locale")
|
|
|
|
// Create team
|
|
teamID, err := provider.CreateTeam(ctx, teamData)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create team: %w", err)
|
|
}
|
|
|
|
// Determine owner member role_id from team config
|
|
ownerRoleID := "owner" // fallback default
|
|
if teamConfig != nil && teamConfig.Role != "" {
|
|
ownerRoleID = teamConfig.Role
|
|
}
|
|
|
|
// Add the creator as an owner member of the team
|
|
ownerMemberData := types.CopyCreateScope(teamData, maps.MapStrAny{
|
|
"team_id": teamID,
|
|
"user_id": userID,
|
|
"member_type": "user",
|
|
"role_id": ownerRoleID,
|
|
"is_owner": true,
|
|
"status": "active",
|
|
"joined_at": time.Now(),
|
|
"created_at": time.Now(),
|
|
"updated_at": time.Now(),
|
|
})
|
|
|
|
_, err = provider.CreateMember(ctx, ownerMemberData)
|
|
if err != nil {
|
|
// Log the error but don't fail the team creation
|
|
log.Error("Failed to add owner as team member: %v", err)
|
|
// Consider whether to rollback team creation or continue
|
|
// For now, we'll continue as the team is already created
|
|
}
|
|
|
|
return teamID, nil
|
|
}
|
|
|
|
// teamUpdate handles the business logic for updating a user team
|
|
func teamUpdate(ctx context.Context, userID, teamID string, updateData maps.MapStrAny) error {
|
|
// Get user provider instance
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
// Check if team exists and user owns it
|
|
teamData, err := provider.GetTeam(ctx, teamID)
|
|
if err != nil {
|
|
return fmt.Errorf("team not found or access denied: %w", err)
|
|
}
|
|
|
|
// Check ownership
|
|
ownerID := utils.ToString(teamData["owner_id"])
|
|
if ownerID != userID {
|
|
return fmt.Errorf("access denied: user does not own this team")
|
|
}
|
|
|
|
// Add updated_at timestamp
|
|
updateData["updated_at"] = time.Now()
|
|
|
|
// Update team
|
|
err = provider.UpdateTeam(ctx, teamID, updateData)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update team: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// teamDelete handles the business logic for deleting a user team
|
|
func teamDelete(ctx context.Context, userID, teamID string) error {
|
|
// Get user provider instance
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
// Check if team exists and user owns it
|
|
teamData, err := provider.GetTeam(ctx, teamID)
|
|
if err != nil {
|
|
return fmt.Errorf("team not found or access denied: %w", err)
|
|
}
|
|
|
|
// Check ownership
|
|
ownerID := utils.ToString(teamData["owner_id"])
|
|
if ownerID != userID {
|
|
return fmt.Errorf("access denied: user does not own this team")
|
|
}
|
|
|
|
// First, remove all team members
|
|
err = provider.RemoveAllTeamMembers(ctx, teamID)
|
|
if err != nil {
|
|
// Log error but don't fail team deletion - members might not exist
|
|
log.Error("Failed to remove team members during team deletion: %v", err)
|
|
}
|
|
|
|
// Then delete the team
|
|
err = provider.DeleteTeam(ctx, teamID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete team: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Private Helper Functions (internal use only)
|
|
|
|
// getUserProvider gets the user provider from the global OAuth service
|
|
func getUserProvider() (*user.DefaultUser, error) {
|
|
// Check if global OAuth service is initialized
|
|
if oauth.OAuth == nil {
|
|
return nil, fmt.Errorf("OAuth service not initialized")
|
|
}
|
|
|
|
// Get user provider from OAuth service
|
|
userProvider, err := oauth.OAuth.GetUserProvider()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
// Type assert to DefaultUser (this should be safe based on the OAuth service implementation)
|
|
if defaultUser, ok := userProvider.(*user.DefaultUser); ok {
|
|
return defaultUser, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("user provider is not of type DefaultUser")
|
|
}
|
|
|
|
// mapToTeamResponse converts a map to TeamResponse
|
|
func mapToTeamResponse(data maps.MapStr) TeamResponse {
|
|
team := TeamResponse{
|
|
ID: utils.ToInt64(data["id"]),
|
|
TeamID: utils.ToString(data["team_id"]),
|
|
Name: utils.ToString(data["name"]),
|
|
Description: utils.ToString(data["description"]),
|
|
Logo: utils.ToString(data["logo"]),
|
|
OwnerID: utils.ToString(data["owner_id"]),
|
|
Status: utils.ToString(data["status"]),
|
|
IsVerified: utils.ToBool(data["is_verified"]),
|
|
VerifiedBy: utils.ToString(data["verified_by"]),
|
|
VerifiedAt: utils.ToTimeString(data["verified_at"]),
|
|
CreatedAt: utils.ToTimeString(data["created_at"]),
|
|
UpdatedAt: utils.ToTimeString(data["updated_at"]),
|
|
}
|
|
|
|
return team
|
|
}
|
|
|
|
// mapToTeamDetailResponse converts a map to TeamDetailResponse
|
|
func mapToTeamDetailResponse(data maps.MapStr) TeamDetailResponse {
|
|
team := TeamDetailResponse{
|
|
TeamResponse: mapToTeamResponse(data),
|
|
}
|
|
|
|
// Add settings if available
|
|
if settings, ok := data["settings"]; ok {
|
|
if teamSettings, ok := settings.(*TeamSettings); ok {
|
|
team.Settings = teamSettings
|
|
} else if settingsMap, ok := settings.(map[string]interface{}); ok {
|
|
// Convert map to TeamSettings (for backward compatibility)
|
|
teamSettings := &TeamSettings{
|
|
Theme: utils.ToString(settingsMap["theme"]),
|
|
Visibility: utils.ToString(settingsMap["visibility"]),
|
|
}
|
|
team.Settings = teamSettings
|
|
}
|
|
}
|
|
|
|
return team
|
|
}
|
|
|
|
// Business Logic Functions for Team Membership
|
|
|
|
// getUserTeams gets all teams where the user is a member (includes role information)
|
|
func getUserTeams(ctx context.Context, userID string) ([]maps.MapStr, error) {
|
|
// Get user provider instance
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
// Get teams with role information
|
|
teams, err := provider.GetTeamsByMember(ctx, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve user teams: %w", err)
|
|
}
|
|
|
|
return teams, nil
|
|
}
|
|
|
|
// getOwnerTeams gets all teams where the user is the owner
|
|
func getOwnerTeams(ctx context.Context, userID string) ([]maps.MapStr, error) {
|
|
// Get user provider instance
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
// Get owner team
|
|
teams, err := provider.GetTeamsByOwner(ctx, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve owner team: %w", err)
|
|
}
|
|
|
|
// Return the first team as the owner team
|
|
return teams, nil
|
|
}
|
|
|
|
// getUserTeamsCount counts the number of teams a user is a member of
|
|
func getUserTeamsCount(ctx context.Context, userID string) (int64, error) {
|
|
// Get user provider instance
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
// Count teams
|
|
count, err := provider.CountTeamsByMember(ctx, userID)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to count user teams: %w", err)
|
|
}
|
|
|
|
return count, nil
|
|
}
|