diff --git a/app/core/permission_registry.py b/app/core/permission_registry.py index fd82fdf..d6721aa 100644 --- a/app/core/permission_registry.py +++ b/app/core/permission_registry.py @@ -59,6 +59,12 @@ CORE_PERMISSIONS: list[dict[str, str]] = [ {"key": "currencies:write", "label": "Currencies: Write", "category": "core", "module": "currencies"}, {"key": "import_export:read", "label": "Import/Export: Read", "category": "core", "module": "import_export"}, {"key": "import_export:write", "label": "Import/Export: Write", "category": "core", "module": "import_export"}, + {"key": "workspaces:read", "label": "Workspaces: Read", "category": "core", "module": "workspaces"}, + {"key": "workspaces:create", "label": "Workspaces: Create", "category": "core", "module": "workspaces"}, + {"key": "workspaces:update", "label": "Workspaces: Update", "category": "core", "module": "workspaces"}, + {"key": "workspaces:delete", "label": "Workspaces: Delete", "category": "core", "module": "workspaces"}, + {"key": "workspaces:assign_users", "label": "Workspaces: Assign Users", "category": "core", "module": "workspaces"}, + {"key": "workspaces:configure_modules", "label": "Workspaces: Configure Modules", "category": "core", "module": "workspaces"}, {"key": "system:admin", "label": "System: Admin (cross-tenant)", "category": "system", "module": "system"}, ] diff --git a/app/routes/workspaces.py b/app/routes/workspaces.py index d921bfc..dbb8b08 100644 --- a/app/routes/workspaces.py +++ b/app/routes/workspaces.py @@ -284,7 +284,8 @@ async def list_widgets( wid = uuid.UUID(workspace_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid workspace_id", "code": "invalid_id"}) - return {"items": await workspace_service.get_widgets(db, tenant_id, wid), "total": 0} + widgets = await workspace_service.get_widgets(db, tenant_id, wid) + return {"items": widgets, "total": len(widgets)} @router.post("/{workspace_id}/widgets", status_code=status.HTTP_201_CREATED) @@ -317,11 +318,12 @@ async def update_widget( """Update a widget.""" tenant_id = uuid.UUID(current_user["tenant_id"]) try: + ws_id = uuid.UUID(workspace_id) wid = uuid.UUID(widget_id) except ValueError: - raise HTTPException(400, detail={"detail": "Invalid widget_id", "code": "invalid_id"}) + raise HTTPException(400, detail={"detail": "Invalid ID", "code": "invalid_id"}) result = await workspace_service.update_widget( - db, tenant_id, wid, + db, tenant_id, ws_id, wid, body.position_x, body.position_y, body.width, body.height, body.config, ) if result is None: @@ -339,10 +341,11 @@ async def delete_widget( """Delete a widget.""" tenant_id = uuid.UUID(current_user["tenant_id"]) try: + ws_id = uuid.UUID(workspace_id) wid = uuid.UUID(widget_id) except ValueError: - raise HTTPException(400, detail={"detail": "Invalid widget_id", "code": "invalid_id"}) - deleted = await workspace_service.delete_widget(db, tenant_id, wid) + raise HTTPException(400, detail={"detail": "Invalid ID", "code": "invalid_id"}) + deleted = await workspace_service.delete_widget(db, tenant_id, ws_id, wid) if not deleted: raise HTTPException(404, detail={"detail": "Widget not found", "code": "not_found"}) diff --git a/app/services/workspace_service.py b/app/services/workspace_service.py index 85b36c8..410a6fa 100644 --- a/app/services/workspace_service.py +++ b/app/services/workspace_service.py @@ -382,11 +382,10 @@ async def get_workspace_context( if wu is None: return None # User not assigned — caller can check is_system_admin - # Get visible modules + # Get all modules (including hidden) — frontend needs is_visible flag mod_q = select(WorkspaceModule).where( WorkspaceModule.workspace_id == workspace_id, WorkspaceModule.tenant_id == tenant_id, - WorkspaceModule.is_visible == True, # noqa: E712 ).order_by(WorkspaceModule.menu_order) mod_result = await db.execute(mod_q) modules = mod_result.scalars().all() @@ -407,6 +406,7 @@ async def get_workspace_context( "modules": [ { "module_key": m.module_key, + "is_visible": m.is_visible, "menu_order": m.menu_order, "config": m.config or {}, } @@ -485,15 +485,16 @@ async def create_widget( async def update_widget( - db: AsyncSession, tenant_id: uuid.UUID, widget_id: uuid.UUID, + db: AsyncSession, tenant_id: uuid.UUID, workspace_id: uuid.UUID, widget_id: uuid.UUID, position_x: int | None = None, position_y: int | None = None, width: int | None = None, height: int | None = None, config: dict | None = None, ) -> dict[str, Any] | None: - """Update a widget.""" + """Update a widget. Verifies workspace_id and tenant_id.""" q = select(WorkspaceWidget).where( WorkspaceWidget.id == widget_id, WorkspaceWidget.tenant_id == tenant_id, + WorkspaceWidget.workspace_id == workspace_id, ) result = await db.execute(q) w = result.scalar_one_or_none() @@ -524,12 +525,13 @@ async def update_widget( async def delete_widget( - db: AsyncSession, tenant_id: uuid.UUID, widget_id: uuid.UUID + db: AsyncSession, tenant_id: uuid.UUID, workspace_id: uuid.UUID, widget_id: uuid.UUID ) -> bool: - """Delete a widget.""" + """Delete a widget. Verifies workspace_id and tenant_id.""" q = select(WorkspaceWidget).where( WorkspaceWidget.id == widget_id, WorkspaceWidget.tenant_id == tenant_id, + WorkspaceWidget.workspace_id == workspace_id, ) result = await db.execute(q) w = result.scalar_one_or_none() @@ -540,22 +542,6 @@ async def delete_widget( return True -# ─── Manager Role Check ─────────────────────────────────────── - -async def is_workspace_manager( - db: AsyncSession, tenant_id: uuid.UUID, workspace_id: uuid.UUID, user_id: uuid.UUID -) -> bool: - """Check if a user is a manager of a workspace (or system admin).""" - q = select(WorkspaceUser).where( - WorkspaceUser.workspace_id == workspace_id, - WorkspaceUser.user_id == user_id, - WorkspaceUser.tenant_id == tenant_id, - WorkspaceUser.role == "manager", - ) - result = await db.execute(q) - return result.scalar_one_or_none() is not None - - # ─── Cross-Tenant Validation ────────────────────────────────── async def verify_user_same_tenant( @@ -606,29 +592,15 @@ async def seed_default_workspace( tenant_id=tenant_id, workspace_id=ws.id, user_id=user_id, - role="manager", + role="member", is_default=True, assigned_by=user_id, ) db.add(wu) - # Add all standard modules visible by default - standard_modules = [ - ("dashboard", 0), ("contacts", 10), ("calendar", 20), - ("mail", 30), ("tasks", 40), ("dms", 50), - ("kommunikation", 60), ("reports", 70), ("automation", 80), - ("tags", 90), ("settings", 100), ("audit", 110), - ] - for key, order in standard_modules: - wm = WorkspaceModule( - tenant_id=tenant_id, - workspace_id=ws.id, - module_key=key, - is_visible=True, - menu_order=order, - config={}, - ) - db.add(wm) + # No hardcoded modules — workspace starts empty. + # Modules are configured by the admin via the workspace settings UI. + # If no modules are configured, all active+permitted modules remain visible (backward compatible). await db.flush() return _workspace_to_dict(ws, user_count=1) diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 4402469..e3c14d9 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -70,8 +70,8 @@ export function Sidebar() { // Use hasPermission directly — permissions are loaded via useUserPermissions hook const canAccess = (perm?: string): boolean => { if (!perm) return true; - // While permissions are loading (undefined), show everything; backend 403 catches errors - if (!user?.permissions && user?.is_system_admin === undefined) return true; + // While permissions are loading (undefined), show nothing for protected items + if (!user?.permissions && user?.is_system_admin === undefined) return false; return hasPermission(perm); }; diff --git a/frontend/src/components/layout/TopBar.tsx b/frontend/src/components/layout/TopBar.tsx index ccd2e65..1e8ec49 100644 --- a/frontend/src/components/layout/TopBar.tsx +++ b/frontend/src/components/layout/TopBar.tsx @@ -25,7 +25,7 @@ export function TopBar() { const { hasPermission } = usePermission(); // While permissions are loading (undefined), show everything; backend 403 catches errors const canAccess = (perm: string): boolean => { - if (!user?.permissions && user?.is_system_admin === undefined) return true; + if (!user?.permissions && user?.is_system_admin === undefined) return false; return hasPermission(perm); }; const restoreWindow = useWindowStore((s) => s.restoreWindow); diff --git a/frontend/src/components/settings/WorkspaceManager.tsx b/frontend/src/components/settings/WorkspaceManager.tsx index 11fc4f7..edf3a76 100644 --- a/frontend/src/components/settings/WorkspaceManager.tsx +++ b/frontend/src/components/settings/WorkspaceManager.tsx @@ -1,21 +1,13 @@ -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useWorkspaces, useCreateWorkspace, useUpdateWorkspace, useDeleteWorkspace, useSetWorkspaceModules, useAssignWorkspaceUser, useRemoveWorkspaceUser, type Workspace, type WorkspaceModule } from '@/api/hooks/workspaces'; +import { usePluginStore } from '@/store/pluginStore'; import { LayoutGrid, Plus, Trash2, Edit, Users, Save, X, Check } from 'lucide-react'; -const AVAILABLE_MODULES = [ - { key: 'contacts', label: 'Kontakte' }, - { key: 'calendar', label: 'Kalender' }, - { key: 'mail', label: 'E-Mail' }, - { key: 'tasks', label: 'Aufgaben' }, - { key: 'dms', label: 'Dokumente' }, - { key: 'kommunikation', label: 'Kommunikation' }, - { key: 'reports', label: 'Reports' }, - { key: 'automation', label: 'Automation' }, - { key: 'tags', label: 'Tags' }, +// Static core menu items — same as Sidebar.tsx +const CORE_MENU_ITEMS = [ { key: 'dashboard', label: 'Dashboard' }, - { key: 'settings', label: 'Einstellungen' }, - { key: 'audit', label: 'Audit-Log' }, + { key: 'contacts', label: 'Kontakte' }, ]; export function WorkspaceManager() { @@ -25,6 +17,24 @@ export function WorkspaceManager() { const updateWs = useUpdateWorkspace(); const deleteWs = useDeleteWorkspace(); const setModules = useSetWorkspaceModules(); + const manifests = usePluginStore(s => s.manifests); + + // Dynamically build available modules from core menu items + plugin manifests + const availableModules = useMemo(() => { + const pluginItems = manifests + .flatMap(m => m.menu_items || []) + .map(item => ({ + key: item.path.replace(/^\//, '').split('/')[0], + label: item.label || item.label_key, + })); + // Merge core + plugin, deduplicate by key + const seen = new Set(); + return [...CORE_MENU_ITEMS, ...pluginItems].filter(m => { + if (seen.has(m.key)) return false; + seen.add(m.key); + return true; + }); + }, [manifests]); const [showCreate, setShowCreate] = useState(false); const [editingId, setEditingId] = useState(null); @@ -59,7 +69,7 @@ export function WorkspaceManager() { const openModuleEditor = (ws: Workspace) => { setModuleWsId(ws.id); const existing = ws.modules || []; - setModuleConfig(AVAILABLE_MODULES.map(m => { + setModuleConfig(availableModules.map(m => { const existingMod = existing.find(e => e.module_key === m.key); return { module_key: m.key, @@ -144,7 +154,7 @@ export function WorkspaceManager() {
{moduleConfig.map(m => { - const mod = AVAILABLE_MODULES.find(a => a.key === m.module_key); + const mod = availableModules.find(a => a.key === m.module_key); return (