RSS Git Download  Clone
Raw Blame History 13kB 326 lines
import path from 'path'
import fs from 'fs'
import os from 'os'
import { program } from 'commander'
import { parseBoolean, readPasswordHashFromFile } from './http-auth.mjs'

const isPlainObject = (value) => {
    return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}

const mergeDeep = (target, source) => {
    const output = isPlainObject(target) ? { ...target } : {}
    if (!isPlainObject(source)) {
        return output
    }

    for (const [key, value] of Object.entries(source)) {
        if (Array.isArray(value)) {
            output[key] = value.slice()
            continue
        }
        if (isPlainObject(value)) {
            output[key] = mergeDeep(isPlainObject(output[key]) ? output[key] : {}, value)
            continue
        }
        output[key] = value
    }

    return output
}

const loadJsonFile = (filePath) => {
    if (!filePath || !fs.existsSync(filePath)) {
        return undefined
    }
    try {
        const content = fs.readFileSync(filePath, 'utf8')
        return JSON.parse(content)
    } catch (e) {
        console.warn(`Could not read config ${filePath}:`, e.message)
        return undefined
    }
}

const cli = async () => {
    const pkg = JSON.parse(fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8'))
    p3xrs.version = pkg.version

    program
        .version(pkg.version)
        .option('-c, --config [config]', 'Set the p3xr.json p3x-redis-ui-server configuration, see more help in p3x-redis-ui-server')
        .option('-r, --readonly-connections', 'Set the connections to be readonly, no adding, saving or delete a connection')
        .option('-n, --connections-file-name [filename]', 'Set the connections file name, overrides default .p3xrs-conns.json')
        .option('--http-auth-enable', 'Enable HTTP Basic auth')
        .option('--http-auth-disable', 'Disable HTTP Basic auth')
        .option('--http-auth-username [username]', 'HTTP Basic auth username')
        .option('--http-auth-password [password]', 'HTTP Basic auth plain password')
        .option('--http-auth-password-hash [hash]', 'HTTP Basic auth bcrypt password hash')
        .option('--http-auth-password-hash-file [file]', 'Read HTTP Basic auth bcrypt password hash from file')
        .option('--groq-api-key [key]', 'Groq API key for AI-powered Redis query translation (get a free key at console.groq.com)')
        .option('--groq-api-key-readonly', 'Prevent users from changing the Groq API key via the UI')
        .parse(process.argv);

    const programOptions = program.opts();

    if (!process.versions.hasOwnProperty('electron') && !process.env.hasOwnProperty('P3XRS_DOCKER_HOME')) {

        if (!programOptions.config) {
            const findConfigFile = (startPath, filename) => {
                let currentPath = startPath;
                while (currentPath !== path.resolve(currentPath, '..')) { // Check until we reach the root directory
                    const filePath = path.join(currentPath, filename);
                    if (fs.existsSync(filePath)) {
                        return filePath;
                    }
                    currentPath = path.resolve(currentPath, '..'); // Move up one directory level
                }
                throw new Error('The specified configuration file could not be found.');
            }
            const resolveConfigPath = () => {
                // Attempt to find the config file starting from the directory of the main script or current directory
                const startPath = process.cwd();
                return findConfigFile(startPath, 'p3xrs.json');
            }
            programOptions.config = resolveConfigPath()

            //        program.outputHelp()
            //        return false
        }

        const configPath = path.resolve(process.cwd(), programOptions.config)
        //console.log(configPath)
        p3xrs.configPath = configPath

        p3xrs.cfg = JSON.parse(fs.readFileSync(configPath, 'utf8')).p3xrs


        if (programOptions.readonlyConnections) {
            // console.warn(programOptions.readonlyConnections)
            p3xrs.cfg.readonlyConnections = true
            //console.warn(p3xrs.cfg.readonlyConnections === true)
        }

        if (typeof programOptions.groqApiKey === 'string' && programOptions.groqApiKey.trim()) {
            p3xrs.cfg.groqApiKey = programOptions.groqApiKey.trim()
        }
        if (programOptions.groqApiKeyReadonly) {
            p3xrs.cfg.groqApiKeyReadonly = true
        }

        if (typeof programOptions.connectionsFileName !== 'undefined' && programOptions.connectionsFileName) {
            // console.warn(programOptions.connectionsFileName)
            p3xrs.cfg.connectionsFileName = programOptions.connectionsFileName
            //console.warn(p3xrs.cfg.readonlyConnections === true)
        }


    } else {
        const defaultElectronConfig = {
            "http": {
                "port-info": "this is ommitted, it will be default 7843",
                "port": process.env.hasOwnProperty('P3XRS_DOCKER_HOME') ? 7843 : global.p3xrsElectronPort,
                "bind-info": "the interface with listen to, could be 127.0.0.1 or 0.0.0.0 or specific interface",
                "bind": "0.0.0.0",
            },
            "connections": {
                "home-dir-info": "if the dir config is empty or home, the connections are saved in the home folder, otherwise it will resolve the directory set as it is, either relative ./ or absolute starting with /. NodeJs will resolve this directory in p3xrs.connections.dir",
                "home-dir": "home"
            },
            "static-info": "This is the best configuration, if it starts with ~, then it is in resolve the path in the node_modules, otherwise it resolves to the current process current working directory.",
            "static": "~p3x-redis-ui-material/dist",
            "httpAuth": {
                "enabled": false,
                "username": "admin",
                "password": "",
                "passwordHash": "",
            },
            "treeDividers": [
                ":",
                "/",
                "|",
                "-",
                "@"
            ]
        }

        let electronUserDataDir = ''
        try {
            const electron = await import('electron')
            const electronApp = electron.default?.app || electron.app
            if (electronApp && typeof electronApp.getPath === 'function') {
                electronUserDataDir = electronApp.getPath('userData')
            }
        } catch (e) {
            electronUserDataDir = ''
        }
        const configuredDir = typeof process.env.P3XRS_ELECTRON_CONFIG_DIR === 'string'
            ? process.env.P3XRS_ELECTRON_CONFIG_DIR.trim()
            : ''
        const electronConfigDir = configuredDir || electronUserDataDir || os.homedir()
        p3xrs.configPath = path.resolve(electronConfigDir, 'p3xrs.json')

        let persistedRoot = loadJsonFile(p3xrs.configPath)
        if ((!persistedRoot || !isPlainObject(persistedRoot.p3xrs))) {
            const legacyConfigPath = path.resolve(process.cwd(), 'p3xrs.json')
            const legacyRoot = loadJsonFile(legacyConfigPath)
            if (legacyRoot && isPlainObject(legacyRoot.p3xrs)) {
                persistedRoot = legacyRoot
            }
        }

        const persistedConfig = persistedRoot && isPlainObject(persistedRoot.p3xrs)
            ? persistedRoot.p3xrs
            : {}

        p3xrs.cfg = mergeDeep(defaultElectronConfig, persistedConfig)

        if (programOptions.readonlyConnections) {
            p3xrs.cfg.readonlyConnections = true
        } else {
            p3xrs.cfg.readonlyConnections = false
        }

        if (typeof programOptions.groqApiKey === 'string' && programOptions.groqApiKey.trim()) {
            p3xrs.cfg.groqApiKey = programOptions.groqApiKey.trim()
        }
        if (programOptions.groqApiKeyReadonly) {
            p3xrs.cfg.groqApiKeyReadonly = true
        }
    }

    const applyHttpAuthConfig = () => {
        if (!p3xrs.cfg.httpAuth || typeof p3xrs.cfg.httpAuth !== 'object') {
            if (p3xrs.cfg.server && typeof p3xrs.cfg.server.httpAuth === 'object') {
                p3xrs.cfg.httpAuth = Object.assign({}, p3xrs.cfg.server.httpAuth)
            } else {
                p3xrs.cfg.httpAuth = {}
            }
        }
        const httpAuth = p3xrs.cfg.httpAuth

        if (typeof p3xrs.cfg.httpUser === 'string' && !httpAuth.username) {
            httpAuth.username = p3xrs.cfg.httpUser
        }
        if (typeof p3xrs.cfg.httpPassword === 'string' && !httpAuth.password) {
            httpAuth.password = p3xrs.cfg.httpPassword
        }

        if (typeof process.env.HTTP_USER === 'string' && process.env.HTTP_USER.trim() !== '') {
            httpAuth.username = process.env.HTTP_USER.trim()
        }
        if (typeof process.env.HTTP_PASSWORD === 'string') {
            httpAuth.password = process.env.HTTP_PASSWORD
        }
        if (typeof process.env.HTTP_PASSWORD_HASH === 'string' && process.env.HTTP_PASSWORD_HASH.trim() !== '') {
            httpAuth.passwordHash = process.env.HTTP_PASSWORD_HASH.trim()
        }
        if (typeof process.env.HTTP_PASSWORD_HASH_FILE === 'string' && process.env.HTTP_PASSWORD_HASH_FILE.trim() !== '') {
            const hashFromFile = readPasswordHashFromFile(process.env.HTTP_PASSWORD_HASH_FILE)
            if (hashFromFile) {
                httpAuth.passwordHash = hashFromFile
            }
        }
        const envEnabled = parseBoolean(process.env.HTTP_AUTH_ENABLED)
        if (envEnabled !== undefined) {
            httpAuth.enabled = envEnabled
        }

        if (typeof programOptions.httpAuthUsername === 'string' && programOptions.httpAuthUsername.trim() !== '') {
            httpAuth.username = programOptions.httpAuthUsername.trim()
        }
        if (typeof programOptions.httpAuthPassword === 'string') {
            httpAuth.password = programOptions.httpAuthPassword
        }
        if (typeof programOptions.httpAuthPasswordHash === 'string' && programOptions.httpAuthPasswordHash.trim() !== '') {
            httpAuth.passwordHash = programOptions.httpAuthPasswordHash.trim()
        }
        if (typeof programOptions.httpAuthPasswordHashFile === 'string' && programOptions.httpAuthPasswordHashFile.trim() !== '') {
            const hashFromCliFile = readPasswordHashFromFile(programOptions.httpAuthPasswordHashFile)
            if (hashFromCliFile) {
                httpAuth.passwordHash = hashFromCliFile
            }
        }
        if (programOptions.httpAuthEnable === true) {
            httpAuth.enabled = true
        }
        if (programOptions.httpAuthDisable === true) {
            httpAuth.enabled = false
        }


    }
    applyHttpAuthConfig()

    const authLog = p3xrs.cfg && p3xrs.cfg.httpAuth && typeof p3xrs.cfg.httpAuth === 'object'
        ? p3xrs.cfg.httpAuth
        : {}
    const authEnabled = parseBoolean(authLog.enabled) === true
    const authHasHash = typeof authLog.passwordHash === 'string' && authLog.passwordHash.trim().length > 0
    const authHasPlain = typeof authLog.password === 'string' && authLog.password.length > 0
    console.info(`http auth: ${authEnabled ? 'enabled' : 'disabled'} (user=${authLog.username || 'admin'}, hash=${authHasHash ? 'set' : 'empty'}, plain=${authHasPlain ? 'set' : 'empty'})`)

    if (p3xrs.cfg.connectionsFileName === undefined) {
        p3xrs.cfg.connectionsFileName = '.p3xrs-conns.json'
    }

    if (!p3xrs.cfg.hasOwnProperty('static')) {
        p3xrs.cfg.static = '~p3x-redis-ui-material/dist'
    }

    // staticReact: no default — auto-detected from static path in http service
    // staticVue: no default — auto-detected from static path in http service

    if (!p3xrs.cfg.hasOwnProperty('connections')) {
        p3xrs.cfg.connections = {}
    }
    if (!p3xrs.cfg.connections.hasOwnProperty('home-dir')) {
        p3xrs.cfg.connections = 'home'
    }

    if (p3xrs.cfg.connections['home-dir'] === 'home') {
        p3xrs.cfg.connections['home-dir'] = os.homedir();
    }
    if (process.env.hasOwnProperty('P3XRS_DOCKER_HOME')) {
        p3xrs.cfg.connections['home-dir'] = process.env.P3XRS_DOCKER_HOME
    }
    if (process.env.FLATPAK_ID) {
        // process.env.XDG_DATA_HOME
        p3xrs.cfg.connections['home-dir'] = '/var/data/'
    }
    if (process.env.hasOwnProperty('P3XRS_PORT')) {
        p3xrs.cfg.http.port = process.env.P3XRS_PORT
    }
    p3xrs.cfg.connections['home'] = path.resolve(p3xrs.cfg.connections['home-dir'], p3xrs.cfg.connectionsFileName)

    console.info('using home config is', p3xrs.cfg.connections['home'])

    if (!fs.existsSync(p3xrs.cfg.connections.home)) {

        fs.writeFileSync(p3xrs.cfg.connections.home, JSON.stringify({
            update: new Date(),
            list: [],
        }, null, 4))
    }
    p3xrs.connections = JSON.parse(fs.readFileSync(p3xrs.cfg.connections.home, 'utf8'))
    //console.log(p3xrs.cfg.connections.home, p3xrs.connections)
    //console.log(p3xrs.connections)

    /*
    p3xrs.redis = {}
    let keyStreamPaging = 10000
    Object.defineProperty(p3xrs.redis, 'key-stream-paging', {
        get: () => {
            return keyStreamPaging
        },
        set: (value) => {
            keyStreamPaging = value
        }
    })
    */

    return true;
}

export default cli;