New cmd.js for hardcore users

This commit is contained in:
Chrystian Huot 2021-06-01 15:27:50 -04:00
parent 9624914cef
commit 9881cce647
2 changed files with 486 additions and 0 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
.DS_Store
.vscode
*.token
/dist

485
cmd.js Executable file
View file

@ -0,0 +1,485 @@
#!/usr/bin/env node
/*
* *****************************************************************************
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
'use strict';
import fs from 'fs';
import http from 'http';
import https from 'https';
import path from 'path';
const APP_NAME = path.parse(process.argv[1]).base;
const ARGUMENT_CODE = '--code';
const ARGUMENT_IDENT = '--ident';
const ARGUMENT_IN = '--in';
const ARGUMENT_OUT = '--out';
const ARGUMENT_PASSWORD = '--password';
const ARGUMENT_TOKEN = '--token';
const ARGUMENT_URL = '--url';
const COMMAND_CONFIG_GET = 'config-get';
const COMMAND_CONFIG_SET = 'config-set';
const COMMAND_HELP = 'help';
const COMMAND_LOGIN = 'login';
const COMMAND_LOGOUT = 'logout';
const COMMAND_USER_ADD = 'user-add';
const COMMAND_USER_REMOVE = 'user-remove';
const DEFAULT_ADMIN_PASSWORD = 'rdio-scanner';
const DEFAULT_URL = 'http://localhost:3000/';
const DEFAULT_TOKEN_FILE = `.${path.parse(process.argv[1]).name}.token`;
const PADDING = 11;
const URL_API = '/api/admin';
const URL_CONFIG = `${URL_API}/config`;
const URL_LOGIN = `${URL_API}/login`;
const URL_LOGOUT = `${URL_API}/logout`;
let tokenFile = DEFAULT_TOKEN_FILE;
export class Cmd {
constructor() {
this.command = null;
this.inputFile = null;
this.outputFile = null;
this.password = process.env.RDIO_ADMIN_PASSWORD || DEFAULT_ADMIN_PASSWORD;
this.token = null;
this.url = new URL(DEFAULT_URL);
const args = process.argv.slice(2).reduce((p, c) => p.concat(c.split(/(--\w+)=(\S+)/).filter((f) => f)), []);
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case COMMAND_CONFIG_GET:
case COMMAND_CONFIG_SET:
case COMMAND_HELP:
case COMMAND_LOGIN:
case COMMAND_LOGOUT:
case COMMAND_USER_ADD:
case COMMAND_USER_REMOVE:
this.command = args[i];
break;
case ARGUMENT_CODE:
this.code = args[++i];
break;
case ARGUMENT_IDENT:
this.ident = args[++i];
break;
case ARGUMENT_IN:
this.inputFile = args[++i];
break;
case ARGUMENT_OUT:
this.outputFile = args[++i];
break;
case ARGUMENT_PASSWORD:
this.password = args[++i];
break;
case ARGUMENT_TOKEN:
tokenFile = args[++i];
break;
case ARGUMENT_URL:
try {
this.url = new URL(args[++i]);
} catch (error) {
exitWithError('Invalid URL');
}
if (!/^https?:/i.test(this.url)) {
exitWithError('Invalid URL');
}
break;
}
}
this.token = readTokenFile();
switch (this.command) {
case COMMAND_CONFIG_GET:
this.configGet();
break;
case COMMAND_CONFIG_SET:
this.configSet();
break;
case COMMAND_LOGIN:
this.login();
break;
case COMMAND_LOGOUT:
this.logout();
break;
case COMMAND_USER_ADD:
this.userAdd();
break;
case COMMAND_USER_REMOVE:
this.userRemove();
break;
default:
this.usage();
}
}
async configGet() {
if (!this.outputFile) {
exitWithError('Missing --out <file.json> arguments.');
return;
}
const res = await httpGet(new URL(URL_CONFIG, this.url), this.token);
if (res.statusCode === 200) {
saveFile(this.outputFile, JSON.stringify(res.data.config, null, 2));
exitWithMessage(`Server's configuration saved to ${this.outputFile}`);
} else {
exitWithError(res.error || res.data);
}
}
async configSet() {
if (!this.inputFile) {
exitWithError('Missing --in <file.json> arguments.');
return;
}
let config = readFile(this.inputFile);
try {
config = JSON.parse(config);
} catch (error) {
exitWithError(error.message);
return;
}
const res = await httpPut(new URL(URL_CONFIG, this.url), config, this.token);
if (res.statusCode === 200) {
exitWithMessage(`Server's configuration applied from file ${this.inputFile}`);
} else {
exitWithError(res.error || res.data);
}
}
async login() {
const res = await httpPost(new URL(URL_LOGIN, this.url), { password: this.password });
if (res.statusCode === 200) {
this.token = res.data.token;
saveTokenFile(this.token);
exitWithMessage('Logged in.');
} else {
exitWithError(res.error || res.data);
}
}
async logout() {
const res = await httpPost(new URL(URL_LOGOUT, this.url), null, this.token);
this.token = null;
saveTokenFile(this.token);
if (res.statusCode === 200) {
exitWithMessage('Logged out.');
} else {
exitWithError(res.error || res.data);
}
}
usage() {
exitWithMessage([
'Available Commands:',
` ${COMMAND_CONFIG_GET.padEnd(PADDING)} Retrieve server's configuration.`,
'',
` ${''.padStart(PADDING)} $ ./${APP_NAME} ${COMMAND_CONFIG_GET} ${ARGUMENT_OUT} <file.json>`,
'',
` ${COMMAND_CONFIG_SET.padEnd(PADDING)} Set server's configuration.`,
'',
` ${''.padStart(PADDING)} $ ./${APP_NAME} ${COMMAND_CONFIG_SET} ${ARGUMENT_IN} <file.json>`,
'',
` ${COMMAND_LOGIN.padEnd(PADDING)} Login to administrative backend.`,
'',
` ${''.padStart(PADDING)} $ RDIO_ADMIN_PASSWORD=<password> ./${APP_NAME} ${COMMAND_LOGIN}`,
` ${''.padStart(PADDING)} $ ./${APP_NAME} ${COMMAND_LOGIN} ${ARGUMENT_PASSWORD} <password>`,
'',
` ${COMMAND_LOGOUT.padEnd(PADDING)} Logout from administrative backend.`,
'',
` ${COMMAND_USER_ADD.padEnd(PADDING)} Add a user access.`,
'',
` ${''.padStart(PADDING)} $ ./${APP_NAME} ${COMMAND_USER_ADD} ${ARGUMENT_IDENT} <ident> ${ARGUMENT_CODE} <code>`,
'',
` ${COMMAND_USER_REMOVE.padEnd(PADDING)} Remove a user access.`,
'',
` ${''.padStart(PADDING)} $ ./${APP_NAME} ${COMMAND_USER_REMOVE} ${ARGUMENT_IDENT} <ident>`,
'',
'Global options:',
` ${ARGUMENT_TOKEN.padEnd(PADDING)} Session token keystore. Default is '${DEFAULT_TOKEN_FILE}'.`,
` ${ARGUMENT_URL.padEnd(PADDING)} Server remote address. Default is '${DEFAULT_URL}'.`,
'',
].join('\n'));
}
async userAdd() {
if (!this.code || !this.ident) {
exitWithError('Missing --ident <ident> or --code <code> arguments.');
return;
}
let res = await httpGet(new URL(URL_CONFIG, this.url), this.token);
if (res.statusCode !== 200) {
exitWithError(res.error || res.data);
return;
}
const config = res.data.config;
config.access = config.access.filter((u) => u.ident.toLowerCase() !== this.ident.toLowerCase());
config.access = config.access.concat({
code: this.code,
ident: this.ident,
systems: '*',
});
res = await httpPut(new URL(URL_CONFIG, this.url), config, this.token);
if (res.statusCode === 200) {
exitWithMessage(`User ${this.ident} added.`);
} else {
exitWithError(res.error || res.data);
}
}
async userRemove() {
if (!this.ident) {
exitWithError('Missing --ident <ident> arguments.');
return;
}
let res = await httpGet(new URL(URL_CONFIG, this.url), this.token);
if (res.statusCode !== 200) {
exitWithError(res.error || res.data);
return;
}
const config = res.data.config;
if (config.access.findIndex((u) => u.ident.toLowerCase() === this.ident.toLowerCase()) === -1) {
exitWithError(`Unknown user ${this.ident}`);
return;
}
config.access = config.access.filter((u) => u.ident.toLowerCase() !== this.ident.toLowerCase());
res = await httpPut(new URL(URL_CONFIG, this.url), config, this.token);
if (res.statusCode === 200) {
exitWithMessage(`User ${this.ident} removed.`);
} else {
exitWithError(res.error || res.data);
}
}
}
function exitWithError(error) {
process.stdout.write(`ERROR: ${error}\n`);
process.exit(1);
}
function exitWithMessage(message) {
process.stdout.write(`${message}\n`);
process.exit(0);
}
async function httpGet(url, autorization) {
return new Promise((resolve) => {
const result = { data: null, error: null, statusCode: null };
const options = autorization ? { headers: { Authorization: autorization } } : {};
try {
const req = (url.protocol === 'https:' ? https : http).get(url, options, (res) => {
result.statusCode = res.statusCode;
res.setEncoding('utf8');
res.on('data', (chunk) => result.data = (result.data || '') + chunk);
res.on('end', () => {
try {
result.data = JSON.parse(result.data);
} catch (error) { }
resolve(result);
});
});
req.on('error', (error) => {
result.error = error.code;
resolve(result);
});
} catch (error) {
result.error = error.message;
resolve(result);
}
});
}
async function httpPost(url, postData, autorization) {
return new Promise((resolve) => {
const result = { data: null, error: null, statusCode: null };
postData = JSON.stringify(postData || {});
const options = {
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData),
},
method: 'POST',
};
if (autorization) {
options.headers.Authorization = autorization;
}
try {
const req = (url.protocol === 'https:' ? https : http).request(url, options, (res) => {
result.statusCode = res.statusCode;
res.setEncoding('utf8');
res.on('data', (chunk) => result.data = (result.data || '') + chunk);
res.on('end', () => {
try {
result.data = JSON.parse(result.data);
} catch (error) { }
resolve(result);
});
});
req.on('error', (error) => {
result.error = error.code;
resolve(result);
});
req.write(postData);
req.end();
} catch (error) {
result.error = error.message;
resolve(result);
}
});
}
async function httpPut(url, putData, autorization) {
return new Promise((resolve) => {
const result = { data: null, error: null, statusCode: null };
putData = JSON.stringify(putData);
const options = {
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(putData),
},
method: 'PUT',
};
if (autorization) {
options.headers.Authorization = autorization;
}
try {
const req = (url.protocol === 'https:' ? https : http).request(url, options, (res) => {
result.statusCode = res.statusCode;
res.setEncoding('utf8');
res.on('data', (chunk) => result.data = (result.data || '') + chunk);
res.on('end', () => {
try {
result.data = JSON.parse(result.data);
} catch (error) { }
resolve(result);
});
});
req.on('error', (error) => {
result.error = error.code;
resolve(result);
});
req.write(putData);
req.end();
} catch (error) {
result.error = error.message;
resolve(result);
}
});
}
function readFile(file) {
if (fs.existsSync(file)) {
try {
return fs.readFileSync(file);
} catch (error) {
exitWithError(error.message);
}
} else {
exitWithError(`Inexistent token keystore file ${file}`);
}
}
function readTokenFile() {
return readFile(tokenFile);
}
function saveFile(file, data) {
try {
fs.writeFileSync(file, data || '');
} catch (error) {
exitWithError(error.message);
}
}
function saveTokenFile(token) {
saveFile(tokenFile, token);
}
new Cmd();