UNPKG

@mountainpass/hooked-cli

Version:
169 lines (168 loc) 6.84 kB
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 };