Files
site11/services/console/backend/app/routes/auth.py
jungwoo choi f4b75b96a5 feat: Phase 1 - Complete authentication system with JWT
Backend Implementation (FastAPI + MongoDB):
- JWT authentication with access/refresh tokens
- User registration and login endpoints
- Password hashing with bcrypt (fixed 72-byte limit)
- Protected endpoints with JWT middleware
- Token refresh mechanism
- Role-Based Access Control (RBAC) structure
- Pydantic v2 models and async MongoDB with Motor
- API endpoints: /api/auth/register, /api/auth/login, /api/auth/me, /api/auth/refresh

Frontend Implementation (React + TypeScript + Material-UI):
- Login and Register pages with validation
- AuthContext for global authentication state
- API client with Axios interceptors for token refresh
- Protected routes with automatic redirect
- User profile display in navigation
- Logout functionality

Technical Achievements:
- Resolved bcrypt 72-byte limit (replaced passlib with native bcrypt)
- Fixed Pydantic v2 compatibility (PyObjectId, ConfigDict)
- Implemented automatic token refresh on 401 errors
- Created comprehensive test suite for all auth endpoints

Docker & Kubernetes:
- Backend image: yakenator/site11-console-backend:latest
- Frontend image: yakenator/site11-console-frontend:latest
- Deployed to site11-pipeline namespace
- Nginx reverse proxy configuration

Documentation:
- CONSOLE_ARCHITECTURE.md - Complete system architecture
- PHASE1_COMPLETION.md - Detailed completion report
- PROGRESS.md - Updated with Phase 1 status

All authentication endpoints tested and verified working.

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 16:23:07 +09:00

168 lines
5.2 KiB
Python

from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from motor.motor_asyncio import AsyncIOMotorDatabase
from ..schemas.auth import UserRegister, Token, TokenRefresh, UserResponse
from ..services.user_service import UserService
from ..db.mongodb import get_database
from ..core.security import (
create_access_token,
create_refresh_token,
decode_token,
get_current_user_id
)
from ..core.config import settings
router = APIRouter(prefix="/api/auth", tags=["authentication"])
def get_user_service(db: AsyncIOMotorDatabase = Depends(get_database)) -> UserService:
"""Dependency to get user service"""
return UserService(db)
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def register(
user_data: UserRegister,
user_service: UserService = Depends(get_user_service)
):
"""Register a new user"""
user = await user_service.create_user(user_data)
return UserResponse(
_id=str(user.id),
email=user.email,
username=user.username,
full_name=user.full_name,
role=user.role,
permissions=user.permissions,
status=user.status,
is_active=user.is_active,
created_at=user.created_at.isoformat(),
last_login_at=user.last_login_at.isoformat() if user.last_login_at else None
)
@router.post("/login", response_model=Token)
async def login(
form_data: OAuth2PasswordRequestForm = Depends(),
user_service: UserService = Depends(get_user_service)
):
"""Login with username/email and password"""
user = await user_service.authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username/email or password",
headers={"WWW-Authenticate": "Bearer"},
)
# Update last login timestamp
await user_service.update_last_login(str(user.id))
# Create tokens
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": str(user.id), "username": user.username},
expires_delta=access_token_expires
)
refresh_token = create_refresh_token(data={"sub": str(user.id)})
return Token(
access_token=access_token,
refresh_token=refresh_token,
token_type="bearer"
)
@router.post("/refresh", response_model=Token)
async def refresh_token(
token_data: TokenRefresh,
user_service: UserService = Depends(get_user_service)
):
"""Refresh access token using refresh token"""
try:
payload = decode_token(token_data.refresh_token)
# Verify it's a refresh token
if payload.get("type") != "refresh":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token type"
)
user_id = payload.get("sub")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
# Verify user still exists and is active
user = await user_service.get_user_by_id(user_id)
if not user or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found or inactive"
)
# Create new access token
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user_id, "username": user.username},
expires_delta=access_token_expires
)
return Token(
access_token=access_token,
refresh_token=token_data.refresh_token,
token_type="bearer"
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired refresh token"
)
@router.get("/me", response_model=UserResponse)
async def get_current_user(
user_id: str = Depends(get_current_user_id),
user_service: UserService = Depends(get_user_service)
):
"""Get current user information"""
user = await user_service.get_user_by_id(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return UserResponse(
_id=str(user.id),
email=user.email,
username=user.username,
full_name=user.full_name,
role=user.role,
permissions=user.permissions,
status=user.status,
is_active=user.is_active,
created_at=user.created_at.isoformat(),
last_login_at=user.last_login_at.isoformat() if user.last_login_at else None
)
@router.post("/logout")
async def logout(user_id: str = Depends(get_current_user_id)):
"""Logout endpoint (token should be removed on client side)"""
# In a more sophisticated system, you might want to:
# 1. Blacklist the token in Redis
# 2. Log the logout event
# 3. Clear any session data
return {"message": "Successfully logged out"}