- store refresh session metadata in redis (ip/user-agent/created_at) - add auth APIs: list sessions, revoke one, revoke all - add web privacy UI for active sessions
This commit is contained in:
@@ -2,11 +2,19 @@ from datetime import datetime, timedelta, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi import Request
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth import repository as auth_repository
|
||||
from app.auth.token_store import get_refresh_token_user_id, revoke_refresh_token_jti, store_refresh_token_jti
|
||||
from app.auth.token_store import (
|
||||
RefreshSession,
|
||||
get_refresh_token_user_id,
|
||||
list_refresh_sessions_for_user,
|
||||
revoke_all_refresh_sessions_for_user,
|
||||
revoke_refresh_token_jti,
|
||||
store_refresh_token_jti,
|
||||
)
|
||||
from app.auth.schemas import (
|
||||
LoginRequest,
|
||||
RefreshTokenRequest,
|
||||
@@ -111,7 +119,13 @@ async def resend_verification_email(
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def login_user(db: AsyncSession, payload: LoginRequest) -> TokenResponse:
|
||||
async def login_user(
|
||||
db: AsyncSession,
|
||||
payload: LoginRequest,
|
||||
*,
|
||||
ip_address: str | None = None,
|
||||
user_agent: str | None = None,
|
||||
) -> TokenResponse:
|
||||
user = await get_user_by_email(db, payload.email)
|
||||
if not user or not verify_password(payload.password, user.password_hash):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
|
||||
@@ -121,14 +135,26 @@ async def login_user(db: AsyncSession, payload: LoginRequest) -> TokenResponse:
|
||||
|
||||
refresh_jti = str(uuid4())
|
||||
refresh_token = create_refresh_token(str(user.id), jti=refresh_jti)
|
||||
await store_refresh_token_jti(user_id=user.id, jti=refresh_jti, ttl_seconds=_refresh_ttl_seconds())
|
||||
await store_refresh_token_jti(
|
||||
user_id=user.id,
|
||||
jti=refresh_jti,
|
||||
ttl_seconds=_refresh_ttl_seconds(),
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
)
|
||||
return TokenResponse(
|
||||
access_token=create_access_token(str(user.id)),
|
||||
refresh_token=refresh_token,
|
||||
)
|
||||
|
||||
|
||||
async def refresh_tokens(db: AsyncSession, payload: RefreshTokenRequest) -> TokenResponse:
|
||||
async def refresh_tokens(
|
||||
db: AsyncSession,
|
||||
payload: RefreshTokenRequest,
|
||||
*,
|
||||
ip_address: str | None = None,
|
||||
user_agent: str | None = None,
|
||||
) -> TokenResponse:
|
||||
credentials_error = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid refresh token",
|
||||
@@ -155,13 +181,40 @@ async def refresh_tokens(db: AsyncSession, payload: RefreshTokenRequest) -> Toke
|
||||
|
||||
await revoke_refresh_token_jti(jti=refresh_jti)
|
||||
new_jti = str(uuid4())
|
||||
await store_refresh_token_jti(user_id=int(user_id), jti=new_jti, ttl_seconds=_refresh_ttl_seconds())
|
||||
await store_refresh_token_jti(
|
||||
user_id=int(user_id),
|
||||
jti=new_jti,
|
||||
ttl_seconds=_refresh_ttl_seconds(),
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
)
|
||||
return TokenResponse(
|
||||
access_token=create_access_token(str(user_id)),
|
||||
refresh_token=create_refresh_token(str(user_id), jti=new_jti),
|
||||
)
|
||||
|
||||
|
||||
async def list_user_sessions(user_id: int) -> list[RefreshSession]:
|
||||
return await list_refresh_sessions_for_user(user_id=user_id)
|
||||
|
||||
|
||||
async def revoke_user_session(*, user_id: int, jti: str) -> None:
|
||||
active_user_id = await get_refresh_token_user_id(jti=jti)
|
||||
if active_user_id is None or active_user_id != user_id:
|
||||
return
|
||||
await revoke_refresh_token_jti(jti=jti)
|
||||
|
||||
|
||||
async def revoke_all_user_sessions(*, user_id: int) -> None:
|
||||
await revoke_all_refresh_sessions_for_user(user_id=user_id)
|
||||
|
||||
|
||||
def get_request_metadata(request: Request) -> tuple[str | None, str | None]:
|
||||
ip_address = request.client.host if request.client else None
|
||||
user_agent = request.headers.get("user-agent")
|
||||
return ip_address, user_agent
|
||||
|
||||
|
||||
async def request_password_reset(
|
||||
db: AsyncSession,
|
||||
payload: RequestPasswordResetRequest,
|
||||
|
||||
Reference in New Issue
Block a user