- Replaced the jaevor/go-nanoid library with the updated matoous/go-nanoid/v2 for generating unique IDs. - Implemented user retrieval methods in the user provider, including GetUsers, PaginateUsers, and CountUsers, enhancing user management capabilities. - Improved error handling and ensured default select fields are set for user queries, streamlining the user data access process.
158 lines
3.9 KiB
Go
158 lines
3.9 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
gonanoid "github.com/matoous/go-nanoid/v2"
|
|
"github.com/yaoapp/gou/model"
|
|
)
|
|
|
|
// Utils
|
|
|
|
// GenerateUserID generates a new unique user_id for user creation
|
|
// safe: optional parameter, if true check for collisions and retry if needed
|
|
//
|
|
// defaults to true for NanoID, false for UUID
|
|
func (u *DefaultUser) GenerateUserID(ctx context.Context, safe ...bool) (string, error) {
|
|
// Determine safe mode: default based on strategy, or use provided value
|
|
var safeMode bool
|
|
if len(safe) > 0 {
|
|
safeMode = safe[0] // Use provided value
|
|
} else {
|
|
// Default: safe for NanoID, unsafe for UUID
|
|
safeMode = u.idStrategy == NanoIDStrategy
|
|
}
|
|
|
|
if !safeMode {
|
|
// Direct generation without collision detection (UUID case)
|
|
return u.generateUserID()
|
|
}
|
|
|
|
// Safe generation with collision detection (NanoID case)
|
|
const maxRetries = 10 // Prevent infinite loops
|
|
|
|
for i := 0; i < maxRetries; i++ {
|
|
// Generate new ID
|
|
id, err := u.generateUserID()
|
|
if err != nil {
|
|
return "", fmt.Errorf(ErrFailedToGenerateUserID, err)
|
|
}
|
|
|
|
// Check if ID already exists
|
|
exists, err := u.userIDExists(ctx, id)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to check user_id existence: %w", err)
|
|
}
|
|
|
|
if !exists {
|
|
return id, nil // Found unique ID
|
|
}
|
|
|
|
// ID exists, retry with new generation
|
|
}
|
|
|
|
return "", fmt.Errorf("failed to generate unique user_id after %d retries", maxRetries)
|
|
}
|
|
|
|
// generateUserID generates a new user_id based on configured strategy (internal use)
|
|
func (u *DefaultUser) generateUserID() (string, error) {
|
|
var id string
|
|
var err error
|
|
|
|
switch u.idStrategy {
|
|
case UUIDStrategy:
|
|
id, err = generateUUID()
|
|
case NanoIDStrategy:
|
|
fallthrough
|
|
default:
|
|
id, err = generateNanoID(12) // 12 characters, URL-safe, readable
|
|
}
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Add prefix if configured
|
|
if u.idPrefix != "" {
|
|
return u.idPrefix + id, nil
|
|
}
|
|
|
|
return id, nil
|
|
}
|
|
|
|
// userIDExists checks if a user_id already exists in the database
|
|
func (u *DefaultUser) userIDExists(ctx context.Context, userID string) (bool, error) {
|
|
m := model.Select(u.model)
|
|
users, err := m.Get(model.QueryParam{
|
|
Select: []interface{}{"id"}, // Just get primary key, minimal data
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "user_id", Value: userID},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return len(users) > 0, nil
|
|
}
|
|
|
|
// GetOAuthUserID quickly retrieves user_id by OAuth provider and subject
|
|
func (u *DefaultUser) GetOAuthUserID(ctx context.Context, provider string, subject string) (string, error) {
|
|
m := model.Select(u.oauthAccountModel)
|
|
accounts, err := m.Get(model.QueryParam{
|
|
Select: []interface{}{"user_id"},
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "provider", Value: provider},
|
|
{Column: "sub", Value: subject},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return "", fmt.Errorf(ErrFailedToGetOAuthAccount, err)
|
|
}
|
|
|
|
if len(accounts) == 0 {
|
|
return "", fmt.Errorf(ErrOAuthAccountNotFound)
|
|
}
|
|
|
|
userID, ok := accounts[0]["user_id"].(string)
|
|
if !ok {
|
|
return "", fmt.Errorf(ErrInvalidUserIDInOAuth)
|
|
}
|
|
|
|
return userID, nil
|
|
}
|
|
|
|
// generateNanoID generates a Nano ID using the library
|
|
func generateNanoID(length int) (string, error) {
|
|
// URL-safe alphabet (no ambiguous characters like 0/O, 1/l/I)
|
|
const alphabet = "23456789ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz"
|
|
return gonanoid.Generate(alphabet, length)
|
|
}
|
|
|
|
// generateUUID generates a traditional UUID using Google's library
|
|
func generateUUID() (string, error) {
|
|
return uuid.NewString(), nil
|
|
}
|
|
|
|
// generateRandomPassword generates a random password with specified length
|
|
func generateRandomPassword(length int) (string, error) {
|
|
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*"
|
|
bytes := make([]byte, length)
|
|
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for i, b := range bytes {
|
|
bytes[i] = charset[b%byte(len(charset))]
|
|
}
|
|
|
|
return string(bytes), nil
|
|
}
|