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:
0
backend/app/api/__init__.py
Normal file
0
backend/app/api/__init__.py
Normal file
6
backend/app/api/main.py
Normal file
6
backend/app/api/main.py
Normal 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"])
|
||||
0
backend/app/api/routes/__init__.py
Normal file
0
backend/app/api/routes/__init__.py
Normal file
182
backend/app/api/routes/auth.py
Normal file
182
backend/app/api/routes/auth.py
Normal 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
|
||||
Reference in New Issue
Block a user