@mountainpass/hooked-cli
Version:
A tool for runnable scripts
169 lines (168 loc) • 6.84 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import bcrypt from './bcrypt.js';
import cors from 'cors';
import express from 'express';
import fsPromise from 'fs/promises';
import http from 'http';
import https from 'https';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { isDefined, isString } from '../types.js';
import logger from '../utils/logger.js';
import { findFileInAncestors } from '../utils/packageJson.js';
import jwt from './auth/jwt.js';
import router, { HttpError } from './router.js';
const corsOptions = {
credentials: true,
origin: (origin, callback) => callback(null, true)
// origin: function(origin: any, callback: any) {
// const originIsSame = typeof origin === 'undefined'
// if (originIsSame || allowlist.indexOf(origin) !== -1) {
// callback(null, true)
// } else {
// callback(new Error(`Domain not allowed by CORS - ${origin}`))
// }
// }
};
/**
* Starts the REST API server.
* @param port
* @param options
* @param config
* @param envVars
* @param env
*/
const startServer = (port, systemProcessEnvs, options, config, serverShutdownSignal) => __awaiter(void 0, void 0, void 0, function* () {
if (serverShutdownSignal.aborted) {
logger.info(`Server startup aborted. Reason=${serverShutdownSignal.reason}`);
return;
}
// determine public path
const dirname = path.dirname(fileURLToPath(import.meta.url));
const publicPath = findFileInAncestors(dirname, 'public', false);
logger.debug(`Serving static files from: ${publicPath}`);
// setup express
const app = express();
app.enable('trust proxy');
app.set('etag', 'strong');
app.use(cors(corsOptions));
jwt.initialise(app,
// authenticator
(auth) => {
var _a;
const { username, password } = auth;
// encrypt and compare the password
const user = ((_a = config.users) !== null && _a !== void 0 ? _a : []).find((user) => user.username === username);
if (isDefined(user)) {
logger.debug(`Authenticating user: ${username}`);
if (bcrypt.compare(password, user.password)) {
const { username, accessRoles = [] } = user;
return { username, accessRoles };
}
else {
logger.info(`Invalid password for user: ${username} pw: ${bcrypt.hash(password, config.auth.salt)}`);
}
}
else {
logger.info(`Rejecting unknown user: ${username}`);
}
},
// authoriser
(jwtPayload) => {
var _a;
// logger.info('Authorising user - ' + JSON.stringify(jwtPayload))
const { username, accessRoles } = jwtPayload;
// verify user exists
const user = ((_a = config.users) !== null && _a !== void 0 ? _a : []).find((user) => user.username === username);
if (isDefined(user)) {
// logger.info(`Authorised user: ${username}`)
return { username, accessRoles };
}
else {
logger.info(`Rejected user: ${username}`);
}
});
app.get('/api/status', (req, res) => res.json({ status: 'ok' }));
// auth by apikey or jwt
app.use('/api', (req, res, next) => {
if (isString(options.apiKey) && req.header('authorization') === `Bearer ${options.apiKey}`) {
next();
}
else {
jwt.requireSigninMiddleware(req, res, next);
}
});
app.use('/api', yield router.router(systemProcessEnvs, options));
app.use('/', express.static(publicPath));
app.get('*', (req, res) => {
let httpPath = req.path;
if (httpPath.endsWith('/'))
httpPath = httpPath.slice(0, -1);
const target = path.join(publicPath, httpPath + '.html');
const relative = path.relative(publicPath, target);
const isSubdir = isString(relative) && !relative.startsWith('..') && !path.isAbsolute(relative);
if (isSubdir && fs.existsSync(target)) {
res.sendFile(target);
}
else {
res.sendStatus(403);
}
});
app.use((err, req, res, next) => {
console.error(err.stack);
if (err instanceof HttpError) {
res.status(err.statusCode).json({ message: err.message }).end();
}
else {
res.status(500).json({ message: err.message }).end();
}
});
// ssl setup
if ((isString(options.sslCert) && !isString(options.sslKey)) || (!isString(options.sslCert) && isString(options.sslKey))) {
throw new Error('sslKey and sslCert must both be provided together.');
}
const isHttps = isString(options.sslCert) && isString(options.sslKey);
const server = isHttps
? https.createServer({
cert: yield fsPromise.readFile(options.sslCert, 'utf-8'),
key: yield fsPromise.readFile(options.sslKey, 'utf-8')
}, app)
: http.createServer(app);
// check again if we should abort, before starting up the server...
if (serverShutdownSignal.aborted) {
logger.info(`Server startup aborted. Reason=${serverShutdownSignal.reason}`);
return;
}
// listen
server.listen(port, () => {
const apiKey = options.apiKey;
const toggles = `api-key=${isString(apiKey) ? apiKey.substring(0, 1).padEnd(apiKey.length, '*') : '🙅'}, ssl=${isHttps ? '✅' : '🙅'}`;
logger.info(`Server listening: ${isHttps ? 'https' : 'http'}://localhost:${port} (${toggles})`);
});
const shutdownServer = () => {
if (server.listening) {
logger.debug('Server shutting down...');
server.close((err) => {
if (err != null) {
console.error(`Error stopping server: ${err.message}`);
process.exit(1);
}
else {
logger.debug('Server shutdown successfully.');
}
});
}
};
process.on('SIGTERM', shutdownServer);
serverShutdownSignal.addEventListener('abort', shutdownServer, { once: true });
});
export default { startServer };