UMBRA/backend/app/routers/events.py
Kyle Pope 27c65ce40d Fix Round 2 code review findings: type safety, security, and correctness
Backend:
- Add Literal types for status/priority fields (project_task, todo, project schemas)
- Add AccentColor Literal validation to prevent CSS injection (settings schema)
- Add PIN max-length (72 char bcrypt limit) validation
- Fix event date filtering to use correct range overlap logic
- Add revocation check to auth_status endpoint for consistency
- Config: env-aware SECRET_KEY fail-fast, configurable COOKIE_SECURE

Frontend:
- Add withCredentials to axios for cross-origin cookie support
- Replace .toISOString() with local date formatter in DashboardPage
- Replace `as any` casts with proper indexed type access in forms
- Nginx: add CSP, Referrer-Policy headers; remove deprecated X-XSS-Protection
- Nginx: duplicate security headers in static asset location block

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 15:18:49 +08:00

123 lines
3.7 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import Optional, List
from datetime import date
from app.database import get_db
from app.models.calendar_event import CalendarEvent
from app.schemas.calendar_event import CalendarEventCreate, CalendarEventUpdate, CalendarEventResponse
from app.routers.auth import get_current_session
from app.models.settings import Settings
router = APIRouter()
@router.get("/", response_model=List[CalendarEventResponse])
async def get_events(
start: Optional[date] = Query(None),
end: Optional[date] = Query(None),
db: AsyncSession = Depends(get_db),
current_user: Settings = Depends(get_current_session)
):
"""Get all calendar events with optional date range filtering."""
query = select(CalendarEvent)
if start:
query = query.where(CalendarEvent.end_datetime >= start)
if end:
query = query.where(CalendarEvent.start_datetime <= end)
query = query.order_by(CalendarEvent.start_datetime.asc())
result = await db.execute(query)
events = result.scalars().all()
return events
@router.post("/", response_model=CalendarEventResponse, status_code=201)
async def create_event(
event: CalendarEventCreate,
db: AsyncSession = Depends(get_db),
current_user: Settings = Depends(get_current_session)
):
"""Create a new calendar event."""
if event.end_datetime < event.start_datetime:
raise HTTPException(status_code=400, detail="End datetime must be after start datetime")
new_event = CalendarEvent(**event.model_dump())
db.add(new_event)
await db.commit()
await db.refresh(new_event)
return new_event
@router.get("/{event_id}", response_model=CalendarEventResponse)
async def get_event(
event_id: int,
db: AsyncSession = Depends(get_db),
current_user: Settings = Depends(get_current_session)
):
"""Get a specific calendar event by ID."""
result = await db.execute(select(CalendarEvent).where(CalendarEvent.id == event_id))
event = result.scalar_one_or_none()
if not event:
raise HTTPException(status_code=404, detail="Calendar event not found")
return event
@router.put("/{event_id}", response_model=CalendarEventResponse)
async def update_event(
event_id: int,
event_update: CalendarEventUpdate,
db: AsyncSession = Depends(get_db),
current_user: Settings = Depends(get_current_session)
):
"""Update a calendar event."""
result = await db.execute(select(CalendarEvent).where(CalendarEvent.id == event_id))
event = result.scalar_one_or_none()
if not event:
raise HTTPException(status_code=404, detail="Calendar event not found")
update_data = event_update.model_dump(exclude_unset=True)
# Validate datetime order if both are being updated
start = update_data.get("start_datetime", event.start_datetime)
end = update_data.get("end_datetime", event.end_datetime)
if end < start:
raise HTTPException(status_code=400, detail="End datetime must be after start datetime")
for key, value in update_data.items():
setattr(event, key, value)
await db.commit()
await db.refresh(event)
return event
@router.delete("/{event_id}", status_code=204)
async def delete_event(
event_id: int,
db: AsyncSession = Depends(get_db),
current_user: Settings = Depends(get_current_session)
):
"""Delete a calendar event."""
result = await db.execute(select(CalendarEvent).where(CalendarEvent.id == event_id))
event = result.scalar_one_or_none()
if not event:
raise HTTPException(status_code=404, detail="Calendar event not found")
await db.delete(event)
await db.commit()
return None