hazo_auth 5.2.0 → 5.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.
- package/SETUP_CHECKLIST.md +46 -0
- package/cli-src/lib/services/email_service.ts +136 -289
- package/cli-src/lib/services/email_template_manifest.ts +104 -0
- package/cli-src/lib/services/email_templates/email_verification.html +18 -0
- package/cli-src/lib/services/email_templates/email_verification.txt +9 -0
- package/cli-src/lib/services/email_templates/forgot_password.html +18 -0
- package/cli-src/lib/services/email_templates/forgot_password.txt +9 -0
- package/cli-src/lib/services/email_templates/password_changed.html +14 -0
- package/cli-src/lib/services/email_templates/password_changed.txt +9 -0
- package/dist/components/ui/button.d.ts +2 -2
- package/dist/lib/services/email_service.d.ts +17 -4
- package/dist/lib/services/email_service.d.ts.map +1 -1
- package/dist/lib/services/email_service.js +93 -221
- package/dist/lib/services/email_template_manifest.d.ts +11 -0
- package/dist/lib/services/email_template_manifest.d.ts.map +1 -0
- package/dist/lib/services/email_template_manifest.js +95 -0
- package/dist/lib/services/email_templates/email_verification.html +18 -0
- package/dist/lib/services/email_templates/email_verification.txt +9 -0
- package/dist/lib/services/email_templates/forgot_password.html +18 -0
- package/dist/lib/services/email_templates/forgot_password.txt +9 -0
- package/dist/lib/services/email_templates/password_changed.html +14 -0
- package/dist/lib/services/email_templates/password_changed.txt +9 -0
- package/dist/server-lib.d.ts +1 -0
- package/dist/server-lib.d.ts.map +1 -1
- package/dist/server-lib.js +1 -0
- package/package.json +4 -3
- package/cli-src/assets/images/new_firm_default.jpg +0 -0
- package/cli-src/lib/auth/org_cache.ts +0 -148
- package/cli-src/lib/index.ts +0 -48
- package/cli-src/lib/services/org_service.ts +0 -965
- package/cli-src/lib/services/scope_labels_service.ts +0 -348
package/SETUP_CHECKLIST.md
CHANGED
|
@@ -2,6 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
This checklist provides step-by-step instructions for setting up the `hazo_auth` package in your Next.js project. AI assistants can follow this guide to ensure complete and correct setup.
|
|
4
4
|
|
|
5
|
+
## v5.3.0 Migration (from v5.2.x)
|
|
6
|
+
|
|
7
|
+
If you are already on hazo_auth `5.2.x`, the only mandatory work is wiring up hazo_notify's template manager at boot. Apps that don't use the templated email path (only the plain `send_email`) keep working unchanged after bumping the peer dep.
|
|
8
|
+
|
|
9
|
+
**1. Bump the peer dep.** Update your app's `package.json`:
|
|
10
|
+
|
|
11
|
+
```diff
|
|
12
|
+
- "hazo_notify": "^1.1.0",
|
|
13
|
+
+ "hazo_notify": "^3.0.0",
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Then `npm install`.
|
|
17
|
+
|
|
18
|
+
**2. Apply the hazo_notify template-manager migration.** hazo_notify 3.0.0 introduces two new tables (`hazo_notify_template_cat`, `hazo_notify_templates`). If you are using SQLite via the existing schema seed, the tables are created on first connect when `initial_sql` is provided to the adapter (see step 4 below). If you are on PostgREST or you manage migrations explicitly, run `migrations/002_template_manager.sql` from hazo_notify.
|
|
19
|
+
|
|
20
|
+
**3. Register the `notify_templates_admin` permission (optional).** Users with this permission can edit auth email templates via the hazo_notify admin UI. Grant `notify_templates_super_admin` to also allow deleting system templates. These are hazo_notify permissions, declared in your app's permission set; hazo_auth does not grant them itself.
|
|
21
|
+
|
|
22
|
+
**4. Wire up `init_template_manager` and `set_hazo_notify_connect` at boot.** In your Next.js `instrumentation.ts` (or equivalent), feed hazo_auth's manifest into hazo_notify and register the connect instance with hazo_auth:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// instrumentation.ts
|
|
26
|
+
import { init_template_manager } from "hazo_notify/template_manager";
|
|
27
|
+
import { hazo_auth_template_manifest } from "hazo_auth/server-lib";
|
|
28
|
+
import { set_hazo_notify_connect } from "hazo_auth/server-lib"; // re-exported from email_service
|
|
29
|
+
import { build_my_hazo_notify_connect } from "./lib/hazo_notify_connect";
|
|
30
|
+
|
|
31
|
+
export async function register() {
|
|
32
|
+
if (process.env.NEXT_RUNTIME !== "nodejs") return;
|
|
33
|
+
|
|
34
|
+
const notify_connect = build_my_hazo_notify_connect();
|
|
35
|
+
|
|
36
|
+
await init_template_manager({
|
|
37
|
+
hazo_connect_factory: () => notify_connect,
|
|
38
|
+
manifests: [...hazo_auth_template_manifest],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
set_hazo_notify_connect(notify_connect);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`build_my_hazo_notify_connect()` is consuming-app code that wraps your `hazo_connect` adapter into the `HazoConnectInstance` shape hazo_notify expects (`list`/`findById`/`findBy`/`insert`/`updateById`/`deleteById`/`query`). hazo_auth's own test-app ships an example wrapper at `instrumentation_hazo_notify_connect.ts` you can copy.
|
|
46
|
+
|
|
47
|
+
**5. Migrate any custom templates.** The `[hazo_auth__email] email_template_main_directory` config key is deprecated and no longer overrides anything — a one-time `warn` log will fire on first send if it is set. Move any custom HTML/text into the admin UI as scope-specific (or super-admin-edited global) templates before `hazo_auth@6.0.0` removes the key.
|
|
48
|
+
|
|
49
|
+
**6. (Optional) Mount the template admin UI.** Use `<TemplateManagerAdmin />` from `hazo_notify/template_manager_admin` to give your admins a UI for managing the auth templates. See hazo_notify's docs for routing and permission-gating.
|
|
50
|
+
|
|
5
51
|
## Quick Start (Recommended)
|
|
6
52
|
|
|
7
53
|
The fastest way to set up hazo_auth:
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// file_description: service for sending emails with template support
|
|
2
2
|
// section: imports
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import path from "path";
|
|
5
3
|
import { create_app_logger } from "../app_logger.js";
|
|
6
4
|
import { read_config_section } from "../config/config_loader.server.js";
|
|
7
5
|
import type { EmailerConfig, SendEmailOptions } from "hazo_notify";
|
|
6
|
+
import type { HazoConnectInstance } from "hazo_notify/template_manager";
|
|
8
7
|
|
|
9
8
|
// section: types
|
|
10
9
|
export type EmailOptions = {
|
|
@@ -26,16 +25,6 @@ export type EmailTemplateData = {
|
|
|
26
25
|
[key: string]: string | undefined;
|
|
27
26
|
};
|
|
28
27
|
|
|
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
28
|
// section: singleton
|
|
40
29
|
/**
|
|
41
30
|
* Singleton instance for hazo_notify emailer configuration
|
|
@@ -43,6 +32,14 @@ function get_default_email_template_dir(): string {
|
|
|
43
32
|
*/
|
|
44
33
|
let hazo_notify_config: EmailerConfig | null = null;
|
|
45
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Singleton hazo_notify-compatible hazo_connect instance used by the template
|
|
37
|
+
* manager. The consuming app builds this once (typically the same instance fed
|
|
38
|
+
* into `init_template_manager({ hazo_connect_factory })`) and registers it via
|
|
39
|
+
* {@link set_hazo_notify_connect}. Required for `send_template_email`.
|
|
40
|
+
*/
|
|
41
|
+
let hazo_notify_connect: HazoConnectInstance | null = null;
|
|
42
|
+
|
|
46
43
|
/**
|
|
47
44
|
* Sets the hazo_notify emailer configuration instance
|
|
48
45
|
* This is called from instrumentation.ts during initialization
|
|
@@ -52,6 +49,17 @@ export function set_hazo_notify_instance(config: EmailerConfig): void {
|
|
|
52
49
|
hazo_notify_config = config;
|
|
53
50
|
}
|
|
54
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Registers the hazo_notify-compatible hazo_connect instance used by
|
|
54
|
+
* `send_template_email`. Call this from `instrumentation.ts` with the same
|
|
55
|
+
* instance you pass to `init_template_manager({ hazo_connect_factory })`.
|
|
56
|
+
*
|
|
57
|
+
* @param instance - hazo_notify-shaped database adapter
|
|
58
|
+
*/
|
|
59
|
+
export function set_hazo_notify_connect(instance: HazoConnectInstance): void {
|
|
60
|
+
hazo_notify_connect = instance;
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
/**
|
|
56
64
|
* Gets the hazo_notify emailer configuration instance
|
|
57
65
|
* If not set, loads it from config file as fallback
|
|
@@ -84,24 +92,36 @@ async function get_hazo_notify_instance(): Promise<EmailerConfig> {
|
|
|
84
92
|
return hazo_notify_config;
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
// section:
|
|
95
|
+
// section: deprecated_config_helpers
|
|
88
96
|
/**
|
|
89
|
-
*
|
|
90
|
-
* @returns Email template directory path
|
|
97
|
+
* One-time warning state for the deprecated email_template_main_directory key.
|
|
91
98
|
*/
|
|
92
|
-
|
|
99
|
+
let warned_about_template_dir = false;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Reads the deprecated [hazo_auth__email] email_template_main_directory key
|
|
103
|
+
* solely to emit a one-time deprecation warning. The value is no longer used —
|
|
104
|
+
* template overrides now live in hazo_notify's database and are managed via
|
|
105
|
+
* the template admin UI. The key will be removed in hazo_auth@6.0.0.
|
|
106
|
+
*/
|
|
107
|
+
function warn_if_template_directory_configured(): void {
|
|
108
|
+
if (warned_about_template_dir) return;
|
|
93
109
|
const email_section = read_config_section("hazo_auth__email");
|
|
94
110
|
const template_dir = email_section?.email_template_main_directory;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
111
|
+
if (!template_dir) return;
|
|
112
|
+
warned_about_template_dir = true;
|
|
113
|
+
create_app_logger().warn("hazo_auth_email_template_main_directory_deprecated", {
|
|
114
|
+
filename: "email_service.ts",
|
|
115
|
+
line_number: 0,
|
|
116
|
+
note:
|
|
117
|
+
"Filesystem template overrides are deprecated in hazo_auth@5.3.0. " +
|
|
118
|
+
"Use the hazo_notify template admin UI (mounted via <TemplateManagerAdmin />) " +
|
|
119
|
+
"to override the email_verification / forgot_password / password_changed templates per scope. " +
|
|
120
|
+
"This config key will be removed in hazo_auth@6.0.0.",
|
|
121
|
+
});
|
|
103
122
|
}
|
|
104
123
|
|
|
124
|
+
// section: helpers
|
|
105
125
|
/**
|
|
106
126
|
* Gets email from address from config
|
|
107
127
|
* Priority: 1. hazo_auth__email.from_email, 2. hazo_notify_config.from_email
|
|
@@ -111,12 +131,12 @@ function get_email_template_directory(): string {
|
|
|
111
131
|
async function get_email_from(notify_config: EmailerConfig): Promise<string> {
|
|
112
132
|
const email_section = read_config_section("hazo_auth__email");
|
|
113
133
|
const hazo_auth_from_email = email_section?.from_email;
|
|
114
|
-
|
|
134
|
+
|
|
115
135
|
// If set in hazo_auth_config.ini, use it (overrides hazo_notify config)
|
|
116
136
|
if (hazo_auth_from_email) {
|
|
117
137
|
return hazo_auth_from_email;
|
|
118
138
|
}
|
|
119
|
-
|
|
139
|
+
|
|
120
140
|
// Fall back to hazo_notify config
|
|
121
141
|
return notify_config.from_email;
|
|
122
142
|
}
|
|
@@ -130,12 +150,12 @@ async function get_email_from(notify_config: EmailerConfig): Promise<string> {
|
|
|
130
150
|
async function get_email_from_name(notify_config: EmailerConfig): Promise<string> {
|
|
131
151
|
const email_section = read_config_section("hazo_auth__email");
|
|
132
152
|
const hazo_auth_from_name = email_section?.from_name;
|
|
133
|
-
|
|
153
|
+
|
|
134
154
|
// If set in hazo_auth_config.ini, use it (overrides hazo_notify config)
|
|
135
155
|
if (hazo_auth_from_name) {
|
|
136
156
|
return hazo_auth_from_name;
|
|
137
157
|
}
|
|
138
|
-
|
|
158
|
+
|
|
139
159
|
// Fall back to hazo_notify config
|
|
140
160
|
return notify_config.from_name;
|
|
141
161
|
}
|
|
@@ -148,11 +168,11 @@ async function get_email_from_name(notify_config: EmailerConfig): Promise<string
|
|
|
148
168
|
function get_base_url(): string {
|
|
149
169
|
const email_section = read_config_section("hazo_auth__email");
|
|
150
170
|
const base_url = email_section?.base_url;
|
|
151
|
-
|
|
171
|
+
|
|
152
172
|
if (base_url) {
|
|
153
173
|
return base_url.endsWith("/") ? base_url.slice(0, -1) : base_url;
|
|
154
174
|
}
|
|
155
|
-
|
|
175
|
+
|
|
156
176
|
// Try to get from APP_DOMAIN_NAME environment variable (adds protocol if needed)
|
|
157
177
|
const app_domain_name = process.env.APP_DOMAIN_NAME;
|
|
158
178
|
if (app_domain_name) {
|
|
@@ -164,13 +184,13 @@ function get_base_url(): string {
|
|
|
164
184
|
// If no protocol, default to https
|
|
165
185
|
return `https://${domain}`;
|
|
166
186
|
}
|
|
167
|
-
|
|
187
|
+
|
|
168
188
|
// Try to get from other environment variables (fallback)
|
|
169
189
|
const env_base_url = process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL;
|
|
170
190
|
if (env_base_url) {
|
|
171
191
|
return env_base_url.endsWith("/") ? env_base_url.slice(0, -1) : env_base_url;
|
|
172
192
|
}
|
|
173
|
-
|
|
193
|
+
|
|
174
194
|
// Default to empty string (will use relative URLs)
|
|
175
195
|
return "";
|
|
176
196
|
}
|
|
@@ -199,195 +219,6 @@ function get_reset_password_url(token: string): string {
|
|
|
199
219
|
return url;
|
|
200
220
|
}
|
|
201
221
|
|
|
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
222
|
/**
|
|
392
223
|
* Gets email subject for a given template type
|
|
393
224
|
* @param template_type - Type of email template
|
|
@@ -397,13 +228,13 @@ function get_email_subject(template_type: EmailTemplateType): string {
|
|
|
397
228
|
const email_section = read_config_section("hazo_auth__email");
|
|
398
229
|
const template_config_key = `email_template__${template_type}`;
|
|
399
230
|
const subject_key = `${template_config_key}__subject`;
|
|
400
|
-
|
|
231
|
+
|
|
401
232
|
// Try to get subject from config
|
|
402
233
|
const subject = email_section?.[subject_key];
|
|
403
234
|
if (subject) {
|
|
404
235
|
return subject;
|
|
405
236
|
}
|
|
406
|
-
|
|
237
|
+
|
|
407
238
|
// Default subjects
|
|
408
239
|
switch (template_type) {
|
|
409
240
|
case "email_verification":
|
|
@@ -417,33 +248,6 @@ function get_email_subject(template_type: EmailTemplateType): string {
|
|
|
417
248
|
}
|
|
418
249
|
}
|
|
419
250
|
|
|
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
251
|
/**
|
|
448
252
|
* Sends an email using hazo_notify
|
|
449
253
|
* @param options - Email options (to, from, subject, html_body, text_body)
|
|
@@ -451,20 +255,20 @@ function get_email_templates(
|
|
|
451
255
|
*/
|
|
452
256
|
export async function send_email(options: EmailOptions): Promise<{ success: boolean; error?: string }> {
|
|
453
257
|
const logger = create_app_logger();
|
|
454
|
-
|
|
258
|
+
|
|
455
259
|
try {
|
|
456
260
|
// Get hazo_notify configuration instance
|
|
457
261
|
const notify_config = await get_hazo_notify_instance();
|
|
458
|
-
|
|
262
|
+
|
|
459
263
|
// Dynamic import to avoid build-time issues with hazo_notify
|
|
460
264
|
const hazo_notify_module = await import("hazo_notify");
|
|
461
265
|
const { send_email: hazo_notify_send_email } = hazo_notify_module;
|
|
462
|
-
|
|
266
|
+
|
|
463
267
|
// Get from email and from name (hazo_auth_config overrides hazo_notify_config)
|
|
464
268
|
// Priority: 1. options.from (explicit parameter), 2. hazo_auth_config.from_email, 3. hazo_notify_config.from_email
|
|
465
269
|
const from_email = options.from || await get_email_from(notify_config);
|
|
466
270
|
const from_name = await get_email_from_name(notify_config);
|
|
467
|
-
|
|
271
|
+
|
|
468
272
|
// Prepare hazo_notify email options
|
|
469
273
|
const hazo_notify_options: SendEmailOptions = {
|
|
470
274
|
to: options.to,
|
|
@@ -477,10 +281,10 @@ export async function send_email(options: EmailOptions): Promise<{ success: bool
|
|
|
477
281
|
from: from_email,
|
|
478
282
|
from_name: from_name,
|
|
479
283
|
};
|
|
480
|
-
|
|
284
|
+
|
|
481
285
|
// Send email using hazo_notify
|
|
482
286
|
const result = await hazo_notify_send_email(hazo_notify_options, notify_config);
|
|
483
|
-
|
|
287
|
+
|
|
484
288
|
if (result.success) {
|
|
485
289
|
logger.info("email_sent", {
|
|
486
290
|
filename: "email_service.ts",
|
|
@@ -490,11 +294,11 @@ export async function send_email(options: EmailOptions): Promise<{ success: bool
|
|
|
490
294
|
subject: options.subject,
|
|
491
295
|
message_id: result.message_id,
|
|
492
296
|
});
|
|
493
|
-
|
|
297
|
+
|
|
494
298
|
return { success: true };
|
|
495
299
|
} else {
|
|
496
300
|
const error_message = result.error || result.message || "Unknown error";
|
|
497
|
-
|
|
301
|
+
|
|
498
302
|
logger.error("email_send_failed", {
|
|
499
303
|
filename: "email_service.ts",
|
|
500
304
|
line_number: 0,
|
|
@@ -504,12 +308,12 @@ export async function send_email(options: EmailOptions): Promise<{ success: bool
|
|
|
504
308
|
error: error_message,
|
|
505
309
|
raw_response: result.raw_response,
|
|
506
310
|
});
|
|
507
|
-
|
|
311
|
+
|
|
508
312
|
return { success: false, error: error_message };
|
|
509
313
|
}
|
|
510
314
|
} catch (error) {
|
|
511
315
|
const error_message = error instanceof Error ? error.message : "Unknown error";
|
|
512
|
-
|
|
316
|
+
|
|
513
317
|
logger.error("email_send_failed", {
|
|
514
318
|
filename: "email_service.ts",
|
|
515
319
|
line_number: 0,
|
|
@@ -518,17 +322,21 @@ export async function send_email(options: EmailOptions): Promise<{ success: bool
|
|
|
518
322
|
subject: options.subject,
|
|
519
323
|
error: error_message,
|
|
520
324
|
});
|
|
521
|
-
|
|
325
|
+
|
|
522
326
|
return { success: false, error: error_message };
|
|
523
327
|
}
|
|
524
328
|
}
|
|
525
329
|
|
|
526
330
|
/**
|
|
527
|
-
* Sends an email using a template
|
|
528
|
-
*
|
|
331
|
+
* Sends an email using a template, delegating rendering to hazo_notify's
|
|
332
|
+
* scope-aware template manager. Templates and per-scope overrides live in
|
|
333
|
+
* hazo_notify's database; hazo_auth ships the global defaults via
|
|
334
|
+
* `hazo_auth_template_manifest`.
|
|
335
|
+
*
|
|
336
|
+
* @param template_type - System template name (email_verification | forgot_password | password_changed)
|
|
529
337
|
* @param to - Recipient email address
|
|
530
|
-
* @param data - Template data
|
|
531
|
-
* @returns Promise that resolves
|
|
338
|
+
* @param data - Template data; `token` is auto-expanded into verification_url/reset_url
|
|
339
|
+
* @returns Promise that resolves with success status
|
|
532
340
|
*/
|
|
533
341
|
export async function send_template_email(
|
|
534
342
|
template_type: EmailTemplateType,
|
|
@@ -536,11 +344,13 @@ export async function send_template_email(
|
|
|
536
344
|
data: EmailTemplateData
|
|
537
345
|
): Promise<{ success: boolean; error?: string }> {
|
|
538
346
|
const logger = create_app_logger();
|
|
539
|
-
|
|
347
|
+
|
|
348
|
+
// Emit a one-time deprecation warning if the old filesystem override key is set.
|
|
349
|
+
warn_if_template_directory_configured();
|
|
350
|
+
|
|
540
351
|
try {
|
|
541
|
-
//
|
|
352
|
+
// URL construction stays here — hazo_notify just renders variables.
|
|
542
353
|
const enhanced_data: EmailTemplateData = { ...data };
|
|
543
|
-
|
|
544
354
|
if (data.token) {
|
|
545
355
|
if (template_type === "email_verification") {
|
|
546
356
|
enhanced_data.verification_url = get_verification_url(data.token);
|
|
@@ -548,31 +358,69 @@ export async function send_template_email(
|
|
|
548
358
|
enhanced_data.reset_url = get_reset_password_url(data.token);
|
|
549
359
|
}
|
|
550
360
|
}
|
|
551
|
-
|
|
552
|
-
//
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
361
|
+
|
|
362
|
+
// Coerce `string | undefined` data into `Record<string, string>` for Handlebars.
|
|
363
|
+
const variables: Record<string, string> = {};
|
|
364
|
+
for (const [key, value] of Object.entries(enhanced_data)) {
|
|
365
|
+
if (typeof value === "string") {
|
|
366
|
+
variables[key] = value;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (!hazo_notify_connect) {
|
|
371
|
+
throw new Error(
|
|
372
|
+
"hazo_notify connect not initialized. Call set_hazo_notify_connect() " +
|
|
373
|
+
"from instrumentation.ts with the same HazoConnectInstance you pass " +
|
|
374
|
+
"to init_template_manager({ hazo_connect_factory })."
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
559
378
|
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
379
|
const from = await get_email_from(notify_config);
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
380
|
+
const from_name = await get_email_from_name(notify_config);
|
|
381
|
+
const subject = get_email_subject(template_type);
|
|
382
|
+
|
|
383
|
+
// Dynamic import keeps hazo_notify optional at build time.
|
|
384
|
+
const { send_template_email: notify_send_template_email } = await import(
|
|
385
|
+
"hazo_notify/template_manager"
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const result = await notify_send_template_email(
|
|
389
|
+
{
|
|
390
|
+
template_name: template_type,
|
|
391
|
+
to,
|
|
392
|
+
variables,
|
|
393
|
+
scope_id: null,
|
|
394
|
+
subject,
|
|
395
|
+
from,
|
|
396
|
+
from_name,
|
|
397
|
+
},
|
|
398
|
+
hazo_notify_connect
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
if (result.success) {
|
|
402
|
+
logger.info("email_sent_via_template", {
|
|
403
|
+
filename: "email_service.ts",
|
|
404
|
+
line_number: 0,
|
|
405
|
+
template_type,
|
|
406
|
+
to,
|
|
407
|
+
message_id: result.message_id,
|
|
408
|
+
});
|
|
409
|
+
return { success: true };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const error_message = result.error || result.message || "Unknown error";
|
|
413
|
+
logger.error("email_template_send_failed", {
|
|
414
|
+
filename: "email_service.ts",
|
|
415
|
+
line_number: 0,
|
|
416
|
+
template_type,
|
|
567
417
|
to,
|
|
568
|
-
|
|
569
|
-
subject,
|
|
570
|
-
html_body,
|
|
571
|
-
text_body,
|
|
418
|
+
error: error_message,
|
|
572
419
|
});
|
|
420
|
+
return { success: false, error: error_message };
|
|
573
421
|
} catch (error) {
|
|
574
422
|
const error_message = error instanceof Error ? error.message : "Unknown error";
|
|
575
|
-
|
|
423
|
+
|
|
576
424
|
logger.error("email_template_send_failed", {
|
|
577
425
|
filename: "email_service.ts",
|
|
578
426
|
line_number: 0,
|
|
@@ -580,8 +428,7 @@ export async function send_template_email(
|
|
|
580
428
|
to,
|
|
581
429
|
error: error_message,
|
|
582
430
|
});
|
|
583
|
-
|
|
431
|
+
|
|
584
432
|
return { success: false, error: error_message };
|
|
585
433
|
}
|
|
586
434
|
}
|
|
587
|
-
|