UMBRA/backend/app/routers/dashboard.py
Kyle Pope 1aaa2b3a74 Fix code review findings: security hardening and frontend fixes
Backend:
- Add rate limiting to login (5 attempts / 5 min window)
- Add secure flag to session cookies with helper function
- Add PIN min-length validation via Pydantic field_validator
- Fix naive datetime usage in todos.py (datetime.now() not UTC)
- Disable SQLAlchemy echo in production
- Remove auto-commit from get_db to prevent double commits
- Add lower bound filter to upcoming events query
- Add SECRET_KEY default warning on startup
- Remove create_all from lifespan (Alembic handles migrations)

Frontend:
- Fix ReminderForm remind_at slice for datetime-local input
- Add window.confirm() dialogs on all destructive actions
- Redirect authenticated users away from login screen
- Replace error: any with getErrorMessage helper in LockScreen

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

195 lines
6.4 KiB
Python

from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from datetime import datetime, date, timedelta
from typing import Optional, List, Dict, Any
from app.database import get_db
from app.models.settings import Settings
from app.models.todo import Todo
from app.models.calendar_event import CalendarEvent
from app.models.reminder import Reminder
from app.models.project import Project
from app.models.person import Person
from app.models.location import Location
from app.routers.auth import get_current_session
router = APIRouter()
@router.get("/dashboard")
async def get_dashboard(
client_date: Optional[date] = Query(None),
db: AsyncSession = Depends(get_db),
current_user: Settings = Depends(get_current_session)
):
"""Get aggregated dashboard data."""
today = client_date or date.today()
upcoming_cutoff = today + timedelta(days=current_user.upcoming_days)
# Today's events
today_start = datetime.combine(today, datetime.min.time())
today_end = datetime.combine(today, datetime.max.time())
events_query = select(CalendarEvent).where(
CalendarEvent.start_datetime >= today_start,
CalendarEvent.start_datetime <= today_end
)
events_result = await db.execute(events_query)
todays_events = events_result.scalars().all()
# Upcoming todos (not completed, with due date within upcoming_days)
todos_query = select(Todo).where(
Todo.completed == False,
Todo.due_date.isnot(None),
Todo.due_date <= upcoming_cutoff
).order_by(Todo.due_date.asc())
todos_result = await db.execute(todos_query)
upcoming_todos = todos_result.scalars().all()
# Active reminders (not dismissed, is_active = true)
reminders_query = select(Reminder).where(
Reminder.is_active == True,
Reminder.is_dismissed == False
).order_by(Reminder.remind_at.asc())
reminders_result = await db.execute(reminders_query)
active_reminders = reminders_result.scalars().all()
# Project stats
total_projects_result = await db.execute(select(func.count(Project.id)))
total_projects = total_projects_result.scalar()
projects_by_status_query = select(
Project.status,
func.count(Project.id).label("count")
).group_by(Project.status)
projects_by_status_result = await db.execute(projects_by_status_query)
projects_by_status = {row[0]: row[1] for row in projects_by_status_result}
# Total people
total_people_result = await db.execute(select(func.count(Person.id)))
total_people = total_people_result.scalar()
# Total locations
total_locations_result = await db.execute(select(func.count(Location.id)))
total_locations = total_locations_result.scalar()
return {
"todays_events": [
{
"id": event.id,
"title": event.title,
"start_datetime": event.start_datetime,
"end_datetime": event.end_datetime,
"all_day": event.all_day,
"color": event.color
}
for event in todays_events
],
"upcoming_todos": [
{
"id": todo.id,
"title": todo.title,
"due_date": todo.due_date,
"priority": todo.priority,
"category": todo.category
}
for todo in upcoming_todos
],
"active_reminders": [
{
"id": reminder.id,
"title": reminder.title,
"remind_at": reminder.remind_at
}
for reminder in active_reminders
],
"project_stats": {
"total": total_projects,
"by_status": projects_by_status
},
"total_people": total_people,
"total_locations": total_locations
}
@router.get("/upcoming")
async def get_upcoming(
days: int = Query(default=7, ge=1, le=90),
db: AsyncSession = Depends(get_db),
current_user: Settings = Depends(get_current_session)
):
"""Get unified list of upcoming items (todos, events, reminders) sorted by date."""
today = date.today()
cutoff_date = today + timedelta(days=days)
cutoff_datetime = datetime.combine(cutoff_date, datetime.max.time())
# Get upcoming todos with due dates
todos_query = select(Todo).where(
Todo.completed == False,
Todo.due_date.isnot(None),
Todo.due_date <= cutoff_date
)
todos_result = await db.execute(todos_query)
todos = todos_result.scalars().all()
# Get upcoming events (from today onward)
today_start = datetime.combine(today, datetime.min.time())
events_query = select(CalendarEvent).where(
CalendarEvent.start_datetime >= today_start,
CalendarEvent.start_datetime <= cutoff_datetime,
)
events_result = await db.execute(events_query)
events = events_result.scalars().all()
# Get upcoming reminders
reminders_query = select(Reminder).where(
Reminder.is_active == True,
Reminder.is_dismissed == False,
Reminder.remind_at <= cutoff_datetime
)
reminders_result = await db.execute(reminders_query)
reminders = reminders_result.scalars().all()
# Combine into unified list
upcoming_items: List[Dict[str, Any]] = []
for todo in todos:
upcoming_items.append({
"type": "todo",
"id": todo.id,
"title": todo.title,
"date": todo.due_date.isoformat() if todo.due_date else None,
"datetime": None,
"priority": todo.priority,
"category": todo.category
})
for event in events:
upcoming_items.append({
"type": "event",
"id": event.id,
"title": event.title,
"date": event.start_datetime.date().isoformat(),
"datetime": event.start_datetime.isoformat(),
"all_day": event.all_day,
"color": event.color
})
for reminder in reminders:
upcoming_items.append({
"type": "reminder",
"id": reminder.id,
"title": reminder.title,
"date": reminder.remind_at.date().isoformat(),
"datetime": reminder.remind_at.isoformat()
})
# Sort by date/datetime
upcoming_items.sort(key=lambda x: x["datetime"] if x["datetime"] else x["date"])
return {
"items": upcoming_items,
"days": days,
"cutoff_date": cutoff_date.isoformat()
}