Initial commit
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""HTTP routes for auth: register, login, logout, me, refresh."""
|
||||
|
||||
from fastapi import APIRouter, Cookie, HTTPException, Response, status
|
||||
from starlette.requests import Request
|
||||
|
||||
from app.config import settings
|
||||
from app.dependencies import AuthServiceDep, CurrentUserDep
|
||||
from app.main import limiter
|
||||
from app.models.auth import LoginRequest, RegisterRequest, TokenResponse, UserMeResponse
|
||||
from app.utils.errors import AuthError, ConflictError, UnauthorizedError
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
|
||||
_REFRESH_COOKIE = "refresh_token"
|
||||
_ACCESS_EXPIRES_IN = settings.jwt_access_token_expire_minutes * 60
|
||||
|
||||
|
||||
@router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)
|
||||
@limiter.limit("10/minute")
|
||||
async def register(
|
||||
request: Request, payload: RegisterRequest, response: Response, service: AuthServiceDep
|
||||
) -> TokenResponse:
|
||||
try:
|
||||
user, _pd = await service.register(
|
||||
email=str(payload.email),
|
||||
password=payload.password,
|
||||
display_name=payload.display_name,
|
||||
)
|
||||
except ConflictError as exc:
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, str(exc)) from exc
|
||||
|
||||
access = service.create_access_token(user.id)
|
||||
refresh = service.create_refresh_token(user.id)
|
||||
_set_refresh_cookie(response, refresh)
|
||||
return TokenResponse(access_token=access, expires_in=_ACCESS_EXPIRES_IN)
|
||||
|
||||
|
||||
@router.post("/login", response_model=TokenResponse)
|
||||
@limiter.limit("10/minute")
|
||||
async def login(
|
||||
request: Request, payload: LoginRequest, response: Response, service: AuthServiceDep
|
||||
) -> TokenResponse:
|
||||
try:
|
||||
user = await service.login(str(payload.email), payload.password)
|
||||
except AuthError as exc:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, str(exc)) from exc
|
||||
|
||||
access = service.create_access_token(user.id)
|
||||
refresh = service.create_refresh_token(user.id)
|
||||
_set_refresh_cookie(response, refresh)
|
||||
return TokenResponse(access_token=access, expires_in=_ACCESS_EXPIRES_IN)
|
||||
|
||||
|
||||
@router.post("/logout", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def logout(response: Response) -> None:
|
||||
response.delete_cookie(_REFRESH_COOKIE)
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=TokenResponse)
|
||||
async def refresh(
|
||||
response: Response,
|
||||
service: AuthServiceDep,
|
||||
refresh_token: str | None = Cookie(default=None, alias=_REFRESH_COOKIE),
|
||||
) -> TokenResponse:
|
||||
if not refresh_token:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "No refresh token")
|
||||
try:
|
||||
access = await service.refresh_access_token(refresh_token)
|
||||
except UnauthorizedError as exc:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, str(exc)) from exc
|
||||
return TokenResponse(access_token=access, expires_in=_ACCESS_EXPIRES_IN)
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserMeResponse)
|
||||
async def me(current_user: CurrentUserDep) -> UserMeResponse:
|
||||
# PersonalData is fetched separately when workspace context is needed.
|
||||
# Here we return just the user-level data.
|
||||
return UserMeResponse(
|
||||
id=str(current_user.id),
|
||||
email=current_user.email,
|
||||
display_name="", # enriched by the frontend via workspace API
|
||||
workspace_id=None, # client fetches via /api/workspace after login
|
||||
)
|
||||
|
||||
|
||||
def _set_refresh_cookie(response: Response, token: str) -> None:
|
||||
response.set_cookie(
|
||||
key=_REFRESH_COOKIE,
|
||||
value=token,
|
||||
httponly=True,
|
||||
secure=True,
|
||||
samesite="lax",
|
||||
max_age=settings.jwt_refresh_token_expire_days * 86400,
|
||||
)
|
||||
Reference in New Issue
Block a user