feat: JWT过期自动重置 + 用户端登录页 + 系统设置前三个真实实现
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

agent-111 JWT过期重置+系统设置去占位:
- ws.ts 识别401/过期 markAuthExpired(清token+停重连+expired态),isTokenExpired;wsParse.ts拆出压行371;scheduleReconnect有token未过期才重连
- api.ts REST 401→auth_expired+清token+引导重登;login走skipAuthExpired密码错不误清
- types.ts WsStatus加expired;TopBar/Nav expired显示登录过期;System.tsx去alert占位改4张事实卡+合并Notice

agent-113 用户端登录页:
- components/Login.tsx Miuix登录卡(用户名/密码→login()→setAuthToken);App !getAuthToken()→<Login>;AdminApp LoginGate指向

agent-112 系统设置前三个(owner+审计):
- auth.APIKey.RevokeAll(吊销全部key);auth.JWTService.RotateSecret(≥32校验+并发安全+新密钥不回显)
- gateway admin_system.go: POST revoke-keys/rotate-jwt/backup(VACUUM INTO)/restore(防路径穿越+恢复前自动备份+原子替换+需重启)
- wire.go 传DatabasePath;全仓 build/vet/test绿+交叉编译;api-client v0.17系统设置契约

实机验证: 登录(role=owner)+revoke-keys 200+渠道列表含kook; 全仓绿+前端build绿; .env密码仅登录验证不打印
This commit is contained in:
Aska committed 2026-09-07 13:57:36 +08:00
1 parent 7e39153a69
commit 1ebc82fffd
19 files changed
+1164 -245

No files matched your search

+26 -2
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"log/slog"
"strings"
"sync"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
@@ -28,8 +29,11 @@ type jwtHeader struct {
Typ string `json:"typ"`
}
// JWTService signs and verifies HS256 JWTs using a shared secret.
// JWTService signs and verifies HS256 JWTs using a shared secret. The secret is
// protected by a RWMutex so RotateSecret can swap it while Sign/Verify read it
// concurrently (the gateway rotates the secret live from the admin panel).
type JWTService struct {
mu sync.RWMutex
secret []byte
log *slog.Logger
}
@@ -76,10 +80,27 @@ func (s *JWTService) Sign(sub string, role Role, ttl time.Duration) (string, err
return "", fmt.Errorf("auth: encode payload: %w", err)
}
signingInput := headerB64 + "." + payloadB64
s.mu.RLock()
sig := hmacSign(s.secret, []byte(signingInput))
s.mu.RUnlock()
return signingInput + "." + base64.RawURLEncoding.EncodeToString(sig), nil
}
// RotateSecret replaces the signing secret with newSecret, invalidating every
// token issued under the previous secret. It returns ErrInvalidSecret when
// newSecret is shorter than the 32-character minimum. The new secret is never
// logged or echoed back to the caller.
func (s *JWTService) RotateSecret(newSecret string) error {
if len(newSecret) < minSecretLen {
return fmt.Errorf("%w: got %d chars", ErrInvalidSecret, len(newSecret))
}
s.mu.Lock()
s.secret = []byte(newSecret)
s.mu.Unlock()
s.log.Info("jwt secret rotated; previously issued tokens are now invalid")
return nil
}
// Verify validates the token signature, algorithm, role, and expiry, returning
// the parsed Claims on success.
func (s *JWTService) Verify(token string) (*Claims, error) {
@@ -94,7 +115,10 @@ func (s *JWTService) Verify(token string) (*Claims, error) {
if err != nil {
return nil, fmt.Errorf("%w: decode signature: %v", ErrMalformedToken, err)
}
if !hmac.Equal(provided, hmacSign(s.secret, []byte(signingInput))) {
s.mu.RLock()
expected := hmacSign(s.secret, []byte(signingInput))
s.mu.RUnlock()
if !hmac.Equal(provided, expected) {
return nil, ErrInvalidSignature
}