millas 0.2.12-beta-1 → 0.2.13
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/package.json +3 -2
- package/src/admin/ActivityLog.js +153 -52
- package/src/admin/Admin.js +516 -199
- package/src/admin/AdminAuth.js +213 -98
- package/src/admin/FormGenerator.js +372 -0
- package/src/admin/HookRegistry.js +256 -0
- package/src/admin/QueryEngine.js +263 -0
- package/src/admin/ViewContext.js +318 -0
- package/src/admin/WidgetRegistry.js +406 -0
- package/src/admin/index.js +17 -0
- package/src/admin/resources/AdminResource.js +393 -97
- package/src/admin/static/admin.css +1422 -0
- package/src/admin/static/date-picker.css +157 -0
- package/src/admin/static/date-picker.js +316 -0
- package/src/admin/static/json-editor.css +649 -0
- package/src/admin/static/json-editor.js +1429 -0
- package/src/admin/static/ui.js +1044 -0
- package/src/admin/views/layouts/base.njk +87 -1046
- package/src/admin/views/pages/detail.njk +56 -21
- package/src/admin/views/pages/error.njk +65 -0
- package/src/admin/views/pages/form.njk +47 -599
- package/src/admin/views/pages/list.njk +270 -62
- package/src/admin/views/partials/form-field.njk +53 -0
- package/src/admin/views/partials/form-footer.njk +28 -0
- package/src/admin/views/partials/form-readonly.njk +114 -0
- package/src/admin/views/partials/form-scripts.njk +480 -0
- package/src/admin/views/partials/form-widget.njk +297 -0
- package/src/admin/views/partials/icons.njk +64 -0
- package/src/admin/views/partials/json-dialog.njk +80 -0
- package/src/admin/views/partials/json-editor.njk +37 -0
- package/src/ai/AIManager.js +954 -0
- package/src/ai/AITokenBudget.js +250 -0
- package/src/ai/PromptGuard.js +216 -0
- package/src/ai/agents.js +218 -0
- package/src/ai/conversation.js +213 -0
- package/src/ai/drivers.js +734 -0
- package/src/ai/files.js +249 -0
- package/src/ai/media.js +303 -0
- package/src/ai/pricing.js +152 -0
- package/src/ai/provider_tools.js +114 -0
- package/src/ai/types.js +356 -0
- package/src/auth/Auth.js +18 -2
- package/src/auth/AuthUser.js +65 -44
- package/src/cli.js +3 -1
- package/src/commands/createsuperuser.js +267 -0
- package/src/commands/lang.js +589 -0
- package/src/commands/migrate.js +154 -81
- package/src/commands/serve.js +3 -4
- package/src/container/AppInitializer.js +101 -20
- package/src/container/Application.js +31 -1
- package/src/container/MillasApp.js +10 -3
- package/src/container/MillasConfig.js +35 -6
- package/src/core/admin.js +5 -0
- package/src/core/db.js +2 -1
- package/src/core/foundation.js +2 -10
- package/src/core/lang.js +1 -0
- package/src/errors/HttpError.js +32 -16
- package/src/facades/AI.js +411 -0
- package/src/facades/Hash.js +67 -0
- package/src/facades/Process.js +144 -0
- package/src/hashing/Hash.js +262 -0
- package/src/http/HtmlEscape.js +162 -0
- package/src/http/MillasRequest.js +63 -7
- package/src/http/MillasResponse.js +70 -4
- package/src/http/ResponseDispatcher.js +21 -27
- package/src/http/SafeFilePath.js +195 -0
- package/src/http/SafeRedirect.js +62 -0
- package/src/http/SecurityBootstrap.js +70 -0
- package/src/http/helpers.js +40 -125
- package/src/http/index.js +10 -1
- package/src/http/middleware/CsrfMiddleware.js +258 -0
- package/src/http/middleware/RateLimiter.js +314 -0
- package/src/http/middleware/SecurityHeaders.js +281 -0
- package/src/i18n/I18nServiceProvider.js +91 -0
- package/src/i18n/Translator.js +643 -0
- package/src/i18n/defaults.js +122 -0
- package/src/i18n/index.js +164 -0
- package/src/i18n/locales/en.js +55 -0
- package/src/i18n/locales/sw.js +48 -0
- package/src/logger/LogRedactor.js +247 -0
- package/src/logger/Logger.js +1 -1
- package/src/logger/formatters/JsonFormatter.js +11 -4
- package/src/logger/formatters/PrettyFormatter.js +103 -65
- package/src/logger/formatters/SimpleFormatter.js +14 -3
- package/src/middleware/ThrottleMiddleware.js +27 -4
- package/src/migrations/system/0001_users.js +21 -0
- package/src/migrations/system/0002_admin_log.js +25 -0
- package/src/migrations/system/0003_sessions.js +23 -0
- package/src/orm/fields/index.js +210 -188
- package/src/orm/migration/DefaultValueParser.js +325 -0
- package/src/orm/migration/InteractiveResolver.js +191 -0
- package/src/orm/migration/Makemigrations.js +312 -0
- package/src/orm/migration/MigrationGraph.js +227 -0
- package/src/orm/migration/MigrationRunner.js +202 -108
- package/src/orm/migration/MigrationWriter.js +463 -0
- package/src/orm/migration/ModelInspector.js +143 -74
- package/src/orm/migration/ModelScanner.js +225 -0
- package/src/orm/migration/ProjectState.js +213 -0
- package/src/orm/migration/RenameDetector.js +175 -0
- package/src/orm/migration/SchemaBuilder.js +8 -81
- package/src/orm/migration/operations/base.js +57 -0
- package/src/orm/migration/operations/column.js +191 -0
- package/src/orm/migration/operations/fields.js +252 -0
- package/src/orm/migration/operations/index.js +55 -0
- package/src/orm/migration/operations/models.js +152 -0
- package/src/orm/migration/operations/registry.js +131 -0
- package/src/orm/migration/operations/special.js +51 -0
- package/src/orm/migration/utils.js +208 -0
- package/src/orm/model/Model.js +81 -13
- package/src/process/Process.js +333 -0
- package/src/providers/AdminServiceProvider.js +66 -9
- package/src/providers/AuthServiceProvider.js +40 -5
- package/src/providers/CacheStorageServiceProvider.js +2 -2
- package/src/providers/DatabaseServiceProvider.js +3 -2
- package/src/providers/LogServiceProvider.js +4 -1
- package/src/providers/MailServiceProvider.js +1 -1
- package/src/providers/QueueServiceProvider.js +1 -1
- package/src/router/MiddlewareRegistry.js +27 -2
- package/src/scaffold/templates.js +80 -21
- package/src/validation/Validator.js +348 -607
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { LEVEL_NAMES }
|
|
3
|
+
const { LEVEL_NAMES } = require('../levels');
|
|
4
|
+
const { LogRedactor } = require('../LogRedactor');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* JsonFormatter
|
|
7
8
|
*
|
|
8
9
|
* Emits one JSON object per log entry — ideal for production environments
|
|
9
10
|
* where logs are shipped to Datadog, Elasticsearch, CloudWatch, etc.
|
|
11
|
+
* Sensitive context fields are automatically redacted before serialisation.
|
|
10
12
|
*
|
|
11
13
|
* Output (one line per entry):
|
|
12
14
|
* {"ts":"2026-03-15T12:00:00.000Z","level":"INFO","tag":"Auth","msg":"Login","ctx":{...}}
|
|
@@ -15,11 +17,13 @@ class JsonFormatter {
|
|
|
15
17
|
/**
|
|
16
18
|
* @param {object} options
|
|
17
19
|
* @param {boolean} [options.pretty=false] — pretty-print JSON (for debugging)
|
|
18
|
-
* @param {object} [options.extra] — static fields merged into every entry
|
|
20
|
+
* @param {object} [options.extra] — static fields merged into every entry
|
|
21
|
+
* @param {boolean} [options.redact=true] — redact sensitive context fields
|
|
19
22
|
*/
|
|
20
23
|
constructor(options = {}) {
|
|
21
24
|
this.pretty = options.pretty || false;
|
|
22
25
|
this.extra = options.extra || {};
|
|
26
|
+
this.redact = options.redact !== false; // default: true
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
format(entry) {
|
|
@@ -33,7 +37,10 @@ class JsonFormatter {
|
|
|
33
37
|
|
|
34
38
|
if (tag) record.tag = tag;
|
|
35
39
|
record.msg = message;
|
|
36
|
-
|
|
40
|
+
|
|
41
|
+
if (context !== undefined && context !== null) {
|
|
42
|
+
record.ctx = this.redact ? LogRedactor.redact(context) : context;
|
|
43
|
+
}
|
|
37
44
|
|
|
38
45
|
if (error instanceof Error) {
|
|
39
46
|
record.error = {
|
|
@@ -49,4 +56,4 @@ class JsonFormatter {
|
|
|
49
56
|
}
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
module.exports = JsonFormatter;
|
|
59
|
+
module.exports = JsonFormatter;
|
|
@@ -1,98 +1,136 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
const { LEVEL_TAGS, LEVEL_COLOURS, RESET, BOLD } = require('../levels');
|
|
4
|
+
const { LogRedactor } = require('../LogRedactor');
|
|
5
|
+
|
|
6
|
+
const SEP = ' ';
|
|
7
|
+
const TAG_WIDTH = 18;
|
|
8
|
+
|
|
9
|
+
// Matches /absolute/path/to/file.js or /absolute/path/to/file.js:12:34
|
|
10
|
+
const FILE_PATH_RE = /(\/[^\s)'"]+\.(js|ts|json|mjs|cjs)(?::\d+(?::\d+)?)?)/g;
|
|
11
|
+
// Matches http(s):// URLs
|
|
12
|
+
const URL_RE = /(https?:\/\/[^\s)'"]+)/g;
|
|
13
|
+
|
|
14
|
+
function linkify(text, dim, useColour) {
|
|
15
|
+
if (!useColour) return text;
|
|
16
|
+
|
|
17
|
+
text = text.replace(FILE_PATH_RE, (match) => {
|
|
18
|
+
const filePath = match.replace(/(:\d+)+$/, '');
|
|
19
|
+
const uri = `file://${filePath}`;
|
|
20
|
+
return `${RESET}\x1b]8;;${uri}\x1b\\${match}\x1b]8;;\x1b\\`;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
text = text.replace(URL_RE, (match) => {
|
|
24
|
+
return `${RESET}\x1b]8;;${match}\x1b\\${match}\x1b]8;;\x1b\\`;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return text;
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
class PrettyFormatter {
|
|
19
|
-
/**
|
|
20
|
-
* @param {object} options
|
|
21
|
-
* @param {boolean} [options.timestamp=true] — show timestamp
|
|
22
|
-
* @param {boolean} [options.tag=true] — show tag/component name
|
|
23
|
-
* @param {boolean} [options.colour=true] — ANSI colour (disable for pipes/files)
|
|
24
|
-
* @param {string} [options.timestampFormat] — 'iso' | 'short' (default: 'short')
|
|
25
|
-
*/
|
|
26
31
|
constructor(options = {}) {
|
|
27
32
|
this.showTimestamp = options.timestamp !== false;
|
|
28
33
|
this.showTag = options.tag !== false;
|
|
29
34
|
this.colour = options.colour !== false;
|
|
30
35
|
this.tsFormat = options.timestampFormat || 'short';
|
|
36
|
+
this.tagWidth = options.tagWidth || TAG_WIDTH;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
format(entry) {
|
|
34
40
|
const { level, tag, message, context, error } = entry;
|
|
35
41
|
|
|
36
|
-
const c
|
|
37
|
-
const r
|
|
38
|
-
const b
|
|
39
|
-
const d
|
|
40
|
-
const lvl
|
|
42
|
+
const c = this.colour ? (LEVEL_COLOURS[level] || '') : '';
|
|
43
|
+
const r = this.colour ? RESET : '';
|
|
44
|
+
const b = this.colour ? BOLD : '';
|
|
45
|
+
const d = this.colour ? '\x1b[2m' : '';
|
|
46
|
+
const lvl = LEVEL_TAGS[level] || '?';
|
|
41
47
|
|
|
42
|
-
|
|
48
|
+
// ── 1. Measure plain prefix once ─────────────────────────────────────────
|
|
49
|
+
const ts = this._timestamp();
|
|
50
|
+
const plainCols = [];
|
|
51
|
+
if (this.showTimestamp) plainCols.push(`[${ts}]`);
|
|
52
|
+
plainCols.push(lvl);
|
|
53
|
+
if (this.showTag && tag) plainCols.push(tag.padEnd(this.tagWidth));
|
|
43
54
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const ts = this._timestamp();
|
|
47
|
-
parts.push(`${d}[${ts}]${r}`);
|
|
48
|
-
}
|
|
55
|
+
const indentWidth = plainCols.join(SEP).length + SEP.length;
|
|
56
|
+
const indent = ' '.repeat(indentWidth);
|
|
49
57
|
|
|
50
|
-
//
|
|
51
|
-
|
|
58
|
+
// ── 2. Coloured prefix ───────────────────────────────────────────────────
|
|
59
|
+
const colCols = [];
|
|
60
|
+
if (this.showTimestamp) colCols.push(`${d}[${ts}]${r}`);
|
|
61
|
+
colCols.push(`${c}${b}${lvl}${r}`);
|
|
62
|
+
if (this.showTag && tag) colCols.push(`${b}${tag.padEnd(this.tagWidth)}${r}`);
|
|
52
63
|
|
|
53
|
-
|
|
54
|
-
if (this.showTag && tag) {
|
|
55
|
-
const tagStr = tag.padEnd(18);
|
|
56
|
-
parts.push(`${b}${tagStr}${r}`);
|
|
57
|
-
}
|
|
64
|
+
const prefix = colCols.join(SEP) + SEP;
|
|
58
65
|
|
|
59
|
-
//
|
|
60
|
-
const
|
|
61
|
-
|
|
66
|
+
// ── 3. Terminal width ────────────────────────────────────────────────────
|
|
67
|
+
const termWidth = (process.stdout.columns || 120);
|
|
68
|
+
const msgWidth = termWidth - indentWidth;
|
|
62
69
|
|
|
63
|
-
|
|
70
|
+
// ── 4. Collect all logical lines ─────────────────────────────────────────
|
|
71
|
+
const logicalLines = [];
|
|
64
72
|
|
|
65
|
-
|
|
66
|
-
if (lines.length > 1) {
|
|
67
|
-
const prefix = parts.slice(0, -1).map(p => p.replace(/\x1b\[[0-9;]*m/g, '')).join(' ');
|
|
68
|
-
const indent = ' '.repeat(prefix.length + 2);
|
|
69
|
-
for (let i = 1; i < lines.length; i++) {
|
|
70
|
-
output += `\n${indent}${c}${lines[i]}${r}`;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
+
for (const l of String(message).split('\n')) logicalLines.push({ text: l, dim: false });
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const ctx
|
|
77
|
-
|
|
78
|
-
: String(context);
|
|
79
|
-
output += ` ${d}${ctx}${r}`;
|
|
75
|
+
if (context != null) {
|
|
76
|
+
const safe = this.redact !== false ? LogRedactor.redact(context) : context;
|
|
77
|
+
const ctx = typeof safe === 'object' ? JSON.stringify(safe) : String(safe);
|
|
78
|
+
logicalLines.push({ text: ctx, dim: true });
|
|
80
79
|
}
|
|
81
80
|
|
|
82
|
-
// Error stack
|
|
83
81
|
if (error instanceof Error) {
|
|
84
|
-
|
|
82
|
+
for (const l of (error.stack || error.message).split('\n'))
|
|
83
|
+
logicalLines.push({ text: l, dim: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── 5. Hard-wrap (skip stack frames so paths stay intact) ────────────────
|
|
87
|
+
const wrappedLines = [];
|
|
88
|
+
|
|
89
|
+
for (const { text, dim } of logicalLines) {
|
|
90
|
+
const isStackFrame = dim && /^\s*at /.test(text);
|
|
91
|
+
|
|
92
|
+
if (isStackFrame || text.length <= msgWidth) {
|
|
93
|
+
wrappedLines.push({ text, dim });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const words = text.split(' ');
|
|
98
|
+
let chunk = '';
|
|
99
|
+
for (const word of words) {
|
|
100
|
+
if (chunk.length === 0) {
|
|
101
|
+
if (word.length > msgWidth) {
|
|
102
|
+
for (let i = 0; i < word.length; i += msgWidth)
|
|
103
|
+
wrappedLines.push({ text: word.slice(i, i + msgWidth), dim });
|
|
104
|
+
} else {
|
|
105
|
+
chunk = word;
|
|
106
|
+
}
|
|
107
|
+
} else if (chunk.length + 1 + word.length <= msgWidth) {
|
|
108
|
+
chunk += ' ' + word;
|
|
109
|
+
} else {
|
|
110
|
+
wrappedLines.push({ text: chunk, dim });
|
|
111
|
+
chunk = word;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (chunk.length) wrappedLines.push({ text: chunk, dim });
|
|
85
115
|
}
|
|
86
116
|
|
|
87
|
-
//
|
|
117
|
+
// ── 6. Render + linkify ───────────────────────────────────────────────────
|
|
118
|
+
const rendered = wrappedLines.map((line, i) => {
|
|
119
|
+
const col = line.dim ? d : c;
|
|
120
|
+
const text = linkify(line.text, line.dim, this.colour);
|
|
121
|
+
if (i === 0) return `${prefix}${col}${text}${r}`;
|
|
122
|
+
return `${indent}${col}${text}${r}`;
|
|
123
|
+
}).join('\n');
|
|
124
|
+
|
|
125
|
+
// ── 7. WTF banner ─────────────────────────────────────────────────────────
|
|
88
126
|
if (level === 5) {
|
|
89
|
-
const
|
|
127
|
+
const bar = this.colour
|
|
90
128
|
? `\x1b[35m\x1b[1m${'━'.repeat(60)}\x1b[0m`
|
|
91
129
|
: '━'.repeat(60);
|
|
92
|
-
|
|
130
|
+
return `${bar}\n${rendered}\n${bar}`;
|
|
93
131
|
}
|
|
94
132
|
|
|
95
|
-
return
|
|
133
|
+
return rendered;
|
|
96
134
|
}
|
|
97
135
|
|
|
98
136
|
_timestamp() {
|
|
@@ -102,4 +140,4 @@ class PrettyFormatter {
|
|
|
102
140
|
}
|
|
103
141
|
}
|
|
104
142
|
|
|
105
|
-
module.exports = PrettyFormatter;
|
|
143
|
+
module.exports = PrettyFormatter;
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { LEVEL_NAMES } = require('../levels');
|
|
4
|
+
const { LogRedactor } = require('../LogRedactor');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* SimpleFormatter
|
|
7
8
|
*
|
|
8
9
|
* Plain, no-colour text. Suitable for file output or any sink
|
|
9
|
-
* where ANSI codes would be noise.
|
|
10
|
+
* where ANSI codes would be noise. Sensitive context fields are
|
|
11
|
+
* automatically redacted before serialisation.
|
|
10
12
|
*
|
|
11
13
|
* Output:
|
|
12
14
|
* [2026-03-15 12:00:00] [INFO] Auth: User logged in
|
|
13
15
|
* [2026-03-15 12:00:01] [ERROR] DB: Query failed {"table":"users"}
|
|
14
16
|
*/
|
|
15
17
|
class SimpleFormatter {
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} [options]
|
|
20
|
+
* @param {boolean} [options.redact=true] — redact sensitive context fields
|
|
21
|
+
*/
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.redact = options.redact !== false;
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
format(entry) {
|
|
17
27
|
const { level, tag, message, context, error, timestamp } = entry;
|
|
18
28
|
|
|
@@ -23,7 +33,8 @@ class SimpleFormatter {
|
|
|
23
33
|
let line = `[${ts}] [${lvlName}] ${tagPart}${message}`;
|
|
24
34
|
|
|
25
35
|
if (context !== undefined && context !== null) {
|
|
26
|
-
|
|
36
|
+
const safe = this.redact ? LogRedactor.redact(context) : context;
|
|
37
|
+
line += ' ' + (typeof safe === 'object' ? JSON.stringify(safe) : String(safe));
|
|
27
38
|
}
|
|
28
39
|
|
|
29
40
|
if (error instanceof Error) {
|
|
@@ -34,4 +45,4 @@ class SimpleFormatter {
|
|
|
34
45
|
}
|
|
35
46
|
}
|
|
36
47
|
|
|
37
|
-
module.exports = SimpleFormatter;
|
|
48
|
+
module.exports = SimpleFormatter;
|
|
@@ -7,18 +7,41 @@ const { jsonify } = require('../http/helpers');
|
|
|
7
7
|
/**
|
|
8
8
|
* ThrottleMiddleware
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Per-IP (or per-user) rate limiter registered as the 'throttle' middleware alias.
|
|
11
|
+
* Used via the route middleware system — developers never import this directly.
|
|
12
|
+
*
|
|
13
|
+
* Usage in routes:
|
|
14
|
+
* Route.middleware('throttle:5,10').group(() => { // 5 req per 10 min
|
|
15
|
+
* Route.post('/login', AuthController, 'login');
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* Route.post('/login', AuthController, 'login') // same, single route
|
|
19
|
+
* — add 'throttle:5,10' to route middleware array
|
|
20
|
+
*
|
|
21
|
+
* Format: 'throttle:<max>,<minutes>'
|
|
22
|
+
* throttle:60,1 — 60 requests per minute
|
|
23
|
+
* throttle:5,10 — 5 requests per 10 minutes
|
|
24
|
+
* throttle:100,15 — 100 requests per 15 minutes
|
|
12
25
|
*/
|
|
13
26
|
class ThrottleMiddleware extends Middleware {
|
|
14
27
|
constructor(options = {}) {
|
|
15
28
|
super();
|
|
16
29
|
this.max = options.max || 60;
|
|
17
|
-
this.window = options.window || 60;
|
|
30
|
+
this.window = options.window || 60; // seconds
|
|
18
31
|
this.keyBy = options.keyBy || ((req) => req.ip || 'anonymous');
|
|
19
32
|
this._store = new Map();
|
|
20
33
|
}
|
|
21
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Factory used by MiddlewareRegistry when parsing 'throttle:max,minutes'.
|
|
37
|
+
* @param {string[]} params — ['5', '10'] from 'throttle:5,10'
|
|
38
|
+
*/
|
|
39
|
+
static fromParams(params) {
|
|
40
|
+
const max = parseInt(params[0], 10) || 60;
|
|
41
|
+
const minutes = parseInt(params[1], 10) || 1;
|
|
42
|
+
return new ThrottleMiddleware({ max, window: minutes * 60 });
|
|
43
|
+
}
|
|
44
|
+
|
|
22
45
|
async handle(req, next) {
|
|
23
46
|
const key = this.keyBy(req);
|
|
24
47
|
const now = Date.now();
|
|
@@ -54,4 +77,4 @@ class ThrottleMiddleware extends Middleware {
|
|
|
54
77
|
}
|
|
55
78
|
}
|
|
56
79
|
|
|
57
|
-
module.exports = ThrottleMiddleware;
|
|
80
|
+
module.exports = ThrottleMiddleware;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* System migration: 0001_users
|
|
5
|
+
*
|
|
6
|
+
* Previously created the users table directly.
|
|
7
|
+
*
|
|
8
|
+
* As of Millas 0.3+, the users table is owned by the APP — not the framework.
|
|
9
|
+
* AuthUser is now fully abstract (no table). The app defines its own User model
|
|
10
|
+
* that extends AuthUser and owns whatever table it wants (typically 'users').
|
|
11
|
+
*
|
|
12
|
+
* This migration is kept as a no-op so existing projects that depend on
|
|
13
|
+
* ['system', '0001_users'] don't break. It creates nothing.
|
|
14
|
+
*
|
|
15
|
+
* Equivalent to Django's pattern: AbstractUser has no table; your custom
|
|
16
|
+
* User model creates its own table via app migrations.
|
|
17
|
+
*/
|
|
18
|
+
module.exports = {
|
|
19
|
+
dependencies: [],
|
|
20
|
+
operations: [],
|
|
21
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { CreateModel } = require('../../orm/migration/operations');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* System migration: millas_admin_log
|
|
7
|
+
* Equivalent to Django's django_admin_log.
|
|
8
|
+
*/
|
|
9
|
+
module.exports = {
|
|
10
|
+
dependencies: [['system', '0001_users']],
|
|
11
|
+
|
|
12
|
+
operations: [
|
|
13
|
+
new CreateModel('millas_admin_log', {
|
|
14
|
+
id: { type: 'id', unsigned: true, nullable: false, unique: false, default: null, max: null, enumValues: null, references: null, precision: null, scale: null },
|
|
15
|
+
user_id: { type: 'integer', unsigned: true, nullable: true, unique: false, default: null, max: null, enumValues: null, references: { table: 'users', column: 'id', onDelete: 'SET NULL' }, precision: null, scale: null },
|
|
16
|
+
user_email: { type: 'string', max: 255, nullable: true, unique: false, default: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
|
|
17
|
+
resource: { type: 'string', max: 100, nullable: false, unique: false, default: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
|
|
18
|
+
record_id: { type: 'string', max: 100, nullable: true, unique: false, default: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
|
|
19
|
+
action: { type: 'enum', enumValues: ['create','update','delete'], nullable: false, unique: false, default: null, max: null, unsigned: false, references: null, precision: null, scale: null },
|
|
20
|
+
label: { type: 'string', max: 255, nullable: true, unique: false, default: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
|
|
21
|
+
change_msg: { type: 'text', nullable: true, unique: false, default: null, max: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
|
|
22
|
+
created_at: { type: 'timestamp', nullable: true, unique: false, default: null, max: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
|
|
23
|
+
}),
|
|
24
|
+
],
|
|
25
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { CreateModel } = require('../../orm/migration/operations');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* System migration: millas_sessions
|
|
7
|
+
* Equivalent to Django's django_session.
|
|
8
|
+
*/
|
|
9
|
+
module.exports = {
|
|
10
|
+
dependencies: [['system', '0001_users']],
|
|
11
|
+
|
|
12
|
+
operations: [
|
|
13
|
+
new CreateModel('millas_sessions', {
|
|
14
|
+
session_key: { type: 'string', max: 64, nullable: false, unique: false, default: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
|
|
15
|
+
user_id: { type: 'integer', unsigned: true, nullable: false, unique: false, default: null, max: null, enumValues: null, references: { table: 'users', column: 'id', onDelete: 'CASCADE' }, precision: null, scale: null },
|
|
16
|
+
payload: { type: 'text', nullable: true, unique: false, default: null, max: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
|
|
17
|
+
ip_address: { type: 'string', max: 45, nullable: true, unique: false, default: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
|
|
18
|
+
user_agent: { type: 'string', max: 512, nullable: true, unique: false, default: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
|
|
19
|
+
expires_at: { type: 'timestamp', nullable: false, unique: false, default: null, max: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
|
|
20
|
+
created_at: { type: 'timestamp', nullable: true, unique: false, default: null, max: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
|
|
21
|
+
}),
|
|
22
|
+
],
|
|
23
|
+
};
|