hazo_auth 1.6.1 → 1.6.2

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 (33) hide show
  1. package/README.md +126 -0
  2. package/SETUP_CHECKLIST.md +55 -1
  3. package/dist/cli/init.d.ts.map +1 -1
  4. package/dist/cli/init.js +5 -0
  5. package/dist/client.d.ts +8 -0
  6. package/dist/client.d.ts.map +1 -0
  7. package/dist/client.js +25 -0
  8. package/dist/components/layouts/email_verification/index.d.ts +2 -1
  9. package/dist/components/layouts/email_verification/index.d.ts.map +1 -1
  10. package/dist/components/layouts/email_verification/index.js +3 -2
  11. package/dist/components/layouts/forgot_password/index.d.ts +3 -1
  12. package/dist/components/layouts/forgot_password/index.d.ts.map +1 -1
  13. package/dist/components/layouts/forgot_password/index.js +3 -2
  14. package/dist/components/layouts/my_settings/components/editable_field.js +1 -1
  15. package/dist/components/layouts/my_settings/components/password_change_dialog.js +1 -1
  16. package/dist/components/layouts/my_settings/components/profile_picture_display.js +1 -1
  17. package/dist/components/layouts/my_settings/components/profile_picture_gravatar_tab.js +1 -1
  18. package/dist/components/layouts/my_settings/components/profile_picture_library_tab.js +4 -4
  19. package/dist/components/layouts/my_settings/components/profile_picture_upload_tab.js +4 -4
  20. package/dist/components/layouts/my_settings/index.js +1 -1
  21. package/dist/components/layouts/shared/components/profile_pic_menu.js +2 -2
  22. package/dist/lib/auth_utility_config.server.js +2 -2
  23. package/dist/lib/services/user_profiles_cache.d.ts +76 -0
  24. package/dist/lib/services/user_profiles_cache.d.ts.map +1 -0
  25. package/dist/lib/services/user_profiles_cache.js +141 -0
  26. package/dist/lib/services/user_profiles_service.d.ts +17 -0
  27. package/dist/lib/services/user_profiles_service.d.ts.map +1 -1
  28. package/dist/lib/services/user_profiles_service.js +136 -44
  29. package/dist/lib/user_profiles_config.server.d.ts +15 -0
  30. package/dist/lib/user_profiles_config.server.d.ts.map +1 -0
  31. package/dist/lib/user_profiles_config.server.js +23 -0
  32. package/hazo_auth_config.example.ini +25 -5
  33. package/package.json +5 -1
package/README.md CHANGED
@@ -106,6 +106,132 @@ import { hazo_get_auth } from "hazo_auth/lib/auth/hazo_get_auth.server";
106
106
 
107
107
  ---
108
108
 
109
+ ## Required Dependencies
110
+
111
+ hazo_auth uses shadcn/ui components. Install the required dependencies in your project:
112
+
113
+ ```bash
114
+ # Required for all auth pages
115
+ npx shadcn@latest add button input label
116
+
117
+ # Required for My Settings page
118
+ npx shadcn@latest add dialog tabs switch avatar dropdown-menu
119
+
120
+ # Required for toast notifications
121
+ npx shadcn@latest add sonner
122
+ ```
123
+
124
+ **Add Toaster to your app layout:**
125
+
126
+ ```tsx
127
+ // app/layout.tsx
128
+ import { Toaster } from "sonner";
129
+
130
+ export default function RootLayout({ children }) {
131
+ return (
132
+ <html>
133
+ <body>
134
+ {children}
135
+ <Toaster />
136
+ </body>
137
+ </html>
138
+ );
139
+ }
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Client vs Server Imports
145
+
146
+ hazo_auth provides separate entry points for client and server code to avoid bundling Node.js modules in the browser:
147
+
148
+ ### Client Components
149
+
150
+ For client components (browser-safe, no Node.js dependencies):
151
+
152
+ ```typescript
153
+ // Use hazo_auth/client for client components
154
+ import {
155
+ ProfilePicMenu,
156
+ use_auth_status,
157
+ use_hazo_auth,
158
+ cn
159
+ } from "hazo_auth/client";
160
+ ```
161
+
162
+ ### Server Components / API Routes
163
+
164
+ For server-side code (API routes, Server Components):
165
+
166
+ ```typescript
167
+ // Use the main hazo_auth export for server-side code
168
+ import { hazo_get_auth, get_config_value } from "hazo_auth";
169
+ import { hazo_get_user_profiles } from "hazo_auth";
170
+ ```
171
+
172
+ ### Why This Matters
173
+
174
+ If you import from the main `hazo_auth` entry in a client component, you'll get bundling errors like:
175
+ ```
176
+ Module not found: Can't resolve 'fs'
177
+ ```
178
+
179
+ Use `hazo_auth/client` to avoid this.
180
+
181
+ ---
182
+
183
+ ## Dark Mode / Theming
184
+
185
+ hazo_auth supports dark mode via CSS custom properties. To enable dark mode:
186
+
187
+ ### 1. Import the theme CSS
188
+
189
+ Copy the variables file to your project:
190
+
191
+ ```bash
192
+ cp node_modules/hazo_auth/src/styles/hazo-auth-variables.css ./app/hazo-auth-theme.css
193
+ ```
194
+
195
+ Import in your `globals.css`:
196
+
197
+ ```css
198
+ @import "./hazo-auth-theme.css";
199
+ ```
200
+
201
+ ### 2. CSS Variables Reference
202
+
203
+ You can customize the theme by overriding these variables:
204
+
205
+ ```css
206
+ :root {
207
+ /* Backgrounds */
208
+ --hazo-bg-subtle: #f8fafc; /* Light background */
209
+ --hazo-bg-muted: #f1f5f9; /* Slightly darker background */
210
+
211
+ /* Text */
212
+ --hazo-text-primary: #0f172a; /* Primary text */
213
+ --hazo-text-secondary: #334155; /* Secondary text */
214
+ --hazo-text-muted: #64748b; /* Muted/subtle text */
215
+
216
+ /* Borders */
217
+ --hazo-border: #e2e8f0; /* Standard border */
218
+ }
219
+
220
+ .dark {
221
+ /* Dark mode overrides */
222
+ --hazo-bg-subtle: #18181b;
223
+ --hazo-bg-muted: #27272a;
224
+ --hazo-text-primary: #fafafa;
225
+ --hazo-text-secondary: #d4d4d8;
226
+ --hazo-text-muted: #a1a1aa;
227
+ --hazo-border: #3f3f46;
228
+ }
229
+ ```
230
+
231
+ The dark class is typically added by next-themes or similar theme providers.
232
+
233
+ ---
234
+
109
235
  ## Configuration Setup
110
236
 
111
237
  After installing the package, you need to set up configuration files in your project root:
@@ -60,7 +60,61 @@ ls node_modules/hazo_auth/package.json
60
60
  # Expected: file exists
61
61
  ```
62
62
 
63
- ### Step 1.2: Initialize project (Recommended)
63
+ ### Step 1.2: Install Required shadcn/ui Components
64
+
65
+ hazo_auth uses shadcn/ui components. Install the required dependencies:
66
+
67
+ **For all auth pages (login, register, etc.):**
68
+ ```bash
69
+ npx shadcn@latest add button input label
70
+ ```
71
+
72
+ **For My Settings page:**
73
+ ```bash
74
+ npx shadcn@latest add dialog tabs switch avatar dropdown-menu
75
+ ```
76
+
77
+ **For toast notifications:**
78
+ ```bash
79
+ npx shadcn@latest add sonner
80
+ ```
81
+
82
+ **Add Toaster to your app layout:**
83
+
84
+ Edit `app/layout.tsx` and add the Toaster component:
85
+
86
+ ```tsx
87
+ import { Toaster } from "sonner";
88
+
89
+ export default function RootLayout({ children }) {
90
+ return (
91
+ <html>
92
+ <body>
93
+ {children}
94
+ <Toaster />
95
+ </body>
96
+ </html>
97
+ );
98
+ }
99
+ ```
100
+
101
+ ### Step 1.4: Enable Dark Mode Support (Optional)
102
+
103
+ hazo_auth components support dark mode via CSS custom properties. Add the CSS variables to your global styles:
104
+
105
+ **Copy the CSS variables file:**
106
+ ```bash
107
+ cp node_modules/hazo_auth/src/styles/hazo-auth-variables.css ./app/hazo-auth-theme.css
108
+ ```
109
+
110
+ **Import in your global styles (`app/globals.css`):**
111
+ ```css
112
+ @import "./hazo-auth-theme.css";
113
+ ```
114
+
115
+ Or add the variables directly to your CSS - see the file for all available variables.
116
+
117
+ ### Step 1.5: Initialize project (Recommended)
64
118
 
65
119
  Use the CLI to automatically set up directories and copy config files:
66
120
 
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"AAqIA,wBAAgB,WAAW,IAAI,IAAI,CAkHlC"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"AA2IA,wBAAgB,WAAW,IAAI,IAAI,CAkHlC"}
package/dist/cli/init.js CHANGED
@@ -1,7 +1,12 @@
1
1
  // file_description: init command for hazo_auth
2
2
  // Creates directories and copies config files to consuming projects
3
+ import { fileURLToPath } from "url";
3
4
  import * as fs from "fs";
4
5
  import * as path from "path";
6
+ // section: esm_shim
7
+ // ESM-compatible __dirname shim
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
5
10
  // section: constants
6
11
  const REQUIRED_DIRECTORIES = [
7
12
  "public/profile_pictures/library",
@@ -0,0 +1,8 @@
1
+ export * from "./components/index";
2
+ export { cn, merge_class_names } from "./lib/utils";
3
+ export * from "./lib/auth/auth_types";
4
+ export { use_auth_status, trigger_auth_status_refresh } from "./components/layouts/shared/hooks/use_auth_status";
5
+ export { use_hazo_auth, trigger_hazo_auth_refresh } from "./components/layouts/shared/hooks/use_hazo_auth";
6
+ export type { UseHazoAuthOptions, UseHazoAuthResult } from "./components/layouts/shared/hooks/use_hazo_auth";
7
+ export * from "./components/layouts/shared/utils/validation";
8
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAYA,cAAc,oBAAoB,CAAC;AAInC,OAAO,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAIpD,cAAc,uBAAuB,CAAC;AAItC,OAAO,EAAE,eAAe,EAAE,2BAA2B,EAAE,MAAM,mDAAmD,CAAC;AACjH,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,iDAAiD,CAAC;AAC3G,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,iDAAiD,CAAC;AAI7G,cAAc,8CAA8C,CAAC"}
package/dist/client.js ADDED
@@ -0,0 +1,25 @@
1
+ // file_description: client-safe exports for hazo_auth package
2
+ // This file exports only modules that are safe to use in client components (browser)
3
+ // It excludes any server-side Node.js dependencies (fs, path, database, etc.)
4
+ //
5
+ // USAGE:
6
+ // import { ProfilePicMenu, use_auth_status, cn } from "hazo_auth/client";
7
+ //
8
+ // For server-side code (API routes, Server Components), use:
9
+ // import { hazo_get_auth, get_config_value } from "hazo_auth";
10
+ // section: component_exports
11
+ // All UI and layout components are client-safe
12
+ export * from "./components/index";
13
+ // section: utility_exports
14
+ // CSS utility functions
15
+ export { cn, merge_class_names } from "./lib/utils";
16
+ // section: type_exports
17
+ // Type definitions are always safe (erased at runtime)
18
+ export * from "./lib/auth/auth_types";
19
+ // section: client_hook_exports
20
+ // Re-export from shared hooks (these are already "use client" components)
21
+ export { use_auth_status, trigger_auth_status_refresh } from "./components/layouts/shared/hooks/use_auth_status";
22
+ export { use_hazo_auth, trigger_hazo_auth_refresh } from "./components/layouts/shared/hooks/use_hazo_auth";
23
+ // section: validation_exports
24
+ // Client-side validation utilities
25
+ export * from "./components/layouts/shared/utils/validation";
@@ -12,6 +12,7 @@ export type EmailVerificationLayoutProps<TClient = unknown> = {
12
12
  error_labels?: Partial<EmailVerificationErrorLabels>;
13
13
  redirect_delay?: number;
14
14
  login_path?: string;
15
+ sign_in_label?: string;
15
16
  already_logged_in_message?: string;
16
17
  showLogoutButton?: boolean;
17
18
  showReturnHomeButton?: boolean;
@@ -19,5 +20,5 @@ export type EmailVerificationLayoutProps<TClient = unknown> = {
19
20
  returnHomePath?: string;
20
21
  data_client: LayoutDataClient<TClient>;
21
22
  };
22
- export default function email_verification_layout<TClient>({ image_src, image_alt, image_background_color, field_overrides, labels, button_colors, success_labels, error_labels, redirect_delay, login_path, data_client, already_logged_in_message, showLogoutButton, showReturnHomeButton, returnHomeButtonLabel, returnHomePath, }: EmailVerificationLayoutProps<TClient>): import("react/jsx-runtime").JSX.Element;
23
+ export default function email_verification_layout<TClient>({ image_src, image_alt, image_background_color, field_overrides, labels, button_colors, success_labels, error_labels, redirect_delay, login_path, sign_in_label, data_client, already_logged_in_message, showLogoutButton, showReturnHomeButton, returnHomeButtonLabel, returnHomePath, }: EmailVerificationLayoutProps<TClient>): import("react/jsx-runtime").JSX.Element;
23
24
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/email_verification/index.tsx"],"names":[],"mappings":"AAWA,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EAC1B,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAOL,KAAK,8BAA8B,EACnC,KAAK,4BAA4B,EAClC,MAAM,0CAA0C,CAAC;AAKlD,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAK1E,MAAM,MAAM,4BAA4B,CAAC,OAAO,GAAG,OAAO,IAAI;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,eAAe,CAAC,EAAE,uBAAuB,CAAC;IAC1C,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,cAAc,CAAC,EAAE,OAAO,CAAC,8BAA8B,CAAC,CAAC;IACzD,YAAY,CAAC,EAAE,OAAO,CAAC,4BAA4B,CAAC,CAAC;IACrD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;CACxC,CAAC;AASF,MAAM,CAAC,OAAO,UAAU,yBAAyB,CAAC,OAAO,EAAE,EACzD,SAAS,EACT,SAAS,EACT,sBAAkC,EAClC,eAAe,EACf,MAAM,EACN,aAAa,EACb,cAAc,EACd,YAAY,EACZ,cAAkB,EAClB,UAA+B,EAC/B,WAAW,EACX,yBAAyB,EACzB,gBAAuB,EACvB,oBAA4B,EAC5B,qBAAqC,EACrC,cAAoB,GACrB,EAAE,4BAA4B,CAAC,OAAO,CAAC,2CAyNvC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/email_verification/index.tsx"],"names":[],"mappings":"AAWA,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EAC1B,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAOL,KAAK,8BAA8B,EACnC,KAAK,4BAA4B,EAClC,MAAM,0CAA0C,CAAC;AAKlD,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAM1E,MAAM,MAAM,4BAA4B,CAAC,OAAO,GAAG,OAAO,IAAI;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,eAAe,CAAC,EAAE,uBAAuB,CAAC;IAC1C,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,cAAc,CAAC,EAAE,OAAO,CAAC,8BAA8B,CAAC,CAAC;IACzD,YAAY,CAAC,EAAE,OAAO,CAAC,4BAA4B,CAAC,CAAC;IACrD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;CACxC,CAAC;AASF,MAAM,CAAC,OAAO,UAAU,yBAAyB,CAAC,OAAO,EAAE,EACzD,SAAS,EACT,SAAS,EACT,sBAAkC,EAClC,eAAe,EACf,MAAM,EACN,aAAa,EACb,cAAc,EACd,YAAY,EACZ,cAAkB,EAClB,UAA+B,EAC/B,aAAyB,EACzB,WAAW,EACX,yBAAyB,EACzB,gBAAuB,EACvB,oBAA4B,EAC5B,qBAAqC,EACrC,cAAoB,GACrB,EAAE,4BAA4B,CAAC,OAAO,CAAC,2CAkOvC"}
@@ -13,11 +13,12 @@ import { EMAIL_VERIFICATION_FIELD_IDS, createEmailVerificationFieldDefinitions,
13
13
  import { use_email_verification, } from "./hooks/use_email_verification";
14
14
  import { CheckCircle, XCircle, Loader2 } from "lucide-react";
15
15
  import { AlreadyLoggedInGuard } from "../shared/components/already_logged_in_guard";
16
+ import Link from "next/link";
16
17
  const ORDERED_FIELDS = [
17
18
  EMAIL_VERIFICATION_FIELD_IDS.EMAIL,
18
19
  ];
19
20
  // section: component
20
- export default function email_verification_layout({ image_src, image_alt, image_background_color = "#f1f5f9", field_overrides, labels, button_colors, success_labels, error_labels, redirect_delay = 5, login_path = "/hazo_auth/login", data_client, already_logged_in_message, showLogoutButton = true, showReturnHomeButton = false, returnHomeButtonLabel = "Return home", returnHomePath = "/", }) {
21
+ export default function email_verification_layout({ image_src, image_alt, image_background_color = "#f1f5f9", field_overrides, labels, button_colors, success_labels, error_labels, redirect_delay = 5, login_path = "/hazo_auth/login", sign_in_label = "Sign in", data_client, already_logged_in_message, showLogoutButton = true, showReturnHomeButton = false, returnHomeButtonLabel = "Return home", returnHomePath = "/", }) {
21
22
  const fieldDefinitions = createEmailVerificationFieldDefinitions(field_overrides);
22
23
  const resolvedLabels = resolveEmailVerificationLabels(labels);
23
24
  const resolvedButtonPalette = resolveEmailVerificationButtonPalette(button_colors);
@@ -57,5 +58,5 @@ export default function email_verification_layout({ image_src, image_alt, image_
57
58
  }, children: resolvedSuccessLabels.goToLoginButton }) })] }) }) }));
58
59
  }
59
60
  // Error state with resend form
60
- return (_jsx(AlreadyLoggedInGuard, { image_src: image_src, image_alt: image_alt, image_background_color: image_background_color, message: already_logged_in_message, showLogoutButton: showLogoutButton, showReturnHomeButton: showReturnHomeButton, returnHomeButtonLabel: returnHomeButtonLabel, returnHomePath: returnHomePath, requireEmailVerified: false, children: _jsx(TwoColumnAuthLayout, { imageSrc: image_src, imageAlt: image_alt, imageBackgroundColor: image_background_color, formContent: _jsxs(_Fragment, { children: [_jsxs("div", { className: "cls_email_verification_error_header flex flex-col items-center gap-4 text-center", children: [_jsx(XCircle, { className: "h-12 w-12 text-red-600", "aria-hidden": "true" }), _jsxs("div", { className: "cls_email_verification_error_text", children: [_jsx("h1", { className: "cls_email_verification_error_heading text-2xl font-semibold text-slate-900", children: resolvedErrorLabels.heading }), _jsx("p", { className: "cls_email_verification_error_message mt-2 text-sm text-slate-600", children: verification.errorMessage || resolvedErrorLabels.message })] })] }), _jsxs("div", { className: "cls_email_verification_resend_form", children: [_jsx(FormHeader, { heading: resolvedErrorLabels.resendFormHeading, subHeading: "Enter your email address to receive a new verification link." }), _jsxs("form", { className: "cls_email_verification_layout_form_fields flex flex-col gap-5", onSubmit: verification.handleResendSubmit, "aria-label": "Resend verification email form", children: [renderFields(verification), _jsx(FormActionButtons, { submitLabel: resolvedLabels.submitButton, cancelLabel: resolvedLabels.cancelButton, buttonPalette: resolvedButtonPalette, isSubmitDisabled: verification.isSubmitDisabled, onCancel: verification.handleCancel, submitAriaLabel: "Submit resend verification email form", cancelAriaLabel: "Cancel resend verification email form" }), verification.isSubmitting && (_jsx("div", { className: "cls_email_verification_submitting_indicator text-sm text-slate-600 text-center", children: "Sending verification email..." }))] })] })] }) }) }));
61
+ return (_jsx(AlreadyLoggedInGuard, { image_src: image_src, image_alt: image_alt, image_background_color: image_background_color, message: already_logged_in_message, showLogoutButton: showLogoutButton, showReturnHomeButton: showReturnHomeButton, returnHomeButtonLabel: returnHomeButtonLabel, returnHomePath: returnHomePath, requireEmailVerified: false, children: _jsx(TwoColumnAuthLayout, { imageSrc: image_src, imageAlt: image_alt, imageBackgroundColor: image_background_color, formContent: _jsxs(_Fragment, { children: [_jsxs("div", { className: "cls_email_verification_error_header flex flex-col items-center gap-4 text-center", children: [_jsx(XCircle, { className: "h-12 w-12 text-red-600", "aria-hidden": "true" }), _jsxs("div", { className: "cls_email_verification_error_text", children: [_jsx("h1", { className: "cls_email_verification_error_heading text-2xl font-semibold text-slate-900", children: resolvedErrorLabels.heading }), _jsx("p", { className: "cls_email_verification_error_message mt-2 text-sm text-slate-600", children: verification.errorMessage || resolvedErrorLabels.message })] })] }), _jsxs("div", { className: "cls_email_verification_resend_form", children: [_jsx(FormHeader, { heading: resolvedErrorLabels.resendFormHeading, subHeading: "Enter your email address to receive a new verification link." }), _jsxs("form", { className: "cls_email_verification_layout_form_fields flex flex-col gap-5", onSubmit: verification.handleResendSubmit, "aria-label": "Resend verification email form", children: [renderFields(verification), _jsx(FormActionButtons, { submitLabel: resolvedLabels.submitButton, cancelLabel: resolvedLabels.cancelButton, buttonPalette: resolvedButtonPalette, isSubmitDisabled: verification.isSubmitDisabled, onCancel: verification.handleCancel, submitAriaLabel: "Submit resend verification email form", cancelAriaLabel: "Cancel resend verification email form" }), verification.isSubmitting && (_jsx("div", { className: "cls_email_verification_submitting_indicator text-sm text-slate-600 text-center", children: "Sending verification email..." }))] }), _jsxs("div", { className: "cls_email_verification_sign_in_link mt-4 text-center text-sm text-slate-600", children: ["Already verified?", " ", _jsx(Link, { href: login_path, className: "font-medium text-slate-900 hover:underline", children: sign_in_label })] })] })] }) }) }));
61
62
  }
@@ -8,11 +8,13 @@ export type ForgotPasswordLayoutProps<TClient = unknown> = {
8
8
  labels?: LayoutLabelOverrides;
9
9
  button_colors?: ButtonPaletteOverrides;
10
10
  data_client: LayoutDataClient<TClient>;
11
+ sign_in_path?: string;
12
+ sign_in_label?: string;
11
13
  alreadyLoggedInMessage?: string;
12
14
  showLogoutButton?: boolean;
13
15
  showReturnHomeButton?: boolean;
14
16
  returnHomeButtonLabel?: string;
15
17
  returnHomePath?: string;
16
18
  };
17
- export default function forgot_password_layout<TClient>({ image_src, image_alt, image_background_color, field_overrides, labels, button_colors, data_client, alreadyLoggedInMessage, showLogoutButton, showReturnHomeButton, returnHomeButtonLabel, returnHomePath, }: ForgotPasswordLayoutProps<TClient>): import("react/jsx-runtime").JSX.Element;
19
+ export default function forgot_password_layout<TClient>({ image_src, image_alt, image_background_color, field_overrides, labels, button_colors, data_client, sign_in_path, sign_in_label, alreadyLoggedInMessage, showLogoutButton, showReturnHomeButton, returnHomeButtonLabel, returnHomePath, }: ForgotPasswordLayoutProps<TClient>): import("react/jsx-runtime").JSX.Element;
18
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/forgot_password/index.tsx"],"names":[],"mappings":"AAWA,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EAC1B,MAAM,uCAAuC,CAAC;AAW/C,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAG1E,MAAM,MAAM,yBAAyB,CAAC,OAAO,GAAG,OAAO,IAAI;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,eAAe,CAAC,EAAE,uBAAuB,CAAC;IAC1C,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,WAAW,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACvC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AASF,MAAM,CAAC,OAAO,UAAU,sBAAsB,CAAC,OAAO,EAAE,EACtD,SAAS,EACT,SAAS,EACT,sBAAkC,EAClC,eAAe,EACf,MAAM,EACN,aAAa,EACb,WAAW,EACX,sBAAoD,EACpD,gBAAuB,EACvB,oBAA4B,EAC5B,qBAAqC,EACrC,cAAoB,GACrB,EAAE,yBAAyB,CAAC,OAAO,CAAC,2CAsGpC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/forgot_password/index.tsx"],"names":[],"mappings":"AAWA,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EAC1B,MAAM,uCAAuC,CAAC;AAW/C,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAI1E,MAAM,MAAM,yBAAyB,CAAC,OAAO,GAAG,OAAO,IAAI;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,eAAe,CAAC,EAAE,uBAAuB,CAAC;IAC1C,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,WAAW,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACvC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AASF,MAAM,CAAC,OAAO,UAAU,sBAAsB,CAAC,OAAO,EAAE,EACtD,SAAS,EACT,SAAS,EACT,sBAAkC,EAClC,eAAe,EACf,MAAM,EACN,aAAa,EACb,WAAW,EACX,YAAiC,EACjC,aAAyB,EACzB,sBAAoD,EACpD,gBAAuB,EACvB,oBAA4B,EAC5B,qBAAqC,EACrC,cAAoB,GACrB,EAAE,yBAAyB,CAAC,OAAO,CAAC,2CA+GpC"}
@@ -11,11 +11,12 @@ import { TwoColumnAuthLayout } from "../shared/components/two_column_auth_layout
11
11
  import { AlreadyLoggedInGuard } from "../shared/components/already_logged_in_guard";
12
12
  import { FORGOT_PASSWORD_FIELD_IDS, createForgotPasswordFieldDefinitions, resolveForgotPasswordButtonPalette, resolveForgotPasswordLabels, } from "./config/forgot_password_field_config";
13
13
  import { use_forgot_password_form, } from "./hooks/use_forgot_password_form";
14
+ import Link from "next/link";
14
15
  const ORDERED_FIELDS = [
15
16
  FORGOT_PASSWORD_FIELD_IDS.EMAIL,
16
17
  ];
17
18
  // section: component
18
- export default function forgot_password_layout({ image_src, image_alt, image_background_color = "#f1f5f9", field_overrides, labels, button_colors, data_client, alreadyLoggedInMessage = "You are already logged in", showLogoutButton = true, showReturnHomeButton = false, returnHomeButtonLabel = "Return home", returnHomePath = "/", }) {
19
+ export default function forgot_password_layout({ image_src, image_alt, image_background_color = "#f1f5f9", field_overrides, labels, button_colors, data_client, sign_in_path = "/hazo_auth/login", sign_in_label = "Sign in", alreadyLoggedInMessage = "You are already logged in", showLogoutButton = true, showReturnHomeButton = false, returnHomeButtonLabel = "Return home", returnHomePath = "/", }) {
19
20
  const fieldDefinitions = createForgotPasswordFieldDefinitions(field_overrides);
20
21
  const resolvedLabels = resolveForgotPasswordLabels(labels);
21
22
  const resolvedButtonPalette = resolveForgotPasswordButtonPalette(button_colors);
@@ -39,5 +40,5 @@ export default function forgot_password_layout({ image_src, image_alt, image_bac
39
40
  return (_jsx(FormFieldWrapper, { fieldId: fieldDefinition.id, label: fieldDefinition.label, input: inputElement, errorMessage: shouldShowError }, fieldId));
40
41
  });
41
42
  };
42
- return (_jsx(AlreadyLoggedInGuard, { image_src: image_src, image_alt: image_alt, image_background_color: image_background_color, message: alreadyLoggedInMessage, showLogoutButton: showLogoutButton, showReturnHomeButton: showReturnHomeButton, returnHomeButtonLabel: returnHomeButtonLabel, returnHomePath: returnHomePath, children: _jsx(TwoColumnAuthLayout, { imageSrc: image_src, imageAlt: image_alt, imageBackgroundColor: image_background_color, formContent: _jsxs(_Fragment, { children: [_jsx(FormHeader, { heading: resolvedLabels.heading, subHeading: resolvedLabels.subHeading }), _jsxs("form", { className: "cls_forgot_password_layout_form_fields flex flex-col gap-5", onSubmit: form.handleSubmit, "aria-label": "Forgot password form", children: [renderFields(form), _jsx(FormActionButtons, { submitLabel: resolvedLabels.submitButton, cancelLabel: resolvedLabels.cancelButton, buttonPalette: resolvedButtonPalette, isSubmitDisabled: form.isSubmitDisabled, onCancel: form.handleCancel, submitAriaLabel: "Submit forgot password form", cancelAriaLabel: "Cancel forgot password form" }), form.isSubmitting && (_jsx("div", { className: "cls_forgot_password_submitting_indicator text-sm text-slate-600 text-center", children: "Sending reset link..." }))] })] }) }) }));
43
+ return (_jsx(AlreadyLoggedInGuard, { image_src: image_src, image_alt: image_alt, image_background_color: image_background_color, message: alreadyLoggedInMessage, showLogoutButton: showLogoutButton, showReturnHomeButton: showReturnHomeButton, returnHomeButtonLabel: returnHomeButtonLabel, returnHomePath: returnHomePath, children: _jsx(TwoColumnAuthLayout, { imageSrc: image_src, imageAlt: image_alt, imageBackgroundColor: image_background_color, formContent: _jsxs(_Fragment, { children: [_jsx(FormHeader, { heading: resolvedLabels.heading, subHeading: resolvedLabels.subHeading }), _jsxs("form", { className: "cls_forgot_password_layout_form_fields flex flex-col gap-5", onSubmit: form.handleSubmit, "aria-label": "Forgot password form", children: [renderFields(form), _jsx(FormActionButtons, { submitLabel: resolvedLabels.submitButton, cancelLabel: resolvedLabels.cancelButton, buttonPalette: resolvedButtonPalette, isSubmitDisabled: form.isSubmitDisabled, onCancel: form.handleCancel, submitAriaLabel: "Submit forgot password form", cancelAriaLabel: "Cancel forgot password form" }), form.isSubmitting && (_jsx("div", { className: "cls_forgot_password_submitting_indicator text-sm text-slate-600 text-center", children: "Sending reset link..." }))] }), _jsxs("div", { className: "cls_forgot_password_sign_in_link mt-4 text-center text-sm text-slate-600", children: ["Remember your password?", " ", _jsx(Link, { href: sign_in_path, className: "font-medium text-slate-900 hover:underline", children: sign_in_label })] })] }) }) }));
43
44
  }
@@ -69,5 +69,5 @@ export function EditableField({ label, value, type = "text", placeholder, onSave
69
69
  handleCancel();
70
70
  }
71
71
  };
72
- return (_jsxs("div", { className: "cls_editable_field flex flex-col gap-2", children: [_jsx(Label, { htmlFor: `editable-field-${label}`, className: "cls_editable_field_label text-sm font-medium text-slate-700", children: label }), _jsx("div", { className: "cls_editable_field_input_container flex items-center gap-2", children: isEditing ? (_jsxs(_Fragment, { children: [_jsx(Input, { id: `editable-field-${label}`, type: type, value: editValue, onChange: (e) => setEditValue(e.target.value), onKeyDown: handleKeyDown, placeholder: placeholder, disabled: isSaving, "aria-label": ariaLabel || label, className: "cls_editable_field_input flex-1" }), _jsx(Button, { type: "button", onClick: handleSave, disabled: isSaving, variant: "ghost", size: "icon", className: "cls_editable_field_save_button text-green-600 hover:text-green-700 hover:bg-green-50", "aria-label": "Save changes", children: _jsx(CheckCircle2, { className: "h-5 w-5", "aria-hidden": "true" }) }), _jsx(Button, { type: "button", onClick: handleCancel, disabled: isSaving, variant: "ghost", size: "icon", className: "cls_editable_field_cancel_button text-red-600 hover:text-red-700 hover:bg-red-50", "aria-label": "Cancel editing", children: _jsx(XCircle, { className: "h-5 w-5", "aria-hidden": "true" }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Input, { id: `editable-field-${label}`, type: type, value: value || "", readOnly: true, disabled: true, placeholder: value ? undefined : placeholder || "Not set", "aria-label": ariaLabel || label, className: "cls_editable_field_display flex-1 bg-slate-50 cursor-not-allowed" }), !disabled && (_jsx(Button, { type: "button", onClick: handleEdit, variant: "ghost", size: "icon", className: "cls_editable_field_edit_button text-slate-600 hover:text-slate-700 hover:bg-slate-50", "aria-label": `Edit ${label}`, children: _jsx(Pencil, { className: "h-5 w-5", "aria-hidden": "true" }) }))] })) }), error && (_jsx("p", { className: "cls_editable_field_error text-sm text-red-600", role: "alert", children: error }))] }));
72
+ return (_jsxs("div", { className: "cls_editable_field flex flex-col gap-2", children: [_jsx(Label, { htmlFor: `editable-field-${label}`, className: "cls_editable_field_label text-sm font-medium text-[var(--hazo-text-secondary)]", children: label }), _jsx("div", { className: "cls_editable_field_input_container flex items-center gap-2", children: isEditing ? (_jsxs(_Fragment, { children: [_jsx(Input, { id: `editable-field-${label}`, type: type, value: editValue, onChange: (e) => setEditValue(e.target.value), onKeyDown: handleKeyDown, placeholder: placeholder, disabled: isSaving, "aria-label": ariaLabel || label, className: "cls_editable_field_input flex-1" }), _jsx(Button, { type: "button", onClick: handleSave, disabled: isSaving, variant: "ghost", size: "icon", className: "cls_editable_field_save_button text-green-600 hover:text-green-700 hover:bg-green-50", "aria-label": "Save changes", children: _jsx(CheckCircle2, { className: "h-5 w-5", "aria-hidden": "true" }) }), _jsx(Button, { type: "button", onClick: handleCancel, disabled: isSaving, variant: "ghost", size: "icon", className: "cls_editable_field_cancel_button text-red-600 hover:text-red-700 hover:bg-red-50", "aria-label": "Cancel editing", children: _jsx(XCircle, { className: "h-5 w-5", "aria-hidden": "true" }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Input, { id: `editable-field-${label}`, type: type, value: value || "", readOnly: true, disabled: true, placeholder: value ? undefined : placeholder || "Not set", "aria-label": ariaLabel || label, className: "cls_editable_field_display flex-1 bg-[var(--hazo-bg-subtle)] cursor-not-allowed" }), !disabled && (_jsx(Button, { type: "button", onClick: handleEdit, variant: "ghost", size: "icon", className: "cls_editable_field_edit_button text-[var(--hazo-text-muted)] hover:text-[var(--hazo-text-secondary)] hover:bg-[var(--hazo-bg-subtle)]", "aria-label": `Edit ${label}`, children: _jsx(Pencil, { className: "h-5 w-5", "aria-hidden": "true" }) }))] })) }), error && (_jsx("p", { className: "cls_editable_field_error text-sm text-red-600", role: "alert", children: error }))] }));
73
73
  }
@@ -123,7 +123,7 @@ export function PasswordChangeDialog({ open, onOpenChange, onSave, passwordRequi
123
123
  if (errors.newPassword) {
124
124
  setErrors(Object.assign(Object.assign({}, errors), { newPassword: undefined }));
125
125
  }
126
- }, onToggleVisibility: () => setNewPasswordVisible(!newPasswordVisible), errorMessage: errors.newPassword }) }), passwordRequirementsList.length > 0 && (_jsxs("div", { className: "cls_password_change_dialog_requirements text-xs text-slate-600", children: [_jsx("p", { className: "cls_password_change_dialog_requirements_label font-medium mb-1", children: "Password requirements:" }), _jsx("ul", { className: "cls_password_change_dialog_requirements_list list-disc list-inside space-y-0.5", children: passwordRequirementsList.map((req, index) => (_jsx("li", { children: req }, index))) })] })), _jsx(FormFieldWrapper, { fieldId: "confirm-password", label: confirmPasswordLabel, input: _jsx(PasswordField, { inputId: "confirm-password", ariaLabel: confirmPasswordLabel, value: confirmPassword, placeholder: "Confirm your new password", autoComplete: "new-password", isVisible: confirmPasswordVisible, onChange: (value) => {
126
+ }, onToggleVisibility: () => setNewPasswordVisible(!newPasswordVisible), errorMessage: errors.newPassword }) }), passwordRequirementsList.length > 0 && (_jsxs("div", { className: "cls_password_change_dialog_requirements text-xs text-[var(--hazo-text-muted)]", children: [_jsx("p", { className: "cls_password_change_dialog_requirements_label font-medium mb-1", children: "Password requirements:" }), _jsx("ul", { className: "cls_password_change_dialog_requirements_list list-disc list-inside space-y-0.5", children: passwordRequirementsList.map((req, index) => (_jsx("li", { children: req }, index))) })] })), _jsx(FormFieldWrapper, { fieldId: "confirm-password", label: confirmPasswordLabel, input: _jsx(PasswordField, { inputId: "confirm-password", ariaLabel: confirmPasswordLabel, value: confirmPassword, placeholder: "Confirm your new password", autoComplete: "new-password", isVisible: confirmPasswordVisible, onChange: (value) => {
127
127
  setConfirmPassword(value);
128
128
  if (errors.confirmPassword) {
129
129
  setErrors(Object.assign(Object.assign({}, errors), { confirmPassword: undefined }));
@@ -29,5 +29,5 @@ export function ProfilePictureDisplay({ profilePictureUrl, name, email, onEdit,
29
29
  return "?";
30
30
  };
31
31
  const initials = getInitials();
32
- return (_jsx("div", { className: "cls_profile_picture_display", children: _jsxs(Avatar, { className: "cls_profile_picture_display_avatar h-32 w-32", children: [_jsx(AvatarImage, { src: profilePictureUrl, alt: name ? `Profile picture of ${name}` : "Profile picture", className: "cls_profile_picture_display_image" }), _jsx(AvatarFallback, { className: "cls_profile_picture_display_fallback bg-slate-200 text-slate-600 text-3xl", children: initials })] }) }));
32
+ return (_jsx("div", { className: "cls_profile_picture_display", children: _jsxs(Avatar, { className: "cls_profile_picture_display_avatar h-32 w-32", children: [_jsx(AvatarImage, { src: profilePictureUrl, alt: name ? `Profile picture of ${name}` : "Profile picture", className: "cls_profile_picture_display_image" }), _jsx(AvatarFallback, { className: "cls_profile_picture_display_fallback bg-[var(--hazo-bg-emphasis)] text-[var(--hazo-text-muted)] text-3xl", children: initials })] }) }));
33
33
  }
@@ -44,5 +44,5 @@ export function ProfilePictureGravatarTab({ email, useGravatar, onUseGravatarCha
44
44
  }
45
45
  return "?";
46
46
  };
47
- return (_jsxs("div", { className: "cls_profile_picture_gravatar_tab flex flex-col gap-4", children: [_jsxs("div", { className: "cls_profile_picture_gravatar_tab_switch flex items-center gap-3", children: [_jsx(Switch, { id: "use-gravatar", checked: useGravatar, onCheckedChange: onUseGravatarChange, disabled: disabled, className: "cls_profile_picture_gravatar_tab_switch_input", "aria-label": "Use Gravatar photo" }), _jsx(Label, { htmlFor: "use-gravatar", className: "cls_profile_picture_gravatar_tab_switch_label text-sm font-medium text-slate-700 cursor-pointer", children: "Use Gravatar photo" })] }), _jsx("div", { className: "cls_profile_picture_gravatar_tab_preview flex flex-col items-center gap-4 p-6 border border-slate-200 rounded-lg bg-slate-50", children: gravatarExists === true ? (_jsxs(_Fragment, { children: [_jsxs(Avatar, { className: "cls_profile_picture_gravatar_tab_avatar h-32 w-32", children: [_jsx(AvatarImage, { src: gravatarUrlState, alt: "Gravatar profile picture", className: "cls_profile_picture_gravatar_tab_avatar_image" }), _jsx(AvatarFallback, { className: "cls_profile_picture_gravatar_tab_avatar_fallback bg-slate-200 text-slate-600 text-3xl", children: getInitials() })] }), _jsx("p", { className: "cls_profile_picture_gravatar_tab_success_text text-sm text-slate-600 text-center", children: "Your Gravatar is available and will be used as your profile picture." })] })) : gravatarExists === false ? (_jsx(_Fragment, { children: _jsxs("div", { className: "cls_profile_picture_gravatar_tab_no_gravatar flex flex-col items-center gap-4", children: [_jsx("div", { className: "cls_profile_picture_gravatar_tab_no_gravatar_icon flex items-center justify-center w-16 h-16 rounded-full bg-slate-100", children: _jsx(Info, { className: "h-8 w-8 text-slate-400", "aria-hidden": "true" }) }), _jsxs("div", { className: "cls_profile_picture_gravatar_tab_no_gravatar_content flex flex-col gap-2 text-center", children: [_jsx("p", { className: "cls_profile_picture_gravatar_tab_no_gravatar_title text-sm font-medium text-slate-900", children: "No Gravatar found" }), _jsxs("p", { className: "cls_profile_picture_gravatar_tab_no_gravatar_message text-sm text-slate-600", children: [gravatarSetupMessage, " ", _jsx("span", { className: "font-semibold", children: email }), ":"] }), _jsxs("ol", { className: "cls_profile_picture_gravatar_tab_no_gravatar_steps text-sm text-slate-600 list-decimal list-inside space-y-1 mt-2", children: [_jsxs("li", { children: ["Visit ", _jsx("a", { href: "https://gravatar.com", target: "_blank", rel: "noopener noreferrer", className: "text-blue-600 hover:text-blue-700 underline", children: "gravatar.com" })] }), _jsxs("li", { children: ["Sign up or log in with your email: ", _jsx("span", { className: "font-mono text-xs", children: email })] }), _jsx("li", { children: "Upload a profile picture" }), _jsx("li", { children: "Return here and refresh to see your Gravatar" })] })] })] }) })) : (_jsx(_Fragment, { children: _jsx("div", { className: "cls_profile_picture_gravatar_tab_loading flex items-center justify-center", children: _jsx("p", { className: "cls_profile_picture_gravatar_tab_loading_text text-sm text-slate-600", children: "Checking Gravatar..." }) }) })) })] }));
47
+ return (_jsxs("div", { className: "cls_profile_picture_gravatar_tab flex flex-col gap-4", children: [_jsxs("div", { className: "cls_profile_picture_gravatar_tab_switch flex items-center gap-3", children: [_jsx(Switch, { id: "use-gravatar", checked: useGravatar, onCheckedChange: onUseGravatarChange, disabled: disabled, className: "cls_profile_picture_gravatar_tab_switch_input", "aria-label": "Use Gravatar photo" }), _jsx(Label, { htmlFor: "use-gravatar", className: "cls_profile_picture_gravatar_tab_switch_label text-sm font-medium text-[var(--hazo-text-secondary)] cursor-pointer", children: "Use Gravatar photo" })] }), _jsx("div", { className: "cls_profile_picture_gravatar_tab_preview flex flex-col items-center gap-4 p-6 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)]", children: gravatarExists === true ? (_jsxs(_Fragment, { children: [_jsxs(Avatar, { className: "cls_profile_picture_gravatar_tab_avatar h-32 w-32", children: [_jsx(AvatarImage, { src: gravatarUrlState, alt: "Gravatar profile picture", className: "cls_profile_picture_gravatar_tab_avatar_image" }), _jsx(AvatarFallback, { className: "cls_profile_picture_gravatar_tab_avatar_fallback bg-[var(--hazo-bg-emphasis)] text-[var(--hazo-text-muted)] text-3xl", children: getInitials() })] }), _jsx("p", { className: "cls_profile_picture_gravatar_tab_success_text text-sm text-[var(--hazo-text-muted)] text-center", children: "Your Gravatar is available and will be used as your profile picture." })] })) : gravatarExists === false ? (_jsx(_Fragment, { children: _jsxs("div", { className: "cls_profile_picture_gravatar_tab_no_gravatar flex flex-col items-center gap-4", children: [_jsx("div", { className: "cls_profile_picture_gravatar_tab_no_gravatar_icon flex items-center justify-center w-16 h-16 rounded-full bg-[var(--hazo-bg-muted)]", children: _jsx(Info, { className: "h-8 w-8 text-[var(--hazo-text-subtle)]", "aria-hidden": "true" }) }), _jsxs("div", { className: "cls_profile_picture_gravatar_tab_no_gravatar_content flex flex-col gap-2 text-center", children: [_jsx("p", { className: "cls_profile_picture_gravatar_tab_no_gravatar_title text-sm font-medium text-[var(--hazo-text-primary)]", children: "No Gravatar found" }), _jsxs("p", { className: "cls_profile_picture_gravatar_tab_no_gravatar_message text-sm text-[var(--hazo-text-muted)]", children: [gravatarSetupMessage, " ", _jsx("span", { className: "font-semibold", children: email }), ":"] }), _jsxs("ol", { className: "cls_profile_picture_gravatar_tab_no_gravatar_steps text-sm text-[var(--hazo-text-muted)] list-decimal list-inside space-y-1 mt-2", children: [_jsxs("li", { children: ["Visit ", _jsx("a", { href: "https://gravatar.com", target: "_blank", rel: "noopener noreferrer", className: "text-blue-600 hover:text-blue-700 underline", children: "gravatar.com" })] }), _jsxs("li", { children: ["Sign up or log in with your email: ", _jsx("span", { className: "font-mono text-xs", children: email })] }), _jsx("li", { children: "Upload a profile picture" }), _jsx("li", { children: "Return here and refresh to see your Gravatar" })] })] })] }) })) : (_jsx(_Fragment, { children: _jsx("div", { className: "cls_profile_picture_gravatar_tab_loading flex items-center justify-center", children: _jsx("p", { className: "cls_profile_picture_gravatar_tab_loading_text text-sm text-[var(--hazo-text-muted)]", children: "Checking Gravatar..." }) }) })) })] }));
48
48
  }
@@ -124,15 +124,15 @@ export function ProfilePictureLibraryTab({ useLibrary, onUseLibraryChange, onPho
124
124
  };
125
125
  return columnMap[columns] || "grid-cols-4";
126
126
  };
127
- return (_jsxs("div", { className: "cls_profile_picture_library_tab flex flex-col gap-4", children: [_jsxs("div", { className: "cls_profile_picture_library_tab_switch flex items-center gap-3", children: [_jsx(Switch, { id: "use-library", checked: useLibrary, onCheckedChange: onUseLibraryChange, disabled: disabled, className: "cls_profile_picture_library_tab_switch_input", "aria-label": "Use library photo" }), _jsxs(Label, { htmlFor: "use-library", className: "cls_profile_picture_library_tab_switch_label text-sm font-medium text-slate-700 cursor-pointer", children: ["Use library photo", _jsx(HazoUITooltip, { message: libraryTooltipMessage, iconSize: tooltipIconSizeSmall, side: "top" })] })] }), _jsxs("div", { className: "cls_profile_picture_library_tab_content grid grid-cols-12 gap-4", children: [_jsxs("div", { className: "cls_profile_picture_library_tab_categories_container flex flex-col gap-2 col-span-3", children: [_jsx(Label, { className: "cls_profile_picture_library_tab_categories_label text-sm font-medium text-slate-700", children: "Categories" }), loadingCategories ? (_jsx("div", { className: "cls_profile_picture_library_tab_loading flex items-center justify-center p-8 border border-slate-200 rounded-lg bg-slate-50 min-h-[400px]", children: _jsx(Loader2, { className: "h-6 w-6 text-slate-400 animate-spin", "aria-hidden": "true" }) })) : categories.length > 0 ? (_jsx(VerticalTabs, { value: selectedCategory || categories[0], onValueChange: setSelectedCategory, className: "cls_profile_picture_library_tab_vertical_tabs", children: _jsx(VerticalTabsList, { className: "cls_profile_picture_library_tab_vertical_tabs_list w-full", children: categories.map((category) => (_jsx(VerticalTabsTrigger, { value: category, className: "cls_profile_picture_library_tab_vertical_tabs_trigger w-full justify-start", children: category }, category))) }) })) : (_jsx("div", { className: "cls_profile_picture_library_tab_no_categories flex items-center justify-center p-8 border border-slate-200 rounded-lg bg-slate-50 min-h-[400px]", children: _jsx("p", { className: "cls_profile_picture_library_tab_no_categories_text text-sm text-slate-600", children: "No categories available" }) }))] }), _jsxs("div", { className: "cls_profile_picture_library_tab_photos_container flex flex-col gap-2 col-span-6", children: [_jsx(Label, { className: "cls_profile_picture_library_tab_photos_label text-sm font-medium text-slate-700", children: "Photos" }), loadingPhotos ? (_jsx("div", { className: "cls_profile_picture_library_tab_photos_loading flex items-center justify-center p-8 border border-slate-200 rounded-lg bg-slate-50 min-h-[400px]", children: _jsx(Loader2, { className: "h-6 w-6 text-slate-400 animate-spin", "aria-hidden": "true" }) })) : photos.length > 0 ? (_jsx("div", { className: `cls_profile_picture_library_tab_photos_grid grid ${getGridColumnsClass(libraryPhotoGridColumns)} gap-3 overflow-y-auto p-4 border border-slate-200 rounded-lg bg-slate-50 min-h-[400px] max-h-[400px]`, children: photos.map((photoUrl) => (_jsx("button", { type: "button", onClick: () => handlePhotoClick(photoUrl), className: `
127
+ return (_jsxs("div", { className: "cls_profile_picture_library_tab flex flex-col gap-4", children: [_jsxs("div", { className: "cls_profile_picture_library_tab_switch flex items-center gap-3", children: [_jsx(Switch, { id: "use-library", checked: useLibrary, onCheckedChange: onUseLibraryChange, disabled: disabled, className: "cls_profile_picture_library_tab_switch_input", "aria-label": "Use library photo" }), _jsxs(Label, { htmlFor: "use-library", className: "cls_profile_picture_library_tab_switch_label text-sm font-medium text-[var(--hazo-text-secondary)] cursor-pointer", children: ["Use library photo", _jsx(HazoUITooltip, { message: libraryTooltipMessage, iconSize: tooltipIconSizeSmall, side: "top" })] })] }), _jsxs("div", { className: "cls_profile_picture_library_tab_content grid grid-cols-12 gap-4", children: [_jsxs("div", { className: "cls_profile_picture_library_tab_categories_container flex flex-col gap-2 col-span-3", children: [_jsx(Label, { className: "cls_profile_picture_library_tab_categories_label text-sm font-medium text-[var(--hazo-text-secondary)]", children: "Categories" }), loadingCategories ? (_jsx("div", { className: "cls_profile_picture_library_tab_loading flex items-center justify-center p-8 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px]", children: _jsx(Loader2, { className: "h-6 w-6 text-[var(--hazo-text-subtle)] animate-spin", "aria-hidden": "true" }) })) : categories.length > 0 ? (_jsx(VerticalTabs, { value: selectedCategory || categories[0], onValueChange: setSelectedCategory, className: "cls_profile_picture_library_tab_vertical_tabs", children: _jsx(VerticalTabsList, { className: "cls_profile_picture_library_tab_vertical_tabs_list w-full", children: categories.map((category) => (_jsx(VerticalTabsTrigger, { value: category, className: "cls_profile_picture_library_tab_vertical_tabs_trigger w-full justify-start", children: category }, category))) }) })) : (_jsx("div", { className: "cls_profile_picture_library_tab_no_categories flex items-center justify-center p-8 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px]", children: _jsx("p", { className: "cls_profile_picture_library_tab_no_categories_text text-sm text-[var(--hazo-text-muted)]", children: "No categories available" }) }))] }), _jsxs("div", { className: "cls_profile_picture_library_tab_photos_container flex flex-col gap-2 col-span-6", children: [_jsx(Label, { className: "cls_profile_picture_library_tab_photos_label text-sm font-medium text-[var(--hazo-text-secondary)]", children: "Photos" }), loadingPhotos ? (_jsx("div", { className: "cls_profile_picture_library_tab_photos_loading flex items-center justify-center p-8 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px]", children: _jsx(Loader2, { className: "h-6 w-6 text-[var(--hazo-text-subtle)] animate-spin", "aria-hidden": "true" }) })) : photos.length > 0 ? (_jsx("div", { className: `cls_profile_picture_library_tab_photos_grid grid ${getGridColumnsClass(libraryPhotoGridColumns)} gap-3 overflow-y-auto p-4 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px] max-h-[400px]`, children: photos.map((photoUrl) => (_jsx("button", { type: "button", onClick: () => handlePhotoClick(photoUrl), className: `
128
128
  cls_profile_picture_library_tab_photo_thumbnail
129
129
  aspect-square rounded-lg overflow-hidden border-2 transition-colors cursor-pointer
130
- ${selectedPhoto === photoUrl ? "border-blue-500 ring-2 ring-blue-200" : "border-slate-200 hover:border-slate-300"}
130
+ ${selectedPhoto === photoUrl ? "border-blue-500 ring-2 ring-blue-200" : "border-[var(--hazo-border)] hover:border-[var(--hazo-border-emphasis)]"}
131
131
  `, "aria-label": `Select photo ${photoUrl.split('/').pop()}`, children: _jsx("img", { src: photoUrl, alt: `Library photo ${photoUrl.split('/').pop()}`, className: "cls_profile_picture_library_tab_photo_thumbnail_image w-full h-full object-cover", loading: "lazy", onError: (e) => {
132
132
  // Fallback if image fails to load
133
133
  const target = e.target;
134
134
  target.style.display = 'none';
135
- } }) }, photoUrl))) })) : (_jsx("div", { className: "cls_profile_picture_library_tab_no_photos flex items-center justify-center p-8 border border-slate-200 rounded-lg bg-slate-50 min-h-[400px]", children: _jsx("p", { className: "cls_profile_picture_library_tab_no_photos_text text-sm text-slate-600", children: "No photos in this category" }) }))] }), _jsxs("div", { className: "cls_profile_picture_library_tab_preview_container flex flex-col gap-2 col-span-3", children: [_jsx(Label, { className: "cls_profile_picture_library_tab_preview_label text-sm font-medium text-slate-700", children: "Preview" }), selectedPhoto ? (_jsxs("div", { className: "cls_profile_picture_library_tab_preview flex flex-col items-center gap-4 p-4 border border-slate-200 rounded-lg bg-slate-50 min-h-[400px] justify-center", children: [_jsx("div", { className: "cls_profile_picture_library_tab_preview_image_wrapper w-full flex items-center justify-center", children: _jsx("img", { src: selectedPhoto, alt: "Selected library photo preview", className: "cls_profile_picture_library_tab_preview_image max-w-full max-h-[350px] rounded-lg object-contain", onError: (e) => {
135
+ } }) }, photoUrl))) })) : (_jsx("div", { className: "cls_profile_picture_library_tab_no_photos flex items-center justify-center p-8 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px]", children: _jsx("p", { className: "cls_profile_picture_library_tab_no_photos_text text-sm text-[var(--hazo-text-muted)]", children: "No photos in this category" }) }))] }), _jsxs("div", { className: "cls_profile_picture_library_tab_preview_container flex flex-col gap-2 col-span-3", children: [_jsx(Label, { className: "cls_profile_picture_library_tab_preview_label text-sm font-medium text-[var(--hazo-text-secondary)]", children: "Preview" }), selectedPhoto ? (_jsxs("div", { className: "cls_profile_picture_library_tab_preview flex flex-col items-center gap-4 p-4 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px] justify-center", children: [_jsx("div", { className: "cls_profile_picture_library_tab_preview_image_wrapper w-full flex items-center justify-center", children: _jsx("img", { src: selectedPhoto, alt: "Selected library photo preview", className: "cls_profile_picture_library_tab_preview_image max-w-full max-h-[350px] rounded-lg object-contain", onError: (e) => {
136
136
  // Fallback if preview image fails to load
137
137
  const target = e.target;
138
138
  target.style.display = 'none';
@@ -140,5 +140,5 @@ export function ProfilePictureLibraryTab({ useLibrary, onUseLibraryChange, onPho
140
140
  if (wrapper) {
141
141
  wrapper.innerHTML = '<p class="text-sm text-red-500">Failed to load preview</p>';
142
142
  }
143
- } }) }), _jsx("p", { className: "cls_profile_picture_library_tab_preview_text text-sm text-slate-600 text-center", children: "Selected photo preview" })] })) : (_jsxs("div", { className: "cls_profile_picture_library_tab_preview_empty flex flex-col items-center justify-center gap-2 p-8 border border-slate-200 rounded-lg bg-slate-50 min-h-[400px]", children: [_jsx(Avatar, { className: "cls_profile_picture_library_tab_preview_empty_avatar h-32 w-32", children: _jsx(AvatarFallback, { className: "cls_profile_picture_library_tab_preview_empty_avatar_fallback bg-slate-200 text-slate-600 text-3xl", children: getInitials() }) }), _jsx("p", { className: "cls_profile_picture_library_tab_preview_empty_text text-sm text-slate-500 text-center", children: "Select a photo to see preview" })] }))] })] })] }));
143
+ } }) }), _jsx("p", { className: "cls_profile_picture_library_tab_preview_text text-sm text-[var(--hazo-text-muted)] text-center", children: "Selected photo preview" })] })) : (_jsxs("div", { className: "cls_profile_picture_library_tab_preview_empty flex flex-col items-center justify-center gap-2 p-8 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px]", children: [_jsx(Avatar, { className: "cls_profile_picture_library_tab_preview_empty_avatar h-32 w-32", children: _jsx(AvatarFallback, { className: "cls_profile_picture_library_tab_preview_empty_avatar_fallback bg-[var(--hazo-bg-emphasis)] text-[var(--hazo-text-muted)] text-3xl", children: getInitials() }) }), _jsx("p", { className: "cls_profile_picture_library_tab_preview_empty_text text-sm text-[var(--hazo-text-muted)] text-center", children: "Select a photo to see preview" })] }))] })] })] }));
144
144
  }
@@ -153,17 +153,17 @@ allowedImageMimeTypes = ["image/jpeg", "image/jpg", "image/png"], }) {
153
153
  const getInitials = () => {
154
154
  return "U";
155
155
  };
156
- return (_jsxs("div", { className: "cls_profile_picture_upload_tab flex flex-col gap-4", children: [_jsxs("div", { className: "cls_profile_picture_upload_tab_switch flex items-center gap-3", children: [_jsx(Switch, { id: "use-upload", checked: useUpload, onCheckedChange: onUseUploadChange, disabled: disabled || !uploadEnabled, className: "cls_profile_picture_upload_tab_switch_input", "aria-label": "Use uploaded photo" }), _jsx(Label, { htmlFor: "use-upload", className: "cls_profile_picture_upload_tab_switch_label text-sm font-medium text-slate-700 cursor-pointer", children: "Use uploaded photo" })] }), !uploadEnabled && (_jsxs("div", { className: "cls_profile_picture_upload_tab_disabled flex items-center gap-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg", children: [_jsx(Info, { className: "h-4 w-4 text-yellow-600", "aria-hidden": "true" }), _jsx("p", { className: "cls_profile_picture_upload_tab_disabled_text text-sm text-yellow-800", children: photoUploadDisabledMessage })] })), _jsxs("div", { className: "cls_profile_picture_upload_tab_content grid grid-cols-1 md:grid-cols-2 gap-6", children: [_jsxs("div", { className: "cls_profile_picture_upload_tab_dropzone_container flex flex-col gap-2", children: [_jsx(Label, { className: "cls_profile_picture_upload_tab_dropzone_label text-sm font-medium text-slate-700", children: "Upload Photo" }), _jsxs("div", { className: `
156
+ return (_jsxs("div", { className: "cls_profile_picture_upload_tab flex flex-col gap-4", children: [_jsxs("div", { className: "cls_profile_picture_upload_tab_switch flex items-center gap-3", children: [_jsx(Switch, { id: "use-upload", checked: useUpload, onCheckedChange: onUseUploadChange, disabled: disabled || !uploadEnabled, className: "cls_profile_picture_upload_tab_switch_input", "aria-label": "Use uploaded photo" }), _jsx(Label, { htmlFor: "use-upload", className: "cls_profile_picture_upload_tab_switch_label text-sm font-medium text-[var(--hazo-text-secondary)] cursor-pointer", children: "Use uploaded photo" })] }), !uploadEnabled && (_jsxs("div", { className: "cls_profile_picture_upload_tab_disabled flex items-center gap-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg", children: [_jsx(Info, { className: "h-4 w-4 text-yellow-600", "aria-hidden": "true" }), _jsx("p", { className: "cls_profile_picture_upload_tab_disabled_text text-sm text-yellow-800", children: photoUploadDisabledMessage })] })), _jsxs("div", { className: "cls_profile_picture_upload_tab_content grid grid-cols-1 md:grid-cols-2 gap-6", children: [_jsxs("div", { className: "cls_profile_picture_upload_tab_dropzone_container flex flex-col gap-2", children: [_jsx(Label, { className: "cls_profile_picture_upload_tab_dropzone_label text-sm font-medium text-[var(--hazo-text-secondary)]", children: "Upload Photo" }), _jsxs("div", { className: `
157
157
  cls_profile_picture_upload_tab_dropzone
158
158
  flex flex-col items-center justify-center
159
159
  border-2 border-dashed rounded-lg p-8
160
160
  transition-colors
161
- ${dragActive ? "border-blue-500 bg-blue-50" : "border-slate-300 bg-slate-50"}
162
- ${disabled || !uploadEnabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer hover:border-slate-400"}
161
+ ${dragActive ? "border-blue-500 bg-blue-50" : "border-[var(--hazo-border-emphasis)] bg-[var(--hazo-bg-subtle)]"}
162
+ ${disabled || !uploadEnabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer hover:border-[var(--hazo-border-emphasis)]"}
163
163
  `, onDragEnter: handleDrag, onDragLeave: handleDrag, onDragOver: handleDrag, onDrop: handleDrop, onClick: () => {
164
164
  var _a;
165
165
  if (!disabled && uploadEnabled) {
166
166
  (_a = document.getElementById("file-upload-input")) === null || _a === void 0 ? void 0 : _a.click();
167
167
  }
168
- }, children: [_jsx("input", { id: "file-upload-input", type: "file", accept: allowedImageMimeTypes.join(","), onChange: handleChange, disabled: disabled || !uploadEnabled, className: "hidden", "aria-label": "Upload profile picture" }), _jsx(Upload, { className: "h-8 w-8 text-slate-400 mb-2", "aria-hidden": "true" }), _jsx("p", { className: "cls_profile_picture_upload_tab_dropzone_text text-sm text-slate-600 text-center", children: "Drag and drop an image here, or click to select" }), _jsxs("p", { className: "cls_profile_picture_upload_tab_dropzone_hint text-xs text-slate-500 text-center mt-1", children: ["JPG or PNG, max ", Math.round(maxSize / 1024), "KB"] })] }), error && (_jsx("p", { className: "cls_profile_picture_upload_tab_error text-sm text-red-600", role: "alert", children: error }))] }), _jsxs("div", { className: "cls_profile_picture_upload_tab_preview_container flex flex-col gap-2", children: [_jsx(Label, { className: "cls_profile_picture_upload_tab_preview_label text-sm font-medium text-slate-700", children: isNewImage ? "Preview (new)" : "Preview (current)" }), _jsx("div", { className: "cls_profile_picture_upload_tab_preview_content flex flex-col items-center justify-center border border-slate-200 rounded-lg p-6 bg-slate-50 min-h-[200px]", children: compressing ? (_jsxs("div", { className: "cls_profile_picture_upload_tab_compressing flex flex-col items-center gap-2", children: [_jsx(Loader2, { className: "h-8 w-8 text-slate-400 animate-spin", "aria-hidden": "true" }), _jsx("p", { className: "cls_profile_picture_upload_tab_compressing_text text-sm text-slate-600", children: "Compressing image..." })] })) : uploading ? (_jsxs("div", { className: "cls_profile_picture_upload_tab_uploading flex flex-col items-center gap-2", children: [_jsx(Loader2, { className: "h-8 w-8 text-slate-400 animate-spin", "aria-hidden": "true" }), _jsx("p", { className: "cls_profile_picture_upload_tab_uploading_text text-sm text-slate-600", children: "Uploading..." })] })) : preview ? (_jsxs("div", { className: "cls_profile_picture_upload_tab_preview_image_container flex flex-col items-center gap-4", children: [_jsxs("div", { className: "cls_profile_picture_upload_tab_preview_image_wrapper relative", children: [_jsxs(Avatar, { className: "cls_profile_picture_upload_tab_preview_avatar h-32 w-32", children: [_jsx(AvatarImage, { src: preview, alt: "Uploaded profile picture preview", className: "cls_profile_picture_upload_tab_preview_avatar_image" }), _jsx(AvatarFallback, { className: "cls_profile_picture_upload_tab_preview_avatar_fallback bg-slate-200 text-slate-600 text-3xl", children: getInitials() })] }), _jsx(Button, { type: "button", onClick: handleRemove, variant: "ghost", size: "icon", className: "cls_profile_picture_upload_tab_preview_remove absolute -top-2 -right-2 rounded-full h-6 w-6 border border-slate-300 bg-white hover:bg-slate-50", "aria-label": "Remove preview", children: _jsx(X, { className: "h-4 w-4", "aria-hidden": "true" }) })] }), _jsx("p", { className: "cls_profile_picture_upload_tab_preview_success_text text-sm text-slate-600 text-center", children: "Preview of your uploaded photo" })] })) : (_jsxs("div", { className: "cls_profile_picture_upload_tab_preview_empty flex flex-col items-center gap-2", children: [_jsx(Avatar, { className: "cls_profile_picture_upload_tab_preview_empty_avatar h-32 w-32", children: _jsx(AvatarFallback, { className: "cls_profile_picture_upload_tab_preview_empty_avatar_fallback bg-slate-200 text-slate-600 text-3xl", children: getInitials() }) }), _jsx("p", { className: "cls_profile_picture_upload_tab_preview_empty_text text-sm text-slate-500 text-center", children: "Upload an image to see preview" })] })) })] })] })] }));
168
+ }, children: [_jsx("input", { id: "file-upload-input", type: "file", accept: allowedImageMimeTypes.join(","), onChange: handleChange, disabled: disabled || !uploadEnabled, className: "hidden", "aria-label": "Upload profile picture" }), _jsx(Upload, { className: "h-8 w-8 text-[var(--hazo-text-subtle)] mb-2", "aria-hidden": "true" }), _jsx("p", { className: "cls_profile_picture_upload_tab_dropzone_text text-sm text-[var(--hazo-text-muted)] text-center", children: "Drag and drop an image here, or click to select" }), _jsxs("p", { className: "cls_profile_picture_upload_tab_dropzone_hint text-xs text-[var(--hazo-text-muted)] text-center mt-1", children: ["JPG or PNG, max ", Math.round(maxSize / 1024), "KB"] })] }), error && (_jsx("p", { className: "cls_profile_picture_upload_tab_error text-sm text-red-600", role: "alert", children: error }))] }), _jsxs("div", { className: "cls_profile_picture_upload_tab_preview_container flex flex-col gap-2", children: [_jsx(Label, { className: "cls_profile_picture_upload_tab_preview_label text-sm font-medium text-[var(--hazo-text-secondary)]", children: isNewImage ? "Preview (new)" : "Preview (current)" }), _jsx("div", { className: "cls_profile_picture_upload_tab_preview_content flex flex-col items-center justify-center border border-[var(--hazo-border)] rounded-lg p-6 bg-[var(--hazo-bg-subtle)] min-h-[200px]", children: compressing ? (_jsxs("div", { className: "cls_profile_picture_upload_tab_compressing flex flex-col items-center gap-2", children: [_jsx(Loader2, { className: "h-8 w-8 text-[var(--hazo-text-subtle)] animate-spin", "aria-hidden": "true" }), _jsx("p", { className: "cls_profile_picture_upload_tab_compressing_text text-sm text-[var(--hazo-text-muted)]", children: "Compressing image..." })] })) : uploading ? (_jsxs("div", { className: "cls_profile_picture_upload_tab_uploading flex flex-col items-center gap-2", children: [_jsx(Loader2, { className: "h-8 w-8 text-[var(--hazo-text-subtle)] animate-spin", "aria-hidden": "true" }), _jsx("p", { className: "cls_profile_picture_upload_tab_uploading_text text-sm text-[var(--hazo-text-muted)]", children: "Uploading..." })] })) : preview ? (_jsxs("div", { className: "cls_profile_picture_upload_tab_preview_image_container flex flex-col items-center gap-4", children: [_jsxs("div", { className: "cls_profile_picture_upload_tab_preview_image_wrapper relative", children: [_jsxs(Avatar, { className: "cls_profile_picture_upload_tab_preview_avatar h-32 w-32", children: [_jsx(AvatarImage, { src: preview, alt: "Uploaded profile picture preview", className: "cls_profile_picture_upload_tab_preview_avatar_image" }), _jsx(AvatarFallback, { className: "cls_profile_picture_upload_tab_preview_avatar_fallback bg-[var(--hazo-bg-emphasis)] text-[var(--hazo-text-muted)] text-3xl", children: getInitials() })] }), _jsx(Button, { type: "button", onClick: handleRemove, variant: "ghost", size: "icon", className: "cls_profile_picture_upload_tab_preview_remove absolute -top-2 -right-2 rounded-full h-6 w-6 border border-[var(--hazo-border-emphasis)] bg-white hover:bg-[var(--hazo-bg-subtle)]", "aria-label": "Remove preview", children: _jsx(X, { className: "h-4 w-4", "aria-hidden": "true" }) })] }), _jsx("p", { className: "cls_profile_picture_upload_tab_preview_success_text text-sm text-[var(--hazo-text-muted)] text-center", children: "Preview of your uploaded photo" })] })) : (_jsxs("div", { className: "cls_profile_picture_upload_tab_preview_empty flex flex-col items-center gap-2", children: [_jsx(Avatar, { className: "cls_profile_picture_upload_tab_preview_empty_avatar h-32 w-32", children: _jsx(AvatarFallback, { className: "cls_profile_picture_upload_tab_preview_empty_avatar_fallback bg-[var(--hazo-bg-emphasis)] text-[var(--hazo-text-muted)] text-3xl", children: getInitials() }) }), _jsx("p", { className: "cls_profile_picture_upload_tab_preview_empty_text text-sm text-[var(--hazo-text-muted)] text-center", children: "Upload an image to see preview" })] })) })] })] })] }));
169
169
  }
@@ -28,7 +28,7 @@ export default function my_settings_layout({ labels, button_colors, password_req
28
28
  const settings = use_my_settings({
29
29
  passwordRequirements: password_requirements,
30
30
  });
31
- return (_jsx(UnauthorizedGuard, { message: unauthorizedMessage, loginButtonLabel: loginButtonLabel, loginPath: loginPath, children: _jsxs("div", { className: "cls_my_settings_layout flex flex-col gap-6 p-6 max-w-4xl mx-auto min-h-screen bg-slate-50", children: [_jsxs("div", { className: "cls_my_settings_layout_header flex flex-col gap-2", children: [_jsx("h1", { className: "cls_my_settings_layout_heading text-3xl font-bold text-slate-900", children: heading }), _jsx("p", { className: "cls_my_settings_layout_subheading text-slate-600", children: subHeading })] }), _jsxs("div", { className: "cls_my_settings_layout_profile_photo_section bg-white rounded-lg border border-slate-200 p-6", children: [_jsx("h2", { className: "cls_my_settings_layout_section_heading text-lg font-semibold text-slate-900 mb-4", children: profilePhotoLabel }), _jsx("div", { className: "cls_my_settings_layout_profile_photo_content flex flex-col items-center", children: _jsxs("div", { className: "cls_my_settings_layout_profile_photo_display relative", children: [_jsx(ProfilePictureDisplay, { profilePictureUrl: settings.profilePictureUrl, name: settings.name, email: settings.email, onEdit: settings.handleProfilePictureEdit }), _jsxs("div", { className: "cls_my_settings_layout_profile_photo_actions absolute left-0 right-0 flex items-center justify-between px-2", style: { bottom: '-20px' }, children: [_jsx(Button, { type: "button", onClick: settings.handleProfilePictureEdit, disabled: settings.loading, variant: "ghost", size: "icon", className: "cls_my_settings_layout_upload_photo_button", "aria-label": uploadPhotoButtonLabel, children: _jsx(Pencil, { className: "h-4 w-4", "aria-hidden": "true" }) }), _jsx(Button, { type: "button", onClick: settings.handleProfilePictureRemove, disabled: settings.loading || !settings.profilePictureUrl, variant: "ghost", size: "icon", className: "cls_my_settings_layout_remove_photo_button text-red-600 hover:text-red-700 hover:bg-red-50", "aria-label": removePhotoButtonLabel, children: _jsx(Trash2, { className: "h-4 w-4", "aria-hidden": "true" }) })] })] }) })] }), _jsxs("div", { className: "cls_my_settings_layout_profile_information_section bg-white rounded-lg border border-slate-200 p-6", children: [_jsx("h2", { className: "cls_my_settings_layout_section_heading text-lg font-semibold text-slate-900 mb-4", children: profileInformationLabel }), _jsxs("div", { className: "cls_my_settings_layout_profile_information_fields grid grid-cols-1 md:grid-cols-2 gap-6", children: [userFields.show_name_field && (_jsx(EditableField, { label: "Full Name", value: settings.name, type: "text", placeholder: "Enter your full name", onSave: settings.handleNameSave, validation: validateName, disabled: settings.loading, ariaLabel: "Full name input field" })), userFields.show_email_field && (_jsx(EditableField, { label: "Email Address", value: settings.email, type: "email", placeholder: "Enter your email address", onSave: settings.handleEmailSave, validation: validateEmail, disabled: settings.loading, ariaLabel: "Email address input field" }))] })] }), userFields.show_password_field && (_jsxs("div", { className: "cls_my_settings_layout_password_section bg-white rounded-lg border border-slate-200 p-6", children: [_jsx("h2", { className: "cls_my_settings_layout_section_heading text-lg font-semibold text-slate-900 mb-4", children: passwordLabel }), _jsxs("div", { className: "cls_my_settings_layout_password_fields flex flex-col gap-6", children: [_jsx(FormFieldWrapper, { fieldId: "current-password", label: currentPasswordLabel, input: _jsx(PasswordField, { inputId: "current-password", ariaLabel: currentPasswordLabel, value: ((_a = settings.passwordFields) === null || _a === void 0 ? void 0 : _a.currentPassword) || "", placeholder: "Enter your current password", autoComplete: "current-password", isVisible: ((_b = settings.passwordFields) === null || _b === void 0 ? void 0 : _b.currentPasswordVisible) || false, onChange: (value) => settings.handlePasswordFieldChange("currentPassword", value), onToggleVisibility: () => settings.togglePasswordVisibility("currentPassword"), errorMessage: (_d = (_c = settings.passwordFields) === null || _c === void 0 ? void 0 : _c.errors) === null || _d === void 0 ? void 0 : _d.currentPassword }) }), _jsxs("div", { className: "cls_my_settings_layout_password_fields_row grid grid-cols-1 md:grid-cols-2 gap-6", children: [_jsx(FormFieldWrapper, { fieldId: "new-password", label: newPasswordLabel, input: _jsx(PasswordField, { inputId: "new-password", ariaLabel: newPasswordLabel, value: ((_e = settings.passwordFields) === null || _e === void 0 ? void 0 : _e.newPassword) || "", placeholder: "Enter your new password", autoComplete: "new-password", isVisible: ((_f = settings.passwordFields) === null || _f === void 0 ? void 0 : _f.newPasswordVisible) || false, onChange: (value) => settings.handlePasswordFieldChange("newPassword", value), onToggleVisibility: () => settings.togglePasswordVisibility("newPassword"), errorMessage: (_h = (_g = settings.passwordFields) === null || _g === void 0 ? void 0 : _g.errors) === null || _h === void 0 ? void 0 : _h.newPassword }) }), _jsx(FormFieldWrapper, { fieldId: "confirm-password", label: confirmPasswordLabel, input: _jsx(PasswordField, { inputId: "confirm-password", ariaLabel: confirmPasswordLabel, value: ((_j = settings.passwordFields) === null || _j === void 0 ? void 0 : _j.confirmPassword) || "", placeholder: "Confirm your new password", autoComplete: "new-password", isVisible: ((_k = settings.passwordFields) === null || _k === void 0 ? void 0 : _k.confirmPasswordVisible) || false, onChange: (value) => settings.handlePasswordFieldChange("confirmPassword", value), onToggleVisibility: () => settings.togglePasswordVisibility("confirmPassword"), errorMessage: (_m = (_l = settings.passwordFields) === null || _l === void 0 ? void 0 : _l.errors) === null || _m === void 0 ? void 0 : _m.confirmPassword }) })] })] }), _jsx("div", { className: "cls_my_settings_layout_password_actions flex justify-end mt-4", children: _jsx(Button, { type: "button", onClick: settings.handlePasswordSave, disabled: settings.loading || settings.isPasswordSaveDisabled, className: "cls_my_settings_layout_save_password_button", style: {
31
+ return (_jsx(UnauthorizedGuard, { message: unauthorizedMessage, loginButtonLabel: loginButtonLabel, loginPath: loginPath, children: _jsxs("div", { className: "cls_my_settings_layout flex flex-col gap-6 p-6 max-w-4xl mx-auto min-h-screen bg-[var(--hazo-bg-subtle)]", children: [_jsxs("div", { className: "cls_my_settings_layout_header flex flex-col gap-2", children: [_jsx("h1", { className: "cls_my_settings_layout_heading text-3xl font-bold text-[var(--hazo-text-primary)]", children: heading }), _jsx("p", { className: "cls_my_settings_layout_subheading text-[var(--hazo-text-muted)]", children: subHeading })] }), _jsxs("div", { className: "cls_my_settings_layout_profile_photo_section bg-white rounded-lg border border-[var(--hazo-border)] p-6", children: [_jsx("h2", { className: "cls_my_settings_layout_section_heading text-lg font-semibold text-[var(--hazo-text-primary)] mb-4", children: profilePhotoLabel }), _jsx("div", { className: "cls_my_settings_layout_profile_photo_content flex flex-col items-center", children: _jsxs("div", { className: "cls_my_settings_layout_profile_photo_display relative", children: [_jsx(ProfilePictureDisplay, { profilePictureUrl: settings.profilePictureUrl, name: settings.name, email: settings.email, onEdit: settings.handleProfilePictureEdit }), _jsxs("div", { className: "cls_my_settings_layout_profile_photo_actions absolute left-0 right-0 flex items-center justify-between px-2", style: { bottom: '-20px' }, children: [_jsx(Button, { type: "button", onClick: settings.handleProfilePictureEdit, disabled: settings.loading, variant: "ghost", size: "icon", className: "cls_my_settings_layout_upload_photo_button", "aria-label": uploadPhotoButtonLabel, children: _jsx(Pencil, { className: "h-4 w-4", "aria-hidden": "true" }) }), _jsx(Button, { type: "button", onClick: settings.handleProfilePictureRemove, disabled: settings.loading || !settings.profilePictureUrl, variant: "ghost", size: "icon", className: "cls_my_settings_layout_remove_photo_button text-red-600 hover:text-red-700 hover:bg-red-50", "aria-label": removePhotoButtonLabel, children: _jsx(Trash2, { className: "h-4 w-4", "aria-hidden": "true" }) })] })] }) })] }), _jsxs("div", { className: "cls_my_settings_layout_profile_information_section bg-white rounded-lg border border-[var(--hazo-border)] p-6", children: [_jsx("h2", { className: "cls_my_settings_layout_section_heading text-lg font-semibold text-[var(--hazo-text-primary)] mb-4", children: profileInformationLabel }), _jsxs("div", { className: "cls_my_settings_layout_profile_information_fields grid grid-cols-1 md:grid-cols-2 gap-6", children: [userFields.show_name_field && (_jsx(EditableField, { label: "Full Name", value: settings.name, type: "text", placeholder: "Enter your full name", onSave: settings.handleNameSave, validation: validateName, disabled: settings.loading, ariaLabel: "Full name input field" })), userFields.show_email_field && (_jsx(EditableField, { label: "Email Address", value: settings.email, type: "email", placeholder: "Enter your email address", onSave: settings.handleEmailSave, validation: validateEmail, disabled: settings.loading, ariaLabel: "Email address input field" }))] })] }), userFields.show_password_field && (_jsxs("div", { className: "cls_my_settings_layout_password_section bg-white rounded-lg border border-[var(--hazo-border)] p-6", children: [_jsx("h2", { className: "cls_my_settings_layout_section_heading text-lg font-semibold text-[var(--hazo-text-primary)] mb-4", children: passwordLabel }), _jsxs("div", { className: "cls_my_settings_layout_password_fields flex flex-col gap-6", children: [_jsx(FormFieldWrapper, { fieldId: "current-password", label: currentPasswordLabel, input: _jsx(PasswordField, { inputId: "current-password", ariaLabel: currentPasswordLabel, value: ((_a = settings.passwordFields) === null || _a === void 0 ? void 0 : _a.currentPassword) || "", placeholder: "Enter your current password", autoComplete: "current-password", isVisible: ((_b = settings.passwordFields) === null || _b === void 0 ? void 0 : _b.currentPasswordVisible) || false, onChange: (value) => settings.handlePasswordFieldChange("currentPassword", value), onToggleVisibility: () => settings.togglePasswordVisibility("currentPassword"), errorMessage: (_d = (_c = settings.passwordFields) === null || _c === void 0 ? void 0 : _c.errors) === null || _d === void 0 ? void 0 : _d.currentPassword }) }), _jsxs("div", { className: "cls_my_settings_layout_password_fields_row grid grid-cols-1 md:grid-cols-2 gap-6", children: [_jsx(FormFieldWrapper, { fieldId: "new-password", label: newPasswordLabel, input: _jsx(PasswordField, { inputId: "new-password", ariaLabel: newPasswordLabel, value: ((_e = settings.passwordFields) === null || _e === void 0 ? void 0 : _e.newPassword) || "", placeholder: "Enter your new password", autoComplete: "new-password", isVisible: ((_f = settings.passwordFields) === null || _f === void 0 ? void 0 : _f.newPasswordVisible) || false, onChange: (value) => settings.handlePasswordFieldChange("newPassword", value), onToggleVisibility: () => settings.togglePasswordVisibility("newPassword"), errorMessage: (_h = (_g = settings.passwordFields) === null || _g === void 0 ? void 0 : _g.errors) === null || _h === void 0 ? void 0 : _h.newPassword }) }), _jsx(FormFieldWrapper, { fieldId: "confirm-password", label: confirmPasswordLabel, input: _jsx(PasswordField, { inputId: "confirm-password", ariaLabel: confirmPasswordLabel, value: ((_j = settings.passwordFields) === null || _j === void 0 ? void 0 : _j.confirmPassword) || "", placeholder: "Confirm your new password", autoComplete: "new-password", isVisible: ((_k = settings.passwordFields) === null || _k === void 0 ? void 0 : _k.confirmPasswordVisible) || false, onChange: (value) => settings.handlePasswordFieldChange("confirmPassword", value), onToggleVisibility: () => settings.togglePasswordVisibility("confirmPassword"), errorMessage: (_m = (_l = settings.passwordFields) === null || _l === void 0 ? void 0 : _l.errors) === null || _m === void 0 ? void 0 : _m.confirmPassword }) })] })] }), _jsx("div", { className: "cls_my_settings_layout_password_actions flex justify-end mt-4", children: _jsx(Button, { type: "button", onClick: settings.handlePasswordSave, disabled: settings.loading || settings.isPasswordSaveDisabled, className: "cls_my_settings_layout_save_password_button", style: {
32
32
  backgroundColor: resolvedButtonPalette.submitBackground,
33
33
  color: resolvedButtonPalette.submitText,
34
34
  }, "aria-label": "Save password", children: "Save Password" }) })] })), _jsx(ProfilePictureDialog, { open: settings.profilePictureDialogOpen, onOpenChange: (open) => {
@@ -138,14 +138,14 @@ export function ProfilePicMenu({ show_single_button = false, sign_up_label = "Si
138
138
  };
139
139
  // Show loading state
140
140
  if (authStatus.loading) {
141
- return (_jsx("div", { className: `cls_profile_pic_menu ${className || ""}`, children: _jsx("div", { className: "h-10 w-10 rounded-full bg-slate-200 animate-pulse" }) }));
141
+ return (_jsx("div", { className: `cls_profile_pic_menu ${className || ""}`, children: _jsx("div", { className: "h-10 w-10 rounded-full bg-[var(--hazo-bg-emphasis)] animate-pulse" }) }));
142
142
  }
143
143
  // Not authenticated - show sign up/sign in buttons
144
144
  if (!authStatus.authenticated) {
145
145
  return (_jsx("div", { className: `cls_profile_pic_menu flex items-center gap-2 ${className || ""}`, children: show_single_button ? (_jsx(Button, { asChild: true, variant: "default", size: "sm", children: _jsx(Link, { href: register_path, className: "cls_profile_pic_menu_sign_up", children: sign_up_label }) })) : (_jsxs(_Fragment, { children: [_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(Link, { href: register_path, className: "cls_profile_pic_menu_sign_up", children: sign_up_label }) }), _jsx(Button, { asChild: true, variant: "default", size: "sm", children: _jsx(Link, { href: login_path, className: "cls_profile_pic_menu_sign_in", children: sign_in_label }) })] })) }));
146
146
  }
147
147
  // Authenticated - show profile picture with dropdown menu
148
- return (_jsx("div", { className: `cls_profile_pic_menu ${className || ""}`, children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("button", { className: "cls_profile_pic_menu_trigger focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary rounded-full", "aria-label": "Profile menu", children: _jsxs(Avatar, { className: `cls_profile_pic_menu_avatar ${avatarSizeClasses[avatar_size]} cursor-pointer`, children: [_jsx(AvatarImage, { src: authStatus.profile_picture_url, alt: authStatus.name ? `Profile picture of ${authStatus.name}` : "Profile picture", className: "cls_profile_pic_menu_image" }), _jsx(AvatarFallback, { className: "cls_profile_pic_menu_fallback bg-slate-200 text-slate-600", children: getInitials() })] }) }) }), _jsx(DropdownMenuContent, { align: "end", className: "cls_profile_pic_menu_dropdown w-56", children: menuItems.map((item) => {
148
+ return (_jsx("div", { className: `cls_profile_pic_menu ${className || ""}`, children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("button", { className: "cls_profile_pic_menu_trigger focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary rounded-full", "aria-label": "Profile menu", children: _jsxs(Avatar, { className: `cls_profile_pic_menu_avatar ${avatarSizeClasses[avatar_size]} cursor-pointer`, children: [_jsx(AvatarImage, { src: authStatus.profile_picture_url, alt: authStatus.name ? `Profile picture of ${authStatus.name}` : "Profile picture", className: "cls_profile_pic_menu_image" }), _jsx(AvatarFallback, { className: "cls_profile_pic_menu_fallback bg-[var(--hazo-bg-emphasis)] text-[var(--hazo-text-muted)]", children: getInitials() })] }) }) }), _jsx(DropdownMenuContent, { align: "end", className: "cls_profile_pic_menu_dropdown w-56", children: menuItems.map((item) => {
149
149
  if (item.type === "separator") {
150
150
  return _jsx(DropdownMenuSeparator, { className: "cls_profile_pic_menu_separator" }, item.id);
151
151
  }
@@ -40,8 +40,8 @@ export function get_auth_utility_config() {
40
40
  const section_name = "hazo_auth__auth_utility";
41
41
  // Cache settings
42
42
  const cache_max_users = get_config_number(section_name, "cache_max_users", 10000);
43
- const cache_ttl_minutes = get_config_number(section_name, "cache_ttl_minutes", 15);
44
- const cache_max_age_minutes = get_config_number(section_name, "cache_max_age_minutes", 30);
43
+ const cache_ttl_minutes = get_config_number(section_name, "cache_ttl_minutes", 5);
44
+ const cache_max_age_minutes = get_config_number(section_name, "cache_max_age_minutes", 10);
45
45
  // Rate limiting
46
46
  const rate_limit_per_user = get_config_number(section_name, "rate_limit_per_user", 100);
47
47
  const rate_limit_per_ip = get_config_number(section_name, "rate_limit_per_ip", 200);
@@ -0,0 +1,76 @@
1
+ import type { UserProfileInfo } from "./user_profiles_service";
2
+ /**
3
+ * LRU cache implementation with TTL and size limits for user profiles
4
+ * Uses Map to maintain insertion order for LRU eviction
5
+ */
6
+ declare class UserProfilesCache {
7
+ private cache;
8
+ private max_size;
9
+ private ttl_ms;
10
+ constructor(max_size: number, ttl_minutes: number);
11
+ /**
12
+ * Gets a cached profile for a user
13
+ * Returns undefined if not found or expired
14
+ * @param user_id - User ID to look up
15
+ * @returns Profile or undefined
16
+ */
17
+ get(user_id: string): UserProfileInfo | undefined;
18
+ /**
19
+ * Gets multiple profiles from cache
20
+ * Returns object with found profiles and missing IDs
21
+ * @param user_ids - Array of user IDs to look up
22
+ * @returns Object with cached profiles and IDs not in cache
23
+ */
24
+ get_many(user_ids: string[]): {
25
+ cached: UserProfileInfo[];
26
+ missing_ids: string[];
27
+ };
28
+ /**
29
+ * Sets a cache entry for a user profile
30
+ * Evicts least recently used entries if cache is full
31
+ * @param user_id - User ID
32
+ * @param profile - User profile data
33
+ */
34
+ set(user_id: string, profile: UserProfileInfo): void;
35
+ /**
36
+ * Sets multiple cache entries at once
37
+ * @param profiles - Array of user profiles to cache
38
+ */
39
+ set_many(profiles: UserProfileInfo[]): void;
40
+ /**
41
+ * Invalidates cache for a specific user
42
+ * @param user_id - User ID to invalidate
43
+ */
44
+ invalidate_user(user_id: string): void;
45
+ /**
46
+ * Invalidates cache for multiple users
47
+ * @param user_ids - Array of user IDs to invalidate
48
+ */
49
+ invalidate_users(user_ids: string[]): void;
50
+ /**
51
+ * Invalidates all cache entries
52
+ */
53
+ invalidate_all(): void;
54
+ /**
55
+ * Gets cache statistics
56
+ * @returns Object with cache size and max size
57
+ */
58
+ get_stats(): {
59
+ size: number;
60
+ max_size: number;
61
+ ttl_minutes: number;
62
+ };
63
+ }
64
+ /**
65
+ * Gets or creates the global user profiles cache instance
66
+ * @param max_size - Maximum cache size (default: 5000)
67
+ * @param ttl_minutes - TTL in minutes (default: 5)
68
+ * @returns User profiles cache instance
69
+ */
70
+ export declare function get_user_profiles_cache(max_size?: number, ttl_minutes?: number): UserProfilesCache;
71
+ /**
72
+ * Resets the global cache instance (useful for testing)
73
+ */
74
+ export declare function reset_user_profiles_cache(): void;
75
+ export {};
76
+ //# sourceMappingURL=user_profiles_cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user_profiles_cache.d.ts","sourceRoot":"","sources":["../../../src/lib/services/user_profiles_cache.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAc/D;;;GAGG;AACH,cAAM,iBAAiB;IACrB,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAS;gBAEX,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;IAMjD;;;;;OAKG;IACH,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAuBjD;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG;QAC5B,MAAM,EAAE,eAAe,EAAE,CAAC;QAC1B,WAAW,EAAE,MAAM,EAAE,CAAC;KACvB;IAgBD;;;;;OAKG;IACH,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI;IAmBpD;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,IAAI;IAM3C;;;OAGG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAItC;;;OAGG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI;IAM1C;;OAEG;IACH,cAAc,IAAI,IAAI;IAItB;;;OAGG;IACH,SAAS,IAAI;QACX,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,MAAM,CAAC;KACrB;CAOF;AAMD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,GAAE,MAAa,EACvB,WAAW,GAAE,MAAU,GACtB,iBAAiB,CAKnB;AAED;;GAEG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD"}
@@ -0,0 +1,141 @@
1
+ // section: cache_class
2
+ /**
3
+ * LRU cache implementation with TTL and size limits for user profiles
4
+ * Uses Map to maintain insertion order for LRU eviction
5
+ */
6
+ class UserProfilesCache {
7
+ constructor(max_size, ttl_minutes) {
8
+ this.cache = new Map();
9
+ this.max_size = max_size;
10
+ this.ttl_ms = ttl_minutes * 60 * 1000;
11
+ }
12
+ /**
13
+ * Gets a cached profile for a user
14
+ * Returns undefined if not found or expired
15
+ * @param user_id - User ID to look up
16
+ * @returns Profile or undefined
17
+ */
18
+ get(user_id) {
19
+ const entry = this.cache.get(user_id);
20
+ if (!entry) {
21
+ return undefined;
22
+ }
23
+ const now = Date.now();
24
+ const age = now - entry.timestamp;
25
+ // Check if entry is expired
26
+ if (age > this.ttl_ms) {
27
+ this.cache.delete(user_id);
28
+ return undefined;
29
+ }
30
+ // Move to end (most recently used)
31
+ this.cache.delete(user_id);
32
+ this.cache.set(user_id, entry);
33
+ return entry.profile;
34
+ }
35
+ /**
36
+ * Gets multiple profiles from cache
37
+ * Returns object with found profiles and missing IDs
38
+ * @param user_ids - Array of user IDs to look up
39
+ * @returns Object with cached profiles and IDs not in cache
40
+ */
41
+ get_many(user_ids) {
42
+ const cached = [];
43
+ const missing_ids = [];
44
+ for (const user_id of user_ids) {
45
+ const profile = this.get(user_id);
46
+ if (profile) {
47
+ cached.push(profile);
48
+ }
49
+ else {
50
+ missing_ids.push(user_id);
51
+ }
52
+ }
53
+ return { cached, missing_ids };
54
+ }
55
+ /**
56
+ * Sets a cache entry for a user profile
57
+ * Evicts least recently used entries if cache is full
58
+ * @param user_id - User ID
59
+ * @param profile - User profile data
60
+ */
61
+ set(user_id, profile) {
62
+ // Evict LRU entries if cache is full
63
+ while (this.cache.size >= this.max_size) {
64
+ const first_key = this.cache.keys().next().value;
65
+ if (first_key) {
66
+ this.cache.delete(first_key);
67
+ }
68
+ else {
69
+ break;
70
+ }
71
+ }
72
+ const entry = {
73
+ profile,
74
+ timestamp: Date.now(),
75
+ };
76
+ this.cache.set(user_id, entry);
77
+ }
78
+ /**
79
+ * Sets multiple cache entries at once
80
+ * @param profiles - Array of user profiles to cache
81
+ */
82
+ set_many(profiles) {
83
+ for (const profile of profiles) {
84
+ this.set(profile.user_id, profile);
85
+ }
86
+ }
87
+ /**
88
+ * Invalidates cache for a specific user
89
+ * @param user_id - User ID to invalidate
90
+ */
91
+ invalidate_user(user_id) {
92
+ this.cache.delete(user_id);
93
+ }
94
+ /**
95
+ * Invalidates cache for multiple users
96
+ * @param user_ids - Array of user IDs to invalidate
97
+ */
98
+ invalidate_users(user_ids) {
99
+ for (const user_id of user_ids) {
100
+ this.cache.delete(user_id);
101
+ }
102
+ }
103
+ /**
104
+ * Invalidates all cache entries
105
+ */
106
+ invalidate_all() {
107
+ this.cache.clear();
108
+ }
109
+ /**
110
+ * Gets cache statistics
111
+ * @returns Object with cache size and max size
112
+ */
113
+ get_stats() {
114
+ return {
115
+ size: this.cache.size,
116
+ max_size: this.max_size,
117
+ ttl_minutes: this.ttl_ms / 60000,
118
+ };
119
+ }
120
+ }
121
+ // section: singleton
122
+ // Global cache instance (initialized with defaults, will be configured on first use)
123
+ let user_profiles_cache_instance = null;
124
+ /**
125
+ * Gets or creates the global user profiles cache instance
126
+ * @param max_size - Maximum cache size (default: 5000)
127
+ * @param ttl_minutes - TTL in minutes (default: 5)
128
+ * @returns User profiles cache instance
129
+ */
130
+ export function get_user_profiles_cache(max_size = 5000, ttl_minutes = 5) {
131
+ if (!user_profiles_cache_instance) {
132
+ user_profiles_cache_instance = new UserProfilesCache(max_size, ttl_minutes);
133
+ }
134
+ return user_profiles_cache_instance;
135
+ }
136
+ /**
137
+ * Resets the global cache instance (useful for testing)
138
+ */
139
+ export function reset_user_profiles_cache() {
140
+ user_profiles_cache_instance = null;
141
+ }
@@ -19,13 +19,30 @@ export type GetProfilesResult = {
19
19
  profiles: UserProfileInfo[];
20
20
  not_found_ids: string[];
21
21
  error?: string;
22
+ cache_stats?: {
23
+ hits: number;
24
+ misses: number;
25
+ cache_enabled: boolean;
26
+ };
22
27
  };
23
28
  /**
24
29
  * Retrieves basic profile information for multiple users in a single batch call
25
30
  * Useful for chat applications and similar use cases where basic user info is needed
31
+ * Uses LRU cache with configurable TTL for performance (default: 5 minutes)
26
32
  * @param adapter - The hazo_connect adapter instance
27
33
  * @param user_ids - Array of user IDs to retrieve profiles for
28
34
  * @returns GetProfilesResult with found profiles and list of not found IDs
29
35
  */
30
36
  export declare function hazo_get_user_profiles(adapter: HazoConnectAdapter, user_ids: string[]): Promise<GetProfilesResult>;
37
+ /**
38
+ * Invalidates cache for specific user IDs
39
+ * Call this after user profile updates to ensure fresh data on next fetch
40
+ * @param user_ids - Array of user IDs to invalidate from cache
41
+ */
42
+ export declare function invalidate_user_profiles_cache(user_ids: string[]): void;
43
+ /**
44
+ * Invalidates the entire user profiles cache
45
+ * Use sparingly - prefer invalidating specific users when possible
46
+ */
47
+ export declare function invalidate_all_user_profiles_cache(): void;
31
48
  //# sourceMappingURL=user_profiles_service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"user_profiles_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/user_profiles_service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAOvD;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAGF;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,MAAM,EAAE,GACjB,OAAO,CAAC,iBAAiB,CAAC,CAkG5B"}
1
+ {"version":3,"file":"user_profiles_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/user_profiles_service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AASvD;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,aAAa,EAAE,OAAO,CAAC;KACxB,CAAC;CACH,CAAC;AAuDF;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,MAAM,EAAE,GACjB,OAAO,CAAC,iBAAiB,CAAC,CA4H5B;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,CAWvE;AAED;;;GAGG;AACH,wBAAgB,kCAAkC,IAAI,IAAI,CAWzD"}
@@ -2,16 +2,60 @@ import { createCrudService } from "hazo_connect/server";
2
2
  import { differenceInDays } from "date-fns";
3
3
  import { create_app_logger } from "../app_logger";
4
4
  import { sanitize_error_for_user } from "../utils/error_sanitizer";
5
+ import { get_user_profiles_cache } from "./user_profiles_cache";
6
+ import { get_user_profiles_cache_config } from "../user_profiles_config.server";
5
7
  // section: helpers
8
+ /**
9
+ * Fetches profiles from database for given user IDs
10
+ * @param adapter - The hazo_connect adapter instance
11
+ * @param user_ids - Array of user IDs to fetch
12
+ * @returns Object with profiles and not found IDs
13
+ */
14
+ async function fetch_profiles_from_db(adapter, user_ids) {
15
+ const users_service = createCrudService(adapter, "hazo_users");
16
+ // Query users by IDs using the 'in' filter
17
+ // PostgREST supports 'in' filter syntax: id=in.(id1,id2,id3)
18
+ const users = await users_service.findBy({
19
+ id: `in.(${user_ids.join(",")})`,
20
+ });
21
+ // Handle case where no users are found
22
+ if (!Array.isArray(users)) {
23
+ return {
24
+ profiles: [],
25
+ not_found_ids: user_ids,
26
+ };
27
+ }
28
+ // Build set of found user IDs for quick lookup
29
+ const found_user_ids = new Set(users.map((user) => user.id));
30
+ // Determine which user IDs were not found
31
+ const not_found_ids = user_ids.filter((id) => !found_user_ids.has(id));
32
+ // Transform database records to UserProfileInfo
33
+ const now = new Date();
34
+ const profiles = users.map((user) => {
35
+ const created_at = user.created_at;
36
+ const created_date = new Date(created_at);
37
+ const days_since_created = differenceInDays(now, created_date);
38
+ return {
39
+ user_id: user.id,
40
+ profile_picture_url: user.profile_picture_url || null,
41
+ email: user.email_address,
42
+ name: user.name || null,
43
+ days_since_created,
44
+ };
45
+ });
46
+ return { profiles, not_found_ids };
47
+ }
6
48
  /**
7
49
  * Retrieves basic profile information for multiple users in a single batch call
8
50
  * Useful for chat applications and similar use cases where basic user info is needed
51
+ * Uses LRU cache with configurable TTL for performance (default: 5 minutes)
9
52
  * @param adapter - The hazo_connect adapter instance
10
53
  * @param user_ids - Array of user IDs to retrieve profiles for
11
54
  * @returns GetProfilesResult with found profiles and list of not found IDs
12
55
  */
13
56
  export async function hazo_get_user_profiles(adapter, user_ids) {
14
57
  const logger = create_app_logger();
58
+ const config = get_user_profiles_cache_config();
15
59
  try {
16
60
  // Handle empty input
17
61
  if (!user_ids || user_ids.length === 0) {
@@ -19,62 +63,85 @@ export async function hazo_get_user_profiles(adapter, user_ids) {
19
63
  success: true,
20
64
  profiles: [],
21
65
  not_found_ids: [],
66
+ cache_stats: {
67
+ hits: 0,
68
+ misses: 0,
69
+ cache_enabled: config.cache_enabled,
70
+ },
22
71
  };
23
72
  }
24
73
  // Remove duplicates from input
25
74
  const unique_user_ids = [...new Set(user_ids)];
26
- // Create CRUD service for hazo_users table
27
- const users_service = createCrudService(adapter, "hazo_users");
28
- // Query users by IDs using the 'in' filter
29
- // PostgREST supports 'in' filter syntax: id=in.(id1,id2,id3)
30
- const users = await users_service.findBy({
31
- id: `in.(${unique_user_ids.join(",")})`,
32
- });
33
- // Handle case where no users are found
34
- if (!Array.isArray(users)) {
35
- logger.warn("hazo_get_user_profiles_unexpected_response", {
36
- filename: "user_profiles_service.ts",
37
- line_number: 70,
38
- message: "Unexpected response format from database query",
39
- user_ids: unique_user_ids,
40
- });
41
- return {
42
- success: true,
43
- profiles: [],
44
- not_found_ids: unique_user_ids,
45
- };
75
+ // Initialize variables for cache tracking
76
+ let cache_hits = 0;
77
+ let cache_misses = 0;
78
+ let all_profiles = [];
79
+ let all_not_found_ids = [];
80
+ if (config.cache_enabled) {
81
+ // Get cache instance with config settings
82
+ const cache = get_user_profiles_cache(config.cache_max_entries, config.cache_ttl_minutes);
83
+ // Check cache first
84
+ const { cached, missing_ids } = cache.get_many(unique_user_ids);
85
+ cache_hits = cached.length;
86
+ cache_misses = missing_ids.length;
87
+ // If all profiles were cached, return immediately
88
+ if (missing_ids.length === 0) {
89
+ logger.info("hazo_get_user_profiles_cache_hit_all", {
90
+ filename: "user_profiles_service.ts",
91
+ line_number: 130,
92
+ message: "All profiles served from cache",
93
+ requested_count: unique_user_ids.length,
94
+ cache_hits,
95
+ });
96
+ return {
97
+ success: true,
98
+ profiles: cached,
99
+ not_found_ids: [],
100
+ cache_stats: {
101
+ hits: cache_hits,
102
+ misses: 0,
103
+ cache_enabled: true,
104
+ },
105
+ };
106
+ }
107
+ // Fetch missing profiles from database
108
+ const db_result = await fetch_profiles_from_db(adapter, missing_ids);
109
+ // Cache the newly fetched profiles
110
+ if (db_result.profiles.length > 0) {
111
+ cache.set_many(db_result.profiles);
112
+ }
113
+ // Combine cached and freshly fetched profiles
114
+ all_profiles = [...cached, ...db_result.profiles];
115
+ all_not_found_ids = db_result.not_found_ids;
116
+ }
117
+ else {
118
+ // Cache disabled - fetch all from database
119
+ cache_misses = unique_user_ids.length;
120
+ const db_result = await fetch_profiles_from_db(adapter, unique_user_ids);
121
+ all_profiles = db_result.profiles;
122
+ all_not_found_ids = db_result.not_found_ids;
46
123
  }
47
- // Build set of found user IDs for quick lookup
48
- const found_user_ids = new Set(users.map((user) => user.id));
49
- // Determine which user IDs were not found
50
- const not_found_ids = unique_user_ids.filter((id) => !found_user_ids.has(id));
51
- // Transform database records to UserProfileInfo
52
- const now = new Date();
53
- const profiles = users.map((user) => {
54
- const created_at = user.created_at;
55
- const created_date = new Date(created_at);
56
- const days_since_created = differenceInDays(now, created_date);
57
- return {
58
- user_id: user.id,
59
- profile_picture_url: user.profile_picture_url || null,
60
- email: user.email_address,
61
- name: user.name || null,
62
- days_since_created,
63
- };
64
- });
65
124
  // Log successful retrieval
66
125
  logger.info("hazo_get_user_profiles_success", {
67
126
  filename: "user_profiles_service.ts",
68
- line_number: 105,
127
+ line_number: 170,
69
128
  message: "Successfully retrieved user profiles",
70
129
  requested_count: unique_user_ids.length,
71
- found_count: profiles.length,
72
- not_found_count: not_found_ids.length,
130
+ found_count: all_profiles.length,
131
+ not_found_count: all_not_found_ids.length,
132
+ cache_hits,
133
+ cache_misses,
134
+ cache_enabled: config.cache_enabled,
73
135
  });
74
136
  return {
75
137
  success: true,
76
- profiles,
77
- not_found_ids,
138
+ profiles: all_profiles,
139
+ not_found_ids: all_not_found_ids,
140
+ cache_stats: {
141
+ hits: cache_hits,
142
+ misses: cache_misses,
143
+ cache_enabled: config.cache_enabled,
144
+ },
78
145
  };
79
146
  }
80
147
  catch (error) {
@@ -84,7 +151,7 @@ export async function hazo_get_user_profiles(adapter, user_ids) {
84
151
  logger,
85
152
  context: {
86
153
  filename: "user_profiles_service.ts",
87
- line_number: 122,
154
+ line_number: 195,
88
155
  operation: "hazo_get_user_profiles",
89
156
  user_ids_count: (user_ids === null || user_ids === void 0 ? void 0 : user_ids.length) || 0,
90
157
  },
@@ -97,3 +164,28 @@ export async function hazo_get_user_profiles(adapter, user_ids) {
97
164
  };
98
165
  }
99
166
  }
167
+ /**
168
+ * Invalidates cache for specific user IDs
169
+ * Call this after user profile updates to ensure fresh data on next fetch
170
+ * @param user_ids - Array of user IDs to invalidate from cache
171
+ */
172
+ export function invalidate_user_profiles_cache(user_ids) {
173
+ const config = get_user_profiles_cache_config();
174
+ if (!config.cache_enabled) {
175
+ return;
176
+ }
177
+ const cache = get_user_profiles_cache(config.cache_max_entries, config.cache_ttl_minutes);
178
+ cache.invalidate_users(user_ids);
179
+ }
180
+ /**
181
+ * Invalidates the entire user profiles cache
182
+ * Use sparingly - prefer invalidating specific users when possible
183
+ */
184
+ export function invalidate_all_user_profiles_cache() {
185
+ const config = get_user_profiles_cache_config();
186
+ if (!config.cache_enabled) {
187
+ return;
188
+ }
189
+ const cache = get_user_profiles_cache(config.cache_max_entries, config.cache_ttl_minutes);
190
+ cache.invalidate_all();
191
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * User profiles cache configuration options
3
+ */
4
+ export type UserProfilesCacheConfig = {
5
+ cache_enabled: boolean;
6
+ cache_max_entries: number;
7
+ cache_ttl_minutes: number;
8
+ };
9
+ /**
10
+ * Reads user profiles cache configuration from hazo_auth_config.ini file
11
+ * Falls back to defaults if hazo_auth_config.ini is not found or section is missing
12
+ * @returns User profiles cache configuration options
13
+ */
14
+ export declare function get_user_profiles_cache_config(): UserProfilesCacheConfig;
15
+ //# sourceMappingURL=user_profiles_config.server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user_profiles_config.server.d.ts","sourceRoot":"","sources":["../../src/lib/user_profiles_config.server.ts"],"names":[],"mappings":"AAQA;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC,aAAa,EAAE,OAAO,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;CAC3B,CAAC;AAIF;;;;GAIG;AACH,wBAAgB,8BAA8B,IAAI,uBAAuB,CA6BxE"}
@@ -0,0 +1,23 @@
1
+ // file_description: server-only helper to read user profiles cache configuration from hazo_auth_config.ini
2
+ // section: imports
3
+ import { get_config_number, } from "./config/config_loader.server";
4
+ // section: helpers
5
+ /**
6
+ * Reads user profiles cache configuration from hazo_auth_config.ini file
7
+ * Falls back to defaults if hazo_auth_config.ini is not found or section is missing
8
+ * @returns User profiles cache configuration options
9
+ */
10
+ export function get_user_profiles_cache_config() {
11
+ const section_name = "hazo_auth__user_profiles";
12
+ // Cache settings
13
+ // cache_enabled: 0 = disabled, 1 = enabled (default: 1)
14
+ const cache_enabled_num = get_config_number(section_name, "cache_enabled", 1);
15
+ const cache_enabled = cache_enabled_num === 1;
16
+ const cache_max_entries = get_config_number(section_name, "cache_max_entries", 5000);
17
+ const cache_ttl_minutes = get_config_number(section_name, "cache_ttl_minutes", 5);
18
+ return {
19
+ cache_enabled,
20
+ cache_max_entries,
21
+ cache_ttl_minutes,
22
+ };
23
+ }
@@ -305,17 +305,21 @@ enable_admin_ui = true
305
305
  # default_super_user_email = admin@example.com
306
306
 
307
307
  [hazo_auth__auth_utility]
308
- # Authentication utility configuration
308
+ # Authentication utility configuration for hazo_get_auth()
309
+ # This function retrieves the current user, their permissions, and role IDs from cookies
310
+ # Results are cached in memory to improve performance on repeated calls
309
311
 
310
312
  # Cache settings
311
313
  # Maximum number of users to cache (LRU eviction, default: 10000)
312
314
  # cache_max_users = 10000
313
315
 
314
- # Cache TTL in minutes (default: 15)
315
- # cache_ttl_minutes = 15
316
+ # Cache TTL in minutes - how long cached auth data is valid (default: 5)
317
+ # After TTL expires, the next call will fetch fresh data from database
318
+ # cache_ttl_minutes = 5
316
319
 
317
- # Force cache refresh if older than this many minutes (default: 30)
318
- # cache_max_age_minutes = 30
320
+ # Force cache refresh if older than this many minutes (default: 10)
321
+ # This is the hard limit - entries older than this are always refreshed
322
+ # cache_max_age_minutes = 10
319
323
 
320
324
  # Rate limiting for /api/auth/get_auth endpoint
321
325
  # Per-user rate limit (requests per minute, default: 100)
@@ -336,6 +340,22 @@ enable_admin_ui = true
336
340
  # Example: admin_user_management:You don't have access to user management,admin_role_management:You don't have access to role management
337
341
  # permission_error_messages =
338
342
 
343
+ [hazo_auth__user_profiles]
344
+ # User profiles cache configuration for hazo_get_user_profiles()
345
+ # This function retrieves basic profile info (name, email, profile picture) for multiple users
346
+ # Used by chat applications and similar use cases where batch user info is needed
347
+ # Results are cached in memory to improve performance on repeated calls
348
+
349
+ # Enable caching (1 = enabled, 0 = disabled, default: 1)
350
+ # cache_enabled = 1
351
+
352
+ # Maximum number of user profiles to cache (LRU eviction, default: 5000)
353
+ # cache_max_entries = 5000
354
+
355
+ # Cache TTL in minutes - how long cached profile data is valid (default: 5)
356
+ # After TTL expires, the next call will fetch fresh data from database
357
+ # cache_ttl_minutes = 5
358
+
339
359
  [hazo_auth__profile_pic_menu]
340
360
  # Profile picture menu configuration
341
361
  # This component can be used in navbar or sidebar to show user profile picture or sign up/sign in buttons
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_auth",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -12,6 +12,10 @@
12
12
  "types": "./dist/index.d.ts",
13
13
  "import": "./dist/index.js"
14
14
  },
15
+ "./client": {
16
+ "types": "./dist/client.d.ts",
17
+ "import": "./dist/client.js"
18
+ },
15
19
  "./components/layouts/login": {
16
20
  "types": "./dist/components/layouts/login/index.d.ts",
17
21
  "import": "./dist/components/layouts/login/index.js"