future-id/mdr-cli

View on GitHub
src/utils/Config.ts

Summary

Maintainability
A
0 mins
Test Coverage
import crypto from "crypto";
import fs from "fs";
import chalk from "chalk";
import semver from "semver";
import isBase64 from "is-base64";
import TOML from "@iarna/toml";
import { name, version } from "../../package.json";
import { AuthType } from "./Types";
import { CONFIG_FILE, logger, checkType } from "./Utils";

class IndexSignature {
    [k: string]: any;
    [k: number]: undefined;
}

interface IConfig extends TOML.JsonMap {
    authType: AuthType;
    user: string;
    password: string;
    host: string;
    apiPath: string;
    useSSL: boolean;
}

// Since null and undefined are incompatible with TOML.AnyJson type
// we have to work around it like this, idk why they did this because undefined and null is valid json
interface NullableConfig {
    lastNotification?: number;
}

export type ConfigType = IConfig & NullableConfig;

export class Config extends IndexSignature implements ConfigType {
    authType!: AuthType;
    user!: string;
    password!: string;
    host!: string;
    apiPath!: string;
    useSSL!: boolean;
    lastNotification!: number;

    #originalPassword: string;
    #config: ConfigType;

    constructor(data: ConfigType) {
        super();

        // If package version is greater than 1.3.1 and lastNotification is missing
        // Fix the config file with the new property
        if (semver.gt(version, "1.3.1") && !data.lastNotification) {
            data.lastNotification = Date.now();
            const newToml = TOML.stringify(data);
            fs.writeFileSync(CONFIG_FILE, newToml, "utf-8");
        }

        this.#originalPassword = data.password;
        this.#config = data;

        Object.assign(this, data);
    }

    validate(): Config {
        if (!this.authType || typeof this.authType !== "string") {
            logger.error(
                `Invalid auth type in ${CONFIG_FILE}\n` +
                "Auth type can only be md5 or plain"
            );
            process.exit(1);
        } else {
            checkType(this.authType);
        }

        if (!this.user || typeof this.user !== "string") {
            logger.error(`Invalid user defined in ${CONFIG_FILE}`);
            process.exit(1);
        }

        if (!this.password || typeof this.password !== "string") {
            logger.error(`Invalid password defined in ${CONFIG_FILE}`);
            process.exit(1);
        }

        // Check if saved password is base64 encoded
        // Warn the user if not and exit the process
        const b64 = isBase64(this.#originalPassword);
        if (!b64) {
            logger.warn(
                "\nYour password is saved as plain text, this can happen because of three reasons:\n" +
                `1. You manually changed the config file ${chalk.bold("(it is recommended to not do this)")}\n` +
                "2. You're running an old version\n" +
                "3. You just updated to version >= 1.0.0 but haven't updated your config file yet\n\n" +
                chalk.bold("To fix this follow these steps:\n") +
                `1. Update mdr to the latest version ${chalk.bold(`npm update -g ${name}`)}\n` +
                `2. Update your password using ${chalk.bold("mdr set password <password>")} (replace ${chalk.bold("<password>")} with your actual password)`
            );
            process.exit(1);
        }

        if (!this.host || typeof this.host !== "string") {
            logger.error(`Invalid host defined in ${CONFIG_FILE}`);
            process.exit(1);
        }

        if (!this.apiPath || typeof this.apiPath !== "string") {
            logger.error(`Invalid api path defined in ${CONFIG_FILE}`);
            process.exit(1);
        }

        if ((this.useSSL !== false && !this.useSSL) || typeof this.useSSL !== "boolean") {
            logger.error(`Invalid useSSL defined in ${CONFIG_FILE}`);
            process.exit(1);
        }

        if (this.useSSL === false && this.authType === "plain") {
            logger.error("When not using ssl the auth type has to be md5");
            process.exit(1);
        }

        // Decode base64 password
        this.password = Buffer.from(this.#originalPassword, "base64").toString("utf-8");

        // If auth type is md5 encode password to md5 hash
        if (this.authType === "md5") {
            this.password = crypto.createHash("md5").update(this.password).digest("hex");
        }

        return this;
    }

    updateNotification(): void {
        this.lastNotification = Date.now();
        this.#config.lastNotification = this.lastNotification;
        const newToml = TOML.stringify(this.#config);
        fs.writeFileSync(CONFIG_FILE, newToml, "utf-8");
    }
}