@axium/server 0.27.0 → 0.28.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/dist/acl.d.ts +26 -46
- package/dist/acl.js +42 -62
- package/dist/api/acl.js +11 -3
- package/dist/api/admin.js +3 -6
- package/dist/api/metadata.js +4 -11
- package/dist/audit.d.ts +0 -3
- package/dist/audit.js +0 -8
- package/dist/auth.d.ts +10 -5
- package/dist/auth.js +29 -23
- package/dist/cli.d.ts +8 -2
- package/dist/cli.js +18 -606
- package/dist/config.js +6 -5
- package/dist/database.d.ts +413 -29
- package/dist/database.js +522 -247
- package/dist/db.json +71 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +833 -0
- package/package.json +6 -4
- package/routes/account/+page.svelte +11 -13
- package/schemas/config.json +207 -0
- package/schemas/db.json +636 -0
package/dist/cli.js
CHANGED
|
@@ -1,89 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
if (value !== null && value !== void 0) {
|
|
4
|
-
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
5
|
-
var dispose, inner;
|
|
6
|
-
if (async) {
|
|
7
|
-
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
8
|
-
dispose = value[Symbol.asyncDispose];
|
|
9
|
-
}
|
|
10
|
-
if (dispose === void 0) {
|
|
11
|
-
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
12
|
-
dispose = value[Symbol.dispose];
|
|
13
|
-
if (async) inner = dispose;
|
|
14
|
-
}
|
|
15
|
-
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
16
|
-
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
17
|
-
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
18
|
-
}
|
|
19
|
-
else if (async) {
|
|
20
|
-
env.stack.push({ async: true });
|
|
21
|
-
}
|
|
22
|
-
return value;
|
|
23
|
-
};
|
|
24
|
-
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
25
|
-
return function (env) {
|
|
26
|
-
function fail(e) {
|
|
27
|
-
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
28
|
-
env.hasError = true;
|
|
29
|
-
}
|
|
30
|
-
var r, s = 0;
|
|
31
|
-
function next() {
|
|
32
|
-
while (r = env.stack.pop()) {
|
|
33
|
-
try {
|
|
34
|
-
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
35
|
-
if (r.dispose) {
|
|
36
|
-
var result = r.dispose.call(r.value);
|
|
37
|
-
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
38
|
-
}
|
|
39
|
-
else s |= 1;
|
|
40
|
-
}
|
|
41
|
-
catch (e) {
|
|
42
|
-
fail(e);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
46
|
-
if (env.hasError) throw env.error;
|
|
47
|
-
}
|
|
48
|
-
return next();
|
|
49
|
-
};
|
|
50
|
-
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
51
|
-
var e = new Error(message);
|
|
52
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
53
|
-
});
|
|
54
|
-
import { apps } from '@axium/core';
|
|
55
|
-
import { AuditFilter, severityNames } from '@axium/core/audit';
|
|
56
|
-
import { formatDateRange } from '@axium/core/format';
|
|
57
|
-
import { outputDaemonStatus, io, pluginText } from '@axium/core/node';
|
|
58
|
-
import { _findPlugin, plugins } from '@axium/core/plugins';
|
|
59
|
-
import { Argument, Option, program } from 'commander';
|
|
60
|
-
import { access } from 'node:fs/promises';
|
|
61
|
-
import { join, resolve } from 'node:path/posix';
|
|
62
|
-
import { createInterface } from 'node:readline/promises';
|
|
1
|
+
// Supporting code for the CLI. The CLI entry point is main.ts
|
|
2
|
+
import { io } from '@axium/core/node';
|
|
63
3
|
import { styleText } from 'node:util';
|
|
64
|
-
import { getByString, isJSON, setByString } from 'utilium';
|
|
65
|
-
import * as z from 'zod';
|
|
66
|
-
import $pkg from '../package.json' with { type: 'json' };
|
|
67
|
-
import { audit, getEvents, styleSeverity } from './audit.js';
|
|
68
|
-
import config, { configFiles, FileSchema, saveConfigTo } from './config.js';
|
|
69
4
|
import * as db from './database.js';
|
|
70
|
-
import
|
|
71
|
-
|
|
72
|
-
import { serve } from './serve.js';
|
|
73
|
-
function userText(user, bold = false) {
|
|
5
|
+
import * as z from 'zod';
|
|
6
|
+
export function userText(user, bold = false) {
|
|
74
7
|
const text = `${user.name} <${user.email}> (${user.id})`;
|
|
75
8
|
return bold ? styleText('bold', text) : text;
|
|
76
9
|
}
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
10
|
+
export async function lookupUser(lookup) {
|
|
11
|
+
const value = await (lookup.includes('@') ? z.email() : z.uuid())
|
|
12
|
+
.parseAsync(lookup.toLowerCase())
|
|
13
|
+
.catch(() => io.exit('Invalid user ID or email.'));
|
|
14
|
+
const result = await db
|
|
15
|
+
.connect()
|
|
16
|
+
.selectFrom('users')
|
|
17
|
+
.where(value.includes('@') ? 'email' : 'id', '=', value)
|
|
18
|
+
.selectAll()
|
|
19
|
+
.executeTakeFirst();
|
|
20
|
+
if (!result)
|
|
21
|
+
io.exit('No user with matching ID or email.');
|
|
22
|
+
return result;
|
|
81
23
|
}
|
|
82
24
|
/**
|
|
83
25
|
* Updates an array of strings by adding or removing items.
|
|
84
26
|
* Only returns whether the array was updated and diff text for what actually changed.
|
|
85
27
|
*/
|
|
86
|
-
function diffUpdate(original, add, remove) {
|
|
28
|
+
export function diffUpdate(original, add, remove) {
|
|
87
29
|
const diffs = [];
|
|
88
30
|
// update the values
|
|
89
31
|
if (add) {
|
|
@@ -103,533 +45,3 @@ function diffUpdate(original, add, remove) {
|
|
|
103
45
|
});
|
|
104
46
|
return [!!diffs.length, original, diffs.join(', ')];
|
|
105
47
|
}
|
|
106
|
-
var rl, safe, noAutoDB, opts, axiumDB, axiumConfig, axiumPlugin, axiumApps, lookup;
|
|
107
|
-
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
108
|
-
try {
|
|
109
|
-
rl = __addDisposableResource(env_1, createInterface({
|
|
110
|
-
input: process.stdin,
|
|
111
|
-
output: process.stdout,
|
|
112
|
-
}), false);
|
|
113
|
-
safe = z.stringbool().default(false).parse(process.env.SAFE?.toLowerCase()) || process.argv.includes('--safe');
|
|
114
|
-
await config.load(join(systemDir, 'config.json'), { safe });
|
|
115
|
-
program
|
|
116
|
-
.version($pkg.version)
|
|
117
|
-
.name('axium')
|
|
118
|
-
.description('Axium server CLI')
|
|
119
|
-
.configureHelp({ showGlobalOptions: true })
|
|
120
|
-
.option('--safe', 'do not execute code from plugins')
|
|
121
|
-
.option('--debug', 'override debug mode')
|
|
122
|
-
.option('--no-debug', 'override debug mode')
|
|
123
|
-
.option('-c, --config <path>', 'path to the config file');
|
|
124
|
-
program.on('option:debug', () => config.set({ debug: true }));
|
|
125
|
-
program.on('option:config', () => void config.load(program.opts().config, { safe }));
|
|
126
|
-
noAutoDB = ['init', 'serve', 'check'];
|
|
127
|
-
program.hook('preAction', async function (_, action) {
|
|
128
|
-
await config.loadDefaults(safe);
|
|
129
|
-
const opt = action.optsWithGlobals();
|
|
130
|
-
opt.force && io.warn('--force: Protections disabled.');
|
|
131
|
-
if (typeof opt.debug == 'boolean') {
|
|
132
|
-
config.set({ debug: opt.debug });
|
|
133
|
-
io._setDebugOutput(opt.debug);
|
|
134
|
-
}
|
|
135
|
-
try {
|
|
136
|
-
db.connect();
|
|
137
|
-
}
|
|
138
|
-
catch (e) {
|
|
139
|
-
if (!noAutoDB.includes(action.name()))
|
|
140
|
-
throw e;
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
program.hook('postAction', async (_, action) => {
|
|
144
|
-
if (!noAutoDB.includes(action.name()))
|
|
145
|
-
await db.database.destroy();
|
|
146
|
-
});
|
|
147
|
-
// Options shared by multiple (sub)commands
|
|
148
|
-
opts = {
|
|
149
|
-
// database specific
|
|
150
|
-
host: new Option('-H, --host <host>', 'the host of the database.').argParser(value => {
|
|
151
|
-
const [hostname, port] = value?.split(':') ?? [];
|
|
152
|
-
config.db.host = hostname || config.db.host;
|
|
153
|
-
config.db.port = port && Number.isSafeInteger(parseInt(port)) ? parseInt(port) : config.db.port;
|
|
154
|
-
}),
|
|
155
|
-
check: new Option('--check', 'check the database schema after initialization').default(false),
|
|
156
|
-
force: new Option('-f, --force', 'force the operation').default(false),
|
|
157
|
-
global: new Option('-g, --global', 'apply the operation globally').default(false),
|
|
158
|
-
timeout: new Option('-t, --timeout <ms>', 'how long to wait for commands to complete.').default('1000').argParser(value => {
|
|
159
|
-
const timeout = parseInt(value);
|
|
160
|
-
if (!Number.isSafeInteger(timeout) || timeout < 0)
|
|
161
|
-
io.warn('Invalid timeout value, using default.');
|
|
162
|
-
io.setCommandTimeout(timeout);
|
|
163
|
-
}),
|
|
164
|
-
packagesDir: new Option('-p, --packages-dir <dir>', 'the directory to look for packages in'),
|
|
165
|
-
};
|
|
166
|
-
axiumDB = program.command('db').alias('database').description('Manage the database').addOption(opts.timeout).addOption(opts.host);
|
|
167
|
-
axiumDB
|
|
168
|
-
.command('init')
|
|
169
|
-
.description('Initialize the database')
|
|
170
|
-
.addOption(opts.force)
|
|
171
|
-
.option('-s, --skip', 'If the user, database, or schema already exists, skip trying to create it.')
|
|
172
|
-
.addOption(opts.check)
|
|
173
|
-
.action(async (_localOpts, _) => {
|
|
174
|
-
const opt = _.optsWithGlobals();
|
|
175
|
-
await db.init(opt).catch(io.handleError);
|
|
176
|
-
});
|
|
177
|
-
axiumDB
|
|
178
|
-
.command('status')
|
|
179
|
-
.alias('stats')
|
|
180
|
-
.description('Check the status of the database')
|
|
181
|
-
.action(async () => {
|
|
182
|
-
try {
|
|
183
|
-
console.log(await db.statText());
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
io.error('Unavailable');
|
|
187
|
-
process.exitCode = 1;
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
axiumDB
|
|
191
|
-
.command('drop')
|
|
192
|
-
.description('Drop the Axium database and user')
|
|
193
|
-
.addOption(opts.force)
|
|
194
|
-
.action(async (opt) => {
|
|
195
|
-
const stats = await db.count('users', 'passkeys', 'sessions').catch(io.exit);
|
|
196
|
-
if (!opt.force)
|
|
197
|
-
for (const key of ['users', 'passkeys', 'sessions']) {
|
|
198
|
-
if (stats[key] == 0)
|
|
199
|
-
continue;
|
|
200
|
-
io.warn(`Database has existing ${key}. Use --force if you really want to drop the database.`);
|
|
201
|
-
process.exit(2);
|
|
202
|
-
}
|
|
203
|
-
await db.uninstall(opt).catch(io.exit);
|
|
204
|
-
});
|
|
205
|
-
axiumDB
|
|
206
|
-
.command('wipe')
|
|
207
|
-
.description('Wipe the database')
|
|
208
|
-
.addOption(opts.force)
|
|
209
|
-
.action(async (opt) => {
|
|
210
|
-
const stats = await db.count('users', 'passkeys', 'sessions').catch(io.exit);
|
|
211
|
-
if (!opt.force)
|
|
212
|
-
for (const key of ['users', 'passkeys', 'sessions']) {
|
|
213
|
-
if (stats[key] == 0)
|
|
214
|
-
continue;
|
|
215
|
-
io.warn(`Database has existing ${key}. Use --force if you really want to wipe the database.`);
|
|
216
|
-
process.exit(2);
|
|
217
|
-
}
|
|
218
|
-
await db.wipe(opt).catch(io.exit);
|
|
219
|
-
});
|
|
220
|
-
axiumDB
|
|
221
|
-
.command('check')
|
|
222
|
-
.description('Check the structure of the database')
|
|
223
|
-
.option('-s, --strict', 'Throw errors instead of emitting warnings for most column problems')
|
|
224
|
-
.action(async (opt) => {
|
|
225
|
-
await db.check(opt).catch(io.exit);
|
|
226
|
-
});
|
|
227
|
-
axiumDB
|
|
228
|
-
.command('clean')
|
|
229
|
-
.description('Remove expired rows')
|
|
230
|
-
.addOption(opts.force)
|
|
231
|
-
.action(async (opt) => {
|
|
232
|
-
await db.clean(opt).catch(io.exit);
|
|
233
|
-
});
|
|
234
|
-
axiumDB
|
|
235
|
-
.command('rotate-password')
|
|
236
|
-
.description('Generate a new password for the database user and update the config')
|
|
237
|
-
.action(db.rotatePassword);
|
|
238
|
-
axiumConfig = program
|
|
239
|
-
.command('config')
|
|
240
|
-
.description('Manage the configuration')
|
|
241
|
-
.addOption(opts.global)
|
|
242
|
-
.option('-j, --json', 'values are JSON encoded')
|
|
243
|
-
.option('-r, --redact', 'Do not output sensitive values');
|
|
244
|
-
axiumConfig
|
|
245
|
-
.command('dump')
|
|
246
|
-
.description('Output the entire current configuration')
|
|
247
|
-
.action(() => {
|
|
248
|
-
const opt = axiumConfig.optsWithGlobals();
|
|
249
|
-
const value = config.plain();
|
|
250
|
-
console.log(opt.json ? JSON.stringify(value, configReplacer(opt), 4) : value);
|
|
251
|
-
});
|
|
252
|
-
axiumConfig
|
|
253
|
-
.command('get')
|
|
254
|
-
.description('Get a config value')
|
|
255
|
-
.argument('<key>', 'the key to get')
|
|
256
|
-
.action((key) => {
|
|
257
|
-
const opt = axiumConfig.optsWithGlobals();
|
|
258
|
-
const value = getByString(config.plain(), key);
|
|
259
|
-
console.log(opt.json ? JSON.stringify(value, configReplacer(opt), 4) : value);
|
|
260
|
-
});
|
|
261
|
-
axiumConfig
|
|
262
|
-
.command('set')
|
|
263
|
-
.description('Set a config value. Note setting objects is not supported.')
|
|
264
|
-
.argument('<key>', 'the key to set')
|
|
265
|
-
.argument('<value>', 'the value')
|
|
266
|
-
.action((key, value) => {
|
|
267
|
-
const opt = axiumConfig.optsWithGlobals();
|
|
268
|
-
if (opt.json && !isJSON(value))
|
|
269
|
-
io.exit('Invalid JSON');
|
|
270
|
-
const obj = {};
|
|
271
|
-
setByString(obj, key, opt.json ? JSON.parse(value) : value);
|
|
272
|
-
config.save(obj, opt.global);
|
|
273
|
-
});
|
|
274
|
-
axiumConfig
|
|
275
|
-
.command('list')
|
|
276
|
-
.alias('ls')
|
|
277
|
-
.alias('files')
|
|
278
|
-
.description('List loaded config files')
|
|
279
|
-
.action(() => {
|
|
280
|
-
for (const path of config.files.keys())
|
|
281
|
-
console.log(path);
|
|
282
|
-
});
|
|
283
|
-
axiumConfig
|
|
284
|
-
.command('schema')
|
|
285
|
-
.description('Get the JSON schema for the configuration file')
|
|
286
|
-
.action(() => {
|
|
287
|
-
const opt = axiumConfig.optsWithGlobals();
|
|
288
|
-
const schema = z.toJSONSchema(FileSchema, { io: 'input' });
|
|
289
|
-
console.log(opt.json ? JSON.stringify(schema, configReplacer(opt), 4) : schema);
|
|
290
|
-
});
|
|
291
|
-
axiumPlugin = program.command('plugin').alias('plugins').description('Manage plugins').addOption(opts.global);
|
|
292
|
-
axiumPlugin
|
|
293
|
-
.command('list')
|
|
294
|
-
.alias('ls')
|
|
295
|
-
.description('List loaded plugins')
|
|
296
|
-
.option('-l, --long', 'use the long listing format')
|
|
297
|
-
.option('--no-versions', 'do not show plugin versions')
|
|
298
|
-
.action((opt) => {
|
|
299
|
-
if (!plugins.size) {
|
|
300
|
-
console.log('No plugins loaded.');
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
if (!opt.long) {
|
|
304
|
-
console.log(Array.from(plugins.keys()).join(', '));
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
console.log(styleText('whiteBright', plugins.size + ' plugin(s) loaded:'));
|
|
308
|
-
for (const plugin of plugins.values()) {
|
|
309
|
-
console.log(plugin.name, opt.versions ? plugin.version : '');
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
axiumPlugin
|
|
313
|
-
.command('info')
|
|
314
|
-
.description('Get information about a plugin')
|
|
315
|
-
.argument('<plugin>', 'the plugin to get information about')
|
|
316
|
-
.action((search) => {
|
|
317
|
-
const plugin = _findPlugin(search);
|
|
318
|
-
for (const line of pluginText(plugin))
|
|
319
|
-
console.log(line);
|
|
320
|
-
});
|
|
321
|
-
axiumPlugin
|
|
322
|
-
.command('remove')
|
|
323
|
-
.alias('rm')
|
|
324
|
-
.description('Remove a plugin')
|
|
325
|
-
.argument('<plugin>', 'the plugin to remove')
|
|
326
|
-
.action(async (search, opt) => {
|
|
327
|
-
const plugin = _findPlugin(search);
|
|
328
|
-
await plugin._hooks?.remove?.(opt);
|
|
329
|
-
for (const [path, data] of configFiles) {
|
|
330
|
-
if (!data.plugins)
|
|
331
|
-
continue;
|
|
332
|
-
data.plugins = data.plugins.filter(p => p !== plugin.specifier);
|
|
333
|
-
saveConfigTo(path, data);
|
|
334
|
-
}
|
|
335
|
-
plugins.delete(plugin.name);
|
|
336
|
-
});
|
|
337
|
-
axiumPlugin
|
|
338
|
-
.command('init')
|
|
339
|
-
.alias('setup')
|
|
340
|
-
.alias('install')
|
|
341
|
-
.description('Initialize a plugin. This could include adding tables to the database or linking routes.')
|
|
342
|
-
.addOption(opts.timeout)
|
|
343
|
-
.addOption(opts.check)
|
|
344
|
-
.argument('<plugin>', 'the plugin to initialize')
|
|
345
|
-
.action(async (search, opt) => {
|
|
346
|
-
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
347
|
-
try {
|
|
348
|
-
const plugin = _findPlugin(search);
|
|
349
|
-
if (!plugin)
|
|
350
|
-
io.exit(`Can't find a plugin matching "${search}"`);
|
|
351
|
-
const _ = __addDisposableResource(env_2, db.connect(), true);
|
|
352
|
-
await plugin._hooks?.db_init?.({ force: false, ...opt, skip: true });
|
|
353
|
-
}
|
|
354
|
-
catch (e_2) {
|
|
355
|
-
env_2.error = e_2;
|
|
356
|
-
env_2.hasError = true;
|
|
357
|
-
}
|
|
358
|
-
finally {
|
|
359
|
-
const result_1 = __disposeResources(env_2);
|
|
360
|
-
if (result_1)
|
|
361
|
-
await result_1;
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
axiumApps = program.command('apps').description('Manage Axium apps').addOption(opts.global);
|
|
365
|
-
axiumApps
|
|
366
|
-
.command('list')
|
|
367
|
-
.alias('ls')
|
|
368
|
-
.description('List apps added by plugins')
|
|
369
|
-
.option('-l, --long', 'use the long listing format')
|
|
370
|
-
.option('-b, --builtin', 'include built-in apps')
|
|
371
|
-
.action((opt) => {
|
|
372
|
-
if (!apps.size) {
|
|
373
|
-
console.log('No apps.');
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
if (!opt.long) {
|
|
377
|
-
console.log(Array.from(apps.values().map(app => app.name)).join(', '));
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
console.log(styleText('whiteBright', apps.size + ' app(s) loaded:'));
|
|
381
|
-
for (const app of apps.values()) {
|
|
382
|
-
console.log(app.name, styleText('dim', `(${app.id})`));
|
|
383
|
-
}
|
|
384
|
-
});
|
|
385
|
-
lookup = new Argument('<user>', 'the UUID or email of the user to operate on').argParser(async (lookup) => {
|
|
386
|
-
const value = await (lookup.includes('@') ? z.email() : z.uuid())
|
|
387
|
-
.parseAsync(lookup.toLowerCase())
|
|
388
|
-
.catch(() => io.exit('Invalid user ID or email.'));
|
|
389
|
-
const result = await db
|
|
390
|
-
.connect()
|
|
391
|
-
.selectFrom('users')
|
|
392
|
-
.where(value.includes('@') ? 'email' : 'id', '=', value)
|
|
393
|
-
.selectAll()
|
|
394
|
-
.executeTakeFirst();
|
|
395
|
-
if (!result)
|
|
396
|
-
io.exit('No user with matching ID or email.');
|
|
397
|
-
return result;
|
|
398
|
-
});
|
|
399
|
-
program
|
|
400
|
-
.command('user')
|
|
401
|
-
.description('Get or change information about a user')
|
|
402
|
-
.addArgument(lookup)
|
|
403
|
-
.option('-S, --sessions', 'show user sessions')
|
|
404
|
-
.option('-P, --passkeys', 'show user passkeys')
|
|
405
|
-
.option('--add-role <role...>', 'add roles to the user')
|
|
406
|
-
.option('--remove-role <role...>', 'remove roles from the user')
|
|
407
|
-
.option('--tag <tag...>', 'Add tags to the user')
|
|
408
|
-
.option('--untag <tag...>', 'Remove tags from the user')
|
|
409
|
-
.option('--delete', 'Delete the user')
|
|
410
|
-
.option('--suspend', 'Suspend the user')
|
|
411
|
-
.addOption(new Option('--unsuspend', 'Un-suspend the user').conflicts('suspend'))
|
|
412
|
-
.action(async (_user, opt) => {
|
|
413
|
-
let user = await _user;
|
|
414
|
-
const [updatedRoles, roles, rolesDiff] = diffUpdate(user.roles, opt.addRole, opt.removeRole);
|
|
415
|
-
const [updatedTags, tags, tagsDiff] = diffUpdate(user.tags, opt.tag, opt.untag);
|
|
416
|
-
const changeSuspend = (opt.suspend || opt.unsuspend) && user.isSuspended != (opt.suspend ?? !opt.unsuspend);
|
|
417
|
-
if (updatedRoles || updatedTags || changeSuspend) {
|
|
418
|
-
user = await db.database
|
|
419
|
-
.updateTable('users')
|
|
420
|
-
.set({ roles, tags, isSuspended: !changeSuspend ? user.isSuspended : (opt.suspend ?? !opt.unsuspend) })
|
|
421
|
-
.returningAll()
|
|
422
|
-
.executeTakeFirstOrThrow()
|
|
423
|
-
.then(u => {
|
|
424
|
-
if (updatedRoles && rolesDiff)
|
|
425
|
-
console.log(`> Updated roles: ${rolesDiff}`);
|
|
426
|
-
if (updatedTags && tagsDiff)
|
|
427
|
-
console.log(`> Updated tags: ${tagsDiff}`);
|
|
428
|
-
if (changeSuspend)
|
|
429
|
-
console.log(opt.suspend ? '> Suspended' : '> Un-suspended');
|
|
430
|
-
return u;
|
|
431
|
-
})
|
|
432
|
-
.catch(e => io.exit('Failed to update user: ' + e.message));
|
|
433
|
-
}
|
|
434
|
-
if (opt.delete) {
|
|
435
|
-
const confirmed = await rl
|
|
436
|
-
.question(`Are you sure you want to delete ${userText(user, true)}? (y/N) `)
|
|
437
|
-
.then(v => z.stringbool().parseAsync(v))
|
|
438
|
-
.catch(() => false);
|
|
439
|
-
if (!confirmed)
|
|
440
|
-
console.log(styleText('dim', '> Delete aborted.'));
|
|
441
|
-
else
|
|
442
|
-
await db.database
|
|
443
|
-
.deleteFrom('users')
|
|
444
|
-
.where('id', '=', user.id)
|
|
445
|
-
.executeTakeFirstOrThrow()
|
|
446
|
-
.then(() => console.log(styleText(['red', 'bold'], '> Deleted')))
|
|
447
|
-
.catch(e => io.exit('Failed to delete user: ' + e.message));
|
|
448
|
-
}
|
|
449
|
-
console.log([
|
|
450
|
-
user.isSuspended && styleText('yellowBright', 'Suspended'),
|
|
451
|
-
user.isAdmin && styleText('redBright', 'Administrator'),
|
|
452
|
-
'UUID: ' + user.id,
|
|
453
|
-
'Name: ' + user.name,
|
|
454
|
-
`Email: ${user.email}, ${user.emailVerified ? 'verified on ' + formatDateRange(user.emailVerified) : styleText(config.auth.email_verification ? 'yellow' : 'dim', 'not verified')}`,
|
|
455
|
-
'Registered ' + formatDateRange(user.registeredAt),
|
|
456
|
-
`Roles: ${user.roles.length ? user.roles.join(', ') : styleText('dim', '(none)')}`,
|
|
457
|
-
`Tags: ${user.tags.length ? user.tags.join(', ') : styleText('dim', '(none)')}`,
|
|
458
|
-
]
|
|
459
|
-
.filter(v => v)
|
|
460
|
-
.join('\n'));
|
|
461
|
-
if (opt.sessions) {
|
|
462
|
-
const sessions = await db.database.selectFrom('sessions').where('userId', '=', user.id).selectAll().execute();
|
|
463
|
-
console.log(styleText('bold', 'Sessions:'));
|
|
464
|
-
if (!sessions.length)
|
|
465
|
-
console.log(styleText('dim', '(none)'));
|
|
466
|
-
else
|
|
467
|
-
for (const session of sessions) {
|
|
468
|
-
console.log(`\t${session.id}\tcreated ${formatDateRange(session.created).padEnd(40)}\texpires ${formatDateRange(session.expires).padEnd(40)}\t${session.elevated ? styleText('yellow', '(elevated)') : ''}`);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
if (opt.passkeys) {
|
|
472
|
-
const passkeys = await db.database.selectFrom('passkeys').where('userId', '=', user.id).selectAll().execute();
|
|
473
|
-
console.log(styleText('bold', 'Passkeys:'));
|
|
474
|
-
for (const passkey of passkeys) {
|
|
475
|
-
console.log(`\t${passkey.id}: created ${formatDateRange(passkey.createdAt).padEnd(40)} used ${passkey.counter} times. ${passkey.deviceType}, ${passkey.backedUp ? '' : 'not '}backed up; transports are [${passkey.transports.join(', ')}], ${passkey.name ? 'named ' + JSON.stringify(passkey.name) : 'unnamed'}.`);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
});
|
|
479
|
-
program
|
|
480
|
-
.command('toggle-admin')
|
|
481
|
-
.description('Toggle whether a user is an administrator')
|
|
482
|
-
.addArgument(lookup)
|
|
483
|
-
.action(async (_user) => {
|
|
484
|
-
const user = await _user;
|
|
485
|
-
const isAdmin = !user.isAdmin;
|
|
486
|
-
await db.database.updateTable('users').set({ isAdmin }).where('id', '=', user.id).executeTakeFirstOrThrow();
|
|
487
|
-
await audit('admin_change', undefined, { user: user.id });
|
|
488
|
-
console.log(`${userText(user)} is ${isAdmin ? 'now' : 'no longer'} an administrator. (${styleText(['whiteBright', 'bold'], isAdmin.toString())})`);
|
|
489
|
-
});
|
|
490
|
-
program
|
|
491
|
-
.command('status')
|
|
492
|
-
.alias('stats')
|
|
493
|
-
.description('Get information about the server')
|
|
494
|
-
.addOption(opts.host)
|
|
495
|
-
.action(async () => {
|
|
496
|
-
console.log('Axium Server v' + $pkg.version);
|
|
497
|
-
console.log(styleText('whiteBright', 'Debug mode:'), config.debug ? styleText('yellow', 'enabled') : 'disabled');
|
|
498
|
-
const configFiles = config.files.keys().toArray();
|
|
499
|
-
console.log(styleText('whiteBright', 'Loaded config files:'), styleText(['dim', 'bold'], `(${configFiles.length})`), configFiles.join(', '));
|
|
500
|
-
outputDaemonStatus('axium');
|
|
501
|
-
process.stdout.write(styleText('whiteBright', 'Database: '));
|
|
502
|
-
try {
|
|
503
|
-
console.log(await db.statText());
|
|
504
|
-
}
|
|
505
|
-
catch {
|
|
506
|
-
console.log(styleText('red', 'Unavailable'));
|
|
507
|
-
}
|
|
508
|
-
console.log(styleText('whiteBright', 'Loaded plugins:'), styleText(['dim', 'bold'], `(${plugins.size || 'none'})`), Array.from(plugins.keys()).join(', '));
|
|
509
|
-
for (const plugin of plugins.values()) {
|
|
510
|
-
if (!plugin._hooks?.statusText)
|
|
511
|
-
continue;
|
|
512
|
-
const text = await plugin._hooks?.statusText();
|
|
513
|
-
console.log(styleText('bold', plugin.name), plugin.version + ':', text.includes('\n') ? '\n' + text : text);
|
|
514
|
-
}
|
|
515
|
-
});
|
|
516
|
-
program
|
|
517
|
-
.command('ports')
|
|
518
|
-
.description('Enable or disable use of restricted ports (e.g. 443)')
|
|
519
|
-
.addArgument(new Argument('<action>', 'The action to take').choices(_portActions))
|
|
520
|
-
.addOption(new Option('-m, --method <method>', 'the method to use').choices(_portMethods).default('node-cap'))
|
|
521
|
-
.option('-N, --node <path>', 'the path to the node binary')
|
|
522
|
-
.action(async (action, opt) => {
|
|
523
|
-
await restrictedPorts({ ...opt, action }).catch(io.handleError);
|
|
524
|
-
});
|
|
525
|
-
program
|
|
526
|
-
.command('init')
|
|
527
|
-
.description('Install Axium server')
|
|
528
|
-
.addOption(opts.force)
|
|
529
|
-
.addOption(opts.host)
|
|
530
|
-
.addOption(opts.check)
|
|
531
|
-
.addOption(opts.packagesDir)
|
|
532
|
-
.option('-s, --skip', 'Skip already initialized steps')
|
|
533
|
-
.action(async (opt) => {
|
|
534
|
-
await db.init(opt).catch(io.handleError);
|
|
535
|
-
await restrictedPorts({ method: 'node-cap', action: 'enable' }).catch(io.handleError);
|
|
536
|
-
});
|
|
537
|
-
program
|
|
538
|
-
.command('serve')
|
|
539
|
-
.description('Start the Axium server')
|
|
540
|
-
.option('-p, --port <port>', 'the port to listen on')
|
|
541
|
-
.option('--ssl <prefix>', 'the prefix for the cert.pem and key.pem SSL files')
|
|
542
|
-
.option('-b, --build <path>', 'the path to the handler build')
|
|
543
|
-
.action(async (opt) => {
|
|
544
|
-
const server = await serve({
|
|
545
|
-
secure: opt.ssl ? true : config.web.secure,
|
|
546
|
-
ssl_cert: opt.ssl ? join(opt.ssl, 'cert.pem') : config.web.ssl_cert,
|
|
547
|
-
ssl_key: opt.ssl ? join(opt.ssl, 'key.pem') : config.web.ssl_key,
|
|
548
|
-
build: opt.build ? resolve(opt.build) : config.web.build,
|
|
549
|
-
});
|
|
550
|
-
const port = !Number.isNaN(Number.parseInt(opt.port ?? 'NaN')) ? Number.parseInt(opt.port) : config.web.port;
|
|
551
|
-
server.listen(port, () => {
|
|
552
|
-
console.log('Server is listening on port ' + port);
|
|
553
|
-
});
|
|
554
|
-
});
|
|
555
|
-
program
|
|
556
|
-
.command('link')
|
|
557
|
-
.description('Link routes provided by plugins and the server')
|
|
558
|
-
.addOption(opts.packagesDir)
|
|
559
|
-
.addOption(new Option('-l, --list', 'list route links').conflicts('delete'))
|
|
560
|
-
.option('-d, --delete', 'delete route links')
|
|
561
|
-
.argument('[name...]', 'List of plugin names to operate on. If not specified, operates on all plugins and built-in routes.')
|
|
562
|
-
.action(async function (names) {
|
|
563
|
-
const opt = this.optsWithGlobals();
|
|
564
|
-
if (names.length)
|
|
565
|
-
opt.only = names;
|
|
566
|
-
if (opt.list) {
|
|
567
|
-
for (const link of listRouteLinks(opt)) {
|
|
568
|
-
const idText = link.id.startsWith('#') ? `(${link.id.slice(1)})` : link.id;
|
|
569
|
-
const fromColor = await access(link.from)
|
|
570
|
-
.then(() => 'cyanBright')
|
|
571
|
-
.catch(() => 'redBright');
|
|
572
|
-
console.log(`${idText}:\t ${styleText(fromColor, link.from)}\t->\t${link.to.replace(/.*\/node_modules\//, styleText('dim', '$&'))}`);
|
|
573
|
-
}
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
if (opt.delete) {
|
|
577
|
-
unlinkRoutes(opt);
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
linkRoutes(opt);
|
|
581
|
-
});
|
|
582
|
-
program
|
|
583
|
-
.command('audit')
|
|
584
|
-
.description('View audit logs')
|
|
585
|
-
.option('-x, --extra', 'Include the extra object when listing events')
|
|
586
|
-
.option('-t, --include-tags', 'Include tags when listing events')
|
|
587
|
-
.addOption(new Option('-s, --summary', 'Summarize audit log entries instead of displaying individual ones').conflicts(['extra', 'includeTags']))
|
|
588
|
-
.optionsGroup('Filters:')
|
|
589
|
-
.option('--since <date>', 'Filter for events since a date')
|
|
590
|
-
.option('--until <date>', 'Filter for events until a date')
|
|
591
|
-
.option('--user <uuid|null>', 'Filter for events triggered by a user')
|
|
592
|
-
.addOption(new Option('--severity <level>', 'Filter for events at or above a severity level').choices(severityNames))
|
|
593
|
-
.option('--source <source>', 'Filter by source')
|
|
594
|
-
.option('--tag <tag...>', 'Filter by tag(s)')
|
|
595
|
-
.option('--event <event>', 'Filter by event name')
|
|
596
|
-
.action(async (opt) => {
|
|
597
|
-
const filter = await AuditFilter.parseAsync(opt).catch(e => io.exit('Invalid filter: ' + z.prettifyError(e)));
|
|
598
|
-
const events = await getEvents(filter).execute();
|
|
599
|
-
if (opt.summary) {
|
|
600
|
-
const groups = Object.groupBy(events, e => e.severity);
|
|
601
|
-
const maxGroupLength = Math.max(...Object.values(groups).map(g => g.length.toString().length), 0);
|
|
602
|
-
for (const [severity, group] of Object.entries(groups)) {
|
|
603
|
-
if (!group?.length)
|
|
604
|
-
continue;
|
|
605
|
-
console.log(styleText('white', group.length.toString().padStart(maxGroupLength)), styleSeverity(severity, true), 'events. Latest occurred', group.at(-1).timestamp.toLocaleString());
|
|
606
|
-
}
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
let maxSource = 0, maxName = 0, maxTags = 0, maxExtra = 0;
|
|
610
|
-
for (const event of events) {
|
|
611
|
-
maxSource = Math.max(maxSource, event.source.length);
|
|
612
|
-
maxName = Math.max(maxName, event.name.length);
|
|
613
|
-
event._tags = !event.tags.length
|
|
614
|
-
? ''
|
|
615
|
-
: opt.includeTags
|
|
616
|
-
? '# ' + event.tags.join(', ')
|
|
617
|
-
: `(${event.tags.length} tag${event.tags.length == 1 ? '' : 's'})`;
|
|
618
|
-
maxTags = Math.max(maxTags, event._tags.length);
|
|
619
|
-
const extraKeys = Object.keys(event.extra);
|
|
620
|
-
event._extra = !extraKeys.length ? '' : opt.extra ? JSON.stringify(event.extra) : '+' + extraKeys.length;
|
|
621
|
-
maxExtra = Math.max(maxExtra, event._extra.length);
|
|
622
|
-
}
|
|
623
|
-
for (const event of events) {
|
|
624
|
-
console.log(styleSeverity(event.severity, true), styleText('dim', io.prettyDate(event.timestamp)), event.source.padEnd(maxSource), styleText('whiteBright', event.name.padEnd(maxName)), styleText('gray', event._tags.padEnd(maxTags)), 'by', event.userId ? event.userId : styleText(['dim', 'italic'], 'unknown'.padEnd(36)), styleText('blue', event._extra));
|
|
625
|
-
}
|
|
626
|
-
});
|
|
627
|
-
await program.parseAsync();
|
|
628
|
-
}
|
|
629
|
-
catch (e_1) {
|
|
630
|
-
env_1.error = e_1;
|
|
631
|
-
env_1.hasError = true;
|
|
632
|
-
}
|
|
633
|
-
finally {
|
|
634
|
-
__disposeResources(env_1);
|
|
635
|
-
}
|
package/dist/config.js
CHANGED
|
@@ -228,8 +228,11 @@ export async function loadConfig(path, options = {}) {
|
|
|
228
228
|
io.debug('Loaded config: ' + path);
|
|
229
229
|
for (const include of file.include ?? [])
|
|
230
230
|
await loadConfig(resolve(dirname(path), include), { ...options, optional: true, _markIncluded: true });
|
|
231
|
-
for (const
|
|
232
|
-
await loadPlugin('server',
|
|
231
|
+
for (const pluginPath of file.plugins ?? []) {
|
|
232
|
+
const plugin = await loadPlugin('server', pluginPath, path, options.safe);
|
|
233
|
+
if (!plugin)
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
233
236
|
}
|
|
234
237
|
export async function loadDefaultConfigs(safe = false) {
|
|
235
238
|
for (const path of findConfigPaths()) {
|
|
@@ -277,9 +280,7 @@ export function saveConfigTo(path, changed) {
|
|
|
277
280
|
*/
|
|
278
281
|
export function findConfigPaths() {
|
|
279
282
|
const paths = dirs.map(dir => join(dir, 'config.json'));
|
|
280
|
-
if (process.env.AXIUM_CONFIG)
|
|
283
|
+
if (process.env.AXIUM_CONFIG && !paths.includes(process.env.AXIUM_CONFIG))
|
|
281
284
|
paths.push(process.env.AXIUM_CONFIG);
|
|
282
285
|
return paths;
|
|
283
286
|
}
|
|
284
|
-
if (process.env.AXIUM_CONFIG)
|
|
285
|
-
await loadConfig(process.env.AXIUM_CONFIG);
|