@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,296 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { shallow } from 'zustand/shallow';
|
|
3
|
+
import type { OxyServices } from '@oxyhq/core';
|
|
4
|
+
|
|
5
|
+
export interface QuickAccount {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
userId?: string; // User ID for deduplication
|
|
8
|
+
username: string;
|
|
9
|
+
displayName: string;
|
|
10
|
+
avatar?: string;
|
|
11
|
+
avatarUrl?: string; // Cached avatar URL to prevent recalculation
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface AccountState {
|
|
15
|
+
// Account data
|
|
16
|
+
accounts: Record<string, QuickAccount>;
|
|
17
|
+
accountOrder: string[]; // Maintain order for display
|
|
18
|
+
accountsArray: QuickAccount[]; // Cached array to prevent infinite loops
|
|
19
|
+
|
|
20
|
+
// Loading states
|
|
21
|
+
loading: boolean;
|
|
22
|
+
loadingSessionIds: Set<string>;
|
|
23
|
+
|
|
24
|
+
// Error state
|
|
25
|
+
error: string | null;
|
|
26
|
+
|
|
27
|
+
// Actions
|
|
28
|
+
setAccounts: (accounts: QuickAccount[]) => void;
|
|
29
|
+
addAccount: (account: QuickAccount) => void;
|
|
30
|
+
updateAccount: (sessionId: string, updates: Partial<QuickAccount>) => void;
|
|
31
|
+
removeAccount: (sessionId: string) => void;
|
|
32
|
+
moveAccountToTop: (sessionId: string) => void;
|
|
33
|
+
|
|
34
|
+
// Loading actions
|
|
35
|
+
setLoading: (loading: boolean) => void;
|
|
36
|
+
setLoadingSession: (sessionId: string, loading: boolean) => void;
|
|
37
|
+
|
|
38
|
+
// Error actions
|
|
39
|
+
setError: (error: string | null) => void;
|
|
40
|
+
|
|
41
|
+
// Load accounts from API
|
|
42
|
+
loadAccounts: (sessionIds: string[], oxyServices: OxyServices, existingAccounts?: QuickAccount[], preserveOrder?: boolean) => Promise<void>;
|
|
43
|
+
|
|
44
|
+
// Reset store
|
|
45
|
+
reset: () => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const initialState = {
|
|
49
|
+
accounts: {} as Record<string, QuickAccount>,
|
|
50
|
+
accountOrder: [] as string[],
|
|
51
|
+
accountsArray: [] as QuickAccount[],
|
|
52
|
+
loading: false,
|
|
53
|
+
loadingSessionIds: new Set<string>(),
|
|
54
|
+
error: null,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Helper: Build accounts array from accounts map and order
|
|
58
|
+
const buildAccountsArray = (accounts: Record<string, QuickAccount>, order: string[]): QuickAccount[] => {
|
|
59
|
+
const result: QuickAccount[] = [];
|
|
60
|
+
for (const id of order) {
|
|
61
|
+
const account = accounts[id];
|
|
62
|
+
if (account) result.push(account);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Helper: Create QuickAccount from user data
|
|
68
|
+
const createQuickAccount = (sessionId: string, userData: any, existingAccount?: QuickAccount, oxyServices?: OxyServices): QuickAccount => {
|
|
69
|
+
const displayName = userData.name?.full || userData.name?.first || userData.username || 'Account';
|
|
70
|
+
const userId = userData.id || userData._id?.toString();
|
|
71
|
+
|
|
72
|
+
// Preserve existing avatarUrl if avatar hasn't changed (prevents image reload)
|
|
73
|
+
let avatarUrl: string | undefined;
|
|
74
|
+
if (existingAccount && existingAccount.avatar === userData.avatar && existingAccount.avatarUrl) {
|
|
75
|
+
avatarUrl = existingAccount.avatarUrl; // Reuse existing URL
|
|
76
|
+
} else if (userData.avatar && oxyServices) {
|
|
77
|
+
avatarUrl = oxyServices.getFileDownloadUrl(userData.avatar, 'thumb');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
sessionId,
|
|
82
|
+
userId,
|
|
83
|
+
username: userData.username || '',
|
|
84
|
+
displayName,
|
|
85
|
+
avatar: userData.avatar,
|
|
86
|
+
avatarUrl,
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const useAccountStore = create<AccountState>((set, get) => ({
|
|
91
|
+
...initialState,
|
|
92
|
+
|
|
93
|
+
setAccounts: (accounts) => set((state) => {
|
|
94
|
+
const accountMap: Record<string, QuickAccount> = {};
|
|
95
|
+
const order: string[] = [];
|
|
96
|
+
const seenSessionIds = new Set<string>();
|
|
97
|
+
|
|
98
|
+
for (const account of accounts) {
|
|
99
|
+
if (seenSessionIds.has(account.sessionId)) continue;
|
|
100
|
+
seenSessionIds.add(account.sessionId);
|
|
101
|
+
accountMap[account.sessionId] = account;
|
|
102
|
+
order.push(account.sessionId);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const accountsArray = buildAccountsArray(accountMap, order);
|
|
106
|
+
const sameOrder = order.length === state.accountOrder.length &&
|
|
107
|
+
order.every((id, i) => id === state.accountOrder[i]);
|
|
108
|
+
const sameAccounts = sameOrder &&
|
|
109
|
+
order.every(id => {
|
|
110
|
+
const existing = state.accounts[id];
|
|
111
|
+
const newAccount = accountMap[id];
|
|
112
|
+
return existing &&
|
|
113
|
+
existing.sessionId === newAccount.sessionId &&
|
|
114
|
+
existing.userId === newAccount.userId &&
|
|
115
|
+
existing.avatar === newAccount.avatar &&
|
|
116
|
+
existing.avatarUrl === newAccount.avatarUrl;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (sameAccounts) return {} as any;
|
|
120
|
+
|
|
121
|
+
return { accounts: accountMap, accountOrder: order, accountsArray };
|
|
122
|
+
}),
|
|
123
|
+
|
|
124
|
+
addAccount: (account) => set((state) => {
|
|
125
|
+
// Check if account with same sessionId exists
|
|
126
|
+
if (state.accounts[account.sessionId]) {
|
|
127
|
+
// Update existing
|
|
128
|
+
const existing = state.accounts[account.sessionId];
|
|
129
|
+
if (existing.avatar === account.avatar && existing.avatarUrl === account.avatarUrl) {
|
|
130
|
+
return {} as any; // No change
|
|
131
|
+
}
|
|
132
|
+
const newAccounts = { ...state.accounts, [account.sessionId]: account };
|
|
133
|
+
return {
|
|
134
|
+
accounts: newAccounts,
|
|
135
|
+
accountsArray: buildAccountsArray(newAccounts, state.accountOrder),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const newAccounts = { ...state.accounts, [account.sessionId]: account };
|
|
140
|
+
const newOrder = [account.sessionId, ...state.accountOrder];
|
|
141
|
+
return {
|
|
142
|
+
accounts: newAccounts,
|
|
143
|
+
accountOrder: newOrder,
|
|
144
|
+
accountsArray: buildAccountsArray(newAccounts, newOrder),
|
|
145
|
+
};
|
|
146
|
+
}),
|
|
147
|
+
|
|
148
|
+
updateAccount: (sessionId, updates) => set((state) => {
|
|
149
|
+
const existing = state.accounts[sessionId];
|
|
150
|
+
if (!existing) return {} as any;
|
|
151
|
+
|
|
152
|
+
const updated = { ...existing, ...updates };
|
|
153
|
+
if (existing.avatar === updated.avatar && existing.avatarUrl === updated.avatarUrl) {
|
|
154
|
+
return {} as any; // No change
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const newAccounts = { ...state.accounts, [sessionId]: updated };
|
|
158
|
+
return {
|
|
159
|
+
accounts: newAccounts,
|
|
160
|
+
accountsArray: buildAccountsArray(newAccounts, state.accountOrder),
|
|
161
|
+
};
|
|
162
|
+
}),
|
|
163
|
+
|
|
164
|
+
removeAccount: (sessionId) => set((state) => {
|
|
165
|
+
if (!state.accounts[sessionId]) return {} as any;
|
|
166
|
+
|
|
167
|
+
const { [sessionId]: _removed, ...rest } = state.accounts;
|
|
168
|
+
const newOrder = state.accountOrder.filter(id => id !== sessionId);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
accounts: rest,
|
|
172
|
+
accountOrder: newOrder,
|
|
173
|
+
accountsArray: buildAccountsArray(rest, newOrder),
|
|
174
|
+
};
|
|
175
|
+
}),
|
|
176
|
+
|
|
177
|
+
moveAccountToTop: (sessionId) => set((state) => {
|
|
178
|
+
if (!state.accounts[sessionId]) return {} as any;
|
|
179
|
+
|
|
180
|
+
const filtered = state.accountOrder.filter(id => id !== sessionId);
|
|
181
|
+
const newOrder = [sessionId, ...filtered];
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
accountOrder: newOrder,
|
|
185
|
+
accountsArray: buildAccountsArray(state.accounts, newOrder),
|
|
186
|
+
};
|
|
187
|
+
}),
|
|
188
|
+
|
|
189
|
+
setLoading: (loading) => set({ loading }),
|
|
190
|
+
|
|
191
|
+
setLoadingSession: (sessionId, loading) => set((state) => {
|
|
192
|
+
const newSet = new Set(state.loadingSessionIds);
|
|
193
|
+
if (loading) {
|
|
194
|
+
newSet.add(sessionId);
|
|
195
|
+
} else {
|
|
196
|
+
newSet.delete(sessionId);
|
|
197
|
+
}
|
|
198
|
+
return { loadingSessionIds: newSet };
|
|
199
|
+
}),
|
|
200
|
+
|
|
201
|
+
setError: (error) => set({ error }),
|
|
202
|
+
|
|
203
|
+
loadAccounts: async (sessionIds, oxyServices, existingAccounts = [], preserveOrder = true) => {
|
|
204
|
+
const state = get();
|
|
205
|
+
|
|
206
|
+
const uniqueSessionIds = Array.from(new Set(sessionIds));
|
|
207
|
+
if (uniqueSessionIds.length === 0) {
|
|
208
|
+
get().setAccounts([]);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Try to get data from TanStack Query cache first
|
|
213
|
+
try {
|
|
214
|
+
// This will be called from a component, so we need to access queryClient differently
|
|
215
|
+
// For now, we'll keep the API call but optimize it
|
|
216
|
+
const existingMap = new Map(existingAccounts.map(a => [a.sessionId, a]));
|
|
217
|
+
for (const account of Object.values(state.accounts)) {
|
|
218
|
+
existingMap.set(account.sessionId, account);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const missingSessionIds = uniqueSessionIds.filter(id => !existingMap.has(id));
|
|
222
|
+
|
|
223
|
+
if (missingSessionIds.length === 0) {
|
|
224
|
+
const ordered = uniqueSessionIds
|
|
225
|
+
.map(id => existingMap.get(id))
|
|
226
|
+
.filter((acc): acc is QuickAccount => acc !== undefined);
|
|
227
|
+
get().setAccounts(ordered);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (state.loading) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
set({ loading: true, error: null });
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const batchResults = await oxyServices.getUsersBySessions(missingSessionIds);
|
|
239
|
+
|
|
240
|
+
const accountMap = new Map<string, QuickAccount>();
|
|
241
|
+
|
|
242
|
+
for (const { sessionId, user: userData } of batchResults) {
|
|
243
|
+
if (userData && !accountMap.has(sessionId)) {
|
|
244
|
+
const existing = existingMap.get(sessionId);
|
|
245
|
+
accountMap.set(sessionId, createQuickAccount(sessionId, userData, existing, oxyServices));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const [sessionId, account] of accountMap) {
|
|
250
|
+
existingMap.set(sessionId, account);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const orderToUse = preserveOrder ? uniqueSessionIds : [...uniqueSessionIds, ...state.accountOrder];
|
|
254
|
+
const seen = new Set<string>();
|
|
255
|
+
const ordered: QuickAccount[] = [];
|
|
256
|
+
|
|
257
|
+
for (const sessionId of orderToUse) {
|
|
258
|
+
if (seen.has(sessionId)) continue;
|
|
259
|
+
seen.add(sessionId);
|
|
260
|
+
|
|
261
|
+
const account = existingMap.get(sessionId);
|
|
262
|
+
if (account) ordered.push(account);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
get().setAccounts(ordered);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to load accounts';
|
|
268
|
+
if (__DEV__) {
|
|
269
|
+
console.error('AccountStore: Failed to load accounts:', error);
|
|
270
|
+
}
|
|
271
|
+
set({ error: errorMessage });
|
|
272
|
+
} finally {
|
|
273
|
+
set({ loading: false });
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to load accounts';
|
|
277
|
+
if (__DEV__) {
|
|
278
|
+
console.error('AccountStore: Failed to load accounts:', error);
|
|
279
|
+
}
|
|
280
|
+
set({ error: errorMessage, loading: false });
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
reset: () => set(initialState),
|
|
285
|
+
}));
|
|
286
|
+
|
|
287
|
+
// Selectors for performance - return cached array to prevent infinite loops
|
|
288
|
+
export const useAccounts = (): QuickAccount[] => {
|
|
289
|
+
return useAccountStore(state => state.accountsArray);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
export const useAccountLoading = () => useAccountStore(s => s.loading);
|
|
293
|
+
export const useAccountError = () => useAccountStore(s => s.error);
|
|
294
|
+
export const useAccountLoadingSession = (sessionId: string) =>
|
|
295
|
+
useAccountStore(s => s.loadingSessionIds.has(sessionId));
|
|
296
|
+
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { Asset, AssetUploadProgress, AssetLink } from '@oxyhq/core';
|
|
3
|
+
|
|
4
|
+
interface AssetState {
|
|
5
|
+
// Asset data
|
|
6
|
+
assets: Record<string, Asset>;
|
|
7
|
+
uploadProgress: Record<string, AssetUploadProgress>;
|
|
8
|
+
|
|
9
|
+
// Loading states
|
|
10
|
+
loading: {
|
|
11
|
+
uploading: boolean;
|
|
12
|
+
linking: boolean;
|
|
13
|
+
deleting: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Error states
|
|
17
|
+
errors: {
|
|
18
|
+
upload?: string;
|
|
19
|
+
link?: string;
|
|
20
|
+
delete?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Actions
|
|
24
|
+
setAsset: (asset: Asset) => void;
|
|
25
|
+
setAssets: (assets: Asset[]) => void;
|
|
26
|
+
removeAsset: (assetId: string) => void;
|
|
27
|
+
|
|
28
|
+
// Upload progress actions
|
|
29
|
+
setUploadProgress: (fileId: string, progress: AssetUploadProgress) => void;
|
|
30
|
+
removeUploadProgress: (fileId: string) => void;
|
|
31
|
+
|
|
32
|
+
// Link management
|
|
33
|
+
addLink: (assetId: string, link: AssetLink) => void;
|
|
34
|
+
removeLink: (assetId: string, app: string, entityType: string, entityId: string) => void;
|
|
35
|
+
|
|
36
|
+
// Loading states
|
|
37
|
+
setUploading: (uploading: boolean) => void;
|
|
38
|
+
setLinking: (linking: boolean) => void;
|
|
39
|
+
setDeleting: (deleting: boolean) => void;
|
|
40
|
+
|
|
41
|
+
// Error management
|
|
42
|
+
setUploadError: (error?: string) => void;
|
|
43
|
+
setLinkError: (error?: string) => void;
|
|
44
|
+
setDeleteError: (error?: string) => void;
|
|
45
|
+
clearErrors: () => void;
|
|
46
|
+
|
|
47
|
+
// Utility methods
|
|
48
|
+
getAssetsByApp: (app: string) => Asset[];
|
|
49
|
+
getAssetsByEntity: (app: string, entityType: string, entityId: string) => Asset[];
|
|
50
|
+
getAssetUsageCount: (assetId: string) => number;
|
|
51
|
+
isAssetLinked: (assetId: string, app: string, entityType: string, entityId: string) => boolean;
|
|
52
|
+
|
|
53
|
+
// Reset store
|
|
54
|
+
reset: () => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const initialState = {
|
|
58
|
+
assets: {},
|
|
59
|
+
uploadProgress: {},
|
|
60
|
+
loading: {
|
|
61
|
+
uploading: false,
|
|
62
|
+
linking: false,
|
|
63
|
+
deleting: false,
|
|
64
|
+
},
|
|
65
|
+
errors: {},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const useAssetStore = create<AssetState>((set, get) => ({
|
|
69
|
+
...initialState,
|
|
70
|
+
|
|
71
|
+
// Asset management
|
|
72
|
+
setAsset: (asset: Asset) => {
|
|
73
|
+
set((state) => ({
|
|
74
|
+
assets: {
|
|
75
|
+
...state.assets,
|
|
76
|
+
[asset.id]: asset,
|
|
77
|
+
},
|
|
78
|
+
}));
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
setAssets: (assets: Asset[]) => {
|
|
82
|
+
set((state) => {
|
|
83
|
+
const assetMap = assets.reduce((acc, asset) => {
|
|
84
|
+
acc[asset.id] = asset;
|
|
85
|
+
return acc;
|
|
86
|
+
}, {} as Record<string, Asset>);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
assets: {
|
|
90
|
+
...state.assets,
|
|
91
|
+
...assetMap,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
removeAsset: (assetId: string) => {
|
|
98
|
+
set((state) => {
|
|
99
|
+
const { [assetId]: removed, ...rest } = state.assets;
|
|
100
|
+
return { assets: rest };
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// Upload progress
|
|
105
|
+
setUploadProgress: (fileId: string, progress: AssetUploadProgress) => {
|
|
106
|
+
set((state) => ({
|
|
107
|
+
uploadProgress: {
|
|
108
|
+
...state.uploadProgress,
|
|
109
|
+
[fileId]: progress,
|
|
110
|
+
},
|
|
111
|
+
}));
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
removeUploadProgress: (fileId: string) => {
|
|
115
|
+
set((state) => {
|
|
116
|
+
const { [fileId]: removed, ...rest } = state.uploadProgress;
|
|
117
|
+
return { uploadProgress: rest };
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// Link management
|
|
122
|
+
addLink: (assetId: string, link: AssetLink) => {
|
|
123
|
+
set((state) => {
|
|
124
|
+
const asset = state.assets[assetId];
|
|
125
|
+
if (!asset) return state;
|
|
126
|
+
|
|
127
|
+
// Check if link already exists
|
|
128
|
+
const existingLink = asset.links.find(
|
|
129
|
+
(l: AssetLink) => l.app === link.app &&
|
|
130
|
+
l.entityType === link.entityType &&
|
|
131
|
+
l.entityId === link.entityId
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (existingLink) return state;
|
|
135
|
+
|
|
136
|
+
const updatedAsset = {
|
|
137
|
+
...asset,
|
|
138
|
+
links: [...asset.links, link],
|
|
139
|
+
usageCount: asset.links.length + 1,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
assets: {
|
|
144
|
+
...state.assets,
|
|
145
|
+
[assetId]: updatedAsset,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
removeLink: (assetId: string, app: string, entityType: string, entityId: string) => {
|
|
152
|
+
set((state) => {
|
|
153
|
+
const asset = state.assets[assetId];
|
|
154
|
+
if (!asset) return state;
|
|
155
|
+
|
|
156
|
+
const filteredLinks = asset.links.filter(
|
|
157
|
+
(link: AssetLink) => !(link.app === app &&
|
|
158
|
+
link.entityType === entityType &&
|
|
159
|
+
link.entityId === entityId)
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const updatedAsset = {
|
|
163
|
+
...asset,
|
|
164
|
+
links: filteredLinks,
|
|
165
|
+
usageCount: filteredLinks.length,
|
|
166
|
+
status: filteredLinks.length === 0 ? 'trash' as const : asset.status,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
assets: {
|
|
171
|
+
...state.assets,
|
|
172
|
+
[assetId]: updatedAsset,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
// Loading states
|
|
179
|
+
setUploading: (uploading: boolean) => {
|
|
180
|
+
set((state) => ({
|
|
181
|
+
loading: { ...state.loading, uploading },
|
|
182
|
+
}));
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
setLinking: (linking: boolean) => {
|
|
186
|
+
set((state) => ({
|
|
187
|
+
loading: { ...state.loading, linking },
|
|
188
|
+
}));
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
setDeleting: (deleting: boolean) => {
|
|
192
|
+
set((state) => ({
|
|
193
|
+
loading: { ...state.loading, deleting },
|
|
194
|
+
}));
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// Error management
|
|
198
|
+
setUploadError: (error?: string) => {
|
|
199
|
+
set((state) => ({
|
|
200
|
+
errors: { ...state.errors, upload: error },
|
|
201
|
+
}));
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
setLinkError: (error?: string) => {
|
|
205
|
+
set((state) => ({
|
|
206
|
+
errors: { ...state.errors, link: error },
|
|
207
|
+
}));
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
setDeleteError: (error?: string) => {
|
|
211
|
+
set((state) => ({
|
|
212
|
+
errors: { ...state.errors, delete: error },
|
|
213
|
+
}));
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
clearErrors: () => {
|
|
217
|
+
set({ errors: {} });
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
// Utility methods
|
|
221
|
+
getAssetsByApp: (app: string) => {
|
|
222
|
+
const { assets } = get();
|
|
223
|
+
return Object.values(assets).filter((asset) =>
|
|
224
|
+
asset.links.some((link: AssetLink) => link.app === app)
|
|
225
|
+
);
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
getAssetsByEntity: (app: string, entityType: string, entityId: string) => {
|
|
229
|
+
const { assets } = get();
|
|
230
|
+
return Object.values(assets).filter((asset) =>
|
|
231
|
+
asset.links.some(
|
|
232
|
+
(link: AssetLink) => link.app === app &&
|
|
233
|
+
link.entityType === entityType &&
|
|
234
|
+
link.entityId === entityId
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
getAssetUsageCount: (assetId: string) => {
|
|
240
|
+
const { assets } = get();
|
|
241
|
+
const asset = assets[assetId];
|
|
242
|
+
return asset ? asset.usageCount : 0;
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
isAssetLinked: (assetId: string, app: string, entityType: string, entityId: string) => {
|
|
246
|
+
const { assets } = get();
|
|
247
|
+
const asset = assets[assetId];
|
|
248
|
+
if (!asset) return false;
|
|
249
|
+
|
|
250
|
+
return asset.links.some(
|
|
251
|
+
(link: AssetLink) => link.app === app &&
|
|
252
|
+
link.entityType === entityType &&
|
|
253
|
+
link.entityId === entityId
|
|
254
|
+
);
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
// Reset store
|
|
258
|
+
reset: () => {
|
|
259
|
+
set(initialState);
|
|
260
|
+
},
|
|
261
|
+
}));
|
|
262
|
+
|
|
263
|
+
// Selector hooks for convenience
|
|
264
|
+
export const useAssets = () => useAssetStore((state) => Object.values(state.assets));
|
|
265
|
+
export const useAsset = (assetId: string) => useAssetStore((state) => state.assets[assetId]);
|
|
266
|
+
export const useUploadProgress = () => useAssetStore((state) => state.uploadProgress);
|
|
267
|
+
export const useAssetLoading = () => useAssetStore((state) => state.loading);
|
|
268
|
+
export const useAssetErrors = () => useAssetStore((state) => state.errors);
|
|
269
|
+
|
|
270
|
+
// Typed selectors for specific use cases
|
|
271
|
+
export const useAssetsByApp = (app: string) =>
|
|
272
|
+
useAssetStore((state) => state.getAssetsByApp(app));
|
|
273
|
+
|
|
274
|
+
export const useAssetsByEntity = (app: string, entityType: string, entityId: string) =>
|
|
275
|
+
useAssetStore((state) => state.getAssetsByEntity(app, entityType, entityId));
|
|
276
|
+
|
|
277
|
+
export const useAssetUsageCount = (assetId: string) =>
|
|
278
|
+
useAssetStore((state) => state.getAssetUsageCount(assetId));
|
|
279
|
+
|
|
280
|
+
export const useIsAssetLinked = (assetId: string, app: string, entityType: string, entityId: string) =>
|
|
281
|
+
useAssetStore((state) => state.isAssetLinked(assetId, app, entityType, entityId));
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import type { User } from '@oxyhq/core';
|
|
3
|
+
import { createDebugLogger } from '@oxyhq/core';
|
|
4
|
+
|
|
5
|
+
const debug = createDebugLogger('AuthStore');
|
|
6
|
+
|
|
7
|
+
export interface AuthState {
|
|
8
|
+
user: User | null;
|
|
9
|
+
isAuthenticated: boolean;
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
error: string | null;
|
|
12
|
+
lastUserFetch: number | null; // Timestamp of last user fetch for caching
|
|
13
|
+
|
|
14
|
+
loginSuccess: (user: User) => void;
|
|
15
|
+
loginFailure: (error: string) => void;
|
|
16
|
+
logout: () => void;
|
|
17
|
+
fetchUser: (oxyServices: { getCurrentUser: () => Promise<User> }, forceRefresh?: boolean) => Promise<void>;
|
|
18
|
+
setUser: (user: User) => void; // Direct user setter for caching
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const useAuthStore = create<AuthState>((set: (state: Partial<AuthState>) => void, get: () => AuthState) => ({
|
|
22
|
+
user: null,
|
|
23
|
+
isAuthenticated: false,
|
|
24
|
+
isLoading: false,
|
|
25
|
+
error: null,
|
|
26
|
+
lastUserFetch: null,
|
|
27
|
+
|
|
28
|
+
loginSuccess: (user: User) => set({
|
|
29
|
+
isLoading: false,
|
|
30
|
+
isAuthenticated: true,
|
|
31
|
+
user,
|
|
32
|
+
lastUserFetch: Date.now(),
|
|
33
|
+
}),
|
|
34
|
+
loginFailure: (error: string) => set({ isLoading: false, error }),
|
|
35
|
+
logout: () => set({
|
|
36
|
+
user: null,
|
|
37
|
+
isAuthenticated: false,
|
|
38
|
+
lastUserFetch: null,
|
|
39
|
+
}),
|
|
40
|
+
setUser: (user: User) => set({ user, lastUserFetch: Date.now() }),
|
|
41
|
+
fetchUser: async (oxyServices, forceRefresh = false) => {
|
|
42
|
+
const state = get();
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
const cacheAge = state.lastUserFetch ? now - state.lastUserFetch : Number.POSITIVE_INFINITY;
|
|
45
|
+
const cacheValid = cacheAge < 5 * 60 * 1000; // 5 minutes cache
|
|
46
|
+
|
|
47
|
+
// Use cached data if available and not forcing refresh
|
|
48
|
+
if (!forceRefresh && state.user && cacheValid) {
|
|
49
|
+
debug.log('Using cached user data (age:', cacheAge, 'ms)');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
set({ isLoading: true, error: null });
|
|
54
|
+
try {
|
|
55
|
+
const user = await oxyServices.getCurrentUser();
|
|
56
|
+
set({ user, isLoading: false, isAuthenticated: true, lastUserFetch: now });
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch user';
|
|
59
|
+
debug.error('Error fetching user:', error);
|
|
60
|
+
set({ error: errorMessage, isLoading: false });
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
}));
|