rehive 4.1.4 → 4.2.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/dist/admin.d.mts +43 -106
- package/dist/admin.d.ts +43 -106
- package/dist/admin.js +1 -1
- package/dist/admin.mjs +1 -1
- package/dist/auth.d.mts +2 -2
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +1 -1
- package/dist/auth.mjs +1 -1
- package/dist/{chunk-AVPKHYGS.js → chunk-7USVOK77.js} +5 -5
- package/dist/{chunk-X6YDNXYV.js → chunk-ERD2XKSM.js} +1 -1
- package/dist/chunk-G3M5QMRX.js +1 -0
- package/dist/chunk-HSPTEN45.mjs +1 -0
- package/dist/chunk-HWXE5WBF.js +7 -0
- package/dist/chunk-MUN3POSM.mjs +7 -0
- package/dist/{chunk-4WSHB6U6.mjs → chunk-PCR54D6A.mjs} +1 -1
- package/dist/chunk-ZUZHOLAX.mjs +7 -0
- package/dist/{create-api-client-ebyTfhcm.d.ts → create-api-client-ADSlg8HX.d.ts} +1 -1
- package/dist/{create-api-client-C-LEZku2.d.mts → create-api-client-B4Q01gHp.d.mts} +1 -1
- package/dist/{create-auth-DsnHpEf0.d.mts → create-auth-CT_IFt3T.d.mts} +53 -47
- package/dist/{create-auth-DsnHpEf0.d.ts → create-auth-CT_IFt3T.d.ts} +53 -47
- package/dist/extensions/alchemy.d.mts +3 -3
- package/dist/extensions/alchemy.d.ts +3 -3
- package/dist/extensions/alchemy.js +5 -5
- package/dist/extensions/alchemy.mjs +5 -5
- package/dist/extensions/app.d.mts +3 -3
- package/dist/extensions/app.d.ts +3 -3
- package/dist/extensions/app.js +5 -5
- package/dist/extensions/app.mjs +5 -5
- package/dist/extensions/billing.d.mts +3 -3
- package/dist/extensions/billing.d.ts +3 -3
- package/dist/extensions/billing.js +5 -5
- package/dist/extensions/billing.mjs +5 -5
- package/dist/extensions/bridge.d.mts +16 -190
- package/dist/extensions/bridge.d.ts +16 -190
- package/dist/extensions/bridge.js +5 -5
- package/dist/extensions/bridge.mjs +5 -5
- package/dist/extensions/builder.d.mts +3 -3
- package/dist/extensions/builder.d.ts +3 -3
- package/dist/extensions/builder.js +5 -5
- package/dist/extensions/builder.mjs +5 -5
- package/dist/extensions/business.d.mts +47 -82
- package/dist/extensions/business.d.ts +47 -82
- package/dist/extensions/business.js +5 -5
- package/dist/extensions/business.mjs +5 -5
- package/dist/extensions/conversion.d.mts +3 -3
- package/dist/extensions/conversion.d.ts +3 -3
- package/dist/extensions/conversion.js +5 -5
- package/dist/extensions/conversion.mjs +5 -5
- package/dist/extensions/mass-send.d.mts +3 -3
- package/dist/extensions/mass-send.d.ts +3 -3
- package/dist/extensions/mass-send.js +5 -5
- package/dist/extensions/mass-send.mjs +5 -5
- package/dist/extensions/notifications.d.mts +3 -3
- package/dist/extensions/notifications.d.ts +3 -3
- package/dist/extensions/notifications.js +5 -5
- package/dist/extensions/notifications.mjs +5 -5
- package/dist/extensions/payment-requests.d.mts +3 -7
- package/dist/extensions/payment-requests.d.ts +3 -7
- package/dist/extensions/payment-requests.js +5 -5
- package/dist/extensions/payment-requests.mjs +5 -5
- package/dist/extensions/products.d.mts +3 -3
- package/dist/extensions/products.d.ts +3 -3
- package/dist/extensions/products.js +5 -5
- package/dist/extensions/products.mjs +5 -5
- package/dist/extensions/rain.d.mts +3 -3
- package/dist/extensions/rain.d.ts +3 -3
- package/dist/extensions/rain.js +5 -5
- package/dist/extensions/rain.mjs +5 -5
- package/dist/extensions/rewards.d.mts +3 -3
- package/dist/extensions/rewards.d.ts +3 -3
- package/dist/extensions/rewards.js +5 -5
- package/dist/extensions/rewards.mjs +5 -5
- package/dist/extensions/stellar-testnet.d.mts +3 -3
- package/dist/extensions/stellar-testnet.d.ts +3 -3
- package/dist/extensions/stellar-testnet.js +5 -5
- package/dist/extensions/stellar-testnet.mjs +5 -5
- package/dist/extensions/stellar.d.mts +3 -3
- package/dist/extensions/stellar.d.ts +3 -3
- package/dist/extensions/stellar.js +5 -5
- package/dist/extensions/stellar.mjs +5 -5
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/react.d.mts +11 -3
- package/dist/react.d.ts +11 -3
- package/dist/react.js +1 -1
- package/dist/react.mjs +1 -1
- package/dist/user.d.mts +6 -16
- package/dist/user.d.ts +6 -16
- package/dist/user.js +1 -1
- package/dist/user.mjs +1 -1
- package/package.json +1 -1
- package/src/auth/create-auth.ts +773 -169
- package/src/auth/index.ts +25 -2
- package/src/auth/types/index.ts +48 -0
- package/src/extensions/alchemy/openapi-ts/client/client.gen.ts +3 -5
- package/src/extensions/alchemy/openapi-ts/core/bodySerializer.gen.ts +8 -6
- package/src/extensions/alchemy/openapi-ts/core/params.gen.ts +1 -1
- package/src/extensions/app/openapi-ts/client/client.gen.ts +3 -5
- package/src/extensions/app/openapi-ts/core/bodySerializer.gen.ts +8 -6
- package/src/extensions/app/openapi-ts/core/params.gen.ts +1 -1
- package/src/extensions/billing/openapi-ts/client/client.gen.ts +3 -5
- package/src/extensions/billing/openapi-ts/core/bodySerializer.gen.ts +8 -6
- package/src/extensions/billing/openapi-ts/core/params.gen.ts +1 -1
- package/src/extensions/bridge/openapi-ts/client/client.gen.ts +3 -5
- package/src/extensions/bridge/openapi-ts/core/bodySerializer.gen.ts +8 -6
- package/src/extensions/bridge/openapi-ts/core/params.gen.ts +1 -1
- package/src/extensions/bridge/openapi-ts/index.ts +2 -2
- package/src/extensions/bridge/openapi-ts/sdk.gen.ts +1 -43
- package/src/extensions/bridge/openapi-ts/types.gen.ts +11 -201
- package/src/extensions/builder/openapi-ts/client/client.gen.ts +3 -5
- package/src/extensions/builder/openapi-ts/core/bodySerializer.gen.ts +8 -6
- package/src/extensions/builder/openapi-ts/core/params.gen.ts +1 -1
- package/src/extensions/business/openapi-ts/client/client.gen.ts +3 -5
- package/src/extensions/business/openapi-ts/core/bodySerializer.gen.ts +8 -6
- package/src/extensions/business/openapi-ts/core/params.gen.ts +1 -1
- package/src/extensions/business/openapi-ts/types.gen.ts +44 -79
- package/src/extensions/conversion/openapi-ts/client/client.gen.ts +3 -5
- package/src/extensions/conversion/openapi-ts/core/bodySerializer.gen.ts +8 -6
- package/src/extensions/conversion/openapi-ts/core/params.gen.ts +1 -1
- package/src/extensions/mass-send/openapi-ts/client/client.gen.ts +3 -5
- package/src/extensions/mass-send/openapi-ts/core/bodySerializer.gen.ts +8 -6
- package/src/extensions/mass-send/openapi-ts/core/params.gen.ts +1 -1
- package/src/extensions/notifications/openapi-ts/client/client.gen.ts +3 -5
- package/src/extensions/notifications/openapi-ts/core/bodySerializer.gen.ts +8 -6
- package/src/extensions/notifications/openapi-ts/core/params.gen.ts +1 -1
- package/src/extensions/payment-requests/openapi-ts/client/client.gen.ts +3 -5
- package/src/extensions/payment-requests/openapi-ts/core/bodySerializer.gen.ts +8 -6
- package/src/extensions/payment-requests/openapi-ts/core/params.gen.ts +1 -1
- package/src/extensions/payment-requests/openapi-ts/types.gen.ts +0 -4
- package/src/extensions/products/openapi-ts/client/client.gen.ts +3 -5
- package/src/extensions/products/openapi-ts/core/bodySerializer.gen.ts +8 -6
- package/src/extensions/products/openapi-ts/core/params.gen.ts +1 -1
- package/src/extensions/rain/openapi-ts/client/client.gen.ts +3 -5
- package/src/extensions/rain/openapi-ts/core/bodySerializer.gen.ts +8 -6
- package/src/extensions/rain/openapi-ts/core/params.gen.ts +1 -1
- package/src/extensions/rewards/openapi-ts/client/client.gen.ts +3 -5
- package/src/extensions/rewards/openapi-ts/core/bodySerializer.gen.ts +8 -6
- package/src/extensions/rewards/openapi-ts/core/params.gen.ts +1 -1
- package/src/extensions/stellar/openapi-ts/client/client.gen.ts +3 -5
- package/src/extensions/stellar/openapi-ts/core/bodySerializer.gen.ts +8 -6
- package/src/extensions/stellar/openapi-ts/core/params.gen.ts +1 -1
- package/src/extensions/stellar-testnet/openapi-ts/client/client.gen.ts +3 -5
- package/src/extensions/stellar-testnet/openapi-ts/core/bodySerializer.gen.ts +8 -6
- package/src/extensions/stellar-testnet/openapi-ts/core/params.gen.ts +1 -1
- package/src/platform/admin/openapi-ts/client/client.gen.ts +3 -5
- package/src/platform/admin/openapi-ts/core/bodySerializer.gen.ts +8 -6
- package/src/platform/admin/openapi-ts/core/params.gen.ts +1 -1
- package/src/platform/admin/openapi-ts/sdk.gen.ts +10 -10
- package/src/platform/admin/openapi-ts/types.gen.ts +40 -103
- package/src/platform/user/openapi-ts/client/client.gen.ts +3 -5
- package/src/platform/user/openapi-ts/core/bodySerializer.gen.ts +8 -6
- package/src/platform/user/openapi-ts/core/params.gen.ts +1 -1
- package/src/platform/user/openapi-ts/index.ts +2 -2
- package/src/platform/user/openapi-ts/sdk.gen.ts +3 -29
- package/src/platform/user/openapi-ts/types.gen.ts +7 -54
- package/dist/chunk-KUT5NSX7.mjs +0 -7
- package/dist/chunk-KZWBOQHZ.mjs +0 -7
- package/dist/chunk-PE6PG7ZE.js +0 -1
- package/dist/chunk-UYYU5OJ4.js +0 -7
- package/dist/chunk-Z7BUNKND.mjs +0 -1
package/src/auth/create-auth.ts
CHANGED
|
@@ -14,10 +14,16 @@ import type {
|
|
|
14
14
|
import { WebStorageAdapter, MemoryStorageAdapter } from './core/storage-adapters.js';
|
|
15
15
|
import { ApiError, normalizeFetch } from '../shared/api-utils.js';
|
|
16
16
|
import type {
|
|
17
|
+
AuthEvent,
|
|
18
|
+
AuthEventListener,
|
|
19
|
+
AuthRecoveryState,
|
|
17
20
|
AuthSession,
|
|
21
|
+
AuthSnapshot,
|
|
18
22
|
AuthState,
|
|
19
|
-
|
|
23
|
+
AuthStateListener,
|
|
24
|
+
AuthStatus,
|
|
20
25
|
ErrorListener,
|
|
26
|
+
SessionListener,
|
|
21
27
|
StorageAdapter,
|
|
22
28
|
} from './types/index.js';
|
|
23
29
|
|
|
@@ -43,6 +49,21 @@ export type RegisterParams = {
|
|
|
43
49
|
|
|
44
50
|
export type RegisterCompanyParams = RegisterCompanyRequestWritable;
|
|
45
51
|
|
|
52
|
+
export interface ImportTokenOptions {
|
|
53
|
+
company?: string;
|
|
54
|
+
expires?: number;
|
|
55
|
+
sessionDuration?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ValidateSessionOptions {
|
|
59
|
+
retryCount?: number;
|
|
60
|
+
retryDelayMs?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type SessionPatch =
|
|
64
|
+
| Partial<AuthSession>
|
|
65
|
+
| ((session: AuthSession) => AuthSession);
|
|
66
|
+
|
|
46
67
|
export interface AuthConfig {
|
|
47
68
|
baseUrl?: string;
|
|
48
69
|
storage?: 'local' | 'memory' | StorageAdapter;
|
|
@@ -61,11 +82,25 @@ export interface Auth {
|
|
|
61
82
|
getActiveSession(): AuthSession | null;
|
|
62
83
|
getSessions(): AuthSession[];
|
|
63
84
|
getSessionsByCompany(company: string): AuthSession[];
|
|
85
|
+
getState(): AuthSnapshot;
|
|
86
|
+
getStatus(): AuthStatus;
|
|
87
|
+
getRecoveryState(): AuthRecoveryState;
|
|
64
88
|
switchToSession(userId: string, company?: string): Promise<AuthSession | null>;
|
|
65
89
|
clearAllSessions(): Promise<void>;
|
|
66
90
|
deleteChallenge(challengeId: string): Promise<void>;
|
|
91
|
+
importToken(token: string, options?: ImportTokenOptions): Promise<AuthSession>;
|
|
92
|
+
validateActiveSession(options?: ValidateSessionOptions): Promise<boolean>;
|
|
93
|
+
syncActiveSessionUser(): Promise<AuthSession | null>;
|
|
94
|
+
updateSession(
|
|
95
|
+
userId: string,
|
|
96
|
+
company: string | undefined,
|
|
97
|
+
patch: SessionPatch,
|
|
98
|
+
): Promise<AuthSession | null>;
|
|
99
|
+
expireActiveSession(): Promise<AuthRecoveryState>;
|
|
67
100
|
subscribe(listener: SessionListener): () => void;
|
|
68
101
|
subscribeToErrors(listener: ErrorListener): () => void;
|
|
102
|
+
subscribeToState(listener: AuthStateListener): () => void;
|
|
103
|
+
subscribeToEvents(listener: AuthEventListener): () => void;
|
|
69
104
|
readonly baseUrl: string;
|
|
70
105
|
}
|
|
71
106
|
|
|
@@ -106,46 +141,195 @@ function errorHandlingFetch(baseFetch: typeof fetch): typeof fetch {
|
|
|
106
141
|
};
|
|
107
142
|
}
|
|
108
143
|
|
|
144
|
+
function sleep(ms: number): Promise<void> {
|
|
145
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getSessionCompany(session: AuthSession): string | undefined {
|
|
149
|
+
return session.company ?? session.user?.company;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function cloneSessions(sessions: AuthSession[]): AuthSession[] {
|
|
153
|
+
return [...sessions];
|
|
154
|
+
}
|
|
155
|
+
|
|
109
156
|
export function createAuth(config: AuthConfig = {}): Auth {
|
|
110
157
|
const baseUrl = config.baseUrl || 'https://api.rehive.com';
|
|
111
158
|
const storage = resolveStorage(config.storage);
|
|
112
159
|
const permanentToken = config.token;
|
|
113
160
|
const enableCrossTabSync = config.enableCrossTabSync ?? true;
|
|
114
161
|
const storageKey = 'rehive_auth_state';
|
|
162
|
+
const baseFetch = normalizeFetch(globalThis.fetch);
|
|
115
163
|
|
|
116
164
|
const client = createClient({
|
|
117
165
|
baseUrl,
|
|
118
166
|
responseStyle: 'data' as const,
|
|
119
|
-
fetch: errorHandlingFetch(
|
|
167
|
+
fetch: errorHandlingFetch(baseFetch),
|
|
120
168
|
});
|
|
121
169
|
|
|
122
170
|
let sessions: AuthSession[] = [];
|
|
123
171
|
let activeSessionIndex = -1;
|
|
124
172
|
let sessionListeners: SessionListener[] = [];
|
|
125
173
|
let errorListeners: ErrorListener[] = [];
|
|
174
|
+
let stateListeners: AuthStateListener[] = [];
|
|
175
|
+
let eventListeners: AuthEventListener[] = [];
|
|
126
176
|
let refreshPromise: Promise<void> | null = null;
|
|
127
177
|
let isRefreshing = false;
|
|
128
178
|
let loadAuthStatePromise: Promise<AuthState> | null = null;
|
|
129
179
|
let initialized = false;
|
|
180
|
+
let initializePromise: Promise<void> | null = null;
|
|
181
|
+
let cachedSnapshot: AuthSnapshot | null = null;
|
|
182
|
+
let lastError: Error | null = null;
|
|
183
|
+
let lastExpiredSession: AuthSession | null = null;
|
|
130
184
|
|
|
131
185
|
function isTokenExpired(expires: number): boolean {
|
|
132
186
|
return Date.now() >= expires - 30 * 1000;
|
|
133
187
|
}
|
|
134
188
|
|
|
135
|
-
function
|
|
136
|
-
if (
|
|
137
|
-
|
|
189
|
+
function getActiveSessionFromState(state: AuthState): AuthSession | null {
|
|
190
|
+
if (
|
|
191
|
+
state.activeSessionIndex >= 0 &&
|
|
192
|
+
state.activeSessionIndex < state.sessions.length
|
|
193
|
+
) {
|
|
194
|
+
return state.sessions[state.activeSessionIndex];
|
|
138
195
|
}
|
|
139
196
|
return null;
|
|
140
197
|
}
|
|
141
198
|
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
199
|
+
function getCurrentState(): AuthState {
|
|
200
|
+
return {
|
|
201
|
+
sessions: cloneSessions(sessions),
|
|
202
|
+
activeSessionIndex,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function getActiveSession(): AuthSession | null {
|
|
207
|
+
return getActiveSessionFromState(getCurrentState());
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function buildRecoveryState(state: AuthState = getCurrentState()): AuthRecoveryState {
|
|
211
|
+
const session = getActiveSessionFromState(state);
|
|
212
|
+
const pending = !permanentToken && !session && state.sessions.length > 0;
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
pending,
|
|
216
|
+
expiredSession: pending ? lastExpiredSession : null,
|
|
217
|
+
remainingSessions: pending ? cloneSessions(state.sessions) : [],
|
|
218
|
+
};
|
|
145
219
|
}
|
|
146
220
|
|
|
147
|
-
function
|
|
148
|
-
|
|
221
|
+
function getStatus(state: AuthState = getCurrentState()): AuthStatus {
|
|
222
|
+
if (!initialized && !permanentToken) {
|
|
223
|
+
return 'loading';
|
|
224
|
+
}
|
|
225
|
+
if (isRefreshing) {
|
|
226
|
+
return 'refreshing';
|
|
227
|
+
}
|
|
228
|
+
if (permanentToken) {
|
|
229
|
+
return 'authenticated';
|
|
230
|
+
}
|
|
231
|
+
if (getActiveSessionFromState(state)) {
|
|
232
|
+
return 'authenticated';
|
|
233
|
+
}
|
|
234
|
+
if (state.sessions.length > 0) {
|
|
235
|
+
return 'recoverable';
|
|
236
|
+
}
|
|
237
|
+
return 'unauthenticated';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function getState(): AuthSnapshot {
|
|
241
|
+
if (cachedSnapshot) {
|
|
242
|
+
return cachedSnapshot;
|
|
243
|
+
}
|
|
244
|
+
const state = getCurrentState();
|
|
245
|
+
cachedSnapshot = {
|
|
246
|
+
status: getStatus(state),
|
|
247
|
+
session: getActiveSessionFromState(state),
|
|
248
|
+
sessions: state.sessions,
|
|
249
|
+
activeSessionIndex: state.activeSessionIndex,
|
|
250
|
+
isRefreshing,
|
|
251
|
+
initialized: initialized || !!permanentToken,
|
|
252
|
+
error: lastError,
|
|
253
|
+
recovery: buildRecoveryState(state),
|
|
254
|
+
};
|
|
255
|
+
return cachedSnapshot;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function invalidateSnapshot(): void {
|
|
259
|
+
cachedSnapshot = null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function emit(event: Omit<AuthEvent, 'snapshot'>): void {
|
|
263
|
+
const snapshot = getState();
|
|
264
|
+
const payload: AuthEvent = {
|
|
265
|
+
...event,
|
|
266
|
+
snapshot,
|
|
267
|
+
};
|
|
268
|
+
eventListeners.forEach((listener) => listener(payload));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function notifyAll(event?: Omit<AuthEvent, 'snapshot'>): void {
|
|
272
|
+
invalidateSnapshot();
|
|
273
|
+
const snapshot = getState();
|
|
274
|
+
sessionListeners.forEach((listener) => listener(snapshot.session));
|
|
275
|
+
errorListeners.forEach((listener) => listener(snapshot.error));
|
|
276
|
+
stateListeners.forEach((listener) => listener(snapshot));
|
|
277
|
+
|
|
278
|
+
if (event) {
|
|
279
|
+
emit(event);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function setError(error: Error | null): void {
|
|
284
|
+
lastError = error;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function persistState(state: AuthState): Promise<void> {
|
|
288
|
+
if (permanentToken) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
await storage.setItem(storageKey, JSON.stringify(state));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function applyState(
|
|
296
|
+
state: AuthState,
|
|
297
|
+
options: {
|
|
298
|
+
clearExpiredSession?: boolean;
|
|
299
|
+
expiredSession?: AuthSession | null;
|
|
300
|
+
error?: Error | null;
|
|
301
|
+
event?: Omit<AuthEvent, 'snapshot'>;
|
|
302
|
+
} = {},
|
|
303
|
+
): Promise<void> {
|
|
304
|
+
sessions = cloneSessions(state.sessions);
|
|
305
|
+
activeSessionIndex =
|
|
306
|
+
state.activeSessionIndex >= 0 && state.activeSessionIndex < sessions.length
|
|
307
|
+
? state.activeSessionIndex
|
|
308
|
+
: -1;
|
|
309
|
+
|
|
310
|
+
if (options.clearExpiredSession || activeSessionIndex >= 0 || sessions.length === 0) {
|
|
311
|
+
lastExpiredSession = null;
|
|
312
|
+
}
|
|
313
|
+
if (Object.prototype.hasOwnProperty.call(options, 'expiredSession')) {
|
|
314
|
+
lastExpiredSession = options.expiredSession ?? null;
|
|
315
|
+
}
|
|
316
|
+
if (Object.prototype.hasOwnProperty.call(options, 'error')) {
|
|
317
|
+
setError(options.error ?? null);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
await persistState({
|
|
322
|
+
sessions,
|
|
323
|
+
activeSessionIndex,
|
|
324
|
+
});
|
|
325
|
+
} catch (error) {
|
|
326
|
+
console.error('Failed to save auth state:', error);
|
|
327
|
+
if (!lastError && error instanceof Error) {
|
|
328
|
+
setError(error);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
notifyAll(options.event);
|
|
149
333
|
}
|
|
150
334
|
|
|
151
335
|
async function loadAuthState(): Promise<AuthState> {
|
|
@@ -168,9 +352,17 @@ export function createAuth(config: AuthConfig = {}): Auth {
|
|
|
168
352
|
} catch (error) {
|
|
169
353
|
console.error('Failed to load auth state:', error);
|
|
170
354
|
}
|
|
355
|
+
|
|
171
356
|
sessions = state.sessions;
|
|
172
|
-
activeSessionIndex =
|
|
173
|
-
|
|
357
|
+
activeSessionIndex =
|
|
358
|
+
state.activeSessionIndex >= 0 && state.activeSessionIndex < state.sessions.length
|
|
359
|
+
? state.activeSessionIndex
|
|
360
|
+
: -1;
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
sessions: cloneSessions(sessions),
|
|
364
|
+
activeSessionIndex,
|
|
365
|
+
};
|
|
174
366
|
})();
|
|
175
367
|
|
|
176
368
|
try {
|
|
@@ -180,51 +372,133 @@ export function createAuth(config: AuthConfig = {}): Auth {
|
|
|
180
372
|
}
|
|
181
373
|
}
|
|
182
374
|
|
|
183
|
-
async function saveAuthState(state: AuthState): Promise<void> {
|
|
184
|
-
try {
|
|
185
|
-
await storage.setItem(storageKey, JSON.stringify(state));
|
|
186
|
-
sessions = state.sessions;
|
|
187
|
-
activeSessionIndex = state.activeSessionIndex;
|
|
188
|
-
notifySessionListeners();
|
|
189
|
-
} catch (error) {
|
|
190
|
-
console.error('Failed to save auth state:', error);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
375
|
function setupCrossTabSync(): void {
|
|
195
376
|
if (typeof window !== 'undefined') {
|
|
196
377
|
window.addEventListener('storage', (event: StorageEvent) => {
|
|
197
|
-
if (event.key
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
378
|
+
if (event.key !== storageKey) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!event.newValue) {
|
|
383
|
+
void applyState(
|
|
384
|
+
{ sessions: [], activeSessionIndex: -1 },
|
|
385
|
+
{ clearExpiredSession: true },
|
|
386
|
+
);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
const newState = JSON.parse(event.newValue);
|
|
392
|
+
void applyState(
|
|
393
|
+
{
|
|
394
|
+
sessions: Array.isArray(newState.sessions) ? newState.sessions : [],
|
|
395
|
+
activeSessionIndex:
|
|
396
|
+
typeof newState.activeSessionIndex === 'number'
|
|
397
|
+
? newState.activeSessionIndex
|
|
398
|
+
: -1,
|
|
399
|
+
},
|
|
400
|
+
{},
|
|
401
|
+
);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.error('Failed to sync auth state from storage event:', error);
|
|
209
404
|
}
|
|
210
405
|
});
|
|
211
406
|
}
|
|
212
407
|
}
|
|
213
408
|
|
|
214
409
|
async function initialize(): Promise<void> {
|
|
215
|
-
if (initialized)
|
|
216
|
-
|
|
410
|
+
if (initialized) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (initializePromise) {
|
|
414
|
+
return initializePromise;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
initializePromise = (async () => {
|
|
418
|
+
if (!permanentToken) {
|
|
419
|
+
if (enableCrossTabSync) {
|
|
420
|
+
setupCrossTabSync();
|
|
421
|
+
}
|
|
422
|
+
await loadAuthState();
|
|
423
|
+
}
|
|
424
|
+
initialized = true;
|
|
425
|
+
notifyAll({ type: 'initialized' });
|
|
426
|
+
})();
|
|
427
|
+
|
|
428
|
+
return initializePromise;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function fetchUserForToken(token: string): Promise<any> {
|
|
432
|
+
const response = await baseFetch(`${baseUrl.replace(/\/$/, '')}/3/user/`, {
|
|
433
|
+
headers: {
|
|
434
|
+
Authorization: `Token ${token}`,
|
|
435
|
+
'Content-Type': 'application/json',
|
|
436
|
+
},
|
|
437
|
+
});
|
|
217
438
|
|
|
218
|
-
if (!
|
|
219
|
-
|
|
220
|
-
|
|
439
|
+
if (!response.ok) {
|
|
440
|
+
const errorText = await response.text();
|
|
441
|
+
let errorJson: any = null;
|
|
442
|
+
try {
|
|
443
|
+
errorJson = JSON.parse(errorText);
|
|
444
|
+
} catch {
|
|
445
|
+
// not JSON
|
|
221
446
|
}
|
|
222
|
-
|
|
223
|
-
|
|
447
|
+
|
|
448
|
+
throw new ApiError({
|
|
449
|
+
status: response.status,
|
|
450
|
+
error: errorJson || errorText,
|
|
451
|
+
message:
|
|
452
|
+
errorJson?.error ||
|
|
453
|
+
errorJson?.message ||
|
|
454
|
+
'A server error occurred. HTTPStatus: ' + response.status,
|
|
455
|
+
});
|
|
224
456
|
}
|
|
457
|
+
|
|
458
|
+
const payload = await response.json();
|
|
459
|
+
return payload?.data ?? payload;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function upsertSession(
|
|
463
|
+
newSession: AuthSession,
|
|
464
|
+
eventType: AuthEvent['type'],
|
|
465
|
+
): Promise<AuthSession> {
|
|
466
|
+
await initialize();
|
|
467
|
+
const currentState = await loadAuthState();
|
|
468
|
+
const sessionCompany = getSessionCompany(newSession);
|
|
469
|
+
const existingIdx = currentState.sessions.findIndex(
|
|
470
|
+
(session) =>
|
|
471
|
+
session.user.id === newSession.user.id &&
|
|
472
|
+
getSessionCompany(session) === sessionCompany,
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
let nextState: AuthState;
|
|
476
|
+
if (existingIdx >= 0) {
|
|
477
|
+
const updatedSessions = cloneSessions(currentState.sessions);
|
|
478
|
+
updatedSessions[existingIdx] = newSession;
|
|
479
|
+
nextState = {
|
|
480
|
+
sessions: updatedSessions,
|
|
481
|
+
activeSessionIndex: existingIdx,
|
|
482
|
+
};
|
|
483
|
+
} else {
|
|
484
|
+
nextState = {
|
|
485
|
+
sessions: [...currentState.sessions, newSession],
|
|
486
|
+
activeSessionIndex: currentState.sessions.length,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
await applyState(nextState, {
|
|
491
|
+
clearExpiredSession: true,
|
|
492
|
+
error: null,
|
|
493
|
+
event: {
|
|
494
|
+
type: eventType,
|
|
495
|
+
session: newSession,
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
return newSession;
|
|
225
500
|
}
|
|
226
501
|
|
|
227
|
-
// Kick off initialization (non-blocking)
|
|
228
502
|
if (!permanentToken) {
|
|
229
503
|
initialize().catch(console.error);
|
|
230
504
|
}
|
|
@@ -244,40 +518,25 @@ export function createAuth(config: AuthConfig = {}): Auth {
|
|
|
244
518
|
const result: any = await authLogin({ client, body, throwOnError: true });
|
|
245
519
|
const data = result?.data ?? result;
|
|
246
520
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const existingIdx = currentState.sessions.findIndex(
|
|
259
|
-
(s: AuthSession) =>
|
|
260
|
-
s.user.id === newSession.user.id && s.company === newSession.company,
|
|
521
|
+
return await upsertSession(
|
|
522
|
+
{
|
|
523
|
+
user: data.user,
|
|
524
|
+
token: data.token,
|
|
525
|
+
refresh_token: data.refresh_token,
|
|
526
|
+
challenges: data.challenges,
|
|
527
|
+
expires: data.expires,
|
|
528
|
+
session_duration: sessionDuration,
|
|
529
|
+
company: params.company,
|
|
530
|
+
},
|
|
531
|
+
'login',
|
|
261
532
|
);
|
|
262
|
-
|
|
263
|
-
let newState: AuthState;
|
|
264
|
-
if (existingIdx !== -1) {
|
|
265
|
-
const updated = [...currentState.sessions];
|
|
266
|
-
updated[existingIdx] = newSession;
|
|
267
|
-
newState = { ...currentState, sessions: updated, activeSessionIndex: existingIdx };
|
|
268
|
-
} else {
|
|
269
|
-
newState = {
|
|
270
|
-
sessions: [...currentState.sessions, newSession],
|
|
271
|
-
activeSessionIndex: currentState.sessions.length,
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
await saveAuthState(newState);
|
|
276
|
-
notifyErrorListeners(null);
|
|
277
|
-
return newSession;
|
|
278
533
|
} catch (e) {
|
|
279
534
|
const error = e instanceof ApiError ? e : new Error('Login failed');
|
|
280
|
-
|
|
535
|
+
setError(error);
|
|
536
|
+
notifyAll({
|
|
537
|
+
type: 'error',
|
|
538
|
+
error,
|
|
539
|
+
});
|
|
281
540
|
throw e;
|
|
282
541
|
}
|
|
283
542
|
}
|
|
@@ -303,28 +562,25 @@ export function createAuth(config: AuthConfig = {}): Auth {
|
|
|
303
562
|
const result: any = await authRegister({ client, body, throwOnError: true });
|
|
304
563
|
const data = result?.data ?? result;
|
|
305
564
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
sessions: [...currentState.sessions, newSession],
|
|
319
|
-
activeSessionIndex: currentState.sessions.length,
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
await saveAuthState(newState);
|
|
323
|
-
notifyErrorListeners(null);
|
|
324
|
-
return newSession;
|
|
565
|
+
return await upsertSession(
|
|
566
|
+
{
|
|
567
|
+
user: data.user,
|
|
568
|
+
token: data.token,
|
|
569
|
+
refresh_token: data.refresh_token,
|
|
570
|
+
challenges: data.challenges,
|
|
571
|
+
expires: data.expires,
|
|
572
|
+
session_duration: sessionDuration,
|
|
573
|
+
company: params.company,
|
|
574
|
+
},
|
|
575
|
+
'register',
|
|
576
|
+
);
|
|
325
577
|
} catch (e) {
|
|
326
578
|
const error = e instanceof ApiError ? e : new Error('Registration failed');
|
|
327
|
-
|
|
579
|
+
setError(error);
|
|
580
|
+
notifyAll({
|
|
581
|
+
type: 'error',
|
|
582
|
+
error,
|
|
583
|
+
});
|
|
328
584
|
throw e;
|
|
329
585
|
}
|
|
330
586
|
}
|
|
@@ -332,47 +588,45 @@ export function createAuth(config: AuthConfig = {}): Auth {
|
|
|
332
588
|
async function registerCompanyFn(params: RegisterCompanyParams): Promise<AuthSession> {
|
|
333
589
|
await initialize();
|
|
334
590
|
try {
|
|
335
|
-
const
|
|
336
|
-
|
|
591
|
+
const result: any = await authRegisterCompany({
|
|
592
|
+
client,
|
|
593
|
+
body: params,
|
|
594
|
+
throwOnError: true,
|
|
595
|
+
});
|
|
337
596
|
const data = result?.data ?? result;
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
await saveAuthState(newState);
|
|
356
|
-
notifyErrorListeners(null);
|
|
357
|
-
return newSession;
|
|
597
|
+
const company =
|
|
598
|
+
typeof params.company === 'string'
|
|
599
|
+
? params.company
|
|
600
|
+
: (params.company as any)?.id;
|
|
601
|
+
|
|
602
|
+
return await upsertSession(
|
|
603
|
+
{
|
|
604
|
+
user: data.user,
|
|
605
|
+
token: data.token,
|
|
606
|
+
refresh_token: data.refresh_token,
|
|
607
|
+
challenges: data.challenges,
|
|
608
|
+
expires: data.expires,
|
|
609
|
+
session_duration: 900,
|
|
610
|
+
company,
|
|
611
|
+
},
|
|
612
|
+
'register-company',
|
|
613
|
+
);
|
|
358
614
|
} catch (e) {
|
|
359
|
-
const error =
|
|
360
|
-
|
|
615
|
+
const error =
|
|
616
|
+
e instanceof ApiError ? e : new Error('Company registration failed');
|
|
617
|
+
setError(error);
|
|
618
|
+
notifyAll({
|
|
619
|
+
type: 'error',
|
|
620
|
+
error,
|
|
621
|
+
});
|
|
361
622
|
throw e;
|
|
362
623
|
}
|
|
363
624
|
}
|
|
364
625
|
|
|
365
626
|
async function logout(): Promise<void> {
|
|
627
|
+
await initialize();
|
|
366
628
|
const currentState = await loadAuthState();
|
|
367
|
-
const session = currentState
|
|
368
|
-
|
|
369
|
-
const newState: AuthState = {
|
|
370
|
-
...currentState,
|
|
371
|
-
sessions: currentState.sessions.filter(
|
|
372
|
-
(_: AuthSession, i: number) => i !== currentState.activeSessionIndex,
|
|
373
|
-
),
|
|
374
|
-
activeSessionIndex: -1,
|
|
375
|
-
};
|
|
629
|
+
const session = getActiveSessionFromState(currentState);
|
|
376
630
|
|
|
377
631
|
if (session?.token) {
|
|
378
632
|
try {
|
|
@@ -383,19 +637,34 @@ export function createAuth(config: AuthConfig = {}): Auth {
|
|
|
383
637
|
throwOnError: true,
|
|
384
638
|
});
|
|
385
639
|
} catch {
|
|
386
|
-
// Continue with local logout even if API call fails
|
|
640
|
+
// Continue with local logout even if API call fails.
|
|
387
641
|
}
|
|
388
642
|
}
|
|
389
643
|
|
|
390
|
-
await
|
|
391
|
-
|
|
644
|
+
await applyState(
|
|
645
|
+
{
|
|
646
|
+
sessions: currentState.sessions.filter(
|
|
647
|
+
(_session, index) => index !== currentState.activeSessionIndex,
|
|
648
|
+
),
|
|
649
|
+
activeSessionIndex: -1,
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
clearExpiredSession: true,
|
|
653
|
+
error: null,
|
|
654
|
+
event: {
|
|
655
|
+
type: 'logout',
|
|
656
|
+
session,
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
);
|
|
392
660
|
}
|
|
393
661
|
|
|
394
662
|
async function logoutAll(): Promise<void> {
|
|
663
|
+
await initialize();
|
|
395
664
|
const currentState = await loadAuthState();
|
|
396
665
|
|
|
397
666
|
await Promise.all(
|
|
398
|
-
currentState.sessions.map(async (session
|
|
667
|
+
currentState.sessions.map(async (session) => {
|
|
399
668
|
try {
|
|
400
669
|
await authLogout({
|
|
401
670
|
client,
|
|
@@ -404,26 +673,44 @@ export function createAuth(config: AuthConfig = {}): Auth {
|
|
|
404
673
|
throwOnError: true,
|
|
405
674
|
});
|
|
406
675
|
} catch {
|
|
407
|
-
//
|
|
676
|
+
// Continue clearing local sessions even if API logout fails.
|
|
408
677
|
}
|
|
409
678
|
}),
|
|
410
679
|
);
|
|
411
680
|
|
|
412
|
-
await
|
|
413
|
-
|
|
681
|
+
await applyState(
|
|
682
|
+
{
|
|
683
|
+
sessions: [],
|
|
684
|
+
activeSessionIndex: -1,
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
clearExpiredSession: true,
|
|
688
|
+
error: null,
|
|
689
|
+
event: {
|
|
690
|
+
type: 'logout-all',
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
);
|
|
414
694
|
}
|
|
415
695
|
|
|
416
696
|
async function refresh(): Promise<void> {
|
|
417
|
-
if (refreshPromise)
|
|
697
|
+
if (refreshPromise) {
|
|
698
|
+
return refreshPromise;
|
|
699
|
+
}
|
|
418
700
|
|
|
419
701
|
refreshPromise = (async () => {
|
|
420
702
|
isRefreshing = true;
|
|
703
|
+
notifyAll();
|
|
704
|
+
|
|
705
|
+
let sessionBeforeRefresh: AuthSession | null = null;
|
|
706
|
+
|
|
421
707
|
try {
|
|
422
708
|
const currentState = await loadAuthState();
|
|
423
709
|
const idx = currentState.activeSessionIndex;
|
|
424
|
-
const session = currentState
|
|
710
|
+
const session = getActiveSessionFromState(currentState);
|
|
711
|
+
sessionBeforeRefresh = session;
|
|
425
712
|
|
|
426
|
-
if (!session?.token || !session
|
|
713
|
+
if (idx < 0 || !session?.token || !session.refresh_token) {
|
|
427
714
|
throw new Error('No active session, token, or refresh token found');
|
|
428
715
|
}
|
|
429
716
|
|
|
@@ -435,33 +722,60 @@ export function createAuth(config: AuthConfig = {}): Auth {
|
|
|
435
722
|
});
|
|
436
723
|
const data = result?.data ?? result;
|
|
437
724
|
|
|
438
|
-
const updatedSessions =
|
|
725
|
+
const updatedSessions = cloneSessions(currentState.sessions);
|
|
439
726
|
updatedSessions[idx] = {
|
|
440
727
|
...session,
|
|
441
|
-
|
|
442
|
-
|
|
728
|
+
token: data.token ?? session.token,
|
|
729
|
+
refresh_token: data.refresh_token ?? session.refresh_token,
|
|
730
|
+
expires: data.expires ?? session.expires,
|
|
443
731
|
session_duration: session.session_duration ?? 900,
|
|
444
|
-
company: session
|
|
732
|
+
company: getSessionCompany(session),
|
|
445
733
|
};
|
|
446
734
|
|
|
447
|
-
await
|
|
448
|
-
|
|
735
|
+
await applyState(
|
|
736
|
+
{
|
|
737
|
+
sessions: updatedSessions,
|
|
738
|
+
activeSessionIndex: idx,
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
clearExpiredSession: true,
|
|
742
|
+
error: null,
|
|
743
|
+
event: {
|
|
744
|
+
type: 'refresh',
|
|
745
|
+
session: updatedSessions[idx],
|
|
746
|
+
},
|
|
747
|
+
},
|
|
748
|
+
);
|
|
449
749
|
} catch (e) {
|
|
450
750
|
const error = e instanceof ApiError ? e : new Error('Refresh failed');
|
|
451
|
-
notifyErrorListeners(error);
|
|
452
|
-
|
|
453
751
|
const currentState = await loadAuthState();
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
),
|
|
459
|
-
|
|
460
|
-
|
|
752
|
+
const expiredSession =
|
|
753
|
+
sessionBeforeRefresh ??
|
|
754
|
+
getActiveSessionFromState(currentState);
|
|
755
|
+
const nextSessions = currentState.sessions.filter(
|
|
756
|
+
(_session, index) => index !== currentState.activeSessionIndex,
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
await applyState(
|
|
760
|
+
{
|
|
761
|
+
sessions: nextSessions,
|
|
762
|
+
activeSessionIndex: -1,
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
expiredSession: expiredSession ?? null,
|
|
766
|
+
error,
|
|
767
|
+
event: {
|
|
768
|
+
type: 'session-expired',
|
|
769
|
+
session: expiredSession,
|
|
770
|
+
error,
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
);
|
|
461
774
|
throw e;
|
|
462
775
|
} finally {
|
|
463
776
|
isRefreshing = false;
|
|
464
777
|
refreshPromise = null;
|
|
778
|
+
notifyAll();
|
|
465
779
|
}
|
|
466
780
|
})();
|
|
467
781
|
|
|
@@ -469,11 +783,15 @@ export function createAuth(config: AuthConfig = {}): Auth {
|
|
|
469
783
|
}
|
|
470
784
|
|
|
471
785
|
async function getToken(): Promise<string | undefined> {
|
|
472
|
-
if (permanentToken)
|
|
786
|
+
if (permanentToken) {
|
|
787
|
+
return permanentToken;
|
|
788
|
+
}
|
|
473
789
|
|
|
474
790
|
await initialize();
|
|
475
791
|
const session = getActiveSession();
|
|
476
|
-
if (!session)
|
|
792
|
+
if (!session) {
|
|
793
|
+
return undefined;
|
|
794
|
+
}
|
|
477
795
|
|
|
478
796
|
if (session.expires && isTokenExpired(session.expires)) {
|
|
479
797
|
try {
|
|
@@ -491,17 +809,34 @@ export function createAuth(config: AuthConfig = {}): Auth {
|
|
|
491
809
|
userId: string,
|
|
492
810
|
company?: string,
|
|
493
811
|
): Promise<AuthSession | null> {
|
|
812
|
+
await initialize();
|
|
494
813
|
const currentState = await loadAuthState();
|
|
495
814
|
const idx = currentState.sessions.findIndex(
|
|
496
|
-
(
|
|
815
|
+
(session) =>
|
|
816
|
+
session.user.id === userId && getSessionCompany(session) === company,
|
|
497
817
|
);
|
|
498
818
|
|
|
499
|
-
if (idx === -1)
|
|
819
|
+
if (idx === -1) {
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
500
822
|
|
|
501
|
-
|
|
502
|
-
|
|
823
|
+
await applyState(
|
|
824
|
+
{
|
|
825
|
+
sessions: currentState.sessions,
|
|
826
|
+
activeSessionIndex: idx,
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
clearExpiredSession: true,
|
|
830
|
+
error: null,
|
|
831
|
+
event: {
|
|
832
|
+
type: 'session-switched',
|
|
833
|
+
session: currentState.sessions[idx],
|
|
834
|
+
},
|
|
835
|
+
},
|
|
836
|
+
);
|
|
503
837
|
|
|
504
|
-
|
|
838
|
+
const session = getActiveSession();
|
|
839
|
+
if (session?.expires && isTokenExpired(session.expires)) {
|
|
505
840
|
await refresh();
|
|
506
841
|
return getActiveSession();
|
|
507
842
|
}
|
|
@@ -510,23 +845,272 @@ export function createAuth(config: AuthConfig = {}): Auth {
|
|
|
510
845
|
}
|
|
511
846
|
|
|
512
847
|
async function clearAllSessions(): Promise<void> {
|
|
513
|
-
await
|
|
514
|
-
|
|
848
|
+
await initialize();
|
|
849
|
+
await applyState(
|
|
850
|
+
{
|
|
851
|
+
sessions: [],
|
|
852
|
+
activeSessionIndex: -1,
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
clearExpiredSession: true,
|
|
856
|
+
error: null,
|
|
857
|
+
event: {
|
|
858
|
+
type: 'session-cleared',
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
);
|
|
515
862
|
}
|
|
516
863
|
|
|
517
864
|
async function deleteChallenge(challengeId: string): Promise<void> {
|
|
865
|
+
await initialize();
|
|
518
866
|
const currentState = await loadAuthState();
|
|
519
867
|
const idx = currentState.activeSessionIndex;
|
|
520
|
-
|
|
868
|
+
|
|
869
|
+
if (idx < 0 || idx >= currentState.sessions.length) {
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
521
872
|
|
|
522
873
|
const session = currentState.sessions[idx];
|
|
523
|
-
const updatedSessions =
|
|
874
|
+
const updatedSessions = cloneSessions(currentState.sessions);
|
|
524
875
|
updatedSessions[idx] = {
|
|
525
876
|
...session,
|
|
526
|
-
challenges:
|
|
877
|
+
challenges:
|
|
878
|
+
session.challenges?.filter((challenge: any) => challenge.id !== challengeId) ||
|
|
879
|
+
[],
|
|
527
880
|
};
|
|
528
881
|
|
|
529
|
-
await
|
|
882
|
+
await applyState(
|
|
883
|
+
{
|
|
884
|
+
sessions: updatedSessions,
|
|
885
|
+
activeSessionIndex: idx,
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
error: null,
|
|
889
|
+
},
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async function importToken(
|
|
894
|
+
token: string,
|
|
895
|
+
options: ImportTokenOptions = {},
|
|
896
|
+
): Promise<AuthSession> {
|
|
897
|
+
await initialize();
|
|
898
|
+
const user = await fetchUserForToken(token);
|
|
899
|
+
const sessionDuration = options.sessionDuration ?? 900;
|
|
900
|
+
|
|
901
|
+
return upsertSession(
|
|
902
|
+
{
|
|
903
|
+
user,
|
|
904
|
+
token,
|
|
905
|
+
refresh_token: '',
|
|
906
|
+
challenges: [],
|
|
907
|
+
expires: options.expires ?? Date.now() + 30 * 24 * 60 * 60 * 1000,
|
|
908
|
+
session_duration: sessionDuration,
|
|
909
|
+
company: options.company ?? user?.company,
|
|
910
|
+
},
|
|
911
|
+
'session-imported',
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
async function validateActiveSession(
|
|
916
|
+
options: ValidateSessionOptions = {},
|
|
917
|
+
): Promise<boolean> {
|
|
918
|
+
await initialize();
|
|
919
|
+
if (!getActiveSession()?.token) {
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const retryCount = options.retryCount ?? 1;
|
|
924
|
+
const retryDelayMs = options.retryDelayMs ?? 400;
|
|
925
|
+
let refreshAttempted = false;
|
|
926
|
+
|
|
927
|
+
for (let attempt = 0; attempt <= retryCount; attempt += 1) {
|
|
928
|
+
const activeSession = getActiveSession();
|
|
929
|
+
|
|
930
|
+
if (!activeSession?.token) {
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
try {
|
|
935
|
+
await fetchUserForToken(activeSession.token);
|
|
936
|
+
return true;
|
|
937
|
+
} catch (error) {
|
|
938
|
+
const status =
|
|
939
|
+
error instanceof ApiError
|
|
940
|
+
? error.status
|
|
941
|
+
: typeof error === 'object' &&
|
|
942
|
+
error &&
|
|
943
|
+
'status' in error &&
|
|
944
|
+
typeof (error as any).status === 'number'
|
|
945
|
+
? (error as any).status
|
|
946
|
+
: undefined;
|
|
947
|
+
|
|
948
|
+
if (status === 401 || status === 403) {
|
|
949
|
+
if (!refreshAttempted && activeSession.refresh_token) {
|
|
950
|
+
refreshAttempted = true;
|
|
951
|
+
|
|
952
|
+
try {
|
|
953
|
+
await refresh();
|
|
954
|
+
} catch {
|
|
955
|
+
await expireActiveSession();
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const refreshedSession = getActiveSession();
|
|
960
|
+
if (!refreshedSession?.token) {
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
try {
|
|
965
|
+
await fetchUserForToken(refreshedSession.token);
|
|
966
|
+
return true;
|
|
967
|
+
} catch {
|
|
968
|
+
await expireActiveSession();
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (attempt < retryCount) {
|
|
974
|
+
await sleep(retryDelayMs);
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
await expireActiveSession();
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (attempt < retryCount) {
|
|
982
|
+
await sleep(retryDelayMs);
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
async function syncActiveSessionUser(): Promise<AuthSession | null> {
|
|
994
|
+
await initialize();
|
|
995
|
+
const currentState = await loadAuthState();
|
|
996
|
+
const idx = currentState.activeSessionIndex;
|
|
997
|
+
const session = getActiveSessionFromState(currentState);
|
|
998
|
+
|
|
999
|
+
if (idx < 0 || !session?.token) {
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const user = await fetchUserForToken(session.token);
|
|
1004
|
+
const updatedSession: AuthSession = {
|
|
1005
|
+
...session,
|
|
1006
|
+
user: {
|
|
1007
|
+
...session.user,
|
|
1008
|
+
...user,
|
|
1009
|
+
},
|
|
1010
|
+
company: user?.company ?? getSessionCompany(session),
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
const updatedSessions = cloneSessions(currentState.sessions);
|
|
1014
|
+
updatedSessions[idx] = updatedSession;
|
|
1015
|
+
|
|
1016
|
+
await applyState(
|
|
1017
|
+
{
|
|
1018
|
+
sessions: updatedSessions,
|
|
1019
|
+
activeSessionIndex: idx,
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
error: null,
|
|
1023
|
+
event: {
|
|
1024
|
+
type: 'session-updated',
|
|
1025
|
+
session: updatedSession,
|
|
1026
|
+
},
|
|
1027
|
+
},
|
|
1028
|
+
);
|
|
1029
|
+
|
|
1030
|
+
return updatedSession;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
async function updateSession(
|
|
1034
|
+
userId: string,
|
|
1035
|
+
company: string | undefined,
|
|
1036
|
+
patch: SessionPatch,
|
|
1037
|
+
): Promise<AuthSession | null> {
|
|
1038
|
+
await initialize();
|
|
1039
|
+
const currentState = await loadAuthState();
|
|
1040
|
+
const idx = currentState.sessions.findIndex(
|
|
1041
|
+
(session) =>
|
|
1042
|
+
session.user.id === userId && getSessionCompany(session) === company,
|
|
1043
|
+
);
|
|
1044
|
+
|
|
1045
|
+
if (idx === -1) {
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const currentSession = currentState.sessions[idx];
|
|
1050
|
+
const updatedSession =
|
|
1051
|
+
typeof patch === 'function'
|
|
1052
|
+
? patch(currentSession)
|
|
1053
|
+
: {
|
|
1054
|
+
...currentSession,
|
|
1055
|
+
...patch,
|
|
1056
|
+
user: patch.user
|
|
1057
|
+
? {
|
|
1058
|
+
...currentSession.user,
|
|
1059
|
+
...patch.user,
|
|
1060
|
+
}
|
|
1061
|
+
: currentSession.user,
|
|
1062
|
+
company:
|
|
1063
|
+
patch.company ??
|
|
1064
|
+
patch.user?.company ??
|
|
1065
|
+
getSessionCompany(currentSession),
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
const updatedSessions = cloneSessions(currentState.sessions);
|
|
1069
|
+
updatedSessions[idx] = updatedSession;
|
|
1070
|
+
|
|
1071
|
+
await applyState(
|
|
1072
|
+
{
|
|
1073
|
+
sessions: updatedSessions,
|
|
1074
|
+
activeSessionIndex: currentState.activeSessionIndex,
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
error: null,
|
|
1078
|
+
event: {
|
|
1079
|
+
type: 'session-updated',
|
|
1080
|
+
session: updatedSession,
|
|
1081
|
+
},
|
|
1082
|
+
},
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1085
|
+
return updatedSession;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
async function expireActiveSession(): Promise<AuthRecoveryState> {
|
|
1089
|
+
await initialize();
|
|
1090
|
+
const currentState = await loadAuthState();
|
|
1091
|
+
const session = getActiveSessionFromState(currentState);
|
|
1092
|
+
|
|
1093
|
+
if (!session) {
|
|
1094
|
+
return buildRecoveryState(currentState);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const nextState: AuthState = {
|
|
1098
|
+
sessions: currentState.sessions.filter(
|
|
1099
|
+
(_entry, index) => index !== currentState.activeSessionIndex,
|
|
1100
|
+
),
|
|
1101
|
+
activeSessionIndex: -1,
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
await applyState(nextState, {
|
|
1105
|
+
expiredSession: session,
|
|
1106
|
+
error: null,
|
|
1107
|
+
event: {
|
|
1108
|
+
type: 'session-expired',
|
|
1109
|
+
session,
|
|
1110
|
+
},
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
return buildRecoveryState();
|
|
530
1114
|
}
|
|
531
1115
|
|
|
532
1116
|
return {
|
|
@@ -538,22 +1122,42 @@ export function createAuth(config: AuthConfig = {}): Auth {
|
|
|
538
1122
|
refresh,
|
|
539
1123
|
getToken,
|
|
540
1124
|
getActiveSession,
|
|
541
|
-
getSessions: () =>
|
|
1125
|
+
getSessions: () => cloneSessions(sessions),
|
|
542
1126
|
getSessionsByCompany: (company: string) =>
|
|
543
|
-
sessions.filter((
|
|
1127
|
+
sessions.filter((session) => getSessionCompany(session) === company),
|
|
1128
|
+
getState,
|
|
1129
|
+
getStatus: () => getStatus(),
|
|
1130
|
+
getRecoveryState: () => buildRecoveryState(),
|
|
544
1131
|
switchToSession,
|
|
545
1132
|
clearAllSessions,
|
|
546
1133
|
deleteChallenge,
|
|
1134
|
+
importToken,
|
|
1135
|
+
validateActiveSession,
|
|
1136
|
+
syncActiveSessionUser,
|
|
1137
|
+
updateSession,
|
|
1138
|
+
expireActiveSession,
|
|
547
1139
|
subscribe(listener: SessionListener): () => void {
|
|
548
1140
|
sessionListeners.push(listener);
|
|
549
1141
|
return () => {
|
|
550
|
-
sessionListeners = sessionListeners.filter((
|
|
1142
|
+
sessionListeners = sessionListeners.filter((entry) => entry !== listener);
|
|
551
1143
|
};
|
|
552
1144
|
},
|
|
553
1145
|
subscribeToErrors(listener: ErrorListener): () => void {
|
|
554
1146
|
errorListeners.push(listener);
|
|
555
1147
|
return () => {
|
|
556
|
-
errorListeners = errorListeners.filter((
|
|
1148
|
+
errorListeners = errorListeners.filter((entry) => entry !== listener);
|
|
1149
|
+
};
|
|
1150
|
+
},
|
|
1151
|
+
subscribeToState(listener: AuthStateListener): () => void {
|
|
1152
|
+
stateListeners.push(listener);
|
|
1153
|
+
return () => {
|
|
1154
|
+
stateListeners = stateListeners.filter((entry) => entry !== listener);
|
|
1155
|
+
};
|
|
1156
|
+
},
|
|
1157
|
+
subscribeToEvents(listener: AuthEventListener): () => void {
|
|
1158
|
+
eventListeners.push(listener);
|
|
1159
|
+
return () => {
|
|
1160
|
+
eventListeners = eventListeners.filter((entry) => entry !== listener);
|
|
557
1161
|
};
|
|
558
1162
|
},
|
|
559
1163
|
get baseUrl() {
|