Build and Push / build (release) Successful in 1m28s
- 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>
117 lines
3.3 KiB
Python
117 lines
3.3 KiB
Python
"""
|
|
Static Files Controller
|
|
|
|
Serves static files for widgets and other frontend assets.
|
|
"""
|
|
from fastapi import APIRouter
|
|
from fastapi.responses import FileResponse, HTMLResponse
|
|
from pathlib import Path
|
|
|
|
from src.shared.base import BaseController
|
|
from src.shared.logging import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class StaticController(BaseController):
|
|
"""
|
|
Controller for serving static files
|
|
|
|
Provides endpoints for:
|
|
- Organizr widgets
|
|
- Other static assets
|
|
"""
|
|
|
|
def __init__(self):
|
|
super().__init__(prefix="/static", tags=["Static"])
|
|
# Static files are at the root of the project
|
|
self.static_dir = Path(__file__).parent.parent.parent.parent / "static"
|
|
|
|
def create_router(self) -> APIRouter:
|
|
"""Create and configure the router"""
|
|
router = APIRouter(prefix=self.prefix, tags=self.tags)
|
|
|
|
@router.get(
|
|
"/widgets/{filename}",
|
|
response_class=HTMLResponse,
|
|
summary="Get widget file"
|
|
)
|
|
async def get_widget(filename: str):
|
|
"""
|
|
Serve widget HTML files
|
|
|
|
Args:
|
|
filename: Widget filename (e.g., service-control.html)
|
|
|
|
Returns:
|
|
HTML file content
|
|
"""
|
|
widget_path = self.static_dir / "widgets" / filename
|
|
|
|
if not widget_path.exists():
|
|
return HTMLResponse(
|
|
content=f"<h1>404 - Widget not found</h1><p>{filename}</p>",
|
|
status_code=404
|
|
)
|
|
|
|
if not widget_path.is_file():
|
|
return HTMLResponse(
|
|
content=f"<h1>400 - Not a file</h1>",
|
|
status_code=400
|
|
)
|
|
|
|
# Security: Ensure the path is within the static directory
|
|
try:
|
|
widget_path.resolve().relative_to(self.static_dir.resolve())
|
|
except ValueError:
|
|
return HTMLResponse(
|
|
content=f"<h1>403 - Forbidden</h1>",
|
|
status_code=403
|
|
)
|
|
|
|
logger.info(f"Serving widget: {filename}")
|
|
return FileResponse(
|
|
widget_path,
|
|
media_type="text/html",
|
|
headers={
|
|
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
"Pragma": "no-cache",
|
|
"Expires": "0"
|
|
}
|
|
)
|
|
|
|
@router.get(
|
|
"/widgets",
|
|
summary="List available widgets"
|
|
)
|
|
async def list_widgets():
|
|
"""
|
|
List all available widget files
|
|
|
|
Returns:
|
|
List of widget filenames
|
|
"""
|
|
widgets_dir = self.static_dir / "widgets"
|
|
|
|
if not widgets_dir.exists():
|
|
return {"widgets": [], "message": "Widgets directory not found"}
|
|
|
|
widgets = []
|
|
for file in widgets_dir.glob("*.html"):
|
|
widgets.append({
|
|
"name": file.name,
|
|
"url": f"/static/widgets/{file.name}",
|
|
"size": file.stat().st_size
|
|
})
|
|
|
|
return {
|
|
"widgets": widgets,
|
|
"count": len(widgets)
|
|
}
|
|
|
|
return router
|
|
|
|
|
|
# Create controller instance
|
|
static_controller = StaticController()
|