feat: 近实时远程下线 — 客户端轮询会话有效性(~15s),服务端 GET /v1/me/session
控制面无推送通道,access token 是无状态 JWT(15min),强制退出后被踢设备要等 token 过期
(≤15min)才登出。改成客户端每 15s 轮询会话是否仍有效,被强制退出即登出 → 延迟压到 ~15s。
- 服务端:sessions.HasActiveSession(user,device) + devices.SessionActive(按 UUID,fail-open)
+ GET /v1/me/session?device_id= 返回 {active}(恒 200,判据在 body)。无新迁移。
- 客户端:account_api.sessionActive + main.dart _RootFlowState 15s 轮询,active=false 即 logout
(网络/鉴权异常不据此登出,fail-safe)。
- 测试:TestSQLite_SessionHasActiveSession(建会话=活跃→RevokeByDevice→非活跃)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,20 @@ func (s *Store) Rotate(ctx context.Context, oldJTI, newJTI string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasActiveSession reports whether the (user, device) has at least one non-revoked
|
||||
// session — i.e. the device has NOT been force-logged-out. Backs the client's
|
||||
// periodic session-validity poll: near-instant remote logout without a push channel
|
||||
// (access token is a stateless JWT, so we can't see revocation on normal requests).
|
||||
func (s *Store) HasActiveSession(ctx context.Context, userID, deviceID int64) (bool, error) {
|
||||
var n int
|
||||
if err := s.db.QueryRowContext(ctx,
|
||||
`SELECT COUNT(1) FROM sessions WHERE user_id=? AND device_id=? AND revoked_at IS NULL`,
|
||||
userID, deviceID).Scan(&n); err != nil {
|
||||
return false, fmt.Errorf("sessions.HasActiveSession: %w", err)
|
||||
}
|
||||
return n > 0, nil
|
||||
}
|
||||
|
||||
// Revoke marks the session with the given JTI revoked (logout). Idempotent.
|
||||
func (s *Store) Revoke(ctx context.Context, jti string) error {
|
||||
now := time.Now().UTC()
|
||||
|
||||
Reference in New Issue
Block a user