fix: embedding migration, worker error handling, path traversal security, response mismatches (unread-count, plugins, manifests), frontend type safety
This commit is contained in:
@@ -6,6 +6,17 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiGet, apiPatch, apiDelete } from './client';
|
||||
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 {
|
||||
type_key: string;
|
||||
plugin_name: string;
|
||||
@@ -16,17 +27,29 @@ export interface NotificationTypeItem {
|
||||
is_enabled: boolean;
|
||||
}
|
||||
|
||||
export interface UnreadCountResponse {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface NotificationPreferenceItem {
|
||||
type_key: string;
|
||||
is_enabled: boolean;
|
||||
}
|
||||
|
||||
export function useNotifications() {
|
||||
return useQuery({
|
||||
queryKey: ['notifications'],
|
||||
queryFn: () => apiGet<PaginatedResponse<any>>('/notifications'),
|
||||
queryFn: () => apiGet<PaginatedResponse<NotificationItem>>('/notifications'),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUnreadNotificationCount() {
|
||||
return useQuery({
|
||||
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() {
|
||||
return useQuery({
|
||||
queryKey: ['notification-preferences'],
|
||||
queryFn: () => apiGet<{ items: { type_key: string; is_enabled: boolean }[] }>('/notifications/preferences'),
|
||||
queryFn: () => apiGet<{ items: NotificationPreferenceItem[] }>('/notifications/preferences'),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+36
-14
@@ -5,22 +5,44 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiGet, apiPost, apiDelete } from './client';
|
||||
|
||||
// ── Types matching backend schemas ──
|
||||
|
||||
export interface Plugin {
|
||||
name: string;
|
||||
display_name?: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
display_name: string;
|
||||
version: string;
|
||||
status: 'discovered' | 'installed' | 'active' | 'inactive';
|
||||
installed?: boolean;
|
||||
active?: boolean;
|
||||
installed: 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() {
|
||||
return useQuery({
|
||||
queryKey: ['plugins'],
|
||||
queryFn: async () => {
|
||||
const data = await apiGet<any>('/plugins');
|
||||
return data;
|
||||
const data = await apiGet<PluginListResponse>('/plugins');
|
||||
return data.plugins;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -28,7 +50,7 @@ export function usePlugins() {
|
||||
export function useInstallPlugin() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (name: string) => apiPost(`/plugins/${name}/install`),
|
||||
mutationFn: (name: string) => apiPost<PluginActionResponse>(`/plugins/${name}/install`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['plugins'] });
|
||||
},
|
||||
@@ -38,7 +60,7 @@ export function useInstallPlugin() {
|
||||
export function useActivatePlugin() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (name: string) => apiPost(`/plugins/${name}/activate`),
|
||||
mutationFn: (name: string) => apiPost<PluginActionResponse>(`/plugins/${name}/activate`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['plugins'] });
|
||||
},
|
||||
@@ -48,7 +70,7 @@ export function useActivatePlugin() {
|
||||
export function useDeactivatePlugin() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (name: string) => apiPost(`/plugins/${name}/deactivate`),
|
||||
mutationFn: (name: string) => apiPost<PluginActionResponse>(`/plugins/${name}/deactivate`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['plugins'] });
|
||||
},
|
||||
@@ -59,7 +81,7 @@ export function useUninstallPlugin() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ name, removeData }: { name: string; removeData: boolean }) =>
|
||||
apiDelete(`/plugins/${name}?remove_data=${removeData}`),
|
||||
apiDelete<PluginActionResponse>(`/plugins/${name}?remove_data=${removeData}`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['plugins'] });
|
||||
},
|
||||
@@ -76,7 +98,7 @@ export function useUploadPlugin() {
|
||||
const res = await apiClient.post('/plugins/upload', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
return res.data;
|
||||
return res.data as PluginActionResponse;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['plugins'] });
|
||||
@@ -90,10 +112,10 @@ export function useInstallPluginFromUrl() {
|
||||
mutationFn: async (url: string) => {
|
||||
const { default: apiClient } = await import('./client');
|
||||
const res = await apiClient.post('/plugins/install-url', { url });
|
||||
return res.data;
|
||||
return res.data as PluginActionResponse;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['plugins'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,19 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiGet, apiPost, apiPatch, apiDelete } from './client';
|
||||
|
||||
// ── Types matching backend schemas ──
|
||||
|
||||
export interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
permissions: Record<string, any>;
|
||||
field_permissions?: Record<string, any>;
|
||||
permissions: Record<string, boolean>;
|
||||
denied_permissions?: string[];
|
||||
field_permissions?: Record<string, Record<string, string>>;
|
||||
permission_version?: number;
|
||||
}
|
||||
|
||||
export interface RoleListResponse {
|
||||
items: Role[];
|
||||
}
|
||||
|
||||
export interface PermissionItem {
|
||||
@@ -33,11 +41,25 @@ export interface PermissionsResponse {
|
||||
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() {
|
||||
return useQuery({
|
||||
queryKey: ['roles'],
|
||||
queryFn: async () => {
|
||||
const data = await apiGet<any>('/roles');
|
||||
const data = await apiGet<RoleListResponse>('/roles');
|
||||
return data;
|
||||
},
|
||||
});
|
||||
@@ -53,7 +75,7 @@ export function usePermissions() {
|
||||
export function useCreateRole() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: { name: string; permissions: Record<string, any>; field_permissions: Record<string, any> }) =>
|
||||
mutationFn: (data: RoleCreate) =>
|
||||
apiPost('/roles', data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['roles'] });
|
||||
@@ -64,7 +86,7 @@ export function useCreateRole() {
|
||||
export function useUpdateRole() {
|
||||
const queryClient = useQueryClient();
|
||||
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),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['roles'] });
|
||||
|
||||
@@ -6,18 +6,46 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiGet, apiPost, apiPatch, apiDelete } from './client';
|
||||
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) {
|
||||
return useQuery({
|
||||
queryKey: ['users', page, pageSize],
|
||||
queryFn: () =>
|
||||
apiGet<PaginatedResponse<any>>(`/users?page=${page}&page_size=${pageSize}`),
|
||||
apiGet<PaginatedResponse<UserResponse>>(`/users?page=${page}&page_size=${pageSize}`),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUser(id?: string) {
|
||||
return useQuery({
|
||||
queryKey: ['users', id],
|
||||
queryFn: () => apiGet<any>(`/users/${id}`),
|
||||
queryFn: () => apiGet<UserResponse>(`/users/${id}`),
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
@@ -25,7 +53,7 @@ export function useUser(id?: string) {
|
||||
export function useCreateUser() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: any) => apiPost('/users', data),
|
||||
mutationFn: (data: UserCreate) => apiPost('/users', data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||
},
|
||||
@@ -35,7 +63,7 @@ export function useCreateUser() {
|
||||
export function useUpdateUser() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: any }) =>
|
||||
mutationFn: ({ id, data }: { id: string; data: UserUpdate }) =>
|
||||
apiPatch(`/users/${id}`, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||
|
||||
@@ -180,7 +180,7 @@ export function SettingsPluginsPage() {
|
||||
const [confirmUninstall, setConfirmUninstall] = useState<Plugin | null>(null);
|
||||
const [confirmRemoveData, setConfirmRemoveData] = useState(false);
|
||||
|
||||
const plugins: Plugin[] = data?.plugins ?? data ?? [];
|
||||
const plugins: Plugin[] = data ?? [];
|
||||
|
||||
const handleInstall = async (plugin: Plugin) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user