@djangocfg/api 2.1.46 → 2.1.48

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/api",
3
- "version": "2.1.46",
3
+ "version": "2.1.48",
4
4
  "description": "Auto-generated TypeScript API client with React hooks, SWR integration, and Zod validation for Django REST Framework backends",
5
5
  "keywords": [
6
6
  "django",
@@ -74,7 +74,7 @@
74
74
  "check": "tsc --noEmit"
75
75
  },
76
76
  "peerDependencies": {
77
- "@djangocfg/ui-nextjs": "^2.1.46",
77
+ "@djangocfg/ui-nextjs": "^2.1.48",
78
78
  "consola": "^3.4.2",
79
79
  "next": "^14 || ^15",
80
80
  "p-retry": "^7.0.0",
@@ -85,7 +85,7 @@
85
85
  "devDependencies": {
86
86
  "@types/node": "^24.7.2",
87
87
  "@types/react": "^19.0.0",
88
- "@djangocfg/typescript-config": "^2.1.46",
88
+ "@djangocfg/typescript-config": "^2.1.48",
89
89
  "next": "^15.0.0",
90
90
  "react": "^19.0.0",
91
91
  "tsup": "^8.5.0",
@@ -10,6 +10,7 @@ import { useCfgRouter, useLocalStorage, useQueryParams } from '@djangocfg/ui-nex
10
10
 
11
11
  import { api as apiAccounts, Enums } from '../../';
12
12
  import { clearProfileCache, getCachedProfile } from '../hooks/useProfileCache';
13
+ import { useAuthRedirectManager } from '../hooks/useAuthRedirect';
13
14
  import { Analytics, AnalyticsCategory, AnalyticsEvent } from '../utils/analytics';
14
15
  import { authLogger } from '../utils/logger';
15
16
  import { AccountsProvider, useAccountsContext } from './AccountsContext';
@@ -38,6 +39,12 @@ const hasValidTokens = (): boolean => {
38
39
  const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config }) => {
39
40
  const accounts = useAccountsContext();
40
41
 
42
+ // Redirect URL manager for saving URL before auth redirect
43
+ const redirectManager = useAuthRedirectManager({
44
+ fallbackUrl: config?.routes?.defaultCallback || defaultRoutes.defaultCallback,
45
+ clearOnUse: true,
46
+ });
47
+
41
48
  // Smart initial loading state: only true if we don't have tokens yet
42
49
  const [isLoading, setIsLoading] = useState(() => {
43
50
  // If we already have tokens and profile, don't show loading
@@ -360,9 +367,11 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
360
367
  Analytics.setUser(String(result.user.id));
361
368
  }
362
369
 
363
- // Handle redirect logic - use provided redirectUrl or fallback to config
370
+ // Handle redirect logic - priority: provided redirectUrl > saved redirect > config default
364
371
  // Use hardPush for full page reload - ensures all React contexts reinitialize
365
- const finalRedirectUrl = redirectUrl || config?.routes?.defaultCallback || defaultRoutes.defaultCallback;
372
+ const savedRedirect = redirectManager.useAndClearRedirect();
373
+ const finalRedirectUrl = redirectUrl || savedRedirect || config?.routes?.defaultCallback || defaultRoutes.defaultCallback;
374
+ authLogger.info('Redirecting after auth to:', finalRedirectUrl);
366
375
  router.hardPush(finalRedirectUrl);
367
376
 
368
377
  return {
@@ -497,6 +506,11 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
497
506
  verifyOTP,
498
507
  refreshToken,
499
508
  logout,
509
+ // Redirect URL methods
510
+ saveRedirectUrl: redirectManager.setRedirect,
511
+ getRedirectUrl: redirectManager.getFinalRedirectUrl,
512
+ clearRedirectUrl: redirectManager.clearRedirect,
513
+ hasRedirectUrl: redirectManager.hasRedirect,
500
514
  }),
501
515
  [
502
516
  user,
@@ -514,6 +528,7 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
514
528
  verifyOTP,
515
529
  refreshToken,
516
530
  logout,
531
+ redirectManager,
517
532
  ],
518
533
  );
519
534
 
@@ -51,6 +51,12 @@ export interface AuthContextType {
51
51
  verifyOTP: (identifier: string, otpCode: string, channel?: 'email' | 'phone', sourceUrl?: string, redirectUrl?: string) => Promise<{ success: boolean; message: string; user?: UserProfile }>;
52
52
  refreshToken: () => Promise<{ success: boolean; message: string }>;
53
53
  logout: () => Promise<void>;
54
+
55
+ // Redirect URL Methods - for saving URL before auth redirect
56
+ saveRedirectUrl: (url: string) => void;
57
+ getRedirectUrl: () => string;
58
+ clearRedirectUrl: () => void;
59
+ hasRedirectUrl: () => boolean;
54
60
  }
55
61
 
56
62
  // Provider props
@@ -43,10 +43,12 @@ export interface UseAuthFormOptions {
43
43
  redirectUrl?: string;
44
44
  /** If true, user must accept terms before submitting. Default: false */
45
45
  requireTermsAcceptance?: boolean;
46
+ /** Path to auth page for auto-OTP detection. Default: '/auth' */
47
+ authPath?: string;
46
48
  }
47
49
 
48
50
  export const useAuthForm = (options: UseAuthFormOptions): AuthFormState & AuthFormHandlers => {
49
- const { onIdentifierSuccess, onOTPSuccess, onError, sourceUrl, redirectUrl, requireTermsAcceptance = false } = options;
51
+ const { onIdentifierSuccess, onOTPSuccess, onError, sourceUrl, redirectUrl, requireTermsAcceptance = false, authPath = '/auth' } = options;
50
52
 
51
53
  // Form state
52
54
  const [identifier, setIdentifier] = useState('');
@@ -273,15 +275,16 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormState & AuthFo
273
275
  setSavedTermsAccepted(checked);
274
276
  }, [setSavedTermsAccepted]);
275
277
 
276
- // Auto-detect OTP from URL query parameters
278
+ // Auto-detect OTP from URL query parameters (only on auth page)
277
279
  useAutoAuth({
280
+ allowedPaths: [authPath],
278
281
  onOTPDetected: (otp: string) => {
279
282
  authLogger.info('OTP detected, auto-submitting');
280
-
283
+
281
284
  // Get saved identifier from auth context
282
285
  const savedEmail = getSavedEmail();
283
286
  const savedPhone = getSavedPhone();
284
-
287
+
285
288
  // Prioritize phone over email if both exist
286
289
  if (savedPhone) {
287
290
  setIdentifier(savedPhone);
@@ -290,11 +293,11 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormState & AuthFo
290
293
  setIdentifier(savedEmail);
291
294
  setChannel('email');
292
295
  }
293
-
296
+
294
297
  // Set OTP and force OTP step
295
298
  setOtp(otp);
296
299
  setStep('otp');
297
-
300
+
298
301
  // Auto-submit after a short delay to ensure state is updated
299
302
  setTimeout(() => {
300
303
  const fakeEvent = { preventDefault: () => {} } as React.FormEvent;
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import { useEffect } from 'react';
3
+ import { useEffect, useState } from 'react';
4
4
 
5
5
  import { useCfgRouter } from '@djangocfg/ui-nextjs/hooks';
6
6
 
@@ -9,18 +9,28 @@ import { useAuth } from '../context';
9
9
  interface UseAuthGuardOptions {
10
10
  redirectTo?: string;
11
11
  requireAuth?: boolean;
12
+ /** Whether to save current URL for redirect after auth (default: true) */
13
+ saveRedirectUrl?: boolean;
12
14
  }
13
15
 
14
16
  export const useAuthGuard = (options: UseAuthGuardOptions = {}) => {
15
- const { redirectTo = '/auth', requireAuth = true } = options;
16
- const { isAuthenticated, isLoading } = useAuth();
17
+ const { redirectTo = '/auth', requireAuth = true, saveRedirectUrl: shouldSaveUrl = true } = options;
18
+ const { isAuthenticated, isLoading, saveRedirectUrl } = useAuth();
17
19
  const router = useCfgRouter();
20
+ const [isRedirecting, setIsRedirecting] = useState(false);
18
21
 
19
22
  useEffect(() => {
20
- if (!isLoading && requireAuth && !isAuthenticated) {
23
+ if (!isLoading && requireAuth && !isAuthenticated && !isRedirecting) {
24
+ // Save current URL before redirecting
25
+ if (shouldSaveUrl && typeof window !== 'undefined') {
26
+ const currentUrl = window.location.pathname + window.location.search;
27
+ saveRedirectUrl(currentUrl);
28
+ }
29
+
30
+ setIsRedirecting(true);
21
31
  router.push(redirectTo);
22
32
  }
23
- }, [isAuthenticated, isLoading, router, redirectTo, requireAuth]);
33
+ }, [isAuthenticated, isLoading, router, redirectTo, requireAuth, isRedirecting, shouldSaveUrl, saveRedirectUrl]);
24
34
 
25
- return { isAuthenticated, isLoading };
35
+ return { isAuthenticated, isLoading, isRedirecting };
26
36
  };
@@ -10,6 +10,8 @@ import { authLogger } from '../utils/logger';
10
10
  export interface UseAutoAuthOptions {
11
11
  onOTPDetected?: (otp: string) => void;
12
12
  cleanupUrl?: boolean;
13
+ /** Paths where auto-auth should be active. Default: ['/auth'] */
14
+ allowedPaths?: string[];
13
15
  }
14
16
 
15
17
  /**
@@ -17,22 +19,26 @@ export interface UseAutoAuthOptions {
17
19
  * Detects OTP from URL and triggers callback
18
20
  */
19
21
  export const useAutoAuth = (options: UseAutoAuthOptions = {}) => {
20
- const { onOTPDetected, cleanupUrl = true } = options;
22
+ const { onOTPDetected, cleanupUrl = true, allowedPaths = ['/auth'] } = options;
21
23
  const queryParams = useQueryParams();
22
24
  const pathname = usePathname();
23
25
  const router = useCfgRouter();
24
26
 
25
- const isReady = !!pathname && !!queryParams.get('otp');
27
+ // Check if current path is in allowed paths
28
+ const isAllowedPath = allowedPaths.some(path => pathname === path || pathname?.startsWith(path + '/'));
29
+
26
30
  const hasOTP = !!(queryParams.get('otp'));
31
+ const isReady = !!pathname && hasOTP && isAllowedPath;
27
32
 
28
33
  useEffect(() => {
29
34
  if (!isReady) return;
30
35
 
31
36
  const queryOtp = queryParams.get('otp') as string;
32
37
 
33
- // Handle OTP detection
38
+ // Handle OTP detection - only on allowed paths to avoid conflicts with other pages
39
+ // (e.g., /dashboard/device uses ?otp= for device authorization, not user auth)
34
40
  if (queryOtp && typeof queryOtp === 'string' && queryOtp.length === 6) {
35
- authLogger.info('OTP detected in URL:', queryOtp);
41
+ authLogger.info('OTP detected in URL on auth page:', queryOtp);
36
42
  onOTPDetected?.(queryOtp);
37
43
  }
38
44
 
@@ -40,12 +46,14 @@ export const useAutoAuth = (options: UseAutoAuthOptions = {}) => {
40
46
  if (cleanupUrl && queryOtp) {
41
47
  const cleanQuery = Object.fromEntries(queryParams.entries());
42
48
  delete cleanQuery.otp;
43
- router.push(`${pathname}?${new URLSearchParams(cleanQuery).toString()}`);
49
+ const queryString = new URLSearchParams(cleanQuery).toString();
50
+ router.push(queryString ? `${pathname}?${queryString}` : pathname);
44
51
  }
45
- }, [pathname, queryParams, onOTPDetected, cleanupUrl, router]);
52
+ }, [pathname, queryParams, onOTPDetected, cleanupUrl, router, isReady]);
46
53
 
47
54
  return {
48
55
  isReady,
49
56
  hasOTP,
57
+ isAllowedPath,
50
58
  };
51
59
  };