1
0
Fork 0

Added bundled apps as system apps.

This commit is contained in:
Alan Plum 2014-06-25 14:54:37 +02:00
parent 66068924e1
commit af2b1c1ff6
16 changed files with 870 additions and 0 deletions

View File

@ -59,6 +59,10 @@ JAVASCRIPT_JSLINT = \
\
`find @srcdir@/js/apps/system/cerberus -name "*.js"` \
`find @srcdir@/js/apps/system/gharial -name "*.js"` \
`find @srcdir@/js/apps/system/oauth2 -name "*.js"` \
`find @srcdir@/js/apps/system/sessions -name "*.js"` \
`find @srcdir@/js/apps/system/simple-auth -name "*.js"` \
`find @srcdir@/js/apps/system/users -name "*.js"` \
\
`find @srcdir@/js/apps/system/aardvark/frontend/js/models -name "*.js"` \
`find @srcdir@/js/apps/system/aardvark/frontend/js/views -name "*.js"` \

View File

@ -0,0 +1,12 @@
/*jslint indent: 2, nomen: true, maxlen: 120, white: true, plusplus: true, unparam: true, regexp: true, vars: true */
/*global require, exports */
(function () {
'use strict';
function ProviderNotFound(key) {
this.message = 'Provider with key ' + key + ' not found.';
}
ProviderNotFound.prototype = new Error();
exports.ProviderNotFound = ProviderNotFound;
}());

View File

@ -0,0 +1,17 @@
{
"name": "oauth2",
"author": "Alan Plum",
"version": "0.1",
"isSystem": true,
"description": "OAuth2 authentication for Foxx.",
"exports": {
"providers": "providers.js"
},
"defaultDocument": "",
"lib": ".",
"setup": "setup.js"
}

View File

@ -0,0 +1,214 @@
/*jslint indent: 2, nomen: true, maxlen: 120, vars: true, es5: true */
/*global require, exports, applicationContext */
(function () {
'use strict';
var _ = require('underscore');
var url = require('url');
var querystring = require('querystring');
var internal = require('internal');
var arangodb = require('org/arangodb');
var db = arangodb.db;
var Foxx = require('org/arangodb/foxx');
var errors = require('./errors');
var Provider = Foxx.Model.extend({}, {
attributes: {
_key: {type: 'string', required: true},
label: {type: 'string', required: true},
authEndpoint: {type: 'string', required: true},
tokenEndpoint: {type: 'string', required: true},
refreshEndpoint: {type: 'string', required: false},
activeUserEndpoint: {type: 'string', required: false},
usernameTemplate: {type: 'string', required: false},
clientId: {type: 'string', required: true},
clientSecret: {type: 'string', required: true}
}
});
var providers = new Foxx.Repository(
applicationContext.collection('providers'),
{model: Provider}
);
function listProviders() {
return providers.collection.all().toArray().forEach(function (provider) {
return _.pick(provider, '_key', 'label', 'clientId');
});
}
function createProvider(data) {
var provider = new Provider(data);
providers.save(provider);
return provider;
}
function getProvider(key) {
var provider;
try {
provider = providers.byId(key);
} catch (err) {
if (
err instanceof arangodb.ArangoError &&
err.errorNum === arangodb.ERROR_ARANGO_DOCUMENT_NOT_FOUND
) {
throw new errors.ProviderNotFound(key);
} else {
throw err;
}
}
return provider;
}
function deleteProvider(key) {
try {
providers.removeById(key);
} catch (err) {
if (
err instanceof arangodb.ArangoError
&& err.errorNum === arangodb.ERROR_ARANGO_DOCUMENT_NOT_FOUND
) {
throw new errors.ProviderNotFound(key);
} else {
throw err;
}
}
return null;
}
_.extend(Provider.prototype, {
getAuthUrl: function (redirect_uri, args) {
if (redirect_uri && typeof redirect_uri === 'object') {
args = redirect_uri;
redirect_uri = undefined;
}
var endpoint = url.parse(this.get('authEndpoint'));
args = _.extend(querystring.parse(endpoint.query), args);
if (redirect_uri) {
args.redirect_uri = redirect_uri;
}
if (!args.response_type) {
args.response_type = 'code';
}
args.client_id = this.get('clientId');
endpoint.search = '?' + querystring.stringify(args);
return url.format(endpoint);
},
_getTokenRequest: function (code, redirect_uri, args) {
if (code && typeof code === 'object') {
args = code;
code = undefined;
redirect_uri = undefined;
} else if (redirect_uri && typeof redirect_uri === 'object') {
args = redirect_uri;
redirect_uri = undefined;
}
var endpoint = url.parse(this.get('tokenEndpoint'));
args = _.extend(querystring.parse(endpoint.query), args);
if (code) {
args.code = code;
}
if (redirect_uri) {
args.redirect_uri = redirect_uri;
}
if (!args.grant_type) {
args.grant_type = 'authorization_code';
}
args.client_id = this.get('clientId');
args.client_secret = this.get('clientSecret');
delete endpoint.search;
delete endpoint.query;
return {url: url.format(endpoint), body: args};
},
getActiveUserUrl: function (access_token, args) {
var endpoint = this.get('activeUserEndpoint');
if (!endpoint) {
return null;
}
if (access_token && typeof access_token === 'object') {
args = access_token;
access_token = undefined;
}
args = _.extend(querystring.parse(endpoint.query), args);
if (access_token) {
args.access_token = access_token;
}
endpoint = url.parse(endpoint);
args = _.extend(querystring.parse(endpoint.query), args);
endpoint.search = '?' + querystring.stringify(args);
return url.format(endpoint);
},
getUsername: function (obj) {
var tpl = this.get('usernameTemplate');
if (!tpl) {
tpl = '<%= id %>';
}
return _.template(tpl)(obj);
},
exchangeGrantToken: function (code, redirect_uri) {
var request = this._getTokenRequest(code, redirect_uri);
var response = internal.download(
request.url,
querystring.stringify(request.body),
{
method: 'POST',
headers: {
accept: 'application/json',
'content-type': 'application/x-www-form-urlencoded'
}
}
);
if (!response.body) {
throw new Error('OAuth provider ' + this.get('_key') + ' returned HTTP ' + response.code);
}
try {
return JSON.parse(response.body);
} catch (err) {
if (err instanceof SyntaxError) {
return querystring.parse(response.body);
}
throw err;
}
},
fetchActiveUser: function (access_token) {
var url = this.getActiveUserUrl(access_token);
if (!url) {
throw new Error('Provider ' + this.get('_key') + ' does not support active user lookup');
}
var response = internal.download(url);
if (!response.body) {
throw new Error('OAuth provider ' + this.get('_key') + ' returned HTTP ' + response.code);
}
try {
return JSON.parse(response.body);
} catch (err) {
if (err instanceof SyntaxError) {
return querystring.parse(response.body);
}
throw err;
}
},
save: function () {
var provider = this;
providers.replace(provider);
return provider;
},
delete: function () {
try {
deleteProvider(this.get('_key'));
return true;
} catch (e) {
if (e instanceof errors.ProviderNotFound) {
return false;
}
throw e;
}
}
});
exports.list = listProviders;
exports.create = createProvider;
exports.get = getProvider;
exports.delete = deleteProvider;
exports.errors = errors;
exports.repository = providers;
}());

View File

@ -0,0 +1,47 @@
/*jslint indent: 2, nomen: true, maxlen: 120, white: true, plusplus: true, unparam: true, regexp: true, vars: true */
/*global require, applicationContext */
(function () {
'use strict';
var db = require('org/arangodb').db;
var providersName = applicationContext.collectionName('providers');
if (db._collection(providersName) === null) {
db._create(providersName);
}
var providers = db._collection(providersName);
[
{
_key: 'github',
label: 'GitHub',
authEndpoint: 'https://github.com/login/oauth/authorize?scope=user',
tokenEndpoint: 'https://github.com/login/oauth/access_token',
activeUserEndpoint: 'https://api.github.com/user',
clientId: null,
clientSecret: null
},
{
_key: 'facebook',
label: 'Facebook',
authEndpoint: 'https://www.facebook.com/dialog/oauth',
tokenEndpoint: 'https://graph.facebook.com/oauth/access_token',
activeUserEndpoint: 'https://graph.facebook.com/v2.0/me',
clientId: null,
clientSecret: null
},
{
_key: 'google',
label: 'Google',
authEndpoint: 'https://accounts.google.com/o/oauth2/auth?access_type=offline&scope=profile',
tokenEndpoint: 'https://accounts.google.com/o/oauth2/token',
refreshEndpoint: 'https://accounts.google.com/o/oauth2/token',
activeUserEndpoint: 'https://www.googleapis.com/plus/v1/people/me',
clientId: null,
clientSecret: null
}
].forEach(function(provider) {
if (!providers.exists(provider._key)) {
providers.save(provider);
}
});
}());

View File

@ -0,0 +1,78 @@
/*jslint indent: 2, nomen: true, maxlen: 120, vars: true, es5: true */
/*global require, applicationContext */
(function () {
'use strict';
var _ = require('underscore');
var Foxx = require('org/arangodb/foxx');
var errors = require('./errors');
var controller = new Foxx.Controller(applicationContext);
var api = Foxx.requireApp('/sessions').sessionStorage;
controller.post('/', function (req, res) {
var session = api.create(req.body());
res.status(201);
res.json(session.forClient());
})
.errorResponse(SyntaxError, 400, 'Malformed or non-JSON session data.')
.summary('Create session')
.notes('Stores the given sessionData in a new session.');
controller.get('/:sid', function (req, res) {
var session = api.get(req.urlParameters.sid);
res.json(session.forClient());
})
.pathParam('sid', {
description: 'Session ID',
type: 'string'
})
.errorResponse(errors.SessionExpired, 404, 'Session has expired')
.errorResponse(errors.SessionNotFound, 404, 'Session does not exist')
.summary('Read session')
.notes('Fetches the session with the given sid.');
controller.put('/:sid', function (req, res) {
var body = JSON.parse(req.rawBody());
var session = api.get(req.urlParameters.sid);
session.set('sessionData', body);
session.save();
res.json(session.forClient());
})
.pathParam('sid', {
description: 'Session ID',
type: 'string'
})
.errorResponse(errors.SessionExpired, 404, 'Session has expired')
.errorResponse(errors.SessionNotFound, 404, 'Session does not exist')
.errorResponse(SyntaxError, 400, 'Malformed or non-JSON session data.')
.summary('Update session (replace)')
.notes('Updates the session with the given sid by replacing the sessionData.');
controller.patch('/:sid', function (req, res) {
var body = JSON.parse(req.rawBody());
var session = api.get(req.urlParameters.sid);
_.extend(session.get('sessionData'), body);
session.save();
res.json(session.forClient());
})
.pathParam('sid', {
description: 'Session ID',
type: 'string'
})
.errorResponse(errors.SessionExpired, 404, 'Session has expired')
.errorResponse(errors.SessionNotFound, 404, 'Session does not exist')
.errorResponse(SyntaxError, 400, 'Malformed or non-JSON session data.')
.summary('Update session')
.notes('Updates the session with the given sid by merging its sessionData.');
controller.delete('/:sid', function (req, res) {
api.destroy(req.urlParameters.sid);
res.status(204);
})
.pathParam('sid', {
description: 'Session ID',
type: 'string'
})
.errorResponse(errors.SessionNotFound, 404, 'Session does not exist')
.summary('Delete session')
.notes('Removes the session with the given sid from the database.');
}());

View File

@ -0,0 +1,18 @@
/*jslint indent: 2, nomen: true, maxlen: 120, vars: true, es5: true */
/*global require, exports */
(function () {
'use strict';
function SessionNotFound(sid) {
this.message = 'Session with session id ' + sid + ' not found.';
}
SessionNotFound.prototype = new Error();
function SessionExpired(sid) {
this.message = 'Session with session id ' + sid + ' has expired.';
}
SessionExpired.prototype = Object.create(SessionNotFound.prototype);
exports.SessionNotFound = SessionNotFound;
exports.SessionExpired = SessionExpired;
}());

View File

@ -0,0 +1,44 @@
{
"name": "sessions",
"author": "Alan Plum",
"version": "0.1",
"isSystem": true,
"description": "Session storage for Foxx.",
"controllers": {
"/": "app.js"
},
"exports": {
"sessionStorage": "storage.js"
},
"defaultDocument": "",
"lib": ".",
"setup": "setup.js",
"configuration": {
"timeToLive": {
"description": "Session expiry timeout in milliseconds.",
"type": "integer",
"default": 604800000
},
"ttlType": {
"description": "Timestamp session expiry should be checked against.",
"type": "string",
"default": "created"
},
"sidTimestamp": {
"description": "Append a timestamp to the session id.",
"type": "boolean",
"default": false
},
"sidLength": {
"description": "Length of the random part of the session id",
"type": "integer",
"default": 20
}
}
}

View File

@ -0,0 +1,11 @@
/*jslint indent: 2, nomen: true, maxlen: 120, vars: true, es5: true */
/*global require, applicationContext */
(function () {
'use strict';
var db = require('org/arangodb').db;
var sessionsName = applicationContext.collectionName('sessions');
if (db._collection(sessionsName) === null) {
db._create(sessionsName);
}
}());

View File

@ -0,0 +1,201 @@
/*jslint indent: 2, nomen: true, maxlen: 120, vars: true, es5: true */
/*global require, exports, applicationContext */
(function () {
'use strict';
var _ = require('underscore');
var internal = require('internal');
var arangodb = require('org/arangodb');
var db = arangodb.db;
var addCookie = require('org/arangodb/actions').addCookie;
var crypto = require('org/arangodb/crypto');
var Foxx = require('org/arangodb/foxx');
var errors = require('./errors');
var cfg = applicationContext.configuration;
var Session = Foxx.Model.extend({}, {
attributes: {
_key: {type: 'string', required: true},
uid: {type: 'string', required: false},
sessionData: {type: 'object', required: true},
userData: {type: 'object', required: true},
created: {type: 'integer', required: true},
lastAccess: {type: 'integer', required: true},
lastUpdate: {type: 'integer', required: true}
}
});
var sessions = new Foxx.Repository(
applicationContext.collection('sessions'),
{model: Session}
);
function generateSessionId() {
var sid = '';
if (cfg.sidTimestamp) {
sid = internal.base64Encode(Number(new Date()));
if (cfg.sidLength === 0) {
return sid;
}
sid += '-';
}
return sid + internal.genRandomAlphaNumbers(cfg.sidLength || 10);
}
function createSession(sessionData) {
var sid = generateSessionId(cfg);
var now = Number(new Date());
var session = new Session({
_key: sid,
sid: sid,
sessionData: sessionData || {},
userData: {},
created: now,
lastAccess: now,
lastUpdate: now
});
sessions.save(session);
return session;
}
function getSession(sid) {
var session;
db._executeTransaction({
collections: {
read: [sessions.collection.name()],
write: [sessions.collection.name()]
},
action: function () {
try {
session = sessions.byId(sid);
session.enforceTimeout();
} catch (err) {
if (
err instanceof arangodb.ArangoError
&& err.errorNum === arangodb.ERROR_ARANGO_DOCUMENT_NOT_FOUND
) {
throw new errors.SessionNotFound(sid);
} else {
throw err;
}
}
var now = Number(new Date());
sessions.collection.update(session.forDB(), {
lastAccess: now
});
session.set('lastAccess', now);
}
});
return session;
}
function deleteSession(sid) {
try {
sessions.removeById(sid);
} catch (err) {
if (
err instanceof arangodb.ArangoError
&& err.errorNum === arangodb.ERROR_ARANGO_DOCUMENT_NOT_FOUND
) {
throw new errors.SessionNotFound(sid);
} else {
throw err;
}
}
return null;
}
function fromCookie(req, cookieName, secret) {
var session = null;
var value = req.cookies[cookieName];
if (value) {
if (secret) {
var signature = req.cookies[cookieName + '_sig'] || '';
if (!crypto.constantEquals(signature, crypto.hmac(secret, value))) {
return null;
}
}
try {
session = getSession(value);
} catch (e) {
if (!(e instanceof errors.SessionNotFound)) {
throw e;
}
}
}
return session;
}
_.extend(Session.prototype, {
enforceTimeout: function () {
if (!cfg.timeToLive) {
return;
}
var now = Number(new Date());
var prop = cfg.ttlType;
if (!prop || !this.get(prop)) {
prop = 'created';
}
if (cfg.timeToLive < (now - this.get(prop))) {
throw new errors.SessionExpired(this.get('_key'));
}
},
addCookie: function (res, cookieName, secret) {
var value = this.get('_key');
var ttl = cfg.timeToLive;
ttl = ttl ? Math.floor(ttl / 1000) : undefined;
addCookie(res, cookieName, value, ttl);
if (secret) {
addCookie(res, cookieName + '_sig', crypto.hmac(secret, value), ttl);
}
},
clearCookie: function (res, cookieName, secret) {
addCookie(res, cookieName, '', -(7 * 24 * 60 * 60));
if (secret) {
addCookie(res, cookieName + '_sig', '', -(7 * 24 * 60 * 60));
}
},
setUser: function (user) {
var session = this;
if (user) {
session.set('uid', user.get('_key'));
session.set('userData', user.get('userData'));
} else {
delete session.attributes.uid;
session.set('userData', {});
}
return session;
},
save: function () {
var session = this;
var now = Number(new Date());
session.set('lastAccess', now);
session.set('lastUpdate', now);
sessions.replace(session);
return session;
},
delete: function () {
var session = this;
var now = Number(new Date());
session.set('lastAccess', now);
session.set('lastUpdate', now);
try {
deleteSession(session.get('_key'));
return true;
} catch (e) {
if (e instanceof errors.SessionNotFound) {
return false;
}
throw e;
}
}
});
exports.fromCookie = fromCookie;
exports.create = createSession;
exports.get = getSession;
exports.delete = deleteSession;
exports.errors = errors;
exports.repository = sessions;
exports._generateSessionId = generateSessionId;
}());

View File

@ -0,0 +1,29 @@
/*jslint indent: 2, nomen: true, maxlen: 120, vars: true, es5: true */
/*global require, exports, applicationContext */
(function () {
'use strict';
var crypto = require('org/arangodb/crypto');
var cfg = applicationContext.configuration;
function verifyPassword(authData, password) {
if (!authData) {
authData = {};
}
var hashMethod = authData.method || cfg.hashMethod;
var salt = authData.salt || '';
var storedHash = authData.hash || '';
var generatedHash = crypto[hashMethod](salt + password);
// non-lazy comparison to avoid timing attacks
return crypto.constantEquals(storedHash, generatedHash);
}
function hashPassword(password) {
var hashMethod = cfg.hashMethod;
var salt = crypto.genRandomAlphaNumbers(cfg.saltLength);
var hash = crypto[hashMethod](salt + password);
return {method: hashMethod, salt: salt, hash: hash};
}
exports.verifyPassword = verifyPassword;
exports.hashPassword = hashPassword;
}());

View File

@ -0,0 +1,28 @@
{
"name": "simple-auth",
"author": "Alan Plum",
"version": "0.1",
"isSystem": true,
"description": "Simple password-based authentication for Foxx.",
"exports": {
"auth": "auth.js"
},
"defaultDocument": "",
"lib": ".",
"configuration": {
"hashMethod": {
"description": "Cryptographic hash function to use for new passwords.",
"type": "string",
"default": "sha256"
},
"saltLength": {
"description": "Length of new salts.",
"type": "integer",
"default": 16
}
}
}

View File

@ -0,0 +1,18 @@
/*jslint indent: 2, nomen: true, maxlen: 120, vars: true, es5: true */
/*global require, exports */
(function () {
'use strict';
function UserNotFound(uid) {
this.message = 'User with user id ' + uid + ' not found.';
}
UserNotFound.prototype = new Error();
function UsernameNotAvailable(username) {
this.message = 'The username ' + username + ' is not available or already taken.';
}
UsernameNotAvailable.prototype = new Error();
exports.UserNotFound = UserNotFound;
exports.UsernameNotAvailable = UsernameNotAvailable;
}());

View File

@ -0,0 +1,17 @@
{
"name": "users",
"author": "Alan Plum",
"version": "0.1",
"isSystem": true,
"description": "Username-based user storage for Foxx.",
"exports": {
"userStorage": "storage.js"
},
"defaultDocument": "",
"lib": ".",
"setup": "setup.js"
}

View File

@ -0,0 +1,11 @@
/*jslint indent: 2, nomen: true, maxlen: 120, vars: true, es5: true */
/*global require, applicationContext */
(function () {
'use strict';
var db = require('org/arangodb').db;
var usersName = applicationContext.collectionName('users');
if (db._collection(usersName) === null) {
db._create(usersName);
}
}());

View File

@ -0,0 +1,121 @@
/*jslint indent: 2, nomen: true, maxlen: 120, vars: true, es5: true */
/*global require, exports, applicationContext */
(function () {
'use strict';
var _ = require('underscore');
var arangodb = require('org/arangodb');
var db = arangodb.db;
var Foxx = require('org/arangodb/foxx');
var errors = require('./errors');
var User = Foxx.Model.extend({}, {
attributes: {
authData: {type: 'object', required: true},
userData: {type: 'object', required: true}
}
});
var users = new Foxx.Repository(
applicationContext.collection('users'),
{model: User}
);
function resolve(username) {
var user = users.firstExample({'userData.username': username});
if (!user.get('_key')) {
return null;
}
return user;
}
function listUsers() {
return users.collection.all().toArray().map(function (user) {
return user.userData ? user.userData.username : null;
}).filter(Boolean);
}
function createUser(userData) {
if (!userData) {
userData = {};
}
if (!userData.username) {
throw new Error('Must provide username!');
}
var user;
db._executeTransaction({
collections: {
read: [users.collection.name()],
write: [users.collection.name()]
},
action: function () {
if (resolve(userData.username)) {
throw new errors.UsernameNotAvailable(userData.username);
}
user = new User({
userData: userData,
authData: {}
});
users.save(user);
}
});
return user;
}
function getUser(uid) {
var user;
try {
user = users.byId(uid);
} catch (err) {
if (
err instanceof arangodb.ArangoError
&& err.errorNum === arangodb.ERROR_ARANGO_DOCUMENT_NOT_FOUND
) {
throw new errors.UserNotFound(uid);
}
throw err;
}
return user;
}
function deleteUser(uid) {
try {
users.removeById(uid);
} catch (err) {
if (
err instanceof arangodb.ArangoError
&& err.errorNum === arangodb.ERROR_ARANGO_DOCUMENT_NOT_FOUND
) {
throw new errors.UserNotFound(uid);
}
throw err;
}
return null;
}
_.extend(User.prototype, {
save: function () {
var user = this;
users.replace(user);
return user;
},
delete: function () {
try {
deleteUser(this.get('_key'));
return true;
} catch (e) {
if (e instanceof errors.UserNotFound) {
return false;
}
throw e;
}
}
});
exports.resolve = resolve;
exports.list = listUsers;
exports.create = createUser;
exports.get = getUser;
exports.delete = deleteUser;
exports.errors = errors;
exports.repository = users;
}());