Skip to content
This repository has been archived by the owner on Apr 19, 2018. It is now read-only.

Email pin auth and tests #34

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
sudo: false
sudo: required
language: node_js
node_js:
- "4"
- "6"
env:
global:
- PGPORT=5433
- PGHOST=localhost
- DATABASE_URL=postgres://localhost:5433/spacekit_test
- CXX=g++-4.8
services:
- postgresql
addons:
apt:
sources:
- ubuntu-toolchain-r-test
- precise-pgdg-9.5
packages:
- g++-4.8
env:
- CXX=g++-4.8
- postgresql-9.5
- postgresql-contrib-9.5
postgresql: 9.5
before_script:
- sudo cp /etc/postgresql/9.4/main/pg_hba.conf /etc/postgresql/9.5/main/pg_hba.conf
- sudo /etc/init.d/postgresql restart
- psql -c 'CREATE DATABASE spacekit_test;' -U postgres
58 changes: 47 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,65 @@ $ npm install spacekit-service -g

## Usage

If there is a `spacekit-service.json` file in the directory you run
`spacekit-service` from, we'll use it to configure the client. Typical
options are as follows:
You'll need a `spacekit-service.json` file in the directory you run the
service. Here's an example config:

```json
{
"pg": "postgres://username:password@host:port/dbname",
"smtpUser": null,
"smtpPass": null,
"awsHostedZoneId": null,
"awsAccessKeyId": null,
"awsSecretAccessKey": null,
"host": "spacekit.io"
"postgres": "postgres://username:password@host:port/dbname",
"service": {
"domain": "spacekit.io",
"subdomains": {
"api": "api.spacekit.io",
"web": "www.spacekit.io"
},
"ports": {
"http": 80,
"https": 443,
"range": {
"start": 8000,
"end": 8999
}
}
},
"letsEncrypt": {
"email": "[email protected]"
},
"aws": {
"hostedZoneId": null,
"accessKeyId": null,
"secretAccessKey": null,
"recordSetTtl": 1
},
"mail": {
"from": {
"name": "SpaceKit",
"address": "[email protected]"
}
},
"smtp": {
"host": "smtp.gmail.com",
"secure": true,
"user": "[email protected]",
"pass": null
}
}
```


## Logs

Log files will be stored in the directory you run `spacekit-service` from.
They're named `spacekit-service.log` and will rotate for 3 days
(`spacekit-service.log.0`, `spacekit-service.log.1`, etc..).


## Certificates

Certificate files will be stored in the directory you run `spacekit-service`
from under the `./certs` folder.


## License

Apache License, Version 2.0
MIT
13 changes: 13 additions & 0 deletions bin/api-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env node
'use strict';
const ApiServer = require('../lib/api');
const Http = require('http');

const server = Http.createServer();
const port = process.env.PORT || 3000;

ApiServer(server);

server.listen(port, () => {
console.log(`listening on port ${port}`);
});
13 changes: 13 additions & 0 deletions bin/web-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env node
'use strict';
const WebServer = require('../lib/web');
const Http = require('http');

const server = Http.createServer();
const port = process.env.PORT || 3000;

WebServer(server);

server.listen(port, () => {
console.log(`listening on port ${port}`);
});
169 changes: 169 additions & 0 deletions lib/api/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
'use strict';
const Async = require('async');
const Boom = require('boom');
const Joi = require('joi');
const Mailer = require('../mailer');
const Secret = require('../secret');
const Session = require('../db/session');
const User = require('../db/user');

exports.register = function (server, options, next) {
server.route({
method: 'POST',
path: '/auth/challenge',
config: {
validate: {
payload: {
username: Joi.string().token().lowercase().required(),
email: Joi.string().email().lowercase().required()
}
},
pre: [{
assign: 'userLookup',
method: function (request, reply) {
const username = request.payload.username;
const email = request.payload.email;

User.findOneByCredentials(username, email, (err, user) => {
if (err) {
return reply(Boom.badImplementation('exception', err));
}

if (!user) {
return reply(Boom.conflict('User not found.'));
}

reply(user);
});
}
}]
},
handler: function (request, reply) {
Async.auto({
pin: function (done) {
Secret.createPin(done);
},
challenge: ['pin', function (results, done) {
const userId = request.pre.userLookup.id;
const challenge = results.pin.hash;
const expires = new Date(Date.now() + 10000000);

User.setChallenge(userId, challenge, expires, done);
}],
sendMail: ['challenge', function (results, done) {
const options = {
subject: 'SpaceKit auth challenge',
to: request.pre.userLookup.email
};
const template = 'auth-challenge';
const context = {
pin: results.pin.plain
};

Mailer.sendEmail(options, template, context, done);
}]
}, (err, results) => {
if (err) {
return reply(Boom.badImplementation('exception', err));
}

const message = 'An email will be sent with a challenge code.';

reply({ message: message });
});
}
});

server.route({
method: 'POST',
path: '/auth/answer',
config: {
validate: {
payload: {
username: Joi.string().token().lowercase().required(),
email: Joi.string().email().lowercase().required(),
pin: Joi.number().integer().min(100000).max(999999).required()
}
},
pre: [{
assign: 'userLookup',
method: function (request, reply) {
const username = request.payload.username;
const email = request.payload.email;

User.findOneByCredentials(username, email, (err, user) => {
if (err) {
return reply(Boom.badImplementation('exception', err));
}

if (!user) {
return reply(Boom.conflict('User not found.'));
}

if (user.challenge === null) {
return reply(Boom.conflict('Challenge not set.'));
}

const expiration = new Date(user.challenge_expires);

if (expiration < new Date()) {
return reply(Boom.conflict('Challenge expired.'));
}

reply(user);
});
}
}, {
assign: 'comparePin',
method: function (request, reply) {
const pin = request.payload.pin;
const pinHash = request.pre.userLookup.challenge;

Secret.compare(pin, pinHash, (err, pass) => {
if (err) {
return reply(Boom.badImplementation('exception', err));
}

if (!pass) {
return reply(Boom.conflict('Incorrect pin.'));
}

reply(true);
});
}
}]
},
handler: function (request, reply) {
Async.auto({
disableChallenge: function (done) {
const userId = request.pre.userLookup.id;
const challenge = null;
const expires = new Date();

User.setChallenge(userId, challenge, expires, done);
},
authKey: function (done) {
Secret.createUuid(done);
},
session: ['authKey', function (results, done) {
const userId = request.pre.userLookup.id;
const authKey = results.authKey.hash;

Session.create(userId, authKey, done);
}]
}, (err, results) => {
if (err) {
return reply(Boom.badImplementation('exception', err));
}

reply({ authKey: results.authKey.plain });
});
}
});

next();
};

exports.register.attributes = {
name: 'auth'
};
17 changes: 17 additions & 0 deletions lib/api/hello.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';

exports.register = function (server, options, next) {
server.route({
method: 'GET',
path: '/',
handler: function (request, reply) {
reply({ message: 'Welcome to the SpaceKit api.' });
}
});

next();
};

exports.register.attributes = {
name: 'hello'
};
52 changes: 16 additions & 36 deletions lib/api/index.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,25 @@
'use strict';
const BodyParser = require('body-parser');
const Cors = require('cors');
const Express = require('express');
const Uuid = require('node-uuid');
const Auth = require('./auth');
const Hapi = require('hapi');
const Hello = require('./hello');
const Signup = require('./signup');

const CreateLogger = require('../create-logger');
const Db = require('../db');
const DynamicDns = require('../dynamic-dns');
const Mailer = require('../mailer');
const Recover = require('./recover');
const Reset = require('./reset');
const SignUp = require('./signup');
module.exports = function (server) {
const api = new Hapi.Server();

const log = CreateLogger('ApiApp');

module.exports = function (config) {
let api = Express();

api.config = config;
api.mailer = new Mailer(config.nodemailer, config.smtpFrom);
api.db = new Db(config.pg);
if (config.awsHostedZoneId) {
api.dynamicDns = new DynamicDns(config);
}

api.use(Cors());
api.use(BodyParser.json());
api.use(BodyParser.urlencoded({ extended: true }));

api.use((req, res, next) => {
req.log = log.child({ reqId: Uuid.v4() });
next();
api.connection({
listener: server
});

api.get('/', (req, res, next) => {
res.json({ message: 'Welcome to the SpaceKit api.' });
});
const plugins = [Auth, Hello, Signup];

api.post('/recover', Recover);
api.post('/reset', Reset);
api.post('/signup', SignUp);
api.register(plugins, (err) => {
/* $lab:coverage:off$ */
if (err) {
throw err;
}
/* $lab:coverage:on$ */
});

return api;
};
Loading