@umituz/web-dashboard 3.2.4 → 4.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/README.md +228 -14
- package/package.json +23 -30
- package/src/domains/analytics/components/AnalyticsCard.tsx +43 -42
- package/src/domains/analytics/components/AnalyticsChart.tsx +345 -149
- package/src/domains/analytics/components/AnalyticsLayout.tsx +93 -112
- package/src/domains/analytics/components/MetricCard.tsx +29 -87
- package/src/domains/analytics/hooks/index.ts +7 -1
- package/src/domains/analytics/hooks/useAnalytics.ts +144 -139
- package/src/domains/analytics/index.ts +11 -0
- package/src/domains/analytics/services/AnalyticsEngineService.ts +103 -55
- package/src/domains/analytics/services/PerformanceService.ts +117 -69
- package/src/domains/analytics/types/analytics.ts +20 -6
- package/src/domains/analytics/types/index.ts +3 -0
- package/src/domains/analytics/utils/analytics.test.ts +183 -0
- package/src/domains/analytics/utils/analytics.ts +26 -9
- package/src/domains/analytics/utils/i18nKeys.ts +40 -0
- package/src/domains/analytics/utils/index.ts +2 -0
- package/src/domains/auth/components/AuthLayout.tsx +63 -54
- package/src/domains/auth/components/ForgotPasswordForm.tsx +96 -98
- package/src/domains/auth/components/LoginForm.tsx +167 -226
- package/src/domains/auth/components/RegisterForm.tsx +202 -272
- package/src/domains/auth/components/ResetPasswordForm.tsx +105 -151
- package/src/domains/auth/components/shared/AuthErrorBanner.tsx +35 -0
- package/src/domains/auth/components/shared/EmailInput.tsx +91 -0
- package/src/domains/auth/components/shared/PasswordInput.tsx +119 -0
- package/src/domains/auth/components/shared/PasswordStrengthIndicator.tsx +75 -0
- package/src/domains/auth/components/shared/SocialLoginButtons.tsx +58 -0
- package/src/domains/auth/components/shared/SocialProviderIcons.tsx +48 -0
- package/src/domains/auth/components/shared/index.ts +20 -0
- package/src/domains/auth/hooks/index.ts +1 -0
- package/src/domains/auth/hooks/useAuth.ts +151 -232
- package/src/domains/auth/index.ts +8 -4
- package/src/domains/auth/types/auth.ts +26 -5
- package/src/domains/auth/types/index.ts +1 -0
- package/src/domains/auth/utils/auth.test.ts +182 -0
- package/src/domains/auth/utils/auth.ts +71 -130
- package/src/domains/auth/utils/i18nKeys.ts +127 -0
- package/src/domains/auth/utils/index.ts +7 -1
- package/src/domains/auth/utils/secureToken.ts +33 -0
- package/src/domains/auth/utils/validationKeys.ts +23 -0
- package/src/domains/calendar/hooks/useCalendar.ts +145 -151
- package/src/domains/calendar/index.ts +0 -1
- package/src/domains/calendar/services/CalendarService.ts +122 -179
- package/src/domains/calendar/utils/contentItemMapper.test.ts +108 -0
- package/src/domains/calendar/utils/contentItemMapper.ts +99 -0
- package/src/domains/calendar/utils/index.ts +33 -30
- package/src/domains/layouts/components/BrandLogo.tsx +12 -9
- package/src/domains/layouts/components/DashboardHeader.tsx +78 -80
- package/src/domains/layouts/components/DashboardLayout.tsx +45 -21
- package/src/domains/layouts/components/DashboardSidebar.tsx +48 -51
- package/src/domains/layouts/types/layout.ts +7 -1
- package/src/domains/layouts/types/user.ts +3 -1
- package/src/domains/modules/build.test.tsx +97 -0
- package/src/domains/modules/build.tsx +71 -0
- package/src/domains/modules/index.ts +2 -0
- package/src/domains/modules/types.ts +29 -0
- package/src/domains/onboarding/components/AppFocusStep.tsx +99 -68
- package/src/domains/onboarding/components/OnboardingWizard.tsx +109 -183
- package/src/domains/onboarding/components/PlanStep.tsx +156 -123
- package/src/domains/onboarding/components/PlatformsStep.tsx +74 -54
- package/src/domains/onboarding/components/StepNavigation.tsx +108 -0
- package/src/domains/onboarding/components/StepProgress.tsx +62 -0
- package/src/domains/onboarding/components/UserTypeStep.tsx +59 -80
- package/src/domains/onboarding/components/index.ts +4 -0
- package/src/domains/onboarding/hooks/useOnboarding.ts +76 -66
- package/src/domains/onboarding/index.ts +1 -0
- package/src/domains/onboarding/types/onboarding.ts +57 -17
- package/src/domains/onboarding/utils/i18nKeys.ts +72 -0
- package/src/domains/onboarding/utils/index.ts +2 -0
- package/src/domains/permissions/PermissionsContext.tsx +103 -0
- package/src/domains/permissions/hasPermission.test.ts +50 -0
- package/src/domains/permissions/hasPermission.ts +38 -0
- package/src/domains/permissions/index.ts +19 -0
- package/src/domains/permissions/types.ts +35 -0
- package/src/domains/permissions/utils/i18nKeys.ts +11 -0
- package/src/domains/session/DemoLogin.tsx +50 -0
- package/src/domains/session/OrgSwitcher.tsx +110 -0
- package/src/domains/session/SessionContext.tsx +150 -0
- package/src/domains/session/inMemoryAuthAdapter.ts +53 -0
- package/src/domains/session/index.ts +18 -0
- package/src/domains/session/types.ts +47 -0
- package/src/domains/session/utils/i18nKeys.ts +12 -0
- package/src/domains/settings/components/AuditLogList.tsx +59 -0
- package/src/domains/settings/components/PermissionMatrixScreen.tsx +80 -0
- package/src/domains/settings/components/SettingsLayout.tsx +75 -60
- package/src/domains/settings/components/SettingsSection.tsx +35 -22
- package/src/domains/settings/components/UserManagementTable.tsx +69 -0
- package/src/domains/settings/components/index.ts +10 -0
- package/src/domains/settings/hooks/useSettings.ts +77 -52
- package/src/domains/settings/types/settings.ts +2 -0
- package/src/domains/settings/utils/i18nKeys.ts +18 -0
- package/src/domains/settings/utils/index.ts +1 -0
- package/src/domains/settings/utils/settings.ts +11 -4
- package/src/i18n/I18nContext.tsx +33 -0
- package/src/i18n/defaultT.test.ts +43 -0
- package/src/i18n/defaultT.ts +34 -0
- package/src/i18n/index.ts +7 -0
- package/src/i18n/types.ts +13 -0
- package/src/index.ts +1 -7
- package/src/primitives/Button.tsx +24 -0
- package/src/primitives/Checkbox.tsx +17 -0
- package/src/primitives/Input.tsx +23 -0
- package/src/primitives/Skeleton.tsx +6 -0
- package/src/primitives/cn.ts +10 -0
- package/src/primitives/index.ts +23 -0
- package/src/styles/dashboard.css +2731 -0
- package/src/styles/tokens.contract.test.ts +91 -0
- package/src/styles/tokens.css +116 -0
- package/src/domains/billing/components/BillingLayout.tsx +0 -63
- package/src/domains/billing/components/BillingPage.tsx +0 -49
- package/src/domains/billing/components/BillingPortal.tsx +0 -198
- package/src/domains/billing/components/InvoiceCard.tsx +0 -143
- package/src/domains/billing/components/PaymentMethodsList.tsx +0 -116
- package/src/domains/billing/components/PlanComparison.tsx +0 -199
- package/src/domains/billing/components/UsageCard.tsx +0 -103
- package/src/domains/billing/components/index.ts +0 -13
- package/src/domains/billing/constants/billing.ts +0 -69
- package/src/domains/billing/hooks/index.ts +0 -7
- package/src/domains/billing/hooks/useBilling.ts +0 -385
- package/src/domains/billing/index.ts +0 -73
- package/src/domains/billing/types/billing.ts +0 -342
- package/src/domains/billing/types/index.ts +0 -30
- package/src/domains/billing/utils/billing.ts +0 -360
- package/src/domains/billing/utils/index.ts +0 -31
- package/src/domains/billing/utils/polar.ts +0 -63
- package/src/domains/calendar/components/index.ts +0 -7
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SocialLoginButtons
|
|
3
|
+
*
|
|
4
|
+
* Renders a list of social auth providers as a row of icon buttons.
|
|
5
|
+
* Generic over the provider id so adding a new provider is a config change,
|
|
6
|
+
* not a code change.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Button } from "../../../../primitives";
|
|
10
|
+
import { GoogleIcon, AppleIcon } from "./SocialProviderIcons";
|
|
11
|
+
|
|
12
|
+
export type SocialProviderId = 'google' | 'apple';
|
|
13
|
+
|
|
14
|
+
export interface SocialLoginButtonsProps {
|
|
15
|
+
/** Provider handlers keyed by provider id */
|
|
16
|
+
onProvider: (id: SocialProviderId) => void;
|
|
17
|
+
/** Translation function */
|
|
18
|
+
translate: (key: string) => string;
|
|
19
|
+
/** Disable all buttons (e.g., during form submission) */
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Centralized provider config. Add new providers here and the UI follows.
|
|
25
|
+
*/
|
|
26
|
+
const SOCIAL_PROVIDERS: ReadonlyArray<{
|
|
27
|
+
id: SocialProviderId;
|
|
28
|
+
labelKey: string;
|
|
29
|
+
Icon: typeof GoogleIcon;
|
|
30
|
+
}> = [
|
|
31
|
+
{ id: 'google', labelKey: 'auth.social.continueWithGoogle', Icon: GoogleIcon },
|
|
32
|
+
{ id: 'apple', labelKey: 'auth.social.continueWithApple', Icon: AppleIcon },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export const SocialLoginButtons = ({
|
|
36
|
+
onProvider,
|
|
37
|
+
translate,
|
|
38
|
+
disabled = false,
|
|
39
|
+
}: SocialLoginButtonsProps) => {
|
|
40
|
+
return (
|
|
41
|
+
<div className="udb-social-grid">
|
|
42
|
+
{SOCIAL_PROVIDERS.map(({ id, labelKey, Icon }) => (
|
|
43
|
+
<Button
|
|
44
|
+
key={id}
|
|
45
|
+
type="button"
|
|
46
|
+
variant="outline"
|
|
47
|
+
onClick={() => onProvider(id)}
|
|
48
|
+
disabled={disabled}
|
|
49
|
+
aria-label={translate(labelKey)}
|
|
50
|
+
className="udb-social-grid__btn"
|
|
51
|
+
>
|
|
52
|
+
<Icon className="udb-social-grid__icon" aria-hidden="true" />
|
|
53
|
+
{translate(labelKey)}
|
|
54
|
+
</Button>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social Provider Icons
|
|
3
|
+
*
|
|
4
|
+
* Reusable SVG icon components for social auth providers.
|
|
5
|
+
* Centralizes the brand mark assets so consuming forms
|
|
6
|
+
* don't embed raw SVG paths in JSX.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { FC } from "react";
|
|
10
|
+
|
|
11
|
+
interface IconProps {
|
|
12
|
+
className?: string;
|
|
13
|
+
"aria-hidden"?: boolean | "true" | "false";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const baseProps = {
|
|
17
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
18
|
+
viewBox: "0 0 24 24",
|
|
19
|
+
fill: "currentColor",
|
|
20
|
+
"aria-hidden": true as const,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const GoogleIcon: FC<IconProps> = ({ className, ...rest }) => (
|
|
24
|
+
<svg className={className} {...baseProps} {...rest}>
|
|
25
|
+
<path
|
|
26
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
27
|
+
fill="#4285F4"
|
|
28
|
+
/>
|
|
29
|
+
<path
|
|
30
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
31
|
+
fill="#34A853"
|
|
32
|
+
/>
|
|
33
|
+
<path
|
|
34
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
35
|
+
fill="#FBBC05"
|
|
36
|
+
/>
|
|
37
|
+
<path
|
|
38
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
39
|
+
fill="#EA4335"
|
|
40
|
+
/>
|
|
41
|
+
</svg>
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
export const AppleIcon: FC<IconProps> = ({ className, ...rest }) => (
|
|
45
|
+
<svg className={className} {...baseProps} {...rest}>
|
|
46
|
+
<path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
|
|
47
|
+
</svg>
|
|
48
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared auth form components — barrel export
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { AuthErrorBanner } from './AuthErrorBanner';
|
|
6
|
+
export type { AuthErrorBannerProps } from './AuthErrorBanner';
|
|
7
|
+
|
|
8
|
+
export { EmailInput } from './EmailInput';
|
|
9
|
+
export type { EmailInputProps } from './EmailInput';
|
|
10
|
+
|
|
11
|
+
export { PasswordInput } from './PasswordInput';
|
|
12
|
+
export type { PasswordInputProps } from './PasswordInput';
|
|
13
|
+
|
|
14
|
+
export { PasswordStrengthIndicator } from './PasswordStrengthIndicator';
|
|
15
|
+
export type { PasswordStrengthIndicatorProps } from './PasswordStrengthIndicator';
|
|
16
|
+
|
|
17
|
+
export { SocialLoginButtons } from './SocialLoginButtons';
|
|
18
|
+
export type { SocialLoginButtonsProps, SocialProviderId } from './SocialLoginButtons';
|
|
19
|
+
|
|
20
|
+
export { GoogleIcon, AppleIcon } from './SocialProviderIcons';
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useAuth Hook
|
|
3
3
|
*
|
|
4
|
-
* Core authentication hook for managing auth state and actions
|
|
4
|
+
* Core authentication hook for managing auth state and actions.
|
|
5
|
+
* Centralizes the try/catch+loading+error pattern via a single helper
|
|
6
|
+
* to keep individual actions focused on their unique logic.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
|
-
import { useState, useCallback
|
|
9
|
+
import { useState, useCallback } from "react";
|
|
8
10
|
import type {
|
|
9
11
|
AuthState,
|
|
10
12
|
User,
|
|
@@ -21,278 +23,195 @@ import {
|
|
|
21
23
|
validateResetPassword,
|
|
22
24
|
} from "../utils/auth";
|
|
23
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Auth provider contract — single source of truth for backend integration.
|
|
28
|
+
*/
|
|
29
|
+
export interface AuthProvider {
|
|
30
|
+
login: (credentials: LoginCredentials) => Promise<User>;
|
|
31
|
+
register: (data: RegisterData) => Promise<User>;
|
|
32
|
+
logout: () => Promise<void>;
|
|
33
|
+
forgotPassword: (data: ForgotPasswordData) => Promise<void>;
|
|
34
|
+
resetPassword: (data: ResetPasswordData) => Promise<void>;
|
|
35
|
+
refreshAuth: () => Promise<User | null>;
|
|
36
|
+
}
|
|
37
|
+
|
|
24
38
|
interface UseAuthOptions {
|
|
25
|
-
/** Initial authenticated state */
|
|
26
|
-
initialAuthenticated?: boolean;
|
|
27
|
-
/** Current user data */
|
|
28
|
-
initialUser?: User | null;
|
|
29
39
|
/** Auth implementation (connects to your backend) */
|
|
30
|
-
authProvider?:
|
|
31
|
-
login: (credentials: LoginCredentials) => Promise<User>;
|
|
32
|
-
register: (data: RegisterData) => Promise<User>;
|
|
33
|
-
logout: () => Promise<void>;
|
|
34
|
-
forgotPassword: (data: ForgotPasswordData) => Promise<void>;
|
|
35
|
-
resetPassword: (data: ResetPasswordData) => Promise<void>;
|
|
36
|
-
refreshAuth: () => Promise<User | null>;
|
|
37
|
-
};
|
|
40
|
+
authProvider?: AuthProvider;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
/**
|
|
41
|
-
*
|
|
44
|
+
* Wrap a side-effectful async function with the standard
|
|
45
|
+
* isLoading + error + throw contract.
|
|
42
46
|
*
|
|
43
|
-
*
|
|
47
|
+
* Important: the wrapped function is the SINGLE point of truth for
|
|
48
|
+
* setting isLoading=false, ensuring errors are surfaced (not swallowed),
|
|
49
|
+
* and re-throwing for callers that need to react.
|
|
50
|
+
*/
|
|
51
|
+
const executeAuthAction = async <T>(
|
|
52
|
+
updateState: (updates: Partial<AuthState>) => void,
|
|
53
|
+
fallbackErrorMessage: string,
|
|
54
|
+
action: () => Promise<T>,
|
|
55
|
+
): Promise<T> => {
|
|
56
|
+
updateState({ isLoading: true, error: null });
|
|
57
|
+
try {
|
|
58
|
+
return await action();
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const errorMessage =
|
|
61
|
+
error instanceof Error && error.message ? error.message : fallbackErrorMessage;
|
|
62
|
+
updateState({ isLoading: false, error: errorMessage });
|
|
63
|
+
throw error instanceof Error ? error : new Error(errorMessage);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* useAuth hook
|
|
44
69
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
70
|
+
* Manages authentication state and provides auth actions.
|
|
71
|
+
* Each action surfaces errors to the caller AND to the auth state.
|
|
47
72
|
*/
|
|
48
73
|
export function useAuth(options: UseAuthOptions = {}) {
|
|
49
|
-
const {
|
|
50
|
-
initialAuthenticated = false,
|
|
51
|
-
initialUser = null,
|
|
52
|
-
authProvider,
|
|
53
|
-
} = options;
|
|
74
|
+
const { authProvider } = options;
|
|
54
75
|
|
|
55
|
-
// State
|
|
56
76
|
const [authState, setAuthState] = useState<AuthState>({
|
|
57
|
-
isAuthenticated:
|
|
58
|
-
user:
|
|
77
|
+
isAuthenticated: false,
|
|
78
|
+
user: null,
|
|
59
79
|
isLoading: false,
|
|
60
80
|
error: null,
|
|
61
81
|
});
|
|
62
82
|
|
|
63
|
-
// Update auth state helper
|
|
64
83
|
const updateState = useCallback((updates: Partial<AuthState>) => {
|
|
65
84
|
setAuthState((prev) => ({ ...prev, ...updates }));
|
|
66
85
|
}, []);
|
|
67
86
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const validation = validateLogin(credentials);
|
|
72
|
-
if (!validation.valid) {
|
|
73
|
-
updateState({ error: validation.error });
|
|
74
|
-
throw new Error(validation.error);
|
|
87
|
+
const requireProvider = useCallback((): AuthProvider => {
|
|
88
|
+
if (!authProvider) {
|
|
89
|
+
throw new Error("Auth provider not configured");
|
|
75
90
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
return authProvider;
|
|
92
|
+
}, [authProvider]);
|
|
93
|
+
|
|
94
|
+
const login = useCallback(
|
|
95
|
+
async (credentials: LoginCredentials): Promise<User> => {
|
|
96
|
+
const validation = validateLogin(credentials);
|
|
97
|
+
if (!validation.valid) {
|
|
98
|
+
const message = validation.error ?? "Login validation failed";
|
|
99
|
+
updateState({ error: message });
|
|
100
|
+
throw new Error(message);
|
|
82
101
|
}
|
|
83
102
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
user,
|
|
88
|
-
|
|
89
|
-
error: null,
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
return user;
|
|
93
|
-
} catch (error) {
|
|
94
|
-
const errorMessage = error instanceof Error ? error.message : "Login failed";
|
|
95
|
-
updateState({
|
|
96
|
-
isAuthenticated: false,
|
|
97
|
-
user: null,
|
|
98
|
-
isLoading: false,
|
|
99
|
-
error: errorMessage,
|
|
103
|
+
return executeAuthAction(updateState, "Login failed", async () => {
|
|
104
|
+
const provider = requireProvider();
|
|
105
|
+
const user = await provider.login(credentials);
|
|
106
|
+
updateState({ isAuthenticated: true, user, isLoading: false, error: null });
|
|
107
|
+
return user;
|
|
100
108
|
});
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
updateState({ isLoading: true, error: null });
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
if (!authProvider) {
|
|
118
|
-
throw new Error("Auth provider not configured");
|
|
109
|
+
},
|
|
110
|
+
[requireProvider, updateState],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const register = useCallback(
|
|
114
|
+
async (data: RegisterData): Promise<User> => {
|
|
115
|
+
const validation = validateRegister(data, false);
|
|
116
|
+
if (!validation.valid) {
|
|
117
|
+
const message = validation.error ?? "Registration validation failed";
|
|
118
|
+
updateState({ error: message });
|
|
119
|
+
throw new Error(message);
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
user,
|
|
125
|
-
|
|
126
|
-
error: null,
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
return user;
|
|
130
|
-
} catch (error) {
|
|
131
|
-
const errorMessage = error instanceof Error ? error.message : "Registration failed";
|
|
132
|
-
updateState({
|
|
133
|
-
isAuthenticated: false,
|
|
134
|
-
user: null,
|
|
135
|
-
isLoading: false,
|
|
136
|
-
error: errorMessage,
|
|
122
|
+
return executeAuthAction(updateState, "Registration failed", async () => {
|
|
123
|
+
const provider = requireProvider();
|
|
124
|
+
const user = await provider.register(data);
|
|
125
|
+
updateState({ isAuthenticated: true, user, isLoading: false, error: null });
|
|
126
|
+
return user;
|
|
137
127
|
});
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
128
|
+
},
|
|
129
|
+
[requireProvider, updateState],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const logout = useCallback(async (): Promise<void> => {
|
|
133
|
+
await executeAuthAction(updateState, "Logout failed", async () => {
|
|
134
|
+
const provider = requireProvider();
|
|
135
|
+
await provider.logout();
|
|
136
|
+
updateState({ isAuthenticated: false, user: null, isLoading: false, error: null });
|
|
137
|
+
});
|
|
138
|
+
}, [requireProvider, updateState]);
|
|
139
|
+
|
|
140
|
+
const forgotPassword = useCallback(
|
|
141
|
+
async (data: ForgotPasswordData): Promise<void> => {
|
|
142
|
+
const validation = validateForgotPassword(data);
|
|
143
|
+
if (!validation.valid) {
|
|
144
|
+
const message = validation.error ?? "Forgot password validation failed";
|
|
145
|
+
updateState({ error: message });
|
|
146
|
+
throw new Error(message);
|
|
149
147
|
}
|
|
150
148
|
|
|
151
|
-
await
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
user: null,
|
|
156
|
-
isLoading: false,
|
|
157
|
-
error: null,
|
|
158
|
-
});
|
|
159
|
-
} catch (error) {
|
|
160
|
-
const errorMessage = error instanceof Error ? error.message : "Logout failed";
|
|
161
|
-
updateState({
|
|
162
|
-
isLoading: false,
|
|
163
|
-
error: errorMessage,
|
|
149
|
+
await executeAuthAction(updateState, "Failed to send reset email", async () => {
|
|
150
|
+
const provider = requireProvider();
|
|
151
|
+
await provider.forgotPassword(data);
|
|
152
|
+
updateState({ isLoading: false });
|
|
164
153
|
});
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
updateState({ isLoading: true, error: null });
|
|
179
|
-
|
|
180
|
-
try {
|
|
181
|
-
if (!authProvider) {
|
|
182
|
-
throw new Error("Auth provider not configured");
|
|
154
|
+
},
|
|
155
|
+
[requireProvider, updateState],
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const resetPassword = useCallback(
|
|
159
|
+
async (data: ResetPasswordData): Promise<void> => {
|
|
160
|
+
const validation = validateResetPassword(data);
|
|
161
|
+
if (!validation.valid) {
|
|
162
|
+
const message = validation.error ?? "Reset password validation failed";
|
|
163
|
+
updateState({ error: message });
|
|
164
|
+
throw new Error(message);
|
|
183
165
|
}
|
|
184
166
|
|
|
185
|
-
await
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
updateState({
|
|
190
|
-
isLoading: false,
|
|
191
|
-
error: errorMessage,
|
|
167
|
+
await executeAuthAction(updateState, "Failed to reset password", async () => {
|
|
168
|
+
const provider = requireProvider();
|
|
169
|
+
await provider.resetPassword(data);
|
|
170
|
+
updateState({ isLoading: false });
|
|
192
171
|
});
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
updateState({ isLoading: true, error: null });
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
if (!authProvider) {
|
|
210
|
-
throw new Error("Auth provider not configured");
|
|
172
|
+
},
|
|
173
|
+
[requireProvider, updateState],
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const updateProfile = useCallback(
|
|
177
|
+
async (data: Partial<User>): Promise<User> => {
|
|
178
|
+
const currentUser = authState.user;
|
|
179
|
+
if (!currentUser) {
|
|
180
|
+
const message = "No user to update";
|
|
181
|
+
updateState({ error: message });
|
|
182
|
+
throw new Error(message);
|
|
211
183
|
}
|
|
212
184
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
}, [authProvider, updateState]);
|
|
224
|
-
|
|
225
|
-
// Update profile action
|
|
226
|
-
const updateProfile = useCallback(async (data: Partial<User>) => {
|
|
227
|
-
updateState({ isLoading: true, error: null });
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
// Update local user state
|
|
231
|
-
let updatedUser: User | null = null;
|
|
232
|
-
setAuthState((prev) => {
|
|
233
|
-
updatedUser = prev.user ? { ...prev.user, ...data } : null;
|
|
234
|
-
return {
|
|
235
|
-
...prev,
|
|
236
|
-
user: updatedUser,
|
|
237
|
-
isLoading: false,
|
|
238
|
-
error: null,
|
|
185
|
+
return executeAuthAction(updateState, "Failed to update profile", async () => {
|
|
186
|
+
// Pure derivation of the new user, no side effects in setState callback.
|
|
187
|
+
// Required fields come from the existing user; data overrides only the rest.
|
|
188
|
+
const { id: _ignoredId, ...rest } = data;
|
|
189
|
+
const updatedUser: User = {
|
|
190
|
+
...currentUser,
|
|
191
|
+
...rest,
|
|
192
|
+
id: currentUser.id,
|
|
193
|
+
email: data.email ?? currentUser.email,
|
|
239
194
|
};
|
|
195
|
+
updateState({ user: updatedUser, isLoading: false, error: null });
|
|
196
|
+
return updatedUser;
|
|
240
197
|
});
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
} catch (error) {
|
|
251
|
-
const errorMessage = error instanceof Error ? error.message : "Failed to update profile";
|
|
252
|
-
updateState({
|
|
253
|
-
isLoading: false,
|
|
254
|
-
error: errorMessage,
|
|
255
|
-
});
|
|
256
|
-
throw error;
|
|
257
|
-
}
|
|
258
|
-
}, [updateState]);
|
|
259
|
-
|
|
260
|
-
// Refresh auth action
|
|
261
|
-
const refresh = useCallback(async () => {
|
|
262
|
-
updateState({ isLoading: true, error: null });
|
|
263
|
-
|
|
264
|
-
try {
|
|
265
|
-
if (!authProvider) {
|
|
266
|
-
throw new Error("Auth provider not configured");
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const user = await authProvider.refreshAuth();
|
|
270
|
-
|
|
271
|
-
updateState({
|
|
272
|
-
isAuthenticated: !!user,
|
|
273
|
-
user,
|
|
274
|
-
isLoading: false,
|
|
275
|
-
error: null,
|
|
276
|
-
});
|
|
277
|
-
|
|
198
|
+
},
|
|
199
|
+
[authState.user, updateState],
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const refresh = useCallback(async (): Promise<User | null> => {
|
|
203
|
+
return executeAuthAction(updateState, "Failed to refresh authentication", async () => {
|
|
204
|
+
const provider = requireProvider();
|
|
205
|
+
const user = await provider.refreshAuth();
|
|
206
|
+
updateState({ isAuthenticated: Boolean(user), user, isLoading: false, error: null });
|
|
278
207
|
return user;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
isAuthenticated: false,
|
|
282
|
-
user: null,
|
|
283
|
-
isLoading: false,
|
|
284
|
-
error: null,
|
|
285
|
-
});
|
|
286
|
-
throw error;
|
|
287
|
-
}
|
|
288
|
-
}, [authProvider, updateState]);
|
|
208
|
+
});
|
|
209
|
+
}, [requireProvider, updateState]);
|
|
289
210
|
|
|
290
|
-
// Clear error action
|
|
291
211
|
const clearError = useCallback(() => {
|
|
292
212
|
updateState({ error: null });
|
|
293
213
|
}, [updateState]);
|
|
294
214
|
|
|
295
|
-
// Auth actions object
|
|
296
215
|
const authActions: AuthActions = {
|
|
297
216
|
login,
|
|
298
217
|
register,
|
|
@@ -14,9 +14,7 @@ export {
|
|
|
14
14
|
} from "./components";
|
|
15
15
|
|
|
16
16
|
// Hooks
|
|
17
|
-
export {
|
|
18
|
-
useAuth,
|
|
19
|
-
} from "./hooks";
|
|
17
|
+
export { useAuth, type AuthProvider } from "./hooks";
|
|
20
18
|
|
|
21
19
|
// Utils
|
|
22
20
|
export {
|
|
@@ -33,8 +31,13 @@ export {
|
|
|
33
31
|
maskEmail,
|
|
34
32
|
generateResetToken,
|
|
35
33
|
calculatePasswordStrength,
|
|
36
|
-
|
|
34
|
+
getPasswordStrengthBand,
|
|
37
35
|
sanitizeInput,
|
|
36
|
+
AUTH_KEYS,
|
|
37
|
+
AUTH_VALIDATION_KEYS,
|
|
38
|
+
type ValidationResult,
|
|
39
|
+
type PasswordStrengthBand,
|
|
40
|
+
type AuthValidationKey,
|
|
38
41
|
} from "./utils";
|
|
39
42
|
|
|
40
43
|
// Types
|
|
@@ -55,4 +58,5 @@ export type {
|
|
|
55
58
|
RegisterFormProps,
|
|
56
59
|
ForgotPasswordFormProps,
|
|
57
60
|
ResetPasswordFormProps,
|
|
61
|
+
Translate,
|
|
58
62
|
} from "./types";
|
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
import type { ComponentType, ReactElement } from "react";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Translation function contract. Auth components stay i18n-agnostic;
|
|
11
|
+
* the consumer plugs in their i18n library of choice.
|
|
12
|
+
* Interpolation params follow the i18next convention (`{{placeholder}}`).
|
|
13
|
+
*/
|
|
14
|
+
export type Translate = (key: string, params?: Record<string, unknown>) => string;
|
|
15
|
+
|
|
9
16
|
/**
|
|
10
17
|
* User credentials for login
|
|
11
18
|
*/
|
|
@@ -156,6 +163,10 @@ export interface AuthConfig {
|
|
|
156
163
|
showSocialLogin?: boolean;
|
|
157
164
|
/** Available social providers */
|
|
158
165
|
socialProviders?: SocialProvider[];
|
|
166
|
+
/** Public home route (header "back" link). Falls back to `afterLoginRoute`. */
|
|
167
|
+
homeRoute?: string;
|
|
168
|
+
/** Footer links (rendered only when provided — no defaults that 404) */
|
|
169
|
+
footerLinks?: Array<{ label: string; href: string }>;
|
|
159
170
|
/** Enable remember me */
|
|
160
171
|
enableRememberMe?: boolean;
|
|
161
172
|
/** Enable email verification */
|
|
@@ -192,6 +203,10 @@ export interface AuthLayoutProps {
|
|
|
192
203
|
authState?: AuthState;
|
|
193
204
|
/** Auth actions */
|
|
194
205
|
authActions?: AuthActions;
|
|
206
|
+
/** Optional translation function for layout strings (falls back to embedded English) */
|
|
207
|
+
translate?: Translate;
|
|
208
|
+
/** Social provider login handler (receives the provider id) */
|
|
209
|
+
onSocialLogin?: (providerId: string) => void | Promise<void>;
|
|
195
210
|
/** Children content */
|
|
196
211
|
children?: React.ReactNode;
|
|
197
212
|
}
|
|
@@ -212,7 +227,13 @@ export interface LoginFormProps {
|
|
|
212
227
|
showRegisterLink?: boolean;
|
|
213
228
|
/** Show social login buttons */
|
|
214
229
|
showSocialLogin?: boolean;
|
|
215
|
-
/**
|
|
230
|
+
/**
|
|
231
|
+
* Allow the built-in mock login (accepts ANY credentials) for demos.
|
|
232
|
+
* Defaults to false — without `onLoginAttempt` the form reports a
|
|
233
|
+
* configuration error instead of silently "logging in".
|
|
234
|
+
*/
|
|
235
|
+
enableMockAuth?: boolean;
|
|
236
|
+
/** Custom login handler (receives credentials, returns user) */
|
|
216
237
|
onLoginAttempt?: (credentials: LoginCredentials) => Promise<User>;
|
|
217
238
|
/** On successful login (called after onLoginAttempt or mock auth succeeds) */
|
|
218
239
|
onLoginSuccess?: (user: User) => void | Promise<void>;
|
|
@@ -258,8 +279,8 @@ export interface RegisterFormProps {
|
|
|
258
279
|
export interface ForgotPasswordFormProps {
|
|
259
280
|
/** Auth configuration */
|
|
260
281
|
config: AuthConfig;
|
|
261
|
-
/**
|
|
262
|
-
onSuccess?: () => void | Promise<void>;
|
|
282
|
+
/** Submit handler — receives the entered email and performs the reset request */
|
|
283
|
+
onSuccess?: (data: { email: string }) => void | Promise<void>;
|
|
263
284
|
/** On error */
|
|
264
285
|
onError?: (error: string) => void;
|
|
265
286
|
/** Show back to login link */
|
|
@@ -274,8 +295,8 @@ export interface ResetPasswordFormProps {
|
|
|
274
295
|
config: AuthConfig;
|
|
275
296
|
/** Reset token */
|
|
276
297
|
token: string;
|
|
277
|
-
/**
|
|
278
|
-
onSuccess?: () => void | Promise<void>;
|
|
298
|
+
/** Submit handler — receives the token and the new password */
|
|
299
|
+
onSuccess?: (data: { token: string; password: string }) => void | Promise<void>;
|
|
279
300
|
/** On error */
|
|
280
301
|
onError?: (error: string) => void;
|
|
281
302
|
}
|