feat: add authentication API with login, register, and user endpoints

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
zephyrdark 2026-02-02 23:20:32 +09:00
parent 01aa339d76
commit 39edc202f8
7 changed files with 191 additions and 0 deletions

View File

@ -0,0 +1,3 @@
from app.api.auth import router as auth_router
__all__ = ["auth_router"]

88
backend/app/api/auth.py Normal file
View File

@ -0,0 +1,88 @@
"""
Authentication API endpoints.
"""
from datetime import timedelta
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.security import (
verify_password,
get_password_hash,
create_access_token,
)
from app.core.config import get_settings
from app.models.user import User
from app.schemas import Token, UserCreate, UserResponse, LoginRequest
from app.api.deps import CurrentUser
router = APIRouter(prefix="/api/auth", tags=["auth"])
settings = get_settings()
@router.post("/login", response_model=Token)
async def login(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: Annotated[Session, Depends(get_db)],
):
"""Login and get access token."""
user = db.query(User).filter(User.username == form_data.username).first()
if not user or not verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = create_access_token(
data={"sub": user.username},
expires_delta=timedelta(minutes=settings.access_token_expire_minutes),
)
return Token(access_token=access_token)
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def register(
user_data: UserCreate,
db: Annotated[Session, Depends(get_db)],
):
"""Register a new user."""
if db.query(User).filter(User.username == user_data.username).first():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username already registered",
)
if db.query(User).filter(User.email == user_data.email).first():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered",
)
user = User(
username=user_data.username,
email=user_data.email,
hashed_password=get_password_hash(user_data.password),
)
db.add(user)
db.commit()
db.refresh(user)
return user
@router.get("/me", response_model=UserResponse)
async def get_current_user_info(current_user: CurrentUser):
"""Get current user information."""
return current_user
@router.post("/logout")
async def logout():
"""Logout (client should discard token)."""
return {"message": "Successfully logged out"}

44
backend/app/api/deps.py Normal file
View File

@ -0,0 +1,44 @@
"""
API dependencies.
"""
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.security import decode_access_token
from app.models.user import User
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
async def get_current_user(
db: Annotated[Session, Depends(get_db)],
token: Annotated[str, Depends(oauth2_scheme)],
) -> User:
"""Get the current authenticated user."""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
payload = decode_access_token(token)
if payload is None:
raise credentials_exception
username: str = payload.get("sub")
if username is None:
raise credentials_exception
user = db.query(User).filter(User.username == username).first()
if user is None:
raise credentials_exception
return user
CurrentUser = Annotated[User, Depends(get_current_user)]
DbSession = Annotated[Session, Depends(get_db)]

View File

@ -4,6 +4,8 @@ Galaxy-PO Backend API
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api import auth_router
app = FastAPI(
title="Galaxy-PO API",
description="Quant Portfolio Management API",
@ -18,6 +20,9 @@ app.add_middleware(
allow_headers=["*"],
)
# Include routers
app.include_router(auth_router)
@app.get("/health")
async def health_check():

View File

@ -0,0 +1,11 @@
from app.schemas.user import UserBase, UserCreate, UserResponse
from app.schemas.auth import Token, TokenPayload, LoginRequest
__all__ = [
"UserBase",
"UserCreate",
"UserResponse",
"Token",
"TokenPayload",
"LoginRequest",
]

View File

@ -0,0 +1,18 @@
"""
Authentication schemas.
"""
from pydantic import BaseModel
class Token(BaseModel):
access_token: str
token_type: str = "bearer"
class TokenPayload(BaseModel):
sub: str | None = None
class LoginRequest(BaseModel):
username: str
password: str

View File

@ -0,0 +1,22 @@
"""
User schemas for request/response validation.
"""
from datetime import datetime
from pydantic import BaseModel, EmailStr
class UserBase(BaseModel):
username: str
email: EmailStr
class UserCreate(UserBase):
password: str
class UserResponse(UserBase):
id: int
created_at: datetime
class Config:
from_attributes = True