mirror of https://gitee.com/bigwinds/arangodb
Merge branch 'devel' of github.com:triAGENS/ArangoDB into devel
This commit is contained in:
commit
06e0859ef5
|
@ -0,0 +1,54 @@
|
|||
!CHAPTER Foxx Sessions
|
||||
|
||||
Foxx provides some convenience methods to make working with sessions easier.
|
||||
|
||||
!SUBSECTION Activate sessions
|
||||
|
||||
Enables session features for the controller.
|
||||
|
||||
`controller.activateSessions(options)`
|
||||
|
||||
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.
|
||||
|
||||
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: *"/_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.
|
||||
* *autoCreateSession* (optional): whether a session should always be created if none exists. Default: *true*.
|
||||
|
||||
@EXAMPLES
|
||||
|
||||
```js
|
||||
var controller = new FoxxController(applicationContext);
|
||||
controller.activateSessions({
|
||||
sessionStorageApp: '/_system/sessions',
|
||||
cookieName: 'sid',
|
||||
cookieSecret: 'secret',
|
||||
type: 'cookie'
|
||||
});
|
||||
```
|
||||
|
||||
!SUBSECTION Define a session destruction route
|
||||
|
||||
Defines a route that will destroy the session.
|
||||
|
||||
`controller.destroySession(path, options)`
|
||||
|
||||
Defines a route handler on the controller that destroys the session.
|
||||
|
||||
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*
|
||||
|
||||
* *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. Default: a function that sends a *{"message": "logged out"}* JSON response.
|
|
@ -0,0 +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.
|
|
@ -0,0 +1,273 @@
|
|||
!CHAPTER The Sessions App
|
||||
|
||||
The sessions app provides a session storage JavaScript API that can be used in other Foxx apps.
|
||||
|
||||
!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('/_system/sessions').sessionStorage;
|
||||
```
|
||||
|
||||
!SUBSECTION Exceptions
|
||||
|
||||
!SUBSUBSECTION 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);
|
||||
} catch(err) {
|
||||
assertTrue(err instanceof sessionStorage.errors.SessionNotFound);
|
||||
}
|
||||
```
|
||||
|
||||
!SUBSUBSECTION 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);
|
||||
} catch(err) {
|
||||
assertTrue(err instanceof sessionStorage.errors.SessionExpired);
|
||||
assertTrue(err instanceof sessionStorage.errors.SessionNotFound);
|
||||
}
|
||||
```
|
||||
|
||||
!SUBSECTION 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* (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.
|
||||
|
||||
!SUBSECTION Create a session
|
||||
|
||||
Creates and saves a 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);
|
||||
```
|
||||
|
||||
!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
|
||||
|
||||
!SUBSUBSECTION 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'));
|
||||
});
|
||||
```
|
||||
|
||||
!SUBSUBSECTION 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);
|
||||
```
|
||||
|
||||
!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
|
||||
|
||||
!SUBSUBSECTION 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);
|
||||
```
|
||||
|
||||
!SUBSUBSECTION 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();
|
||||
```
|
||||
|
||||
!SUBSECTION 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();
|
||||
```
|
||||
|
||||
!SUBSECTION 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'));
|
||||
```
|
||||
|
||||
!SUBSECTION 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
|
||||
controller.get('/hello', function(request, response) {
|
||||
session.addCookie(response, cookieName, secret);
|
||||
});
|
||||
```
|
||||
|
||||
!SUBSECTION 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.
|
||||
|
||||
@EXAMPLES
|
||||
|
||||
```js
|
||||
controller.get('/goodbye', function(request, response) {
|
||||
session.clearCookie(response, cookieName, secret);
|
||||
});
|
||||
```
|
|
@ -0,0 +1,49 @@
|
|||
!CHAPTER The Simple Authentication App
|
||||
|
||||
The simple auth app provides hashed password-based authentication with automatically generated salts and constant-time password verification.
|
||||
|
||||
!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('/_system/simple-auth').auth;
|
||||
```
|
||||
|
||||
!SUBSECTION 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.
|
||||
|
||||
!SUBSECTION 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.
|
|
@ -0,0 +1,184 @@
|
|||
!CHAPTER The Users App
|
||||
|
||||
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('/_system/users').userStorage;
|
||||
```
|
||||
|
||||
!SUBSECTION Exceptions
|
||||
|
||||
!SUBSUBSECTION 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);
|
||||
}
|
||||
```
|
||||
|
||||
!SUBSUBSECTION 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('alreadyTaken', {some: 'data'});
|
||||
} catch(err) {
|
||||
assertTrue(err instanceof userStorage.errors.UsernameNotAvailable);
|
||||
}
|
||||
```
|
||||
|
||||
!SUBSECTION The user object
|
||||
|
||||
User objects are instances of a Foxx model with the following attributes:
|
||||
|
||||
* *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(username, userData)`
|
||||
|
||||
Throws *UsernameNotAvailable* if a user with the given username already exists.
|
||||
|
||||
*Parameter*
|
||||
|
||||
* *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('malaclypse', {hair: 'fuzzy'});
|
||||
assertEqual(user.get('userData').hair, 'fuzzy');
|
||||
```
|
||||
|
||||
!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
|
||||
|
||||
!SUBSUBSECTION 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.user, 'malaclypse');
|
||||
```
|
||||
|
||||
!SUBSUBSECTION 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);
|
||||
```
|
||||
|
||||
!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
|
||||
|
||||
!SUBSUBSECTION 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);
|
||||
```
|
||||
|
||||
!SUBSUBSECTION 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
|
||||
var user = userStorage.get(userId);
|
||||
user.delete();
|
||||
```
|
||||
|
||||
!SUBSECTION 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();
|
||||
```
|
|
@ -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)
|
||||
|
|
|
@ -96,7 +96,12 @@
|
|||
* [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)
|
||||
* [User Storage](FoxxBundledApps/Users.md)
|
||||
* [Simple Authentication](FoxxBundledApps/SimpleAuth.md)
|
||||
<!-- 16 -->
|
||||
* [Foxx Manager](FoxxManager/README.md)
|
||||
* [First Steps](FoxxManager/FirstSteps.md)
|
||||
|
@ -115,6 +120,7 @@
|
|||
* [Example Setup](Replication/ExampleSetup.md)
|
||||
* [Replication Limitations](Replication/Limitations.md)
|
||||
* [Replication Overhead](Replication/Overhead.md)
|
||||
* [Replication Events](Replication/Events.md)
|
||||
<!-- 19 -->
|
||||
* [Sharding](Sharding/README.md)
|
||||
* [How to try it out](Sharding/HowTo.md)
|
||||
|
|
|
@ -59,6 +59,9 @@ JAVASCRIPT_JSLINT = \
|
|||
\
|
||||
`find @srcdir@/js/apps/system/cerberus -name "*.js"` \
|
||||
`find @srcdir@/js/apps/system/gharial -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"` \
|
||||
|
|
|
@ -31,5 +31,5 @@
|
|||
</span>
|
||||
</div>
|
||||
<%}%>
|
||||
<h5 class="collectionName"><%= appInfos[1] %><%= attributes.isSystem ? " (system)" : "" %><%= appInfos[0] === "dev" ? " (dev)" : ""%></h5>
|
||||
<h5 class="collectionName"><%= appInfos[1] %><%= attributes.isSystem && attributes.mount.charAt(0) === "/" && attributes.mount.charAt(1) === "_" ? " (system)" : "" %><%= appInfos[0] === "dev" ? " (dev)" : ""%></h5>
|
||||
</script>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */
|
||||
/*global require, applicationContext */
|
||||
(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,
|
||||
sessionId = joi.string().description('Session ID');
|
||||
|
||||
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', {type: sessionId})
|
||||
.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()),
|
||||
session = api.get(req.urlParameters.sid);
|
||||
session.set('sessionData', body);
|
||||
session.save();
|
||||
res.json(session.forClient());
|
||||
})
|
||||
.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.')
|
||||
.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()),
|
||||
session = api.get(req.urlParameters.sid);
|
||||
_.extend(session.get('sessionData'), body);
|
||||
session.save();
|
||||
res.json(session.forClient());
|
||||
})
|
||||
.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.')
|
||||
.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', {type: sessionId})
|
||||
.errorResponse(errors.SessionNotFound, 404, 'Session does not exist')
|
||||
.summary('Delete session')
|
||||
.notes('Removes the session with the given sid from the database.');
|
||||
}());
|
|
@ -0,0 +1,18 @@
|
|||
/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */
|
||||
/*global require, exports */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function SessionNotFound(sid) {
|
||||
this.message = 'Session with session id ' + sid + ' not found.';
|
||||
}
|
||||
SessionNotFound.prototype = new Error();
|
||||
|
||||
function SessionExpired(sid) {
|
||||
this.message = 'Session with session id ' + sid + ' has expired.';
|
||||
}
|
||||
SessionExpired.prototype = Object.create(SessionNotFound.prototype);
|
||||
|
||||
exports.SessionNotFound = SessionNotFound;
|
||||
exports.SessionExpired = SessionExpired;
|
||||
}());
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"name": "sessions",
|
||||
"author": "Alan Plum",
|
||||
"version": "0.1",
|
||||
"isSystem": true,
|
||||
"description": "Session storage for Foxx.",
|
||||
|
||||
"controllers": {
|
||||
"/": "app.js"
|
||||
},
|
||||
|
||||
"exports": {
|
||||
"sessionStorage": "storage.js"
|
||||
},
|
||||
|
||||
"defaultDocument": "",
|
||||
|
||||
"lib": ".",
|
||||
|
||||
"setup": "setup.js",
|
||||
|
||||
"configuration": {
|
||||
"timeToLive": {
|
||||
"description": "Session expiry timeout in milliseconds.",
|
||||
"type": "integer",
|
||||
"default": 604800000
|
||||
},
|
||||
"ttlType": {
|
||||
"description": "Timestamp session expiry should be checked against.",
|
||||
"type": "string",
|
||||
"default": "created"
|
||||
},
|
||||
"sidTimestamp": {
|
||||
"description": "Append a timestamp to the session id.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"sidLength": {
|
||||
"description": "Length of the random part of the session id",
|
||||
"type": "integer",
|
||||
"default": 20
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */
|
||||
/*global require, applicationContext */
|
||||
(function () {
|
||||
'use strict';
|
||||
var db = require('org/arangodb').db,
|
||||
sessionsName = applicationContext.collectionName('sessions');
|
||||
|
||||
if (db._collection(sessionsName) === null) {
|
||||
db._create(sessionsName);
|
||||
}
|
||||
}());
|
|
@ -0,0 +1,200 @@
|
|||
/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */
|
||||
/*global require, exports, applicationContext */
|
||||
(function () {
|
||||
'use strict';
|
||||
var _ = require('underscore'),
|
||||
joi = require('joi'),
|
||||
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({
|
||||
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(
|
||||
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),
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
value = req.cookies[cookieName],
|
||||
signature;
|
||||
if (value) {
|
||||
if (secret) {
|
||||
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()),
|
||||
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'),
|
||||
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,
|
||||
now = Number(new Date());
|
||||
session.set('lastAccess', now);
|
||||
session.set('lastUpdate', now);
|
||||
sessions.replace(session);
|
||||
return session;
|
||||
},
|
||||
delete: function () {
|
||||
var session = this,
|
||||
now = Number(new Date());
|
||||
session.set('lastAccess', now);
|
||||
session.set('lastUpdate', now);
|
||||
try {
|
||||
deleteSession(session.get('_key'));
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof errors.SessionNotFound) {
|
||||
return false;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
exports.fromCookie = fromCookie;
|
||||
exports.create = createSession;
|
||||
exports.get = getSession;
|
||||
exports.delete = deleteSession;
|
||||
exports.errors = errors;
|
||||
exports.repository = sessions;
|
||||
exports._generateSessionId = generateSessionId;
|
||||
}());
|
|
@ -0,0 +1,29 @@
|
|||
/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */
|
||||
/*global require, exports, applicationContext */
|
||||
(function () {
|
||||
'use strict';
|
||||
var crypto = require('org/arangodb/crypto'),
|
||||
cfg = applicationContext.configuration;
|
||||
|
||||
function verifyPassword(authData, password) {
|
||||
if (!authData) {
|
||||
authData = {};
|
||||
}
|
||||
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,
|
||||
salt = crypto.genRandomAlphaNumbers(cfg.saltLength),
|
||||
hash = crypto[hashMethod](salt + password);
|
||||
return {method: hashMethod, salt: salt, hash: hash};
|
||||
}
|
||||
|
||||
exports.verifyPassword = verifyPassword;
|
||||
exports.hashPassword = hashPassword;
|
||||
}());
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "simple-auth",
|
||||
"author": "Alan Plum",
|
||||
"version": "0.1",
|
||||
"isSystem": true,
|
||||
"description": "Simple password-based authentication for Foxx.",
|
||||
|
||||
"exports": {
|
||||
"auth": "auth.js"
|
||||
},
|
||||
|
||||
"defaultDocument": "",
|
||||
|
||||
"lib": ".",
|
||||
|
||||
"configuration": {
|
||||
"hashMethod": {
|
||||
"description": "Cryptographic hash function to use for new passwords.",
|
||||
"type": "string",
|
||||
"default": "sha256"
|
||||
},
|
||||
"saltLength": {
|
||||
"description": "Length of new salts.",
|
||||
"type": "integer",
|
||||
"default": 16
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */
|
||||
/*global require, exports */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function UserNotFound(uid) {
|
||||
this.message = 'User with user id ' + uid + ' not found.';
|
||||
}
|
||||
UserNotFound.prototype = new Error();
|
||||
|
||||
function UsernameNotAvailable(username) {
|
||||
this.message = 'The username ' + username + ' is not available or already taken.';
|
||||
}
|
||||
UsernameNotAvailable.prototype = new Error();
|
||||
|
||||
exports.UserNotFound = UserNotFound;
|
||||
exports.UsernameNotAvailable = UsernameNotAvailable;
|
||||
}());
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "users",
|
||||
"author": "Alan Plum",
|
||||
"version": "0.1",
|
||||
"isSystem": true,
|
||||
"description": "Username-based user storage for Foxx.",
|
||||
|
||||
"exports": {
|
||||
"userStorage": "storage.js"
|
||||
},
|
||||
|
||||
"defaultDocument": "",
|
||||
|
||||
"lib": ".",
|
||||
|
||||
"setup": "setup.js"
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */
|
||||
/*global require, applicationContext */
|
||||
(function () {
|
||||
'use strict';
|
||||
var db = require('org/arangodb').db,
|
||||
usersName = applicationContext.collectionName('users');
|
||||
|
||||
if (db._collection(usersName) === null) {
|
||||
db._create(usersName);
|
||||
}
|
||||
}());
|
|
@ -0,0 +1,122 @@
|
|||
/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */
|
||||
/*global require, exports, applicationContext */
|
||||
(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({
|
||||
schema: {
|
||||
user: joi.string().required(),
|
||||
authData: joi.object().required(),
|
||||
userData: joi.object().required()
|
||||
}
|
||||
}),
|
||||
users = new Foxx.Repository(
|
||||
applicationContext.collection('users'),
|
||||
{model: User}
|
||||
);
|
||||
|
||||
function resolve(username) {
|
||||
var user = users.firstExample({user: username});
|
||||
if (!user.get('_key')) {
|
||||
return null;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
function listUsers() {
|
||||
return users.collection.all().toArray().map(function (user) {
|
||||
return user.user;
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
function createUser(username, userData) {
|
||||
if (!userData) {
|
||||
userData = {};
|
||||
}
|
||||
if (!username) {
|
||||
throw new Error('Must provide username!');
|
||||
}
|
||||
var user;
|
||||
db._executeTransaction({
|
||||
collections: {
|
||||
read: [users.collection.name()],
|
||||
write: [users.collection.name()]
|
||||
},
|
||||
action: function () {
|
||||
if (resolve(username)) {
|
||||
throw new errors.UsernameNotAvailable(username);
|
||||
}
|
||||
user = new User({
|
||||
user: username,
|
||||
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;
|
||||
}());
|
|
@ -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);
|
||||
|
|
|
@ -129,8 +129,8 @@ function UsersSuite () {
|
|||
|
||||
users.save(username, passwd);
|
||||
assertEqual(username, c.firstExample({ user: username }).user);
|
||||
|
||||
try {
|
||||
|
||||
try {
|
||||
users.save(username, passwd);
|
||||
fail();
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 */
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -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
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
@ -448,13 +456,14 @@ extend(Controller.prototype, {
|
|||
/// @EXAMPLES
|
||||
///
|
||||
/// ```js
|
||||
/// app.activateAuthentication({
|
||||
/// type: "cookie",
|
||||
/// cookieLifetime: 360000,
|
||||
/// cookieName: "my_cookie",
|
||||
/// sessionLifetime: 400,
|
||||
/// });
|
||||
/// app.activateAuthentication({
|
||||
/// type: "cookie",
|
||||
/// cookieLifetime: 360000,
|
||||
/// cookieName: "my_cookie",
|
||||
/// sessionLifetime: 400,
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// @endDocuBlock
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
activateAuthentication: function (opts) {
|
||||
|
@ -496,6 +505,7 @@ extend(Controller.prototype, {
|
|||
/// }
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// @endDocuBlock
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
login: function (route, opts) {
|
||||
|
@ -533,6 +543,7 @@ extend(Controller.prototype, {
|
|||
/// }
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// @endDocuBlock
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
logout: function (route, opts) {
|
||||
|
@ -581,6 +592,7 @@ extend(Controller.prototype, {
|
|||
/// }
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// @endDocuBlock
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
register: function (route, opts) {
|
||||
|
@ -622,14 +634,98 @@ extend(Controller.prototype, {
|
|||
/// }
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// @endDocuBlock
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
changePassword: function (route, opts) {
|
||||
'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_activateSessions
|
||||
///
|
||||
/// `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 */_system/sessions*
|
||||
///
|
||||
///
|
||||
/// @EXAMPLES
|
||||
///
|
||||
/// ```js
|
||||
/// app.activateSessions({
|
||||
/// type: "cookie",
|
||||
/// cookieName: "my_cookie",
|
||||
/// autoCreateSession: true,
|
||||
/// sessionStorageApp: "/my-sessions"
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// @endDocuBlock
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
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_destroySession
|
||||
///
|
||||
/// `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"});
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// @endDocuBlock
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
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;
|
||||
|
|
|
@ -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 + "'");
|
||||
|
@ -1023,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
|
||||
// -----------------------------------------------------------------------------
|
||||
|
@ -1236,7 +1253,7 @@ exports.unmount = function (mount) {
|
|||
|
||||
var doc = mountFromId(mount);
|
||||
|
||||
if (doc.isSystem) {
|
||||
if (doc.isSystem && mount.charAt(1) === '_') {
|
||||
throw new Error("Cannot unmount system application");
|
||||
}
|
||||
|
||||
|
@ -1764,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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 = '/_system/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:
|
|
@ -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 */
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -43,28 +43,31 @@ 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
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
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 +75,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 +88,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 +109,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 +123,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 +145,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 convertToLegacyFormat(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,159 +194,146 @@ 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 convertToLegacyFormat(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 convertToLegacyFormat(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
|
||||
};
|
||||
return convertToLegacyFormat(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 +341,9 @@ 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 = {
|
||||
user: doc.user,
|
||||
active: doc.active,
|
||||
extra: doc.extra || { },
|
||||
changePassword: doc.changePassword
|
||||
};
|
||||
|
||||
result.push(user);
|
||||
}
|
||||
|
||||
return result;
|
||||
return users.all().toArray().map(convertToLegacyFormat);
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -409,11 +383,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 +394,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 +405,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 +414,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 authData = user._shallowCopy.authData;
|
||||
|
||||
users.update(current, { passwordToken: null, password: hash, changePassword: false });
|
||||
exports.reload();
|
||||
delete authData.passwordToken;
|
||||
authData.simple = hashPassword(password);
|
||||
authData.changePassword = false;
|
||||
|
||||
users.update(user, {authData: authData});
|
||||
|
||||
// not exports.reload() as this is an abstract method...
|
||||
require("org/arangodb/users").reload();
|
||||
|
||||
return true;
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
///
|
||||
|
|
Loading…
Reference in New Issue