@djangocfg/layouts 2.1.455 → 2.1.457
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 +15 -15
- package/src/components/RedirectPage/RedirectPage.tsx +5 -4
- package/src/layouts/AuthLayout/AuthLayout.tsx +9 -4
- package/src/layouts/AuthLayout/components/oauth/OAuthCallback.tsx +3 -2
- package/src/layouts/AuthLayout/constants.ts +3 -5
- package/src/layouts/PrivateLayout/hooks/useAuthGuard.ts +6 -4
- package/src/layouts/PublicLayout/components/UserMenu.tsx +5 -4
- package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +2 -2
- package/src/layouts/SettingsLayout/components/TwoFactorSection/TwoFactorSection.tsx +47 -58
- package/src/layouts/SettingsLayout/sections/builtins.tsx +5 -2
- package/src/snippets/AuthDialog/AuthDialog.tsx +8 -5
- package/src/testing/MockAuthProvider.tsx +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.457",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -89,12 +89,12 @@
|
|
|
89
89
|
"check": "tsc --noEmit"
|
|
90
90
|
},
|
|
91
91
|
"peerDependencies": {
|
|
92
|
-
"@djangocfg/api": "^2.1.
|
|
93
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
94
|
-
"@djangocfg/debuger": "^2.1.
|
|
95
|
-
"@djangocfg/i18n": "^2.1.
|
|
96
|
-
"@djangocfg/monitor": "^2.1.
|
|
97
|
-
"@djangocfg/ui-core": "^2.1.
|
|
92
|
+
"@djangocfg/api": "^2.1.457",
|
|
93
|
+
"@djangocfg/centrifugo": "^2.1.457",
|
|
94
|
+
"@djangocfg/debuger": "^2.1.457",
|
|
95
|
+
"@djangocfg/i18n": "^2.1.457",
|
|
96
|
+
"@djangocfg/monitor": "^2.1.457",
|
|
97
|
+
"@djangocfg/ui-core": "^2.1.457",
|
|
98
98
|
"@hookform/resolvers": "^5.2.2",
|
|
99
99
|
"consola": "^3.4.2",
|
|
100
100
|
"lucide-react": "^0.545.0",
|
|
@@ -125,14 +125,14 @@
|
|
|
125
125
|
"uuid": "^11.1.0"
|
|
126
126
|
},
|
|
127
127
|
"devDependencies": {
|
|
128
|
-
"@djangocfg/api": "^2.1.
|
|
129
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
130
|
-
"@djangocfg/debuger": "^2.1.
|
|
131
|
-
"@djangocfg/i18n": "^2.1.
|
|
132
|
-
"@djangocfg/monitor": "^2.1.
|
|
133
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
134
|
-
"@djangocfg/ui-core": "^2.1.
|
|
135
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
128
|
+
"@djangocfg/api": "^2.1.457",
|
|
129
|
+
"@djangocfg/centrifugo": "^2.1.457",
|
|
130
|
+
"@djangocfg/debuger": "^2.1.457",
|
|
131
|
+
"@djangocfg/i18n": "^2.1.457",
|
|
132
|
+
"@djangocfg/monitor": "^2.1.457",
|
|
133
|
+
"@djangocfg/typescript-config": "^2.1.457",
|
|
134
|
+
"@djangocfg/ui-core": "^2.1.457",
|
|
135
|
+
"@djangocfg/ui-tools": "^2.1.457",
|
|
136
136
|
"@types/node": "^25.2.3",
|
|
137
137
|
"@types/react": "^19.2.15",
|
|
138
138
|
"@types/react-dom": "^19.2.3",
|
|
@@ -46,19 +46,20 @@ export interface RedirectPageProps {
|
|
|
46
46
|
*/
|
|
47
47
|
export function RedirectPage({
|
|
48
48
|
authenticatedPath = '/private',
|
|
49
|
-
unauthenticatedPath
|
|
49
|
+
unauthenticatedPath,
|
|
50
50
|
loadingText = 'Loading...',
|
|
51
51
|
}: RedirectPageProps) {
|
|
52
|
-
const { isAuthenticated } = useAuth();
|
|
52
|
+
const { isAuthenticated, routes } = useAuth();
|
|
53
53
|
const router = useCfgRouter();
|
|
54
|
+
const resolvedUnauthPath = unauthenticatedPath ?? routes.auth;
|
|
54
55
|
|
|
55
56
|
useEffect(() => {
|
|
56
57
|
if (!isAuthenticated) {
|
|
57
|
-
router.hardPush(
|
|
58
|
+
router.hardPush(resolvedUnauthPath);
|
|
58
59
|
} else {
|
|
59
60
|
router.hardPush(authenticatedPath);
|
|
60
61
|
}
|
|
61
|
-
}, [isAuthenticated, router, authenticatedPath,
|
|
62
|
+
}, [isAuthenticated, router, authenticatedPath, resolvedUnauthPath]);
|
|
62
63
|
|
|
63
64
|
return (
|
|
64
65
|
<Preloader
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import React, { createContext, memo, useContext, useMemo } from 'react';
|
|
14
14
|
|
|
15
|
-
import { useCfgRouter } from '@djangocfg/api/auth';
|
|
15
|
+
import { consumeSavedRedirect, useAuth, useCfgRouter } from '@djangocfg/api/auth';
|
|
16
16
|
import { useAppT } from '@djangocfg/i18n';
|
|
17
17
|
|
|
18
18
|
import { Suspense } from '../../components';
|
|
@@ -49,7 +49,9 @@ export const AuthLayout: React.FC<AuthLayoutProps> = (props) => {
|
|
|
49
49
|
background,
|
|
50
50
|
sidebar,
|
|
51
51
|
enableGithubAuth,
|
|
52
|
-
|
|
52
|
+
// No default here — an unset prop falls through to the saved back-url /
|
|
53
|
+
// resolved routes.defaultCallback chain at navigation time (AuthSuccess).
|
|
54
|
+
redirectUrl,
|
|
53
55
|
onOAuthSuccess,
|
|
54
56
|
onError,
|
|
55
57
|
className,
|
|
@@ -143,6 +145,7 @@ const AuthSuccess: React.FC<AuthSuccessInlineProps> = memo(({
|
|
|
143
145
|
redirectDelay = AUTH.REDIRECT_DELAY,
|
|
144
146
|
}) => {
|
|
145
147
|
const router = useCfgRouter();
|
|
148
|
+
const { routes } = useAuth();
|
|
146
149
|
const t = useAppT();
|
|
147
150
|
const [isVisible, setIsVisible] = React.useState(false);
|
|
148
151
|
|
|
@@ -151,7 +154,9 @@ const AuthSuccess: React.FC<AuthSuccessInlineProps> = memo(({
|
|
|
151
154
|
React.useEffect(() => {
|
|
152
155
|
const animTimer = setTimeout(() => setIsVisible(true), AUTH.ANIMATION_START_DELAY);
|
|
153
156
|
const redirectTimer = setTimeout(() => {
|
|
154
|
-
|
|
157
|
+
// Priority: saved back-url (where the guard bounced the user from,
|
|
158
|
+
// single-use) > the app's static redirectUrl prop > resolved default.
|
|
159
|
+
const finalUrl = consumeSavedRedirect() || redirectUrl || routes.defaultCallback;
|
|
155
160
|
router.hardPush(finalUrl);
|
|
156
161
|
}, redirectDelay);
|
|
157
162
|
|
|
@@ -159,7 +164,7 @@ const AuthSuccess: React.FC<AuthSuccessInlineProps> = memo(({
|
|
|
159
164
|
clearTimeout(animTimer);
|
|
160
165
|
clearTimeout(redirectTimer);
|
|
161
166
|
};
|
|
162
|
-
}, [redirectUrl, redirectDelay, router]);
|
|
167
|
+
}, [redirectUrl, redirectDelay, router, routes.defaultCallback]);
|
|
163
168
|
|
|
164
169
|
return (
|
|
165
170
|
<div className="auth-success-overlay">
|
|
@@ -4,7 +4,7 @@ import { AlertCircle, Loader2 } from 'lucide-react';
|
|
|
4
4
|
import { useSearchParams } from 'next/navigation';
|
|
5
5
|
import React, { memo, useEffect, useState } from 'react';
|
|
6
6
|
|
|
7
|
-
import { useGithubAuth } from '@djangocfg/api/auth';
|
|
7
|
+
import { useAuth, useGithubAuth } from '@djangocfg/api/auth';
|
|
8
8
|
import {
|
|
9
9
|
Card, CardContent, CardDescription, CardHeader, CardTitle
|
|
10
10
|
} from '@djangocfg/ui-core/components';
|
|
@@ -58,6 +58,7 @@ function OAuthCallbackRaw({
|
|
|
58
58
|
redirectUrl,
|
|
59
59
|
}: OAuthCallbackProps) {
|
|
60
60
|
const searchParams = useSearchParams();
|
|
61
|
+
const { routes } = useAuth();
|
|
61
62
|
const { setStep, setTwoFactorSessionId, setShouldPrompt2FA } = useAuthFormContext();
|
|
62
63
|
const [status, setStatus] = useState<CallbackStatus | null>(null);
|
|
63
64
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
@@ -165,7 +166,7 @@ function OAuthCallbackRaw({
|
|
|
165
166
|
{status === 'error' && (
|
|
166
167
|
<CardContent className="text-center">
|
|
167
168
|
<a
|
|
168
|
-
href=
|
|
169
|
+
href={routes.auth}
|
|
169
170
|
className="text-primary hover:underline text-sm"
|
|
170
171
|
>
|
|
171
172
|
Try again
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* AuthLayout Constants
|
|
3
3
|
*
|
|
4
4
|
* Code lengths come from the shared single source of truth in
|
|
5
|
-
* `@djangocfg/api/auth` (AUTH_CONSTANTS). Layout-only values (timings, sizes
|
|
6
|
-
*
|
|
5
|
+
* `@djangocfg/api/auth` (AUTH_CONSTANTS). Layout-only values (timings, sizes)
|
|
6
|
+
* live here. Routes deliberately do NOT — they come resolved from
|
|
7
|
+
* `useAuth().routes` so an app's AuthConfig override is honored everywhere.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import { AUTH_CONSTANTS } from '@djangocfg/api/auth';
|
|
@@ -23,7 +24,4 @@ export const AUTH = {
|
|
|
23
24
|
|
|
24
25
|
// Sizes (px)
|
|
25
26
|
QR_CODE_SIZE: 160,
|
|
26
|
-
|
|
27
|
-
// Routes
|
|
28
|
-
DEFAULT_REDIRECT: '/dashboard',
|
|
29
27
|
} as const;
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
|
|
19
19
|
interface UseAuthGuardOptions {
|
|
20
20
|
requireAuth?: boolean;
|
|
21
|
+
/** Explicit override; defaults to the app's resolved `useAuth().routes.auth`. */
|
|
21
22
|
authPath?: string;
|
|
22
23
|
/**
|
|
23
24
|
* Safety net: if auth is still "loading" this many ms after mount, stop
|
|
@@ -39,10 +40,11 @@ interface UseAuthGuardResult {
|
|
|
39
40
|
|
|
40
41
|
export function useAuthGuard({
|
|
41
42
|
requireAuth = true,
|
|
42
|
-
authPath
|
|
43
|
+
authPath,
|
|
43
44
|
loadingTimeoutMs = 8000,
|
|
44
45
|
}: UseAuthGuardOptions): UseAuthGuardResult {
|
|
45
|
-
const { isAuthenticated, isLoading: authLoading, saveRedirectUrl } = useAuth();
|
|
46
|
+
const { isAuthenticated, isLoading: authLoading, saveRedirectUrl, routes } = useAuth();
|
|
47
|
+
const resolvedAuthPath = authPath ?? routes.auth;
|
|
46
48
|
const router = useRouter();
|
|
47
49
|
const [isRedirecting, setIsRedirecting] = useState(false);
|
|
48
50
|
|
|
@@ -83,10 +85,10 @@ export function useAuthGuard({
|
|
|
83
85
|
const currentUrl = window.location.pathname + window.location.search;
|
|
84
86
|
saveRedirectUrl(currentUrl);
|
|
85
87
|
setIsRedirecting(true);
|
|
86
|
-
router.push(
|
|
88
|
+
router.push(resolvedAuthPath);
|
|
87
89
|
}
|
|
88
90
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
89
|
-
}, [mounted, requireAuth, isAuthenticated, effectiveAuthLoading, isRedirecting, router, saveRedirectUrl,
|
|
91
|
+
}, [mounted, requireAuth, isAuthenticated, effectiveAuthLoading, isRedirecting, router, saveRedirectUrl, resolvedAuthPath]);
|
|
90
92
|
|
|
91
93
|
const isLoading = resolveGuardIsLoading(guardState);
|
|
92
94
|
const loadingText = isRedirecting ? 'Redirecting to login...' : 'Authenticating...';
|
|
@@ -69,11 +69,12 @@ export interface UserMenuProps {
|
|
|
69
69
|
export function UserMenu({
|
|
70
70
|
variant = 'desktop',
|
|
71
71
|
groups,
|
|
72
|
-
authPath
|
|
72
|
+
authPath,
|
|
73
73
|
disableAuth = false,
|
|
74
74
|
i18n,
|
|
75
75
|
}: UserMenuProps) {
|
|
76
|
-
const { user, isAuthenticated } = useAuth();
|
|
76
|
+
const { user, isAuthenticated, routes } = useAuth();
|
|
77
|
+
const resolvedAuthPath = authPath ?? routes.auth;
|
|
77
78
|
const handleLogout = useLogout();
|
|
78
79
|
const [mounted, setMounted] = React.useState(false);
|
|
79
80
|
const t = useAppT();
|
|
@@ -128,7 +129,7 @@ export function UserMenu({
|
|
|
128
129
|
return (
|
|
129
130
|
<div className="pt-4 border-t border-border/50">
|
|
130
131
|
<Link
|
|
131
|
-
href={
|
|
132
|
+
href={resolvedAuthPath}
|
|
132
133
|
className="group flex items-center justify-between rounded-full px-4 py-2.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground hover:bg-accent/40"
|
|
133
134
|
>
|
|
134
135
|
<span>{labels.signIn}</span>
|
|
@@ -145,7 +146,7 @@ export function UserMenu({
|
|
|
145
146
|
);
|
|
146
147
|
}
|
|
147
148
|
return (
|
|
148
|
-
<Link href={
|
|
149
|
+
<Link href={resolvedAuthPath}>
|
|
149
150
|
<Button variant="default" size="sm" className="rounded-full px-5">
|
|
150
151
|
{labels.signIn}
|
|
151
152
|
</Button>
|
|
@@ -47,7 +47,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
|
47
47
|
const navigation = props.navigation ?? [];
|
|
48
48
|
const userMenu = props.userMenu;
|
|
49
49
|
|
|
50
|
-
const { isAuthenticated, user } = useAuth();
|
|
50
|
+
const { isAuthenticated, user, routes } = useAuth();
|
|
51
51
|
const pathname = usePathnameWithoutLocale();
|
|
52
52
|
const t = useAppT();
|
|
53
53
|
const { mounted, visible } = useMobileNavPanel({
|
|
@@ -211,7 +211,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
|
211
211
|
{showSignInFooter && (
|
|
212
212
|
<div className="shrink-0 border-t border-border/50 p-4">
|
|
213
213
|
<Link
|
|
214
|
-
href={userMenu?.authPath ||
|
|
214
|
+
href={userMenu?.authPath || routes.auth}
|
|
215
215
|
onClick={closeMobileMenu}
|
|
216
216
|
className="block"
|
|
217
217
|
>
|
|
@@ -9,12 +9,6 @@ import {
|
|
|
9
9
|
AlertDescription,
|
|
10
10
|
Badge,
|
|
11
11
|
Button,
|
|
12
|
-
Dialog,
|
|
13
|
-
DialogContent,
|
|
14
|
-
DialogDescription,
|
|
15
|
-
DialogFooter,
|
|
16
|
-
DialogHeader,
|
|
17
|
-
DialogTitle,
|
|
18
12
|
OTPInput,
|
|
19
13
|
Preloader,
|
|
20
14
|
SettingRow,
|
|
@@ -49,69 +43,62 @@ function StatusBadge({ enabled }: { enabled: boolean }) {
|
|
|
49
43
|
}
|
|
50
44
|
|
|
51
45
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
-
// Disable dialog
|
|
46
|
+
// Disable confirmation — inline, not a dialog. Settings already lives in a
|
|
47
|
+
// dialog; stacking a second modal on top fights its focus trap and dims the
|
|
48
|
+
// context the user needs. Destructive confirmation expands in place instead
|
|
49
|
+
// (progressive disclosure), so the device list stays visible while confirming.
|
|
53
50
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
51
|
|
|
55
|
-
function
|
|
56
|
-
open,
|
|
52
|
+
function DisableConfirm({
|
|
57
53
|
isLoading,
|
|
58
54
|
error,
|
|
59
55
|
onConfirm,
|
|
60
56
|
onCancel,
|
|
61
57
|
}: {
|
|
62
|
-
open: boolean;
|
|
63
58
|
isLoading: boolean;
|
|
64
59
|
error: string | null;
|
|
65
60
|
onConfirm: (code: string) => void;
|
|
66
61
|
onCancel: () => void;
|
|
67
62
|
}) {
|
|
63
|
+
// Mounted fresh each time the confirmation opens — state starts clean.
|
|
68
64
|
const [code, setCode] = useState('');
|
|
69
65
|
|
|
70
|
-
// Reset on open
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
if (open) setCode('');
|
|
73
|
-
}, [open]);
|
|
74
|
-
|
|
75
66
|
return (
|
|
76
|
-
<
|
|
77
|
-
<
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
</DialogTitle>
|
|
83
|
-
<DialogDescription>
|
|
67
|
+
<div className="mt-3 space-y-4 rounded-lg border border-destructive/30 bg-destructive/5 p-4">
|
|
68
|
+
<div className="flex gap-2.5">
|
|
69
|
+
<ShieldOff className="mt-0.5 h-4 w-4 flex-shrink-0 text-destructive" />
|
|
70
|
+
<div className="space-y-1">
|
|
71
|
+
<p className="text-sm font-medium">Disable two-factor authentication?</p>
|
|
72
|
+
<p className="text-[13px] leading-relaxed text-muted-foreground">
|
|
84
73
|
Enter the 6-digit code from your authenticator app to confirm.
|
|
85
74
|
This will make your account less secure.
|
|
86
|
-
</
|
|
87
|
-
</DialogHeader>
|
|
88
|
-
|
|
89
|
-
<div className="py-2 space-y-4">
|
|
90
|
-
{error && (
|
|
91
|
-
<Alert variant="destructive">
|
|
92
|
-
<AlertDescription>{error}</AlertDescription>
|
|
93
|
-
</Alert>
|
|
94
|
-
)}
|
|
95
|
-
<div className="flex justify-center">
|
|
96
|
-
<OTPInput
|
|
97
|
-
length={6}
|
|
98
|
-
validationMode="numeric"
|
|
99
|
-
pasteBehavior="clean"
|
|
100
|
-
value={code}
|
|
101
|
-
onChange={setCode}
|
|
102
|
-
disabled={isLoading}
|
|
103
|
-
autoFocus
|
|
104
|
-
size="lg"
|
|
105
|
-
/>
|
|
106
|
-
</div>
|
|
75
|
+
</p>
|
|
107
76
|
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{error && (
|
|
80
|
+
<Alert variant="destructive">
|
|
81
|
+
<AlertDescription>{error}</AlertDescription>
|
|
82
|
+
</Alert>
|
|
83
|
+
)}
|
|
108
84
|
|
|
109
|
-
|
|
110
|
-
|
|
85
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
86
|
+
<OTPInput
|
|
87
|
+
length={6}
|
|
88
|
+
validationMode="numeric"
|
|
89
|
+
pasteBehavior="clean"
|
|
90
|
+
value={code}
|
|
91
|
+
onChange={setCode}
|
|
92
|
+
disabled={isLoading}
|
|
93
|
+
autoFocus
|
|
94
|
+
/>
|
|
95
|
+
<div className="flex gap-2">
|
|
96
|
+
<Button variant="outline" size="sm" onClick={onCancel} disabled={isLoading}>
|
|
111
97
|
Cancel
|
|
112
98
|
</Button>
|
|
113
99
|
<Button
|
|
114
100
|
variant="destructive"
|
|
101
|
+
size="sm"
|
|
115
102
|
onClick={() => onConfirm(code)}
|
|
116
103
|
disabled={isLoading || code.length !== 6}
|
|
117
104
|
>
|
|
@@ -121,9 +108,9 @@ function DisableDialog({
|
|
|
121
108
|
'Disable 2FA'
|
|
122
109
|
)}
|
|
123
110
|
</Button>
|
|
124
|
-
</
|
|
125
|
-
</
|
|
126
|
-
</
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
127
114
|
);
|
|
128
115
|
}
|
|
129
116
|
|
|
@@ -224,7 +211,7 @@ export const TwoFactorSection: React.FC = () => {
|
|
|
224
211
|
variant="outline"
|
|
225
212
|
size="sm"
|
|
226
213
|
onClick={() => { clearError(); setShowDisable(true); }}
|
|
227
|
-
disabled={isLoading}
|
|
214
|
+
disabled={isLoading || showDisable}
|
|
228
215
|
className="text-destructive hover:text-destructive hover:bg-destructive/10 border-destructive/30"
|
|
229
216
|
>
|
|
230
217
|
Disable
|
|
@@ -245,6 +232,16 @@ export const TwoFactorSection: React.FC = () => {
|
|
|
245
232
|
action={statusAction}
|
|
246
233
|
/>
|
|
247
234
|
|
|
235
|
+
{/* Inline destructive confirmation (no nested dialog) */}
|
|
236
|
+
{showDisable && (
|
|
237
|
+
<DisableConfirm
|
|
238
|
+
isLoading={isLoading}
|
|
239
|
+
error={error}
|
|
240
|
+
onConfirm={handleDisableConfirm}
|
|
241
|
+
onCancel={handleDisableCancel}
|
|
242
|
+
/>
|
|
243
|
+
)}
|
|
244
|
+
|
|
248
245
|
{/* Fetch error */}
|
|
249
246
|
{error && !showDisable && (
|
|
250
247
|
<div className="py-3">
|
|
@@ -279,14 +276,6 @@ export const TwoFactorSection: React.FC = () => {
|
|
|
279
276
|
</SettingsBlock>
|
|
280
277
|
)}
|
|
281
278
|
</div>
|
|
282
|
-
|
|
283
|
-
<DisableDialog
|
|
284
|
-
open={showDisable}
|
|
285
|
-
isLoading={isLoading}
|
|
286
|
-
error={error}
|
|
287
|
-
onConfirm={handleDisableConfirm}
|
|
288
|
-
onCancel={handleDisableCancel}
|
|
289
|
-
/>
|
|
290
279
|
</>
|
|
291
280
|
);
|
|
292
281
|
};
|
|
@@ -25,13 +25,16 @@ interface BuildBuiltinArgs {
|
|
|
25
25
|
* are left as plain strings here (the inner section bodies pull localized copy
|
|
26
26
|
* from ProfileProvider); only the nav-level label/title live at this layer.
|
|
27
27
|
*
|
|
28
|
-
* Defaults:
|
|
28
|
+
* Defaults: everything on. Security (2FA management) is a built-in the user
|
|
29
|
+
* must be able to find without the app opting in — `/cfg/totp/*` ships with
|
|
30
|
+
* every django-cfg backend. Apps that truly lack it opt out explicitly via
|
|
31
|
+
* `builtins.security: false`.
|
|
29
32
|
*/
|
|
30
33
|
export function buildBuiltinSections({
|
|
31
34
|
builtins,
|
|
32
35
|
enableDeleteAccount,
|
|
33
36
|
}: BuildBuiltinArgs): SettingsSection[] {
|
|
34
|
-
const { account = true, security =
|
|
37
|
+
const { account = true, security = true, apiKeys = true } = builtins;
|
|
35
38
|
const sections: SettingsSection[] = [];
|
|
36
39
|
|
|
37
40
|
if (account) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { LogIn } from 'lucide-react';
|
|
4
4
|
import React, { useMemo } from 'react';
|
|
5
5
|
|
|
6
|
-
import { useCfgRouter } from '@djangocfg/api/auth';
|
|
6
|
+
import { useAuth, useCfgRouter } from '@djangocfg/api/auth';
|
|
7
7
|
import { useAppT } from '@djangocfg/i18n';
|
|
8
8
|
import {
|
|
9
9
|
Button, Dialog, DialogContent, DialogHeader, DialogTitle
|
|
@@ -17,7 +17,7 @@ interface AuthDialogProps {
|
|
|
17
17
|
|
|
18
18
|
export const AuthDialog: React.FC<AuthDialogProps> = ({
|
|
19
19
|
onAuthRequired,
|
|
20
|
-
authPath
|
|
20
|
+
authPath
|
|
21
21
|
}) => {
|
|
22
22
|
const open = useDialogStore((s) => s.authOpen);
|
|
23
23
|
const authOptions = useDialogStore((s) => s.authOptions);
|
|
@@ -32,21 +32,24 @@ export const AuthDialog: React.FC<AuthDialogProps> = ({
|
|
|
32
32
|
|
|
33
33
|
const message = authOptions?.message || labels.pleaseSignIn;
|
|
34
34
|
const router = useCfgRouter();
|
|
35
|
+
const { saveRedirectUrl, routes } = useAuth();
|
|
35
36
|
|
|
36
37
|
const handleClose = () => {
|
|
37
38
|
resolveAuth(false);
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
const handleGoToAuth = () => {
|
|
42
|
+
// Canonical post-login redirect persistence (the same store the guard and
|
|
43
|
+
// verifyOTP read). The old private `redirectAfterAuth` sessionStorage key
|
|
44
|
+
// was write-only — nothing ever read it.
|
|
41
45
|
if (typeof window !== 'undefined') {
|
|
42
|
-
|
|
43
|
-
sessionStorage.setItem('redirectAfterAuth', redirect);
|
|
46
|
+
saveRedirectUrl(authOptions?.redirectUrl || window.location.pathname);
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
if (onAuthRequired) {
|
|
47
50
|
onAuthRequired();
|
|
48
51
|
} else {
|
|
49
|
-
router.push(authPath);
|
|
52
|
+
router.push(authPath ?? routes.auth);
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
resolveAuth(true);
|
|
@@ -62,6 +62,7 @@ export const MockAuthProvider: React.FC<MockAuthProviderProps> = ({
|
|
|
62
62
|
isLoading,
|
|
63
63
|
isAuthenticated: isAuthenticated ?? Boolean(user),
|
|
64
64
|
isAdminUser,
|
|
65
|
+
routes: { auth: '/auth', defaultCallback: '/', defaultAuthCallback: '/auth' },
|
|
65
66
|
|
|
66
67
|
loadCurrentProfile: noopAsync,
|
|
67
68
|
checkAuthAndRedirect: noopAsync,
|