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
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn, execSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
// ── ProcessResult ─────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
class ProcessResult {
|
|
8
|
+
constructor({ exitCode, stdout, stderr, command }) {
|
|
9
|
+
this._exitCode = exitCode;
|
|
10
|
+
this._stdout = stdout || '';
|
|
11
|
+
this._stderr = stderr || '';
|
|
12
|
+
this._command = command || '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Raw stdout string. */
|
|
16
|
+
output() { return this._stdout; }
|
|
17
|
+
|
|
18
|
+
/** Raw stderr string. */
|
|
19
|
+
errorOutput() { return this._stderr; }
|
|
20
|
+
|
|
21
|
+
/** Exit code. */
|
|
22
|
+
exitCode() { return this._exitCode; }
|
|
23
|
+
|
|
24
|
+
/** True if exit code is 0. */
|
|
25
|
+
get successful() { return this._exitCode === 0; }
|
|
26
|
+
|
|
27
|
+
/** True if exit code is not 0. */
|
|
28
|
+
get failed() { return this._exitCode !== 0; }
|
|
29
|
+
|
|
30
|
+
/** Throw ProcessFailedException if the process failed. */
|
|
31
|
+
throw() {
|
|
32
|
+
if (this.failed) throw new ProcessFailedException(this);
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Alias for throw(). */
|
|
37
|
+
throwIfFailed() { return this.throw(); }
|
|
38
|
+
|
|
39
|
+
toString() { return this._stdout; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── ProcessFailedException ────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
class ProcessFailedException extends Error {
|
|
45
|
+
constructor(result) {
|
|
46
|
+
super(
|
|
47
|
+
`Process "${result._command}" failed with exit code ${result._exitCode}.\n` +
|
|
48
|
+
(result._stderr ? `STDERR: ${result._stderr.trim()}` : '')
|
|
49
|
+
);
|
|
50
|
+
this.name = 'ProcessFailedException';
|
|
51
|
+
this.result = result;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── PendingProcess ────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
class PendingProcess {
|
|
58
|
+
constructor() {
|
|
59
|
+
this._cwd = process.cwd();
|
|
60
|
+
this._env = null;
|
|
61
|
+
this._timeout = 0;
|
|
62
|
+
this._input = null;
|
|
63
|
+
this._quietly = false;
|
|
64
|
+
this._shell = false;
|
|
65
|
+
this._throwOnFail = false;
|
|
66
|
+
this._onOutput = null;
|
|
67
|
+
this._onError = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Builder ────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Set working directory.
|
|
74
|
+
* Process.path('/var/www').run('ls -la')
|
|
75
|
+
*/
|
|
76
|
+
path(dir) { this._cwd = dir; return this; }
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Set environment variables, merged with parent env by default.
|
|
80
|
+
* Pass inherit=false to use only the provided vars.
|
|
81
|
+
* Process.env({ NODE_ENV: 'production' }).run('node server.js')
|
|
82
|
+
*/
|
|
83
|
+
env(vars, inherit = true) {
|
|
84
|
+
this._env = inherit ? { ...process.env, ...vars } : vars;
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Timeout in seconds. Kills the process if exceeded.
|
|
90
|
+
* Process.timeout(30).run('npm install')
|
|
91
|
+
*/
|
|
92
|
+
timeout(seconds) { this._timeout = seconds * 1000; return this; }
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Pipe data to stdin.
|
|
96
|
+
* Process.input('hello').run('cat')
|
|
97
|
+
*/
|
|
98
|
+
input(data) { this._input = data; return this; }
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Suppress stdout/stderr passthrough to parent process.
|
|
102
|
+
* Output is still captured on ProcessResult.
|
|
103
|
+
*/
|
|
104
|
+
quietly() { this._quietly = true; return this; }
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Run through /bin/sh — enables pipes, &&, ||, globs.
|
|
108
|
+
* Process.shell().run('ls | grep .js && echo done')
|
|
109
|
+
*/
|
|
110
|
+
shell() { this._shell = true; return this; }
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Throw ProcessFailedException automatically on non-zero exit.
|
|
114
|
+
* await Process.throwOnFailure().run('npm test')
|
|
115
|
+
*/
|
|
116
|
+
throwOnFailure() { this._throwOnFail = true; return this; }
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Register a line-by-line callback for stdout.
|
|
120
|
+
* Called for each newline-delimited line as the process runs.
|
|
121
|
+
* Can be combined with .run() — no need to use .pipe().
|
|
122
|
+
*
|
|
123
|
+
* await Process
|
|
124
|
+
* .onOutput(line => console.log('[OUT]', line))
|
|
125
|
+
* .onError(line => console.error('[ERR]', line))
|
|
126
|
+
* .run('npm test');
|
|
127
|
+
*/
|
|
128
|
+
onOutput(fn) { this._onOutput = fn; return this; }
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Register a line-by-line callback for stderr.
|
|
132
|
+
* .onError(line => logger.error(line))
|
|
133
|
+
*/
|
|
134
|
+
onError(fn) { this._onError = fn; return this; }
|
|
135
|
+
|
|
136
|
+
// ── Execution ──────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Run asynchronously. Returns Promise<ProcessResult>.
|
|
140
|
+
*
|
|
141
|
+
* const result = await Process.run('node --version');
|
|
142
|
+
* const result = await Process.path('/app').timeout(60).quietly().run('npm ci');
|
|
143
|
+
*/
|
|
144
|
+
run(command) {
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
const { cmd, args } = this._parse(command);
|
|
147
|
+
const child = spawn(cmd, args, {
|
|
148
|
+
cwd: this._cwd,
|
|
149
|
+
env: this._env || process.env,
|
|
150
|
+
shell: this._shell,
|
|
151
|
+
...(this._timeout ? { timeout: this._timeout } : {}),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
let stdout = '';
|
|
155
|
+
let stderr = '';
|
|
156
|
+
|
|
157
|
+
if (this._input !== null) {
|
|
158
|
+
child.stdin.write(String(this._input));
|
|
159
|
+
child.stdin.end();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let _outBuf = '';
|
|
163
|
+
let _errBuf = '';
|
|
164
|
+
|
|
165
|
+
const _flushLines = (buf, cb) => {
|
|
166
|
+
const lines = buf.split('\n');
|
|
167
|
+
buf = lines.pop();
|
|
168
|
+
lines.forEach(line => cb && cb(line));
|
|
169
|
+
return buf;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
child.stdout.on('data', chunk => {
|
|
173
|
+
stdout += chunk;
|
|
174
|
+
if (!this._quietly) process.stdout.write(chunk);
|
|
175
|
+
if (this._onOutput) _outBuf = _flushLines(_outBuf + chunk, this._onOutput);
|
|
176
|
+
});
|
|
177
|
+
child.stderr.on('data', chunk => {
|
|
178
|
+
stderr += chunk;
|
|
179
|
+
if (!this._quietly) process.stderr.write(chunk);
|
|
180
|
+
if (this._onError) _errBuf = _flushLines(_errBuf + chunk, this._onError);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
child.on('error', reject);
|
|
184
|
+
child.on('close', code => {
|
|
185
|
+
if (_outBuf && this._onOutput) this._onOutput(_outBuf);
|
|
186
|
+
if (_errBuf && this._onError) this._onError(_errBuf);
|
|
187
|
+
const result = new ProcessResult({ exitCode: code ?? 1, stdout, stderr, command });
|
|
188
|
+
if (this._throwOnFail && result.failed) return reject(new ProcessFailedException(result));
|
|
189
|
+
resolve(result);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Stream output line-by-line in real-time via callbacks.
|
|
196
|
+
* Returns Promise<ProcessResult>.
|
|
197
|
+
*
|
|
198
|
+
* await Process.pipe('npm install', {
|
|
199
|
+
* stdout: line => console.log('[out]', line),
|
|
200
|
+
* stderr: line => console.error('[err]', line),
|
|
201
|
+
* });
|
|
202
|
+
*/
|
|
203
|
+
pipe(command, { stdout: onOut, stderr: onErr } = {}) {
|
|
204
|
+
return new Promise((resolve, reject) => {
|
|
205
|
+
const { cmd, args } = this._parse(command);
|
|
206
|
+
const child = spawn(cmd, args, {
|
|
207
|
+
cwd: this._cwd,
|
|
208
|
+
env: this._env || process.env,
|
|
209
|
+
shell: this._shell,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
let outBuf = '', errBuf = '', fullOut = '', fullErr = '';
|
|
213
|
+
|
|
214
|
+
const flush = (buf, cb) => {
|
|
215
|
+
const lines = buf.split('\n');
|
|
216
|
+
const rem = lines.pop();
|
|
217
|
+
lines.forEach(l => cb && cb(l));
|
|
218
|
+
return rem;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
child.stdout.on('data', chunk => {
|
|
222
|
+
fullOut += chunk; outBuf += chunk;
|
|
223
|
+
outBuf = flush(outBuf, onOut);
|
|
224
|
+
if (!this._quietly) process.stdout.write(chunk);
|
|
225
|
+
});
|
|
226
|
+
child.stderr.on('data', chunk => {
|
|
227
|
+
fullErr += chunk; errBuf += chunk;
|
|
228
|
+
errBuf = flush(errBuf, onErr);
|
|
229
|
+
if (!this._quietly) process.stderr.write(chunk);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
child.on('error', reject);
|
|
233
|
+
child.on('close', code => {
|
|
234
|
+
if (outBuf) onOut && onOut(outBuf);
|
|
235
|
+
if (errBuf) onErr && onErr(errBuf);
|
|
236
|
+
const result = new ProcessResult({ exitCode: code ?? 1, stdout: fullOut, stderr: fullErr, command });
|
|
237
|
+
if (this._throwOnFail && result.failed) return reject(new ProcessFailedException(result));
|
|
238
|
+
resolve(result);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Run synchronously (blocking). Returns ProcessResult directly.
|
|
245
|
+
*
|
|
246
|
+
* const result = Process.quietly().runSync('node --version');
|
|
247
|
+
*/
|
|
248
|
+
runSync(command) {
|
|
249
|
+
try {
|
|
250
|
+
const out = execSync(command, {
|
|
251
|
+
cwd: this._cwd,
|
|
252
|
+
env: this._env || process.env,
|
|
253
|
+
timeout: this._timeout || undefined,
|
|
254
|
+
input: this._input !== null ? String(this._input) : undefined,
|
|
255
|
+
stdio: 'pipe',
|
|
256
|
+
});
|
|
257
|
+
const result = new ProcessResult({ exitCode: 0, stdout: out.toString(), stderr: '', command });
|
|
258
|
+
if (!this._quietly) process.stdout.write(out);
|
|
259
|
+
return result;
|
|
260
|
+
} catch (err) {
|
|
261
|
+
const result = new ProcessResult({
|
|
262
|
+
exitCode: err.status ?? 1,
|
|
263
|
+
stdout: err.stdout?.toString() || '',
|
|
264
|
+
stderr: err.stderr?.toString() || err.message || '',
|
|
265
|
+
command,
|
|
266
|
+
});
|
|
267
|
+
if (this._throwOnFail) throw new ProcessFailedException(result);
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── Internal ───────────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
_parse(command) {
|
|
275
|
+
if (this._shell) return { cmd: command, args: [] };
|
|
276
|
+
const parts = command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
|
|
277
|
+
const cmd = parts[0];
|
|
278
|
+
const args = parts.slice(1).map(a => a.replace(/^["']|["']$/g, ''));
|
|
279
|
+
return { cmd, args };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── ProcessPool ───────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
class ProcessPool {
|
|
286
|
+
async start(callback) {
|
|
287
|
+
const factory = new PendingProcess();
|
|
288
|
+
const tasks = callback(factory) || [];
|
|
289
|
+
return Promise.all(tasks);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── ProcessManager ────────────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
class ProcessManager {
|
|
296
|
+
// Shorthand — execute immediately
|
|
297
|
+
run(command) { return new PendingProcess().run(command); }
|
|
298
|
+
runSync(command) { return new PendingProcess().runSync(command); }
|
|
299
|
+
pipe(command, handlers) { return new PendingProcess().pipe(command, handlers); }
|
|
300
|
+
onOutput(fn) { return new PendingProcess().onOutput(fn); }
|
|
301
|
+
onError(fn) { return new PendingProcess().onError(fn); }
|
|
302
|
+
|
|
303
|
+
// Builder starters — return PendingProcess for chaining
|
|
304
|
+
path(dir) { return new PendingProcess().path(dir); }
|
|
305
|
+
env(vars, inherit) { return new PendingProcess().env(vars, inherit); }
|
|
306
|
+
timeout(seconds) { return new PendingProcess().timeout(seconds); }
|
|
307
|
+
input(data) { return new PendingProcess().input(data); }
|
|
308
|
+
quietly() { return new PendingProcess().quietly(); }
|
|
309
|
+
shell() { return new PendingProcess().shell(); }
|
|
310
|
+
throwOnFailure() { return new PendingProcess().throwOnFailure(); }
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Run multiple processes concurrently.
|
|
314
|
+
*
|
|
315
|
+
* const [lint, test, build] = await Process.pool(pool => [
|
|
316
|
+
* pool.quietly().run('npm run lint'),
|
|
317
|
+
* pool.quietly().run('npm test'),
|
|
318
|
+
* pool.quietly().run('npm run build'),
|
|
319
|
+
* ]);
|
|
320
|
+
*/
|
|
321
|
+
pool(callback) { return new ProcessPool().start(callback); }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── Singleton ─────────────────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
const defaultProcess = new ProcessManager();
|
|
327
|
+
|
|
328
|
+
module.exports = defaultProcess;
|
|
329
|
+
module.exports.ProcessManager = ProcessManager;
|
|
330
|
+
module.exports.PendingProcess = PendingProcess;
|
|
331
|
+
module.exports.ProcessResult = ProcessResult;
|
|
332
|
+
module.exports.ProcessPool = ProcessPool;
|
|
333
|
+
module.exports.ProcessFailedException = ProcessFailedException;
|
|
@@ -2,39 +2,96 @@
|
|
|
2
2
|
|
|
3
3
|
const ServiceProvider = require('./ServiceProvider');
|
|
4
4
|
const Admin = require('../admin/Admin');
|
|
5
|
+
const AdminAuth = require('../admin/AdminAuth');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* AdminServiceProvider
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
+
* Boots the admin panel and wires it to the app's User model.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
* app.providers([..., AdminServiceProvider])
|
|
12
|
+
* ── Authentication flow ───────────────────────────────────────────────────
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* AdminAuth resolves the User model in this priority order:
|
|
15
|
+
* 1. Explicit model in config/admin.js auth.model
|
|
16
|
+
* 2. The same model AuthServiceProvider resolved (app/models/User)
|
|
17
|
+
* 3. Built-in AuthUser (framework fallback)
|
|
18
|
+
*
|
|
19
|
+
* It then enforces:
|
|
20
|
+
* - user.is_active === true (account not disabled)
|
|
21
|
+
* - user.is_staff === true (has admin panel access)
|
|
22
|
+
*
|
|
23
|
+
* Run `millas createsuperuser` to create your first admin user.
|
|
24
|
+
* Run `millas migrate` first if you haven't — the users table must exist.
|
|
25
|
+
*
|
|
26
|
+
* ── Usage (bootstrap/app.js) ─────────────────────────────────────────────
|
|
27
|
+
*
|
|
28
|
+
* module.exports = Millas.config()
|
|
29
|
+
* .providers([AppServiceProvider])
|
|
30
|
+
* .withAdmin()
|
|
31
|
+
* .create();
|
|
32
|
+
*
|
|
33
|
+
* ── Optional config/admin.js ─────────────────────────────────────────────
|
|
34
|
+
*
|
|
35
|
+
* module.exports = {
|
|
36
|
+
* prefix: '/admin',
|
|
37
|
+
* title: 'My App Admin',
|
|
38
|
+
* auth: {
|
|
39
|
+
* cookieMaxAge: 60 * 60 * 8,
|
|
40
|
+
* rememberAge: 60 * 60 * 24 * 30,
|
|
41
|
+
* maxAttempts: 5,
|
|
42
|
+
* lockoutMinutes: 15,
|
|
43
|
+
* // model: require('../app/models/AdminUser'), // explicit override
|
|
44
|
+
* },
|
|
45
|
+
* // auth: false — disable auth entirely (not recommended)
|
|
46
|
+
* };
|
|
17
47
|
*/
|
|
18
48
|
class AdminServiceProvider extends ServiceProvider {
|
|
19
49
|
register(container) {
|
|
20
50
|
container.instance('Admin', Admin);
|
|
51
|
+
container.instance('AdminAuth', AdminAuth);
|
|
21
52
|
container.instance('AdminResource', require('../admin/resources/AdminResource').AdminResource);
|
|
22
53
|
container.instance('AdminField', require('../admin/resources/AdminResource').AdminField);
|
|
23
54
|
container.instance('AdminFilter', require('../admin/resources/AdminResource').AdminFilter);
|
|
24
55
|
}
|
|
25
56
|
|
|
26
57
|
async boot(container) {
|
|
58
|
+
const basePath = container.make('basePath') || process.cwd();
|
|
27
59
|
let adminConfig = {};
|
|
28
60
|
try {
|
|
29
|
-
adminConfig = require(
|
|
30
|
-
} catch { /*
|
|
61
|
+
adminConfig = require(basePath + '/config/admin');
|
|
62
|
+
} catch { /* optional */ }
|
|
63
|
+
|
|
64
|
+
// auth: {} means "use the User model with is_staff gate" — the Django default.
|
|
65
|
+
// auth: false disables auth entirely.
|
|
66
|
+
// Anything else is passed through as-is (model override, cookie settings, etc.)
|
|
67
|
+
const authConfig = adminConfig.auth !== undefined ? adminConfig.auth : {};
|
|
31
68
|
|
|
32
69
|
Admin.configure({
|
|
33
70
|
prefix: adminConfig.prefix || '/admin',
|
|
34
71
|
title: adminConfig.title || process.env.APP_NAME || 'Millas Admin',
|
|
35
72
|
...adminConfig,
|
|
73
|
+
auth: authConfig,
|
|
36
74
|
});
|
|
75
|
+
|
|
76
|
+
// ── Wire basePath + User model into AdminAuth ──────────────────────────
|
|
77
|
+
// Pass basePath so AdminAuth._resolveUserModel() can find app/models/User
|
|
78
|
+
// without calling process.cwd() at request time.
|
|
79
|
+
AdminAuth.setBasePath(basePath);
|
|
80
|
+
|
|
81
|
+
// AuthServiceProvider runs before us (Database → Auth → Admin order).
|
|
82
|
+
// It already resolved app/models/User → AuthUser fallback and gave it
|
|
83
|
+
// to the Auth singleton. We grab the same model so AdminAuth and the
|
|
84
|
+
// API auth system always use the same table.
|
|
85
|
+
if (authConfig !== false) {
|
|
86
|
+
try {
|
|
87
|
+
const Auth = require('../auth/Auth');
|
|
88
|
+
// Auth._UserModel is the resolved model — reuse it directly
|
|
89
|
+
if (Auth._UserModel) {
|
|
90
|
+
AdminAuth.setUserModel(Auth._UserModel);
|
|
91
|
+
}
|
|
92
|
+
} catch { /* Auth not booted yet — AdminAuth will lazy-resolve */ }
|
|
93
|
+
}
|
|
37
94
|
}
|
|
38
95
|
}
|
|
39
96
|
|
|
40
|
-
module.exports = AdminServiceProvider;
|
|
97
|
+
module.exports = AdminServiceProvider;
|
|
@@ -22,10 +22,11 @@ class AuthServiceProvider extends ServiceProvider {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
async boot(container, app) {
|
|
25
|
+
const basePath = container.make('basePath') || process.cwd();
|
|
25
26
|
// Load auth config
|
|
26
27
|
let authConfig;
|
|
27
28
|
try {
|
|
28
|
-
authConfig = require(
|
|
29
|
+
authConfig = require(basePath + '/config/auth');
|
|
29
30
|
} catch {
|
|
30
31
|
authConfig = {
|
|
31
32
|
default: 'jwt',
|
|
@@ -33,12 +34,46 @@ class AuthServiceProvider extends ServiceProvider {
|
|
|
33
34
|
};
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
37
|
+
// ── Resolve the User model ──────────────────────────────────────────────
|
|
38
|
+
//
|
|
39
|
+
// Priority order (mirrors Django's AUTH_USER_MODEL pattern):
|
|
40
|
+
//
|
|
41
|
+
// 1. config/app.js → auth_user: 'User'
|
|
42
|
+
// The model name is looked up in app/models/index.js exports.
|
|
43
|
+
// This is the recommended approach — explicit and refactor-safe.
|
|
44
|
+
//
|
|
45
|
+
// 2. app/models/User.js (default export or named User export)
|
|
46
|
+
// Conventional fallback — works if auth_user is not set and
|
|
47
|
+
// the file exists at the default path.
|
|
48
|
+
//
|
|
49
|
+
// 3. Built-in AuthUser
|
|
50
|
+
// Abstract base class — no table. Used only as a last resort
|
|
51
|
+
// so Auth always has a model to work with during early dev.
|
|
52
|
+
//
|
|
38
53
|
let UserModel;
|
|
39
54
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
// Step 1: read auth_user from config/app.js
|
|
56
|
+
let authUserName = null;
|
|
57
|
+
try {
|
|
58
|
+
const appConfig = require(basePath + '/config/app');
|
|
59
|
+
authUserName = appConfig.auth_user || null;
|
|
60
|
+
} catch { /* config/app.js missing or has no auth_user key */ }
|
|
61
|
+
|
|
62
|
+
if (authUserName) {
|
|
63
|
+
// Resolve by name from app/models/index.js
|
|
64
|
+
const modelsIndex = require(basePath + '/app/models/index');
|
|
65
|
+
const resolved = modelsIndex[authUserName];
|
|
66
|
+
if (!resolved) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`[AuthServiceProvider] auth_user: '${authUserName}' not found in app/models/index.js.\n` +
|
|
69
|
+
` Available exports: ${Object.keys(modelsIndex).join(', ')}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
UserModel = resolved;
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (err.message.includes('[AuthServiceProvider]')) throw err; // re-throw config errors
|
|
76
|
+
// Step 3: fall back to built-in AuthUser (abstract — no table)
|
|
42
77
|
UserModel = require('../auth/AuthUser');
|
|
43
78
|
}
|
|
44
79
|
|
|
@@ -18,7 +18,7 @@ class CacheServiceProvider extends ServiceProvider {
|
|
|
18
18
|
async boot() {
|
|
19
19
|
let cacheConfig;
|
|
20
20
|
try {
|
|
21
|
-
cacheConfig = require(process.cwd() + '/config/cache');
|
|
21
|
+
cacheConfig = require((container.make('basePath') || process.cwd()) + '/config/cache');
|
|
22
22
|
} catch {
|
|
23
23
|
cacheConfig = {
|
|
24
24
|
default: process.env.CACHE_DRIVER || 'memory',
|
|
@@ -48,7 +48,7 @@ class StorageServiceProvider extends ServiceProvider {
|
|
|
48
48
|
async boot() {
|
|
49
49
|
let storageConfig;
|
|
50
50
|
try {
|
|
51
|
-
storageConfig = require(process.cwd() + '/config/storage');
|
|
51
|
+
storageConfig = require((container.make('basePath') || process.cwd()) + '/config/storage');
|
|
52
52
|
} catch {
|
|
53
53
|
storageConfig = {
|
|
54
54
|
default: process.env.STORAGE_DRIVER || 'local',
|
|
@@ -21,10 +21,11 @@ class DatabaseServiceProvider extends ServiceProvider {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
async boot(container) {
|
|
24
|
+
const basePath = container.make('basePath') || process.cwd();
|
|
24
25
|
// Load the database config
|
|
25
26
|
let dbConfig;
|
|
26
27
|
try {
|
|
27
|
-
dbConfig = require(
|
|
28
|
+
dbConfig = require(basePath + '/config/database');
|
|
28
29
|
} catch {
|
|
29
30
|
// Fallback for tests / programmatic use
|
|
30
31
|
dbConfig = {
|
|
@@ -42,4 +43,4 @@ class DatabaseServiceProvider extends ServiceProvider {
|
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
module.exports = DatabaseServiceProvider;
|
|
46
|
+
module.exports = DatabaseServiceProvider;
|
|
@@ -59,6 +59,9 @@ class LogServiceProvider extends ServiceProvider {
|
|
|
59
59
|
beforeBoot(container) {
|
|
60
60
|
let config = {};
|
|
61
61
|
try {
|
|
62
|
+
// Note: beforeBoot runs before any container bindings exist.
|
|
63
|
+
// basePath is not yet available — process.cwd() is the correct fallback here.
|
|
64
|
+
// The path will be correct as long as `millas serve` is run from project root.
|
|
62
65
|
config = require(process.cwd() + '/config/logging');
|
|
63
66
|
} catch {
|
|
64
67
|
// No config file — defaults already applied in logger/index.js
|
|
@@ -206,4 +209,4 @@ class LogServiceProvider extends ServiceProvider {
|
|
|
206
209
|
}
|
|
207
210
|
}
|
|
208
211
|
|
|
209
|
-
module.exports = LogServiceProvider;
|
|
212
|
+
module.exports = LogServiceProvider;
|
|
@@ -27,7 +27,7 @@ class MailServiceProvider extends ServiceProvider {
|
|
|
27
27
|
async boot(container) {
|
|
28
28
|
let mailConfig;
|
|
29
29
|
try {
|
|
30
|
-
mailConfig = require(process.cwd() + '/config/mail');
|
|
30
|
+
mailConfig = require((container.make('basePath') || process.cwd()) + '/config/mail');
|
|
31
31
|
} catch {
|
|
32
32
|
mailConfig = {
|
|
33
33
|
default: process.env.MAIL_DRIVER || 'log',
|
|
@@ -29,7 +29,7 @@ class QueueServiceProvider extends ServiceProvider {
|
|
|
29
29
|
async boot(container) {
|
|
30
30
|
let queueConfig;
|
|
31
31
|
try {
|
|
32
|
-
queueConfig = require(process.cwd() + '/config/queue');
|
|
32
|
+
queueConfig = require((container.make('basePath') || process.cwd()) + '/config/queue');
|
|
33
33
|
} catch {
|
|
34
34
|
queueConfig = {
|
|
35
35
|
default: process.env.QUEUE_DRIVER || 'sync',
|
|
@@ -27,17 +27,42 @@ class MiddlewareRegistry {
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Resolve a single alias to an Express-compatible function.
|
|
30
|
+
* Supports parameterised aliases: 'throttle:60,1' → 60 req per 1 minute.
|
|
31
|
+
*
|
|
30
32
|
* @param {string|Function} aliasOrFn
|
|
31
33
|
* @returns {Function}
|
|
32
34
|
*/
|
|
33
35
|
resolve(aliasOrFn) {
|
|
34
36
|
if (typeof aliasOrFn === 'function') return aliasOrFn;
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
// Parse parameterised alias: 'throttle:60,1' → alias='throttle', params=['60','1']
|
|
39
|
+
let alias = aliasOrFn;
|
|
40
|
+
let params = [];
|
|
41
|
+
if (typeof aliasOrFn === 'string' && aliasOrFn.includes(':')) {
|
|
42
|
+
const colonIdx = aliasOrFn.indexOf(':');
|
|
43
|
+
alias = aliasOrFn.slice(0, colonIdx);
|
|
44
|
+
params = aliasOrFn.slice(colonIdx + 1).split(',').map(s => s.trim());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const Handler = this._map[alias];
|
|
37
48
|
if (!Handler) {
|
|
38
49
|
throw new Error(`Middleware "${aliasOrFn}" is not registered.`);
|
|
39
50
|
}
|
|
40
51
|
|
|
52
|
+
// If params provided, instantiate the class with them via fromParams() or constructor
|
|
53
|
+
if (params.length > 0) {
|
|
54
|
+
if (typeof Handler === 'function' && Handler.prototype &&
|
|
55
|
+
typeof Handler.prototype.handle === 'function') {
|
|
56
|
+
const instance = typeof Handler.fromParams === 'function'
|
|
57
|
+
? Handler.fromParams(params)
|
|
58
|
+
: new Handler(...params);
|
|
59
|
+
return (req, res, next) => {
|
|
60
|
+
const result = instance.handle(req, res, next);
|
|
61
|
+
if (result && typeof result.catch === 'function') result.catch(next);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
41
66
|
// Pre-instantiated object with handle() method (e.g. new ThrottleMiddleware())
|
|
42
67
|
if (typeof Handler === 'object' && Handler !== null && typeof Handler.handle === 'function') {
|
|
43
68
|
return (req, res, next) => {
|
|
@@ -79,4 +104,4 @@ class MiddlewareRegistry {
|
|
|
79
104
|
}
|
|
80
105
|
}
|
|
81
106
|
|
|
82
|
-
module.exports = MiddlewareRegistry;
|
|
107
|
+
module.exports = MiddlewareRegistry;
|