@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,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useAuthStore = void 0;
|
|
4
|
+
const zustand_1 = require("zustand");
|
|
5
|
+
const core_1 = require("@oxyhq/core");
|
|
6
|
+
const debug = (0, core_1.createDebugLogger)('AuthStore');
|
|
7
|
+
exports.useAuthStore = (0, zustand_1.create)((set, get) => ({
|
|
8
|
+
user: null,
|
|
9
|
+
isAuthenticated: false,
|
|
10
|
+
isLoading: false,
|
|
11
|
+
error: null,
|
|
12
|
+
lastUserFetch: null,
|
|
13
|
+
loginSuccess: (user) => set({
|
|
14
|
+
isLoading: false,
|
|
15
|
+
isAuthenticated: true,
|
|
16
|
+
user,
|
|
17
|
+
lastUserFetch: Date.now(),
|
|
18
|
+
}),
|
|
19
|
+
loginFailure: (error) => set({ isLoading: false, error }),
|
|
20
|
+
logout: () => set({
|
|
21
|
+
user: null,
|
|
22
|
+
isAuthenticated: false,
|
|
23
|
+
lastUserFetch: null,
|
|
24
|
+
}),
|
|
25
|
+
setUser: (user) => set({ user, lastUserFetch: Date.now() }),
|
|
26
|
+
fetchUser: async (oxyServices, forceRefresh = false) => {
|
|
27
|
+
const state = get();
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
const cacheAge = state.lastUserFetch ? now - state.lastUserFetch : Number.POSITIVE_INFINITY;
|
|
30
|
+
const cacheValid = cacheAge < 5 * 60 * 1000; // 5 minutes cache
|
|
31
|
+
// Use cached data if available and not forcing refresh
|
|
32
|
+
if (!forceRefresh && state.user && cacheValid) {
|
|
33
|
+
debug.log('Using cached user data (age:', cacheAge, 'ms)');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
set({ isLoading: true, error: null });
|
|
37
|
+
try {
|
|
38
|
+
const user = await oxyServices.getCurrentUser();
|
|
39
|
+
set({ user, isLoading: false, isAuthenticated: true, lastUserFetch: now });
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch user';
|
|
43
|
+
debug.error('Error fetching user:', error);
|
|
44
|
+
set({ error: errorMessage, isLoading: false });
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
}));
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useFollowStore = void 0;
|
|
4
|
+
const zustand_1 = require("zustand");
|
|
5
|
+
exports.useFollowStore = (0, zustand_1.create)((set, get) => ({
|
|
6
|
+
followingUsers: {},
|
|
7
|
+
loadingUsers: {},
|
|
8
|
+
fetchingUsers: {},
|
|
9
|
+
errors: {},
|
|
10
|
+
followerCounts: {},
|
|
11
|
+
followingCounts: {},
|
|
12
|
+
loadingCounts: {},
|
|
13
|
+
setFollowingStatus: (userId, isFollowing) => set((state) => ({
|
|
14
|
+
followingUsers: { ...state.followingUsers, [userId]: isFollowing },
|
|
15
|
+
errors: { ...state.errors, [userId]: null },
|
|
16
|
+
})),
|
|
17
|
+
clearFollowError: (userId) => set((state) => ({
|
|
18
|
+
errors: { ...state.errors, [userId]: null },
|
|
19
|
+
})),
|
|
20
|
+
resetFollowState: () => set({
|
|
21
|
+
followingUsers: {},
|
|
22
|
+
loadingUsers: {},
|
|
23
|
+
fetchingUsers: {},
|
|
24
|
+
errors: {},
|
|
25
|
+
followerCounts: {},
|
|
26
|
+
followingCounts: {},
|
|
27
|
+
loadingCounts: {},
|
|
28
|
+
}),
|
|
29
|
+
fetchFollowStatus: async (userId, oxyServices) => {
|
|
30
|
+
set((state) => ({
|
|
31
|
+
fetchingUsers: { ...state.fetchingUsers, [userId]: true },
|
|
32
|
+
errors: { ...state.errors, [userId]: null },
|
|
33
|
+
}));
|
|
34
|
+
try {
|
|
35
|
+
const response = await oxyServices.getFollowStatus(userId);
|
|
36
|
+
set((state) => ({
|
|
37
|
+
followingUsers: { ...state.followingUsers, [userId]: response.isFollowing },
|
|
38
|
+
fetchingUsers: { ...state.fetchingUsers, [userId]: false },
|
|
39
|
+
errors: { ...state.errors, [userId]: null },
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
set((state) => ({
|
|
44
|
+
fetchingUsers: { ...state.fetchingUsers, [userId]: false },
|
|
45
|
+
errors: { ...state.errors, [userId]: error?.message || 'Failed to fetch follow status' },
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
toggleFollowUser: async (userId, oxyServices, isCurrentlyFollowing) => {
|
|
50
|
+
set((state) => ({
|
|
51
|
+
loadingUsers: { ...state.loadingUsers, [userId]: true },
|
|
52
|
+
errors: { ...state.errors, [userId]: null },
|
|
53
|
+
}));
|
|
54
|
+
try {
|
|
55
|
+
let response;
|
|
56
|
+
let newFollowState;
|
|
57
|
+
if (isCurrentlyFollowing) {
|
|
58
|
+
response = await oxyServices.unfollowUser(userId);
|
|
59
|
+
newFollowState = false;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
response = await oxyServices.followUser(userId);
|
|
63
|
+
newFollowState = true;
|
|
64
|
+
}
|
|
65
|
+
// Update follow status
|
|
66
|
+
set((state) => ({
|
|
67
|
+
followingUsers: { ...state.followingUsers, [userId]: newFollowState },
|
|
68
|
+
loadingUsers: { ...state.loadingUsers, [userId]: false },
|
|
69
|
+
errors: { ...state.errors, [userId]: null },
|
|
70
|
+
}));
|
|
71
|
+
// Update counts if the response includes them
|
|
72
|
+
// The API returns counts for both users:
|
|
73
|
+
// - followers: target user's follower count (the user being followed)
|
|
74
|
+
// - following: current user's following count (the user doing the following)
|
|
75
|
+
if (response && response.counts) {
|
|
76
|
+
const { counts } = response;
|
|
77
|
+
// Get current user ID from oxyServices
|
|
78
|
+
const currentUserId = oxyServices.getCurrentUserId();
|
|
79
|
+
set((state) => {
|
|
80
|
+
const updates = {};
|
|
81
|
+
// Update target user's follower count (the user being followed)
|
|
82
|
+
updates.followerCounts = {
|
|
83
|
+
...state.followerCounts,
|
|
84
|
+
[userId]: counts.followers
|
|
85
|
+
};
|
|
86
|
+
// Update current user's following count (the user doing the following)
|
|
87
|
+
if (currentUserId) {
|
|
88
|
+
updates.followingCounts = {
|
|
89
|
+
...state.followingCounts,
|
|
90
|
+
[currentUserId]: counts.following
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return updates;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
set((state) => ({
|
|
99
|
+
loadingUsers: { ...state.loadingUsers, [userId]: false },
|
|
100
|
+
errors: { ...state.errors, [userId]: error?.message || 'Failed to update follow status' },
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
setFollowerCount: (userId, count) => set((state) => ({
|
|
105
|
+
followerCounts: { ...state.followerCounts, [userId]: count },
|
|
106
|
+
})),
|
|
107
|
+
setFollowingCount: (userId, count) => set((state) => ({
|
|
108
|
+
followingCounts: { ...state.followingCounts, [userId]: count },
|
|
109
|
+
})),
|
|
110
|
+
updateCountsFromFollowAction: (targetUserId, action, counts, currentUserId) => {
|
|
111
|
+
set((state) => {
|
|
112
|
+
const updates = {};
|
|
113
|
+
// Update target user's follower count (the user being followed)
|
|
114
|
+
updates.followerCounts = {
|
|
115
|
+
...state.followerCounts,
|
|
116
|
+
[targetUserId]: counts.followers
|
|
117
|
+
};
|
|
118
|
+
// Update current user's following count (the user doing the following)
|
|
119
|
+
if (currentUserId) {
|
|
120
|
+
updates.followingCounts = {
|
|
121
|
+
...state.followingCounts,
|
|
122
|
+
[currentUserId]: counts.following
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return updates;
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
fetchUserCounts: async (userId, oxyServices) => {
|
|
129
|
+
set((state) => ({
|
|
130
|
+
loadingCounts: { ...state.loadingCounts, [userId]: true },
|
|
131
|
+
}));
|
|
132
|
+
try {
|
|
133
|
+
const user = await oxyServices.getUserById(userId);
|
|
134
|
+
if (user && user._count) {
|
|
135
|
+
set((state) => ({
|
|
136
|
+
followerCounts: {
|
|
137
|
+
...state.followerCounts,
|
|
138
|
+
[userId]: user._count?.followers || 0
|
|
139
|
+
},
|
|
140
|
+
followingCounts: {
|
|
141
|
+
...state.followingCounts,
|
|
142
|
+
[userId]: user._count?.following || 0
|
|
143
|
+
},
|
|
144
|
+
loadingCounts: { ...state.loadingCounts, [userId]: false },
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
set((state) => ({
|
|
150
|
+
loadingCounts: { ...state.loadingCounts, [userId]: false },
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
}));
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Authentication helper utilities to reduce code duplication across hooks and utilities.
|
|
4
|
+
* These functions handle common token validation and authentication error patterns.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.AuthenticationFailedError = exports.SessionSyncRequiredError = void 0;
|
|
8
|
+
exports.ensureValidToken = ensureValidToken;
|
|
9
|
+
exports.isAuthenticationError = isAuthenticationError;
|
|
10
|
+
exports.withAuthErrorHandling = withAuthErrorHandling;
|
|
11
|
+
exports.authenticatedApiCall = authenticatedApiCall;
|
|
12
|
+
/**
|
|
13
|
+
* Error thrown when session sync is required
|
|
14
|
+
*/
|
|
15
|
+
class SessionSyncRequiredError extends Error {
|
|
16
|
+
constructor(message = 'Session needs to be synced. Please try again.') {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'SessionSyncRequiredError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.SessionSyncRequiredError = SessionSyncRequiredError;
|
|
22
|
+
/**
|
|
23
|
+
* Error thrown when authentication fails
|
|
24
|
+
*/
|
|
25
|
+
class AuthenticationFailedError extends Error {
|
|
26
|
+
constructor(message = 'Authentication failed. Please sign in again.') {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = 'AuthenticationFailedError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.AuthenticationFailedError = AuthenticationFailedError;
|
|
32
|
+
/**
|
|
33
|
+
* Ensures a valid token exists before making authenticated API calls.
|
|
34
|
+
* If no valid token exists and an active session ID is available,
|
|
35
|
+
* attempts to refresh the token using the session.
|
|
36
|
+
*
|
|
37
|
+
* @param oxyServices - The OxyServices instance
|
|
38
|
+
* @param activeSessionId - The active session ID (if available)
|
|
39
|
+
* @throws {SessionSyncRequiredError} If the session needs to be synced (offline session)
|
|
40
|
+
* @throws {Error} If token refresh fails for other reasons
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* // In a mutation or query function:
|
|
45
|
+
* await ensureValidToken(oxyServices, activeSessionId);
|
|
46
|
+
* return await oxyServices.updateProfile(updates);
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
async function ensureValidToken(oxyServices, activeSessionId) {
|
|
50
|
+
if (oxyServices.hasValidToken() || !activeSessionId) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
55
|
+
}
|
|
56
|
+
catch (tokenError) {
|
|
57
|
+
const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
|
|
58
|
+
if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
|
|
59
|
+
throw new SessionSyncRequiredError();
|
|
60
|
+
}
|
|
61
|
+
throw tokenError;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Checks if an error is an authentication error (401 or auth-related message)
|
|
66
|
+
*
|
|
67
|
+
* @param error - The error to check
|
|
68
|
+
* @returns True if the error is an authentication error
|
|
69
|
+
*/
|
|
70
|
+
function isAuthenticationError(error) {
|
|
71
|
+
if (!error || typeof error !== 'object') {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const errorObj = error;
|
|
75
|
+
const errorMessage = errorObj.message || '';
|
|
76
|
+
const status = errorObj.status || errorObj.response?.status;
|
|
77
|
+
return (status === 401 ||
|
|
78
|
+
errorMessage.includes('Authentication required') ||
|
|
79
|
+
errorMessage.includes('Invalid or missing authorization header'));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Wraps an API call with authentication error handling.
|
|
83
|
+
* If an authentication error occurs, it can optionally attempt to sync the session and retry.
|
|
84
|
+
*
|
|
85
|
+
* @param apiCall - The API call function to execute
|
|
86
|
+
* @param options - Optional error handling configuration
|
|
87
|
+
* @returns The result of the API call
|
|
88
|
+
* @throws {AuthenticationFailedError} If authentication fails and cannot be recovered
|
|
89
|
+
* @throws {Error} If the API call fails for non-auth reasons
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* // Simple usage:
|
|
94
|
+
* const result = await withAuthErrorHandling(
|
|
95
|
+
* () => oxyServices.updateProfile(updates)
|
|
96
|
+
* );
|
|
97
|
+
*
|
|
98
|
+
* // With retry on auth failure:
|
|
99
|
+
* const result = await withAuthErrorHandling(
|
|
100
|
+
* () => oxyServices.updateProfile(updates),
|
|
101
|
+
* { syncSession, activeSessionId, oxyServices }
|
|
102
|
+
* );
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
async function withAuthErrorHandling(apiCall, options) {
|
|
106
|
+
try {
|
|
107
|
+
return await apiCall();
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (!isAuthenticationError(error)) {
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
// If we have sync capabilities, try to recover
|
|
114
|
+
if (options?.syncSession && options?.activeSessionId && options?.oxyServices) {
|
|
115
|
+
try {
|
|
116
|
+
await options.syncSession();
|
|
117
|
+
await options.oxyServices.getTokenBySession(options.activeSessionId);
|
|
118
|
+
// Retry the API call after refreshing token
|
|
119
|
+
return await apiCall();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
throw new AuthenticationFailedError();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw new AuthenticationFailedError();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Combines token validation and auth error handling for a complete authenticated API call.
|
|
130
|
+
* This is the recommended helper for most authenticated API operations.
|
|
131
|
+
*
|
|
132
|
+
* @param oxyServices - The OxyServices instance
|
|
133
|
+
* @param activeSessionId - The active session ID
|
|
134
|
+
* @param apiCall - The API call function to execute
|
|
135
|
+
* @param syncSession - Optional callback to sync session on auth failure
|
|
136
|
+
* @returns The result of the API call
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* return await authenticatedApiCall(
|
|
141
|
+
* oxyServices,
|
|
142
|
+
* activeSessionId,
|
|
143
|
+
* () => oxyServices.updateProfile(updates)
|
|
144
|
+
* );
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
async function authenticatedApiCall(oxyServices, activeSessionId, apiCall, syncSession) {
|
|
148
|
+
await ensureValidToken(oxyServices, activeSessionId);
|
|
149
|
+
return withAuthErrorHandling(apiCall, {
|
|
150
|
+
syncSession,
|
|
151
|
+
activeSessionId,
|
|
152
|
+
oxyServices,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.updateAvatarVisibility = updateAvatarVisibility;
|
|
4
|
+
exports.refreshAvatarInStore = refreshAvatarInStore;
|
|
5
|
+
exports.updateProfileWithAvatar = updateProfileWithAvatar;
|
|
6
|
+
const accountStore_1 = require("../stores/accountStore");
|
|
7
|
+
const authStore_1 = require("../stores/authStore");
|
|
8
|
+
const queryKeys_1 = require("../hooks/queries/queryKeys");
|
|
9
|
+
const authHelpers_1 = require("./authHelpers");
|
|
10
|
+
/**
|
|
11
|
+
* Updates file visibility to public for avatar use.
|
|
12
|
+
* Handles errors gracefully, only logging non-404 errors.
|
|
13
|
+
*
|
|
14
|
+
* @param fileId - The file ID to update visibility for
|
|
15
|
+
* @param oxyServices - OxyServices instance
|
|
16
|
+
* @param contextName - Optional context name for logging
|
|
17
|
+
* @returns Promise that resolves when visibility is updated (or skipped)
|
|
18
|
+
*/
|
|
19
|
+
async function updateAvatarVisibility(fileId, oxyServices, contextName = 'AvatarUtils') {
|
|
20
|
+
// Skip if temporary asset ID or no file ID
|
|
21
|
+
if (!fileId || fileId.startsWith('temp-')) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
await oxyServices.assetUpdateVisibility(fileId, 'public');
|
|
26
|
+
// Visibility update is logged by the API
|
|
27
|
+
}
|
|
28
|
+
catch (visError) {
|
|
29
|
+
// Silently handle errors - 404 means asset doesn't exist yet (which is OK)
|
|
30
|
+
// Other errors are logged by the API, so no need to log here
|
|
31
|
+
// Function continues gracefully regardless of visibility update success
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Refreshes avatar in accountStore with cache-busted URL to force image reload.
|
|
36
|
+
*
|
|
37
|
+
* @param sessionId - The session ID for the account to update
|
|
38
|
+
* @param avatarFileId - The new avatar file ID
|
|
39
|
+
* @param oxyServices - OxyServices instance to generate download URL
|
|
40
|
+
*/
|
|
41
|
+
function refreshAvatarInStore(sessionId, avatarFileId, oxyServices) {
|
|
42
|
+
const { updateAccount } = accountStore_1.useAccountStore.getState();
|
|
43
|
+
const cacheBustedUrl = oxyServices.getFileDownloadUrl(avatarFileId, 'thumb') + `?t=${Date.now()}`;
|
|
44
|
+
updateAccount(sessionId, {
|
|
45
|
+
avatar: avatarFileId,
|
|
46
|
+
avatarUrl: cacheBustedUrl,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Updates user profile with avatar and handles all side effects (query invalidation, accountStore update).
|
|
51
|
+
* This function can be used from within OxyContext provider without requiring useOxy hook.
|
|
52
|
+
*
|
|
53
|
+
* @param updates - Profile updates including avatar
|
|
54
|
+
* @param oxyServices - OxyServices instance
|
|
55
|
+
* @param activeSessionId - Active session ID
|
|
56
|
+
* @param queryClient - TanStack Query client
|
|
57
|
+
* @param syncSession - Optional function to sync session/refresh token when auth errors occur
|
|
58
|
+
* @returns Promise that resolves with updated user data
|
|
59
|
+
*/
|
|
60
|
+
async function updateProfileWithAvatar(updates, oxyServices, activeSessionId, queryClient, syncSession) {
|
|
61
|
+
const data = await (0, authHelpers_1.authenticatedApiCall)(oxyServices, activeSessionId, () => oxyServices.updateProfile(updates), syncSession);
|
|
62
|
+
// Update cache with server response
|
|
63
|
+
queryClient.setQueryData(queryKeys_1.queryKeys.accounts.current(), data);
|
|
64
|
+
if (activeSessionId) {
|
|
65
|
+
queryClient.setQueryData(queryKeys_1.queryKeys.users.profile(activeSessionId), data);
|
|
66
|
+
}
|
|
67
|
+
// Update authStore so frontend components see the changes immediately
|
|
68
|
+
authStore_1.useAuthStore.getState().setUser(data);
|
|
69
|
+
// If avatar was updated, refresh accountStore with cache-busted URL
|
|
70
|
+
if (updates.avatar && activeSessionId) {
|
|
71
|
+
refreshAvatarInStore(activeSessionId, updates.avatar, oxyServices);
|
|
72
|
+
}
|
|
73
|
+
// Invalidate all related queries to refresh everywhere
|
|
74
|
+
(0, queryKeys_1.invalidateUserQueries)(queryClient);
|
|
75
|
+
(0, queryKeys_1.invalidateAccountQueries)(queryClient);
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleAuthError = exports.extractErrorMessage = exports.isTimeoutOrNetworkError = exports.isInvalidSessionError = void 0;
|
|
4
|
+
const DEFAULT_INVALID_SESSION_MESSAGES = [
|
|
5
|
+
'Invalid or expired session',
|
|
6
|
+
'Session is invalid',
|
|
7
|
+
'Session not found',
|
|
8
|
+
'Session expired',
|
|
9
|
+
];
|
|
10
|
+
const isObject = (value) => typeof value === 'object' && value !== null;
|
|
11
|
+
const getResponseStatus = (error) => {
|
|
12
|
+
if (!isObject(error))
|
|
13
|
+
return undefined;
|
|
14
|
+
const response = error.response;
|
|
15
|
+
return response?.status;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Determine whether the error represents an invalid session condition.
|
|
19
|
+
* This centralizes 401 detection across different fetch clients.
|
|
20
|
+
*/
|
|
21
|
+
const isInvalidSessionError = (error) => {
|
|
22
|
+
const status = getResponseStatus(error);
|
|
23
|
+
if (status === 401) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (!isObject(error)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
// Check error.status directly (HttpService sets this)
|
|
30
|
+
if (error.status === 401) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
const normalizedMessage = (0, exports.extractErrorMessage)(error)?.toLowerCase();
|
|
34
|
+
if (!normalizedMessage) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
// Check for HTTP 401 in message (HttpService creates errors with "HTTP 401:" format)
|
|
38
|
+
if (normalizedMessage.includes('http 401') || normalizedMessage.includes('401')) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return DEFAULT_INVALID_SESSION_MESSAGES.some((msg) => normalizedMessage.includes(msg.toLowerCase()));
|
|
42
|
+
};
|
|
43
|
+
exports.isInvalidSessionError = isInvalidSessionError;
|
|
44
|
+
/**
|
|
45
|
+
* Determine whether the error represents a timeout or network error.
|
|
46
|
+
* These are expected when the device is offline or has poor connectivity.
|
|
47
|
+
*/
|
|
48
|
+
const isTimeoutOrNetworkError = (error) => {
|
|
49
|
+
if (!isObject(error) && !(error instanceof Error)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const message = (0, exports.extractErrorMessage)(error, '').toLowerCase();
|
|
53
|
+
const errorCode = error.code;
|
|
54
|
+
// Check for timeout/cancelled messages
|
|
55
|
+
if (message.includes('timeout') ||
|
|
56
|
+
message.includes('cancelled') ||
|
|
57
|
+
message.includes('econnaborted') ||
|
|
58
|
+
message.includes('aborted') ||
|
|
59
|
+
message.includes('request timeout or cancelled')) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
// Check for timeout/network error codes
|
|
63
|
+
if (errorCode === 'TIMEOUT' || errorCode === 'NETWORK_ERROR' || errorCode === 'ECONNABORTED') {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
// Check for AbortError
|
|
67
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
// Check for network-related TypeErrors
|
|
71
|
+
if (error instanceof TypeError) {
|
|
72
|
+
const typeErrorMessage = error.message.toLowerCase();
|
|
73
|
+
if (typeErrorMessage.includes('fetch') ||
|
|
74
|
+
typeErrorMessage.includes('network') ||
|
|
75
|
+
typeErrorMessage.includes('failed to fetch')) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
};
|
|
81
|
+
exports.isTimeoutOrNetworkError = isTimeoutOrNetworkError;
|
|
82
|
+
/**
|
|
83
|
+
* Extract a consistent error message from unknown error shapes.
|
|
84
|
+
*
|
|
85
|
+
* @param error - The unknown error payload
|
|
86
|
+
* @param fallbackMessage - Message to return when no concrete message is available
|
|
87
|
+
*/
|
|
88
|
+
const extractErrorMessage = (error, fallbackMessage = 'Unexpected error') => {
|
|
89
|
+
if (typeof error === 'string' && error.trim().length > 0) {
|
|
90
|
+
return error;
|
|
91
|
+
}
|
|
92
|
+
if (!isObject(error)) {
|
|
93
|
+
return fallbackMessage;
|
|
94
|
+
}
|
|
95
|
+
const withMessage = error;
|
|
96
|
+
if (withMessage.message && withMessage.message.trim().length > 0) {
|
|
97
|
+
return withMessage.message;
|
|
98
|
+
}
|
|
99
|
+
const withResponse = error;
|
|
100
|
+
const responseMessage = withResponse.response?.data?.message ?? withResponse.response?.data?.error;
|
|
101
|
+
if (typeof responseMessage === 'string' && responseMessage.trim().length > 0) {
|
|
102
|
+
return responseMessage;
|
|
103
|
+
}
|
|
104
|
+
return fallbackMessage;
|
|
105
|
+
};
|
|
106
|
+
exports.extractErrorMessage = extractErrorMessage;
|
|
107
|
+
/**
|
|
108
|
+
* Centralized error handler for auth-related operations.
|
|
109
|
+
*
|
|
110
|
+
* @param error - Unknown error object
|
|
111
|
+
* @param options - Error handling configuration
|
|
112
|
+
* @returns Resolved error message
|
|
113
|
+
*/
|
|
114
|
+
const handleAuthError = (error, { defaultMessage, code, status, onError, setAuthError, logger, }) => {
|
|
115
|
+
const resolvedStatus = status ?? getResponseStatus(error) ?? ((0, exports.isInvalidSessionError)(error) ? 401 : 500);
|
|
116
|
+
const message = (0, exports.extractErrorMessage)(error, defaultMessage);
|
|
117
|
+
if (logger) {
|
|
118
|
+
logger(message, error);
|
|
119
|
+
}
|
|
120
|
+
setAuthError?.(message);
|
|
121
|
+
onError?.({
|
|
122
|
+
message,
|
|
123
|
+
code,
|
|
124
|
+
status: resolvedStatus,
|
|
125
|
+
});
|
|
126
|
+
return message;
|
|
127
|
+
};
|
|
128
|
+
exports.handleAuthError = handleAuthError;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateSessionBatch = exports.fetchSessionsWithFallback = exports.mapSessionsToClient = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Normalize backend session payloads into `ClientSession` objects.
|
|
6
|
+
*
|
|
7
|
+
* @param sessions - Raw session array returned from the API
|
|
8
|
+
* @param fallbackDeviceId - Device identifier to use when missing from payload
|
|
9
|
+
* @param fallbackUserId - User identifier to use when missing from payload
|
|
10
|
+
*/
|
|
11
|
+
const mapSessionsToClient = (sessions, fallbackDeviceId, fallbackUserId) => {
|
|
12
|
+
const now = new Date();
|
|
13
|
+
return sessions.map((session) => ({
|
|
14
|
+
sessionId: session.sessionId,
|
|
15
|
+
deviceId: session.deviceId || fallbackDeviceId || '',
|
|
16
|
+
expiresAt: session.expiresAt || new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
17
|
+
lastActive: session.lastActive || now.toISOString(),
|
|
18
|
+
userId: session.user?.id ||
|
|
19
|
+
session.userId ||
|
|
20
|
+
(session.user?._id ? session.user._id.toString() : undefined) ||
|
|
21
|
+
fallbackUserId ||
|
|
22
|
+
'',
|
|
23
|
+
isCurrent: Boolean(session.isCurrent),
|
|
24
|
+
}));
|
|
25
|
+
};
|
|
26
|
+
exports.mapSessionsToClient = mapSessionsToClient;
|
|
27
|
+
/**
|
|
28
|
+
* Fetch device sessions with fallback to the legacy session endpoint when needed.
|
|
29
|
+
*
|
|
30
|
+
* @param oxyServices - Oxy service instance
|
|
31
|
+
* @param sessionId - Session identifier to fetch
|
|
32
|
+
* @param options - Optional fallback options
|
|
33
|
+
*/
|
|
34
|
+
const fetchSessionsWithFallback = async (oxyServices, sessionId, { fallbackDeviceId, fallbackUserId, logger, } = {}) => {
|
|
35
|
+
try {
|
|
36
|
+
const deviceSessions = await oxyServices.getDeviceSessions(sessionId);
|
|
37
|
+
return (0, exports.mapSessionsToClient)(deviceSessions, fallbackDeviceId, fallbackUserId);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (__DEV__ && logger) {
|
|
41
|
+
logger('Failed to get device sessions, falling back to user sessions', error);
|
|
42
|
+
}
|
|
43
|
+
const userSessions = await oxyServices.getSessionsBySessionId(sessionId);
|
|
44
|
+
return (0, exports.mapSessionsToClient)(userSessions, fallbackDeviceId, fallbackUserId);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
exports.fetchSessionsWithFallback = fetchSessionsWithFallback;
|
|
48
|
+
/**
|
|
49
|
+
* Validate multiple sessions concurrently with configurable concurrency.
|
|
50
|
+
*
|
|
51
|
+
* @param oxyServices - Oxy service instance
|
|
52
|
+
* @param sessionIds - Session identifiers to validate
|
|
53
|
+
* @param options - Validation options
|
|
54
|
+
*/
|
|
55
|
+
const validateSessionBatch = async (oxyServices, sessionIds, { useHeaderValidation = true, maxConcurrency = 5 } = {}) => {
|
|
56
|
+
if (!sessionIds.length) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
const uniqueSessionIds = Array.from(new Set(sessionIds));
|
|
60
|
+
const safeConcurrency = Math.max(1, Math.min(maxConcurrency, uniqueSessionIds.length));
|
|
61
|
+
const results = [];
|
|
62
|
+
let index = 0;
|
|
63
|
+
const worker = async () => {
|
|
64
|
+
while (index < uniqueSessionIds.length) {
|
|
65
|
+
const currentIndex = index;
|
|
66
|
+
index += 1;
|
|
67
|
+
const sessionId = uniqueSessionIds[currentIndex];
|
|
68
|
+
try {
|
|
69
|
+
const validation = await oxyServices.validateSession(sessionId, { useHeaderValidation });
|
|
70
|
+
const valid = Boolean(validation?.valid);
|
|
71
|
+
results.push({
|
|
72
|
+
sessionId,
|
|
73
|
+
valid,
|
|
74
|
+
user: validation?.user,
|
|
75
|
+
raw: validation,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
results.push({
|
|
80
|
+
sessionId,
|
|
81
|
+
valid: false,
|
|
82
|
+
error,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
await Promise.all(Array.from({ length: safeConcurrency }, worker));
|
|
88
|
+
return results;
|
|
89
|
+
};
|
|
90
|
+
exports.validateSessionBatch = validateSessionBatch;
|