yao/openapi/oauth/providers/user/team.go
Max 35afc1d58c Add team ownership and membership checks in user management
- Introduced IsTeamOwner and IsTeamMember methods to verify user ownership and membership status within a team.
- Added CheckTeamAccess method to assess a user's access level, returning ownership and membership status.
- Enhanced user types with new MemberResponse and MemberDetailResponse structures for improved member management.
- Updated routing for team member management to utilize new handler functions for clarity and consistency.
2025-09-23 16:14:42 +08:00

398 lines
10 KiB
Go

package user
import (
"context"
"fmt"
"time"
"github.com/yaoapp/gou/model"
"github.com/yaoapp/kun/maps"
)
// Team Resource
// GetTeam retrieves team information by team_id
func (u *DefaultUser) GetTeam(ctx context.Context, teamID string) (maps.MapStrAny, error) {
m := model.Select(u.teamModel)
teams, err := m.Get(model.QueryParam{
Select: u.teamFields,
Wheres: []model.QueryWhere{
{Column: "team_id", Value: teamID},
},
Limit: 1,
})
if err != nil {
return nil, fmt.Errorf(ErrFailedToGetTeam, err)
}
if len(teams) == 0 {
return nil, fmt.Errorf(ErrTeamNotFound)
}
return teams[0], nil
}
// GetTeamDetail retrieves detailed team information by team_id
func (u *DefaultUser) GetTeamDetail(ctx context.Context, teamID string) (maps.MapStrAny, error) {
m := model.Select(u.teamModel)
teams, err := m.Get(model.QueryParam{
Select: u.teamDetailFields,
Wheres: []model.QueryWhere{
{Column: "team_id", Value: teamID},
},
Limit: 1,
})
if err != nil {
return nil, fmt.Errorf(ErrFailedToGetTeam, err)
}
if len(teams) == 0 {
return nil, fmt.Errorf(ErrTeamNotFound)
}
return teams[0], nil
}
// TeamExists checks if a team exists by team_id (lightweight query)
func (u *DefaultUser) TeamExists(ctx context.Context, teamID string) (bool, error) {
m := model.Select(u.teamModel)
teams, err := m.Get(model.QueryParam{
Select: []interface{}{"id"}, // Only select ID for existence check
Wheres: []model.QueryWhere{
{Column: "team_id", Value: teamID},
},
Limit: 1, // Only need to know if at least one exists
})
if err != nil {
return false, fmt.Errorf(ErrFailedToGetTeam, err)
}
return len(teams) > 0, nil
}
// CreateTeam creates a new team
func (u *DefaultUser) CreateTeam(ctx context.Context, teamData maps.MapStrAny) (string, error) {
// Generate team_id if not provided
if _, exists := teamData["team_id"]; !exists {
teamID, err := u.GenerateUserID(ctx, true) // Reuse user ID generation logic for team ID
if err != nil {
return "", fmt.Errorf("failed to generate team_id: %w", err)
}
teamData["team_id"] = teamID
}
// Validate required fields
if _, exists := teamData["name"]; !exists {
return "", fmt.Errorf("name is required in teamData")
}
if _, exists := teamData["owner_id"]; !exists {
return "", fmt.Errorf("owner_id is required in teamData")
}
// Set default values if not provided
if _, exists := teamData["status"]; !exists {
teamData["status"] = "pending"
}
if _, exists := teamData["is_verified"]; !exists {
teamData["is_verified"] = false
}
m := model.Select(u.teamModel)
id, err := m.Create(teamData)
if err != nil {
return "", fmt.Errorf(ErrFailedToCreateTeam, err)
}
// Return the team_id as string (preferred approach)
if teamID, ok := teamData["team_id"].(string); ok {
return teamID, nil
}
// Fallback: convert the returned int id to string
return fmt.Sprintf("%d", id), nil
}
// UpdateTeam updates an existing team
func (u *DefaultUser) UpdateTeam(ctx context.Context, teamID string, teamData maps.MapStrAny) error {
// Remove sensitive fields that should not be updated directly
sensitiveFields := []string{"id", "team_id", "created_at", "verified_at", "verified_by"}
for _, field := range sensitiveFields {
delete(teamData, field)
}
// Skip update if no valid fields remain
if len(teamData) == 0 {
return nil
}
m := model.Select(u.teamModel)
affected, err := m.UpdateWhere(model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "team_id", Value: teamID},
},
Limit: 1, // Safety: ensure only one record is updated
}, teamData)
if err != nil {
return fmt.Errorf(ErrFailedToUpdateTeam, err)
}
if affected == 0 {
return fmt.Errorf(ErrTeamNotFound)
}
return nil
}
// DeleteTeam soft deletes a team
func (u *DefaultUser) DeleteTeam(ctx context.Context, teamID string) error {
// First check if team exists
m := model.Select(u.teamModel)
teams, err := m.Get(model.QueryParam{
Select: []interface{}{"id", "team_id"},
Wheres: []model.QueryWhere{
{Column: "team_id", Value: teamID},
},
Limit: 1,
})
if err != nil {
return fmt.Errorf(ErrFailedToGetTeam, err)
}
if len(teams) == 0 {
return fmt.Errorf(ErrTeamNotFound)
}
// Proceed with soft delete
affected, err := m.DeleteWhere(model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "team_id", Value: teamID},
},
Limit: 1, // Safety: ensure only one record is deleted
})
if err != nil {
return fmt.Errorf(ErrFailedToDeleteTeam, err)
}
if affected == 0 {
return fmt.Errorf(ErrTeamNotFound)
}
return nil
}
// GetTeams retrieves teams by query parameters
func (u *DefaultUser) GetTeams(ctx context.Context, param model.QueryParam) ([]maps.MapStr, error) {
// Set default select fields if not provided
if param.Select == nil {
param.Select = u.teamFields
}
m := model.Select(u.teamModel)
teams, err := m.Get(param)
if err != nil {
return nil, fmt.Errorf(ErrFailedToGetTeam, err)
}
return teams, nil
}
// PaginateTeams retrieves paginated list of teams
func (u *DefaultUser) PaginateTeams(ctx context.Context, param model.QueryParam, page int, pagesize int) (maps.MapStr, error) {
// Set default select fields if not provided
if param.Select == nil {
param.Select = u.teamFields
}
m := model.Select(u.teamModel)
result, err := m.Paginate(param, page, pagesize)
if err != nil {
return nil, fmt.Errorf(ErrFailedToGetTeam, err)
}
return result, nil
}
// CountTeams returns total count of teams with optional filters
func (u *DefaultUser) CountTeams(ctx context.Context, param model.QueryParam) (int64, error) {
// Use Paginate with a small page size to get the total count
// This is more reliable than manual COUNT(*) queries
m := model.Select(u.teamModel)
result, err := m.Paginate(param, 1, 1) // Get first page with 1 item to get total
if err != nil {
return 0, fmt.Errorf(ErrFailedToGetTeam, err)
}
// Extract total from pagination result using utility function
if totalInterface, ok := result["total"]; ok {
return parseIntFromDB(totalInterface)
}
return 0, fmt.Errorf("total not found in pagination result")
}
// GetTeamsByOwner retrieves teams owned by a specific user
func (u *DefaultUser) GetTeamsByOwner(ctx context.Context, ownerID string) ([]maps.MapStr, error) {
param := model.QueryParam{
Select: u.teamFields,
Wheres: []model.QueryWhere{
{Column: "owner_id", Value: ownerID},
},
Orders: []model.QueryOrder{
{Column: "created_at", Option: "desc"},
},
}
return u.GetTeams(ctx, param)
}
// GetTeamsByStatus retrieves teams by status
func (u *DefaultUser) GetTeamsByStatus(ctx context.Context, status string) ([]maps.MapStr, error) {
param := model.QueryParam{
Select: u.teamFields,
Wheres: []model.QueryWhere{
{Column: "status", Value: status},
},
Orders: []model.QueryOrder{
{Column: "created_at", Option: "desc"},
},
}
return u.GetTeams(ctx, param)
}
// UpdateTeamStatus updates team status
func (u *DefaultUser) UpdateTeamStatus(ctx context.Context, teamID string, status string) error {
updateData := maps.MapStrAny{
"status": status,
}
return u.UpdateTeam(ctx, teamID, updateData)
}
// VerifyTeam marks a team as verified
func (u *DefaultUser) VerifyTeam(ctx context.Context, teamID string, verifiedBy string) error {
updateData := maps.MapStrAny{
"is_verified": true,
"verified_by": verifiedBy,
"verified_at": time.Now(), // Set current timestamp explicitly
}
// Direct model update to bypass sensitive field filtering
m := model.Select(u.teamModel)
affected, err := m.UpdateWhere(model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "team_id", Value: teamID},
},
Limit: 1,
}, updateData)
if err != nil {
return fmt.Errorf(ErrFailedToUpdateTeam, err)
}
if affected == 0 {
return fmt.Errorf(ErrTeamNotFound)
}
return nil
}
// UnverifyTeam removes verification from a team
func (u *DefaultUser) UnverifyTeam(ctx context.Context, teamID string) error {
updateData := maps.MapStrAny{
"is_verified": false,
"verified_by": nil,
"verified_at": nil,
}
// Direct model update to bypass sensitive field filtering
m := model.Select(u.teamModel)
affected, err := m.UpdateWhere(model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "team_id", Value: teamID},
},
Limit: 1,
}, updateData)
if err != nil {
return fmt.Errorf(ErrFailedToUpdateTeam, err)
}
if affected == 0 {
return fmt.Errorf(ErrTeamNotFound)
}
return nil
}
// TransferTeamOwnership transfers team ownership to another user
func (u *DefaultUser) TransferTeamOwnership(ctx context.Context, teamID string, newOwnerID string) error {
// First verify the new owner exists
exists, err := u.UserExists(ctx, newOwnerID)
if err != nil {
return fmt.Errorf("failed to verify new owner: %w", err)
}
if !exists {
return fmt.Errorf("new owner user not found: %s", newOwnerID)
}
updateData := maps.MapStrAny{
"owner_id": newOwnerID,
}
return u.UpdateTeam(ctx, teamID, updateData)
}
// IsTeamOwner checks if a user is the owner of a team
func (u *DefaultUser) IsTeamOwner(ctx context.Context, teamID string, userID string) (bool, error) {
teamData, err := u.GetTeam(ctx, teamID)
if err != nil {
return false, fmt.Errorf("failed to get team: %w", err)
}
ownerID, ok := teamData["owner_id"].(string)
if !ok {
return false, fmt.Errorf("invalid owner_id type in team data")
}
return ownerID == userID, nil
}
// IsTeamMember checks if a user is a member of a team (includes owner)
func (u *DefaultUser) IsTeamMember(ctx context.Context, teamID string, userID string) (bool, error) {
// First check if user is the owner
isOwner, err := u.IsTeamOwner(ctx, teamID, userID)
if err != nil {
return false, err
}
if isOwner {
return true, nil
}
// Then check if user is a member
return u.MemberExists(ctx, teamID, userID)
}
// CheckTeamAccess checks user's access level to a team
// Returns: (isOwner bool, isMember bool, error)
func (u *DefaultUser) CheckTeamAccess(ctx context.Context, teamID string, userID string) (bool, bool, error) {
// Check if user is the owner
isOwner, err := u.IsTeamOwner(ctx, teamID, userID)
if err != nil {
return false, false, err
}
// Check if user is a member (this will return true for owner as well, but we already know that)
isMember, err := u.IsTeamMember(ctx, teamID, userID)
if err != nil {
return false, false, err
}
return isOwner, isMember, nil
}