@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.
Files changed (126) hide show
  1. package/README.md +228 -14
  2. package/package.json +23 -30
  3. package/src/domains/analytics/components/AnalyticsCard.tsx +43 -42
  4. package/src/domains/analytics/components/AnalyticsChart.tsx +345 -149
  5. package/src/domains/analytics/components/AnalyticsLayout.tsx +93 -112
  6. package/src/domains/analytics/components/MetricCard.tsx +29 -87
  7. package/src/domains/analytics/hooks/index.ts +7 -1
  8. package/src/domains/analytics/hooks/useAnalytics.ts +144 -139
  9. package/src/domains/analytics/index.ts +11 -0
  10. package/src/domains/analytics/services/AnalyticsEngineService.ts +103 -55
  11. package/src/domains/analytics/services/PerformanceService.ts +117 -69
  12. package/src/domains/analytics/types/analytics.ts +20 -6
  13. package/src/domains/analytics/types/index.ts +3 -0
  14. package/src/domains/analytics/utils/analytics.test.ts +183 -0
  15. package/src/domains/analytics/utils/analytics.ts +26 -9
  16. package/src/domains/analytics/utils/i18nKeys.ts +40 -0
  17. package/src/domains/analytics/utils/index.ts +2 -0
  18. package/src/domains/auth/components/AuthLayout.tsx +63 -54
  19. package/src/domains/auth/components/ForgotPasswordForm.tsx +96 -98
  20. package/src/domains/auth/components/LoginForm.tsx +167 -226
  21. package/src/domains/auth/components/RegisterForm.tsx +202 -272
  22. package/src/domains/auth/components/ResetPasswordForm.tsx +105 -151
  23. package/src/domains/auth/components/shared/AuthErrorBanner.tsx +35 -0
  24. package/src/domains/auth/components/shared/EmailInput.tsx +91 -0
  25. package/src/domains/auth/components/shared/PasswordInput.tsx +119 -0
  26. package/src/domains/auth/components/shared/PasswordStrengthIndicator.tsx +75 -0
  27. package/src/domains/auth/components/shared/SocialLoginButtons.tsx +58 -0
  28. package/src/domains/auth/components/shared/SocialProviderIcons.tsx +48 -0
  29. package/src/domains/auth/components/shared/index.ts +20 -0
  30. package/src/domains/auth/hooks/index.ts +1 -0
  31. package/src/domains/auth/hooks/useAuth.ts +151 -232
  32. package/src/domains/auth/index.ts +8 -4
  33. package/src/domains/auth/types/auth.ts +26 -5
  34. package/src/domains/auth/types/index.ts +1 -0
  35. package/src/domains/auth/utils/auth.test.ts +182 -0
  36. package/src/domains/auth/utils/auth.ts +71 -130
  37. package/src/domains/auth/utils/i18nKeys.ts +127 -0
  38. package/src/domains/auth/utils/index.ts +7 -1
  39. package/src/domains/auth/utils/secureToken.ts +33 -0
  40. package/src/domains/auth/utils/validationKeys.ts +23 -0
  41. package/src/domains/calendar/hooks/useCalendar.ts +145 -151
  42. package/src/domains/calendar/index.ts +0 -1
  43. package/src/domains/calendar/services/CalendarService.ts +122 -179
  44. package/src/domains/calendar/utils/contentItemMapper.test.ts +108 -0
  45. package/src/domains/calendar/utils/contentItemMapper.ts +99 -0
  46. package/src/domains/calendar/utils/index.ts +33 -30
  47. package/src/domains/layouts/components/BrandLogo.tsx +12 -9
  48. package/src/domains/layouts/components/DashboardHeader.tsx +78 -80
  49. package/src/domains/layouts/components/DashboardLayout.tsx +45 -21
  50. package/src/domains/layouts/components/DashboardSidebar.tsx +48 -51
  51. package/src/domains/layouts/types/layout.ts +7 -1
  52. package/src/domains/layouts/types/user.ts +3 -1
  53. package/src/domains/modules/build.test.tsx +97 -0
  54. package/src/domains/modules/build.tsx +71 -0
  55. package/src/domains/modules/index.ts +2 -0
  56. package/src/domains/modules/types.ts +29 -0
  57. package/src/domains/onboarding/components/AppFocusStep.tsx +99 -68
  58. package/src/domains/onboarding/components/OnboardingWizard.tsx +109 -183
  59. package/src/domains/onboarding/components/PlanStep.tsx +156 -123
  60. package/src/domains/onboarding/components/PlatformsStep.tsx +74 -54
  61. package/src/domains/onboarding/components/StepNavigation.tsx +108 -0
  62. package/src/domains/onboarding/components/StepProgress.tsx +62 -0
  63. package/src/domains/onboarding/components/UserTypeStep.tsx +59 -80
  64. package/src/domains/onboarding/components/index.ts +4 -0
  65. package/src/domains/onboarding/hooks/useOnboarding.ts +76 -66
  66. package/src/domains/onboarding/index.ts +1 -0
  67. package/src/domains/onboarding/types/onboarding.ts +57 -17
  68. package/src/domains/onboarding/utils/i18nKeys.ts +72 -0
  69. package/src/domains/onboarding/utils/index.ts +2 -0
  70. package/src/domains/permissions/PermissionsContext.tsx +103 -0
  71. package/src/domains/permissions/hasPermission.test.ts +50 -0
  72. package/src/domains/permissions/hasPermission.ts +38 -0
  73. package/src/domains/permissions/index.ts +19 -0
  74. package/src/domains/permissions/types.ts +35 -0
  75. package/src/domains/permissions/utils/i18nKeys.ts +11 -0
  76. package/src/domains/session/DemoLogin.tsx +50 -0
  77. package/src/domains/session/OrgSwitcher.tsx +110 -0
  78. package/src/domains/session/SessionContext.tsx +150 -0
  79. package/src/domains/session/inMemoryAuthAdapter.ts +53 -0
  80. package/src/domains/session/index.ts +18 -0
  81. package/src/domains/session/types.ts +47 -0
  82. package/src/domains/session/utils/i18nKeys.ts +12 -0
  83. package/src/domains/settings/components/AuditLogList.tsx +59 -0
  84. package/src/domains/settings/components/PermissionMatrixScreen.tsx +80 -0
  85. package/src/domains/settings/components/SettingsLayout.tsx +75 -60
  86. package/src/domains/settings/components/SettingsSection.tsx +35 -22
  87. package/src/domains/settings/components/UserManagementTable.tsx +69 -0
  88. package/src/domains/settings/components/index.ts +10 -0
  89. package/src/domains/settings/hooks/useSettings.ts +77 -52
  90. package/src/domains/settings/types/settings.ts +2 -0
  91. package/src/domains/settings/utils/i18nKeys.ts +18 -0
  92. package/src/domains/settings/utils/index.ts +1 -0
  93. package/src/domains/settings/utils/settings.ts +11 -4
  94. package/src/i18n/I18nContext.tsx +33 -0
  95. package/src/i18n/defaultT.test.ts +43 -0
  96. package/src/i18n/defaultT.ts +34 -0
  97. package/src/i18n/index.ts +7 -0
  98. package/src/i18n/types.ts +13 -0
  99. package/src/index.ts +1 -7
  100. package/src/primitives/Button.tsx +24 -0
  101. package/src/primitives/Checkbox.tsx +17 -0
  102. package/src/primitives/Input.tsx +23 -0
  103. package/src/primitives/Skeleton.tsx +6 -0
  104. package/src/primitives/cn.ts +10 -0
  105. package/src/primitives/index.ts +23 -0
  106. package/src/styles/dashboard.css +2731 -0
  107. package/src/styles/tokens.contract.test.ts +91 -0
  108. package/src/styles/tokens.css +116 -0
  109. package/src/domains/billing/components/BillingLayout.tsx +0 -63
  110. package/src/domains/billing/components/BillingPage.tsx +0 -49
  111. package/src/domains/billing/components/BillingPortal.tsx +0 -198
  112. package/src/domains/billing/components/InvoiceCard.tsx +0 -143
  113. package/src/domains/billing/components/PaymentMethodsList.tsx +0 -116
  114. package/src/domains/billing/components/PlanComparison.tsx +0 -199
  115. package/src/domains/billing/components/UsageCard.tsx +0 -103
  116. package/src/domains/billing/components/index.ts +0 -13
  117. package/src/domains/billing/constants/billing.ts +0 -69
  118. package/src/domains/billing/hooks/index.ts +0 -7
  119. package/src/domains/billing/hooks/useBilling.ts +0 -385
  120. package/src/domains/billing/index.ts +0 -73
  121. package/src/domains/billing/types/billing.ts +0 -342
  122. package/src/domains/billing/types/index.ts +0 -30
  123. package/src/domains/billing/utils/billing.ts +0 -360
  124. package/src/domains/billing/utils/index.ts +0 -31
  125. package/src/domains/billing/utils/polar.ts +0 -63
  126. package/src/domains/calendar/components/index.ts +0 -7
@@ -21,4 +21,5 @@ export type {
21
21
  RegisterFormProps,
22
22
  ForgotPasswordFormProps,
23
23
  ResetPasswordFormProps,
24
+ Translate,
24
25
  } from "./auth";
@@ -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
- * Utility functions for authentication operations
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 strength
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: "Email is required" };
43
+ return { valid: false, error: AUTH_VALIDATION_KEYS.emailRequired };
43
44
  }
44
-
45
45
  if (!isValidEmail(credentials.email)) {
46
- return { valid: false, error: "Invalid email format" };
46
+ return { valid: false, error: AUTH_VALIDATION_KEYS.emailInvalid };
47
47
  }
48
-
49
48
  if (!credentials.password) {
50
- return { valid: false, error: "Password is required" };
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: "Email is required" };
63
+ return { valid: false, error: AUTH_VALIDATION_KEYS.emailRequired };
74
64
  }
75
-
76
65
  if (!isValidEmail(data.email)) {
77
- return { valid: false, error: "Invalid email format" };
66
+ return { valid: false, error: AUTH_VALIDATION_KEYS.emailInvalid };
78
67
  }
79
-
80
68
  if (requireName && !data.name) {
81
- return { valid: false, error: "Name is required" };
69
+ return { valid: false, error: AUTH_VALIDATION_KEYS.nameRequired };
82
70
  }
83
-
84
71
  if (!data.password) {
85
- return { valid: false, error: "Password is required" };
72
+ return { valid: false, error: AUTH_VALIDATION_KEYS.passwordRequired };
86
73
  }
87
-
88
74
  if (!isValidPassword(data.password)) {
89
- return { valid: false, error: "Password must be at least 8 characters" };
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: "Passwords do not match" };
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 reset request
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: "Email is required" };
88
+ return { valid: false, error: AUTH_VALIDATION_KEYS.emailRequired };
111
89
  }
112
-
113
90
  if (!isValidEmail(data.email)) {
114
- return { valid: false, error: "Invalid email format" };
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: "Invalid reset token" };
105
+ return { valid: false, error: AUTH_VALIDATION_KEYS.invalidResetToken };
136
106
  }
137
-
138
107
  if (!data.password) {
139
- return { valid: false, error: "Password is required" };
108
+ return { valid: false, error: AUTH_VALIDATION_KEYS.passwordRequired };
140
109
  }
141
-
142
110
  if (!isValidPassword(data.password)) {
143
- return { valid: false, error: "Password must be at least 8 characters" };
111
+ return { valid: false, error: AUTH_VALIDATION_KEYS.passwordTooShort };
144
112
  }
145
-
146
113
  if (data.password !== data.confirmPassword) {
147
- return { valid: false, error: "Passwords do not match" };
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
- * @param user - User object
157
- * @returns Display name or email fallback
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 "Guest";
161
- return user.name || user.email || "User";
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
- * @param user - User object
168
- * @returns User initials (up to 2 characters)
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 "G";
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
- * @param user - User object
194
- * @param locale - Locale for formatting (default: en-US)
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(user: User | null, locale: string = "en-US"): string {
198
- if (!user || !user.createdAt) return "";
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
- * @param email - Email address
210
- * @returns Masked email
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 email;
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
- * Generate secure random token (for demo purposes)
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
- * Get password strength label
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 function getPasswordStrengthLabel(password: string): string {
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 "Weak";
267
- if (score < 50) return "Fair";
268
- if (score < 75) return "Good";
269
- return "Strong";
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, 1000);
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
- getPasswordStrengthLabel,
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";