yao/openapi/oauth/providers/user/invitation.go
Max f71ce2ac9a Implement existence checks for various entities in update functions
- Added functions to check the existence of invitation codes, members, OAuth accounts, roles, teams, and user types before performing updates, enhancing error handling and user feedback.
- Updated relevant update functions to utilize these existence checks, ensuring accurate error messages when no changes are made or when entities do not exist.
- Refactored tests to validate the new existence check logic, improving overall test coverage and reliability.
2025-10-26 11:09:07 +08:00

234 lines
5.9 KiB
Go

package user
import (
"context"
"fmt"
"time"
"github.com/yaoapp/gou/model"
"github.com/yaoapp/kun/maps"
)
// Invitation Code Resource (Official Platform Invitation Codes)
// invitationCodeExists checks if an invitation code exists by code
func (u *DefaultUser) invitationCodeExists(ctx context.Context, code string) (bool, error) {
m := model.Select(u.invitationModel)
invitations, err := m.Get(model.QueryParam{
Select: []interface{}{"id"},
Wheres: []model.QueryWhere{
{Column: "code", Value: code},
},
Limit: 1,
})
if err != nil {
return false, fmt.Errorf("failed to check invitation code existence: %w", err)
}
return len(invitations) > 0, nil
}
// CreateInvitationCodes creates invitation codes in batch
// Supports creating multiple invitation codes at once for efficiency
func (u *DefaultUser) CreateInvitationCodes(ctx context.Context, codeData []maps.MapStrAny) ([]string, error) {
if len(codeData) == 0 {
return []string{}, nil
}
codes := make([]string, 0, len(codeData))
m := model.Select(u.invitationModel)
// Validate and prepare data - collect all possible columns
columnsSet := make(map[string]bool)
columnsSet["code"] = true
columnsSet["status"] = true
columnsSet["is_published"] = true
columnsSet["code_type"] = true
for i := range codeData {
// Validate required fields
code, hasCode := codeData[i]["code"].(string)
if !hasCode || code == "" {
return nil, fmt.Errorf("code is required in codeData at index %d", i)
}
// Set default values if not provided
if _, exists := codeData[i]["status"]; !exists {
codeData[i]["status"] = "draft"
}
if _, exists := codeData[i]["is_published"]; !exists {
codeData[i]["is_published"] = false
}
if _, exists := codeData[i]["code_type"]; !exists {
codeData[i]["code_type"] = "official"
}
// Collect optional columns
for _, col := range []string{"owner_id", "description", "source", "expires_at", "metadata"} {
if _, exists := codeData[i][col]; exists {
columnsSet[col] = true
}
}
codes = append(codes, code)
}
// Build ordered column list
columns := []string{"code", "status", "is_published", "code_type"}
for _, col := range []string{"owner_id", "description", "source", "expires_at", "metadata"} {
if columnsSet[col] {
columns = append(columns, col)
}
}
// Build values matrix
values := make([][]interface{}, 0, len(codeData))
for i := range codeData {
row := make([]interface{}, len(columns))
for j, col := range columns {
if val, exists := codeData[i][col]; exists {
row[j] = val
} else {
row[j] = nil
}
}
values = append(values, row)
}
// Batch insert
err := m.Insert(columns, values)
if err != nil {
return nil, fmt.Errorf(ErrFailedToCreateInvitationCode, err)
}
return codes, nil
}
// UseInvitationCode marks an invitation code as used (redemption)
// This is called when a user successfully uses an invitation code during registration
func (u *DefaultUser) UseInvitationCode(ctx context.Context, code string, userID string) error {
m := model.Select(u.invitationModel)
// First, get the invitation code to validate it
invitations, err := m.Get(model.QueryParam{
Select: []interface{}{"id", "code", "status", "is_published", "expires_at", "used_by"},
Wheres: []model.QueryWhere{
{Column: "code", Value: code},
},
Limit: 1,
})
if err != nil {
return fmt.Errorf(ErrFailedToUseInvitationCode, err)
}
if len(invitations) == 0 {
return fmt.Errorf(ErrInvitationCodeNotFound)
}
invitation := invitations[0]
// Check if already used
if usedBy := invitation["used_by"]; usedBy != nil && usedBy != "" {
return fmt.Errorf(ErrInvitationCodeAlreadyUsed)
}
// Check if published (handle both bool and int64 types from different databases)
isPublished := false
switch v := invitation["is_published"].(type) {
case bool:
isPublished = v
case int64:
isPublished = v != 0
case int:
isPublished = v != 0
}
if !isPublished {
return fmt.Errorf(ErrInvitationCodeNotPublished)
}
// Check status
status, ok := invitation["status"].(string)
if !ok || status != "active" {
return fmt.Errorf("invitation code status must be 'active' to use, current status: %s", status)
}
// Check if expired
if expiresAt := invitation["expires_at"]; expiresAt != nil {
if expired, err := checkTimeExpired(expiresAt); err == nil && expired {
return fmt.Errorf(ErrInvitationCodeExpired)
}
}
// Mark as used
updateData := maps.MapStrAny{
"used_by": userID,
"used_at": time.Now(),
"status": "used",
}
affected, err := m.UpdateWhere(model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "code", Value: code},
},
Limit: 1,
}, updateData)
if err != nil {
return fmt.Errorf(ErrFailedToUseInvitationCode, err)
}
if affected == 0 {
// Check if invitation code still exists
exists, checkErr := u.invitationCodeExists(ctx, code)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUseInvitationCode, checkErr)
}
if !exists {
return fmt.Errorf(ErrInvitationCodeNotFound)
}
// Invitation code exists but no changes were made (already in this state)
}
return nil
}
// DeleteInvitationCode soft deletes an invitation code
func (u *DefaultUser) DeleteInvitationCode(ctx context.Context, code string) error {
// First check if invitation code exists
m := model.Select(u.invitationModel)
invitations, err := m.Get(model.QueryParam{
Select: []interface{}{"id", "code"},
Wheres: []model.QueryWhere{
{Column: "code", Value: code},
},
Limit: 1,
})
if err != nil {
return fmt.Errorf(ErrFailedToDeleteInvitationCode, err)
}
if len(invitations) == 0 {
return fmt.Errorf(ErrInvitationCodeNotFound)
}
// Proceed with soft delete
affected, err := m.DeleteWhere(model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "code", Value: code},
},
Limit: 1,
})
if err != nil {
return fmt.Errorf(ErrFailedToDeleteInvitationCode, err)
}
if affected == 0 {
return fmt.Errorf(ErrInvitationCodeNotFound)
}
return nil
}