66
77from __future__ import annotations
88
9- from fastapi import APIRouter , Depends , Query , status
9+ from fastapi import APIRouter , Depends , Query , Request , status
1010from fastapi .security import HTTPAuthorizationCredentials , HTTPBearer
1111from sqlalchemy .orm import Session
1212
1313from ..controllers import accounts
14+ from ..controllers .errors import TooManyRequests
15+ from ..infra .config import get_settings
16+ from ..infra .db import get_db
17+ from ..infra .ratelimit import limiter
1418from ..models import User
15- from ..models .db import get_db
16- from ..validation .schemas import RefreshIn , RegisterIn , TokenPair , UserOut
19+ from ..validation .schemas import PasswordChange , RefreshIn , RegisterIn , TokenPair , UserOut
1720
1821router = APIRouter (prefix = "/auth" , tags = ["auth" ])
1922_bearer = HTTPBearer (auto_error = False )
2023
2124
25+ def limit_auth (request : Request ) -> None :
26+ """Per-IP limit on unauthenticated auth attempts (brute force / signup
27+ abuse). Kept here rather than in ``limits.py`` to avoid an import
28+ cycle with ``authenticate_user``."""
29+ ip = request .client .host if request .client else "unknown"
30+ s = get_settings ()
31+ allowed , retry_after = limiter .check (f"auth:{ ip } " , s .rate_limit_auth , s .rate_limit_window_s )
32+ if not allowed :
33+ secs = int (retry_after ) + 1
34+ raise TooManyRequests (f"rate limit exceeded; retry in { secs } s" , retry_after = secs )
35+
36+
2237def authenticate_user (
2338 db : Session = Depends (get_db ),
2439 credentials : HTTPAuthorizationCredentials | None = Depends (_bearer ),
@@ -53,12 +68,17 @@ def _user_out(user: User) -> UserOut:
5368 )
5469
5570
56- @router .post ("/register" , response_model = TokenPair , status_code = status .HTTP_201_CREATED )
71+ @router .post (
72+ "/register" ,
73+ response_model = TokenPair ,
74+ status_code = status .HTTP_201_CREATED ,
75+ dependencies = [Depends (limit_auth )],
76+ )
5777def register (body : RegisterIn , db : Session = Depends (get_db )) -> TokenPair :
5878 return _token_pair (accounts .register (db , body .email , body .password ))
5979
6080
61- @router .post ("/login" , response_model = TokenPair )
81+ @router .post ("/login" , response_model = TokenPair , dependencies = [ Depends ( limit_auth )] )
6282def login (body : RegisterIn , db : Session = Depends (get_db )) -> TokenPair :
6383 return _token_pair (accounts .login (db , body .email , body .password ))
6484
@@ -73,6 +93,25 @@ def me(user: User = Depends(authenticate_user)) -> UserOut:
7393 return _user_out (user )
7494
7595
96+ @router .post ("/logout" )
97+ def logout (user : User = Depends (authenticate_user ), db : Session = Depends (get_db )) -> dict :
98+ """Revoke all of the caller's tokens (access + refresh)."""
99+ accounts .revoke_all_tokens (db , user )
100+ return {"revoked" : True }
101+
102+
103+ @router .post ("/password" )
104+ def change_password (
105+ body : PasswordChange ,
106+ user : User = Depends (authenticate_user ),
107+ db : Session = Depends (get_db ),
108+ ) -> dict :
109+ """Change password (verifying the current one) and revoke existing
110+ sessions."""
111+ accounts .change_password (db , user , body .old_password , body .new_password )
112+ return {"changed" : True }
113+
114+
76115@router .get ("/admin-check" )
77116def admin_check (user : User = Depends (require_admin )) -> dict :
78117 return {"admin" : user .email }
0 commit comments