mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
162 lines
5.3 KiB
Go
162 lines
5.3 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
|
)
|
|
|
|
// UserRepository persists model.User records.
|
|
type UserRepository struct{ db *gorm.DB }
|
|
|
|
// Create inserts a new user. Caller must pre-hash the password.
|
|
func (r *UserRepository) Create(ctx context.Context, u *model.User) error {
|
|
return r.db.WithContext(ctx).Create(u).Error
|
|
}
|
|
|
|
// ReleaseDeletedUsername renames soft-deleted rows that still hold a unique
|
|
// username so the same account name can be created again.
|
|
func (r *UserRepository) ReleaseDeletedUsername(ctx context.Context, username string) error {
|
|
if username == "" {
|
|
return nil
|
|
}
|
|
released := username + "__deleted__" + time.Now().Format("20060102150405.000000000")
|
|
if len(released) > 64 {
|
|
sum := sha256.Sum256([]byte(released))
|
|
released = username
|
|
if len(released) > 43 {
|
|
released = released[:43]
|
|
}
|
|
released += "__deleted__" + hex.EncodeToString(sum[:])[:10]
|
|
}
|
|
return r.db.WithContext(ctx).Unscoped().
|
|
Model(&model.User{}).
|
|
Where("username = ? AND deleted_at IS NOT NULL", username).
|
|
Update("username", released).Error
|
|
}
|
|
|
|
// FindByUsername returns the user matching username, or (nil, nil) when absent.
|
|
func (r *UserRepository) FindByUsername(ctx context.Context, username string) (*model.User, error) {
|
|
var u model.User
|
|
err := withSQLiteBusyRetry(ctx, func() error {
|
|
u = model.User{}
|
|
err := r.db.WithContext(ctx).Where("username = ?", username).First(&u).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) && username != "" {
|
|
err = r.db.WithContext(ctx).Where("LOWER(username) = LOWER(?)", username).First(&u).Error
|
|
}
|
|
return err
|
|
})
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &u, nil
|
|
}
|
|
|
|
// FindByID returns the user with the matching primary key, or (nil, nil).
|
|
func (r *UserRepository) FindByID(ctx context.Context, id string) (*model.User, error) {
|
|
var u model.User
|
|
err := withSQLiteBusyRetry(ctx, func() error {
|
|
u = model.User{}
|
|
return r.db.WithContext(ctx).Where("id = ?", id).First(&u).Error
|
|
})
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &u, nil
|
|
}
|
|
|
|
// Count returns the total number of non-deleted users.
|
|
func (r *UserRepository) Count(ctx context.Context) (int64, error) {
|
|
var n int64
|
|
err := r.db.WithContext(ctx).Model(&model.User{}).Count(&n).Error
|
|
return n, err
|
|
}
|
|
|
|
// CountAdmins returns the number of users that hold the admin role.
|
|
func (r *UserRepository) CountAdmins(ctx context.Context) (int64, error) {
|
|
var n int64
|
|
err := r.db.WithContext(ctx).Model(&model.User{}).
|
|
Where("role = ?", "admin").Count(&n).Error
|
|
return n, err
|
|
}
|
|
|
|
// FirstAdmin returns the earliest admin user. This row represents the protected
|
|
// built-in/default administrator even if its username is later changed.
|
|
func (r *UserRepository) FirstAdmin(ctx context.Context) (*model.User, error) {
|
|
var u model.User
|
|
err := r.db.WithContext(ctx).Where("role = ?", "admin").Order("created_at asc").First(&u).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &u, nil
|
|
}
|
|
|
|
// List returns all users ordered by creation time desc.
|
|
func (r *UserRepository) List(ctx context.Context) ([]model.User, error) {
|
|
var users []model.User
|
|
err := r.db.WithContext(ctx).Order("created_at desc").Find(&users).Error
|
|
return users, err
|
|
}
|
|
|
|
// UpdateFields applies a narrow set of user field updates.
|
|
func (r *UserRepository) UpdateFields(ctx context.Context, id string, updates map[string]any) error {
|
|
return r.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", id).Updates(updates).Error
|
|
}
|
|
|
|
// UpdatePassword sets a new password hash and clears ForcePasswordReset.
|
|
func (r *UserRepository) UpdatePassword(ctx context.Context, id, hash string) error {
|
|
return r.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", id).
|
|
Updates(map[string]any{"password_hash": hash, "force_password_reset": false}).Error
|
|
}
|
|
|
|
// TouchLogin updates the last login timestamp.
|
|
func (r *UserRepository) TouchLogin(ctx context.Context, id string) error {
|
|
now := time.Now()
|
|
return withSQLiteBusyRetry(ctx, func() error {
|
|
return r.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", id).
|
|
Update("last_login_at", &now).Error
|
|
})
|
|
}
|
|
|
|
// Delete removes a user (soft-delete via gorm.DeletedAt), releases the unique
|
|
// username, and drops Telegram bindings so future re-created users bind cleanly.
|
|
func (r *UserRepository) Delete(ctx context.Context, id string) error {
|
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
var user model.User
|
|
if err := tx.Where("id = ?", id).First(&user).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Unscoped().Where("user_id = ?", id).Delete(&model.TelegramBinding{}).Error; err != nil {
|
|
return err
|
|
}
|
|
released := user.Username + "__deleted__" + time.Now().Format("20060102150405.000000000")
|
|
if len(released) > 64 {
|
|
sum := sha256.Sum256([]byte(user.ID + user.Username))
|
|
base := user.Username
|
|
if len(base) > 43 {
|
|
base = base[:43]
|
|
}
|
|
released = base + "__deleted__" + hex.EncodeToString(sum[:])[:10]
|
|
}
|
|
if err := tx.Model(&model.User{}).Where("id = ?", id).Update("username", released).Error; err != nil {
|
|
return err
|
|
}
|
|
return tx.Delete(&model.User{}, "id = ?", id).Error
|
|
})
|
|
}
|