@oxyhq/auth 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -0
- package/dist/cjs/WebOxyProvider.js +287 -0
- package/dist/cjs/hooks/mutations/index.js +23 -0
- package/dist/cjs/hooks/mutations/mutationFactory.js +126 -0
- package/dist/cjs/hooks/mutations/useAccountMutations.js +275 -0
- package/dist/cjs/hooks/mutations/useServicesMutations.js +149 -0
- package/dist/cjs/hooks/queries/index.js +35 -0
- package/dist/cjs/hooks/queries/queryKeys.js +82 -0
- package/dist/cjs/hooks/queries/useAccountQueries.js +141 -0
- package/dist/cjs/hooks/queries/useSecurityQueries.js +45 -0
- package/dist/cjs/hooks/queries/useServicesQueries.js +113 -0
- package/dist/cjs/hooks/queryClient.js +110 -0
- package/dist/cjs/hooks/useAssets.js +225 -0
- package/dist/cjs/hooks/useFileDownloadUrl.js +91 -0
- package/dist/cjs/hooks/useFileFiltering.js +81 -0
- package/dist/cjs/hooks/useFollow.js +159 -0
- package/dist/cjs/hooks/useFollow.types.js +4 -0
- package/dist/cjs/hooks/useQueryClient.js +16 -0
- package/dist/cjs/hooks/useSessionSocket.js +215 -0
- package/dist/cjs/hooks/useWebSSO.js +146 -0
- package/dist/cjs/index.js +115 -0
- package/dist/cjs/stores/accountStore.js +226 -0
- package/dist/cjs/stores/assetStore.js +192 -0
- package/dist/cjs/stores/authStore.js +47 -0
- package/dist/cjs/stores/followStore.js +154 -0
- package/dist/cjs/utils/authHelpers.js +154 -0
- package/dist/cjs/utils/avatarUtils.js +77 -0
- package/dist/cjs/utils/errorHandlers.js +128 -0
- package/dist/cjs/utils/sessionHelpers.js +90 -0
- package/dist/cjs/utils/storageHelpers.js +147 -0
- package/dist/esm/WebOxyProvider.js +282 -0
- package/dist/esm/hooks/mutations/index.js +10 -0
- package/dist/esm/hooks/mutations/mutationFactory.js +122 -0
- package/dist/esm/hooks/mutations/useAccountMutations.js +267 -0
- package/dist/esm/hooks/mutations/useServicesMutations.js +141 -0
- package/dist/esm/hooks/queries/index.js +14 -0
- package/dist/esm/hooks/queries/queryKeys.js +76 -0
- package/dist/esm/hooks/queries/useAccountQueries.js +131 -0
- package/dist/esm/hooks/queries/useSecurityQueries.js +40 -0
- package/dist/esm/hooks/queries/useServicesQueries.js +105 -0
- package/dist/esm/hooks/queryClient.js +104 -0
- package/dist/esm/hooks/useAssets.js +220 -0
- package/dist/esm/hooks/useFileDownloadUrl.js +86 -0
- package/dist/esm/hooks/useFileFiltering.js +78 -0
- package/dist/esm/hooks/useFollow.js +154 -0
- package/dist/esm/hooks/useFollow.types.js +3 -0
- package/dist/esm/hooks/useQueryClient.js +12 -0
- package/dist/esm/hooks/useSessionSocket.js +209 -0
- package/dist/esm/hooks/useWebSSO.js +143 -0
- package/dist/esm/index.js +48 -0
- package/dist/esm/stores/accountStore.js +219 -0
- package/dist/esm/stores/assetStore.js +180 -0
- package/dist/esm/stores/authStore.js +44 -0
- package/dist/esm/stores/followStore.js +151 -0
- package/dist/esm/utils/authHelpers.js +145 -0
- package/dist/esm/utils/avatarUtils.js +72 -0
- package/dist/esm/utils/errorHandlers.js +121 -0
- package/dist/esm/utils/sessionHelpers.js +84 -0
- package/dist/esm/utils/storageHelpers.js +108 -0
- package/dist/types/WebOxyProvider.d.ts +97 -0
- package/dist/types/hooks/mutations/index.d.ts +8 -0
- package/dist/types/hooks/mutations/mutationFactory.d.ts +75 -0
- package/dist/types/hooks/mutations/useAccountMutations.d.ts +68 -0
- package/dist/types/hooks/mutations/useServicesMutations.d.ts +22 -0
- package/dist/types/hooks/queries/index.d.ts +10 -0
- package/dist/types/hooks/queries/queryKeys.d.ts +64 -0
- package/dist/types/hooks/queries/useAccountQueries.d.ts +42 -0
- package/dist/types/hooks/queries/useSecurityQueries.d.ts +14 -0
- package/dist/types/hooks/queries/useServicesQueries.d.ts +31 -0
- package/dist/types/hooks/queryClient.d.ts +18 -0
- package/dist/types/hooks/useAssets.d.ts +34 -0
- package/dist/types/hooks/useFileDownloadUrl.d.ts +18 -0
- package/dist/types/hooks/useFileFiltering.d.ts +28 -0
- package/dist/types/hooks/useFollow.d.ts +61 -0
- package/dist/types/hooks/useFollow.types.d.ts +32 -0
- package/dist/types/hooks/useQueryClient.d.ts +6 -0
- package/dist/types/hooks/useSessionSocket.d.ts +13 -0
- package/dist/types/hooks/useWebSSO.d.ts +57 -0
- package/dist/types/index.d.ts +46 -0
- package/dist/types/stores/accountStore.d.ts +33 -0
- package/dist/types/stores/assetStore.d.ts +53 -0
- package/dist/types/stores/authStore.d.ts +16 -0
- package/dist/types/stores/followStore.d.ts +24 -0
- package/dist/types/utils/authHelpers.d.ts +98 -0
- package/dist/types/utils/avatarUtils.d.ts +33 -0
- package/dist/types/utils/errorHandlers.d.ts +34 -0
- package/dist/types/utils/sessionHelpers.d.ts +63 -0
- package/dist/types/utils/storageHelpers.d.ts +27 -0
- package/package.json +71 -0
- package/src/WebOxyProvider.tsx +372 -0
- package/src/global.d.ts +1 -0
- package/src/hooks/mutations/index.ts +25 -0
- package/src/hooks/mutations/mutationFactory.ts +215 -0
- package/src/hooks/mutations/useAccountMutations.ts +344 -0
- package/src/hooks/mutations/useServicesMutations.ts +164 -0
- package/src/hooks/queries/index.ts +36 -0
- package/src/hooks/queries/queryKeys.ts +88 -0
- package/src/hooks/queries/useAccountQueries.ts +152 -0
- package/src/hooks/queries/useSecurityQueries.ts +64 -0
- package/src/hooks/queries/useServicesQueries.ts +126 -0
- package/src/hooks/queryClient.ts +112 -0
- package/src/hooks/useAssets.ts +291 -0
- package/src/hooks/useFileDownloadUrl.ts +118 -0
- package/src/hooks/useFileFiltering.ts +115 -0
- package/src/hooks/useFollow.ts +175 -0
- package/src/hooks/useFollow.types.ts +33 -0
- package/src/hooks/useQueryClient.ts +17 -0
- package/src/hooks/useSessionSocket.ts +233 -0
- package/src/hooks/useWebSSO.ts +187 -0
- package/src/index.ts +144 -0
- package/src/stores/accountStore.ts +296 -0
- package/src/stores/assetStore.ts +281 -0
- package/src/stores/authStore.ts +63 -0
- package/src/stores/followStore.ts +181 -0
- package/src/utils/authHelpers.ts +183 -0
- package/src/utils/avatarUtils.ts +103 -0
- package/src/utils/errorHandlers.ts +194 -0
- package/src/utils/sessionHelpers.ts +151 -0
- package/src/utils/storageHelpers.ts +130 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import { queryKeys } from './queryKeys';
|
|
3
|
+
import { useWebOxy } from '../../WebOxyProvider';
|
|
4
|
+
import { fetchSessionsWithFallback, mapSessionsToClient } from '../../utils/sessionHelpers';
|
|
5
|
+
import { authenticatedApiCall } from '../../utils/authHelpers';
|
|
6
|
+
/**
|
|
7
|
+
* Get all active sessions for the current user
|
|
8
|
+
*/
|
|
9
|
+
export const useSessions = (userId, options) => {
|
|
10
|
+
const { oxyServices, activeSessionId } = useWebOxy();
|
|
11
|
+
return useQuery({
|
|
12
|
+
queryKey: queryKeys.sessions.list(userId),
|
|
13
|
+
queryFn: async () => {
|
|
14
|
+
if (!activeSessionId) {
|
|
15
|
+
throw new Error('No active session');
|
|
16
|
+
}
|
|
17
|
+
const sessions = await fetchSessionsWithFallback(oxyServices, activeSessionId, {
|
|
18
|
+
fallbackDeviceId: undefined,
|
|
19
|
+
fallbackUserId: userId,
|
|
20
|
+
});
|
|
21
|
+
return mapSessionsToClient(sessions, activeSessionId);
|
|
22
|
+
},
|
|
23
|
+
enabled: (options?.enabled !== false) && !!activeSessionId,
|
|
24
|
+
staleTime: 2 * 60 * 1000, // 2 minutes (sessions change frequently)
|
|
25
|
+
gcTime: 10 * 60 * 1000, // 10 minutes
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Get specific session by ID
|
|
30
|
+
*/
|
|
31
|
+
export const useSession = (sessionId, options) => {
|
|
32
|
+
const { oxyServices } = useWebOxy();
|
|
33
|
+
return useQuery({
|
|
34
|
+
queryKey: queryKeys.sessions.detail(sessionId || ''),
|
|
35
|
+
queryFn: async () => {
|
|
36
|
+
if (!sessionId) {
|
|
37
|
+
throw new Error('Session ID is required');
|
|
38
|
+
}
|
|
39
|
+
const validation = await oxyServices.validateSession(sessionId, { useHeaderValidation: true });
|
|
40
|
+
if (!validation?.valid || !validation.user) {
|
|
41
|
+
throw new Error('Session not found or invalid');
|
|
42
|
+
}
|
|
43
|
+
const now = new Date();
|
|
44
|
+
return {
|
|
45
|
+
sessionId,
|
|
46
|
+
deviceId: '', // Device ID not available from validation response
|
|
47
|
+
expiresAt: validation.expiresAt || new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
48
|
+
lastActive: validation.lastActivity || now.toISOString(),
|
|
49
|
+
userId: validation.user.id?.toString() ?? '',
|
|
50
|
+
isCurrent: false,
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
enabled: (options?.enabled !== false) && !!sessionId,
|
|
54
|
+
staleTime: 2 * 60 * 1000,
|
|
55
|
+
gcTime: 10 * 60 * 1000,
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Get device sessions for the current active session
|
|
60
|
+
*/
|
|
61
|
+
export const useDeviceSessions = (options) => {
|
|
62
|
+
const { oxyServices, activeSessionId } = useWebOxy();
|
|
63
|
+
return useQuery({
|
|
64
|
+
queryKey: queryKeys.sessions.active(),
|
|
65
|
+
queryFn: async () => {
|
|
66
|
+
if (!activeSessionId) {
|
|
67
|
+
throw new Error('No active session');
|
|
68
|
+
}
|
|
69
|
+
return await oxyServices.getDeviceSessions(activeSessionId);
|
|
70
|
+
},
|
|
71
|
+
enabled: (options?.enabled !== false) && !!activeSessionId,
|
|
72
|
+
staleTime: 2 * 60 * 1000,
|
|
73
|
+
gcTime: 10 * 60 * 1000,
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Get user devices
|
|
78
|
+
*/
|
|
79
|
+
export const useUserDevices = (options) => {
|
|
80
|
+
const { oxyServices, isAuthenticated, activeSessionId } = useWebOxy();
|
|
81
|
+
return useQuery({
|
|
82
|
+
queryKey: queryKeys.devices.list(),
|
|
83
|
+
queryFn: async () => {
|
|
84
|
+
return authenticatedApiCall(oxyServices, activeSessionId, () => oxyServices.getUserDevices());
|
|
85
|
+
},
|
|
86
|
+
enabled: (options?.enabled !== false) && isAuthenticated,
|
|
87
|
+
staleTime: 5 * 60 * 1000,
|
|
88
|
+
gcTime: 30 * 60 * 1000,
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Get security information
|
|
93
|
+
*/
|
|
94
|
+
export const useSecurityInfo = (options) => {
|
|
95
|
+
const { oxyServices, isAuthenticated } = useWebOxy();
|
|
96
|
+
return useQuery({
|
|
97
|
+
queryKey: [...queryKeys.devices.all, 'security'],
|
|
98
|
+
queryFn: async () => {
|
|
99
|
+
return await oxyServices.getSecurityInfo();
|
|
100
|
+
},
|
|
101
|
+
enabled: (options?.enabled !== false) && isAuthenticated,
|
|
102
|
+
staleTime: 5 * 60 * 1000,
|
|
103
|
+
gcTime: 30 * 60 * 1000,
|
|
104
|
+
});
|
|
105
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
2
|
+
const QUERY_CACHE_KEY = 'oxy_query_cache';
|
|
3
|
+
const QUERY_CACHE_VERSION = '1';
|
|
4
|
+
/**
|
|
5
|
+
* Custom persistence adapter for TanStack Query using our StorageInterface
|
|
6
|
+
*/
|
|
7
|
+
export const createPersistenceAdapter = (storage) => {
|
|
8
|
+
return {
|
|
9
|
+
persistClient: async (client) => {
|
|
10
|
+
try {
|
|
11
|
+
const serialized = JSON.stringify({
|
|
12
|
+
clientState: client,
|
|
13
|
+
timestamp: Date.now(),
|
|
14
|
+
version: QUERY_CACHE_VERSION,
|
|
15
|
+
});
|
|
16
|
+
await storage.setItem(QUERY_CACHE_KEY, serialized);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (__DEV__) {
|
|
20
|
+
console.warn('[QueryClient] Failed to persist cache:', error);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
restoreClient: async () => {
|
|
25
|
+
try {
|
|
26
|
+
const cached = await storage.getItem(QUERY_CACHE_KEY);
|
|
27
|
+
if (!cached)
|
|
28
|
+
return undefined;
|
|
29
|
+
const parsed = JSON.parse(cached);
|
|
30
|
+
// Check version compatibility
|
|
31
|
+
if (parsed.version !== QUERY_CACHE_VERSION) {
|
|
32
|
+
// Clear old cache on version mismatch
|
|
33
|
+
await storage.removeItem(QUERY_CACHE_KEY);
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
// Check if cache is too old (30 days)
|
|
37
|
+
const maxAge = 30 * 24 * 60 * 60 * 1000;
|
|
38
|
+
if (parsed.timestamp && Date.now() - parsed.timestamp > maxAge) {
|
|
39
|
+
await storage.removeItem(QUERY_CACHE_KEY);
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return parsed.clientState;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (__DEV__) {
|
|
46
|
+
console.warn('[QueryClient] Failed to restore cache:', error);
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
removeClient: async () => {
|
|
52
|
+
try {
|
|
53
|
+
await storage.removeItem(QUERY_CACHE_KEY);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (__DEV__) {
|
|
57
|
+
console.warn('[QueryClient] Failed to remove cache:', error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Create a QueryClient with offline-first configuration
|
|
65
|
+
*/
|
|
66
|
+
export const createQueryClient = (storage) => {
|
|
67
|
+
const client = new QueryClient({
|
|
68
|
+
defaultOptions: {
|
|
69
|
+
queries: {
|
|
70
|
+
// Data is fresh for 5 minutes
|
|
71
|
+
staleTime: 5 * 60 * 1000,
|
|
72
|
+
// Keep unused data in cache for 30 minutes
|
|
73
|
+
gcTime: 30 * 60 * 1000,
|
|
74
|
+
// Retry 3 times with exponential backoff
|
|
75
|
+
retry: 3,
|
|
76
|
+
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
|
77
|
+
// Refetch on reconnect
|
|
78
|
+
refetchOnReconnect: true,
|
|
79
|
+
// Don't refetch on window focus (better for mobile)
|
|
80
|
+
refetchOnWindowFocus: false,
|
|
81
|
+
// Offline-first: use cache when offline
|
|
82
|
+
networkMode: 'offlineFirst',
|
|
83
|
+
},
|
|
84
|
+
mutations: {
|
|
85
|
+
// Retry once for mutations
|
|
86
|
+
retry: 1,
|
|
87
|
+
// Offline-first: queue mutations when offline
|
|
88
|
+
networkMode: 'offlineFirst',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
// Note: Persistence is handled by TanStack Query's built-in persistence
|
|
93
|
+
// For now, we rely on the query client's default behavior with networkMode: 'offlineFirst'
|
|
94
|
+
// The cache will be available in memory and queries will use cached data when offline
|
|
95
|
+
// Full persistence to AsyncStorage can be added later with @tanstack/react-query-persist-client if needed
|
|
96
|
+
return client;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Clear persisted query cache
|
|
100
|
+
*/
|
|
101
|
+
export const clearQueryCache = async (storage) => {
|
|
102
|
+
const adapter = createPersistenceAdapter(storage);
|
|
103
|
+
await adapter.removeClient();
|
|
104
|
+
};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useAssetStore } from '../stores/assetStore';
|
|
3
|
+
// Create a singleton instance for the hook
|
|
4
|
+
let oxyInstance = null;
|
|
5
|
+
export const setOxyAssetInstance = (instance) => {
|
|
6
|
+
oxyInstance = instance;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Hook for managing assets with Zustand store integration
|
|
10
|
+
*/
|
|
11
|
+
export const useAssets = () => {
|
|
12
|
+
const { assets, uploadProgress, loading, errors, setAsset, setAssets, removeAsset, setUploadProgress, removeUploadProgress, addLink, removeLink, setUploading, setLinking, setDeleting, setUploadError, setLinkError, setDeleteError, clearErrors, getAssetsByApp, getAssetsByEntity, getAssetUsageCount, isAssetLinked, reset } = useAssetStore();
|
|
13
|
+
// Upload asset with progress tracking
|
|
14
|
+
const upload = useCallback(async (file, metadata) => {
|
|
15
|
+
if (!oxyInstance) {
|
|
16
|
+
throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
clearErrors();
|
|
20
|
+
setUploading(true);
|
|
21
|
+
// Upload file (progress tracking simplified for now)
|
|
22
|
+
const result = await oxyInstance.assetUpload(file, undefined, metadata);
|
|
23
|
+
// Update progress with final status
|
|
24
|
+
if (result?.file) {
|
|
25
|
+
const fileId = result.file.id;
|
|
26
|
+
setUploadProgress(fileId, {
|
|
27
|
+
fileId,
|
|
28
|
+
uploaded: file.size,
|
|
29
|
+
total: file.size,
|
|
30
|
+
percentage: 100,
|
|
31
|
+
status: 'complete'
|
|
32
|
+
});
|
|
33
|
+
// Remove progress after a short delay
|
|
34
|
+
setTimeout(() => {
|
|
35
|
+
removeUploadProgress(fileId);
|
|
36
|
+
}, 2000);
|
|
37
|
+
}
|
|
38
|
+
// Add asset to store
|
|
39
|
+
if (result.file) {
|
|
40
|
+
setAsset(result.file);
|
|
41
|
+
return result.file;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
setUploadError(error.message || 'Upload failed');
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
setUploading(false);
|
|
51
|
+
}
|
|
52
|
+
}, [
|
|
53
|
+
clearErrors,
|
|
54
|
+
setUploading,
|
|
55
|
+
setUploadProgress,
|
|
56
|
+
removeUploadProgress,
|
|
57
|
+
setAsset,
|
|
58
|
+
setUploadError
|
|
59
|
+
]);
|
|
60
|
+
// Link asset to entity
|
|
61
|
+
const link = useCallback(async (assetId, app, entityType, entityId) => {
|
|
62
|
+
if (!oxyInstance) {
|
|
63
|
+
throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
clearErrors();
|
|
67
|
+
setLinking(true);
|
|
68
|
+
// Auto-detect visibility for avatars and profile banners
|
|
69
|
+
const visibility = (entityType === 'avatar' || entityType === 'profile-banner')
|
|
70
|
+
? 'public'
|
|
71
|
+
: undefined;
|
|
72
|
+
const result = await oxyInstance.assetLink(assetId, app, entityType, entityId, visibility);
|
|
73
|
+
if (result.file) {
|
|
74
|
+
setAsset(result.file);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// If API doesn't return full file, update store optimistically
|
|
78
|
+
addLink(assetId, {
|
|
79
|
+
app,
|
|
80
|
+
entityType,
|
|
81
|
+
entityId,
|
|
82
|
+
createdBy: '', // Will be filled by server
|
|
83
|
+
createdAt: new Date().toISOString()
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
setLinkError(error.message || 'Link failed');
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
setLinking(false);
|
|
93
|
+
}
|
|
94
|
+
}, [clearErrors, setLinking, setAsset, addLink, setLinkError]);
|
|
95
|
+
// Unlink asset from entity
|
|
96
|
+
const unlink = useCallback(async (assetId, app, entityType, entityId) => {
|
|
97
|
+
if (!oxyInstance) {
|
|
98
|
+
throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
clearErrors();
|
|
102
|
+
setLinking(true);
|
|
103
|
+
const result = await oxyInstance.assetUnlink(assetId, app, entityType, entityId);
|
|
104
|
+
if (result.file) {
|
|
105
|
+
setAsset(result.file);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// Update store optimistically
|
|
109
|
+
removeLink(assetId, app, entityType, entityId);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
setLinkError(error.message || 'Unlink failed');
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
setLinking(false);
|
|
118
|
+
}
|
|
119
|
+
}, [clearErrors, setLinking, setAsset, removeLink, setLinkError]);
|
|
120
|
+
// Get asset URL
|
|
121
|
+
const getUrl = useCallback(async (assetId, variant, expiresIn) => {
|
|
122
|
+
if (!oxyInstance) {
|
|
123
|
+
throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const result = await oxyInstance.assetGetUrl(assetId, variant, expiresIn);
|
|
127
|
+
return result.url;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}, []);
|
|
133
|
+
// Get asset metadata
|
|
134
|
+
const getAsset = useCallback(async (assetId) => {
|
|
135
|
+
if (!oxyInstance) {
|
|
136
|
+
throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const result = await oxyInstance.assetGet(assetId);
|
|
140
|
+
if (result.file) {
|
|
141
|
+
setAsset(result.file);
|
|
142
|
+
return result.file;
|
|
143
|
+
}
|
|
144
|
+
throw new Error('Asset not found');
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}, [setAsset]);
|
|
150
|
+
// Delete asset
|
|
151
|
+
const deleteAsset = useCallback(async (assetId, force = false) => {
|
|
152
|
+
if (!oxyInstance) {
|
|
153
|
+
throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
clearErrors();
|
|
157
|
+
setDeleting(true);
|
|
158
|
+
await oxyInstance.assetDelete(assetId, force);
|
|
159
|
+
removeAsset(assetId);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
setDeleteError(error.message || 'Delete failed');
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
setDeleting(false);
|
|
167
|
+
}
|
|
168
|
+
}, [clearErrors, setDeleting, removeAsset, setDeleteError]);
|
|
169
|
+
// Restore asset from trash
|
|
170
|
+
const restore = useCallback(async (assetId) => {
|
|
171
|
+
if (!oxyInstance) {
|
|
172
|
+
throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const result = await oxyInstance.assetRestore(assetId);
|
|
176
|
+
if (result.file) {
|
|
177
|
+
setAsset(result.file);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}, [setAsset]);
|
|
184
|
+
// Get variants
|
|
185
|
+
const getVariants = useCallback(async (assetId) => {
|
|
186
|
+
if (!oxyInstance) {
|
|
187
|
+
throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
return await oxyInstance.assetGetVariants(assetId);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}, []);
|
|
196
|
+
return {
|
|
197
|
+
// State
|
|
198
|
+
assets: Object.values(assets),
|
|
199
|
+
uploadProgress,
|
|
200
|
+
loading,
|
|
201
|
+
errors,
|
|
202
|
+
// Actions
|
|
203
|
+
upload,
|
|
204
|
+
link,
|
|
205
|
+
unlink,
|
|
206
|
+
getUrl,
|
|
207
|
+
getAsset,
|
|
208
|
+
deleteAsset,
|
|
209
|
+
restore,
|
|
210
|
+
getVariants,
|
|
211
|
+
// Utility methods
|
|
212
|
+
getAssetsByApp,
|
|
213
|
+
getAssetsByEntity,
|
|
214
|
+
getAssetUsageCount,
|
|
215
|
+
isAssetLinked,
|
|
216
|
+
// Store management
|
|
217
|
+
clearErrors,
|
|
218
|
+
reset
|
|
219
|
+
};
|
|
220
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
let oxyInstance = null;
|
|
3
|
+
export const setOxyFileUrlInstance = (instance) => {
|
|
4
|
+
oxyInstance = instance;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Hook to resolve a file's download URL asynchronously.
|
|
8
|
+
*
|
|
9
|
+
* Prefers `getFileDownloadUrlAsync` and falls back to the synchronous
|
|
10
|
+
* `getFileDownloadUrl` helper if the async call fails.
|
|
11
|
+
*/
|
|
12
|
+
export const useFileDownloadUrl = (fileId, options) => {
|
|
13
|
+
const [url, setUrl] = useState(null);
|
|
14
|
+
const [loading, setLoading] = useState(false);
|
|
15
|
+
const [error, setError] = useState(null);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!fileId) {
|
|
18
|
+
setUrl(null);
|
|
19
|
+
setLoading(false);
|
|
20
|
+
setError(null);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (!oxyInstance) {
|
|
24
|
+
// Fail silently but don't crash the UI – caller can decide what to do with null URL.
|
|
25
|
+
setUrl(null);
|
|
26
|
+
setLoading(false);
|
|
27
|
+
setError(new Error('OxyServices instance not configured for useFileDownloadUrl'));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
let cancelled = false;
|
|
31
|
+
const load = async () => {
|
|
32
|
+
setLoading(true);
|
|
33
|
+
setError(null);
|
|
34
|
+
// Store instance in local variable for TypeScript null checking
|
|
35
|
+
const instance = oxyInstance;
|
|
36
|
+
if (!instance) {
|
|
37
|
+
setLoading(false);
|
|
38
|
+
setError(new Error('OxyServices instance not configured for useFileDownloadUrl'));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const { variant, expiresIn } = options || {};
|
|
43
|
+
let resolvedUrl = null;
|
|
44
|
+
if (typeof instance.getFileDownloadUrlAsync === 'function') {
|
|
45
|
+
resolvedUrl = await instance.getFileDownloadUrlAsync(fileId, variant, expiresIn);
|
|
46
|
+
}
|
|
47
|
+
if (!resolvedUrl && typeof instance.getFileDownloadUrl === 'function') {
|
|
48
|
+
resolvedUrl = instance.getFileDownloadUrl(fileId, variant, expiresIn);
|
|
49
|
+
}
|
|
50
|
+
if (!cancelled) {
|
|
51
|
+
setUrl(resolvedUrl || null);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
// Fallback to sync URL on error where possible
|
|
56
|
+
try {
|
|
57
|
+
if (typeof instance.getFileDownloadUrl === 'function') {
|
|
58
|
+
const { variant, expiresIn } = options || {};
|
|
59
|
+
const fallbackUrl = instance.getFileDownloadUrl(fileId, variant, expiresIn);
|
|
60
|
+
if (!cancelled) {
|
|
61
|
+
setUrl(fallbackUrl || null);
|
|
62
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// ignore secondary failure, we'll surface the original error below
|
|
69
|
+
}
|
|
70
|
+
if (!cancelled) {
|
|
71
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
if (!cancelled) {
|
|
76
|
+
setLoading(false);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
load();
|
|
81
|
+
return () => {
|
|
82
|
+
cancelled = true;
|
|
83
|
+
};
|
|
84
|
+
}, [fileId, options?.variant, options?.expiresIn]);
|
|
85
|
+
return { url, loading, error };
|
|
86
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useMemo, useState, useCallback } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Hook for file filtering, sorting, and search functionality
|
|
4
|
+
* Extracts common file management logic for reuse across components
|
|
5
|
+
*/
|
|
6
|
+
export function useFileFiltering({ files, initialViewMode = 'all', initialSortBy = 'date', initialSortOrder = 'desc', }) {
|
|
7
|
+
const [viewMode, setViewMode] = useState(initialViewMode);
|
|
8
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
9
|
+
const [sortBy, setSortBy] = useState(initialSortBy);
|
|
10
|
+
const [sortOrder, setSortOrder] = useState(initialSortOrder);
|
|
11
|
+
const toggleSortOrder = useCallback(() => {
|
|
12
|
+
setSortOrder((prev) => (prev === 'asc' ? 'desc' : 'asc'));
|
|
13
|
+
}, []);
|
|
14
|
+
const filteredFiles = useMemo(() => {
|
|
15
|
+
// Filter by view mode
|
|
16
|
+
let filteredByMode = files;
|
|
17
|
+
if (viewMode === 'photos') {
|
|
18
|
+
filteredByMode = files.filter((file) => file.contentType.startsWith('image/'));
|
|
19
|
+
}
|
|
20
|
+
else if (viewMode === 'videos') {
|
|
21
|
+
filteredByMode = files.filter((file) => file.contentType.startsWith('video/'));
|
|
22
|
+
}
|
|
23
|
+
else if (viewMode === 'documents') {
|
|
24
|
+
filteredByMode = files.filter((file) => file.contentType.includes('pdf') ||
|
|
25
|
+
file.contentType.includes('document') ||
|
|
26
|
+
file.contentType.includes('text') ||
|
|
27
|
+
file.contentType.includes('msword') ||
|
|
28
|
+
file.contentType.includes('excel') ||
|
|
29
|
+
file.contentType.includes('spreadsheet') ||
|
|
30
|
+
file.contentType.includes('presentation') ||
|
|
31
|
+
file.contentType.includes('powerpoint'));
|
|
32
|
+
}
|
|
33
|
+
else if (viewMode === 'audio') {
|
|
34
|
+
filteredByMode = files.filter((file) => file.contentType.startsWith('audio/'));
|
|
35
|
+
}
|
|
36
|
+
// Filter by search query
|
|
37
|
+
let filtered = filteredByMode;
|
|
38
|
+
if (searchQuery.trim()) {
|
|
39
|
+
const query = searchQuery.toLowerCase();
|
|
40
|
+
filtered = filteredByMode.filter((file) => file.filename.toLowerCase().includes(query) ||
|
|
41
|
+
file.contentType.toLowerCase().includes(query) ||
|
|
42
|
+
(file.metadata?.description &&
|
|
43
|
+
file.metadata.description.toLowerCase().includes(query)));
|
|
44
|
+
}
|
|
45
|
+
// Sort files
|
|
46
|
+
const sorted = [...filtered].sort((a, b) => {
|
|
47
|
+
let comparison = 0;
|
|
48
|
+
if (sortBy === 'date') {
|
|
49
|
+
const dateA = new Date(a.uploadDate || 0).getTime();
|
|
50
|
+
const dateB = new Date(b.uploadDate || 0).getTime();
|
|
51
|
+
comparison = dateA - dateB;
|
|
52
|
+
}
|
|
53
|
+
else if (sortBy === 'size') {
|
|
54
|
+
comparison = (a.length || 0) - (b.length || 0);
|
|
55
|
+
}
|
|
56
|
+
else if (sortBy === 'name') {
|
|
57
|
+
comparison = (a.filename || '').localeCompare(b.filename || '');
|
|
58
|
+
}
|
|
59
|
+
else if (sortBy === 'type') {
|
|
60
|
+
comparison = (a.contentType || '').localeCompare(b.contentType || '');
|
|
61
|
+
}
|
|
62
|
+
return sortOrder === 'asc' ? comparison : -comparison;
|
|
63
|
+
});
|
|
64
|
+
return sorted;
|
|
65
|
+
}, [files, searchQuery, viewMode, sortBy, sortOrder]);
|
|
66
|
+
return {
|
|
67
|
+
filteredFiles,
|
|
68
|
+
viewMode,
|
|
69
|
+
setViewMode,
|
|
70
|
+
searchQuery,
|
|
71
|
+
setSearchQuery,
|
|
72
|
+
sortBy,
|
|
73
|
+
setSortBy,
|
|
74
|
+
sortOrder,
|
|
75
|
+
setSortOrder,
|
|
76
|
+
toggleSortOrder,
|
|
77
|
+
};
|
|
78
|
+
}
|