Files
weibo_signin/backend/auth_service/app/main.py
2026-03-09 14:05:00 +08:00

224 lines
6.8 KiB
Python

"""
Weibo-HotSign Authentication Service
Main FastAPI application entry point
"""
from fastapi import FastAPI, Depends, HTTPException, status, Security
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
import uvicorn
import os
import logging
from shared.models import get_db, User
from auth_service.app.models.database import create_tables
from auth_service.app.schemas.user import (
UserCreate, UserLogin, UserResponse, Token, TokenData, RefreshTokenRequest,
)
from auth_service.app.services.auth_service import AuthService
from auth_service.app.utils.security import (
verify_password, create_access_token, decode_access_token,
create_refresh_token, verify_refresh_token, revoke_refresh_token,
)
# Configure logger
logger = logging.getLogger(__name__)
# Initialize FastAPI app
app = FastAPI(
title="Weibo-HotSign Authentication Service",
description="Handles user authentication and authorization for Weibo-HotSign system",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc"
)
# CORS middleware configuration
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "http://localhost:80"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Security scheme for JWT
security = HTTPBearer()
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Security(security),
db: AsyncSession = Depends(get_db)
) -> UserResponse:
"""
Dependency to get current user from JWT token
"""
token = credentials.credentials
payload = decode_access_token(token)
if payload is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"},
)
user_id = payload.get("sub")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token payload",
headers={"WWW-Authenticate": "Bearer"},
)
auth_service = AuthService(db)
user = await auth_service.get_user_by_id(user_id)
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User account is deactivated",
)
return UserResponse.from_orm(user)
@app.on_event("startup")
async def startup_event():
"""Initialize database tables on startup"""
await create_tables()
@app.get("/")
async def root():
return {
"service": "Weibo-HotSign Authentication Service",
"status": "running",
"version": "1.0.0"
}
@app.get("/health")
async def health_check():
return {"status": "healthy"}
@app.post("/auth/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def register_user(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
"""
Register a new user account
"""
auth_service = AuthService(db)
# Check if user already exists - optimized with single query
email_user, username_user = await auth_service.check_user_exists(user_data.email, user_data.username)
if email_user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="User with this email already exists"
)
if username_user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Username already taken"
)
# Create new user
try:
user = await auth_service.create_user(user_data)
return UserResponse.from_orm(user)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to create user: {str(e)}"
)
@app.post("/auth/login", response_model=Token)
async def login_user(login_data: UserLogin, db: AsyncSession = Depends(get_db)):
"""
Authenticate user and return JWT token
"""
auth_service = AuthService(db)
# Find user by email
user = await auth_service.get_user_by_email(login_data.email)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid email or password"
)
# Verify password
if not verify_password(login_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid email or password"
)
# Check if user is active
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User account is deactivated"
)
# Create access token
access_token = create_access_token(data={"sub": str(user.id), "username": user.username})
# Create refresh token (stored in Redis)
refresh_token = await create_refresh_token(str(user.id))
return Token(
access_token=access_token,
refresh_token=refresh_token,
token_type="bearer",
expires_in=3600 # 1 hour
)
@app.post("/auth/refresh", response_model=Token)
async def refresh_token(body: RefreshTokenRequest, db: AsyncSession = Depends(get_db)):
"""
Exchange a valid refresh token for a new access + refresh token pair (Token Rotation).
The old refresh token is revoked immediately.
"""
# Verify the incoming refresh token
user_id = await verify_refresh_token(body.refresh_token)
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired refresh token",
)
# Ensure the user still exists and is active
auth_service = AuthService(db)
user = await auth_service.get_user_by_id(user_id)
if user is None or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found or deactivated",
)
# Revoke old token, issue new pair
await revoke_refresh_token(body.refresh_token)
new_access = create_access_token(data={"sub": str(user.id), "username": user.username})
new_refresh = await create_refresh_token(str(user.id))
return Token(
access_token=new_access,
refresh_token=new_refresh,
token_type="bearer",
expires_in=3600,
)
@app.get("/auth/me", response_model=UserResponse)
async def get_current_user_info(current_user: UserResponse = Depends(get_current_user)):
"""
Get current user information
"""
return current_user