88from typing import Any
99
1010import jwt
11- from fastapi import APIRouter , HTTPException , Request
11+ from fastapi import APIRouter , Depends , HTTPException , Request
12+ from fastapi .security import HTTPBasicCredentials
1213
1314from fastapi_admin_kit .api .schemas import (
1415 RefreshRequest ,
1516 RefreshResponse ,
1617 TokenRequest ,
1718 TokenResponse ,
1819)
20+ from fastapi_admin_kit .api .security import basic_scheme , bearer_scheme
1921from fastapi_admin_kit .auth .ratelimit import RateLimiter , check_rate_limit
2022from fastapi_admin_kit .db import get_db_session
2123
@@ -161,10 +163,22 @@ def _hash_token(token: str) -> str:
161163@router .post ("/token" , response_model = TokenResponse )
162164async def obtain_token (
163165 request : Request ,
164- body : TokenRequest ,
166+ body : TokenRequest | None = None ,
167+ credentials : HTTPBasicCredentials | None = Depends (basic_scheme ),
165168) -> TokenResponse :
166- """POST /api/auth/token — obtain JWT access + refresh tokens."""
167- check_rate_limit (_api_rate_limiter , body .email )
169+ """POST /api/auth/token — obtain JWT access + refresh tokens.
170+
171+ Credentials may be provided either as a JSON body (``email``/``password``)
172+ or via HTTP Basic auth. Basic auth takes precedence when both are given.
173+ """
174+ if credentials is not None :
175+ email , password = credentials .username , credentials .password
176+ elif body is not None :
177+ email , password = body .email , body .password
178+ else :
179+ raise HTTPException (status_code = 422 , detail = "Credentials required." )
180+
181+ check_rate_limit (_api_rate_limiter , email )
168182
169183 auth_backend = getattr (request .app .state , "admin_auth_backend" , None )
170184 if auth_backend is None :
@@ -174,12 +188,12 @@ async def obtain_token(
174188 if db_session is None :
175189 raise HTTPException (status_code = 500 , detail = "Database session not available." )
176190
177- user = await auth_backend .authenticate (body . email , body . password , db_session )
191+ user = await auth_backend .authenticate (email , password , db_session )
178192 if user is None :
179- _api_rate_limiter .record_attempt (body . email )
193+ _api_rate_limiter .record_attempt (email )
180194 raise HTTPException (status_code = 401 , detail = "Invalid credentials." )
181195
182- _api_rate_limiter .reset (body . email )
196+ _api_rate_limiter .reset (email )
183197
184198 secret_key = _get_secret_key (request )
185199 ttl = _get_token_ttl (request )
@@ -221,6 +235,7 @@ async def refresh_token(
221235 raise HTTPException (status_code = 500 , detail = "Database session not available." )
222236
223237 from sqlalchemy import select
238+ from sqlalchemy .orm import selectinload
224239
225240 from fastapi_admin_kit .auth .models import RefreshToken , User
226241
@@ -235,12 +250,17 @@ async def refresh_token(
235250 if refresh_record is None :
236251 raise HTTPException (status_code = 401 , detail = "Invalid refresh token." )
237252
238- if refresh_record .expires_at < datetime .now (UTC ):
253+ expires_at = refresh_record .expires_at
254+ if expires_at .tzinfo is None :
255+ expires_at = expires_at .replace (tzinfo = UTC )
256+ if expires_at < datetime .now (UTC ):
239257 raise HTTPException (status_code = 401 , detail = "Refresh token expired." )
240258
241- # Load user
259+ # Load user (eagerly load roles to avoid lazy-load in async session)
242260 user = await db_session .scalar_one_or_none (
243- select (User ).where (
261+ select (User )
262+ .options (selectinload (User .roles ))
263+ .where (
244264 User .id == refresh_record .user_id ,
245265 User .is_active ,
246266 )
@@ -305,6 +325,7 @@ async def api_logout(
305325@router .get ("/me" )
306326async def get_current_user_info (
307327 request : Request ,
328+ _ : Any = Depends (bearer_scheme ),
308329) -> dict [str , Any ]:
309330 """GET /api/auth/me — return current user info from JWT (no DB hit)."""
310331 auth_header = request .headers .get ("Authorization" , "" )
0 commit comments