feat(07.1): session revocation on privilege change — CR-01/CR-02/CR-03
- revoke_all_refresh_tokens: add skip_token_hash optional param (exclude
current session while revoking others)
- change_password, enable_totp, disable_totp: call revoke with skip hash
derived from refresh cookie; return sessions_revoked in response and
write to audit log metadata_
- 3 new tests: test_{change_password,enable_totp,disable_totp}_revokes_other_sessions
— all PASSED; 373 total passing, 0 regressions
- Frontend toasts: SettingsAccountTab + TotpEnrollment show
"Other sessions have been terminated." when sessions_revoked > 0
- Companion fixes: rate_limiting get_client_ip refactor, deps/auth.py
request.state.current_user, locustfile refresh-token task removal
- Version bump: 0.1.0 → 0.1.1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
8d060a5da4
commit
c38c6b1c01
@@ -216,17 +216,21 @@ async def rotate_refresh_token(
|
||||
|
||||
|
||||
async def revoke_all_refresh_tokens(
|
||||
session: AsyncSession, user_id: uuid.UUID
|
||||
session: AsyncSession, user_id: uuid.UUID, skip_token_hash: Optional[str] = None
|
||||
) -> int:
|
||||
"""Mark all active refresh tokens for user_id as revoked.
|
||||
|
||||
Returns the count of revoked tokens (supports sign-out-all-devices).
|
||||
skip_token_hash: if set, the token with this hash is excluded (keep current session alive).
|
||||
"""
|
||||
conditions = [
|
||||
RefreshToken.user_id == user_id,
|
||||
RefreshToken.revoked.is_(False),
|
||||
]
|
||||
if skip_token_hash is not None:
|
||||
conditions.append(RefreshToken.token_hash != skip_token_hash)
|
||||
result = await session.execute(
|
||||
select(RefreshToken).where(
|
||||
RefreshToken.user_id == user_id,
|
||||
RefreshToken.revoked.is_(False),
|
||||
)
|
||||
select(RefreshToken).where(*conditions)
|
||||
)
|
||||
rows = result.scalars().all()
|
||||
count = 0
|
||||
|
||||
@@ -4,14 +4,15 @@ from __future__ import annotations
|
||||
from fastapi import Request
|
||||
from slowapi import Limiter
|
||||
|
||||
from deps.utils import get_client_ip
|
||||
|
||||
|
||||
def _account_key(request: Request) -> str:
|
||||
user = getattr(request.state, "current_user", None)
|
||||
if user is not None:
|
||||
return str(user.id)
|
||||
if request.client:
|
||||
return request.client.host
|
||||
return "anonymous"
|
||||
ip = get_client_ip(request)
|
||||
return ip if ip is not None else "anonymous"
|
||||
|
||||
|
||||
account_limiter = Limiter(key_func=_account_key)
|
||||
|
||||
Reference in New Issue
Block a user