Skip to content

Username & Password Connector Example

smurthas edited this page Jul 26, 2011 · 37 revisions

The easiest way to get going is to copy and paste the code in Locker/Connectors/skeleton into a new directory for your connector. Let's pretend there isn't a yet a IMAP connector (we'll use "GmailExample" as the name so nothing bad happens):

cd Connectors
mkdir GmailExample
cd GmailExample
cp -r ../skeleton/* .

The first thing we'll want to do is change the name of the .connector manifest file:

mv skeleton.connector GmailExample.connector

Now let's edit that file. The only things that MUST be changed are the handle, mongoCollections, and provides fields.

{
    "title":"Gmail Example",
    "action":"Connect to a Gmail account",
    "desc":"Collect and sync my data from my Gmail account",
    "run":"node init.js",
    "status":"unstable",
    "handle":"gmailexample",
    "mongoCollections": ["messages"],
    "provides":["message/gmailexample"]
}

More info can be found about the provides field on the service types page.

Next let's take a look at the init.js file. The require('connector/client').init({...}) function loads the common connector bootstrap file and initializes it with some startup info. For more details about the init.js file and client.init function, see the Custom Files section below.

For username/password auth, there is no init data needed, so in the init.js file:

require('connector/client').init({});

We don't have a generic username/password module like the OAuth 2 module, so we'll have to write our own. In this case, the twitter-js client library, provides almost all of the functionality, we just need to provide some endpoints. In the GmailExample directory, create an auth.js file that looks like this:

var request = require('request'),
    lfs = require('lfs'),
    lcrypto = require("lcrypto"),
    fs = require('fs');

var lconfig = require('lconfig');
//TODO: fix lconfig and remove this!
lconfig.load('../../config.json');
var completedCallback, uri;

exports.auth = {};

//TODO: this is almost definitely a race condition!
lcrypto.loadKeys(function(){});

exports.authAndRun = function(app, externalUrl, onCompletedCallback) {
    uri = externalUrl;
    
    if(exports.isAuthed()) {
        onCompletedCallback();
        return;
    }
    completedCallback = onCompletedCallback;
    app.get('/auth', go);
    app.post('/saveAuth', saveAuth);
};

exports.isAuthed = function() {
    try {
        if(!exports.auth)
            exports.auth = {};
      
        if(exports.auth.username && exports.auth.password)
            return true;
    
        var authData = JSON.parse(fs.readFileSync('auth.json', 'utf-8'));
        
        if(authData.username && authData.password) {
            authData.username = lcrypto.decrypt(authData.username);
            authData.password = lcrypto.decrypt(authData.password);
            exports.auth = authData;
            return true;
        }
    } catch (E) {
        if(E.code !== 'EBADF')
            console.error(E);
    }
    return false;
};

function go(req, res) {
    if(!(exports.auth.username && exports.auth.password)) {
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end("<html>Enter your username and password" + 
                "<form method='post' action='saveAuth'>" + 
                    "Username: <input name='username'><br>" +
                    "Password: <input type='password' name='password'><br>" +
                    "<input type='submit' value='Save'>" +
                "</form></html>");
    } else {
        res.redirect(uri);
    }
}

function saveAuth(req, res) {
    if(!req.body.username || !req.body.password) {
        res.writeHead(200, {'Content-Type': 'text/html'});
        res.end("missing field(s)?");
        return;
    }
    lcrypto.loadKeys(function() {
        // We put in the encrypted values for writing to the disk
        exports.auth.username = lcrypto.encrypt(req.body.username);
        exports.auth.password = lcrypto.encrypt(req.body.password);
        exports.auth.host = 'imap.gmail.com';
        exports.auth.port = 993;
        exports.auth.secure = true;
        lfs.writeObjectToFile('auth.json', exports.auth);
        // Put back the non encrypted values
        exports.auth.username = req.body.username;
        exports.auth.password = req.body.password;
        completedCallback(exports.auth);
        res.redirect(uri);
    });
}

Next, let's edit the sync.js file. Let's change the syncItems function to syncInbox and add the node imap module to the require section:

var fs = require('fs'),
    ...
    EventEmitter = require('events').EventEmitter,
    ImapConnection = require('imap').ImapConnection;

...

// Syncs the inbox of the auth'd user
exports.syncInbox = function(callback) {
    var messages = [];
    var imap = new ImapConnection(auth);
    imap.connect(function() {
        imap.openBox('INBOX', false, function(err, box) {
            var fetch = imap.fetch(['1:*'], { request: { headers: ['from', 'to', 'subject', 'date'] } });
            fetch.on('message', function(msg) {
                msg.on('end', function() {
                    messages.push(msg);
                });
            });
            fetch.on('end', function() {
                imap.logout(function() {
                    saveMessages(messages, callback);
                });
            });
        });
    });
}

function saveMessages(msgs, callback) {
    if(!msgs || !msgs.length) {
        process.nextTick(callback);
        return;
    }
    var msg = msgs.shift();
    dataStore.addObject('messages', msg, function(err) {
        var eventObj = {source:'friends', type:'new', data:msg};
        exports.eventEmitter.emit('message/imapexample', eventObj);
        saveMessages(msgs, callback);
    });
}

Now let's change sync-api.js to call our new syncInbox function and make a slight tweak to the index function:

function index(req, res) {
    if(!(auth && auth.username && auth.password))
        ...
}

function items(req, res) {
    ...
    sync.syncInbox(function(err, repeatAfter, diaryEntry) {
        ...
    });
}

Optional Cleanup

At this point, the connector is in a functioning state, but if we'd like, we can tailor the sync-api.js file to represent what we are doing (if not, JMP Done!). Let's change the name of the /items endpoint to /updateMessages:

app.get('/items', items);

becomes:

app.get('/updateMessages', updateMessages);

and then we change the items function to updateMessages:

function index(req, res) {
    if(!(auth && auth.username && auth.password))
        ...
}

function items(req, res) {
    ...
    sync.syncInbox(function(err, repeatAfter, diaryEntry) {
        ...
    });
}

// this is the basic structure of an endpoint for something you'd be parsing.
function updateMessages(req, res) {
    ...
    locker.at('/updateProfile', repeatAfter);
    ...
}

and finally edit the html in the index function to point to /updateMessages instead of /items:

res.end("<html>found a token, load <a href='updateMessages'>messages</a>");

Done!

All we need to do is run.

cd ../..
node lockerd.js
  • Then open up the dashboard at http://localhost:8042 and navigate to the Services section.
  • Click the "[show unstable]" link in the top right hand corner. "Gmail Example" should now be listed as an installable connector.
  • Click the Install Connector button.

The connector should now install and take you to the auth page. Follow the instructions from there.

You've made your first connector!

Clone this wiki locally