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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - make:mail Command
|
|
5
|
+
* Scaffolds a declarative Mailable class in emails/<name>.js
|
|
6
|
+
* @module bin/commands/make-mail
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
function generateMailContent(rawName) {
|
|
13
|
+
const baseName = rawName.replace(/email$/i, '').replace(/mail$/i, '').trim();
|
|
14
|
+
const cleanBase = baseName.charAt(0).toUpperCase() + baseName.slice(1);
|
|
15
|
+
const className = `${cleanBase}Email`;
|
|
16
|
+
const tplName = cleanBase.toLowerCase();
|
|
17
|
+
|
|
18
|
+
return `'use strict';
|
|
19
|
+
|
|
20
|
+
const { Mailable } = require('webspresso');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Mailable: ${className}
|
|
24
|
+
* Send via: req.mailer.send(new ${className}(user))
|
|
25
|
+
*/
|
|
26
|
+
class ${className} extends Mailable {
|
|
27
|
+
constructor(recipient, data = {}) {
|
|
28
|
+
super();
|
|
29
|
+
this.recipient = recipient;
|
|
30
|
+
this.data = data;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build email definition
|
|
35
|
+
*/
|
|
36
|
+
build() {
|
|
37
|
+
const toAddress = typeof this.recipient === 'string' ? this.recipient : this.recipient?.email;
|
|
38
|
+
const toName = typeof this.recipient === 'object' ? this.recipient?.name : undefined;
|
|
39
|
+
|
|
40
|
+
this.to(toAddress, toName)
|
|
41
|
+
.subject('Hello from Webspresso')
|
|
42
|
+
.view('${tplName}', this.data);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
${className},
|
|
48
|
+
};
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function registerCommand(program) {
|
|
53
|
+
program
|
|
54
|
+
.command('make:mail <name>')
|
|
55
|
+
.alias('mail:make')
|
|
56
|
+
.alias('make:mailable')
|
|
57
|
+
.description('Scaffold a declarative Mailable class in emails/<name>.js')
|
|
58
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
59
|
+
.action((nameArg, options) => {
|
|
60
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
61
|
+
const emailsDir = path.join(rootDir, 'emails');
|
|
62
|
+
|
|
63
|
+
if (!fs.existsSync(emailsDir)) {
|
|
64
|
+
fs.mkdirSync(emailsDir, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const rawName = String(nameArg || '').trim();
|
|
68
|
+
if (!rawName) {
|
|
69
|
+
console.error('❌ Mailable name is required.');
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const cleanName = rawName.replace(/\.js$/, '');
|
|
75
|
+
const fileName = cleanName.endsWith('Email') ? cleanName : `${cleanName}Email`;
|
|
76
|
+
const targetFilePath = path.join(emailsDir, `${fileName}.js`);
|
|
77
|
+
|
|
78
|
+
if (fs.existsSync(targetFilePath)) {
|
|
79
|
+
console.error(`❌ Mailable file already exists: ${path.relative(rootDir, targetFilePath)}`);
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const content = generateMailContent(cleanName);
|
|
85
|
+
fs.writeFileSync(targetFilePath, content, 'utf-8');
|
|
86
|
+
console.log(`\n✅ Created Mailable class: ${path.relative(rootDir, targetFilePath)}\n`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
registerCommand,
|
|
92
|
+
generateMailContent,
|
|
93
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - make:policy Command
|
|
5
|
+
* Scaffolds an authorization policy in policies/<name>.policy.js
|
|
6
|
+
* @module bin/commands/make-policy
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
function generatePolicyContent(policyName) {
|
|
13
|
+
const modelName = policyName.replace(/policy$/i, '').trim();
|
|
14
|
+
const cleanName = modelName.charAt(0).toUpperCase() + modelName.slice(1);
|
|
15
|
+
|
|
16
|
+
return `'use strict';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Authorization Policy: ${cleanName}
|
|
20
|
+
* Invoked via req.bouncer.authorize('${cleanName.toLowerCase()}.update', record)
|
|
21
|
+
* or {% if fsy.can('${cleanName.toLowerCase()}.update', record) %}
|
|
22
|
+
*/
|
|
23
|
+
module.exports = {
|
|
24
|
+
name: '${cleanName.toLowerCase()}',
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Determine whether the user can view any records
|
|
28
|
+
*/
|
|
29
|
+
async viewAny(user) {
|
|
30
|
+
return true;
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Determine whether the user can view the specific record
|
|
35
|
+
*/
|
|
36
|
+
async view(user, record) {
|
|
37
|
+
return true;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Determine whether the user can create records
|
|
42
|
+
*/
|
|
43
|
+
async create(user) {
|
|
44
|
+
return Boolean(user);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Determine whether the user can update the record
|
|
49
|
+
*/
|
|
50
|
+
async update(user, record) {
|
|
51
|
+
if (!user) return false;
|
|
52
|
+
if (user.isAdmin || user.role === 'admin') return true;
|
|
53
|
+
return record && (record.userId === user.id || record.user_id === user.id);
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Determine whether the user can delete the record
|
|
58
|
+
*/
|
|
59
|
+
async delete(user, record) {
|
|
60
|
+
if (!user) return false;
|
|
61
|
+
return Boolean(user.isAdmin || user.role === 'admin');
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function registerCommand(program) {
|
|
68
|
+
program
|
|
69
|
+
.command('make:policy <name>')
|
|
70
|
+
.alias('policy:make')
|
|
71
|
+
.description('Scaffold an authorization policy in policies/<name>.policy.js')
|
|
72
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
73
|
+
.action((nameArg, options) => {
|
|
74
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
75
|
+
const policiesDir = path.join(rootDir, 'policies');
|
|
76
|
+
|
|
77
|
+
if (!fs.existsSync(policiesDir)) {
|
|
78
|
+
fs.mkdirSync(policiesDir, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const rawName = String(nameArg || '').trim();
|
|
82
|
+
if (!rawName) {
|
|
83
|
+
console.error('❌ Policy name is required.');
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const policyBase = rawName.replace(/\.policy(\.js)?$/, '').replace(/\.js$/, '');
|
|
89
|
+
const targetFilePath = path.join(policiesDir, `${policyBase}.policy.js`);
|
|
90
|
+
|
|
91
|
+
if (fs.existsSync(targetFilePath)) {
|
|
92
|
+
console.error(`❌ Policy file already exists: ${path.relative(rootDir, targetFilePath)}`);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const content = generatePolicyContent(policyBase);
|
|
98
|
+
fs.writeFileSync(targetFilePath, content, 'utf-8');
|
|
99
|
+
console.log(`\n✅ Created authorization policy: ${path.relative(rootDir, targetFilePath)}\n`);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
registerCommand,
|
|
105
|
+
generatePolicyContent,
|
|
106
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - run Command
|
|
5
|
+
* Executes custom management commands with injected framework context
|
|
6
|
+
* @module bin/commands/run
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const { createCommandRegistry, executeCommand } = require('../../core/commands');
|
|
12
|
+
const { createEventDispatcher } = require('../../core/events');
|
|
13
|
+
const { createMailer } = require('../../core/mail');
|
|
14
|
+
const { resolveDbConfigIfExists, createDbInstance } = require('../utils/db');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Helper to parse unknown CLI options into a flags object
|
|
18
|
+
* e.g. ['--dry-run', '--limit', '50', 'arg1'] -> flags: { dryRun: true, limit: 50 }, args: ['arg1']
|
|
19
|
+
* @param {string[]} rawArgs
|
|
20
|
+
* @returns {{ flags: Object, args: string[] }}
|
|
21
|
+
*/
|
|
22
|
+
function parseRawArgs(rawArgs = []) {
|
|
23
|
+
const flags = {};
|
|
24
|
+
const args = [];
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
27
|
+
const item = rawArgs[i];
|
|
28
|
+
if (item.startsWith('--')) {
|
|
29
|
+
const withoutDashes = item.slice(2);
|
|
30
|
+
if (withoutDashes.includes('=')) {
|
|
31
|
+
const [k, v] = withoutDashes.split('=');
|
|
32
|
+
flags[camelCase(k)] = parseVal(v);
|
|
33
|
+
} else {
|
|
34
|
+
const next = rawArgs[i + 1];
|
|
35
|
+
if (next && !next.startsWith('-')) {
|
|
36
|
+
flags[camelCase(withoutDashes)] = parseVal(next);
|
|
37
|
+
i++;
|
|
38
|
+
} else {
|
|
39
|
+
flags[camelCase(withoutDashes)] = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} else if (item.startsWith('-') && item.length === 2) {
|
|
43
|
+
const flagChar = item.slice(1);
|
|
44
|
+
const next = rawArgs[i + 1];
|
|
45
|
+
if (next && !next.startsWith('-')) {
|
|
46
|
+
flags[flagChar] = parseVal(next);
|
|
47
|
+
i++;
|
|
48
|
+
} else {
|
|
49
|
+
flags[flagChar] = true;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
args.push(item);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { flags, args };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function camelCase(str) {
|
|
60
|
+
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseVal(val) {
|
|
64
|
+
if (val === 'true') return true;
|
|
65
|
+
if (val === 'false') return false;
|
|
66
|
+
if (val === 'null') return null;
|
|
67
|
+
if (!Number.isNaN(Number(val)) && val.trim() !== '') return Number(val);
|
|
68
|
+
return val;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function registerCommand(program) {
|
|
72
|
+
program
|
|
73
|
+
.command('run [commandName] [args...]')
|
|
74
|
+
.alias('command:run')
|
|
75
|
+
.description('Run a custom management command from commands/ with injected framework context')
|
|
76
|
+
.option('-d, --dir <path>', 'Custom commands directory (default: ./commands)')
|
|
77
|
+
.option('-c, --config <path>', 'Path to database config file (optional)')
|
|
78
|
+
.option('-E, --env <environment>', 'Environment name (default: development)')
|
|
79
|
+
.allowUnknownOption(true)
|
|
80
|
+
.action(async (commandName, rawArgs = [], options = {}) => {
|
|
81
|
+
const rootDir = process.cwd();
|
|
82
|
+
const commandsDir = options.dir
|
|
83
|
+
? path.resolve(rootDir, options.dir)
|
|
84
|
+
: path.resolve(rootDir, 'commands');
|
|
85
|
+
|
|
86
|
+
const registry = createCommandRegistry({ commandsDir });
|
|
87
|
+
const available = registry.list();
|
|
88
|
+
|
|
89
|
+
if (!commandName) {
|
|
90
|
+
if (available.length === 0) {
|
|
91
|
+
console.log(`\n⚠️ No custom commands found in "${path.relative(rootDir, commandsDir) || 'commands/'}".`);
|
|
92
|
+
console.log(' Create your first custom command with:');
|
|
93
|
+
console.log(' npx webspresso make:command <name>\n');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(`\n⚡ Available Custom Management Commands (${available.length}):\n`);
|
|
98
|
+
const maxLen = Math.max(...available.map((c) => c.name.length), 10);
|
|
99
|
+
for (const cmd of available) {
|
|
100
|
+
console.log(` webspresso run ${cmd.name.padEnd(maxLen + 2)} ${cmd.description ? `# ${cmd.description}` : ''}`);
|
|
101
|
+
}
|
|
102
|
+
console.log('');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!registry.has(commandName)) {
|
|
107
|
+
console.error(`\n❌ Error: Command "${commandName}" not found in ${path.relative(rootDir, commandsDir) || 'commands/'}.`);
|
|
108
|
+
if (available.length > 0) {
|
|
109
|
+
console.log(` Available commands: ${registry.names().join(', ')}\n`);
|
|
110
|
+
} else {
|
|
111
|
+
console.log(' Use `npx webspresso make:command <name>` to create it.\n');
|
|
112
|
+
}
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const cmdDef = registry.get(commandName);
|
|
118
|
+
const { flags, args } = parseRawArgs(rawArgs);
|
|
119
|
+
|
|
120
|
+
// Boot Context
|
|
121
|
+
let knexInstance = null;
|
|
122
|
+
let db = null;
|
|
123
|
+
let services = null;
|
|
124
|
+
const events = createEventDispatcher();
|
|
125
|
+
const mailer = createMailer({ backend: 'console' });
|
|
126
|
+
|
|
127
|
+
// 1. Database
|
|
128
|
+
try {
|
|
129
|
+
const dbInfo = resolveDbConfigIfExists(options.config);
|
|
130
|
+
if (dbInfo) {
|
|
131
|
+
knexInstance = await createDbInstance(dbInfo.config, options.env);
|
|
132
|
+
const { createDatabase } = require('../../core/orm');
|
|
133
|
+
db = createDatabase({ knex: knexInstance });
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.warn('⚠️ Could not initialize database for command:', err.message);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 2. Services
|
|
140
|
+
try {
|
|
141
|
+
const servicesDir = path.resolve(rootDir, 'services');
|
|
142
|
+
if (fs.existsSync(servicesDir)) {
|
|
143
|
+
const { createServiceRegistry } = require('../../src/services');
|
|
144
|
+
services = createServiceRegistry({ servicesDir, isDev: true });
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.warn('⚠️ Could not initialize service registry for command:', err.message);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 3. Queue
|
|
151
|
+
let queue = null;
|
|
152
|
+
try {
|
|
153
|
+
const { createQueueDispatcher } = require('../../core/queue');
|
|
154
|
+
queue = createQueueDispatcher({ db: knexInstance });
|
|
155
|
+
} catch (_) {
|
|
156
|
+
queue = {
|
|
157
|
+
dispatch: async () => {},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log(`\n🚀 [webspresso run] Executing "${commandName}"...`);
|
|
162
|
+
const executionResult = await executeCommand(cmdDef, {
|
|
163
|
+
args,
|
|
164
|
+
flags,
|
|
165
|
+
db,
|
|
166
|
+
queue,
|
|
167
|
+
services,
|
|
168
|
+
mailer,
|
|
169
|
+
events,
|
|
170
|
+
logger: console,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Cleanup
|
|
174
|
+
if (knexInstance && typeof knexInstance.destroy === 'function') {
|
|
175
|
+
try {
|
|
176
|
+
await knexInstance.destroy();
|
|
177
|
+
} catch (_) {}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (executionResult.success) {
|
|
181
|
+
console.log(`\n✨ Command "${commandName}" completed successfully in ${executionResult.durationMs}ms.\n`);
|
|
182
|
+
} else {
|
|
183
|
+
console.error(`\n❌ Command "${commandName}" failed after ${executionResult.durationMs}ms:`);
|
|
184
|
+
if (executionResult.error) {
|
|
185
|
+
console.error(executionResult.error.stack || executionResult.error.message);
|
|
186
|
+
}
|
|
187
|
+
process.exitCode = 1;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = {
|
|
193
|
+
registerCommand,
|
|
194
|
+
parseRawArgs,
|
|
195
|
+
};
|
package/bin/webspresso.js
CHANGED
|
@@ -26,6 +26,7 @@ const { registerCommand: registerDbRollback } = require('./commands/db-rollback'
|
|
|
26
26
|
const { registerCommand: registerDbStatus } = require('./commands/db-status');
|
|
27
27
|
const { registerCommand: registerDbMake } = require('./commands/db-make');
|
|
28
28
|
const { registerCommand: registerDbScaffold } = require('./commands/db-scaffold');
|
|
29
|
+
const { registerCommand: registerDbMakeMigrations } = require('./commands/db-makemigrations');
|
|
29
30
|
const { registerCommand: registerSeed } = require('./commands/seed');
|
|
30
31
|
const { registerCommand: registerAdminSetup } = require('./commands/admin-setup');
|
|
31
32
|
const { registerCommand: registerAdminPassword } = require('./commands/admin-password');
|
|
@@ -47,6 +48,15 @@ const { registerCommand: registerMakeModel } = require('./commands/make-model');
|
|
|
47
48
|
const { registerCommand: registerMakeJob } = require('./commands/make-job');
|
|
48
49
|
const { registerCommand: registerOpenapi } = require('./commands/openapi');
|
|
49
50
|
const { registerCommand: registerTypegen } = require('./commands/typegen');
|
|
51
|
+
const { registerCommand: registerAddDocker } = require('./commands/add-docker');
|
|
52
|
+
const { registerCommand: registerDeployCheck } = require('./commands/deploy-check');
|
|
53
|
+
const { registerCommand: registerRun } = require('./commands/run');
|
|
54
|
+
const { registerCommand: registerList } = require('./commands/list');
|
|
55
|
+
const { registerCommand: registerMakeCommand } = require('./commands/make-command');
|
|
56
|
+
const { registerCommand: registerMakePolicy } = require('./commands/make-policy');
|
|
57
|
+
const { registerCommand: registerMakeFactory } = require('./commands/make-factory');
|
|
58
|
+
const { registerCommand: registerMakeEvent } = require('./commands/make-event');
|
|
59
|
+
const { registerCommand: registerMakeMail } = require('./commands/make-mail');
|
|
50
60
|
|
|
51
61
|
registerNew(program);
|
|
52
62
|
registerPage(program);
|
|
@@ -56,11 +66,13 @@ registerStart(program);
|
|
|
56
66
|
registerBuild(program);
|
|
57
67
|
registerAddTailwind(program);
|
|
58
68
|
registerAddEsbuild(program);
|
|
69
|
+
registerAddDocker(program);
|
|
59
70
|
registerDbMigrate(program);
|
|
60
71
|
registerDbRollback(program);
|
|
61
72
|
registerDbStatus(program);
|
|
62
73
|
registerDbMake(program);
|
|
63
74
|
registerDbScaffold(program);
|
|
75
|
+
registerDbMakeMigrations(program);
|
|
64
76
|
registerSeed(program);
|
|
65
77
|
registerAdminSetup(program);
|
|
66
78
|
registerAdminPassword(program);
|
|
@@ -71,6 +83,7 @@ registerEmailPrune(program);
|
|
|
71
83
|
registerQueuePrune(program);
|
|
72
84
|
registerUploadPrune(program);
|
|
73
85
|
registerDoctor(program);
|
|
86
|
+
registerDeployCheck(program);
|
|
74
87
|
registerUpgrade(program);
|
|
75
88
|
registerOrmMap(program);
|
|
76
89
|
registerService(program);
|
|
@@ -80,8 +93,16 @@ registerAgentsInit(program);
|
|
|
80
93
|
registerRoutes(program);
|
|
81
94
|
registerMakeModel(program);
|
|
82
95
|
registerMakeJob(program);
|
|
96
|
+
registerMakeCommand(program);
|
|
97
|
+
registerMakePolicy(program);
|
|
98
|
+
registerMakeFactory(program);
|
|
99
|
+
registerMakeEvent(program);
|
|
100
|
+
registerMakeMail(program);
|
|
101
|
+
registerRun(program);
|
|
102
|
+
registerList(program);
|
|
83
103
|
registerOpenapi(program);
|
|
84
104
|
registerTypegen(program);
|
|
85
105
|
|
|
86
106
|
// Parse arguments
|
|
87
107
|
program.parse();
|
|
108
|
+
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso Auth - Bouncer & Policy Authorization Engine (AdonisJS Bouncer & Django Permissions-inspired)
|
|
3
|
+
* Context-aware authorization helper for routes, handlers, and templates
|
|
4
|
+
* @module core/auth/bouncer
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { defaultPolicyManager, AuthorizationError } = require('./policy');
|
|
8
|
+
const { ForbiddenError, UnauthorizedError } = require('../errors');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Context-aware Bouncer instance bound to a specific user
|
|
12
|
+
*/
|
|
13
|
+
class Bouncer {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object|null} user - The subject user (or null for guest)
|
|
16
|
+
* @param {import('./policy').PolicyManager} [policyManager]
|
|
17
|
+
*/
|
|
18
|
+
constructor(user = null, policyManager = defaultPolicyManager) {
|
|
19
|
+
this.user = user;
|
|
20
|
+
this.policyManager = policyManager;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a new Bouncer instance for another user
|
|
25
|
+
* @param {Object|null} user
|
|
26
|
+
* @returns {Bouncer}
|
|
27
|
+
*/
|
|
28
|
+
forUser(user) {
|
|
29
|
+
return new Bouncer(user, this.policyManager);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse policy and action from arguments
|
|
34
|
+
* Supports:
|
|
35
|
+
* 1. ('Post.update', post) -> policy='Post', action='update', resource=post
|
|
36
|
+
* 2. ('Post', 'update', post) -> policy='Post', action='update', resource=post
|
|
37
|
+
* 3. ('gate-name', resource) -> policy=null, action='gate-name', resource=resource
|
|
38
|
+
* 4. (functionRule, resource) -> direct function check
|
|
39
|
+
* @protected
|
|
40
|
+
*/
|
|
41
|
+
_parseArgs(actionOrPolicy, ...rest) {
|
|
42
|
+
if (typeof actionOrPolicy === 'function') {
|
|
43
|
+
return {
|
|
44
|
+
isFn: true,
|
|
45
|
+
fn: actionOrPolicy,
|
|
46
|
+
resource: rest[0],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (typeof actionOrPolicy === 'string') {
|
|
51
|
+
// 1. Dot-notation: 'Post.update'
|
|
52
|
+
if (actionOrPolicy.includes('.')) {
|
|
53
|
+
const parts = actionOrPolicy.split('.');
|
|
54
|
+
const policy = parts[0];
|
|
55
|
+
const action = parts.slice(1).join('.');
|
|
56
|
+
return { policy, action, resource: rest[0] };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. Separate strings: ('Post', 'update', post)
|
|
60
|
+
if (rest.length > 0 && typeof rest[0] === 'string') {
|
|
61
|
+
const policy = actionOrPolicy;
|
|
62
|
+
const action = rest[0];
|
|
63
|
+
const resource = rest[1];
|
|
64
|
+
return { policy, action, resource };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 3. Single gate / permission name: ('edit-settings', setting)
|
|
68
|
+
return { policy: null, action: actionOrPolicy, resource: rest[0] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { policy: null, action: String(actionOrPolicy), resource: rest[0] };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if current user is allowed to perform an action
|
|
76
|
+
* @param {string|Function} actionOrPolicy
|
|
77
|
+
* @param {...any} rest
|
|
78
|
+
* @returns {boolean}
|
|
79
|
+
*/
|
|
80
|
+
allows(actionOrPolicy, ...rest) {
|
|
81
|
+
const parsed = this._parseArgs(actionOrPolicy, ...rest);
|
|
82
|
+
|
|
83
|
+
// Direct function rule
|
|
84
|
+
if (parsed.isFn) {
|
|
85
|
+
try {
|
|
86
|
+
return Boolean(parsed.fn(this.user, parsed.resource));
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { policy, action, resource } = parsed;
|
|
93
|
+
|
|
94
|
+
// 1. Check Policy Manager (Gates & Policies)
|
|
95
|
+
if (policy) {
|
|
96
|
+
if (this.policyManager.hasPolicy(policy)) {
|
|
97
|
+
return this.policyManager.can(this.user, action, policy, resource);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
if (this.policyManager.hasGate(action)) {
|
|
101
|
+
return this.policyManager.can(this.user, action, null, resource);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 2. Django-style user permissions matrix check
|
|
106
|
+
// e.g. user.permissions = ['post.create', 'post.update'] or user.hasPerm(...)
|
|
107
|
+
if (this.user) {
|
|
108
|
+
const fullPerm = policy ? `${policy.toLowerCase()}.${action.toLowerCase()}` : action.toLowerCase();
|
|
109
|
+
|
|
110
|
+
// Check hasPerm / hasPermission method on user object
|
|
111
|
+
if (typeof this.user.hasPerm === 'function') {
|
|
112
|
+
if (this.user.hasPerm(fullPerm, resource)) return true;
|
|
113
|
+
}
|
|
114
|
+
if (typeof this.user.hasPermission === 'function') {
|
|
115
|
+
if (this.user.hasPermission(fullPerm, resource)) return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check permissions array or set
|
|
119
|
+
const perms = this.user.permissions;
|
|
120
|
+
if (Array.isArray(perms) || perms instanceof Set) {
|
|
121
|
+
const permSet = perms instanceof Set ? perms : new Set(perms);
|
|
122
|
+
if (permSet.has('*') || permSet.has(fullPerm)) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (policy && permSet.has(`${policy.toLowerCase()}.*`)) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// If user has superuser / is_admin flag
|
|
131
|
+
if (this.user.isSuperuser || this.user.is_superuser) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if current user is denied from performing an action
|
|
141
|
+
* @param {string|Function} actionOrPolicy
|
|
142
|
+
* @param {...any} rest
|
|
143
|
+
* @returns {boolean}
|
|
144
|
+
*/
|
|
145
|
+
denies(actionOrPolicy, ...rest) {
|
|
146
|
+
return !this.allows(actionOrPolicy, ...rest);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Authorize an action. Throws ForbiddenError (or UnauthorizedError) if access is denied.
|
|
151
|
+
* @param {string|Function} actionOrPolicy
|
|
152
|
+
* @param {...any} rest
|
|
153
|
+
* @throws {ForbiddenError|UnauthorizedError}
|
|
154
|
+
*/
|
|
155
|
+
authorize(actionOrPolicy, ...rest) {
|
|
156
|
+
if (this.denies(actionOrPolicy, ...rest)) {
|
|
157
|
+
if (!this.user) {
|
|
158
|
+
throw new UnauthorizedError('Authentication required to perform this action');
|
|
159
|
+
}
|
|
160
|
+
const label = typeof actionOrPolicy === 'string' ? actionOrPolicy : 'action';
|
|
161
|
+
throw new ForbiddenError(`You are not authorized to perform "${label}"`);
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Create a Bouncer instance for a user
|
|
169
|
+
* @param {Object|null} user
|
|
170
|
+
* @param {import('./policy').PolicyManager} [policyManager]
|
|
171
|
+
* @returns {Bouncer}
|
|
172
|
+
*/
|
|
173
|
+
function createBouncer(user = null, policyManager = defaultPolicyManager) {
|
|
174
|
+
return new Bouncer(user, policyManager);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Express middleware that attaches `req.bouncer` automatically
|
|
179
|
+
* @param {Object} [options={}]
|
|
180
|
+
* @returns {import('express').RequestHandler}
|
|
181
|
+
*/
|
|
182
|
+
function createBouncerMiddleware(options = {}) {
|
|
183
|
+
const policyManager = options.policyManager || defaultPolicyManager;
|
|
184
|
+
|
|
185
|
+
return function bouncerMiddleware(req, res, next) {
|
|
186
|
+
const user = req.user || req.auth?.user || null;
|
|
187
|
+
req.bouncer = new Bouncer(user, policyManager);
|
|
188
|
+
next();
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = {
|
|
193
|
+
Bouncer,
|
|
194
|
+
createBouncer,
|
|
195
|
+
createBouncerMiddleware,
|
|
196
|
+
};
|