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,587 @@
1
+ // file_description: service for sending emails with template support
2
+ // section: imports
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { create_app_logger } from "../app_logger";
6
+ import { read_config_section } from "../config/config_loader.server";
7
+ import type { EmailerConfig, SendEmailOptions } from "hazo_notify";
8
+
9
+ // section: types
10
+ export type EmailOptions = {
11
+ to: string;
12
+ from: string;
13
+ subject: string;
14
+ html_body?: string;
15
+ text_body?: string;
16
+ };
17
+
18
+ export type EmailTemplateType = "forgot_password" | "email_verification" | "password_changed";
19
+
20
+ export type EmailTemplateData = {
21
+ token?: string;
22
+ verification_url?: string;
23
+ reset_url?: string;
24
+ user_email?: string;
25
+ user_name?: string;
26
+ [key: string]: string | undefined;
27
+ };
28
+
29
+ // section: constants
30
+ const DEFAULT_EMAIL_FROM = "noreply@hazo_auth.local";
31
+
32
+ /**
33
+ * Gets the default email template directory (lazy-evaluated to avoid Edge Runtime issues)
34
+ */
35
+ function get_default_email_template_dir(): string {
36
+ return path.resolve(process.cwd(), "email_templates");
37
+ }
38
+
39
+ // section: singleton
40
+ /**
41
+ * Singleton instance for hazo_notify emailer configuration
42
+ * This is initialized once in instrumentation.ts and reused across all email sends
43
+ */
44
+ let hazo_notify_config: EmailerConfig | null = null;
45
+
46
+ /**
47
+ * Sets the hazo_notify emailer configuration instance
48
+ * This is called from instrumentation.ts during initialization
49
+ * @param config - The hazo_notify emailer configuration instance
50
+ */
51
+ export function set_hazo_notify_instance(config: EmailerConfig): void {
52
+ hazo_notify_config = config;
53
+ }
54
+
55
+ /**
56
+ * Gets the hazo_notify emailer configuration instance
57
+ * If not set, loads it from config file as fallback
58
+ * @returns The hazo_notify emailer configuration instance
59
+ */
60
+ async function get_hazo_notify_instance(): Promise<EmailerConfig> {
61
+ if (!hazo_notify_config) {
62
+ // Fallback: load from config file if not initialized
63
+ const logger = create_app_logger();
64
+ logger.warn("hazo_notify_instance_not_initialized", {
65
+ filename: "email_service.ts",
66
+ line_number: 0,
67
+ note: "hazo_notify instance not initialized in instrumentation.ts, loading from config file as fallback",
68
+ });
69
+ try {
70
+ // Dynamic import to avoid build-time issues with hazo_notify
71
+ const hazo_notify_module = await import("hazo_notify");
72
+ const { load_emailer_config } = hazo_notify_module;
73
+ hazo_notify_config = load_emailer_config();
74
+ } catch (error) {
75
+ const error_message = error instanceof Error ? error.message : "Unknown error";
76
+ logger.error("hazo_notify_config_load_failed", {
77
+ filename: "email_service.ts",
78
+ line_number: 0,
79
+ error: error_message,
80
+ });
81
+ throw new Error(`Failed to load hazo_notify config: ${error_message}`);
82
+ }
83
+ }
84
+ return hazo_notify_config;
85
+ }
86
+
87
+ // section: helpers
88
+ /**
89
+ * Gets email template directory from config
90
+ * @returns Email template directory path
91
+ */
92
+ function get_email_template_directory(): string {
93
+ const email_section = read_config_section("hazo_auth__email");
94
+ const template_dir = email_section?.email_template_main_directory;
95
+
96
+ if (template_dir) {
97
+ return path.isAbsolute(template_dir)
98
+ ? template_dir
99
+ : path.resolve(process.cwd(), template_dir);
100
+ }
101
+
102
+ return get_default_email_template_dir();
103
+ }
104
+
105
+ /**
106
+ * Gets email from address from config
107
+ * Priority: 1. hazo_auth__email.from_email, 2. hazo_notify_config.from_email
108
+ * @param notify_config - The hazo_notify configuration instance (for fallback)
109
+ * @returns Email from address
110
+ */
111
+ async function get_email_from(notify_config: EmailerConfig): Promise<string> {
112
+ const email_section = read_config_section("hazo_auth__email");
113
+ const hazo_auth_from_email = email_section?.from_email;
114
+
115
+ // If set in hazo_auth_config.ini, use it (overrides hazo_notify config)
116
+ if (hazo_auth_from_email) {
117
+ return hazo_auth_from_email;
118
+ }
119
+
120
+ // Fall back to hazo_notify config
121
+ return notify_config.from_email;
122
+ }
123
+
124
+ /**
125
+ * Gets email from name from config
126
+ * Priority: 1. hazo_auth__email.from_name, 2. hazo_notify_config.from_name
127
+ * @param notify_config - The hazo_notify configuration instance (for fallback)
128
+ * @returns Email from name
129
+ */
130
+ async function get_email_from_name(notify_config: EmailerConfig): Promise<string> {
131
+ const email_section = read_config_section("hazo_auth__email");
132
+ const hazo_auth_from_name = email_section?.from_name;
133
+
134
+ // If set in hazo_auth_config.ini, use it (overrides hazo_notify config)
135
+ if (hazo_auth_from_name) {
136
+ return hazo_auth_from_name;
137
+ }
138
+
139
+ // Fall back to hazo_notify config
140
+ return notify_config.from_name;
141
+ }
142
+
143
+ /**
144
+ * Gets base URL for email links from config or environment variable
145
+ * Priority: 1. hazo_auth__email.base_url, 2. APP_DOMAIN_NAME, 3. NEXT_PUBLIC_APP_URL/APP_URL
146
+ * @returns Base URL for email links
147
+ */
148
+ function get_base_url(): string {
149
+ const email_section = read_config_section("hazo_auth__email");
150
+ const base_url = email_section?.base_url;
151
+
152
+ if (base_url) {
153
+ return base_url.endsWith("/") ? base_url.slice(0, -1) : base_url;
154
+ }
155
+
156
+ // Try to get from APP_DOMAIN_NAME environment variable (adds protocol if needed)
157
+ const app_domain_name = process.env.APP_DOMAIN_NAME;
158
+ if (app_domain_name) {
159
+ // Ensure protocol is included (default to https if not specified)
160
+ const domain = app_domain_name.trim();
161
+ if (domain.startsWith("http://") || domain.startsWith("https://")) {
162
+ return domain.endsWith("/") ? domain.slice(0, -1) : domain;
163
+ }
164
+ // If no protocol, default to https
165
+ return `https://${domain}`;
166
+ }
167
+
168
+ // Try to get from other environment variables (fallback)
169
+ const env_base_url = process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL;
170
+ if (env_base_url) {
171
+ return env_base_url.endsWith("/") ? env_base_url.slice(0, -1) : env_base_url;
172
+ }
173
+
174
+ // Default to empty string (will use relative URLs)
175
+ return "";
176
+ }
177
+
178
+ /**
179
+ * Constructs verification URL from token
180
+ * @param token - Verification token
181
+ * @returns Verification URL
182
+ */
183
+ function get_verification_url(token: string): string {
184
+ const base_url = get_base_url();
185
+ const path = "/hazo_auth/verify_email";
186
+ const url = base_url ? `${base_url}${path}?token=${encodeURIComponent(token)}` : `${path}?token=${encodeURIComponent(token)}`;
187
+ return url;
188
+ }
189
+
190
+ /**
191
+ * Constructs reset password URL from token
192
+ * @param token - Password reset token
193
+ * @returns Reset password URL
194
+ */
195
+ function get_reset_password_url(token: string): string {
196
+ const base_url = get_base_url();
197
+ const path = "/hazo_auth/reset_password";
198
+ const url = base_url ? `${base_url}${path}?token=${encodeURIComponent(token)}` : `${path}?token=${encodeURIComponent(token)}`;
199
+ return url;
200
+ }
201
+
202
+ /**
203
+ * Gets default HTML template for a given template type
204
+ * @param template_type - Type of email template
205
+ * @param data - Template data for variable substitution
206
+ * @returns Default HTML template content
207
+ */
208
+ function get_default_html_template(
209
+ template_type: EmailTemplateType,
210
+ data: EmailTemplateData
211
+ ): string {
212
+ switch (template_type) {
213
+ case "email_verification":
214
+ return `
215
+ <!DOCTYPE html>
216
+ <html>
217
+ <head>
218
+ <meta charset="UTF-8">
219
+ <title>Verify Your Email</title>
220
+ </head>
221
+ <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
222
+ <h1 style="color: #0f172a;">Verify Your Email Address</h1>
223
+ <p>Thank you for registering! Please click the link below to verify your email address:</p>
224
+ <p style="margin: 20px 0;">
225
+ <a href="${data.verification_url || "#"}" style="display: inline-block; padding: 12px 24px; background-color: #0f172a; color: #ffffff; text-decoration: none; border-radius: 4px;">Verify Email Address</a>
226
+ </p>
227
+ <p>Or copy and paste this link into your browser:</p>
228
+ <p style="word-break: break-all; color: #666;">${data.verification_url || data.token || ""}</p>
229
+ <p>This link will expire in 48 hours.</p>
230
+ <p style="margin-top: 30px; color: #666; font-size: 12px;">If you didn't create an account, you can safely ignore this email.</p>
231
+ </body>
232
+ </html>
233
+ `.trim();
234
+
235
+ case "forgot_password":
236
+ return `
237
+ <!DOCTYPE html>
238
+ <html>
239
+ <head>
240
+ <meta charset="UTF-8">
241
+ <title>Reset Your Password</title>
242
+ </head>
243
+ <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
244
+ <h1 style="color: #0f172a;">Reset Your Password</h1>
245
+ <p>We received a request to reset your password. Click the link below to reset it:</p>
246
+ <p style="margin: 20px 0;">
247
+ <a href="${data.reset_url || "#"}" style="display: inline-block; padding: 12px 24px; background-color: #0f172a; color: #ffffff; text-decoration: none; border-radius: 4px;">Reset Password</a>
248
+ </p>
249
+ <p>Or copy and paste this link into your browser:</p>
250
+ <p style="word-break: break-all; color: #666;">${data.reset_url || data.token || ""}</p>
251
+ <p>This link will expire in 10 minutes.</p>
252
+ <p style="margin-top: 30px; color: #666; font-size: 12px;">If you didn't request a password reset, you can safely ignore this email.</p>
253
+ </body>
254
+ </html>
255
+ `.trim();
256
+
257
+ case "password_changed":
258
+ return `
259
+ <!DOCTYPE html>
260
+ <html>
261
+ <head>
262
+ <meta charset="UTF-8">
263
+ <title>Password Changed</title>
264
+ </head>
265
+ <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
266
+ <h1 style="color: #0f172a;">Password Changed Successfully</h1>
267
+ <p>Hello${data.user_name ? ` ${data.user_name}` : ""},</p>
268
+ <p>This email confirms that your password has been changed successfully.</p>
269
+ <p>If you did not make this change, please contact support immediately to secure your account.</p>
270
+ <p style="margin-top: 30px; color: #666; font-size: 12px;">This is an automated notification. Please do not reply to this email.</p>
271
+ </body>
272
+ </html>
273
+ `.trim();
274
+
275
+ default:
276
+ return "<p>Email content</p>";
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Gets default text template for a given template type
282
+ * @param template_type - Type of email template
283
+ * @param data - Template data for variable substitution
284
+ * @returns Default text template content
285
+ */
286
+ function get_default_text_template(
287
+ template_type: EmailTemplateType,
288
+ data: EmailTemplateData
289
+ ): string {
290
+ switch (template_type) {
291
+ case "email_verification":
292
+ return `
293
+ Verify Your Email Address
294
+
295
+ Thank you for registering! Please click the link below to verify your email address:
296
+
297
+ ${data.verification_url || data.token || ""}
298
+
299
+ This link will expire in 48 hours.
300
+
301
+ If you didn't create an account, you can safely ignore this email.
302
+ `.trim();
303
+
304
+ case "forgot_password":
305
+ return `
306
+ Reset Your Password
307
+
308
+ We received a request to reset your password. Click the link below to reset it:
309
+
310
+ ${data.reset_url || data.token || ""}
311
+
312
+ This link will expire in 10 minutes.
313
+
314
+ If you didn't request a password reset, you can safely ignore this email.
315
+ `.trim();
316
+
317
+ case "password_changed":
318
+ return `
319
+ Password Changed Successfully
320
+
321
+ Hello${data.user_name ? ` ${data.user_name}` : ""},
322
+
323
+ This email confirms that your password has been changed successfully.
324
+
325
+ If you did not make this change, please contact support immediately to secure your account.
326
+
327
+ This is an automated notification. Please do not reply to this email.
328
+ `.trim();
329
+
330
+ default:
331
+ return "Email content";
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Loads email template from file system
337
+ * @param template_type - Type of email template
338
+ * @param extension - File extension (html or txt)
339
+ * @returns Template content or undefined if not found
340
+ */
341
+ function load_template_file(
342
+ template_type: EmailTemplateType,
343
+ extension: "html" | "txt"
344
+ ): string | undefined {
345
+ const template_dir = get_email_template_directory();
346
+ const template_filename = `${template_type}.${extension}`;
347
+ const template_path = path.join(template_dir, template_filename);
348
+
349
+ if (!fs.existsSync(template_path)) {
350
+ return undefined;
351
+ }
352
+
353
+ try {
354
+ return fs.readFileSync(template_path, "utf-8");
355
+ } catch (error) {
356
+ const logger = create_app_logger();
357
+ logger.error("email_service_template_load_failed", {
358
+ filename: "email_service.ts",
359
+ line_number: 0,
360
+ template_path,
361
+ error: error instanceof Error ? error.message : "Unknown error",
362
+ });
363
+ return undefined;
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Simple template variable substitution
369
+ * Replaces {{variable_name}} with values from data object
370
+ * @param template - Template string with variables
371
+ * @param data - Data object for variable substitution
372
+ * @returns Template with variables substituted
373
+ */
374
+ function substitute_template_variables(
375
+ template: string,
376
+ data: EmailTemplateData
377
+ ): string {
378
+ let result = template;
379
+
380
+ // Replace {{variable}} with values from data
381
+ Object.entries(data).forEach(([key, value]) => {
382
+ if (value !== undefined) {
383
+ const regex = new RegExp(`{{\\s*${key}\\s*}}`, "g");
384
+ result = result.replace(regex, value);
385
+ }
386
+ });
387
+
388
+ return result;
389
+ }
390
+
391
+ /**
392
+ * Gets email subject for a given template type
393
+ * @param template_type - Type of email template
394
+ * @returns Email subject
395
+ */
396
+ function get_email_subject(template_type: EmailTemplateType): string {
397
+ const email_section = read_config_section("hazo_auth__email");
398
+ const template_config_key = `email_template__${template_type}`;
399
+ const subject_key = `${template_config_key}__subject`;
400
+
401
+ // Try to get subject from config
402
+ const subject = email_section?.[subject_key];
403
+ if (subject) {
404
+ return subject;
405
+ }
406
+
407
+ // Default subjects
408
+ switch (template_type) {
409
+ case "email_verification":
410
+ return "Verify Your Email Address";
411
+ case "forgot_password":
412
+ return "Reset Your Password";
413
+ case "password_changed":
414
+ return "Password Changed Successfully";
415
+ default:
416
+ return "Email from hazo_auth";
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Gets email templates (HTML and text) for a given template type
422
+ * Falls back to default templates if custom templates are not found
423
+ * @param template_type - Type of email template
424
+ * @param data - Template data for variable substitution
425
+ * @returns Object with html_body and text_body
426
+ */
427
+ function get_email_templates(
428
+ template_type: EmailTemplateType,
429
+ data: EmailTemplateData
430
+ ): { html_body: string; text_body: string } {
431
+ // Try to load custom templates
432
+ const html_template = load_template_file(template_type, "html");
433
+ const text_template = load_template_file(template_type, "txt");
434
+
435
+ // Use custom templates if found, otherwise use defaults
436
+ const html_body = html_template
437
+ ? substitute_template_variables(html_template, data)
438
+ : get_default_html_template(template_type, data);
439
+
440
+ const text_body = text_template
441
+ ? substitute_template_variables(text_template, data)
442
+ : get_default_text_template(template_type, data);
443
+
444
+ return { html_body, text_body };
445
+ }
446
+
447
+ /**
448
+ * Sends an email using hazo_notify
449
+ * @param options - Email options (to, from, subject, html_body, text_body)
450
+ * @returns Promise that resolves when email is sent
451
+ */
452
+ export async function send_email(options: EmailOptions): Promise<{ success: boolean; error?: string }> {
453
+ const logger = create_app_logger();
454
+
455
+ try {
456
+ // Get hazo_notify configuration instance
457
+ const notify_config = await get_hazo_notify_instance();
458
+
459
+ // Dynamic import to avoid build-time issues with hazo_notify
460
+ const hazo_notify_module = await import("hazo_notify");
461
+ const { send_email: hazo_notify_send_email } = hazo_notify_module;
462
+
463
+ // Get from email and from name (hazo_auth_config overrides hazo_notify_config)
464
+ // Priority: 1. options.from (explicit parameter), 2. hazo_auth_config.from_email, 3. hazo_notify_config.from_email
465
+ const from_email = options.from || await get_email_from(notify_config);
466
+ const from_name = await get_email_from_name(notify_config);
467
+
468
+ // Prepare hazo_notify email options
469
+ const hazo_notify_options: SendEmailOptions = {
470
+ to: options.to,
471
+ subject: options.subject,
472
+ content: {
473
+ ...(options.html_body && { html: options.html_body }),
474
+ ...(options.text_body && { text: options.text_body }),
475
+ },
476
+ // Use from_email and from_name (hazo_notify will use these instead of config defaults)
477
+ from: from_email,
478
+ from_name: from_name,
479
+ };
480
+
481
+ // Send email using hazo_notify
482
+ const result = await hazo_notify_send_email(hazo_notify_options, notify_config);
483
+
484
+ if (result.success) {
485
+ logger.info("email_sent", {
486
+ filename: "email_service.ts",
487
+ line_number: 0,
488
+ to: options.to,
489
+ from: options.from || notify_config.from_email,
490
+ subject: options.subject,
491
+ message_id: result.message_id,
492
+ });
493
+
494
+ return { success: true };
495
+ } else {
496
+ const error_message = result.error || result.message || "Unknown error";
497
+
498
+ logger.error("email_send_failed", {
499
+ filename: "email_service.ts",
500
+ line_number: 0,
501
+ to: options.to,
502
+ from: options.from || notify_config.from_email,
503
+ subject: options.subject,
504
+ error: error_message,
505
+ raw_response: result.raw_response,
506
+ });
507
+
508
+ return { success: false, error: error_message };
509
+ }
510
+ } catch (error) {
511
+ const error_message = error instanceof Error ? error.message : "Unknown error";
512
+
513
+ logger.error("email_send_failed", {
514
+ filename: "email_service.ts",
515
+ line_number: 0,
516
+ to: options.to,
517
+ from: options.from,
518
+ subject: options.subject,
519
+ error: error_message,
520
+ });
521
+
522
+ return { success: false, error: error_message };
523
+ }
524
+ }
525
+
526
+ /**
527
+ * Sends an email using a template
528
+ * @param template_type - Type of email template
529
+ * @param to - Recipient email address
530
+ * @param data - Template data for variable substitution
531
+ * @returns Promise that resolves when email is sent
532
+ */
533
+ export async function send_template_email(
534
+ template_type: EmailTemplateType,
535
+ to: string,
536
+ data: EmailTemplateData
537
+ ): Promise<{ success: boolean; error?: string }> {
538
+ const logger = create_app_logger();
539
+
540
+ try {
541
+ // Enhance data with URLs if token is provided
542
+ const enhanced_data: EmailTemplateData = { ...data };
543
+
544
+ if (data.token) {
545
+ if (template_type === "email_verification") {
546
+ enhanced_data.verification_url = get_verification_url(data.token);
547
+ } else if (template_type === "forgot_password") {
548
+ enhanced_data.reset_url = get_reset_password_url(data.token);
549
+ }
550
+ }
551
+
552
+ // Get email templates
553
+ const { html_body, text_body } = get_email_templates(template_type, enhanced_data);
554
+
555
+ // Get email subject
556
+ const subject = get_email_subject(template_type);
557
+
558
+ // Get hazo_notify config instance
559
+ const notify_config = await get_hazo_notify_instance();
560
+
561
+ // Get email from address and from name
562
+ // Priority: 1. hazo_auth_config.from_email/from_name, 2. hazo_notify_config.from_email/from_name
563
+ const from = await get_email_from(notify_config);
564
+
565
+ // Send email (from_name is handled inside send_email function)
566
+ return await send_email({
567
+ to,
568
+ from,
569
+ subject,
570
+ html_body,
571
+ text_body,
572
+ });
573
+ } catch (error) {
574
+ const error_message = error instanceof Error ? error.message : "Unknown error";
575
+
576
+ logger.error("email_template_send_failed", {
577
+ filename: "email_service.ts",
578
+ line_number: 0,
579
+ template_type,
580
+ to,
581
+ error: error_message,
582
+ });
583
+
584
+ return { success: false, error: error_message };
585
+ }
586
+ }
587
+