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/core/auth/index.js
CHANGED
|
@@ -46,6 +46,25 @@ const {
|
|
|
46
46
|
generateQrSvg,
|
|
47
47
|
generateQrDataUri,
|
|
48
48
|
} = require('./qrcode');
|
|
49
|
+
const {
|
|
50
|
+
buildMagicLinkUrl,
|
|
51
|
+
formatMagicLinkHtml,
|
|
52
|
+
createMagicLinkVerifyMiddleware,
|
|
53
|
+
createMagicLinkRequestHandler,
|
|
54
|
+
} = require('./magic-link');
|
|
55
|
+
const {
|
|
56
|
+
createUserSessionsTable,
|
|
57
|
+
dropUserSessionsTable,
|
|
58
|
+
createKnexSessionTracker,
|
|
59
|
+
createMemorySessionTracker,
|
|
60
|
+
getClientIp,
|
|
61
|
+
parseUserAgent,
|
|
62
|
+
} = require('./sessions');
|
|
63
|
+
const {
|
|
64
|
+
SessionManager,
|
|
65
|
+
createListSessionsHandler,
|
|
66
|
+
createRevokeSessionHandler,
|
|
67
|
+
} = require('./session-manager');
|
|
49
68
|
|
|
50
69
|
/**
|
|
51
70
|
* Create authentication instance
|
|
@@ -73,6 +92,8 @@ function createAuth(config) {
|
|
|
73
92
|
* @param {Object} [options.session] - Session config
|
|
74
93
|
* @param {boolean} [options.rememberMe=true] - Enable remember me
|
|
75
94
|
* @param {boolean} [options.authTokens=false] - Enable auth_tokens adapter (password reset / email verify)
|
|
95
|
+
* @param {boolean} [options.magicLink=false] - Enable magic link / passwordless auth tokens adapter
|
|
96
|
+
* @param {boolean} [options.sessionTracking=false] - Enable active session tracking & revocation
|
|
76
97
|
* @param {Object|boolean} [options.jwt=false] - Enable JWT support or JWT config
|
|
77
98
|
* @param {string} [options.verifiedField='email_verified_at'] - Verified timestamp column
|
|
78
99
|
* @returns {AuthManager}
|
|
@@ -86,6 +107,8 @@ function quickAuth(options) {
|
|
|
86
107
|
session = {},
|
|
87
108
|
rememberMe = true,
|
|
88
109
|
authTokens: enableAuthTokens = false,
|
|
110
|
+
magicLink = false,
|
|
111
|
+
sessionTracking = false,
|
|
89
112
|
jwt = false,
|
|
90
113
|
verifiedField = 'email_verified_at',
|
|
91
114
|
} = options;
|
|
@@ -151,10 +174,15 @@ function quickAuth(options) {
|
|
|
151
174
|
};
|
|
152
175
|
}
|
|
153
176
|
|
|
154
|
-
if (enableAuthTokens) {
|
|
177
|
+
if (enableAuthTokens || magicLink) {
|
|
155
178
|
config.authTokens = createKnexAuthTokensAdapter(db);
|
|
156
179
|
}
|
|
157
180
|
|
|
181
|
+
if (sessionTracking) {
|
|
182
|
+
config.sessionTracking = true;
|
|
183
|
+
config.sessionTracker = createKnexSessionTracker(db);
|
|
184
|
+
}
|
|
185
|
+
|
|
158
186
|
return createAuth(config);
|
|
159
187
|
}
|
|
160
188
|
|
|
@@ -197,6 +225,16 @@ module.exports = {
|
|
|
197
225
|
// Classes
|
|
198
226
|
AuthManager,
|
|
199
227
|
PolicyManager,
|
|
228
|
+
Bouncer: require('./bouncer').Bouncer,
|
|
229
|
+
createBouncer: require('./bouncer').createBouncer,
|
|
230
|
+
createBouncerMiddleware: require('./bouncer').createBouncerMiddleware,
|
|
231
|
+
|
|
232
|
+
// Policies & Authorization (AdonisJS Bouncer & Django Permissions-inspired)
|
|
233
|
+
definePolicy: require('./policy').definePolicy,
|
|
234
|
+
defineGate: require('./policy').defineGate,
|
|
235
|
+
can: require('./policy').can,
|
|
236
|
+
cannot: require('./policy').cannot,
|
|
237
|
+
authorize: require('./policy').authorize,
|
|
200
238
|
|
|
201
239
|
// Errors
|
|
202
240
|
AuthenticationError,
|
|
@@ -255,6 +293,40 @@ module.exports = {
|
|
|
255
293
|
generateQrSvg,
|
|
256
294
|
generateQrDataUri,
|
|
257
295
|
|
|
296
|
+
// Magic Link / Passwordless Authentication
|
|
297
|
+
buildMagicLinkUrl,
|
|
298
|
+
formatMagicLinkHtml,
|
|
299
|
+
createMagicLinkVerifyMiddleware,
|
|
300
|
+
createMagicLinkRequestHandler,
|
|
301
|
+
magicLink: {
|
|
302
|
+
buildMagicLinkUrl,
|
|
303
|
+
formatMagicLinkHtml,
|
|
304
|
+
createMagicLinkVerifyMiddleware,
|
|
305
|
+
createMagicLinkRequestHandler,
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
// Active Devices & Session Management
|
|
309
|
+
SessionManager,
|
|
310
|
+
createUserSessionsTable,
|
|
311
|
+
dropUserSessionsTable,
|
|
312
|
+
createKnexSessionTracker,
|
|
313
|
+
createMemorySessionTracker,
|
|
314
|
+
getClientIp,
|
|
315
|
+
parseUserAgent,
|
|
316
|
+
createListSessionsHandler,
|
|
317
|
+
createRevokeSessionHandler,
|
|
318
|
+
sessions: {
|
|
319
|
+
SessionManager,
|
|
320
|
+
createUserSessionsTable,
|
|
321
|
+
dropUserSessionsTable,
|
|
322
|
+
createKnexSessionTracker,
|
|
323
|
+
createMemorySessionTracker,
|
|
324
|
+
getClientIp,
|
|
325
|
+
parseUserAgent,
|
|
326
|
+
createListSessionsHandler,
|
|
327
|
+
createRevokeSessionHandler,
|
|
328
|
+
},
|
|
329
|
+
|
|
258
330
|
// Config
|
|
259
331
|
DEFAULT_CONFIG,
|
|
260
332
|
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso Magic Link & Passwordless Authentication Module
|
|
3
|
+
* Cryptographically signed one-time tokens, session login, and verification helpers
|
|
4
|
+
* @module core/auth/magic-link
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Construct full magic link verification URL
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} options
|
|
13
|
+
* @param {string} [options.baseUrl] - Base site URL (e.g. 'https://example.com')
|
|
14
|
+
* @param {string} [options.path='/auth/magic-link/verify'] - Relative path
|
|
15
|
+
* @param {string} options.token - Raw unhashed token
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
function buildMagicLinkUrl(options = {}) {
|
|
19
|
+
let baseUrl = String(options.baseUrl || process.env.BASE_URL || 'http://localhost:3000');
|
|
20
|
+
while (baseUrl.endsWith('/')) {
|
|
21
|
+
baseUrl = baseUrl.slice(0, -1);
|
|
22
|
+
}
|
|
23
|
+
const path = options.path || '/auth/magic-link/verify';
|
|
24
|
+
const token = options.token;
|
|
25
|
+
|
|
26
|
+
if (!token) {
|
|
27
|
+
throw new Error('Token is required to build magic link URL');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const cleanPath = path.startsWith('/') ? path : `/${path}`;
|
|
31
|
+
const sep = cleanPath.includes('?') ? '&' : '?';
|
|
32
|
+
return `${baseUrl}${cleanPath}${sep}token=${encodeURIComponent(token)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Default HTML template for magic link emails
|
|
37
|
+
*
|
|
38
|
+
* @param {Object} data
|
|
39
|
+
* @param {string} [data.name='there']
|
|
40
|
+
* @param {string} data.magicLinkUrl
|
|
41
|
+
* @param {number} [data.expiresMinutes=15]
|
|
42
|
+
* @returns {string} Clean HTML email body
|
|
43
|
+
*/
|
|
44
|
+
function formatMagicLinkHtml(data) {
|
|
45
|
+
const name = data.name || 'there';
|
|
46
|
+
const url = data.magicLinkUrl;
|
|
47
|
+
const minutes = data.expiresMinutes || 15;
|
|
48
|
+
|
|
49
|
+
return `<!DOCTYPE html>
|
|
50
|
+
<html>
|
|
51
|
+
<head>
|
|
52
|
+
<meta charset="utf-8">
|
|
53
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
54
|
+
<title>Sign in to your account</title>
|
|
55
|
+
</head>
|
|
56
|
+
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: #f4f4f5; margin: 0; padding: 24px; color: #18181b;">
|
|
57
|
+
<div style="max-width: 520px; margin: 0 auto; background: #ffffff; border-radius: 16px; padding: 32px; border: 1px solid #e4e4e7;">
|
|
58
|
+
<h2 style="margin-top: 0; font-size: 22px; font-weight: 700; color: #18181b;">Sign in to your account</h2>
|
|
59
|
+
<p style="font-size: 15px; line-height: 1.6; color: #52525b;">Hello ${name},</p>
|
|
60
|
+
<p style="font-size: 15px; line-height: 1.6; color: #52525b;">Click the button below to sign in to your account without a password. This link is valid for <strong>${minutes} minutes</strong>.</p>
|
|
61
|
+
<div style="margin: 28px 0; text-align: center;">
|
|
62
|
+
<a href="${url}" style="display: inline-block; background-color: #4f46e5; color: #ffffff; text-decoration: none; padding: 12px 28px; font-weight: 600; font-size: 15px; border-radius: 12px;">Sign In →</a>
|
|
63
|
+
</div>
|
|
64
|
+
<p style="font-size: 13px; line-height: 1.5; color: #a1a1aa; border-top: 1px solid #f4f4f5; padding-top: 16px;">If you did not request this link, you can safely ignore this email.</p>
|
|
65
|
+
</div>
|
|
66
|
+
</body>
|
|
67
|
+
</html>`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Express / Webspresso route middleware for magic link verification and session login
|
|
72
|
+
*
|
|
73
|
+
* @param {import('./manager').AuthManager} authManager
|
|
74
|
+
* @param {Object} [options]
|
|
75
|
+
* @param {string} [options.tokenParam='token'] - Query / body parameter name
|
|
76
|
+
* @param {string} [options.redirectTo='/'] - Redirect URL on successful HTML login
|
|
77
|
+
* @param {string} [options.failureRedirect='/login?error=invalid_magic_link'] - Redirect URL on failure
|
|
78
|
+
* @param {boolean} [options.markEmailVerified=true] - Auto-verify email on login
|
|
79
|
+
* @param {boolean} [options.jwt=false] - Generate JWT tokens in response
|
|
80
|
+
* @returns {Function} Express middleware
|
|
81
|
+
*/
|
|
82
|
+
function createMagicLinkVerifyMiddleware(authManager, options = {}) {
|
|
83
|
+
const {
|
|
84
|
+
tokenParam = 'token',
|
|
85
|
+
redirectTo = '/',
|
|
86
|
+
failureRedirect = '/login?error=invalid_magic_link',
|
|
87
|
+
markEmailVerified = true,
|
|
88
|
+
jwt = false,
|
|
89
|
+
} = options;
|
|
90
|
+
|
|
91
|
+
return async function magicLinkVerifyMiddleware(req, res, next) {
|
|
92
|
+
const rawToken = req.query?.[tokenParam] || req.body?.[tokenParam];
|
|
93
|
+
const isHtml = req.accepts && req.accepts('html') && req.method === 'GET';
|
|
94
|
+
|
|
95
|
+
if (!rawToken) {
|
|
96
|
+
if (isHtml) {
|
|
97
|
+
return res.redirect(failureRedirect);
|
|
98
|
+
}
|
|
99
|
+
return res.status(400).json({ error: 'Missing magic link token' });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const { user } = await authManager.verifyMagicLink(rawToken, { markEmailVerified });
|
|
104
|
+
|
|
105
|
+
if (req.auth && typeof req.auth.login === 'function') {
|
|
106
|
+
await req.auth.login(user);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let jwtTokens = null;
|
|
110
|
+
if (jwt) {
|
|
111
|
+
jwtTokens = {
|
|
112
|
+
accessToken: authManager.generateUserToken(user),
|
|
113
|
+
refreshToken: authManager.generateRefreshToken(user),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (isHtml) {
|
|
118
|
+
return res.redirect(redirectTo);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return res.status(200).json({
|
|
122
|
+
ok: true,
|
|
123
|
+
user,
|
|
124
|
+
...(jwtTokens || {}),
|
|
125
|
+
});
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (isHtml) {
|
|
128
|
+
return res.redirect(failureRedirect);
|
|
129
|
+
}
|
|
130
|
+
return res.status(400).json({
|
|
131
|
+
error: err.message || 'Invalid or expired magic link',
|
|
132
|
+
code: err.code || 'INVALID_MAGIC_LINK',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Express / Webspresso route handler for requesting a magic link
|
|
140
|
+
*
|
|
141
|
+
* @param {import('./manager').AuthManager} authManager
|
|
142
|
+
* @param {Object} [options]
|
|
143
|
+
* @returns {Function} Express route handler
|
|
144
|
+
*/
|
|
145
|
+
function createMagicLinkRequestHandler(authManager, options = {}) {
|
|
146
|
+
return async function magicLinkRequestHandler(req, res) {
|
|
147
|
+
const identifier = req.body?.email || req.body?.identifier || req.query?.email;
|
|
148
|
+
if (!identifier) {
|
|
149
|
+
return res.status(400).json({ error: 'Email or identifier is required' });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const result = await authManager.requestMagicLink(identifier, options);
|
|
154
|
+
return res.status(200).json({
|
|
155
|
+
ok: true,
|
|
156
|
+
message: 'If the account exists, a magic link has been sent.',
|
|
157
|
+
expiresAt: result.expiresAt,
|
|
158
|
+
});
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return res.status(500).json({ error: err.message || 'Failed to send magic link' });
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
buildMagicLinkUrl,
|
|
167
|
+
formatMagicLinkHtml,
|
|
168
|
+
createMagicLinkVerifyMiddleware,
|
|
169
|
+
createMagicLinkRequestHandler,
|
|
170
|
+
};
|