yao/openapi/oauth/providers/user/user_basic.go
Max 7c332c6811 Add go-nanoid dependency and refactor user provider methods
- 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.
2025-08-02 17:49:03 +08:00

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)
}