Add foundational user authentication and registration system

Introduces schemas for user management, token handling, and password hashing. Implements routes for user registration, login, token refresh, and user info retrieval. Sets up authentication dependencies and integrates the API router with the application.
This commit is contained in:
2025-02-28 16:18:03 +01:00
parent 290d91d395
commit 43df9d73b0
11 changed files with 467 additions and 1 deletions

View File

6
backend/app/api/main.py Normal file
View File

@@ -0,0 +1,6 @@
from fastapi import APIRouter
from app.api.routes import auth
api_router = APIRouter()
api_router.include_router(auth.router, tags=["auth"])

View File

View File

@@ -0,0 +1,182 @@
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError
from sqlalchemy.orm import Session
from app.auth.security import (
verify_password,
get_password_hash,
create_tokens,
decode_token,
)
from app.core.database import get_db
from app.models.user import User
from app.schemas.token import TokenResponse, RefreshToken
from app.schemas.user import UserCreate, UserResponse
router = APIRouter()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
db: Session = Depends(get_db)
) -> User:
"""
Get the current user based on the JWT token.
Args:
token: JWT token from authorization header
db: Database session
Returns:
User object if valid token
Raises:
HTTPException: If token is invalid or user not found
"""
try:
payload = decode_token(token)
if payload.type != "access":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid access token type"
)
user = db.query(User).filter(User.id == payload.sub).first()
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found"
)
return user
except JWTError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Could not validate credentials: {str(e)}"
)
async def authenticate_user(
email: str,
password: str,
db: Session
) -> User:
"""
Authenticate a user by email and password.
Args:
email: User's email
password: User's password
db: Database session
Returns:
User object if authentication successful, None otherwise
"""
user = db.query(User).filter(User.email == email).first()
if not user or not verify_password(password, user.password_hash):
return None
return user
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def register(
user_create: UserCreate,
db: Session = Depends(get_db)
):
"""
Register a new user.
"""
# Check if user already exists
if db.query(User).filter(User.email == user_create.email).first():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
# Create new user
user = User(
email=user_create.email,
password_hash=get_password_hash(user_create.password),
first_name=user_create.first_name,
last_name=user_create.last_name,
phone_number=user_create.phone_number
)
db.add(user)
db.commit()
db.refresh(user)
return user
@router.post("/login", response_model=TokenResponse)
async def login(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: Session = Depends(get_db)
):
"""
OAuth2 compatible token login, get an access token for future requests.
"""
user = await authenticate_user(form_data.username, form_data.password, db)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
return create_tokens(str(user.id))
@router.post("/refresh", response_model=TokenResponse)
async def refresh_token(
refresh_token: RefreshToken,
db: Session = Depends(get_db)
):
"""
Refresh access token using refresh token.
"""
try:
payload = decode_token(refresh_token.refresh_token)
# Validate token type
if payload.type != "refresh":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token type"
)
# Verify user still exists and is active
user = db.query(User).filter(User.id == payload.sub).first()
if not user or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found or inactive"
)
return create_tokens(str(user.id))
except JWTError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Invalid refresh token: {str(e)}"
)
@router.get("/me", response_model=UserResponse)
async def read_users_me(
current_user: Annotated[User, Depends(get_current_user)]
):
"""
Get current user information.
"""
return current_user