238 lines
7.3 KiB
Python
238 lines
7.3 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, AuthResponse,
|
|
)
|
|
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"""
|
|
# 表已通过 create_sqlite_db.py 创建,无需重复创建
|
|
# await create_tables()
|
|
pass
|
|
|
|
@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=AuthResponse, status_code=status.HTTP_201_CREATED)
|
|
async def register_user(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
|
|
"""
|
|
Register a new user account and return tokens
|
|
"""
|
|
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)
|
|
|
|
# Create tokens for auto-login
|
|
access_token = create_access_token(data={"sub": str(user.id), "username": user.username})
|
|
refresh_token = await create_refresh_token(str(user.id))
|
|
|
|
return AuthResponse(
|
|
access_token=access_token,
|
|
refresh_token=refresh_token,
|
|
token_type="bearer",
|
|
expires_in=3600,
|
|
user=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=AuthResponse)
|
|
async def login_user(login_data: UserLogin, db: AsyncSession = Depends(get_db)):
|
|
"""
|
|
Authenticate user and return JWT token with user info
|
|
"""
|
|
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 AuthResponse(
|
|
access_token=access_token,
|
|
refresh_token=refresh_token,
|
|
token_type="bearer",
|
|
expires_in=3600,
|
|
user=UserResponse.from_orm(user)
|
|
)
|
|
|
|
@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
|