webspresso 0.0.98 → 0.0.100
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 +11 -1
- package/.agents/AGENTS.md +2 -2
- package/.agents/AUTH.md +415 -0
- package/.agents/CLI.md +47 -0
- package/.agents/ERRORS.md +59 -0
- package/.agents/ORM.md +178 -4
- package/.agents/PLUGINS.md +113 -8
- package/.agents/QUEUE.md +92 -0
- package/.agents/ROUTING.md +160 -7
- 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/deploy-check.js +339 -0
- package/bin/webspresso.js +4 -0
- package/core/auth/index.js +123 -1
- package/core/auth/magic-link.js +170 -0
- package/core/auth/manager.js +477 -3
- package/core/auth/middleware.js +118 -5
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- 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 +507 -0
- package/core/auth/user-agent.js +161 -0
- package/core/orm/eager-loader.js +144 -10
- package/core/orm/index.js +10 -1
- package/core/orm/model.js +55 -1
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +327 -16
- package/core/orm/scopes.js +3 -2
- package/core/orm/search.js +213 -0
- package/core/orm/types.js +6 -1
- package/core/queue/cron.js +544 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +100 -1
- package/core/queue/scheduler.js +415 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/core/ssr/stream.js +62 -2
- package/index.d.ts +794 -6
- package/index.js +48 -0
- package/package.json +7 -5
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +323 -31
- package/plugins/admin-panel/client/parts/08-record-list.js +5 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
- package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
- package/plugins/admin-panel/field-renderers/index.js +2 -0
- package/plugins/admin-panel/modules/admin-users.js +1 -1
- package/plugins/api-key/crypto.js +101 -0
- package/plugins/api-key/index.js +288 -0
- package/plugins/api-key/middleware.js +461 -0
- package/plugins/api-key/migration-template.js +48 -0
- package/plugins/api-key/model.js +50 -0
- package/plugins/api-key/scopes.js +125 -0
- package/plugins/audit-log/admin-component.js +487 -70
- package/plugins/audit-log/api-handlers.js +114 -0
- package/plugins/audit-log/index.js +24 -0
- package/plugins/content/client/inline-edit.js +2 -46
- package/plugins/cors.js +13 -2
- package/plugins/email/auth-bridge.js +66 -0
- package/plugins/email/bundled-templates.js +15 -0
- package/plugins/email/templates/auth-magic-link.mjml +13 -0
- package/plugins/file-manager/admin-component.js +69 -53
- package/plugins/file-manager/index.js +25 -8
- package/plugins/index.js +8 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/queue/index.js +4 -0
- 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 -0
- package/src/bundle/index.js +2 -1
- package/src/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +310 -104
- package/src/helpers.js +16 -1
- package/src/njk-frontmatter.js +3 -0
- package/src/routing/cascading.js +490 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +6 -0
|
@@ -21,12 +21,34 @@ function createAuditLogHandlers(options) {
|
|
|
21
21
|
if (query.action) {
|
|
22
22
|
qb.where('action', String(query.action));
|
|
23
23
|
}
|
|
24
|
+
const recordId = query.recordId || query.resource_id || query.resourceId;
|
|
25
|
+
if (recordId !== undefined && recordId !== null && String(recordId).trim() !== '') {
|
|
26
|
+
qb.where('resource_id', String(recordId));
|
|
27
|
+
}
|
|
28
|
+
const actorId = query.actorId || query.actor_id;
|
|
29
|
+
if (actorId !== undefined && actorId !== null && String(actorId).trim() !== '') {
|
|
30
|
+
qb.where('actor_id', Number(actorId));
|
|
31
|
+
}
|
|
32
|
+
const actorEmail = query.actorEmail || query.actor_email;
|
|
33
|
+
if (actorEmail) {
|
|
34
|
+
qb.where('actor_email', 'like', `%${String(actorEmail).trim()}%`);
|
|
35
|
+
}
|
|
24
36
|
if (query.from) {
|
|
25
37
|
qb.where('created_at', '>=', String(query.from));
|
|
26
38
|
}
|
|
27
39
|
if (query.to) {
|
|
28
40
|
qb.where('created_at', '<=', String(query.to));
|
|
29
41
|
}
|
|
42
|
+
const searchTerm = query.search || query.q;
|
|
43
|
+
if (searchTerm && String(searchTerm).trim() !== '') {
|
|
44
|
+
const term = `%${String(searchTerm).trim()}%`;
|
|
45
|
+
qb.where((sub) => {
|
|
46
|
+
sub.where('actor_email', 'like', term)
|
|
47
|
+
.orWhere('path', 'like', term)
|
|
48
|
+
.orWhere('resource_id', 'like', term)
|
|
49
|
+
.orWhere('resource_model', 'like', term);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
30
52
|
}
|
|
31
53
|
|
|
32
54
|
async function listHandler(req, res) {
|
|
@@ -53,6 +75,79 @@ function createAuditLogHandlers(options) {
|
|
|
53
75
|
}
|
|
54
76
|
}
|
|
55
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Dedicated Record Timeline Handler
|
|
80
|
+
* GET /audit-logs/timeline/:model/:id or GET /audit-logs/timeline?model=Post&id=1
|
|
81
|
+
*/
|
|
82
|
+
async function timelineHandler(req, res) {
|
|
83
|
+
try {
|
|
84
|
+
const model = req.params?.model || req.query?.model;
|
|
85
|
+
const recordId = req.params?.id || req.query?.id || req.query?.recordId || req.query?.resource_id;
|
|
86
|
+
|
|
87
|
+
if (!model || recordId === undefined) {
|
|
88
|
+
return res.status(400).json({ error: 'model and id are required to fetch timeline' });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const rows = await knex(tableName)
|
|
92
|
+
.where({
|
|
93
|
+
resource_model: String(model),
|
|
94
|
+
resource_id: String(recordId),
|
|
95
|
+
})
|
|
96
|
+
.orderBy('created_at', 'asc');
|
|
97
|
+
|
|
98
|
+
const totalChanges = rows.length;
|
|
99
|
+
const createdEvent = rows.find((r) => r.action === 'create') || null;
|
|
100
|
+
const lastModifiedEvent = rows.length > 0 ? rows[rows.length - 1] : null;
|
|
101
|
+
|
|
102
|
+
const events = rows.map((r, index) => {
|
|
103
|
+
let parsedMetadata = r.metadata;
|
|
104
|
+
if (typeof parsedMetadata === 'string') {
|
|
105
|
+
try {
|
|
106
|
+
parsedMetadata = JSON.parse(parsedMetadata);
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const changedFields = Array.isArray(parsedMetadata?.changedFields)
|
|
111
|
+
? parsedMetadata.changedFields
|
|
112
|
+
: [];
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
id: r.id,
|
|
116
|
+
action: r.action,
|
|
117
|
+
createdAt: r.created_at,
|
|
118
|
+
actor: {
|
|
119
|
+
id: r.actor_id,
|
|
120
|
+
email: r.actor_email,
|
|
121
|
+
},
|
|
122
|
+
resourceModel: r.resource_model,
|
|
123
|
+
resourceId: r.resource_id,
|
|
124
|
+
httpMethod: r.http_method,
|
|
125
|
+
path: r.path,
|
|
126
|
+
ip: r.ip,
|
|
127
|
+
userAgent: r.user_agent,
|
|
128
|
+
changedFields,
|
|
129
|
+
metadata: parsedMetadata,
|
|
130
|
+
index: index + 1,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
res.json({
|
|
135
|
+
model: String(model),
|
|
136
|
+
recordId: String(recordId),
|
|
137
|
+
summary: {
|
|
138
|
+
totalChanges,
|
|
139
|
+
createdAt: createdEvent?.created_at || (events[0] ? events[0].createdAt : null),
|
|
140
|
+
createdBy: createdEvent?.actor_email || (events[0] ? events[0].actor?.email : null),
|
|
141
|
+
lastModifiedAt: lastModifiedEvent?.created_at || null,
|
|
142
|
+
lastModifiedBy: lastModifiedEvent?.actor_email || null,
|
|
143
|
+
},
|
|
144
|
+
timeline: events,
|
|
145
|
+
});
|
|
146
|
+
} catch (err) {
|
|
147
|
+
res.status(500).json({ error: err.message });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
56
151
|
async function purgeHandler(req, res) {
|
|
57
152
|
try {
|
|
58
153
|
const body = req.body || {};
|
|
@@ -89,8 +184,10 @@ function createAuditLogHandlers(options) {
|
|
|
89
184
|
* @param {Object} [filters]
|
|
90
185
|
* @param {string} [filters.model]
|
|
91
186
|
* @param {string} [filters.action]
|
|
187
|
+
* @param {string} [filters.recordId]
|
|
92
188
|
* @param {string} [filters.from]
|
|
93
189
|
* @param {string} [filters.to]
|
|
190
|
+
* @param {string} [filters.search]
|
|
94
191
|
* @param {number} [filters.limit=100]
|
|
95
192
|
* @param {number} [filters.offset=0]
|
|
96
193
|
*/
|
|
@@ -105,10 +202,27 @@ function createAuditLogHandlers(options) {
|
|
|
105
202
|
.limit(limit);
|
|
106
203
|
}
|
|
107
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Programmatic query for single record timeline
|
|
207
|
+
* @param {string} model
|
|
208
|
+
* @param {string|number} recordId
|
|
209
|
+
*/
|
|
210
|
+
async function queryTimeline(model, recordId) {
|
|
211
|
+
if (!model || recordId === undefined) return [];
|
|
212
|
+
return knex(tableName)
|
|
213
|
+
.where({
|
|
214
|
+
resource_model: String(model),
|
|
215
|
+
resource_id: String(recordId),
|
|
216
|
+
})
|
|
217
|
+
.orderBy('created_at', 'asc');
|
|
218
|
+
}
|
|
219
|
+
|
|
108
220
|
return {
|
|
109
221
|
listHandler,
|
|
222
|
+
timelineHandler,
|
|
110
223
|
purgeHandler,
|
|
111
224
|
queryLogs,
|
|
225
|
+
queryTimeline,
|
|
112
226
|
};
|
|
113
227
|
}
|
|
114
228
|
|
|
@@ -36,6 +36,7 @@ function auditLogPlugin(options = {}) {
|
|
|
36
36
|
tableName = 'audit_logs',
|
|
37
37
|
includeDefaultPage = true,
|
|
38
38
|
apiPrefix = '/audit-logs',
|
|
39
|
+
registerRecordAction = true,
|
|
39
40
|
} = options;
|
|
40
41
|
|
|
41
42
|
const pageTitle = title || adminTitle || customPageTitle || 'Audit log';
|
|
@@ -152,6 +153,8 @@ function auditLogPlugin(options = {}) {
|
|
|
152
153
|
return { ...retentionConfig };
|
|
153
154
|
},
|
|
154
155
|
queryLogs: (filters) => handlers.queryLogs(filters),
|
|
156
|
+
queryTimeline: (model, recordId) => handlers.queryTimeline(model, recordId),
|
|
157
|
+
getRecordTimeline: (model, recordId) => handlers.queryTimeline(model, recordId),
|
|
155
158
|
getMigrationTemplate: (name) => generateAuditLogsMigration(name || tableName),
|
|
156
159
|
},
|
|
157
160
|
|
|
@@ -211,10 +214,31 @@ function auditLogPlugin(options = {}) {
|
|
|
211
214
|
prefix: apiPrefix,
|
|
212
215
|
routes: [
|
|
213
216
|
{ method: 'get', path: '', handler: handlers.listHandler },
|
|
217
|
+
{ method: 'get', path: '/timeline', handler: handlers.timelineHandler },
|
|
218
|
+
{ method: 'get', path: '/timeline/:model/:id', handler: handlers.timelineHandler },
|
|
214
219
|
{ method: 'post', path: '/purge', handler: handlers.purgeHandler },
|
|
215
220
|
],
|
|
216
221
|
},
|
|
217
222
|
});
|
|
223
|
+
|
|
224
|
+
if (registerRecordAction && typeof adminApi.registerAction === 'function') {
|
|
225
|
+
adminApi.registerAction('audit-timeline', {
|
|
226
|
+
id: 'audit-timeline',
|
|
227
|
+
label: 'Timeline',
|
|
228
|
+
icon: 'clock',
|
|
229
|
+
color: 'blue',
|
|
230
|
+
models: '*',
|
|
231
|
+
handler: async (record, model) => {
|
|
232
|
+
const primaryKey = model?.primaryKey || 'id';
|
|
233
|
+
const id = record ? record[primaryKey] : null;
|
|
234
|
+
const modelName = model?.name || (typeof model === 'string' ? model : '');
|
|
235
|
+
return {
|
|
236
|
+
redirect: `/audit-log?model=${encodeURIComponent(modelName)}&recordId=${encodeURIComponent(id)}`,
|
|
237
|
+
message: `Timeline: ${modelName} #${id}`,
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|
|
218
242
|
},
|
|
219
243
|
};
|
|
220
244
|
}
|
|
@@ -249,54 +249,10 @@
|
|
|
249
249
|
return;
|
|
250
250
|
}
|
|
251
251
|
if (typeof window.DOMPurify !== 'undefined' && typeof window.DOMPurify.sanitize === 'function') {
|
|
252
|
-
el.innerHTML = window.DOMPurify.sanitize(htmlStr);
|
|
252
|
+
el.innerHTML = window.DOMPurify.sanitize(String(htmlStr));
|
|
253
253
|
return;
|
|
254
254
|
}
|
|
255
|
-
|
|
256
|
-
var template = document.createElement('template');
|
|
257
|
-
template.innerHTML = htmlStr;
|
|
258
|
-
var content = template.content;
|
|
259
|
-
var allowedTags = {
|
|
260
|
-
p: 1, br: 1, b: 1, i: 1, strong: 1, em: 1, u: 1, s: 1, strike: 1,
|
|
261
|
-
h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, ul: 1, ol: 1, li: 1,
|
|
262
|
-
blockquote: 1, pre: 1, code: 1, a: 1, span: 1, div: 1, hr: 1,
|
|
263
|
-
table: 1, thead: 1, tbody: 1, tr: 1, th: 1, td: 1, img: 1
|
|
264
|
-
};
|
|
265
|
-
var allowedAttrs = {
|
|
266
|
-
href: 1, title: 1, target: 1, rel: 1, class: 1, id: 1, src: 1, alt: 1, width: 1, height: 1
|
|
267
|
-
};
|
|
268
|
-
var allNodes = content.querySelectorAll('*');
|
|
269
|
-
for (var i = allNodes.length - 1; i >= 0; i--) {
|
|
270
|
-
var node = allNodes[i];
|
|
271
|
-
var tag = node.tagName.toLowerCase();
|
|
272
|
-
if (!allowedTags[tag]) {
|
|
273
|
-
if (node.parentNode) {
|
|
274
|
-
node.parentNode.removeChild(node);
|
|
275
|
-
} else if (typeof node.remove === 'function') {
|
|
276
|
-
node.remove();
|
|
277
|
-
}
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
var attrs = Array.prototype.slice.call(node.attributes);
|
|
281
|
-
for (var j = 0; j < attrs.length; j++) {
|
|
282
|
-
var attrName = attrs[j].name.toLowerCase();
|
|
283
|
-
var attrVal = (attrs[j].value || '').toLowerCase().replace(/[\x00-\x20\s]+/g, '').trim();
|
|
284
|
-
if (attrName.indexOf('on') === 0 || !allowedAttrs[attrName]) {
|
|
285
|
-
node.removeAttribute(attrs[j].name);
|
|
286
|
-
} else if ((attrName === 'href' || attrName === 'src' || attrName === 'action' || attrName === 'formaction' || attrName === 'xlink:href') &&
|
|
287
|
-
/^(?:javascript|data|vbscript):/i.test(attrVal)) {
|
|
288
|
-
node.removeAttribute(attrs[j].name);
|
|
289
|
-
} else if (tag === 'a' && node.getAttribute('target') === '_blank') {
|
|
290
|
-
node.setAttribute('rel', 'noopener noreferrer');
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
el.textContent = '';
|
|
296
|
-
el.appendChild(content.cloneNode(true));
|
|
297
|
-
} catch (e) {
|
|
298
|
-
el.textContent = htmlStr;
|
|
299
|
-
}
|
|
255
|
+
el.textContent = String(htmlStr);
|
|
300
256
|
}
|
|
301
257
|
|
|
302
258
|
function updateDomFields(entryId, data) {
|
package/plugins/cors.js
CHANGED
|
@@ -76,7 +76,18 @@ function createCorsMiddleware(options = {}) {
|
|
|
76
76
|
return config.origin(reqOrigin, callback);
|
|
77
77
|
}
|
|
78
78
|
if (config.origin === true) {
|
|
79
|
-
|
|
79
|
+
if (!reqOrigin || reqOrigin === 'null') {
|
|
80
|
+
return callback(null, false);
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const u = new URL(reqOrigin);
|
|
84
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
|
|
85
|
+
return callback(null, false);
|
|
86
|
+
}
|
|
87
|
+
return callback(null, u.origin);
|
|
88
|
+
} catch {
|
|
89
|
+
return callback(null, false);
|
|
90
|
+
}
|
|
80
91
|
}
|
|
81
92
|
return callback(null, false);
|
|
82
93
|
}
|
|
@@ -101,7 +112,7 @@ function createCorsMiddleware(options = {}) {
|
|
|
101
112
|
return next(err);
|
|
102
113
|
}
|
|
103
114
|
|
|
104
|
-
if (allowedOrigin && allowedOrigin !== '*') {
|
|
115
|
+
if (allowedOrigin && allowedOrigin !== '*' && allowedOrigin !== 'null') {
|
|
105
116
|
res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
|
|
106
117
|
res.setHeader('Vary', 'Origin');
|
|
107
118
|
if (config.credentials) {
|
|
@@ -26,6 +26,13 @@ const DEFAULT_AUTH_EMAILS = {
|
|
|
26
26
|
template: 'auth-welcome',
|
|
27
27
|
subject: 'Welcome',
|
|
28
28
|
},
|
|
29
|
+
magicLink: {
|
|
30
|
+
enabled: true,
|
|
31
|
+
template: 'auth-magic-link',
|
|
32
|
+
subject: 'Your sign-in link',
|
|
33
|
+
path: '/auth/magic-link/verify',
|
|
34
|
+
tokenTtlMs: 15 * 60 * 1000,
|
|
35
|
+
},
|
|
29
36
|
registerRoutes: true,
|
|
30
37
|
routePrefix: '/api/auth',
|
|
31
38
|
};
|
|
@@ -46,6 +53,7 @@ function wireAuthEmails(authManager, emailService, registry, config, db) {
|
|
|
46
53
|
passwordReset: { ...DEFAULT_AUTH_EMAILS.passwordReset, ...config.passwordReset },
|
|
47
54
|
emailVerification: { ...DEFAULT_AUTH_EMAILS.emailVerification, ...config.emailVerification },
|
|
48
55
|
welcome: { ...DEFAULT_AUTH_EMAILS.welcome, ...config.welcome },
|
|
56
|
+
magicLink: { ...DEFAULT_AUTH_EMAILS.magicLink, ...config.magicLink },
|
|
49
57
|
};
|
|
50
58
|
|
|
51
59
|
registerBundledAuthTemplates(registry);
|
|
@@ -111,6 +119,23 @@ function wireAuthEmails(authManager, emailService, registry, config, db) {
|
|
|
111
119
|
};
|
|
112
120
|
}
|
|
113
121
|
|
|
122
|
+
if (merged.magicLink?.enabled) {
|
|
123
|
+
authManager.notifications.magicLink = async (user, { token, magicLinkUrl, expiresMinutes }) => {
|
|
124
|
+
const url = magicLinkUrl || buildUrl(merged.magicLink.path, token);
|
|
125
|
+
await emailService.sendTemplate(merged.magicLink.template, {
|
|
126
|
+
to: user[identifierField],
|
|
127
|
+
subject: merged.magicLink.subject,
|
|
128
|
+
data: {
|
|
129
|
+
name: userDisplayName(user),
|
|
130
|
+
token,
|
|
131
|
+
magicLinkUrl: url,
|
|
132
|
+
expiresMinutes: expiresMinutes || 15,
|
|
133
|
+
email: user[identifierField],
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
114
139
|
return merged;
|
|
115
140
|
}
|
|
116
141
|
|
|
@@ -193,6 +218,41 @@ function registerAuthEmailRoutes(options) {
|
|
|
193
218
|
}
|
|
194
219
|
}
|
|
195
220
|
|
|
221
|
+
async function magicLinkRequestHandler(req, res) {
|
|
222
|
+
try {
|
|
223
|
+
const email = req.body?.email || req.body?.identifier;
|
|
224
|
+
if (email) {
|
|
225
|
+
await authManager.requestMagicLink(String(email), {
|
|
226
|
+
tokenTtlMs: authEmailsConfig.magicLink?.tokenTtlMs,
|
|
227
|
+
path: authEmailsConfig.magicLink?.path,
|
|
228
|
+
buildMagicLinkUrl: (token) => {
|
|
229
|
+
const pathSeg = authEmailsConfig.magicLink?.path || '/auth/magic-link/verify';
|
|
230
|
+
return `${base}${pathSeg}?token=${encodeURIComponent(token)}`;
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
res.json({ ok: true, message: 'If the account exists, a sign-in link has been sent.' });
|
|
235
|
+
} catch (err) {
|
|
236
|
+
res.status(500).json({ error: err.message });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function magicLinkVerifyHandler(req, res) {
|
|
241
|
+
try {
|
|
242
|
+
const token = req.body?.token || req.query?.token;
|
|
243
|
+
if (!token) {
|
|
244
|
+
return res.status(400).json({ error: 'token is required' });
|
|
245
|
+
}
|
|
246
|
+
const { user } = await authManager.verifyMagicLink(String(token));
|
|
247
|
+
if (req.auth && typeof req.auth.login === 'function') {
|
|
248
|
+
await req.auth.login(user);
|
|
249
|
+
}
|
|
250
|
+
res.json({ ok: true, user });
|
|
251
|
+
} catch (err) {
|
|
252
|
+
res.status(err.status || 400).json({ error: err.message, code: err.code });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
196
256
|
const forgotMw = guest ? [guest, forgotPasswordHandler] : [forgotPasswordHandler];
|
|
197
257
|
const resetMw = guest ? [guest, resetPasswordHandler] : [resetPasswordHandler];
|
|
198
258
|
const resendMw = authMw ? [authMw, resendVerificationHandler] : [resendVerificationHandler];
|
|
@@ -201,6 +261,12 @@ function registerAuthEmailRoutes(options) {
|
|
|
201
261
|
ctx.addRoute('post', `${prefix}/reset-password`, ...resetMw);
|
|
202
262
|
ctx.addRoute('post', `${prefix}/verify-email`, verifyEmailHandler);
|
|
203
263
|
ctx.addRoute('post', `${prefix}/resend-verification`, ...resendMw);
|
|
264
|
+
|
|
265
|
+
if (authEmailsConfig.magicLink?.enabled) {
|
|
266
|
+
const magicMw = guest ? [guest, magicLinkRequestHandler] : [magicLinkRequestHandler];
|
|
267
|
+
ctx.addRoute('post', `${prefix}/magic-link`, ...magicMw);
|
|
268
|
+
ctx.addRoute('post', `${prefix}/magic-link/verify`, magicLinkVerifyHandler);
|
|
269
|
+
}
|
|
204
270
|
}
|
|
205
271
|
|
|
206
272
|
module.exports = {
|
|
@@ -47,6 +47,21 @@ const BUNDLED_AUTH_TEMPLATES = {
|
|
|
47
47
|
</mj-column>
|
|
48
48
|
</mj-section>
|
|
49
49
|
</mj-body>
|
|
50
|
+
</mjml>`,
|
|
51
|
+
},
|
|
52
|
+
'auth-magic-link': {
|
|
53
|
+
mjml: `<mjml>
|
|
54
|
+
<mj-body background-color="#f4f4f5">
|
|
55
|
+
<mj-section background-color="#ffffff" padding="32px 24px">
|
|
56
|
+
<mj-column>
|
|
57
|
+
<mj-text font-size="20px" font-weight="600" color="#18181b">Your sign-in link</mj-text>
|
|
58
|
+
<mj-text color="#52525b">Hi {{name}},</mj-text>
|
|
59
|
+
<mj-text color="#52525b">Click the button below to securely sign in to your account. This link will expire in {{expiresMinutes}} minutes.</mj-text>
|
|
60
|
+
<mj-button background-color="#4f46e5" href="{{magicLinkUrl}}">Sign In</mj-button>
|
|
61
|
+
<mj-text color="#71717a" font-size="12px">If you did not request this link, you can safely ignore this email.</mj-text>
|
|
62
|
+
</mj-column>
|
|
63
|
+
</mj-section>
|
|
64
|
+
</mj-body>
|
|
50
65
|
</mjml>`,
|
|
51
66
|
},
|
|
52
67
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<mjml>
|
|
2
|
+
<mj-body background-color="#f4f4f5">
|
|
3
|
+
<mj-section background-color="#ffffff" padding="32px 24px">
|
|
4
|
+
<mj-column>
|
|
5
|
+
<mj-text font-size="20px" font-weight="600" color="#18181b">Your sign-in link</mj-text>
|
|
6
|
+
<mj-text color="#52525b">Hi {{name}},</mj-text>
|
|
7
|
+
<mj-text color="#52525b">Click the button below to securely sign in to your account. This link will expire in {{expiresMinutes}} minutes.</mj-text>
|
|
8
|
+
<mj-button background-color="#4f46e5" href="{{magicLinkUrl}}">Sign In</mj-button>
|
|
9
|
+
<mj-text color="#71717a" font-size="12px">If you did not request this link, you can safely ignore this email.</mj-text>
|
|
10
|
+
</mj-column>
|
|
11
|
+
</mj-section>
|
|
12
|
+
</mj-body>
|
|
13
|
+
</mjml>
|