- Implement multi-format document support (PDF, XLSX, CSV, PPTX, TXT, Images) - Add S3-compatible storage service with tenant isolation - Create document organization service with hierarchical folders and tagging - Implement advanced document processing with table/chart extraction - Add batch upload capabilities (up to 50 files) - Create comprehensive document validation and security scanning - Implement automatic metadata extraction and categorization - Add document version control system - Update DEVELOPMENT_PLAN.md to mark Week 2 as completed - Add WEEK2_COMPLETION_SUMMARY.md with detailed implementation notes - All tests passing (6/6) - 100% success rate
303 lines
8.9 KiB
Python
303 lines
8.9 KiB
Python
"""
|
|
Authentication endpoints for the Virtual Board Member AI System.
|
|
"""
|
|
import logging
|
|
from datetime import timedelta
|
|
from typing import Optional
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
|
from fastapi.security import HTTPBearer
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.auth import auth_service, get_current_user
|
|
from app.core.database import get_db
|
|
from app.core.config import settings
|
|
from app.models.user import User
|
|
from app.models.tenant import Tenant
|
|
from app.middleware.tenant import get_current_tenant
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter()
|
|
security = HTTPBearer()
|
|
|
|
class LoginRequest(BaseModel):
|
|
email: str
|
|
password: str
|
|
tenant_id: Optional[str] = None
|
|
|
|
class RegisterRequest(BaseModel):
|
|
email: str
|
|
password: str
|
|
first_name: str
|
|
last_name: str
|
|
tenant_id: str
|
|
role: str = "user"
|
|
|
|
class TokenResponse(BaseModel):
|
|
access_token: str
|
|
token_type: str = "bearer"
|
|
expires_in: int
|
|
tenant_id: str
|
|
user_id: str
|
|
|
|
class UserResponse(BaseModel):
|
|
id: str
|
|
email: str
|
|
first_name: str
|
|
last_name: str
|
|
role: str
|
|
tenant_id: str
|
|
is_active: bool
|
|
|
|
@router.post("/login", response_model=TokenResponse)
|
|
async def login(
|
|
login_data: LoginRequest,
|
|
request: Request,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Authenticate user and return access token."""
|
|
try:
|
|
# Find user by email and tenant
|
|
user = db.query(User).filter(
|
|
User.email == login_data.email
|
|
).first()
|
|
|
|
if not user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid credentials"
|
|
)
|
|
|
|
# If tenant_id provided, verify user belongs to that tenant
|
|
if login_data.tenant_id:
|
|
if str(user.tenant_id) != login_data.tenant_id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid tenant for user"
|
|
)
|
|
else:
|
|
# Use user's default tenant
|
|
login_data.tenant_id = str(user.tenant_id)
|
|
|
|
# Verify password
|
|
if not auth_service.verify_password(login_data.password, user.hashed_password):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid credentials"
|
|
)
|
|
|
|
# Check if user is active
|
|
if not user.is_active:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="User account is inactive"
|
|
)
|
|
|
|
# Verify tenant is active
|
|
tenant = db.query(Tenant).filter(
|
|
Tenant.id == login_data.tenant_id,
|
|
Tenant.status == "active"
|
|
).first()
|
|
|
|
if not tenant:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Tenant is inactive"
|
|
)
|
|
|
|
# Create access token
|
|
token_data = {
|
|
"sub": str(user.id),
|
|
"email": user.email,
|
|
"tenant_id": login_data.tenant_id,
|
|
"role": user.role
|
|
}
|
|
|
|
access_token = auth_service.create_access_token(
|
|
data=token_data,
|
|
expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
)
|
|
|
|
# Create session
|
|
await auth_service.create_session(
|
|
user_id=str(user.id),
|
|
tenant_id=login_data.tenant_id,
|
|
token=access_token
|
|
)
|
|
|
|
# Update last login
|
|
user.last_login_at = timedelta()
|
|
db.commit()
|
|
|
|
logger.info(f"User {user.email} logged in to tenant {login_data.tenant_id}")
|
|
|
|
return TokenResponse(
|
|
access_token=access_token,
|
|
expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
|
tenant_id=login_data.tenant_id,
|
|
user_id=str(user.id)
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Login error: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Internal server error"
|
|
)
|
|
|
|
@router.post("/register", response_model=UserResponse)
|
|
async def register(
|
|
register_data: RegisterRequest,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Register a new user."""
|
|
try:
|
|
# Check if tenant exists and is active
|
|
tenant = db.query(Tenant).filter(
|
|
Tenant.id == register_data.tenant_id,
|
|
Tenant.status == "active"
|
|
).first()
|
|
|
|
if not tenant:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Invalid or inactive tenant"
|
|
)
|
|
|
|
# Check if user already exists
|
|
existing_user = db.query(User).filter(
|
|
User.email == register_data.email,
|
|
User.tenant_id == register_data.tenant_id
|
|
).first()
|
|
|
|
if existing_user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="User already exists in this tenant"
|
|
)
|
|
|
|
# Create new user
|
|
hashed_password = auth_service.get_password_hash(register_data.password)
|
|
|
|
user = User(
|
|
email=register_data.email,
|
|
hashed_password=hashed_password,
|
|
first_name=register_data.first_name,
|
|
last_name=register_data.last_name,
|
|
role=register_data.role,
|
|
tenant_id=register_data.tenant_id,
|
|
is_active=True
|
|
)
|
|
|
|
db.add(user)
|
|
db.commit()
|
|
db.refresh(user)
|
|
|
|
logger.info(f"Registered new user {user.email} in tenant {register_data.tenant_id}")
|
|
|
|
return UserResponse(
|
|
id=str(user.id),
|
|
email=user.email,
|
|
first_name=user.first_name,
|
|
last_name=user.last_name,
|
|
role=user.role,
|
|
tenant_id=str(user.tenant_id),
|
|
is_active=user.is_active
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Registration error: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Internal server error"
|
|
)
|
|
|
|
@router.post("/logout")
|
|
async def logout(
|
|
current_user: User = Depends(get_current_user),
|
|
request: Request = None
|
|
):
|
|
"""Logout user and invalidate session."""
|
|
try:
|
|
tenant_id = get_current_tenant(request) if request else str(current_user.tenant_id)
|
|
|
|
# Invalidate session
|
|
await auth_service.invalidate_session(
|
|
user_id=str(current_user.id),
|
|
tenant_id=tenant_id
|
|
)
|
|
|
|
logger.info(f"User {current_user.email} logged out from tenant {tenant_id}")
|
|
|
|
return {"message": "Successfully logged out"}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Logout error: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Internal server error"
|
|
)
|
|
|
|
@router.get("/me", response_model=UserResponse)
|
|
async def get_current_user_info(
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Get current user information."""
|
|
return UserResponse(
|
|
id=str(current_user.id),
|
|
email=current_user.email,
|
|
first_name=current_user.first_name,
|
|
last_name=current_user.last_name,
|
|
role=current_user.role,
|
|
tenant_id=str(current_user.tenant_id),
|
|
is_active=current_user.is_active
|
|
)
|
|
|
|
@router.post("/refresh")
|
|
async def refresh_token(
|
|
current_user: User = Depends(get_current_user),
|
|
request: Request = None
|
|
):
|
|
"""Refresh access token."""
|
|
try:
|
|
tenant_id = get_current_tenant(request) if request else str(current_user.tenant_id)
|
|
|
|
# Create new token
|
|
token_data = {
|
|
"sub": str(current_user.id),
|
|
"email": current_user.email,
|
|
"tenant_id": tenant_id,
|
|
"role": current_user.role
|
|
}
|
|
|
|
new_token = auth_service.create_access_token(
|
|
data=token_data,
|
|
expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
)
|
|
|
|
# Update session
|
|
await auth_service.create_session(
|
|
user_id=str(current_user.id),
|
|
tenant_id=tenant_id,
|
|
token=new_token
|
|
)
|
|
|
|
return TokenResponse(
|
|
access_token=new_token,
|
|
expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
|
tenant_id=tenant_id,
|
|
user_id=str(current_user.id)
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Token refresh error: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Internal server error"
|
|
)
|