完善初始化更新
This commit is contained in:
214
sproutgate-backend/internal/storage/registration.go
Normal file
214
sproutgate-backend/internal/storage/registration.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sproutgate-backend/internal/models"
|
||||
)
|
||||
|
||||
// InviteEntry 管理员发放的注册邀请码。
|
||||
type InviteEntry struct {
|
||||
Code string `json:"code"`
|
||||
Note string `json:"note,omitempty"`
|
||||
MaxUses int `json:"maxUses"` // 0 表示不限次数
|
||||
Uses int `json:"uses"`
|
||||
ExpiresAt string `json:"expiresAt,omitempty"` // RFC3339,空表示不过期
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
// RegistrationConfig 注册策略与邀请码列表(data/config/registration.json)。
|
||||
type RegistrationConfig struct {
|
||||
RequireInviteCode bool `json:"requireInviteCode"`
|
||||
Invites []InviteEntry `json:"invites"`
|
||||
}
|
||||
|
||||
func normalizeInviteCode(raw string) string {
|
||||
return strings.ToUpper(strings.TrimSpace(raw))
|
||||
}
|
||||
|
||||
func (s *Store) loadOrCreateRegistrationConfig() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if _, err := os.Stat(s.registrationPath); errors.Is(err, os.ErrNotExist) {
|
||||
cfg := RegistrationConfig{RequireInviteCode: false, Invites: []InviteEntry{}}
|
||||
if err := writeJSONFile(s.registrationPath, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
s.registrationConfig = cfg
|
||||
return nil
|
||||
}
|
||||
var cfg RegistrationConfig
|
||||
if err := readJSONFile(s.registrationPath, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.Invites == nil {
|
||||
cfg.Invites = []InviteEntry{}
|
||||
}
|
||||
s.registrationConfig = cfg
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) persistRegistrationConfigLocked() error {
|
||||
return writeJSONFile(s.registrationPath, s.registrationConfig)
|
||||
}
|
||||
|
||||
// RegistrationRequireInvite 是否强制要求邀请码才能发起注册(发邮件验证码)。
|
||||
func (s *Store) RegistrationRequireInvite() bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.registrationConfig.RequireInviteCode
|
||||
}
|
||||
|
||||
// GetRegistrationConfig 返回配置副本(管理端)。
|
||||
func (s *Store) GetRegistrationConfig() RegistrationConfig {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
out := s.registrationConfig
|
||||
out.Invites = append([]InviteEntry(nil), s.registrationConfig.Invites...)
|
||||
return out
|
||||
}
|
||||
|
||||
// SetRegistrationRequireInvite 更新是否强制邀请码。
|
||||
func (s *Store) SetRegistrationRequireInvite(require bool) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.registrationConfig.RequireInviteCode = require
|
||||
return s.persistRegistrationConfigLocked()
|
||||
}
|
||||
|
||||
func inviteEntryValid(e *InviteEntry) error {
|
||||
if strings.TrimSpace(e.ExpiresAt) != "" {
|
||||
t, err := time.Parse(time.RFC3339, e.ExpiresAt)
|
||||
if err == nil && time.Now().After(t) {
|
||||
return errors.New("invite code expired")
|
||||
}
|
||||
}
|
||||
if e.MaxUses > 0 && e.Uses >= e.MaxUses {
|
||||
return errors.New("invite code has been fully used")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateInviteForRegister 校验邀请码是否可用(发验证码前,不扣次)。
|
||||
func (s *Store) ValidateInviteForRegister(code string) error {
|
||||
n := normalizeInviteCode(code)
|
||||
if n == "" {
|
||||
return errors.New("invite code is required")
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for i := range s.registrationConfig.Invites {
|
||||
e := &s.registrationConfig.Invites[i]
|
||||
if strings.EqualFold(e.Code, n) {
|
||||
return inviteEntryValid(e)
|
||||
}
|
||||
}
|
||||
return errors.New("invalid invite code")
|
||||
}
|
||||
|
||||
// RedeemInvite 邮箱验证通过创建用户后扣减邀请码使用次数。
|
||||
func (s *Store) RedeemInvite(code string) error {
|
||||
n := normalizeInviteCode(code)
|
||||
if n == "" {
|
||||
return nil
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for i := range s.registrationConfig.Invites {
|
||||
e := &s.registrationConfig.Invites[i]
|
||||
if strings.EqualFold(e.Code, n) {
|
||||
if err := inviteEntryValid(e); err != nil {
|
||||
return err
|
||||
}
|
||||
e.Uses++
|
||||
return s.persistRegistrationConfigLocked()
|
||||
}
|
||||
}
|
||||
return errors.New("invalid invite code")
|
||||
}
|
||||
|
||||
const inviteCodeAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||
|
||||
func randomInviteToken(n int) (string, error) {
|
||||
b := make([]byte, n)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.Grow(n)
|
||||
for i := 0; i < n; i++ {
|
||||
sb.WriteByte(inviteCodeAlphabet[int(b[i])%len(inviteCodeAlphabet)])
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// AddInviteEntry 生成新邀请码并写入配置。
|
||||
func (s *Store) AddInviteEntry(note string, maxUses int, expiresAt string) (InviteEntry, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
var code string
|
||||
for attempt := 0; attempt < 24; attempt++ {
|
||||
c, err := randomInviteToken(8)
|
||||
if err != nil {
|
||||
return InviteEntry{}, err
|
||||
}
|
||||
dup := false
|
||||
for _, ex := range s.registrationConfig.Invites {
|
||||
if strings.EqualFold(ex.Code, c) {
|
||||
dup = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !dup {
|
||||
code = c
|
||||
break
|
||||
}
|
||||
}
|
||||
if code == "" {
|
||||
return InviteEntry{}, errors.New("failed to generate unique invite code")
|
||||
}
|
||||
expiresAt = strings.TrimSpace(expiresAt)
|
||||
if expiresAt != "" {
|
||||
if _, err := time.Parse(time.RFC3339, expiresAt); err != nil {
|
||||
return InviteEntry{}, errors.New("invalid expiresAt (use RFC3339)")
|
||||
}
|
||||
}
|
||||
if maxUses < 0 {
|
||||
maxUses = 0
|
||||
}
|
||||
entry := InviteEntry{
|
||||
Code: code,
|
||||
Note: strings.TrimSpace(note),
|
||||
MaxUses: maxUses,
|
||||
Uses: 0,
|
||||
ExpiresAt: expiresAt,
|
||||
CreatedAt: models.NowISO(),
|
||||
}
|
||||
s.registrationConfig.Invites = append(s.registrationConfig.Invites, entry)
|
||||
if err := s.persistRegistrationConfigLocked(); err != nil {
|
||||
s.registrationConfig.Invites = s.registrationConfig.Invites[:len(s.registrationConfig.Invites)-1]
|
||||
return InviteEntry{}, err
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// DeleteInviteEntry 按码删除(大小写不敏感)。
|
||||
func (s *Store) DeleteInviteEntry(code string) error {
|
||||
n := normalizeInviteCode(code)
|
||||
if n == "" {
|
||||
return errors.New("code is required")
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for i, e := range s.registrationConfig.Invites {
|
||||
if strings.EqualFold(e.Code, n) {
|
||||
s.registrationConfig.Invites = append(s.registrationConfig.Invites[:i], s.registrationConfig.Invites[i+1:]...)
|
||||
return s.persistRegistrationConfigLocked()
|
||||
}
|
||||
}
|
||||
return errors.New("invite not found")
|
||||
}
|
||||
Reference in New Issue
Block a user