1
\$\begingroup\$

I have created a library which consumes the Imgur API.

This is the main file.

var request = require('request-promise'),
    fs = require('fs'),
    path = require('path');

var Imgur = function(options) {

    this.options = {};

    if (typeof options.clientId === 'undefined' || !options.clientId) {
        throw new Error('No client ID specified, register one at "https://api.imgur.com/oauth2/addclient"');
    }

    if (typeof options.clientSecret !== 'undefined') {
        this.options.clientSecret = options.clientSecret;
    }

    this.options.clientId = options.clientId;
    this.options.version = options.version || 3;
    this.options.endPoint = 'https://api.imgur.com/' + this.options.version + '/';

    fs
        .readdirSync(path.join(__dirname, './endpoints'))
        .forEach(function(file) {
            if (~file.indexOf('.js')) {
                this[file.substring(0, file.indexOf('.'))] = require(path.join(__dirname, './endpoints', file))(this);
            }
        }.bind(this));
};

Imgur.prototype.extend = function(target, source) {
    var extended = {};
    for (var prop in target) {
        if (Object.prototype.hasOwnProperty.call(target, prop)) {
            extended[prop] = target[prop];
        }
    }
    for (var prop in source) {
        if (Object.prototype.hasOwnProperty.call(source, prop)) {
            extended[prop] = source[prop];
        }
    }
    return extended;
};

Imgur.prototype.request = function(method, path, params) {
    params = params || {};
    if (typeof method === 'undefined' || ['get', 'post', 'head', 'delete'].indexOf(method.toLowerCase()) === -1) {
        throw new Error('no method specified or method isn\'t in [get, post, head, delete]');
    }
    var options = {
        method: method,
        headers: {
            Authorization: 'Client-ID ' + this.options.clientId
        },
        json: true
    };
    if (typeof params.data !== 'undefined' && method.toLowerCase() !== 'get') {
        options.form = params.data;
    }
    if (typeof params.file !== 'undefined') {
        options.formData = params.file;
    }
    if (typeof params.access_token !== 'undefined') {
        options.headers.Authorization = 'Bearer ' + params.access_token;
    }
    for (var key in params) {
        if (['data', 'file', 'access_token'].indexOf(key) != -1)
            continue;
        if (params.hasOwnProperty(key)) {
            path = path.replace(new RegExp('\{' + key + '\}', 'g'), params[key]);
        }
    }
    options.url = /^https?:\/\//.test(path) ? path : this.options.endPoint + path;
    return request(options);
};

module.exports = Imgur;

It is then extended with endpoints such as:

var stream = require('stream');

var Image = function(imgur) {

    this.image = function(params) {
        params = params || {};
        if (!params.hasOwnProperty('id')) {
            throw new Error('id required');
        }
        var options = imgur.extend(params, {
            id: params.id
        });
        return imgur.request('get', 'image/{id}', options);
    };

    this.upload = function(params) {
        params = params || {};
        if (!params.hasOwnProperty('image')) {
            throw new Error('image required, can be a binary file, base64 data or a URL');
        }
        var options = imgur.extend(params, {
            data: params.data,
            file: params.file
        });
        if (typeof params.file === 'string') {
            if (/https?:\/\//.test(params.file)) {
                options.data.file = 'URL';
            } else {
                options.data.type = 'base64';
            }
        } else if (params.file instanceof stream.Stream) {
            options.data.type = 'file'
        }
        return imgur.request('post', 'image', options);
    };

    this.delete = function(params) {
        params = params || {};
        if (!params.hasOwnProperty('id')) {
            throw new Error('id required');
        }
        var options = imgur.extend(params, {
            id: params.id
        });
        return imgur.request('delete', 'image/{id}', options);
    };

    this.update = function(params) {
        params = params || {};
        if (!params.hasOwnProperty('id')) {
            throw new Error('id required');
        }
        var options = imgur.extend(params, {
            id: params.id,
            data: params.data
        });
        return imgur.request('post', 'image/{id}', options);
    };

    this.favorite = function(params) {
        params = params || {};
        if (!params.hasOwnProperty('id')) {
            throw new Error('id required');
        }
        var options = imgur.extend(params, {
            id: params.id
        });
        return imgur.request('post', 'image/{id}/favorite', options);
    };

    return this;

};

module.exports = Image;

There is an oauth endpoint that looks like this.

var Oauth = function(imgur) {

    this.getOauthUrl = function() {
        return 'https://api.imgur.com/oauth2/authorize?client_id=' + imgur.options.clientId + '&response_type=code';
    };

    this.authorizeCallback = function(authCode) {
        if (typeof authCode === "undefined" || !authCode) {
            throw new Error('authorization code required');
        }
        var options = {
            data: {
                client_id: imgur.options.clientId,
                client_secret: imgur.options.clientSecret,
                grant_type: 'authorization_code',
                code: authCode
            }
        };
        return imgur.request('post', 'https://api.imgur.com/oauth2/token', options).then(function(data) {
            data.expiresAt = Math.round(Date.now() + (data.expires_in * 1000));
            return data;
        });
    };

    this.refreshToken = function(refreshToken) {
        if (typeof refreshToken === "undefined" || !refreshToken) {
            throw new Error('No refresh token defined');
        }
        var options = {
            data: {
                client_id: imgur.options.clientId,
                client_secret: imgur.options.clientSecret,
                grant_type: 'refresh_token',
                refresh_token: refreshToken
            }
        };
        return imgur.request('post', 'https://api.imgur.com/oauth2/token', options).then(function(data) {
            data.expiresAt = Math.round(Date.now() + (data.expires_in * 1000));
            return data;
        });
    };

    return this;

};

module.exports = Oauth;

It is all pulled together with the following code.

var Imgur = require('./imgurjs');
var imgur = new Imgur({
    clientId: '<clientid>',
    clientSecret: '<clientsecret>'
});

An example of using it would be

imgur.image.favourite({id: 1})
    .then(function(response) {
        console.log(response);
    })
    .catch(function(error) {
        console.log(error);
    });

You can authenticate using the oauth like so (example uses express)

app.get('/login', function(req, res) {
    res.redirect(imgur.oauth.getOauthUrl());
});

app.get('/loginCallback', function(req, res) {
    imgur.oauth.authorizeCallback(req.query.code).then(function(user) {
        req.session.user = user;
        res.redirect('/');
    });
});

Then you can access authenticated routes by passing in the token stored in the session

imgur.image.delete(imgur.extend({id: 1}, req.session.user))
//or imgur.image.delete({id: 1, access_token: req.session.user.access_token})
    .then(function(response) {

    });

I feel that there's a better way to do this rather than passing the oauth token to every method, but I can't get my head around it.

Is there a way to have it so I only need to call the constructor once to instantiate the client id and secret, but also have an instance for each user's session which would store their oauth token.

\$\endgroup\$
3
  • \$\begingroup\$ by the way, please don't modify the code in your question after receiving answers (unless it's to fix something silly like a typo) \$\endgroup\$
    – Dan
    Mar 12, 2015 at 10:59
  • \$\begingroup\$ @DanPantry Sorry, I was just adding an alternative way to pass the oauth token in case it wasn't clear. \$\endgroup\$ Mar 12, 2015 at 11:00
  • \$\begingroup\$ just giving fair warning :P \$\endgroup\$
    – Dan
    Mar 12, 2015 at 11:00

1 Answer 1

2
+50
\$\begingroup\$

Short review ahead - I'll post a longer one during my lunch break if no one else has.

You mention this:

I feel that there's a better way to do this rather than passing the oauth token to every method, but I can't get my head around it.

I don't have enough time (before lunch) to look over this and learn the API enough to fully review it, but you could try something like this:

module.exports = (function() {
  return function(token) {
    var favourite = function() {
      // use the token in some manner
    };

    return {
      favourite: favourite
    };
  }
}());

var Image = require('./image');
var image = Image(oauthToken);
image.favourite();

This will mean you only ever have to pass the token in once. The downside is that you won't be able to automatically 'refresh' a token - the user will have to invoke the exported function again to refresh the token (or you could add a method on the image export to do this as well)

\$\endgroup\$
3
  • \$\begingroup\$ Just an FYI, there's more than one endpoint. (gallery/album/account) etc. There are too many to post in the question though. \$\endgroup\$ Mar 12, 2015 at 11:03
  • \$\begingroup\$ I'd also like to call everything from the constructor. So imgur.image(oauthToken).favourite() maybe. \$\endgroup\$ Mar 12, 2015 at 11:10
  • \$\begingroup\$ That's what this code sample addresses. You could easily make imgur.image = Image, where Image is as it is in the code sample above - ie, a function that takes oauthToken. :-) Yeah, I gathered there is more than one endpoint. \$\endgroup\$
    – Dan
    Mar 12, 2015 at 11:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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