Skip to content

Username & Password Connector Example

smurthas edited this page Jul 21, 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 "IMAPExample" as the name so nothing bad happens):

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

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

mv skeleton.connector IMAPExample.connector

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

{
    "title":"IMAP Example",
    "action":"Connect to a IMAP account",
    "desc":"Collect and sync my data from my IMAP account",
    ...
    "handle":"imapexample",
    "mongoCollections": ["messages"],
    "provides":["message/imapexample"]
}

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

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

This line 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 IMAPExample 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.hasOwnProperty('username') && authData.hasOwnProperty('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 IMAP server info that will be used to sync your data" + 
                "<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 {
        sys.debug('redirecting to ' + uri);
        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, assembly 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. "IMAP Example" should now be listed as an installable connector. Clicking the Install Connector button should now install the connector and take you to the auth page. Follow the instructions from there.

You've made your first connector!

Clone this wiki locally