@basictech/react 0.6.0 → 0.7.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/changelog.md +12 -0
- package/dist/index.d.mts +14 -4
- package/dist/index.d.ts +14 -4
- package/dist/index.js +329 -76
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +329 -76
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/readme.md +127 -1
- package/src/AuthContext.tsx +424 -120
- package/src/index.ts +2 -2
package/src/AuthContext.tsx
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
|
|
3
1
|
import React, { createContext, useContext, useEffect, useState, useRef } from 'react'
|
|
4
2
|
import { jwtDecode } from 'jwt-decode'
|
|
5
3
|
|
|
@@ -9,6 +7,25 @@ import { validateSchema, compareSchemas } from '@basictech/schema'
|
|
|
9
7
|
|
|
10
8
|
import { log } from './config'
|
|
11
9
|
import {version as currentVersion} from '../package.json'
|
|
10
|
+
export interface BasicStorage {
|
|
11
|
+
get(key: string): Promise<string | null>
|
|
12
|
+
set(key: string, value: string): Promise<void>
|
|
13
|
+
remove(key: string): Promise<void>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class LocalStorageAdapter implements BasicStorage {
|
|
17
|
+
async get(key: string): Promise<string | null> {
|
|
18
|
+
return localStorage.getItem(key)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async set(key: string, value: string): Promise<void> {
|
|
22
|
+
localStorage.setItem(key, value)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async remove(key: string): Promise<void> {
|
|
26
|
+
localStorage.removeItem(key)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
12
29
|
|
|
13
30
|
type BasicSyncType = {
|
|
14
31
|
basic_schema: any;
|
|
@@ -20,7 +37,7 @@ type BasicSyncType = {
|
|
|
20
37
|
count: () => Promise<number>;
|
|
21
38
|
};
|
|
22
39
|
};
|
|
23
|
-
[key: string]: any;
|
|
40
|
+
[key: string]: any;
|
|
24
41
|
};
|
|
25
42
|
|
|
26
43
|
|
|
@@ -46,7 +63,7 @@ type Token = {
|
|
|
46
63
|
access_token: string,
|
|
47
64
|
token_type: string,
|
|
48
65
|
expires_in: number,
|
|
49
|
-
|
|
66
|
+
refresh_token: string,
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
export const BasicContext = createContext<{
|
|
@@ -54,10 +71,11 @@ export const BasicContext = createContext<{
|
|
|
54
71
|
isAuthReady: boolean,
|
|
55
72
|
isSignedIn: boolean,
|
|
56
73
|
user: User | null,
|
|
57
|
-
signout: () => void
|
|
58
|
-
signin: () => void
|
|
74
|
+
signout: () => Promise<void>,
|
|
75
|
+
signin: () => Promise<void>,
|
|
76
|
+
signinWithCode: (code: string, state?: string) => Promise<{ success: boolean, error?: string }>,
|
|
59
77
|
getToken: () => Promise<string>,
|
|
60
|
-
getSignInLink: () => string
|
|
78
|
+
getSignInLink: (redirectUri?: string) => Promise<string>,
|
|
61
79
|
db: any,
|
|
62
80
|
dbStatus: DBStatus
|
|
63
81
|
}>({
|
|
@@ -65,21 +83,25 @@ export const BasicContext = createContext<{
|
|
|
65
83
|
isAuthReady: false,
|
|
66
84
|
isSignedIn: false,
|
|
67
85
|
user: null,
|
|
68
|
-
signout: () =>
|
|
69
|
-
signin: () =>
|
|
86
|
+
signout: () => Promise.resolve(),
|
|
87
|
+
signin: () => Promise.resolve(),
|
|
88
|
+
signinWithCode: () => new Promise(() => { }),
|
|
70
89
|
getToken: () => new Promise(() => { }),
|
|
71
|
-
getSignInLink: () => "",
|
|
90
|
+
getSignInLink: () => Promise.resolve(""),
|
|
72
91
|
db: {},
|
|
73
92
|
dbStatus: DBStatus.LOADING
|
|
74
93
|
});
|
|
75
94
|
|
|
76
95
|
const EmptyDB: BasicSyncType = {
|
|
96
|
+
basic_schema: {},
|
|
97
|
+
connect: () => {},
|
|
98
|
+
debugeroo: () => {},
|
|
77
99
|
isOpen: false,
|
|
78
100
|
collection: () => {
|
|
79
101
|
return {
|
|
80
102
|
ref: {
|
|
81
|
-
toArray: () => [],
|
|
82
|
-
count: () => 0
|
|
103
|
+
toArray: () => Promise.resolve([]),
|
|
104
|
+
count: () => Promise.resolve(0)
|
|
83
105
|
}
|
|
84
106
|
}
|
|
85
107
|
}
|
|
@@ -223,7 +245,19 @@ async function checkForNewVersion(): Promise<{ hasNewVersion: boolean, latestVer
|
|
|
223
245
|
}
|
|
224
246
|
}
|
|
225
247
|
|
|
226
|
-
export function BasicProvider({
|
|
248
|
+
export function BasicProvider({
|
|
249
|
+
children,
|
|
250
|
+
project_id,
|
|
251
|
+
schema,
|
|
252
|
+
debug = false,
|
|
253
|
+
storage
|
|
254
|
+
}: {
|
|
255
|
+
children: React.ReactNode,
|
|
256
|
+
project_id?: string,
|
|
257
|
+
schema?: any,
|
|
258
|
+
debug?: boolean,
|
|
259
|
+
storage?: BasicStorage
|
|
260
|
+
}) {
|
|
227
261
|
const [isAuthReady, setIsAuthReady] = useState(false)
|
|
228
262
|
const [isSignedIn, setIsSignedIn] = useState<boolean>(false)
|
|
229
263
|
const [token, setToken] = useState<Token | null>(null)
|
|
@@ -233,8 +267,71 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
233
267
|
|
|
234
268
|
const [dbStatus, setDbStatus] = useState<DBStatus>(DBStatus.OFFLINE)
|
|
235
269
|
const [error, setError] = useState<ErrorObject | null>(null)
|
|
270
|
+
const [isOnline, setIsOnline] = useState<boolean>(navigator.onLine)
|
|
271
|
+
const [pendingRefresh, setPendingRefresh] = useState<boolean>(false)
|
|
236
272
|
|
|
237
273
|
const syncRef = useRef<BasicSync | null>(null);
|
|
274
|
+
const storageAdapter = storage || new LocalStorageAdapter();
|
|
275
|
+
const STORAGE_KEYS = {
|
|
276
|
+
REFRESH_TOKEN: 'basic_refresh_token',
|
|
277
|
+
USER_INFO: 'basic_user_info',
|
|
278
|
+
AUTH_STATE: 'basic_auth_state',
|
|
279
|
+
DEBUG: 'basic_debug'
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const isDevelopment = () => {
|
|
283
|
+
return (
|
|
284
|
+
window.location.hostname === 'localhost' ||
|
|
285
|
+
window.location.hostname === '127.0.0.1' ||
|
|
286
|
+
window.location.hostname.includes('localhost') ||
|
|
287
|
+
window.location.hostname.includes('127.0.0.1') ||
|
|
288
|
+
window.location.hostname.includes('.local') ||
|
|
289
|
+
process.env.NODE_ENV === 'development' ||
|
|
290
|
+
debug === true
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const cleanOAuthParamsFromUrl = () => {
|
|
295
|
+
if (window.location.search.includes('code') || window.location.search.includes('state')) {
|
|
296
|
+
const url = new URL(window.location.href)
|
|
297
|
+
url.searchParams.delete('code')
|
|
298
|
+
url.searchParams.delete('state')
|
|
299
|
+
window.history.pushState({}, document.title, url.pathname + url.search)
|
|
300
|
+
log('Cleaned OAuth parameters from URL')
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
const handleOnline = () => {
|
|
306
|
+
log('Network came back online')
|
|
307
|
+
setIsOnline(true)
|
|
308
|
+
if (pendingRefresh) {
|
|
309
|
+
log('Retrying pending token refresh')
|
|
310
|
+
setPendingRefresh(false)
|
|
311
|
+
if (token) {
|
|
312
|
+
const refreshToken = token.refresh_token || localStorage.getItem('basic_refresh_token')
|
|
313
|
+
if (refreshToken) {
|
|
314
|
+
fetchToken(refreshToken).catch(error => {
|
|
315
|
+
log('Retry refresh failed:', error)
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const handleOffline = () => {
|
|
323
|
+
log('Network went offline')
|
|
324
|
+
setIsOnline(false)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
window.addEventListener('online', handleOnline)
|
|
328
|
+
window.addEventListener('offline', handleOffline)
|
|
329
|
+
|
|
330
|
+
return () => {
|
|
331
|
+
window.removeEventListener('online', handleOnline)
|
|
332
|
+
window.removeEventListener('offline', handleOffline)
|
|
333
|
+
}
|
|
334
|
+
}, [pendingRefresh, token])
|
|
238
335
|
|
|
239
336
|
useEffect(() => {
|
|
240
337
|
function initDb(options: { shouldConnect: boolean }) {
|
|
@@ -243,12 +340,12 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
243
340
|
syncRef.current = new BasicSync('basicdb', { schema: schema });
|
|
244
341
|
|
|
245
342
|
syncRef.current.syncable.on('statusChanged', (status: number, url: string) => {
|
|
246
|
-
setDbStatus(getSyncStatus(status))
|
|
343
|
+
setDbStatus(getSyncStatus(status) as DBStatus)
|
|
247
344
|
})
|
|
248
345
|
|
|
249
|
-
syncRef.current.syncable.getStatus().then((status) => {
|
|
250
|
-
|
|
251
|
-
})
|
|
346
|
+
// syncRef.current.syncable.getStatus().then((status: number) => {
|
|
347
|
+
// setDbStatus(getSyncStatus(status) as DBStatus)
|
|
348
|
+
// })
|
|
252
349
|
|
|
253
350
|
if (options.shouldConnect) {
|
|
254
351
|
setShouldConnect(true)
|
|
@@ -257,12 +354,6 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
257
354
|
}
|
|
258
355
|
|
|
259
356
|
setIsReady(true)
|
|
260
|
-
|
|
261
|
-
// log('db is open', syncRef.current.isOpen())
|
|
262
|
-
// syncRef.current.open()
|
|
263
|
-
// .then(() => {
|
|
264
|
-
// log("is open now:", syncRef.current.isOpen())
|
|
265
|
-
// })
|
|
266
357
|
}
|
|
267
358
|
}
|
|
268
359
|
|
|
@@ -276,7 +367,7 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
276
367
|
log(`${index + 1}:`, error.message, ` - at ${error.instancePath}`)
|
|
277
368
|
errorMessage += `${index + 1}: ${error.message} - at ${error.instancePath}\n`
|
|
278
369
|
})
|
|
279
|
-
console.groupEnd(
|
|
370
|
+
console.groupEnd()
|
|
280
371
|
setError({
|
|
281
372
|
code: 'schema_invalid',
|
|
282
373
|
title: 'Basic Schema is invalid!',
|
|
@@ -319,39 +410,87 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
319
410
|
}
|
|
320
411
|
}, [isSignedIn, shouldConnect])
|
|
321
412
|
|
|
413
|
+
const connectToDb = async () => {
|
|
414
|
+
const tok = await getToken()
|
|
415
|
+
if (!tok) {
|
|
416
|
+
log('no token found')
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
log('connecting to db...')
|
|
421
|
+
|
|
422
|
+
syncRef.current?.connect({ access_token: tok })
|
|
423
|
+
.catch((e) => {
|
|
424
|
+
log('error connecting to db', e)
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
|
|
322
428
|
useEffect(() => {
|
|
323
|
-
|
|
429
|
+
const initializeAuth = async () => {
|
|
430
|
+
await storageAdapter.set(STORAGE_KEYS.DEBUG, debug ? 'true' : 'false')
|
|
324
431
|
|
|
325
|
-
|
|
432
|
+
try {
|
|
326
433
|
if (window.location.search.includes('code')) {
|
|
327
|
-
let code = window.location?.search?.split('code=')[1]
|
|
434
|
+
let code = window.location?.search?.split('code=')[1]?.split('&')[0]
|
|
435
|
+
if (!code) return
|
|
328
436
|
|
|
329
|
-
const state =
|
|
330
|
-
|
|
437
|
+
const state = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE)
|
|
438
|
+
const urlState = window.location.search.split('state=')[1]?.split('&')[0]
|
|
439
|
+
if (!state || state !== urlState) {
|
|
331
440
|
log('error: auth state does not match')
|
|
332
441
|
setIsAuthReady(true)
|
|
333
442
|
|
|
334
|
-
|
|
335
|
-
|
|
443
|
+
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
|
|
444
|
+
// Clean OAuth parameters from URL
|
|
445
|
+
cleanOAuthParamsFromUrl()
|
|
336
446
|
return
|
|
337
447
|
}
|
|
338
448
|
|
|
339
|
-
|
|
449
|
+
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
|
|
450
|
+
// Clean OAuth parameters from URL
|
|
451
|
+
cleanOAuthParamsFromUrl()
|
|
340
452
|
|
|
341
|
-
fetchToken(code)
|
|
453
|
+
fetchToken(code).catch((error) => {
|
|
454
|
+
log('Error fetching token:', error)
|
|
455
|
+
})
|
|
342
456
|
} else {
|
|
343
|
-
|
|
344
|
-
if (
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
457
|
+
const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
|
|
458
|
+
if (refreshToken) {
|
|
459
|
+
log('Found refresh token in storage, attempting to refresh access token')
|
|
460
|
+
fetchToken(refreshToken).catch((error) => {
|
|
461
|
+
log('Error fetching refresh token:', error)
|
|
462
|
+
})
|
|
463
|
+
} else {
|
|
464
|
+
let cookie_token = getCookie('basic_token')
|
|
465
|
+
if (cookie_token !== '') {
|
|
466
|
+
const tokenData = JSON.parse(cookie_token)
|
|
467
|
+
setToken(tokenData)
|
|
468
|
+
if (tokenData.refresh_token) {
|
|
469
|
+
await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, tokenData.refresh_token)
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
const cachedUserInfo = await storageAdapter.get(STORAGE_KEYS.USER_INFO)
|
|
473
|
+
if (cachedUserInfo) {
|
|
474
|
+
try {
|
|
475
|
+
const userData = JSON.parse(cachedUserInfo)
|
|
476
|
+
setUser(userData)
|
|
477
|
+
setIsSignedIn(true)
|
|
478
|
+
log('Loaded cached user info for offline mode')
|
|
479
|
+
} catch (error) {
|
|
480
|
+
log('Error parsing cached user info:', error)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
setIsAuthReady(true)
|
|
484
|
+
}
|
|
348
485
|
}
|
|
349
486
|
}
|
|
350
487
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
488
|
+
} catch (e) {
|
|
489
|
+
log('error getting token', e)
|
|
490
|
+
}
|
|
354
491
|
}
|
|
492
|
+
|
|
493
|
+
initializeAuth()
|
|
355
494
|
}, [])
|
|
356
495
|
|
|
357
496
|
useEffect(() => {
|
|
@@ -368,16 +507,18 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
368
507
|
|
|
369
508
|
if (user.error) {
|
|
370
509
|
log('error fetching user', user.error)
|
|
371
|
-
// refreshToken()
|
|
372
510
|
return
|
|
373
511
|
} else {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (window.location.search.includes('code')) {
|
|
378
|
-
window.history.pushState({}, document.title, "/");
|
|
512
|
+
if (token?.refresh_token) {
|
|
513
|
+
await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token)
|
|
379
514
|
}
|
|
380
515
|
|
|
516
|
+
await storageAdapter.set(STORAGE_KEYS.USER_INFO, JSON.stringify(user))
|
|
517
|
+
log('Cached user info in storage')
|
|
518
|
+
|
|
519
|
+
document.cookie = `basic_access_token=${token?.access_token}; Secure; SameSite=Strict; HttpOnly=false`;
|
|
520
|
+
document.cookie = `basic_token=${JSON.stringify(token)}; Secure; SameSite=Strict`;
|
|
521
|
+
|
|
381
522
|
setUser(user)
|
|
382
523
|
setIsSignedIn(true)
|
|
383
524
|
|
|
@@ -398,10 +539,21 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
398
539
|
|
|
399
540
|
if (isExpired) {
|
|
400
541
|
log('token is expired - refreshing ...')
|
|
401
|
-
|
|
402
|
-
|
|
542
|
+
try {
|
|
543
|
+
const newToken = await fetchToken(token?.refresh_token || '')
|
|
544
|
+
fetchUser(newToken?.access_token || '')
|
|
545
|
+
} catch (error) {
|
|
546
|
+
log('Failed to refresh token in checkToken:', error)
|
|
547
|
+
|
|
548
|
+
if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
|
|
549
|
+
log('Network issue - continuing with expired token until online')
|
|
550
|
+
fetchUser(token?.access_token || '')
|
|
551
|
+
} else {
|
|
552
|
+
setIsAuthReady(true)
|
|
553
|
+
}
|
|
554
|
+
}
|
|
403
555
|
} else {
|
|
404
|
-
fetchUser(token
|
|
556
|
+
fetchUser(token?.access_token || '')
|
|
405
557
|
}
|
|
406
558
|
}
|
|
407
559
|
|
|
@@ -410,65 +562,125 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
410
562
|
}
|
|
411
563
|
}, [token])
|
|
412
564
|
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
log('no token found')
|
|
417
|
-
return
|
|
418
|
-
}
|
|
565
|
+
const getSignInLink = async (redirectUri?: string) => {
|
|
566
|
+
try {
|
|
567
|
+
log('getting sign in link...')
|
|
419
568
|
|
|
420
|
-
|
|
569
|
+
if (!project_id) {
|
|
570
|
+
throw new Error('Project ID is required to generate sign-in link')
|
|
571
|
+
}
|
|
421
572
|
|
|
422
|
-
|
|
573
|
+
const randomState = Math.random().toString(36).substring(6);
|
|
574
|
+
await storageAdapter.set(STORAGE_KEYS.AUTH_STATE, randomState)
|
|
423
575
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
576
|
+
const redirectUrl = redirectUri || window.location.href
|
|
577
|
+
|
|
578
|
+
if (!redirectUrl || (!redirectUrl.startsWith('http://') && !redirectUrl.startsWith('https://'))) {
|
|
579
|
+
throw new Error('Invalid redirect URI provided')
|
|
580
|
+
}
|
|
429
581
|
|
|
430
|
-
|
|
431
|
-
|
|
582
|
+
let baseUrl = "https://api.basic.tech/auth/authorize"
|
|
583
|
+
baseUrl += `?client_id=${project_id}`
|
|
584
|
+
baseUrl += `&redirect_uri=${encodeURIComponent(redirectUrl)}`
|
|
585
|
+
baseUrl += `&response_type=code`
|
|
586
|
+
baseUrl += `&scope=profile`
|
|
587
|
+
baseUrl += `&state=${randomState}`
|
|
432
588
|
|
|
433
|
-
|
|
434
|
-
|
|
589
|
+
log('Generated sign-in link successfully')
|
|
590
|
+
return baseUrl;
|
|
435
591
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
baseUrl += `&state=${randomState}`
|
|
592
|
+
} catch (error) {
|
|
593
|
+
log('Error generating sign-in link:', error)
|
|
594
|
+
throw error
|
|
595
|
+
}
|
|
596
|
+
}
|
|
442
597
|
|
|
443
|
-
|
|
598
|
+
const signin = async () => {
|
|
599
|
+
try {
|
|
600
|
+
log('signing in...')
|
|
601
|
+
|
|
602
|
+
if (!project_id) {
|
|
603
|
+
log('Error: project_id is required for sign-in')
|
|
604
|
+
throw new Error('Project ID is required for authentication')
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const signInLink = await getSignInLink()
|
|
608
|
+
log('Generated sign-in link:', signInLink)
|
|
609
|
+
|
|
610
|
+
if (!signInLink || !signInLink.startsWith('https://')) {
|
|
611
|
+
log('Error: Invalid sign-in link generated')
|
|
612
|
+
throw new Error('Failed to generate valid sign-in URL')
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
window.location.href = signInLink
|
|
616
|
+
|
|
617
|
+
} catch (error) {
|
|
618
|
+
log('Error during sign-in:', error)
|
|
619
|
+
|
|
620
|
+
if (isDevelopment()) {
|
|
621
|
+
setError({
|
|
622
|
+
code: 'signin_error',
|
|
623
|
+
title: 'Sign-in Failed',
|
|
624
|
+
message: (error as Error).message || 'An error occurred during sign-in. Please try again.'
|
|
625
|
+
})
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
throw error
|
|
629
|
+
}
|
|
444
630
|
}
|
|
445
631
|
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
632
|
+
const signinWithCode = async (code: string, state?: string): Promise<{ success: boolean, error?: string }> => {
|
|
633
|
+
try {
|
|
634
|
+
log('signinWithCode called with code:', code)
|
|
635
|
+
|
|
636
|
+
if (!code || typeof code !== 'string') {
|
|
637
|
+
return { success: false, error: 'Invalid authorization code' }
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (state) {
|
|
641
|
+
const storedState = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE)
|
|
642
|
+
if (storedState && storedState !== state) {
|
|
643
|
+
log('State parameter mismatch:', { provided: state, stored: storedState })
|
|
644
|
+
return { success: false, error: 'State parameter mismatch' }
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
|
|
649
|
+
cleanOAuthParamsFromUrl()
|
|
650
|
+
|
|
651
|
+
const token = await fetchToken(code)
|
|
652
|
+
|
|
653
|
+
if (token) {
|
|
654
|
+
log('signinWithCode successful')
|
|
655
|
+
return { success: true }
|
|
656
|
+
} else {
|
|
657
|
+
return { success: false, error: 'Failed to exchange code for token' }
|
|
658
|
+
}
|
|
659
|
+
} catch (error) {
|
|
660
|
+
log('signinWithCode error:', error)
|
|
661
|
+
return {
|
|
662
|
+
success: false,
|
|
663
|
+
error: (error as Error).message || 'Authentication failed'
|
|
664
|
+
}
|
|
665
|
+
}
|
|
451
666
|
}
|
|
452
667
|
|
|
453
|
-
const signout = () => {
|
|
668
|
+
const signout = async () => {
|
|
454
669
|
log('signing out!')
|
|
455
670
|
setUser({})
|
|
456
671
|
setIsSignedIn(false)
|
|
457
672
|
setToken(null)
|
|
673
|
+
|
|
458
674
|
document.cookie = `basic_token=; Secure; SameSite=Strict`;
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
// syncRef.current.disconnect()
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
// }
|
|
675
|
+
document.cookie = `basic_access_token=; Secure; SameSite=Strict`;
|
|
676
|
+
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
|
|
677
|
+
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
|
|
678
|
+
await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
|
|
467
679
|
if (syncRef.current) {
|
|
468
680
|
(async () => {
|
|
469
681
|
try {
|
|
470
|
-
await syncRef.current
|
|
471
|
-
await syncRef.current
|
|
682
|
+
await syncRef.current?.close()
|
|
683
|
+
await syncRef.current?.delete({disableAutoOpen: false})
|
|
472
684
|
syncRef.current = null
|
|
473
685
|
window?.location?.reload()
|
|
474
686
|
} catch (error) {
|
|
@@ -481,7 +693,32 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
481
693
|
const getToken = async (): Promise<string> => {
|
|
482
694
|
log('getting token...')
|
|
483
695
|
|
|
696
|
+
|
|
484
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
|
+
try {
|
|
703
|
+
const newToken = await fetchToken(refreshToken)
|
|
704
|
+
if (newToken?.access_token) {
|
|
705
|
+
return newToken.access_token
|
|
706
|
+
}
|
|
707
|
+
} catch (error) {
|
|
708
|
+
log('Failed to refresh token from storage:', error)
|
|
709
|
+
|
|
710
|
+
if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
|
|
711
|
+
log('Network issue - continuing with potentially expired token')
|
|
712
|
+
const lastToken = localStorage.getItem('basic_access_token')
|
|
713
|
+
if (lastToken) {
|
|
714
|
+
return lastToken
|
|
715
|
+
}
|
|
716
|
+
throw new Error('Network offline - authentication will be retried when online')
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
throw new Error('Authentication expired. Please sign in again.')
|
|
720
|
+
}
|
|
721
|
+
}
|
|
485
722
|
log('no token found')
|
|
486
723
|
throw new Error('no token found')
|
|
487
724
|
}
|
|
@@ -491,8 +728,24 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
491
728
|
|
|
492
729
|
if (isExpired) {
|
|
493
730
|
log('token is expired - refreshing ...')
|
|
494
|
-
const
|
|
495
|
-
|
|
731
|
+
const refreshToken = token?.refresh_token || await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
|
|
732
|
+
if (refreshToken) {
|
|
733
|
+
try {
|
|
734
|
+
const newToken = await fetchToken(refreshToken)
|
|
735
|
+
return newToken?.access_token || ''
|
|
736
|
+
} catch (error) {
|
|
737
|
+
log('Failed to refresh expired token:', error)
|
|
738
|
+
|
|
739
|
+
if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
|
|
740
|
+
log('Network issue - using expired token until network is restored')
|
|
741
|
+
return token.access_token
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
throw new Error('Authentication expired. Please sign in again.')
|
|
745
|
+
}
|
|
746
|
+
} else {
|
|
747
|
+
throw new Error('no refresh token available')
|
|
748
|
+
}
|
|
496
749
|
}
|
|
497
750
|
|
|
498
751
|
return token?.access_token || ''
|
|
@@ -503,8 +756,8 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
503
756
|
if (document.cookie && document.cookie !== '') {
|
|
504
757
|
const cookies = document.cookie.split(';');
|
|
505
758
|
for (let i = 0; i < cookies.length; i++) {
|
|
506
|
-
const cookie = cookies[i]
|
|
507
|
-
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
|
759
|
+
const cookie = cookies[i]?.trim();
|
|
760
|
+
if (cookie && cookie.substring(0, name.length + 1) === (name + '=')) {
|
|
508
761
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
509
762
|
break;
|
|
510
763
|
}
|
|
@@ -514,24 +767,79 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
514
767
|
}
|
|
515
768
|
|
|
516
769
|
const fetchToken = async (code: string) => {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
})
|
|
524
|
-
.then(response => response.json())
|
|
525
|
-
.catch(error => log('Error:', error))
|
|
770
|
+
try {
|
|
771
|
+
if (!isOnline) {
|
|
772
|
+
log('Network is offline, marking refresh as pending')
|
|
773
|
+
setPendingRefresh(true)
|
|
774
|
+
throw new Error('Network offline - refresh will be retried when online')
|
|
775
|
+
}
|
|
526
776
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
777
|
+
const token = await fetch('https://api.basic.tech/auth/token', {
|
|
778
|
+
method: 'POST',
|
|
779
|
+
headers: {
|
|
780
|
+
'Content-Type': 'application/json'
|
|
781
|
+
},
|
|
782
|
+
body: JSON.stringify({ code: code })
|
|
783
|
+
})
|
|
784
|
+
.then(response => response.json())
|
|
785
|
+
.catch(error => {
|
|
786
|
+
log('Network error fetching token:', error)
|
|
787
|
+
if (!isOnline) {
|
|
788
|
+
setPendingRefresh(true)
|
|
789
|
+
throw new Error('Network offline - refresh will be retried when online')
|
|
790
|
+
}
|
|
791
|
+
throw new Error('Network error during token refresh')
|
|
792
|
+
})
|
|
793
|
+
|
|
794
|
+
if (token.error) {
|
|
795
|
+
log('error fetching token', token.error)
|
|
796
|
+
|
|
797
|
+
if (token.error.includes('network') || token.error.includes('timeout')) {
|
|
798
|
+
setPendingRefresh(true)
|
|
799
|
+
throw new Error('Network issue - refresh will be retried when online')
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
|
|
803
|
+
await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
|
|
804
|
+
document.cookie = `basic_token=; Secure; SameSite=Strict`;
|
|
805
|
+
document.cookie = `basic_access_token=; Secure; SameSite=Strict`;
|
|
806
|
+
|
|
807
|
+
setUser({})
|
|
808
|
+
setIsSignedIn(false)
|
|
809
|
+
setToken(null)
|
|
810
|
+
setIsAuthReady(true)
|
|
811
|
+
|
|
812
|
+
throw new Error(`Token refresh failed: ${token.error}`)
|
|
813
|
+
} else {
|
|
814
|
+
setToken(token)
|
|
815
|
+
setPendingRefresh(false)
|
|
816
|
+
|
|
817
|
+
if (token.refresh_token) {
|
|
818
|
+
await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token)
|
|
819
|
+
log('Updated refresh token in storage')
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
document.cookie = `basic_access_token=${token.access_token}; Secure; SameSite=Strict; HttpOnly=false`;
|
|
823
|
+
log('Updated access token in cookie')
|
|
824
|
+
}
|
|
825
|
+
return token
|
|
826
|
+
} catch (error) {
|
|
827
|
+
log('Token refresh error:', error)
|
|
828
|
+
|
|
829
|
+
if (!(error as Error).message.includes('offline') && !(error as Error).message.includes('Network')) {
|
|
830
|
+
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
|
|
831
|
+
await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
|
|
832
|
+
document.cookie = `basic_token=; Secure; SameSite=Strict`;
|
|
833
|
+
document.cookie = `basic_access_token=; Secure; SameSite=Strict`;
|
|
834
|
+
|
|
835
|
+
setUser({})
|
|
836
|
+
setIsSignedIn(false)
|
|
837
|
+
setToken(null)
|
|
838
|
+
setIsAuthReady(true)
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
throw error
|
|
533
842
|
}
|
|
534
|
-
return token
|
|
535
843
|
}
|
|
536
844
|
|
|
537
845
|
|
|
@@ -546,22 +854,22 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
546
854
|
get: async () => {
|
|
547
855
|
checkSignIn()
|
|
548
856
|
const tok = await getToken()
|
|
549
|
-
return get({ projectId: project_id, accountId: user
|
|
857
|
+
return get({ projectId: project_id, accountId: user?.id, tableName: tableName, token: tok })
|
|
550
858
|
},
|
|
551
859
|
add: async (value: any) => {
|
|
552
860
|
checkSignIn()
|
|
553
861
|
const tok = await getToken()
|
|
554
|
-
return add({ projectId: project_id, accountId: user
|
|
862
|
+
return add({ projectId: project_id, accountId: user?.id, tableName: tableName, value: value, token: tok })
|
|
555
863
|
},
|
|
556
864
|
update: async (id: string, value: any) => {
|
|
557
865
|
checkSignIn()
|
|
558
866
|
const tok = await getToken()
|
|
559
|
-
return update({ projectId: project_id, accountId: user
|
|
867
|
+
return update({ projectId: project_id, accountId: user?.id, tableName: tableName, id: id, value: value, token: tok })
|
|
560
868
|
},
|
|
561
869
|
delete: async (id: string) => {
|
|
562
870
|
checkSignIn()
|
|
563
871
|
const tok = await getToken()
|
|
564
|
-
return deleteRecord({ projectId: project_id, accountId: user
|
|
872
|
+
return deleteRecord({ projectId: project_id, accountId: user?.id, tableName: tableName, id: id, token: tok })
|
|
565
873
|
}
|
|
566
874
|
|
|
567
875
|
}
|
|
@@ -582,13 +890,14 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
582
890
|
user,
|
|
583
891
|
signout,
|
|
584
892
|
signin,
|
|
893
|
+
signinWithCode,
|
|
585
894
|
getToken,
|
|
586
895
|
getSignInLink,
|
|
587
896
|
db: syncRef.current ? syncRef.current : noDb,
|
|
588
897
|
dbStatus
|
|
589
898
|
}}>
|
|
590
899
|
|
|
591
|
-
{error && <ErrorDisplay error={error} />}
|
|
900
|
+
{error && isDevelopment() && <ErrorDisplay error={error} />}
|
|
592
901
|
{isReady && children}
|
|
593
902
|
</BasicContext.Provider>
|
|
594
903
|
)
|
|
@@ -615,11 +924,6 @@ function ErrorDisplay({ error }: { error: ErrorObject }) {
|
|
|
615
924
|
</div>
|
|
616
925
|
}
|
|
617
926
|
|
|
618
|
-
/*
|
|
619
|
-
possible errors:
|
|
620
|
-
- projectid missing / invalid
|
|
621
|
-
- schema missing / invalid
|
|
622
|
-
*/
|
|
623
927
|
|
|
624
928
|
export function useBasic() {
|
|
625
929
|
return useContext(BasicContext);
|