@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,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for pure auth utility functions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
isValidEmail,
|
|
8
|
+
isValidPassword,
|
|
9
|
+
validateLogin,
|
|
10
|
+
validateRegister,
|
|
11
|
+
validateResetPassword,
|
|
12
|
+
getUserDisplayName,
|
|
13
|
+
getUserInitials,
|
|
14
|
+
calculatePasswordStrength,
|
|
15
|
+
getPasswordStrengthBand,
|
|
16
|
+
maskEmail,
|
|
17
|
+
sanitizeInput,
|
|
18
|
+
} from "./auth";
|
|
19
|
+
import { generateResetToken } from "./secureToken";
|
|
20
|
+
import { AUTH_VALIDATION_KEYS } from "./validationKeys";
|
|
21
|
+
|
|
22
|
+
describe("isValidEmail", () => {
|
|
23
|
+
it("accepts plain addresses", () => {
|
|
24
|
+
expect(isValidEmail("user@example.com")).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("rejects missing local part, domain, or TLD", () => {
|
|
28
|
+
expect(isValidEmail("@example.com")).toBe(false);
|
|
29
|
+
expect(isValidEmail("user@")).toBe(false);
|
|
30
|
+
expect(isValidEmail("user@example")).toBe(false);
|
|
31
|
+
expect(isValidEmail("user example.com")).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("isValidPassword", () => {
|
|
36
|
+
it("enforces the minimum length", () => {
|
|
37
|
+
expect(isValidPassword("short")).toBe(false);
|
|
38
|
+
expect(isValidPassword("longenough")).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("honors a custom minimum", () => {
|
|
42
|
+
expect(isValidPassword("12345678", 12)).toBe(false);
|
|
43
|
+
expect(isValidPassword("123456789012", 12)).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("validateLogin", () => {
|
|
48
|
+
it("passes valid credentials", () => {
|
|
49
|
+
expect(validateLogin({ email: "a@b.co", password: "x".repeat(8) })).toEqual({ valid: true });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("requires an email first", () => {
|
|
53
|
+
expect(validateLogin({ email: "", password: "x" })).toEqual({
|
|
54
|
+
valid: false,
|
|
55
|
+
error: AUTH_VALIDATION_KEYS.emailRequired,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("rejects a malformed email", () => {
|
|
60
|
+
expect(validateLogin({ email: "nope", password: "x" })).toEqual({
|
|
61
|
+
valid: false,
|
|
62
|
+
error: AUTH_VALIDATION_KEYS.emailInvalid,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("requires a password", () => {
|
|
67
|
+
expect(validateLogin({ email: "a@b.co", password: "" })).toEqual({
|
|
68
|
+
valid: false,
|
|
69
|
+
error: AUTH_VALIDATION_KEYS.passwordRequired,
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("validateRegister", () => {
|
|
75
|
+
const base = { email: "a@b.co", password: "longenough1" };
|
|
76
|
+
|
|
77
|
+
it("passes with minimal data", () => {
|
|
78
|
+
expect(validateRegister(base)).toEqual({ valid: true });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("requires a name when asked to", () => {
|
|
82
|
+
expect(validateRegister(base, true)).toEqual({
|
|
83
|
+
valid: false,
|
|
84
|
+
error: AUTH_VALIDATION_KEYS.nameRequired,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("enforces password length", () => {
|
|
89
|
+
expect(validateRegister({ ...base, password: "short" })).toEqual({
|
|
90
|
+
valid: false,
|
|
91
|
+
error: AUTH_VALIDATION_KEYS.passwordTooShort,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("checks confirmation when required", () => {
|
|
96
|
+
expect(
|
|
97
|
+
validateRegister({ ...base, confirmPassword: "different" }, false, true),
|
|
98
|
+
).toEqual({
|
|
99
|
+
valid: false,
|
|
100
|
+
error: AUTH_VALIDATION_KEYS.passwordsDoNotMatch,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("validateResetPassword", () => {
|
|
106
|
+
const base = { token: "tok", password: "longenough", confirmPassword: "longenough" };
|
|
107
|
+
|
|
108
|
+
it("passes matching valid data", () => {
|
|
109
|
+
expect(validateResetPassword(base)).toEqual({ valid: true });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("requires a token", () => {
|
|
113
|
+
expect(validateResetPassword({ ...base, token: "" })).toEqual({
|
|
114
|
+
valid: false,
|
|
115
|
+
error: AUTH_VALIDATION_KEYS.invalidResetToken,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("rejects mismatched confirmation", () => {
|
|
120
|
+
expect(validateResetPassword({ ...base, confirmPassword: "other" })).toEqual({
|
|
121
|
+
valid: false,
|
|
122
|
+
error: AUTH_VALIDATION_KEYS.passwordsDoNotMatch,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("display helpers", () => {
|
|
128
|
+
it("getUserDisplayName prefers name, then email, then null", () => {
|
|
129
|
+
expect(getUserDisplayName({ id: "1", name: "Ada", email: "a@b.co" } as never)).toBe("Ada");
|
|
130
|
+
expect(getUserDisplayName({ id: "1", email: "a@b.co" } as never)).toBe("a@b.co");
|
|
131
|
+
expect(getUserDisplayName(null)).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("getUserInitials uses first and last name parts", () => {
|
|
135
|
+
expect(getUserInitials({ id: "1", name: "Ada Lovelace" } as never)).toBe("AL");
|
|
136
|
+
expect(getUserInitials({ id: "1", name: "ada" } as never)).toBe("AD");
|
|
137
|
+
expect(getUserInitials(null)).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("password strength", () => {
|
|
142
|
+
it("scores a strong password at 100", () => {
|
|
143
|
+
expect(calculatePasswordStrength("Str0ng!Password")).toBe(100);
|
|
144
|
+
expect(getPasswordStrengthBand("Str0ng!Password")).toBe("strong");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("scores a weak password low", () => {
|
|
148
|
+
expect(getPasswordStrengthBand("abc")).toBe("weak");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("never exceeds 100", () => {
|
|
152
|
+
expect(calculatePasswordStrength("aaaaaaaaaaaaaaaaaaaa!A1")).toBeLessThanOrEqual(100);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("maskEmail", () => {
|
|
157
|
+
it("masks the local part", () => {
|
|
158
|
+
expect(maskEmail("umit@example.com")).toBe("u***@example.com");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("returns null for invalid input", () => {
|
|
162
|
+
expect(maskEmail("not-an-email")).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("sanitizeInput", () => {
|
|
167
|
+
it("trims and truncates", () => {
|
|
168
|
+
expect(sanitizeInput(" hello ")).toBe("hello");
|
|
169
|
+
expect(sanitizeInput("abcdef", 3)).toBe("abc");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("generateResetToken", () => {
|
|
174
|
+
it("produces a hex string of 2 * length chars", () => {
|
|
175
|
+
const token = generateResetToken(16);
|
|
176
|
+
expect(token).toMatch(/^[0-9a-f]{32}$/);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("produces different tokens on each call", () => {
|
|
180
|
+
expect(generateResetToken()).not.toBe(generateResetToken());
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth Utilities
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Validation and helper functions for authentication.
|
|
5
|
+
* Validation messages are i18n keys, not hardcoded strings —
|
|
6
|
+
* the consumer renders the localized text in its UI layer.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import type { LoginCredentials, RegisterData, User } from "../types/auth";
|
|
10
|
+
import { AUTH_VALIDATION_KEYS } from './validationKeys';
|
|
11
|
+
|
|
12
|
+
export { generateResetToken } from './secureToken';
|
|
13
|
+
export { AUTH_VALIDATION_KEYS } from './validationKeys';
|
|
14
|
+
export type { AuthValidationKey } from './validationKeys';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validation result type — error is now an i18n key.
|
|
18
|
+
*/
|
|
19
|
+
export type ValidationResult =
|
|
20
|
+
| { valid: true }
|
|
21
|
+
| { valid: false; error: string };
|
|
8
22
|
|
|
9
23
|
/**
|
|
10
24
|
* Validate email format
|
|
11
|
-
*
|
|
12
|
-
* @param email - Email address
|
|
13
|
-
* @returns Whether email is valid
|
|
14
25
|
*/
|
|
15
26
|
export function isValidEmail(email: string): boolean {
|
|
16
27
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -18,11 +29,7 @@ export function isValidEmail(email: string): boolean {
|
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
/**
|
|
21
|
-
* Validate password
|
|
22
|
-
*
|
|
23
|
-
* @param password - Password
|
|
24
|
-
* @param minLength - Minimum length (default: 8)
|
|
25
|
-
* @returns Whether password meets requirements
|
|
32
|
+
* Validate password length
|
|
26
33
|
*/
|
|
27
34
|
export function isValidPassword(password: string, minLength: number = 8): boolean {
|
|
28
35
|
return password.length >= minLength;
|
|
@@ -30,148 +37,108 @@ export function isValidPassword(password: string, minLength: number = 8): boolea
|
|
|
30
37
|
|
|
31
38
|
/**
|
|
32
39
|
* Validate login credentials
|
|
33
|
-
*
|
|
34
|
-
* @param credentials - Login credentials
|
|
35
|
-
* @returns Validation result with error message
|
|
36
40
|
*/
|
|
37
|
-
export function validateLogin(credentials: LoginCredentials): {
|
|
38
|
-
valid: boolean;
|
|
39
|
-
error?: string;
|
|
40
|
-
} {
|
|
41
|
+
export function validateLogin(credentials: LoginCredentials): ValidationResult {
|
|
41
42
|
if (!credentials.email) {
|
|
42
|
-
return { valid: false, error:
|
|
43
|
+
return { valid: false, error: AUTH_VALIDATION_KEYS.emailRequired };
|
|
43
44
|
}
|
|
44
|
-
|
|
45
45
|
if (!isValidEmail(credentials.email)) {
|
|
46
|
-
return { valid: false, error:
|
|
46
|
+
return { valid: false, error: AUTH_VALIDATION_KEYS.emailInvalid };
|
|
47
47
|
}
|
|
48
|
-
|
|
49
48
|
if (!credentials.password) {
|
|
50
|
-
return { valid: false, error:
|
|
49
|
+
return { valid: false, error: AUTH_VALIDATION_KEYS.passwordRequired };
|
|
51
50
|
}
|
|
52
|
-
|
|
53
51
|
return { valid: true };
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
/**
|
|
57
55
|
* Validate registration data
|
|
58
|
-
*
|
|
59
|
-
* @param data - Registration data
|
|
60
|
-
* @param requireName - Whether name is required (default: false)
|
|
61
|
-
* @param requirePasswordConfirm - Whether password confirmation is required
|
|
62
|
-
* @returns Validation result with error message
|
|
63
56
|
*/
|
|
64
57
|
export function validateRegister(
|
|
65
58
|
data: RegisterData & { confirmPassword?: string },
|
|
66
59
|
requireName: boolean = false,
|
|
67
|
-
requirePasswordConfirm?: boolean
|
|
68
|
-
): {
|
|
69
|
-
valid: boolean;
|
|
70
|
-
error?: string;
|
|
71
|
-
} {
|
|
60
|
+
requirePasswordConfirm?: boolean,
|
|
61
|
+
): ValidationResult {
|
|
72
62
|
if (!data.email) {
|
|
73
|
-
return { valid: false, error:
|
|
63
|
+
return { valid: false, error: AUTH_VALIDATION_KEYS.emailRequired };
|
|
74
64
|
}
|
|
75
|
-
|
|
76
65
|
if (!isValidEmail(data.email)) {
|
|
77
|
-
return { valid: false, error:
|
|
66
|
+
return { valid: false, error: AUTH_VALIDATION_KEYS.emailInvalid };
|
|
78
67
|
}
|
|
79
|
-
|
|
80
68
|
if (requireName && !data.name) {
|
|
81
|
-
return { valid: false, error:
|
|
69
|
+
return { valid: false, error: AUTH_VALIDATION_KEYS.nameRequired };
|
|
82
70
|
}
|
|
83
|
-
|
|
84
71
|
if (!data.password) {
|
|
85
|
-
return { valid: false, error:
|
|
72
|
+
return { valid: false, error: AUTH_VALIDATION_KEYS.passwordRequired };
|
|
86
73
|
}
|
|
87
|
-
|
|
88
74
|
if (!isValidPassword(data.password)) {
|
|
89
|
-
return { valid: false, error:
|
|
75
|
+
return { valid: false, error: AUTH_VALIDATION_KEYS.passwordTooShort };
|
|
90
76
|
}
|
|
91
|
-
|
|
92
77
|
if (requirePasswordConfirm && data.password !== data.confirmPassword) {
|
|
93
|
-
return { valid: false, error:
|
|
78
|
+
return { valid: false, error: AUTH_VALIDATION_KEYS.passwordsDoNotMatch };
|
|
94
79
|
}
|
|
95
|
-
|
|
96
80
|
return { valid: true };
|
|
97
81
|
}
|
|
98
82
|
|
|
99
83
|
/**
|
|
100
|
-
* Validate password
|
|
101
|
-
*
|
|
102
|
-
* @param data - Forgot password data
|
|
103
|
-
* @returns Validation result with error message
|
|
84
|
+
* Validate forgot-password request
|
|
104
85
|
*/
|
|
105
|
-
export function validateForgotPassword(data: { email: string }): {
|
|
106
|
-
valid: boolean;
|
|
107
|
-
error?: string;
|
|
108
|
-
} {
|
|
86
|
+
export function validateForgotPassword(data: { email: string }): ValidationResult {
|
|
109
87
|
if (!data.email) {
|
|
110
|
-
return { valid: false, error:
|
|
88
|
+
return { valid: false, error: AUTH_VALIDATION_KEYS.emailRequired };
|
|
111
89
|
}
|
|
112
|
-
|
|
113
90
|
if (!isValidEmail(data.email)) {
|
|
114
|
-
return { valid: false, error:
|
|
91
|
+
return { valid: false, error: AUTH_VALIDATION_KEYS.emailInvalid };
|
|
115
92
|
}
|
|
116
|
-
|
|
117
93
|
return { valid: true };
|
|
118
94
|
}
|
|
119
95
|
|
|
120
96
|
/**
|
|
121
97
|
* Validate password reset confirmation
|
|
122
|
-
*
|
|
123
|
-
* @param data - Reset password data
|
|
124
|
-
* @returns Validation result with error message
|
|
125
98
|
*/
|
|
126
99
|
export function validateResetPassword(data: {
|
|
127
100
|
token: string;
|
|
128
101
|
password: string;
|
|
129
102
|
confirmPassword: string;
|
|
130
|
-
}): {
|
|
131
|
-
valid: boolean;
|
|
132
|
-
error?: string;
|
|
133
|
-
} {
|
|
103
|
+
}): ValidationResult {
|
|
134
104
|
if (!data.token) {
|
|
135
|
-
return { valid: false, error:
|
|
105
|
+
return { valid: false, error: AUTH_VALIDATION_KEYS.invalidResetToken };
|
|
136
106
|
}
|
|
137
|
-
|
|
138
107
|
if (!data.password) {
|
|
139
|
-
return { valid: false, error:
|
|
108
|
+
return { valid: false, error: AUTH_VALIDATION_KEYS.passwordRequired };
|
|
140
109
|
}
|
|
141
|
-
|
|
142
110
|
if (!isValidPassword(data.password)) {
|
|
143
|
-
return { valid: false, error:
|
|
111
|
+
return { valid: false, error: AUTH_VALIDATION_KEYS.passwordTooShort };
|
|
144
112
|
}
|
|
145
|
-
|
|
146
113
|
if (data.password !== data.confirmPassword) {
|
|
147
|
-
return { valid: false, error:
|
|
114
|
+
return { valid: false, error: AUTH_VALIDATION_KEYS.passwordsDoNotMatch };
|
|
148
115
|
}
|
|
149
|
-
|
|
150
116
|
return { valid: true };
|
|
151
117
|
}
|
|
152
118
|
|
|
153
119
|
/**
|
|
154
120
|
* Get user display name
|
|
155
121
|
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
122
|
+
* Returns null when no usable identifier exists, letting the caller
|
|
123
|
+
* decide on a localized fallback. Avoids hardcoded "Guest"/"User" strings.
|
|
158
124
|
*/
|
|
159
|
-
export function getUserDisplayName(user: User | null): string {
|
|
160
|
-
if (!user) return
|
|
161
|
-
return user.name || user.email ||
|
|
125
|
+
export function getUserDisplayName(user: User | null): string | null {
|
|
126
|
+
if (!user) return null;
|
|
127
|
+
return user.name || user.email || null;
|
|
162
128
|
}
|
|
163
129
|
|
|
164
130
|
/**
|
|
165
131
|
* Get user initials
|
|
166
132
|
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
133
|
+
* Returns null when no name is available, so the caller can decide
|
|
134
|
+
* whether to render a placeholder or hide the avatar.
|
|
169
135
|
*/
|
|
170
|
-
export function getUserInitials(user: User | null): string {
|
|
171
|
-
if (!user) return
|
|
136
|
+
export function getUserInitials(user: User | null): string | null {
|
|
137
|
+
if (!user) return null;
|
|
172
138
|
const name = user.name || user.email || "";
|
|
139
|
+
if (!name) return null;
|
|
173
140
|
const parts = name.trim().split(" ");
|
|
174
|
-
if (parts.length >= 2) {
|
|
141
|
+
if (parts.length >= 2 && parts[0] && parts[parts.length - 1]) {
|
|
175
142
|
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
176
143
|
}
|
|
177
144
|
return name.slice(0, 2).toUpperCase();
|
|
@@ -179,9 +146,6 @@ export function getUserInitials(user: User | null): string {
|
|
|
179
146
|
|
|
180
147
|
/**
|
|
181
148
|
* Check if user has verified email
|
|
182
|
-
*
|
|
183
|
-
* @param user - User object
|
|
184
|
-
* @returns Whether email is verified
|
|
185
149
|
*/
|
|
186
150
|
export function isEmailVerified(user: User | null): boolean {
|
|
187
151
|
return user?.emailVerified === true;
|
|
@@ -190,12 +154,14 @@ export function isEmailVerified(user: User | null): boolean {
|
|
|
190
154
|
/**
|
|
191
155
|
* Format user creation date
|
|
192
156
|
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* @returns Formatted date string or empty string
|
|
157
|
+
* Returns null when the user has no createdAt; the caller decides
|
|
158
|
+
* whether to display "—" or hide the field.
|
|
196
159
|
*/
|
|
197
|
-
export function formatUserCreatedAt(
|
|
198
|
-
|
|
160
|
+
export function formatUserCreatedAt(
|
|
161
|
+
user: User | null,
|
|
162
|
+
locale: string = "en-US",
|
|
163
|
+
): string | null {
|
|
164
|
+
if (!user?.createdAt) return null;
|
|
199
165
|
return new Date(user.createdAt).toLocaleDateString(locale, {
|
|
200
166
|
year: "numeric",
|
|
201
167
|
month: "long",
|
|
@@ -206,47 +172,25 @@ export function formatUserCreatedAt(user: User | null, locale: string = "en-US")
|
|
|
206
172
|
/**
|
|
207
173
|
* Mask email for privacy (e.g., u***@example.com)
|
|
208
174
|
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
175
|
+
* Returns null when the email is invalid so the caller can show
|
|
176
|
+
* a "—" placeholder rather than the raw input.
|
|
211
177
|
*/
|
|
212
|
-
export function maskEmail(email: string): string {
|
|
178
|
+
export function maskEmail(email: string): string | null {
|
|
213
179
|
const [local, domain] = email.split("@");
|
|
214
|
-
if (!local || !domain) return
|
|
215
|
-
|
|
180
|
+
if (!local || !domain) return null;
|
|
216
181
|
const maskedLocal = local[0] + "***";
|
|
217
182
|
return `${maskedLocal}@${domain}`;
|
|
218
183
|
}
|
|
219
184
|
|
|
220
185
|
/**
|
|
221
|
-
*
|
|
222
|
-
* In production, use a proper crypto library
|
|
223
|
-
*
|
|
224
|
-
* @param length - Token length in bytes (default: 32)
|
|
225
|
-
* @returns Random hex token
|
|
226
|
-
*/
|
|
227
|
-
export function generateResetToken(length: number = 32): string {
|
|
228
|
-
const chars = "0123456789abcdef";
|
|
229
|
-
let token = "";
|
|
230
|
-
for (let i = 0; i < length * 2; i++) {
|
|
231
|
-
token += chars[Math.floor(Math.random() * chars.length)];
|
|
232
|
-
}
|
|
233
|
-
return token;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Calculate password strength score (0-100)
|
|
238
|
-
*
|
|
239
|
-
* @param password - Password to evaluate
|
|
240
|
-
* @returns Strength score
|
|
186
|
+
* Password strength score (0-100)
|
|
241
187
|
*/
|
|
242
188
|
export function calculatePasswordStrength(password: string): number {
|
|
243
189
|
let score = 0;
|
|
244
190
|
|
|
245
|
-
// Length
|
|
246
191
|
if (password.length >= 8) score += 25;
|
|
247
192
|
if (password.length >= 12) score += 25;
|
|
248
193
|
|
|
249
|
-
// Variety
|
|
250
194
|
if (/[a-z]/.test(password)) score += 12.5;
|
|
251
195
|
if (/[A-Z]/.test(password)) score += 12.5;
|
|
252
196
|
if (/[0-9]/.test(password)) score += 12.5;
|
|
@@ -256,25 +200,22 @@ export function calculatePasswordStrength(password: string): number {
|
|
|
256
200
|
}
|
|
257
201
|
|
|
258
202
|
/**
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
* @param password - Password to evaluate
|
|
262
|
-
* @returns Strength label
|
|
203
|
+
* Password strength band. Numeric so the UI can map it to a label
|
|
204
|
+
* via its own translation catalog.
|
|
263
205
|
*/
|
|
264
|
-
export
|
|
206
|
+
export type PasswordStrengthBand = 'weak' | 'fair' | 'good' | 'strong';
|
|
207
|
+
|
|
208
|
+
export function getPasswordStrengthBand(password: string): PasswordStrengthBand {
|
|
265
209
|
const score = calculatePasswordStrength(password);
|
|
266
|
-
if (score < 25) return
|
|
267
|
-
if (score < 50) return
|
|
268
|
-
if (score < 75) return
|
|
269
|
-
return
|
|
210
|
+
if (score < 25) return 'weak';
|
|
211
|
+
if (score < 50) return 'fair';
|
|
212
|
+
if (score < 75) return 'good';
|
|
213
|
+
return 'strong';
|
|
270
214
|
}
|
|
271
215
|
|
|
272
216
|
/**
|
|
273
217
|
* Sanitize user input for display
|
|
274
|
-
*
|
|
275
|
-
* @param input - User input string
|
|
276
|
-
* @returns Sanitized string
|
|
277
218
|
*/
|
|
278
|
-
export function sanitizeInput(input: string): string {
|
|
279
|
-
return input.trim().slice(0,
|
|
219
|
+
export function sanitizeInput(input: string, maxLength: number = 1000): string {
|
|
220
|
+
return input.trim().slice(0, maxLength);
|
|
280
221
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Domain i18n Keys
|
|
3
|
+
*
|
|
4
|
+
* Centralized catalog of i18n keys used across auth components.
|
|
5
|
+
* Components stay free of hardcoded strings and consume these
|
|
6
|
+
* constants — the consumer app provides the actual translations.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const AUTH_KEYS = {
|
|
10
|
+
// Login screen
|
|
11
|
+
login: {
|
|
12
|
+
welcomeBack: 'auth.login.welcomeBack',
|
|
13
|
+
signInTo: 'auth.login.signInTo',
|
|
14
|
+
submit: 'auth.login.submit',
|
|
15
|
+
submitting: 'auth.login.submitting',
|
|
16
|
+
rememberMe: 'auth.login.rememberMe',
|
|
17
|
+
forgotPassword: 'auth.login.forgotPassword',
|
|
18
|
+
noAccount: 'auth.login.noAccount',
|
|
19
|
+
signUp: 'auth.login.signUp',
|
|
20
|
+
orContinueWith: 'auth.login.orContinueWith',
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
// Register screen
|
|
24
|
+
register: {
|
|
25
|
+
createAccount: 'auth.register.createAccount',
|
|
26
|
+
join: 'auth.register.join',
|
|
27
|
+
fullName: 'auth.register.fullName',
|
|
28
|
+
fullNamePlaceholder: 'auth.placeholders.fullName',
|
|
29
|
+
confirmPassword: 'auth.register.confirmPassword',
|
|
30
|
+
confirmPasswordPlaceholder: 'auth.placeholders.confirmPassword',
|
|
31
|
+
acceptTerms: 'auth.register.acceptTerms',
|
|
32
|
+
termsOfService: 'auth.register.termsOfService',
|
|
33
|
+
privacyPolicy: 'auth.register.privacyPolicy',
|
|
34
|
+
submit: 'auth.register.submit',
|
|
35
|
+
submitting: 'auth.register.submitting',
|
|
36
|
+
haveAccount: 'auth.register.haveAccount',
|
|
37
|
+
signIn: 'auth.register.signIn',
|
|
38
|
+
mustAcceptTerms: 'auth.register.mustAcceptTerms',
|
|
39
|
+
passwordsDoNotMatch: 'auth.register.passwordsDoNotMatch',
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// Forgot password
|
|
43
|
+
forgotPassword: {
|
|
44
|
+
title: 'auth.forgotPassword.title',
|
|
45
|
+
description: 'auth.forgotPassword.description',
|
|
46
|
+
submit: 'auth.forgotPassword.submit',
|
|
47
|
+
submitting: 'auth.forgotPassword.submitting',
|
|
48
|
+
backToSignIn: 'auth.forgotPassword.backToSignIn',
|
|
49
|
+
successTitle: 'auth.forgotPassword.successTitle',
|
|
50
|
+
successMessage: 'auth.forgotPassword.successMessage',
|
|
51
|
+
sentTo: 'auth.forgotPassword.sentTo',
|
|
52
|
+
checkEmail: 'auth.forgotPassword.checkEmail',
|
|
53
|
+
step1: 'auth.forgotPassword.steps.step1',
|
|
54
|
+
step2: 'auth.forgotPassword.steps.step2',
|
|
55
|
+
step3: 'auth.forgotPassword.steps.step3',
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// Reset password
|
|
59
|
+
resetPassword: {
|
|
60
|
+
title: 'auth.resetPassword.title',
|
|
61
|
+
description: 'auth.resetPassword.description',
|
|
62
|
+
newPassword: 'auth.resetPassword.newPassword',
|
|
63
|
+
confirmNewPassword: 'auth.resetPassword.confirmNewPassword',
|
|
64
|
+
submit: 'auth.resetPassword.submit',
|
|
65
|
+
submitting: 'auth.resetPassword.submitting',
|
|
66
|
+
successTitle: 'auth.resetPassword.successTitle',
|
|
67
|
+
successMessage: 'auth.resetPassword.successMessage',
|
|
68
|
+
goToSignIn: 'auth.resetPassword.goToSignIn',
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// Shared fields
|
|
72
|
+
fields: {
|
|
73
|
+
email: 'auth.fields.email',
|
|
74
|
+
password: 'auth.fields.password',
|
|
75
|
+
name: 'auth.fields.name',
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
placeholders: {
|
|
79
|
+
emailExample: 'auth.placeholders.emailExample',
|
|
80
|
+
password: 'auth.placeholders.password',
|
|
81
|
+
fullName: 'auth.placeholders.fullName',
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// Errors
|
|
85
|
+
errors: {
|
|
86
|
+
invalidCredentials: 'auth.errors.invalidCredentials',
|
|
87
|
+
loginFailed: 'auth.errors.loginFailed',
|
|
88
|
+
noAuthProvider: 'auth.errors.noAuthProvider',
|
|
89
|
+
registrationFailed: 'auth.errors.registrationFailed',
|
|
90
|
+
emailRequired: 'auth.errors.emailRequired',
|
|
91
|
+
emailInvalid: 'auth.errors.emailInvalid',
|
|
92
|
+
passwordRequired: 'auth.errors.passwordRequired',
|
|
93
|
+
passwordTooShort: 'auth.errors.passwordTooShort',
|
|
94
|
+
passwordsDoNotMatch: 'auth.errors.passwordsDoNotMatch',
|
|
95
|
+
sendResetFailed: 'auth.errors.sendResetFailed',
|
|
96
|
+
resetFailed: 'auth.errors.resetFailed',
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// Password strength
|
|
100
|
+
passwordStrength: {
|
|
101
|
+
weak: 'auth.passwordStrength.weak',
|
|
102
|
+
fair: 'auth.passwordStrength.fair',
|
|
103
|
+
good: 'auth.passwordStrength.good',
|
|
104
|
+
strong: 'auth.passwordStrength.strong',
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
// Social
|
|
108
|
+
social: {
|
|
109
|
+
continueWithGoogle: 'auth.social.continueWithGoogle',
|
|
110
|
+
continueWithApple: 'auth.social.continueWithApple',
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
// Layout shell
|
|
114
|
+
layout: {
|
|
115
|
+
backToHome: 'auth.layout.backToHome',
|
|
116
|
+
orContinueWith: 'auth.layout.orContinueWith',
|
|
117
|
+
allRightsReserved: 'auth.layout.allRightsReserved',
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// Accessibility
|
|
121
|
+
a11y: {
|
|
122
|
+
showPassword: 'auth.a11y.showPassword',
|
|
123
|
+
hidePassword: 'auth.a11y.hidePassword',
|
|
124
|
+
},
|
|
125
|
+
} as const;
|
|
126
|
+
|
|
127
|
+
export type AuthI18nKey = string;
|
|
@@ -18,6 +18,12 @@ export {
|
|
|
18
18
|
maskEmail,
|
|
19
19
|
generateResetToken,
|
|
20
20
|
calculatePasswordStrength,
|
|
21
|
-
|
|
21
|
+
getPasswordStrengthBand,
|
|
22
22
|
sanitizeInput,
|
|
23
|
+
type ValidationResult,
|
|
24
|
+
type PasswordStrengthBand,
|
|
23
25
|
} from "./auth";
|
|
26
|
+
|
|
27
|
+
export { AUTH_KEYS } from "./i18nKeys";
|
|
28
|
+
export { AUTH_VALIDATION_KEYS } from "./validationKeys";
|
|
29
|
+
export type { AuthValidationKey } from "./validationKeys";
|