hazo_auth 4.2.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/hazo_auth.mjs +35 -0
- package/cli-src/assets/images/forgot_password_default.jpg +0 -0
- package/cli-src/assets/images/login_default.jpg +0 -0
- package/cli-src/assets/images/register_default.jpg +0 -0
- package/cli-src/assets/images/reset_password_default.jpg +0 -0
- package/cli-src/assets/images/verify_email_default.jpg +0 -0
- package/cli-src/cli/generate.ts +276 -0
- package/cli-src/cli/index.ts +207 -0
- package/cli-src/cli/init.ts +254 -0
- package/cli-src/cli/init_users.ts +376 -0
- package/cli-src/cli/validate.ts +581 -0
- package/cli-src/lib/already_logged_in_config.server.ts +46 -0
- package/cli-src/lib/app_logger.ts +24 -0
- package/cli-src/lib/auth/auth_cache.ts +220 -0
- package/cli-src/lib/auth/auth_rate_limiter.ts +121 -0
- package/cli-src/lib/auth/auth_types.ts +117 -0
- package/cli-src/lib/auth/auth_utils.server.ts +196 -0
- package/cli-src/lib/auth/dev_lock_validator.edge.ts +171 -0
- package/cli-src/lib/auth/hazo_get_auth.server.ts +583 -0
- package/cli-src/lib/auth/index.ts +23 -0
- package/cli-src/lib/auth/nextauth_config.ts +227 -0
- package/cli-src/lib/auth/org_cache.ts +148 -0
- package/cli-src/lib/auth/scope_cache.ts +233 -0
- package/cli-src/lib/auth/server_auth.ts +88 -0
- package/cli-src/lib/auth/session_token_validator.edge.ts +92 -0
- package/cli-src/lib/auth_utility_config.server.ts +136 -0
- package/cli-src/lib/config/config_loader.server.ts +164 -0
- package/cli-src/lib/config/default_config.ts +243 -0
- package/cli-src/lib/dev_lock_config.server.ts +148 -0
- package/cli-src/lib/email_verification_config.server.ts +63 -0
- package/cli-src/lib/file_types_config.server.ts +25 -0
- package/cli-src/lib/forgot_password_config.server.ts +63 -0
- package/cli-src/lib/hazo_connect_instance.server.ts +101 -0
- package/cli-src/lib/hazo_connect_setup.server.ts +194 -0
- package/cli-src/lib/hazo_connect_setup.ts +54 -0
- package/cli-src/lib/index.ts +46 -0
- package/cli-src/lib/login_config.server.ts +106 -0
- package/cli-src/lib/messages_config.server.ts +45 -0
- package/cli-src/lib/migrations/apply_migration.ts +105 -0
- package/cli-src/lib/multi_tenancy_config.server.ts +94 -0
- package/cli-src/lib/my_settings_config.server.ts +135 -0
- package/cli-src/lib/oauth_config.server.ts +87 -0
- package/cli-src/lib/password_requirements_config.server.ts +40 -0
- package/cli-src/lib/profile_pic_menu_config.server.ts +138 -0
- package/cli-src/lib/profile_picture_config.server.ts +56 -0
- package/cli-src/lib/register_config.server.ts +101 -0
- package/cli-src/lib/reset_password_config.server.ts +103 -0
- package/cli-src/lib/scope_hierarchy_config.server.ts +151 -0
- package/cli-src/lib/services/email_service.ts +587 -0
- package/cli-src/lib/services/email_verification_service.ts +270 -0
- package/cli-src/lib/services/index.ts +16 -0
- package/cli-src/lib/services/login_service.ts +150 -0
- package/cli-src/lib/services/oauth_service.ts +494 -0
- package/cli-src/lib/services/org_service.ts +965 -0
- package/cli-src/lib/services/password_change_service.ts +154 -0
- package/cli-src/lib/services/password_reset_service.ts +418 -0
- package/cli-src/lib/services/profile_picture_remove_service.ts +120 -0
- package/cli-src/lib/services/profile_picture_service.ts +451 -0
- package/cli-src/lib/services/profile_picture_source_mapper.ts +62 -0
- package/cli-src/lib/services/registration_service.ts +185 -0
- package/cli-src/lib/services/scope_labels_service.ts +348 -0
- package/cli-src/lib/services/scope_service.ts +778 -0
- package/cli-src/lib/services/session_token_service.ts +178 -0
- package/cli-src/lib/services/token_service.ts +240 -0
- package/cli-src/lib/services/user_profiles_cache.ts +189 -0
- package/cli-src/lib/services/user_profiles_service.ts +264 -0
- package/cli-src/lib/services/user_scope_service.ts +554 -0
- package/cli-src/lib/services/user_update_service.ts +141 -0
- package/cli-src/lib/ui_shell_config.server.ts +73 -0
- package/cli-src/lib/ui_sizes_config.server.ts +37 -0
- package/cli-src/lib/user_fields_config.server.ts +31 -0
- package/cli-src/lib/user_management_config.server.ts +39 -0
- package/cli-src/lib/user_profiles_config.server.ts +55 -0
- package/cli-src/lib/utils/api_route_helpers.ts +60 -0
- package/cli-src/lib/utils/error_sanitizer.ts +75 -0
- package/cli-src/lib/utils/password_validator.ts +65 -0
- package/cli-src/lib/utils.ts +11 -0
- package/cli-src/server/logging/logger_service.ts +56 -0
- package/cli-src/server/types/app_types.ts +74 -0
- package/cli-src/server/types/express.d.ts +16 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli/init_users.d.ts +17 -0
- package/dist/cli/init_users.d.ts.map +1 -0
- package/dist/cli/init_users.js +307 -0
- package/dist/components/layouts/dev_lock/index.d.ts +29 -0
- package/dist/components/layouts/dev_lock/index.d.ts.map +1 -0
- package/dist/components/layouts/dev_lock/index.js +60 -0
- package/dist/components/layouts/index.d.ts +2 -0
- package/dist/components/layouts/index.d.ts.map +1 -1
- package/dist/components/layouts/index.js +1 -0
- package/dist/components/layouts/org_management/index.d.ts +26 -0
- package/dist/components/layouts/org_management/index.d.ts.map +1 -0
- package/dist/components/layouts/org_management/index.js +75 -0
- package/dist/components/layouts/shared/config/layout_customization.d.ts +2 -7
- package/dist/components/layouts/shared/config/layout_customization.d.ts.map +1 -1
- package/dist/components/layouts/user_management/components/org_hierarchy_tab.d.ts +13 -0
- package/dist/components/layouts/user_management/components/org_hierarchy_tab.d.ts.map +1 -0
- package/dist/components/layouts/user_management/components/org_hierarchy_tab.js +276 -0
- package/dist/components/layouts/user_management/index.d.ts +3 -1
- package/dist/components/layouts/user_management/index.d.ts.map +1 -1
- package/dist/components/layouts/user_management/index.js +10 -4
- package/dist/lib/auth/auth_types.d.ts +6 -0
- package/dist/lib/auth/auth_types.d.ts.map +1 -1
- package/dist/lib/auth/dev_lock_validator.edge.d.ts +38 -0
- package/dist/lib/auth/dev_lock_validator.edge.d.ts.map +1 -0
- package/dist/lib/auth/dev_lock_validator.edge.js +122 -0
- package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
- package/dist/lib/auth/hazo_get_auth.server.js +61 -1
- package/dist/lib/auth/org_cache.d.ts +65 -0
- package/dist/lib/auth/org_cache.d.ts.map +1 -0
- package/dist/lib/auth/org_cache.js +103 -0
- package/dist/lib/config/default_config.d.ts +76 -0
- package/dist/lib/config/default_config.d.ts.map +1 -1
- package/dist/lib/config/default_config.js +42 -0
- package/dist/lib/dev_lock_config.server.d.ts +41 -0
- package/dist/lib/dev_lock_config.server.d.ts.map +1 -0
- package/dist/lib/dev_lock_config.server.js +50 -0
- package/dist/lib/multi_tenancy_config.server.d.ts +30 -0
- package/dist/lib/multi_tenancy_config.server.d.ts.map +1 -0
- package/dist/lib/multi_tenancy_config.server.js +41 -0
- package/dist/lib/services/org_service.d.ts +191 -0
- package/dist/lib/services/org_service.d.ts.map +1 -0
- package/dist/lib/services/org_service.js +746 -0
- package/dist/lib/utils/password_validator.d.ts +7 -1
- package/dist/lib/utils/password_validator.d.ts.map +1 -1
- package/dist/page_components/dev_lock.d.ts +11 -0
- package/dist/page_components/dev_lock.d.ts.map +1 -0
- package/dist/page_components/dev_lock.js +17 -0
- package/dist/page_components/index.d.ts +1 -0
- package/dist/page_components/index.d.ts.map +1 -1
- package/dist/page_components/index.js +1 -0
- package/dist/page_components/org_management.d.ts +27 -0
- package/dist/page_components/org_management.d.ts.map +1 -0
- package/dist/page_components/org_management.js +18 -0
- package/hazo_auth_config.example.ini +30 -0
- package/package.json +27 -3
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
// file_description: service for profile picture management including default photo logic, Gravatar, and library photos
|
|
2
|
+
// section: imports
|
|
3
|
+
import type { HazoConnectAdapter } from "hazo_connect";
|
|
4
|
+
import { createCrudService } from "hazo_connect/server";
|
|
5
|
+
import gravatarUrl from "gravatar-url";
|
|
6
|
+
import { get_profile_picture_config } from "../profile_picture_config.server.js";
|
|
7
|
+
import { get_ui_sizes_config } from "../ui_sizes_config.server.js";
|
|
8
|
+
import { get_file_types_config } from "../file_types_config.server.js";
|
|
9
|
+
import { create_app_logger } from "../app_logger.js";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import { map_ui_source_to_db, type ProfilePictureSourceUI } from "./profile_picture_source_mapper.js";
|
|
13
|
+
|
|
14
|
+
// section: types
|
|
15
|
+
export type ProfilePictureSource = ProfilePictureSourceUI;
|
|
16
|
+
|
|
17
|
+
export type DefaultProfilePictureResult = {
|
|
18
|
+
profile_picture_url: string;
|
|
19
|
+
profile_source: ProfilePictureSource;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type LibraryPhotosResult = {
|
|
23
|
+
photos: string[];
|
|
24
|
+
total: number;
|
|
25
|
+
page: number;
|
|
26
|
+
page_size: number;
|
|
27
|
+
has_more: boolean;
|
|
28
|
+
source: "project" | "node_modules";
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// section: cache
|
|
32
|
+
// Cache the resolved library path to avoid repeated filesystem checks
|
|
33
|
+
let cached_library_path: string | null = null;
|
|
34
|
+
let cached_library_source: "project" | "node_modules" | null = null;
|
|
35
|
+
|
|
36
|
+
// section: helpers
|
|
37
|
+
/**
|
|
38
|
+
* Resolves the library path, checking project's public folder first, then node_modules
|
|
39
|
+
* @returns Object with path and source, or null if not found
|
|
40
|
+
*/
|
|
41
|
+
function resolve_library_path(): { path: string; source: "project" | "node_modules" } | null {
|
|
42
|
+
// Return cached value if available
|
|
43
|
+
if (cached_library_path && cached_library_source) {
|
|
44
|
+
if (fs.existsSync(cached_library_path)) {
|
|
45
|
+
return { path: cached_library_path, source: cached_library_source };
|
|
46
|
+
}
|
|
47
|
+
// Cache is stale, clear it
|
|
48
|
+
cached_library_path = null;
|
|
49
|
+
cached_library_source = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const config = get_profile_picture_config();
|
|
53
|
+
const library_subpath = config.library_photo_path.replace(/^\//, "");
|
|
54
|
+
|
|
55
|
+
// Try 1: Project's public folder
|
|
56
|
+
const project_library_path = path.resolve(process.cwd(), "public", library_subpath);
|
|
57
|
+
if (fs.existsSync(project_library_path)) {
|
|
58
|
+
// Check if it has any content (not just empty directory)
|
|
59
|
+
try {
|
|
60
|
+
const entries = fs.readdirSync(project_library_path);
|
|
61
|
+
if (entries.length > 0) {
|
|
62
|
+
cached_library_path = project_library_path;
|
|
63
|
+
cached_library_source = "project";
|
|
64
|
+
return { path: project_library_path, source: "project" };
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Continue to fallback
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Try 2: node_modules/hazo_auth/public folder
|
|
72
|
+
const node_modules_library_path = path.resolve(
|
|
73
|
+
process.cwd(),
|
|
74
|
+
"node_modules",
|
|
75
|
+
"hazo_auth",
|
|
76
|
+
"public",
|
|
77
|
+
library_subpath
|
|
78
|
+
);
|
|
79
|
+
if (fs.existsSync(node_modules_library_path)) {
|
|
80
|
+
cached_library_path = node_modules_library_path;
|
|
81
|
+
cached_library_source = "node_modules";
|
|
82
|
+
return { path: node_modules_library_path, source: "node_modules" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Generates Gravatar URL from email address
|
|
90
|
+
* @param email - User's email address
|
|
91
|
+
* @param size - Image size in pixels (defaults to config value)
|
|
92
|
+
* @returns Gravatar URL
|
|
93
|
+
*/
|
|
94
|
+
export function get_gravatar_url(email: string, size?: number): string {
|
|
95
|
+
const uiSizes = get_ui_sizes_config();
|
|
96
|
+
const gravatarSize = size || uiSizes.gravatar_size;
|
|
97
|
+
return gravatarUrl(email, {
|
|
98
|
+
size: gravatarSize,
|
|
99
|
+
default: "identicon",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Gets library photo categories by reading subdirectories
|
|
105
|
+
* @returns Array of category names
|
|
106
|
+
*/
|
|
107
|
+
export function get_library_categories(): string[] {
|
|
108
|
+
const resolved = resolve_library_path();
|
|
109
|
+
|
|
110
|
+
if (!resolved) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const entries = fs.readdirSync(resolved.path, { withFileTypes: true });
|
|
116
|
+
return entries
|
|
117
|
+
.filter((entry) => entry.isDirectory())
|
|
118
|
+
.map((entry) => entry.name)
|
|
119
|
+
.sort();
|
|
120
|
+
} catch (error) {
|
|
121
|
+
const logger = create_app_logger();
|
|
122
|
+
const error_message = error instanceof Error ? error.message : "Unknown error";
|
|
123
|
+
logger.warn("profile_picture_service_read_categories_failed", {
|
|
124
|
+
filename: "profile_picture_service.ts",
|
|
125
|
+
line_number: 0,
|
|
126
|
+
library_path: resolved.path,
|
|
127
|
+
source: resolved.source,
|
|
128
|
+
error: error_message,
|
|
129
|
+
});
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Gets photos in a specific library category with pagination support
|
|
136
|
+
* @param category - Category name
|
|
137
|
+
* @param page - Page number (1-indexed, default 1)
|
|
138
|
+
* @param page_size - Number of photos per page (default 20, max 100)
|
|
139
|
+
* @returns Object with photos array and pagination info
|
|
140
|
+
*/
|
|
141
|
+
export function get_library_photos_paginated(
|
|
142
|
+
category: string,
|
|
143
|
+
page: number = 1,
|
|
144
|
+
page_size: number = 20
|
|
145
|
+
): LibraryPhotosResult {
|
|
146
|
+
const resolved = resolve_library_path();
|
|
147
|
+
const config = get_profile_picture_config();
|
|
148
|
+
|
|
149
|
+
// Ensure page_size is within bounds
|
|
150
|
+
const effective_page_size = Math.min(Math.max(1, page_size), 100);
|
|
151
|
+
const effective_page = Math.max(1, page);
|
|
152
|
+
|
|
153
|
+
if (!resolved) {
|
|
154
|
+
return {
|
|
155
|
+
photos: [],
|
|
156
|
+
total: 0,
|
|
157
|
+
page: effective_page,
|
|
158
|
+
page_size: effective_page_size,
|
|
159
|
+
has_more: false,
|
|
160
|
+
source: "project",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const category_path = path.join(resolved.path, category);
|
|
165
|
+
|
|
166
|
+
if (!fs.existsSync(category_path)) {
|
|
167
|
+
return {
|
|
168
|
+
photos: [],
|
|
169
|
+
total: 0,
|
|
170
|
+
page: effective_page,
|
|
171
|
+
page_size: effective_page_size,
|
|
172
|
+
has_more: false,
|
|
173
|
+
source: resolved.source,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const fileTypes = get_file_types_config();
|
|
179
|
+
const allowedExtensions = fileTypes.allowed_image_extensions.map(ext =>
|
|
180
|
+
ext.startsWith(".") ? ext.toLowerCase() : `.${ext.toLowerCase()}`
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const entries = fs.readdirSync(category_path, { withFileTypes: true });
|
|
184
|
+
const all_photos = entries
|
|
185
|
+
.filter((entry) => {
|
|
186
|
+
if (!entry.isFile()) return false;
|
|
187
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
188
|
+
return allowedExtensions.includes(ext);
|
|
189
|
+
})
|
|
190
|
+
.map((entry) => entry.name)
|
|
191
|
+
.sort();
|
|
192
|
+
|
|
193
|
+
const total = all_photos.length;
|
|
194
|
+
const start_index = (effective_page - 1) * effective_page_size;
|
|
195
|
+
const end_index = start_index + effective_page_size;
|
|
196
|
+
const page_photos = all_photos.slice(start_index, end_index);
|
|
197
|
+
|
|
198
|
+
// Generate URLs based on source
|
|
199
|
+
// For node_modules source, we need to serve via API route
|
|
200
|
+
const photo_urls = page_photos.map((filename) => {
|
|
201
|
+
if (resolved.source === "node_modules") {
|
|
202
|
+
// Serve via API route that reads from node_modules
|
|
203
|
+
return `/api/hazo_auth/library_photo/${category}/${filename}`;
|
|
204
|
+
} else {
|
|
205
|
+
// Serve directly from public folder
|
|
206
|
+
return `${config.library_photo_path}/${category}/${filename}`;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
photos: photo_urls,
|
|
212
|
+
total,
|
|
213
|
+
page: effective_page,
|
|
214
|
+
page_size: effective_page_size,
|
|
215
|
+
has_more: end_index < total,
|
|
216
|
+
source: resolved.source,
|
|
217
|
+
};
|
|
218
|
+
} catch (error) {
|
|
219
|
+
const logger = create_app_logger();
|
|
220
|
+
const error_message = error instanceof Error ? error.message : "Unknown error";
|
|
221
|
+
logger.warn("profile_picture_service_read_photos_failed", {
|
|
222
|
+
filename: "profile_picture_service.ts",
|
|
223
|
+
line_number: 0,
|
|
224
|
+
category,
|
|
225
|
+
category_path,
|
|
226
|
+
source: resolved.source,
|
|
227
|
+
error: error_message,
|
|
228
|
+
});
|
|
229
|
+
return {
|
|
230
|
+
photos: [],
|
|
231
|
+
total: 0,
|
|
232
|
+
page: effective_page,
|
|
233
|
+
page_size: effective_page_size,
|
|
234
|
+
has_more: false,
|
|
235
|
+
source: resolved.source,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Gets photos in a specific library category (legacy non-paginated version)
|
|
242
|
+
* @param category - Category name
|
|
243
|
+
* @returns Array of photo URLs (relative to public directory or API route)
|
|
244
|
+
*/
|
|
245
|
+
export function get_library_photos(category: string): string[] {
|
|
246
|
+
// Use paginated version with large page size for backwards compatibility
|
|
247
|
+
const result = get_library_photos_paginated(category, 1, 1000);
|
|
248
|
+
return result.photos;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Gets the physical file path for a library photo (used for serving from node_modules)
|
|
253
|
+
* @param category - Category name
|
|
254
|
+
* @param filename - Photo filename
|
|
255
|
+
* @returns Full file path or null if not found
|
|
256
|
+
*/
|
|
257
|
+
export function get_library_photo_path(category: string, filename: string): string | null {
|
|
258
|
+
const resolved = resolve_library_path();
|
|
259
|
+
|
|
260
|
+
if (!resolved) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const photo_path = path.join(resolved.path, category, filename);
|
|
265
|
+
|
|
266
|
+
if (fs.existsSync(photo_path)) {
|
|
267
|
+
return photo_path;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Gets the source of library photos (for diagnostic purposes)
|
|
275
|
+
* @returns Source type or null if no library found
|
|
276
|
+
*/
|
|
277
|
+
export function get_library_source(): "project" | "node_modules" | null {
|
|
278
|
+
const resolved = resolve_library_path();
|
|
279
|
+
return resolved?.source ?? null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Clears the library path cache (useful for testing or after copying files)
|
|
284
|
+
*/
|
|
285
|
+
export function clear_library_cache(): void {
|
|
286
|
+
cached_library_path = null;
|
|
287
|
+
cached_library_source = null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Gets default profile picture based on configuration priority
|
|
292
|
+
* @param user_email - User's email address
|
|
293
|
+
* @param user_name - User's name (optional)
|
|
294
|
+
* @returns Default profile picture URL and source, or null if no default available
|
|
295
|
+
*/
|
|
296
|
+
/**
|
|
297
|
+
* Checks if a Gravatar exists for the given email by making a HEAD request
|
|
298
|
+
* @param email - User email address
|
|
299
|
+
* @returns Promise<boolean> - true if Gravatar exists (status 200), false otherwise (404 or error)
|
|
300
|
+
*/
|
|
301
|
+
async function check_gravatar_exists(email: string): Promise<boolean> {
|
|
302
|
+
try {
|
|
303
|
+
const uiSizes = get_ui_sizes_config();
|
|
304
|
+
const gravatar_url = get_gravatar_url(email, uiSizes.gravatar_size);
|
|
305
|
+
|
|
306
|
+
// Make HEAD request to check if image exists without downloading it
|
|
307
|
+
const response = await fetch(gravatar_url, {
|
|
308
|
+
method: 'HEAD',
|
|
309
|
+
// Add timeout to prevent hanging
|
|
310
|
+
signal: AbortSignal.timeout(5000) // 5 second timeout
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Gravatar returns 200 if user has an image, 404 if using default/mystery-person
|
|
314
|
+
return response.ok;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
// If fetch fails (network error, timeout, etc.), assume no Gravatar
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Gets a random image from a random category in the library
|
|
323
|
+
* @returns string | null - Random library photo URL or null if no photos available
|
|
324
|
+
*/
|
|
325
|
+
function get_random_library_photo(): string | null {
|
|
326
|
+
const categories = get_library_categories();
|
|
327
|
+
|
|
328
|
+
if (categories.length === 0) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Pick a random category
|
|
333
|
+
const random_category = categories[Math.floor(Math.random() * categories.length)];
|
|
334
|
+
|
|
335
|
+
// Get photos from that category
|
|
336
|
+
const photos = get_library_photos(random_category);
|
|
337
|
+
|
|
338
|
+
if (photos.length === 0) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Pick a random photo from that category
|
|
343
|
+
const random_photo = photos[Math.floor(Math.random() * photos.length)];
|
|
344
|
+
|
|
345
|
+
return random_photo;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export async function get_default_profile_picture(
|
|
349
|
+
user_email: string,
|
|
350
|
+
user_name?: string,
|
|
351
|
+
): Promise<DefaultProfilePictureResult | null> {
|
|
352
|
+
const config = get_profile_picture_config();
|
|
353
|
+
|
|
354
|
+
if (!config.user_photo_default) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const uiSizes = get_ui_sizes_config();
|
|
359
|
+
|
|
360
|
+
// Try priority 1
|
|
361
|
+
if (config.user_photo_default_priority1 === "gravatar") {
|
|
362
|
+
// Check if Gravatar actually exists for this email
|
|
363
|
+
const gravatar_exists = await check_gravatar_exists(user_email);
|
|
364
|
+
|
|
365
|
+
if (gravatar_exists) {
|
|
366
|
+
const gravatar_url = get_gravatar_url(user_email, uiSizes.gravatar_size);
|
|
367
|
+
return {
|
|
368
|
+
profile_picture_url: gravatar_url,
|
|
369
|
+
profile_source: "gravatar",
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
// If Gravatar doesn't exist, fall through to priority 2
|
|
373
|
+
} else if (config.user_photo_default_priority1 === "library") {
|
|
374
|
+
// Use random library photo instead of first photo
|
|
375
|
+
const random_photo = get_random_library_photo();
|
|
376
|
+
if (random_photo) {
|
|
377
|
+
return {
|
|
378
|
+
profile_picture_url: random_photo,
|
|
379
|
+
profile_source: "library",
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Try priority 2 if priority 1 didn't work (only if priority2 is different from priority1)
|
|
385
|
+
const priority1 = config.user_photo_default_priority1;
|
|
386
|
+
const priority2 = config.user_photo_default_priority2;
|
|
387
|
+
|
|
388
|
+
if (priority2 && priority2 !== priority1) {
|
|
389
|
+
if (priority2 === "gravatar") {
|
|
390
|
+
// Check if Gravatar actually exists for this email
|
|
391
|
+
const gravatar_exists = await check_gravatar_exists(user_email);
|
|
392
|
+
|
|
393
|
+
if (gravatar_exists) {
|
|
394
|
+
const gravatar_url = get_gravatar_url(user_email, uiSizes.gravatar_size);
|
|
395
|
+
return {
|
|
396
|
+
profile_picture_url: gravatar_url,
|
|
397
|
+
profile_source: "gravatar",
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
} else if (priority2 === "library") {
|
|
401
|
+
// Use random library photo instead of first photo
|
|
402
|
+
const random_photo = get_random_library_photo();
|
|
403
|
+
if (random_photo) {
|
|
404
|
+
return {
|
|
405
|
+
profile_picture_url: random_photo,
|
|
406
|
+
profile_source: "library",
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// No default photo available
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Updates user profile picture in database
|
|
418
|
+
* @param adapter - The hazo_connect adapter instance
|
|
419
|
+
* @param user_id - User ID
|
|
420
|
+
* @param profile_picture_url - Profile picture URL
|
|
421
|
+
* @param profile_source - Profile picture source type
|
|
422
|
+
* @returns Success status
|
|
423
|
+
*/
|
|
424
|
+
export async function update_user_profile_picture(
|
|
425
|
+
adapter: HazoConnectAdapter,
|
|
426
|
+
user_id: string,
|
|
427
|
+
profile_picture_url: string,
|
|
428
|
+
profile_source: ProfilePictureSource,
|
|
429
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
430
|
+
try {
|
|
431
|
+
const users_service = createCrudService(adapter, "hazo_users");
|
|
432
|
+
const now = new Date().toISOString();
|
|
433
|
+
|
|
434
|
+
// Map UI source value to database enum value
|
|
435
|
+
const db_profile_source = map_ui_source_to_db(profile_source);
|
|
436
|
+
|
|
437
|
+
await users_service.updateById(user_id, {
|
|
438
|
+
profile_picture_url,
|
|
439
|
+
profile_source: db_profile_source,
|
|
440
|
+
changed_at: now,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
return { success: true };
|
|
444
|
+
} catch (error) {
|
|
445
|
+
const error_message = error instanceof Error ? error.message : "Unknown error";
|
|
446
|
+
return {
|
|
447
|
+
success: false,
|
|
448
|
+
error: error_message,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// file_description: helper to map between UI profile picture source values and database enum values
|
|
2
|
+
// section: types
|
|
3
|
+
/**
|
|
4
|
+
* UI representation of profile picture source
|
|
5
|
+
* Used in components and API interfaces
|
|
6
|
+
*/
|
|
7
|
+
export type ProfilePictureSourceUI = "upload" | "library" | "gravatar" | "custom";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Database enum values for profile_source
|
|
11
|
+
* Must match the CHECK constraint in the database
|
|
12
|
+
*/
|
|
13
|
+
export type ProfilePictureSourceDB = "gravatar" | "custom" | "predefined";
|
|
14
|
+
|
|
15
|
+
// section: helpers
|
|
16
|
+
/**
|
|
17
|
+
* Maps UI profile picture source to database enum value
|
|
18
|
+
* @param uiSource - UI representation of source ("upload", "library", "gravatar", "custom")
|
|
19
|
+
* @returns Database enum value ("gravatar", "custom", "predefined")
|
|
20
|
+
*/
|
|
21
|
+
export function map_ui_source_to_db(uiSource: ProfilePictureSourceUI): ProfilePictureSourceDB {
|
|
22
|
+
switch (uiSource) {
|
|
23
|
+
case "upload":
|
|
24
|
+
return "custom"; // User uploaded their own photo
|
|
25
|
+
case "library":
|
|
26
|
+
return "predefined"; // User selected from predefined library
|
|
27
|
+
case "gravatar":
|
|
28
|
+
return "gravatar"; // User's Gravatar
|
|
29
|
+
case "custom":
|
|
30
|
+
return "custom"; // Already in database format
|
|
31
|
+
default:
|
|
32
|
+
// Fallback to custom for unknown values
|
|
33
|
+
return "custom";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Maps database enum value to UI representation
|
|
39
|
+
* @param dbSource - Database enum value ("gravatar", "custom", "predefined")
|
|
40
|
+
* @returns UI representation ("upload", "library", "gravatar", "custom")
|
|
41
|
+
*/
|
|
42
|
+
export function map_db_source_to_ui(dbSource: ProfilePictureSourceDB | string | null | undefined): ProfilePictureSourceUI {
|
|
43
|
+
if (!dbSource) {
|
|
44
|
+
return "custom"; // Default fallback
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
switch (dbSource) {
|
|
48
|
+
case "gravatar":
|
|
49
|
+
return "gravatar";
|
|
50
|
+
case "custom":
|
|
51
|
+
return "upload"; // Map custom to upload in UI (user uploaded their own)
|
|
52
|
+
case "predefined":
|
|
53
|
+
return "library"; // Map predefined to library in UI (user selected from library)
|
|
54
|
+
default:
|
|
55
|
+
// For unknown values, try to return as-is if it matches UI format
|
|
56
|
+
if (dbSource === "upload" || dbSource === "library") {
|
|
57
|
+
return dbSource as ProfilePictureSourceUI;
|
|
58
|
+
}
|
|
59
|
+
return "custom"; // Fallback
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// file_description: service for user registration operations using hazo_connect
|
|
2
|
+
// section: imports
|
|
3
|
+
import type { HazoConnectAdapter } from "hazo_connect";
|
|
4
|
+
import { createCrudService } from "hazo_connect/server";
|
|
5
|
+
import argon2 from "argon2";
|
|
6
|
+
import { randomUUID } from "crypto";
|
|
7
|
+
import { create_token } from "./token_service.js";
|
|
8
|
+
import { get_default_profile_picture } from "./profile_picture_service.js";
|
|
9
|
+
import { get_profile_picture_config } from "../profile_picture_config.server.js";
|
|
10
|
+
import { map_ui_source_to_db } from "./profile_picture_source_mapper.js";
|
|
11
|
+
import { create_app_logger } from "../app_logger.js";
|
|
12
|
+
import { send_template_email } from "./email_service.js";
|
|
13
|
+
import { sanitize_error_for_user } from "../utils/error_sanitizer.js";
|
|
14
|
+
import { get_filename, get_line_number } from "../utils/api_route_helpers.js";
|
|
15
|
+
|
|
16
|
+
// section: types
|
|
17
|
+
export type RegistrationData = {
|
|
18
|
+
email: string;
|
|
19
|
+
password: string;
|
|
20
|
+
name?: string;
|
|
21
|
+
url_on_logon?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type RegistrationResult = {
|
|
25
|
+
success: boolean;
|
|
26
|
+
user_id?: string;
|
|
27
|
+
error?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// section: helpers
|
|
31
|
+
/**
|
|
32
|
+
* Registers a new user in the database using hazo_connect
|
|
33
|
+
* @param adapter - The hazo_connect adapter instance
|
|
34
|
+
* @param data - Registration data (email, password, optional name)
|
|
35
|
+
* @returns Registration result with success status and user_id or error
|
|
36
|
+
*/
|
|
37
|
+
export async function register_user(
|
|
38
|
+
adapter: HazoConnectAdapter,
|
|
39
|
+
data: RegistrationData,
|
|
40
|
+
): Promise<RegistrationResult> {
|
|
41
|
+
try {
|
|
42
|
+
const { email, password, name, url_on_logon } = data;
|
|
43
|
+
|
|
44
|
+
// Create CRUD service for hazo_users table
|
|
45
|
+
const users_service = createCrudService(adapter, "hazo_users");
|
|
46
|
+
|
|
47
|
+
// Check if user already exists
|
|
48
|
+
const existing_users = await users_service.findBy({
|
|
49
|
+
email_address: email,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (Array.isArray(existing_users) && existing_users.length > 0) {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
error: "Email address already registered",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Hash password using argon2
|
|
60
|
+
const password_hash = await argon2.hash(password);
|
|
61
|
+
|
|
62
|
+
// Generate user ID
|
|
63
|
+
const user_id = randomUUID();
|
|
64
|
+
const now = new Date().toISOString();
|
|
65
|
+
|
|
66
|
+
// Insert user into database using CRUD service
|
|
67
|
+
const insert_data: Record<string, unknown> = {
|
|
68
|
+
id: user_id,
|
|
69
|
+
email_address: email,
|
|
70
|
+
password_hash: password_hash,
|
|
71
|
+
email_verified: false,
|
|
72
|
+
is_active: true,
|
|
73
|
+
login_attempts: 0,
|
|
74
|
+
auth_providers: "email", // Track that this user registered with email/password
|
|
75
|
+
created_at: now,
|
|
76
|
+
changed_at: now,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Include name if provided
|
|
80
|
+
if (name) {
|
|
81
|
+
insert_data.name = name;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Validate and include url_on_logon if provided
|
|
85
|
+
if (url_on_logon) {
|
|
86
|
+
// Ensure it's a relative path starting with / but not //
|
|
87
|
+
if (url_on_logon.startsWith("/") && !url_on_logon.startsWith("//")) {
|
|
88
|
+
insert_data.url_on_logon = url_on_logon;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Set default profile picture if enabled
|
|
93
|
+
const profile_picture_config = get_profile_picture_config();
|
|
94
|
+
if (profile_picture_config.user_photo_default) {
|
|
95
|
+
const default_photo = await get_default_profile_picture(email, name);
|
|
96
|
+
if (default_photo) {
|
|
97
|
+
insert_data.profile_picture_url = default_photo.profile_picture_url;
|
|
98
|
+
// Map UI source value to database enum value
|
|
99
|
+
insert_data.profile_source = map_ui_source_to_db(default_photo.profile_source);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const inserted_users = await users_service.insert(insert_data);
|
|
104
|
+
|
|
105
|
+
// Verify insertion was successful
|
|
106
|
+
if (!Array.isArray(inserted_users) || inserted_users.length === 0) {
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: "Failed to create user account",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Create email verification token for the new user
|
|
114
|
+
const token_result = await create_token({
|
|
115
|
+
adapter,
|
|
116
|
+
user_id,
|
|
117
|
+
token_type: "email_verification",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (!token_result.success) {
|
|
121
|
+
// Log error but don't fail registration - token can be resent later
|
|
122
|
+
const logger = create_app_logger();
|
|
123
|
+
const error_message = token_result.error || "Unknown error";
|
|
124
|
+
logger.error("registration_service_token_creation_failed", {
|
|
125
|
+
filename: "registration_service.ts",
|
|
126
|
+
line_number: 0,
|
|
127
|
+
user_id,
|
|
128
|
+
error: error_message,
|
|
129
|
+
note: "This may be due to missing token_type column in hazo_refresh_tokens table. Please ensure migration 001_add_token_type_to_refresh_tokens.sql has been applied.",
|
|
130
|
+
});
|
|
131
|
+
} else {
|
|
132
|
+
const logger = create_app_logger();
|
|
133
|
+
logger.info("registration_service_token_created", {
|
|
134
|
+
filename: "registration_service.ts",
|
|
135
|
+
line_number: 0,
|
|
136
|
+
user_id,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Send verification email if token was created successfully
|
|
141
|
+
if (token_result.success && token_result.raw_token) {
|
|
142
|
+
const email_result = await send_template_email("email_verification", email, {
|
|
143
|
+
token: token_result.raw_token,
|
|
144
|
+
user_email: email,
|
|
145
|
+
user_name: name,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (!email_result.success) {
|
|
149
|
+
const logger = create_app_logger();
|
|
150
|
+
logger.error("registration_service_email_send_failed", {
|
|
151
|
+
filename: "registration_service.ts",
|
|
152
|
+
line_number: 0,
|
|
153
|
+
user_id,
|
|
154
|
+
email,
|
|
155
|
+
error: email_result.error,
|
|
156
|
+
note: "User registration succeeded but verification email failed to send",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
success: true,
|
|
163
|
+
user_id,
|
|
164
|
+
};
|
|
165
|
+
} catch (error) {
|
|
166
|
+
const logger = create_app_logger();
|
|
167
|
+
const user_friendly_error = sanitize_error_for_user(error, {
|
|
168
|
+
logToConsole: true,
|
|
169
|
+
logToLogger: true,
|
|
170
|
+
logger,
|
|
171
|
+
context: {
|
|
172
|
+
filename: "registration_service.ts",
|
|
173
|
+
line_number: get_line_number(),
|
|
174
|
+
email: data.email,
|
|
175
|
+
operation: "register_user",
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
success: false,
|
|
181
|
+
error: user_friendly_error,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|