Files
core-api/src/domains/dashboard/controller.py
T
Jeroen SchweitzerandClaude Opus 4.5 381d43b60b
Build and Push / build (release) Successful in 1m28s
feat: add dashboard API with quick links and widgets
- Dashboard domain with Quick Links CRUD + reorder endpoints
- Dashboard widgets management endpoints
- Database migrations for quick_links and dashboard_widgets tables
- Static file controller for Organizr widgets
- Default local user when OIDC is disabled
- Domain-based architecture refactor (src/domains/, src/shared/)
- Test suite updated for new structure (285 tests passing)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 12:27:57 +01:00

306 lines
11 KiB
Python

"""
Dashboard Controller
Provides API endpoints for dashboard management including quick links.
"""
from fastapi import APIRouter, HTTPException, Depends, Query
from typing import Dict, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from src.shared.base import BaseController
from src.shared.database import get_async_session
from src.shared.logging import get_logger
from src.domains.auth.oidc import get_current_user, get_optional_user
from src.domains.dashboard.service import get_dashboard_service
from src.domains.dashboard.schemas import (
QuickLinkCreate,
QuickLinkUpdate,
QuickLinkResponse,
QuickLinkListResponse,
QuickLinkReorderRequest,
QuickLinkReorderResponse,
DashboardWidgetCreate,
DashboardWidgetUpdate,
DashboardWidgetResponse,
DashboardWidgetListResponse,
)
logger = get_logger(__name__)
class DashboardController(BaseController):
"""
Controller for dashboard operations
Provides endpoints for:
- Quick links CRUD
- Quick links reordering
- Dashboard widgets management
"""
def __init__(self):
super().__init__(prefix="/dashboard", tags=["Dashboard"])
def create_router(self) -> APIRouter:
"""Create and configure the router"""
router = APIRouter(prefix=self.prefix, tags=self.tags)
service = get_dashboard_service()
# =====================================================================
# Quick Links
# =====================================================================
@router.get(
"/quick-links",
response_model=QuickLinkListResponse,
summary="List quick links"
)
async def list_quick_links(
category: Optional[str] = Query(None, description="Filter by category"),
include_global: bool = Query(True, description="Include global links"),
visible_only: bool = Query(True, description="Only visible links"),
session: AsyncSession = Depends(get_async_session),
user: Optional[Dict] = Depends(get_optional_user),
):
"""
List quick links for the current user
Returns user-specific links plus global links (if include_global=True).
"""
user_id = user.get("sub") if user else None
links = await service.get_quick_links(
session=session,
user_id=user_id,
include_global=include_global,
category=category,
visible_only=visible_only,
)
return QuickLinkListResponse(
links=[QuickLinkResponse.model_validate(link, from_attributes=True) for link in links],
total=len(links)
)
@router.get(
"/quick-links/{link_id}",
response_model=QuickLinkResponse,
summary="Get a quick link"
)
async def get_quick_link(
link_id: int,
session: AsyncSession = Depends(get_async_session),
user: Optional[Dict] = Depends(get_optional_user),
):
"""Get a specific quick link by ID"""
user_id = user.get("sub") if user else None
link = await service.get_quick_link(session, link_id, user_id)
if not link:
raise HTTPException(status_code=404, detail="Quick link not found")
return QuickLinkResponse.model_validate(link, from_attributes=True)
@router.post(
"/quick-links",
response_model=QuickLinkResponse,
status_code=201,
summary="Create a quick link"
)
async def create_quick_link(
data: QuickLinkCreate,
session: AsyncSession = Depends(get_async_session),
user: Dict = Depends(get_current_user),
):
"""
Create a new quick link for the current user
Links are user-specific by default. Admins can create global links
by setting user_id to null.
"""
user_id = user.get("sub")
link = await service.create_quick_link(session, data, user_id)
logger.info(f"Quick link created: {link.title} by user {user.get('preferred_username')}")
return QuickLinkResponse.model_validate(link, from_attributes=True)
@router.put(
"/quick-links/{link_id}",
response_model=QuickLinkResponse,
summary="Update a quick link"
)
async def update_quick_link(
link_id: int,
data: QuickLinkUpdate,
session: AsyncSession = Depends(get_async_session),
user: Dict = Depends(get_current_user),
):
"""Update an existing quick link"""
user_id = user.get("sub")
link = await service.update_quick_link(session, link_id, data, user_id)
if not link:
raise HTTPException(status_code=404, detail="Quick link not found or not authorized")
return QuickLinkResponse.model_validate(link, from_attributes=True)
@router.delete(
"/quick-links/{link_id}",
status_code=204,
summary="Delete a quick link"
)
async def delete_quick_link(
link_id: int,
session: AsyncSession = Depends(get_async_session),
user: Dict = Depends(get_current_user),
):
"""Delete a quick link"""
user_id = user.get("sub")
success = await service.delete_quick_link(session, link_id, user_id)
if not success:
raise HTTPException(status_code=404, detail="Quick link not found or not authorized")
return None
@router.post(
"/quick-links/reorder",
response_model=QuickLinkReorderResponse,
summary="Reorder quick links"
)
async def reorder_quick_links(
data: QuickLinkReorderRequest,
session: AsyncSession = Depends(get_async_session),
user: Dict = Depends(get_current_user),
):
"""
Reorder quick links by providing link IDs in desired order
The position of each link will be set to its index in the provided list.
"""
user_id = user.get("sub")
reordered = await service.reorder_quick_links(session, data.link_ids, user_id)
return QuickLinkReorderResponse(
success=True,
message=f"Reordered {reordered} links",
reordered_count=reordered
)
# =====================================================================
# Dashboard Widgets
# =====================================================================
@router.get(
"/widgets",
response_model=DashboardWidgetListResponse,
summary="List dashboard widgets"
)
async def list_widgets(
include_defaults: bool = Query(True, description="Include default widgets"),
visible_only: bool = Query(True, description="Only visible widgets"),
session: AsyncSession = Depends(get_async_session),
user: Optional[Dict] = Depends(get_optional_user),
):
"""List dashboard widgets for the current user"""
user_id = user.get("sub") if user else None
widgets = await service.get_widgets(
session=session,
user_id=user_id,
include_defaults=include_defaults,
visible_only=visible_only,
)
return DashboardWidgetListResponse(
widgets=[DashboardWidgetResponse.model_validate(w, from_attributes=True) for w in widgets],
total=len(widgets)
)
@router.get(
"/widgets/{widget_id}",
response_model=DashboardWidgetResponse,
summary="Get a dashboard widget"
)
async def get_widget(
widget_id: int,
session: AsyncSession = Depends(get_async_session),
user: Optional[Dict] = Depends(get_optional_user),
):
"""Get a specific dashboard widget by ID"""
user_id = user.get("sub") if user else None
widget = await service.get_widget(session, widget_id, user_id)
if not widget:
raise HTTPException(status_code=404, detail="Widget not found")
return DashboardWidgetResponse.model_validate(widget, from_attributes=True)
@router.post(
"/widgets",
response_model=DashboardWidgetResponse,
status_code=201,
summary="Create a dashboard widget"
)
async def create_widget(
data: DashboardWidgetCreate,
session: AsyncSession = Depends(get_async_session),
user: Dict = Depends(get_current_user),
):
"""Create a new dashboard widget"""
user_id = user.get("sub")
widget = await service.create_widget(session, data, user_id)
logger.info(f"Widget created: {widget.widget_type} by user {user.get('preferred_username')}")
return DashboardWidgetResponse.model_validate(widget, from_attributes=True)
@router.put(
"/widgets/{widget_id}",
response_model=DashboardWidgetResponse,
summary="Update a dashboard widget"
)
async def update_widget(
widget_id: int,
data: DashboardWidgetUpdate,
session: AsyncSession = Depends(get_async_session),
user: Dict = Depends(get_current_user),
):
"""Update an existing dashboard widget"""
user_id = user.get("sub")
widget = await service.update_widget(session, widget_id, data, user_id)
if not widget:
raise HTTPException(status_code=404, detail="Widget not found or not authorized")
return DashboardWidgetResponse.model_validate(widget, from_attributes=True)
@router.delete(
"/widgets/{widget_id}",
status_code=204,
summary="Delete a dashboard widget"
)
async def delete_widget(
widget_id: int,
session: AsyncSession = Depends(get_async_session),
user: Dict = Depends(get_current_user),
):
"""Delete a dashboard widget"""
user_id = user.get("sub")
success = await service.delete_widget(session, widget_id, user_id)
if not success:
raise HTTPException(status_code=404, detail="Widget not found or not authorized")
return None
return router
# Create controller instance
dashboard_controller = DashboardController()