refactor: 拆分 middleware/router 为单一职责文件,新增站点设置管理页面

middleware:
- 拆分 auth.go → auth.go + auth_token.go + auth_parser.go + auth_admin.go
- 拆分 csrf.go → csrf.go + csrf_token.go
- 拆分 ratelimit.go → ratelimit.go + ratelimit_core.go + ratelimit_cleanup.go
- 修复 import 未使用/缺失问题

router:
- 拆分 router.go → admin.go + api.go + frontend.go + deps_core.go + deps_extra.go

feat(admin): 站点设置管理页面
- 新增 SSR 页面 /admin/site-settings(仅 Owner)
- 审核开关 Toggle 即时保存
- 通用设置展示
- 侧边栏新增站点设置入口
- 注册 UpdateBoolSetting/GetBoolSetting API 路由
This commit is contained in:
2026-05-27 15:03:04 +08:00
parent a19b1fcb51
commit 7bc2dbd970
20 changed files with 774 additions and 446 deletions

View File

@ -1,47 +1,16 @@
package middleware
import (
"fmt"
"sync"
"time"
)
import "sync"
// RateLimiter 双维度滑动窗口登录限流
// 维度一(账户):同邮箱 1 分钟内失败 3 次 → 锁定 1 分钟
// 维度二IP同 IP 1 分钟内失败 10 次 → 锁定 1 分钟
type RateLimiter struct {
mu sync.RWMutex
// accountFailures key = 邮箱(小写)
mu sync.RWMutex
accountFailures map[string]*windowState
// ipFailures key = IP
ipFailures map[string]*windowState
ipFailures map[string]*windowState
}
// windowState 单个维度的限流状态
type windowState struct {
count int
windowStart time.Time
blockedUntil time.Time // 零值为未封锁
}
// RateLimitResult 限流检查结果
type RateLimitResult struct {
Blocked bool
RetryAfter int // 剩余封锁秒数
Message string
}
const (
accountWindow = 1 * time.Minute
accountMaxFails = 3
accountBlockDur = 1 * time.Minute
ipWindow = 1 * time.Minute
ipMaxFails = 10
ipBlockDur = 1 * time.Minute
cleanupInterval = 2 * time.Minute
maxEntries = 10000 // 单维度最大条目数(防内存耗尽)
)
// NewRateLimiter 创建限流器并启动后台清理
func NewRateLimiter() *RateLimiter {
rl := &RateLimiter{
@ -53,7 +22,6 @@ func NewRateLimiter() *RateLimiter {
}
// AllowAccount 检查账户维度是否允许登录尝试
// 返回 (result, 登录失败时应调用的记录函数)
func (rl *RateLimiter) AllowAccount(email string) (RateLimitResult, func()) {
return rl.check(rl.accountFailures, email, accountWindow, accountBlockDur, accountMaxFails, true)
}
@ -62,88 +30,3 @@ func (rl *RateLimiter) AllowAccount(email string) (RateLimitResult, func()) {
func (rl *RateLimiter) AllowIP(ip string) (RateLimitResult, func()) {
return rl.check(rl.ipFailures, ip, ipWindow, ipBlockDur, ipMaxFails, false)
}
// check 核心检查逻辑
// lowKey: 是否需要脱敏日志true = 暗示账户存在,仅泄漏给已知该邮箱的人)
func (rl *RateLimiter) check(m map[string]*windowState, key string, window, blockDur time.Duration, maxFails int, lowKey bool) (RateLimitResult, func()) {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
state, exists := m[key]
if !exists {
state = &windowState{}
if len(m) < maxEntries {
m[key] = state
}
}
// 检查是否处于封锁期
if !state.blockedUntil.IsZero() && now.Before(state.blockedUntil) {
retry := int(state.blockedUntil.Sub(now).Seconds()) + 1
msg := "请求过于频繁,请稍后重试"
if lowKey {
msg = fmt.Sprintf("该账号登录尝试过于频繁,请 %d 秒后重试", retry)
}
return RateLimitResult{Blocked: true, RetryAfter: retry, Message: msg}, nil
}
// 窗口过期 → 重置
if now.Sub(state.windowStart) > window {
state.count = 0
state.windowStart = now
state.blockedUntil = time.Time{}
}
// 记录失败(由调用方在登录失败时调用)的闭包
recordFail := func() {
rl.mu.Lock()
defer rl.mu.Unlock()
s := m[key]
if s == nil {
return
}
if now.Sub(s.windowStart) > window {
s.count = 1
s.windowStart = now
return
}
s.count++
if s.count >= maxFails {
s.blockedUntil = now.Add(blockDur)
}
}
return RateLimitResult{Blocked: false}, recordFail
}
// Clear 登录成功后清除该 email 和 IP 的失败计数
func (rl *RateLimiter) Clear(email, ip string) {
rl.mu.Lock()
defer rl.mu.Unlock()
delete(rl.accountFailures, email)
delete(rl.ipFailures, ip)
}
// cleanupLoop 定期清理过期条目
func (rl *RateLimiter) cleanupLoop() {
ticker := time.NewTicker(cleanupInterval)
defer ticker.Stop()
for range ticker.C {
rl.mu.Lock()
now := time.Now()
clean := func(m map[string]*windowState) {
for k, v := range m {
// 封锁期已过且窗口已过期 → 删除
if (v.blockedUntil.IsZero() || now.After(v.blockedUntil)) &&
now.Sub(v.windowStart) > accountWindow+ipBlockDur {
delete(m, k)
}
}
}
clean(rl.accountFailures)
clean(rl.ipFailures)
rl.mu.Unlock()
}
}