From 0d653ca67a45715565d94f042590654a300ed1ff Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Mon, 16 Jun 2014 17:34:52 +0200 Subject: [PATCH 01/44] Implemented activateSessions. --- .../org/arangodb/foxx/authentication.js | 4 +- .../modules/org/arangodb/foxx/controller.js | 130 ++++++++-- .../org/arangodb/foxx/request_context.js | 10 +- .../modules/org/arangodb/foxx/sessions.js | 237 ++++++++++++++++++ js/server/tests/shell-foxx.js | 75 ++++++ 5 files changed, 427 insertions(+), 29 deletions(-) create mode 100644 js/server/modules/org/arangodb/foxx/sessions.js diff --git a/js/server/modules/org/arangodb/foxx/authentication.js b/js/server/modules/org/arangodb/foxx/authentication.js index dd43b87ff9..57563c5626 100644 --- a/js/server/modules/org/arangodb/foxx/authentication.js +++ b/js/server/modules/org/arangodb/foxx/authentication.js @@ -52,7 +52,7 @@ var db = require("org/arangodb").db, CookieAuthentication, Authentication, UserAlreadyExistsError, - UnauthorizedError; + UnauthorizedError = require("./sessions").UnauthorizedError; // ----------------------------------------------------------------------------- // --SECTION-- helper functions @@ -711,7 +711,7 @@ Users.prototype.exists = function (identifier) { }; //////////////////////////////////////////////////////////////////////////////// -/// @brief check whether a user is valid +/// @brief check whether a user is valid //////////////////////////////////////////////////////////////////////////////// Users.prototype.isValid = function (identifier, password) { diff --git a/js/server/modules/org/arangodb/foxx/controller.js b/js/server/modules/org/arangodb/foxx/controller.js index b6ac6f87d0..919c4efa04 100644 --- a/js/server/modules/org/arangodb/foxx/controller.js +++ b/js/server/modules/org/arangodb/foxx/controller.js @@ -1,4 +1,4 @@ -/*jslint indent: 2, nomen: true, maxlen: 120, regexp: true */ +/*jslint indent: 2, nomen: true, maxlen: 120, regexp: true, vars: true */ /*global module, require, exports */ //////////////////////////////////////////////////////////////////////////////// @@ -433,29 +433,30 @@ extend(Controller.prototype, { }, //////////////////////////////////////////////////////////////////////////////// -/// @startDocuBlock JSF_foxx_controller_activateAuthentication +/// @fn JSF_foxx_controller_activateAuthentication +/// @brief Activate authentication for this app /// /// `FoxxController#activateAuthentication(opts)` /// /// To activate authentication for this authentication, first call this function. /// Provide the following arguments: /// -/// * *type*: Currently we only support *cookie*, but this will change in the future -/// * *cookieLifetime*: An integer. Lifetime of cookies in seconds -/// * *cookieName*: A string used as the name of the cookie -/// * *sessionLifetime*: An integer. Lifetime of sessions in seconds +/// * `type`: Currently we only support `cookie`, but this will change in the future. +/// * `cookieLifetime`: An integer. Lifetime of cookies in seconds. +/// * `cookieName`: A string used as the name of the cookie. +/// * `sessionLifetime`: An integer. Lifetime of sessions in seconds. /// -/// @EXAMPLES /// -/// ```js -/// app.activateAuthentication({ -/// type: "cookie", -/// cookieLifetime: 360000, -/// cookieName: "my_cookie", -/// sessionLifetime: 400, -/// }); -/// ``` -/// @endDocuBlock +/// *Examples* +/// +/// @code +/// app.activateAuthentication({ +/// type: "cookie", +/// cookieLifetime: 360000, +/// cookieName: "my_cookie", +/// sessionLifetime: 400, +/// }); +/// @endcode //////////////////////////////////////////////////////////////////////////////// activateAuthentication: function (opts) { 'use strict'; @@ -467,7 +468,8 @@ extend(Controller.prototype, { }, //////////////////////////////////////////////////////////////////////////////// -/// @startDocuBlock JSF_foxx_controller_login +/// @fn JSF_foxx_controller_login +/// @brief Add a login handler /// /// `FoxxController#login(path, opts)` /// @@ -505,7 +507,8 @@ extend(Controller.prototype, { }, //////////////////////////////////////////////////////////////////////////////// -/// @startDocuBlock JSF_foxx_controller_logout +/// @fn JSF_foxx_controller_logout +/// @brief Add a logout handler /// /// `FoxxController#logout(path, opts)` /// @@ -542,7 +545,8 @@ extend(Controller.prototype, { }, //////////////////////////////////////////////////////////////////////////////// -/// @startDocuBlock JSF_foxx_controller_register +/// @fn JSF_foxx_controller_register +/// @brief Add a register handler /// /// `FoxxController#register(path, opts)` /// @@ -566,9 +570,8 @@ extend(Controller.prototype, { /// *acceptedAttributes* and set it to an array containing strings with the names of /// the additional attributes you want to accept. All other attributes in the request /// will be ignored. -/// /// If you want default attributes for the accepted attributes or set additional fields -/// (for example *admin*) use the option *defaultAttributes* which should be a hash +/// (for example *admin*) use the option `defaultAttributes` which should be a hash /// mapping attribute names to default values. /// /// @EXAMPLES @@ -581,7 +584,6 @@ extend(Controller.prototype, { /// } /// }); /// ``` -/// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// register: function (route, opts) { 'use strict'; @@ -593,7 +595,8 @@ extend(Controller.prototype, { }, //////////////////////////////////////////////////////////////////////////////// -/// @startDocuBlock JSF_foxx_controller_changePassword +/// @fn JSF_foxx_controller_changePassword +/// @brief Add a change password handler /// /// FoxxController#changePassword(route, opts)` /// @@ -628,8 +631,87 @@ extend(Controller.prototype, { 'use strict'; var authentication = require("org/arangodb/foxx/authentication"); return this.post(route, authentication.createStandardChangePasswordHandler(this.getUsers(), opts)); - } + }, +//////////////////////////////////////////////////////////////////////////////// +/// @fn JSF_foxx_controller_getSessions +/// @brief Get the sessions object of this controller +//////////////////////////////////////////////////////////////////////////////// + getSessions: function () { + 'use strict'; + return this.sessions; + }, + +//////////////////////////////////////////////////////////////////////////////// +/// @startDocuBlock JSF_foxx_controller_activateAuthentication +/// +/// `FoxxController#activateAuthentication(opts)` +/// +/// To activate sessions for this sessions, first call this function. +/// Provide the following arguments: +/// +/// * *type*: Currently we only support *cookie*, but this will change in the future. Defaults to *"cookie"*. +/// * *cookieName*: A string used as the name of the cookie. Defaults to *"sid"*. +/// * *cookieSecret*: A secret string used to sign the cookie (as "*cookieName*_sig"). Optional. +/// * *autoCreateSession*: Whether to always create a session if none exists. Defaults to *true*. +/// * *sessionStorageApp*: Mount path of the app to use for sessions. Defaults to */_sessions* +/// +/// +/// @EXAMPLES +/// +/// ```js +/// app.activateSessions({ +/// type: "cookie", +/// cookieName: "my_cookie", +/// autoCreateSession: true, +/// sessionStorageApp: "/my-sessions" +/// }); +/// ``` +//////////////////////////////////////////////////////////////////////////////// + activateSessions: function (opts) { + 'use strict'; + var sessions = require("org/arangodb/foxx/sessions"); + + this.sessions = new sessions.Sessions(opts); + sessions.decorateController(this.sessions, this); + }, + +//////////////////////////////////////////////////////////////////////////////// +/// @startDocuBlock JSF_foxx_controller_logout +/// +/// `FoxxController#logout(path, opts)` +/// +/// This adds a path to your app for the destroySession functionality. +/// You can customize it with custom *before* and *after* function: +/// *before* is a function that you can define to do something before +/// the session is destroyed. +/// *after* is a function that you can define to do something after the +/// session is destroyed. This defaults to a function that returns a +/// JSON object with *message* set to "logged out". +/// Both *before* and *after* should take request and result as arguments. +/// If you only want to provide an *after* function, you can pass the +/// function directly instead of an object. +/// +/// @EXAMPLES +/// +/// ```js +/// app.destroySession('/logout', function (req, res) { +/// res.json({"message": "Bye, Bye"}); +/// }); +/// ``` +//////////////////////////////////////////////////////////////////////////////// + destroySession: function (route, opts) { + 'use strict'; + var method = opts.method; + if (typeof method === 'string') { + method = method.toLowerCase(); + } + if (!method || typeof this[method] !== 'function') { + method = 'post'; + } + var sessions = require("org/arangodb/foxx/sessions"); + return this[method](route, sessions.createDestroySessionHandler(this.getSessions(), opts)); + } }); exports.Controller = Controller; diff --git a/js/server/modules/org/arangodb/foxx/request_context.js b/js/server/modules/org/arangodb/foxx/request_context.js index 464b792828..4db76f7417 100644 --- a/js/server/modules/org/arangodb/foxx/request_context.js +++ b/js/server/modules/org/arangodb/foxx/request_context.js @@ -35,9 +35,9 @@ var RequestContext, extend = _.extend, internal = require("org/arangodb/foxx/internals"), is = require("org/arangodb/is"), - UnauthorizedError = require("org/arangodb/foxx/authentication").UnauthorizedError, elementExtractFactory, bubbleWrapFactory, + UnauthorizedError = require("org/arangodb/foxx/sessions").UnauthorizedError, createErrorBubbleWrap, createBodyParamBubbleWrap, addCheck; @@ -524,7 +524,8 @@ extend(RequestContext.prototype, { /// /// `FoxxController#onlyIf(code, reason)` /// -/// Please activate authentification for this app if you want to use this function. +/// Please activate sessions for this app if you want to use this function. +/// Or activate authentication (deprecated). /// If the user is logged in, it will do nothing. Otherwise it will respond with /// the status code and the reason you provided (the route handler won't be called). /// This will also add the according documentation for this route. @@ -543,7 +544,10 @@ extend(RequestContext.prototype, { var check; check = function (req) { - if (!(req.user && req.currentSession)) { + if ( + !(req.session && req.session.get('uid')) // new and shiny + && !(req.user && req.currentSession) // old and busted + ) { throw new UnauthorizedError(); } }; diff --git a/js/server/modules/org/arangodb/foxx/sessions.js b/js/server/modules/org/arangodb/foxx/sessions.js new file mode 100644 index 0000000000..8ab9a7ec7f --- /dev/null +++ b/js/server/modules/org/arangodb/foxx/sessions.js @@ -0,0 +1,237 @@ +/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */ +/*global require, exports */ + +//////////////////////////////////////////////////////////////////////////////// +/// @brief Foxx Sessions +/// +/// @file +/// +/// DISCLAIMER +/// +/// Copyright 2013 triagens GmbH, Cologne, Germany +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// +/// Copyright holder is triAGENS GmbH, Cologne, Germany +/// +/// @author Jan Steemann, Lucas Dohmen +/// @author Copyright 2013, triAGENS GmbH, Cologne, Germany +//////////////////////////////////////////////////////////////////////////////// + +var Foxx = require('org/arangodb/foxx'); + +// ----------------------------------------------------------------------------- +// --SECTION-- helper functions +// ----------------------------------------------------------------------------- + +//////////////////////////////////////////////////////////////////////////////// +/// @addtogroup Foxx +/// @{ +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +/// @brief decorates the controller with session logic +//////////////////////////////////////////////////////////////////////////////// + +function decorateController(auth, controller) { + 'use strict'; + var cfg = auth.configuration; + + controller.before('/*', function (req) { + var sessions = auth.getSessionStorage(); + if (cfg.type === 'cookie') { + req.session = sessions.fromCookie(req, cfg.cookieName, cfg.cookieSecret); + if (!req.session && cfg.autoCreateSession) { + req.session = sessions.create(); + } + } + }); + + controller.after('/*', function (req, res) { + if (req.session) { + if (cfg.type === 'cookie') { + req.session.addCookie(res, cfg.cookieName, cfg.cookieSecret); + } + } + }); +} + +//////////////////////////////////////////////////////////////////////////////// +/// @brief convenience wrapper for session destruction logic +//////////////////////////////////////////////////////////////////////////////// + +function createDestroySessionHandler(auth, opts) { + 'use strict'; + if (!opts) { + opts = {}; + } + if (typeof opts === 'function') { + opts = {after: opts}; + } + if (!opts.after) { + opts.after = function (req, res) { + res.json({message: 'logged out'}); + }; + } + var cfg = auth.configuration; + return function (req, res, injected) { + if (typeof opts.before === 'function') { + opts.before(req, res, injected); + } + if (req.session) { + req.session.delete(); + } + if (cfg.autoCreateSession) { + req.session = auth.getSessionStorage().create(); + } else { + if (cfg.type === 'cookie') { + req.session.clearCookie(res, cfg.cookieName, cfg.cookieSecret); + } + delete req.session; + } + if (typeof opts.after === 'function') { + opts.after(req, res, injected); + } + }; +} + +//////////////////////////////////////////////////////////////////////////////// +/// @} +//////////////////////////////////////////////////////////////////////////////// + +// ----------------------------------------------------------------------------- +// --SECTION-- FOXX SESSIONS +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// --SECTION-- constructors and destructors +// ----------------------------------------------------------------------------- + +//////////////////////////////////////////////////////////////////////////////// +/// @addtogroup Foxx +/// @{ +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +/// @brief constructor +//////////////////////////////////////////////////////////////////////////////// + +function Sessions(opts) { + 'use strict'; + if (!opts) { + opts = {}; + } + if (opts.type !== 'cookie') { + throw new Error('Only "cookie" type sessions are supported at this time.'); + } + if (opts.cookieSecret && typeof opts.cookieSecret !== 'string') { + throw new Error('Cookie secret must be a string or empty.'); + } + if (opts.cookieName && typeof opts.cookieName !== 'string') { + throw new Error('Cookie name must be a string or empty.'); + } + + if (!opts.type) { + opts.type = 'cookie'; + } + if (!opts.cookieName) { + opts.cookieName = 'sid'; + } + if (opts.autoCreateSession !== false) { + opts.autoCreateSession = true; + } + if (!opts.sessionStorageApp) { + opts.sessionStorageApp = '/_sessions'; + } + this.configuration = opts; +} + +//////////////////////////////////////////////////////////////////////////////// +/// @} +//////////////////////////////////////////////////////////////////////////////// + +// ----------------------------------------------------------------------------- +// --SECTION-- public functions +// ----------------------------------------------------------------------------- + +//////////////////////////////////////////////////////////////////////////////// +/// @addtogroup Foxx +/// @{ +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +/// @brief fetches the session storage +//////////////////////////////////////////////////////////////////////////////// + +Sessions.prototype.getSessionStorage = function () { + 'use strict'; + return Foxx.requireApp(this.configuration.sessionStorageApp).sessionStorage; +}; + +//////////////////////////////////////////////////////////////////////////////// +/// @} +//////////////////////////////////////////////////////////////////////////////// + +// ----------------------------------------------------------------------------- +// --SECTION-- custom errors +// ----------------------------------------------------------------------------- + +// http://stackoverflow.com/questions/783818/how-do-i-create-a-custom-error-in-javascript + +//////////////////////////////////////////////////////////////////////////////// +/// @addtogroup Foxx +/// @{ +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +/// @brief constructor +//////////////////////////////////////////////////////////////////////////////// + +function UnauthorizedError(message) { + 'use strict'; + this.message = message; + this.statusCode = 401; +} + +UnauthorizedError.prototype = new Error(); + +//////////////////////////////////////////////////////////////////////////////// +/// @} +//////////////////////////////////////////////////////////////////////////////// + +// ----------------------------------------------------------------------------- +// --SECTION-- module exports +// ----------------------------------------------------------------------------- + +//////////////////////////////////////////////////////////////////////////////// +/// @addtogroup Foxx +/// @{ +//////////////////////////////////////////////////////////////////////////////// + +exports.UnauthorizedError = UnauthorizedError; +exports.Sessions = Sessions; +exports.decorateController = decorateController; +exports.createDestroySessionHandler = createDestroySessionHandler; + +//////////////////////////////////////////////////////////////////////////////// +/// @} +//////////////////////////////////////////////////////////////////////////////// + +// ----------------------------------------------------------------------------- +// --SECTION-- END-OF-FILE +// ----------------------------------------------------------------------------- + +/// Local Variables: +/// mode: outline-minor +/// outline-regexp: "/// @brief\\|/// @addtogroup\\|/// @page\\|// --SECTION--\\|/// @\\}\\|/\\*jslint" +/// End: diff --git a/js/server/tests/shell-foxx.js b/js/server/tests/shell-foxx.js index f80b658b26..a5365a3652 100644 --- a/js/server/tests/shell-foxx.js +++ b/js/server/tests/shell-foxx.js @@ -324,6 +324,36 @@ function SetRoutesFoxxControllerSpec () { assertEqual(error, new Error("Setup authentication first")); }, + + testAddADestroySessionRoute: function () { + var myFunc = function () {}, + routes = app.routingInfo.routes; + + app.activateSessions({ + sessionStorageApp: 'sessions', + cookieName: 'sid', + cookieSecret: 'secret', + type: 'cookie' + }); + app.destroySession('/simple/route', myFunc); + assertEqual(routes[0].docs.httpMethod, 'POST'); + assertEqual(routes[0].url.methods, ["post"]); + }, + + testRefuseDestroySessionWhenSessionsAreNotSetUp: function () { + var myFunc = function () {}, + error; + + try { + app.destroySession('/simple/route', myFunc); + } catch(e) { + error = e; + } + + assertEqual(error, new Error("Setup sessions first")); + } + + }; } @@ -1370,6 +1400,50 @@ function SetupAuthorization () { }; } +function SetupSessions () { + var app; + + return { + testWorksWithAllParameters: function () { + var err; + + app = new FoxxController(fakeContext); + + try { + app.activateSessions({ + sessionStorageApp: 'sessions', + cookieName: 'sid', + cookieSecret: 'secret', + type: 'cookie' + }); + } catch (e) { + err = e; + } + + assertUndefined(err); + }, + + testRefusesUnknownSessionsTypes: function () { + var err; + + app = new FoxxController(fakeContext); + + try { + app.activateSessions({ + sessionStorageApp: 'sessions', + cookieName: 'sid', + cookieSecret: 'secret', + type: 'magic' + }); + } catch (e) { + err = e; + } + + assertEqual(err.message, 'Only "cookie" type sessions are supported at this time.'); + } + }; +} + function FoxxControllerWithRootElement () { var app; @@ -1444,6 +1518,7 @@ jsunity.run(DocumentationAndConstraintsSpec); jsunity.run(AddMiddlewareFoxxControllerSpec); jsunity.run(CommentDrivenDocumentationSpec); jsunity.run(SetupAuthorization); +jsunity.run(SetupSessions); jsunity.run(FoxxControllerWithRootElement); return jsunity.done(); From e6c72060389d0ed63e6492b678eaf3218e11e481 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Mon, 23 Jun 2014 13:19:27 +0200 Subject: [PATCH 02/44] Added documentation stubs for auth. --- Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp | 3 +++ Documentation/Books/Users/FoxxBundledApps/README.mdpp | 5 +++++ Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp | 3 +++ Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp | 3 +++ Documentation/Books/Users/FoxxBundledApps/Users.mdpp | 3 +++ Documentation/Books/Users/SUMMARY.md | 5 +++++ 6 files changed, 22 insertions(+) create mode 100644 Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp create mode 100644 Documentation/Books/Users/FoxxBundledApps/README.mdpp create mode 100644 Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp create mode 100644 Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp create mode 100644 Documentation/Books/Users/FoxxBundledApps/Users.mdpp diff --git a/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp b/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp new file mode 100644 index 0000000000..90b22adf7e --- /dev/null +++ b/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp @@ -0,0 +1,3 @@ +!CHAPTER The OAuth2 Authentication App + +WRITEME \ No newline at end of file diff --git a/Documentation/Books/Users/FoxxBundledApps/README.mdpp b/Documentation/Books/Users/FoxxBundledApps/README.mdpp new file mode 100644 index 0000000000..5ddf6ac64e --- /dev/null +++ b/Documentation/Books/Users/FoxxBundledApps/README.mdpp @@ -0,0 +1,5 @@ +!CHAPTER Bundled Foxx Applications + +ArangoDB ships with a number of Foxx apps that allow you to implement basic authentication and user management with minimal effort. + +WRITEME \ No newline at end of file diff --git a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp new file mode 100644 index 0000000000..2e30c664c1 --- /dev/null +++ b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp @@ -0,0 +1,3 @@ +!CHAPTER The Sessions App + +WRITEME \ No newline at end of file diff --git a/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp b/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp new file mode 100644 index 0000000000..11d5fa34ee --- /dev/null +++ b/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp @@ -0,0 +1,3 @@ +!CHAPTER The Simple Authentication App + +WRITEME \ No newline at end of file diff --git a/Documentation/Books/Users/FoxxBundledApps/Users.mdpp b/Documentation/Books/Users/FoxxBundledApps/Users.mdpp new file mode 100644 index 0000000000..17e031398f --- /dev/null +++ b/Documentation/Books/Users/FoxxBundledApps/Users.mdpp @@ -0,0 +1,3 @@ +!CHAPTER The Users App + +WRITEME \ No newline at end of file diff --git a/Documentation/Books/Users/SUMMARY.md b/Documentation/Books/Users/SUMMARY.md index 87376280d2..07c5a1e9e9 100644 --- a/Documentation/Books/Users/SUMMARY.md +++ b/Documentation/Books/Users/SUMMARY.md @@ -97,6 +97,10 @@ * [Foxx Exports](Foxx/FoxxExports.md) * [Foxx Job Queues](Foxx/FoxxQueues.md) * [Optional Functionality](Foxx/FoxxOptional.md) + * [Bundled Applications](FoxxBundledApps/README.md) + * [Session Storage](FoxxBundledApps/Sessions.md) + * [User Storage](FoxxBundledApps/Users.md) + * [Simple Authentication](FoxxBundledApps/SimpleAuth.md) * [Foxx Manager](FoxxManager/README.md) * [First Steps](FoxxManager/FirstSteps.md) @@ -115,6 +119,7 @@ * [Example Setup](Replication/ExampleSetup.md) * [Replication Limitations](Replication/Limitations.md) * [Replication Overhead](Replication/Overhead.md) + * [Replication Events](Replication/Events.md) * [Sharding](Sharding/README.md) * [How to try it out](Sharding/HowTo.md) From b36239dd259eca4c794242b09306a7c54591ec10 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Mon, 23 Jun 2014 15:18:31 +0200 Subject: [PATCH 03/44] Documented sessions app. --- .../Books/Users/FoxxBundledApps/Sessions.mdpp | 177 +++++++++++++++++- 1 file changed, 176 insertions(+), 1 deletion(-) diff --git a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp index 2e30c664c1..c9ab42c4f0 100644 --- a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp @@ -1,3 +1,178 @@ !CHAPTER The Sessions App -WRITEME \ No newline at end of file +The sessions app provides a session storage JavaScript API that can be used in other Foxx apps. + +```js +var sessionStorage = Foxx.requireApp('/_sessions').sessionStorage; +``` + +!SECTION Exceptions + +!SUBSECTION Session Not Found + +```js +try { + sessionStorage.get(invalidSessionId); +} catch(err) { + assertTrue(err instanceof sessionStorage.errors.SessionNotFound); +} +``` + +!SUBSUBSECTION Description + +Thrown by the session storage's `delete` and `get` methods if passed a session ID that does not exist in the database. + +!SUBSECTION Session Expired + +```js +try { + sessionStorage.get(expiredSessionId); +} catch(err) { + assertTrue(err instanceof sessionStorage.errors.SessionExpired); + assertTrue(err instanceof sessionStorage.errors.SessionNotFound); +} +``` + +!SUBSUBSECTION Description + +Thrown by the session storage's `get` method if passed a session ID for a session that has expired. See also this app's configuration options. + +!SECTION Create a session + +```js +var session = sessionStorage.create(sessionData); +assertEqual(session.get('sessionData'), sessionData); +``` + +!SUBSECTION Description + +A session can be created by using the session storage's `create` method. The method optionally takes an object that will be stored as the session's `sessionData` attribute. The created session will be saved automatically with its initial data. + +!SECTION Fetch an existing session + +There are two ways to fetch a session via the session storage API: + +* resolving a session cookie with the `fromCookie` method +* calling the session storage's `get` method with a session ID directly + +!SUBSECTION Resolve a session cookie + +```js +var session = sessionStorage.fromCookie(request, cookieName, secret); +``` + +!SUBSUBSECTION Description + +The method `fromCookie` will attempt to parse the given request's session cookie and return a session with the matching session ID. + +Optionally, a `secret` string can be passed which will be used to verify the cookie's signature (see `addCookie` below). + +The method will return `null` instead of a session object in the following cases: + +* the request has no session cookie +* the request's session cookie does not match a known session ID +* the matching session has expired +* the cookie's signature is missing (if a `secret` is provided) +* the cookie's signature does not match (if a `secret` is provided) + +!SUBSECTION Resolve a session ID directly + +```js +var session = sessionStorage.get(sessionId); +``` + +!SUBSUBSECTION Description + +The method `get` will attempt to load the session with the given session ID from the database. If the session does not exist, a `SessionNotFound` exception will be thrown. If the session does exist, but has already expired, a `SessionExpired` exception will be thrown instead. + +!SECTION Delete a session + +There are two ways to delete a session from the database: + +* calling the session storage's `delete` method with a session ID directly +* telling a session to delete itself + +!SUBSECTION Delete a session by its ID + +```js +sessionStorage.delete(sessionId); +``` + +!SUBSUBSECTION Description + +The method `delete` will attempt to delete the session with the given session ID from the database. If the session does not exist, a `SessionNotFound` exception will be thrown. The method always returns `null`. + +!SUBSECTION Tell a session to delete itself + +```js +session.delete(); +``` + +!SUBSUBSECTION Description + +The session's `delete` method will attempt to delete the session from the database. + +Returns `true` if the session was deleted successfully. + +Returns `false` if the session already didn't exist. + +!SECTION The session object + +Session objects are instances of a Foxx model with the following attributes: + +`sessionData` +Volatile session data. This can be an arbitrary object that will be stored with the session in the database. If you want to store session-specific (rather than user-specific) data in the database, this is the right place for that. + +`uid` +The session's active user's `_key` or `undefined`. + +`userData` +The session's active user's `userData` attribute or an empty object. + +!SECTION Save a session + +```js +session.save(); +``` + +!SUBSECTION Description + +Saves the session to the database. If you made any changes to the session and are not using the sessions app via Foxx Authentication, you must call this method to commit the changes to the database. + +!SECTION Set a session's active user + +```js +session.setUser(user); +assertEqual(session.get('uid'), user.get('_key')); +assertEqual(session.get('userData'), user.get('userData')); +``` + +!SUBSECTION Description + +The `setUser` method expects a Foxx model with a `userData` attribute and sets the session's `uid` attribute to the model's `_key` and the session's `userData` attribute to the model's `userData` attribute. + +!SECTION Add a session cookie to a response + +```js +session.addCookie(response, cookieName, secret); +``` + +!SUBSECTION Description + +The `addCookie` method adds a session cookie to the response. + +If a `secret` string is provided, the cookie is signed using that secret (a second cookie with the name `cookieName + '_sig'` containing the cryptographic signature of the cookie value is added to the response). + +If you want to use signed cookies, you must make sure to pass the same `secret` to the `fromCookie` method when fetching the session from a cookie later. + +!SECTION Clear a session cookie + +```js +session.clearCookie(response, cookieName, secret); +``` + +!SUBSECTION Description + +The `clearCookie` method adds a blank expired cookie to clear the user's previously set session cookie. + +If the method is passed a `secret` string, a second blank expired cookie is added that overwrites the signature cookie (see above). \ No newline at end of file From 5767abf7f259af8e3b949ccbfbd29854e775de86 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Mon, 23 Jun 2014 16:06:33 +0200 Subject: [PATCH 04/44] Split lines. --- Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp index c9ab42c4f0..dbb7250436 100644 --- a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp @@ -121,12 +121,15 @@ Returns `false` if the session already didn't exist. Session objects are instances of a Foxx model with the following attributes: `sessionData` + Volatile session data. This can be an arbitrary object that will be stored with the session in the database. If you want to store session-specific (rather than user-specific) data in the database, this is the right place for that. `uid` + The session's active user's `_key` or `undefined`. `userData` + The session's active user's `userData` attribute or an empty object. !SECTION Save a session From 7f72d484dcbe221de5ecefc6ccb855b1a7a19985 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Mon, 23 Jun 2014 16:12:10 +0200 Subject: [PATCH 05/44] Removed 'Description' headers. --- .../Books/Users/FoxxBundledApps/Sessions.mdpp | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp index dbb7250436..c0ca6af813 100644 --- a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp @@ -18,8 +18,6 @@ try { } ``` -!SUBSUBSECTION Description - Thrown by the session storage's `delete` and `get` methods if passed a session ID that does not exist in the database. !SUBSECTION Session Expired @@ -33,8 +31,6 @@ try { } ``` -!SUBSUBSECTION Description - Thrown by the session storage's `get` method if passed a session ID for a session that has expired. See also this app's configuration options. !SECTION Create a session @@ -44,8 +40,6 @@ var session = sessionStorage.create(sessionData); assertEqual(session.get('sessionData'), sessionData); ``` -!SUBSECTION Description - A session can be created by using the session storage's `create` method. The method optionally takes an object that will be stored as the session's `sessionData` attribute. The created session will be saved automatically with its initial data. !SECTION Fetch an existing session @@ -61,8 +55,6 @@ There are two ways to fetch a session via the session storage API: var session = sessionStorage.fromCookie(request, cookieName, secret); ``` -!SUBSUBSECTION Description - The method `fromCookie` will attempt to parse the given request's session cookie and return a session with the matching session ID. Optionally, a `secret` string can be passed which will be used to verify the cookie's signature (see `addCookie` below). @@ -81,8 +73,6 @@ The method will return `null` instead of a session object in the following cases var session = sessionStorage.get(sessionId); ``` -!SUBSUBSECTION Description - The method `get` will attempt to load the session with the given session ID from the database. If the session does not exist, a `SessionNotFound` exception will be thrown. If the session does exist, but has already expired, a `SessionExpired` exception will be thrown instead. !SECTION Delete a session @@ -98,8 +88,6 @@ There are two ways to delete a session from the database: sessionStorage.delete(sessionId); ``` -!SUBSUBSECTION Description - The method `delete` will attempt to delete the session with the given session ID from the database. If the session does not exist, a `SessionNotFound` exception will be thrown. The method always returns `null`. !SUBSECTION Tell a session to delete itself @@ -108,8 +96,6 @@ The method `delete` will attempt to delete the session with the given session ID session.delete(); ``` -!SUBSUBSECTION Description - The session's `delete` method will attempt to delete the session from the database. Returns `true` if the session was deleted successfully. @@ -138,8 +124,6 @@ The session's active user's `userData` attribute or an empty object. session.save(); ``` -!SUBSECTION Description - Saves the session to the database. If you made any changes to the session and are not using the sessions app via Foxx Authentication, you must call this method to commit the changes to the database. !SECTION Set a session's active user @@ -150,8 +134,6 @@ assertEqual(session.get('uid'), user.get('_key')); assertEqual(session.get('userData'), user.get('userData')); ``` -!SUBSECTION Description - The `setUser` method expects a Foxx model with a `userData` attribute and sets the session's `uid` attribute to the model's `_key` and the session's `userData` attribute to the model's `userData` attribute. !SECTION Add a session cookie to a response @@ -160,8 +142,6 @@ The `setUser` method expects a Foxx model with a `userData` attribute and sets t session.addCookie(response, cookieName, secret); ``` -!SUBSECTION Description - The `addCookie` method adds a session cookie to the response. If a `secret` string is provided, the cookie is signed using that secret (a second cookie with the name `cookieName + '_sig'` containing the cryptographic signature of the cookie value is added to the response). @@ -174,8 +154,6 @@ If you want to use signed cookies, you must make sure to pass the same `secret` session.clearCookie(response, cookieName, secret); ``` -!SUBSECTION Description - The `clearCookie` method adds a blank expired cookie to clear the user's previously set session cookie. If the method is passed a `secret` string, a second blank expired cookie is added that overwrites the signature cookie (see above). \ No newline at end of file From 779518baee36675476c1b13817b9e477714a9907 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Mon, 23 Jun 2014 16:16:13 +0200 Subject: [PATCH 06/44] Subsections for model attributes. --- Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp index c0ca6af813..fb33f83cf5 100644 --- a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp @@ -106,15 +106,15 @@ Returns `false` if the session already didn't exist. Session objects are instances of a Foxx model with the following attributes: -`sessionData` +!SUBSECTION sessionData Volatile session data. This can be an arbitrary object that will be stored with the session in the database. If you want to store session-specific (rather than user-specific) data in the database, this is the right place for that. -`uid` +!SUBSECTION uid The session's active user's `_key` or `undefined`. -`userData` +!SUBSECTION userData The session's active user's `userData` attribute or an empty object. From 83279e64c810a4a97f1d94f0b531eed8128ff2e1 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Mon, 23 Jun 2014 17:07:24 +0200 Subject: [PATCH 07/44] Cleaned up session docs to match style. --- .../Books/Users/FoxxBundledApps/Sessions.mdpp | 288 ++++++++++++------ 1 file changed, 196 insertions(+), 92 deletions(-) diff --git a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp index fb33f83cf5..708bc8996a 100644 --- a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp @@ -10,6 +10,14 @@ var sessionStorage = Foxx.requireApp('/_sessions').sessionStorage; !SUBSECTION Session Not Found +Indicates a session could not be found in the database. + +`new sessionStorage.errors.SessionNotFound(sessionId)` + +Thrown by the session storage's *delete* and *get* methods if passed a session ID that does not exist in the database. + +@EXAMPLES + ```js try { sessionStorage.get(invalidSessionId); @@ -18,10 +26,16 @@ try { } ``` -Thrown by the session storage's `delete` and `get` methods if passed a session ID that does not exist in the database. - !SUBSECTION Session Expired +Indicates the session exists in the database but has expired. + +`new sessionStorage.errors.SessionExpired(sessionId)` + +Thrown by the session storage's *get* method if passed a session ID for a session that has expired. See also this app's configuration options. + +@EXAMPLES + ```js try { sessionStorage.get(expiredSessionId); @@ -31,77 +45,6 @@ try { } ``` -Thrown by the session storage's `get` method if passed a session ID for a session that has expired. See also this app's configuration options. - -!SECTION Create a session - -```js -var session = sessionStorage.create(sessionData); -assertEqual(session.get('sessionData'), sessionData); -``` - -A session can be created by using the session storage's `create` method. The method optionally takes an object that will be stored as the session's `sessionData` attribute. The created session will be saved automatically with its initial data. - -!SECTION Fetch an existing session - -There are two ways to fetch a session via the session storage API: - -* resolving a session cookie with the `fromCookie` method -* calling the session storage's `get` method with a session ID directly - -!SUBSECTION Resolve a session cookie - -```js -var session = sessionStorage.fromCookie(request, cookieName, secret); -``` - -The method `fromCookie` will attempt to parse the given request's session cookie and return a session with the matching session ID. - -Optionally, a `secret` string can be passed which will be used to verify the cookie's signature (see `addCookie` below). - -The method will return `null` instead of a session object in the following cases: - -* the request has no session cookie -* the request's session cookie does not match a known session ID -* the matching session has expired -* the cookie's signature is missing (if a `secret` is provided) -* the cookie's signature does not match (if a `secret` is provided) - -!SUBSECTION Resolve a session ID directly - -```js -var session = sessionStorage.get(sessionId); -``` - -The method `get` will attempt to load the session with the given session ID from the database. If the session does not exist, a `SessionNotFound` exception will be thrown. If the session does exist, but has already expired, a `SessionExpired` exception will be thrown instead. - -!SECTION Delete a session - -There are two ways to delete a session from the database: - -* calling the session storage's `delete` method with a session ID directly -* telling a session to delete itself - -!SUBSECTION Delete a session by its ID - -```js -sessionStorage.delete(sessionId); -``` - -The method `delete` will attempt to delete the session with the given session ID from the database. If the session does not exist, a `SessionNotFound` exception will be thrown. The method always returns `null`. - -!SUBSECTION Tell a session to delete itself - -```js -session.delete(); -``` - -The session's `delete` method will attempt to delete the session from the database. - -Returns `true` if the session was deleted successfully. - -Returns `false` if the session already didn't exist. - !SECTION The session object Session objects are instances of a Foxx model with the following attributes: @@ -112,48 +55,209 @@ Volatile session data. This can be an arbitrary object that will be stored with !SUBSECTION uid -The session's active user's `_key` or `undefined`. +The session's active user's *_key* or *undefined* (no active user). !SUBSECTION userData -The session's active user's `userData` attribute or an empty object. +The session's active user's *userData* attribute or an empty object. + +!SECTION Create a session + +Creates and saves new instance of the session model. + +`sessionStorage.create(sessionData)` + +*Parameter* + +* *sessionData* (optional): an arbitrary object that will be stored as the session's *sessionData* attribute when the model is saved to the database. + +@EXAMPLES + +```js +var session = sessionStorage.create(sessionData); +assertEqual(session.get('sessionData'), sessionData); +``` + +!SECTION Fetch an existing session + +There are two ways to fetch a session via the session storage API: + +* resolving a session cookie with the *fromCookie* method +* calling the session storage's *get* method with a session ID directly + +!SUBSECTION Resolve a session cookie + +Fetch a session matching a cookie in a Foxx request. + +`sessionStorage.fromCookie(request, cookieName, secret)` + +Parses a request's cookies and returns the matching instance of the session model. + +The method will return *null* instead of a session object in the following cases: + +* the request has no session cookie +* the request's session cookie does not match a known session ID +* the matching session has expired +* the cookie's signature is missing (if a *secret* is provided) +* the cookie's signature does not match (if a *secret* is provided) + +*Parameter* + +* *request*: a Foxx request object as passed to controller routes. +* *cookieName*: name of the cookie to parse. +* *secret* (optional): secret string to validate the cookie's signature with. + +@EXAMPLES + +```js +controller.get('/hello', function(request, response) { + var session = sessionStorage.fromCookie(request, cookieName, secret); + response.json(session.get('sessionData')); +}); +``` + +!SUBSECTION Resolve a session ID directly + +Fetch a session from the database for a given ID. + +`sessionStorage.get(sessionId)` + +Attempts to load the session with the given session ID from the database. If the session does not exist, a *SessionNotFound* exception will be thrown. If the session does exist, but has already expired, a *SessionExpired* exception will be thrown instead. + +*Parameter* + +* *sessionId*: a session *_key*. + +@EXAMPLES + +```js +var session = sessionStorage.get(sessionId); +``` + +!SECTION Delete a session + +There are two ways to delete a session from the database: + +* calling the session storage's *delete* method with a session ID directly +* telling a session to delete itself + +!SUBSECTION Delete a session by its ID + +Delete a session with a given ID. + +`sessionStorage.delete(sessionId)` + +Attempts to delete the session with the given session ID from the database. If the session does not exist, a *SessionNotFound* exception will be thrown. The method always returns *null*. + +*Parameter* + +* *sessionId*: a session *_key*. + +@EXAMPLES + +```js +sessionStorage.delete(sessionId); +``` + +!SUBSECTION Tell a session to delete itself + +Delete a session from the database. + +`session.delete()` + +Attempts to delete the session from the database. + +Returns *true* if the session was deleted successfully. + +Returns *false* if the session already didn't exist. + +@EXAMPLES + +```js +session.delete(); +``` !SECTION Save a session +Save a session to the database. + +`session.save()` + +If you made any changes to the session and are not using the sessions app via Foxx Authentication, you must call this method to commit the changes to the database. + +@EXAMPLES + ```js +session.setUser(user); session.save(); ``` -Saves the session to the database. If you made any changes to the session and are not using the sessions app via Foxx Authentication, you must call this method to commit the changes to the database. - !SECTION Set a session's active user +Set the active user of a session. + +`session.setUser(user)` + +Expects a Foxx model with a *userData* attribute and sets the session's *uid* attribute to the model's *_key* and the session's *userData* attribute to the model's *userData* attribute. + +*Parameter* + +* *user*: instance of a Foxx model with a *`userData* attribute. + +@EXAMPLES + ```js session.setUser(user); assertEqual(session.get('uid'), user.get('_key')); assertEqual(session.get('userData'), user.get('userData')); ``` -The `setUser` method expects a Foxx model with a `userData` attribute and sets the session's `uid` attribute to the model's `_key` and the session's `userData` attribute to the model's `userData` attribute. - !SECTION Add a session cookie to a response +Add a session cookie to a Foxx response. + +`session.addCookie(response, cookieName, secret)` + +Adds a session cookie to the response. + +If a *secret* string is provided, the cookie is signed using that secret (a second cookie with the name *cookieName + '_sig'* containing the cryptographic signature of the cookie value is added to the response). + +If you want to use signed cookies, you must make sure to pass the same *secret* to the *fromCookie* method when fetching the session from a cookie later. + +*Parameter* + +* *response*: a Foxx response object as passed to controller routes. +* *cookieName*: name of the cookie to parse. +* *secret* (optional): secret string to sign the cookie with. + +@EXAMPLES + ```js -session.addCookie(response, cookieName, secret); +controller.get('/hello', function(request, response) { + session.addCookie(response, cookieName, secret); +}); ``` -The `addCookie` method adds a session cookie to the response. - -If a `secret` string is provided, the cookie is signed using that secret (a second cookie with the name `cookieName + '_sig'` containing the cryptographic signature of the cookie value is added to the response). - -If you want to use signed cookies, you must make sure to pass the same `secret` to the `fromCookie` method when fetching the session from a cookie later. - !SECTION Clear a session cookie +Clear the session cookie of a Foxx response. + +`session.clearCookie(response, cookieName, secret)` + +Adds a blank expired cookie to clear the user's previously set session cookie. + +If the method is passed a *secret* string, a second blank expired cookie is added that overwrites the signature cookie (see above). + +*Parameter* + +* *response*: a Foxx response object as passed to controller routes. +* *cookieName*: name of the cookie to parse. +* *secret* (optional): indicates the signature should be cleared also. + +@EXAMPLE + ```js -session.clearCookie(response, cookieName, secret); -``` - -The `clearCookie` method adds a blank expired cookie to clear the user's previously set session cookie. - -If the method is passed a `secret` string, a second blank expired cookie is added that overwrites the signature cookie (see above). \ No newline at end of file +controller.get('/goodbye', function(request, response) { + session.clearCookie(response, cookieName, secret); +}); +``` \ No newline at end of file From 5510f1bd149285efb36b900f3a443b729d75f49f Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Mon, 23 Jun 2014 17:26:30 +0200 Subject: [PATCH 08/44] Typo. --- Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp index 708bc8996a..9a6f47ca58 100644 --- a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp @@ -254,7 +254,7 @@ If the method is passed a *secret* string, a second blank expired cookie is adde * *cookieName*: name of the cookie to parse. * *secret* (optional): indicates the signature should be cleared also. -@EXAMPLE +@EXAMPLES ```js controller.get('/goodbye', function(request, response) { From 49e3cf967856d67eb7a9f7451fd53f759fe0958f Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Tue, 24 Jun 2014 10:54:01 +0200 Subject: [PATCH 09/44] Fixed typo. --- Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp index 9a6f47ca58..646b550961 100644 --- a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp @@ -63,7 +63,7 @@ The session's active user's *userData* attribute or an empty object. !SECTION Create a session -Creates and saves new instance of the session model. +Creates and saves a new instance of the session model. `sessionStorage.create(sessionData)` From b9edef489ca52838ec0682514c74b8d673d3f9e4 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Tue, 24 Jun 2014 10:54:16 +0200 Subject: [PATCH 10/44] Documented users app. --- .../Books/Users/FoxxBundledApps/Users.mdpp | 179 +++++++++++++++++- 1 file changed, 178 insertions(+), 1 deletion(-) diff --git a/Documentation/Books/Users/FoxxBundledApps/Users.mdpp b/Documentation/Books/Users/FoxxBundledApps/Users.mdpp index 17e031398f..986f1987d4 100644 --- a/Documentation/Books/Users/FoxxBundledApps/Users.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/Users.mdpp @@ -1,3 +1,180 @@ !CHAPTER The Users App -WRITEME \ No newline at end of file +The users app provides a username-based user storage JavaScript API that can be used in other Foxx apps. + +```js +var userStorage = Foxx.requireApp('/_users').userStorage; +``` + +!SECTION Exceptions + +!SUBSECTION User Not Found + +Indicates a user could not be found in the database. + +`new userStorage.errors.UserNotFound(userId)` + +Thrown by the user storage's *delete* and *get* methods if passed a user ID that does not exist in the database. + +@EXAMPLES + +```js +try { + userStorage.get(invalidUserId); +} catch(err) { + assertTrue(err instanceof userStorage.errors.UserNotFound); +} +``` + +!SUBSECTION Username Not Available + +Indicates a username is already in use. + +`new userStorage.errors.UsernameNotAvailable(username)` + +Thrown by the user storage's *create* method if passed a *userData* object with a *username* that is already in use. + +@EXAMPLES + +```js +try { + userStorage.create({username: 'alreadyTaken'}); +} catch(err) { + assertTrue(err instanceof userStorage.errors.UsernameNotAvailable); +} +``` + +!SECTION The user object + +User objects are instances of a Foxx model with the following attributes: + +!SUBSECTION userData + +Application-specific user data. This is an arbitrary object that must at least have a *username* property set to the user's username. + +!SUBSECTION authData + +An arbitrary object used by authentication apps to store sensitive data. For password authentication this could be a hash, for third-party authentication services this could be information about the user's identity. This attribute should never be exposed to the user directly. + +!SECTION Create a user + +Creates and saves a new instance of the user model. + +`userStorage.create(userData)` + +Throws *UsernameNotAvailable* if a user with the given username already exists. + +*Parameter* + +* *userData*: an arbitrary object that will be stored as the user's *userData* attribute when the model is saved to the database. This object must at least have a *username* property set to a string. + +@EXAMPLES + +```js +var user = userStorage.create({username: 'malaclypse'}); +assertEqual(user.get('userData').username, 'malaclypse'); +``` + +!SECTION Fetch an existing user + +There are two ways to fetch a user via the user storage API: + +* resolving a *username* with the user storage's *resolve* method +* calling the user storage's *get* method with a user ID directly + +!SUBSECTION Resolve a *username* + +Fetches a user with a given *username*. + +`userStorage.resolve(username)` + +If the username can not be resolved, a *UserNotFound* exception will be thrown instead. + +*Parameter* + +* *username*: an arbitrary string matching the username of the user you are trying to fetch. + +@EXAMPLES + +```js +var user = userStorage.resolve('malaclypse'); +assertEqual(user.get('userData').username, 'malaclypse'); +``` + +!SUBSECTION Resolve a user ID directly + +Fetches a user with a given ID. + +`userStorage.get(userId)` + +Attempts to fetch the user with the given ID from the database. If the user does not exist, a *UserNotFound* exception will be thrown instead. + +*Parameter* + +* *userId*: a user *_key*. + +@EXAMPLES + +```js +var user = userStorage.get(userId); +assertEqual(user.get('_key'), userId); +``` + +!SECTION Delete a user + +There are two ways to delete a user from the database: + +* calling the user storage's *delete* method with a user ID directly +* telling a user to delete itself + +!SUBSECTION Delete a user by its ID + +Delete a user with a given ID. + +`userStorage.delete(userId)` + +Attempts to delete the user with the given user ID from the database. If the user does not exist, a *UserNotFound* exception will be thrown. The method always returns *null*. + +*Parameter* + +* *userId*: a user *_key*. + +@EXAMPLES + +```js +userStorage.delete(userId); +``` + +!SUBSECTION Tell a user to delete itself + +Delete a user from the database. + +`user.delete()` + +Attempts to delete the user from the database. + +Returns *true* if the user was deleted successfully. + +Returns *false* if the user already didn't exist. + +@EXAMPLES + +```js +user.delete(); +``` + +!SECTION Save a user + +Save a user to the database. + +`user.save()` + +In order to commit changes made to the user in your code, you need to call this method. + +@EXAMPLES + +```js +var userData = user.get('userData'); +userData.counter++; +user.save(); +``` From ef03b6091607f1dbd0a3b261b5436e4b093f36ff Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Tue, 24 Jun 2014 13:21:26 +0200 Subject: [PATCH 11/44] Formatting. --- .../Books/Users/FoxxBundledApps/Sessions.mdpp | 17 ++++++----------- .../Books/Users/FoxxBundledApps/Users.mdpp | 9 ++------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp index 646b550961..b81611df0c 100644 --- a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp @@ -49,17 +49,12 @@ try { Session objects are instances of a Foxx model with the following attributes: -!SUBSECTION sessionData - -Volatile session data. This can be an arbitrary object that will be stored with the session in the database. If you want to store session-specific (rather than user-specific) data in the database, this is the right place for that. - -!SUBSECTION uid - -The session's active user's *_key* or *undefined* (no active user). - -!SUBSECTION userData - -The session's active user's *userData* attribute or an empty object. +* *sessionData*: volatile session data. This can be an arbitrary object that will be stored with the session in the database. If you want to store session-specific (rather than user-specific) data in the database, this is the right place for that. +* *uid*: the session's active user's *_key* or *undefined* (no active user). +* *userData*: the session's active user's *userData* attribute or an empty object. +* *created*: timestamp the session was created at. +* *lastAccess*: timestamp of the last time the session was fetched from the database. +* *lastUpdate*: timestamp of the last time the session was written to the database. !SECTION Create a session diff --git a/Documentation/Books/Users/FoxxBundledApps/Users.mdpp b/Documentation/Books/Users/FoxxBundledApps/Users.mdpp index 986f1987d4..32ea472e0a 100644 --- a/Documentation/Books/Users/FoxxBundledApps/Users.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/Users.mdpp @@ -48,13 +48,8 @@ try { User objects are instances of a Foxx model with the following attributes: -!SUBSECTION userData - -Application-specific user data. This is an arbitrary object that must at least have a *username* property set to the user's username. - -!SUBSECTION authData - -An arbitrary object used by authentication apps to store sensitive data. For password authentication this could be a hash, for third-party authentication services this could be information about the user's identity. This attribute should never be exposed to the user directly. +* *userData*: application-specific user data. This is an arbitrary object that must at least have a *username* property set to the user's username. +* *authData*: an arbitrary object used by authentication apps to store sensitive data. For password authentication this could be a hash, for third-party authentication services this could be information about the user's identity. This attribute should never be exposed to the user directly. !SECTION Create a user From a5302532fba18e8e37bc668f91ae52f0dbb607f8 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Tue, 24 Jun 2014 13:21:44 +0200 Subject: [PATCH 12/44] Documented session configs. --- Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp index b81611df0c..ab242d4024 100644 --- a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp @@ -2,6 +2,13 @@ The sessions app provides a session storage JavaScript API that can be used in other Foxx apps. +*Configuration* + +* *timeToLive* (optional): number of milliseconds until the session expires. Default: *604800000* (one week). +* *ttlType* (optional): attribute against which the *timeToLive* is enforced. Valid options: *lastAccess*, *lastUpdate*, *created*. Default: *"created"*. +* *sidTimestamp* (optional): whether to append a timestamp to the random part of generated session IDs. Default: *false*. +* *sidLength* (optional): number of random characters to use for new session IDs. Default *20*. + ```js var sessionStorage = Foxx.requireApp('/_sessions').sessionStorage; ``` From 6dd8bf6a527a5f03b556724c96ca980c67939d39 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Tue, 24 Jun 2014 13:21:55 +0200 Subject: [PATCH 13/44] Documented simple auth app. --- .../Users/FoxxBundledApps/SimpleAuth.mdpp | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp b/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp index 11d5fa34ee..92b2cf8827 100644 --- a/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp @@ -1,3 +1,41 @@ !CHAPTER The Simple Authentication App -WRITEME \ No newline at end of file +The simple auth app provides hashed password-based authentication with automatically generated salts and constant-time password verification. + +*Configuration* + +* *saltLength* (optional): length of newly generated salts. Default: *16*. +* *hashMethod* (optional): hash algorithm to use. Supported values: *sha1*, *sha224*, *sha256*, *md5*. Default: *"sha256"*. + +```js +var auth = Foxx.requireApp('/_auth').auth; +``` + +!SECTION Generate an authentication object + +Generates an authentication object for a given password. + +`auth.hashPassword(password)` + +Returns an authentication object with the following properties: + +* *hash*: the generated hex-encoded hash. +* *salt*: the salt used to generate the hash. +* *method*: the algorithm used to generate the hash. + +*Parameter* + +* *password*: the password to hash. + +!SECTION Verify a password + +Verifies a password against a given authentication object. + +`auth.verifyPassword(authData, password)` + +Generates a hash for the given password using the *salt* and *method* stored in the authentication object and performs a constant time string comparison on them. Returns *true* if the password is valid or *false* otherwise. + +*Parameter* + +* *authData*: an authentication object. +* *password*: a password to verify. \ No newline at end of file From 63e8a432738a0fb9cbf86815b86ad7ec31d22265 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Tue, 24 Jun 2014 14:19:26 +0200 Subject: [PATCH 14/44] Documented Foxx auth. --- .../Books/Users/Foxx/FoxxAuthentication.mdpp | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp diff --git a/Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp b/Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp new file mode 100644 index 0000000000..68dc021009 --- /dev/null +++ b/Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp @@ -0,0 +1,54 @@ +!CHAPTER Foxx Authentication + +Foxx provides some convenience methods to make working with sessions easier. + +!SUBSECTION Activate authentication + +Enables authentication features for the controller. + +`controller.activateAuthentication(options)` + +Once authentication has been activated, a *session* property will be added to the *request* object passed to route handlers defined on the controller, which will be a saved instance of the session model provided by the session storage. + +If the option *autoCreateSession* has not explicitly been set to *false*, a new session will be created for users that do not yet have an active session. + +If *type* is set to *"cookie"*, the session cookie will be updated after every route. + +*Parameter* + +* *options* (optional): an object with any of the following properties: + * *sessionStorageApp* (optional): mount point of the session storage app to use. Default: *"/_sessions"*. + * *type* (optional): authentication type, currently only *"cookie"* is supported. Default: *"cookie"*. + * *cookieName* (optional): name of the session cookie if using cookie authentication. If a *cookieSecret* is provided, the signature will be stored in a cookie named *cookieName + "_sig"*. Defaults to *"sid"*. + * *cookieSecret* (optional): secret string to sign session cookies with if using cookie authentication. + * *autoCreateSession* (optional): whether a session should always be created if none exists. Default: *true*. + +@EXAMPLES + +```js +var controller = new FoxxController(applicationContext); +controller.activateAuthentication({ + sessionStorageApp: '/_sessions', + cookieName: 'sid', + cookieSecret: 'secret', + type: 'cookie' +}); +``` + +!SUBSECTION Define a logout route + +Defines a route that will destroy the session. + +`controller.logout(path, options)` + +Defines a route handler on the controller that performs a logout. + +When using cookie authentication, this function will clear the session cookie (if *autoCreateSession* was disabled) or create a new session cookie, before calling the *after* function. + +*Parameter* + +* *path*: route path as passed to *controller.get*, *controller.post*, etc. +* *options* (optional): an object with any of the following properties: + * *method* (optional): HTTP method to handle. Default: *"post"*. + * *before* (optional): function to execute before the session is destroyed. Receives the same arguments as a regular route handler. + * *after* (optional): function to execute after the session is destroyed. Receives the same arguments as a regular route handler. \ No newline at end of file From b3ecb4f2b3a31839dbace1d4f6b71ed67b2d2850 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Tue, 24 Jun 2014 14:23:04 +0200 Subject: [PATCH 15/44] Documented default 'after' handler for Foxx Auth logout. --- Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp b/Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp index 68dc021009..7c5991d576 100644 --- a/Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp +++ b/Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp @@ -51,4 +51,4 @@ When using cookie authentication, this function will clear the session cookie (i * *options* (optional): an object with any of the following properties: * *method* (optional): HTTP method to handle. Default: *"post"*. * *before* (optional): function to execute before the session is destroyed. Receives the same arguments as a regular route handler. - * *after* (optional): function to execute after the session is destroyed. Receives the same arguments as a regular route handler. \ No newline at end of file + * *after* (optional): function to execute after the session is destroyed. Receives the same arguments as a regular route handler. Default: a function that sends a *{"message": "logged out"}* JSON response. \ No newline at end of file From c4173ae1922ced90d7c99ebf4bc86145515eab4f Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Tue, 24 Jun 2014 16:16:41 +0200 Subject: [PATCH 16/44] Expanded example. --- Documentation/Books/Users/FoxxBundledApps/Users.mdpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/Books/Users/FoxxBundledApps/Users.mdpp b/Documentation/Books/Users/FoxxBundledApps/Users.mdpp index 32ea472e0a..ea0ca8ab35 100644 --- a/Documentation/Books/Users/FoxxBundledApps/Users.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/Users.mdpp @@ -155,6 +155,7 @@ Returns *false* if the user already didn't exist. @EXAMPLES ```js +var user = userStorage.get(userId); user.delete(); ``` From 7c17a98ade8dde09e6c32e3c431c0fd0def6d67c Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Tue, 24 Jun 2014 16:16:52 +0200 Subject: [PATCH 17/44] Documented OAuth2 app. --- .../Books/Users/FoxxBundledApps/OAuth2.mdpp | 300 +++++++++++++++++- 1 file changed, 299 insertions(+), 1 deletion(-) diff --git a/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp b/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp index 90b22adf7e..02d60906cd 100644 --- a/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp @@ -1,3 +1,301 @@ !CHAPTER The OAuth2 Authentication App -WRITEME \ No newline at end of file +The OAuth2 authentication app provides authentication abstractions over OAuth2 providers like Facebook, GitHub and Google. If you want to support additional providers, you can easily define your own. + +```js +var providers = Foxx.requireApp('/_oauth2').providers; +``` + +!SECTION Configuration + +The app requires no configuration, but you need to update its *providers* collection with your client ID and client secret for each provider you wish to support. + +!SUBSECTION Registering your app with GitHub + +If you want to use the *github* provider, you need to obtain a client ID and client secret from GitHub. + +1. Create a regular account at [GitHub](https://github.com) or use an existing account you own. +2. Go to [Account Settings > Applications > Register new application](https://github.com/settings/applications/new). +3. Provide an *authorization callback URL*. This must match your *redirect_uri* later. +4. Fill in the other required details and follow the instructions provided. +5. Open the application page, then note down the *Client ID* and *Client Secret*. +6. Update the *github* provider by setting its *clientId* attribute to the *Client ID* and its *clientSecret* attribute to the *Client Secret*. Don't forget to save your changes. + +!SUBSECTION Registering your app with Facebook + +If you want to use the *facebook* provider, you need to obtain a client ID and client secret from Facebook. + +1. Create a regular account at [Facebook](https://www.facebook.com) or use an existing account you own. +2. Visit the [Facebook Developers](https://developers.facebook.com) page. +3. Click on *Apps* in the menu, then select *Register as a Developer* (the only option) and follow the instructions provided. You may need to verify your account by phone. +4. Click on *Apps* in the menu, then select *Create a New App* and follow the instructions provided. +5. Open the app dashboard, then note down the *App ID* and *App Secret*. The secret may be hidden by default. +6. Click on *Settings*, then *Advanced* and enter one or more *Valid OAuth redirect URIs*. At least one of them must match your *redirect_uri* later. Don't forget to save your changes. +7. Update the *facebook* provider by setting its *clientId* attribute to the *App ID* and its *clientSecret* attribute to the *App Secret*. Don't forget to save your changes. + +!SUBSECTION Registering your app with Google + +If you want to use the *google* provider, you need to obtain a client ID and client secret from Google. + +1. Create a regular account at [Google](https://www.google.com) or use an existing account you own. +2. Visit the [Google Developers Console](https://console.developers.google.com). +3. Click on *Create Project*, then follow the instructions provided. +4. When your project is ready, open the project dashboard, then click on *Enable an API*. +5. Enable the *Google+ API* to allow your app to distinguish between different users. +6. Open the *Credentials* page and click *Create new Client ID*, then follow the instructions provided. At least one *Authorized Redirect URI* must match your *redirect_uri* later. At least one *Authorized JavaScript Origin* must match your app's fully-qualified domain. +7. When the Client ID is ready, note down the *Client ID* and *Client secret*. +8. Update the *google* provider by settiing its *clientId* attribute to the *Client ID* and its *clientSecret* attribute to the *Client secret*. Don't forget to save your changes. + +!SECTION Exceptions + +!SUBSECTION Provider Not Found + +Indicates a provider could not be found in the database. + +`new providers.errors.ProviderNotFound(providerId)` + +Thrown by the provider storage's *delete* and *get* methods if passed a provider ID that does not exist in the database. + +@EXAMPLES + +```js +try { + providers.get(invalidProviderId); +} catch(err) { + assertTrue(err instanceof providers.errors.ProviderNotFound); +} +``` + +!SECTION The provider object + +Provider objects are instances of a Foxx model with the following attributes: + +* *label*: a human-readable identifier for the provider (e.g. *"GitHub"*). +* *authEndpoint*: the fully-qualified URL of the provider's [authorization endpoint](http://tools.ietf.org/html/rfc6749#section-3.1). +* *tokenEndpoint*: the fully-qualified URL of the provider's [token endpoint](http://tools.ietf.org/html/rfc6749#section-3.2). +* *refreshEndpoint* (optional): the fully-qualified URL of the provider's [refresh token](http://tools.ietf.org/html/rfc6749#section-6) endpoint. +* *activeUserEndpoint* (optional): the fully-qualified URL of the provider's endpoint for fetching details about the current user. +* *usernameTemplate* (optional): An underscore.js template for extracting the user's permanent identifier from the user's details. Default: *"<%= id %>"*. +* *clientId*: the client ID of the app registered with the provider. +* *clientSecret*: the client secret of the app registered with the provider. + +!SECTION List available OAuth2 providers + +Returns a list of OAuth2 providers that can be presented to the front-end. + +`providers.list()` + +Each item in the list is an object with the following properties: + +* *_key*: *_key* of the provider. +* *label*: a human-readable identifier that can be presented to the user. +* *clientId*: the client ID stored for the given provider. + +If you wish to exclude providers you don't support in your app, you need to filter the result manually. + +@EXAMPLES + +```js +var supportedProviders = providers.list().filter(function(obj) { + return Boolean(obj.clientId); +}); +``` + +!SECTION Define a new OAuth2 provider + +Creates a new OAuth2 provider with the given data. + +`providers.create(data)` + +Saves and returns a new instance of the provider model with its attributes set to the properties of the given object. + +*Parameter* + +* *data*: an arbitrary object, see above. + +@EXAMPLES + +```js +var provider = providers.create({ + _key: 'myoauth2', + label: 'My OAuth2 Provider', + authEndpoint: 'https://example.com/oauth2/authorize', + tokenEndpoint: 'https://example.com/oauth2/access_token', + refreshEndpoint: 'https://example.com/oauth2/access_token', + activeUserEndpoint: 'https://example.com/api/v2/users/me', + usernameTemplate: '<%= user_id %>', + clientId: '1234567890', + clientSecret: 'kEyBoArDcAt' +}); +``` + +!SECTION Fetch an existing OAuth2 provider + +Fetches an existing OAuth2 provider from the database. + +`providers.get(providerId)` + +Throws a *ProviderNotFound* exception if the provider does not exist. + +*Parameters* + +* *providerId*: the provider's *_key*. + +@EXAMPLES + +```js +var provider = providers.get('github'); +assertTrue(provider.get('_key'), 'github'); +``` + +!SECTION Delete a provider + +There are two ways to delete a provider from the database: + +* calling the provider storage's *delete* method with a provider's *_key* directly +* telling a provider to delete itself + +!SUBSECTION Delete a provider by its ID + +Delete a provider with a given ID. + +`providers.delete(providerId)` + +Attempts to delete the provider with the given *providerId* from the database. If the provider does not exist, a *ProviderNotFound* exception will be thrown. The method always returns *null*. + +*Parameter* + +* *providerId*: a provider *_key*. + +@EXAMPLES + +```js +providers.delete('github'); +``` + +!SUBSECTION Tell a provider to delete itself + +Delete a provider from the database. + +`provider.delete()` + +Attempts to delete the provider from the database. + +Returns *true* if the provider was deleted successfully. + +Returns *false* if the provider already didn't exist. + +@EXAMPLES + +```js +var provider = providers.get('github'); +provider.delete(); +``` + +!SECTION Save a provider + +Save a provider to the database. + +`provider.save()` + +In order to commit changes made to the provider in your code, you need to call this method. + +@EXAMPLES + +```js +provider.set('clientId', '1234567890'); +provider.set('clientSecret', 'kEyBoArDcAt'); +provider.save(); +``` + + +!SECTION Get the authorization URL of a provider + +Generates the authorization URL for the authorization endpoint of the provider. + +`provider.getAuthUrl(redirect_uri, args)` + +Returns a fully-qualified URL for the authorization endpoint of the provider by appending the provider object's client ID and any additional arguments from *args* to the provider object's *authEndpoint*. + +*Parameter* + +* *redirect_uri*: the fully-qualified URL of your app's OAuth2 callback. +* *args* (optional): an object with any of the following properties: + * *response_type* (optional): See [RFC 6749](http://tools.ietf.org/html/rfc6749). Default: *"code"*. + +!SECTION _getTokenRequest + +(Internal.) Generates the token URL and request body for token endpoint of the provider. + +`provider._getTokenRequest(code, redirect_uri, args)` + +Returns an object with two properties: + +* *url*: the fully-qualified URL for the token endpoint. +* *body*: the form-encoded request body for the token endpoint created by appending the provider object's client ID, client secret and any additional arguments from *args* to the provider object's *tokenEndpoint*. + +*Parameter* + +* *code*: a grant code returned by the provider's authorization endpoint. +* *redirect_uri*: the original callback URL with which the code was requested. +* *args* (optional): an object with any of the following properties: + * *grant_type* (optional): see [RFC 6749](http://tools.ietf.org/html/rfc6749). Default: *"authorization_code"*. + +!SECTION Exchange a grant code for an access token + +Exchanges a grant code for an access token. + +`provider.exchangeGrantToken(code, redirect_uri)` + +Performs a *POST* response to the provider object's *tokenEndpoint* and returns the parsed response body. + +Throws an exception if the remote server responds with an empty response body. + +*Parameter* + +* *code*: a grant code returned by the provider's authorization endpoint. +* *redirect_uri*: the original callback URL with which the code was requested. +* *args* (optional): an object with any of the following properties: + * *grant_type* (optional): see [RFC 6749](http://tools.ietf.org/html/rfc6749). Default: *"authorization_code"*. + +!SECTION Fetch the active user + +Fetches details of the active user. + +`provider.fetchActiveUser(access_token)` + +Performs a *GET* response to the provider object's *activeUserEndpoint* and returns the parsed response body. + +Throws an exception if the remote server responds with an empty response body. + +Also throws an exception if the provider object has no *activeUserEndpoint*. + +*Parameter* + +* *access_token*: an OAuth2 access token as returned by *exchangeGrantToken*. + +@EXAMPLES + +```js +var authData = provider.exchangeGrantToken(code, redirect_uri); +var userData = provider.fetchActiveUser(authData.access_token); +``` + +!SECTION Get a user's identifier + +Fetches the user's identifier from a user object returned by the provider. + +`provider.getUsername(userData)` + +Applies the provider's *usernameTemplate* to the given user object. + +*Parameter* + +* *userData*: the object returned by *getActiveUser*. + +@EXAMPLES + +```js +var userData = provider.fetchActiveUser(access_token); +var username = provider.getUsername(userData); +``` From 4e7bb5f2a21c6c98e06bbb6149b5792d33f0116b Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Tue, 24 Jun 2014 16:18:35 +0200 Subject: [PATCH 18/44] Consistency. --- Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp b/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp index 02d60906cd..fd701a2690 100644 --- a/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp @@ -12,7 +12,7 @@ The app requires no configuration, but you need to update its *providers* collec !SUBSECTION Registering your app with GitHub -If you want to use the *github* provider, you need to obtain a client ID and client secret from GitHub. +If you want to use the *github* provider, you need to obtain a client ID and client secret from GitHub: 1. Create a regular account at [GitHub](https://github.com) or use an existing account you own. 2. Go to [Account Settings > Applications > Register new application](https://github.com/settings/applications/new). @@ -23,7 +23,7 @@ If you want to use the *github* provider, you need to obtain a client ID and cli !SUBSECTION Registering your app with Facebook -If you want to use the *facebook* provider, you need to obtain a client ID and client secret from Facebook. +If you want to use the *facebook* provider, you need to obtain a client ID and client secret from Facebook: 1. Create a regular account at [Facebook](https://www.facebook.com) or use an existing account you own. 2. Visit the [Facebook Developers](https://developers.facebook.com) page. @@ -35,7 +35,7 @@ If you want to use the *facebook* provider, you need to obtain a client ID and c !SUBSECTION Registering your app with Google -If you want to use the *google* provider, you need to obtain a client ID and client secret from Google. +If you want to use the *google* provider, you need to obtain a client ID and client secret from Google: 1. Create a regular account at [Google](https://www.google.com) or use an existing account you own. 2. Visit the [Google Developers Console](https://console.developers.google.com). From 96d1452c475db0ab6ed7e11f9339bd3a1a271a06 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Tue, 24 Jun 2014 16:23:14 +0200 Subject: [PATCH 19/44] Wroteyou. --- Documentation/Books/Users/FoxxBundledApps/README.mdpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Documentation/Books/Users/FoxxBundledApps/README.mdpp b/Documentation/Books/Users/FoxxBundledApps/README.mdpp index 5ddf6ac64e..16f20ff67d 100644 --- a/Documentation/Books/Users/FoxxBundledApps/README.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/README.mdpp @@ -1,5 +1,3 @@ !CHAPTER Bundled Foxx Applications -ArangoDB ships with a number of Foxx apps that allow you to implement basic authentication and user management with minimal effort. - -WRITEME \ No newline at end of file +ArangoDB ships with a number of Foxx apps that allow you to implement basic authentication and user management with minimal effort. \ No newline at end of file From 7ef8e7d9d4e581fded761d7c1139245699d11ad1 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Tue, 24 Jun 2014 17:14:35 +0200 Subject: [PATCH 20/44] Reformatted docs for consistency. --- .../Books/Users/FoxxBundledApps/OAuth2.mdpp | 42 ++++++++++--------- .../Books/Users/FoxxBundledApps/Sessions.mdpp | 40 +++++++++++------- .../Users/FoxxBundledApps/SimpleAuth.mdpp | 14 +++++-- .../Books/Users/FoxxBundledApps/Users.mdpp | 30 +++++++------ 4 files changed, 76 insertions(+), 50 deletions(-) diff --git a/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp b/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp index fd701a2690..81542d451c 100644 --- a/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp @@ -2,10 +2,6 @@ The OAuth2 authentication app provides authentication abstractions over OAuth2 providers like Facebook, GitHub and Google. If you want to support additional providers, you can easily define your own. -```js -var providers = Foxx.requireApp('/_oauth2').providers; -``` - !SECTION Configuration The app requires no configuration, but you need to update its *providers* collection with your client ID and client secret for each provider you wish to support. @@ -46,9 +42,17 @@ If you want to use the *google* provider, you need to obtain a client ID and cli 7. When the Client ID is ready, note down the *Client ID* and *Client secret*. 8. Update the *google* provider by settiing its *clientId* attribute to the *Client ID* and its *clientSecret* attribute to the *Client secret*. Don't forget to save your changes. -!SECTION Exceptions +!SECTION JavaScript API: *providers* -!SUBSECTION Provider Not Found +This app exposes its functionality via a JavaScript API named *providers*. + +```js +var providers = Foxx.requireApp('/_oauth2').providers; +``` + +!SUBSECTION Exceptions + +!SUBSUBSECTION Provider Not Found Indicates a provider could not be found in the database. @@ -66,7 +70,7 @@ try { } ``` -!SECTION The provider object +!SUBSECTION The provider object Provider objects are instances of a Foxx model with the following attributes: @@ -79,7 +83,7 @@ Provider objects are instances of a Foxx model with the following attributes: * *clientId*: the client ID of the app registered with the provider. * *clientSecret*: the client secret of the app registered with the provider. -!SECTION List available OAuth2 providers +!SUBSECTION List available OAuth2 providers Returns a list of OAuth2 providers that can be presented to the front-end. @@ -101,7 +105,7 @@ var supportedProviders = providers.list().filter(function(obj) { }); ``` -!SECTION Define a new OAuth2 provider +!SUBSECTION Define a new OAuth2 provider Creates a new OAuth2 provider with the given data. @@ -129,7 +133,7 @@ var provider = providers.create({ }); ``` -!SECTION Fetch an existing OAuth2 provider +!SUBSECTION Fetch an existing OAuth2 provider Fetches an existing OAuth2 provider from the database. @@ -148,14 +152,14 @@ var provider = providers.get('github'); assertTrue(provider.get('_key'), 'github'); ``` -!SECTION Delete a provider +!SUBSECTION Delete a provider There are two ways to delete a provider from the database: * calling the provider storage's *delete* method with a provider's *_key* directly * telling a provider to delete itself -!SUBSECTION Delete a provider by its ID +!SUBSUBSECTION Delete a provider by its ID Delete a provider with a given ID. @@ -173,7 +177,7 @@ Attempts to delete the provider with the given *providerId* from the database. I providers.delete('github'); ``` -!SUBSECTION Tell a provider to delete itself +!SUBSUBSECTION Tell a provider to delete itself Delete a provider from the database. @@ -192,7 +196,7 @@ var provider = providers.get('github'); provider.delete(); ``` -!SECTION Save a provider +!SUBSECTION Save a provider Save a provider to the database. @@ -209,7 +213,7 @@ provider.save(); ``` -!SECTION Get the authorization URL of a provider +!SUBSECTION Get the authorization URL of a provider Generates the authorization URL for the authorization endpoint of the provider. @@ -223,7 +227,7 @@ Returns a fully-qualified URL for the authorization endpoint of the provider by * *args* (optional): an object with any of the following properties: * *response_type* (optional): See [RFC 6749](http://tools.ietf.org/html/rfc6749). Default: *"code"*. -!SECTION _getTokenRequest +!SUBSECTION _getTokenRequest (Internal.) Generates the token URL and request body for token endpoint of the provider. @@ -241,7 +245,7 @@ Returns an object with two properties: * *args* (optional): an object with any of the following properties: * *grant_type* (optional): see [RFC 6749](http://tools.ietf.org/html/rfc6749). Default: *"authorization_code"*. -!SECTION Exchange a grant code for an access token +!SUBSECTION Exchange a grant code for an access token Exchanges a grant code for an access token. @@ -258,7 +262,7 @@ Throws an exception if the remote server responds with an empty response body. * *args* (optional): an object with any of the following properties: * *grant_type* (optional): see [RFC 6749](http://tools.ietf.org/html/rfc6749). Default: *"authorization_code"*. -!SECTION Fetch the active user +!SUBSECTION Fetch the active user Fetches details of the active user. @@ -281,7 +285,7 @@ var authData = provider.exchangeGrantToken(code, redirect_uri); var userData = provider.fetchActiveUser(authData.access_token); ``` -!SECTION Get a user's identifier +!SUBSECTION Get a user's identifier Fetches the user's identifier from a user object returned by the provider. diff --git a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp index ab242d4024..68b7eafc45 100644 --- a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp @@ -2,20 +2,28 @@ The sessions app provides a session storage JavaScript API that can be used in other Foxx apps. -*Configuration* +!SECTION Configuration + +This app has the following configuration options: * *timeToLive* (optional): number of milliseconds until the session expires. Default: *604800000* (one week). * *ttlType* (optional): attribute against which the *timeToLive* is enforced. Valid options: *lastAccess*, *lastUpdate*, *created*. Default: *"created"*. * *sidTimestamp* (optional): whether to append a timestamp to the random part of generated session IDs. Default: *false*. * *sidLength* (optional): number of random characters to use for new session IDs. Default *20*. +!SECTION JavaScript API: *sessionStorage* + +This app exposes a session storage via a JavaScript API named *sessionStorage*. + +@EXAMPLES + ```js var sessionStorage = Foxx.requireApp('/_sessions').sessionStorage; ``` -!SECTION Exceptions +!SUBSECTION Exceptions -!SUBSECTION Session Not Found +!SUBSUBSECTION Session Not Found Indicates a session could not be found in the database. @@ -33,7 +41,7 @@ try { } ``` -!SUBSECTION Session Expired +!SUBSUBSECTION Session Expired Indicates the session exists in the database but has expired. @@ -52,7 +60,7 @@ try { } ``` -!SECTION The session object +!SUBSECTION The session object Session objects are instances of a Foxx model with the following attributes: @@ -63,7 +71,7 @@ Session objects are instances of a Foxx model with the following attributes: * *lastAccess*: timestamp of the last time the session was fetched from the database. * *lastUpdate*: timestamp of the last time the session was written to the database. -!SECTION Create a session +!SUBSECTION Create a session Creates and saves a new instance of the session model. @@ -80,14 +88,14 @@ var session = sessionStorage.create(sessionData); assertEqual(session.get('sessionData'), sessionData); ``` -!SECTION Fetch an existing session +!SUBSECTION Fetch an existing session There are two ways to fetch a session via the session storage API: * resolving a session cookie with the *fromCookie* method * calling the session storage's *get* method with a session ID directly -!SUBSECTION Resolve a session cookie +!SUBSUBSECTION Resolve a session cookie Fetch a session matching a cookie in a Foxx request. @@ -118,7 +126,7 @@ controller.get('/hello', function(request, response) { }); ``` -!SUBSECTION Resolve a session ID directly +!SUBSUBSECTION Resolve a session ID directly Fetch a session from the database for a given ID. @@ -136,14 +144,14 @@ Attempts to load the session with the given session ID from the database. If the var session = sessionStorage.get(sessionId); ``` -!SECTION Delete a session +!SUBSECTION Delete a session There are two ways to delete a session from the database: * calling the session storage's *delete* method with a session ID directly * telling a session to delete itself -!SUBSECTION Delete a session by its ID +!SUBSUBSECTION Delete a session by its ID Delete a session with a given ID. @@ -161,7 +169,7 @@ Attempts to delete the session with the given session ID from the database. If t sessionStorage.delete(sessionId); ``` -!SUBSECTION Tell a session to delete itself +!SUBSUBSECTION Tell a session to delete itself Delete a session from the database. @@ -179,7 +187,7 @@ Returns *false* if the session already didn't exist. session.delete(); ``` -!SECTION Save a session +!SUBSECTION Save a session Save a session to the database. @@ -194,7 +202,7 @@ session.setUser(user); session.save(); ``` -!SECTION Set a session's active user +!SUBSECTION Set a session's active user Set the active user of a session. @@ -214,7 +222,7 @@ assertEqual(session.get('uid'), user.get('_key')); assertEqual(session.get('userData'), user.get('userData')); ``` -!SECTION Add a session cookie to a response +!SUBSECTION Add a session cookie to a response Add a session cookie to a Foxx response. @@ -240,7 +248,7 @@ controller.get('/hello', function(request, response) { }); ``` -!SECTION Clear a session cookie +!SUBSECTION Clear a session cookie Clear the session cookie of a Foxx response. diff --git a/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp b/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp index 92b2cf8827..51483481cb 100644 --- a/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp @@ -2,16 +2,24 @@ The simple auth app provides hashed password-based authentication with automatically generated salts and constant-time password verification. -*Configuration* +!SECTION Configuration + +This app has the following configuration options: * *saltLength* (optional): length of newly generated salts. Default: *16*. * *hashMethod* (optional): hash algorithm to use. Supported values: *sha1*, *sha224*, *sha256*, *md5*. Default: *"sha256"*. +!SECTION JavaScript API: *auth* + +This app exposes its functionality via a JavaScript API named *auth*. + +@EXAMPLES + ```js var auth = Foxx.requireApp('/_auth').auth; ``` -!SECTION Generate an authentication object +!SUBSECTION Generate an authentication object Generates an authentication object for a given password. @@ -27,7 +35,7 @@ Returns an authentication object with the following properties: * *password*: the password to hash. -!SECTION Verify a password +!SUBSECTION Verify a password Verifies a password against a given authentication object. diff --git a/Documentation/Books/Users/FoxxBundledApps/Users.mdpp b/Documentation/Books/Users/FoxxBundledApps/Users.mdpp index ea0ca8ab35..1e61710fce 100644 --- a/Documentation/Books/Users/FoxxBundledApps/Users.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/Users.mdpp @@ -2,13 +2,19 @@ The users app provides a username-based user storage JavaScript API that can be used in other Foxx apps. +!SECTION JavaScript API: *userStorage* + +This app exposes a user storage via a JavaScript API named *userStorage*. + +@EXAMPLES + ```js var userStorage = Foxx.requireApp('/_users').userStorage; ``` -!SECTION Exceptions +!SUBSECTION Exceptions -!SUBSECTION User Not Found +!SUBSUBSECTION User Not Found Indicates a user could not be found in the database. @@ -26,7 +32,7 @@ try { } ``` -!SUBSECTION Username Not Available +!SUBSUBSECTION Username Not Available Indicates a username is already in use. @@ -44,14 +50,14 @@ try { } ``` -!SECTION The user object +!SUBSECTION The user object User objects are instances of a Foxx model with the following attributes: * *userData*: application-specific user data. This is an arbitrary object that must at least have a *username* property set to the user's username. * *authData*: an arbitrary object used by authentication apps to store sensitive data. For password authentication this could be a hash, for third-party authentication services this could be information about the user's identity. This attribute should never be exposed to the user directly. -!SECTION Create a user +!SUBSECTION Create a user Creates and saves a new instance of the user model. @@ -70,14 +76,14 @@ var user = userStorage.create({username: 'malaclypse'}); assertEqual(user.get('userData').username, 'malaclypse'); ``` -!SECTION Fetch an existing user +!SUBSECTION Fetch an existing user There are two ways to fetch a user via the user storage API: * resolving a *username* with the user storage's *resolve* method * calling the user storage's *get* method with a user ID directly -!SUBSECTION Resolve a *username* +!SUBSUBSECTION Resolve a *username* Fetches a user with a given *username*. @@ -96,7 +102,7 @@ var user = userStorage.resolve('malaclypse'); assertEqual(user.get('userData').username, 'malaclypse'); ``` -!SUBSECTION Resolve a user ID directly +!SUBSUBSECTION Resolve a user ID directly Fetches a user with a given ID. @@ -115,14 +121,14 @@ var user = userStorage.get(userId); assertEqual(user.get('_key'), userId); ``` -!SECTION Delete a user +!SUBSECTION Delete a user There are two ways to delete a user from the database: * calling the user storage's *delete* method with a user ID directly * telling a user to delete itself -!SUBSECTION Delete a user by its ID +!SUBSUBSECTION Delete a user by its ID Delete a user with a given ID. @@ -140,7 +146,7 @@ Attempts to delete the user with the given user ID from the database. If the use userStorage.delete(userId); ``` -!SUBSECTION Tell a user to delete itself +!SUBSUBSECTION Tell a user to delete itself Delete a user from the database. @@ -159,7 +165,7 @@ var user = userStorage.get(userId); user.delete(); ``` -!SECTION Save a user +!SUBSECTION Save a user Save a user to the database. From c3a6c8cce2c1e7609cfe397971b298776bfe0bce Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Wed, 25 Jun 2014 10:51:16 +0200 Subject: [PATCH 21/44] activateAuthentication -> activateSessions. Moved new functionality to new names to allow re-enabling legacy functionality. --- ...xAuthentication.mdpp => FoxxSessions.mdpp} | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) rename Documentation/Books/Users/Foxx/{FoxxAuthentication.mdpp => FoxxSessions.mdpp} (59%) diff --git a/Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp b/Documentation/Books/Users/Foxx/FoxxSessions.mdpp similarity index 59% rename from Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp rename to Documentation/Books/Users/Foxx/FoxxSessions.mdpp index 7c5991d576..8a4d65e0b3 100644 --- a/Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp +++ b/Documentation/Books/Users/Foxx/FoxxSessions.mdpp @@ -1,14 +1,14 @@ -!CHAPTER Foxx Authentication +!CHAPTER Foxx Sessions Foxx provides some convenience methods to make working with sessions easier. -!SUBSECTION Activate authentication +!SUBSECTION Activate sessions -Enables authentication features for the controller. +Enables session features for the controller. -`controller.activateAuthentication(options)` +`controller.activateSessions(options)` -Once authentication has been activated, a *session* property will be added to the *request* object passed to route handlers defined on the controller, which will be a saved instance of the session model provided by the session storage. +Once sessions have been activated, a *session* property will be added to the *request* object passed to route handlers defined on the controller, which will be a saved instance of the session model provided by the session storage. If the option *autoCreateSession* has not explicitly been set to *false*, a new session will be created for users that do not yet have an active session. @@ -18,16 +18,16 @@ If *type* is set to *"cookie"*, the session cookie will be updated after every r * *options* (optional): an object with any of the following properties: * *sessionStorageApp* (optional): mount point of the session storage app to use. Default: *"/_sessions"*. - * *type* (optional): authentication type, currently only *"cookie"* is supported. Default: *"cookie"*. - * *cookieName* (optional): name of the session cookie if using cookie authentication. If a *cookieSecret* is provided, the signature will be stored in a cookie named *cookieName + "_sig"*. Defaults to *"sid"*. - * *cookieSecret* (optional): secret string to sign session cookies with if using cookie authentication. + * *type* (optional): sessions type, currently only *"cookie"* is supported. Default: *"cookie"*. + * *cookieName* (optional): name of the session cookie if using cookie sessions. If a *cookieSecret* is provided, the signature will be stored in a cookie named *cookieName + "_sig"*. Defaults to *"sid"*. + * *cookieSecret* (optional): secret string to sign session cookies with if using cookie sessions. * *autoCreateSession* (optional): whether a session should always be created if none exists. Default: *true*. @EXAMPLES ```js var controller = new FoxxController(applicationContext); -controller.activateAuthentication({ +controller.activateSessions({ sessionStorageApp: '/_sessions', cookieName: 'sid', cookieSecret: 'secret', @@ -35,15 +35,15 @@ controller.activateAuthentication({ }); ``` -!SUBSECTION Define a logout route +!SUBSECTION Define a session destruction route Defines a route that will destroy the session. -`controller.logout(path, options)` +`controller.destroySession(path, options)` -Defines a route handler on the controller that performs a logout. +Defines a route handler on the controller that destroys the session. -When using cookie authentication, this function will clear the session cookie (if *autoCreateSession* was disabled) or create a new session cookie, before calling the *after* function. +When using cookie sessions, this function will clear the session cookie (if *autoCreateSession* was disabled) or create a new session cookie, before calling the *after* function. *Parameter* @@ -51,4 +51,4 @@ When using cookie authentication, this function will clear the session cookie (i * *options* (optional): an object with any of the following properties: * *method* (optional): HTTP method to handle. Default: *"post"*. * *before* (optional): function to execute before the session is destroyed. Receives the same arguments as a regular route handler. - * *after* (optional): function to execute after the session is destroyed. Receives the same arguments as a regular route handler. Default: a function that sends a *{"message": "logged out"}* JSON response. \ No newline at end of file + * *after* (optional): function to execute after the session is destroyed. Receives the same arguments as a regular route handler. Default: a function that sends a *{"message": "logged out"}* JSON response. From 6d9313f532573173a6dc602cdd9a634a59454b8b Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Wed, 25 Jun 2014 11:28:40 +0200 Subject: [PATCH 22/44] Fixed documentation reference. --- Documentation/Books/Users/SUMMARY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/Books/Users/SUMMARY.md b/Documentation/Books/Users/SUMMARY.md index 07c5a1e9e9..1823b29570 100644 --- a/Documentation/Books/Users/SUMMARY.md +++ b/Documentation/Books/Users/SUMMARY.md @@ -96,6 +96,7 @@ * [Dependency Injection](Foxx/FoxxInjection.md) * [Foxx Exports](Foxx/FoxxExports.md) * [Foxx Job Queues](Foxx/FoxxQueues.md) + * [Foxx Sessions](Foxx/FoxxSessions.md) * [Optional Functionality](Foxx/FoxxOptional.md) * [Bundled Applications](FoxxBundledApps/README.md) * [Session Storage](FoxxBundledApps/Sessions.md) From 66068924e16f041bf2fb6a971ac60b5584db5d1c Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Wed, 25 Jun 2014 11:29:10 +0200 Subject: [PATCH 23/44] Added stub documentation for FoxxAuthentication. --- Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp diff --git a/Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp b/Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp new file mode 100644 index 0000000000..1f2d7e0a9c --- /dev/null +++ b/Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp @@ -0,0 +1,6 @@ +!CHAPTER Foxx Authentication + +**Warning Deprecated** + +This functionality is deprecated and will be removed soon. +Please use [Foxx Sessions](./FoxxSessions.md) instead. \ No newline at end of file From af2b1c1ff6db1a60accf6638084d56cd709ee664 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Wed, 25 Jun 2014 14:54:37 +0200 Subject: [PATCH 24/44] Added bundled apps as system apps. --- js/Makefile.files | 4 + js/apps/system/oauth2/errors.js | 12 ++ js/apps/system/oauth2/manifest.json | 17 ++ js/apps/system/oauth2/providers.js | 214 +++++++++++++++++++++++ js/apps/system/oauth2/setup.js | 47 +++++ js/apps/system/sessions/app.js | 78 +++++++++ js/apps/system/sessions/errors.js | 18 ++ js/apps/system/sessions/manifest.json | 44 +++++ js/apps/system/sessions/setup.js | 11 ++ js/apps/system/sessions/storage.js | 201 +++++++++++++++++++++ js/apps/system/simple-auth/auth.js | 29 +++ js/apps/system/simple-auth/manifest.json | 28 +++ js/apps/system/users/errors.js | 18 ++ js/apps/system/users/manifest.json | 17 ++ js/apps/system/users/setup.js | 11 ++ js/apps/system/users/storage.js | 121 +++++++++++++ 16 files changed, 870 insertions(+) create mode 100644 js/apps/system/oauth2/errors.js create mode 100644 js/apps/system/oauth2/manifest.json create mode 100644 js/apps/system/oauth2/providers.js create mode 100644 js/apps/system/oauth2/setup.js create mode 100644 js/apps/system/sessions/app.js create mode 100644 js/apps/system/sessions/errors.js create mode 100644 js/apps/system/sessions/manifest.json create mode 100644 js/apps/system/sessions/setup.js create mode 100644 js/apps/system/sessions/storage.js create mode 100644 js/apps/system/simple-auth/auth.js create mode 100644 js/apps/system/simple-auth/manifest.json create mode 100644 js/apps/system/users/errors.js create mode 100644 js/apps/system/users/manifest.json create mode 100644 js/apps/system/users/setup.js create mode 100644 js/apps/system/users/storage.js diff --git a/js/Makefile.files b/js/Makefile.files index e8aa030420..919df4269c 100644 --- a/js/Makefile.files +++ b/js/Makefile.files @@ -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"` \ diff --git a/js/apps/system/oauth2/errors.js b/js/apps/system/oauth2/errors.js new file mode 100644 index 0000000000..6ad3ce55ff --- /dev/null +++ b/js/apps/system/oauth2/errors.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; +}()); \ No newline at end of file diff --git a/js/apps/system/oauth2/manifest.json b/js/apps/system/oauth2/manifest.json new file mode 100644 index 0000000000..b1a4db8fdf --- /dev/null +++ b/js/apps/system/oauth2/manifest.json @@ -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" +} diff --git a/js/apps/system/oauth2/providers.js b/js/apps/system/oauth2/providers.js new file mode 100644 index 0000000000..698f934824 --- /dev/null +++ b/js/apps/system/oauth2/providers.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; +}()); \ No newline at end of file diff --git a/js/apps/system/oauth2/setup.js b/js/apps/system/oauth2/setup.js new file mode 100644 index 0000000000..4dea8c6c26 --- /dev/null +++ b/js/apps/system/oauth2/setup.js @@ -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); + } + }); +}()); \ No newline at end of file diff --git a/js/apps/system/sessions/app.js b/js/apps/system/sessions/app.js new file mode 100644 index 0000000000..bc571422c9 --- /dev/null +++ b/js/apps/system/sessions/app.js @@ -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.'); +}()); \ No newline at end of file diff --git a/js/apps/system/sessions/errors.js b/js/apps/system/sessions/errors.js new file mode 100644 index 0000000000..e9d79e6164 --- /dev/null +++ b/js/apps/system/sessions/errors.js @@ -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; +}()); \ No newline at end of file diff --git a/js/apps/system/sessions/manifest.json b/js/apps/system/sessions/manifest.json new file mode 100644 index 0000000000..ac6ca75d76 --- /dev/null +++ b/js/apps/system/sessions/manifest.json @@ -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 + } + } +} diff --git a/js/apps/system/sessions/setup.js b/js/apps/system/sessions/setup.js new file mode 100644 index 0000000000..35a6ee8997 --- /dev/null +++ b/js/apps/system/sessions/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 sessionsName = applicationContext.collectionName('sessions'); + + if (db._collection(sessionsName) === null) { + db._create(sessionsName); + } +}()); \ No newline at end of file diff --git a/js/apps/system/sessions/storage.js b/js/apps/system/sessions/storage.js new file mode 100644 index 0000000000..c380bb8127 --- /dev/null +++ b/js/apps/system/sessions/storage.js @@ -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; +}()); \ No newline at end of file diff --git a/js/apps/system/simple-auth/auth.js b/js/apps/system/simple-auth/auth.js new file mode 100644 index 0000000000..cb40930ad2 --- /dev/null +++ b/js/apps/system/simple-auth/auth.js @@ -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; +}()); \ No newline at end of file diff --git a/js/apps/system/simple-auth/manifest.json b/js/apps/system/simple-auth/manifest.json new file mode 100644 index 0000000000..1f803ba56a --- /dev/null +++ b/js/apps/system/simple-auth/manifest.json @@ -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 + } + } +} diff --git a/js/apps/system/users/errors.js b/js/apps/system/users/errors.js new file mode 100644 index 0000000000..bcc31bf1a7 --- /dev/null +++ b/js/apps/system/users/errors.js @@ -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; +}()); \ No newline at end of file diff --git a/js/apps/system/users/manifest.json b/js/apps/system/users/manifest.json new file mode 100644 index 0000000000..b0e437bd65 --- /dev/null +++ b/js/apps/system/users/manifest.json @@ -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" +} diff --git a/js/apps/system/users/setup.js b/js/apps/system/users/setup.js new file mode 100644 index 0000000000..d6417b2345 --- /dev/null +++ b/js/apps/system/users/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); + } +}()); \ No newline at end of file diff --git a/js/apps/system/users/storage.js b/js/apps/system/users/storage.js new file mode 100644 index 0000000000..16a753e8ff --- /dev/null +++ b/js/apps/system/users/storage.js @@ -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; +}()); \ No newline at end of file From a85bc09de4f79dbd00f7f8c9b81ac69fd45cfb4e Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Wed, 25 Jun 2014 15:45:59 +0200 Subject: [PATCH 25/44] Only prohibit unmounting system apps mounted at system paths. --- js/server/modules/org/arangodb/foxx/manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/server/modules/org/arangodb/foxx/manager.js b/js/server/modules/org/arangodb/foxx/manager.js index 74ed9e3f7f..4b14fbe398 100644 --- a/js/server/modules/org/arangodb/foxx/manager.js +++ b/js/server/modules/org/arangodb/foxx/manager.js @@ -1236,7 +1236,7 @@ exports.unmount = function (mount) { var doc = mountFromId(mount); - if (doc.isSystem) { + if (doc.isSystem && (mount.charAt(1) === '_' || mount.indexOf('system') === 1)) { throw new Error("Cannot unmount system application"); } From 7bfb6d5d3ff8a6f499a043aae60357a8011d96f9 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Wed, 25 Jun 2014 16:53:18 +0200 Subject: [PATCH 26/44] Fixed activateSessions default app path. --- js/server/modules/org/arangodb/foxx/sessions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/server/modules/org/arangodb/foxx/sessions.js b/js/server/modules/org/arangodb/foxx/sessions.js index 8ab9a7ec7f..08a15a4450 100644 --- a/js/server/modules/org/arangodb/foxx/sessions.js +++ b/js/server/modules/org/arangodb/foxx/sessions.js @@ -151,7 +151,7 @@ function Sessions(opts) { opts.autoCreateSession = true; } if (!opts.sessionStorageApp) { - opts.sessionStorageApp = '/_sessions'; + opts.sessionStorageApp = '/system/sessions'; } this.configuration = opts; } From cb19215d85b2d13b2aa3f8df1dc65f0442f5dd7e Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Wed, 25 Jun 2014 16:55:58 +0200 Subject: [PATCH 27/44] Documentation now reflects correct bundled app paths. --- Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp | 2 +- Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp | 2 +- Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp | 2 +- Documentation/Books/Users/FoxxBundledApps/Users.mdpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp b/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp index 81542d451c..3a8506c241 100644 --- a/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp @@ -47,7 +47,7 @@ If you want to use the *google* provider, you need to obtain a client ID and cli This app exposes its functionality via a JavaScript API named *providers*. ```js -var providers = Foxx.requireApp('/_oauth2').providers; +var providers = Foxx.requireApp('/system/oauth2').providers; ``` !SUBSECTION Exceptions diff --git a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp index 68b7eafc45..b3e773b094 100644 --- a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp @@ -18,7 +18,7 @@ This app exposes a session storage via a JavaScript API named *sessionStorage*. @EXAMPLES ```js -var sessionStorage = Foxx.requireApp('/_sessions').sessionStorage; +var sessionStorage = Foxx.requireApp('/system/sessions').sessionStorage; ``` !SUBSECTION Exceptions diff --git a/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp b/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp index 51483481cb..dfd6ef9c68 100644 --- a/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp @@ -16,7 +16,7 @@ This app exposes its functionality via a JavaScript API named *auth*. @EXAMPLES ```js -var auth = Foxx.requireApp('/_auth').auth; +var auth = Foxx.requireApp('/system/simple-auth').auth; ``` !SUBSECTION Generate an authentication object diff --git a/Documentation/Books/Users/FoxxBundledApps/Users.mdpp b/Documentation/Books/Users/FoxxBundledApps/Users.mdpp index 1e61710fce..2d040a941a 100644 --- a/Documentation/Books/Users/FoxxBundledApps/Users.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/Users.mdpp @@ -9,7 +9,7 @@ This app exposes a user storage via a JavaScript API named *userStorage*. @EXAMPLES ```js -var userStorage = Foxx.requireApp('/_users').userStorage; +var userStorage = Foxx.requireApp('/system/users').userStorage; ``` !SUBSECTION Exceptions From 5767a12bcdcc2ae19be5c435c8b0a9b64658d64a Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Wed, 25 Jun 2014 17:00:59 +0200 Subject: [PATCH 28/44] Documentation for sessions reflects correct sessions app path. --- Documentation/Books/Users/Foxx/FoxxSessions.mdpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/Books/Users/Foxx/FoxxSessions.mdpp b/Documentation/Books/Users/Foxx/FoxxSessions.mdpp index 8a4d65e0b3..41ad553024 100644 --- a/Documentation/Books/Users/Foxx/FoxxSessions.mdpp +++ b/Documentation/Books/Users/Foxx/FoxxSessions.mdpp @@ -17,7 +17,7 @@ If *type* is set to *"cookie"*, the session cookie will be updated after every r *Parameter* * *options* (optional): an object with any of the following properties: - * *sessionStorageApp* (optional): mount point of the session storage app to use. Default: *"/_sessions"*. + * *sessionStorageApp* (optional): mount point of the session storage app to use. Default: *"/system/sessions"*. * *type* (optional): sessions type, currently only *"cookie"* is supported. Default: *"cookie"*. * *cookieName* (optional): name of the session cookie if using cookie sessions. If a *cookieSecret* is provided, the signature will be stored in a cookie named *cookieName + "_sig"*. Defaults to *"sid"*. * *cookieSecret* (optional): secret string to sign session cookies with if using cookie sessions. @@ -28,7 +28,7 @@ If *type* is set to *"cookie"*, the session cookie will be updated after every r ```js var controller = new FoxxController(applicationContext); controller.activateSessions({ - sessionStorageApp: '/_sessions', + sessionStorageApp: '/system/sessions', cookieName: 'sid', cookieSecret: 'secret', type: 'cookie' From 38297b88cc94e6eec327d949c8d14c9177a2d22c Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Wed, 25 Jun 2014 17:19:00 +0200 Subject: [PATCH 29/44] Fixed sessions app relying on exports from static mount path. --- js/apps/system/sessions/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/apps/system/sessions/app.js b/js/apps/system/sessions/app.js index bc571422c9..8c8c226e83 100644 --- a/js/apps/system/sessions/app.js +++ b/js/apps/system/sessions/app.js @@ -6,7 +6,7 @@ var Foxx = require('org/arangodb/foxx'); var errors = require('./errors'); var controller = new Foxx.Controller(applicationContext); - var api = Foxx.requireApp('/sessions').sessionStorage; + var api = Foxx.requireApp(applicationContext.mount).sessionStorage; controller.post('/', function (req, res) { var session = api.create(req.body()); From cb3842b1497f41f777dfb03178e447bacdc51f74 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Wed, 25 Jun 2014 17:47:22 +0200 Subject: [PATCH 30/44] System apps now mount at _system instead of system. --- Documentation/Books/Users/Foxx/FoxxSessions.mdpp | 4 ++-- Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp | 2 +- Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp | 2 +- Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp | 2 +- Documentation/Books/Users/FoxxBundledApps/Users.mdpp | 2 +- js/server/modules/org/arangodb/foxx/sessions.js | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Documentation/Books/Users/Foxx/FoxxSessions.mdpp b/Documentation/Books/Users/Foxx/FoxxSessions.mdpp index 41ad553024..e867e28e27 100644 --- a/Documentation/Books/Users/Foxx/FoxxSessions.mdpp +++ b/Documentation/Books/Users/Foxx/FoxxSessions.mdpp @@ -17,7 +17,7 @@ If *type* is set to *"cookie"*, the session cookie will be updated after every r *Parameter* * *options* (optional): an object with any of the following properties: - * *sessionStorageApp* (optional): mount point of the session storage app to use. Default: *"/system/sessions"*. + * *sessionStorageApp* (optional): mount point of the session storage app to use. Default: *"/_system/sessions"*. * *type* (optional): sessions type, currently only *"cookie"* is supported. Default: *"cookie"*. * *cookieName* (optional): name of the session cookie if using cookie sessions. If a *cookieSecret* is provided, the signature will be stored in a cookie named *cookieName + "_sig"*. Defaults to *"sid"*. * *cookieSecret* (optional): secret string to sign session cookies with if using cookie sessions. @@ -28,7 +28,7 @@ If *type* is set to *"cookie"*, the session cookie will be updated after every r ```js var controller = new FoxxController(applicationContext); controller.activateSessions({ - sessionStorageApp: '/system/sessions', + sessionStorageApp: '/_system/sessions', cookieName: 'sid', cookieSecret: 'secret', type: 'cookie' diff --git a/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp b/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp index 3a8506c241..4882340a54 100644 --- a/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp @@ -47,7 +47,7 @@ If you want to use the *google* provider, you need to obtain a client ID and cli This app exposes its functionality via a JavaScript API named *providers*. ```js -var providers = Foxx.requireApp('/system/oauth2').providers; +var providers = Foxx.requireApp('/_system/oauth2').providers; ``` !SUBSECTION Exceptions diff --git a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp index b3e773b094..88b014f048 100644 --- a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp @@ -18,7 +18,7 @@ This app exposes a session storage via a JavaScript API named *sessionStorage*. @EXAMPLES ```js -var sessionStorage = Foxx.requireApp('/system/sessions').sessionStorage; +var sessionStorage = Foxx.requireApp('/_system/sessions').sessionStorage; ``` !SUBSECTION Exceptions diff --git a/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp b/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp index dfd6ef9c68..0191ac2b58 100644 --- a/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp @@ -16,7 +16,7 @@ This app exposes its functionality via a JavaScript API named *auth*. @EXAMPLES ```js -var auth = Foxx.requireApp('/system/simple-auth').auth; +var auth = Foxx.requireApp('/_system/simple-auth').auth; ``` !SUBSECTION Generate an authentication object diff --git a/Documentation/Books/Users/FoxxBundledApps/Users.mdpp b/Documentation/Books/Users/FoxxBundledApps/Users.mdpp index 2d040a941a..d19b5f5c7a 100644 --- a/Documentation/Books/Users/FoxxBundledApps/Users.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/Users.mdpp @@ -9,7 +9,7 @@ This app exposes a user storage via a JavaScript API named *userStorage*. @EXAMPLES ```js -var userStorage = Foxx.requireApp('/system/users').userStorage; +var userStorage = Foxx.requireApp('/_system/users').userStorage; ``` !SUBSECTION Exceptions diff --git a/js/server/modules/org/arangodb/foxx/sessions.js b/js/server/modules/org/arangodb/foxx/sessions.js index 08a15a4450..37a8d132c3 100644 --- a/js/server/modules/org/arangodb/foxx/sessions.js +++ b/js/server/modules/org/arangodb/foxx/sessions.js @@ -151,7 +151,7 @@ function Sessions(opts) { opts.autoCreateSession = true; } if (!opts.sessionStorageApp) { - opts.sessionStorageApp = '/system/sessions'; + opts.sessionStorageApp = '/_system/sessions'; } this.configuration = opts; } From 3be0e11110b65d6678ee50d619d42df501b0c0ba Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Wed, 25 Jun 2014 17:55:41 +0200 Subject: [PATCH 31/44] Simplified is-mounted-as-system-app check. --- js/server/modules/org/arangodb/foxx/manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/server/modules/org/arangodb/foxx/manager.js b/js/server/modules/org/arangodb/foxx/manager.js index 4b14fbe398..79b7532fe2 100644 --- a/js/server/modules/org/arangodb/foxx/manager.js +++ b/js/server/modules/org/arangodb/foxx/manager.js @@ -1236,7 +1236,7 @@ exports.unmount = function (mount) { var doc = mountFromId(mount); - if (doc.isSystem && (mount.charAt(1) === '_' || mount.indexOf('system') === 1)) { + if (doc.isSystem && mount.charAt(1) === '_') { throw new Error("Cannot unmount system application"); } From 54615415c24631fcb50fa6acbcfb2345ece45090 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Wed, 25 Jun 2014 18:18:48 +0200 Subject: [PATCH 32/44] Removed redundant foxx auth doc stub. --- Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp diff --git a/Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp b/Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp deleted file mode 100644 index 1f2d7e0a9c..0000000000 --- a/Documentation/Books/Users/Foxx/FoxxAuthentication.mdpp +++ /dev/null @@ -1,6 +0,0 @@ -!CHAPTER Foxx Authentication - -**Warning Deprecated** - -This functionality is deprecated and will be removed soon. -Please use [Foxx Sessions](./FoxxSessions.md) instead. \ No newline at end of file From 3b00e307ea501cb1043b7c4de8507cb1c7f0ac15 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Mon, 30 Jun 2014 10:45:13 +0200 Subject: [PATCH 33/44] Formatting. Fixed inline docs in controller. --- .../modules/org/arangodb/foxx/controller.js | 58 ++++++++++++------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/js/server/modules/org/arangodb/foxx/controller.js b/js/server/modules/org/arangodb/foxx/controller.js index 919c4efa04..76f2d1ca02 100644 --- a/js/server/modules/org/arangodb/foxx/controller.js +++ b/js/server/modules/org/arangodb/foxx/controller.js @@ -43,8 +43,8 @@ var Controller, // ----------------------------------------------------------------------------- //////////////////////////////////////////////////////////////////////////////// -/// @brief Create a new Controller /// @startDocuBlock JSF_foxx_controller_initializer +/// /// `new FoxxController(applicationContext, options)` /// /// This creates a new Controller. The first argument is the controller @@ -60,6 +60,7 @@ var Controller, /// urlPrefix: "/meadow" /// }); /// ``` +/// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// @@ -221,6 +222,7 @@ extend(Controller.prototype, { /// // Take this request and deal with it! /// }); /// ``` +/// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// @@ -244,6 +246,7 @@ extend(Controller.prototype, { /// // Take this request and deal with it! /// }); /// ``` +/// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// @@ -267,6 +270,7 @@ extend(Controller.prototype, { /// // Take this request and deal with it! /// }); /// ``` +/// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// @@ -290,6 +294,7 @@ extend(Controller.prototype, { /// // Take this request and deal with it! /// }); /// ``` +/// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// @@ -321,6 +326,7 @@ extend(Controller.prototype, { /// // Take this request and deal with it! /// }); /// ``` +/// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// @@ -351,6 +357,7 @@ extend(Controller.prototype, { /// //Do some crazy request logging /// }); /// ``` +/// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// @@ -388,6 +395,7 @@ extend(Controller.prototype, { /// //Do some crazy response logging /// }); /// ``` +/// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// @@ -433,30 +441,30 @@ extend(Controller.prototype, { }, //////////////////////////////////////////////////////////////////////////////// -/// @fn JSF_foxx_controller_activateAuthentication -/// @brief Activate authentication for this app +/// @startDocuBlock JSF_foxx_controller_activateAuthentication /// /// `FoxxController#activateAuthentication(opts)` /// /// To activate authentication for this authentication, first call this function. /// Provide the following arguments: /// -/// * `type`: Currently we only support `cookie`, but this will change in the future. -/// * `cookieLifetime`: An integer. Lifetime of cookies in seconds. -/// * `cookieName`: A string used as the name of the cookie. -/// * `sessionLifetime`: An integer. Lifetime of sessions in seconds. +/// * *type*: Currently we only support *cookie*, but this will change in the future +/// * *cookieLifetime*: An integer. Lifetime of cookies in seconds +/// * *cookieName*: A string used as the name of the cookie +/// * *sessionLifetime*: An integer. Lifetime of sessions in seconds /// +/// @EXAMPLES /// -/// *Examples* -/// -/// @code +/// ```js /// app.activateAuthentication({ /// type: "cookie", /// cookieLifetime: 360000, /// cookieName: "my_cookie", /// sessionLifetime: 400, /// }); -/// @endcode +/// ``` +/// +/// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// activateAuthentication: function (opts) { 'use strict'; @@ -468,8 +476,7 @@ extend(Controller.prototype, { }, //////////////////////////////////////////////////////////////////////////////// -/// @fn JSF_foxx_controller_login -/// @brief Add a login handler +/// @startDocuBlock JSF_foxx_controller_login /// /// `FoxxController#login(path, opts)` /// @@ -498,6 +505,7 @@ extend(Controller.prototype, { /// } /// }); /// ``` +/// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// login: function (route, opts) { @@ -507,8 +515,7 @@ extend(Controller.prototype, { }, //////////////////////////////////////////////////////////////////////////////// -/// @fn JSF_foxx_controller_logout -/// @brief Add a logout handler +/// @startDocuBlock JSF_foxx_controller_logout /// /// `FoxxController#logout(path, opts)` /// @@ -536,6 +543,7 @@ extend(Controller.prototype, { /// } /// }); /// ``` +/// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// logout: function (route, opts) { @@ -545,8 +553,7 @@ extend(Controller.prototype, { }, //////////////////////////////////////////////////////////////////////////////// -/// @fn JSF_foxx_controller_register -/// @brief Add a register handler +/// @startDocuBlock JSF_foxx_controller_register /// /// `FoxxController#register(path, opts)` /// @@ -570,8 +577,9 @@ extend(Controller.prototype, { /// *acceptedAttributes* and set it to an array containing strings with the names of /// the additional attributes you want to accept. All other attributes in the request /// will be ignored. +/// /// If you want default attributes for the accepted attributes or set additional fields -/// (for example *admin*) use the option `defaultAttributes` which should be a hash +/// (for example *admin*) use the option *defaultAttributes* which should be a hash /// mapping attribute names to default values. /// /// @EXAMPLES @@ -584,6 +592,8 @@ extend(Controller.prototype, { /// } /// }); /// ``` +/// +/// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// register: function (route, opts) { 'use strict'; @@ -595,8 +605,7 @@ extend(Controller.prototype, { }, //////////////////////////////////////////////////////////////////////////////// -/// @fn JSF_foxx_controller_changePassword -/// @brief Add a change password handler +/// @startDocuBlock JSF_foxx_controller_changePassword /// /// FoxxController#changePassword(route, opts)` /// @@ -625,6 +634,7 @@ extend(Controller.prototype, { /// } /// }); /// ``` +/// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// changePassword: function (route, opts) { @@ -643,7 +653,7 @@ extend(Controller.prototype, { }, //////////////////////////////////////////////////////////////////////////////// -/// @startDocuBlock JSF_foxx_controller_activateAuthentication +/// @startDocuBlock JSF_foxx_controller_activateSessions /// /// `FoxxController#activateAuthentication(opts)` /// @@ -667,6 +677,8 @@ extend(Controller.prototype, { /// sessionStorageApp: "/my-sessions" /// }); /// ``` +/// +/// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// activateSessions: function (opts) { 'use strict'; @@ -677,7 +689,7 @@ extend(Controller.prototype, { }, //////////////////////////////////////////////////////////////////////////////// -/// @startDocuBlock JSF_foxx_controller_logout +/// @startDocuBlock JSF_foxx_controller_destroySession /// /// `FoxxController#logout(path, opts)` /// @@ -699,6 +711,8 @@ extend(Controller.prototype, { /// res.json({"message": "Bye, Bye"}); /// }); /// ``` +/// +/// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// destroySession: function (route, opts) { 'use strict'; From 6a89d78929ed227e517ea4e80a78d12389747532 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Mon, 30 Jun 2014 13:08:05 +0200 Subject: [PATCH 34/44] Removed oauth2 app from system apps. --- .../Books/Users/FoxxBundledApps/OAuth2.mdpp | 305 ------------------ js/Makefile.files | 1 - js/apps/system/oauth2/errors.js | 12 - js/apps/system/oauth2/manifest.json | 17 - js/apps/system/oauth2/providers.js | 214 ------------ js/apps/system/oauth2/setup.js | 47 --- 6 files changed, 596 deletions(-) delete mode 100644 Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp delete mode 100644 js/apps/system/oauth2/errors.js delete mode 100644 js/apps/system/oauth2/manifest.json delete mode 100644 js/apps/system/oauth2/providers.js delete mode 100644 js/apps/system/oauth2/setup.js diff --git a/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp b/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp deleted file mode 100644 index 4882340a54..0000000000 --- a/Documentation/Books/Users/FoxxBundledApps/OAuth2.mdpp +++ /dev/null @@ -1,305 +0,0 @@ -!CHAPTER The OAuth2 Authentication App - -The OAuth2 authentication app provides authentication abstractions over OAuth2 providers like Facebook, GitHub and Google. If you want to support additional providers, you can easily define your own. - -!SECTION Configuration - -The app requires no configuration, but you need to update its *providers* collection with your client ID and client secret for each provider you wish to support. - -!SUBSECTION Registering your app with GitHub - -If you want to use the *github* provider, you need to obtain a client ID and client secret from GitHub: - -1. Create a regular account at [GitHub](https://github.com) or use an existing account you own. -2. Go to [Account Settings > Applications > Register new application](https://github.com/settings/applications/new). -3. Provide an *authorization callback URL*. This must match your *redirect_uri* later. -4. Fill in the other required details and follow the instructions provided. -5. Open the application page, then note down the *Client ID* and *Client Secret*. -6. Update the *github* provider by setting its *clientId* attribute to the *Client ID* and its *clientSecret* attribute to the *Client Secret*. Don't forget to save your changes. - -!SUBSECTION Registering your app with Facebook - -If you want to use the *facebook* provider, you need to obtain a client ID and client secret from Facebook: - -1. Create a regular account at [Facebook](https://www.facebook.com) or use an existing account you own. -2. Visit the [Facebook Developers](https://developers.facebook.com) page. -3. Click on *Apps* in the menu, then select *Register as a Developer* (the only option) and follow the instructions provided. You may need to verify your account by phone. -4. Click on *Apps* in the menu, then select *Create a New App* and follow the instructions provided. -5. Open the app dashboard, then note down the *App ID* and *App Secret*. The secret may be hidden by default. -6. Click on *Settings*, then *Advanced* and enter one or more *Valid OAuth redirect URIs*. At least one of them must match your *redirect_uri* later. Don't forget to save your changes. -7. Update the *facebook* provider by setting its *clientId* attribute to the *App ID* and its *clientSecret* attribute to the *App Secret*. Don't forget to save your changes. - -!SUBSECTION Registering your app with Google - -If you want to use the *google* provider, you need to obtain a client ID and client secret from Google: - -1. Create a regular account at [Google](https://www.google.com) or use an existing account you own. -2. Visit the [Google Developers Console](https://console.developers.google.com). -3. Click on *Create Project*, then follow the instructions provided. -4. When your project is ready, open the project dashboard, then click on *Enable an API*. -5. Enable the *Google+ API* to allow your app to distinguish between different users. -6. Open the *Credentials* page and click *Create new Client ID*, then follow the instructions provided. At least one *Authorized Redirect URI* must match your *redirect_uri* later. At least one *Authorized JavaScript Origin* must match your app's fully-qualified domain. -7. When the Client ID is ready, note down the *Client ID* and *Client secret*. -8. Update the *google* provider by settiing its *clientId* attribute to the *Client ID* and its *clientSecret* attribute to the *Client secret*. Don't forget to save your changes. - -!SECTION JavaScript API: *providers* - -This app exposes its functionality via a JavaScript API named *providers*. - -```js -var providers = Foxx.requireApp('/_system/oauth2').providers; -``` - -!SUBSECTION Exceptions - -!SUBSUBSECTION Provider Not Found - -Indicates a provider could not be found in the database. - -`new providers.errors.ProviderNotFound(providerId)` - -Thrown by the provider storage's *delete* and *get* methods if passed a provider ID that does not exist in the database. - -@EXAMPLES - -```js -try { - providers.get(invalidProviderId); -} catch(err) { - assertTrue(err instanceof providers.errors.ProviderNotFound); -} -``` - -!SUBSECTION The provider object - -Provider objects are instances of a Foxx model with the following attributes: - -* *label*: a human-readable identifier for the provider (e.g. *"GitHub"*). -* *authEndpoint*: the fully-qualified URL of the provider's [authorization endpoint](http://tools.ietf.org/html/rfc6749#section-3.1). -* *tokenEndpoint*: the fully-qualified URL of the provider's [token endpoint](http://tools.ietf.org/html/rfc6749#section-3.2). -* *refreshEndpoint* (optional): the fully-qualified URL of the provider's [refresh token](http://tools.ietf.org/html/rfc6749#section-6) endpoint. -* *activeUserEndpoint* (optional): the fully-qualified URL of the provider's endpoint for fetching details about the current user. -* *usernameTemplate* (optional): An underscore.js template for extracting the user's permanent identifier from the user's details. Default: *"<%= id %>"*. -* *clientId*: the client ID of the app registered with the provider. -* *clientSecret*: the client secret of the app registered with the provider. - -!SUBSECTION List available OAuth2 providers - -Returns a list of OAuth2 providers that can be presented to the front-end. - -`providers.list()` - -Each item in the list is an object with the following properties: - -* *_key*: *_key* of the provider. -* *label*: a human-readable identifier that can be presented to the user. -* *clientId*: the client ID stored for the given provider. - -If you wish to exclude providers you don't support in your app, you need to filter the result manually. - -@EXAMPLES - -```js -var supportedProviders = providers.list().filter(function(obj) { - return Boolean(obj.clientId); -}); -``` - -!SUBSECTION Define a new OAuth2 provider - -Creates a new OAuth2 provider with the given data. - -`providers.create(data)` - -Saves and returns a new instance of the provider model with its attributes set to the properties of the given object. - -*Parameter* - -* *data*: an arbitrary object, see above. - -@EXAMPLES - -```js -var provider = providers.create({ - _key: 'myoauth2', - label: 'My OAuth2 Provider', - authEndpoint: 'https://example.com/oauth2/authorize', - tokenEndpoint: 'https://example.com/oauth2/access_token', - refreshEndpoint: 'https://example.com/oauth2/access_token', - activeUserEndpoint: 'https://example.com/api/v2/users/me', - usernameTemplate: '<%= user_id %>', - clientId: '1234567890', - clientSecret: 'kEyBoArDcAt' -}); -``` - -!SUBSECTION Fetch an existing OAuth2 provider - -Fetches an existing OAuth2 provider from the database. - -`providers.get(providerId)` - -Throws a *ProviderNotFound* exception if the provider does not exist. - -*Parameters* - -* *providerId*: the provider's *_key*. - -@EXAMPLES - -```js -var provider = providers.get('github'); -assertTrue(provider.get('_key'), 'github'); -``` - -!SUBSECTION Delete a provider - -There are two ways to delete a provider from the database: - -* calling the provider storage's *delete* method with a provider's *_key* directly -* telling a provider to delete itself - -!SUBSUBSECTION Delete a provider by its ID - -Delete a provider with a given ID. - -`providers.delete(providerId)` - -Attempts to delete the provider with the given *providerId* from the database. If the provider does not exist, a *ProviderNotFound* exception will be thrown. The method always returns *null*. - -*Parameter* - -* *providerId*: a provider *_key*. - -@EXAMPLES - -```js -providers.delete('github'); -``` - -!SUBSUBSECTION Tell a provider to delete itself - -Delete a provider from the database. - -`provider.delete()` - -Attempts to delete the provider from the database. - -Returns *true* if the provider was deleted successfully. - -Returns *false* if the provider already didn't exist. - -@EXAMPLES - -```js -var provider = providers.get('github'); -provider.delete(); -``` - -!SUBSECTION Save a provider - -Save a provider to the database. - -`provider.save()` - -In order to commit changes made to the provider in your code, you need to call this method. - -@EXAMPLES - -```js -provider.set('clientId', '1234567890'); -provider.set('clientSecret', 'kEyBoArDcAt'); -provider.save(); -``` - - -!SUBSECTION Get the authorization URL of a provider - -Generates the authorization URL for the authorization endpoint of the provider. - -`provider.getAuthUrl(redirect_uri, args)` - -Returns a fully-qualified URL for the authorization endpoint of the provider by appending the provider object's client ID and any additional arguments from *args* to the provider object's *authEndpoint*. - -*Parameter* - -* *redirect_uri*: the fully-qualified URL of your app's OAuth2 callback. -* *args* (optional): an object with any of the following properties: - * *response_type* (optional): See [RFC 6749](http://tools.ietf.org/html/rfc6749). Default: *"code"*. - -!SUBSECTION _getTokenRequest - -(Internal.) Generates the token URL and request body for token endpoint of the provider. - -`provider._getTokenRequest(code, redirect_uri, args)` - -Returns an object with two properties: - -* *url*: the fully-qualified URL for the token endpoint. -* *body*: the form-encoded request body for the token endpoint created by appending the provider object's client ID, client secret and any additional arguments from *args* to the provider object's *tokenEndpoint*. - -*Parameter* - -* *code*: a grant code returned by the provider's authorization endpoint. -* *redirect_uri*: the original callback URL with which the code was requested. -* *args* (optional): an object with any of the following properties: - * *grant_type* (optional): see [RFC 6749](http://tools.ietf.org/html/rfc6749). Default: *"authorization_code"*. - -!SUBSECTION Exchange a grant code for an access token - -Exchanges a grant code for an access token. - -`provider.exchangeGrantToken(code, redirect_uri)` - -Performs a *POST* response to the provider object's *tokenEndpoint* and returns the parsed response body. - -Throws an exception if the remote server responds with an empty response body. - -*Parameter* - -* *code*: a grant code returned by the provider's authorization endpoint. -* *redirect_uri*: the original callback URL with which the code was requested. -* *args* (optional): an object with any of the following properties: - * *grant_type* (optional): see [RFC 6749](http://tools.ietf.org/html/rfc6749). Default: *"authorization_code"*. - -!SUBSECTION Fetch the active user - -Fetches details of the active user. - -`provider.fetchActiveUser(access_token)` - -Performs a *GET* response to the provider object's *activeUserEndpoint* and returns the parsed response body. - -Throws an exception if the remote server responds with an empty response body. - -Also throws an exception if the provider object has no *activeUserEndpoint*. - -*Parameter* - -* *access_token*: an OAuth2 access token as returned by *exchangeGrantToken*. - -@EXAMPLES - -```js -var authData = provider.exchangeGrantToken(code, redirect_uri); -var userData = provider.fetchActiveUser(authData.access_token); -``` - -!SUBSECTION Get a user's identifier - -Fetches the user's identifier from a user object returned by the provider. - -`provider.getUsername(userData)` - -Applies the provider's *usernameTemplate* to the given user object. - -*Parameter* - -* *userData*: the object returned by *getActiveUser*. - -@EXAMPLES - -```js -var userData = provider.fetchActiveUser(access_token); -var username = provider.getUsername(userData); -``` diff --git a/js/Makefile.files b/js/Makefile.files index 919df4269c..b63c1167a5 100644 --- a/js/Makefile.files +++ b/js/Makefile.files @@ -59,7 +59,6 @@ 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"` \ diff --git a/js/apps/system/oauth2/errors.js b/js/apps/system/oauth2/errors.js deleted file mode 100644 index 6ad3ce55ff..0000000000 --- a/js/apps/system/oauth2/errors.js +++ /dev/null @@ -1,12 +0,0 @@ -/*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; -}()); \ No newline at end of file diff --git a/js/apps/system/oauth2/manifest.json b/js/apps/system/oauth2/manifest.json deleted file mode 100644 index b1a4db8fdf..0000000000 --- a/js/apps/system/oauth2/manifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "oauth2", - "author": "Alan Plum", - "version": "0.1", - "isSystem": true, - "description": "OAuth2 authentication for Foxx.", - - "exports": { - "providers": "providers.js" - }, - - "defaultDocument": "", - - "lib": ".", - - "setup": "setup.js" -} diff --git a/js/apps/system/oauth2/providers.js b/js/apps/system/oauth2/providers.js deleted file mode 100644 index 698f934824..0000000000 --- a/js/apps/system/oauth2/providers.js +++ /dev/null @@ -1,214 +0,0 @@ -/*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; -}()); \ No newline at end of file diff --git a/js/apps/system/oauth2/setup.js b/js/apps/system/oauth2/setup.js deleted file mode 100644 index 4dea8c6c26..0000000000 --- a/js/apps/system/oauth2/setup.js +++ /dev/null @@ -1,47 +0,0 @@ -/*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); - } - }); -}()); \ No newline at end of file From 288c6d8d31d503e4588abf08d4c6c88aae11f76c Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Mon, 30 Jun 2014 14:43:59 +0200 Subject: [PATCH 35/44] Allow specifying "_" as collection prefix. --- js/server/modules/org/arangodb/foxx/manager.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/js/server/modules/org/arangodb/foxx/manager.js b/js/server/modules/org/arangodb/foxx/manager.js index 79b7532fe2..1e80d9931e 100644 --- a/js/server/modules/org/arangodb/foxx/manager.js +++ b/js/server/modules/org/arangodb/foxx/manager.js @@ -192,14 +192,13 @@ function extendContext (context, app, root) { 'use strict'; var cp = context.collectionPrefix; - var cname = ""; - if (cp !== "") { - cname = cp + "_"; + if (cp !== "" && cp !== "_") { + cp += "_"; } context.collectionName = function (name) { - var replaced = (cname + name).replace(/[^a-zA-Z0-9]/g, '_').replace(/(^_+|_+$)/g, '').substr(0, 64); + var replaced = (cp + name.replace(/[^a-zA-Z0-9]/g, '_').replace(/(^_+|_+$)/g, '')).substr(0, 64); if (replaced.length === 0) { throw new Error("Cannot derive collection name from '" + name + "'"); From fefe0d0e6d0c4a30b3bfa0e339856b4846e55278 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Mon, 30 Jun 2014 17:40:09 +0200 Subject: [PATCH 36/44] Replaced org/arangodb/users internals with users app compatible format. --- js/apps/system/users/storage.js | 14 +- js/common/tests/shell-users.js | 22 +- .../modules/org/arangodb/foxx/manager.js | 25 +- js/server/modules/org/arangodb/users.js | 256 ++++++++---------- js/server/upgrade-database.js | 83 ++++++ 5 files changed, 240 insertions(+), 160 deletions(-) diff --git a/js/apps/system/users/storage.js b/js/apps/system/users/storage.js index 16a753e8ff..5408c663bb 100644 --- a/js/apps/system/users/storage.js +++ b/js/apps/system/users/storage.js @@ -10,6 +10,7 @@ var User = Foxx.Model.extend({}, { attributes: { + user: {type: 'string', required: true}, authData: {type: 'object', required: true}, userData: {type: 'object', required: true} } @@ -21,7 +22,7 @@ ); function resolve(username) { - var user = users.firstExample({'userData.username': username}); + var user = users.firstExample({user: username}); if (!user.get('_key')) { return null; } @@ -30,15 +31,15 @@ function listUsers() { return users.collection.all().toArray().map(function (user) { - return user.userData ? user.userData.username : null; + return user.user; }).filter(Boolean); } - function createUser(userData) { + function createUser(username, userData) { if (!userData) { userData = {}; } - if (!userData.username) { + if (!username) { throw new Error('Must provide username!'); } var user; @@ -48,10 +49,11 @@ write: [users.collection.name()] }, action: function () { - if (resolve(userData.username)) { - throw new errors.UsernameNotAvailable(userData.username); + if (resolve(username)) { + throw new errors.UsernameNotAvailable(username); } user = new User({ + user: username, userData: userData, authData: {} }); diff --git a/js/common/tests/shell-users.js b/js/common/tests/shell-users.js index a060a90a06..0213676db5 100644 --- a/js/common/tests/shell-users.js +++ b/js/common/tests/shell-users.js @@ -115,7 +115,7 @@ function UsersSuite () { var passwd = "passwd-" + i; users.save(username, passwd); - assertEqual(username, c.firstExample({ user: username }).user); + assertEqual(username, c.firstExample({ 'userData.username': username }).userData.username); } }, @@ -128,9 +128,9 @@ function UsersSuite () { var passwd = "passwd"; users.save(username, passwd); - assertEqual(username, c.firstExample({ user: username }).user); - - try { + assertEqual(username, c.firstExample({ 'userData.username': username }).userData.username); + + try { users.save(username, passwd); fail(); } @@ -148,7 +148,7 @@ function UsersSuite () { var passwd = ""; users.save(username, passwd); - assertEqual(username, c.firstExample({ user: username }).user); + assertEqual(username, c.firstExample({ 'userData.username': username }).userData.username); }, //////////////////////////////////////////////////////////////////////////////// @@ -159,7 +159,7 @@ function UsersSuite () { var username = "users-1"; users.save(username); - assertEqual(username, c.firstExample({ user: username }).user); + assertEqual(username, c.firstExample({ 'userData.username': username }).userData.username); }, //////////////////////////////////////////////////////////////////////////////// @@ -171,7 +171,7 @@ function UsersSuite () { var passwd = "arangodb-loves-you"; users.save(username, passwd); - assertEqual(username, c.firstExample({ user: username }).user); + assertEqual(username, c.firstExample({ 'userData.username': username }).userData.username); }, //////////////////////////////////////////////////////////////////////////////// @@ -184,11 +184,11 @@ function UsersSuite () { var passwd = "passwd-" + i; users.save(username, passwd); - assertEqual(username, c.firstExample({ user: username }).user); + assertEqual(username, c.firstExample({ 'userData.username': username }).userData.username); var d2 = users.replace(username, passwd + "xxx"); - assertEqual(username, c.firstExample({ user: username }).user); - assertEqual(username, d2.user); + assertEqual(username, c.firstExample({ 'userData.username': username }).userData.username); + assertEqual(username, d2.userData.username); } }, @@ -216,7 +216,7 @@ function UsersSuite () { var passwd = "passwd-" + i; users.save(username, passwd); - assertEqual(username, c.firstExample({ user: username }).user); + assertEqual(username, c.firstExample({ 'userData.username': username }).userData.username); users.remove(username); } }, diff --git a/js/server/modules/org/arangodb/foxx/manager.js b/js/server/modules/org/arangodb/foxx/manager.js index 1e80d9931e..ff320c7e3f 100644 --- a/js/server/modules/org/arangodb/foxx/manager.js +++ b/js/server/modules/org/arangodb/foxx/manager.js @@ -1022,6 +1022,24 @@ function checkConfiguration (app, options) { return false; } +//////////////////////////////////////////////////////////////////////////////// +/// @brief returns collection prefix for system apps +//////////////////////////////////////////////////////////////////////////////// + + function systemCollectionPrefix (appName) { + 'use strict'; + + if (appName === "sessions") { + return "_"; + } + + if (appName === "users") { + return "_"; + } + + return false; + } + // ----------------------------------------------------------------------------- // --SECTION-- public functions // ----------------------------------------------------------------------------- @@ -1763,7 +1781,12 @@ exports.initializeFoxx = function () { var found = aal.firstExample({ type: "mount", mount: mount }); if (found === null) { - exports.mount(appName, mount, {reload: false}); + var opts = {reload: false}; + var prefix = systemCollectionPrefix(appName); + if (prefix) { + opts.collectionPrefix = prefix; + } + exports.mount(appName, mount, opts); var doc = mountFromId(mount); var app = appFromAppId(doc.app); diff --git a/js/server/modules/org/arangodb/users.js b/js/server/modules/org/arangodb/users.js index 57dbb6e56a..c18d721c92 100644 --- a/js/server/modules/org/arangodb/users.js +++ b/js/server/modules/org/arangodb/users.js @@ -1,4 +1,4 @@ -/*jslint indent: 2, nomen: true, maxlen: 120, sloppy: true, vars: true, white: true, plusplus: true */ +/*jslint indent: 2, nomen: true, maxlen: 120, sloppy: true, vars: true, white: true, plusplus: true, es5: true */ /*global require, exports, ArangoAgency */ //////////////////////////////////////////////////////////////////////////////// @@ -47,24 +47,14 @@ var ArangoError = arangodb.ArangoError; /// @brief encode password using SHA256 //////////////////////////////////////////////////////////////////////////////// -var encodePassword = function (password) { - var salt; - var encoded; +var hashPassword = function (password) { + var salt = internal.genRandomAlphaNumbers(16); - var random = crypto.rand(); - if (random === undefined) { - random = "time:" + internal.time(); - } - else { - random = "random:" + random; - } - - salt = crypto.sha256(random); - salt = salt.substr(0,8); - - encoded = "$1$" + salt + "$" + crypto.sha256(salt + password); - - return encoded; + return { + hash: crypto.sha256(salt + password), + salt: salt, + method: "sha256" + }; }; //////////////////////////////////////////////////////////////////////////////// @@ -72,7 +62,7 @@ var encodePassword = function (password) { //////////////////////////////////////////////////////////////////////////////// var validateName = function (username) { - if (typeof username !== 'string' || username === '') { + if (typeof username !== "string" || username === "") { var err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_USER_INVALID_NAME.code; err.errorMessage = arangodb.errors.ERROR_USER_INVALID_NAME.message; @@ -85,8 +75,8 @@ var validateName = function (username) { /// @brief validates password //////////////////////////////////////////////////////////////////////////////// -var validatePassword = function (passwd) { - if (typeof passwd !== 'string') { +var validatePassword = function (password) { + if (typeof password !== "string") { var err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_USER_INVALID_PASSWORD.code; err.errorMessage = arangodb.errors.ERROR_USER_INVALID_PASSWORD.message; @@ -106,7 +96,6 @@ var getStorage = function () { var err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_ARANGO_COLLECTION_NOT_FOUND.code; err.errorMessage = "collection _users not found"; - throw err; } @@ -121,14 +110,14 @@ var getStorage = function () { /// @brief creates a new user //////////////////////////////////////////////////////////////////////////////// -exports.save = function (user, passwd, active, extra, changePassword) { - if (passwd === null || passwd === undefined) { - passwd = ""; +exports.save = function (username, password, active, userData, changePassword) { + if (password === null || password === undefined) { + password = ""; } // validate input - validateName(user); - validatePassword(passwd); + validateName(username); + validatePassword(password); if (active === undefined || active === null) { active = true; // this is the default value @@ -143,47 +132,45 @@ exports.save = function (user, passwd, active, extra, changePassword) { } var users = getStorage(); - var previous = users.firstExample({ user: user }); + var user = users.firstExample({user: username}); - if (previous === null) { - var hash = encodePassword(passwd); - var data = { - user: user, - password: hash, - active: active, - changePassword: changePassword - }; - - if (extra !== undefined) { - data.extra = extra; - } - - var doc = users.save(data); - - // not exports.reload() as this is an abstract method... - require("org/arangodb/users").reload(); - return users.document(doc._id); + if (user !== null) { + var err = new ArangoError(); + err.errorNum = arangodb.errors.ERROR_USER_DUPLICATE.code; + err.errorMessage = arangodb.errors.ERROR_USER_DUPLICATE.message; + throw err; } - var err = new ArangoError(); - err.errorNum = arangodb.errors.ERROR_USER_DUPLICATE.code; - err.errorMessage = arangodb.errors.ERROR_USER_DUPLICATE.message; + var data = { + user: username, + userData: userData || {}, + authData: { + simple: hashPassword(password), + active: Boolean(active), + changePassword: Boolean(changePassword) + } + }; - throw err; + var doc = users.save(data); + + // not exports.reload() as this is an abstract method... + require("org/arangodb/users").reload(); + + return users.document(doc._id); }; //////////////////////////////////////////////////////////////////////////////// /// @brief replaces an existing user //////////////////////////////////////////////////////////////////////////////// -exports.replace = function (user, passwd, active, extra, changePassword) { - if (passwd === null || passwd === undefined) { - passwd = ""; +exports.replace = function (username, password, active, userData, changePassword) { + if (password === null || password === undefined) { + password = ""; } // validate input - validateName(user); - validatePassword(passwd); + validateName(username); + validatePassword(password); if (active === undefined || active === null) { active = true; // this is the default @@ -194,137 +181,128 @@ exports.replace = function (user, passwd, active, extra, changePassword) { } var users = getStorage(); - var previous = users.firstExample({ user: user }); + var user = users.firstExample({user: username}); - if (previous === null) { + if (user === null) { var err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_USER_NOT_FOUND.code; err.errorMessage = arangodb.errors.ERROR_USER_NOT_FOUND.message; - throw err; } - var hash = encodePassword(passwd); var data = { - user: user, - password: hash, - active: active, - changePassword: changePassword + user: username, + userData: userData || {}, + authData: { + simple: hashPassword(password), + active: Boolean(active), + changePassword: Boolean(changePassword) + } }; - if (extra !== undefined) { - data.extra = extra; - } - - users.replace(previous, data); + var doc = users.replace(user, data); // not exports.reload() as this is an abstract method... require("org/arangodb/users").reload(); - return users.document(previous._id); + return users.document(doc._id); }; //////////////////////////////////////////////////////////////////////////////// /// @brief updates an existing user //////////////////////////////////////////////////////////////////////////////// -exports.update = function (user, passwd, active, extra, changePassword) { - +exports.update = function (username, password, active, userData, changePassword) { // validate input - validateName(user); + validateName(username); - if (passwd !== undefined) { - validatePassword(passwd); + if (password !== undefined) { + validatePassword(password); } var users = getStorage(); - var previous = users.firstExample({ user: user }); + var user = users.firstExample({user: username}); - if (previous === null) { + if (user === null) { var err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_USER_NOT_FOUND.code; err.errorMessage = arangodb.errors.ERROR_USER_NOT_FOUND.message; - throw err; } - var data = previous._shallowCopy; + var data = user._shallowCopy; - if (passwd !== undefined) { - var hash = encodePassword(passwd); - data.password = hash; + if (password !== undefined) { + data.authData.simple = hashPassword(password); } if (active !== undefined && active !== null) { - data.active = active; + data.authData.active = active; } - if (extra !== undefined) { - data.extra = extra; + if (userData !== undefined) { + data.userData = userData; } if (changePassword !== undefined && changePassword !== null) { - data.changePassword = changePassword; + data.authData.changePassword = changePassword; } - users.update(previous, data); + users.update(user, data); // not exports.reload() as this is an abstract method... require("org/arangodb/users").reload(); - return users.document(previous._id); + return users.document(user._id); }; //////////////////////////////////////////////////////////////////////////////// /// @brief deletes an existing user //////////////////////////////////////////////////////////////////////////////// -exports.remove = function (user) { +exports.remove = function (username) { // validate input - validateName(user); + validateName(username); var users = getStorage(); - var previous = users.firstExample({ user: user }); + var user = users.firstExample({user: username}); - if (previous === null) { + if (user === null) { var err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_USER_NOT_FOUND.code; err.errorMessage = arangodb.errors.ERROR_USER_NOT_FOUND.message; - throw err; } - users.remove(previous); - // not exports.reload() as this is an abstract method... require("org/arangodb/users").reload(); + + users.remove(user); }; //////////////////////////////////////////////////////////////////////////////// /// @brief gets an existing user //////////////////////////////////////////////////////////////////////////////// -exports.document = function (user) { - +exports.document = function (username) { // validate name - validateName(user); + validateName(username); var users = getStorage(); - var doc = users.firstExample({ user: user }); + var user = users.firstExample({user: username}); - if (doc === null) { + if (user === null) { var err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_USER_NOT_FOUND.code; err.errorMessage = arangodb.errors.ERROR_USER_NOT_FOUND.message; - throw err; } return { - user: doc.user, - active: doc.active, - extra: doc.extra || {}, - changePassword: doc.changePassword + user: user.user, + active: user.authData.active, + userData: user.userData || {}, + changePassword: user.authData.changePassword }; }; @@ -332,21 +310,22 @@ exports.document = function (user) { /// @brief checks whether a combination of username / password is valid. //////////////////////////////////////////////////////////////////////////////// -exports.isValid = function (user, password) { - var users = getStorage(); - var previous = users.firstExample({ user: user }); +exports.isValid = function (username, password) { + // validate name + validateName(username); - if (previous === null || ! previous.active) { + var users = getStorage(); + var user = users.firstExample({user: username}); + + if (user === null || !user.authData.active) { return false; } - var salted = previous.password.substr(3, 8) + password; - var hex = crypto.sha256(salted); - // penalize the call internal.sleep(Math.random()); - return (previous.password.substr(12) === hex); + var hash = crypto[user.authData.simple.method](user.authData.simple.salt + password); + return crypto.constantEquals(user.authData.simple.hash, hash); }; //////////////////////////////////////////////////////////////////////////////// @@ -354,22 +333,16 @@ exports.isValid = function (user, password) { //////////////////////////////////////////////////////////////////////////////// exports.all = function () { - var cursor = getStorage().all(); - var result = [ ]; + var users = getStorage(); - while (cursor.hasNext()) { - var doc = cursor.next(); - var user = { + return users.all().toArray().map(function(doc) { + return { user: doc.user, - active: doc.active, - extra: doc.extra || { }, - changePassword: doc.changePassword + active: doc.authData.active, + userData: doc.userData || {}, + changePassword: doc.authData.changePassword }; - - result.push(user); - } - - return result; + }); }; //////////////////////////////////////////////////////////////////////////////// @@ -409,11 +382,10 @@ exports.reload = function () { /// @brief sets a password-change token //////////////////////////////////////////////////////////////////////////////// -exports.setPasswordToken = function (user, token) { +exports.setPasswordToken = function (username, token) { var users = getStorage(); - var current = users.firstExample({ user: user }); - - if (current === null) { + var user = users.firstExample({user: username}); + if (user === null) { return null; } @@ -421,7 +393,7 @@ exports.setPasswordToken = function (user, token) { token = internal.genRandomAlphaNumbers(50); } - users.update(current, { passwordToken: token }); + users.update(user, {authData: {passwordToken: token}}); return token; }; @@ -432,13 +404,7 @@ exports.setPasswordToken = function (user, token) { exports.userByToken = function (token) { var users = getStorage(); - var current = users.firstExample({ passwordToken: token }); - - if (current === null) { - return null; - } - - return current.user; + return users.firstExample({"authData.passwordToken": token}); }; //////////////////////////////////////////////////////////////////////////////// @@ -447,18 +413,24 @@ exports.userByToken = function (token) { exports.changePassword = function (token, password) { var users = getStorage(); - var current = users.firstExample({ passwordToken: token }); + var user = users.firstExample({'authData.passwordToken': token}); - if (current === null) { + if (user === null) { return false; } validatePassword(password); - var hash = encodePassword(password); + var data = user._shallowCopy; - users.update(current, { passwordToken: null, password: hash, changePassword: false }); - exports.reload(); + delete data.authData.passwordToken; + data.authData.simple = hashPassword(password); + data.authData.changePassword = false; + + users.replace(user, data); + + // not exports.reload() as this is an abstract method... + require("org/arangodb/users").reload(); return true; }; diff --git a/js/server/upgrade-database.js b/js/server/upgrade-database.js index 62c108087e..53502bf315 100644 --- a/js/server/upgrade-database.js +++ b/js/server/upgrade-database.js @@ -726,6 +726,89 @@ } }); +//////////////////////////////////////////////////////////////////////////////// +/// @brief upgradeUserModel +//////////////////////////////////////////////////////////////////////////////// + + // create a unique index on "user" attribute in _users + addTask({ + name: "updateUserModel", + description: "convert documents in _users collection to new format", + + mode: [ MODE_PRODUCTION, MODE_DEVELOPMENT ], + cluster: [ CLUSTER_NONE, CLUSTER_COORDINATOR_GLOBAL ], + database: [ DATABASE_INIT, DATABASE_UPGRADE ], + + task: function () { + var users = getCollection("_users"); + if (! users) { + return false; + } + + var results = users.all().toArray().map(function (oldDoc) { + if (!oldDoc.hasOwnProperty('userData')) { + if (typeof oldDoc.user !== 'string') { + logger.error("user with _key " + oldDoc._key + " has no username"); + return false; + } + if (typeof oldDoc.password !== 'string') { + logger.error("user with username " + oldDoc.user + " has no password"); + return false; + } + var newDoc = { + user: oldDoc.user, + userData: oldDoc.extra || {}, + authData: { + active: Boolean(oldDoc.active), + changePassword: Boolean(oldDoc.changePassword) + } + }; + if (oldDoc.passwordToken) { + newDoc.authData.passwordToken = oldDoc.passwordToken; + } + var passwd = oldDoc.password.split('$'); + if (passwd[0] !== '' || passwd.length !== 4) { + logger.error("user with username " + oldDoc.user + " has unexpected password format"); + return false; + } + newDoc.authData.simple = { + method: 'sha256', + salt: passwd[2], + hash: passwd[3] + }; + var result = users.replace(oldDoc, newDoc); + return !result.errors; + } + if (!oldDoc.hasOwnProperty('authData')) { + logger.error("user with _key " + oldDoc._key + " has no authData"); + return false; + } + return true; + }); + + return results.every(Boolean); + } + }); + +//////////////////////////////////////////////////////////////////////////////// +/// @brief setupSessions +/// +/// set up the collection _sessions +//////////////////////////////////////////////////////////////////////////////// + + addTask({ + name: "setupSessions", + description: "setup _sessions collection", + + mode: [ MODE_PRODUCTION, MODE_DEVELOPMENT ], + cluster: [ CLUSTER_NONE, CLUSTER_COORDINATOR_GLOBAL ], + database: [ DATABASE_INIT, DATABASE_UPGRADE ], + + task: function () { + return createSystemCollection("_sessions", { waitForSync : true }); + } + }); + //////////////////////////////////////////////////////////////////////////////// /// @brief setupGraphs /// From 369485b1299aca9eff3b9b0856411f3b760d2f7e Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Mon, 30 Jun 2014 18:42:59 +0200 Subject: [PATCH 37/44] Replaced backticks with splats to follow doc styleguide. --- js/server/modules/org/arangodb/foxx/controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/server/modules/org/arangodb/foxx/controller.js b/js/server/modules/org/arangodb/foxx/controller.js index 76f2d1ca02..cf96fb65f0 100644 --- a/js/server/modules/org/arangodb/foxx/controller.js +++ b/js/server/modules/org/arangodb/foxx/controller.js @@ -664,7 +664,7 @@ extend(Controller.prototype, { /// * *cookieName*: A string used as the name of the cookie. Defaults to *"sid"*. /// * *cookieSecret*: A secret string used to sign the cookie (as "*cookieName*_sig"). Optional. /// * *autoCreateSession*: Whether to always create a session if none exists. Defaults to *true*. -/// * *sessionStorageApp*: Mount path of the app to use for sessions. Defaults to */_sessions* +/// * *sessionStorageApp*: Mount path of the app to use for sessions. Defaults to */_system/sessions* /// /// /// @EXAMPLES From 00a0b4513a6996b9dfd675e4962b8e4f79c35986 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Tue, 1 Jul 2014 11:42:47 +0200 Subject: [PATCH 38/44] Aardvark: allow (un-)mounting non-internal system apps, prohibit mounting reserved paths. --- .../frontend/js/templates/foxxActiveView.ejs | 2 +- .../frontend/js/views/foxxActiveView.js | 80 ++++++++++++------- .../frontend/js/views/foxxInstalledView.js | 46 +++-------- 3 files changed, 67 insertions(+), 61 deletions(-) diff --git a/js/apps/system/aardvark/frontend/js/templates/foxxActiveView.ejs b/js/apps/system/aardvark/frontend/js/templates/foxxActiveView.ejs index 8668442166..14cb4a6ac9 100644 --- a/js/apps/system/aardvark/frontend/js/templates/foxxActiveView.ejs +++ b/js/apps/system/aardvark/frontend/js/templates/foxxActiveView.ejs @@ -31,5 +31,5 @@ <%}%> -
<%= appInfos[1] %><%= attributes.isSystem ? " (system)" : "" %><%= appInfos[0] === "dev" ? " (dev)" : ""%>
+
<%= appInfos[1] %><%= attributes.isSystem && attributes.mount.charAt(0) === "/" && attributes.mount.charAt(1) === "_" ? " (system)" : "" %><%= appInfos[0] === "dev" ? " (dev)" : ""%>
diff --git a/js/apps/system/aardvark/frontend/js/views/foxxActiveView.js b/js/apps/system/aardvark/frontend/js/views/foxxActiveView.js index b8fdfc4866..6f7b53150a 100644 --- a/js/apps/system/aardvark/frontend/js/views/foxxActiveView.js +++ b/js/apps/system/aardvark/frontend/js/views/foxxActiveView.js @@ -27,14 +27,24 @@ initialize: function(){ this._show = true; - this.buttonConfig = [ - window.modalView.createDeleteButton( - "Uninstall", this.uninstall.bind(this) - ), - window.modalView.createSuccessButton( - "Save", this.changeFoxx.bind(this) - ) - ]; + var mount = this.model.get("mount"); + var isSystem = ( + this.model.get("isSystem") && + mount.charAt(0) === '/' && + mount.charAt(1) === '_' + ); + if (isSystem) { + this.buttonConfig = []; + } else { + this.buttonConfig = [ + window.modalView.createDeleteButton( + "Uninstall", this.uninstall.bind(this) + ), + window.modalView.createSuccessButton( + "Save", this.changeFoxx.bind(this) + ) + ]; + } this.showMod = window.modalView.show.bind( window.modalView, "modalTable.ejs", @@ -72,6 +82,12 @@ isSystem, active, modView = window.modalView; + var mount = this.model.get("mount"); + var editable = !( + this.model.get("isSystem") && + mount.charAt(0) === '/' && + mount.charAt(1) === '_' + ); if (this.model.get("isSystem")) { isSystem = "Yes"; } else { @@ -85,18 +101,24 @@ list.push(modView.createReadOnlyEntry( "id_name", "Name", name )); - list.push(modView.createTextEntry( - "change-mount-point", "Mount", this.model.get("mount"), - "The path where the app can be reached.", - "mount-path", - true, - [ - { - rule: Joi.string().required(), - msg: "No mount-path given." - } - ] - )); + if (editable) { + list.push(modView.createTextEntry( + "change-mount-point", "Mount", this.model.get("mount"), + "The path where the app can be reached.", + "mount-path", + true, + [ + { + rule: Joi.string().required(), + msg: "No mount-path given." + } + ] + )); + } else { + list.push(modView.createReadOnlyEntry( + "change-mount-point", "Mount", this.model.get("mount") + )); + } /* * For the future, update apps to available newer versions * versOptions.push(modView.createOptionEntry(appInfos[2])); @@ -116,9 +138,15 @@ editFoxxDialog: function(event) { event.stopPropagation(); - if (this.model.get("isSystem") || this.model.get("development")) { + var mount = this.model.get("mount"); + var isSystem = ( + this.model.get("isSystem") && + mount.charAt(0) === '/' && + mount.charAt(1) === '_' + ); + if (this.model.get("development")) { this.buttonConfig[0].disabled = true; - } else { + } else if (!isSystem) { delete this.buttonConfig[0].disabled; } this.showMod(this.buttonConfig, this.fillValues()); @@ -163,11 +191,9 @@ }, uninstall: function () { - if (!this.model.get("isSystem")) { - this.model.destroy({ async: false }); - window.modalView.hide(); - this.appsView.reload(); - } + this.model.destroy({ async: false }); + window.modalView.hide(); + this.appsView.reload(); }, showDocu: function(event) { diff --git a/js/apps/system/aardvark/frontend/js/views/foxxInstalledView.js b/js/apps/system/aardvark/frontend/js/views/foxxInstalledView.js index 5991c6eed1..aaf0884927 100644 --- a/js/apps/system/aardvark/frontend/js/views/foxxInstalledView.js +++ b/js/apps/system/aardvark/frontend/js/views/foxxInstalledView.js @@ -91,8 +91,6 @@ "Install", this.installDialog.bind(this) ) ]; - var buttonSystemInfoConfig = [ - ]; this.showMod = window.modalView.show.bind( window.modalView, "modalTable.ejs", @@ -123,12 +121,6 @@ "Application Settings", buttonInfoMultipleVersionsConfigUpdate ); - this.showSystemInfoMod = window.modalView.show.bind( - window.modalView, - "modalTable.ejs", - "Application Settings", - buttonSystemInfoConfig - ); this.showPurgeMod = window.modalView.show.bind( window.modalView, "modalTable.ejs", @@ -334,38 +326,23 @@ infoDialog: function(event) { var name = this.model.get("name"), mountinfo = this.model.collection.gitInfo(name), - versions, isSystem = false, isGit; + isGit; if (mountinfo.git === true) { this.model.set("isGit", mountinfo.git); this.model.set("gitUrl", mountinfo.url); } - if (this.model.get("isSystem")) { - isSystem = true; - } else { - isSystem = false; - } - - versions = this.model.get("versions"); isGit = this.model.get("isGit"); event.stopPropagation(); - if (isSystem === false && !versions && !isGit) { - this.showInfoMod(this.fillInfoValues()); - } - else if (isSystem === false && !versions && isGit) { - this.showInfoModUpdate(this.fillInfoValues()); - } - else if (isSystem === false && versions && !isGit) { - this.showInfoMultipleVersionsMod(this.fillInfoValues()); - } - else if (isSystem === false && versions && isGit) { - this.showInfoMultipleVersionsModUpdate(this.fillInfoValues()); - } - else { - this.showSystemInfoMod(this.fillInfoValues()); - } + + this[ + this.model.get("versions") + ? (isGit ? 'showInfoMultipleVersionsModUpdate' : 'showInfoMultipleVersionsMod') + : (isGit ? 'showInfoModUpdate' : 'showInfoMod') + ](this.fillInfoValues()); + this.selectHighestVersion(); }, @@ -401,9 +378,12 @@ install: function() { var mountPoint = $("#mount-point").val(), version = "", - regex = /^(\/[^\/\s]+)+$/, self = this; - if (!regex.test(mountPoint)){ + if (/^\/_.+$/.test(mountPoint)) { + alert("Sorry, mount paths starting with an underscore are reserved for internal use."); + return false; + } + if (!/^(\/[^\/\s]+)+$/.test(mountPoint)){ alert("Sorry, you have to give a valid mount point, e.g.: /myPath"); return false; } From e73ab3872070cb7f0fd7796079f988bf6cc056dc Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Wed, 9 Jul 2014 14:42:54 +0200 Subject: [PATCH 39/44] Code style. --- js/apps/system/sessions/app.js | 20 +++--- js/apps/system/sessions/errors.js | 2 +- js/apps/system/sessions/setup.js | 6 +- js/apps/system/sessions/storage.js | 98 +++++++++++++++--------------- js/apps/system/simple-auth/auth.js | 20 +++--- js/apps/system/users/errors.js | 2 +- js/apps/system/users/setup.js | 6 +- js/apps/system/users/storage.js | 36 ++++++----- 8 files changed, 93 insertions(+), 97 deletions(-) diff --git a/js/apps/system/sessions/app.js b/js/apps/system/sessions/app.js index 8c8c226e83..f0a87105ce 100644 --- a/js/apps/system/sessions/app.js +++ b/js/apps/system/sessions/app.js @@ -1,12 +1,12 @@ -/*jslint indent: 2, nomen: true, maxlen: 120, vars: true, es5: true */ +/*jslint indent: 2, nomen: true, maxlen: 120, 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(applicationContext.mount).sessionStorage; + var _ = require('underscore'), + Foxx = require('org/arangodb/foxx'), + errors = require('./errors'), + controller = new Foxx.Controller(applicationContext), + api = Foxx.requireApp(applicationContext.mount).sessionStorage; controller.post('/', function (req, res) { var session = api.create(req.body()); @@ -31,8 +31,8 @@ .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); + var body = JSON.parse(req.rawBody()), + session = api.get(req.urlParameters.sid); session.set('sessionData', body); session.save(); res.json(session.forClient()); @@ -48,8 +48,8 @@ .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); + var body = JSON.parse(req.rawBody()), + session = api.get(req.urlParameters.sid); _.extend(session.get('sessionData'), body); session.save(); res.json(session.forClient()); diff --git a/js/apps/system/sessions/errors.js b/js/apps/system/sessions/errors.js index e9d79e6164..b55816c382 100644 --- a/js/apps/system/sessions/errors.js +++ b/js/apps/system/sessions/errors.js @@ -1,4 +1,4 @@ -/*jslint indent: 2, nomen: true, maxlen: 120, vars: true, es5: true */ +/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */ /*global require, exports */ (function () { 'use strict'; diff --git a/js/apps/system/sessions/setup.js b/js/apps/system/sessions/setup.js index 35a6ee8997..593d6a228f 100644 --- a/js/apps/system/sessions/setup.js +++ b/js/apps/system/sessions/setup.js @@ -1,9 +1,9 @@ -/*jslint indent: 2, nomen: true, maxlen: 120, vars: true, es5: true */ +/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */ /*global require, applicationContext */ (function () { 'use strict'; - var db = require('org/arangodb').db; - var sessionsName = applicationContext.collectionName('sessions'); + var db = require('org/arangodb').db, + sessionsName = applicationContext.collectionName('sessions'); if (db._collection(sessionsName) === null) { db._create(sessionsName); diff --git a/js/apps/system/sessions/storage.js b/js/apps/system/sessions/storage.js index c380bb8127..67afab2e7f 100644 --- a/js/apps/system/sessions/storage.js +++ b/js/apps/system/sessions/storage.js @@ -1,34 +1,31 @@ -/*jslint indent: 2, nomen: true, maxlen: 120, vars: true, es5: true */ +/*jslint indent: 2, nomen: true, maxlen: 120, 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} - ); + var _ = require('underscore'), + internal = require('internal'), + arangodb = require('org/arangodb'), + db = arangodb.db, + addCookie = require('org/arangodb/actions').addCookie, + crypto = require('org/arangodb/crypto'), + Foxx = require('org/arangodb/foxx'), + errors = require('./errors'), + cfg = applicationContext.configuration, + 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} + } + }), + sessions = new Foxx.Repository( + applicationContext.collection('sessions'), + {model: Session} + ); function generateSessionId() { var sid = ''; @@ -43,17 +40,17 @@ } 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 - }); + var sid = generateSessionId(cfg), + now = Number(new Date()), + session = new Session({ + _key: sid, + sid: sid, + sessionData: sessionData || {}, + userData: {}, + created: now, + lastAccess: now, + lastUpdate: now + }); sessions.save(session); return session; } @@ -106,11 +103,12 @@ } function fromCookie(req, cookieName, secret) { - var session = null; - var value = req.cookies[cookieName]; + var session = null, + value = req.cookies[cookieName], + signature; if (value) { if (secret) { - var signature = req.cookies[cookieName + '_sig'] || ''; + signature = req.cookies[cookieName + '_sig'] || ''; if (!crypto.constantEquals(signature, crypto.hmac(secret, value))) { return null; } @@ -131,8 +129,8 @@ if (!cfg.timeToLive) { return; } - var now = Number(new Date()); - var prop = cfg.ttlType; + var now = Number(new Date()), + prop = cfg.ttlType; if (!prop || !this.get(prop)) { prop = 'created'; } @@ -141,8 +139,8 @@ } }, addCookie: function (res, cookieName, secret) { - var value = this.get('_key'); - var ttl = cfg.timeToLive; + var value = this.get('_key'), + ttl = cfg.timeToLive; ttl = ttl ? Math.floor(ttl / 1000) : undefined; addCookie(res, cookieName, value, ttl); if (secret) { @@ -167,16 +165,16 @@ return session; }, save: function () { - var session = this; - var now = Number(new Date()); + var session = this, + 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()); + var session = this, + now = Number(new Date()); session.set('lastAccess', now); session.set('lastUpdate', now); try { diff --git a/js/apps/system/simple-auth/auth.js b/js/apps/system/simple-auth/auth.js index cb40930ad2..725a7daef0 100644 --- a/js/apps/system/simple-auth/auth.js +++ b/js/apps/system/simple-auth/auth.js @@ -1,26 +1,26 @@ -/*jslint indent: 2, nomen: true, maxlen: 120, vars: true, es5: true */ +/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */ /*global require, exports, applicationContext */ (function () { 'use strict'; - var crypto = require('org/arangodb/crypto'); - var cfg = applicationContext.configuration; + var crypto = require('org/arangodb/crypto'), + 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); + var hashMethod = authData.method || cfg.hashMethod, + salt = authData.salt || '', + storedHash = authData.hash || '', + 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); + var hashMethod = cfg.hashMethod, + salt = crypto.genRandomAlphaNumbers(cfg.saltLength), + hash = crypto[hashMethod](salt + password); return {method: hashMethod, salt: salt, hash: hash}; } diff --git a/js/apps/system/users/errors.js b/js/apps/system/users/errors.js index bcc31bf1a7..59a268dd22 100644 --- a/js/apps/system/users/errors.js +++ b/js/apps/system/users/errors.js @@ -1,4 +1,4 @@ -/*jslint indent: 2, nomen: true, maxlen: 120, vars: true, es5: true */ +/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */ /*global require, exports */ (function () { 'use strict'; diff --git a/js/apps/system/users/setup.js b/js/apps/system/users/setup.js index d6417b2345..564bda8c74 100644 --- a/js/apps/system/users/setup.js +++ b/js/apps/system/users/setup.js @@ -1,9 +1,9 @@ -/*jslint indent: 2, nomen: true, maxlen: 120, vars: true, es5: true */ +/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */ /*global require, applicationContext */ (function () { 'use strict'; - var db = require('org/arangodb').db; - var usersName = applicationContext.collectionName('users'); + var db = require('org/arangodb').db, + usersName = applicationContext.collectionName('users'); if (db._collection(usersName) === null) { db._create(usersName); diff --git a/js/apps/system/users/storage.js b/js/apps/system/users/storage.js index 5408c663bb..bb72b1336a 100644 --- a/js/apps/system/users/storage.js +++ b/js/apps/system/users/storage.js @@ -1,25 +1,23 @@ -/*jslint indent: 2, nomen: true, maxlen: 120, vars: true, es5: true */ +/*jslint indent: 2, nomen: true, maxlen: 120, 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: { - user: {type: 'string', required: true}, - authData: {type: 'object', required: true}, - userData: {type: 'object', required: true} - } - }); - - var users = new Foxx.Repository( - applicationContext.collection('users'), - {model: User} - ); + var _ = require('underscore'), + arangodb = require('org/arangodb'), + db = arangodb.db, + Foxx = require('org/arangodb/foxx'), + errors = require('./errors'), + User = Foxx.Model.extend({}, { + attributes: { + user: {type: 'string', required: true}, + authData: {type: 'object', required: true}, + userData: {type: 'object', required: true} + } + }), + users = new Foxx.Repository( + applicationContext.collection('users'), + {model: User} + ); function resolve(username) { var user = users.firstExample({user: username}); From e0e003d0601882ed9cb1a730dddbff787c144043 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Wed, 9 Jul 2014 15:11:55 +0200 Subject: [PATCH 40/44] Migrated session/user routes and models to joi. --- js/apps/system/sessions/app.js | 24 +++++++----------------- js/apps/system/sessions/storage.js | 19 ++++++++++--------- js/apps/system/users/storage.js | 11 ++++++----- 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/js/apps/system/sessions/app.js b/js/apps/system/sessions/app.js index f0a87105ce..5161f60dbc 100644 --- a/js/apps/system/sessions/app.js +++ b/js/apps/system/sessions/app.js @@ -3,10 +3,12 @@ (function () { 'use strict'; var _ = require('underscore'), + joi = require('joi'), Foxx = require('org/arangodb/foxx'), errors = require('./errors'), controller = new Foxx.Controller(applicationContext), - api = Foxx.requireApp(applicationContext.mount).sessionStorage; + api = Foxx.requireApp(applicationContext.mount).sessionStorage, + sessionId = joi.string().description('Session ID'); controller.post('/', function (req, res) { var session = api.create(req.body()); @@ -21,10 +23,7 @@ var session = api.get(req.urlParameters.sid); res.json(session.forClient()); }) - .pathParam('sid', { - description: 'Session ID', - type: 'string' - }) + .pathParam('sid', {type: sessionId}) .errorResponse(errors.SessionExpired, 404, 'Session has expired') .errorResponse(errors.SessionNotFound, 404, 'Session does not exist') .summary('Read session') @@ -37,10 +36,7 @@ session.save(); res.json(session.forClient()); }) - .pathParam('sid', { - description: 'Session ID', - type: 'string' - }) + .pathParam('sid', {type: sessionId}) .errorResponse(errors.SessionExpired, 404, 'Session has expired') .errorResponse(errors.SessionNotFound, 404, 'Session does not exist') .errorResponse(SyntaxError, 400, 'Malformed or non-JSON session data.') @@ -54,10 +50,7 @@ session.save(); res.json(session.forClient()); }) - .pathParam('sid', { - description: 'Session ID', - type: 'string' - }) + .pathParam('sid', {type: sessionId}) .errorResponse(errors.SessionExpired, 404, 'Session has expired') .errorResponse(errors.SessionNotFound, 404, 'Session does not exist') .errorResponse(SyntaxError, 400, 'Malformed or non-JSON session data.') @@ -68,10 +61,7 @@ api.destroy(req.urlParameters.sid); res.status(204); }) - .pathParam('sid', { - description: 'Session ID', - type: 'string' - }) + .pathParam('sid', {type: sessionId}) .errorResponse(errors.SessionNotFound, 404, 'Session does not exist') .summary('Delete session') .notes('Removes the session with the given sid from the database.'); diff --git a/js/apps/system/sessions/storage.js b/js/apps/system/sessions/storage.js index 67afab2e7f..77794310a6 100644 --- a/js/apps/system/sessions/storage.js +++ b/js/apps/system/sessions/storage.js @@ -3,6 +3,7 @@ (function () { 'use strict'; var _ = require('underscore'), + joi = require('joi'), internal = require('internal'), arangodb = require('org/arangodb'), db = arangodb.db, @@ -11,15 +12,15 @@ Foxx = require('org/arangodb/foxx'), errors = require('./errors'), cfg = applicationContext.configuration, - 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} + Session = Foxx.Model.extend({ + schema: { + _key: joi.string().required(), + uid: joi.string().optional(), + sessionData: joi.object().required(), + userData: joi.object().required(), + created: joi.number().integer().required(), + lastAccess: joi.number().integer().required(), + lastUpdate: joi.number().integer().required() } }), sessions = new Foxx.Repository( diff --git a/js/apps/system/users/storage.js b/js/apps/system/users/storage.js index bb72b1336a..0a02c83569 100644 --- a/js/apps/system/users/storage.js +++ b/js/apps/system/users/storage.js @@ -3,15 +3,16 @@ (function () { 'use strict'; var _ = require('underscore'), + joi = require('joi'), arangodb = require('org/arangodb'), db = arangodb.db, Foxx = require('org/arangodb/foxx'), errors = require('./errors'), - User = Foxx.Model.extend({}, { - attributes: { - user: {type: 'string', required: true}, - authData: {type: 'object', required: true}, - userData: {type: 'object', required: true} + User = Foxx.Model.extend({ + schema: { + user: joi.string().required(), + authData: joi.object().required(), + userData: joi.object().required() } }), users = new Foxx.Repository( From 9da4255c16bc2ad9473bb101d06a0bf0bac3c7a7 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Wed, 20 Aug 2014 11:27:27 +0200 Subject: [PATCH 41/44] Fixed shell-users tests. --- js/common/tests/shell-users.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/js/common/tests/shell-users.js b/js/common/tests/shell-users.js index 0213676db5..a32e7cb914 100644 --- a/js/common/tests/shell-users.js +++ b/js/common/tests/shell-users.js @@ -115,7 +115,7 @@ function UsersSuite () { var passwd = "passwd-" + i; users.save(username, passwd); - assertEqual(username, c.firstExample({ 'userData.username': username }).userData.username); + assertEqual(username, c.firstExample({ user: username }).user); } }, @@ -128,7 +128,7 @@ function UsersSuite () { var passwd = "passwd"; users.save(username, passwd); - assertEqual(username, c.firstExample({ 'userData.username': username }).userData.username); + assertEqual(username, c.firstExample({ user: username }).user); try { users.save(username, passwd); @@ -148,7 +148,7 @@ function UsersSuite () { var passwd = ""; users.save(username, passwd); - assertEqual(username, c.firstExample({ 'userData.username': username }).userData.username); + assertEqual(username, c.firstExample({ user: username }).user); }, //////////////////////////////////////////////////////////////////////////////// @@ -159,7 +159,7 @@ function UsersSuite () { var username = "users-1"; users.save(username); - assertEqual(username, c.firstExample({ 'userData.username': username }).userData.username); + assertEqual(username, c.firstExample({ user: username }).user); }, //////////////////////////////////////////////////////////////////////////////// @@ -171,7 +171,7 @@ function UsersSuite () { var passwd = "arangodb-loves-you"; users.save(username, passwd); - assertEqual(username, c.firstExample({ 'userData.username': username }).userData.username); + assertEqual(username, c.firstExample({ user: username }).user); }, //////////////////////////////////////////////////////////////////////////////// @@ -184,11 +184,11 @@ function UsersSuite () { var passwd = "passwd-" + i; users.save(username, passwd); - assertEqual(username, c.firstExample({ 'userData.username': username }).userData.username); + assertEqual(username, c.firstExample({ user: username }).user); var d2 = users.replace(username, passwd + "xxx"); - assertEqual(username, c.firstExample({ 'userData.username': username }).userData.username); - assertEqual(username, d2.userData.username); + assertEqual(username, c.firstExample({ user: username }).user); + assertEqual(username, d2.user); } }, @@ -216,7 +216,7 @@ function UsersSuite () { var passwd = "passwd-" + i; users.save(username, passwd); - assertEqual(username, c.firstExample({ 'userData.username': username }).userData.username); + assertEqual(username, c.firstExample({ user: username }).user); users.remove(username); } }, From 501735c78ba1920a2a00e42b060c32b3e0464b1c Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Wed, 20 Aug 2014 11:34:33 +0200 Subject: [PATCH 42/44] Keep legacy users API backwards-compatible. --- js/common/tests/shell-database.js | 10 +++--- js/server/modules/org/arangodb/users.js | 45 +++++++++++++------------ 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/js/common/tests/shell-database.js b/js/common/tests/shell-database.js index 22ac7d5ae6..72caa2ccea 100644 --- a/js/common/tests/shell-database.js +++ b/js/common/tests/shell-database.js @@ -251,14 +251,14 @@ function DatabaseSuite () { assertTrue(internal.db._createDatabase("UnitTestsDatabase0", { }, users)); internal.db._useDatabase("UnitTestsDatabase0"); - var m = require("org/arangodb/users"); - var user = m.document("admin"); + var userManager = require("org/arangodb/users"); + var user = userManager.document("admin"); assertEqual("admin", user.user); assertTrue(user.active); assertEqual("m", user.extra.gender); - user = m.document("foo"); + user = userManager.document("foo"); assertEqual("foo", user.user); assertFalse(user.active); assertEqual("f", user.extra.gender); @@ -288,8 +288,8 @@ function DatabaseSuite () { assertTrue(internal.db._createDatabase("UnitTestsDatabase0", { }, users)); internal.db._useDatabase("UnitTestsDatabase0"); - var m = require("org/arangodb/users"); - var user = m.document("admin"); + var userManager = require("org/arangodb/users"); + var user = userManager.document("admin"); assertEqual("admin", user.user); assertTrue(user.active); assertEqual("m", user.extra.gender); diff --git a/js/server/modules/org/arangodb/users.js b/js/server/modules/org/arangodb/users.js index c18d721c92..2e891b7803 100644 --- a/js/server/modules/org/arangodb/users.js +++ b/js/server/modules/org/arangodb/users.js @@ -43,6 +43,19 @@ var ArangoError = arangodb.ArangoError; // --SECTION-- private functions // ----------------------------------------------------------------------------- +//////////////////////////////////////////////////////////////////////////////// +/// @brief converts a user document to the legacy format +//////////////////////////////////////////////////////////////////////////////// + +var convertToLegacyFormat = function (doc) { + return { + user: doc.user, + active: doc.authData.active, + extra: doc.userData || {}, + changePassword: doc.authData.changePassword + }; +}; + //////////////////////////////////////////////////////////////////////////////// /// @brief encode password using SHA256 //////////////////////////////////////////////////////////////////////////////// @@ -156,7 +169,7 @@ exports.save = function (username, password, active, userData, changePassword) { // not exports.reload() as this is an abstract method... require("org/arangodb/users").reload(); - return users.document(doc._id); + return convertToLegacyFormat(users.document(doc._id)); }; //////////////////////////////////////////////////////////////////////////////// @@ -205,7 +218,7 @@ exports.replace = function (username, password, active, userData, changePassword // not exports.reload() as this is an abstract method... require("org/arangodb/users").reload(); - return users.document(doc._id); + return convertToLegacyFormat(users.document(doc._id)); }; //////////////////////////////////////////////////////////////////////////////// @@ -253,7 +266,7 @@ exports.update = function (username, password, active, userData, changePassword) // not exports.reload() as this is an abstract method... require("org/arangodb/users").reload(); - return users.document(user._id); + return convertToLegacyFormat(users.document(user._id)); }; //////////////////////////////////////////////////////////////////////////////// @@ -298,12 +311,7 @@ exports.document = function (username) { throw err; } - return { - user: user.user, - active: user.authData.active, - userData: user.userData || {}, - changePassword: user.authData.changePassword - }; + return convertToLegacyFormat(user); }; //////////////////////////////////////////////////////////////////////////////// @@ -335,14 +343,7 @@ exports.isValid = function (username, password) { exports.all = function () { var users = getStorage(); - return users.all().toArray().map(function(doc) { - return { - user: doc.user, - active: doc.authData.active, - userData: doc.userData || {}, - changePassword: doc.authData.changePassword - }; - }); + return users.all().toArray().map(convertToLegacyFormat); }; //////////////////////////////////////////////////////////////////////////////// @@ -421,13 +422,13 @@ exports.changePassword = function (token, password) { validatePassword(password); - var data = user._shallowCopy; + var authData = user._shallowCopy.authData; - delete data.authData.passwordToken; - data.authData.simple = hashPassword(password); - data.authData.changePassword = false; + delete authData.passwordToken; + authData.simple = hashPassword(password); + authData.changePassword = false; - users.replace(user, data); + users.update(user, {authData: authData}); // not exports.reload() as this is an abstract method... require("org/arangodb/users").reload(); From 545df18f8037827f6d3e5a07dcfb8ba205a72e06 Mon Sep 17 00:00:00 2001 From: baslr Date: Wed, 20 Aug 2014 11:04:49 +0000 Subject: [PATCH 43/44] basir -> baslr fixed my username :) --- Documentation/Books/Users/Installing/Linux.mdpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/Books/Users/Installing/Linux.mdpp b/Documentation/Books/Users/Installing/Linux.mdpp index a5fd65236b..5094834664 100644 --- a/Documentation/Books/Users/Installing/Linux.mdpp +++ b/Documentation/Books/Users/Installing/Linux.mdpp @@ -32,7 +32,7 @@ provided by @mgiken. !SUBSECTION Debian sid To use ArangoDB on Debian sid (the development version of Debian), a different version -of ICU is required. User basir provided the following instructions for getting ArangoDB 2.0.7 +of ICU is required. User baslr provided the following instructions for getting ArangoDB 2.0.7 to work on an x86_64: [link to Github issue](https://github.com/triAGENS/ArangoDB/issues/865) From a067104acea823c56e29311dd31bcead5b404dec Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Wed, 20 Aug 2014 15:25:57 +0200 Subject: [PATCH 44/44] Removed filthy lies in users app docs. --- .../Books/Users/FoxxBundledApps/Users.mdpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Documentation/Books/Users/FoxxBundledApps/Users.mdpp b/Documentation/Books/Users/FoxxBundledApps/Users.mdpp index d19b5f5c7a..bda7a3c44a 100644 --- a/Documentation/Books/Users/FoxxBundledApps/Users.mdpp +++ b/Documentation/Books/Users/FoxxBundledApps/Users.mdpp @@ -44,7 +44,7 @@ Thrown by the user storage's *create* method if passed a *userData* object with ```js try { - userStorage.create({username: 'alreadyTaken'}); + userStorage.create('alreadyTaken', {some: 'data'}); } catch(err) { assertTrue(err instanceof userStorage.errors.UsernameNotAvailable); } @@ -54,26 +54,28 @@ try { User objects are instances of a Foxx model with the following attributes: -* *userData*: application-specific user data. This is an arbitrary object that must at least have a *username* property set to the user's username. +* *user*: the user's unique *username*. +* *userData*: application-specific user data. * *authData*: an arbitrary object used by authentication apps to store sensitive data. For password authentication this could be a hash, for third-party authentication services this could be information about the user's identity. This attribute should never be exposed to the user directly. !SUBSECTION Create a user Creates and saves a new instance of the user model. -`userStorage.create(userData)` +`userStorage.create(username, userData)` Throws *UsernameNotAvailable* if a user with the given username already exists. *Parameter* -* *userData*: an arbitrary object that will be stored as the user's *userData* attribute when the model is saved to the database. This object must at least have a *username* property set to a string. +* *username*: an arbitrary string that will be used as the user's username +* *userData*: an arbitrary object that will be stored as the user's *userData* attribute when the model is saved to the database. @EXAMPLES ```js -var user = userStorage.create({username: 'malaclypse'}); -assertEqual(user.get('userData').username, 'malaclypse'); +var user = userStorage.create('malaclypse', {hair: 'fuzzy'}); +assertEqual(user.get('userData').hair, 'fuzzy'); ``` !SUBSECTION Fetch an existing user @@ -99,7 +101,7 @@ If the username can not be resolved, a *UserNotFound* exception will be thrown i ```js var user = userStorage.resolve('malaclypse'); -assertEqual(user.get('userData').username, 'malaclypse'); +assertEqual(user.user, 'malaclypse'); ``` !SUBSUBSECTION Resolve a user ID directly