@djangocfg/layouts 2.1.411 → 2.1.413
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 +22 -17
- package/src/components/errors/ErrorBoundary.tsx +23 -2
- package/src/components/errors/ErrorLayout.tsx +2 -3
- package/src/layouts/AuthLayout/context.tsx +3 -1
- package/src/layouts/AuthLayout/shells/CenteredShell.tsx +5 -17
- package/src/testing/MockAuthFormProvider.tsx +185 -0
- package/src/testing/MockAuthProvider.tsx +97 -0
- package/src/testing/index.ts +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.413",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -70,6 +70,11 @@
|
|
|
70
70
|
"import": "./src/configurator/private/index.ts",
|
|
71
71
|
"require": "./src/configurator/private/index.ts"
|
|
72
72
|
},
|
|
73
|
+
"./testing": {
|
|
74
|
+
"types": "./src/testing/index.ts",
|
|
75
|
+
"import": "./src/testing/index.ts",
|
|
76
|
+
"require": "./src/testing/index.ts"
|
|
77
|
+
},
|
|
73
78
|
"./styles": "./src/styles/index.css",
|
|
74
79
|
"./styles/dashboard": "./src/styles/dashboard.css"
|
|
75
80
|
},
|
|
@@ -84,13 +89,13 @@
|
|
|
84
89
|
"check": "tsc --noEmit"
|
|
85
90
|
},
|
|
86
91
|
"peerDependencies": {
|
|
87
|
-
"@djangocfg/api": "^2.1.
|
|
88
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
89
|
-
"@djangocfg/debuger": "^2.1.
|
|
90
|
-
"@djangocfg/i18n": "^2.1.
|
|
91
|
-
"@djangocfg/monitor": "^2.1.
|
|
92
|
-
"@djangocfg/ui-core": "^2.1.
|
|
93
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
92
|
+
"@djangocfg/api": "^2.1.413",
|
|
93
|
+
"@djangocfg/centrifugo": "^2.1.413",
|
|
94
|
+
"@djangocfg/debuger": "^2.1.413",
|
|
95
|
+
"@djangocfg/i18n": "^2.1.413",
|
|
96
|
+
"@djangocfg/monitor": "^2.1.413",
|
|
97
|
+
"@djangocfg/ui-core": "^2.1.413",
|
|
98
|
+
"@djangocfg/ui-nextjs": "^2.1.413",
|
|
94
99
|
"@hookform/resolvers": "^5.2.2",
|
|
95
100
|
"consola": "^3.4.2",
|
|
96
101
|
"lucide-react": "^0.545.0",
|
|
@@ -121,15 +126,15 @@
|
|
|
121
126
|
"uuid": "^11.1.0"
|
|
122
127
|
},
|
|
123
128
|
"devDependencies": {
|
|
124
|
-
"@djangocfg/api": "^2.1.
|
|
125
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
126
|
-
"@djangocfg/debuger": "^2.1.
|
|
127
|
-
"@djangocfg/i18n": "^2.1.
|
|
128
|
-
"@djangocfg/monitor": "^2.1.
|
|
129
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
130
|
-
"@djangocfg/ui-core": "^2.1.
|
|
131
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
132
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
129
|
+
"@djangocfg/api": "^2.1.413",
|
|
130
|
+
"@djangocfg/centrifugo": "^2.1.413",
|
|
131
|
+
"@djangocfg/debuger": "^2.1.413",
|
|
132
|
+
"@djangocfg/i18n": "^2.1.413",
|
|
133
|
+
"@djangocfg/monitor": "^2.1.413",
|
|
134
|
+
"@djangocfg/typescript-config": "^2.1.413",
|
|
135
|
+
"@djangocfg/ui-core": "^2.1.413",
|
|
136
|
+
"@djangocfg/ui-nextjs": "^2.1.413",
|
|
137
|
+
"@djangocfg/ui-tools": "^2.1.413",
|
|
133
138
|
"@types/node": "^25.2.3",
|
|
134
139
|
"@types/react": "^19.2.15",
|
|
135
140
|
"@types/react-dom": "^19.2.3",
|
|
@@ -23,6 +23,8 @@ interface ErrorBoundaryState {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
26
|
+
private handlePopState: (() => void) | null = null;
|
|
27
|
+
|
|
26
28
|
constructor(props: ErrorBoundaryProps) {
|
|
27
29
|
super(props);
|
|
28
30
|
this.state = { hasError: false, error: null };
|
|
@@ -33,17 +35,36 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
36
|
-
// Call custom error handler if provided
|
|
37
38
|
if (this.props.onError) {
|
|
38
39
|
this.props.onError(error, errorInfo);
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
// Log to console in development
|
|
42
42
|
if (isDev) {
|
|
43
43
|
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
componentDidUpdate(_prevProps: ErrorBoundaryProps, prevState: ErrorBoundaryState) {
|
|
48
|
+
if (this.state.hasError && !prevState.hasError) {
|
|
49
|
+
this.handlePopState = () => {
|
|
50
|
+
window.location.reload();
|
|
51
|
+
};
|
|
52
|
+
window.addEventListener('popstate', this.handlePopState);
|
|
53
|
+
} else if (!this.state.hasError && prevState.hasError) {
|
|
54
|
+
if (this.handlePopState) {
|
|
55
|
+
window.removeEventListener('popstate', this.handlePopState);
|
|
56
|
+
this.handlePopState = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
componentWillUnmount() {
|
|
62
|
+
if (this.handlePopState) {
|
|
63
|
+
window.removeEventListener('popstate', this.handlePopState);
|
|
64
|
+
this.handlePopState = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
47
68
|
render() {
|
|
48
69
|
if (this.state.hasError) {
|
|
49
70
|
const title = getT('layouts.errors.somethingWentWrong');
|
|
@@ -258,10 +258,9 @@ export function ErrorLayout({
|
|
|
258
258
|
const handleGoBack = () => {
|
|
259
259
|
if (document.referrer && document.referrer !== window.location.href) {
|
|
260
260
|
window.location.href = document.referrer;
|
|
261
|
-
} else if (window.history.length > 1) {
|
|
262
|
-
window.history.back();
|
|
263
261
|
} else {
|
|
264
|
-
window.
|
|
262
|
+
window.history.back();
|
|
263
|
+
setTimeout(() => window.location.reload(), 100);
|
|
265
264
|
}
|
|
266
265
|
};
|
|
267
266
|
|
|
@@ -6,7 +6,9 @@ import { useAuthForm } from '@djangocfg/api/auth';
|
|
|
6
6
|
|
|
7
7
|
import type { AuthFormContextType, AuthLayoutProps } from './types';
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// Exported so test/mock providers (storybook) can supply their own value
|
|
10
|
+
// without re-implementing AuthFormProvider end-to-end.
|
|
11
|
+
export const AuthFormContext = createContext<AuthFormContextType | undefined>(undefined);
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* AuthFormProvider — wraps useAuthForm and merges UI config into context.
|
|
@@ -2,22 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { memo } from 'react';
|
|
4
4
|
|
|
5
|
-
import { GlowBackground } from '@djangocfg/ui-core/components';
|
|
6
|
-
|
|
7
5
|
import type { ShellRenderProps } from './types';
|
|
8
6
|
|
|
9
7
|
/**
|
|
10
|
-
* CenteredShell —
|
|
11
|
-
*
|
|
12
|
-
* Features:
|
|
13
|
-
* - Full viewport centering
|
|
14
|
-
* - Glow background (default)
|
|
15
|
-
* - Optional custom background image/gradient via shell props
|
|
16
|
-
* - No visible card chrome
|
|
8
|
+
* CenteredShell — frameless auth layout, full viewport centering.
|
|
17
9
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
10
|
+
* Default background is plain `var(--background)` — the form chrome
|
|
11
|
+
* carries the design. Callers can still supply a custom
|
|
12
|
+
* image/gradient/overlay via `bgStyle` / `overlayStyle`.
|
|
21
13
|
*/
|
|
22
14
|
function CenteredShellRaw({
|
|
23
15
|
children,
|
|
@@ -29,19 +21,15 @@ function CenteredShellRaw({
|
|
|
29
21
|
|
|
30
22
|
return (
|
|
31
23
|
<div className={`auth-shell-centered ${className || ''}`}>
|
|
32
|
-
{
|
|
33
|
-
{hasCustomBg ? (
|
|
24
|
+
{hasCustomBg && (
|
|
34
25
|
<>
|
|
35
26
|
<div className="auth-shell-centered__bg" style={bgStyle} />
|
|
36
27
|
{overlayStyle && (
|
|
37
28
|
<div className="auth-shell-centered__overlay" style={overlayStyle} />
|
|
38
29
|
)}
|
|
39
30
|
</>
|
|
40
|
-
) : (
|
|
41
|
-
<GlowBackground />
|
|
42
31
|
)}
|
|
43
32
|
|
|
44
|
-
{/* Content */}
|
|
45
33
|
<div className="auth-shell-centered__content">
|
|
46
34
|
{children}
|
|
47
35
|
</div>
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useMemo, useRef, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
AuthFormReturn,
|
|
7
|
+
AuthStep,
|
|
8
|
+
} from '@djangocfg/api/auth';
|
|
9
|
+
|
|
10
|
+
import { AuthFormContext } from '../layouts/AuthLayout/context';
|
|
11
|
+
import type { AuthFormContextType, AuthLayoutConfig } from '../layouts/AuthLayout/types';
|
|
12
|
+
|
|
13
|
+
// Storybook-only provider. Reproduces the AuthFormContext shape with
|
|
14
|
+
// in-memory state and no-op async handlers, so step components and the
|
|
15
|
+
// full AuthLayout can be exercised without hitting @djangocfg/api/auth.
|
|
16
|
+
|
|
17
|
+
export interface MockAuthFormProviderProps extends Partial<AuthLayoutConfig> {
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
|
|
20
|
+
// Initial state — every field has a sensible default. Override per story.
|
|
21
|
+
step?: AuthStep;
|
|
22
|
+
identifier?: string;
|
|
23
|
+
otp?: string;
|
|
24
|
+
isLoading?: boolean;
|
|
25
|
+
acceptedTerms?: boolean;
|
|
26
|
+
error?: string;
|
|
27
|
+
twoFactorSessionId?: string | null;
|
|
28
|
+
shouldPrompt2FA?: boolean;
|
|
29
|
+
twoFactorCode?: string;
|
|
30
|
+
useBackupCode?: boolean;
|
|
31
|
+
rateLimitSeconds?: number;
|
|
32
|
+
is2FALoading?: boolean;
|
|
33
|
+
twoFactorWarning?: string | null;
|
|
34
|
+
twoFactorAttemptsRemaining?: number | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatRateLimit(seconds: number): string {
|
|
38
|
+
if (seconds <= 0) return '';
|
|
39
|
+
if (seconds >= 60) {
|
|
40
|
+
const m = Math.floor(seconds / 60);
|
|
41
|
+
const s = seconds % 60;
|
|
42
|
+
return `${m}:${String(s).padStart(2, '0')}`;
|
|
43
|
+
}
|
|
44
|
+
return `${seconds}s`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
|
|
48
|
+
children,
|
|
49
|
+
|
|
50
|
+
// Layout config
|
|
51
|
+
sourceUrl = 'https://example.com',
|
|
52
|
+
supportUrl,
|
|
53
|
+
termsUrl,
|
|
54
|
+
privacyUrl,
|
|
55
|
+
enableGithubAuth = false,
|
|
56
|
+
enable2FASetup = true,
|
|
57
|
+
logoUrl,
|
|
58
|
+
redirectUrl = '/dashboard',
|
|
59
|
+
|
|
60
|
+
// Initial state
|
|
61
|
+
step: initialStep = 'identifier',
|
|
62
|
+
identifier: initialIdentifier = '',
|
|
63
|
+
otp: initialOtp = '',
|
|
64
|
+
isLoading: initialLoading = false,
|
|
65
|
+
acceptedTerms: initialAcceptedTerms = false,
|
|
66
|
+
error: initialError = '',
|
|
67
|
+
twoFactorSessionId: initialTwoFactorSessionId = null,
|
|
68
|
+
shouldPrompt2FA: initialShouldPrompt2FA = false,
|
|
69
|
+
twoFactorCode: initialTwoFactorCode = '',
|
|
70
|
+
useBackupCode: initialUseBackupCode = false,
|
|
71
|
+
rateLimitSeconds: initialRateLimitSeconds = 0,
|
|
72
|
+
is2FALoading = false,
|
|
73
|
+
twoFactorWarning = null,
|
|
74
|
+
twoFactorAttemptsRemaining = null,
|
|
75
|
+
}) => {
|
|
76
|
+
const [step, setStep] = useState<AuthStep>(initialStep);
|
|
77
|
+
const [identifier, setIdentifier] = useState(initialIdentifier);
|
|
78
|
+
const [otp, setOtp] = useState(initialOtp);
|
|
79
|
+
const [isLoading, setIsLoading] = useState(initialLoading);
|
|
80
|
+
const [acceptedTerms, setAcceptedTerms] = useState(initialAcceptedTerms);
|
|
81
|
+
const [error, setError] = useState(initialError);
|
|
82
|
+
const [twoFactorSessionId, setTwoFactorSessionId] = useState<string | null>(initialTwoFactorSessionId);
|
|
83
|
+
const [shouldPrompt2FA, setShouldPrompt2FA] = useState(initialShouldPrompt2FA);
|
|
84
|
+
const [twoFactorCode, setTwoFactorCode] = useState(initialTwoFactorCode);
|
|
85
|
+
const [useBackupCode, setUseBackupCode] = useState(initialUseBackupCode);
|
|
86
|
+
const [rateLimitSeconds, setRateLimitSeconds] = useState(initialRateLimitSeconds);
|
|
87
|
+
|
|
88
|
+
const isAutoSubmittingFromUrl = useRef(false);
|
|
89
|
+
|
|
90
|
+
const value: AuthFormContextType = useMemo(() => {
|
|
91
|
+
const stop = (e: React.FormEvent) => {
|
|
92
|
+
e.preventDefault?.();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
// State
|
|
97
|
+
identifier,
|
|
98
|
+
otp,
|
|
99
|
+
isLoading,
|
|
100
|
+
acceptedTerms,
|
|
101
|
+
step,
|
|
102
|
+
error,
|
|
103
|
+
twoFactorSessionId,
|
|
104
|
+
shouldPrompt2FA,
|
|
105
|
+
twoFactorCode,
|
|
106
|
+
useBackupCode,
|
|
107
|
+
rateLimitSeconds,
|
|
108
|
+
isRateLimited: rateLimitSeconds > 0,
|
|
109
|
+
rateLimitLabel: formatRateLimit(rateLimitSeconds),
|
|
110
|
+
|
|
111
|
+
// State handlers
|
|
112
|
+
setIdentifier,
|
|
113
|
+
setOtp,
|
|
114
|
+
setAcceptedTerms,
|
|
115
|
+
setError,
|
|
116
|
+
clearError: () => setError(''),
|
|
117
|
+
setStep,
|
|
118
|
+
setIsLoading,
|
|
119
|
+
setTwoFactorSessionId,
|
|
120
|
+
setShouldPrompt2FA,
|
|
121
|
+
setTwoFactorCode,
|
|
122
|
+
setUseBackupCode,
|
|
123
|
+
startRateLimitCountdown: (seconds: number) => setRateLimitSeconds(seconds),
|
|
124
|
+
|
|
125
|
+
// Submit handlers — all no-op (preventDefault only) so the visible
|
|
126
|
+
// step never changes from under the storyteller. To preview a
|
|
127
|
+
// different step, render a new story with `step="..."`.
|
|
128
|
+
handleIdentifierSubmit: async (e) => stop(e),
|
|
129
|
+
handleOTPSubmit: async (e) => stop(e),
|
|
130
|
+
handleResendOTP: async () => {},
|
|
131
|
+
handleBackToIdentifier: () => setStep('identifier'),
|
|
132
|
+
forceOTPStep: () => setStep('otp'),
|
|
133
|
+
handle2FASubmit: async (e) => stop(e),
|
|
134
|
+
handleUseBackupCode: () => setUseBackupCode(true),
|
|
135
|
+
handleUseTOTP: () => setUseBackupCode(false),
|
|
136
|
+
|
|
137
|
+
// Validation — accept anything that looks email-ish or 10+ digits.
|
|
138
|
+
validateIdentifier: (id: string) =>
|
|
139
|
+
/.+@.+\..+/.test(id) || /^\+?\d{10,}$/.test(id),
|
|
140
|
+
|
|
141
|
+
// Auto-submit ref
|
|
142
|
+
isAutoSubmittingFromUrl,
|
|
143
|
+
|
|
144
|
+
// 2FA state (read-only here; tests/stories override via props)
|
|
145
|
+
is2FALoading,
|
|
146
|
+
twoFactorWarning,
|
|
147
|
+
twoFactorAttemptsRemaining,
|
|
148
|
+
|
|
149
|
+
// Layout config
|
|
150
|
+
sourceUrl,
|
|
151
|
+
supportUrl,
|
|
152
|
+
termsUrl,
|
|
153
|
+
privacyUrl,
|
|
154
|
+
enableGithubAuth,
|
|
155
|
+
enable2FASetup,
|
|
156
|
+
logoUrl,
|
|
157
|
+
redirectUrl,
|
|
158
|
+
};
|
|
159
|
+
}, [
|
|
160
|
+
identifier,
|
|
161
|
+
otp,
|
|
162
|
+
isLoading,
|
|
163
|
+
acceptedTerms,
|
|
164
|
+
step,
|
|
165
|
+
error,
|
|
166
|
+
twoFactorSessionId,
|
|
167
|
+
shouldPrompt2FA,
|
|
168
|
+
twoFactorCode,
|
|
169
|
+
useBackupCode,
|
|
170
|
+
rateLimitSeconds,
|
|
171
|
+
is2FALoading,
|
|
172
|
+
twoFactorWarning,
|
|
173
|
+
twoFactorAttemptsRemaining,
|
|
174
|
+
sourceUrl,
|
|
175
|
+
supportUrl,
|
|
176
|
+
termsUrl,
|
|
177
|
+
privacyUrl,
|
|
178
|
+
enableGithubAuth,
|
|
179
|
+
enable2FASetup,
|
|
180
|
+
logoUrl,
|
|
181
|
+
redirectUrl,
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
return <AuthFormContext.Provider value={value}>{children}</AuthFormContext.Provider>;
|
|
185
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useMemo, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
AuthContext,
|
|
7
|
+
type AuthContextType,
|
|
8
|
+
type UserProfile,
|
|
9
|
+
} from '@djangocfg/api/auth';
|
|
10
|
+
|
|
11
|
+
// Storybook-only auth provider. Reproduces the AuthContext shape with
|
|
12
|
+
// fixed in-memory values so components that call `useAuth()` (Profile,
|
|
13
|
+
// PrivateLayout, etc.) can render without hitting the real auth API.
|
|
14
|
+
|
|
15
|
+
export interface MockAuthProviderProps {
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
user?: UserProfile | null;
|
|
18
|
+
isLoading?: boolean;
|
|
19
|
+
/** Override `isAuthenticated`. Defaults to `!!user`. */
|
|
20
|
+
isAuthenticated?: boolean;
|
|
21
|
+
/** Override `isAdminUser`. Defaults to `false`. */
|
|
22
|
+
isAdminUser?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const noopAsync = async () => {};
|
|
26
|
+
|
|
27
|
+
export function buildMockUser(overrides: Partial<UserProfile> = {}): UserProfile {
|
|
28
|
+
return {
|
|
29
|
+
id: 1,
|
|
30
|
+
email: 'jane.cooper@example.com',
|
|
31
|
+
username: 'jane.cooper',
|
|
32
|
+
first_name: 'Jane',
|
|
33
|
+
last_name: 'Cooper',
|
|
34
|
+
full_name: 'Jane Cooper',
|
|
35
|
+
is_active: true,
|
|
36
|
+
is_staff: false,
|
|
37
|
+
is_superuser: false,
|
|
38
|
+
date_joined: '2025-01-15T10:00:00Z',
|
|
39
|
+
last_login: '2026-05-20T14:30:00Z',
|
|
40
|
+
initials: 'JC',
|
|
41
|
+
display_username: 'jane.cooper',
|
|
42
|
+
company: null,
|
|
43
|
+
phone: null,
|
|
44
|
+
position: null,
|
|
45
|
+
unanswered_messages_count: 0,
|
|
46
|
+
avatar: null,
|
|
47
|
+
...overrides,
|
|
48
|
+
} as UserProfile;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const MockAuthProvider: React.FC<MockAuthProviderProps> = ({
|
|
52
|
+
children,
|
|
53
|
+
user: initialUser = buildMockUser(),
|
|
54
|
+
isLoading = false,
|
|
55
|
+
isAuthenticated,
|
|
56
|
+
isAdminUser = false,
|
|
57
|
+
}) => {
|
|
58
|
+
const [user, setUser] = useState<UserProfile | null>(initialUser);
|
|
59
|
+
|
|
60
|
+
const value = useMemo<AuthContextType>(() => ({
|
|
61
|
+
user,
|
|
62
|
+
isLoading,
|
|
63
|
+
isAuthenticated: isAuthenticated ?? Boolean(user),
|
|
64
|
+
isAdminUser,
|
|
65
|
+
|
|
66
|
+
loadCurrentProfile: noopAsync,
|
|
67
|
+
checkAuthAndRedirect: noopAsync,
|
|
68
|
+
|
|
69
|
+
getToken: () => 'mock-access-token',
|
|
70
|
+
getRefreshToken: () => 'mock-refresh-token',
|
|
71
|
+
|
|
72
|
+
getSavedEmail: () => user?.email ?? null,
|
|
73
|
+
saveEmail: () => {},
|
|
74
|
+
clearSavedEmail: () => {},
|
|
75
|
+
|
|
76
|
+
requestOTP: async () => ({ success: true, message: 'mock' }),
|
|
77
|
+
verifyOTP: async () => ({ success: true, message: 'mock', user: user ?? undefined }),
|
|
78
|
+
refreshToken: async () => ({ success: true, message: 'mock' }),
|
|
79
|
+
logout: () => {},
|
|
80
|
+
|
|
81
|
+
saveRedirectUrl: () => {},
|
|
82
|
+
getRedirectUrl: () => '/dashboard',
|
|
83
|
+
clearRedirectUrl: () => {},
|
|
84
|
+
hasRedirectUrl: () => false,
|
|
85
|
+
|
|
86
|
+
// Edits update local state so the form reflects the change in
|
|
87
|
+
// Storybook just like a real backend round-trip would.
|
|
88
|
+
updateProfile: async (data) => {
|
|
89
|
+
const next = { ...(user ?? buildMockUser()), ...data } as UserProfile;
|
|
90
|
+
setUser(next);
|
|
91
|
+
return next;
|
|
92
|
+
},
|
|
93
|
+
uploadAvatar: async () => user ?? buildMockUser(),
|
|
94
|
+
}), [user, isLoading, isAuthenticated, isAdminUser]);
|
|
95
|
+
|
|
96
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
97
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Storybook / test-only helpers. Public consumers should NOT import
|
|
2
|
+
// from this entrypoint in production code.
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
MockAuthFormProvider,
|
|
6
|
+
type MockAuthFormProviderProps,
|
|
7
|
+
} from './MockAuthFormProvider';
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
MockAuthProvider,
|
|
11
|
+
buildMockUser,
|
|
12
|
+
type MockAuthProviderProps,
|
|
13
|
+
} from './MockAuthProvider';
|
|
14
|
+
|
|
15
|
+
// Auth step type, re-exported so storybook stories don't have to depend
|
|
16
|
+
// on @djangocfg/api/auth directly.
|
|
17
|
+
export type { AuthStep } from '@djangocfg/api/auth';
|
|
18
|
+
|
|
19
|
+
// Re-export auth internals so storybook can mount step components
|
|
20
|
+
// directly under MockAuthFormProvider without going through the full
|
|
21
|
+
// AuthLayout (which mounts its own real AuthFormProvider).
|
|
22
|
+
export { AuthShell } from '../layouts/AuthLayout/shells';
|
|
23
|
+
export {
|
|
24
|
+
IdentifierStep,
|
|
25
|
+
OTPStep,
|
|
26
|
+
TwoFactorStep,
|
|
27
|
+
SetupStep,
|
|
28
|
+
} from '../layouts/AuthLayout/components/steps';
|