3
\$\begingroup\$

In order to learn more about Node.js and promises I followed a tutorial on Sitepoint about building a command line app. I liked the idea and simplicity of Node so I decided to build my own that initializes a local git repo and using the Bitbucket 2.0 API creates a remote repo. Now that I have it working I would love some feedback on how to make the code cleaner or more closely follow best practices. I would love any feedback people have. I have published it on NPM for people to use but I would love to make it better. Source code is here. Does anyone have any suggestions?

/**
 * Prompt the user for Key & Secret
 * @param callback - Uses the key and secret to gain access tokens
 */
function getCredentials(callback) {
    let questions = [
        {
            name: 'key',
            type: 'key',
            message: 'Enter your OAuth Consumer Key:',
            validate: function (value) {
                if (value.length) {
                    return true;
                } else {
                    return 'Please enter your consumer key';
                }
            }
        },
        {
            name: 'secret',
            type: 'secret',
            message: 'Enter your OAuth Consumer Secret:',
            validate: function (value) {
                if (value.length) {
                    return true;
                } else {
                    return 'Please enter your consumer secret';
                }
            }
        }
    ];

    inquirer.prompt(questions).then(callback);
}


/**
 * Check if there is a local token in prefs
 * @type {Promise} Returns the token in memory if found
 */
function getSavedToken() {

    return new Promise((resolve, reject) => {
        // If token exists in prefs, return it...
        if (prefs.schemeinit && prefs.schemeinit.token != '') {
            console.log(chalk.green('\n' + 'Token already in memory.' + '\n'));
            resolve(prefs.schemeinit.token);
        }

        // Otherwise reject
        reject(new Error('No token saved in prefs.'));
    })
}


/**
 * Get a new token
 * @returns {Promise} Returns a new access token
 */
function getNewToken() {
    return new Promise((resolve, reject) => {
        getCredentials(function (credentials) {
            let status = new Spinner('Authenticating you, please wait...');

            status.start();

            // Using Api endpoints
            request({
                url: 'https://bitbucket.org/site/oauth2/access_token',
                method: 'POST',
                headers: {'content-type': 'application/json'},
                auth: {
                    user: credentials.key,
                    pass: credentials.secret
                },
                form: {
                    'grant_type': 'client_credentials'
                }
            }, function (err, res) {
                status.stop();

                if (err) {
                    reject(new Error('Could not get new token.'));
                }

                if (res.statusCode == 200) {
                    let json = JSON.parse(res.body);

                    // Set the token in prefs, also set key and secret as it was successfull
                    prefs.schemeinit = {
                        token: json.access_token,
                        refresh: json.refresh_token,
                        key: credentials.key,
                        secret: credentials.secret
                    };

                    resolve(json.access_token);
                }

            });

        });
    });
}


/**
 * Refresh the access token
 * @returns {Promise} Sets new token info in prefs and returns the new access token
 */
function refreshToken() {

    console.log(chalk.green('\n' + 'Refreshing token...' + '\n'));

    return new Promise((resolve, reject) => {
        let status = new Spinner('Token expired, getting a new one...');

        status.start();

        request({
            url: 'https://bitbucket.org/site/oauth2/access_token',
            method: 'POST',
            headers: {'content-type': 'application/json'},
            auth: {
                user: prefs.schemeinit.key,
                pass: prefs.schemeinit.secret
            },
            form: {
                'grant_type': 'refresh_token',
                'refresh_token': prefs.schemeinit.refresh
            }
        }, function (err, res) {
            status.stop();

            if (err) {
                console.log(chalk.red(err.message + '\n'));
                reject(new Error('Couldn\'t get refresh token.'));
            }

            if (res.statusCode == 400) {
                let json = JSON.parse(res.body);

                console.log(chalk.red(res.statusMessage));
                console.log(chalk.red(json.error_description + '\n'));
            }

            if (res.statusCode == 200) {
                let json = JSON.parse(res.body);

                // TODO: Do I need to re-set the key and secret here? If I don't does it wipe them?
                // Update the token in prefs
                prefs.schemeinit = {
                    token: json.access_token,
                    refresh: json.refresh_token,
                    key: prefs.schemeinit.key,
                    secret: prefs.schemeinit.secret
                };

                console.log(chalk.yellow('New Tokens received.' + '\n'));
                resolve(json.access_token);
            }

        });
    })

}


/**
 * Get User
 * @param {string} token - OAuth Token
 * @returns {Promise} Return the username
 */
function getUser(token) {
    return new Promise((resolve, reject) => {
        request({
            url: 'https://api.bitbucket.org/2.0/user',
            method: 'GET',
            headers: {'Authorization': 'Bearer ' + token}
        }, function (err, res) {

            let user = JSON.parse(res.body);

            if (err) {
                reject(new Error(err));
            }

            if (res.statusCode == 401) {
                reject(new Error('Token expired. Need to refresh token.'));
            }

            if (res) {
                resolve(user.username);
            }

        });
    });
}

/**
 * Create the remote repository on Bitbucket
 * @param {string} username - Bitbucket username
 * @returns {Promise} Returns object of newly created repo information
 */
function createRepo(username) {

    return new Promise((resolve, reject) => {
        let questions = [
            {
                type: 'input',
                name: 'reponame',
                message: 'Enter a name for the repository:',
                default: files.getCurrentDirectoryBase(),
                validate: function (value) {
                    if (value.length) {
                        return true;
                    } else {
                        return 'Please enter a name for the repository';
                    }
                }
            },
            {
                type: 'input',
                name: 'description',
                default: null,
                message: 'Optionally enter a description of the repository:'
            },
            {
                type: 'list',
                name: 'visibility',
                message: 'Public or private:',
                choices: ['public', 'private'],
                default: 'private'
            },
            {
                type: 'list',
                name: 'language',
                message: 'Language:',
                choices: ['css', 'html/css', 'javascript', 'nodejs', 'php'],
                default: 'php'
            }
        ];

        inquirer.prompt(questions).then(function (answers) {
            let status = new Spinner('Creating repository...');

            status.start();

            request({
                url: 'https://api.bitbucket.org/2.0/repositories/' + username + '/' + _.kebabCase(answers.reponame),
                method: 'POST',
                headers: {'Authorization': 'Bearer ' + prefs.schemeinit.token},
                form: {
                    "scm": "git",
                    "name": answers.reponame,
                    "is_private": answers.visibility === 'private' ? true : false,
                    "description": answers.description,
                    "language": answers.language,
                }
            }, function (err, res) {
                status.stop();

                if (err) {
                    console.log(err);
                    reject(new Error('Couldn\'t create remote repo.'));
                }

                let json = JSON.parse(res.body);

                if(res.statusCode == 400) {
                    reject(new Error(json.error.message));
                }

                if (res.statusCode == 200) {
                    console.log(chalk.green('\n' + json.name + ' created sucessfully.'));
                    console.log(chalk.green('You can view it here: ' + json.links.html.href + '\n'));

                    resolve(json);
                }

            });

        });
    });
}

/**
 * Create a local .gitignore file
 * @returns {Promise}
 */
function createGitignore() {

    return new Promise((resolve, reject) => {
        let filelist = _.without(fs.readdirSync('.'), '.git', '.gitignore');

        if (filelist.length) {
            inquirer.prompt(
                [
                    {
                        type: 'checkbox',
                        name: 'ignore',
                        message: 'Select the local files and/or folders you wish to ignore:',
                        choices: filelist,
                        default: ['node_modules', 'bower_components']
                    }
                ]
            ).then(function (answers) {
                if (answers.ignore.length) {
                    fs.writeFileSync('.gitignore', answers.ignore.join('\n'));
                } else {
                    touch('.gitignore');
                }
                resolve();
            });
        } else {
            touch('.gitignore');
            resolve();
        }
    })

}

/**
 * Create a local git repository and set it's remote to
 * previously created remote repository
 * @param {object} remoteRepo - Information object for remote repo
 * @returns {Promise}
 */
function localRepo(remoteRepo) {

    return new Promise((resolve, reject) => {
        let status = new Spinner('Setting up the repository...');

        status.start();

        git
            .init()
            .add('.gitignore')
            .add('./*')
            .commit('Initial commit')
            .addRemote('origin', 'https://bitbucket.org/' + remoteRepo.full_name)
            .push('origin', 'master')
            .exec(function () {
                status.stop();
                resolve('Local repository created and connected to remote.');
            });
    })

}

/**
 * Start the process by creating the remote repository and returning it
 * @type {Promise.<TResult>}
 */
const repo = getSavedToken()
    .catch(err => {
        // if no local token, get a new one.
        console.log(chalk.yellow('\n' + err.message + '\n'));
        return getNewToken();
    })
    .then(getUser)
    .catch(err => {
        // if error, most likely that the token needs to be refreshed
        console.log(chalk.yellow(err.message));
        return refreshToken()
            .then(getUser);
    })
    .then(createRepo);


/**
 * Once remote repository is created, setup the local repository
 * and connect to the remote
 */
repo.then(res => {

    let remoteRepo = res;

    createGitignore()
        .then(res => {
            console.log(chalk.green('\n' + '.gitignore created.'));
            return localRepo(remoteRepo);
        })
        .then(res => {
            console.log(chalk.green('\n' + res));
            console.log(chalk.green('All done! Now go take over the world!' + '\n'));
        });
})
.catch(err => {
    console.log(chalk.red('\n' + err.message + '\n'));
});
\$\endgroup\$
0
1
\$\begingroup\$

A few comments about your use of the api

Make authentication easier

The api supports using a username/password pair to login. This might be easier to implement and also easier for the user rather than going through the troubles of creating an oauth key which will probably be forgotten after they use your program a few times.

I've done this sort of thing before in python and it is as easy as

requests.get('<url>', auth=(user,pass))

The advantage to this method is that if the user uses 2-factor authentication, they can simply get an app password which can be used with your program.

EDIT

As per the comment below, it seems that using your oauth method is actually more user friendly than what I was suggesting, so simply ignore the above

Make use of partial responses

Take a look here; The api also allows your to specify exactly what fields you are interested in. This allows you to reduce the amount of data downloaded and only get what you need. For example, since you are only interested in the username of the user, you could have added a GET param to your request like: ?fields=username, and this will give you a simple JSON like: {"username": "smac89"}, rather than the other mass of data you were getting.

Other comments

If you are going to use es6, use it, don't switch back and forth between es5 and es6.

  • import x from 'lib-name' vs const lib_name = require('lib-name')
  • Prefer arrow functions over your nested functions

Create reusable functions. Your code to get token and to refresh token, look almost similar. Can you think of a way to make them into one function?


Well done on following through the tutorial and making something useful out of it. I hope you find the comments useful. Leave a comment if you don't understand something I said.

\$\endgroup\$
4
  • \$\begingroup\$ Thank you very much @smac89 for your feedback. I really appreciate it! :) My thoughts behind using the oauth key and getting a token was to save the user from inputing their username/password all the time. Instead they could use a token and when it needs to be refreshed it can do so automagically without the users involvement. Also, in the case of a security breach, a user could just invalidate the token. Is my thinking incorrect? \$\endgroup\$ – 5k313t0r Sep 19 '17 at 16:09
  • \$\begingroup\$ @5k313t0r, actually that's a good idea. I guess I wasn't clear on how often the user would have to input the token. I was under the impression it was acting like a password which needs to be given each time the program is used. Good idea! \$\endgroup\$ – smac89 Sep 19 '17 at 16:15
  • 2
    \$\begingroup\$ Also, your feedback of partial responses is a great idea. I didn't know I could do this. I will try and combine the token functions as well. I don't have too much experience with this but I will give it a shot! I will take your feedback and try to make it better. Thank you very much for your help! :) \$\endgroup\$ – 5k313t0r Sep 19 '17 at 16:15
  • \$\begingroup\$ thanks! Sorry, I should have made that clearer. I was thinking that this command line tool could be used on an ongoing basis by users to easily create repos as part of their workflow. Using the preferences module I save the encrypted tokens for retrieval later. Thanks again! \$\endgroup\$ – 5k313t0r Sep 19 '17 at 17:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.