mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
Public groups have always been bindable by every user: CanBindGroup returned true for any non-exclusive group, and user_allowed_groups only ever carried the exclusive groups an admin had granted. Admins had no way to hand a single user a subset of the public groups short of converting a group to exclusive, which changes it for everyone already using it. A user now carries restrict_public_groups. While it is false, which is the default and what every existing row migrates to, nothing changes: every public group stays bindable. Once an admin turns it on for a user, that user's public groups are narrowed to the ones listed in user_allowed_groups, the same table that already gates exclusive groups. The flag is an administrative control, so it rides on the admin user DTO only and leaves the shape of the end-user endpoints alone. The model plaza filter honours the flag as well, so a restricted user is not shown groups they would be refused when binding a key. Anonymous visitors have no user record and keep the previous view. Enforcement rides on the existing CanBindGroup choke point, so it covers key creation, key updates, and per-request authorization together. An API key bound to a group that is later withdrawn stops working at request time rather than lingering as a key that can be listed but not used. The admin dialog gains a toggle over the public group list. Turning it off re-checks every public group, so an admin cannot save a list that reads as restrictive while the restriction itself is disabled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
111 lines
3.5 KiB
Go
111 lines
3.5 KiB
Go
package service
|
|
|
|
import (
|
|
"time"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
type User struct {
|
|
ID int64
|
|
Email string
|
|
Username string
|
|
Notes string
|
|
AvatarURL string
|
|
AvatarSource string
|
|
AvatarMIME string
|
|
AvatarByteSize int
|
|
AvatarSHA256 string
|
|
PasswordHash string
|
|
Role string
|
|
Balance float64
|
|
FrozenBalance float64
|
|
Concurrency int
|
|
Status string
|
|
AllowedGroups []int64
|
|
// RestrictPublicGroups narrows the public groups this user may bind to the
|
|
// ones listed in AllowedGroups. False keeps the default, where every public
|
|
// group is bindable.
|
|
RestrictPublicGroups bool
|
|
TokenVersion int64 // Incremented on password change to invalidate existing tokens
|
|
// TokenVersionResolved indicates TokenVersion already contains the fingerprint-derived
|
|
// value expected in JWT claims and refresh-token state.
|
|
TokenVersionResolved bool
|
|
SignupSource string
|
|
LastLoginAt *time.Time
|
|
LastActiveAt *time.Time
|
|
LastUsedAt *time.Time
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
DeletedAt *time.Time // 非 nil 表示用户已软删除
|
|
|
|
// GroupRates 用户专属分组倍率配置
|
|
// map[groupID]rateMultiplier
|
|
GroupRates map[int64]float64
|
|
|
|
// TOTP 双因素认证字段
|
|
TotpSecretEncrypted *string // AES-256-GCM 加密的 TOTP 密钥
|
|
TotpEnabled bool // 是否启用 TOTP
|
|
TotpEnabledAt *time.Time // TOTP 启用时间
|
|
|
|
// 余额不足通知
|
|
BalanceNotifyEnabled bool
|
|
BalanceNotifyThresholdType string // "fixed" (default) | "percentage"
|
|
BalanceNotifyThreshold *float64
|
|
BalanceNotifyExtraEmails []NotifyEmailEntry
|
|
TotalRecharged float64
|
|
|
|
// RPMLimit 用户级每分钟请求数上限(0 = 不限制)。仅在所用分组未设置 rpm_limit
|
|
// 且该 (用户, 分组) 无 rpm_override 时作为全局兜底生效,计数键 rpm:u:{userID}:{min}。
|
|
RPMLimit int
|
|
|
|
// UserGroupRPMOverride 来自 auth cache snapshot 的 (user, group) RPM 覆盖值。
|
|
// nil = 该 API Key 对应的 (user, group) 无 override;非 nil 时 checkRPM 直接使用,
|
|
// 避免每请求查 DB。字段不持久化到数据库。
|
|
UserGroupRPMOverride *int
|
|
|
|
APIKeys []APIKey
|
|
Subscriptions []UserSubscription
|
|
}
|
|
|
|
func (u *User) IsAdmin() bool {
|
|
return u.Role == RoleAdmin
|
|
}
|
|
|
|
func (u *User) IsActive() bool {
|
|
return u.Status == StatusActive
|
|
}
|
|
|
|
// CanBindGroup checks whether a user can bind to a given group.
|
|
// For standard groups:
|
|
// - Public groups (non-exclusive): bindable by every user, unless the user has
|
|
// RestrictPublicGroups set, in which case the group must be in AllowedGroups
|
|
// - Exclusive groups: only users with the group in AllowedGroups can bind
|
|
func (u *User) CanBindGroup(groupID int64, isExclusive bool) bool {
|
|
// 公开分组(非专属):默认所有用户都可以绑定;仅当该用户开启了公开分组
|
|
// 限制时,才需要落在 AllowedGroups 中。
|
|
if !isExclusive && !u.RestrictPublicGroups {
|
|
return true
|
|
}
|
|
// 专属分组,以及受限用户的公开分组:需要在 AllowedGroups 中
|
|
for _, id := range u.AllowedGroups {
|
|
if id == groupID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (u *User) SetPassword(password string) error {
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
u.PasswordHash = string(hash)
|
|
return nil
|
|
}
|
|
|
|
func (u *User) CheckPassword(password string) bool {
|
|
return bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) == nil
|
|
}
|