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>
168 lines
5.2 KiB
Python
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"}
|