@basictech/react 0.7.0 → 0.8.0-beta.1
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/.turbo/turbo-build.log +13 -12
- package/AUTH_IMPLEMENTATION_GUIDE.md +20 -18
- package/changelog.md +18 -2
- package/dist/index.d.mts +78 -21
- package/dist/index.d.ts +78 -21
- package/dist/index.js +1250 -779
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1246 -779
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/readme.md +17 -1
- package/src/AuthContext.tsx +202 -702
- package/src/config.ts +1 -19
- package/src/core/auth/AuthManager.ts +858 -0
- package/src/core/db/RemoteCollection.ts +30 -16
- package/src/core/db/index.ts +1 -1
- package/src/core/db/types.ts +13 -1
- package/src/index.ts +7 -1
- package/src/sync/index.ts +15 -29
- package/src/sync/syncProtocol.js +84 -22
- package/src/sync/tokenRegistry.ts +20 -0
- package/src/updater/updateMigrations.ts +3 -3
- package/src/updater/versionUpdater.ts +3 -10
- package/src/utils/normalizeClientId.ts +22 -0
- package/src/utils/resolveDid.ts +101 -0
- package/src/utils/schema.ts +3 -4
- package/src/utils/storage.ts +4 -1
package/src/AuthContext.tsx
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
import React, { createContext, useContext, useEffect, useState, useRef } from 'react'
|
|
2
|
-
import { jwtDecode } from 'jwt-decode'
|
|
3
2
|
|
|
4
3
|
import { BasicSync, initDexieExtensions } from './sync'
|
|
5
4
|
import { RemoteDB, DBMode, BasicDB } from './core/db'
|
|
5
|
+
import { AuthManager } from './core/auth/AuthManager'
|
|
6
|
+
import type { Token, User, AuthResult, GetTokenOptions, PdsEndpoints } from './core/auth/AuthManager'
|
|
6
7
|
|
|
7
8
|
import { log } from './config'
|
|
8
9
|
import { version as currentVersion } from '../package.json'
|
|
9
10
|
import { createVersionUpdater } from './updater/versionUpdater'
|
|
10
11
|
import { getMigrations } from './updater/updateMigrations'
|
|
11
|
-
import { BasicStorage, LocalStorageAdapter, STORAGE_KEYS
|
|
12
|
-
import { isDevelopment, checkForNewVersion,
|
|
13
|
-
import {
|
|
12
|
+
import { BasicStorage, LocalStorageAdapter, STORAGE_KEYS } from './utils/storage'
|
|
13
|
+
import { isDevelopment, checkForNewVersion, getSyncStatus } from './utils/network'
|
|
14
|
+
import { validateAndCheckSchema } from './utils/schema'
|
|
14
15
|
|
|
15
16
|
export type { BasicStorage, LocalStorageAdapter } from './utils/storage'
|
|
16
17
|
export type { DBMode, BasicDB, Collection } from './core/db'
|
|
18
|
+
export type { Token, User, AuthResult, GetTokenOptions, PdsEndpoints }
|
|
17
19
|
|
|
18
20
|
export type AuthConfig = {
|
|
19
21
|
scopes?: string | string[];
|
|
22
|
+
/** @deprecated Use pds_url instead */
|
|
20
23
|
server_url?: string;
|
|
24
|
+
/** PDS URL for auth and data (default: https://pds.basic.id) */
|
|
25
|
+
pds_url?: string;
|
|
26
|
+
/** Admin server URL for connect reporting (default: https://api.basic.tech) */
|
|
27
|
+
admin_url?: string;
|
|
21
28
|
ws_url?: string;
|
|
22
29
|
}
|
|
23
30
|
|
|
@@ -43,57 +50,23 @@ export type BasicProviderProps = {
|
|
|
43
50
|
|
|
44
51
|
const DEFAULT_AUTH_CONFIG = {
|
|
45
52
|
scopes: 'profile,email,app:admin',
|
|
46
|
-
|
|
53
|
+
pds_url: 'https://pds.basic.id',
|
|
54
|
+
admin_url: 'https://api.basic.tech',
|
|
47
55
|
ws_url: 'wss://pds.basic.id/ws'
|
|
48
56
|
} as const
|
|
49
57
|
|
|
50
58
|
|
|
51
|
-
|
|
52
|
-
basic_schema: any;
|
|
53
|
-
connect: (options: { access_token: string; ws_url?: string }) => void;
|
|
54
|
-
debugeroo: () => void;
|
|
55
|
-
collection: (name: string) => {
|
|
56
|
-
ref: {
|
|
57
|
-
toArray: () => Promise<any[]>;
|
|
58
|
-
count: () => Promise<number>;
|
|
59
|
-
};
|
|
60
|
-
};
|
|
61
|
-
[key: string]: any;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
enum DBStatus {
|
|
59
|
+
export enum DBStatus {
|
|
66
60
|
LOADING = "LOADING",
|
|
67
61
|
OFFLINE = "OFFLINE",
|
|
68
62
|
CONNECTING = "CONNECTING",
|
|
69
63
|
ONLINE = "ONLINE",
|
|
70
64
|
SYNCING = "SYNCING",
|
|
71
|
-
ERROR = "ERROR"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
email?: string,
|
|
77
|
-
id?: string,
|
|
78
|
-
primaryEmailAddress?: {
|
|
79
|
-
emailAddress: string
|
|
80
|
-
},
|
|
81
|
-
fullName?: string
|
|
82
|
-
}
|
|
83
|
-
type Token = {
|
|
84
|
-
access_token: string,
|
|
85
|
-
token_type: string,
|
|
86
|
-
expires_in: number,
|
|
87
|
-
refresh_token: string,
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Auth result type for signInWithCode
|
|
92
|
-
*/
|
|
93
|
-
export type AuthResult = {
|
|
94
|
-
success: boolean;
|
|
95
|
-
error?: string;
|
|
96
|
-
code?: string;
|
|
65
|
+
ERROR = "ERROR",
|
|
66
|
+
/** Sync reported an error but will retry (e.g. expired token). Used for status code 4 from dexie-syncable. */
|
|
67
|
+
ERROR_WILL_RETRY = "ERROR_WILL_RETRY",
|
|
68
|
+
/** Token expired; the SDK is refreshing and will reconnect automatically. */
|
|
69
|
+
ERROR_TOKEN_EXPIRED = "ERROR_TOKEN_EXPIRED"
|
|
97
70
|
}
|
|
98
71
|
|
|
99
72
|
/**
|
|
@@ -104,21 +77,30 @@ export type BasicContextType = {
|
|
|
104
77
|
isReady: boolean;
|
|
105
78
|
isSignedIn: boolean;
|
|
106
79
|
user: User | null;
|
|
107
|
-
|
|
80
|
+
/** The user's DID (Decentralized Identifier), extracted from the access token `sub` claim */
|
|
81
|
+
did: string | null;
|
|
82
|
+
/** Space-separated scope string from the access token */
|
|
83
|
+
scope: string | null;
|
|
84
|
+
/** Check if a specific scope is granted (e.g., hasScope('profile')) */
|
|
85
|
+
hasScope: (scope: string) => boolean;
|
|
86
|
+
/** Returns scopes that were requested but not granted in the current token */
|
|
87
|
+
missingScopes: () => string[];
|
|
88
|
+
|
|
108
89
|
// Auth actions (new camelCase naming)
|
|
109
90
|
signIn: () => Promise<void>;
|
|
91
|
+
signInWithHandle: (handle: string) => Promise<void>;
|
|
110
92
|
signOut: () => Promise<void>;
|
|
111
93
|
signInWithCode: (code: string, state?: string) => Promise<AuthResult>;
|
|
112
|
-
|
|
94
|
+
|
|
113
95
|
// Token management
|
|
114
|
-
getToken: () => Promise<string>;
|
|
96
|
+
getToken: (options?: GetTokenOptions) => Promise<string>;
|
|
115
97
|
getSignInUrl: (redirectUri?: string) => Promise<string>;
|
|
116
|
-
|
|
98
|
+
|
|
117
99
|
// DB access
|
|
118
100
|
db: BasicDB;
|
|
119
101
|
dbStatus: DBStatus;
|
|
120
102
|
dbMode: DBMode;
|
|
121
|
-
|
|
103
|
+
|
|
122
104
|
// Legacy aliases (deprecated - will be removed in future version)
|
|
123
105
|
/** @deprecated Use isReady instead */
|
|
124
106
|
isAuthReady: boolean;
|
|
@@ -143,21 +125,26 @@ export const BasicContext = createContext<BasicContextType>({
|
|
|
143
125
|
isReady: false,
|
|
144
126
|
isSignedIn: false,
|
|
145
127
|
user: null,
|
|
146
|
-
|
|
128
|
+
did: null,
|
|
129
|
+
scope: null,
|
|
130
|
+
hasScope: () => false,
|
|
131
|
+
missingScopes: () => [],
|
|
132
|
+
|
|
147
133
|
// Auth actions
|
|
148
134
|
signIn: () => Promise.resolve(),
|
|
135
|
+
signInWithHandle: () => Promise.resolve(),
|
|
149
136
|
signOut: () => Promise.resolve(),
|
|
150
137
|
signInWithCode: () => Promise.resolve({ success: false }),
|
|
151
|
-
|
|
138
|
+
|
|
152
139
|
// Token management
|
|
153
|
-
getToken: () => Promise.reject(new Error('no token')),
|
|
140
|
+
getToken: (_options?: GetTokenOptions) => Promise.reject(new Error('no token')),
|
|
154
141
|
getSignInUrl: () => Promise.resolve(""),
|
|
155
|
-
|
|
142
|
+
|
|
156
143
|
// DB access
|
|
157
144
|
db: noDb,
|
|
158
145
|
dbStatus: DBStatus.LOADING,
|
|
159
146
|
dbMode: 'sync',
|
|
160
|
-
|
|
147
|
+
|
|
161
148
|
// Legacy aliases
|
|
162
149
|
isAuthReady: false,
|
|
163
150
|
signin: () => Promise.resolve(),
|
|
@@ -172,6 +159,27 @@ type ErrorObject = {
|
|
|
172
159
|
message: string;
|
|
173
160
|
}
|
|
174
161
|
|
|
162
|
+
// Tracks the subset of AuthManager state that React effects depend on.
|
|
163
|
+
type AuthSnapshot = {
|
|
164
|
+
isSignedIn: boolean
|
|
165
|
+
hasToken: boolean
|
|
166
|
+
isAuthReady: boolean
|
|
167
|
+
user: User | null
|
|
168
|
+
did: string | null
|
|
169
|
+
tokenScope: string | null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function snapshotAuth(mgr: AuthManager): AuthSnapshot {
|
|
173
|
+
return {
|
|
174
|
+
isSignedIn: mgr.isSignedIn,
|
|
175
|
+
hasToken: !!mgr.token,
|
|
176
|
+
isAuthReady: mgr.isAuthReady,
|
|
177
|
+
user: mgr.user,
|
|
178
|
+
did: mgr.did,
|
|
179
|
+
tokenScope: mgr.tokenScope,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
175
183
|
export function BasicProvider({
|
|
176
184
|
children,
|
|
177
185
|
project_id: project_id_prop,
|
|
@@ -181,88 +189,103 @@ export function BasicProvider({
|
|
|
181
189
|
auth,
|
|
182
190
|
dbMode = 'sync'
|
|
183
191
|
}: BasicProviderProps) {
|
|
184
|
-
// Extract project_id from schema, fall back to prop for backward compatibility
|
|
185
192
|
const project_id = schema?.project_id || project_id_prop
|
|
186
|
-
|
|
187
|
-
const [isAuthReady, setIsAuthReady] = useState(false)
|
|
188
|
-
const [isSignedIn, setIsSignedIn] = useState<boolean>(false)
|
|
189
|
-
const [token, setToken] = useState<Token | null>(null)
|
|
190
|
-
const [user, setUser] = useState<User>({})
|
|
191
|
-
const [shouldConnect, setShouldConnect] = useState<boolean>(false)
|
|
192
|
-
const [isReady, setIsReady] = useState<boolean>(false)
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const syncRef = useRef<BasicSync | null>(null);
|
|
200
|
-
const remoteDbRef = useRef<RemoteDB | null>(null);
|
|
201
|
-
const storageAdapter = storage || new LocalStorageAdapter();
|
|
202
|
-
|
|
203
|
-
// Merge auth config with defaults
|
|
194
|
+
// Merge auth config with defaults (server_url is deprecated in favor of pds_url)
|
|
195
|
+
if (auth?.server_url && !auth?.pds_url) {
|
|
196
|
+
log('Warning: auth.server_url is deprecated, use auth.pds_url instead')
|
|
197
|
+
}
|
|
204
198
|
const authConfig = {
|
|
205
199
|
scopes: auth?.scopes || DEFAULT_AUTH_CONFIG.scopes,
|
|
206
|
-
|
|
200
|
+
pds_url: auth?.pds_url || auth?.server_url || DEFAULT_AUTH_CONFIG.pds_url,
|
|
201
|
+
admin_url: auth?.admin_url || DEFAULT_AUTH_CONFIG.admin_url,
|
|
207
202
|
ws_url: auth?.ws_url || DEFAULT_AUTH_CONFIG.ws_url
|
|
208
203
|
}
|
|
209
|
-
|
|
210
|
-
// Normalize scopes to space-separated string
|
|
211
|
-
const scopesString = Array.isArray(authConfig.scopes)
|
|
212
|
-
? authConfig.scopes.join(' ')
|
|
213
|
-
: authConfig.scopes;
|
|
214
204
|
|
|
215
|
-
|
|
216
|
-
|
|
205
|
+
const scopesString = Array.isArray(authConfig.scopes)
|
|
206
|
+
? authConfig.scopes.join(' ')
|
|
207
|
+
: authConfig.scopes
|
|
208
|
+
|
|
209
|
+
const storageRef = useRef<BasicStorage>(storage || new LocalStorageAdapter())
|
|
210
|
+
const storageAdapter = storageRef.current
|
|
211
|
+
|
|
212
|
+
// --- AuthManager (stable instance held in a ref) ---
|
|
213
|
+
const [authState, setAuthState] = useState<AuthSnapshot>({
|
|
214
|
+
isSignedIn: false,
|
|
215
|
+
hasToken: false,
|
|
216
|
+
isAuthReady: false,
|
|
217
|
+
user: null,
|
|
218
|
+
did: null,
|
|
219
|
+
tokenScope: null,
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const authRef = useRef<AuthManager>(null!)
|
|
223
|
+
if (!authRef.current) {
|
|
224
|
+
authRef.current = new AuthManager(
|
|
225
|
+
{
|
|
226
|
+
projectId: project_id,
|
|
227
|
+
scopes: scopesString,
|
|
228
|
+
pdsUrl: authConfig.pds_url,
|
|
229
|
+
adminUrl: authConfig.admin_url,
|
|
230
|
+
debug,
|
|
231
|
+
},
|
|
232
|
+
storageAdapter,
|
|
233
|
+
() => setAuthState(snapshotAuth(authRef.current)),
|
|
234
|
+
)
|
|
235
|
+
}
|
|
217
236
|
|
|
218
|
-
|
|
237
|
+
// --- DB state (stays in React) ---
|
|
238
|
+
const syncRef = useRef<BasicSync | null>(null)
|
|
239
|
+
const remoteDbRef = useRef<RemoteDB | null>(null)
|
|
240
|
+
const [shouldConnect, setShouldConnect] = useState(false)
|
|
241
|
+
const [dbStatus, setDbStatus] = useState<DBStatus>(DBStatus.OFFLINE)
|
|
242
|
+
const [isReady, setIsReady] = useState(false)
|
|
243
|
+
const [error, setError] = useState<ErrorObject | null>(null)
|
|
219
244
|
|
|
220
|
-
const
|
|
245
|
+
const isDevMode = () => isDevelopment(debug)
|
|
221
246
|
|
|
247
|
+
// --- Mount: version updater + auth init + DB init ---
|
|
222
248
|
useEffect(() => {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
log('Retry refresh failed:', error)
|
|
234
|
-
})
|
|
235
|
-
}
|
|
249
|
+
// Version updater (SDK migration, not auth-related)
|
|
250
|
+
const runVersionUpdater = async () => {
|
|
251
|
+
try {
|
|
252
|
+
const versionUpdater = createVersionUpdater(storageAdapter, currentVersion, getMigrations())
|
|
253
|
+
const updateResult = await versionUpdater.checkAndUpdate()
|
|
254
|
+
|
|
255
|
+
if (updateResult.updated) {
|
|
256
|
+
log(`App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`)
|
|
257
|
+
} else {
|
|
258
|
+
log(`App version ${updateResult.toVersion} is current`)
|
|
236
259
|
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
log('Version update failed:', error)
|
|
237
262
|
}
|
|
238
263
|
}
|
|
239
264
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
setIsOnline(false)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
window.addEventListener('online', handleOnline)
|
|
246
|
-
window.addEventListener('offline', handleOffline)
|
|
265
|
+
runVersionUpdater()
|
|
266
|
+
authRef.current.initialize()
|
|
247
267
|
|
|
248
|
-
return ()
|
|
249
|
-
|
|
250
|
-
window.removeEventListener('offline', handleOffline)
|
|
251
|
-
}
|
|
252
|
-
}, [pendingRefresh, token])
|
|
268
|
+
return authRef.current.setupNetworkListeners()
|
|
269
|
+
}, [])
|
|
253
270
|
|
|
271
|
+
// --- DB init (separate mount effect) ---
|
|
254
272
|
useEffect(() => {
|
|
255
273
|
async function initSyncDb(options: { shouldConnect: boolean }) {
|
|
256
274
|
if (!syncRef.current) {
|
|
257
275
|
log('Initializing Basic Sync DB')
|
|
258
|
-
|
|
259
|
-
// Initialize Dexie extensions before creating BasicSync
|
|
276
|
+
|
|
260
277
|
await initDexieExtensions()
|
|
261
|
-
|
|
278
|
+
|
|
262
279
|
syncRef.current = new BasicSync('basicdb', { schema: schema });
|
|
263
280
|
|
|
264
|
-
syncRef.current.syncable.on('statusChanged', (status: number
|
|
265
|
-
|
|
281
|
+
syncRef.current.syncable.on('statusChanged', (status: number) => {
|
|
282
|
+
const newStatus = getSyncStatus(status) as DBStatus
|
|
283
|
+
setDbStatus(newStatus)
|
|
284
|
+
|
|
285
|
+
if (newStatus === DBStatus.ERROR_WILL_RETRY) {
|
|
286
|
+
log('Sync entered ERROR_WILL_RETRY - proactively refreshing token')
|
|
287
|
+
authRef.current.getToken({ forceRefresh: true }).catch(() => {})
|
|
288
|
+
}
|
|
266
289
|
})
|
|
267
290
|
|
|
268
291
|
if (options.shouldConnect) {
|
|
@@ -289,15 +312,14 @@ export function BasicProvider({
|
|
|
289
312
|
|
|
290
313
|
log('Initializing Basic Remote DB')
|
|
291
314
|
remoteDbRef.current = new RemoteDB({
|
|
292
|
-
serverUrl: authConfig.
|
|
315
|
+
serverUrl: authConfig.pds_url,
|
|
293
316
|
projectId: project_id,
|
|
294
|
-
getToken: getToken,
|
|
317
|
+
getToken: (opts) => authRef.current.getToken(opts),
|
|
295
318
|
schema: schema,
|
|
296
319
|
debug: debug,
|
|
297
320
|
onAuthError: (error) => {
|
|
298
321
|
log('RemoteDB auth error:', error)
|
|
299
|
-
|
|
300
|
-
signout()
|
|
322
|
+
handleSignOut()
|
|
301
323
|
}
|
|
302
324
|
})
|
|
303
325
|
setDbStatus(DBStatus.ONLINE)
|
|
@@ -311,7 +333,7 @@ export function BasicProvider({
|
|
|
311
333
|
if (!result.isValid) {
|
|
312
334
|
let errorMessage = ''
|
|
313
335
|
if (result.errors) {
|
|
314
|
-
result.errors.forEach((error, index) => {
|
|
336
|
+
result.errors.forEach((error: any, index: number) => {
|
|
315
337
|
errorMessage += `${index + 1}: ${error.message} - at ${error.instancePath}\n`
|
|
316
338
|
})
|
|
317
339
|
}
|
|
@@ -324,15 +346,17 @@ export function BasicProvider({
|
|
|
324
346
|
return null
|
|
325
347
|
}
|
|
326
348
|
|
|
327
|
-
// Initialize the appropriate DB based on mode
|
|
328
349
|
if (dbMode === 'remote') {
|
|
329
350
|
initRemoteDb()
|
|
330
351
|
} else {
|
|
331
|
-
// Sync mode
|
|
332
352
|
if (result.schemaStatus.valid) {
|
|
333
353
|
await initSyncDb({ shouldConnect: true })
|
|
334
354
|
} else {
|
|
335
|
-
|
|
355
|
+
if (result.schemaStatus.status === 'unpublished') {
|
|
356
|
+
log('Schema not published yet (version 0) - sync is disabled. Publish your schema to enable sync.')
|
|
357
|
+
} else {
|
|
358
|
+
log('Schema is invalid!', result.schemaStatus)
|
|
359
|
+
}
|
|
336
360
|
await initSyncDb({ shouldConnect: false })
|
|
337
361
|
}
|
|
338
362
|
}
|
|
@@ -343,7 +367,6 @@ export function BasicProvider({
|
|
|
343
367
|
if (schema) {
|
|
344
368
|
checkSchema()
|
|
345
369
|
} else {
|
|
346
|
-
// No schema - still initialize remote DB if in remote mode
|
|
347
370
|
if (dbMode === 'remote' && project_id) {
|
|
348
371
|
initRemoteDb()
|
|
349
372
|
} else {
|
|
@@ -352,270 +375,41 @@ export function BasicProvider({
|
|
|
352
375
|
}
|
|
353
376
|
}, []);
|
|
354
377
|
|
|
378
|
+
// --- Connect sync DB when auth is ready ---
|
|
355
379
|
useEffect(() => {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const tok = await getToken()
|
|
359
|
-
if (!tok) {
|
|
360
|
-
log('no token found')
|
|
361
|
-
return
|
|
362
|
-
}
|
|
380
|
+
if (authState.hasToken && syncRef.current && authState.isSignedIn && shouldConnect) {
|
|
381
|
+
log('connecting to db...')
|
|
363
382
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
383
|
+
syncRef.current?.connect({
|
|
384
|
+
getToken: (opts?: GetTokenOptions) => authRef.current.getToken(opts),
|
|
385
|
+
ws_url: authConfig.ws_url
|
|
386
|
+
})
|
|
387
|
+
.catch((e: any) => {
|
|
388
|
+
log('error connecting to db', e)
|
|
369
389
|
})
|
|
370
|
-
.catch((e) => {
|
|
371
|
-
log('error connecting to db', e)
|
|
372
|
-
})
|
|
373
|
-
}
|
|
374
390
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}, [isSignedIn, shouldConnect])
|
|
391
|
+
}, [authState.isSignedIn, authState.hasToken, shouldConnect])
|
|
378
392
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
// Check if server URL has changed - if so, clear tokens
|
|
384
|
-
const storedServerUrl = await storageAdapter.get(STORAGE_KEYS.SERVER_URL)
|
|
385
|
-
if (storedServerUrl && storedServerUrl !== authConfig.server_url) {
|
|
386
|
-
log('Server URL changed, clearing stored tokens')
|
|
387
|
-
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
|
|
388
|
-
await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
|
|
389
|
-
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
|
|
390
|
-
await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI)
|
|
391
|
-
clearCookie('basic_token')
|
|
392
|
-
clearCookie('basic_access_token')
|
|
393
|
-
}
|
|
394
|
-
await storageAdapter.set(STORAGE_KEYS.SERVER_URL, authConfig.server_url)
|
|
395
|
-
|
|
396
|
-
try {
|
|
397
|
-
const versionUpdater = createVersionUpdater(storageAdapter, currentVersion, getMigrations())
|
|
398
|
-
const updateResult = await versionUpdater.checkAndUpdate()
|
|
399
|
-
|
|
400
|
-
if (updateResult.updated) {
|
|
401
|
-
log(`App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`)
|
|
402
|
-
} else {
|
|
403
|
-
log(`App version ${updateResult.toVersion} is current`)
|
|
404
|
-
}
|
|
405
|
-
} catch (error) {
|
|
406
|
-
log('Version update failed:', error)
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
try {
|
|
410
|
-
if (window.location.search.includes('code')) {
|
|
411
|
-
let code = window.location?.search?.split('code=')[1]?.split('&')[0]
|
|
412
|
-
if (!code) return
|
|
413
|
-
|
|
414
|
-
const state = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE)
|
|
415
|
-
const urlState = window.location.search.split('state=')[1]?.split('&')[0]
|
|
416
|
-
if (!state || state !== urlState) {
|
|
417
|
-
log('error: auth state does not match')
|
|
418
|
-
setIsAuthReady(true)
|
|
419
|
-
|
|
420
|
-
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
|
|
421
|
-
cleanOAuthParams()
|
|
422
|
-
return
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
|
|
426
|
-
cleanOAuthParams()
|
|
427
|
-
|
|
428
|
-
fetchToken(code, false).catch((error) => {
|
|
429
|
-
log('Error fetching token:', error)
|
|
430
|
-
})
|
|
431
|
-
} else {
|
|
432
|
-
const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
|
|
433
|
-
if (refreshToken) {
|
|
434
|
-
log('Found refresh token in storage, attempting to refresh access token')
|
|
435
|
-
fetchToken(refreshToken, true).catch((error) => {
|
|
436
|
-
log('Error fetching refresh token:', error)
|
|
437
|
-
})
|
|
438
|
-
} else {
|
|
439
|
-
let cookie_token = getCookie('basic_token')
|
|
440
|
-
if (cookie_token !== '') {
|
|
441
|
-
const tokenData = JSON.parse(cookie_token)
|
|
442
|
-
setToken(tokenData)
|
|
443
|
-
if (tokenData.refresh_token) {
|
|
444
|
-
await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, tokenData.refresh_token)
|
|
445
|
-
}
|
|
446
|
-
} else {
|
|
447
|
-
const cachedUserInfo = await storageAdapter.get(STORAGE_KEYS.USER_INFO)
|
|
448
|
-
if (cachedUserInfo) {
|
|
449
|
-
try {
|
|
450
|
-
const userData = JSON.parse(cachedUserInfo)
|
|
451
|
-
setUser(userData)
|
|
452
|
-
setIsSignedIn(true)
|
|
453
|
-
log('Loaded cached user info for offline mode')
|
|
454
|
-
} catch (error) {
|
|
455
|
-
log('Error parsing cached user info:', error)
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
setIsAuthReady(true)
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
} catch (e) {
|
|
464
|
-
log('error getting token', e)
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
initializeAuth()
|
|
469
|
-
}, [])
|
|
470
|
-
|
|
471
|
-
useEffect(() => {
|
|
472
|
-
async function fetchUser(acc_token: string) {
|
|
473
|
-
console.info('fetching user')
|
|
393
|
+
// --- Sign out (auth cleanup + sync teardown) ---
|
|
394
|
+
const handleSignOut = async () => {
|
|
395
|
+
await authRef.current.signOut()
|
|
396
|
+
if (syncRef.current) {
|
|
474
397
|
try {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
}
|
|
480
|
-
})
|
|
481
|
-
|
|
482
|
-
if (!response.ok) {
|
|
483
|
-
throw new Error(`Failed to fetch user info: ${response.status}`)
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const user = await response.json()
|
|
487
|
-
|
|
488
|
-
if (user.error) {
|
|
489
|
-
log('error fetching user', user.error)
|
|
490
|
-
throw new Error(`User info error: ${user.error}`)
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if (token?.refresh_token) {
|
|
494
|
-
await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token)
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
await storageAdapter.set(STORAGE_KEYS.USER_INFO, JSON.stringify(user))
|
|
498
|
-
log('Cached user info in storage')
|
|
499
|
-
|
|
500
|
-
setCookie('basic_access_token', token?.access_token || '', { httpOnly: false });
|
|
501
|
-
setCookie('basic_token', JSON.stringify(token));
|
|
502
|
-
|
|
503
|
-
setUser(user)
|
|
504
|
-
setIsSignedIn(true)
|
|
505
|
-
setIsAuthReady(true)
|
|
398
|
+
await syncRef.current.close()
|
|
399
|
+
await syncRef.current.delete({ disableAutoOpen: false })
|
|
400
|
+
syncRef.current = null
|
|
401
|
+
window?.location?.reload()
|
|
506
402
|
} catch (error) {
|
|
507
|
-
|
|
508
|
-
// Don't clear tokens here - may be temporary network issue
|
|
509
|
-
setIsAuthReady(true)
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
async function checkToken() {
|
|
514
|
-
if (!token) {
|
|
515
|
-
log('error: no user token found')
|
|
516
|
-
|
|
517
|
-
setIsAuthReady(true)
|
|
518
|
-
return
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const decoded = jwtDecode(token?.access_token)
|
|
522
|
-
// Add 5 second buffer to prevent edge cases
|
|
523
|
-
const expirationBuffer = 5
|
|
524
|
-
const isExpired = decoded.exp && decoded.exp < (Date.now() / 1000) + expirationBuffer
|
|
525
|
-
|
|
526
|
-
if (isExpired) {
|
|
527
|
-
log('token is expired - refreshing ...')
|
|
528
|
-
const refreshToken = token?.refresh_token
|
|
529
|
-
if (!refreshToken) {
|
|
530
|
-
log('Error: No refresh token available for expired token')
|
|
531
|
-
setIsAuthReady(true)
|
|
532
|
-
return
|
|
533
|
-
}
|
|
534
|
-
try {
|
|
535
|
-
const newToken = await fetchToken(refreshToken, true)
|
|
536
|
-
fetchUser(newToken?.access_token || '')
|
|
537
|
-
} catch (error) {
|
|
538
|
-
log('Failed to refresh token in checkToken:', error)
|
|
539
|
-
|
|
540
|
-
if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
|
|
541
|
-
log('Network issue - continuing with expired token until online')
|
|
542
|
-
fetchUser(token?.access_token || '')
|
|
543
|
-
} else {
|
|
544
|
-
setIsAuthReady(true)
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
} else {
|
|
548
|
-
fetchUser(token?.access_token || '')
|
|
403
|
+
console.error('Error during database cleanup:', error)
|
|
549
404
|
}
|
|
550
405
|
}
|
|
551
|
-
|
|
552
|
-
if (token) {
|
|
553
|
-
checkToken()
|
|
554
|
-
}
|
|
555
|
-
}, [token])
|
|
556
|
-
|
|
557
|
-
const getSignInLink = async (redirectUri?: string) => {
|
|
558
|
-
try {
|
|
559
|
-
log('getting sign in link...')
|
|
560
|
-
|
|
561
|
-
if (!project_id) {
|
|
562
|
-
throw new Error('Project ID is required to generate sign-in link')
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const randomState = Math.random().toString(36).substring(6);
|
|
566
|
-
await storageAdapter.set(STORAGE_KEYS.AUTH_STATE, randomState)
|
|
567
|
-
|
|
568
|
-
const redirectUrl = redirectUri || window.location.href
|
|
569
|
-
|
|
570
|
-
if (!redirectUrl || (!redirectUrl.startsWith('http://') && !redirectUrl.startsWith('https://'))) {
|
|
571
|
-
throw new Error('Invalid redirect URI provided')
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Store redirect_uri for token exchange
|
|
575
|
-
await storageAdapter.set(STORAGE_KEYS.REDIRECT_URI, redirectUrl)
|
|
576
|
-
log('Stored redirect_uri for token exchange:', redirectUrl)
|
|
577
|
-
|
|
578
|
-
let baseUrl = `${authConfig.server_url}/auth/authorize`
|
|
579
|
-
baseUrl += `?client_id=${project_id}`
|
|
580
|
-
baseUrl += `&redirect_uri=${encodeURIComponent(redirectUrl)}`
|
|
581
|
-
baseUrl += `&response_type=code`
|
|
582
|
-
baseUrl += `&scope=${encodeURIComponent(scopesString)}`
|
|
583
|
-
baseUrl += `&state=${randomState}`
|
|
584
|
-
|
|
585
|
-
log('Generated sign-in link successfully with scopes:', scopesString)
|
|
586
|
-
return baseUrl;
|
|
587
|
-
|
|
588
|
-
} catch (error) {
|
|
589
|
-
log('Error generating sign-in link:', error)
|
|
590
|
-
throw error
|
|
591
|
-
}
|
|
592
406
|
}
|
|
593
407
|
|
|
594
|
-
|
|
408
|
+
// --- Sign in wrappers (add dev-mode error display) ---
|
|
409
|
+
const handleSignIn = async () => {
|
|
595
410
|
try {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if (!project_id) {
|
|
599
|
-
log('Error: project_id is required for sign-in')
|
|
600
|
-
throw new Error('Project ID is required for authentication')
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
const signInLink = await getSignInLink()
|
|
604
|
-
log('Generated sign-in link:', signInLink)
|
|
605
|
-
|
|
606
|
-
// Validate URL format (supports https://, http://, and custom URI schemes)
|
|
607
|
-
try {
|
|
608
|
-
new URL(signInLink)
|
|
609
|
-
} catch {
|
|
610
|
-
log('Error: Invalid sign-in link generated')
|
|
611
|
-
throw new Error('Failed to generate valid sign-in URL')
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
window.location.href = signInLink
|
|
615
|
-
|
|
411
|
+
await authRef.current.signIn()
|
|
616
412
|
} catch (error) {
|
|
617
|
-
log('Error during sign-in:', error)
|
|
618
|
-
|
|
619
413
|
if (isDevMode()) {
|
|
620
414
|
setError({
|
|
621
415
|
code: 'signin_error',
|
|
@@ -623,325 +417,26 @@ export function BasicProvider({
|
|
|
623
417
|
message: (error as Error).message || 'An error occurred during sign-in. Please try again.'
|
|
624
418
|
})
|
|
625
419
|
}
|
|
626
|
-
|
|
627
420
|
throw error
|
|
628
421
|
}
|
|
629
422
|
}
|
|
630
423
|
|
|
631
|
-
const
|
|
424
|
+
const handleSignInWithHandle = async (handle: string) => {
|
|
632
425
|
try {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
if (!code || typeof code !== 'string') {
|
|
636
|
-
return { success: false, error: 'Invalid authorization code' }
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
if (state) {
|
|
640
|
-
const storedState = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE)
|
|
641
|
-
if (storedState && storedState !== state) {
|
|
642
|
-
log('State parameter mismatch:', { provided: state, stored: storedState })
|
|
643
|
-
return { success: false, error: 'State parameter mismatch' }
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
|
|
648
|
-
cleanOAuthParams()
|
|
649
|
-
|
|
650
|
-
const token = await fetchToken(code, false)
|
|
651
|
-
|
|
652
|
-
if (token) {
|
|
653
|
-
log('signinWithCode successful')
|
|
654
|
-
return { success: true }
|
|
655
|
-
} else {
|
|
656
|
-
return { success: false, error: 'Failed to exchange code for token' }
|
|
657
|
-
}
|
|
426
|
+
await authRef.current.signInWithHandle(handle)
|
|
658
427
|
} catch (error) {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
const signout = async () => {
|
|
668
|
-
log('signing out!')
|
|
669
|
-
setUser({})
|
|
670
|
-
setIsSignedIn(false)
|
|
671
|
-
setToken(null)
|
|
672
|
-
|
|
673
|
-
clearCookie('basic_token');
|
|
674
|
-
clearCookie('basic_access_token');
|
|
675
|
-
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
|
|
676
|
-
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
|
|
677
|
-
await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
|
|
678
|
-
await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI)
|
|
679
|
-
await storageAdapter.remove(STORAGE_KEYS.SERVER_URL)
|
|
680
|
-
if (syncRef.current) {
|
|
681
|
-
(async () => {
|
|
682
|
-
try {
|
|
683
|
-
await syncRef.current?.close()
|
|
684
|
-
await syncRef.current?.delete({ disableAutoOpen: false })
|
|
685
|
-
syncRef.current = null
|
|
686
|
-
window?.location?.reload()
|
|
687
|
-
} catch (error) {
|
|
688
|
-
console.error('Error during database cleanup:', error)
|
|
689
|
-
}
|
|
690
|
-
})()
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
const getToken = async (): Promise<string> => {
|
|
695
|
-
log('getting token...')
|
|
696
|
-
|
|
697
|
-
if (!token) {
|
|
698
|
-
// Try to recover from storage refresh token
|
|
699
|
-
const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
|
|
700
|
-
if (refreshToken) {
|
|
701
|
-
log('No token in memory, attempting to refresh from storage')
|
|
702
|
-
|
|
703
|
-
// Check if refresh is already in progress
|
|
704
|
-
if (refreshPromiseRef.current) {
|
|
705
|
-
log('Token refresh already in progress, waiting...')
|
|
706
|
-
try {
|
|
707
|
-
const newToken = await refreshPromiseRef.current
|
|
708
|
-
if (newToken?.access_token) {
|
|
709
|
-
return newToken.access_token
|
|
710
|
-
}
|
|
711
|
-
} catch (error) {
|
|
712
|
-
log('In-flight refresh failed:', error)
|
|
713
|
-
throw error
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
try {
|
|
718
|
-
const newToken = await fetchToken(refreshToken, true)
|
|
719
|
-
if (newToken?.access_token) {
|
|
720
|
-
return newToken.access_token
|
|
721
|
-
}
|
|
722
|
-
} catch (error) {
|
|
723
|
-
log('Failed to refresh token from storage:', error)
|
|
724
|
-
|
|
725
|
-
if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
|
|
726
|
-
log('Network issue - continuing with potentially expired token')
|
|
727
|
-
const lastToken = localStorage.getItem('basic_access_token')
|
|
728
|
-
if (lastToken) {
|
|
729
|
-
return lastToken
|
|
730
|
-
}
|
|
731
|
-
throw new Error('Network offline - authentication will be retried when online')
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
throw new Error('Authentication expired. Please sign in again.')
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
log('no token found')
|
|
738
|
-
throw new Error('no token found')
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
const decoded = jwtDecode(token?.access_token)
|
|
742
|
-
// Add 5 second buffer to prevent edge cases where token expires during request
|
|
743
|
-
const expirationBuffer = 5
|
|
744
|
-
const isExpired = decoded.exp && decoded.exp < (Date.now() / 1000) + expirationBuffer
|
|
745
|
-
|
|
746
|
-
if (isExpired) {
|
|
747
|
-
log('token is expired - refreshing ...')
|
|
748
|
-
|
|
749
|
-
// Check if refresh is already in progress
|
|
750
|
-
if (refreshPromiseRef.current) {
|
|
751
|
-
log('Token refresh already in progress, waiting...')
|
|
752
|
-
try {
|
|
753
|
-
const newToken = await refreshPromiseRef.current
|
|
754
|
-
return newToken?.access_token || ''
|
|
755
|
-
} catch (error) {
|
|
756
|
-
log('In-flight refresh failed:', error)
|
|
757
|
-
|
|
758
|
-
if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
|
|
759
|
-
log('Network issue - using expired token until network is restored')
|
|
760
|
-
return token.access_token
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
throw error
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
const refreshToken = token?.refresh_token || await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
|
|
768
|
-
if (refreshToken) {
|
|
769
|
-
try {
|
|
770
|
-
const newToken = await fetchToken(refreshToken, true)
|
|
771
|
-
return newToken?.access_token || ''
|
|
772
|
-
} catch (error) {
|
|
773
|
-
log('Failed to refresh expired token:', error)
|
|
774
|
-
|
|
775
|
-
if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
|
|
776
|
-
log('Network issue - using expired token until network is restored')
|
|
777
|
-
return token.access_token
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
throw new Error('Authentication expired. Please sign in again.')
|
|
781
|
-
}
|
|
782
|
-
} else {
|
|
783
|
-
throw new Error('no refresh token available')
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
return token?.access_token || ''
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
const fetchToken = async (codeOrRefreshToken: string, isRefreshToken: boolean = false): Promise<Token | null> => {
|
|
791
|
-
// Validate input
|
|
792
|
-
if (!codeOrRefreshToken || codeOrRefreshToken.trim() === '') {
|
|
793
|
-
const errorMsg = isRefreshToken ? 'Refresh token is empty or undefined' : 'Authorization code is empty or undefined'
|
|
794
|
-
log('Error:', errorMsg)
|
|
795
|
-
throw new Error(errorMsg)
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// If this is a refresh token request and one is already in progress, return that promise
|
|
799
|
-
if (isRefreshToken && refreshPromiseRef.current) {
|
|
800
|
-
log('Reusing in-flight refresh token request')
|
|
801
|
-
return refreshPromiseRef.current
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
// Create new promise for this refresh attempt
|
|
805
|
-
const refreshPromise = (async (): Promise<Token | null> => {
|
|
806
|
-
try {
|
|
807
|
-
if (!isOnline) {
|
|
808
|
-
log('Network is offline, marking refresh as pending')
|
|
809
|
-
setPendingRefresh(true)
|
|
810
|
-
throw new Error('Network offline - refresh will be retried when online')
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
let requestBody: any
|
|
814
|
-
|
|
815
|
-
if (isRefreshToken) {
|
|
816
|
-
// Refresh token request
|
|
817
|
-
requestBody = {
|
|
818
|
-
grant_type: 'refresh_token',
|
|
819
|
-
refresh_token: codeOrRefreshToken
|
|
820
|
-
}
|
|
821
|
-
// Include client_id if available for validation
|
|
822
|
-
if (project_id) {
|
|
823
|
-
requestBody.client_id = project_id
|
|
824
|
-
}
|
|
825
|
-
} else {
|
|
826
|
-
// Authorization code exchange
|
|
827
|
-
requestBody = {
|
|
828
|
-
grant_type: 'authorization_code',
|
|
829
|
-
code: codeOrRefreshToken
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// Retrieve stored redirect_uri (required by OAuth2 spec)
|
|
833
|
-
const storedRedirectUri = await storageAdapter.get(STORAGE_KEYS.REDIRECT_URI)
|
|
834
|
-
if (storedRedirectUri) {
|
|
835
|
-
requestBody.redirect_uri = storedRedirectUri
|
|
836
|
-
log('Including redirect_uri in token exchange:', storedRedirectUri)
|
|
837
|
-
} else {
|
|
838
|
-
log('Warning: No redirect_uri found in storage for token exchange')
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
// Include client_id for validation
|
|
842
|
-
if (project_id) {
|
|
843
|
-
requestBody.client_id = project_id
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
log('Token exchange request body:', { ...requestBody, refresh_token: isRefreshToken ? '[REDACTED]' : undefined, code: !isRefreshToken ? '[REDACTED]' : undefined })
|
|
848
|
-
|
|
849
|
-
const token = await fetch(`${authConfig.server_url}/auth/token`, {
|
|
850
|
-
method: 'POST',
|
|
851
|
-
headers: {
|
|
852
|
-
'Content-Type': 'application/json'
|
|
853
|
-
},
|
|
854
|
-
body: JSON.stringify(requestBody)
|
|
428
|
+
if (isDevMode()) {
|
|
429
|
+
setError({
|
|
430
|
+
code: 'signin_error',
|
|
431
|
+
title: 'Sign-in Failed',
|
|
432
|
+
message: (error as Error).message || 'An error occurred during sign-in. Please try again.'
|
|
855
433
|
})
|
|
856
|
-
.then(response => response.json())
|
|
857
|
-
.catch(error => {
|
|
858
|
-
log('Network error fetching token:', error)
|
|
859
|
-
if (!isOnline) {
|
|
860
|
-
setPendingRefresh(true)
|
|
861
|
-
throw new Error('Network offline - refresh will be retried when online')
|
|
862
|
-
}
|
|
863
|
-
throw new Error('Network error during token refresh')
|
|
864
|
-
})
|
|
865
|
-
|
|
866
|
-
if (token.error) {
|
|
867
|
-
log('error fetching token', token.error)
|
|
868
|
-
|
|
869
|
-
if (token.error.includes('network') || token.error.includes('timeout')) {
|
|
870
|
-
setPendingRefresh(true)
|
|
871
|
-
throw new Error('Network issue - refresh will be retried when online')
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
|
|
875
|
-
await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
|
|
876
|
-
await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI)
|
|
877
|
-
await storageAdapter.remove(STORAGE_KEYS.SERVER_URL)
|
|
878
|
-
clearCookie('basic_token');
|
|
879
|
-
clearCookie('basic_access_token');
|
|
880
|
-
|
|
881
|
-
setUser({})
|
|
882
|
-
setIsSignedIn(false)
|
|
883
|
-
setToken(null)
|
|
884
|
-
setIsAuthReady(true)
|
|
885
|
-
|
|
886
|
-
throw new Error(`Token refresh failed: ${token.error}`)
|
|
887
|
-
} else {
|
|
888
|
-
setToken(token)
|
|
889
|
-
setPendingRefresh(false)
|
|
890
|
-
|
|
891
|
-
if (token.refresh_token) {
|
|
892
|
-
await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token)
|
|
893
|
-
log('Updated refresh token in storage')
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
// Clean up redirect_uri after successful token exchange
|
|
897
|
-
if (!isRefreshToken) {
|
|
898
|
-
await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI)
|
|
899
|
-
log('Cleaned up redirect_uri from storage after successful exchange')
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
setCookie('basic_access_token', token.access_token, { httpOnly: false });
|
|
903
|
-
setCookie('basic_token', JSON.stringify(token));
|
|
904
|
-
log('Updated access token and full token in cookies')
|
|
905
|
-
}
|
|
906
|
-
return token
|
|
907
|
-
} catch (error) {
|
|
908
|
-
log('Token refresh error:', error)
|
|
909
|
-
|
|
910
|
-
if (!(error as Error).message.includes('offline') && !(error as Error).message.includes('Network')) {
|
|
911
|
-
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
|
|
912
|
-
await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
|
|
913
|
-
await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI)
|
|
914
|
-
await storageAdapter.remove(STORAGE_KEYS.SERVER_URL)
|
|
915
|
-
clearCookie('basic_token');
|
|
916
|
-
clearCookie('basic_access_token');
|
|
917
|
-
|
|
918
|
-
setUser({})
|
|
919
|
-
setIsSignedIn(false)
|
|
920
|
-
setToken(null)
|
|
921
|
-
setIsAuthReady(true)
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
throw error
|
|
925
434
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
// Store promise if this is a refresh token request
|
|
929
|
-
if (isRefreshToken) {
|
|
930
|
-
refreshPromiseRef.current = refreshPromise
|
|
931
|
-
|
|
932
|
-
// Clear the promise reference when done (success or failure)
|
|
933
|
-
refreshPromise.finally(() => {
|
|
934
|
-
if (refreshPromiseRef.current === refreshPromise) {
|
|
935
|
-
refreshPromiseRef.current = null
|
|
936
|
-
log('Cleared refresh promise reference')
|
|
937
|
-
}
|
|
938
|
-
})
|
|
435
|
+
throw error
|
|
939
436
|
}
|
|
940
|
-
|
|
941
|
-
return refreshPromise
|
|
942
437
|
}
|
|
943
438
|
|
|
944
|
-
//
|
|
439
|
+
// --- DB accessor ---
|
|
945
440
|
const getCurrentDb = (): BasicDB => {
|
|
946
441
|
if (dbMode === 'remote') {
|
|
947
442
|
return remoteDbRef.current || noDb
|
|
@@ -949,33 +444,38 @@ export function BasicProvider({
|
|
|
949
444
|
return syncRef.current || noDb
|
|
950
445
|
}
|
|
951
446
|
|
|
952
|
-
//
|
|
447
|
+
// --- Context value ---
|
|
953
448
|
const contextValue: BasicContextType = {
|
|
954
|
-
// Auth state
|
|
955
|
-
isReady: isAuthReady,
|
|
956
|
-
isSignedIn,
|
|
957
|
-
user,
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
449
|
+
// Auth state
|
|
450
|
+
isReady: authState.isAuthReady,
|
|
451
|
+
isSignedIn: authState.isSignedIn,
|
|
452
|
+
user: authState.user,
|
|
453
|
+
did: authState.did,
|
|
454
|
+
scope: authState.tokenScope,
|
|
455
|
+
hasScope: (scope: string) => authRef.current.hasScope(scope),
|
|
456
|
+
missingScopes: () => authRef.current.missingScopes(),
|
|
457
|
+
|
|
458
|
+
// Auth actions
|
|
459
|
+
signIn: handleSignIn,
|
|
460
|
+
signInWithHandle: handleSignInWithHandle,
|
|
461
|
+
signOut: handleSignOut,
|
|
462
|
+
signInWithCode: (code: string, state?: string) => authRef.current.signInWithCode(code, state),
|
|
463
|
+
|
|
964
464
|
// Token management
|
|
965
|
-
getToken,
|
|
966
|
-
getSignInUrl:
|
|
967
|
-
|
|
465
|
+
getToken: (opts?: GetTokenOptions) => authRef.current.getToken(opts),
|
|
466
|
+
getSignInUrl: (redirectUri?: string) => authRef.current.getSignInUrl(redirectUri),
|
|
467
|
+
|
|
968
468
|
// DB access
|
|
969
469
|
db: getCurrentDb(),
|
|
970
470
|
dbStatus,
|
|
971
471
|
dbMode,
|
|
972
|
-
|
|
472
|
+
|
|
973
473
|
// Legacy aliases (deprecated)
|
|
974
|
-
isAuthReady,
|
|
975
|
-
signin,
|
|
976
|
-
signout,
|
|
977
|
-
signinWithCode,
|
|
978
|
-
getSignInLink,
|
|
474
|
+
isAuthReady: authState.isAuthReady,
|
|
475
|
+
signin: handleSignIn,
|
|
476
|
+
signout: handleSignOut,
|
|
477
|
+
signinWithCode: (code: string, state?: string) => authRef.current.signInWithCode(code, state),
|
|
478
|
+
getSignInLink: (redirectUri?: string) => authRef.current.getSignInUrl(redirectUri),
|
|
979
479
|
}
|
|
980
480
|
|
|
981
481
|
return (
|