fix: embedding migration, worker error handling, path traversal security, response mismatches (unread-count, plugins, manifests), frontend type safety
This commit is contained in:
@@ -346,6 +346,10 @@ def create_app() -> FastAPI:
|
|||||||
# Don't intercept API routes
|
# Don't intercept API routes
|
||||||
if full_path.startswith(("api/", "docs", "openapi", "redoc")):
|
if full_path.startswith(("api/", "docs", "openapi", "redoc")):
|
||||||
raise HTTPException(status_code=404, detail="Not Found")
|
raise HTTPException(status_code=404, detail="Not Found")
|
||||||
|
# Block path traversal and system file access
|
||||||
|
blocked_prefixes = ("var/log/", "error/", "error_log", "var/", "etc/", "proc/", "sys/")
|
||||||
|
if full_path.startswith(blocked_prefixes) or "/../" in full_path or full_path.endswith("/.."):
|
||||||
|
raise HTTPException(status_code=404, detail="Not Found")
|
||||||
index_path = os.path.join(frontend_dist, "index.html")
|
index_path = os.path.join(frontend_dist, "index.html")
|
||||||
if os.path.isfile(index_path):
|
if os.path.isfile(index_path):
|
||||||
return FileResponse(index_path)
|
return FileResponse(index_path)
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ async def index_mails(ctx: dict[str, Any], mail_ids: list[str]) -> None:
|
|||||||
await index_entity("mail", eid, tenant_id, db)
|
await index_entity("mail", eid, tenant_id, db)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to index mail %s", mail_id)
|
logger.exception("Failed to index mail %s", mail_id)
|
||||||
|
try:
|
||||||
|
await db.rollback()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def index_file(ctx: dict[str, Any], file_id: str) -> None:
|
async def index_file(ctx: dict[str, Any], file_id: str) -> None:
|
||||||
@@ -98,6 +102,10 @@ async def index_file(ctx: dict[str, Any], file_id: str) -> None:
|
|||||||
logger.info("Indexed file %s", file_id)
|
logger.info("Indexed file %s", file_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to index file %s", file_id)
|
logger.exception("Failed to index file %s", file_id)
|
||||||
|
try:
|
||||||
|
await db.rollback()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def index_contact(ctx: dict[str, Any], contact_id: str) -> None:
|
async def index_contact(ctx: dict[str, Any], contact_id: str) -> None:
|
||||||
@@ -119,6 +127,10 @@ async def index_contact(ctx: dict[str, Any], contact_id: str) -> None:
|
|||||||
await index_entity("contact", eid, row["tenant_id"], db)
|
await index_entity("contact", eid, row["tenant_id"], db)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to index contact %s", contact_id)
|
logger.exception("Failed to index contact %s", contact_id)
|
||||||
|
try:
|
||||||
|
await db.rollback()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def index_event(ctx: dict[str, Any], event_id: str) -> None:
|
async def index_event(ctx: dict[str, Any], event_id: str) -> None:
|
||||||
@@ -140,6 +152,10 @@ async def index_event(ctx: dict[str, Any], event_id: str) -> None:
|
|||||||
await index_entity("event", eid, row["tenant_id"], db)
|
await index_entity("event", eid, row["tenant_id"], db)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to index event %s", event_id)
|
logger.exception("Failed to index event %s", event_id)
|
||||||
|
try:
|
||||||
|
await db.rollback()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def reindex(ctx: dict[str, Any], entity_type: str) -> None:
|
async def reindex(ctx: dict[str, Any], entity_type: str) -> None:
|
||||||
@@ -182,6 +198,10 @@ async def reindex(ctx: dict[str, Any], entity_type: str) -> None:
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Reindex failed for %s/%s", entity_type, row["id"])
|
logger.exception("Reindex failed for %s/%s", entity_type, row["id"])
|
||||||
|
try:
|
||||||
|
await db.rollback()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
offset += BATCH_SIZE
|
offset += BATCH_SIZE
|
||||||
|
|
||||||
logger.info("Reindex complete for %s", entity_type)
|
logger.info("Reindex complete for %s", entity_type)
|
||||||
@@ -217,7 +237,15 @@ async def embedding_batch(ctx: dict[str, Any]) -> None:
|
|||||||
await index_entity(etype, row["id"], row["tenant_id"], db)
|
await index_entity(etype, row["id"], row["tenant_id"], db)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Batch index failed for %s/%s", etype, row["id"])
|
logger.exception("Batch index failed for %s/%s", etype, row["id"])
|
||||||
|
try:
|
||||||
|
await db.rollback()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Batch query failed for %s", etype)
|
logger.exception("Batch query failed for %s", etype)
|
||||||
|
try:
|
||||||
|
await db.rollback()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
logger.info("Embedding batch job complete")
|
logger.info("Embedding batch job complete")
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
-- Unified Search: Embedding columns and HNSW indexes
|
-- Unified Search: Embedding columns and HNSW indexes
|
||||||
|
|
||||||
|
-- Ensure pgvector extension is installed
|
||||||
|
CREATE EXTENSION IF NOT EXISTS vector;
|
||||||
|
|
||||||
-- ─── Mails ───
|
-- ─── Mails ───
|
||||||
ALTER TABLE mails ADD COLUMN IF NOT EXISTS embedding vector(768);
|
ALTER TABLE mails ADD COLUMN IF NOT EXISTS embedding vector(768);
|
||||||
CREATE INDEX IF NOT EXISTS ix_mails_embedding ON mails USING hnsw(embedding vector_cosine_ops);
|
CREATE INDEX IF NOT EXISTS ix_mails_embedding ON mails USING hnsw(embedding vector_cosine_ops);
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ async def mark_notification_read_endpoint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/unread-count", response_model=UnreadCountResponse)
|
@router.get("/unread-count")
|
||||||
async def unread_count_endpoint(
|
async def unread_count_endpoint(
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: dict = Depends(require_permission("notifications:read")),
|
current_user: dict = Depends(require_permission("notifications:read")),
|
||||||
@@ -76,7 +76,7 @@ async def unread_count_endpoint(
|
|||||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||||
user_id = uuid.UUID(current_user["user_id"])
|
user_id = uuid.UUID(current_user["user_id"])
|
||||||
count = await get_unread_count(db, tenant_id, user_id)
|
count = await get_unread_count(db, tenant_id, user_id)
|
||||||
return {"count": count}
|
return count
|
||||||
|
|
||||||
|
|
||||||
@router.get("/types")
|
@router.get("/types")
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ async def list_plugins(
|
|||||||
"""List all plugins with their current status (discovered, installed, active, inactive)."""
|
"""List all plugins with their current status (discovered, installed, active, inactive)."""
|
||||||
service = get_plugin_service()
|
service = get_plugin_service()
|
||||||
plugins = await service.list_plugins(db)
|
plugins = await service.list_plugins(db)
|
||||||
return {"plugins": plugins, "total": len(plugins)}
|
return plugins
|
||||||
|
|
||||||
|
|
||||||
@router.get("/manifest")
|
@router.get("/manifest")
|
||||||
@@ -76,7 +76,7 @@ async def get_active_manifests(
|
|||||||
"""
|
"""
|
||||||
service = get_plugin_service()
|
service = get_plugin_service()
|
||||||
manifests = await service.get_active_manifests(db)
|
manifests = await service.get_active_manifests(db)
|
||||||
return {"plugins": manifests, "total": len(manifests)}
|
return manifests
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{name}/config")
|
@router.get("/{name}/config")
|
||||||
|
|||||||
@@ -6,6 +6,17 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { apiGet, apiPatch, apiDelete } from './client';
|
import { apiGet, apiPatch, apiDelete } from './client';
|
||||||
import { PaginatedResponse } from './types';
|
import { PaginatedResponse } from './types';
|
||||||
|
|
||||||
|
// ── Types matching backend schemas ──
|
||||||
|
|
||||||
|
export interface NotificationItem {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
body?: string | null;
|
||||||
|
read_at?: string | null;
|
||||||
|
created_at?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface NotificationTypeItem {
|
export interface NotificationTypeItem {
|
||||||
type_key: string;
|
type_key: string;
|
||||||
plugin_name: string;
|
plugin_name: string;
|
||||||
@@ -16,17 +27,29 @@ export interface NotificationTypeItem {
|
|||||||
is_enabled: boolean;
|
is_enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UnreadCountResponse {
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationPreferenceItem {
|
||||||
|
type_key: string;
|
||||||
|
is_enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export function useNotifications() {
|
export function useNotifications() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['notifications'],
|
queryKey: ['notifications'],
|
||||||
queryFn: () => apiGet<PaginatedResponse<any>>('/notifications'),
|
queryFn: () => apiGet<PaginatedResponse<NotificationItem>>('/notifications'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUnreadNotificationCount() {
|
export function useUnreadNotificationCount() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['notifications', 'unread-count'],
|
queryKey: ['notifications', 'unread-count'],
|
||||||
queryFn: () => apiGet<number>('/notifications/unread-count'),
|
queryFn: async () => {
|
||||||
|
const data = await apiGet<UnreadCountResponse>('/notifications/unread-count');
|
||||||
|
return data.count;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +83,7 @@ export function useNotificationTypes() {
|
|||||||
export function useNotificationPreferences() {
|
export function useNotificationPreferences() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['notification-preferences'],
|
queryKey: ['notification-preferences'],
|
||||||
queryFn: () => apiGet<{ items: { type_key: string; is_enabled: boolean }[] }>('/notifications/preferences'),
|
queryFn: () => apiGet<{ items: NotificationPreferenceItem[] }>('/notifications/preferences'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+35
-13
@@ -5,22 +5,44 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { apiGet, apiPost, apiDelete } from './client';
|
import { apiGet, apiPost, apiDelete } from './client';
|
||||||
|
|
||||||
|
// ── Types matching backend schemas ──
|
||||||
|
|
||||||
export interface Plugin {
|
export interface Plugin {
|
||||||
name: string;
|
name: string;
|
||||||
display_name?: string;
|
display_name: string;
|
||||||
description?: string;
|
version: string;
|
||||||
version?: string;
|
|
||||||
status: 'discovered' | 'installed' | 'active' | 'inactive';
|
status: 'discovered' | 'installed' | 'active' | 'inactive';
|
||||||
installed?: boolean;
|
installed: boolean;
|
||||||
active?: boolean;
|
active: boolean;
|
||||||
|
description: string;
|
||||||
|
dependencies: string[];
|
||||||
|
events: string[];
|
||||||
|
migrations: string[];
|
||||||
|
permissions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginListResponse {
|
||||||
|
plugins: Plugin[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginActionResponse {
|
||||||
|
name: string;
|
||||||
|
display_name: string;
|
||||||
|
version: string;
|
||||||
|
status: string;
|
||||||
|
installed: boolean;
|
||||||
|
active: boolean;
|
||||||
|
dropped_tables: string[];
|
||||||
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePlugins() {
|
export function usePlugins() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['plugins'],
|
queryKey: ['plugins'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const data = await apiGet<any>('/plugins');
|
const data = await apiGet<PluginListResponse>('/plugins');
|
||||||
return data;
|
return data.plugins;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -28,7 +50,7 @@ export function usePlugins() {
|
|||||||
export function useInstallPlugin() {
|
export function useInstallPlugin() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (name: string) => apiPost(`/plugins/${name}/install`),
|
mutationFn: (name: string) => apiPost<PluginActionResponse>(`/plugins/${name}/install`),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['plugins'] });
|
queryClient.invalidateQueries({ queryKey: ['plugins'] });
|
||||||
},
|
},
|
||||||
@@ -38,7 +60,7 @@ export function useInstallPlugin() {
|
|||||||
export function useActivatePlugin() {
|
export function useActivatePlugin() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (name: string) => apiPost(`/plugins/${name}/activate`),
|
mutationFn: (name: string) => apiPost<PluginActionResponse>(`/plugins/${name}/activate`),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['plugins'] });
|
queryClient.invalidateQueries({ queryKey: ['plugins'] });
|
||||||
},
|
},
|
||||||
@@ -48,7 +70,7 @@ export function useActivatePlugin() {
|
|||||||
export function useDeactivatePlugin() {
|
export function useDeactivatePlugin() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (name: string) => apiPost(`/plugins/${name}/deactivate`),
|
mutationFn: (name: string) => apiPost<PluginActionResponse>(`/plugins/${name}/deactivate`),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['plugins'] });
|
queryClient.invalidateQueries({ queryKey: ['plugins'] });
|
||||||
},
|
},
|
||||||
@@ -59,7 +81,7 @@ export function useUninstallPlugin() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ name, removeData }: { name: string; removeData: boolean }) =>
|
mutationFn: ({ name, removeData }: { name: string; removeData: boolean }) =>
|
||||||
apiDelete(`/plugins/${name}?remove_data=${removeData}`),
|
apiDelete<PluginActionResponse>(`/plugins/${name}?remove_data=${removeData}`),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['plugins'] });
|
queryClient.invalidateQueries({ queryKey: ['plugins'] });
|
||||||
},
|
},
|
||||||
@@ -76,7 +98,7 @@ export function useUploadPlugin() {
|
|||||||
const res = await apiClient.post('/plugins/upload', formData, {
|
const res = await apiClient.post('/plugins/upload', formData, {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
});
|
});
|
||||||
return res.data;
|
return res.data as PluginActionResponse;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['plugins'] });
|
queryClient.invalidateQueries({ queryKey: ['plugins'] });
|
||||||
@@ -90,7 +112,7 @@ export function useInstallPluginFromUrl() {
|
|||||||
mutationFn: async (url: string) => {
|
mutationFn: async (url: string) => {
|
||||||
const { default: apiClient } = await import('./client');
|
const { default: apiClient } = await import('./client');
|
||||||
const res = await apiClient.post('/plugins/install-url', { url });
|
const res = await apiClient.post('/plugins/install-url', { url });
|
||||||
return res.data;
|
return res.data as PluginActionResponse;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['plugins'] });
|
queryClient.invalidateQueries({ queryKey: ['plugins'] });
|
||||||
|
|||||||
@@ -5,11 +5,19 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { apiGet, apiPost, apiPatch, apiDelete } from './client';
|
import { apiGet, apiPost, apiPatch, apiDelete } from './client';
|
||||||
|
|
||||||
|
// ── Types matching backend schemas ──
|
||||||
|
|
||||||
export interface Role {
|
export interface Role {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
permissions: Record<string, any>;
|
permissions: Record<string, boolean>;
|
||||||
field_permissions?: Record<string, any>;
|
denied_permissions?: string[];
|
||||||
|
field_permissions?: Record<string, Record<string, string>>;
|
||||||
|
permission_version?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoleListResponse {
|
||||||
|
items: Role[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PermissionItem {
|
export interface PermissionItem {
|
||||||
@@ -33,11 +41,25 @@ export interface PermissionsResponse {
|
|||||||
field_definitions?: FieldDefinition[];
|
field_definitions?: FieldDefinition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RoleCreate {
|
||||||
|
name: string;
|
||||||
|
permissions: Record<string, boolean>;
|
||||||
|
denied_permissions?: string[];
|
||||||
|
field_permissions?: Record<string, Record<string, string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoleUpdate {
|
||||||
|
name?: string;
|
||||||
|
permissions?: Record<string, boolean>;
|
||||||
|
denied_permissions?: string[];
|
||||||
|
field_permissions?: Record<string, Record<string, string>>;
|
||||||
|
}
|
||||||
|
|
||||||
export function useRoles() {
|
export function useRoles() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['roles'],
|
queryKey: ['roles'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const data = await apiGet<any>('/roles');
|
const data = await apiGet<RoleListResponse>('/roles');
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -53,7 +75,7 @@ export function usePermissions() {
|
|||||||
export function useCreateRole() {
|
export function useCreateRole() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (data: { name: string; permissions: Record<string, any>; field_permissions: Record<string, any> }) =>
|
mutationFn: (data: RoleCreate) =>
|
||||||
apiPost('/roles', data),
|
apiPost('/roles', data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['roles'] });
|
queryClient.invalidateQueries({ queryKey: ['roles'] });
|
||||||
@@ -64,7 +86,7 @@ export function useCreateRole() {
|
|||||||
export function useUpdateRole() {
|
export function useUpdateRole() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ id, data }: { id: string; data: { name?: string; permissions?: Record<string, any>; field_permissions?: Record<string, any> } }) =>
|
mutationFn: ({ id, data }: { id: string; data: RoleUpdate }) =>
|
||||||
apiPatch(`/roles/${id}`, data),
|
apiPatch(`/roles/${id}`, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['roles'] });
|
queryClient.invalidateQueries({ queryKey: ['roles'] });
|
||||||
|
|||||||
@@ -6,18 +6,46 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { apiGet, apiPost, apiPatch, apiDelete } from './client';
|
import { apiGet, apiPost, apiPatch, apiDelete } from './client';
|
||||||
import { PaginatedResponse } from './types';
|
import { PaginatedResponse } from './types';
|
||||||
|
|
||||||
|
// ── Types matching backend schemas ──
|
||||||
|
|
||||||
|
export interface UserResponse {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
role_id: string | null;
|
||||||
|
is_active: boolean;
|
||||||
|
tenant_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserCreate {
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
password: string;
|
||||||
|
role?: string;
|
||||||
|
role_id?: string | null;
|
||||||
|
is_active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserUpdate {
|
||||||
|
name?: string;
|
||||||
|
role?: string;
|
||||||
|
role_id?: string | null;
|
||||||
|
is_active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export function useUsers(page = 1, pageSize = 25) {
|
export function useUsers(page = 1, pageSize = 25) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['users', page, pageSize],
|
queryKey: ['users', page, pageSize],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
apiGet<PaginatedResponse<any>>(`/users?page=${page}&page_size=${pageSize}`),
|
apiGet<PaginatedResponse<UserResponse>>(`/users?page=${page}&page_size=${pageSize}`),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUser(id?: string) {
|
export function useUser(id?: string) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['users', id],
|
queryKey: ['users', id],
|
||||||
queryFn: () => apiGet<any>(`/users/${id}`),
|
queryFn: () => apiGet<UserResponse>(`/users/${id}`),
|
||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -25,7 +53,7 @@ export function useUser(id?: string) {
|
|||||||
export function useCreateUser() {
|
export function useCreateUser() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (data: any) => apiPost('/users', data),
|
mutationFn: (data: UserCreate) => apiPost('/users', data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||||
},
|
},
|
||||||
@@ -35,7 +63,7 @@ export function useCreateUser() {
|
|||||||
export function useUpdateUser() {
|
export function useUpdateUser() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ id, data }: { id: string; data: any }) =>
|
mutationFn: ({ id, data }: { id: string; data: UserUpdate }) =>
|
||||||
apiPatch(`/users/${id}`, data),
|
apiPatch(`/users/${id}`, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export function SettingsPluginsPage() {
|
|||||||
const [confirmUninstall, setConfirmUninstall] = useState<Plugin | null>(null);
|
const [confirmUninstall, setConfirmUninstall] = useState<Plugin | null>(null);
|
||||||
const [confirmRemoveData, setConfirmRemoveData] = useState(false);
|
const [confirmRemoveData, setConfirmRemoveData] = useState(false);
|
||||||
|
|
||||||
const plugins: Plugin[] = data?.plugins ?? data ?? [];
|
const plugins: Plugin[] = data ?? [];
|
||||||
|
|
||||||
const handleInstall = async (plugin: Plugin) => {
|
const handleInstall = async (plugin: Plugin) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user