Files
Jonathan Pressnell 1a8ec37bed feat: Complete Week 2 - Document Processing Pipeline
- 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
2025-08-08 15:47:43 -04:00

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"
)