@basictech/react 0.7.0-beta.0 → 0.7.0-beta.2
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 -5
- package/dist/index.d.mts +121 -5
- package/dist/index.d.ts +121 -5
- package/dist/index.js +978 -244
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +977 -244
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/readme.md +127 -1
- package/src/AuthContext.tsx +427 -347
- package/src/index.ts +6 -31
- 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 +62 -0
package/src/AuthContext.tsx
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
|
|
3
1
|
import React, { createContext, useContext, useEffect, useState, useRef } from 'react'
|
|
4
2
|
import { jwtDecode } from 'jwt-decode'
|
|
5
3
|
|
|
6
4
|
import { BasicSync } from './sync'
|
|
7
|
-
import { get, add, update, deleteRecord } from './db'
|
|
8
|
-
import { validateSchema, compareSchemas } from '@basictech/schema'
|
|
9
5
|
|
|
10
6
|
import { log } from './config'
|
|
11
|
-
import {version as currentVersion} from '../package.json'
|
|
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
|
+
|
|
12
16
|
|
|
13
17
|
type BasicSyncType = {
|
|
14
18
|
basic_schema: any;
|
|
@@ -20,7 +24,7 @@ type BasicSyncType = {
|
|
|
20
24
|
count: () => Promise<number>;
|
|
21
25
|
};
|
|
22
26
|
};
|
|
23
|
-
[key: string]: any;
|
|
27
|
+
[key: string]: any;
|
|
24
28
|
};
|
|
25
29
|
|
|
26
30
|
|
|
@@ -46,7 +50,7 @@ type Token = {
|
|
|
46
50
|
access_token: string,
|
|
47
51
|
token_type: string,
|
|
48
52
|
expires_in: number,
|
|
49
|
-
|
|
53
|
+
refresh_token: string,
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
export const BasicContext = createContext<{
|
|
@@ -54,10 +58,11 @@ export const BasicContext = createContext<{
|
|
|
54
58
|
isAuthReady: boolean,
|
|
55
59
|
isSignedIn: boolean,
|
|
56
60
|
user: User | null,
|
|
57
|
-
signout: () => void
|
|
58
|
-
signin: () => void
|
|
61
|
+
signout: () => Promise<void>,
|
|
62
|
+
signin: () => Promise<void>,
|
|
63
|
+
signinWithCode: (code: string, state?: string) => Promise<{ success: boolean, error?: string }>,
|
|
59
64
|
getToken: () => Promise<string>,
|
|
60
|
-
getSignInLink: () => string
|
|
65
|
+
getSignInLink: (redirectUri?: string) => Promise<string>,
|
|
61
66
|
db: any,
|
|
62
67
|
dbStatus: DBStatus
|
|
63
68
|
}>({
|
|
@@ -65,165 +70,34 @@ export const BasicContext = createContext<{
|
|
|
65
70
|
isAuthReady: false,
|
|
66
71
|
isSignedIn: false,
|
|
67
72
|
user: null,
|
|
68
|
-
signout: () =>
|
|
69
|
-
signin: () =>
|
|
73
|
+
signout: () => Promise.resolve(),
|
|
74
|
+
signin: () => Promise.resolve(),
|
|
75
|
+
signinWithCode: () => new Promise(() => { }),
|
|
70
76
|
getToken: () => new Promise(() => { }),
|
|
71
|
-
getSignInLink: () => "",
|
|
77
|
+
getSignInLink: () => Promise.resolve(""),
|
|
72
78
|
db: {},
|
|
73
79
|
dbStatus: DBStatus.LOADING
|
|
74
80
|
});
|
|
75
81
|
|
|
76
|
-
const EmptyDB: BasicSyncType = {
|
|
77
|
-
isOpen: false,
|
|
78
|
-
collection: () => {
|
|
79
|
-
return {
|
|
80
|
-
ref: {
|
|
81
|
-
toArray: () => [],
|
|
82
|
-
count: () => 0
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function getSchemaStatus(schema: any) {
|
|
89
|
-
const projectId = schema.project_id
|
|
90
|
-
let status = ''
|
|
91
|
-
const valid = validateSchema(schema)
|
|
92
|
-
|
|
93
|
-
if (!valid.valid) {
|
|
94
|
-
console.warn('BasicDB Error: your local schema is invalid. Please fix errors and try again - sync is disabled')
|
|
95
|
-
return {
|
|
96
|
-
valid: false,
|
|
97
|
-
status: 'invalid',
|
|
98
|
-
latest: null
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const latestSchema = await fetch(`https://api.basic.tech/project/${projectId}/schema`)
|
|
103
|
-
.then(res => res.json())
|
|
104
|
-
.then(data => data.data[0].schema)
|
|
105
|
-
.catch(err => {
|
|
106
|
-
return {
|
|
107
|
-
valid: false,
|
|
108
|
-
status: 'error',
|
|
109
|
-
latest: null
|
|
110
|
-
}
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
console.log('latestSchema', latestSchema)
|
|
114
|
-
|
|
115
|
-
if (!latestSchema.version) {
|
|
116
|
-
return {
|
|
117
|
-
valid: false,
|
|
118
|
-
status: 'error',
|
|
119
|
-
latest: null
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (latestSchema.version > schema.version) {
|
|
124
|
-
// error_code: schema_behind
|
|
125
|
-
console.warn('BasicDB Error: your local schema version is behind the latest. Found version:', schema.version, 'but expected', latestSchema.version, " - sync is disabled")
|
|
126
|
-
return {
|
|
127
|
-
valid: false,
|
|
128
|
-
status: 'behind',
|
|
129
|
-
latest: latestSchema
|
|
130
|
-
}
|
|
131
|
-
} else if (latestSchema.version < schema.version) {
|
|
132
|
-
// error_code: schema_ahead
|
|
133
|
-
console.warn('BasicDB Error: your local schema version is ahead of the latest. Found version:', schema.version, 'but expected', latestSchema.version, " - sync is disabled")
|
|
134
|
-
return {
|
|
135
|
-
valid: false,
|
|
136
|
-
status: 'ahead',
|
|
137
|
-
latest: latestSchema
|
|
138
|
-
}
|
|
139
|
-
} else if (latestSchema.version === schema.version) {
|
|
140
|
-
const changes = compareSchemas(schema, latestSchema)
|
|
141
|
-
if (changes.valid) {
|
|
142
|
-
return {
|
|
143
|
-
valid: true,
|
|
144
|
-
status: 'current',
|
|
145
|
-
latest: latestSchema
|
|
146
|
-
}
|
|
147
|
-
} else {
|
|
148
|
-
// error_code: schema_conflict
|
|
149
|
-
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")
|
|
150
|
-
return {
|
|
151
|
-
valid: false,
|
|
152
|
-
status: 'conflict',
|
|
153
|
-
latest: latestSchema
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
} else {
|
|
157
|
-
return {
|
|
158
|
-
valid: false,
|
|
159
|
-
status: 'error',
|
|
160
|
-
latest: null
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
function getSyncStatus(statusCode: number): string {
|
|
167
|
-
switch (statusCode) {
|
|
168
|
-
case -1:
|
|
169
|
-
return "ERROR";
|
|
170
|
-
case 0:
|
|
171
|
-
return "OFFLINE";
|
|
172
|
-
case 1:
|
|
173
|
-
return "CONNECTING";
|
|
174
|
-
case 2:
|
|
175
|
-
return "ONLINE";
|
|
176
|
-
case 3:
|
|
177
|
-
return "SYNCING";
|
|
178
|
-
case 4:
|
|
179
|
-
return "ERROR_WILL_RETRY";
|
|
180
|
-
default:
|
|
181
|
-
return "UNKNOWN";
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
82
|
type ErrorObject = {
|
|
186
83
|
code: string;
|
|
187
84
|
title: string;
|
|
188
85
|
message: string;
|
|
189
86
|
}
|
|
190
87
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (latestVersion !== currentVersion) {
|
|
205
|
-
console.warn('[basic] New version available:', latestVersion, `\nrun "npm install @basictech/react@${latestVersion}" to update`);
|
|
206
|
-
}
|
|
207
|
-
if (isBeta) {
|
|
208
|
-
log('thank you for being on basictech/react beta :)')
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
hasNewVersion: currentVersion !== latestVersion,
|
|
213
|
-
latestVersion,
|
|
214
|
-
currentVersion
|
|
215
|
-
};
|
|
216
|
-
} catch (error) {
|
|
217
|
-
log('Error checking for new version:', error);
|
|
218
|
-
return {
|
|
219
|
-
hasNewVersion: false,
|
|
220
|
-
latestVersion: null,
|
|
221
|
-
currentVersion: null
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export function BasicProvider({ children, project_id, schema, debug = false }: { children: React.ReactNode, project_id?: string, schema?: any, debug?: boolean }) {
|
|
88
|
+
export function BasicProvider({
|
|
89
|
+
children,
|
|
90
|
+
project_id,
|
|
91
|
+
schema,
|
|
92
|
+
debug = false,
|
|
93
|
+
storage
|
|
94
|
+
}: {
|
|
95
|
+
children: React.ReactNode,
|
|
96
|
+
project_id?: string,
|
|
97
|
+
schema?: any,
|
|
98
|
+
debug?: boolean,
|
|
99
|
+
storage?: BasicStorage
|
|
100
|
+
}) {
|
|
227
101
|
const [isAuthReady, setIsAuthReady] = useState(false)
|
|
228
102
|
const [isSignedIn, setIsSignedIn] = useState<boolean>(false)
|
|
229
103
|
const [token, setToken] = useState<Token | null>(null)
|
|
@@ -233,50 +107,82 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
233
107
|
|
|
234
108
|
const [dbStatus, setDbStatus] = useState<DBStatus>(DBStatus.OFFLINE)
|
|
235
109
|
const [error, setError] = useState<ErrorObject | null>(null)
|
|
110
|
+
const [isOnline, setIsOnline] = useState<boolean>(navigator.onLine)
|
|
111
|
+
const [pendingRefresh, setPendingRefresh] = useState<boolean>(false)
|
|
236
112
|
|
|
237
113
|
const syncRef = useRef<BasicSync | null>(null);
|
|
114
|
+
const storageAdapter = storage || new LocalStorageAdapter();
|
|
115
|
+
|
|
116
|
+
const isDevMode = () => isDevelopment(debug)
|
|
117
|
+
|
|
118
|
+
const cleanOAuthParams = () => cleanOAuthParamsFromUrl()
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
const handleOnline = () => {
|
|
122
|
+
log('Network came back online')
|
|
123
|
+
setIsOnline(true)
|
|
124
|
+
if (pendingRefresh) {
|
|
125
|
+
log('Retrying pending token refresh')
|
|
126
|
+
setPendingRefresh(false)
|
|
127
|
+
if (token) {
|
|
128
|
+
const refreshToken = token.refresh_token || localStorage.getItem('basic_refresh_token')
|
|
129
|
+
if (refreshToken) {
|
|
130
|
+
fetchToken(refreshToken, true).catch(error => {
|
|
131
|
+
log('Retry refresh failed:', error)
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const handleOffline = () => {
|
|
139
|
+
log('Network went offline')
|
|
140
|
+
setIsOnline(false)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
window.addEventListener('online', handleOnline)
|
|
144
|
+
window.addEventListener('offline', handleOffline)
|
|
145
|
+
|
|
146
|
+
return () => {
|
|
147
|
+
window.removeEventListener('online', handleOnline)
|
|
148
|
+
window.removeEventListener('offline', handleOffline)
|
|
149
|
+
}
|
|
150
|
+
}, [pendingRefresh, token])
|
|
238
151
|
|
|
239
152
|
useEffect(() => {
|
|
240
153
|
function initDb(options: { shouldConnect: boolean }) {
|
|
241
154
|
if (!syncRef.current) {
|
|
242
155
|
log('Initializing Basic DB')
|
|
243
156
|
syncRef.current = new BasicSync('basicdb', { schema: schema });
|
|
244
|
-
|
|
157
|
+
|
|
245
158
|
syncRef.current.syncable.on('statusChanged', (status: number, url: string) => {
|
|
246
|
-
setDbStatus(getSyncStatus(status))
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
syncRef.current.syncable.getStatus().then((status) => {
|
|
250
|
-
setDbStatus(getSyncStatus(status))
|
|
159
|
+
setDbStatus(getSyncStatus(status) as DBStatus)
|
|
251
160
|
})
|
|
252
161
|
|
|
253
|
-
|
|
162
|
+
// syncRef.current.syncable.getStatus().then((status: number) => {
|
|
163
|
+
// setDbStatus(getSyncStatus(status) as DBStatus)
|
|
164
|
+
// })
|
|
165
|
+
|
|
166
|
+
if (options.shouldConnect) {
|
|
254
167
|
setShouldConnect(true)
|
|
255
|
-
} else {
|
|
168
|
+
} else {
|
|
256
169
|
log('Sync is disabled')
|
|
257
170
|
}
|
|
258
171
|
|
|
259
172
|
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
173
|
}
|
|
267
174
|
}
|
|
268
175
|
|
|
269
176
|
async function checkSchema() {
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
console.group('Schema Errors')
|
|
177
|
+
const result = await validateAndCheckSchema(schema)
|
|
178
|
+
|
|
179
|
+
if (!result.isValid) {
|
|
274
180
|
let errorMessage = ''
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
181
|
+
if (result.errors) {
|
|
182
|
+
result.errors.forEach((error, index) => {
|
|
183
|
+
errorMessage += `${index + 1}: ${error.message} - at ${error.instancePath}\n`
|
|
184
|
+
})
|
|
185
|
+
}
|
|
280
186
|
setError({
|
|
281
187
|
code: 'schema_invalid',
|
|
282
188
|
title: 'Basic Schema is invalid!',
|
|
@@ -286,22 +192,13 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
286
192
|
return null
|
|
287
193
|
}
|
|
288
194
|
|
|
289
|
-
|
|
290
|
-
let schemaStatus = { valid: false }
|
|
291
|
-
if (schema.version !== 0) {
|
|
292
|
-
schemaStatus = await getSchemaStatus(schema)
|
|
293
|
-
log('schemaStatus', schemaStatus)
|
|
294
|
-
}else {
|
|
295
|
-
log("schema not published - at version 0")
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (schemaStatus.valid) {
|
|
195
|
+
if (result.schemaStatus.valid) {
|
|
299
196
|
initDb({ shouldConnect: true })
|
|
300
197
|
} else {
|
|
301
|
-
log('Schema is invalid!', schemaStatus)
|
|
198
|
+
log('Schema is invalid!', result.schemaStatus)
|
|
302
199
|
initDb({ shouldConnect: false })
|
|
303
200
|
}
|
|
304
|
-
|
|
201
|
+
|
|
305
202
|
checkForNewVersion()
|
|
306
203
|
}
|
|
307
204
|
|
|
@@ -312,46 +209,104 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
312
209
|
}
|
|
313
210
|
}, []);
|
|
314
211
|
|
|
315
|
-
|
|
316
212
|
useEffect(() => {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
213
|
+
async function connectToDb() {
|
|
214
|
+
if (token && syncRef.current && isSignedIn && shouldConnect) {
|
|
215
|
+
const tok = await getToken()
|
|
216
|
+
if (!tok) {
|
|
217
|
+
log('no token found')
|
|
218
|
+
return
|
|
219
|
+
}
|
|
321
220
|
|
|
322
|
-
|
|
323
|
-
localStorage.setItem('basic_debug', debug ? 'true' : 'false')
|
|
221
|
+
log('connecting to db...')
|
|
324
222
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
223
|
+
syncRef.current?.connect({ access_token: tok })
|
|
224
|
+
.catch((e) => {
|
|
225
|
+
log('error connecting to db', e)
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
connectToDb()
|
|
328
230
|
|
|
329
|
-
|
|
330
|
-
if (!state || state !== window.location.search.split('state=')[1].split('&')[0]) {
|
|
331
|
-
log('error: auth state does not match')
|
|
332
|
-
setIsAuthReady(true)
|
|
231
|
+
}, [isSignedIn, shouldConnect])
|
|
333
232
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
const initializeAuth = async () => {
|
|
235
|
+
await storageAdapter.set(STORAGE_KEYS.DEBUG, debug ? 'true' : 'false')
|
|
338
236
|
|
|
339
|
-
|
|
237
|
+
try {
|
|
238
|
+
const versionUpdater = createVersionUpdater(storageAdapter, currentVersion, getMigrations())
|
|
239
|
+
const updateResult = await versionUpdater.checkAndUpdate()
|
|
340
240
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
setToken(JSON.parse(cookie_token))
|
|
346
|
-
} else {
|
|
347
|
-
setIsAuthReady(true)
|
|
241
|
+
if (updateResult.updated) {
|
|
242
|
+
log(`App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`)
|
|
243
|
+
} else {
|
|
244
|
+
log(`App version ${updateResult.toVersion} is current`)
|
|
348
245
|
}
|
|
246
|
+
} catch (error) {
|
|
247
|
+
log('Version update failed:', error)
|
|
349
248
|
}
|
|
350
249
|
|
|
250
|
+
try {
|
|
251
|
+
if (window.location.search.includes('code')) {
|
|
252
|
+
let code = window.location?.search?.split('code=')[1]?.split('&')[0]
|
|
253
|
+
if (!code) return
|
|
254
|
+
|
|
255
|
+
const state = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE)
|
|
256
|
+
const urlState = window.location.search.split('state=')[1]?.split('&')[0]
|
|
257
|
+
if (!state || state !== urlState) {
|
|
258
|
+
log('error: auth state does not match')
|
|
259
|
+
setIsAuthReady(true)
|
|
260
|
+
|
|
261
|
+
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
|
|
262
|
+
cleanOAuthParams()
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
|
|
267
|
+
cleanOAuthParams()
|
|
268
|
+
|
|
269
|
+
fetchToken(code, false).catch((error) => {
|
|
270
|
+
log('Error fetching token:', error)
|
|
271
|
+
})
|
|
272
|
+
} else {
|
|
273
|
+
const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
|
|
274
|
+
if (refreshToken) {
|
|
275
|
+
log('Found refresh token in storage, attempting to refresh access token')
|
|
276
|
+
fetchToken(refreshToken, true).catch((error) => {
|
|
277
|
+
log('Error fetching refresh token:', error)
|
|
278
|
+
})
|
|
279
|
+
} else {
|
|
280
|
+
let cookie_token = getCookie('basic_token')
|
|
281
|
+
if (cookie_token !== '') {
|
|
282
|
+
const tokenData = JSON.parse(cookie_token)
|
|
283
|
+
setToken(tokenData)
|
|
284
|
+
if (tokenData.refresh_token) {
|
|
285
|
+
await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, tokenData.refresh_token)
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
const cachedUserInfo = await storageAdapter.get(STORAGE_KEYS.USER_INFO)
|
|
289
|
+
if (cachedUserInfo) {
|
|
290
|
+
try {
|
|
291
|
+
const userData = JSON.parse(cachedUserInfo)
|
|
292
|
+
setUser(userData)
|
|
293
|
+
setIsSignedIn(true)
|
|
294
|
+
log('Loaded cached user info for offline mode')
|
|
295
|
+
} catch (error) {
|
|
296
|
+
log('Error parsing cached user info:', error)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
setIsAuthReady(true)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
351
303
|
|
|
352
|
-
|
|
353
|
-
|
|
304
|
+
} catch (e) {
|
|
305
|
+
log('error getting token', e)
|
|
306
|
+
}
|
|
354
307
|
}
|
|
308
|
+
|
|
309
|
+
initializeAuth()
|
|
355
310
|
}, [])
|
|
356
311
|
|
|
357
312
|
useEffect(() => {
|
|
@@ -368,16 +323,18 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
368
323
|
|
|
369
324
|
if (user.error) {
|
|
370
325
|
log('error fetching user', user.error)
|
|
371
|
-
// refreshToken()
|
|
372
326
|
return
|
|
373
327
|
} else {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (window.location.search.includes('code')) {
|
|
378
|
-
window.history.pushState({}, document.title, "/");
|
|
328
|
+
if (token?.refresh_token) {
|
|
329
|
+
await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token)
|
|
379
330
|
}
|
|
380
|
-
|
|
331
|
+
|
|
332
|
+
await storageAdapter.set(STORAGE_KEYS.USER_INFO, JSON.stringify(user))
|
|
333
|
+
log('Cached user info in storage')
|
|
334
|
+
|
|
335
|
+
setCookie('basic_access_token', token?.access_token || '', { httpOnly: false });
|
|
336
|
+
setCookie('basic_token', JSON.stringify(token));
|
|
337
|
+
|
|
381
338
|
setUser(user)
|
|
382
339
|
setIsSignedIn(true)
|
|
383
340
|
|
|
@@ -398,77 +355,148 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
398
355
|
|
|
399
356
|
if (isExpired) {
|
|
400
357
|
log('token is expired - refreshing ...')
|
|
401
|
-
|
|
402
|
-
|
|
358
|
+
try {
|
|
359
|
+
const newToken = await fetchToken(token?.refresh_token || '', true)
|
|
360
|
+
fetchUser(newToken?.access_token || '')
|
|
361
|
+
} catch (error) {
|
|
362
|
+
log('Failed to refresh token in checkToken:', error)
|
|
363
|
+
|
|
364
|
+
if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
|
|
365
|
+
log('Network issue - continuing with expired token until online')
|
|
366
|
+
fetchUser(token?.access_token || '')
|
|
367
|
+
} else {
|
|
368
|
+
setIsAuthReady(true)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
403
371
|
} else {
|
|
404
|
-
fetchUser(token
|
|
372
|
+
fetchUser(token?.access_token || '')
|
|
405
373
|
}
|
|
406
374
|
}
|
|
407
375
|
|
|
408
376
|
if (token) {
|
|
409
377
|
checkToken()
|
|
410
|
-
}
|
|
378
|
+
}
|
|
411
379
|
}, [token])
|
|
412
380
|
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
log('no token found')
|
|
417
|
-
return
|
|
418
|
-
}
|
|
381
|
+
const getSignInLink = async (redirectUri?: string) => {
|
|
382
|
+
try {
|
|
383
|
+
log('getting sign in link...')
|
|
419
384
|
|
|
420
|
-
|
|
385
|
+
if (!project_id) {
|
|
386
|
+
throw new Error('Project ID is required to generate sign-in link')
|
|
387
|
+
}
|
|
421
388
|
|
|
422
|
-
|
|
389
|
+
const randomState = Math.random().toString(36).substring(6);
|
|
390
|
+
await storageAdapter.set(STORAGE_KEYS.AUTH_STATE, randomState)
|
|
423
391
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
392
|
+
const redirectUrl = redirectUri || window.location.href
|
|
393
|
+
|
|
394
|
+
if (!redirectUrl || (!redirectUrl.startsWith('http://') && !redirectUrl.startsWith('https://'))) {
|
|
395
|
+
throw new Error('Invalid redirect URI provided')
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
let baseUrl = "https://api.basic.tech/auth/authorize"
|
|
399
|
+
baseUrl += `?client_id=${project_id}`
|
|
400
|
+
baseUrl += `&redirect_uri=${encodeURIComponent(redirectUrl)}`
|
|
401
|
+
baseUrl += `&response_type=code`
|
|
402
|
+
baseUrl += `&scope=profile`
|
|
403
|
+
baseUrl += `&state=${randomState}`
|
|
404
|
+
|
|
405
|
+
log('Generated sign-in link successfully')
|
|
406
|
+
return baseUrl;
|
|
407
|
+
|
|
408
|
+
} catch (error) {
|
|
409
|
+
log('Error generating sign-in link:', error)
|
|
410
|
+
throw error
|
|
411
|
+
}
|
|
428
412
|
}
|
|
429
413
|
|
|
430
|
-
const
|
|
431
|
-
|
|
414
|
+
const signin = async () => {
|
|
415
|
+
try {
|
|
416
|
+
log('signing in...')
|
|
417
|
+
|
|
418
|
+
if (!project_id) {
|
|
419
|
+
log('Error: project_id is required for sign-in')
|
|
420
|
+
throw new Error('Project ID is required for authentication')
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const signInLink = await getSignInLink()
|
|
424
|
+
log('Generated sign-in link:', signInLink)
|
|
425
|
+
|
|
426
|
+
if (!signInLink || !signInLink.startsWith('https://')) {
|
|
427
|
+
log('Error: Invalid sign-in link generated')
|
|
428
|
+
throw new Error('Failed to generate valid sign-in URL')
|
|
429
|
+
}
|
|
432
430
|
|
|
433
|
-
|
|
434
|
-
localStorage.setItem('basic_auth_state', randomState)
|
|
431
|
+
window.location.href = signInLink
|
|
435
432
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
baseUrl += `&redirect_uri=${encodeURIComponent(window.location.href)}`
|
|
439
|
-
baseUrl += `&response_type=code`
|
|
440
|
-
baseUrl += `&scope=profile`
|
|
441
|
-
baseUrl += `&state=${randomState}`
|
|
433
|
+
} catch (error) {
|
|
434
|
+
log('Error during sign-in:', error)
|
|
442
435
|
|
|
443
|
-
|
|
436
|
+
if (isDevMode()) {
|
|
437
|
+
setError({
|
|
438
|
+
code: 'signin_error',
|
|
439
|
+
title: 'Sign-in Failed',
|
|
440
|
+
message: (error as Error).message || 'An error occurred during sign-in. Please try again.'
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
throw error
|
|
445
|
+
}
|
|
444
446
|
}
|
|
445
447
|
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
448
|
+
const signinWithCode = async (code: string, state?: string): Promise<{ success: boolean, error?: string }> => {
|
|
449
|
+
try {
|
|
450
|
+
log('signinWithCode called with code:', code)
|
|
451
|
+
|
|
452
|
+
if (!code || typeof code !== 'string') {
|
|
453
|
+
return { success: false, error: 'Invalid authorization code' }
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (state) {
|
|
457
|
+
const storedState = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE)
|
|
458
|
+
if (storedState && storedState !== state) {
|
|
459
|
+
log('State parameter mismatch:', { provided: state, stored: storedState })
|
|
460
|
+
return { success: false, error: 'State parameter mismatch' }
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
|
|
465
|
+
cleanOAuthParams()
|
|
466
|
+
|
|
467
|
+
const token = await fetchToken(code, false)
|
|
468
|
+
|
|
469
|
+
if (token) {
|
|
470
|
+
log('signinWithCode successful')
|
|
471
|
+
return { success: true }
|
|
472
|
+
} else {
|
|
473
|
+
return { success: false, error: 'Failed to exchange code for token' }
|
|
474
|
+
}
|
|
475
|
+
} catch (error) {
|
|
476
|
+
log('signinWithCode error:', error)
|
|
477
|
+
return {
|
|
478
|
+
success: false,
|
|
479
|
+
error: (error as Error).message || 'Authentication failed'
|
|
480
|
+
}
|
|
481
|
+
}
|
|
451
482
|
}
|
|
452
483
|
|
|
453
|
-
const signout = () => {
|
|
484
|
+
const signout = async () => {
|
|
454
485
|
log('signing out!')
|
|
455
486
|
setUser({})
|
|
456
487
|
setIsSignedIn(false)
|
|
457
488
|
setToken(null)
|
|
458
|
-
document.cookie = `basic_token=; Secure; SameSite=Strict`;
|
|
459
|
-
localStorage.removeItem('basic_auth_state')
|
|
460
|
-
|
|
461
|
-
// if (syncRef.current) {
|
|
462
|
-
// // WIP - BUG - sometimes connects even after signout
|
|
463
|
-
// syncRef.current.disconnect()
|
|
464
489
|
|
|
465
|
-
|
|
466
|
-
|
|
490
|
+
clearCookie('basic_token');
|
|
491
|
+
clearCookie('basic_access_token');
|
|
492
|
+
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
|
|
493
|
+
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
|
|
494
|
+
await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
|
|
467
495
|
if (syncRef.current) {
|
|
468
496
|
(async () => {
|
|
469
497
|
try {
|
|
470
|
-
await syncRef.current
|
|
471
|
-
await syncRef.current
|
|
498
|
+
await syncRef.current?.close()
|
|
499
|
+
await syncRef.current?.delete({ disableAutoOpen: false })
|
|
472
500
|
syncRef.current = null
|
|
473
501
|
window?.location?.reload()
|
|
474
502
|
} catch (error) {
|
|
@@ -482,6 +510,30 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
482
510
|
log('getting token...')
|
|
483
511
|
|
|
484
512
|
if (!token) {
|
|
513
|
+
// Try to recover from storage refresh token
|
|
514
|
+
const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
|
|
515
|
+
if (refreshToken) {
|
|
516
|
+
log('No token in memory, attempting to refresh from storage')
|
|
517
|
+
try {
|
|
518
|
+
const newToken = await fetchToken(refreshToken, true)
|
|
519
|
+
if (newToken?.access_token) {
|
|
520
|
+
return newToken.access_token
|
|
521
|
+
}
|
|
522
|
+
} catch (error) {
|
|
523
|
+
log('Failed to refresh token from storage:', error)
|
|
524
|
+
|
|
525
|
+
if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
|
|
526
|
+
log('Network issue - continuing with potentially expired token')
|
|
527
|
+
const lastToken = localStorage.getItem('basic_access_token')
|
|
528
|
+
if (lastToken) {
|
|
529
|
+
return lastToken
|
|
530
|
+
}
|
|
531
|
+
throw new Error('Network offline - authentication will be retried when online')
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
throw new Error('Authentication expired. Please sign in again.')
|
|
535
|
+
}
|
|
536
|
+
}
|
|
485
537
|
log('no token found')
|
|
486
538
|
throw new Error('no token found')
|
|
487
539
|
}
|
|
@@ -491,84 +543,116 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
491
543
|
|
|
492
544
|
if (isExpired) {
|
|
493
545
|
log('token is expired - refreshing ...')
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
546
|
+
const refreshToken = token?.refresh_token || await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
|
|
547
|
+
if (refreshToken) {
|
|
548
|
+
try {
|
|
549
|
+
const newToken = await fetchToken(refreshToken, true)
|
|
550
|
+
return newToken?.access_token || ''
|
|
551
|
+
} catch (error) {
|
|
552
|
+
log('Failed to refresh expired token:', error)
|
|
497
553
|
|
|
498
|
-
|
|
499
|
-
|
|
554
|
+
if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
|
|
555
|
+
log('Network issue - using expired token until network is restored')
|
|
556
|
+
return token.access_token
|
|
557
|
+
}
|
|
500
558
|
|
|
501
|
-
|
|
502
|
-
let cookieValue = '';
|
|
503
|
-
if (document.cookie && document.cookie !== '') {
|
|
504
|
-
const cookies = document.cookie.split(';');
|
|
505
|
-
for (let i = 0; i < cookies.length; i++) {
|
|
506
|
-
const cookie = cookies[i].trim();
|
|
507
|
-
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
|
508
|
-
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
509
|
-
break;
|
|
559
|
+
throw new Error('Authentication expired. Please sign in again.')
|
|
510
560
|
}
|
|
561
|
+
} else {
|
|
562
|
+
throw new Error('no refresh token available')
|
|
511
563
|
}
|
|
512
564
|
}
|
|
513
|
-
return cookieValue;
|
|
514
|
-
}
|
|
515
565
|
|
|
516
|
-
|
|
517
|
-
const token = await fetch('https://api.basic.tech/auth/token', {
|
|
518
|
-
method: 'POST',
|
|
519
|
-
headers: {
|
|
520
|
-
'Content-Type': 'application/json'
|
|
521
|
-
},
|
|
522
|
-
body: JSON.stringify({ code: code })
|
|
523
|
-
})
|
|
524
|
-
.then(response => response.json())
|
|
525
|
-
.catch(error => log('Error:', error))
|
|
526
|
-
|
|
527
|
-
if (token.error) {
|
|
528
|
-
log('error fetching token', token.error)
|
|
529
|
-
return
|
|
530
|
-
} else {
|
|
531
|
-
// log('token', token)
|
|
532
|
-
setToken(token)
|
|
533
|
-
}
|
|
534
|
-
return token
|
|
566
|
+
return token?.access_token || ''
|
|
535
567
|
}
|
|
536
568
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
569
|
+
const fetchToken = async (codeOrRefreshToken: string, isRefreshToken: boolean = false) => {
|
|
570
|
+
try {
|
|
571
|
+
if (!isOnline) {
|
|
572
|
+
log('Network is offline, marking refresh as pending')
|
|
573
|
+
setPendingRefresh(true)
|
|
574
|
+
throw new Error('Network offline - refresh will be retried when online')
|
|
542
575
|
}
|
|
543
|
-
}
|
|
544
576
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
577
|
+
const requestBody = isRefreshToken
|
|
578
|
+
? {
|
|
579
|
+
grant_type: 'refresh_token',
|
|
580
|
+
refresh_token: codeOrRefreshToken
|
|
581
|
+
}
|
|
582
|
+
: {
|
|
583
|
+
grant_type: 'authorization_code',
|
|
584
|
+
code: codeOrRefreshToken
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const token = await fetch('https://api.basic.tech/auth/token', {
|
|
588
|
+
method: 'POST',
|
|
589
|
+
headers: {
|
|
590
|
+
'Content-Type': 'application/json'
|
|
591
|
+
},
|
|
592
|
+
body: JSON.stringify(requestBody)
|
|
593
|
+
})
|
|
594
|
+
.then(response => response.json())
|
|
595
|
+
.catch(error => {
|
|
596
|
+
log('Network error fetching token:', error)
|
|
597
|
+
if (!isOnline) {
|
|
598
|
+
setPendingRefresh(true)
|
|
599
|
+
throw new Error('Network offline - refresh will be retried when online')
|
|
600
|
+
}
|
|
601
|
+
throw new Error('Network error during token refresh')
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
if (token.error) {
|
|
605
|
+
log('error fetching token', token.error)
|
|
606
|
+
|
|
607
|
+
if (token.error.includes('network') || token.error.includes('timeout')) {
|
|
608
|
+
setPendingRefresh(true)
|
|
609
|
+
throw new Error('Network issue - refresh will be retried when online')
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
|
|
613
|
+
await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
|
|
614
|
+
clearCookie('basic_token');
|
|
615
|
+
clearCookie('basic_access_token');
|
|
616
|
+
|
|
617
|
+
setUser({})
|
|
618
|
+
setIsSignedIn(false)
|
|
619
|
+
setToken(null)
|
|
620
|
+
setIsAuthReady(true)
|
|
621
|
+
|
|
622
|
+
throw new Error(`Token refresh failed: ${token.error}`)
|
|
623
|
+
} else {
|
|
624
|
+
setToken(token)
|
|
625
|
+
setPendingRefresh(false)
|
|
626
|
+
|
|
627
|
+
if (token.refresh_token) {
|
|
628
|
+
await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token)
|
|
629
|
+
log('Updated refresh token in storage')
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
setCookie('basic_access_token', token.access_token, { httpOnly: false });
|
|
633
|
+
log('Updated access token in cookie')
|
|
634
|
+
}
|
|
635
|
+
return token
|
|
636
|
+
} catch (error) {
|
|
637
|
+
log('Token refresh error:', error)
|
|
638
|
+
|
|
639
|
+
if (!(error as Error).message.includes('offline') && !(error as Error).message.includes('Network')) {
|
|
640
|
+
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
|
|
641
|
+
await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
|
|
642
|
+
clearCookie('basic_token');
|
|
643
|
+
clearCookie('basic_access_token');
|
|
644
|
+
|
|
645
|
+
setUser({})
|
|
646
|
+
setIsSignedIn(false)
|
|
647
|
+
setToken(null)
|
|
648
|
+
setIsAuthReady(true)
|
|
565
649
|
}
|
|
566
650
|
|
|
651
|
+
throw error
|
|
567
652
|
}
|
|
568
|
-
|
|
569
653
|
}
|
|
570
654
|
|
|
571
|
-
const noDb = ({
|
|
655
|
+
const noDb = ({
|
|
572
656
|
collection: () => {
|
|
573
657
|
throw new Error('no basicdb found - initialization failed. double check your schema.')
|
|
574
658
|
}
|
|
@@ -582,22 +666,23 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
|
|
|
582
666
|
user,
|
|
583
667
|
signout,
|
|
584
668
|
signin,
|
|
669
|
+
signinWithCode,
|
|
585
670
|
getToken,
|
|
586
671
|
getSignInLink,
|
|
587
672
|
db: syncRef.current ? syncRef.current : noDb,
|
|
588
673
|
dbStatus
|
|
589
674
|
}}>
|
|
590
|
-
|
|
591
|
-
{error && <ErrorDisplay error={error} />}
|
|
675
|
+
|
|
676
|
+
{error && isDevMode() && <ErrorDisplay error={error} />}
|
|
592
677
|
{isReady && children}
|
|
593
678
|
</BasicContext.Provider>
|
|
594
679
|
)
|
|
595
680
|
}
|
|
596
681
|
|
|
597
682
|
function ErrorDisplay({ error }: { error: ErrorObject }) {
|
|
598
|
-
return <div style={{
|
|
683
|
+
return <div style={{
|
|
599
684
|
position: 'absolute',
|
|
600
|
-
top: 20,
|
|
685
|
+
top: 20,
|
|
601
686
|
left: 20,
|
|
602
687
|
color: 'black',
|
|
603
688
|
backgroundColor: '#f8d7da',
|
|
@@ -607,19 +692,14 @@ function ErrorDisplay({ error }: { error: ErrorObject }) {
|
|
|
607
692
|
maxWidth: '400px',
|
|
608
693
|
margin: '20px auto',
|
|
609
694
|
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
|
|
610
|
-
fontFamily: 'monospace',
|
|
611
|
-
|
|
612
|
-
<h3 style={{fontSize: '0.8rem', opacity: 0.8}}>code: {error.code}</h3>
|
|
613
|
-
<h1 style={{fontSize: '1.2rem', lineHeight: '1.5'}}>{error.title}</h1>
|
|
695
|
+
fontFamily: 'monospace',
|
|
696
|
+
}}>
|
|
697
|
+
<h3 style={{ fontSize: '0.8rem', opacity: 0.8 }}>code: {error.code}</h3>
|
|
698
|
+
<h1 style={{ fontSize: '1.2rem', lineHeight: '1.5' }}>{error.title}</h1>
|
|
614
699
|
<p>{error.message}</p>
|
|
615
700
|
</div>
|
|
616
701
|
}
|
|
617
702
|
|
|
618
|
-
/*
|
|
619
|
-
possible errors:
|
|
620
|
-
- projectid missing / invalid
|
|
621
|
-
- schema missing / invalid
|
|
622
|
-
*/
|
|
623
703
|
|
|
624
704
|
export function useBasic() {
|
|
625
705
|
return useContext(BasicContext);
|