feat: 7. REST-эндпоинты /auth/*
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.audit.service import write_event
|
||||
from app.auth.schemas import Credentials, TokenResponse, UserResponse
|
||||
from app.core.deps import get_current_user
|
||||
from app.core.jwt import create_access_token
|
||||
from app.core.passwords import (
|
||||
WeakPasswordError,
|
||||
hash_password,
|
||||
validate_policy,
|
||||
verify_password,
|
||||
)
|
||||
from app.db import get_db
|
||||
from app.models import User
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
def _client_info(request: Request) -> tuple[str | None, str | None]:
|
||||
ip = request.client.host if request.client else None
|
||||
return ip, request.headers.get("user-agent")
|
||||
|
||||
|
||||
# POST /auth/register – принимает JSON `{email, password}`
|
||||
@router.post(
|
||||
"/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
def register(
|
||||
payload: Credentials, request: Request, db: Session = Depends(get_db)
|
||||
) -> TokenResponse:
|
||||
# проверить, что email ещё не занят;
|
||||
if db.scalar(select(User).where(User.email == payload.email)) is not None:
|
||||
raise HTTPException(status_code=409, detail="Email already registered")
|
||||
|
||||
# валидировать пароль через validate_policy
|
||||
try:
|
||||
validate_policy(payload.password, user_inputs=[payload.email])
|
||||
except WeakPasswordError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
# создать пользователя с hashed_password
|
||||
user = User(email=payload.email, hashed_password=hash_password(payload.password))
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
ip, user_agent = _client_info(request)
|
||||
# записать событие аудита `user.created`;
|
||||
write_event(db, "user.created", actor_id=user.id, ip=ip, user_agent=user_agent)
|
||||
return TokenResponse(access_token=create_access_token(str(user.id)))
|
||||
|
||||
|
||||
# POST /auth/login – принимает {email, password}
|
||||
@router.post("/login", response_model=TokenResponse)
|
||||
def login(
|
||||
payload: Credentials, request: Request, db: Session = Depends(get_db)
|
||||
) -> TokenResponse:
|
||||
ip, user_agent = _client_info(request)
|
||||
|
||||
# найти пользователя по email;
|
||||
user = db.scalar(select(User).where(User.email == payload.email))
|
||||
|
||||
# проверить пароль через verify_password;
|
||||
if user is None or not verify_password(payload.password, user.hashed_password):
|
||||
# при неудаче:
|
||||
# записать user.login.failure (с email в metadata, но без пароля!)
|
||||
# и вернуть 401 с нейтральным сообщением «Invalid credentials»
|
||||
# (не раскрывать, что именно неверно – email или пароль);
|
||||
write_event(
|
||||
db,
|
||||
"user.login.failure",
|
||||
actor_id=user.id if user else None,
|
||||
ip=ip,
|
||||
user_agent=user_agent,
|
||||
metadata={"email": payload.email},
|
||||
)
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
|
||||
# при успехе: записать user.login.success, вернуть токен.
|
||||
write_event(
|
||||
db, "user.login.success", actor_id=user.id, ip=ip, user_agent=user_agent
|
||||
)
|
||||
return TokenResponse(access_token=create_access_token(str(user.id)))
|
||||
|
||||
|
||||
# GET /auth/me – защищённый:
|
||||
# принимает зависимость get_current_user;
|
||||
# возвращает {id, email, is_admin}.
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
def me(user: User = Depends(get_current_user)) -> UserResponse:
|
||||
return UserResponse(id=user.id, email=user.email, is_admin=user.is_admin)
|
||||
Reference in New Issue
Block a user