webspresso 0.0.99 → 0.0.101
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/.agents/ADMIN.md +47 -4
- package/.agents/AGENTS.md +4 -2
- package/.agents/AUTH.md +309 -0
- package/.agents/CLI.md +128 -0
- package/.agents/EMAIL.md +84 -2
- package/.agents/ERRORS.md +59 -0
- package/.agents/EVENTS.md +179 -0
- package/.agents/ORM.md +149 -0
- package/.agents/PLUGINS.md +44 -7
- package/.agents/QUEUE.md +18 -10
- package/.agents/ROUTING.md +97 -1
- package/.agents/STREAMING.md +35 -0
- package/AGENTS.md +2 -0
- package/README.md +145 -93
- package/bin/commands/add-docker.js +467 -0
- package/bin/commands/db-makemigrations.js +83 -0
- package/bin/commands/deploy-check.js +339 -0
- package/bin/commands/list.js +96 -0
- package/bin/commands/make-command.js +93 -0
- package/bin/commands/make-event.js +80 -0
- package/bin/commands/make-factory.js +72 -0
- package/bin/commands/make-mail.js +93 -0
- package/bin/commands/make-policy.js +106 -0
- package/bin/commands/run.js +195 -0
- package/bin/webspresso.js +21 -0
- package/core/auth/bouncer.js +196 -0
- package/core/auth/index.js +73 -1
- package/core/auth/magic-link.js +170 -0
- package/core/auth/manager.js +343 -3
- package/core/auth/middleware.js +45 -6
- package/core/auth/session-manager.js +255 -0
- package/core/auth/sessions.js +351 -0
- package/core/auth/tokens.js +1 -0
- package/core/auth/totp.js +4 -1
- package/core/auth/user-agent.js +161 -0
- package/core/commands/define-command.js +48 -0
- package/core/commands/index.js +18 -0
- package/core/commands/registry.js +156 -0
- package/core/commands/runner.js +76 -0
- package/core/compression/compressible.js +1 -1
- package/core/compression/middleware.js +35 -251
- package/core/content/service.js +8 -2
- package/core/events/discovery.js +142 -0
- package/core/events/dispatcher.js +552 -0
- package/core/events/index.js +23 -0
- package/core/events/signal.js +92 -0
- package/core/forms/flash.js +234 -0
- package/core/forms/form-builder.js +214 -0
- package/core/forms/index.js +16 -0
- package/core/mail/index.js +23 -0
- package/core/mail/mailable.js +250 -0
- package/core/mail/mailer.js +184 -0
- package/core/mail/transports.js +238 -0
- package/core/orm/factory.js +250 -0
- package/core/orm/index.js +8 -1
- package/core/orm/migrations/diff.js +185 -0
- package/core/orm/migrations/index.js +22 -4
- package/core/orm/model.js +55 -1
- package/core/orm/repository.js +283 -13
- package/core/orm/scopes.js +3 -2
- package/core/orm/types.js +6 -1
- package/core/queue/cron.js +252 -39
- package/core/queue/manager.js +20 -2
- package/core/queue/scheduler.js +115 -59
- package/core/ssr/stream.js +62 -2
- package/index.d.ts +540 -14
- package/index.js +44 -0
- package/package.json +11 -5
- package/plugins/admin-panel/api.js +509 -3
- package/plugins/admin-panel/index.js +17 -0
- package/plugins/admin-panel/modules/admin-users.js +1 -1
- package/plugins/audit-log/admin-component.js +54 -54
- package/plugins/audit-log/index.js +2 -2
- package/plugins/content/client/inline-edit.js +2 -46
- package/plugins/content/models/content-entry.js +4 -3
- package/plugins/content/models/content-type.js +4 -3
- package/plugins/cors.js +15 -4
- package/plugins/email/auth-bridge.js +66 -0
- package/plugins/email/bundled-templates.js +15 -0
- package/plugins/email/index.js +48 -0
- package/plugins/email/preview-handler.js +555 -0
- package/plugins/email/service.js +23 -12
- package/plugins/email/templates/auth-magic-link.mjml +13 -0
- package/plugins/email/transport.js +30 -11
- package/plugins/file-manager/admin-component.js +49 -49
- package/plugins/file-manager/index.js +4 -4
- package/plugins/index.js +2 -0
- package/plugins/redirect/index.js +1 -1
- package/plugins/webhooks/admin-component.js +608 -0
- package/plugins/webhooks/api-handlers.js +237 -0
- package/plugins/webhooks/crypto.js +144 -0
- package/plugins/webhooks/dispatcher.js +708 -0
- package/plugins/webhooks/index.js +263 -0
- package/plugins/webhooks/matcher.js +113 -0
- package/plugins/webhooks/migration-template.js +75 -0
- package/plugins/webhooks/models.js +98 -0
- package/plugins/webhooks/sender.js +176 -0
- package/src/api/api-loader.js +1 -1
- package/src/app-context.js +48 -6
- package/src/bundle/index.js +2 -1
- package/src/dev-reload/index.js +9 -1
- package/src/discovery/scan-routes.js +10 -1
- package/src/file-router.js +173 -4
- package/src/helpers.js +150 -1
- package/src/njk-frontmatter.js +3 -0
- package/src/pages/page-loader.js +1 -1
- package/src/routing/cascading.js +120 -4
- package/src/server.js +180 -26
package/plugins/email/index.js
CHANGED
|
@@ -11,6 +11,9 @@ const { generateEmailLogsMigration } = require('./migration-template');
|
|
|
11
11
|
const { purgeEmailLogs } = require('./purge');
|
|
12
12
|
const { wireAuthEmails, registerAuthEmailRoutes } = require('./auth-bridge');
|
|
13
13
|
const { emailPluginOptionsFromManifest } = require('./manifest');
|
|
14
|
+
const { Mailable } = require('../../core/mail/mailable');
|
|
15
|
+
const { createMailer } = require('../../core/mail/mailer');
|
|
16
|
+
const { setAppContext } = require('../../src/app-context');
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* @param {Object} options
|
|
@@ -173,6 +176,17 @@ function emailPlugin(options = {}) {
|
|
|
173
176
|
tableName,
|
|
174
177
|
});
|
|
175
178
|
|
|
179
|
+
const mailer = {
|
|
180
|
+
send: (opts) => emailService.send(opts),
|
|
181
|
+
sendTemplate: (name, opts) => emailService.sendTemplate(name, opts),
|
|
182
|
+
preview: (opts, data) => emailService.preview(opts, data),
|
|
183
|
+
getOutbox: () => emailService.getOutbox(),
|
|
184
|
+
clearOutbox: () => emailService.clearOutbox(),
|
|
185
|
+
lastMessage: () => emailService.lastMessage(),
|
|
186
|
+
verify: () => emailService.verifyConnection(),
|
|
187
|
+
getTransport: () => emailService.getTransport(),
|
|
188
|
+
};
|
|
189
|
+
|
|
176
190
|
let wiredAuthEmailsConfig = null;
|
|
177
191
|
|
|
178
192
|
return {
|
|
@@ -182,9 +196,14 @@ function emailPlugin(options = {}) {
|
|
|
182
196
|
dependencies: includeAdminPage ? { 'admin-panel': '*' } : {},
|
|
183
197
|
|
|
184
198
|
api: {
|
|
199
|
+
mailer,
|
|
200
|
+
Mailable,
|
|
185
201
|
send: (opts) => emailService.send(opts),
|
|
186
202
|
sendTemplate: (name, opts) => emailService.sendTemplate(name, opts),
|
|
187
203
|
preview: (nameOrOpts, data) => emailService.preview(nameOrOpts, data),
|
|
204
|
+
getOutbox: () => emailService.getOutbox(),
|
|
205
|
+
clearOutbox: () => emailService.clearOutbox(),
|
|
206
|
+
lastMessage: () => emailService.lastMessage(),
|
|
188
207
|
registerTemplate: (id, source) => registry.register(id, source),
|
|
189
208
|
listTemplates: () => registry.list(),
|
|
190
209
|
verifyConnection: () => emailService.verifyConnection(),
|
|
@@ -219,6 +238,18 @@ function emailPlugin(options = {}) {
|
|
|
219
238
|
},
|
|
220
239
|
|
|
221
240
|
register(ctx) {
|
|
241
|
+
setAppContext({ mailer });
|
|
242
|
+
if (ctx.app) {
|
|
243
|
+
ctx.app.mailer = mailer;
|
|
244
|
+
if (typeof ctx.app.use === 'function') {
|
|
245
|
+
ctx.app.use((req, res, next) => {
|
|
246
|
+
req.mailer = mailer;
|
|
247
|
+
next();
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
ctx.mailer = mailer;
|
|
252
|
+
|
|
222
253
|
if (ctx.app && ctx.app.serviceRegistry) {
|
|
223
254
|
const { createMailServices } = require('../../src/services/builtins/mail');
|
|
224
255
|
const mailServices = createMailServices({
|
|
@@ -271,6 +302,21 @@ function emailPlugin(options = {}) {
|
|
|
271
302
|
console.warn('[email] authEmails.enabled but no auth manager found — pass auth to createApp or emailPlugin');
|
|
272
303
|
}
|
|
273
304
|
|
|
305
|
+
// Mount Dev Mail Preview & Inbox
|
|
306
|
+
const enableDevInbox = options.devInbox !== false && (process.env.NODE_ENV !== 'production' || options.devInbox === true);
|
|
307
|
+
if (enableDevInbox && typeof ctx.addRoute === 'function') {
|
|
308
|
+
const { createMailPreviewHandlers } = require('./preview-handler');
|
|
309
|
+
const previewPath = options.previewPath || '/_webspresso/mail-preview';
|
|
310
|
+
const previewHandlers = createMailPreviewHandlers({ mailer, basePath: previewPath });
|
|
311
|
+
|
|
312
|
+
ctx.addRoute('get', previewPath, previewHandlers.indexHandler);
|
|
313
|
+
ctx.addRoute('get', `${previewPath}/api/messages`, previewHandlers.listMessagesHandler);
|
|
314
|
+
ctx.addRoute('get', `${previewPath}/api/messages/:id`, previewHandlers.getMessageHandler);
|
|
315
|
+
ctx.addRoute('get', `${previewPath}/api/messages/:id/html`, previewHandlers.getMessageHtmlHandler);
|
|
316
|
+
ctx.addRoute('post', `${previewPath}/api/clear`, previewHandlers.clearMessagesHandler);
|
|
317
|
+
ctx.addRoute('post', `${previewPath}/api/preview`, previewHandlers.previewHandler);
|
|
318
|
+
}
|
|
319
|
+
|
|
274
320
|
if (!includeAdminPage) return;
|
|
275
321
|
|
|
276
322
|
const adminApi = ctx.usePlugin('admin-panel');
|
|
@@ -324,6 +370,8 @@ function emailPlugin(options = {}) {
|
|
|
324
370
|
|
|
325
371
|
module.exports = emailPlugin;
|
|
326
372
|
module.exports.emailPlugin = emailPlugin;
|
|
373
|
+
module.exports.Mailable = Mailable;
|
|
374
|
+
module.exports.createMailer = createMailer;
|
|
327
375
|
module.exports.generateEmailLogsMigration = generateEmailLogsMigration;
|
|
328
376
|
module.exports.purgeEmailLogs = purgeEmailLogs;
|
|
329
377
|
module.exports.emailPluginOptionsFromManifest = emailPluginOptionsFromManifest;
|
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Developer Mail Preview & Dev Inbox (AdonisJS & Laravel-inspired)
|
|
3
|
+
* Mounted at /_webspresso/mail-preview in development
|
|
4
|
+
* @module plugins/email/preview-handler
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate standalone responsive HTML for Dev Mail Preview UI
|
|
9
|
+
* @param {Array<Object>} messages
|
|
10
|
+
* @param {string} [basePath='/_webspresso/mail-preview']
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
function renderPreviewHtml(messages = [], basePath = '/_webspresso/mail-preview') {
|
|
14
|
+
const safeJson = JSON.stringify(messages).replace(/</g, '\\u003c');
|
|
15
|
+
|
|
16
|
+
return `<!DOCTYPE html>
|
|
17
|
+
<html lang="en">
|
|
18
|
+
<head>
|
|
19
|
+
<meta charset="UTF-8">
|
|
20
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
21
|
+
<title>Mail Preview & Dev Inbox — Webspresso</title>
|
|
22
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
23
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
24
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
25
|
+
<style>
|
|
26
|
+
:root {
|
|
27
|
+
--bg: #090d16;
|
|
28
|
+
--surface: #111827;
|
|
29
|
+
--surface-hover: #1f2937;
|
|
30
|
+
--border: #1e293b;
|
|
31
|
+
--text: #f1f5f9;
|
|
32
|
+
--text-muted: #94a3b8;
|
|
33
|
+
--accent: #6366f1;
|
|
34
|
+
--accent-hover: #4f46e5;
|
|
35
|
+
--accent-glow: rgba(99, 102, 241, 0.15);
|
|
36
|
+
--success: #10b981;
|
|
37
|
+
--danger: #ef4444;
|
|
38
|
+
}
|
|
39
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
40
|
+
body {
|
|
41
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
42
|
+
background: var(--bg);
|
|
43
|
+
color: var(--text);
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
height: 100vh;
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
}
|
|
49
|
+
header {
|
|
50
|
+
height: 56px;
|
|
51
|
+
background: var(--surface);
|
|
52
|
+
border-bottom: 1px solid var(--border);
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: space-between;
|
|
56
|
+
padding: 0 20px;
|
|
57
|
+
z-index: 10;
|
|
58
|
+
}
|
|
59
|
+
.logo-group {
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 12px;
|
|
63
|
+
}
|
|
64
|
+
.logo-badge {
|
|
65
|
+
background: linear-gradient(135deg, #6366f1, #a855f7);
|
|
66
|
+
color: #fff;
|
|
67
|
+
font-weight: 700;
|
|
68
|
+
font-size: 14px;
|
|
69
|
+
padding: 4px 10px;
|
|
70
|
+
border-radius: 8px;
|
|
71
|
+
letter-spacing: 0.5px;
|
|
72
|
+
}
|
|
73
|
+
.header-title {
|
|
74
|
+
font-size: 15px;
|
|
75
|
+
font-weight: 600;
|
|
76
|
+
color: var(--text);
|
|
77
|
+
}
|
|
78
|
+
.header-count {
|
|
79
|
+
font-size: 12px;
|
|
80
|
+
color: var(--text-muted);
|
|
81
|
+
background: rgba(255,255,255,0.06);
|
|
82
|
+
padding: 2px 8px;
|
|
83
|
+
border-radius: 999px;
|
|
84
|
+
}
|
|
85
|
+
.header-actions {
|
|
86
|
+
display: flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
gap: 10px;
|
|
89
|
+
}
|
|
90
|
+
.btn {
|
|
91
|
+
background: rgba(255,255,255,0.06);
|
|
92
|
+
color: var(--text);
|
|
93
|
+
border: 1px solid var(--border);
|
|
94
|
+
padding: 6px 14px;
|
|
95
|
+
border-radius: 6px;
|
|
96
|
+
font-size: 13px;
|
|
97
|
+
font-weight: 500;
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
transition: all 0.2s;
|
|
100
|
+
display: inline-flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
gap: 6px;
|
|
103
|
+
}
|
|
104
|
+
.btn:hover { background: var(--surface-hover); }
|
|
105
|
+
.btn-danger { color: #fca5a5; }
|
|
106
|
+
.btn-danger:hover { background: rgba(239, 68, 68, 0.15); border-color: var(--danger); }
|
|
107
|
+
.main-layout {
|
|
108
|
+
display: flex;
|
|
109
|
+
flex: 1;
|
|
110
|
+
height: calc(100vh - 56px);
|
|
111
|
+
overflow: hidden;
|
|
112
|
+
}
|
|
113
|
+
.sidebar {
|
|
114
|
+
width: 360px;
|
|
115
|
+
background: var(--surface);
|
|
116
|
+
border-right: 1px solid var(--border);
|
|
117
|
+
overflow-y: auto;
|
|
118
|
+
display: flex;
|
|
119
|
+
flex-direction: column;
|
|
120
|
+
}
|
|
121
|
+
.email-item {
|
|
122
|
+
padding: 14px 16px;
|
|
123
|
+
border-bottom: 1px solid var(--border);
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
transition: background 0.15s;
|
|
126
|
+
}
|
|
127
|
+
.email-item:hover { background: rgba(255,255,255,0.03); }
|
|
128
|
+
.email-item.active {
|
|
129
|
+
background: var(--accent-glow);
|
|
130
|
+
border-left: 3px solid var(--accent);
|
|
131
|
+
}
|
|
132
|
+
.email-to {
|
|
133
|
+
font-size: 13px;
|
|
134
|
+
font-weight: 600;
|
|
135
|
+
color: var(--text);
|
|
136
|
+
display: flex;
|
|
137
|
+
justify-content: space-between;
|
|
138
|
+
margin-bottom: 4px;
|
|
139
|
+
}
|
|
140
|
+
.email-time {
|
|
141
|
+
font-size: 11px;
|
|
142
|
+
font-weight: 400;
|
|
143
|
+
color: var(--text-muted);
|
|
144
|
+
}
|
|
145
|
+
.email-subject {
|
|
146
|
+
font-size: 13px;
|
|
147
|
+
color: #cbd5e1;
|
|
148
|
+
white-space: nowrap;
|
|
149
|
+
overflow: hidden;
|
|
150
|
+
text-overflow: ellipsis;
|
|
151
|
+
margin-bottom: 4px;
|
|
152
|
+
}
|
|
153
|
+
.email-preview {
|
|
154
|
+
font-size: 12px;
|
|
155
|
+
color: var(--text-muted);
|
|
156
|
+
white-space: nowrap;
|
|
157
|
+
overflow: hidden;
|
|
158
|
+
text-overflow: ellipsis;
|
|
159
|
+
}
|
|
160
|
+
.content-area {
|
|
161
|
+
flex: 1;
|
|
162
|
+
display: flex;
|
|
163
|
+
flex-direction: column;
|
|
164
|
+
background: var(--bg);
|
|
165
|
+
overflow: hidden;
|
|
166
|
+
}
|
|
167
|
+
.viewer-header {
|
|
168
|
+
background: var(--surface);
|
|
169
|
+
border-bottom: 1px solid var(--border);
|
|
170
|
+
padding: 16px 24px;
|
|
171
|
+
}
|
|
172
|
+
.vh-subject {
|
|
173
|
+
font-size: 18px;
|
|
174
|
+
font-weight: 700;
|
|
175
|
+
margin-bottom: 8px;
|
|
176
|
+
}
|
|
177
|
+
.vh-meta {
|
|
178
|
+
display: flex;
|
|
179
|
+
flex-wrap: wrap;
|
|
180
|
+
gap: 16px;
|
|
181
|
+
font-size: 12px;
|
|
182
|
+
color: var(--text-muted);
|
|
183
|
+
}
|
|
184
|
+
.vh-meta strong { color: var(--text); }
|
|
185
|
+
.viewer-toolbar {
|
|
186
|
+
padding: 8px 24px;
|
|
187
|
+
background: rgba(17, 24, 39, 0.6);
|
|
188
|
+
border-bottom: 1px solid var(--border);
|
|
189
|
+
display: flex;
|
|
190
|
+
justify-content: space-between;
|
|
191
|
+
align-items: center;
|
|
192
|
+
}
|
|
193
|
+
.tab-group, .viewport-group {
|
|
194
|
+
display: flex;
|
|
195
|
+
gap: 6px;
|
|
196
|
+
}
|
|
197
|
+
.tab-btn, .vp-btn {
|
|
198
|
+
padding: 4px 10px;
|
|
199
|
+
border-radius: 4px;
|
|
200
|
+
font-size: 12px;
|
|
201
|
+
background: transparent;
|
|
202
|
+
border: 1px solid transparent;
|
|
203
|
+
color: var(--text-muted);
|
|
204
|
+
cursor: pointer;
|
|
205
|
+
}
|
|
206
|
+
.tab-btn.active, .vp-btn.active {
|
|
207
|
+
background: rgba(255,255,255,0.08);
|
|
208
|
+
border-color: var(--border);
|
|
209
|
+
color: var(--text);
|
|
210
|
+
font-weight: 600;
|
|
211
|
+
}
|
|
212
|
+
.viewport-container {
|
|
213
|
+
flex: 1;
|
|
214
|
+
display: flex;
|
|
215
|
+
justify-content: center;
|
|
216
|
+
align-items: stretch;
|
|
217
|
+
padding: 20px;
|
|
218
|
+
overflow-y: auto;
|
|
219
|
+
background: #020617;
|
|
220
|
+
}
|
|
221
|
+
.preview-frame-wrapper {
|
|
222
|
+
width: 100%;
|
|
223
|
+
height: 100%;
|
|
224
|
+
background: #ffffff;
|
|
225
|
+
border-radius: 8px;
|
|
226
|
+
overflow: hidden;
|
|
227
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
|
|
228
|
+
transition: width 0.3s ease;
|
|
229
|
+
}
|
|
230
|
+
.preview-frame-wrapper.mobile { width: 375px; }
|
|
231
|
+
.preview-frame-wrapper.tablet { width: 768px; }
|
|
232
|
+
iframe {
|
|
233
|
+
width: 100%;
|
|
234
|
+
height: 100%;
|
|
235
|
+
border: none;
|
|
236
|
+
}
|
|
237
|
+
pre.code-view {
|
|
238
|
+
font-family: 'JetBrains Mono', monospace;
|
|
239
|
+
font-size: 13px;
|
|
240
|
+
line-height: 1.6;
|
|
241
|
+
background: #090d16;
|
|
242
|
+
color: #93c5fd;
|
|
243
|
+
padding: 20px;
|
|
244
|
+
border-radius: 8px;
|
|
245
|
+
width: 100%;
|
|
246
|
+
height: 100%;
|
|
247
|
+
overflow: auto;
|
|
248
|
+
white-space: pre-wrap;
|
|
249
|
+
}
|
|
250
|
+
.empty-state {
|
|
251
|
+
flex: 1;
|
|
252
|
+
display: flex;
|
|
253
|
+
flex-direction: column;
|
|
254
|
+
align-items: center;
|
|
255
|
+
justify-content: center;
|
|
256
|
+
color: var(--text-muted);
|
|
257
|
+
text-align: center;
|
|
258
|
+
padding: 40px;
|
|
259
|
+
}
|
|
260
|
+
.empty-icon {
|
|
261
|
+
font-size: 48px;
|
|
262
|
+
margin-bottom: 16px;
|
|
263
|
+
opacity: 0.8;
|
|
264
|
+
}
|
|
265
|
+
.empty-title {
|
|
266
|
+
font-size: 18px;
|
|
267
|
+
font-weight: 600;
|
|
268
|
+
color: var(--text);
|
|
269
|
+
margin-bottom: 8px;
|
|
270
|
+
}
|
|
271
|
+
.empty-code {
|
|
272
|
+
margin-top: 20px;
|
|
273
|
+
background: rgba(255,255,255,0.04);
|
|
274
|
+
border: 1px solid var(--border);
|
|
275
|
+
padding: 12px 18px;
|
|
276
|
+
border-radius: 8px;
|
|
277
|
+
font-family: 'JetBrains Mono', monospace;
|
|
278
|
+
font-size: 12px;
|
|
279
|
+
color: #a5b4fc;
|
|
280
|
+
text-align: left;
|
|
281
|
+
}
|
|
282
|
+
</style>
|
|
283
|
+
</head>
|
|
284
|
+
<body>
|
|
285
|
+
<header>
|
|
286
|
+
<div class="logo-group">
|
|
287
|
+
<span class="logo-badge">Webspresso</span>
|
|
288
|
+
<span class="header-title">📬 Mail Preview & Dev Inbox</span>
|
|
289
|
+
<span class="header-count" id="headerCount">0 emails</span>
|
|
290
|
+
</div>
|
|
291
|
+
<div class="header-actions">
|
|
292
|
+
<button class="btn" onclick="fetchMessages()">↻ Refresh</button>
|
|
293
|
+
<button class="btn btn-danger" onclick="clearOutbox()">🗑️ Clear Outbox</button>
|
|
294
|
+
</div>
|
|
295
|
+
</header>
|
|
296
|
+
|
|
297
|
+
<div class="main-layout">
|
|
298
|
+
<div class="sidebar" id="sidebarList">
|
|
299
|
+
<!-- Populated via JS -->
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<div class="content-area" id="viewerArea">
|
|
303
|
+
<!-- Populated via JS -->
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<script>
|
|
308
|
+
let messages = ${safeJson};
|
|
309
|
+
let selectedId = messages.length > 0 ? messages[messages.length - 1].id : null;
|
|
310
|
+
let activeTab = 'html';
|
|
311
|
+
let activeViewport = 'desktop';
|
|
312
|
+
|
|
313
|
+
function renderSidebar() {
|
|
314
|
+
const sidebar = document.getElementById('sidebarList');
|
|
315
|
+
const countEl = document.getElementById('headerCount');
|
|
316
|
+
countEl.textContent = messages.length + ' email' + (messages.length === 1 ? '' : 's');
|
|
317
|
+
|
|
318
|
+
if (messages.length === 0) {
|
|
319
|
+
sidebar.innerHTML = '<div style="padding: 24px; text-align: center; color: var(--text-muted); font-size: 13px;">No emails captured yet</div>';
|
|
320
|
+
renderEmptyViewer();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
sidebar.innerHTML = messages.slice().reverse().map(msg => {
|
|
325
|
+
const isSelected = msg.id === selectedId;
|
|
326
|
+
const toDisplay = Array.isArray(msg.to) ? msg.to.join(', ') : (typeof msg.to === 'object' ? (msg.to.address || JSON.stringify(msg.to)) : String(msg.to || ''));
|
|
327
|
+
const dateStr = msg.sentAt ? new Date(msg.sentAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '';
|
|
328
|
+
const preview = msg.text || (msg.html ? msg.html.replace(/<[^>]+>/g, ' ').slice(0, 80) : '');
|
|
329
|
+
|
|
330
|
+
return \`
|
|
331
|
+
<div class="email-item \${isSelected ? 'active' : ''}" onclick="selectEmail('\${msg.id}')">
|
|
332
|
+
<div class="email-to">
|
|
333
|
+
<span>\${escapeHtml(toDisplay)}</span>
|
|
334
|
+
<span class="email-time">\${dateStr}</span>
|
|
335
|
+
</div>
|
|
336
|
+
<div class="email-subject">\${escapeHtml(msg.subject || '(No Subject)')}</div>
|
|
337
|
+
<div class="email-preview">\${escapeHtml(preview)}</div>
|
|
338
|
+
</div>
|
|
339
|
+
\`;
|
|
340
|
+
}).join('');
|
|
341
|
+
|
|
342
|
+
renderContent();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function renderEmptyViewer() {
|
|
346
|
+
const viewer = document.getElementById('viewerArea');
|
|
347
|
+
viewer.innerHTML = \`
|
|
348
|
+
<div class="empty-state">
|
|
349
|
+
<div class="empty-icon">✉️</div>
|
|
350
|
+
<div class="empty-title">Inbox is Empty</div>
|
|
351
|
+
<p>Send an email using <code>req.mailer.send(...)</code> or a <code>new Mailable()</code> class in your application to preview it here.</p>
|
|
352
|
+
<div class="empty-code">
|
|
353
|
+
// Example dispatch:<br>
|
|
354
|
+
await req.mailer.send({<br>
|
|
355
|
+
to: 'user@example.com',<br>
|
|
356
|
+
subject: 'Welcome!',<br>
|
|
357
|
+
html: '<h1>Welcome to Webspresso!</h1>'<br>
|
|
358
|
+
});
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
\`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function renderContent() {
|
|
365
|
+
const viewer = document.getElementById('viewerArea');
|
|
366
|
+
const msg = messages.find(m => m.id === selectedId);
|
|
367
|
+
if (!msg) {
|
|
368
|
+
if (messages.length > 0) {
|
|
369
|
+
selectedId = messages[messages.length - 1].id;
|
|
370
|
+
return renderContent();
|
|
371
|
+
}
|
|
372
|
+
return renderEmptyViewer();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const toDisplay = Array.isArray(msg.to) ? msg.to.join(', ') : (typeof msg.to === 'object' ? (msg.to.address || JSON.stringify(msg.to)) : String(msg.to || ''));
|
|
376
|
+
const fromDisplay = typeof msg.from === 'object' ? (msg.from.address || JSON.stringify(msg.from)) : String(msg.from || '');
|
|
377
|
+
const dateStr = msg.sentAt ? new Date(msg.sentAt).toLocaleString() : '';
|
|
378
|
+
|
|
379
|
+
viewer.innerHTML = \`
|
|
380
|
+
<div class="viewer-header">
|
|
381
|
+
<div class="vh-subject">\${escapeHtml(msg.subject || '(No Subject)')}</div>
|
|
382
|
+
<div class="vh-meta">
|
|
383
|
+
<div><strong>From:</strong> \${escapeHtml(fromDisplay)}</div>
|
|
384
|
+
<div><strong>To:</strong> \${escapeHtml(toDisplay)}</div>
|
|
385
|
+
\${msg.cc ? \`<div><strong>Cc:</strong> \${escapeHtml(String(msg.cc))}</div>\` : ''}
|
|
386
|
+
<div><strong>Date:</strong> \${dateStr}</div>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<div class="viewer-toolbar">
|
|
391
|
+
<div class="tab-group">
|
|
392
|
+
<button class="tab-btn \${activeTab === 'html' ? 'active' : ''}" onclick="setTab('html')">HTML View</button>
|
|
393
|
+
<button class="tab-btn \${activeTab === 'text' ? 'active' : ''}" onclick="setTab('text')">Plain Text</button>
|
|
394
|
+
<button class="tab-btn \${activeTab === 'raw' ? 'active' : ''}" onclick="setTab('raw')">Headers & Payload</button>
|
|
395
|
+
</div>
|
|
396
|
+
\${activeTab === 'html' ? \`
|
|
397
|
+
<div class="viewport-group">
|
|
398
|
+
<button class="vp-btn \${activeViewport === 'desktop' ? 'active' : ''}" onclick="setViewport('desktop')">🖥️ Desktop</button>
|
|
399
|
+
<button class="vp-btn \${activeViewport === 'tablet' ? 'active' : ''}" onclick="setViewport('tablet')">📱 Tablet</button>
|
|
400
|
+
<button class="vp-btn \${activeViewport === 'mobile' ? 'active' : ''}" onclick="setViewport('mobile')">📱 Mobile</button>
|
|
401
|
+
</div>
|
|
402
|
+
\` : '<div></div>'}
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
<div class="viewport-container">
|
|
406
|
+
\${activeTab === 'html' ? \`
|
|
407
|
+
<div class="preview-frame-wrapper \${activeViewport}">
|
|
408
|
+
<iframe id="previewFrame" src="${basePath}/api/messages/\${msg.id}/html"></iframe>
|
|
409
|
+
</div>
|
|
410
|
+
\` : activeTab === 'text' ? \`
|
|
411
|
+
<pre class="code-view">\${escapeHtml(msg.text || '(No plain text body)')}</pre>
|
|
412
|
+
\` : \`
|
|
413
|
+
<pre class="code-view">\${escapeHtml(JSON.stringify(msg, null, 2))}</pre>
|
|
414
|
+
\`}
|
|
415
|
+
</div>
|
|
416
|
+
\`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function selectEmail(id) {
|
|
420
|
+
selectedId = id;
|
|
421
|
+
renderSidebar();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function setTab(tab) {
|
|
425
|
+
activeTab = tab;
|
|
426
|
+
renderContent();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function setViewport(vp) {
|
|
430
|
+
activeViewport = vp;
|
|
431
|
+
renderContent();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function fetchMessages() {
|
|
435
|
+
try {
|
|
436
|
+
const res = await fetch('${basePath}/api/messages');
|
|
437
|
+
const data = await res.json();
|
|
438
|
+
messages = data.messages || [];
|
|
439
|
+
if (!messages.some(m => m.id === selectedId) && messages.length > 0) {
|
|
440
|
+
selectedId = messages[messages.length - 1].id;
|
|
441
|
+
}
|
|
442
|
+
renderSidebar();
|
|
443
|
+
} catch (err) {
|
|
444
|
+
console.error('Failed to refresh messages:', err);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function clearOutbox() {
|
|
449
|
+
if (!confirm('Clear all captured emails from outbox?')) return;
|
|
450
|
+
try {
|
|
451
|
+
await fetch('${basePath}/api/clear', { method: 'POST' });
|
|
452
|
+
messages = [];
|
|
453
|
+
selectedId = null;
|
|
454
|
+
renderSidebar();
|
|
455
|
+
} catch (err) {
|
|
456
|
+
console.error('Failed to clear outbox:', err);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function escapeHtml(str) {
|
|
461
|
+
if (!str) return '';
|
|
462
|
+
return String(str)
|
|
463
|
+
.replace(/&/g, '&')
|
|
464
|
+
.replace(/</g, '<')
|
|
465
|
+
.replace(/>/g, '>')
|
|
466
|
+
.replace(/"/g, '"')
|
|
467
|
+
.replace(/'/g, ''');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
renderSidebar();
|
|
471
|
+
</script>
|
|
472
|
+
</body>
|
|
473
|
+
</html>`;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Create Dev Mail Preview HTTP handlers
|
|
478
|
+
* @param {Object} options
|
|
479
|
+
* @param {import('../../core/mail/mailer').Mailer} options.mailer
|
|
480
|
+
* @param {string} [options.basePath='/_webspresso/mail-preview']
|
|
481
|
+
*/
|
|
482
|
+
function createMailPreviewHandlers({ mailer, basePath = '/_webspresso/mail-preview' }) {
|
|
483
|
+
return {
|
|
484
|
+
/**
|
|
485
|
+
* GET /_webspresso/mail-preview
|
|
486
|
+
* Serve HTML Dev Inbox SPA
|
|
487
|
+
*/
|
|
488
|
+
indexHandler(req, res) {
|
|
489
|
+
const messages = mailer.getOutbox();
|
|
490
|
+
const html = renderPreviewHtml(messages, basePath);
|
|
491
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
492
|
+
res.send(html);
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* GET /_webspresso/mail-preview/api/messages
|
|
497
|
+
*/
|
|
498
|
+
listMessagesHandler(req, res) {
|
|
499
|
+
const messages = mailer.getOutbox();
|
|
500
|
+
res.json({ success: true, count: messages.length, messages });
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* GET /_webspresso/mail-preview/api/messages/:id
|
|
505
|
+
*/
|
|
506
|
+
getMessageHandler(req, res) {
|
|
507
|
+
const { id } = req.params;
|
|
508
|
+
const messages = mailer.getOutbox();
|
|
509
|
+
const msg = messages.find((m) => m.id === id || m.messageId === id);
|
|
510
|
+
if (!msg) {
|
|
511
|
+
return res.status(404).json({ error: 'Message not found' });
|
|
512
|
+
}
|
|
513
|
+
res.json({ success: true, message: msg });
|
|
514
|
+
},
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* GET /_webspresso/mail-preview/api/messages/:id/html
|
|
518
|
+
*/
|
|
519
|
+
getMessageHtmlHandler(req, res) {
|
|
520
|
+
const { id } = req.params;
|
|
521
|
+
const messages = mailer.getOutbox();
|
|
522
|
+
const msg = messages.find((m) => m.id === id || m.messageId === id);
|
|
523
|
+
if (!msg) {
|
|
524
|
+
return res.status(404).send('<p>Message not found</p>');
|
|
525
|
+
}
|
|
526
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
527
|
+
res.send(msg.html || `<pre>${msg.text || 'No content'}</pre>`);
|
|
528
|
+
},
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* POST /_webspresso/mail-preview/api/clear
|
|
532
|
+
*/
|
|
533
|
+
clearMessagesHandler(req, res) {
|
|
534
|
+
mailer.clearOutbox();
|
|
535
|
+
res.json({ success: true, message: 'Outbox cleared' });
|
|
536
|
+
},
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* POST /_webspresso/mail-preview/api/preview
|
|
540
|
+
*/
|
|
541
|
+
async previewHandler(req, res) {
|
|
542
|
+
try {
|
|
543
|
+
const previewResult = await mailer.preview(req.body);
|
|
544
|
+
res.json({ success: true, preview: previewResult });
|
|
545
|
+
} catch (err) {
|
|
546
|
+
res.status(400).json({ error: err.message });
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
module.exports = {
|
|
553
|
+
renderPreviewHtml,
|
|
554
|
+
createMailPreviewHandlers,
|
|
555
|
+
};
|