-
Notifications
You must be signed in to change notification settings - Fork 2
Dev documentation
This file document some parts of this project that are not easy to understand only from comments. Please keep it up to date if you modify the behaviour of something.
Here you can find some examples of code that we use a lot in this project. Praise the lord of copy-past.
# Get authenticated user or undefined
req.user
# Get authenticated team or undefined
req.team
# Access data (from body, query in uri or param in uri)
req.data
# Test if a team has a permission
team.can('permission')
# Send a 200 response with the given data.
res.ok();
res.ok({data: 'data', data2: 'data2'});
To send an error to the client, you just have to throw a special error. You can find erros in /lib/Error.js
-
NotFoundError
will send an error 400 -
ForbiddenError
will send an error 403 -
BadRequestError
will send an error 400 -
ExpectedError
: Custom error, see after - sequelize validation error: it will send a 400 with
_error.validation
containing an array of validation errors. - Other Error thrown: it will send an error 500 with "Unexpected error" message. For security reason, the client will not be able to get more informations, but their will be more informations in server log.
Example:
const {NotFoundError, ForbiddenError} = require('../../lib/Errors');
throw new NotFoundError('The element you want cannot be found');
To generate custom error without creating a new exception, there is Expected error that you can generate with a Status code, StatusShortText, and full english technical message.
throw new ExpectedError(401, 'IPNotFound', 'There is no User associated with this IP');
Having a global object is generaly considered as a bad practice for obvious reasons. But it's also annoying to have to include in every files things that are used everywhere.
So for this reason, we choosed to create a Flux object, that is not global, but you can include it from everywhere and it will always be the same object (singleton). So if you want to use the Flux object, you just have to do :
import Flux from './Flux';
-
Flux.config
: read configuration files (merged into one object according to the configuration part of this docummentation) -
Flux.configReload()
: Method to force Flux to reload configuration from files -
Flux.rootdir
: Contains the absolute path to the root directory of the projet, without/
at the end. -
Flux.log.error
: Log in console in red, and append to production and development log -
Flux.log.warn
: Log in console in orange, and append to production and development log -
Flux.log.info
: Log in console in blue, and append to development log -
Flux.log.debug
: Log in console in green, and append to development log -
Flux.env
: Contains current environnement name (production, development, etc) -
Flux.controllers
: Loaded controllers -
Flux.io
: Socket.io object -
Flux.express
: Express object -
Flux.server
: HTTP server object -
Flux.sequelize
: Sequelize object -
Flux.[Model]
: You can access every loaded model. Example:
Flux.User.findAll()
As you can see, all methods and attributes of the Flux
object except models begin with a lowercase letter.
This is necessary to avoid collisions between models and other properties. Please keep it that way.
All configuration of the project is in the config/
directory. All files are
loaded and exped into Flux.config
. Files finishing by .NODE_ENV.js
will replace
values from original config file. and files finishing by .local.js
will replace
values from both precedent files. However .local.js
files are ignored by .git
,
so you can customize configuration for your local use.
Example: If we are in production (NODE_ENV=production
)
# `config/test.js`
module.exports = {
test1: 'test.js',
test2: 'test.js',
test3: 'test.js',
}
# `config/test.production.js`
module.exports = {
test2: 'test.production.js',
test3: 'test.production.js',
}
# `config/test.local.js`
module.exports = {
test3: 'test.local.js',
}
Then, because of priorities the value of Flux.config.test
will be
{
test1: 'test.js',
test2: 'test.production.js',
test3: 'test.local.js',
}
Note also that every file that doesn't follow the conventions will throw a warning
on server start, except if the file ends with .dist
.
To be able to process HTTP websocket request exactly the same way, Flux2-server rely internally on an Express server. As Express is only a HTTP server, request received via Socket.io are injected into Express server as if they were simple http requests.
Routes are defined by the config file config/routes.js
which is an object
that associate
'METHOD /path/:param': { action: 'CONTROLLER.ACTION', middlewares: ['MIDDLEWARE1', 'MIDDLEWARE2'], file: 'NAME OF THE MULTIPART FILE FIELD' }
# note: `file` attribute is optionnal. Currently, it only allow the upload of a single
# file `multer`, however if you need more one day, you can edit `app.js` to handle
# array or objects associated with this `file` attribute.
# Example
'put /alert/:id/users': { action: 'AlertController.updateAssignedUsers', middlewares: ['auth', 'requireAuth'] },
See Express documentation get more informations about path
format.
-
auth
: This middleware try to authenticate user with bearer jwt or socket id. On success,req.user
andreq.team
will be set. On failure, no error will happend, userequireAuth
afterauth
, if you want to restrict access to logged in users. -
requireAuth
: This middleware will throw401 Unauthorized
error if user is not authenticated before withauth
middleware. -
reqLogger
: Debug middleware that will write to console every received requests -
resLogger
: Debug middleware that will write to console every sent responses
Core middlewares : Thoses middlewares are automatically added, and should not be removed.
-
errorHandler
: This will handle any exception or error (except 404) and throw an error500
-
notFoundHandler
: This will be executed when the route doesn't lead to an action and will throw a404
. -
extendRes
: Extend theres
object with helpers (ok, error, error500), that help you to format your response. -
dataParser
: This middleware parse and merge all data sources (query, body, params) and expose an uniquereq.data
object.
Each controllers associated with a model that inherit Controller.js
will have thoses actions available :
-
find(req,res)
: Return a list of associated items with a parameterfilters
which have to be compatible with sequelize where condition. -
create(req,res)
: Create an item. Every parameter that has the name of an attribut will be set to this new item -
update(req,res)
: Update the item with the givenid
. Every parameter that has the name of an attribut will be updated in this item. -
destroy(req,res)
: Destroy the item with the givenid
-
subscribe(req,res)
: Subscribe to all events on this model -
unsubscribe(req,res)
: Unsubscribe from all events on this model
To make thoses actions available in the api, you have to create routes for them in config/routes.js
.
By default, any user with model/admin
, will have access to all generated actions,
and model/read
, will only be able to read. But if you want to tweak permissions,
you will have to overload some methods in the model.
Note: even if thoses methods are in the model, this permission system only apply to generated actions in the controller. Other methods can still read or modify model item even if user doen't have the permission to do it.
The idea behind this permission system, is that for each action type (read, created, update, delete), user is associated to groups, and each item is also associated with group. If one of thoses groups match, then the user can do the action on this item.
We will take for exemple the UserController
configuration for read
. We want
-
user/admin
anduser/read
can read everything -
user/team
can read only member from his team - Other users can only read their own user entry
// For each item, we define groups that can read it
Model.prototype.getReadGroups = function() {
// Every user which is in one of thoses groups will be able to read it
return [
'all', # Used for user that can read everything
'id:' + this.id, # Used for user that can only read one entry
'team:' + this.teamId, # Used for user that can only read entry from one team
];
};
// Then we define groups for the given user
Model.getUserReadGroups = (team, user) => {
let groups = [];
// Admin can read eveything so we put them in the `all` group
if(team.can(Model.name + '/read') || team.can(Model.name + '/admin')) {
groups.push('all');
}
// user can always read his own user entry, so we let him read only its own id
groups.push('id:' + user.id);
// If you can only see member of your team
if(team.can(Model.name + '/team')) {
groups.push('team:' + team.id);
}
return groups;
};
We known that it's not the easyest system to understand, but it's extremly powerfull because it work for simple CRUD, but it also work for socket publish/subscribe system that will publish item only to allowed users.
This exemple is for read action, but you can do the same for other actions:
Model.prototype.getReadGroups
Model.prototype.getUpdateGroups
Model.prototype.getCreateGroups
Model.prototype.getDestroyGroups
Model.getUserReadGroups
Model.getUserUpdateGroups
Model.getUserCreateGroups
Model.getUserDestroyGroups
For optimisation reason, there is a another overloadable Model method.
This method create Sequelize filters that will be happend to requested filter
for find()
request. This method exist only for the read
action.
Model.getReadFilters = function(team, user) {
let filters = [];
// Get groups associated with the user
let groups = this.getUserReadGroups(team, user);
// And generate sequelize filters from thoses groups
for (let group of groups) {
let split = group.split(':');
// Can read all
if(group == 'all') {
return [{true: true}];
}
// Can read only one id
else if(split.length == 2 && split[0] == 'id') {
filters.push({'id': split[1]});
}
// Can read only one team
else if(split.length == 2 && split[0] == 'team') {
filters.push({'teamId': split[1]});
}
}
return filters;
};
If you don't understand something, just try to look at others controllers and models, you should be able to find any use case you want.
To execute websocket request exactly like HTTP requests, we have to simulate an HTTP request. Their is two options to do that:
- Forge response and request object and inject them into Express server
- Create a new request and send it to localhost so Express take it as a legit request.
We choosed to use the first solution because it should be faster. However, it's possible that this code break in the future because we play with not fully docummented code, nearly internal code. Either express or nodejs could change a bit the API and break our code.
We choosed to keep this solution because the risky code is small and not so
hard to understand. So if for future upgrades, websocket requests are not executed
anymore, you can take a look at lib/FluxWebSocket:injectHttpRequest()
.