From 924d28cbf23c21b1711c303461f9fe23070d2593 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Fri, 24 Jul 2026 08:19:45 +0200 Subject: [PATCH] fix: resolve menu duplicates, route prefix conflicts, API path bugs, permissions.deleted_at, remove test plugin from production --- .../versions/0031_permissions_soft_delete.py | 37 ++++++ app/plugins/builtins/entity_links/plugin.py | 2 +- app/plugins/builtins/entity_links/routes.py | 2 +- .../migrations/0001_bad_migration.sql | 7 - .../builtins/migrations/0001_test_plugin.sql | 10 -- .../permissions/migrations/0001_initial.sql | 2 + app/plugins/builtins/permissions/plugin.py | 2 +- app/plugins/builtins/permissions/routes.py | 2 +- frontend/src/api/plugins.ts | 4 +- frontend/src/api/search.ts | 6 +- frontend/src/components/layout/Sidebar.tsx | 121 ++---------------- .../test_sample_plugin.py | 0 12 files changed, 61 insertions(+), 134 deletions(-) create mode 100644 alembic/versions/0031_permissions_soft_delete.py delete mode 100644 app/plugins/builtins/migrations/0001_bad_migration.sql delete mode 100644 app/plugins/builtins/migrations/0001_test_plugin.sql rename app/plugins/builtins/test_sample.py => tests/test_sample_plugin.py (100%) diff --git a/alembic/versions/0031_permissions_soft_delete.py b/alembic/versions/0031_permissions_soft_delete.py new file mode 100644 index 0000000..4d7f2f9 --- /dev/null +++ b/alembic/versions/0031_permissions_soft_delete.py @@ -0,0 +1,37 @@ +"""Add deleted_at column to permissions and share_links tables. + +The Permission and ShareLink models inherit TenantMixin which includes +SoftDeleteMixin (deleted_at), but the original plugin migration did not +create this column. This migration adds it for existing databases. + +Revision ID: 0031_permissions_soft_delete +Revises: 0030_contact_merge_history +Create Date: 2025-07-24 +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers +revision = "0031_permissions_soft_delete" +down_revision = "0030_contact_merge_history" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Add deleted_at to permissions table (if not exists) + op.add_column( + "permissions", + sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), + ) + # Add deleted_at to share_links table (if not exists) + op.add_column( + "share_links", + sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), + ) + + +def downgrade() -> None: + op.drop_column("share_links", "deleted_at") + op.drop_column("permissions", "deleted_at") diff --git a/app/plugins/builtins/entity_links/plugin.py b/app/plugins/builtins/entity_links/plugin.py index e3c329a..f423f90 100644 --- a/app/plugins/builtins/entity_links/plugin.py +++ b/app/plugins/builtins/entity_links/plugin.py @@ -19,7 +19,7 @@ class EntityLinksPlugin(BasePlugin): dependencies=[], routes=[ PluginRouteDef( - path="/api/v1/dms", + path="/api/v1/entity-links", module="app.plugins.builtins.entity_links.routes", router_attr="router", ), diff --git a/app/plugins/builtins/entity_links/routes.py b/app/plugins/builtins/entity_links/routes.py index ee578bf..54ca10d 100644 --- a/app/plugins/builtins/entity_links/routes.py +++ b/app/plugins/builtins/entity_links/routes.py @@ -13,7 +13,7 @@ from app.deps import get_current_user, require_permission from app.plugins.builtins.entity_links.models import EntityLink from app.plugins.builtins.entity_links.schemas import EntityLinkRequest -router = APIRouter(prefix="/api/v1/dms", tags=["entity-links"]) +router = APIRouter(prefix="/api/v1/entity-links", tags=["entity-links"]) contact_router = APIRouter(prefix="/api/v1/contacts", tags=["entity-links"]) VALID_ENTITY_TYPES = {"contact"} diff --git a/app/plugins/builtins/migrations/0001_bad_migration.sql b/app/plugins/builtins/migrations/0001_bad_migration.sql deleted file mode 100644 index 11de211..0000000 --- a/app/plugins/builtins/migrations/0001_bad_migration.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Bad migration: creates table WITHOUT tenant_id (should be rejected by validator) -CREATE TABLE IF NOT EXISTS plugin_bad_table ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - title VARCHAR(200) NOT NULL, - content TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT now() -); diff --git a/app/plugins/builtins/migrations/0001_test_plugin.sql b/app/plugins/builtins/migrations/0001_test_plugin.sql deleted file mode 100644 index 921a4d5..0000000 --- a/app/plugins/builtins/migrations/0001_test_plugin.sql +++ /dev/null @@ -1,10 +0,0 @@ --- Test plugin migration: creates plugin_test_data table with tenant_id -CREATE TABLE IF NOT EXISTS plugin_test_data ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL, - title VARCHAR(200) NOT NULL, - content TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -); -CREATE INDEX IF NOT EXISTS ix_plugin_test_data_tenant ON plugin_test_data(tenant_id); diff --git a/app/plugins/builtins/permissions/migrations/0001_initial.sql b/app/plugins/builtins/permissions/migrations/0001_initial.sql index 4c84cee..1fbdd54 100644 --- a/app/plugins/builtins/permissions/migrations/0001_initial.sql +++ b/app/plugins/builtins/permissions/migrations/0001_initial.sql @@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS permissions ( user_id UUID NOT NULL, group_id UUID, access_level VARCHAR(10) NOT NULL DEFAULT 'read', + deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT uq_permissions_file_user_level UNIQUE (tenant_id, file_id, user_id, access_level) @@ -22,6 +23,7 @@ CREATE TABLE IF NOT EXISTS share_links ( expires_at TIMESTAMPTZ, access_level VARCHAR(10) NOT NULL DEFAULT 'download', created_by UUID, + deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); diff --git a/app/plugins/builtins/permissions/plugin.py b/app/plugins/builtins/permissions/plugin.py index a3fb1fb..9968381 100644 --- a/app/plugins/builtins/permissions/plugin.py +++ b/app/plugins/builtins/permissions/plugin.py @@ -17,7 +17,7 @@ class PermissionsPlugin(BasePlugin): dependencies=[], routes=[ PluginRouteDef( - path="/api/v1/dms", + path="/api/v1/permissions", module="app.plugins.builtins.permissions.routes", router_attr="router", ), diff --git a/app/plugins/builtins/permissions/routes.py b/app/plugins/builtins/permissions/routes.py index c3e6eab..d724cdc 100644 --- a/app/plugins/builtins/permissions/routes.py +++ b/app/plugins/builtins/permissions/routes.py @@ -20,7 +20,7 @@ from app.plugins.builtins.permissions.schemas import ( ShareLinkVerifyRequest, ) -router = APIRouter(prefix="/api/v1/dms", tags=["permissions"]) +router = APIRouter(prefix="/api/v1/permissions", tags=["permissions"]) public_router = APIRouter(prefix="/api/public", tags=["public-share"]) diff --git a/frontend/src/api/plugins.ts b/frontend/src/api/plugins.ts index fd3b3e7..76dea95 100644 --- a/frontend/src/api/plugins.ts +++ b/frontend/src/api/plugins.ts @@ -73,7 +73,7 @@ export function useUploadPlugin() { const formData = new FormData(); formData.append('file', file); const { default: apiClient } = await import('./client'); - const res = await apiClient.post('/api/v1/plugins/upload', formData, { + const res = await apiClient.post('/plugins/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); return res.data; @@ -89,7 +89,7 @@ export function useInstallPluginFromUrl() { return useMutation({ mutationFn: async (url: string) => { const { default: apiClient } = await import('./client'); - const res = await apiClient.post('/api/v1/plugins/install-url', { url }); + const res = await apiClient.post('/plugins/install-url', { url }); return res.data; }, onSuccess: () => { diff --git a/frontend/src/api/search.ts b/frontend/src/api/search.ts index 49ad5c3..87a5d34 100644 --- a/frontend/src/api/search.ts +++ b/frontend/src/api/search.ts @@ -16,7 +16,7 @@ export interface SearchResponse { } export async function search(query: string, entityTypes?: string[], limit = 20): Promise { - const r = await apiClient.post('/api/v1/search', { + const r = await apiClient.post('/search', { query, entity_types: entityTypes, limit, @@ -25,12 +25,12 @@ export async function search(query: string, entityTypes?: string[], limit = 20): } export async function searchSuggest(q: string): Promise { - const r = await apiClient.get('/api/v1/search/suggest', { params: { q } }); + const r = await apiClient.get('/search/suggest', { params: { q } }); return r.data.suggestions; } export async function searchSimilar(entityType: string, entityId: string): Promise { - const r = await apiClient.post('/api/v1/search/similar', { + const r = await apiClient.post('/search/similar', { entity_type: entityType, entity_id: entityId, }); diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index da7244d..cb2b274 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -3,21 +3,10 @@ import clsx from 'clsx'; import { NavLink, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useUIStore } from '@/store/uiStore'; -import { Calendar, ChevronRight, FileText, Home, Mail, Monitor, Users } from 'lucide-react'; +import { ChevronRight, FileText, Home, Settings, Users } from 'lucide-react'; import { usePluginStore } from '@/store/pluginStore'; import * as LucideIcons from 'lucide-react'; -interface NavLeaf { - to: string; - labelKey: string; -} - -interface NavTreeItem { - labelKey: string; - icon: React.ReactNode; - children: NavLeaf[]; -} - interface NavSingleItem { to: string; labelKey: string; @@ -33,40 +22,16 @@ function getIcon(name: string): React.ReactNode { return Icon ? : ; } +// Only non-plugin items: dashboard, contacts, settings. +// All other menu entries (calendar, dms, mail, ai-assistant, automation, reports, tasks, communication) +// are provided by plugin manifests via usePluginStore(s => s.getAllMenuItems()). const singleItems: NavSingleItem[] = [ { to: '/dashboard', labelKey: 'nav.dashboard', icon: