olivas/backend/app/api/endpoints/projects.py
Vadym Samoilenko f217a5aea6 Add Azure AD SSO authentication for backend and frontend
Replace X-User-Id header auth with Azure AD JWT token validation.
Backend validates tokens via JWKS, frontend uses MSAL for login/token
acquisition. Adds logout button, 401 handling, and configurable
AZURE_AUTH_ENABLED toggle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:41:06 +00:00

136 lines
4 KiB
Python

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.db.session import get_db
from app.dependencies import get_user_id
from app.models.analysis import Analysis
from app.models.project import Project
from app.schemas.project import ProjectCreate, ProjectDetail, ProjectSummary, ProjectUpdate
router = APIRouter(prefix="/projects", tags=["projects"])
@router.post("", response_model=ProjectSummary, status_code=201)
async def create_project(
body: ProjectCreate,
db: AsyncSession = Depends(get_db),
user_id: str = Depends(get_user_id),
):
project = Project(user_id=user_id, name=body.name, description=body.description)
db.add(project)
await db.flush()
await db.refresh(project)
return ProjectSummary(
id=project.id,
name=project.name,
description=project.description,
analysis_count=0,
created_at=project.created_at,
updated_at=project.updated_at,
)
@router.get("", response_model=list[ProjectSummary])
async def list_projects(
page: int = 1,
per_page: int = 20,
db: AsyncSession = Depends(get_db),
user_id: str = Depends(get_user_id),
):
offset = (page - 1) * per_page
stmt = (
select(
Project,
func.count(Analysis.id).label("analysis_count"),
)
.outerjoin(Analysis)
.where(Project.user_id == user_id)
.group_by(Project.id)
.order_by(Project.updated_at.desc())
.offset(offset)
.limit(per_page)
)
result = await db.execute(stmt)
rows = result.all()
return [
ProjectSummary(
id=p.id,
name=p.name,
description=p.description,
analysis_count=count,
created_at=p.created_at,
updated_at=p.updated_at,
)
for p, count in rows
]
@router.get("/{project_id}", response_model=ProjectDetail)
async def get_project(
project_id: str,
db: AsyncSession = Depends(get_db),
user_id: str = Depends(get_user_id),
):
stmt = (
select(Project)
.options(selectinload(Project.analyses))
.where(Project.id == project_id, Project.user_id == user_id)
)
result = await db.execute(stmt)
project = result.scalar_one_or_none()
if not project:
raise HTTPException(status_code=404, detail="Project not found")
return project
@router.put("/{project_id}", response_model=ProjectSummary)
async def update_project(
project_id: str,
body: ProjectUpdate,
db: AsyncSession = Depends(get_db),
user_id: str = Depends(get_user_id),
):
stmt = select(Project).where(Project.id == project_id, Project.user_id == user_id)
result = await db.execute(stmt)
project = result.scalar_one_or_none()
if not project:
raise HTTPException(status_code=404, detail="Project not found")
if body.name is not None:
project.name = body.name
if body.description is not None:
project.description = body.description
await db.flush()
await db.refresh(project)
count_stmt = select(func.count(Analysis.id)).where(Analysis.project_id == project_id)
count_result = await db.execute(count_stmt)
analysis_count = count_result.scalar() or 0
return ProjectSummary(
id=project.id,
name=project.name,
description=project.description,
analysis_count=analysis_count,
created_at=project.created_at,
updated_at=project.updated_at,
)
@router.delete("/{project_id}", status_code=204)
async def delete_project(
project_id: str,
db: AsyncSession = Depends(get_db),
user_id: str = Depends(get_user_id),
):
stmt = select(Project).where(Project.id == project_id, Project.user_id == user_id)
result = await db.execute(stmt)
project = result.scalar_one_or_none()
if not project:
raise HTTPException(status_code=404, detail="Project not found")
await db.delete(project)