- Added the go-nanoid library for generating unique IDs, enhancing user ID management. - Refactored user provider methods to improve clarity and consistency, including updates to user retrieval and authentication processes. - Adjusted user model fields to align with new ID generation strategy, ensuring compliance with best practices. - Cleaned up code by removing obsolete test files and improving overall structure for better maintainability.
295 lines
7.5 KiB
Go
295 lines
7.5 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/yaoapp/gou/model"
|
|
"github.com/yaoapp/kun/maps"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// User Basic Operations
|
|
|
|
// GetUser retrieves user information using the global user_id
|
|
func (u *DefaultUser) GetUser(ctx context.Context, userID string) (maps.MapStrAny, error) {
|
|
m := model.Select(u.model)
|
|
users, err := m.Get(model.QueryParam{
|
|
Select: u.publicUserFields,
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "user_id", Value: userID},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf(ErrFailedToGetUser, err)
|
|
}
|
|
|
|
if len(users) == 0 {
|
|
return nil, fmt.Errorf(ErrUserNotFound)
|
|
}
|
|
|
|
return users[0], nil
|
|
}
|
|
|
|
// GetUserByPreferredUsername retrieves user by preferred_username (OIDC standard)
|
|
func (u *DefaultUser) GetUserByPreferredUsername(ctx context.Context, preferredUsername string) (maps.MapStrAny, error) {
|
|
m := model.Select(u.model)
|
|
users, err := m.Get(model.QueryParam{
|
|
Select: u.publicUserFields,
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "preferred_username", Value: preferredUsername},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf(ErrFailedToGetUser, err)
|
|
}
|
|
|
|
if len(users) == 0 {
|
|
return nil, fmt.Errorf(ErrUserNotFound)
|
|
}
|
|
|
|
return users[0], nil
|
|
}
|
|
|
|
// GetUserByEmail retrieves user by email address
|
|
func (u *DefaultUser) GetUserByEmail(ctx context.Context, email string) (maps.MapStrAny, error) {
|
|
m := model.Select(u.model)
|
|
users, err := m.Get(model.QueryParam{
|
|
Select: u.publicUserFields,
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "email", Value: email},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf(ErrFailedToGetUser, err)
|
|
}
|
|
|
|
if len(users) == 0 {
|
|
return nil, fmt.Errorf(ErrUserNotFound)
|
|
}
|
|
|
|
return users[0], nil
|
|
}
|
|
|
|
// GetUserForAuth retrieves user information for authentication purposes (internal use only)
|
|
func (u *DefaultUser) GetUserForAuth(ctx context.Context, identifier string, identifierType string) (maps.MapStrAny, error) {
|
|
m := model.Select(u.model)
|
|
|
|
var column string
|
|
switch identifierType {
|
|
case "user_id":
|
|
column = "user_id"
|
|
case "preferred_username":
|
|
column = "preferred_username"
|
|
case "email":
|
|
column = "email"
|
|
case "phone_number":
|
|
column = "phone_number"
|
|
default:
|
|
return nil, fmt.Errorf(ErrInvalidIdentifierType, identifierType)
|
|
}
|
|
|
|
users, err := m.Get(model.QueryParam{
|
|
Select: u.authUserFields,
|
|
Wheres: []model.QueryWhere{
|
|
{Column: column, Value: identifier},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf(ErrFailedToGetUser, err)
|
|
}
|
|
|
|
if len(users) == 0 {
|
|
return nil, fmt.Errorf(ErrUserNotFound)
|
|
}
|
|
|
|
return users[0], nil
|
|
}
|
|
|
|
// VerifyPassword verifies password against password hash (no database query needed)
|
|
func (u *DefaultUser) VerifyPassword(ctx context.Context, password string, passwordHash string) (bool, error) {
|
|
if passwordHash == "" {
|
|
return false, fmt.Errorf(ErrNoPasswordHash)
|
|
}
|
|
|
|
// Verify password using bcrypt (copied from yao/helper/password.go logic)
|
|
err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password))
|
|
if err != nil {
|
|
return false, nil // Invalid password, but no error (return false)
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// UpdatePassword updates user password (requires current password verification)
|
|
func (u *DefaultUser) UpdatePassword(ctx context.Context, userID string, newPassword string) error {
|
|
updateData := maps.MapStrAny{
|
|
"password_hash": newPassword, // Yao will auto-hash
|
|
"password_changed_at": time.Now(),
|
|
}
|
|
|
|
m := model.Select(u.model)
|
|
affected, err := m.UpdateWhere(model.QueryParam{
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "user_id", Value: userID},
|
|
},
|
|
Limit: 1, // Safety: ensure only one record is updated
|
|
}, updateData)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf(ErrFailedToUpdateUser, err)
|
|
}
|
|
|
|
if affected == 0 {
|
|
return fmt.Errorf(ErrUserNotFound)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ResetPassword generates and sets a new random password (admin/recovery operation)
|
|
func (u *DefaultUser) ResetPassword(ctx context.Context, userID string) (string, error) {
|
|
// Generate a random password
|
|
randomPassword, err := generateRandomPassword(12) // 12 characters
|
|
if err != nil {
|
|
return "", fmt.Errorf(ErrFailedToGeneratePassword, err)
|
|
}
|
|
|
|
updateData := maps.MapStrAny{
|
|
"password_hash": randomPassword, // Yao will auto-hash
|
|
"password_changed_at": time.Now(),
|
|
}
|
|
|
|
m := model.Select(u.model)
|
|
affected, err := m.UpdateWhere(model.QueryParam{
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "user_id", Value: userID},
|
|
},
|
|
Limit: 1, // Safety: ensure only one record is updated
|
|
}, updateData)
|
|
|
|
if err != nil {
|
|
return "", fmt.Errorf(ErrFailedToUpdateUser, err)
|
|
}
|
|
|
|
if affected == 0 {
|
|
return "", fmt.Errorf(ErrUserNotFound)
|
|
}
|
|
|
|
return randomPassword, nil
|
|
}
|
|
|
|
// CreateUser creates a new user with OIDC standard fields
|
|
func (u *DefaultUser) CreateUser(ctx context.Context, userData maps.MapStrAny) (interface{}, error) {
|
|
// Auto-generate user_id if not provided
|
|
if _, exists := userData["user_id"]; !exists {
|
|
userID, err := u.GenerateUserID(ctx, true) // Force safe mode to ensure uniqueness
|
|
if err != nil {
|
|
return nil, fmt.Errorf(ErrFailedToGenerateUserID, err)
|
|
}
|
|
userData["user_id"] = userID
|
|
}
|
|
|
|
// Yao Model will auto-hash password if provided as password_hash field
|
|
if password, ok := userData["password"].(string); ok && password != "" {
|
|
userData["password_hash"] = password // Let Yao handle the hashing
|
|
delete(userData, "password") // Remove plain password key
|
|
}
|
|
|
|
// Set default status if not provided
|
|
if _, exists := userData["status"]; !exists {
|
|
userData["status"] = "pending"
|
|
}
|
|
|
|
m := model.Select(u.model)
|
|
id, err := m.Create(userData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(ErrFailedToCreateUser, err)
|
|
}
|
|
|
|
return id, nil
|
|
}
|
|
|
|
// UpdateUser updates user information (excludes sensitive fields like password, MFA)
|
|
func (u *DefaultUser) UpdateUser(ctx context.Context, userID string, userData maps.MapStrAny) error {
|
|
// Remove sensitive fields that should use dedicated methods
|
|
sensitiveFields := []string{
|
|
"password", "password_hash", "password_changed_at",
|
|
"mfa_secret", "mfa_recovery_hash", "mfa_enabled", "mfa_enabled_at",
|
|
}
|
|
|
|
for _, field := range sensitiveFields {
|
|
delete(userData, field)
|
|
}
|
|
|
|
// Skip update if no valid fields remain
|
|
if len(userData) == 0 {
|
|
return nil
|
|
}
|
|
|
|
m := model.Select(u.model)
|
|
affected, err := m.UpdateWhere(model.QueryParam{
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "user_id", Value: userID},
|
|
},
|
|
Limit: 1, // Safety: ensure only one record is updated
|
|
}, userData)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf(ErrFailedToUpdateUser, err)
|
|
}
|
|
|
|
if affected == 0 {
|
|
return fmt.Errorf(ErrUserNotFound)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteUser soft deletes a user account
|
|
func (u *DefaultUser) DeleteUser(ctx context.Context, userID string) error {
|
|
m := model.Select(u.model)
|
|
affected, err := m.DeleteWhere(model.QueryParam{
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "user_id", Value: userID},
|
|
},
|
|
Limit: 1, // Safety: ensure only one record is deleted
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf(ErrFailedToDeleteUser, err)
|
|
}
|
|
|
|
if affected == 0 {
|
|
return fmt.Errorf(ErrUserNotFound)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateUserLastLogin updates the user's last login timestamp
|
|
func (u *DefaultUser) UpdateUserLastLogin(ctx context.Context, userID string) error {
|
|
updateData := maps.MapStrAny{
|
|
"last_login_at": time.Now(),
|
|
}
|
|
|
|
return u.UpdateUser(ctx, userID, updateData)
|
|
}
|
|
|
|
// UpdateUserStatus updates user account status (active, disabled, suspended, etc.)
|
|
func (u *DefaultUser) UpdateUserStatus(ctx context.Context, userID string, status string) error {
|
|
updateData := maps.MapStrAny{
|
|
"status": status,
|
|
}
|
|
|
|
return u.UpdateUser(ctx, userID, updateData)
|
|
}
|