@basictech/react 0.7.0-beta.1 → 0.7.0-beta.3
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/AUTH_IMPLEMENTATION_GUIDE.md +2009 -0
- package/changelog.md +12 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +362 -363
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +360 -351
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/AuthContext.tsx +242 -404
- package/src/index.ts +11 -32
- package/src/sync/index.ts +1 -1
- package/src/updater/updateMigrations.ts +22 -0
- package/src/updater/versionUpdater.ts +160 -0
- package/src/utils/network.ts +82 -0
- package/src/utils/schema.ts +120 -0
- package/src/utils/storage.ts +63 -0
- package/src/schema.ts +0 -159
package/src/AuthContext.tsx
CHANGED
|
@@ -2,31 +2,28 @@ import React, { createContext, useContext, useEffect, useState, useRef } from 'r
|
|
|
2
2
|
import { jwtDecode } from 'jwt-decode'
|
|
3
3
|
|
|
4
4
|
import { BasicSync } from './sync'
|
|
5
|
-
import { get, add, update, deleteRecord } from './db'
|
|
6
|
-
import { validateSchema, compareSchemas } from '@basictech/schema'
|
|
7
5
|
|
|
8
6
|
import { log } from './config'
|
|
9
|
-
import {version as currentVersion} from '../package.json'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
import { version as currentVersion } from '../package.json'
|
|
8
|
+
import { createVersionUpdater } from './updater/versionUpdater'
|
|
9
|
+
import { getMigrations } from './updater/updateMigrations'
|
|
10
|
+
import { BasicStorage, LocalStorageAdapter, STORAGE_KEYS, getCookie, setCookie, clearCookie } from './utils/storage'
|
|
11
|
+
import { isDevelopment, checkForNewVersion, cleanOAuthParamsFromUrl, getSyncStatus } from './utils/network'
|
|
12
|
+
import { getSchemaStatus, validateAndCheckSchema } from './utils/schema'
|
|
13
|
+
|
|
14
|
+
export type { BasicStorage, LocalStorageAdapter } from './utils/storage'
|
|
15
|
+
|
|
16
|
+
export type AuthConfig = {
|
|
17
|
+
scopes?: string | string[];
|
|
18
|
+
server_url?: string;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
}
|
|
21
|
+
const DEFAULT_AUTH_CONFIG: Required<AuthConfig> = {
|
|
22
|
+
scopes: 'profile email app:admin',
|
|
23
|
+
server_url: 'https://api.basic.tech'
|
|
28
24
|
}
|
|
29
25
|
|
|
26
|
+
|
|
30
27
|
type BasicSyncType = {
|
|
31
28
|
basic_schema: any;
|
|
32
29
|
connect: (options: { access_token: string }) => void;
|
|
@@ -92,171 +89,26 @@ export const BasicContext = createContext<{
|
|
|
92
89
|
dbStatus: DBStatus.LOADING
|
|
93
90
|
});
|
|
94
91
|
|
|
95
|
-
const EmptyDB: BasicSyncType = {
|
|
96
|
-
basic_schema: {},
|
|
97
|
-
connect: () => {},
|
|
98
|
-
debugeroo: () => {},
|
|
99
|
-
isOpen: false,
|
|
100
|
-
collection: () => {
|
|
101
|
-
return {
|
|
102
|
-
ref: {
|
|
103
|
-
toArray: () => Promise.resolve([]),
|
|
104
|
-
count: () => Promise.resolve(0)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async function getSchemaStatus(schema: any) {
|
|
111
|
-
const projectId = schema.project_id
|
|
112
|
-
let status = ''
|
|
113
|
-
const valid = validateSchema(schema)
|
|
114
|
-
|
|
115
|
-
if (!valid.valid) {
|
|
116
|
-
console.warn('BasicDB Error: your local schema is invalid. Please fix errors and try again - sync is disabled')
|
|
117
|
-
return {
|
|
118
|
-
valid: false,
|
|
119
|
-
status: 'invalid',
|
|
120
|
-
latest: null
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const latestSchema = await fetch(`https://api.basic.tech/project/${projectId}/schema`)
|
|
125
|
-
.then(res => res.json())
|
|
126
|
-
.then(data => data.data[0].schema)
|
|
127
|
-
.catch(err => {
|
|
128
|
-
return {
|
|
129
|
-
valid: false,
|
|
130
|
-
status: 'error',
|
|
131
|
-
latest: null
|
|
132
|
-
}
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
console.log('latestSchema', latestSchema)
|
|
136
|
-
|
|
137
|
-
if (!latestSchema.version) {
|
|
138
|
-
return {
|
|
139
|
-
valid: false,
|
|
140
|
-
status: 'error',
|
|
141
|
-
latest: null
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (latestSchema.version > schema.version) {
|
|
146
|
-
// error_code: schema_behind
|
|
147
|
-
console.warn('BasicDB Error: your local schema version is behind the latest. Found version:', schema.version, 'but expected', latestSchema.version, " - sync is disabled")
|
|
148
|
-
return {
|
|
149
|
-
valid: false,
|
|
150
|
-
status: 'behind',
|
|
151
|
-
latest: latestSchema
|
|
152
|
-
}
|
|
153
|
-
} else if (latestSchema.version < schema.version) {
|
|
154
|
-
// error_code: schema_ahead
|
|
155
|
-
console.warn('BasicDB Error: your local schema version is ahead of the latest. Found version:', schema.version, 'but expected', latestSchema.version, " - sync is disabled")
|
|
156
|
-
return {
|
|
157
|
-
valid: false,
|
|
158
|
-
status: 'ahead',
|
|
159
|
-
latest: latestSchema
|
|
160
|
-
}
|
|
161
|
-
} else if (latestSchema.version === schema.version) {
|
|
162
|
-
const changes = compareSchemas(schema, latestSchema)
|
|
163
|
-
if (changes.valid) {
|
|
164
|
-
return {
|
|
165
|
-
valid: true,
|
|
166
|
-
status: 'current',
|
|
167
|
-
latest: latestSchema
|
|
168
|
-
}
|
|
169
|
-
} else {
|
|
170
|
-
// error_code: schema_conflict
|
|
171
|
-
console.warn('BasicDB Error: your local schema is conflicting with the latest. Your version:', schema.version, 'does not match origin version', latestSchema.version, " - sync is disabled")
|
|
172
|
-
return {
|
|
173
|
-
valid: false,
|
|
174
|
-
status: 'conflict',
|
|
175
|
-
latest: latestSchema
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
} else {
|
|
179
|
-
return {
|
|
180
|
-
valid: false,
|
|
181
|
-
status: 'error',
|
|
182
|
-
latest: null
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
function getSyncStatus(statusCode: number): string {
|
|
189
|
-
switch (statusCode) {
|
|
190
|
-
case -1:
|
|
191
|
-
return "ERROR";
|
|
192
|
-
case 0:
|
|
193
|
-
return "OFFLINE";
|
|
194
|
-
case 1:
|
|
195
|
-
return "CONNECTING";
|
|
196
|
-
case 2:
|
|
197
|
-
return "ONLINE";
|
|
198
|
-
case 3:
|
|
199
|
-
return "SYNCING";
|
|
200
|
-
case 4:
|
|
201
|
-
return "ERROR_WILL_RETRY";
|
|
202
|
-
default:
|
|
203
|
-
return "UNKNOWN";
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
92
|
type ErrorObject = {
|
|
208
93
|
code: string;
|
|
209
94
|
title: string;
|
|
210
95
|
message: string;
|
|
211
96
|
}
|
|
212
97
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const latestVersion = data.version;
|
|
225
|
-
|
|
226
|
-
if (latestVersion !== currentVersion) {
|
|
227
|
-
console.warn('[basic] New version available:', latestVersion, `\nrun "npm install @basictech/react@${latestVersion}" to update`);
|
|
228
|
-
}
|
|
229
|
-
if (isBeta) {
|
|
230
|
-
log('thank you for being on basictech/react beta :)')
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
hasNewVersion: currentVersion !== latestVersion,
|
|
235
|
-
latestVersion,
|
|
236
|
-
currentVersion
|
|
237
|
-
};
|
|
238
|
-
} catch (error) {
|
|
239
|
-
log('Error checking for new version:', error);
|
|
240
|
-
return {
|
|
241
|
-
hasNewVersion: false,
|
|
242
|
-
latestVersion: null,
|
|
243
|
-
currentVersion: null
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
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,
|
|
98
|
+
export function BasicProvider({
|
|
99
|
+
children,
|
|
100
|
+
project_id,
|
|
101
|
+
schema,
|
|
102
|
+
debug = false,
|
|
103
|
+
storage,
|
|
104
|
+
auth
|
|
105
|
+
}: {
|
|
106
|
+
children: React.ReactNode,
|
|
107
|
+
project_id?: string,
|
|
108
|
+
schema?: any,
|
|
258
109
|
debug?: boolean,
|
|
259
|
-
storage?: BasicStorage
|
|
110
|
+
storage?: BasicStorage,
|
|
111
|
+
auth?: AuthConfig
|
|
260
112
|
}) {
|
|
261
113
|
const [isAuthReady, setIsAuthReady] = useState(false)
|
|
262
114
|
const [isSignedIn, setIsSignedIn] = useState<boolean>(false)
|
|
@@ -272,34 +124,21 @@ export function BasicProvider({
|
|
|
272
124
|
|
|
273
125
|
const syncRef = useRef<BasicSync | null>(null);
|
|
274
126
|
const storageAdapter = storage || new LocalStorageAdapter();
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
127
|
+
|
|
128
|
+
// Merge auth config with defaults
|
|
129
|
+
const authConfig: Required<AuthConfig> = {
|
|
130
|
+
scopes: auth?.scopes || DEFAULT_AUTH_CONFIG.scopes,
|
|
131
|
+
server_url: auth?.server_url || DEFAULT_AUTH_CONFIG.server_url
|
|
280
132
|
}
|
|
133
|
+
|
|
134
|
+
// Normalize scopes to space-separated string
|
|
135
|
+
const scopesString = Array.isArray(authConfig.scopes)
|
|
136
|
+
? authConfig.scopes.join(' ')
|
|
137
|
+
: authConfig.scopes;
|
|
281
138
|
|
|
282
|
-
const
|
|
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
|
-
}
|
|
139
|
+
const isDevMode = () => isDevelopment(debug)
|
|
293
140
|
|
|
294
|
-
const
|
|
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
|
-
}
|
|
141
|
+
const cleanOAuthParams = () => cleanOAuthParamsFromUrl()
|
|
303
142
|
|
|
304
143
|
useEffect(() => {
|
|
305
144
|
const handleOnline = () => {
|
|
@@ -311,14 +150,14 @@ export function BasicProvider({
|
|
|
311
150
|
if (token) {
|
|
312
151
|
const refreshToken = token.refresh_token || localStorage.getItem('basic_refresh_token')
|
|
313
152
|
if (refreshToken) {
|
|
314
|
-
fetchToken(refreshToken).catch(error => {
|
|
153
|
+
fetchToken(refreshToken, true).catch(error => {
|
|
315
154
|
log('Retry refresh failed:', error)
|
|
316
155
|
})
|
|
317
156
|
}
|
|
318
157
|
}
|
|
319
158
|
}
|
|
320
159
|
}
|
|
321
|
-
|
|
160
|
+
|
|
322
161
|
const handleOffline = () => {
|
|
323
162
|
log('Network went offline')
|
|
324
163
|
setIsOnline(false)
|
|
@@ -338,18 +177,18 @@ export function BasicProvider({
|
|
|
338
177
|
if (!syncRef.current) {
|
|
339
178
|
log('Initializing Basic DB')
|
|
340
179
|
syncRef.current = new BasicSync('basicdb', { schema: schema });
|
|
341
|
-
|
|
180
|
+
|
|
342
181
|
syncRef.current.syncable.on('statusChanged', (status: number, url: string) => {
|
|
343
182
|
setDbStatus(getSyncStatus(status) as DBStatus)
|
|
344
183
|
})
|
|
345
|
-
|
|
184
|
+
|
|
346
185
|
// syncRef.current.syncable.getStatus().then((status: number) => {
|
|
347
186
|
// setDbStatus(getSyncStatus(status) as DBStatus)
|
|
348
187
|
// })
|
|
349
188
|
|
|
350
|
-
if (options.shouldConnect) {
|
|
189
|
+
if (options.shouldConnect) {
|
|
351
190
|
setShouldConnect(true)
|
|
352
|
-
} else {
|
|
191
|
+
} else {
|
|
353
192
|
log('Sync is disabled')
|
|
354
193
|
}
|
|
355
194
|
|
|
@@ -358,16 +197,15 @@ export function BasicProvider({
|
|
|
358
197
|
}
|
|
359
198
|
|
|
360
199
|
async function checkSchema() {
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
console.group('Schema Errors')
|
|
200
|
+
const result = await validateAndCheckSchema(schema)
|
|
201
|
+
|
|
202
|
+
if (!result.isValid) {
|
|
365
203
|
let errorMessage = ''
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
204
|
+
if (result.errors) {
|
|
205
|
+
result.errors.forEach((error, index) => {
|
|
206
|
+
errorMessage += `${index + 1}: ${error.message} - at ${error.instancePath}\n`
|
|
207
|
+
})
|
|
208
|
+
}
|
|
371
209
|
setError({
|
|
372
210
|
code: 'schema_invalid',
|
|
373
211
|
title: 'Basic Schema is invalid!',
|
|
@@ -377,22 +215,13 @@ export function BasicProvider({
|
|
|
377
215
|
return null
|
|
378
216
|
}
|
|
379
217
|
|
|
380
|
-
|
|
381
|
-
let schemaStatus = { valid: false }
|
|
382
|
-
if (schema.version !== 0) {
|
|
383
|
-
schemaStatus = await getSchemaStatus(schema)
|
|
384
|
-
log('schemaStatus', schemaStatus)
|
|
385
|
-
}else {
|
|
386
|
-
log("schema not published - at version 0")
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
if (schemaStatus.valid) {
|
|
218
|
+
if (result.schemaStatus.valid) {
|
|
390
219
|
initDb({ shouldConnect: true })
|
|
391
220
|
} else {
|
|
392
|
-
log('Schema is invalid!', schemaStatus)
|
|
221
|
+
log('Schema is invalid!', result.schemaStatus)
|
|
393
222
|
initDb({ shouldConnect: false })
|
|
394
223
|
}
|
|
395
|
-
|
|
224
|
+
|
|
396
225
|
checkForNewVersion()
|
|
397
226
|
}
|
|
398
227
|
|
|
@@ -403,100 +232,110 @@ export function BasicProvider({
|
|
|
403
232
|
}
|
|
404
233
|
}, []);
|
|
405
234
|
|
|
406
|
-
|
|
407
235
|
useEffect(() => {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
236
|
+
async function connectToDb() {
|
|
237
|
+
if (token && syncRef.current && isSignedIn && shouldConnect) {
|
|
238
|
+
const tok = await getToken()
|
|
239
|
+
if (!tok) {
|
|
240
|
+
log('no token found')
|
|
241
|
+
return
|
|
242
|
+
}
|
|
412
243
|
|
|
413
|
-
|
|
414
|
-
const tok = await getToken()
|
|
415
|
-
if (!tok) {
|
|
416
|
-
log('no token found')
|
|
417
|
-
return
|
|
418
|
-
}
|
|
244
|
+
log('connecting to db...')
|
|
419
245
|
|
|
420
|
-
|
|
246
|
+
syncRef.current?.connect({ access_token: tok })
|
|
247
|
+
.catch((e) => {
|
|
248
|
+
log('error connecting to db', e)
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
connectToDb()
|
|
421
253
|
|
|
422
|
-
|
|
423
|
-
.catch((e) => {
|
|
424
|
-
log('error connecting to db', e)
|
|
425
|
-
})
|
|
426
|
-
}
|
|
254
|
+
}, [isSignedIn, shouldConnect])
|
|
427
255
|
|
|
428
256
|
useEffect(() => {
|
|
429
257
|
const initializeAuth = async () => {
|
|
430
258
|
await storageAdapter.set(STORAGE_KEYS.DEBUG, debug ? 'true' : 'false')
|
|
431
259
|
|
|
432
260
|
try {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
261
|
+
const versionUpdater = createVersionUpdater(storageAdapter, currentVersion, getMigrations())
|
|
262
|
+
const updateResult = await versionUpdater.checkAndUpdate()
|
|
263
|
+
|
|
264
|
+
if (updateResult.updated) {
|
|
265
|
+
log(`App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`)
|
|
266
|
+
} else {
|
|
267
|
+
log(`App version ${updateResult.toVersion} is current`)
|
|
268
|
+
}
|
|
269
|
+
} catch (error) {
|
|
270
|
+
log('Version update failed:', error)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
if (window.location.search.includes('code')) {
|
|
275
|
+
let code = window.location?.search?.split('code=')[1]?.split('&')[0]
|
|
276
|
+
if (!code) return
|
|
277
|
+
|
|
278
|
+
const state = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE)
|
|
279
|
+
const urlState = window.location.search.split('state=')[1]?.split('&')[0]
|
|
280
|
+
if (!state || state !== urlState) {
|
|
281
|
+
log('error: auth state does not match')
|
|
282
|
+
setIsAuthReady(true)
|
|
436
283
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
setIsAuthReady(true)
|
|
284
|
+
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
|
|
285
|
+
cleanOAuthParams()
|
|
286
|
+
return
|
|
287
|
+
}
|
|
442
288
|
|
|
443
289
|
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
|
|
444
|
-
|
|
445
|
-
cleanOAuthParamsFromUrl()
|
|
446
|
-
return
|
|
447
|
-
}
|
|
290
|
+
cleanOAuthParams()
|
|
448
291
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
cleanOAuthParamsFromUrl()
|
|
452
|
-
|
|
453
|
-
fetchToken(code).catch((error) => {
|
|
454
|
-
log('Error fetching token:', error)
|
|
455
|
-
})
|
|
456
|
-
} else {
|
|
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)
|
|
292
|
+
fetchToken(code, false).catch((error) => {
|
|
293
|
+
log('Error fetching token:', error)
|
|
462
294
|
})
|
|
463
295
|
} else {
|
|
464
|
-
|
|
465
|
-
if (
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
296
|
+
const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
|
|
297
|
+
if (refreshToken) {
|
|
298
|
+
log('Found refresh token in storage, attempting to refresh access token')
|
|
299
|
+
fetchToken(refreshToken, true).catch((error) => {
|
|
300
|
+
log('Error fetching refresh token:', error)
|
|
301
|
+
})
|
|
302
|
+
} else {
|
|
303
|
+
let cookie_token = getCookie('basic_token')
|
|
304
|
+
if (cookie_token !== '') {
|
|
305
|
+
const tokenData = JSON.parse(cookie_token)
|
|
306
|
+
setToken(tokenData)
|
|
307
|
+
if (tokenData.refresh_token) {
|
|
308
|
+
await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, tokenData.refresh_token)
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
const cachedUserInfo = await storageAdapter.get(STORAGE_KEYS.USER_INFO)
|
|
312
|
+
if (cachedUserInfo) {
|
|
313
|
+
try {
|
|
314
|
+
const userData = JSON.parse(cachedUserInfo)
|
|
315
|
+
setUser(userData)
|
|
316
|
+
setIsSignedIn(true)
|
|
317
|
+
log('Loaded cached user info for offline mode')
|
|
318
|
+
} catch (error) {
|
|
319
|
+
log('Error parsing cached user info:', error)
|
|
320
|
+
}
|
|
481
321
|
}
|
|
322
|
+
setIsAuthReady(true)
|
|
482
323
|
}
|
|
483
|
-
setIsAuthReady(true)
|
|
484
324
|
}
|
|
485
325
|
}
|
|
486
|
-
}
|
|
487
326
|
|
|
488
327
|
} catch (e) {
|
|
489
328
|
log('error getting token', e)
|
|
490
329
|
}
|
|
491
330
|
}
|
|
492
|
-
|
|
331
|
+
|
|
493
332
|
initializeAuth()
|
|
494
333
|
}, [])
|
|
495
334
|
|
|
496
335
|
useEffect(() => {
|
|
497
336
|
async function fetchUser(acc_token: string) {
|
|
498
337
|
console.info('fetching user')
|
|
499
|
-
const user = await fetch(
|
|
338
|
+
const user = await fetch(`${authConfig.server_url}/auth/userInfo`, {
|
|
500
339
|
method: 'GET',
|
|
501
340
|
headers: {
|
|
502
341
|
'Authorization': `Bearer ${acc_token}`
|
|
@@ -512,13 +351,13 @@ export function BasicProvider({
|
|
|
512
351
|
if (token?.refresh_token) {
|
|
513
352
|
await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token)
|
|
514
353
|
}
|
|
515
|
-
|
|
354
|
+
|
|
516
355
|
await storageAdapter.set(STORAGE_KEYS.USER_INFO, JSON.stringify(user))
|
|
517
356
|
log('Cached user info in storage')
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
357
|
+
|
|
358
|
+
setCookie('basic_access_token', token?.access_token || '', { httpOnly: false });
|
|
359
|
+
setCookie('basic_token', JSON.stringify(token));
|
|
360
|
+
|
|
522
361
|
setUser(user)
|
|
523
362
|
setIsSignedIn(true)
|
|
524
363
|
|
|
@@ -540,11 +379,11 @@ export function BasicProvider({
|
|
|
540
379
|
if (isExpired) {
|
|
541
380
|
log('token is expired - refreshing ...')
|
|
542
381
|
try {
|
|
543
|
-
const newToken = await fetchToken(token?.refresh_token || '')
|
|
382
|
+
const newToken = await fetchToken(token?.refresh_token || '', true)
|
|
544
383
|
fetchUser(newToken?.access_token || '')
|
|
545
384
|
} catch (error) {
|
|
546
385
|
log('Failed to refresh token in checkToken:', error)
|
|
547
|
-
|
|
386
|
+
|
|
548
387
|
if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
|
|
549
388
|
log('Network issue - continuing with expired token until online')
|
|
550
389
|
fetchUser(token?.access_token || '')
|
|
@@ -559,7 +398,7 @@ export function BasicProvider({
|
|
|
559
398
|
|
|
560
399
|
if (token) {
|
|
561
400
|
checkToken()
|
|
562
|
-
}
|
|
401
|
+
}
|
|
563
402
|
}, [token])
|
|
564
403
|
|
|
565
404
|
const getSignInLink = async (redirectUri?: string) => {
|
|
@@ -579,14 +418,18 @@ export function BasicProvider({
|
|
|
579
418
|
throw new Error('Invalid redirect URI provided')
|
|
580
419
|
}
|
|
581
420
|
|
|
582
|
-
|
|
421
|
+
// Store redirect_uri for token exchange
|
|
422
|
+
await storageAdapter.set(STORAGE_KEYS.REDIRECT_URI, redirectUrl)
|
|
423
|
+
log('Stored redirect_uri for token exchange:', redirectUrl)
|
|
424
|
+
|
|
425
|
+
let baseUrl = `${authConfig.server_url}/auth/authorize`
|
|
583
426
|
baseUrl += `?client_id=${project_id}`
|
|
584
427
|
baseUrl += `&redirect_uri=${encodeURIComponent(redirectUrl)}`
|
|
585
428
|
baseUrl += `&response_type=code`
|
|
586
|
-
baseUrl += `&scope
|
|
429
|
+
baseUrl += `&scope=${encodeURIComponent(scopesString)}`
|
|
587
430
|
baseUrl += `&state=${randomState}`
|
|
588
431
|
|
|
589
|
-
log('Generated sign-in link successfully')
|
|
432
|
+
log('Generated sign-in link successfully with scopes:', scopesString)
|
|
590
433
|
return baseUrl;
|
|
591
434
|
|
|
592
435
|
} catch (error) {
|
|
@@ -598,33 +441,33 @@ export function BasicProvider({
|
|
|
598
441
|
const signin = async () => {
|
|
599
442
|
try {
|
|
600
443
|
log('signing in...')
|
|
601
|
-
|
|
444
|
+
|
|
602
445
|
if (!project_id) {
|
|
603
446
|
log('Error: project_id is required for sign-in')
|
|
604
447
|
throw new Error('Project ID is required for authentication')
|
|
605
448
|
}
|
|
606
|
-
|
|
449
|
+
|
|
607
450
|
const signInLink = await getSignInLink()
|
|
608
451
|
log('Generated sign-in link:', signInLink)
|
|
609
|
-
|
|
452
|
+
|
|
610
453
|
if (!signInLink || !signInLink.startsWith('https://')) {
|
|
611
454
|
log('Error: Invalid sign-in link generated')
|
|
612
455
|
throw new Error('Failed to generate valid sign-in URL')
|
|
613
456
|
}
|
|
614
|
-
|
|
457
|
+
|
|
615
458
|
window.location.href = signInLink
|
|
616
|
-
|
|
459
|
+
|
|
617
460
|
} catch (error) {
|
|
618
461
|
log('Error during sign-in:', error)
|
|
619
|
-
|
|
620
|
-
if (
|
|
462
|
+
|
|
463
|
+
if (isDevMode()) {
|
|
621
464
|
setError({
|
|
622
465
|
code: 'signin_error',
|
|
623
466
|
title: 'Sign-in Failed',
|
|
624
467
|
message: (error as Error).message || 'An error occurred during sign-in. Please try again.'
|
|
625
468
|
})
|
|
626
469
|
}
|
|
627
|
-
|
|
470
|
+
|
|
628
471
|
throw error
|
|
629
472
|
}
|
|
630
473
|
}
|
|
@@ -632,7 +475,7 @@ export function BasicProvider({
|
|
|
632
475
|
const signinWithCode = async (code: string, state?: string): Promise<{ success: boolean, error?: string }> => {
|
|
633
476
|
try {
|
|
634
477
|
log('signinWithCode called with code:', code)
|
|
635
|
-
|
|
478
|
+
|
|
636
479
|
if (!code || typeof code !== 'string') {
|
|
637
480
|
return { success: false, error: 'Invalid authorization code' }
|
|
638
481
|
}
|
|
@@ -646,10 +489,10 @@ export function BasicProvider({
|
|
|
646
489
|
}
|
|
647
490
|
|
|
648
491
|
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
|
|
649
|
-
|
|
492
|
+
cleanOAuthParams()
|
|
493
|
+
|
|
494
|
+
const token = await fetchToken(code, false)
|
|
650
495
|
|
|
651
|
-
const token = await fetchToken(code)
|
|
652
|
-
|
|
653
496
|
if (token) {
|
|
654
497
|
log('signinWithCode successful')
|
|
655
498
|
return { success: true }
|
|
@@ -658,9 +501,9 @@ export function BasicProvider({
|
|
|
658
501
|
}
|
|
659
502
|
} catch (error) {
|
|
660
503
|
log('signinWithCode error:', error)
|
|
661
|
-
return {
|
|
662
|
-
success: false,
|
|
663
|
-
error: (error as Error).message || 'Authentication failed'
|
|
504
|
+
return {
|
|
505
|
+
success: false,
|
|
506
|
+
error: (error as Error).message || 'Authentication failed'
|
|
664
507
|
}
|
|
665
508
|
}
|
|
666
509
|
}
|
|
@@ -670,17 +513,18 @@ export function BasicProvider({
|
|
|
670
513
|
setUser({})
|
|
671
514
|
setIsSignedIn(false)
|
|
672
515
|
setToken(null)
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
516
|
+
|
|
517
|
+
clearCookie('basic_token');
|
|
518
|
+
clearCookie('basic_access_token');
|
|
676
519
|
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
|
|
677
520
|
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
|
|
678
521
|
await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
|
|
522
|
+
await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI)
|
|
679
523
|
if (syncRef.current) {
|
|
680
524
|
(async () => {
|
|
681
525
|
try {
|
|
682
526
|
await syncRef.current?.close()
|
|
683
|
-
await syncRef.current?.delete({disableAutoOpen: false})
|
|
527
|
+
await syncRef.current?.delete({ disableAutoOpen: false })
|
|
684
528
|
syncRef.current = null
|
|
685
529
|
window?.location?.reload()
|
|
686
530
|
} catch (error) {
|
|
@@ -693,20 +537,19 @@ export function BasicProvider({
|
|
|
693
537
|
const getToken = async (): Promise<string> => {
|
|
694
538
|
log('getting token...')
|
|
695
539
|
|
|
696
|
-
|
|
697
540
|
if (!token) {
|
|
698
541
|
// Try to recover from storage refresh token
|
|
699
542
|
const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
|
|
700
543
|
if (refreshToken) {
|
|
701
544
|
log('No token in memory, attempting to refresh from storage')
|
|
702
545
|
try {
|
|
703
|
-
const newToken = await fetchToken(refreshToken)
|
|
546
|
+
const newToken = await fetchToken(refreshToken, true)
|
|
704
547
|
if (newToken?.access_token) {
|
|
705
548
|
return newToken.access_token
|
|
706
549
|
}
|
|
707
550
|
} catch (error) {
|
|
708
551
|
log('Failed to refresh token from storage:', error)
|
|
709
|
-
|
|
552
|
+
|
|
710
553
|
if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
|
|
711
554
|
log('Network issue - continuing with potentially expired token')
|
|
712
555
|
const lastToken = localStorage.getItem('basic_access_token')
|
|
@@ -715,7 +558,7 @@ export function BasicProvider({
|
|
|
715
558
|
}
|
|
716
559
|
throw new Error('Network offline - authentication will be retried when online')
|
|
717
560
|
}
|
|
718
|
-
|
|
561
|
+
|
|
719
562
|
throw new Error('Authentication expired. Please sign in again.')
|
|
720
563
|
}
|
|
721
564
|
}
|
|
@@ -731,16 +574,16 @@ export function BasicProvider({
|
|
|
731
574
|
const refreshToken = token?.refresh_token || await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
|
|
732
575
|
if (refreshToken) {
|
|
733
576
|
try {
|
|
734
|
-
const newToken = await fetchToken(refreshToken)
|
|
577
|
+
const newToken = await fetchToken(refreshToken, true)
|
|
735
578
|
return newToken?.access_token || ''
|
|
736
579
|
} catch (error) {
|
|
737
580
|
log('Failed to refresh expired token:', error)
|
|
738
|
-
|
|
581
|
+
|
|
739
582
|
if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
|
|
740
583
|
log('Network issue - using expired token until network is restored')
|
|
741
584
|
return token.access_token
|
|
742
585
|
}
|
|
743
|
-
|
|
586
|
+
|
|
744
587
|
throw new Error('Authentication expired. Please sign in again.')
|
|
745
588
|
}
|
|
746
589
|
} else {
|
|
@@ -751,22 +594,7 @@ export function BasicProvider({
|
|
|
751
594
|
return token?.access_token || ''
|
|
752
595
|
}
|
|
753
596
|
|
|
754
|
-
|
|
755
|
-
let cookieValue = '';
|
|
756
|
-
if (document.cookie && document.cookie !== '') {
|
|
757
|
-
const cookies = document.cookie.split(';');
|
|
758
|
-
for (let i = 0; i < cookies.length; i++) {
|
|
759
|
-
const cookie = cookies[i]?.trim();
|
|
760
|
-
if (cookie && cookie.substring(0, name.length + 1) === (name + '=')) {
|
|
761
|
-
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
762
|
-
break;
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
return cookieValue;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
const fetchToken = async (code: string) => {
|
|
597
|
+
const fetchToken = async (codeOrRefreshToken: string, isRefreshToken: boolean = false) => {
|
|
770
598
|
try {
|
|
771
599
|
if (!isOnline) {
|
|
772
600
|
log('Network is offline, marking refresh as pending')
|
|
@@ -774,12 +602,48 @@ export function BasicProvider({
|
|
|
774
602
|
throw new Error('Network offline - refresh will be retried when online')
|
|
775
603
|
}
|
|
776
604
|
|
|
777
|
-
|
|
605
|
+
let requestBody: any
|
|
606
|
+
|
|
607
|
+
if (isRefreshToken) {
|
|
608
|
+
// Refresh token request
|
|
609
|
+
requestBody = {
|
|
610
|
+
grant_type: 'refresh_token',
|
|
611
|
+
refresh_token: codeOrRefreshToken
|
|
612
|
+
}
|
|
613
|
+
// Include client_id if available for validation
|
|
614
|
+
if (project_id) {
|
|
615
|
+
requestBody.client_id = project_id
|
|
616
|
+
}
|
|
617
|
+
} else {
|
|
618
|
+
// Authorization code exchange
|
|
619
|
+
requestBody = {
|
|
620
|
+
grant_type: 'authorization_code',
|
|
621
|
+
code: codeOrRefreshToken
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Retrieve stored redirect_uri (required by OAuth2 spec)
|
|
625
|
+
const storedRedirectUri = await storageAdapter.get(STORAGE_KEYS.REDIRECT_URI)
|
|
626
|
+
if (storedRedirectUri) {
|
|
627
|
+
requestBody.redirect_uri = storedRedirectUri
|
|
628
|
+
log('Including redirect_uri in token exchange:', storedRedirectUri)
|
|
629
|
+
} else {
|
|
630
|
+
log('Warning: No redirect_uri found in storage for token exchange')
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Include client_id for validation
|
|
634
|
+
if (project_id) {
|
|
635
|
+
requestBody.client_id = project_id
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
log('Token exchange request body:', { ...requestBody, refresh_token: isRefreshToken ? '[REDACTED]' : undefined, code: !isRefreshToken ? '[REDACTED]' : undefined })
|
|
640
|
+
|
|
641
|
+
const token = await fetch(`${authConfig.server_url}/auth/token`, {
|
|
778
642
|
method: 'POST',
|
|
779
643
|
headers: {
|
|
780
644
|
'Content-Type': 'application/json'
|
|
781
645
|
},
|
|
782
|
-
body: JSON.stringify(
|
|
646
|
+
body: JSON.stringify(requestBody)
|
|
783
647
|
})
|
|
784
648
|
.then(response => response.json())
|
|
785
649
|
.catch(error => {
|
|
@@ -793,90 +657,64 @@ export function BasicProvider({
|
|
|
793
657
|
|
|
794
658
|
if (token.error) {
|
|
795
659
|
log('error fetching token', token.error)
|
|
796
|
-
|
|
660
|
+
|
|
797
661
|
if (token.error.includes('network') || token.error.includes('timeout')) {
|
|
798
662
|
setPendingRefresh(true)
|
|
799
663
|
throw new Error('Network issue - refresh will be retried when online')
|
|
800
664
|
}
|
|
801
|
-
|
|
665
|
+
|
|
802
666
|
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
|
|
803
667
|
await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
668
|
+
await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI)
|
|
669
|
+
clearCookie('basic_token');
|
|
670
|
+
clearCookie('basic_access_token');
|
|
671
|
+
|
|
807
672
|
setUser({})
|
|
808
673
|
setIsSignedIn(false)
|
|
809
674
|
setToken(null)
|
|
810
675
|
setIsAuthReady(true)
|
|
811
|
-
|
|
676
|
+
|
|
812
677
|
throw new Error(`Token refresh failed: ${token.error}`)
|
|
813
678
|
} else {
|
|
814
679
|
setToken(token)
|
|
815
680
|
setPendingRefresh(false)
|
|
816
|
-
|
|
681
|
+
|
|
817
682
|
if (token.refresh_token) {
|
|
818
683
|
await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token)
|
|
819
684
|
log('Updated refresh token in storage')
|
|
820
685
|
}
|
|
821
|
-
|
|
822
|
-
|
|
686
|
+
|
|
687
|
+
// Clean up redirect_uri after successful token exchange
|
|
688
|
+
if (!isRefreshToken) {
|
|
689
|
+
await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI)
|
|
690
|
+
log('Cleaned up redirect_uri from storage after successful exchange')
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
setCookie('basic_access_token', token.access_token, { httpOnly: false });
|
|
823
694
|
log('Updated access token in cookie')
|
|
824
695
|
}
|
|
825
696
|
return token
|
|
826
697
|
} catch (error) {
|
|
827
698
|
log('Token refresh error:', error)
|
|
828
|
-
|
|
699
|
+
|
|
829
700
|
if (!(error as Error).message.includes('offline') && !(error as Error).message.includes('Network')) {
|
|
830
701
|
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
|
|
831
702
|
await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
703
|
+
await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI)
|
|
704
|
+
clearCookie('basic_token');
|
|
705
|
+
clearCookie('basic_access_token');
|
|
706
|
+
|
|
835
707
|
setUser({})
|
|
836
708
|
setIsSignedIn(false)
|
|
837
709
|
setToken(null)
|
|
838
710
|
setIsAuthReady(true)
|
|
839
711
|
}
|
|
840
|
-
|
|
841
|
-
throw error
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
const db_ = (tableName: string) => {
|
|
847
|
-
const checkSignIn = () => {
|
|
848
|
-
if (!isSignedIn) {
|
|
849
|
-
throw new Error('cannot use db. user not logged in.')
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
return {
|
|
854
|
-
get: async () => {
|
|
855
|
-
checkSignIn()
|
|
856
|
-
const tok = await getToken()
|
|
857
|
-
return get({ projectId: project_id, accountId: user?.id, tableName: tableName, token: tok })
|
|
858
|
-
},
|
|
859
|
-
add: async (value: any) => {
|
|
860
|
-
checkSignIn()
|
|
861
|
-
const tok = await getToken()
|
|
862
|
-
return add({ projectId: project_id, accountId: user?.id, tableName: tableName, value: value, token: tok })
|
|
863
|
-
},
|
|
864
|
-
update: async (id: string, value: any) => {
|
|
865
|
-
checkSignIn()
|
|
866
|
-
const tok = await getToken()
|
|
867
|
-
return update({ projectId: project_id, accountId: user?.id, tableName: tableName, id: id, value: value, token: tok })
|
|
868
|
-
},
|
|
869
|
-
delete: async (id: string) => {
|
|
870
|
-
checkSignIn()
|
|
871
|
-
const tok = await getToken()
|
|
872
|
-
return deleteRecord({ projectId: project_id, accountId: user?.id, tableName: tableName, id: id, token: tok })
|
|
873
|
-
}
|
|
874
712
|
|
|
713
|
+
throw error
|
|
875
714
|
}
|
|
876
|
-
|
|
877
715
|
}
|
|
878
716
|
|
|
879
|
-
const noDb = ({
|
|
717
|
+
const noDb = ({
|
|
880
718
|
collection: () => {
|
|
881
719
|
throw new Error('no basicdb found - initialization failed. double check your schema.')
|
|
882
720
|
}
|
|
@@ -896,17 +734,17 @@ export function BasicProvider({
|
|
|
896
734
|
db: syncRef.current ? syncRef.current : noDb,
|
|
897
735
|
dbStatus
|
|
898
736
|
}}>
|
|
899
|
-
|
|
900
|
-
{error &&
|
|
737
|
+
|
|
738
|
+
{error && isDevMode() && <ErrorDisplay error={error} />}
|
|
901
739
|
{isReady && children}
|
|
902
740
|
</BasicContext.Provider>
|
|
903
741
|
)
|
|
904
742
|
}
|
|
905
743
|
|
|
906
744
|
function ErrorDisplay({ error }: { error: ErrorObject }) {
|
|
907
|
-
return <div style={{
|
|
745
|
+
return <div style={{
|
|
908
746
|
position: 'absolute',
|
|
909
|
-
top: 20,
|
|
747
|
+
top: 20,
|
|
910
748
|
left: 20,
|
|
911
749
|
color: 'black',
|
|
912
750
|
backgroundColor: '#f8d7da',
|
|
@@ -916,10 +754,10 @@ function ErrorDisplay({ error }: { error: ErrorObject }) {
|
|
|
916
754
|
maxWidth: '400px',
|
|
917
755
|
margin: '20px auto',
|
|
918
756
|
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
|
|
919
|
-
fontFamily: 'monospace',
|
|
920
|
-
|
|
921
|
-
<h3 style={{fontSize: '0.8rem', opacity: 0.8}}>code: {error.code}</h3>
|
|
922
|
-
<h1 style={{fontSize: '1.2rem', lineHeight: '1.5'}}>{error.title}</h1>
|
|
757
|
+
fontFamily: 'monospace',
|
|
758
|
+
}}>
|
|
759
|
+
<h3 style={{ fontSize: '0.8rem', opacity: 0.8 }}>code: {error.code}</h3>
|
|
760
|
+
<h1 style={{ fontSize: '1.2rem', lineHeight: '1.5' }}>{error.title}</h1>
|
|
923
761
|
<p>{error.message}</p>
|
|
924
762
|
</div>
|
|
925
763
|
}
|