millas 0.1.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/LICENSE +21 -0
- package/README.md +137 -0
- package/bin/millas.js +6 -0
- package/package.json +56 -0
- package/src/admin/Admin.js +617 -0
- package/src/admin/index.js +13 -0
- package/src/admin/resources/AdminResource.js +317 -0
- package/src/auth/Auth.js +254 -0
- package/src/auth/AuthController.js +188 -0
- package/src/auth/AuthMiddleware.js +67 -0
- package/src/auth/Hasher.js +51 -0
- package/src/auth/JwtDriver.js +74 -0
- package/src/auth/RoleMiddleware.js +44 -0
- package/src/cache/Cache.js +231 -0
- package/src/cache/drivers/FileDriver.js +152 -0
- package/src/cache/drivers/MemoryDriver.js +158 -0
- package/src/cache/drivers/NullDriver.js +27 -0
- package/src/cache/index.js +8 -0
- package/src/cli.js +27 -0
- package/src/commands/make.js +61 -0
- package/src/commands/migrate.js +174 -0
- package/src/commands/new.js +50 -0
- package/src/commands/queue.js +92 -0
- package/src/commands/route.js +93 -0
- package/src/commands/serve.js +50 -0
- package/src/container/Application.js +177 -0
- package/src/container/Container.js +281 -0
- package/src/container/index.js +13 -0
- package/src/controller/Controller.js +367 -0
- package/src/errors/HttpError.js +29 -0
- package/src/events/Event.js +39 -0
- package/src/events/EventEmitter.js +151 -0
- package/src/events/Listener.js +46 -0
- package/src/events/index.js +15 -0
- package/src/index.js +93 -0
- package/src/mail/Mail.js +210 -0
- package/src/mail/MailMessage.js +196 -0
- package/src/mail/TemplateEngine.js +150 -0
- package/src/mail/drivers/LogDriver.js +36 -0
- package/src/mail/drivers/MailgunDriver.js +84 -0
- package/src/mail/drivers/SendGridDriver.js +97 -0
- package/src/mail/drivers/SmtpDriver.js +67 -0
- package/src/mail/index.js +19 -0
- package/src/middleware/AuthMiddleware.js +46 -0
- package/src/middleware/CorsMiddleware.js +59 -0
- package/src/middleware/LogMiddleware.js +61 -0
- package/src/middleware/Middleware.js +36 -0
- package/src/middleware/MiddlewarePipeline.js +94 -0
- package/src/middleware/ThrottleMiddleware.js +61 -0
- package/src/orm/drivers/DatabaseManager.js +135 -0
- package/src/orm/fields/index.js +132 -0
- package/src/orm/index.js +19 -0
- package/src/orm/migration/MigrationRunner.js +216 -0
- package/src/orm/migration/ModelInspector.js +338 -0
- package/src/orm/migration/SchemaBuilder.js +173 -0
- package/src/orm/model/Model.js +371 -0
- package/src/orm/query/QueryBuilder.js +197 -0
- package/src/providers/AdminServiceProvider.js +40 -0
- package/src/providers/AuthServiceProvider.js +53 -0
- package/src/providers/CacheStorageServiceProvider.js +71 -0
- package/src/providers/DatabaseServiceProvider.js +45 -0
- package/src/providers/EventServiceProvider.js +34 -0
- package/src/providers/MailServiceProvider.js +51 -0
- package/src/providers/ProviderRegistry.js +82 -0
- package/src/providers/QueueServiceProvider.js +52 -0
- package/src/providers/ServiceProvider.js +45 -0
- package/src/queue/Job.js +135 -0
- package/src/queue/Queue.js +147 -0
- package/src/queue/drivers/DatabaseDriver.js +194 -0
- package/src/queue/drivers/SyncDriver.js +72 -0
- package/src/queue/index.js +16 -0
- package/src/queue/workers/QueueWorker.js +140 -0
- package/src/router/MiddlewareRegistry.js +82 -0
- package/src/router/Route.js +255 -0
- package/src/router/RouteGroup.js +19 -0
- package/src/router/RouteRegistry.js +55 -0
- package/src/router/Router.js +138 -0
- package/src/router/index.js +15 -0
- package/src/scaffold/generator.js +34 -0
- package/src/scaffold/maker.js +272 -0
- package/src/scaffold/templates.js +350 -0
- package/src/storage/Storage.js +170 -0
- package/src/storage/drivers/LocalDriver.js +215 -0
- package/src/storage/index.js +6 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MailgunDriver
|
|
5
|
+
*
|
|
6
|
+
* Sends mail via the Mailgun API v3.
|
|
7
|
+
* No extra npm package needed.
|
|
8
|
+
*
|
|
9
|
+
* Config (config/mail.js):
|
|
10
|
+
* drivers: {
|
|
11
|
+
* mailgun: {
|
|
12
|
+
* key: process.env.MAILGUN_API_KEY,
|
|
13
|
+
* domain: process.env.MAILGUN_DOMAIN,
|
|
14
|
+
* region: 'us', // 'us' | 'eu'
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
class MailgunDriver {
|
|
19
|
+
constructor(config = {}) {
|
|
20
|
+
this._key = config.key || process.env.MAILGUN_API_KEY;
|
|
21
|
+
this._domain = config.domain || process.env.MAILGUN_DOMAIN;
|
|
22
|
+
this._region = config.region || 'us';
|
|
23
|
+
|
|
24
|
+
if (!this._key) throw new Error('MailgunDriver: API key is required');
|
|
25
|
+
if (!this._domain) throw new Error('MailgunDriver: domain is required');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async send(message) {
|
|
29
|
+
const params = new URLSearchParams();
|
|
30
|
+
|
|
31
|
+
const toStr = [].concat(message.to).join(', ');
|
|
32
|
+
params.append('from', message.from);
|
|
33
|
+
params.append('to', toStr);
|
|
34
|
+
params.append('subject', message.subject);
|
|
35
|
+
|
|
36
|
+
if (message.cc) params.append('cc', [].concat(message.cc).join(', '));
|
|
37
|
+
if (message.bcc) params.append('bcc', [].concat(message.bcc).join(', '));
|
|
38
|
+
if (message.html) params.append('html', message.html);
|
|
39
|
+
if (message.text) params.append('text', message.text);
|
|
40
|
+
|
|
41
|
+
const host = this._region === 'eu'
|
|
42
|
+
? 'api.eu.mailgun.net'
|
|
43
|
+
: 'api.mailgun.net';
|
|
44
|
+
|
|
45
|
+
return this._post(host, `/v3/${this._domain}/messages`, params.toString());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_post(host, path, body) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const https = require('https');
|
|
51
|
+
const auth = Buffer.from(`api:${this._key}`).toString('base64');
|
|
52
|
+
|
|
53
|
+
const options = {
|
|
54
|
+
hostname: host,
|
|
55
|
+
port: 443,
|
|
56
|
+
path,
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: {
|
|
59
|
+
'Authorization': `Basic ${auth}`,
|
|
60
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
61
|
+
'Content-Length': Buffer.byteLength(body),
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const req = https.request(options, (res) => {
|
|
66
|
+
let raw = '';
|
|
67
|
+
res.on('data', c => raw += c);
|
|
68
|
+
res.on('end', () => {
|
|
69
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
70
|
+
resolve(JSON.parse(raw || '{}'));
|
|
71
|
+
} else {
|
|
72
|
+
reject(new Error(`Mailgun error ${res.statusCode}: ${raw}`));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
req.on('error', reject);
|
|
78
|
+
req.write(body);
|
|
79
|
+
req.end();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = MailgunDriver;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SendGridDriver
|
|
5
|
+
*
|
|
6
|
+
* Sends mail via the SendGrid Web API v3.
|
|
7
|
+
* Does not require any extra npm package — uses the built-in https module.
|
|
8
|
+
*
|
|
9
|
+
* Config (config/mail.js):
|
|
10
|
+
* drivers: {
|
|
11
|
+
* sendgrid: {
|
|
12
|
+
* key: process.env.SENDGRID_API_KEY,
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
class SendGridDriver {
|
|
17
|
+
constructor(config = {}) {
|
|
18
|
+
this._apiKey = config.key || process.env.SENDGRID_API_KEY;
|
|
19
|
+
if (!this._apiKey) {
|
|
20
|
+
throw new Error('SendGridDriver: API key is required (config/mail.js → drivers.sendgrid.key)');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async send(message) {
|
|
25
|
+
const payload = {
|
|
26
|
+
personalizations: [{
|
|
27
|
+
to: this._toRecipients(message.to),
|
|
28
|
+
cc: message.cc ? this._toRecipients(message.cc) : undefined,
|
|
29
|
+
bcc: message.bcc ? this._toRecipients(message.bcc) : undefined,
|
|
30
|
+
subject: message.subject,
|
|
31
|
+
}],
|
|
32
|
+
from: this._parseAddress(message.from),
|
|
33
|
+
reply_to: message.replyTo ? this._parseAddress(message.replyTo) : undefined,
|
|
34
|
+
content: [
|
|
35
|
+
...(message.text ? [{ type: 'text/plain', value: message.text }] : []),
|
|
36
|
+
...(message.html ? [{ type: 'text/html', value: message.html }] : []),
|
|
37
|
+
],
|
|
38
|
+
attachments: message.attachments
|
|
39
|
+
? message.attachments.map(a => ({
|
|
40
|
+
content: Buffer.isBuffer(a.content)
|
|
41
|
+
? a.content.toString('base64')
|
|
42
|
+
: Buffer.from(a.content || '').toString('base64'),
|
|
43
|
+
filename: a.filename,
|
|
44
|
+
type: a.contentType,
|
|
45
|
+
}))
|
|
46
|
+
: undefined,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return this._post('/v3/mail/send', payload);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async _post(path, body) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const https = require('https');
|
|
55
|
+
const data = JSON.stringify(body);
|
|
56
|
+
const options = {
|
|
57
|
+
hostname: 'api.sendgrid.com',
|
|
58
|
+
port: 443,
|
|
59
|
+
path,
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: {
|
|
62
|
+
'Authorization': `Bearer ${this._apiKey}`,
|
|
63
|
+
'Content-Type': 'application/json',
|
|
64
|
+
'Content-Length': Buffer.byteLength(data),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const req = https.request(options, (res) => {
|
|
69
|
+
let raw = '';
|
|
70
|
+
res.on('data', chunk => raw += chunk);
|
|
71
|
+
res.on('end', () => {
|
|
72
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
73
|
+
resolve({ statusCode: res.statusCode });
|
|
74
|
+
} else {
|
|
75
|
+
reject(new Error(`SendGrid API error ${res.statusCode}: ${raw}`));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
req.on('error', reject);
|
|
81
|
+
req.write(data);
|
|
82
|
+
req.end();
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_toRecipients(addresses) {
|
|
87
|
+
return [].concat(addresses).map(a => this._parseAddress(a));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_parseAddress(address) {
|
|
91
|
+
const match = String(address).match(/^(.+?)\s*<(.+?)>$/);
|
|
92
|
+
if (match) return { name: match[1].trim(), email: match[2].trim() };
|
|
93
|
+
return { email: address };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = SendGridDriver;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SmtpDriver
|
|
5
|
+
*
|
|
6
|
+
* Sends mail via SMTP using nodemailer.
|
|
7
|
+
*
|
|
8
|
+
* Config (config/mail.js):
|
|
9
|
+
* drivers: {
|
|
10
|
+
* smtp: {
|
|
11
|
+
* host: 'smtp.mailtrap.io',
|
|
12
|
+
* port: 2525,
|
|
13
|
+
* username: 'user',
|
|
14
|
+
* password: 'pass',
|
|
15
|
+
* encryption: 'tls', // 'tls' | 'ssl' | false
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
class SmtpDriver {
|
|
20
|
+
constructor(config = {}) {
|
|
21
|
+
this._config = config;
|
|
22
|
+
this._transport = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async send(message) {
|
|
26
|
+
const transport = this._getTransport();
|
|
27
|
+
return transport.sendMail(this._toNodemailer(message));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_getTransport() {
|
|
31
|
+
if (this._transport) return this._transport;
|
|
32
|
+
|
|
33
|
+
const nodemailer = require('nodemailer');
|
|
34
|
+
const cfg = this._config;
|
|
35
|
+
|
|
36
|
+
const secure = cfg.encryption === 'ssl';
|
|
37
|
+
const tls = cfg.encryption === 'tls' ? { rejectUnauthorized: false } : undefined;
|
|
38
|
+
|
|
39
|
+
this._transport = nodemailer.createTransport({
|
|
40
|
+
host: cfg.host || 'localhost',
|
|
41
|
+
port: cfg.port || 587,
|
|
42
|
+
secure,
|
|
43
|
+
auth: cfg.username ? { user: cfg.username, pass: cfg.password } : undefined,
|
|
44
|
+
tls,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return this._transport;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_toNodemailer(msg) {
|
|
51
|
+
return {
|
|
52
|
+
from: msg.from,
|
|
53
|
+
to: Array.isArray(msg.to) ? msg.to.join(', ') : msg.to,
|
|
54
|
+
cc: Array.isArray(msg.cc) ? msg.cc.join(', ') : msg.cc,
|
|
55
|
+
bcc: Array.isArray(msg.bcc) ? msg.bcc.join(', ') : msg.bcc,
|
|
56
|
+
replyTo: msg.replyTo,
|
|
57
|
+
subject: msg.subject,
|
|
58
|
+
html: msg.html,
|
|
59
|
+
text: msg.text,
|
|
60
|
+
attachments: msg.attachments,
|
|
61
|
+
headers: msg.headers,
|
|
62
|
+
priority: msg.priority,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = SmtpDriver;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Mail = require('./Mail');
|
|
4
|
+
const MailMessage = require('./MailMessage');
|
|
5
|
+
const TemplateEngine = require('./TemplateEngine');
|
|
6
|
+
const SmtpDriver = require('./drivers/SmtpDriver');
|
|
7
|
+
const SendGridDriver = require('./drivers/SendGridDriver');
|
|
8
|
+
const MailgunDriver = require('./drivers/MailgunDriver');
|
|
9
|
+
const LogDriver = require('./drivers/LogDriver');
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
Mail,
|
|
13
|
+
MailMessage,
|
|
14
|
+
TemplateEngine,
|
|
15
|
+
SmtpDriver,
|
|
16
|
+
SendGridDriver,
|
|
17
|
+
MailgunDriver,
|
|
18
|
+
LogDriver,
|
|
19
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Middleware = require('./Middleware');
|
|
4
|
+
const HttpError = require('../errors/HttpError');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* AuthMiddleware
|
|
8
|
+
*
|
|
9
|
+
* Guards routes from unauthenticated access.
|
|
10
|
+
* Full JWT/session implementation unlocked in Phase 7.
|
|
11
|
+
*
|
|
12
|
+
* For now: checks for presence of Authorization header.
|
|
13
|
+
* Replace the verify() method with real token logic in Phase 7.
|
|
14
|
+
*
|
|
15
|
+
* Register:
|
|
16
|
+
* middlewareRegistry.register('auth', AuthMiddleware);
|
|
17
|
+
*
|
|
18
|
+
* Apply:
|
|
19
|
+
* Route.prefix('/api').middleware(['auth']).group(() => { ... });
|
|
20
|
+
*/
|
|
21
|
+
class AuthMiddleware extends Middleware {
|
|
22
|
+
async handle(req, res, next) {
|
|
23
|
+
const header = req.headers['authorization'];
|
|
24
|
+
|
|
25
|
+
if (!header) {
|
|
26
|
+
throw new HttpError(401, 'No authorization token provided');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const [scheme, token] = header.split(' ');
|
|
30
|
+
|
|
31
|
+
if (scheme !== 'Bearer' || !token) {
|
|
32
|
+
throw new HttpError(401, 'Invalid authorization format. Use: Bearer <token>');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Phase 7 will replace this with real JWT verification:
|
|
36
|
+
// const user = await Auth.verifyToken(token);
|
|
37
|
+
// req.user = user;
|
|
38
|
+
|
|
39
|
+
// For now: attach a stub user so downstream handlers don't break
|
|
40
|
+
req.user = { id: null, token, authenticated: false, _stub: true };
|
|
41
|
+
|
|
42
|
+
next();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = AuthMiddleware;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Middleware = require('./Middleware');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CorsMiddleware
|
|
7
|
+
*
|
|
8
|
+
* Adds Cross-Origin Resource Sharing headers to every response.
|
|
9
|
+
*
|
|
10
|
+
* Register:
|
|
11
|
+
* middlewareRegistry.register('cors', CorsMiddleware);
|
|
12
|
+
*
|
|
13
|
+
* Configure in bootstrap/app.js:
|
|
14
|
+
* new CorsMiddleware({
|
|
15
|
+
* origins: ['https://myapp.com'],
|
|
16
|
+
* methods: ['GET', 'POST'],
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* Or register the class directly for default permissive settings.
|
|
20
|
+
*/
|
|
21
|
+
class CorsMiddleware extends Middleware {
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
super();
|
|
24
|
+
this.origins = options.origins || ['*'];
|
|
25
|
+
this.methods = options.methods || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
|
|
26
|
+
this.headers = options.headers || ['Content-Type', 'Authorization', 'X-Requested-With'];
|
|
27
|
+
this.credentials = options.credentials ?? false;
|
|
28
|
+
this.maxAge = options.maxAge || 86400;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async handle(req, res, next) {
|
|
32
|
+
const origin = req.headers.origin;
|
|
33
|
+
|
|
34
|
+
// Origin matching
|
|
35
|
+
if (this.origins.includes('*')) {
|
|
36
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
37
|
+
} else if (origin && this.origins.includes(origin)) {
|
|
38
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
39
|
+
res.setHeader('Vary', 'Origin');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
res.setHeader('Access-Control-Allow-Methods', this.methods.join(', '));
|
|
43
|
+
res.setHeader('Access-Control-Allow-Headers', this.headers.join(', '));
|
|
44
|
+
res.setHeader('Access-Control-Max-Age', String(this.maxAge));
|
|
45
|
+
|
|
46
|
+
if (this.credentials) {
|
|
47
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Handle preflight
|
|
51
|
+
if (req.method === 'OPTIONS') {
|
|
52
|
+
return res.status(204).send();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
next();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = CorsMiddleware;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Middleware = require('./Middleware');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* LogMiddleware
|
|
7
|
+
*
|
|
8
|
+
* Logs every incoming request and its response time.
|
|
9
|
+
*
|
|
10
|
+
* Output format:
|
|
11
|
+
* [2026-03-14 12:00:00] GET /api/users 200 12ms
|
|
12
|
+
*
|
|
13
|
+
* Register:
|
|
14
|
+
* middlewareRegistry.register('log', LogMiddleware);
|
|
15
|
+
*
|
|
16
|
+
* Options:
|
|
17
|
+
* silent — suppress output (default: false)
|
|
18
|
+
* format — 'dev' (coloured) | 'combined' (plain)
|
|
19
|
+
*/
|
|
20
|
+
class LogMiddleware extends Middleware {
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
super();
|
|
23
|
+
this.silent = options.silent || false;
|
|
24
|
+
this.format = options.format || 'dev';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async handle(req, res, next) {
|
|
28
|
+
if (this.silent) return next();
|
|
29
|
+
|
|
30
|
+
const start = Date.now();
|
|
31
|
+
const { method, path: routePath, url } = req;
|
|
32
|
+
|
|
33
|
+
res.on('finish', () => {
|
|
34
|
+
const ms = Date.now() - start;
|
|
35
|
+
const status = res.statusCode;
|
|
36
|
+
const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
37
|
+
|
|
38
|
+
if (this.format === 'dev') {
|
|
39
|
+
const statusColor = status < 300 ? '\x1b[32m'
|
|
40
|
+
: status < 400 ? '\x1b[36m'
|
|
41
|
+
: status < 500 ? '\x1b[33m'
|
|
42
|
+
: '\x1b[31m';
|
|
43
|
+
const reset = '\x1b[0m';
|
|
44
|
+
const methodColor = '\x1b[1m';
|
|
45
|
+
console.log(
|
|
46
|
+
` ${'\x1b[90m'}[${ts}]${reset} ` +
|
|
47
|
+
`${methodColor}${method.padEnd(7)}${reset} ` +
|
|
48
|
+
`${routePath || url} ` +
|
|
49
|
+
`${statusColor}${status}${reset} ` +
|
|
50
|
+
`${'\x1b[90m'}${ms}ms${reset}`
|
|
51
|
+
);
|
|
52
|
+
} else {
|
|
53
|
+
console.log(`[${ts}] ${method} ${routePath || url} ${status} ${ms}ms`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
next();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = LogMiddleware;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Middleware
|
|
5
|
+
*
|
|
6
|
+
* Base class for all Millas middleware.
|
|
7
|
+
* Subclasses must implement handle(req, res, next).
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* class AuthMiddleware extends Middleware {
|
|
11
|
+
* async handle(req, res, next) {
|
|
12
|
+
* // ...
|
|
13
|
+
* next();
|
|
14
|
+
* }
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
class Middleware {
|
|
18
|
+
/**
|
|
19
|
+
* Handle the incoming request.
|
|
20
|
+
*
|
|
21
|
+
* @param {import('express').Request} req
|
|
22
|
+
* @param {import('express').Response} res
|
|
23
|
+
* @param {Function} next
|
|
24
|
+
*/
|
|
25
|
+
async handle(req, res, next) {
|
|
26
|
+
throw new Error(`${this.constructor.name} must implement handle(req, res, next)`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Called after the response is sent.
|
|
31
|
+
* Override for cleanup, logging, etc.
|
|
32
|
+
*/
|
|
33
|
+
async terminate(req, res) {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = Middleware;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MiddlewarePipeline
|
|
5
|
+
*
|
|
6
|
+
* Composes an ordered list of middleware handlers into a single
|
|
7
|
+
* Express-compatible function. Used internally by the Router.
|
|
8
|
+
*
|
|
9
|
+
* Each handler can be:
|
|
10
|
+
* - A Middleware subclass (instantiated automatically)
|
|
11
|
+
* - An already-instantiated Middleware object
|
|
12
|
+
* - A raw Express function (req, res, next) => {}
|
|
13
|
+
*
|
|
14
|
+
* Usage (internal):
|
|
15
|
+
* const pipeline = new MiddlewarePipeline([AuthMiddleware, LogMiddleware]);
|
|
16
|
+
* app.use(pipeline.compose());
|
|
17
|
+
*/
|
|
18
|
+
class MiddlewarePipeline {
|
|
19
|
+
constructor(handlers = []) {
|
|
20
|
+
this._handlers = handlers;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Add a handler to the end of the pipeline.
|
|
25
|
+
*/
|
|
26
|
+
pipe(handler) {
|
|
27
|
+
this._handlers.push(handler);
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Add a handler to the beginning of the pipeline.
|
|
33
|
+
*/
|
|
34
|
+
prepend(handler) {
|
|
35
|
+
this._handlers.unshift(handler);
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Compose all handlers into a single (req, res, next) function.
|
|
41
|
+
*/
|
|
42
|
+
compose() {
|
|
43
|
+
const fns = this._handlers.map(h => this._resolve(h));
|
|
44
|
+
|
|
45
|
+
return function pipeline(req, res, next) {
|
|
46
|
+
let index = 0;
|
|
47
|
+
|
|
48
|
+
function dispatch(i) {
|
|
49
|
+
if (i >= fns.length) return next();
|
|
50
|
+
const fn = fns[i];
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const result = fn(req, res, (err) => {
|
|
54
|
+
if (err) return next(err);
|
|
55
|
+
dispatch(i + 1);
|
|
56
|
+
});
|
|
57
|
+
// Handle async middleware that returns a Promise
|
|
58
|
+
if (result && typeof result.catch === 'function') {
|
|
59
|
+
result.catch(next);
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
next(err);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
dispatch(0);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve a handler to a plain (req, res, next) => {} function.
|
|
72
|
+
*/
|
|
73
|
+
_resolve(handler) {
|
|
74
|
+
// Raw Express function
|
|
75
|
+
if (typeof handler === 'function' && !(handler.prototype instanceof require('./Middleware'))) {
|
|
76
|
+
return handler;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Middleware class (not yet instantiated)
|
|
80
|
+
if (typeof handler === 'function' && handler.prototype instanceof require('./Middleware')) {
|
|
81
|
+
const instance = new handler();
|
|
82
|
+
return (req, res, next) => instance.handle(req, res, next);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Instantiated Middleware object
|
|
86
|
+
if (handler && typeof handler.handle === 'function') {
|
|
87
|
+
return (req, res, next) => handler.handle(req, res, next);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
throw new Error(`Invalid middleware: ${handler}. Must be a Middleware class, instance, or function.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = MiddlewarePipeline;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Middleware = require('./Middleware');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ThrottleMiddleware
|
|
7
|
+
*
|
|
8
|
+
* Simple in-memory rate limiter.
|
|
9
|
+
* For production use, replace the store with a Redis-backed one (Phase 11).
|
|
10
|
+
*
|
|
11
|
+
* Register:
|
|
12
|
+
* middlewareRegistry.register('throttle', new ThrottleMiddleware({ max: 60, window: 60 }));
|
|
13
|
+
*
|
|
14
|
+
* Options:
|
|
15
|
+
* max — max requests per window (default: 60)
|
|
16
|
+
* window — window in seconds (default: 60)
|
|
17
|
+
* keyBy — function(req) => string, defaults to IP
|
|
18
|
+
*/
|
|
19
|
+
class ThrottleMiddleware extends Middleware {
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
super();
|
|
22
|
+
this.max = options.max || 60;
|
|
23
|
+
this.window = options.window || 60; // seconds
|
|
24
|
+
this.keyBy = options.keyBy || ((req) => req.ip || 'anonymous');
|
|
25
|
+
this._store = new Map(); // { key: { count, resetAt } }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async handle(req, res, next) {
|
|
29
|
+
const key = this.keyBy(req);
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
|
|
32
|
+
let record = this._store.get(key);
|
|
33
|
+
|
|
34
|
+
if (!record || now > record.resetAt) {
|
|
35
|
+
record = { count: 0, resetAt: now + this.window * 1000 };
|
|
36
|
+
this._store.set(key, record);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
record.count++;
|
|
40
|
+
|
|
41
|
+
const remaining = Math.max(0, this.max - record.count);
|
|
42
|
+
const resetIn = Math.ceil((record.resetAt - now) / 1000);
|
|
43
|
+
|
|
44
|
+
res.setHeader('X-RateLimit-Limit', String(this.max));
|
|
45
|
+
res.setHeader('X-RateLimit-Remaining', String(remaining));
|
|
46
|
+
res.setHeader('X-RateLimit-Reset', String(Math.ceil(record.resetAt / 1000)));
|
|
47
|
+
|
|
48
|
+
if (record.count > this.max) {
|
|
49
|
+
return res.status(429).json({
|
|
50
|
+
error: 'Too Many Requests',
|
|
51
|
+
message: `Rate limit exceeded. Try again in ${resetIn}s.`,
|
|
52
|
+
status: 429,
|
|
53
|
+
retryAfter: resetIn,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
next();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = ThrottleMiddleware;
|