Browse files

Merge pull request #51 from hueniverse/develop

hapi 0.10.x
  • Loading branch information...
2 parents 4a7c845 + 3d18972 commit 4030a671a1ec6b18688fca1e3494d6310ebc2078 @hueniverse committed Jan 2, 2013
View
13 Readme.md
@@ -18,7 +18,7 @@ $ cd api
$ cp vault.js.example vault.js
```
-Edit postmile/api/vault.js and set the values of the 'aes256Key' variables to different random secrets sufficiently long (e.g. 40 characters).
+Edit postmile/api/vault.js and set the values of the 'aes256Key' and 'passowrd' variables to different random secrets sufficiently long (e.g. 40 characters).
If your MongoDB requires authentication, set the values of the database 'username' and 'password' (otherwise leave empty).
@@ -28,10 +28,12 @@ $ node install
110827/005720.948, info, Database initialized
110827/005720.952, info, Initial dataset created successfully
-110827/005720.952, info, >>>>> postmile.web client secret: __some__secret__
+110827/005720.952, info, >>>>> WEB client id: <id>
+110827/005720.952, info, >>>>> WEB client secret: <secret>
+110827/005720.952, info, >>>>> VIEW client id: <id>
```
-Copy the postmile.web client secret and save it for later.
+Copy the WEB client id and secret, and VIEW client id, and save them for later.
```bash
$ cd ../web
@@ -41,7 +43,8 @@ $ cp vault.js.example vault.js
Edit postmile/web/vault.js and set the values of the 'aes256Key' variables to different random secrets sufficiently long (e.g. 40 characters).
-Set the value of the postmileAPI 'clientSecret' variable to the client secret saved earlier.
+Set the values of the postmileAPI 'clientId' and 'clientSecret' variables to the WEB client id and secret saved earlier.
+Set the value of the 'viewClientId' variable to the VIEW client id saved earlier.
Enter at least one third-party API credentials (Twitter, Facebook, or Yahoo!) as received from each provider when you registered the application.
If asked, the callback URI is your web server configuration entered above with the path '/auth/twitter', '/auth/facebook', or '/auth/yahoo'.
@@ -53,7 +56,7 @@ $ cd ..
```
Make sure to protect your vault.js files. If an attacker gets hold of them, you're screwed.
-If you are going to run this in a production environment, you should use TLS (HTTPS) for the web server (otherwise it's cookies and OAuth 2.0 bits are
+If you are going to run this in a production environment, you should use TLS (HTTPS) for the web server (otherwise it's cookies and Oz bits are
pretty open for attacks). To configure TLS, set the 'process.web.tls' variable in the postmile/config.js file to point to your TLS key and certificate.
# Startup
View
302 api/batch.js
@@ -1,302 +0,0 @@
-/*
-* Copyright (c) 2011 Eran Hammer-Lahav. All rights reserved. Copyrights licensed under the New BSD License.
-* See LICENSE file included with this code project for license terms.
-*/
-
-// Load modules
-
-var Hapi = require('hapi');
-var Http = require('http');
-var MAC = require('mac');
-var Config = require('./config');
-
-
-// Declare internals
-
-var internals = {};
-
-
-// Batch processing
-
-exports.post = {
-
- schema: {
-
- get: Hapi.Types.Array().required().includes(Hapi.Types.String())
- },
-
- handler: function (request) {
-
- var requests = [];
- var results = [];
- var resultsMap = {};
-
- function entry() {
-
- var requestRegex = /(?:\/)(?:\$(\d)+\.)?([\w:\.]+)/g; // /project/$1.project/tasks, does not allow using array responses
-
- // Validate requests
-
- var error = null;
- var parseRequest = function ($0, $1, $2) {
-
- if ($1) {
-
- if ($1 < i) {
-
- if ($1.indexOf(':') === -1) {
-
- parts.push({ type: 'ref', index: $1, value: $2 });
- return '';
- }
- else {
-
- error = 'Request reference includes invalid ":" character (' + i + ')';
- return $0;
- }
- }
- else {
-
- error = 'Request reference is beyond array size (' + i + ')';
- return $0;
- }
- }
- else {
-
- parts.push({ type: 'text', value: $2 });
- return '';
- }
- };
-
- for (var i = 0, il = request.payload.get.length; i < il; ++i) {
-
- // Break into parts
-
- var parts = [];
- var result = request.payload.get[i].replace(requestRegex, parseRequest);
-
- // Make sure entire string was processed (empty)
-
- if (result === '') {
-
- requests.push(parts);
- }
- else {
-
- error = error || 'Invalid request format (' + i + ')';
- break;
- }
- }
-
- if (error === null) {
-
- process();
- }
- else {
-
- request.reply(Hapi.Error.badRequest(error));
- }
- }
-
- function process() {
-
- batch(0, function () {
-
- // Return results
-
- request.reply(results);
- });
- }
-
- function batch(pos, callback) {
-
- if (pos >= requests.length) {
-
- callback();
- }
- else {
-
- // Prepare request
-
- var parts = requests[pos];
- var path = '';
- var error = null;
-
- for (var i = 0, il = parts.length; i < il; ++i) {
-
- path += '/';
-
- if (parts[i].type === 'ref') {
-
- var ref = resultsMap[parts[i].index];
- if (ref) {
-
- var value = null;
-
- try {
-
- eval('value = ref.' + parts[i].value + ';');
- }
- catch (e) {
-
- error = e.message;
- }
-
- if (value) {
-
- if (value.match(/^[\w:]+$/)) {
-
- path += value;
- }
- else {
-
- error = 'Reference value includes illegal characters';
- break;
- }
- }
- else {
-
- error = error || 'Reference not found';
- break;
- }
- }
- else {
-
- error = 'Missing reference response';
- break;
- }
- }
- else {
-
- path += parts[i].value;
- }
- }
-
- if (error === null) {
-
- // Make request
-
- internals.call('GET', path, null, request.session, function (data, err) {
-
- if (err === null) {
-
- // Process response
-
- results.push(data);
- resultsMap[pos] = data;
- }
- else {
-
- results.push(err);
- }
-
- // Call next
-
- batch(pos + 1, callback);
- });
- }
- else {
-
- // Set error response (as string)
-
- results.push(error);
-
- // Call next
-
- batch(pos + 1, callback);
- }
- }
- }
-
- entry();
- }
-};
-
-
-// Make API call
-
-internals.call = function (method, path, content, arg1, arg2) { // session, callback
-
- var callback = arg2 || arg1;
- var session = (arg2 ? arg1 : null);
- var body = content !== null ? JSON.stringify(content) : null;
-
- var authorization = null;
-
- if (session) {
-
- authorization = MAC.getAuthorizationHeader(method, path, Config.host.api.domain, Config.host.api.port, session);
-
- if (authorization === null ||
- authorization === '') {
-
- callback(null, 'Failed to create authorization header: ' + session);
- }
- }
-
- var hreq = Http.request({ host: Config.host.api.domain, port: Config.host.api.port, path: path, method: method }, function (hres) {
-
- if (hres) {
-
- var response = '';
-
- hres.setEncoding('utf8');
- hres.on('data', function (chunk) {
-
- response += chunk;
- });
-
- hres.on('end', function () {
-
- var data = null;
- var error = null;
-
- try {
-
- data = JSON.parse(response);
- }
- catch (err) {
-
- error = 'Invalid response body from API server: ' + response + '(' + err + ')';
- }
-
- if (error) {
-
- callback(null, error);
- }
- else if (hres.statusCode === 200) {
-
- callback(data, null);
- }
- else {
-
- callback(null, data);
- }
- });
- }
- else {
-
- callback(null, 'Failed sending API server request');
- }
- });
-
- hreq.on('error', function (err) {
-
- callback(null, 'HTTP socket error: ' + JSON.stringify(err));
- });
-
- if (authorization) {
-
- hreq.setHeader('Authorization', authorization);
- }
-
- if (body !== null) {
-
- hreq.setHeader('Content-Type', 'application/json');
- hreq.write(body);
- }
-
- hreq.end();
-};
-
-
View
21 api/details.js
@@ -19,7 +19,7 @@ exports.get = {
query: {
- since: Hapi.Types.Number().min(0)
+ since: Hapi.types.Number().min(0)
},
handler: function (request) {
@@ -83,18 +83,15 @@ exports.get = {
// Add task detail
exports.post = {
-
- query: {
-
- last: Hapi.Types.Boolean()
- },
-
- schema: {
-
- type: Hapi.Types.String().required().valid('text'),
- content: Hapi.Types.String().required()
+ validate: {
+ query: {
+ last: Hapi.types.Boolean()
+ },
+ schema: {
+ type: Hapi.types.String().required().valid('text'),
+ content: Hapi.types.String().required()
+ }
},
-
handler: function (request) {
var now = Date.now();
View
5 api/email.js
@@ -12,6 +12,7 @@ var Db = require('./db');
var Vault = require('./vault');
var User = require('./user');
var Config = require('./config');
+var Utils = require('./utils');
// Declare internals
@@ -30,7 +31,7 @@ exports.generateTicket = function (user, email, arg1, arg2) {
var now = Date.now();
var ticketId = now.toString(36); // assuming users cannot generate more than one ticket per msec
- var token = Hapi.Session.encrypt(Vault.emailToken.aes256Key, [user._id, ticketId]);
+ var token = Utils.encrypt(Vault.emailToken.aes256Key, [user._id, ticketId]);
var ticket = { timestamp: now, email: email };
@@ -103,7 +104,7 @@ exports.loadTicket = function (token, callback) {
// Decode ticket
- var record = Hapi.Session.decrypt(Vault.emailToken.aes256Key, token);
+ var record = Utils.decrypt(Vault.emailToken.aes256Key, token);
if (record &&
record instanceof Array &&
View
103 api/index.js
@@ -21,106 +21,73 @@ var Vault = require('./vault');
var internals = {};
-// Post handler extension middleware
-
-internals.onPostHandler = function (request, next) {
-
- if (request.response &&
- request.response.result) {
+// Catch uncaught exceptions
- var result = request.response.result;
+process.on('uncaughtException', function (err) {
+ Hapi.Utils.abort('Uncaught exception: ' + err.stack);
+});
- // Sanitize database fields
- if (result._id) {
+// Post handler extension middleware
- result.id = result._id;
- delete result._id;
- }
+internals.formatPayload = function (payload) {
- if (result instanceof Object) {
+ if (typeof payload !== 'object' ||
+ payload instanceof Array) {
- for (var i in result) {
+ return payload;
+ }
- if (result.hasOwnProperty(i)) {
+ // Sanitize database fields
- if (i[0] === '_') {
+ if (payload._id) {
+ payload.id = payload._id;
+ delete payload._id;
+ }
- delete result[i];
- }
- }
+ for (var i in payload) {
+ if (payload.hasOwnProperty(i)) {
+ if (i[0] === '_') {
+ delete payload[i];
}
}
}
- next();
+ return payload;
};
-// Catch uncaught exceptions
-
-process.on('uncaughtException', function (err) {
- Hapi.Utils.abort('Uncaught exception: ' + err.stack);
-});
-
+// Create server
var configuration = {
-
- name: 'http',
-
- // Extension points
-
- ext: {
- onPostHandler: internals.onPostHandler
+ format: {
+ payload: internals.formatPayload
},
+ auth: {
+ scheme: 'oz',
+ encryptionPassword: Vault.ozTicket.password,
- // Authentication
-
- authentication: {
-
- loadClientFunc: Session.loadClient,
- loadUserFunc: Session.loadUser,
- extensionFunc: Session.extensionGrant,
- checkAuthorizationFunc: Session.checkAuthorization,
- aes256Keys: {
-
- oauthRefresh: Vault.oauthRefresh.aes256Key,
- oauthToken: Vault.oauthToken.aes256Key
- },
- tos: {
- min: '20110623'
- }
+ loadAppFunc: Session.loadApp,
+ loadGrantFunc: Session.loadGrant,
+ tos: 20110623
},
-
debug: true,
monitor: true
};
var server = new Hapi.Server(Config.host.api.domain, Config.host.api.port, configuration);
server.addRoutes(Routes.endpoints);
-// Initialize database connection
-
Db.initialize(function (err) {
- if (err === null) {
-
- // Load in-memory cache
-
- Suggestions.initialize();
- Tips.initialize();
-
- // Start Server
-
- server.start();
- Stream.initialize(server.listener);
- }
- else {
-
- // Database connection failed
-
+ if (err) {
Hapi.Log.event('err', err);
process.exit(1);
}
+
+ Suggestions.initialize();
+ Tips.initialize();
+ server.start();
+ Stream.initialize(server.listener);
});
View
11 api/install.js
@@ -2,6 +2,7 @@
var Hapi = require('hapi');
var Db = require('./db');
+var Utils = require('./utils');
// Initialize database connection
@@ -18,13 +19,13 @@ Db.initialize(true, function (err) {
{
name: 'postmile.web',
- scope: { authorized: true, login: true, reminder: true, signup: true, tos: true },
- secret: Hapi.Session.getRandomString(64)
+ scope: ['authorized', 'login', 'reminder', 'signup', 'tos'],
+ secret: Utils.getRandomString(64)
},
{
name: 'postmile.view',
- scope: {}
+ scope: []
}
];
@@ -39,7 +40,9 @@ Db.initialize(true, function (err) {
if (err === null) {
Hapi.Log.event('info', 'Initial dataset created successfully');
- Hapi.Log.event('info', '>>>>> postmile.web client secret: ' + clients[0].secret);
+ Hapi.Log.event('info', '>>>>> WEB client id: ' + clients[0]._id);
+ Hapi.Log.event('info', '>>>>> WEB client secret: ' + clients[0].secret);
+ Hapi.Log.event('info', '>>>>> VIEW client id: ' + clients[1]._id);
process.exit(0);
}
else {
View
8 api/package.json
@@ -1,18 +1,18 @@
{
"name": "postmile.api",
"description": "Postmile API Server",
- "version": "0.0.3",
+ "version": "0.0.4",
"author": "Eran Hammer <eran@hueniverse.com>",
"private": true,
"dependencies": {
- "hapi": "0.7.x",
+ "hapi": "0.10.x",
"mongodb": "0.9.x",
"oauth": "0.9.x",
+ "oz": "0.0.x",
"socket.io": "0.8.x",
- "mac": "0.1.x",
"opts": "1.x.x",
"validator": "0.x.x",
"emailjs": "0.x.x"
},
- "engines": { "node": "0.6.x" }
+ "engines": { "node": "0.8.x" }
}
View
109 api/project.js
@@ -26,7 +26,7 @@ var internals = {};
// Get project information
exports.get = {
-
+
handler: function (request) {
exports.load(request.params.id, request.session.user, false, function (project, member, err) {
@@ -52,7 +52,7 @@ exports.get = {
// Get list of projects for current user
exports.list = {
-
+
handler: function (request) {
Sort.list('project', request.session.user, 'participants.id', function (projects) {
@@ -112,20 +112,17 @@ exports.list = {
// Update project properties
exports.post = {
-
- query: {
-
- position: Hapi.Types.Number().min(0)
- },
-
- schema: {
-
- title: Hapi.Types.String(),
- date: Hapi.Types.String().regex(Utils.dateRegex).emptyOk(),
- time: Hapi.Types.String().regex(Utils.timeRegex).emptyOk(),
- place: Hapi.Types.String().emptyOk()
+ validate: {
+ query: {
+ position: Hapi.types.Number().min(0)
+ },
+ schema: {
+ title: Hapi.types.String(),
+ date: Hapi.types.String().regex(Utils.dateRegex).emptyOk(),
+ time: Hapi.types.String().regex(Utils.timeRegex).emptyOk(),
+ place: Hapi.types.String().emptyOk()
+ }
},
-
handler: function (request) {
exports.load(request.params.id, request.session.user, true, function (project, member, err) {
@@ -198,32 +195,28 @@ exports.post = {
// Create new project
exports.put = {
-
- schema: {
-
- title: Hapi.Types.String().required(),
- date: Hapi.Types.String().regex(Utils.dateRegex).emptyOk(),
- time: Hapi.Types.String().regex(Utils.timeRegex).emptyOk(),
- place: Hapi.Types.String().emptyOk()
+ validate: {
+ schema: {
+ title: Hapi.types.String().required(),
+ date: Hapi.types.String().regex(Utils.dateRegex).emptyOk(),
+ time: Hapi.types.String().regex(Utils.timeRegex).emptyOk(),
+ place: Hapi.types.String().emptyOk()
+ }
},
-
handler: function (request) {
var project = request.payload;
project.participants = [{ id: request.session.user }];
-
Db.insert('project', project, function (items, err) {
- if (err === null) {
-
- Stream.update({ object: 'projects', user: request.session.user }, request);
- request.created('project/' + items[0]._id);
- request.reply({ status: 'ok', id: items[0]._id });
+ if (err) {
+ return request.reply(err);
}
- else {
- request.reply(err);
- }
+ Stream.update({ object: 'projects', user: request.session.user }, request);
+ return request.reply.payload({ status: 'ok', id: items[0]._id })
+ .created('project/' + items[0]._id)
+ .send();
});
}
};
@@ -232,7 +225,7 @@ exports.put = {
// Delete a project
exports.del = {
-
+
handler: function (request) {
exports.load(request.params.id, request.session.user, false, function (project, member, err) {
@@ -315,7 +308,7 @@ exports.del = {
// Get list of project tips
exports.tips = {
-
+
handler: function (request) {
// Get project
@@ -343,7 +336,7 @@ exports.tips = {
// Get list of project suggestions
exports.suggestions = {
-
+
handler: function (request) {
// Get project
@@ -371,18 +364,15 @@ exports.suggestions = {
// Add new participants to a project
exports.participants = {
-
- query: {
-
- message: Hapi.Types.String().max(250)
- },
-
- schema: {
-
- participants: Hapi.Types.Array().includes(Hapi.Types.String()), //!! ids or emails
- names: Hapi.Types.Array().includes(Hapi.Types.String())
+ validate: {
+ query: {
+ message: Hapi.types.String().max(250)
+ },
+ schema: {
+ participants: Hapi.types.Array().includes(Hapi.types.String()), //!! ids or emails
+ names: Hapi.types.Array().includes(Hapi.types.String())
+ }
},
-
handler: function (request) {
if (request.query.message) {
@@ -502,7 +492,7 @@ exports.participants = {
// Internal fields
email: emailsNotFound[i],
- code: Hapi.Session.getRandomString(6),
+ code: Utils.getRandomString(6),
inviter: user._id
};
@@ -613,12 +603,11 @@ exports.participants = {
// Remove participant from project
exports.uninvite = {
-
- schema: {
-
- participants: Hapi.Types.Array().required().includes(Hapi.Types.String())
+ validate: {
+ schema: {
+ participants: Hapi.types.Array().required().includes(Hapi.types.String())
+ }
},
-
handler: function (request) {
// Load project for write
@@ -673,7 +662,7 @@ exports.uninvite = {
}
else if (request.payload.participants) {
- // Batch delete
+ // Batch delete
var error = null;
var uninvitedMembers = [];
@@ -808,7 +797,7 @@ exports.uninvite = {
// Accept project invitation
exports.join = {
-
+
handler: function (request) {
// The only place allowed to request a non-writable copy for modification
@@ -1070,14 +1059,14 @@ internals.leave = function (project, member, callback) {
// Move any assignments to pid account (not details) and save tasks
var taskCriteria = { project: project._id, participants: userId };
- var taskChange = { $set: { 'participants.$': 'pid:' + participant.pid} };
+ var taskChange = { $set: { 'participants.$': 'pid:' + participant.pid } };
Db.updateCriteria('task', null, taskCriteria, taskChange, function (err) {
if (err === null) {
// Save project
- Db.updateCriteria('project', project._id, { 'participants.id': userId }, { $set: { 'participants.$': participant} }, function (err) {
+ Db.updateCriteria('project', project._id, { 'participants.id': userId }, { $set: { 'participants.$': participant } }, function (err) {
if (err === null) {
@@ -1127,7 +1116,7 @@ internals.leave = function (project, member, callback) {
}
else {
- var change = { $pull: { participants: {}} };
+ var change = { $pull: { participants: {} } };
change.$pull.participants[isPid ? 'pid' : 'id'] = userId;
Db.update('project', project._id, change, function (err) {
@@ -1165,7 +1154,7 @@ exports.replacePid = function (project, pid, userId, callback) {
// Move any assignments to pid account (not details) and save tasks
var taskCriteria = { project: project._id, participants: 'pid:' + pid };
- var taskChange = { $set: { 'participants.$': userId} };
+ var taskChange = { $set: { 'participants.$': userId } };
Db.updateCriteria('task', null, taskCriteria, taskChange, function (err) {
if (err === null) {
@@ -1176,7 +1165,7 @@ exports.replacePid = function (project, pid, userId, callback) {
// Remove Pid without adding
- Db.update('project', project._id, { $pull: { participants: { pid: pid}} }, function (err) {
+ Db.update('project', project._id, { $pull: { participants: { pid: pid } } }, function (err) {
if (err === null) {
@@ -1192,7 +1181,7 @@ exports.replacePid = function (project, pid, userId, callback) {
// Replace pid with user
- Db.updateCriteria('project', project._id, { 'participants.pid': pid }, { $set: { 'participants.$': { id: userId}} }, function (err) {
+ Db.updateCriteria('project', project._id, { 'participants.pid': pid }, { $set: { 'participants.$': { id: userId } } }, function (err) {
if (err === null) {
View
86 api/routes.js
@@ -6,7 +6,6 @@
// Load modules
var Hapi = require('hapi');
-var Batch = require('./batch');
var Details = require('./details');
var Invite = require('./invite');
var Last = require('./last');
@@ -24,7 +23,8 @@ var User = require('./user');
exports.endpoints = [
- { method: 'GET', path: '/oauth/client/:id', config: Session.client },
+ { method: 'GET', path: '/oz/app/{id}', config: Session.app },
+ { method: 'POST', path: '/oz/login', config: Session.login },
{ method: 'GET', path: '/profile', config: User.get },
{ method: 'POST', path: '/profile', config: User.post },
@@ -33,52 +33,50 @@ exports.endpoints = [
{ method: 'GET', path: '/who', config: User.who },
{ method: 'PUT', path: '/user', config: User.put },
- { method: 'POST', path: '/user/:id/tos/:version', config: User.tos },
- { method: 'POST', path: '/user/:id/link/:network', config: User.link },
- { method: 'DELETE', path: '/user/:id/link/:network', config: User.unlink },
- { method: 'POST', path: '/user/:id/view/:path', config: User.view },
- { method: 'GET', path: '/user/lookup/:type/:id', config: User.lookup },
+ { method: 'POST', path: '/user/{id}/tos/{version}', config: User.tos },
+ { method: 'POST', path: '/user/{id}/link/{network}', config: User.link },
+ { method: 'DELETE', path: '/user/{id}/link/{network}', config: User.unlink },
+ { method: 'POST', path: '/user/{id}/view/{path}', config: User.view },
+ { method: 'GET', path: '/user/lookup/{type}/{id}', config: User.lookup },
{ method: 'POST', path: '/user/reminder', config: User.reminder },
{ method: 'DELETE', path: '/user', config: User.del },
{ method: 'GET', path: '/projects', config: Project.list },
- { method: 'GET', path: '/project/:id', config: Project.get },
- { method: 'POST', path: '/project/:id', config: Project.post },
+ { method: 'GET', path: '/project/{id}', config: Project.get },
+ { method: 'POST', path: '/project/{id}', config: Project.post },
{ method: 'PUT', path: '/project', config: Project.put },
- { method: 'DELETE', path: '/project/:id', config: Project.del },
- { method: 'GET', path: '/project/:id/tips', config: Project.tips },
- { method: 'GET', path: '/project/:id/suggestions', config: Project.suggestions },
- { method: 'POST', path: '/project/:id/participants', config: Project.participants },
- { method: 'DELETE', path: '/project/:id/participants', config: Project.uninvite },
- { method: 'DELETE', path: '/project/:id/participant/:user', config: Project.uninvite },
- { method: 'POST', path: '/project/:id/join', config: Project.join },
-
- { method: 'GET', path: '/project/:id/tasks', config: Task.list },
- { method: 'GET', path: '/task/:id', config: Task.get },
- { method: 'POST', path: '/task/:id', config: Task.post },
- { method: 'PUT', path: '/project/:id/task', config: Task.put },
- { method: 'DELETE', path: '/task/:id', config: Task.del },
-
- { method: 'GET', path: '/task/:id/details', config: Details.get },
- { method: 'POST', path: '/task/:id/detail', config: Details.post },
-
- { method: 'DELETE', path: '/project/:id/suggestion/:drop', config: Suggestions.exclude },
-
- { method: 'GET', path: '/project/:id/last', config: Last.getProject },
- { method: 'POST', path: '/project/:id/last', config: Last.postProject },
- { method: 'GET', path: '/task/:id/last', config: Last.getTask },
- { method: 'POST', path: '/task/:id/last', config: Last.postTask },
-
- { method: 'GET', path: '/storage/:id?', config: Storage.get },
- { method: 'POST', path: '/storage/:id', config: Storage.post },
- { method: 'DELETE', path: '/storage/:id', config: Storage.del },
-
- { method: 'GET', path: '/invite/:id', config: Invite.get },
- { method: 'POST', path: '/invite/:id/claim', config: Invite.claim },
-
- { method: 'POST', path: '/stream/:id/project/:project', config: Stream.subscribe },
- { method: 'DELETE', path: '/stream/:id/project/:project', config: Stream.unsubscribe },
-
- { method: 'POST', path: '/batch', config: Batch.post }
+ { method: 'DELETE', path: '/project/{id}', config: Project.del },
+ { method: 'GET', path: '/project/{id}/tips', config: Project.tips },
+ { method: 'GET', path: '/project/{id}/suggestions', config: Project.suggestions },
+ { method: 'POST', path: '/project/{id}/participants', config: Project.participants },
+ { method: 'DELETE', path: '/project/{id}/participants', config: Project.uninvite },
+ { method: 'DELETE', path: '/project/{id}/participant/{user}', config: Project.uninvite },
+ { method: 'POST', path: '/project/{id}/join', config: Project.join },
+
+ { method: 'GET', path: '/project/{id}/tasks', config: Task.list },
+ { method: 'GET', path: '/task/{id}', config: Task.get },
+ { method: 'POST', path: '/task/{id}', config: Task.post },
+ { method: 'PUT', path: '/project/{id}/task', config: Task.put },
+ { method: 'DELETE', path: '/task/{id}', config: Task.del },
+
+ { method: 'GET', path: '/task/{id}/details', config: Details.get },
+ { method: 'POST', path: '/task/{id}/detail', config: Details.post },
+
+ { method: 'DELETE', path: '/project/{id}/suggestion/{drop}', config: Suggestions.exclude },
+
+ { method: 'GET', path: '/project/{id}/last', config: Last.getProject },
+ { method: 'POST', path: '/project/{id}/last', config: Last.postProject },
+ { method: 'GET', path: '/task/{id}/last', config: Last.getTask },
+ { method: 'POST', path: '/task/{id}/last', config: Last.postTask },
+
+ { method: 'GET', path: '/storage/{id?}', config: Storage.get },
+ { method: 'POST', path: '/storage/{id}', config: Storage.post },
+ { method: 'DELETE', path: '/storage/{id}', config: Storage.del },
+
+ { method: 'GET', path: '/invite/{id}', config: Invite.get },
+ { method: 'POST', path: '/invite/{id}/claim', config: Invite.claim },
+
+ { method: 'POST', path: '/stream/{id}/project/{project}', config: Stream.subscribe },
+ { method: 'DELETE', path: '/stream/{id}/project/{project}', config: Stream.unsubscribe }
];
View
384 api/session.js
@@ -6,6 +6,7 @@
// Load modules
var Hapi = require('hapi');
+var Oz = require('oz');
var Crypto = require('crypto');
var Db = require('./db');
var User = require('./user');
@@ -18,291 +19,265 @@ var Vault = require('./vault');
var internals = {};
-// Get client information endpoint
+// Get application information endpoint
-exports.client = {
-
+exports.app = {
auth: {
-
scope: 'login',
- entity: 'client'
+ entity: 'app'
},
-
handler: function (request) {
- exports.loadClient(request.params.id, function (err, client) {
-
- if (err === null) {
+ Db.queryUnique('client', { name: request.params.id }, function (client, err) {
- if (client) {
-
- Hapi.Utils.removeKeys(client, ['secret', 'scope']);
- request.reply(client);
- }
- else {
-
- request.reply(Hapi.Error.notFound());
- }
+ if (err) {
+ return request.reply(err);
}
- else {
- request.reply(err);
+ if (!client) {
+ return request.reply(Hapi.Error.notFound());
}
+
+ Hapi.Utils.removeKeys(client, ['secret', 'scope']);
+ return request.reply(client);
});
}
};
-// Get client
+exports.login = {
+ validate: {
+ schema: {
+ type: Hapi.types.String().valid('id', 'twitter', 'facebook', 'yahoo', 'email').required(),
+ id: Hapi.types.String().required(),
+ issueTo: Hapi.types.String()
+ }
+ },
+ auth: {
+ scope: 'login',
+ entity: 'app'
+ },
+ handler: function (request) {
-exports.loadClient = function (id, callback) {
+ var type = request.payload.type;
+ var id = request.payload.id;
- Db.queryUnique('client', { name: id }, function (client, err) {
+ var loadUser = function () {
- if (client) {
+ if (type === 'id') {
- callback(null, client);
- }
- else {
+ User.load(id, function (user, err) {
- if (err === null) {
+ if (err) {
+ return request.reply(Hapi.Error.unauthorized(err.message));
+ }
- callback(null, null);
+ loadGrant(user);
+ });
}
- else {
+ else if (type === 'email') {
- callback(err, null);
- }
- }
- });
-};
+ Email.loadTicket(id, function (emailTicket, user, err) {
+
+ if (err) {
+ return request.reply(Hapi.Error.unauthorized(err.message));
+ }
+ loadGrant(user, { 'action': emailTicket.action });
+ });
+ }
+ else {
+
+ // twitter, facebook, yahoo
-// Get user authentication information
+ User.validate(id, type, function (err, user) {
-exports.loadUser = function (id, callback) {
+ if (err || !user) {
+ return request.reply(Hapi.Error.unauthorized());
+ }
- User.load(id, function (user, err) {
+ loadGrant(user);
+ });
+ }
+ };
- if (user) {
+ var loadGrant = function (user, ext) {
- callback(null, user);
- }
- else {
+ // Lookup existing grant
- callback(err);
- }
- });
-};
+ var now = Date.now();
+ var appId = request.payload.issueTo || request.session.app;
+ Db.query('grant', { user: user.id, app: appId }, function (items, err) {
-// Check client authorization grant
+ if (err) {
+ return request.reply(err);
+ }
-exports.checkAuthorization = function (userId, clientId, callback) {
+ if (items &&
+ items.length > 0) {
- Db.query('grant', { user: userId, client: clientId }, function (items, err) {
+ items.sort(function (a, b) {
- if (err === null) {
+ if (a.exp < b.exp) {
+ return -1;
+ }
- if (items &&
- items.length > 0) {
+ if (a.exp > b.exp) {
+ return 1;
+ }
- items.sort(function (a, b) {
+ return 0;
+ });
- if (a.expiration < b.expiration) {
+ var grant = null;
- return -1;
+ var expired = [];
+ for (var i = 0, il = items.length; i < il; ++i) {
+ if ((items[i].exp || 0) <= now) {
+ expired.push(items[i]._id);
+ }
+ else {
+ grant = items[i];
+ }
}
- if (a.expiration > b.expiration) {
-
- return 1;
+ if (expired.length > 0) {
+ Db.removeMany('grant', expired, function (err) { }); // Ignore callback
}
- return 0;
- });
+ if (grant) {
+ return issue(appId, grant._id, ext);
+ }
+ }
- var isAuthorized = false;
- var now = Date.now();
+ // No active grant
- var expired = [];
- for (var i = 0, il = items.length; i < il; ++i) {
+ var newGrant = {
+ user: user._id,
+ app: appId,
+ exp: now + 30 * 24 * 60 * 60 * 1000, // 30 days //////////////////
+ scope: [] // Find app scope ////////////
+ };
- if ((items[i].expiration || 0) <= now) {
+ Db.insert('grant', newGrant, function (items, err) {
- expired.push(items[i]._id);
+ if (err) {
+ return request.reply(err);
}
- else {
- isAuthorized = true;
+ if (items.length !== 1 ||
+ !items[0]._id) {
+
+ return request.reply(Hapi.Error.internal('Failed to add new grant'));
}
- }
- if (expired.length > 0) {
+ return issue(appId, items[0]._id, ext);
+ });
+ });
+ };
- Db.removeMany('grant', expired, function (err) {}); // Ignore callback
- }
+ var issue = function (appId, grantId, ext) {
- if (isAuthorized) {
+ Oz.rsvp.issue({ id: appId }, { id: grantId }, Vault.ozTicket.password, function (err, rsvp) {
- callback(null);
+ if (err) {
+ return request.reply(Hapi.Error.internal('Failed generating rsvp: ' + err));
}
- else {
- callback(Hapi.Error._oauth('invalid_grant', 'Client authorization expired'));
+ var response = {
+ rsvp: rsvp
+ };
+
+ if (ext) {
+ response.ext = ext;
}
- }
- else {
- callback(Hapi.Error._oauth('invalid_grant', 'Client is not authorized'));
- }
- }
- else {
+ return request.reply(response);
+ });
+ };
- callback(Hapi.Error._oauth('server_error', 'Failed retrieving authorization'));
- }
- });
+ loadUser();
+ }
};
-// Extension OAuth grant types
-
-exports.extensionGrant = function (request, client, callback) {
-
- // Verify grant type prefix
-
- if (request.payload.grant_type.search('http://ns.postmile.net/') !== 0) {
+exports.loadApp = function (id, callback) {
- // Unsupported grant type namespace
- callback(Hapi.Error._oauth('unsupported_grant_type', 'Unknown or unsupported grant type namespace'));
+ if (!id) {
+ return callback();
}
- else {
- var grantType = request.payload.grant_type.replace('http://ns.postmile.net/', '');
+ Db.get('client', id, function (client, err) {
- // Check if client has 'login' scope
-
- if ((client.scope && client.scope.login === true) ||
- (request.session && request.session.scope && request.session.scope.login === true)) {
-
- // Switch on grant type
-
- if (grantType === 'id') {
-
- // Get user
+ if (err || !client) {
+ return callback();
+ }
- User.load(request.payload.x_user_id, function (user, err) {
+ var app = {
+ id: client._id,
+ secret: client.secret,
+ scope: client.scope
+ };
- if (user) {
+ return callback(app);
+ });
+};
- callback(null, user);
- }
- else {
- // Unknown local account
- callback(Hapi.Error._oauth('invalid_grant', 'Unknown local account'));
- }
- });
- }
- else if (grantType === 'twitter' ||
- grantType === 'facebook' ||
- grantType === 'yahoo') {
+exports.loadGrant = function (grantId, callback) {
- // Check network identifier
+ Db.get('grant', grantId, function (item, err) {
- User.validate(request.payload.x_user_id, grantType, function (user, err) {
+ // Verify grant is still valid
- if (user) {
+ if (err || !item) {
+ return callback();
+ }
- callback(null, user);
- }
- else {
+ User.load(item.user, function (user, err) {
- // Unregistered network account
- callback(Hapi.Error._oauth('invalid_grant', 'Unknown ' + grantType.charAt(0).toUpperCase() + grantType.slice(1) + ' account: ' + request.payload.x_user_id));
- }
- });
+ if (err || !user) {
+ callback();
}
- else if (grantType === 'email') {
-
- // Check email identifier
- Email.loadTicket(request.payload.x_email_token, function (ticket, user, err) {
+ var result = {
+ id: item._id,
+ app: item.app,
+ user: item.user,
+ exp: item.exp,
+ scope: item.scope
+ };
- if (ticket) {
-
- callback(null, user, { 'x_action': ticket.action });
- }
- else {
+ var ext = {
+ tos: internals.getLatestTOS(user)
+ };
- // Invalid email token
- callback(Hapi.Error._oauth('invalid_grant', err.message));
- }
- });
- }
- else {
-
- // Unsupported grant type
- callback(Hapi.Error._oauth('unsupported_grant_type', 'Unknown or unsupported grant type: ' + grantType));
- }
- }
- else {
-
- // No client scope for local account access
- callback(Hapi.Error._oauth('unauthorized_client', 'Client missing \'login\' scope'));
- }
- }
+ return callback(result, ext);
+ });
+ });
};
// Validate message
-exports.validate = function (message, token, mac, callback) {
-
- Hapi.Session.loadToken(Vault.oauthToken.aes256Key, token, function (session) {
-
- if (session &&
- session.algorithm &&
- session.key &&
- session.user) {
-
- // Lookup hash function
-
- var hashMethod = null;
- switch (session.algorithm) {
+exports.validate = function (message, ticket, mac, callback) {
- case 'hmac-sha-1': hashMethod = 'sha1'; break;
- case 'hmac-sha-256': hashMethod = 'sha256'; break;
- }
-
- if (hashMethod) {
-
- // Sign message
-
- var hmac = Crypto.createHmac(hashMethod, session.key).update(message);
- var digest = hmac.digest('base64');
-
- if (digest === mac) {
-
- callback(session.user, null);
- }
- else {
+ Oz.Ticket.parse(ticket, Vault.ozTicket.password, function (err, session) {
- // Invalid signature
- callback(null, Hapi.Error.unauthorized('Invalid mac'));
- }
- }
- else {
-
- // Invalid algorithm
- callback(null, Hapi.Error.internal('Unknown algorithm'));
- }
+ if (err || !session) {
+ return callback(null, Hapi.Error.notFound('Invalid ticket'));
}
- else {
- // Invalid token
- callback(null, Hapi.Error.notFound('Invalid token'));
+ // Mac message
+
+ var hmac = Crypto.createHmac(session.algorithm, session.key).update(message);
+ var digest = hmac.digest('base64');
+ if (digest !== mac) {
+ return callback(null, Hapi.Error.unauthorized('Invalid mac'));
}
+
+ return callback(session.user, null);
});
};
@@ -315,4 +290,21 @@ exports.delUser = function (userId, callback) {
};
+// Find latest accepted TOS
+
+internals.getLatestTOS = function (user) {
+
+ if (user &&
+ user.tos &&
+ typeof user.tos === 'object') {
+
+ var versions = Object.keys(user.tos);
+ if (versions.length > 0) {
+ versions.sort();
+ return versions[versions.length - 1];
+ }
+ }
+
+ return 0;
+};
View
39 api/storage.js
@@ -24,16 +24,16 @@ exports.get = {
if (storage &&
storage.clients &&
- storage.clients[request.session.client]) {
+ storage.clients[request.session.app]) {
if (request.params.id) {
if (internals.checkKey(request.params.id)) {
- if (storage.clients[request.session.client][request.params.id]) {
+ if (storage.clients[request.session.app][request.params.id]) {
var result = {};
- result[request.params.id] = storage.clients[request.session.client][request.params.id];
+ result[request.params.id] = storage.clients[request.session.app][request.params.id];
request.reply(result);
}
@@ -49,7 +49,7 @@ exports.get = {
}
else {
- request.reply(storage.clients[request.session.client]);
+ request.reply(storage.clients[request.session.app]);
}
}
else if (err === null) {
@@ -75,12 +75,11 @@ exports.get = {
// Set user client data
exports.post = {
-
- schema: {
-
- value: Hapi.Types.String().required()
+ validate: {
+ schema: {
+ value: Hapi.types.String().required()
+ }
},
-
handler: function (request) {
if (internals.checkKey(request.params.id)) {
@@ -96,21 +95,21 @@ exports.post = {
var changes = { $set: {} };
if (storage.clients) {
- if (storage.clients[request.session.client]) {
+ if (storage.clients[request.session.app]) {
- changes.$set['clients.' + request.session.client + '.' + request.params.id] = request.payload.value;
+ changes.$set['clients.' + request.session.app + '.' + request.params.id] = request.payload.value;
}
else {
- changes.$set['clients.' + request.session.client] = {};
- changes.$set['clients.' + request.session.client][request.params.id] = request.payload.value;
+ changes.$set['clients.' + request.session.app] = {};
+ changes.$set['clients.' + request.session.app][request.params.id] = request.payload.value;
}
}
else {
changes.$set.clients = {};
- changes.$set.clients[request.session.client] = {};
- changes.$set.clients[request.session.client][request.params.id] = request.payload.value;
+ changes.$set.clients[request.session.app] = {};
+ changes.$set.clients[request.session.app][request.params.id] = request.payload.value;
}
Db.update('user.storage', storage._id, changes, function (err) {
@@ -130,8 +129,8 @@ exports.post = {
// First client data
storage = { _id: request.session.user, clients: {} };
- storage.clients[request.session.client] = {};
- storage.clients[request.session.client][request.params.id] = request.payload.value;
+ storage.clients[request.session.app] = {};
+ storage.clients[request.session.app][request.params.id] = request.payload.value;
Db.insert('user.storage', storage, function (items, err) {
@@ -174,11 +173,11 @@ exports.del = {
if (storage &&
storage.clients &&
- storage.clients[request.session.client] &&
- storage.clients[request.session.client][request.params.id]) {
+ storage.clients[request.session.app] &&
+ storage.clients[request.session.app][request.params.id]) {
var changes = { $unset: {} };
- changes.$unset['clients.' + request.session.client + '.' + request.params.id] = 1;
+ changes.$unset['clients.' + request.session.app + '.' + request.params.id] = 1;
Db.update('user.storage', storage._id, changes, function (err) {
View
12 api/stream.js
@@ -249,19 +249,15 @@ internals.messageHandler = function (socket) {
return function (message) {
- if (internals.socketsById[socket.id]) {
-
+ var connection = internals.socketsById[socket.id];
+ if (connection) {
if (message) {
-
switch (message.type) {
-
case 'initialize':
-
Session.validate(socket.id, message.id, message.mac, function (userId, err) {
if (userId) {
-
- internals.socketsById[socket.id].userId = userId;
+ connection.userId = userId;
internals.idsByUserId[userId] = internals.idsByUserId[userId] || {};
internals.idsByUserId[userId][socket.id] = true;
@@ -272,11 +268,9 @@ internals.messageHandler = function (socket) {
socket.json.send({ type: 'initialize', status: 'error', error: err });
}
});
-
break;
default:
-
socket.json.send({ type: 'error', error: 'Unknown message type: ' + message.type });
break;
}
View
54 api/task.js
@@ -131,19 +131,16 @@ exports.list = {
// Update task properties
exports.post = {
-
- query: {
-
- position: Hapi.Types.Number().min(0)
- },
-
- schema: {
-
- title: Hapi.Types.String(),
- status: Hapi.Types.String().valid('open', 'pending', 'close'),
- participants: Hapi.Types.Array().includes(Hapi.Types.String()) //!! .emptyOk()
+ validate: {
+ query: {
+ position: Hapi.types.Number().min(0)
+ },
+ schema: {
+ title: Hapi.types.String(),
+ status: Hapi.types.String().valid('open', 'pending', 'close'),
+ participants: Hapi.types.Array().includes(Hapi.types.String()) //!! .emptyOk()
+ }
},
-
handler: function (request) {
exports.load(request.params.id, request.session.user, true, function (task, err, project) {
@@ -216,7 +213,7 @@ exports.post = {
else if (request.query.position !== null &&
request.query.position !== undefined) { // Must test explicitly as value can be 0
- // Set task position in list
+ // Set task position in list
Sort.set('task', task.project, 'project', request.params.id, request.query.position, function (err) {
@@ -248,19 +245,16 @@ exports.post = {
// Create new task
exports.put = {
-
- query: {
-
- position: Hapi.Types.Number(),
- suggestion: Hapi.Types.String()
- },
-
- schema: {
-
- title: Hapi.Types.String(),
- status: Hapi.Types.String().valid('open', 'pending', 'close')
+ validate: {
+ query: {
+ position: Hapi.types.Number(),
+ suggestion: Hapi.types.String()
+ },
+ schema: {
+ title: Hapi.types.String(),
+ status: Hapi.types.String().valid('open', 'pending', 'close')
+ }
},
-
handler: function (request) {
Project.load(request.params.id, request.session.user, true, function (project, member, err) {
@@ -319,10 +313,9 @@ exports.put = {
Db.insert('task', task, function (items, err) {
if (err === null) {
-
Stream.update({ object: 'tasks', project: task.project }, request);
var result = { status: 'ok', id: items[0]._id };
- request.created('task/' + items[0]._id);
+ var created = 'task/' + items[0]._id;
if (request.query.position !== null &&
request.query.position !== undefined) { // Must test explicitly as value can be 0
@@ -332,20 +325,17 @@ exports.put = {
Sort.set('task', task.project, 'project', result.id, request.query.position, function (err) {
if (err === null) {
-
result.position = request.query.position;
}
- request.reply(result);
+ request.reply.payload(result).created(created).send();
});
}
else {
-
- request.reply(result);
+ request.reply.payload(result).created(created).send();
}
}
else {
-
request.reply(err);
}
});
View
168 api/user.js
@@ -41,7 +41,6 @@ internals.forbiddenUsernames = {
imwithstupid: true,
login: true,
logout: true,
- oauth: true,
privacy: true,
scripts: true,
script: true,
@@ -59,23 +58,18 @@ internals.forbiddenUsernames = {
// Current user information
exports.get = {
-
auth: {
-
- tos: 'none'
+ tos: null
},
-
handler: function (request) {
exports.load(request.session.user, function (user, err) {
if (user) {
-
Hapi.Utils.removeKeys(user, ['contacts', 'origin', 'tos', 'tickets']);
request.reply(user);
}
else {
-
request.reply(err);
}
});
@@ -86,18 +80,15 @@ exports.get = {
// Change profile properties
exports.post = {
-
- schema: {
-
- name: Hapi.Types.String(),
- username: Hapi.Types.String().emptyOk()
+ validate: {
+ schema: {
+ name: Hapi.types.String(),
+ username: Hapi.types.String().emptyOk()
+ }
},
-
auth: {
-
- tos: 'none'
+ tos: null
},
-
handler: function (request) {
exports.load(request.session.user, function (user, err) {
@@ -168,18 +159,15 @@ exports.post = {
// Change profile email settings
exports.email = {
-
- schema: {
-
- address: Hapi.Types.String().required(),
- action: Hapi.Types.String().required().valid('remove', 'primary', 'add', 'verify')
+ validate: {
+ schema: {
+ address: Hapi.types.String().required(),
+ action: Hapi.types.String().required().valid('remove', 'primary', 'add', 'verify')
+ }
},
-
auth: {
-
- tos: 'none'
+ tos: null
},
-
handler: function (request) {
var address = request.payload.address.toLowerCase();
@@ -390,17 +378,12 @@ exports.email = {
// Current user contacts list
exports.contacts = {
-
query: {
-
- exclude: Hapi.Types.String()
+ exclude: Hapi.types.String()
},
-
auth: {
-
- tos: 'none'
+ tos: null
},
-
handler: function (request) {
if (request.query.exclude) {
@@ -463,7 +446,7 @@ exports.contacts = {
}
else if (user.contacts[i].type === 'email') {
- // Email contact
+ // Email contact
var email = Db.decodeKey(i);
if (exclude === null ||
@@ -514,12 +497,9 @@ exports.contacts = {
// Who am I?
exports.who = {
-
auth: {
-
- tos: 'none'
+ tos: null
},
-
handler: function (request) {
request.reply({ user: request.session.user });
@@ -530,26 +510,21 @@ exports.who = {
// Register new user
exports.put = {
-
- query: {
-
- invite: Hapi.Types.String().required()
- },
-
- schema: {
-
- username: Hapi.Types.String(),
- name: Hapi.Types.String(),
- network: Hapi.Types.Array().includes(Hapi.Types.String()),
- email: Hapi.Types.String().email()
+ validate: {
+ query: {
+ invite: Hapi.types.String().required()
+ },
+ schema: {
+ username: Hapi.types.String(),
+ name: Hapi.types.String(),
+ network: Hapi.types.Array().includes(Hapi.types.String()),
+ email: Hapi.types.String().email()
+ }
},
-
auth: {
-
scope: 'signup',
- entity: 'client'
+ entity: 'app'
},
-
handler: function (request) {
// Check invitation code
@@ -891,11 +866,11 @@ exports.put = {
// Set Terms of Service version
exports.tos = {
-
- auth: {
+ auth: {
+ tos: null,
scope: 'tos',
- entity: 'client'
+ entity: 'app'
},
handler: function (request) {
@@ -931,18 +906,15 @@ exports.tos = {
// Link other account
exports.link = {
-
- schema: {
-
- id: Hapi.Types.String().required()
+ validate: {
+ schema: {
+ id: Hapi.types.String().required()
+ }
},
-
auth: {
-
scope: 'login',
- entity: 'client'
+ entity: 'app'
},
-
handler: function (request) {
if (request.params.network === 'facebook' ||
@@ -1017,11 +989,11 @@ exports.link = {
// Unlink other account
exports.unlink = {
-
+
auth: {
scope: 'login',
- entity: 'client'
+ entity: 'app'
},
handler: function (request) {
@@ -1088,11 +1060,11 @@ exports.unlink = {
// Set default view
exports.view = {
-
+
auth: {
scope: 'view',
- entity: 'client'
+ entity: 'app'
},
handler: function (request) {
@@ -1125,12 +1097,9 @@ exports.view = {
// Lookup user based on account and type
exports.lookup = {
-
auth: {
-
mode: 'none'
},
-
handler: function (request) {
if (request.params.type === 'username') {
@@ -1212,17 +1181,15 @@ exports.lookup = {
// Send email reminder account based on email or username and take action
exports.reminder = {
-
- schema: {
-
- account: Hapi.Types.String().required()
+ validate: {
+ schema: {
+ account: Hapi.types.String().required()
+ }
},
-
auth: {
scope: 'reminder',
- entity: 'client'