"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.startServer = void 0;
const express_1 = __importDefault(require("express"));
const connect_history_api_fallback_1 = __importDefault(require("connect-history-api-fallback"));
const cors_1 = __importDefault(require("cors"));
const csurf_1 = __importDefault(require("csurf"));
const morgan_1 = __importDefault(require("morgan"));
const store_1 = __importDefault(require("./config/store"));
const Gateway_1 = __importStar(require("./lib/Gateway"));
const jsonStore_1 = __importDefault(require("./lib/jsonStore"));
const loggers = __importStar(require("./lib/logger"));
const MqttClient_1 = __importDefault(require("./lib/MqttClient"));
const SocketManager_1 = __importDefault(require("./lib/SocketManager"));
const ZwaveClient_1 = __importDefault(require("./lib/ZwaveClient"));
const multer_1 = __importStar(require("multer"));
const extract_zip_1 = __importDefault(require("extract-zip"));
const server_1 = require("@zwave-js/server");
const archiver_1 = __importDefault(require("archiver"));
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
const express_session_1 = __importDefault(require("express-session"));
const fs_extra_1 = __importStar(require("fs-extra"));
const http_1 = require("http");
const https_1 = require("https");
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
const path_1 = __importDefault(require("path"));
const session_file_store_1 = __importDefault(require("session-file-store"));
const util_1 = require("util");
const zwave_js_1 = require("zwave-js");
const app_1 = require("./config/app");
const CustomPlugin_1 = require("./lib/CustomPlugin");
const SocketEvents_1 = require("./lib/SocketEvents");
const utils = __importStar(require("./lib/utils"));
const BackupManager_1 = __importDefault(require("./lib/BackupManager"));
const promises_1 = require("fs/promises");
const selfsigned_1 = require("selfsigned");
const ZnifferManager_1 = __importDefault(require("./lib/ZnifferManager"));
const core_1 = require("@zwave-js/core");
const createCertificate = (0, util_1.promisify)(selfsigned_1.generate);
function multerPromise(m, req, res) {
    return new Promise((resolve, reject) => {
        m(req, res, (err) => {
            if (err) {
                reject(err);
            }
            else {
                resolve();
            }
        });
    });
}
const Storage = (0, multer_1.diskStorage)({
    async destination(reqD, file, callback) {
        await (0, fs_extra_1.mkdirp)(app_1.tmpDir);
        callback(null, app_1.tmpDir);
    },
    filename(reqF, file, callback) {
        callback(null, file.originalname);
    },
});
const multerUpload = (0, multer_1.default)({
    storage: Storage,
}).array('upload', 1); // Field name and max count
const FileStore = (0, session_file_store_1.default)(express_session_1.default);
const app = (0, express_1.default)();
const logger = loggers.module('App');
const verifyJWT = (0, util_1.promisify)(jsonwebtoken_1.default.verify.bind(jsonwebtoken_1.default));
const storeLimiter = (0, express_rate_limit_1.default)({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100,
    handler: function (req, res) {
        res.json({
            success: false,
            message: 'Request limit reached. You can make only 100 requests every 15 minutes',
        });
    },
});
const loginLimiter = (0, express_rate_limit_1.default)({
    windowMs: 60 * 60 * 1000, // keep in memory for 1 hour
    max: 5, // start blocking after 5 requests
    handler: function (req, res) {
        res.json({ success: false, message: 'Max requests limit reached' });
    },
});
const apisLimiter = (0, express_rate_limit_1.default)({
    windowMs: 60 * 60 * 1000, // keep in memory for 1 hour
    max: 500, // start blocking after 500 requests
    handler: function (req, res) {
        res.json({ success: false, message: 'Max requests limit reached' });
    },
});
function sslDisabled() {
    return process.env.FORCE_DISABLE_SSL === 'true';
}
// apis response codes
var RESPONSE_CODES;
(function (RESPONSE_CODES) {
    RESPONSE_CODES["OK"] = "OK";
    RESPONSE_CODES["GENERAL_ERROR"] = "General Error";
    RESPONSE_CODES["INVALID"] = "Invalid data";
    RESPONSE_CODES["AUTH_FAILED"] = "Authentication failed";
    RESPONSE_CODES["PERMISSION_ERROR"] = "Insufficient permissions";
})(RESPONSE_CODES || (RESPONSE_CODES = {}));
const socketManager = new SocketManager_1.default();
socketManager.authMiddleware = function (socket, next) {
    if (!isAuthEnabled()) {
        next();
    }
    else if (socket.handshake.query && socket.handshake.query.token) {
        jsonwebtoken_1.default.verify(socket.handshake.query.token, app_1.sessionSecret, function (err, decoded) {
            if (err)
                return next(new Error('Authentication error'));
            socket.user = decoded;
            next();
        });
    }
    else {
        next(new Error('Authentication error'));
    }
};
let gw; // the gateway instance
let zniffer; // the zniffer instance
const plugins = [];
let pluginsRouter;
// flag used to prevent multiple restarts while one is already in progress
let restarting = false;
// ### UTILS
/**
 * Start http/https server and all the manager
 */
async function startServer(port, host) {
    let server;
    const settings = jsonStore_1.default.get(store_1.default.settings);
    // as the really first thing setup loggers so all logs will go to file if specified in settings
    setupLogging(settings);
    const httpsEnabled = process.env.HTTPS || settings?.gateway?.https;
    if (httpsEnabled) {
        if (!sslDisabled()) {
            logger.info('HTTPS is enabled. Loading cert and keys');
            const { cert, key } = await loadCertKey();
            if (cert && key) {
                server = (0, https_1.createServer)({
                    key,
                    cert,
                    rejectUnauthorized: false,
                }, app);
            }
            else {
                logger.warn('HTTPS is enabled but cert or key cannot be generated. Falling back to HTTP');
            }
        }
        else {
            logger.warn('HTTPS enabled but FORCE_DISABLE_SSL env var is set. Falling back to HTTP');
        }
    }
    if (!server) {
        server = (0, http_1.createServer)(app);
    }
    server.listen(port, host, function () {
        const addr = server.address();
        const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr?.port;
        logger.info(`Listening on ${bind}${host ? 'host ' + host : ''} protocol ${httpsEnabled ? 'HTTPS' : 'HTTP'}`);
    });
    server.on('error', function (error) {
        if (error.syscall !== 'listen') {
            throw error;
        }
        const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port;
        // handle specific listen errors with friendly messages
        switch (error.code) {
            case 'EACCES':
                logger.error(bind + ' requires elevated privileges');
                process.exit(1);
                break;
            case 'EADDRINUSE':
                logger.error(bind + ' is already in use');
                process.exit(1);
                break;
            default:
                throw error;
        }
    });
    const users = jsonStore_1.default.get(store_1.default.users);
    if (users.length === 0) {
        users.push({
            username: app_1.defaultUser,
            passwordHash: await utils.hashPsw(app_1.defaultPsw),
        });
        await jsonStore_1.default.put(store_1.default.users, users);
    }
    setupSocket(server);
    setupInterceptor();
    await loadSnippets();
    startZniffer(settings.zniffer);
    await startGateway(settings);
}
exports.startServer = startServer;
const defaultSnippets = [];
async function loadSnippets() {
    const localSnippetsDir = utils.joinPath(false, 'snippets');
    await (0, fs_extra_1.mkdirp)(app_1.snippetsDir);
    const files = await (0, fs_extra_1.readdir)(localSnippetsDir);
    for (const file of files) {
        const filePath = path_1.default.join(localSnippetsDir, file);
        if (await isSnippet(filePath)) {
            const content = await (0, promises_1.readFile)(filePath, 'utf8');
            const name = path_1.default.basename(filePath, '.js');
            defaultSnippets.push({ name, content });
        }
    }
}
async function isSnippet(file) {
    return (await (0, fs_extra_1.stat)(file)).isFile() && file.endsWith('.js');
}
async function getSnippets() {
    const files = await (0, fs_extra_1.readdir)(app_1.snippetsDir);
    const snippets = [];
    for (const file of files) {
        const filePath = path_1.default.join(app_1.snippetsDir, file);
        if (await isSnippet(filePath)) {
            snippets.push({
                name: file.replace('.js', ''),
                content: await (0, promises_1.readFile)(filePath, 'utf8'),
            });
        }
    }
    const snippetsCache = gw.zwave?.cacheSnippets ?? [];
    return [...snippetsCache, ...defaultSnippets, ...snippets];
}
/**
 * Get the `path` param from a request. Throws if the path is not safe
 */
function getSafePath(req) {
    let reqPath = typeof req === 'string' ? req : req.query.path;
    if (typeof reqPath !== 'string') {
        throw Error('Invalid path');
    }
    reqPath = path_1.default.normalize(reqPath);
    if (!reqPath.startsWith(app_1.storeDir) || reqPath === app_1.storeDir) {
        throw Error('Path not allowed');
    }
    return reqPath;
}
async function loadCertKey() {
    const certFile = process.env.SSL_CERTIFICATE || utils.joinPath(app_1.storeDir, 'cert.pem');
    const keyFile = process.env.SSL_KEY || utils.joinPath(app_1.storeDir, 'key.pem');
    let key;
    let cert;
    try {
        cert = await fs_extra_1.default.readFile(certFile, 'utf8');
        key = await fs_extra_1.default.readFile(keyFile, 'utf8');
    }
    catch (error) {
        // noop
    }
    if (!cert || !key) {
        logger.info('Cert and key not found in store, generating fresh new ones...');
        try {
            const result = await createCertificate([], {
                days: 99999,
                keySize: 2048,
            });
            key = result.private;
            cert = result.cert;
            await fs_extra_1.default.writeFile(utils.joinPath(app_1.storeDir, 'key.pem'), key);
            await fs_extra_1.default.writeFile(utils.joinPath(app_1.storeDir, 'cert.pem'), cert);
            logger.info('New cert and key created');
        }
        catch (error) {
            logger.error('Error creating cert and key for HTTPS', error);
        }
    }
    return { cert, key };
}
function setupLogging(settings) {
    loggers.setupAll(settings ? settings.gateway : null);
}
async function startGateway(settings) {
    let mqtt;
    let zwave;
    if (isAuthEnabled() &&
        app_1.sessionSecret === 'DEFAULT_SESSION_SECRET_CHANGE_ME') {
        logger.error('Session secret is the default one. For security reasons you should change it by using SESSION_SECRET env var');
    }
    if (settings.mqtt) {
        mqtt = new MqttClient_1.default(settings.mqtt);
    }
    if (settings.zwave) {
        zwave = new ZwaveClient_1.default(settings.zwave, socketManager.io);
    }
    BackupManager_1.default.init(zwave);
    gw = new Gateway_1.default(settings.gateway, zwave, mqtt);
    await gw.start();
    const pluginsConfig = settings.gateway?.plugins ?? null;
    pluginsRouter = express_1.default.Router();
    // load custom plugins
    if (pluginsConfig && Array.isArray(pluginsConfig)) {
        for (const plugin of pluginsConfig) {
            try {
                const pluginName = path_1.default.basename(plugin);
                const pluginsContext = {
                    zwave,
                    mqtt,
                    app: pluginsRouter,
                    logger: loggers.module(pluginName),
                };
                const instance = (0, CustomPlugin_1.createPlugin)(
                // eslint-disable-next-line @typescript-eslint/no-var-requires
                require(plugin), pluginsContext, pluginName);
                plugins.push(instance);
                logger.info(`Successfully loaded plugin ${instance.name}`);
            }
            catch (error) {
                logger.error(`Error while loading ${plugin} plugin`, error);
            }
        }
    }
    restarting = false;
}
function startZniffer(settings) {
    if (settings) {
        zniffer = new ZnifferManager_1.default(settings, socketManager.io);
    }
}
async function destroyPlugins() {
    while (plugins.length > 0) {
        const instance = plugins.pop();
        if (instance && typeof instance.destroy === 'function') {
            logger.info('Closing plugin ' + instance.name);
            await instance.destroy();
        }
    }
}
function setupInterceptor() {
    // intercept logs and redirect them to socket
    loggers.logStream.on('data', (chunk) => {
        socketManager.io.emit(SocketEvents_1.socketEvents.debug, chunk.toString());
    });
}
async function parseDir(dir) {
    const toReturn = [];
    const files = await fs_extra_1.default.readdir(dir);
    for (const file of files) {
        try {
            const entry = {
                name: path_1.default.basename(file),
                path: utils.joinPath(dir, file),
            };
            const stats = await fs_extra_1.default.lstat(entry.path);
            if (stats.isDirectory()) {
                if (entry.path === process.env.ZWAVEJS_EXTERNAL_CONFIG) {
                    // hide config-db
                    continue;
                }
                entry.children = await parseDir(entry.path);
                sortStore(entry.children);
            }
            else {
                entry.ext = file.split('.').pop();
            }
            entry.size = utils.humanSize(stats.size);
            toReturn.push(entry);
        }
        catch (error) {
            logger.error(`Error while parsing ${file} in ${dir}`, error);
        }
    }
    sortStore(toReturn);
    return toReturn;
}
/**
 *
 * Sort children folders first and files after
 */
function sortStore(store) {
    return store.sort((a, b) => {
        if (a.children && !b.children) {
            return -1;
        }
        if (!a.children && b.children) {
            return 1;
        }
        return 0;
    });
}
// ### EXPRESS SETUP
logger.info(`Version: ${utils.getVersion()}`);
logger.info('Application path:' + utils.getPath(true));
if (process.env.TRUST_PROXY) {
    app.set('trust proxy', process.env.TRUST_PROXY === 'true' ? true : process.env.TRUST_PROXY);
}
app.use((0, morgan_1.default)(loggers.disableColors ? 'tiny' : 'dev', {
    stream: { write: (msg) => logger.info(msg.trimEnd()) },
}));
app.use(express_1.default.json({ limit: '50mb' }));
app.use(express_1.default.urlencoded({
    limit: '50mb',
    extended: true,
    parameterLimit: 50000,
}));
// must be placed before history middleware
app.use(function (req, res, next) {
    if (pluginsRouter !== undefined) {
        pluginsRouter(req, res, next);
    }
    else {
        next();
    }
});
app.use((0, connect_history_api_fallback_1.default)({
    index: '/',
}));
// fix back compatibility with old history mode after switching to hash mode
const redirectPaths = [
    '/control-panel',
    '/smart-start',
    '/settings',
    '/scenes',
    '/debug',
    '/store',
    '/mesh',
];
app.use('/', (req, res, next) => {
    if (redirectPaths.includes(req.originalUrl)) {
        // get path when running behind a proxy
        const path = req.header('X-External-Path')?.replace(/\/$/, '') ?? '';
        res.redirect(`${path}/#${req.originalUrl}`);
    }
    else {
        next();
    }
});
app.use('/', express_1.default.static(utils.joinPath(false, 'dist')));
app.use((0, cors_1.default)({ credentials: true, origin: true }));
// enable sessions management
app.use((0, express_session_1.default)({
    name: 'zwave-js-ui-session',
    secret: app_1.sessionSecret,
    resave: false,
    saveUninitialized: false,
    store: new FileStore({
        path: path_1.default.join(app_1.storeDir, 'sessions'),
        logFn: (...args) => {
            // skip ENOENT errors
            if (args &&
                args.filter((a) => a.indexOf('ENOENT') >= 0).length === 0) {
                logger.debug(args[0]);
            }
        },
    }),
    cookie: {
        secure: !!process.env.HTTPS || !!process.env.USE_SECURE_COOKIE,
        httpOnly: true, // prevents cookie to be sent by client javascript
        maxAge: 24 * 60 * 60 * 1000, // one day
    },
}));
// Node.js CSRF protection middleware.
// Requires either a session middleware or cookie-parser to be initialized first.
const csrfProtection = (0, csurf_1.default)({
    value: (req) => req.csrfToken(),
});
// ### SOCKET SETUP
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => { };
/**
 * Binds socketManager to `server`
 */
function setupSocket(server) {
    socketManager.bindServer(server);
    socketManager.io.on('connection', (socket) => {
        // Server: https://socket.io/docs/v4/server-application-structure/#all-event-handlers-are-registered-in-the-indexjs-file
        // Client: https://socket.io/docs/v4/client-api/#socketemiteventname-args
        socket.on(SocketEvents_1.inboundEvents.init, (data, cb = noop) => {
            let state = {};
            if (gw.zwave) {
                state = gw.zwave.getState();
            }
            if (zniffer) {
                state.zniffer = zniffer.status();
            }
            cb(state);
        });
        socket.on(SocketEvents_1.inboundEvents.zwave, 
        // eslint-disable-next-line @typescript-eslint/no-misused-promises
        async (data, cb = noop) => {
            if (gw.zwave) {
                if (!data.args)
                    data.args = [];
                const result = await gw.zwave.callApi(data.api, ...data.args);
                result.api = data.api;
                cb(result);
            }
            else {
                cb({
                    success: false,
                    message: 'Zwave client not connected',
                });
            }
        });
        // eslint-disable-next-line @typescript-eslint/no-misused-promises
        socket.on(SocketEvents_1.inboundEvents.mqtt, (data, cb = noop) => {
            logger.info(`Mqtt api call: ${data.api}`);
            let res, err;
            try {
                switch (data.api) {
                    case 'updateNodeTopics':
                        res = gw.updateNodeTopics(data.args[0]);
                        break;
                    case 'removeNodeRetained':
                        res = gw.removeNodeRetained(data.args[0]);
                        break;
                    default:
                        err = `Unknown MQTT api ${data.apiName}`;
                }
            }
            catch (error) {
                logger.error('Error while calling MQTT api', error);
                err = error.message;
            }
            const result = {
                success: !err,
                message: err || 'Success MQTT api call',
                result: res,
                api: data.api,
            };
            cb(result);
        });
        // eslint-disable-next-line @typescript-eslint/no-misused-promises
        socket.on(SocketEvents_1.inboundEvents.hass, async (data, cb = noop) => {
            logger.info(`Hass api call: ${data.apiName}`);
            let res, err;
            try {
                switch (data.apiName) {
                    case 'delete':
                        res = gw.publishDiscovery(data.device, data.nodeId, {
                            deleteDevice: true,
                            forceUpdate: true,
                        });
                        break;
                    case 'discover':
                        res = gw.publishDiscovery(data.device, data.nodeId, {
                            deleteDevice: false,
                            forceUpdate: true,
                        });
                        break;
                    case 'rediscoverNode':
                        res = gw.rediscoverNode(data.nodeId);
                        break;
                    case 'disableDiscovery':
                        res = gw.disableDiscovery(data.nodeId);
                        break;
                    case 'update':
                        res = gw.zwave.updateDevice(data.device, data.nodeId);
                        break;
                    case 'add':
                        res = gw.zwave.addDevice(data.device, data.nodeId);
                        break;
                    case 'store':
                        res = await gw.zwave.storeDevices(data.devices, data.nodeId, data.remove);
                        break;
                }
            }
            catch (error) {
                logger.error('Error while calling HASS api', error);
                err = error.message;
            }
            const result = {
                success: !err,
                message: err || 'Success HASS api call',
                result: res,
                api: data.apiName,
            };
            cb(result);
        });
        // eslint-disable-next-line @typescript-eslint/no-misused-promises
        socket.on(SocketEvents_1.inboundEvents.zniffer, async (data, cb = noop) => {
            logger.info(`Zniffer api call: ${data.api}`);
            let res, err;
            try {
                switch (data.apiName) {
                    case 'start':
                        res = await zniffer.start();
                        break;
                    case 'stop':
                        res = await zniffer.stop();
                        break;
                    case 'clear':
                        res = zniffer.clear();
                        break;
                    case 'getFrames':
                        res = zniffer.getFrames();
                        break;
                    case 'setFrequency':
                        res = await zniffer.setFrequency(data.frequency);
                        break;
                    case 'setLRChannelConfig':
                        res = await zniffer.setLRChannelConfig(data.channelConfig);
                        break;
                    case 'saveCaptureToFile':
                        res = await zniffer.saveCaptureToFile();
                        break;
                    default:
                        throw new Error(`Unknown ZNIFFER api ${data.apiName}`);
                }
            }
            catch (error) {
                logger.error('Error while calling ZNIFFER api', error);
                err = error.message;
            }
            const result = {
                success: !err,
                message: err || 'Success ZNIFFER api call',
                result: res,
                api: data.apiName,
            };
            cb(result);
        });
    });
    // emitted every time a new client connects/disconnects
    socketManager.on('clients', (event, activeSockets) => {
        if (event === 'connection' && activeSockets.size === 1) {
            gw.zwave?.setUserCallbacks();
        }
        else if (event === 'disconnect' && activeSockets.size === 0) {
            gw.zwave?.removeUserCallbacks();
        }
    });
}
// ### APIs
function isAuthEnabled() {
    const settings = jsonStore_1.default.get(store_1.default.settings);
    return settings.gateway?.authEnabled === true;
}
async function parseJWT(req) {
    // if not authenticated check if he has a valid token
    let token = req.headers['x-access-token'] || req.headers.authorization; // Express headers are auto converted to lowercase
    token = Array.isArray(token) ? token[0] : token;
    if (token && token.startsWith('Bearer ')) {
        // Remove Bearer from string
        token = token.slice(7, token.length);
    }
    // third-party cookies must be allowed in order to work
    if (!token) {
        throw Error('Invalid token header');
    }
    const decoded = await verifyJWT(token, app_1.sessionSecret);
    // Successfully authenticated, token is valid and the user _id of its content
    // is the same of the current session
    const users = jsonStore_1.default.get(store_1.default.users);
    const user = users.find((u) => u.username === decoded.username);
    if (user) {
        return user;
    }
    else {
        throw Error('User not found');
    }
}
// middleware to check if user is authenticated
async function isAuthenticated(req, res, next) {
    // if user is authenticated in the session, carry on
    if (req?.session?.user || !isAuthEnabled()) {
        return next();
    }
    // third-party cookies must be allowed in order to work
    try {
        const user = await parseJWT(req);
        req.session.user = user;
        next();
    }
    catch (error) {
        logger.debug('Authentication failed', error);
        res.json({
            success: false,
            message: RESPONSE_CODES.GENERAL_ERROR,
            code: 3,
        });
    }
}
// logout the user
app.get('/api/auth-enabled', apisLimiter, function (req, res) {
    res.json({ success: true, data: isAuthEnabled() });
});
// api to authenticate user
app.post('/api/authenticate', loginLimiter, csrfProtection, async function (req, res) {
    const token = req.body.token;
    let user;
    try {
        // token auth, mostly used to restore sessions when user refresh the page
        if (token) {
            const decoded = await verifyJWT(token, app_1.sessionSecret);
            // Successfully authenticated, token is valid and the user _id of its content
            // is the same of the current session
            const users = jsonStore_1.default.get(store_1.default.users);
            user = users.find((u) => u.username === decoded.username);
        }
        else {
            // credentials auth
            const users = jsonStore_1.default.get(store_1.default.users);
            const username = req.body.username;
            const password = req.body.password;
            user = users.find((u) => u.username === username);
            if (user &&
                !(await utils.verifyPsw(password, user.passwordHash))) {
                user = null;
            }
        }
        const result = {
            success: !!user,
            code: undefined,
            message: '',
            user: undefined,
        };
        if (result.success) {
            // don't edit the original user object, remove the password from jwt payload
            const userData = Object.assign({}, user);
            delete userData.passwordHash;
            const token = jsonwebtoken_1.default.sign(userData, app_1.sessionSecret, {
                expiresIn: '1d',
            });
            userData.token = token;
            req.session.user = userData;
            result.user = userData;
            loginLimiter.resetKey(req.ip);
            logger.info(`User ${user.username} logged in successfully from ${req.ip}`);
        }
        else {
            result.code = 3;
            result.message = RESPONSE_CODES.GENERAL_ERROR;
            logger.error(`User ${user?.username || req.body.username} failed to login from ${req.ip}: wrong credentials`);
        }
        res.json(result);
    }
    catch (error) {
        res.json({
            success: false,
            message: 'Authentication failed',
            code: 3,
        });
        logger.error(`User ${user?.username || req.body.username} failed to login from ${req.ip}: ${error.message}`);
    }
});
// logout the user
app.get('/api/logout', apisLimiter, isAuthenticated, function (req, res) {
    req.session.destroy((err) => {
        if (err) {
            res.json({ success: false, message: err.message });
        }
        else {
            res.json({ success: true, message: 'User logged out' });
        }
    });
});
// update user password
app.put('/api/password', apisLimiter, csrfProtection, isAuthenticated, async function (req, res) {
    try {
        const users = jsonStore_1.default.get(store_1.default.users);
        const user = req.session.user;
        const oldUser = users.find((u) => u.username === user.username);
        if (!oldUser) {
            return res.json({ success: false, message: 'User not found' });
        }
        if (!(await utils.verifyPsw(req.body.current, oldUser.passwordHash))) {
            return res.json({
                success: false,
                message: 'Current password is wrong',
            });
        }
        if (req.body.new !== req.body.confirmNew) {
            return res.json({
                success: false,
                message: "Passwords doesn't match",
            });
        }
        oldUser.passwordHash = await utils.hashPsw(req.body.new);
        req.session.user = oldUser;
        await jsonStore_1.default.put(store_1.default.users, users);
        res.json({
            success: true,
            message: 'Password updated',
            user: oldUser,
        });
    }
    catch (error) {
        res.json({
            success: false,
            message: 'Error while updating passwords',
            error: error.message,
        });
        logger.error('Error while updating password', error);
    }
});
app.get('/health', apisLimiter, function (req, res) {
    let mqtt;
    let zwave;
    if (gw) {
        mqtt = gw.mqtt?.getStatus() ?? false;
        zwave = gw.zwave?.getStatus().status ?? false;
    }
    // if mqtt is disabled, return true. Fixes #469
    if (mqtt && typeof mqtt !== 'boolean') {
        mqtt = mqtt.status || mqtt.config.disabled;
    }
    const status = mqtt && zwave;
    res.status(status ? 200 : 500).send(status ? 'Ok' : 'Error');
});
app.get('/health/:client', apisLimiter, function (req, res) {
    const client = req.params.client;
    let status;
    if (client !== 'zwave' && client !== 'mqtt') {
        res.status(500).send("Requested client doesn 't exist");
    }
    else {
        status = gw?.[client]?.getStatus().status ?? false;
    }
    res.status(status ? 200 : 500).send(status ? 'Ok' : 'Error');
});
app.get('/version', apisLimiter, function (req, res) {
    res.json({
        appVersion: utils.getVersion(),
        zwavejs: zwave_js_1.libVersion,
        zwavejsServer: server_1.serverVersion,
    });
});
// get settings
app.get('/api/settings', apisLimiter, isAuthenticated, async function (req, res) {
    const allSensors = (0, core_1.getAllSensors)();
    const namedScaleGroups = (0, core_1.getAllNamedScaleGroups)();
    const scales = [];
    for (const group of namedScaleGroups) {
        for (const scale of Object.values(group.scales)) {
            scales.push({
                key: group.name,
                sensor: group.name,
                unit: scale.unit,
                label: scale.label,
                description: scale.description,
            });
        }
    }
    for (const sensor of allSensors) {
        for (const scale of Object.values(sensor.scales)) {
            scales.push({
                key: sensor.key,
                sensor: sensor.label,
                label: scale.label,
                unit: scale.unit,
                description: scale.description,
            });
        }
    }
    const settings = jsonStore_1.default.get(store_1.default.settings);
    const data = {
        success: true,
        settings,
        devices: gw?.zwave?.devices ?? {},
        serial_ports: [],
        scales: scales,
        sslDisabled: sslDisabled(),
        tz: process.env.TZ,
        locale: process.env.LOCALE,
        deprecationWarning: process.env.TAG_NAME === 'zwavejs2mqtt',
    };
    if (process.platform !== 'sunos') {
        try {
            data.serial_ports = await zwave_js_1.Driver.enumerateSerialPorts({
                local: true,
                remote: true,
            });
        }
        catch (error) {
            logger.error(error);
            data.serial_ports = [];
        }
        res.json(data);
    }
    else
        res.json(data);
});
// update settings
app.post('/api/settings', apisLimiter, isAuthenticated, async function (req, res) {
    try {
        if (restarting) {
            throw Error('Gateway is restarting, wait a moment before doing another request');
        }
        let settings = req.body;
        let restartAll = false;
        let shouldRestartGw = false;
        let shouldRestartZniffer = false;
        const actualSettings = jsonStore_1.default.get(store_1.default.settings);
        // TODO: validate settings using calss-validator
        // when settings is null consider a force restart
        if (settings && Object.keys(settings).length > 0) {
            shouldRestartGw = !utils.deepEqual({
                zwave: actualSettings.zwave,
                gateway: actualSettings.gateway,
                mqtt: actualSettings.mqtt,
            }, {
                zwave: settings.zwave,
                gateway: settings.gateway,
                mqtt: settings.mqtt,
            });
            shouldRestartZniffer = !utils.deepEqual(actualSettings.zniffer, settings.zniffer);
            // nothing changed, consider it a forced restart
            restartAll = !shouldRestartGw && !shouldRestartZniffer;
            await jsonStore_1.default.put(store_1.default.settings, settings);
        }
        else {
            restartAll = true;
            settings = actualSettings;
        }
        if (restartAll || shouldRestartGw) {
            restarting = true;
            await gw.close();
            await destroyPlugins();
            // reload loggers settings
            setupLogging(settings);
            // restart clients and gateway
            await startGateway(settings);
            BackupManager_1.default.init(gw.zwave);
        }
        if (restartAll || shouldRestartZniffer) {
            if (zniffer) {
                await zniffer.close();
            }
            startZniffer(settings.zniffer);
        }
        res.json({
            success: true,
            message: 'Configuration updated successfully',
            data: settings,
        });
    }
    catch (error) {
        restarting = false;
        logger.error(error);
        res.json({ success: false, message: error.message });
    }
});
// update settings
app.post('/api/statistics', apisLimiter, isAuthenticated, async function (req, res) {
    try {
        if (restarting) {
            throw Error('Gateway is restarting, wait a moment before doing another request');
        }
        const { enableStatistics } = req.body;
        const settings = jsonStore_1.default.get(store_1.default.settings) || {};
        if (!settings.zwave) {
            settings.zwave = {};
        }
        settings.zwave.enableStatistics = enableStatistics;
        settings.zwave.disclaimerVersion = 1;
        await jsonStore_1.default.put(store_1.default.settings, settings);
        if (gw && gw.zwave) {
            if (enableStatistics) {
                gw.zwave.enableStatistics();
            }
            else {
                gw.zwave.disableStatistics();
            }
        }
        res.json({
            success: true,
            enabled: enableStatistics,
            message: 'Statistics configuration updated successfully',
        });
    }
    catch (error) {
        logger.error(error);
        res.json({ success: false, message: error.message });
    }
});
// update versions
app.post('/api/versions', apisLimiter, isAuthenticated, async function (req, res) {
    try {
        const { disableChangelog } = req.body;
        const settings = jsonStore_1.default.get(store_1.default.settings) || {};
        if (!settings.gateway) {
            settings.gateway = {
                type: Gateway_1.GatewayType.NAMED,
            };
            settings.gateway.versions = {};
        }
        // update versions to actual ones
        settings.gateway.versions = {
            app: utils.pkgJson.version, // don't use getVersion here as it may include commit sha
            driver: zwave_js_1.libVersion,
            server: server_1.serverVersion,
        };
        settings.gateway.disableChangelog = disableChangelog;
        await jsonStore_1.default.put(store_1.default.settings, settings);
        res.json({
            success: true,
            message: 'Versions updated successfully',
        });
    }
    catch (error) {
        logger.error(error);
        res.json({ success: false, message: error.message });
    }
});
// get config
app.get('/api/exportConfig', apisLimiter, isAuthenticated, function (req, res) {
    return res.json({
        success: true,
        data: jsonStore_1.default.get(store_1.default.nodes),
        message: 'Successfully exported nodes JSON configuration',
    });
});
// import config
app.post('/api/importConfig', apisLimiter, isAuthenticated, async function (req, res) {
    let config = req.body.data;
    try {
        if (!gw.zwave)
            throw Error('Z-Wave client not inited');
        // try convert to node object
        if (Array.isArray(config)) {
            const parsed = {};
            for (let i = 0; i < config.length; i++) {
                if (config[i]) {
                    parsed[i] = config[i];
                }
            }
            config = parsed;
        }
        for (const nodeId in config) {
            const node = config[nodeId];
            if (!node || typeof node !== 'object')
                continue;
            // All API calls expect nodeId to be a number, so convert it here.
            const nodeIdNumber = Number(nodeId);
            if (utils.hasProperty(node, 'name')) {
                await gw.zwave.callApi('setNodeName', nodeIdNumber, node.name || '');
            }
            if (utils.hasProperty(node, 'loc')) {
                await gw.zwave.callApi('setNodeLocation', nodeIdNumber, node.loc || '');
            }
            if (node.hassDevices) {
                await gw.zwave.storeDevices(node.hassDevices, nodeIdNumber, false);
            }
        }
        res.json({
            success: true,
            message: 'Configuration imported successfully',
        });
    }
    catch (error) {
        logger.error(error.message);
        return res.json({ success: false, message: error.message });
    }
});
// if no path provided return all store dir files/folders, otherwise return the file content
app.get('/api/store', storeLimiter, isAuthenticated, async function (req, res) {
    try {
        let data;
        if (req.query.path) {
            const reqPath = getSafePath(req);
            // lgtm [js/path-injection]
            let stat = await fs_extra_1.default.lstat(reqPath);
            // check symlink is secure
            if (stat.isSymbolicLink()) {
                const realPath = await (0, promises_1.realpath)(reqPath);
                getSafePath(realPath);
                stat = await fs_extra_1.default.lstat(realPath);
            }
            if (stat.isFile()) {
                // lgtm [js/path-injection]
                data = await fs_extra_1.default.readFile(reqPath, 'utf8');
            }
            else {
                throw Error('Path is not a file');
            }
        }
        else {
            data = [
                {
                    name: 'store',
                    path: app_1.storeDir,
                    isRoot: true,
                    children: await parseDir(app_1.storeDir),
                },
            ];
        }
        res.json({ success: true, data: data });
    }
    catch (error) {
        logger.error(error.message);
        return res.json({ success: false, message: error.message });
    }
});
app.put('/api/store', storeLimiter, isAuthenticated, async function (req, res) {
    try {
        const reqPath = getSafePath(req);
        const isNew = req.query.isNew === 'true';
        const isDirectory = req.query.isDirectory === 'true';
        if (!isNew) {
            // lgtm [js/path-injection]
            const stat = await fs_extra_1.default.lstat(reqPath);
            if (!stat.isFile()) {
                throw Error('Path is not a file');
            }
        }
        if (!isDirectory) {
            // lgtm [js/path-injection]
            await fs_extra_1.default.writeFile(reqPath, req.body.content, 'utf8');
        }
        else {
            // lgtm [js/path-injection]
            await fs_extra_1.default.mkdir(reqPath);
        }
        res.json({ success: true });
    }
    catch (error) {
        logger.error(error.message);
        return res.json({ success: false, message: error.message });
    }
});
app.delete('/api/store', storeLimiter, isAuthenticated, async function (req, res) {
    try {
        const reqPath = getSafePath(req);
        // lgtm [js/path-injection]
        await fs_extra_1.default.remove(reqPath);
        res.json({ success: true });
    }
    catch (error) {
        logger.error(error.message);
        return res.json({ success: false, message: error.message });
    }
});
app.put('/api/store-multi', storeLimiter, isAuthenticated, async function (req, res) {
    try {
        const files = req.body.files || [];
        for (const f of files) {
            await fs_extra_1.default.remove(f);
        }
        res.json({ success: true });
    }
    catch (error) {
        logger.error(error.message);
        return res.json({ success: false, message: error.message });
    }
});
app.post('/api/store-multi', storeLimiter, isAuthenticated, async function (req, res) {
    const files = req.body.files || [];
    const archive = (0, archiver_1.default)('zip');
    archive.on('error', function (err) {
        res.status(500).send({
            error: err.message,
        });
    });
    // on stream closed we can end the request
    archive.on('end', function () {
        logger.debug('zip archive ready');
    });
    // set the archive name
    res.attachment('zwave-js-ui-store.zip');
    res.setHeader('Content-Type', 'application/zip');
    // use res as stream so I don't need to create a temp file
    archive.pipe(res);
    for (const f of files) {
        const s = await fs_extra_1.default.lstat(f);
        const name = f.replace(app_1.storeDir, '');
        if (s.isFile()) {
            archive.file(f, { name });
        }
        else if (s.isSymbolicLink()) {
            const targetPath = await (0, promises_1.realpath)(f);
            try {
                // check path is secure, if so add it as file
                getSafePath(targetPath);
                archive.file(targetPath, { name });
            }
            catch (e) {
                // ignore
            }
        }
    }
    await archive.finalize();
});
app.get('/api/store/backup', storeLimiter, isAuthenticated, async function (req, res) {
    try {
        await jsonStore_1.default.backup(res);
    }
    catch (error) {
        res.status(500).send({
            error: error.message,
        });
    }
});
app.post('/api/store/upload', storeLimiter, isAuthenticated, async function (req, res) {
    let file;
    let isRestore = false;
    try {
        // read files from request
        await multerPromise(multerUpload, req, res);
        isRestore = req.body.restore === 'true';
        const folder = req.body.folder;
        file = req.files[0];
        if (!file || !file.path) {
            throw Error('No file uploaded');
        }
        if (isRestore) {
            await (0, extract_zip_1.default)(file.path, { dir: app_1.storeDir });
        }
        else {
            const destinationPath = getSafePath(path_1.default.join(app_1.storeDir, folder, file.originalname));
            await (0, fs_extra_1.move)(file.path, destinationPath);
        }
        res.json({ success: true });
    }
    catch (err) {
        res.json({ success: false, message: err.message });
    }
    if (file && isRestore) {
        await (0, fs_extra_1.rm)(file.path);
    }
});
app.get('/api/snippet', apisLimiter, async function (req, res) {
    try {
        const snippets = await getSnippets();
        res.json({ success: true, data: snippets });
    }
    catch (err) {
        res.json({ success: false, message: err.message });
    }
});
// catch 404 and forward to error handler
app.use(function (req, res, next) {
    const err = new Error('Not Found');
    err.status = 404;
    next(err);
});
// error handler
app.use(function (err, req, res) {
    logger.error(`${req.method} ${req.url} ${err.status} - Error: ${err.message}`);
    // render the error page
    res.status(err.status || 500);
    res.redirect('/');
});
process.removeAllListeners('SIGINT');
async function gracefuShutdown() {
    logger.warn('Shutdown detected: closing clients...');
    try {
        if (gw)
            await gw.close();
        await destroyPlugins();
    }
    catch (error) {
        logger.error('Error while closing clients', error);
    }
    return process.exit();
}
process.on('uncaughtException', (reason) => {
    const stack = reason.stack || '';
    logger.error(
    // eslint-disable-next-line @typescript-eslint/no-base-to-string
    `Unhandled Rejection, reason: ${reason}${stack ? `\n${stack}` : ''}`);
});
for (const signal of ['SIGINT', 'SIGTERM']) {
    // eslint-disable-next-line @typescript-eslint/no-misused-promises
    process.once(signal, gracefuShutdown);
}
exports.default = app;
