@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/dist/auth.cjs +252 -230
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +12 -0
- package/dist/auth.d.ts +12 -0
- package/dist/auth.mjs +237 -215
- package/dist/auth.mjs.map +1 -1
- package/package.json +3 -3
- package/src/auth/context/AuthContext.tsx +17 -2
- package/src/auth/context/types.ts +6 -0
- package/src/auth/hooks/useAuthForm.ts +9 -6
- package/src/auth/hooks/useAuthGuard.ts +16 -6
- package/src/auth/hooks/useAutoAuth.ts +14 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/api",
|
|
3
|
-
"version": "2.1.
|
|
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.
|
|
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.
|
|
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 -
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|