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.
- package/README.md +126 -0
- package/SETUP_CHECKLIST.md +55 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +5 -0
- package/dist/client.d.ts +8 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +25 -0
- package/dist/components/layouts/email_verification/index.d.ts +2 -1
- package/dist/components/layouts/email_verification/index.d.ts.map +1 -1
- package/dist/components/layouts/email_verification/index.js +3 -2
- package/dist/components/layouts/forgot_password/index.d.ts +3 -1
- package/dist/components/layouts/forgot_password/index.d.ts.map +1 -1
- package/dist/components/layouts/forgot_password/index.js +3 -2
- package/dist/components/layouts/my_settings/components/editable_field.js +1 -1
- package/dist/components/layouts/my_settings/components/password_change_dialog.js +1 -1
- package/dist/components/layouts/my_settings/components/profile_picture_display.js +1 -1
- package/dist/components/layouts/my_settings/components/profile_picture_gravatar_tab.js +1 -1
- package/dist/components/layouts/my_settings/components/profile_picture_library_tab.js +4 -4
- package/dist/components/layouts/my_settings/components/profile_picture_upload_tab.js +4 -4
- package/dist/components/layouts/my_settings/index.js +1 -1
- package/dist/components/layouts/shared/components/profile_pic_menu.js +2 -2
- package/dist/lib/auth_utility_config.server.js +2 -2
- package/dist/lib/services/user_profiles_cache.d.ts +76 -0
- package/dist/lib/services/user_profiles_cache.d.ts.map +1 -0
- package/dist/lib/services/user_profiles_cache.js +141 -0
- package/dist/lib/services/user_profiles_service.d.ts +17 -0
- package/dist/lib/services/user_profiles_service.d.ts.map +1 -1
- package/dist/lib/services/user_profiles_service.js +136 -44
- package/dist/lib/user_profiles_config.server.d.ts +15 -0
- package/dist/lib/user_profiles_config.server.d.ts.map +1 -0
- package/dist/lib/user_profiles_config.server.js +23 -0
- package/hazo_auth_config.example.ini +25 -5
- 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:
|
package/SETUP_CHECKLIST.md
CHANGED
|
@@ -60,7 +60,61 @@ ls node_modules/hazo_auth/package.json
|
|
|
60
60
|
# Expected: file exists
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
### Step 1.2:
|
|
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
|
|
package/dist/cli/init.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"
|
|
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",
|
package/dist/client.d.ts
ADDED
|
@@ -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;
|
|
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;
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
162
|
-
${disabled || !uploadEnabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer hover:border-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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",
|
|
44
|
-
const cache_max_age_minutes = get_config_number(section_name, "cache_max_age_minutes",
|
|
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":"
|
|
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
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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:
|
|
127
|
+
line_number: 170,
|
|
69
128
|
message: "Successfully retrieved user profiles",
|
|
70
129
|
requested_count: unique_user_ids.length,
|
|
71
|
-
found_count:
|
|
72
|
-
not_found_count:
|
|
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:
|
|
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:
|
|
315
|
-
#
|
|
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:
|
|
318
|
-
#
|
|
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.
|
|
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"
|