Initial Claude implementation
This commit is contained in:
commit
e2d3ab9c2a
63 files changed
+3930
No files matched your search
@@ -0,0 +1,65 @@
|
||||
"""Token issuance / rotation shared by the JSON API and the HTMX UI."""
|
||||
import jwt
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.security import (
|
||||
REFRESH_TOKEN_TYPE,
|
||||
create_access_token,
|
||||
create_refresh_token,
|
||||
decode_token,
|
||||
)
|
||||
from app.crud import token as token_crud
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
async def issue_token_pair(db: AsyncSession, user: User) -> tuple[str, str]:
|
||||
"""Create an access + refresh token and persist the refresh token for revocation."""
|
||||
access = create_access_token(user.id)
|
||||
refresh, payload = create_refresh_token(user.id)
|
||||
await token_crud.store(
|
||||
db,
|
||||
user_id=user.id,
|
||||
jti=payload["jti"],
|
||||
raw_token=refresh,
|
||||
expires_at=payload["exp"], # timezone-aware UTC datetime
|
||||
)
|
||||
return access, refresh
|
||||
|
||||
|
||||
class RefreshError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
async def rotate_refresh_token(db: AsyncSession, raw_refresh: str) -> tuple[str, str, User]:
|
||||
"""Validate a refresh token, revoke it, and issue a fresh pair.
|
||||
|
||||
Raises ``RefreshError`` if the token is invalid, expired, or already revoked.
|
||||
"""
|
||||
try:
|
||||
payload = decode_token(raw_refresh)
|
||||
except jwt.PyJWTError as exc:
|
||||
raise RefreshError("Invalid refresh token") from exc
|
||||
|
||||
if payload.get("type") != REFRESH_TOKEN_TYPE:
|
||||
raise RefreshError("Not a refresh token")
|
||||
|
||||
jti = payload.get("jti")
|
||||
sub = payload.get("sub")
|
||||
if not jti or sub is None:
|
||||
raise RefreshError("Malformed refresh token")
|
||||
|
||||
record = await token_crud.get_active(db, jti=jti, raw_token=raw_refresh)
|
||||
if record is None:
|
||||
raise RefreshError("Refresh token is revoked or expired")
|
||||
|
||||
# Local import avoids a circular import at module load time.
|
||||
from app.crud import user as user_crud
|
||||
|
||||
user = await user_crud.get(db, int(sub))
|
||||
if user is None or not user.is_active:
|
||||
raise RefreshError("User is inactive or missing")
|
||||
|
||||
# Rotate: revoke the presented token, then mint a new pair.
|
||||
await token_crud.revoke(db, record)
|
||||
access, refresh = await issue_token_pair(db, user)
|
||||
return access, refresh, user
|
||||
Reference in new issue
Block a user