Skip to content

Commit dae3437

Browse files
authored
Update auth.py
1 parent fc78d8f commit dae3437

1 file changed

Lines changed: 44 additions & 5 deletions

File tree

‎app/routes/auth.py‎

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,34 @@
66

77
from __future__ import annotations
88

9-
from fastapi import APIRouter, Depends, Query, status
9+
from fastapi import APIRouter, Depends, Query, Request, status
1010
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
1111
from sqlalchemy.orm import Session
1212

1313
from ..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
1418
from ..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

1821
router = 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+
2237
def 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+
)
5777
def 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)])
6282
def 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")
77116
def admin_check(user: User = Depends(require_admin)) -> dict:
78117
return {"admin": user.email}

0 commit comments

Comments
 (0)