mirror of https://gitee.com/bigwinds/arangodb
Added bundled apps as system apps.
This commit is contained in:
parent
66068924e1
commit
af2b1c1ff6
|
@ -59,6 +59,10 @@ JAVASCRIPT_JSLINT = \
|
||||||
\
|
\
|
||||||
`find @srcdir@/js/apps/system/cerberus -name "*.js"` \
|
`find @srcdir@/js/apps/system/cerberus -name "*.js"` \
|
||||||
`find @srcdir@/js/apps/system/gharial -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/models -name "*.js"` \
|
||||||
`find @srcdir@/js/apps/system/aardvark/frontend/js/views -name "*.js"` \
|
`find @srcdir@/js/apps/system/aardvark/frontend/js/views -name "*.js"` \
|
||||||
|
|
|
@ -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;
|
||||||
|
}());
|
|
@ -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"
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}());
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}());
|
|
@ -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.');
|
||||||
|
}());
|
|
@ -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;
|
||||||
|
}());
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}());
|
|
@ -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;
|
||||||
|
}());
|
|
@ -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;
|
||||||
|
}());
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}());
|
|
@ -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"
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}());
|
|
@ -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;
|
||||||
|
}());
|
Loading…
Reference in New Issue