@umituz/web-dashboard 5.0.0 → 6.0.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 (35) hide show
  1. package/README.md +4 -3
  2. package/package.json +2 -12
  3. package/src/index.ts +0 -10
  4. package/src/styles/dashboard.css +93 -544
  5. package/src/domains/auth/components/AuthLayout.tsx +0 -99
  6. package/src/domains/auth/components/ForgotPasswordForm.tsx +0 -167
  7. package/src/domains/auth/components/LoginForm.tsx +0 -222
  8. package/src/domains/auth/components/RegisterForm.tsx +0 -281
  9. package/src/domains/auth/components/ResetPasswordForm.tsx +0 -173
  10. package/src/domains/auth/components/index.ts +0 -11
  11. package/src/domains/auth/components/shared/AuthErrorBanner.tsx +0 -25
  12. package/src/domains/auth/components/shared/EmailInput.tsx +0 -73
  13. package/src/domains/auth/components/shared/PasswordInput.tsx +0 -100
  14. package/src/domains/auth/components/shared/PasswordStrengthIndicator.tsx +0 -63
  15. package/src/domains/auth/components/shared/SocialLoginButtons.tsx +0 -44
  16. package/src/domains/auth/components/shared/SocialProviderIcons.tsx +0 -48
  17. package/src/domains/auth/components/shared/index.ts +0 -20
  18. package/src/domains/auth/hooks/index.ts +0 -8
  19. package/src/domains/auth/hooks/useAuth.ts +0 -230
  20. package/src/domains/auth/index.ts +0 -62
  21. package/src/domains/auth/types/auth.ts +0 -302
  22. package/src/domains/auth/types/index.ts +0 -25
  23. package/src/domains/auth/utils/auth.test.ts +0 -188
  24. package/src/domains/auth/utils/auth.ts +0 -221
  25. package/src/domains/auth/utils/i18nKeys.ts +0 -109
  26. package/src/domains/auth/utils/index.ts +0 -29
  27. package/src/domains/auth/utils/secureToken.ts +0 -33
  28. package/src/domains/auth/utils/validationKeys.ts +0 -23
  29. package/src/domains/session/DemoLogin.tsx +0 -42
  30. package/src/domains/session/OrgSwitcher.tsx +0 -102
  31. package/src/domains/session/SessionContext.tsx +0 -150
  32. package/src/domains/session/inMemoryAuthAdapter.ts +0 -53
  33. package/src/domains/session/index.ts +0 -18
  34. package/src/domains/session/types.ts +0 -47
  35. package/src/domains/session/utils/i18nKeys.ts +0 -12
@@ -1,221 +0,0 @@
1
- /**
2
- * Auth Utilities
3
- *
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.
7
- */
8
-
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 };
22
-
23
- /**
24
- * Validate email format
25
- */
26
- export function isValidEmail(email: string): boolean {
27
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
28
- return emailRegex.test(email);
29
- }
30
-
31
- /**
32
- * Validate password length
33
- */
34
- export function isValidPassword(password: string, minLength: number = 8): boolean {
35
- return password.length >= minLength;
36
- }
37
-
38
- /**
39
- * Validate login credentials
40
- */
41
- export function validateLogin(credentials: LoginCredentials): ValidationResult {
42
- if (!credentials.email) {
43
- return { valid: false, error: AUTH_VALIDATION_KEYS.emailRequired };
44
- }
45
- if (!isValidEmail(credentials.email)) {
46
- return { valid: false, error: AUTH_VALIDATION_KEYS.emailInvalid };
47
- }
48
- if (!credentials.password) {
49
- return { valid: false, error: AUTH_VALIDATION_KEYS.passwordRequired };
50
- }
51
- return { valid: true };
52
- }
53
-
54
- /**
55
- * Validate registration data
56
- */
57
- export function validateRegister(
58
- data: RegisterData & { confirmPassword?: string },
59
- requireName: boolean = false,
60
- requirePasswordConfirm?: boolean,
61
- ): ValidationResult {
62
- if (!data.email) {
63
- return { valid: false, error: AUTH_VALIDATION_KEYS.emailRequired };
64
- }
65
- if (!isValidEmail(data.email)) {
66
- return { valid: false, error: AUTH_VALIDATION_KEYS.emailInvalid };
67
- }
68
- if (requireName && !data.name) {
69
- return { valid: false, error: AUTH_VALIDATION_KEYS.nameRequired };
70
- }
71
- if (!data.password) {
72
- return { valid: false, error: AUTH_VALIDATION_KEYS.passwordRequired };
73
- }
74
- if (!isValidPassword(data.password)) {
75
- return { valid: false, error: AUTH_VALIDATION_KEYS.passwordTooShort };
76
- }
77
- if (requirePasswordConfirm && data.password !== data.confirmPassword) {
78
- return { valid: false, error: AUTH_VALIDATION_KEYS.passwordsDoNotMatch };
79
- }
80
- return { valid: true };
81
- }
82
-
83
- /**
84
- * Validate forgot-password request
85
- */
86
- export function validateForgotPassword(data: { email: string }): ValidationResult {
87
- if (!data.email) {
88
- return { valid: false, error: AUTH_VALIDATION_KEYS.emailRequired };
89
- }
90
- if (!isValidEmail(data.email)) {
91
- return { valid: false, error: AUTH_VALIDATION_KEYS.emailInvalid };
92
- }
93
- return { valid: true };
94
- }
95
-
96
- /**
97
- * Validate password reset confirmation
98
- */
99
- export function validateResetPassword(data: {
100
- token: string;
101
- password: string;
102
- confirmPassword: string;
103
- }): ValidationResult {
104
- if (!data.token) {
105
- return { valid: false, error: AUTH_VALIDATION_KEYS.invalidResetToken };
106
- }
107
- if (!data.password) {
108
- return { valid: false, error: AUTH_VALIDATION_KEYS.passwordRequired };
109
- }
110
- if (!isValidPassword(data.password)) {
111
- return { valid: false, error: AUTH_VALIDATION_KEYS.passwordTooShort };
112
- }
113
- if (data.password !== data.confirmPassword) {
114
- return { valid: false, error: AUTH_VALIDATION_KEYS.passwordsDoNotMatch };
115
- }
116
- return { valid: true };
117
- }
118
-
119
- /**
120
- * Get user display name
121
- *
122
- * Returns null when no usable identifier exists, letting the caller
123
- * decide on a localized fallback. Avoids hardcoded "Guest"/"User" strings.
124
- */
125
- export function getUserDisplayName(user: User | null): string | null {
126
- if (!user) return null;
127
- return user.name || user.email || null;
128
- }
129
-
130
- /**
131
- * Get user initials
132
- *
133
- * Returns null when no name is available, so the caller can decide
134
- * whether to render a placeholder or hide the avatar.
135
- */
136
- export function getUserInitials(user: User | null): string | null {
137
- if (!user) return null;
138
- const name = user.name || user.email || "";
139
- if (!name) return null;
140
- const parts = name.trim().split(" ");
141
- if (parts.length >= 2 && parts[0] && parts[parts.length - 1]) {
142
- return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
143
- }
144
- return name.slice(0, 2).toUpperCase();
145
- }
146
-
147
- /**
148
- * Check if user has verified email
149
- */
150
- export function isEmailVerified(user: User | null): boolean {
151
- return user?.emailVerified === true;
152
- }
153
-
154
- /**
155
- * Format user creation date
156
- *
157
- * Returns null when the user has no createdAt; the caller decides
158
- * whether to display "—" or hide the field.
159
- */
160
- export function formatUserCreatedAt(
161
- user: User | null,
162
- locale: string = "en-US",
163
- ): string | null {
164
- if (!user?.createdAt) return null;
165
- return new Date(user.createdAt).toLocaleDateString(locale, {
166
- year: "numeric",
167
- month: "long",
168
- day: "numeric",
169
- });
170
- }
171
-
172
- /**
173
- * Mask email for privacy (e.g., u***@example.com)
174
- *
175
- * Returns null when the email is invalid so the caller can show
176
- * a "—" placeholder rather than the raw input.
177
- */
178
- export function maskEmail(email: string): string | null {
179
- const [local, domain] = email.split("@");
180
- if (!local || !domain) return null;
181
- const maskedLocal = local[0] + "***";
182
- return `${maskedLocal}@${domain}`;
183
- }
184
-
185
- /**
186
- * Password strength score (0-100)
187
- */
188
- export function calculatePasswordStrength(password: string): number {
189
- let score = 0;
190
-
191
- if (password.length >= 8) score += 25;
192
- if (password.length >= 12) score += 25;
193
-
194
- if (/[a-z]/.test(password)) score += 12.5;
195
- if (/[A-Z]/.test(password)) score += 12.5;
196
- if (/[0-9]/.test(password)) score += 12.5;
197
- if (/[^a-zA-Z0-9]/.test(password)) score += 12.5;
198
-
199
- return Math.min(score, 100);
200
- }
201
-
202
- /**
203
- * Password strength band. Numeric so the UI can map it to a label
204
- * via its own translation catalog.
205
- */
206
- export type PasswordStrengthBand = 'weak' | 'fair' | 'good' | 'strong';
207
-
208
- export function getPasswordStrengthBand(password: string): PasswordStrengthBand {
209
- const score = calculatePasswordStrength(password);
210
- if (score < 25) return 'weak';
211
- if (score < 50) return 'fair';
212
- if (score < 75) return 'good';
213
- return 'strong';
214
- }
215
-
216
- /**
217
- * Sanitize user input for display
218
- */
219
- export function sanitizeInput(input: string, maxLength: number = 1000): string {
220
- return input.trim().slice(0, maxLength);
221
- }
@@ -1,109 +0,0 @@
1
- export const AUTH_KEYS = {
2
- login: {
3
- welcomeBack: 'auth.login.welcomeBack',
4
- signInTo: 'auth.login.signInTo',
5
- submit: 'auth.login.submit',
6
- submitting: 'auth.login.submitting',
7
- rememberMe: 'auth.login.rememberMe',
8
- forgotPassword: 'auth.login.forgotPassword',
9
- noAccount: 'auth.login.noAccount',
10
- signUp: 'auth.login.signUp',
11
- orContinueWith: 'auth.login.orContinueWith',
12
- },
13
-
14
- register: {
15
- createAccount: 'auth.register.createAccount',
16
- join: 'auth.register.join',
17
- fullName: 'auth.register.fullName',
18
- fullNamePlaceholder: 'auth.placeholders.fullName',
19
- confirmPassword: 'auth.register.confirmPassword',
20
- confirmPasswordPlaceholder: 'auth.placeholders.confirmPassword',
21
- acceptTerms: 'auth.register.acceptTerms',
22
- termsOfService: 'auth.register.termsOfService',
23
- privacyPolicy: 'auth.register.privacyPolicy',
24
- submit: 'auth.register.submit',
25
- submitting: 'auth.register.submitting',
26
- haveAccount: 'auth.register.haveAccount',
27
- signIn: 'auth.register.signIn',
28
- mustAcceptTerms: 'auth.register.mustAcceptTerms',
29
- passwordsDoNotMatch: 'auth.register.passwordsDoNotMatch',
30
- },
31
-
32
- forgotPassword: {
33
- title: 'auth.forgotPassword.title',
34
- description: 'auth.forgotPassword.description',
35
- submit: 'auth.forgotPassword.submit',
36
- submitting: 'auth.forgotPassword.submitting',
37
- backToSignIn: 'auth.forgotPassword.backToSignIn',
38
- successTitle: 'auth.forgotPassword.successTitle',
39
- successMessage: 'auth.forgotPassword.successMessage',
40
- sentTo: 'auth.forgotPassword.sentTo',
41
- checkEmail: 'auth.forgotPassword.checkEmail',
42
- step1: 'auth.forgotPassword.steps.step1',
43
- step2: 'auth.forgotPassword.steps.step2',
44
- step3: 'auth.forgotPassword.steps.step3',
45
- },
46
-
47
- resetPassword: {
48
- title: 'auth.resetPassword.title',
49
- description: 'auth.resetPassword.description',
50
- newPassword: 'auth.resetPassword.newPassword',
51
- confirmNewPassword: 'auth.resetPassword.confirmNewPassword',
52
- submit: 'auth.resetPassword.submit',
53
- submitting: 'auth.resetPassword.submitting',
54
- successTitle: 'auth.resetPassword.successTitle',
55
- successMessage: 'auth.resetPassword.successMessage',
56
- goToSignIn: 'auth.resetPassword.goToSignIn',
57
- },
58
-
59
- fields: {
60
- email: 'auth.fields.email',
61
- password: 'auth.fields.password',
62
- name: 'auth.fields.name',
63
- },
64
-
65
- placeholders: {
66
- emailExample: 'auth.placeholders.emailExample',
67
- password: 'auth.placeholders.password',
68
- fullName: 'auth.placeholders.fullName',
69
- },
70
-
71
- errors: {
72
- invalidCredentials: 'auth.errors.invalidCredentials',
73
- loginFailed: 'auth.errors.loginFailed',
74
- noAuthProvider: 'auth.errors.noAuthProvider',
75
- registrationFailed: 'auth.errors.registrationFailed',
76
- emailRequired: 'auth.errors.emailRequired',
77
- emailInvalid: 'auth.errors.emailInvalid',
78
- passwordRequired: 'auth.errors.passwordRequired',
79
- passwordTooShort: 'auth.errors.passwordTooShort',
80
- passwordsDoNotMatch: 'auth.errors.passwordsDoNotMatch',
81
- sendResetFailed: 'auth.errors.sendResetFailed',
82
- resetFailed: 'auth.errors.resetFailed',
83
- },
84
-
85
- passwordStrength: {
86
- weak: 'auth.passwordStrength.weak',
87
- fair: 'auth.passwordStrength.fair',
88
- good: 'auth.passwordStrength.good',
89
- strong: 'auth.passwordStrength.strong',
90
- },
91
-
92
- social: {
93
- continueWithGoogle: 'auth.social.continueWithGoogle',
94
- continueWithApple: 'auth.social.continueWithApple',
95
- },
96
-
97
- layout: {
98
- backToHome: 'auth.layout.backToHome',
99
- orContinueWith: 'auth.layout.orContinueWith',
100
- allRightsReserved: 'auth.layout.allRightsReserved',
101
- },
102
-
103
- a11y: {
104
- showPassword: 'auth.a11y.showPassword',
105
- hidePassword: 'auth.a11y.hidePassword',
106
- },
107
- } as const;
108
-
109
- export type AuthI18nKey = string;
@@ -1,29 +0,0 @@
1
- /**
2
- * Auth Utilities
3
- *
4
- * Export all auth utilities
5
- */
6
-
7
- export {
8
- isValidEmail,
9
- isValidPassword,
10
- validateLogin,
11
- validateRegister,
12
- validateForgotPassword,
13
- validateResetPassword,
14
- getUserDisplayName,
15
- getUserInitials,
16
- isEmailVerified,
17
- formatUserCreatedAt,
18
- maskEmail,
19
- generateResetToken,
20
- calculatePasswordStrength,
21
- getPasswordStrengthBand,
22
- sanitizeInput,
23
- type ValidationResult,
24
- type PasswordStrengthBand,
25
- } from "./auth";
26
-
27
- export { AUTH_KEYS } from "./i18nKeys";
28
- export { AUTH_VALIDATION_KEYS } from "./validationKeys";
29
- export type { AuthValidationKey } from "./validationKeys";
@@ -1,33 +0,0 @@
1
- /**
2
- * Auth Utilities - Secure Token
3
- *
4
- * Cryptographically secure token generation utilities
5
- */
6
-
7
- /**
8
- * Generate a cryptographically secure random token.
9
- *
10
- * Requires the Web Crypto API (`crypto.getRandomValues`), available in
11
- * all modern browsers, Node.js >= 19 (and >= 15 via `globalThis.crypto`),
12
- * and React Native with a crypto polyfill. Throws instead of silently
13
- * degrading to an insecure PRNG — predictable tokens are worse than
14
- * a loud failure.
15
- *
16
- * @param length - Token length in bytes (default: 32)
17
- * @returns Hex-encoded random token
18
- * @throws Error when no CSPRNG is available in the runtime
19
- */
20
- export function generateResetToken(length: number = 32): string {
21
- if (typeof crypto === 'undefined' || typeof crypto.getRandomValues !== 'function') {
22
- throw new Error(
23
- 'generateResetToken: no cryptographic random source available in this runtime ' +
24
- '(requires crypto.getRandomValues — Web Crypto, Node >= 19, or a polyfill)',
25
- );
26
- }
27
-
28
- const byteLength = Math.max(1, Math.floor(length));
29
- const bytes = new Uint8Array(byteLength);
30
- crypto.getRandomValues(bytes);
31
-
32
- return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
33
- }
@@ -1,23 +0,0 @@
1
- /**
2
- * Auth Validation Error Keys
3
- *
4
- * Stable i18n keys for validation errors. The auth UI maps these to
5
- * localized messages via the consumer's translation system.
6
- *
7
- * Why keys instead of literal strings:
8
- * - No hardcoded user-facing strings in source code
9
- * - Centralized catalog: easier to audit and translate
10
- * - Validators stay pure and testable
11
- */
12
-
13
- export const AUTH_VALIDATION_KEYS = {
14
- emailRequired: 'auth.validation.emailRequired',
15
- emailInvalid: 'auth.validation.emailInvalid',
16
- passwordRequired: 'auth.validation.passwordRequired',
17
- passwordTooShort: 'auth.validation.passwordTooShort',
18
- passwordsDoNotMatch: 'auth.validation.passwordsDoNotMatch',
19
- nameRequired: 'auth.validation.nameRequired',
20
- invalidResetToken: 'auth.validation.invalidResetToken',
21
- } as const;
22
-
23
- export type AuthValidationKey = (typeof AUTH_VALIDATION_KEYS)[keyof typeof AUTH_VALIDATION_KEYS];
@@ -1,42 +0,0 @@
1
- import { UserRound } from "lucide-react";
2
- import { Button } from "@umituz/web-design-system/atoms";
3
- import { useT } from "@umituz/web-localization/react";
4
- import { SESSION_KEYS } from "./utils/i18nKeys";
5
-
6
- export interface DemoLoginProps {
7
- onDemo: (userId: string) => void | Promise<void>;
8
- userId: string;
9
- label?: string;
10
- variant?: "default" | "outline" | "ghost";
11
- size?: "default" | "sm";
12
- disabled?: boolean;
13
- className?: string;
14
- }
15
-
16
- export const DemoLogin = ({
17
- onDemo,
18
- userId,
19
- label,
20
- variant = "outline",
21
- size = "default",
22
- disabled = false,
23
- className,
24
- }: DemoLoginProps) => {
25
- const t = useT();
26
-
27
- return (
28
- <Button
29
- type="button"
30
- variant={variant}
31
- size={size}
32
- disabled={disabled}
33
- className={className}
34
- onClick={() => void onDemo(userId)}
35
- >
36
- <UserRound aria-hidden="true" />
37
- {label ?? t(SESSION_KEYS.demoLogin.action)}
38
- </Button>
39
- );
40
- };
41
-
42
- export default DemoLogin;
@@ -1,102 +0,0 @@
1
- import { useState } from "react";
2
- import { Building2, Check, ChevronDown } from "lucide-react";
3
- import { cn } from "@umituz/web-design-system/utils/cn";
4
- import { useT } from "@umituz/web-localization/react";
5
- import { SESSION_KEYS } from "./utils/i18nKeys";
6
- import type { Tenant } from "./types";
7
-
8
- export interface OrgSwitcherProps {
9
- tenants: Tenant[];
10
- activeTenantId?: string | null;
11
- onSwitch: (tenantId: string) => void;
12
- disabled?: boolean;
13
- className?: string;
14
- }
15
-
16
- const initialsOf = (tenant: Tenant): string =>
17
- tenant.initials ??
18
- tenant.name
19
- .split(/\s+/)
20
- .slice(0, 2)
21
- .map((word) => word.charAt(0).toUpperCase())
22
- .join("");
23
-
24
- export const OrgSwitcher = ({
25
- tenants,
26
- activeTenantId,
27
- onSwitch,
28
- disabled = false,
29
- className,
30
- }: OrgSwitcherProps) => {
31
- const t = useT();
32
- const [open, setOpen] = useState(false);
33
-
34
- if (tenants.length === 0) return null;
35
-
36
- const active = tenants.find((tenant) => tenant.id === activeTenantId) ?? tenants[0];
37
-
38
- return (
39
- <div className={cn("udb-header__anchor", className)}>
40
- <button
41
- type="button"
42
- className="udb-profile-btn"
43
- disabled={disabled}
44
- aria-haspopup="menu"
45
- aria-expanded={open}
46
- aria-label={t(SESSION_KEYS.orgSwitcher.label)}
47
- onClick={() => setOpen((prev) => !prev)}
48
- >
49
- <span className="udb-avatar">{initialsOf(active)}</span>
50
- <span className="udb-org">
51
- <span className="udb-org__name">{active.name}</span>
52
- {active.plan && <span className="udb-org__meta">{active.plan}</span>}
53
- </span>
54
- <ChevronDown
55
- className={cn(
56
- "udb-profile-btn__chevron",
57
- open && "udb-profile-btn__chevron--open",
58
- )}
59
- aria-hidden="true"
60
- />
61
- </button>
62
-
63
- {open && (
64
- <>
65
- <button
66
- type="button"
67
- className="udb-overlay"
68
- aria-label="close tenant menu"
69
- tabIndex={-1}
70
- onClick={() => setOpen(false)}
71
- />
72
- <div className="udb-menu udb-org-menu" role="menu">
73
- {tenants.map((tenant) => {
74
- const isActive = tenant.id === active.id;
75
- return (
76
- <button
77
- key={tenant.id}
78
- type="button"
79
- role="menuitem"
80
- className={cn(
81
- "udb-menu__item",
82
- isActive && "udb-menu__item--active",
83
- )}
84
- onClick={() => {
85
- onSwitch(tenant.id);
86
- setOpen(false);
87
- }}
88
- >
89
- <Building2 className="udb-menu__item-icon" aria-hidden="true" />
90
- <span className="udb-org-menu__label">{tenant.name}</span>
91
- {isActive && <Check className="udb-org-menu__check" aria-hidden="true" />}
92
- </button>
93
- );
94
- })}
95
- </div>
96
- </>
97
- )}
98
- </div>
99
- );
100
- };
101
-
102
- export default OrgSwitcher;