hazo_auth 4.1.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +230 -0
  2. package/SETUP_CHECKLIST.md +202 -0
  3. package/bin/hazo_auth.mjs +35 -0
  4. package/cli-src/assets/images/forgot_password_default.jpg +0 -0
  5. package/cli-src/assets/images/login_default.jpg +0 -0
  6. package/cli-src/assets/images/register_default.jpg +0 -0
  7. package/cli-src/assets/images/reset_password_default.jpg +0 -0
  8. package/cli-src/assets/images/verify_email_default.jpg +0 -0
  9. package/cli-src/cli/generate.ts +276 -0
  10. package/cli-src/cli/index.ts +207 -0
  11. package/cli-src/cli/init.ts +254 -0
  12. package/cli-src/cli/init_users.ts +376 -0
  13. package/cli-src/cli/validate.ts +581 -0
  14. package/cli-src/lib/already_logged_in_config.server.ts +46 -0
  15. package/cli-src/lib/app_logger.ts +24 -0
  16. package/cli-src/lib/auth/auth_cache.ts +220 -0
  17. package/cli-src/lib/auth/auth_rate_limiter.ts +121 -0
  18. package/cli-src/lib/auth/auth_types.ts +110 -0
  19. package/cli-src/lib/auth/auth_utils.server.ts +196 -0
  20. package/cli-src/lib/auth/hazo_get_auth.server.ts +512 -0
  21. package/cli-src/lib/auth/index.ts +23 -0
  22. package/cli-src/lib/auth/nextauth_config.ts +227 -0
  23. package/cli-src/lib/auth/scope_cache.ts +233 -0
  24. package/cli-src/lib/auth/server_auth.ts +88 -0
  25. package/cli-src/lib/auth/session_token_validator.edge.ts +91 -0
  26. package/cli-src/lib/auth_utility_config.server.ts +136 -0
  27. package/cli-src/lib/config/config_loader.server.ts +164 -0
  28. package/cli-src/lib/config/default_config.ts +199 -0
  29. package/cli-src/lib/email_verification_config.server.ts +63 -0
  30. package/cli-src/lib/file_types_config.server.ts +25 -0
  31. package/cli-src/lib/forgot_password_config.server.ts +63 -0
  32. package/cli-src/lib/hazo_connect_instance.server.ts +101 -0
  33. package/cli-src/lib/hazo_connect_setup.server.ts +194 -0
  34. package/cli-src/lib/hazo_connect_setup.ts +54 -0
  35. package/cli-src/lib/index.ts +46 -0
  36. package/cli-src/lib/login_config.server.ts +106 -0
  37. package/cli-src/lib/messages_config.server.ts +45 -0
  38. package/cli-src/lib/migrations/apply_migration.ts +105 -0
  39. package/cli-src/lib/my_settings_config.server.ts +135 -0
  40. package/cli-src/lib/oauth_config.server.ts +87 -0
  41. package/cli-src/lib/password_requirements_config.server.ts +40 -0
  42. package/cli-src/lib/profile_pic_menu_config.server.ts +138 -0
  43. package/cli-src/lib/profile_picture_config.server.ts +56 -0
  44. package/cli-src/lib/register_config.server.ts +101 -0
  45. package/cli-src/lib/reset_password_config.server.ts +103 -0
  46. package/cli-src/lib/scope_hierarchy_config.server.ts +151 -0
  47. package/cli-src/lib/services/email_service.ts +587 -0
  48. package/cli-src/lib/services/email_verification_service.ts +270 -0
  49. package/cli-src/lib/services/index.ts +16 -0
  50. package/cli-src/lib/services/login_service.ts +150 -0
  51. package/cli-src/lib/services/oauth_service.ts +494 -0
  52. package/cli-src/lib/services/password_change_service.ts +154 -0
  53. package/cli-src/lib/services/password_reset_service.ts +418 -0
  54. package/cli-src/lib/services/profile_picture_remove_service.ts +120 -0
  55. package/cli-src/lib/services/profile_picture_service.ts +451 -0
  56. package/cli-src/lib/services/profile_picture_source_mapper.ts +62 -0
  57. package/cli-src/lib/services/registration_service.ts +185 -0
  58. package/cli-src/lib/services/scope_labels_service.ts +348 -0
  59. package/cli-src/lib/services/scope_service.ts +778 -0
  60. package/cli-src/lib/services/session_token_service.ts +177 -0
  61. package/cli-src/lib/services/token_service.ts +240 -0
  62. package/cli-src/lib/services/user_profiles_cache.ts +189 -0
  63. package/cli-src/lib/services/user_profiles_service.ts +264 -0
  64. package/cli-src/lib/services/user_scope_service.ts +554 -0
  65. package/cli-src/lib/services/user_update_service.ts +141 -0
  66. package/cli-src/lib/ui_shell_config.server.ts +73 -0
  67. package/cli-src/lib/ui_sizes_config.server.ts +37 -0
  68. package/cli-src/lib/user_fields_config.server.ts +31 -0
  69. package/cli-src/lib/user_management_config.server.ts +39 -0
  70. package/cli-src/lib/user_profiles_config.server.ts +55 -0
  71. package/cli-src/lib/utils/api_route_helpers.ts +60 -0
  72. package/cli-src/lib/utils/error_sanitizer.ts +75 -0
  73. package/cli-src/lib/utils/password_validator.ts +65 -0
  74. package/cli-src/lib/utils.ts +11 -0
  75. package/cli-src/server/logging/logger_service.ts +56 -0
  76. package/dist/app/api/hazo_auth/forgot_password/route.d.ts.map +1 -1
  77. package/dist/app/api/hazo_auth/forgot_password/route.js +15 -0
  78. package/dist/app/api/hazo_auth/logout/route.d.ts.map +1 -1
  79. package/dist/app/api/hazo_auth/logout/route.js +31 -0
  80. package/dist/app/api/hazo_auth/me/route.d.ts.map +1 -1
  81. package/dist/app/api/hazo_auth/me/route.js +10 -0
  82. package/dist/cli/index.js +18 -0
  83. package/dist/cli/init_users.d.ts +17 -0
  84. package/dist/cli/init_users.d.ts.map +1 -0
  85. package/dist/cli/init_users.js +307 -0
  86. package/dist/components/layouts/forgot_password/hooks/use_forgot_password_form.d.ts +2 -0
  87. package/dist/components/layouts/forgot_password/hooks/use_forgot_password_form.d.ts.map +1 -1
  88. package/dist/components/layouts/forgot_password/hooks/use_forgot_password_form.js +8 -0
  89. package/dist/components/layouts/forgot_password/index.d.ts +7 -1
  90. package/dist/components/layouts/forgot_password/index.d.ts.map +1 -1
  91. package/dist/components/layouts/forgot_password/index.js +7 -2
  92. package/dist/components/layouts/login/index.d.ts +13 -1
  93. package/dist/components/layouts/login/index.d.ts.map +1 -1
  94. package/dist/components/layouts/login/index.js +11 -2
  95. package/dist/components/layouts/my_settings/components/connected_accounts_section.d.ts +17 -0
  96. package/dist/components/layouts/my_settings/components/connected_accounts_section.d.ts.map +1 -0
  97. package/dist/components/layouts/my_settings/components/connected_accounts_section.js +17 -0
  98. package/dist/components/layouts/my_settings/components/set_password_section.d.ts +26 -0
  99. package/dist/components/layouts/my_settings/components/set_password_section.d.ts.map +1 -0
  100. package/dist/components/layouts/my_settings/components/set_password_section.js +127 -0
  101. package/dist/components/layouts/my_settings/hooks/use_my_settings.d.ts +3 -0
  102. package/dist/components/layouts/my_settings/hooks/use_my_settings.d.ts.map +1 -1
  103. package/dist/components/layouts/my_settings/hooks/use_my_settings.js +9 -0
  104. package/dist/components/layouts/my_settings/index.d.ts.map +1 -1
  105. package/dist/components/layouts/my_settings/index.js +4 -2
  106. package/dist/components/layouts/shared/components/google_icon.d.ts +12 -0
  107. package/dist/components/layouts/shared/components/google_icon.d.ts.map +1 -0
  108. package/dist/components/layouts/shared/components/google_icon.js +9 -0
  109. package/dist/components/layouts/shared/components/google_sign_in_button.d.ts +21 -0
  110. package/dist/components/layouts/shared/components/google_sign_in_button.d.ts.map +1 -0
  111. package/dist/components/layouts/shared/components/google_sign_in_button.js +50 -0
  112. package/dist/components/layouts/shared/components/oauth_divider.d.ts +13 -0
  113. package/dist/components/layouts/shared/components/oauth_divider.d.ts.map +1 -0
  114. package/dist/components/layouts/shared/components/oauth_divider.js +13 -0
  115. package/dist/components/layouts/shared/config/layout_customization.d.ts +2 -7
  116. package/dist/components/layouts/shared/config/layout_customization.d.ts.map +1 -1
  117. package/dist/components/layouts/shared/hooks/use_auth_status.d.ts +3 -0
  118. package/dist/components/layouts/shared/hooks/use_auth_status.d.ts.map +1 -1
  119. package/dist/components/layouts/shared/hooks/use_auth_status.js +4 -0
  120. package/dist/components/layouts/shared/index.d.ts +5 -0
  121. package/dist/components/layouts/shared/index.d.ts.map +1 -1
  122. package/dist/components/layouts/shared/index.js +3 -0
  123. package/dist/components/ui/button.d.ts +1 -1
  124. package/dist/lib/auth/nextauth_config.d.ts +34 -0
  125. package/dist/lib/auth/nextauth_config.d.ts.map +1 -0
  126. package/dist/lib/auth/nextauth_config.js +171 -0
  127. package/dist/lib/config/default_config.d.ts +24 -0
  128. package/dist/lib/config/default_config.d.ts.map +1 -1
  129. package/dist/lib/config/default_config.js +14 -0
  130. package/dist/lib/index.d.ts +2 -0
  131. package/dist/lib/index.d.ts.map +1 -1
  132. package/dist/lib/index.js +1 -0
  133. package/dist/lib/login_config.server.d.ts +3 -0
  134. package/dist/lib/login_config.server.d.ts.map +1 -1
  135. package/dist/lib/login_config.server.js +4 -0
  136. package/dist/lib/oauth_config.server.d.ts +29 -0
  137. package/dist/lib/oauth_config.server.d.ts.map +1 -0
  138. package/dist/lib/oauth_config.server.js +40 -0
  139. package/dist/lib/services/login_service.d.ts.map +1 -1
  140. package/dist/lib/services/login_service.js +16 -1
  141. package/dist/lib/services/oauth_service.d.ts +88 -0
  142. package/dist/lib/services/oauth_service.d.ts.map +1 -0
  143. package/dist/lib/services/oauth_service.js +376 -0
  144. package/dist/lib/services/password_reset_service.d.ts +2 -0
  145. package/dist/lib/services/password_reset_service.d.ts.map +1 -1
  146. package/dist/lib/services/password_reset_service.js +10 -0
  147. package/dist/lib/services/registration_service.d.ts.map +1 -1
  148. package/dist/lib/services/registration_service.js +1 -0
  149. package/dist/lib/utils/password_validator.d.ts +19 -0
  150. package/dist/lib/utils/password_validator.d.ts.map +1 -0
  151. package/dist/lib/utils/password_validator.js +36 -0
  152. package/dist/server_pages/login.d.ts.map +1 -1
  153. package/dist/server_pages/login.js +6 -1
  154. package/dist/server_pages/login_client_wrapper.d.ts +5 -2
  155. package/dist/server_pages/login_client_wrapper.d.ts.map +1 -1
  156. package/dist/server_pages/login_client_wrapper.js +2 -2
  157. package/package.json +6 -2
@@ -0,0 +1,494 @@
1
+ // file_description: service for OAuth authentication operations using hazo_connect
2
+ // section: imports
3
+ import type { HazoConnectAdapter } from "hazo_connect";
4
+ import { createCrudService } from "hazo_connect/server";
5
+ import { randomUUID } from "crypto";
6
+ import { create_app_logger } from "../app_logger";
7
+ import { sanitize_error_for_user } from "../utils/error_sanitizer";
8
+ import { get_line_number } from "../utils/api_route_helpers";
9
+ import { get_oauth_config } from "../oauth_config.server";
10
+
11
+ // section: types
12
+ export type GoogleOAuthData = {
13
+ /** Google's unique user ID (sub claim from JWT) */
14
+ google_id: string;
15
+ /** User's email address from Google */
16
+ email: string;
17
+ /** User's full name from Google profile */
18
+ name?: string;
19
+ /** User's profile picture URL from Google */
20
+ profile_picture_url?: string;
21
+ /** Whether Google has verified this email */
22
+ email_verified: boolean;
23
+ };
24
+
25
+ export type OAuthLoginResult = {
26
+ success: boolean;
27
+ user_id?: string;
28
+ /** True if this was a newly created account */
29
+ is_new_user?: boolean;
30
+ /** True if Google was linked to an existing account */
31
+ was_linked?: boolean;
32
+ /** The user's email address */
33
+ email?: string;
34
+ /** The user's name */
35
+ name?: string;
36
+ error?: string;
37
+ };
38
+
39
+ export type LinkGoogleResult = {
40
+ success: boolean;
41
+ error?: string;
42
+ };
43
+
44
+ export type AuthProvidersResult = {
45
+ success: boolean;
46
+ auth_providers?: string[];
47
+ has_password?: boolean;
48
+ error?: string;
49
+ };
50
+
51
+ // section: helpers
52
+ /**
53
+ * Handles Google OAuth login/registration flow
54
+ * 1. Check if user exists with google_id -> login
55
+ * 2. Check if user exists with email -> link Google account
56
+ * 3. Create new user with Google data
57
+ *
58
+ * @param adapter - The hazo_connect adapter instance
59
+ * @param data - Google OAuth user data
60
+ * @returns OAuth login result with user_id and status flags
61
+ */
62
+ export async function handle_google_oauth_login(
63
+ adapter: HazoConnectAdapter,
64
+ data: GoogleOAuthData
65
+ ): Promise<OAuthLoginResult> {
66
+ const logger = create_app_logger();
67
+
68
+ try {
69
+ const { google_id, email, name, profile_picture_url, email_verified } = data;
70
+ const oauth_config = get_oauth_config();
71
+
72
+ const users_service = createCrudService(adapter, "hazo_users");
73
+ const now = new Date().toISOString();
74
+
75
+ // Step 1: Check if user exists with this google_id
76
+ const users_by_google_id = await users_service.findBy({ google_id });
77
+
78
+ if (Array.isArray(users_by_google_id) && users_by_google_id.length > 0) {
79
+ const user = users_by_google_id[0];
80
+
81
+ // Update last_logon timestamp
82
+ await users_service.updateById(user.id, {
83
+ last_logon: now,
84
+ changed_at: now,
85
+ });
86
+
87
+ logger.info("oauth_service_google_login_existing_google_user", {
88
+ filename: "oauth_service.ts",
89
+ line_number: get_line_number(),
90
+ user_id: user.id,
91
+ email: user.email_address,
92
+ });
93
+
94
+ return {
95
+ success: true,
96
+ user_id: user.id as string,
97
+ is_new_user: false,
98
+ was_linked: false,
99
+ email: user.email_address as string,
100
+ name: user.name as string | undefined,
101
+ };
102
+ }
103
+
104
+ // Step 2: Check if user exists with this email
105
+ const users_by_email = await users_service.findBy({ email_address: email });
106
+
107
+ if (Array.isArray(users_by_email) && users_by_email.length > 0) {
108
+ const user = users_by_email[0];
109
+ const user_email_verified = user.email_verified as boolean;
110
+
111
+ // Check if auto-linking is enabled for unverified accounts
112
+ if (!user_email_verified && !oauth_config.auto_link_unverified_accounts) {
113
+ return {
114
+ success: false,
115
+ error: "An account with this email exists but is not verified. Please verify your email first.",
116
+ };
117
+ }
118
+
119
+ // Link Google account to existing user
120
+ const current_auth_providers = (user.auth_providers as string) || "email";
121
+ const new_auth_providers = current_auth_providers.includes("google")
122
+ ? current_auth_providers
123
+ : `${current_auth_providers},google`;
124
+
125
+ const update_data: Record<string, unknown> = {
126
+ google_id,
127
+ auth_providers: new_auth_providers,
128
+ last_logon: now,
129
+ changed_at: now,
130
+ };
131
+
132
+ // If user was unverified and Google verified the email, mark as verified
133
+ if (!user_email_verified && email_verified) {
134
+ update_data.email_verified = true;
135
+ logger.info("oauth_service_auto_verified_email", {
136
+ filename: "oauth_service.ts",
137
+ line_number: get_line_number(),
138
+ user_id: user.id,
139
+ email,
140
+ });
141
+ }
142
+
143
+ // Update name if not set and Google provides one
144
+ if (!user.name && name) {
145
+ update_data.name = name;
146
+ }
147
+
148
+ // Update profile picture if not set and Google provides one
149
+ if (!user.profile_picture_url && profile_picture_url) {
150
+ update_data.profile_picture_url = profile_picture_url;
151
+ update_data.profile_source = "custom"; // Use 'custom' for external URLs (Google profile pics)
152
+ }
153
+
154
+ await users_service.updateById(user.id, update_data);
155
+
156
+ logger.info("oauth_service_google_linked_to_existing", {
157
+ filename: "oauth_service.ts",
158
+ line_number: get_line_number(),
159
+ user_id: user.id,
160
+ email,
161
+ was_unverified: !user_email_verified,
162
+ });
163
+
164
+ return {
165
+ success: true,
166
+ user_id: user.id as string,
167
+ is_new_user: false,
168
+ was_linked: true,
169
+ email: user.email_address as string,
170
+ name: (update_data.name as string) || (user.name as string | undefined),
171
+ };
172
+ }
173
+
174
+ // Step 3: Create new user with Google data
175
+ const user_id = randomUUID();
176
+
177
+ const insert_data: Record<string, unknown> = {
178
+ id: user_id,
179
+ email_address: email,
180
+ password_hash: "", // Empty string for Google-only users
181
+ email_verified: email_verified, // Trust Google's verification
182
+ is_active: true,
183
+ login_attempts: 0,
184
+ google_id,
185
+ auth_providers: "google",
186
+ created_at: now,
187
+ changed_at: now,
188
+ last_logon: now,
189
+ };
190
+
191
+ if (name) {
192
+ insert_data.name = name;
193
+ }
194
+
195
+ if (profile_picture_url) {
196
+ insert_data.profile_picture_url = profile_picture_url;
197
+ insert_data.profile_source = "custom"; // Use 'custom' for external URLs (Google profile pics)
198
+ }
199
+
200
+ const inserted_users = await users_service.insert(insert_data);
201
+
202
+ if (!Array.isArray(inserted_users) || inserted_users.length === 0) {
203
+ return {
204
+ success: false,
205
+ error: "Failed to create user account",
206
+ };
207
+ }
208
+
209
+ logger.info("oauth_service_google_new_user_created", {
210
+ filename: "oauth_service.ts",
211
+ line_number: get_line_number(),
212
+ user_id,
213
+ email,
214
+ });
215
+
216
+ return {
217
+ success: true,
218
+ user_id,
219
+ is_new_user: true,
220
+ was_linked: false,
221
+ email,
222
+ name,
223
+ };
224
+ } catch (error) {
225
+ const user_friendly_error = sanitize_error_for_user(error, {
226
+ logToConsole: true,
227
+ logToLogger: true,
228
+ logger,
229
+ context: {
230
+ filename: "oauth_service.ts",
231
+ line_number: get_line_number(),
232
+ email: data.email,
233
+ operation: "handle_google_oauth_login",
234
+ },
235
+ });
236
+
237
+ return {
238
+ success: false,
239
+ error: user_friendly_error,
240
+ };
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Links a Google account to an existing user
246
+ * @param adapter - The hazo_connect adapter instance
247
+ * @param user_id - The user's ID
248
+ * @param google_id - Google's unique user ID
249
+ * @returns Result indicating success or failure
250
+ */
251
+ export async function link_google_account(
252
+ adapter: HazoConnectAdapter,
253
+ user_id: string,
254
+ google_id: string
255
+ ): Promise<LinkGoogleResult> {
256
+ const logger = create_app_logger();
257
+
258
+ try {
259
+ const users_service = createCrudService(adapter, "hazo_users");
260
+ const now = new Date().toISOString();
261
+
262
+ // Get current user
263
+ const users = await users_service.findBy({ id: user_id });
264
+
265
+ if (!Array.isArray(users) || users.length === 0) {
266
+ return {
267
+ success: false,
268
+ error: "User not found",
269
+ };
270
+ }
271
+
272
+ const user = users[0];
273
+
274
+ // Check if Google is already linked
275
+ if (user.google_id) {
276
+ return {
277
+ success: false,
278
+ error: "Google account is already linked",
279
+ };
280
+ }
281
+
282
+ // Update auth_providers
283
+ const current_auth_providers = (user.auth_providers as string) || "email";
284
+ const new_auth_providers = current_auth_providers.includes("google")
285
+ ? current_auth_providers
286
+ : `${current_auth_providers},google`;
287
+
288
+ await users_service.updateById(user_id, {
289
+ google_id,
290
+ auth_providers: new_auth_providers,
291
+ changed_at: now,
292
+ });
293
+
294
+ logger.info("oauth_service_google_account_linked", {
295
+ filename: "oauth_service.ts",
296
+ line_number: get_line_number(),
297
+ user_id,
298
+ });
299
+
300
+ return { success: true };
301
+ } catch (error) {
302
+ const user_friendly_error = sanitize_error_for_user(error, {
303
+ logToConsole: true,
304
+ logToLogger: true,
305
+ logger,
306
+ context: {
307
+ filename: "oauth_service.ts",
308
+ line_number: get_line_number(),
309
+ user_id,
310
+ operation: "link_google_account",
311
+ },
312
+ });
313
+
314
+ return {
315
+ success: false,
316
+ error: user_friendly_error,
317
+ };
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Checks if a user has a password set (non-empty password_hash)
323
+ * @param adapter - The hazo_connect adapter instance
324
+ * @param user_id - The user's ID
325
+ * @returns True if user has a password set
326
+ */
327
+ export async function user_has_password(
328
+ adapter: HazoConnectAdapter,
329
+ user_id: string
330
+ ): Promise<boolean> {
331
+ try {
332
+ const users_service = createCrudService(adapter, "hazo_users");
333
+ const users = await users_service.findBy({ id: user_id });
334
+
335
+ if (!Array.isArray(users) || users.length === 0) {
336
+ return false;
337
+ }
338
+
339
+ const password_hash = users[0].password_hash as string;
340
+ return password_hash !== null && password_hash !== undefined && password_hash !== "";
341
+ } catch {
342
+ return false;
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Checks if a user has a password set by email
348
+ * @param adapter - The hazo_connect adapter instance
349
+ * @param email - The user's email address
350
+ * @returns True if user has a password set
351
+ */
352
+ export async function user_has_password_by_email(
353
+ adapter: HazoConnectAdapter,
354
+ email: string
355
+ ): Promise<boolean> {
356
+ try {
357
+ const users_service = createCrudService(adapter, "hazo_users");
358
+ const users = await users_service.findBy({ email_address: email });
359
+
360
+ if (!Array.isArray(users) || users.length === 0) {
361
+ return false;
362
+ }
363
+
364
+ const password_hash = users[0].password_hash as string;
365
+ return password_hash !== null && password_hash !== undefined && password_hash !== "";
366
+ } catch {
367
+ return false;
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Gets a user's authentication providers and password status
373
+ * @param adapter - The hazo_connect adapter instance
374
+ * @param user_id - The user's ID
375
+ * @returns Auth providers array and has_password flag
376
+ */
377
+ export async function get_user_auth_providers(
378
+ adapter: HazoConnectAdapter,
379
+ user_id: string
380
+ ): Promise<AuthProvidersResult> {
381
+ try {
382
+ const users_service = createCrudService(adapter, "hazo_users");
383
+ const users = await users_service.findBy({ id: user_id });
384
+
385
+ if (!Array.isArray(users) || users.length === 0) {
386
+ return {
387
+ success: false,
388
+ error: "User not found",
389
+ };
390
+ }
391
+
392
+ const user = users[0];
393
+ const auth_providers_str = (user.auth_providers as string) || "email";
394
+ const auth_providers = auth_providers_str.split(",").map((p) => p.trim());
395
+
396
+ const password_hash = user.password_hash as string;
397
+ const has_password = password_hash !== null && password_hash !== undefined && password_hash !== "";
398
+
399
+ return {
400
+ success: true,
401
+ auth_providers,
402
+ has_password,
403
+ };
404
+ } catch (error) {
405
+ const logger = create_app_logger();
406
+ const user_friendly_error = sanitize_error_for_user(error, {
407
+ logToConsole: true,
408
+ logToLogger: true,
409
+ logger,
410
+ context: {
411
+ filename: "oauth_service.ts",
412
+ line_number: get_line_number(),
413
+ user_id,
414
+ operation: "get_user_auth_providers",
415
+ },
416
+ });
417
+
418
+ return {
419
+ success: false,
420
+ error: user_friendly_error,
421
+ };
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Sets a password for a user who doesn't have one (e.g., Google-only users)
427
+ * @param adapter - The hazo_connect adapter instance
428
+ * @param user_id - The user's ID
429
+ * @param password_hash - The hashed password to set
430
+ * @returns Result indicating success or failure
431
+ */
432
+ export async function set_user_password(
433
+ adapter: HazoConnectAdapter,
434
+ user_id: string,
435
+ password_hash: string
436
+ ): Promise<{ success: boolean; error?: string }> {
437
+ const logger = create_app_logger();
438
+
439
+ try {
440
+ const users_service = createCrudService(adapter, "hazo_users");
441
+ const now = new Date().toISOString();
442
+
443
+ // Get current user
444
+ const users = await users_service.findBy({ id: user_id });
445
+
446
+ if (!Array.isArray(users) || users.length === 0) {
447
+ return {
448
+ success: false,
449
+ error: "User not found",
450
+ };
451
+ }
452
+
453
+ const user = users[0];
454
+
455
+ // Update password and auth_providers
456
+ const current_auth_providers = (user.auth_providers as string) || "";
457
+ const new_auth_providers = current_auth_providers.includes("email")
458
+ ? current_auth_providers
459
+ : current_auth_providers
460
+ ? `${current_auth_providers},email`
461
+ : "email";
462
+
463
+ await users_service.updateById(user_id, {
464
+ password_hash,
465
+ auth_providers: new_auth_providers,
466
+ changed_at: now,
467
+ });
468
+
469
+ logger.info("oauth_service_password_set", {
470
+ filename: "oauth_service.ts",
471
+ line_number: get_line_number(),
472
+ user_id,
473
+ });
474
+
475
+ return { success: true };
476
+ } catch (error) {
477
+ const user_friendly_error = sanitize_error_for_user(error, {
478
+ logToConsole: true,
479
+ logToLogger: true,
480
+ logger,
481
+ context: {
482
+ filename: "oauth_service.ts",
483
+ line_number: get_line_number(),
484
+ user_id,
485
+ operation: "set_user_password",
486
+ },
487
+ });
488
+
489
+ return {
490
+ success: false,
491
+ error: user_friendly_error,
492
+ };
493
+ }
494
+ }
@@ -0,0 +1,154 @@
1
+ // file_description: service for changing user password 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 { get_password_requirements_config } from "../password_requirements_config.server";
7
+ import { send_template_email } from "./email_service";
8
+ import { create_app_logger } from "../app_logger";
9
+
10
+ // section: types
11
+ export type PasswordChangeData = {
12
+ current_password: string;
13
+ new_password: string;
14
+ };
15
+
16
+ export type PasswordChangeResult = {
17
+ success: boolean;
18
+ error?: string;
19
+ };
20
+
21
+ // section: helpers
22
+ /**
23
+ * Changes a user's password
24
+ * Verifies the current password, validates the new password, and updates the password hash
25
+ * @param adapter - The hazo_connect adapter instance
26
+ * @param user_id - The user ID to update
27
+ * @param data - Password change data (current_password, new_password)
28
+ * @returns Password change result with success status or error
29
+ */
30
+ export async function change_password(
31
+ adapter: HazoConnectAdapter,
32
+ user_id: string,
33
+ data: PasswordChangeData,
34
+ ): Promise<PasswordChangeResult> {
35
+ try {
36
+ const { current_password, new_password } = data;
37
+
38
+ // Create CRUD service for hazo_users table
39
+ const users_service = createCrudService(adapter, "hazo_users");
40
+
41
+ // Get current user data
42
+ const users = await users_service.findBy({
43
+ id: user_id,
44
+ });
45
+
46
+ if (!Array.isArray(users) || users.length === 0) {
47
+ return {
48
+ success: false,
49
+ error: "User not found",
50
+ };
51
+ }
52
+
53
+ const user = users[0];
54
+ const password_hash = user.password_hash as string;
55
+ const email = user.email_address as string;
56
+ const user_name = user.name as string | undefined;
57
+
58
+ // Verify current password
59
+ try {
60
+ const is_valid = await argon2.verify(password_hash, current_password);
61
+ if (!is_valid) {
62
+ return {
63
+ success: false,
64
+ error: "Current password is incorrect",
65
+ };
66
+ }
67
+ } catch (error) {
68
+ return {
69
+ success: false,
70
+ error: "Failed to verify current password",
71
+ };
72
+ }
73
+
74
+ // Get password requirements from config
75
+ const password_requirements = get_password_requirements_config();
76
+
77
+ // Validate new password
78
+ if (!new_password || new_password.length < password_requirements.minimum_length) {
79
+ return {
80
+ success: false,
81
+ error: `Password must be at least ${password_requirements.minimum_length} characters long`,
82
+ };
83
+ }
84
+
85
+ if (password_requirements.require_uppercase && !/[A-Z]/.test(new_password)) {
86
+ return {
87
+ success: false,
88
+ error: "Password must contain at least one uppercase letter",
89
+ };
90
+ }
91
+
92
+ if (password_requirements.require_lowercase && !/[a-z]/.test(new_password)) {
93
+ return {
94
+ success: false,
95
+ error: "Password must contain at least one lowercase letter",
96
+ };
97
+ }
98
+
99
+ if (password_requirements.require_number && !/[0-9]/.test(new_password)) {
100
+ return {
101
+ success: false,
102
+ error: "Password must contain at least one number",
103
+ };
104
+ }
105
+
106
+ if (password_requirements.require_special && !/[^A-Za-z0-9]/.test(new_password)) {
107
+ return {
108
+ success: false,
109
+ error: "Password must contain at least one special character",
110
+ };
111
+ }
112
+
113
+ // Hash the new password
114
+ const new_password_hash = await argon2.hash(new_password);
115
+
116
+ // Update password hash in database
117
+ const now = new Date().toISOString();
118
+ await users_service.updateById(user_id, {
119
+ password_hash: new_password_hash,
120
+ changed_at: now,
121
+ });
122
+
123
+ // Send password changed notification email
124
+ const email_result = await send_template_email("password_changed", email, {
125
+ user_email: email,
126
+ user_name: user_name,
127
+ });
128
+
129
+ if (!email_result.success) {
130
+ const logger = create_app_logger();
131
+ logger.error("password_change_service_email_failed", {
132
+ filename: "password_change_service.ts",
133
+ line_number: 0,
134
+ user_id,
135
+ email,
136
+ error: email_result.error,
137
+ note: "Password was changed successfully but notification email failed to send",
138
+ });
139
+ }
140
+
141
+ return {
142
+ success: true,
143
+ };
144
+ } catch (error) {
145
+ const error_message =
146
+ error instanceof Error ? error.message : "Unknown error";
147
+
148
+ return {
149
+ success: false,
150
+ error: error_message,
151
+ };
152
+ }
153
+ }
154
+