"""MiniApps API — universal registry listing (Phase M1). Platform-level endpoint (the registry is a platform concept, not owned by a single plugin): lists MiniApps with server-side permission filtering (fail-closed) and optional host filter. """ from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException from app.core.permissions import check_permission from app.deps import get_current_user from app.plugins.miniapp_registry import get_miniapp_registry router = APIRouter(prefix="/api/v1/miniapps", tags=["miniapps"]) def _user_permits(current_user: dict, app: dict) -> bool: """Empty permission = visible to everyone; otherwise fail-closed check.""" required = app.get("permission") or "" if not required: return True if current_user.get("is_system_admin"): return True return check_permission(current_user, required) @router.get("") async def list_miniapps( host: str | None = None, current_user: dict = Depends(get_current_user), ): """List MiniApps visible to the current user (permission-filtered). ``?host=chat|dashboard|window`` filters by the hosts declared on the MiniApp definition. """ registry = get_miniapp_registry() items = [a for a in registry.list_apps(host=host) if _user_permits(current_user, a)] items.sort(key=lambda a: a.get("order", 100)) return {"items": items, "total": len(items)} @router.get("/{app_id}") async def get_miniapp( app_id: str, current_user: dict = Depends(get_current_user), ): """Get a single MiniApp definition (403 without permission, 404 unknown).""" app = get_miniapp_registry().get_app(app_id) if app is None: raise HTTPException(404, detail={"detail": "MiniApp not found", "code": "not_found"}) data = app.model_dump() if not _user_permits(current_user, data): raise HTTPException( 403, detail={ "detail": f"Permission '{data['permission']}' required", "code": "forbidden", }, ) return data