Files
2026-06-01 18:59:55 +08:00

147 lines
4.3 KiB
Go

package store
import (
"strings"
"mail_go/internal/db"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// UserStore defines the interface for user data operations.
type UserStore interface {
Create(user *db.User) error
GetByID(id uint) (*db.User, error)
GetByUsername(username string, domainID uint) (*db.User, error)
GetByEmail(email string) (*db.User, error)
Authenticate(email, password string) (*db.User, error)
Update(user *db.User) error
Delete(id uint) error
List(domainID uint, page, size int) ([]db.User, int64, error)
ListAll(page, size int) ([]db.User, int64, error)
UpdateUsedBytes(id uint, delta int64) error
UpdatePassword(userID uint, hashedPassword string) error
}
// userStoreGorm implements UserStore using GORM.
type userStoreGorm struct {
db *gorm.DB
}
// newUserStore creates a new GORM-backed UserStore.
func newUserStore(database *gorm.DB) UserStore {
return &userStoreGorm{db: database}
}
// Create inserts a new user record.
func (s *userStoreGorm) Create(user *db.User) error {
return s.db.Create(user).Error
}
// GetByID retrieves a user by primary key.
func (s *userStoreGorm) GetByID(id uint) (*db.User, error) {
var user db.User
if err := s.db.Preload("Domain").First(&user, id).Error; err != nil {
return nil, err
}
return &user, nil
}
// GetByUsername retrieves a user by username and domain ID.
func (s *userStoreGorm) GetByUsername(username string, domainID uint) (*db.User, error) {
var user db.User
if err := s.db.Where("username = ? AND domain_id = ?", username, domainID).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
// GetByEmail retrieves a user by email address (user@domain format).
func (s *userStoreGorm) GetByEmail(email string) (*db.User, error) {
parts := strings.SplitN(email, "@", 2)
if len(parts) != 2 {
return nil, ErrInvalidEmail
}
username := parts[0]
domainName := parts[1]
var user db.User
if err := s.db.Joins("JOIN domains ON domains.id = users.domain_id").
Where("users.username = ? AND domains.name = ?", username, domainName).
Preload("Domain").
First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
// Authenticate verifies an email/password combination and returns the user on success.
func (s *userStoreGorm) Authenticate(email, password string) (*db.User, error) {
user, err := s.GetByEmail(email)
if err != nil {
return nil, ErrInvalidCredentials
}
if !user.IsActive {
return nil, ErrUserInactive
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
return nil, ErrInvalidCredentials
}
return user, nil
}
// Update saves changes to an existing user record.
func (s *userStoreGorm) Update(user *db.User) error {
return s.db.Save(user).Error
}
// Delete removes a user by ID (soft delete if supported, hard delete otherwise).
func (s *userStoreGorm) Delete(id uint) error {
return s.db.Delete(&db.User{}, id).Error
}
// List retrieves a paginated list of users for a given domain.
func (s *userStoreGorm) List(domainID uint, page, size int) ([]db.User, int64, error) {
var users []db.User
var total int64
query := s.db.Where("domain_id = ?", domainID)
if err := query.Model(&db.User{}).Count(&total).Error; err != nil {
return nil, 0, err
}
offset := (page - 1) * size
if err := s.db.Preload("Domain").Where("domain_id = ?", domainID).Offset(offset).Limit(size).Find(&users).Error; err != nil {
return nil, 0, err
}
return users, total, nil
}
// UpdateUsedBytes atomically adjusts the UsedBytes field by delta.
func (s *userStoreGorm) UpdateUsedBytes(id uint, delta int64) error {
return s.db.Model(&db.User{}).Where("id = ?", id).
Update("used_bytes", gorm.Expr("used_bytes + ?", delta)).Error
}
// UpdatePassword updates the password hash for a user.
func (s *userStoreGorm) UpdatePassword(userID uint, hashedPassword string) error {
return s.db.Model(&db.User{}).Where("id = ?", userID).Update("password_hash", hashedPassword).Error
}
// ListAll retrieves a paginated list of all users across all domains.
func (s *userStoreGorm) ListAll(page, size int) ([]db.User, int64, error) {
var users []db.User
var total int64
if err := s.db.Model(&db.User{}).Count(&total).Error; err != nil {
return nil, 0, err
}
offset := (page - 1) * size
if err := s.db.Preload("Domain").Offset(offset).Limit(size).Find(&users).Error; err != nil {
return nil, 0, err
}
return users, total, nil
}