@rationalbloks/frontblok-auth 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,603 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const react = require("react");
5
+ const client = require("react-dom/client");
6
+ const google = require("@react-oauth/google");
7
+ class BaseApi {
8
+ constructor(apiBaseUrl) {
9
+ this.token = null;
10
+ this.refreshToken = null;
11
+ this.isRefreshing = false;
12
+ this.refreshPromise = null;
13
+ this.proactiveRefreshTimer = null;
14
+ this.refreshCheckInterval = null;
15
+ this.tokenRefreshPromise = null;
16
+ this.apiBaseUrl = apiBaseUrl;
17
+ this.loadTokens();
18
+ this.setupVisibilityHandler();
19
+ this.setupStorageListener();
20
+ this.startRefreshCheckInterval();
21
+ console.log(`[API] Base URL: ${this.apiBaseUrl}`);
22
+ console.log(`[API] Environment: ${"Production"}`);
23
+ }
24
+ // ========================================================================
25
+ // TOKEN MANAGEMENT
26
+ // ========================================================================
27
+ /**
28
+ * Sync tokens across browser tabs when localStorage changes.
29
+ * Prevents "stale refresh token" issue where Tab A rotates the token
30
+ * but Tab B still has the old (now invalid) refresh token in memory.
31
+ */
32
+ setupStorageListener() {
33
+ if (typeof window !== "undefined") {
34
+ window.addEventListener("storage", (event) => {
35
+ if (event.key === "auth_token") {
36
+ console.log("[API] Token updated in another tab, syncing...");
37
+ this.token = event.newValue;
38
+ } else if (event.key === "refresh_token") {
39
+ console.log("[API] Refresh token updated in another tab, syncing...");
40
+ this.refreshToken = event.newValue;
41
+ } else if (event.key === null) {
42
+ console.log("[API] Storage cleared in another tab, syncing logout...");
43
+ this.token = null;
44
+ this.refreshToken = null;
45
+ window.dispatchEvent(new CustomEvent("auth:cleared"));
46
+ }
47
+ });
48
+ }
49
+ }
50
+ /**
51
+ * Start interval-based token check (runs every 60 seconds).
52
+ * More reliable than setTimeout which browsers throttle in background tabs.
53
+ */
54
+ startRefreshCheckInterval() {
55
+ if (this.refreshCheckInterval) {
56
+ clearInterval(this.refreshCheckInterval);
57
+ }
58
+ this.refreshCheckInterval = setInterval(() => {
59
+ if (this.token) {
60
+ this.checkAndRefreshToken();
61
+ }
62
+ }, 60 * 1e3);
63
+ }
64
+ /**
65
+ * Handle tab visibility changes - check token when user returns to tab.
66
+ */
67
+ setupVisibilityHandler() {
68
+ if (typeof document !== "undefined") {
69
+ document.addEventListener("visibilitychange", () => {
70
+ if (document.visibilityState === "visible" && this.token) {
71
+ console.log("[API] Tab became visible, checking token validity...");
72
+ this.checkAndRefreshToken();
73
+ }
74
+ });
75
+ window.addEventListener("focus", () => {
76
+ if (this.token) {
77
+ console.log("[API] Window focused, checking token validity...");
78
+ this.checkAndRefreshToken();
79
+ }
80
+ });
81
+ }
82
+ }
83
+ /**
84
+ * Check token expiry and refresh if needed.
85
+ */
86
+ async checkAndRefreshToken() {
87
+ if (!this.token) return;
88
+ const expiry = this.getTokenExpiry(this.token);
89
+ if (!expiry) return;
90
+ const now = Date.now();
91
+ const timeUntilExpiry = expiry - now;
92
+ const refreshBuffer = 3 * 60 * 1e3;
93
+ if (timeUntilExpiry <= 0) {
94
+ console.log("[API] Token expired, refreshing immediately...");
95
+ await this.refreshAccessToken();
96
+ } else if (timeUntilExpiry <= refreshBuffer) {
97
+ console.log(`[API] Token expires in ${Math.round(timeUntilExpiry / 1e3)}s, refreshing proactively...`);
98
+ await this.refreshAccessToken();
99
+ }
100
+ }
101
+ /**
102
+ * Parse JWT to get expiration time.
103
+ */
104
+ getTokenExpiry(token) {
105
+ try {
106
+ const payload = JSON.parse(atob(token.split(".")[1]));
107
+ return payload.exp ? payload.exp * 1e3 : null;
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+ /**
113
+ * Schedule proactive refresh 5 minutes before token expires.
114
+ */
115
+ scheduleProactiveRefresh() {
116
+ if (this.proactiveRefreshTimer) {
117
+ clearTimeout(this.proactiveRefreshTimer);
118
+ this.proactiveRefreshTimer = null;
119
+ }
120
+ if (!this.token) return;
121
+ const expiry = this.getTokenExpiry(this.token);
122
+ if (!expiry) return;
123
+ const now = Date.now();
124
+ const timeUntilExpiry = expiry - now;
125
+ const refreshBuffer = 5 * 60 * 1e3;
126
+ if (timeUntilExpiry <= 0) {
127
+ console.log("[API] Token already expired, refreshing immediately...");
128
+ this.refreshAccessToken();
129
+ return;
130
+ }
131
+ if (timeUntilExpiry <= refreshBuffer) {
132
+ console.log("[API] Token expiring soon, refreshing immediately...");
133
+ this.refreshAccessToken();
134
+ return;
135
+ }
136
+ const refreshIn = timeUntilExpiry - refreshBuffer;
137
+ console.log(`[API] Scheduling proactive token refresh in ${Math.round(refreshIn / 6e4)} minutes`);
138
+ this.proactiveRefreshTimer = setTimeout(async () => {
139
+ console.log("[API] Proactive token refresh triggered");
140
+ await this.refreshAccessToken();
141
+ }, refreshIn);
142
+ }
143
+ /**
144
+ * Load tokens from localStorage on initialization.
145
+ */
146
+ loadTokens() {
147
+ const storedToken = localStorage.getItem("auth_token");
148
+ const storedRefreshToken = localStorage.getItem("refresh_token");
149
+ if (storedToken) {
150
+ this.token = storedToken;
151
+ console.log("[API] Access token loaded from localStorage");
152
+ }
153
+ if (storedRefreshToken) {
154
+ this.refreshToken = storedRefreshToken;
155
+ console.log("[API] Refresh token loaded from localStorage");
156
+ }
157
+ if (this.token) {
158
+ this.tokenRefreshPromise = this.checkAndRefreshToken();
159
+ this.scheduleProactiveRefresh();
160
+ }
161
+ }
162
+ /**
163
+ * Wait for any pending token refresh to complete before making API calls.
164
+ */
165
+ async ensureTokenReady() {
166
+ if (this.tokenRefreshPromise) {
167
+ await this.tokenRefreshPromise;
168
+ this.tokenRefreshPromise = null;
169
+ }
170
+ }
171
+ /**
172
+ * Refresh the access token using the refresh token.
173
+ */
174
+ async refreshAccessToken() {
175
+ if (this.isRefreshing) {
176
+ return this.refreshPromise || Promise.resolve(false);
177
+ }
178
+ const latestRefreshToken = localStorage.getItem("refresh_token");
179
+ if (latestRefreshToken && latestRefreshToken !== this.refreshToken) {
180
+ console.log("[API] Syncing refresh token from localStorage");
181
+ this.refreshToken = latestRefreshToken;
182
+ }
183
+ if (!this.refreshToken) {
184
+ console.warn("[API] No refresh token available");
185
+ return false;
186
+ }
187
+ this.isRefreshing = true;
188
+ this.refreshPromise = (async () => {
189
+ try {
190
+ console.log("[API] Refreshing access token...");
191
+ const response = await fetch(`${this.apiBaseUrl}/api/auth/refresh`, {
192
+ method: "POST",
193
+ headers: { "Content-Type": "application/json" },
194
+ body: JSON.stringify({ refresh_token: this.refreshToken })
195
+ });
196
+ if (!response.ok) {
197
+ console.error("[API] Token refresh failed:", response.status);
198
+ if (response.status === 401 || response.status === 403) {
199
+ console.log("[API] Refresh token is invalid, logging out...");
200
+ this.clearAuth();
201
+ } else {
202
+ console.warn(`[API] Refresh failed with ${response.status}, will retry on next interval`);
203
+ }
204
+ return false;
205
+ }
206
+ const data = await response.json();
207
+ this.token = data.access_token;
208
+ localStorage.setItem("auth_token", data.access_token);
209
+ if (data.refresh_token) {
210
+ this.refreshToken = data.refresh_token;
211
+ localStorage.setItem("refresh_token", data.refresh_token);
212
+ console.log("[API] Refresh token rotated");
213
+ }
214
+ this.scheduleProactiveRefresh();
215
+ console.log("[API] Access token refreshed successfully");
216
+ return true;
217
+ } catch (error) {
218
+ console.error("[API] Token refresh network error:", error);
219
+ console.warn("[API] Will retry refresh on next interval");
220
+ return false;
221
+ } finally {
222
+ this.isRefreshing = false;
223
+ this.refreshPromise = null;
224
+ }
225
+ })();
226
+ return this.refreshPromise;
227
+ }
228
+ // ========================================================================
229
+ // HTTP REQUEST METHOD
230
+ // ========================================================================
231
+ /**
232
+ * Make an authenticated HTTP request.
233
+ * Handles token refresh, 401 retry, and rate limiting automatically.
234
+ *
235
+ * This method is public so apps can make custom API calls without extending BaseApi.
236
+ *
237
+ * @example
238
+ * const authApi = createAuthApi(API_URL);
239
+ * const data = await authApi.request<MyType>('/api/my-endpoint/', { method: 'POST', body: JSON.stringify(payload) });
240
+ */
241
+ async request(endpoint, options = {}, isRetry = false, retryCount = 0) {
242
+ await this.ensureTokenReady();
243
+ if (this.token && !isRetry && !endpoint.includes("/auth/")) {
244
+ const expiry = this.getTokenExpiry(this.token);
245
+ if (expiry) {
246
+ const now = Date.now();
247
+ const timeUntilExpiry = expiry - now;
248
+ if (timeUntilExpiry < 2 * 60 * 1e3) {
249
+ console.log(`[API] Token expires in ${Math.round(timeUntilExpiry / 1e3)}s, refreshing before request...`);
250
+ await this.refreshAccessToken();
251
+ }
252
+ }
253
+ }
254
+ const url = `${this.apiBaseUrl}${endpoint}`;
255
+ const headers = {
256
+ "Content-Type": "application/json",
257
+ ...options.headers
258
+ };
259
+ if (this.token) {
260
+ headers.Authorization = `Bearer ${this.token}`;
261
+ }
262
+ console.log(`[API Request] ${options.method || "GET"} ${url}`);
263
+ try {
264
+ const response = await fetch(url, { ...options, headers });
265
+ console.log(`[API Response] ${response.status} ${response.statusText}`);
266
+ if (!response.ok) {
267
+ const error = await response.text();
268
+ console.error(`[API Error] ${response.status}: ${error}`);
269
+ if (response.status === 429 && retryCount < 3) {
270
+ const backoffMs = Math.pow(2, retryCount) * 1e3;
271
+ console.log(`[API] Rate limited (429), retrying in ${backoffMs}ms (attempt ${retryCount + 1}/3)...`);
272
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
273
+ return this.request(endpoint, options, isRetry, retryCount + 1);
274
+ }
275
+ if (response.status === 401 && !isRetry && !endpoint.includes("/auth/login") && !endpoint.includes("/auth/register") && !endpoint.includes("/auth/refresh")) {
276
+ console.log("[API] Attempting token refresh...");
277
+ const refreshed = await this.refreshAccessToken();
278
+ if (refreshed) {
279
+ return this.request(endpoint, options, true, retryCount);
280
+ }
281
+ console.warn("[API] Token refresh failed, returning error to caller");
282
+ }
283
+ const apiError = new Error(`API Error: ${response.status} - ${error}`);
284
+ apiError.status = response.status;
285
+ throw apiError;
286
+ }
287
+ return response.json();
288
+ } catch (error) {
289
+ console.error("[API Connection Error]:", error);
290
+ if (error instanceof TypeError && error.message.includes("Failed to fetch")) {
291
+ throw new Error(`Cannot connect to API: ${this.apiBaseUrl}`);
292
+ }
293
+ throw error;
294
+ }
295
+ }
296
+ // ========================================================================
297
+ // AUTHENTICATION METHODS
298
+ // ========================================================================
299
+ async login(email, password) {
300
+ console.log("[API] Login attempt:", email);
301
+ const data = await this.request("/api/auth/login", {
302
+ method: "POST",
303
+ body: JSON.stringify({ email, password })
304
+ });
305
+ this.token = data.access_token;
306
+ this.refreshToken = data.refresh_token || null;
307
+ localStorage.setItem("auth_token", data.access_token);
308
+ if (data.refresh_token) {
309
+ localStorage.setItem("refresh_token", data.refresh_token);
310
+ }
311
+ localStorage.setItem("user_data", JSON.stringify(data.user));
312
+ this.scheduleProactiveRefresh();
313
+ console.log("[API] Login successful");
314
+ return data;
315
+ }
316
+ async register(email, password, firstName, lastName) {
317
+ console.log("[API] Registration attempt:", email);
318
+ const data = await this.request("/api/auth/register", {
319
+ method: "POST",
320
+ body: JSON.stringify({
321
+ email,
322
+ password,
323
+ first_name: firstName,
324
+ last_name: lastName
325
+ })
326
+ });
327
+ this.token = data.access_token;
328
+ this.refreshToken = data.refresh_token || null;
329
+ localStorage.setItem("auth_token", data.access_token);
330
+ if (data.refresh_token) {
331
+ localStorage.setItem("refresh_token", data.refresh_token);
332
+ }
333
+ localStorage.setItem("user_data", JSON.stringify(data.user));
334
+ this.scheduleProactiveRefresh();
335
+ console.log("[API] Registration successful");
336
+ return data;
337
+ }
338
+ async getMe() {
339
+ console.log("[API] Fetching current user");
340
+ return this.request("/api/auth/me");
341
+ }
342
+ async getCurrentUser() {
343
+ console.log("[API] Fetching current user data");
344
+ const response = await this.request("/api/auth/me");
345
+ localStorage.setItem("user", JSON.stringify(response.user));
346
+ return response.user;
347
+ }
348
+ async deleteAccount(password, confirmText) {
349
+ console.log("[API] Deleting account (DESTRUCTIVE)");
350
+ const response = await this.request("/api/auth/me", {
351
+ method: "DELETE",
352
+ body: JSON.stringify({
353
+ password,
354
+ confirm_text: confirmText
355
+ })
356
+ });
357
+ this.clearAuth();
358
+ console.log("[API] Account deleted successfully");
359
+ return response;
360
+ }
361
+ async logout() {
362
+ console.log("[API] Logging out");
363
+ if (this.refreshToken) {
364
+ try {
365
+ await this.request("/api/auth/logout", {
366
+ method: "POST",
367
+ body: JSON.stringify({ refresh_token: this.refreshToken })
368
+ });
369
+ console.log("[API] Refresh token revoked on server");
370
+ } catch (error) {
371
+ console.warn("[API] Failed to revoke refresh token on server:", error);
372
+ }
373
+ }
374
+ this.clearAuth();
375
+ }
376
+ async logoutAllDevices() {
377
+ console.log("[API] Logging out from all devices");
378
+ try {
379
+ await this.request("/api/auth/logout-all", { method: "POST" });
380
+ console.log("[API] All sessions revoked on server");
381
+ } catch (error) {
382
+ console.warn("[API] Failed to revoke all sessions:", error);
383
+ }
384
+ this.clearAuth();
385
+ }
386
+ clearAuth() {
387
+ if (this.proactiveRefreshTimer) {
388
+ clearTimeout(this.proactiveRefreshTimer);
389
+ this.proactiveRefreshTimer = null;
390
+ }
391
+ this.token = null;
392
+ this.refreshToken = null;
393
+ localStorage.removeItem("auth_token");
394
+ localStorage.removeItem("refresh_token");
395
+ localStorage.removeItem("user_data");
396
+ console.log("[API] Auth cache cleared");
397
+ window.dispatchEvent(new CustomEvent("auth:cleared"));
398
+ }
399
+ // ========================================================================
400
+ // API KEY MANAGEMENT
401
+ // ========================================================================
402
+ async listApiKeys() {
403
+ console.log("[API] Listing user API keys");
404
+ return this.request("/api/auth/api-keys", { method: "GET" });
405
+ }
406
+ async createApiKey(data) {
407
+ console.log(`[API] Creating API key: ${data.name}`);
408
+ return this.request("/api/auth/api-keys", {
409
+ method: "POST",
410
+ body: JSON.stringify({
411
+ name: data.name,
412
+ scopes: data.scopes || "read,write",
413
+ expires_in_days: data.expires_in_days,
414
+ rate_limit_per_minute: data.rate_limit_per_minute || 60
415
+ })
416
+ });
417
+ }
418
+ async revokeApiKey(keyId) {
419
+ console.log(`[API] Revoking API key: ${keyId}`);
420
+ return this.request(`/api/auth/api-keys/${keyId}`, { method: "DELETE" });
421
+ }
422
+ }
423
+ function createAuthApi(apiBaseUrl) {
424
+ return new BaseApi(apiBaseUrl);
425
+ }
426
+ const getStoredUser = () => {
427
+ const userData = localStorage.getItem("user_data");
428
+ if (!userData) return null;
429
+ try {
430
+ return JSON.parse(userData);
431
+ } catch (error) {
432
+ console.error("[API] Invalid user_data in localStorage:", error);
433
+ localStorage.removeItem("user_data");
434
+ return null;
435
+ }
436
+ };
437
+ const getStoredToken = () => {
438
+ return localStorage.getItem("auth_token");
439
+ };
440
+ const isAuthenticated = () => {
441
+ return !!getStoredToken();
442
+ };
443
+ const AuthContext = react.createContext(void 0);
444
+ const useAuth = () => {
445
+ const context = react.useContext(AuthContext);
446
+ if (!context) {
447
+ throw new Error("useAuth must be used within AuthProvider");
448
+ }
449
+ return context;
450
+ };
451
+ function createAuthProvider(api) {
452
+ const AuthProvider = ({ children }) => {
453
+ const [state, setState] = react.useState({
454
+ user: getStoredUser(),
455
+ isLoading: false,
456
+ isAuthenticated: !!getStoredToken(),
457
+ error: null
458
+ });
459
+ react.useEffect(() => {
460
+ const handleAuthCleared = () => {
461
+ console.log("[Auth] Session expired or invalidated - clearing auth state");
462
+ setState({
463
+ user: null,
464
+ isAuthenticated: false,
465
+ isLoading: false,
466
+ error: null
467
+ });
468
+ };
469
+ window.addEventListener("auth:cleared", handleAuthCleared);
470
+ return () => window.removeEventListener("auth:cleared", handleAuthCleared);
471
+ }, []);
472
+ react.useEffect(() => {
473
+ const refreshUserData = async () => {
474
+ const token = getStoredToken();
475
+ if (token) {
476
+ try {
477
+ const currentUser = await api.getCurrentUser();
478
+ setState((prev) => ({
479
+ ...prev,
480
+ user: currentUser,
481
+ isAuthenticated: true
482
+ }));
483
+ } catch (error) {
484
+ console.error("[Auth] Failed to refresh user data:", error);
485
+ const httpError = error;
486
+ if ((httpError == null ? void 0 : httpError.status) === 401 || (httpError == null ? void 0 : httpError.status) === 403) {
487
+ console.warn("[Auth] Auth failed - clearing local state only");
488
+ localStorage.removeItem("auth_token");
489
+ localStorage.removeItem("user_data");
490
+ setState({
491
+ user: null,
492
+ isAuthenticated: false,
493
+ isLoading: false,
494
+ error: null
495
+ });
496
+ } else {
497
+ console.warn("[Auth] Keeping user logged in with cached data");
498
+ }
499
+ }
500
+ }
501
+ };
502
+ refreshUserData();
503
+ }, []);
504
+ const login = async (email, password) => {
505
+ setState((prev) => ({ ...prev, isLoading: true, error: null }));
506
+ try {
507
+ const result = await api.login(email, password);
508
+ setState((prev) => ({
509
+ ...prev,
510
+ user: result.user,
511
+ isAuthenticated: true,
512
+ isLoading: false
513
+ }));
514
+ return true;
515
+ } catch (error) {
516
+ setState((prev) => ({
517
+ ...prev,
518
+ error: error instanceof Error ? error.message : "Login failed",
519
+ isLoading: false
520
+ }));
521
+ return false;
522
+ }
523
+ };
524
+ const register = async (email, password, firstName, lastName) => {
525
+ setState((prev) => ({ ...prev, isLoading: true, error: null }));
526
+ try {
527
+ const result = await api.register(email, password, firstName, lastName);
528
+ setState((prev) => ({
529
+ ...prev,
530
+ user: result.user,
531
+ isAuthenticated: true,
532
+ isLoading: false
533
+ }));
534
+ return true;
535
+ } catch (error) {
536
+ setState((prev) => ({
537
+ ...prev,
538
+ error: error instanceof Error ? error.message : "Registration failed",
539
+ isLoading: false
540
+ }));
541
+ return false;
542
+ }
543
+ };
544
+ const logout = () => {
545
+ api.logout();
546
+ setState({
547
+ user: null,
548
+ isAuthenticated: false,
549
+ isLoading: false,
550
+ error: null
551
+ });
552
+ };
553
+ const clearError = () => {
554
+ setState((prev) => ({ ...prev, error: null }));
555
+ };
556
+ const refreshUser = async () => {
557
+ try {
558
+ const currentUser = await api.getCurrentUser();
559
+ setState((prev) => ({
560
+ ...prev,
561
+ user: currentUser
562
+ }));
563
+ } catch (error) {
564
+ console.error("[Auth] Failed to refresh user:", error);
565
+ }
566
+ };
567
+ return /* @__PURE__ */ jsxRuntime.jsx(
568
+ AuthContext.Provider,
569
+ {
570
+ value: {
571
+ ...state,
572
+ login,
573
+ register,
574
+ logout,
575
+ clearError,
576
+ refreshUser
577
+ },
578
+ children
579
+ }
580
+ );
581
+ };
582
+ return AuthProvider;
583
+ }
584
+ function createAppRoot(App, config = {}) {
585
+ const { googleClientId } = config;
586
+ const root = client.createRoot(document.getElementById("root"));
587
+ let appElement = /* @__PURE__ */ jsxRuntime.jsx(App, {});
588
+ if (googleClientId) {
589
+ appElement = /* @__PURE__ */ jsxRuntime.jsx(google.GoogleOAuthProvider, { clientId: googleClientId, children: appElement });
590
+ }
591
+ root.render(
592
+ /* @__PURE__ */ jsxRuntime.jsx(react.StrictMode, { children: appElement })
593
+ );
594
+ }
595
+ exports.BaseApi = BaseApi;
596
+ exports.createAppRoot = createAppRoot;
597
+ exports.createAuthApi = createAuthApi;
598
+ exports.createAuthProvider = createAuthProvider;
599
+ exports.getStoredToken = getStoredToken;
600
+ exports.getStoredUser = getStoredUser;
601
+ exports.isAuthenticated = isAuthenticated;
602
+ exports.useAuth = useAuth;
603
+ //# sourceMappingURL=index.cjs.map