AutoForge / backend /api /auth.py
NOT-OMEGA's picture
Upload 68 files
6a0ff33 verified
Raw History Blame Contribute Delete
3.81 kB
"""Authentication API: signup, login, and current-user retrieval."""
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, HTTPException, status
from jose import jwt
from passlib.context import CryptContext
from pydantic import BaseModel, EmailStr, Field
from sqlalchemy.ext.asyncio import AsyncSession
from backend.api.deps import get_current_user, get_db
from backend.config.settings import get_settings
from backend.db.models import User
from backend.db.repository import create_user, get_user_by_email, get_user_by_username
router = APIRouter(prefix="/api/auth", tags=["auth"])
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# ── Schemas ──────────────────────────────────────────────────────────
class SignupRequest(BaseModel):
username: str = Field(..., min_length=3, max_length=80)
email: EmailStr
password: str = Field(..., min_length=6)
class LoginRequest(BaseModel):
username: str
password: str
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
class UserResponse(BaseModel):
id: str
username: str
email: str
created_at: datetime
model_config = {"from_attributes": True}
# ── Helpers ──────────────────────────────────────────────────────────
def _hash_password(password: str) -> str:
return pwd_context.hash(password)
def _verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
def _create_token(user_id: str) -> str:
settings = get_settings()
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.jwt_expire_minutes)
payload = {"sub": user_id, "exp": expire}
return jwt.encode(payload, settings.secret_key, algorithm=settings.jwt_algorithm)
# ── Routes ───────────────────────────────────────────────────────────
@router.post("/signup", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)
async def signup(body: SignupRequest, session: AsyncSession = Depends(get_db)):
"""Register a new user account."""
# Check for duplicate username
if await get_user_by_username(session, body.username):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Username already taken",
)
# Check for duplicate email
if await get_user_by_email(session, body.email):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Email already registered",
)
user = await create_user(
session,
username=body.username,
email=body.email,
hashed_password=_hash_password(body.password),
)
return TokenResponse(access_token=_create_token(user.id))
@router.post("/login", response_model=TokenResponse)
async def login(body: LoginRequest, session: AsyncSession = Depends(get_db)):
"""Authenticate and receive a JWT."""
user = await get_user_by_username(session, body.username)
if not user or not _verify_password(body.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid username or password",
)
return TokenResponse(access_token=_create_token(user.id))
@router.get("/me", response_model=UserResponse)
async def me(current_user: User = Depends(get_current_user)):
"""Return the currently authenticated user."""
return current_user