Linz.js¶
Linz is a framework for creating administration interfaces. Linz is not a CMS, but is capable of CMS like functionality. Linz is a good choice of framework when the administration interface is the website itself.
Linz is built on Node.js, Express and MongoDB.
Linz is quite new and under rapid development. It is used quite successfully in a number of production sites however. This documentation is the first effort in making this open source project accessible to more developers.
Getting started with Linz¶
This will help you create a new Linz-based website. If you’d like to develop Linz itself, see Getting started with Linz development.
While we’re working on our documentation, you can get started with Linz via our example project, see mini-twitter.
Linz tries to force as little new syntax on you as possible. Of course, this is unavoidable in certain situations, and there are some conventions you’ll need to learn. We’ve tried to keep them as simple as possible.
Linz does make use of many other open source tools, such as Mongoose or Express. Linz tries to keep the usage of these tools as plain and simple as possible. Linz doesn’t wrap, customise or prettify syntax for other libraries/tools used within Linz. The three primary opensource tools that Linz relies on are:
- Express
- Mongoose
- Passport
The following will be a general overview of some of the core concepts of Linz.
Singleton¶
When you require Linz, you’re returned a singleton. This has the advantage that no matter where you require Linz, you get the same Linz instance.
Initialization¶
Linz must be initialized. During initialization, Linz accepts an options object with any of the following optional keys:
express
: An initialized Express intance.passport
: An initialized Passport instance.mongoose
: An initialized Mongoose instance.options
: An options object to customise Linz.
For example:
var express = require('express'),
mongoose = require('mongoose'),
passport = require('passport'),
linz = require('linz');
linz.init({
express: express(),
mongoose: mongoose
passport: passport,
options: {
'load configs': false
}
});
If neither an initialized instance of Express, Passport or Mongoose, nor an options object have been passed, Linz will create them for you:
// Use anything that has been passed through, or default it as required.
this.app = opts.express || express();
this.mongoose = opts.mongoose || require('mongoose');
this.passport = opts.passport || require('passport');
// overlay runtime options, these will override linz defaults
this.options(opts.options || {});
Options object¶
An object can be used to customize Linz. For example:
linz.init({
options: {
'mongo': `mongodb://${process.env.MONGO_HOST}/db`
}
});
You can read more about Linz defaults.
Events¶
The Linz object is an event emitter, and will emit the initialized
event when Linz has finished initializing.
It will also emit an event whenever a configuration is set (i.e. using the linz.set method). The name of the event will be the same as the name of the configuration that is set.
A common pattern for setting up Linz, using the event emitter, is as follows:
server.js:
var linz = require('linz');
linz.on('initialised', require('./app'));
// Initialize Linz.
linz.init({
options: {
mongo: `mongodb://${process.env.DB_HOST || 'localhost'}/lmt`,
'user model': 'mtUser'
}
});
app.js:
var http = require('http'),
linz = require('linz'),
routes = require('./routes'),
port = process.env.APP_PORT || 4000;
module.exports = function () {
// Mount routes on Express.
linz.app.get('/', routes.home);
linz.app.get('/bootstrap-users', routes.users);
// Linz error handling midleware.
linz.app.use(linz.middleware.error);
// Start the app.
http.createServer(linz.app).listen(port, function(){
console.log('');
console.log(`mini-twitter app started and running on port ${port}`);
});
};
Directory structure¶
Linz expects a common directory structure. If provided, it will load content from these directories. These directories should live alongside your Node.js entry point file (i.e. node server.js
).
models
: a directory of model files.schemas
: a directory of schemas, which are used as nested schemas within a model.configs
: a directory of config files.
You can read more about each of the above and what Linz expects in the documentation covering each area.
Linz defaults¶
Linz has a bunch of defaults, which can be use to alter Linz and the way it works. The coverage of the defaults and what they actually do, vary from default to default.
This document outlines the defaults and how they can be used to alter Linz.
Please note: this document is a work in progress and not all defaults have been documented. For a complete list of customizations you can make, view Linz’s defaults.
Setting defaults¶
There are two ways to alter the defaults:
- At init time.
- Any other time using the
linz.set
function.
Read about the Options object for more information about how to set the defaults at init time.
Alternatively, you can set a default using the linz.set
function:
var linz = require('linz');
linz.on('initialised', require('./app'));
linz.set('mongo', `mongodb://${process.env.MONGO_HOST}/db`);
The defaults¶
The following lists the defaults that you can use to customise Linz. Please note, not all defaults are detailed. Please review the code for more information.
cookie options¶
This is the options object that is passed to the session middleware, and cookie parser middleware. By default the HttpOnly
and SameSite
attributes are enabled. The Secure
attribute is off by default, but it’s recommended to turn this on if you’re delivering Linz over HTTPS (which we highly encourage).
Review the options for the session middleware and the cookie parser middleware.
cookie secret¶
This is the secret that is used to secure the session cookie used by Linz. You can change this to any string that you’d like. It is passed to the default session middleware and cookie parser middleware.
customAttributes¶
The customAttributes
default allows you to customise the HTML attributes applied to the body
. This can be useful for a range of things including targeting styles specific to a custom attribute, or supplying information about the user which can be used by JavaScript widgets.
To use customAttributes
define a function with the following signature:
/**
* @param {Object} req A HTTP request object.
* @return Array
*/
customAttributes (req)
The function should return an array of objects containing attributes in a name/value pair:
customAttributes (req) => {
const attributes = [];
if (req.user && req.user.group) {
attributes.push({
name: 'data-linz-usergroup',
value: req.user.group
});
}
return attributes;
}
This will result in a body
tag with custom attributes on all Linz pages:
<body data-linz-usergroup='20'>
routes¶
The routes
default allows you to define middleware that should be executed during the process of specific Linz routes.
To use routes
create an object, keyed by a HTTP verb (i.e. get
), with the value being an object keyed by a Linz route path, and the value being a middleware function. For example:
const routes = {
get: {
'/model/:model/:id/overview': (req, res, next) => {
// Middleware functionality
return next();
},
},
};
This would result in the middleware being executed whenever an overview page for any record of any model is viewed.
Importantly, the middleware above is executed after Linz middleware processing and before Linz route processing (i.e. where the view is rendered). This gives you great flexibility and also allows you to benefit from all of the work already completed by Linz. For example, you can inspect the record by referencing req.linz.record
.
The following is an object containing an example showing all routes you can hook into:
const routes = {
get: {
'/models/list': (req, res, next) => next(),
'/model/:model/list': (req, res, next) => next(),
'/model/:model/new': (req, res, next) => next(),
'/model/:model/export': (req, res, next) => next(),
'/model/:model/:id/overview': (req, res, next) => next(),
'/model/:model/:id/edit': (req, res, next) => next(),
'/model/:model/:id/delete': (req, res, next) => next(),
'/model/:model/:id/json': (req, res, next) => next(),
'/model/:model/:id/action/:action': (req, res, next) => next(),
'/configs/list': (req, res, next) => next(),
'/config/:config/overview': (req, res, next) => next(),
'/config/:config/edit': (req, res, next) => next(),
'/config/:config/default': (req, res, next) => next(),
'/config/:config/json': (req, res, next) => next(),
},
post: {
'/model/:model/list': (req, res, next) => next(),
'/model/:model/create': (req, res, next) => next(),
'/model/:model/export': (req, res, next) => next(),
'/model/:model/:id/save': (req, res, next) => next(),
'/config/:config/save': (req, res, next) => next(),
},
};
scripts¶
The scripts
default allows you to customise the external JavaScripts that are loaded on each page in Linz.
To use scripts
define a function with the following signature:
/**
* @param {Object} req A HTTP request object.
* @param {Object} res A HTTP response object.
* @return {Promise} Resolves with an array of script objects.
*/
scripts (req, res)
The function should return an array of objects containing the same HTML attributes as the <script>
tag:
scripts (req, res) => {
return Promise.resolve(res.locals.scripts.concat([
{
crossorigin: 'anonymous',
integrity: 'sha256-5YmaxAwMjIpMrVlK84Y/+NjCpKnFYa8bWWBbUHSBGfU=',
src: '//cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.47/js/bootstrap-datetimepicker.min.js',
},
]));
}
res.locals.scripts
contains all the scripts used by Linz, be careful when removing/updating these as it could break functionality within Linz.
You should use the existing array as the array that is resolved with the promise because it will replace res.locals.scripts
, not append to it.
The script objects can contain an additional inHead
boolean option to optionally load the script in the head tag.
To create data attributes, you can add a dataAttributes
property with a key that will be prefixed with data-
when output in HTML. For example:
scripts (req, res) => {
return Promise.resolve(res.locals.scripts.concat([
{
dataAttributes: {
test: 'test',
},
},
]));
}
will create the script:
<script data-test="test"></script>
You can also supply a content
property, which if provided, will add the value of the content
property within the script open and close tags.
session options¶
This is the options object that is passed to the session middleware. The cookie
property is set based on the cookie options default.
Review the options for the session middleware and the cookie parser middleware.
styles¶
The styles
default allows you to customise the external CSS stylesheets that are loaded on each page in Linz.
To use styles
define a function with the following signature:
/**
* @param {Object} req A HTTP request object.
* @param {Object} res A HTTP response object.
* @return {Promise} Resolves with an array of style objects.
*/
styles (req, res)
The function should return an array of objects containing the same HTML attributes as the <link>
tag:
styles (req, res) => {
return Promise.resolve(res.locals.styles.concat([
{
crossorigin: 'anonymous',
href: '//cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.47/css/bootstrap-datetimepicker.min.css',
integrity: 'sha256-yMjaV542P+q1RnH6XByCPDfUFhmOafWbeLPmqKh11zo=',
rel: 'stylesheet',
},
]));
}
res.locals.styles
contains all the styles used by Linz, be careful when removing/updating these as it could break functionality within Linz.
You should use the existing array as the array that is resolved with the promise because it will replace res.locals.styles
, not append to it.
To create data attributes, you can add a dataAttributes
property with a key that will be prefixed with data-
when output in HTML. For example:
styles (req, res) => {
return Promise.resolve(res.locals.styles.concat([
{
dataAttributes: {
test: 'test',
},
},
]));
}
will create the script:
<link data-test="test" />
You can also supply a content
property, which if provided, will add the value of the content
property within a style
open and close tags.
mongoOptions¶
Mongoose’s default connection logic is deprecated as of 4.11.0. mongoOptions
contains the minimum default connection logic required for a connection:
'mongoOptions': {
useMongoClient: true
}
See Mongoose connections. for more details and configurations.
404¶
The 404 default allows you to pass in your own 404 html.
To use 404
define a function with the following signature:
/**
* @param {Object} req A HTTP request object.
* @return {Promise} Resolves with the html.
*/
404 (req) => Promise.resolve(html)
The function should return a Promise that resolves with the html string.
Models¶
One of the primary reasons to use Linz is to ease model development and scaffold highly customizable interfaces for managing these models. Linz provides a simple DSL you can use to describe your model. Using the content of your DSL, Linz will scaffold an index, overview, edit handlers and provide a basic CRUD HTTP API for your model.
All Linz models are bootstrapped with two properties (created by Linz):
dateCreated
with a label of Date created.dateModified
with a label of Date modified.
Note
Linz will display your models in a list. The label used for each record is derived from the title field, or a virtual title field if one does not exist in your schema.
If your model has a title field, you don’t have to do anything. If your model doesn’t have a title field, you can tell Linz about another field in the schema that you would like used to derive a value and label for each record. The title is the default way to reference a record within Linz.
You create Models in the model
directory; one file per model. The file should have the following basic structure:
person.js:
var linz = require('linz');
// Create a new mongoose schema.
var personSchema = new linz.mongoose.Schema({
name: String,
email: String
});
// Add the Linz formtools plugin.
personSchema.plugin(linz.formtools.plugins.document, {
model: {
label: 'Person',
description: 'A person.',
title: 'name'
},
labels: {
name: 'Name',
email: 'Email'
},
list: {
fields: {
name: true,
email: true
}
},
form: {
name: {
fieldset: 'Details',
helpText: 'The users full name.'
},
email: {
fieldset: 'Details'
}
},
overview: {
body: {
fields: [
'name',
'email',
],
},
},
});
var person = module.exports = linz.mongoose.model('person', personSchema);
The file is broken down in the following parts:
- You
require
Linz, as you’ll need to register the model with Linz. - Create a standard Mongoose schema.
- Use the
linz.formtools.plugins.document
Mongoose plugin to register the model with Linz, passing in an object containing Linz’s model DSL. - Create a Mongoose model from the schema, and
export
it.
Mongoose schemas¶
Linz works directly with Mongoose schemas. Anything you can do with a Mongoose schema is acceptable to Linz.
Model DSL¶
Linz uses a Model DSL, which is an object that can be used to describe your model. Linz will use this information to scaffold user interfaces for you. The Model DSL contains six main parts:
model
contains basic information such as thetitle
field,label
anddescription
of the model.labels
contains human friendly versions of your model’s properties, keyed by the property name.list
contains information used to scaffold the list displaying model records.form
contains information used to scaffold the edit handler for a model record.overview
contains information used to scaffold the overview for a model record.fields
contains directives to enable/disable fields that Linz automatically adds to models.permissions
is a function used to limit access to a model.
You supply the DSL to Linz in the form of an object, to the linz.formtools.plugins.document
Mongoose plugin:
personSchema.plugin(linz.formtools.plugins.document, {
model: {
// ...
},
labels: {
// ...
},
list: {
// ...
},
form: {
// ...
},
overview: {
// ...
},
fields: {
// ...
},
permissions: function () {
}
});
Models model DSL¶
The model
keys value should be an object with three keys:
title
is required, unless you have atitle
field in your schema. If not, you should reference another field in your schema. This field will be used to derive the title for the record, and label for the field.label
should be a singular noun describing the model.description
should be a short sentence describing the noun.
The label
is used in many places and is automatically pluralized based on the usage context. The description
is only used on the Models index within Linz.
For example:
model: {
label: 'Person',
description: 'A person.',
title: 'name'
}
Models label DSL¶
The label DSL is used to provide a label and description for the model.
The labels
keys value should be an object, keyed by field names and strings of the human friendly versions of your field names.
For example:
labels: {
name: 'Name',
email: 'Email'
}
You can customize the labels for the default dateModified
and dateCreated
using this object. You can also supply the key title
with a value that should be used for the label of the record’s title.
Models list DSL¶
The list DSL is used to customize the model index that is generated for each model.
The list
keys value should be an Object, containing the following top-level keys:
actions
fields
sortBy
toolbarItems
showSummary
filters
paging
groupActions
recordActions
export
These allow you to describe how the model index should function. The list DSL is discussed in more detail in List DSL.
Models list DSL function¶
The list
keys value can also be a function. It should be a function with the following signature:
function listDSL (req, callback) {
For example:
{
list: (req, callback) => callback(null, {
fields: {...}
})
}
The function receives a HTTP request object, which provides lots of flexibility to alter the DSL object returned based on the user making the request, and the model record itself.
Models form DSL¶
The form DSL is used to customize the model record create and edit pages.
The form
keys value should be an Object, keyed by field names of the model, in the order you’d like each field’s edit control rendered. For example:
form: {
name: {
fieldset: 'Details',
helpText: 'The users full name.'
},
email: {
fieldset: 'Details'
}
}
This will generate a form with two fields that you can provide data for. Both fields will appear in the Details fieldset, in the order name
and then email
.
Each field object can contain the following keys:
label
placeholder
helpText
type
default
list
visible
disabled
fieldset
widget
required
query
transform
transpose
schema
relationship
These allow you to describe how the create and edit forms should function. The form DSL is discussed in more detail in Form DSL.
Models form DSL function¶
The form
keys value can also be a function. It should be a function with the following signature:
function formDSL (req, callback) {
For example:
{
form: (req, callback) => callback(null, {
name: {...}
})
}
The function receives a HTTP request object, which provides lots of flexibility to alter the DSL object returned based on the user making the request, and the model record itself.
Model permissions¶
Model permissions is an in-depth topic and should be considered amongst other permission capabilities. Read more about Permissions.
Model statics, virtuals and methods¶
When working with models, Linz makes use of specific Mongoose statics, virtuals and methods if they’ve been provided.
The following documents them, and their functionality.
listQuery static¶
You can create a Mongoose static called listQuery
for a model with the following signature:
function listQuery (req, query, callback)
If found, Linz will execute this function with req
and a Mongoose query before executing it, when retrieving data for the model list view. This provides an opportunity to customise the query before execution.
For example, if you’d like to return more fields from MongoDB than those listed in list.fields
you can do it here:
model.static.listQuery = (req, query, callback) => callback(null, query.select('anotherField anotherOne'));
canDelete method¶
You can create a Mongoose method called canDelete
for a model, with the following signature:
function canDelete (req, callback)
If found, Linz will execute this function before rendering the Model index page. This provides an opportunity to customise the delete record action. Because it is a Mongoose method, inside the function this
is scoped to the record itself.
The callback has the following signature callback (err, isEnabled, message)
. isEnabled
should be a boolean; true
to enable the delete action, false
to disable it. If it is disabled, you can use message
to provide a message that will be displayed to the user if they click on the delete button.
canEdit method¶
You can create a Mongoose method called canEdit
for a model, with the following signature:
function canEdit (req, callback)
If found, Linz will execute this function before rendering the Model index page. This provides an opportunity to customise the edit record action. Because it is a Mongoose method, inside the function this
is scoped to the record itself.
The callback has the following signature callback (err, isEnabled, message)
. isEnabled
should be a boolean; true
to enable the edit action, false
to disable it. If it is disabled, you can use message
to provide a message that will be displayed to the user if they click on the edit button.
Permissions¶
Linz has a unique permissions model, mostly due to the fact that it assumes nothing (well, nothing that you can’t alter anyway) about your user model; only that you have one.
Many frameworks define a user model that you must adhere to. Linz doesn’t. This provides an opportunity for a simplified yet highly flexible permissions model.
Permissions can be provided for both models and configs.
There are a few contexts you should be aware of.
The full scope of contexts are:
- In the context of all models:
models.canList
- In the context of a particular model:
model.canCreate
model.canDelete
model.canEdit
model.canList
model.canView
- In the context of all configs:
configs.canList
- In the context of a particular config:
config.canEdit
config.canList
config.canReset
config.canView
config.canView
Linz enforces permissions in two places:
- The UI
- A route execution
Linz will not render buttons, links to or actions for functionality that a user doesn’t have access to. Routes are completely protected. So even if a route was discovered, a user without permissions would not be able to resolve it.
Default permissions¶
This is Linz’s default permissions implementation:
function (user, context, permission, callback) {
return callback(true);
}
In short, there are no permissions.
Global permissions¶
Linz implementations can provide a function (in the options
object when initializing Linz) called permissions
. It should have the signature:
function permission (user, context, permission, callback)
This function will be called whenever Linz is evaluating the models.canList
and configs.canList
. Most commonly when generating navigation for a user, but also on the models list and configs list pages.
The user
is the user making the request for which permissions are being sought.
The context will be either a string, or an object. If it is a string, it will be either:
// In the context of all models
'models'
// In the context of all configs
'configs'
// In the context of a particular model
{
'type': 'model',
'model': 'modelName'
}
// In the context of a particular config
{
'type': 'config',
'config': 'configName'
}
The permission
will be one of the following strings:
canCreate
canDelete
canEdit
canList
canReset
(configs only)canView
The callback
accepts the following signature:
function callback (result)
result
is a boolean. Please note, this is different from the standard Node.js callback signature of function callback (err, result)
. You should design your function so that it returns false in the event of an error and logs the error for a post-mortem.
Throwing errors and failing at the point of checking permissions would not be a good look for anyone, hence the design to not provide this capability. This is something that needs to be handled by a developer.
Model and config permissions function¶
Determining permissions for models and configs is more contextually sensitive. To do this, when defining a model or config, you can also provide a permissions
key.
The key can have a value of either an object or a function. If an object is provided, it is used directly. If a function is provided, you have the benefit of knowing which user the permissions are being requested for. A function should have the following signature:
function modelPermission (user, callback)
The callback accepts the following signature:
function callback (err, result)
err
should be null
if no error occurred. If an error has occurred, you can return it to the callback which will then default the result
to false
. result
should be an object.
The result object should contain, optionally, the following keys with boolean values:
canEdit
canDelete
canList
canCreate
canView
Each key is optional, and defaults to true
if not provided. Linz evaluates the values with the ===
operator so an explicit false
must be provided to limit permissions.
API¶
Linz contains many useful APIs to simplify development.
At present, we’re working on a long term effort to expose all of Linz’s functionality via APIs and have Linz use these APIs itself. This will provide the most flexibility to customise Linz, without having to do the heavy lifting yourself when choosing to change something about Linz.
You can access the Linz APIs via linz.api
.
error¶
error.json(err, statusCode = 500)¶
Take an Error object and add err
and statusCode
properties. If an error is encountered by the Linz error handling middleware with these properties, the error will be returned via JSON, rather than HTML.
error.store(err, req)¶
Take an Error object and store it on req
within the Linz namespace at req.linz.error
. This becomes useful for logging errors that might be produced by Linz.
formtools¶
list.getFilters(req)¶
Get the filters for a request. Used when you want to match a query to what is displayed in the list view.
model¶
model.generateForm(model, options)¶
Generate a form in the context of a Linz model. This is useful if you want to customise the form or to present a Linz form other than the create and edit views. This API is also used by Linz internally.
model: object¶
The mongoose model.
options: object¶
Property | Type | Description |
---|---|---|
form |
Object | Provide a custom form dsl for the model. |
record |
Object | Provide a record to prepopulate the form. |
req |
Object | Provide a req object to customise the form. |
type |
String | The form type. Can be edit or create (The default). |
model.generateFormString(model, options)¶
Generate form HTML in the context of a Linz model. This uses model.generateForm to generate the form object and returns the rendered view as a HTML string. Generally, this should be used when you want to render a customised Linz form.
model: object¶
The mongoose model.
options: object¶
In addition to the generateForm options, you can also pass in the following options:
Property | Type | Description |
---|---|---|
actionUrl |
String | The form post url. |
cancelUrl |
String | The url to return to upon cancelling the form. |
Session¶
The methods exposed via the linz.api.session
namespace have functionality centered around working with the user session.
session.getTimezone(req)¶
Get the timezone offset of the current user.
Util¶
The methods exposed via the linz.api.util
namespace have functionality that are broad and general in nature.
util.escape(userInputFromDatabase)¶
Use this method to escape a string before using it in HTML. This method should be used whenever you’re displaying information from an untrusted source (i.e. information from the database supplied by a user).
Views¶
The methods exposed via the linz.api.views
namespace have functionality centered around working with Linz’s views.
You can use these methods to render your own content, within a Linz template.
views.getScripts(req, res, scripts = [])¶
Get the scripts that Linz uses for a particular route.
views.getStyles(req, res, scripts = [])¶
Get the styles that Linz uses for a particular route.
views.notification(noty)¶
Takes an object, and applies noty defaults to it, making it easy to create noty objects for notifications. You can ready more about Noty options.
views.render(options, callback)¶
Render some HTML, within a Linz template. Useful for developing completely custom page content, without having to provide the Linz basics such as navigation, log out controls, etc.
options: object¶
options
object is used to pass the HTML that should be rendered within the Linz template. The following table describes the properties of the options
object.
Property | Type | Description |
---|---|---|
header |
HTML | This is the header of the page. You’ll need to include wrapping div elements in your HTML to make it look like the default. |
body |
HTML | This is the body of the page. You’ll need to include wrapping div elements in your HTML to make it look like the default. |
page |
HTML | If provided, header and body are ignored, allowing completely custom content. |
script |
HTML | This is intended to be <script> tags placed at the bottom (just above </body> ). |
template |
String | Either wrapper or wrapper-preauth . This is the template to use to wrap the provided HTML. Defaults to layout . |
callback¶
callback
can be one of two things:
- A standard callback
function
in the formatcallback(err, html)
. - An Express response (
res
) object.
If an Express reponse object is provided, Linz will automatically call res.render
with the rendered HTML.
Example¶
const linz = require('linz');
module.exports = function (req, res, next) {
const locals = {
header: '<div class="col-xs-12"><div class="model-title"><h1>Header.</h1></div></div>',
body: '<div class="container linz-container index"><div class="col-xs-12"><p>Body.</p></div></div>'
};
linz.api.views.render(locals, req, res, (err, html) => {
if (err) {
return next(err);
}
return res.send(html);
});
};
List DSL¶
The Models list DSL is used to customise the model index that is generated for each model. The list DSL has quite a few options, as the model index is highly customizable.
list
should be an object, containing the following top-level keys:
actions
export
fields
filters
groupActions
help
paging
recordActions
showSummary
sortBy
toolbarItems
These allow you to describe how the model index should function.
list.actions¶
list.actions
should be an Array of Objects. Each object describes an action that a user can make, at the model level. Each action should be an Object with the following keys:
label
is the name of the action.action
is the last portion of a URL, which is used to perform the action.modal
optionally render the results in a modal view.
For example:
actions: [
{
label: 'Import people',
action: 'import-from-csv',
modal: true
}
]
This will generate a button, on the model index, next to the model label. Multiple actions will produce a button titled Actions with a drop-down list attached to it, containing all possible actions.
The evaluated string /{linz-admin-path}/model/{model-name}/action/{action.action}
will be prefixed to the value provided for action
to generate a URL, for example /admin/model/person/import-from-csv
. It is the developers responsibility to mount the GET
route using Express, and respond to it accordingly.
The actions will be rendered in the order they’re provided.
If using a modal, make sure the HTML returned from the route starts with <div class="modal-dialog"><div class="modal-content"></div></div>
.
list.export¶
list.export
is used to denote that a particular model is exportable. Linz takes care of the exporting for you, unless you want to provide a custom action to handle it yourself.
When a user clicks on an export, they’ll be provided a pop-up modal asking them to choose and order the fields they’d like to export.
list.export
should be an Array of Objects. Each object describes an export option, for example:
export: [
{
label: 'Choose fields to export',
inclusions: 'dateModified,dateCreated',
dateFormat: linz.get('date format'),
useLocalTime: false,
modal: false,
}
]
Each object should contain the following keys:
label
which is the name of the export.inclusions
which is a list of fields that can be exported.dateFormat
which allows you to format the exported dates using moment date formats. (Defaults to false)useLocalTime
which allows you to export date fields in the browsers timezone offset.modal
optionally render the results in a modal view.
If you’d like to provide your own export route, you can. Replace the inclusions
key with an action
key that works the same as list.actions. Rather than a modal, a request to that route will be made. You’re responsible for mounting a GET
route in Express to respond to it.
list.fields¶
list.fields
is used to customize the fields that appear in the listing on the model index.
list.fields
should be an Object, keyed by each field in your model. The value for each key should be true
to include the field or false
to exclude the field. For example:
fields: {
name: true,
username: true
}
Linz will convert the above into the following:
fields: {
name: {
label: 'Name',
renderer: linz.formtools.cellRenderers.default
},
username: {
label: 'Username',
renderer: linz.formtools.cellRenderers.default
}
}
If you like, you can pass an object rather than the boolean. This also allows you to customize the cell renderer used to display the data within the column.
If you provide a label
, it will override what is defined in the Models label DSL.
The fields will be rendered in the order they’re provided.
list.filters¶
list.filters
can be used to include filters which will alter the data included in the dataset for a particular model. Filters can contain a custom user interface, but Linz comes with a standard set of filters.
list.filters
should be an object, keyed by each field in your model. Each object must contain a filter, which should be an object adhering to the Linz model filter DSL. For example:
filters: {
dateModified: {
alwaysOn: true,
filter: linz.formtools.filters.dateRange,
}
}
The above will allow your model to be filtered by a date range filter, on the dateModified
property.
Each filter, keyed by the field name, can have the following keys:
alwaysOn
will ensure that the filter is always rendered in the list view.default
allows you to provide a default value for the filter. It only takes affect when usingalwaysOn
.filter
this is optional, but allows you to specify a filter and should point to a Linz filter, or your own custom one.once
will ensure that a user can only add that filter once (works well with theboolean
filter).
Note
Be aware of the default
values. Because of Linz’s internal query structure most filters will need to provide the default
value as an array, but there are some exceptions.
Below is an example of the default
data type for each filter:
dateRange
:{ dateFrom: [ '2017-10-15' ], dateTo: [ '2017-10-28' ] }
date
:['2017-10-01']
boolean
:true
default
,fulltext
,list
:['string']
number
:[4]
See also
View the complete list of Linz filters.
list.groupActions¶
list.groupActions
can be used to define certain actions that are only available once a subset of data has been chosen.
Each record displayed on a model index has a checkbox, checking two or more records creates a group. If groupActions
have been defined for that model, those actions will become choosable by the user.
list.groupActions
should be an Array of Objects. Each object describes an action that a user can make, and the object takes on the same form as those described in list.actions.
You’re responsible for mounting a GET
route in Express to respond to it.
list.help¶
The list.help
key can be used to provide information for a particular model. The information will appear in a Bootstrap popover.
The list.help
key accepts either false
, or a Bootstrap popovers options object.
list.paging¶
list.paging
can be used to customise the paging controls for the model index. Paging controls will only be shown when the number of results for a model index, are greater than the per page total.
list.paging
should be an Object, with the following keys:
active
is an optional Boolean used to turn paging on or off. It defaults totrue
.size
is the default page size. It defaults to20
.sizes
is an Array of the page sizes available for a user to choose from on the model index. It defaults to[20, 50, 100, 200]
.
For example:
paging: {
active: true,
size: 50,
sizes: [50, 100, 150, 200]
}
If you don’t provide a paging object it defaults to:
paging: {
active: true,
size: 20,
sizes: [20, 500, 100, 200]
}
list.recordActions¶
list.recordActions
can be used to customise record specific actions. These are actions that act upon a specific model record. The actions appear as buttons for each record in a model list. The buttons can either appear in a drop-down list, or next to the edit and delete buttons for the record.
list.recordActions
should be an Array of Objects. Each object describes an action that a user can make, specific to the record, and the object takes on the same form as those described in list.actions.
list.recordActions
can have an optional key type
and when set to primary
, the action will be renderered next to the edit and delete buttons for the record (i.e. not within the dropdown). You can also supply a key icon
, which if supplied, will be used rather than a label for the button. The value for icon
should correspond with name of a Bootstrap glyphicon.
list.recordActions
can also accept a function, as the value to a disabled
property. If provided, the function will be excuted with the following signature disabled (record, callback)
. The callback has the following signature callback (error, isDisabled, message)
. isDisabled
should be a boolean. true
to disable the record action, false
to enable it and you can provide a message if the action is to be disabled.
You’re responsible for mounting a GET
route in Express to respond to it.
list.showSummary¶
list.showSummary
can be used to include or exclude the paging controls from a model index.
list.showSummary
expects a boolean. Truthy/falsy values will also be interpreted, for example:
showSummary: true
list.sortBy¶
list.sortBy
is used to customise the sort field(s) which the data in the model index will be retrieved with.
list.sortBy
should be Array of field names or objects. If using an object, the field property is required for example:
sortBy: [
'name',
'username',
{
defaultOrder: 'asc',
field: 'dateModified',
label: 'Date modified',
},
]
This Array will be used to populate a drop-down list on the model index. The user can choose an option from the drop-down to sort the list with.
The defaultOrder
property is used to set the default ordering of the sort. You may want to change it to desc
when you want the latest records first.
list.toolbarItems¶
list.toolbarItems
can be used to provide completely customised content on the toolbar of a model index. The toolbar on the model index sits directly to the right of the Model label, and includes action buttons and drop-downs.
list.toolbarItems
should be an Array of Objects. Each object should provide a render
key with the value of a Function. The function will be executed to retrieve HTML to be placed within the toolbar. The function will be provided the request req, the response object res and callback function which should be executed with the HTML. The callback function has the signature callback(err, html)
For example:
toolbarItems: [
{
renderer: function (req, res, cb) {
let locals = {};
return cb(null, templates.render('toolbarItems', locals));
}
}
]
Form DSL¶
The Models form DSL is used to customise the create and edit forms that are generated for each model. The form DSL has quite a few options as the model create and edit forms are highly customizable.
The form DSL is used to construct create and edit form controls (for example checkboxes, or text inputs) for a model record. Each key in the form
object represents one of your model’s fields.
The type of form control used for each field can be defined explicitly, or determined by Linz (the default) based on the fields data type, as specificed when defining the field with Mongoose.
Each form control comes in the form of a widget, and can be explicitly altered by providing a different Linz widget, or creating your own widget.
form
should be an object. It should contain a key, labelled with the name of the model field you’re providing information for.
For example, if you had a model with the fields name
and email
your form
DSL might look like:
form: {
name: {
// configure the edit widget for the name field
},
email: {
// configure the edit widget for the name field
}
}
Each field object can contain the following top-level keys:
label
placeholder
helpText
type
default
list
visible
disabled
fieldset
widget
required
query
transform
transpose
These allow you to describe how the model create and edit forms should function.
Specialized contexts¶
There are two specialized contexts in which the form
DSL operates:
- When creating a model
- When editing a model
From time to time, you’ll want to have different settings for one field, based on the context. Linz supports this through use of create
and edit
keys. Each of the above top-level keys can also be provided as a child of either create
and edit
. For example:
form: {
username: {
create: {
label: 'Create a username',
helpText: 'You can\'t change this later on, so choose wisely.'
},
edit: {
label: 'The person\'s username',
disabled: true,
helpText: 'Once created, you can\'t edit the username.'
}
}
}
You can also use a combination of the default context and the specialized contexts create
and edit
contexts, for example:
form: {
username: {
label: 'The person\'s username',
edit: {
label: 'Uneditable username'
}
}
}
On the create form, the label for the username
field will be The person’s username, but Uneditable username on the edit form.
The specialized create
and edit
contexts always supersede the default context.
{field-name}.label¶
The label
property is optional. If not provided, it takes the label from the Models label DSL. If a label hasn’t been provided for that particular model field, it simply shows the name of the field itself.
The label property gives you an opportunity to customize it explicitly for the create and edit views.
{field-name}.placeholder¶
When you have the field of an appropriate type (such as text field), you can define the placeholder
which sets the content of the HTML’s <input>
tag placeholder
attribute.
When used in conjunction with a ref
field, it can be used to create optional references. For example, a select
in which the first option
has no value
but contains the placeholder
value as the label.
{field-name}.helpText¶
The helpText
property can be used to supply additional text that sits below the form input control, providing contextual information to the user filling out the form.
{field-name}.type¶
The type
property is intended to help Linz with two things:
- Manage the data that the field contains in an appropriate manner.
- To determine which widget to use if the
widget
property wasn’t provided.
type
accepts the following strings:
array
to render checkboxes for multiple select.boolean
to render radio inputs.date
to render a date input.digit
to render a text input with a regex of[0-9]*
.documentarray
to render a custom control to manage multiple sub-documents.email
to render an email input.enum
to render a select input.hidden
to render a hidden input.number
to render a text input with a regex of[0-9,.]*
.password
to render a password input.string
to render a text input.tel
to render a tel input with a regex of^[0-9 +]+$
.text
to render a text input.url
to render a url input.
The default widget, and the widget for all other types is the text widget.
{field-name}.default¶
The default
property can be supplied to define the default value of the field. The default if provided, will be used when a field has no value.
If the default
property is not provided, Linz will fallback to the default
value as provided when defining the Mongoose schemas.
{field-name}.list¶
The list
property is a special property for use with the enum
type. It is used to provide all values from which a list field value can be derived.
Please bear in mind, that the list
property is not involved in Mongoose validation.
The list
property can either be an array of strings, or an array of objects.
For example, an array of strings:
list: [ 'Dog', 'Cat', 'Sheep' ]
If an array of objects is supplied, it must be in the format:
form: {
sounds: {
list: [
{
label: 'Dog',
value: 'woof.mp3'
},
{
label: 'Cat',
value: 'meow.mp3'
},
{
label: 'Sheep',
value: 'baa.mp3'
}
]
}
}
There is also a more advanced use case in which you can provide a function which Linz will execute. This will allow you to generate at run time rather than start time, after Linz has been initialized:
form: {
sounds: {
list: function (cb) {
return cb(null, {
label: 'Dog',
value: 'woof.mp3'
},
{
label: 'Cat',
value: 'meow.mp3'
},
{
label: 'Sheep',
value: 'baa.mp3'
});
}
}
}
{field-name}.visible¶
The boolean visible
property can be set to a value of false
to stop the field from being rendered on the form.
{field-name}.disabled¶
The boolean disabled
property can be set to a value of true
to render the input field, with a disabled attribute.
{field-name}.fieldset¶
The fieldset
property should be supplied to control which fields are grouped together under the same fieldset.
The fieldset
property should be human readable, such as:
form: {
username: {
fieldset: 'User access details'
}
}
{field-name}.widget¶
The widget
property can be set to one of the many built-in Linz widgets. For example:
form: {
sounds: {
widget: linz.formtools.widget.multipleSelect()
list: [
{
name: 'Dog',
value: 'woof.mp3'
},
{
name: 'Cat',
value: 'meow.mp3'
},
{
name: 'Sheep',
value: 'baa.mp3'
}
]
}
}
{field-name}.required¶
The boolean required
property can be set to true
to require that a field has a value before the form can be saved (using client-side) validation.
{field-name}.query¶
The query
property can be used to directly alter the Mongoose query object that is generated while querying the database for records to display.
query
should be an object with the following keys:
filter
sort
select
label
{field-name}.transform¶
The transform
property will accept a function with the signature:
transform (field, record)
If provided, it will be executed before a record is saved to the database. It is useful if you need to manipulate client side data before it is stored in the database.
In some instances, client side data requirements are different from that of data storage requirements. transform
in combination with transpose
can be used effectively to manage these scenarios.
{field-name}.transpose¶
The transpose
property will accept an object with context.
Currently, the following contexts are accepted:
form
Used within context of forms and creating and editing model records.export
Only used for exporting records.
{field-name}.transpose.form¶
The transpose.form
property will accept a function with the signature:
transpose.form (field, record)
If provided, it will be executed before a field’s value is rendered to a form. It is useful if you’d like to manipulate the server-side data that is rendered to a form.
In some instances, data storage requirements are different form that of client side data requirements. transpose
in combination with transform
can be used effectively to manage these scenarios.
{field-name}.transpose.export¶
The transpose.export
property can accept a function with the signature:
transpose.export (val) => Promise
If provided, it will be executed before a field’s value is exported. It is useful if you’d like to manipulate the server-side data that is rendered to the export csv.
Notifications¶
Linz provides the ability to pop up notifications, into a Noty message.
It works well when paired with model, group, overview and record actions that perform a task, and then redirect back to the original page.
When using Linz notifications in this manner, you must make use of connect-flash like so:
// Perform task.
// Create the notification.
req.flash('linz-notification', linz.api.views.notification({ text: 'Notification message', type: 'success' }));
// Redirect the user back.
return res.redirect('back');
When the page is rendered, a notification will appear, informing the user that the action they took was successful.
req.linz.notifications¶
Before rendering a page, at some point in the route execution middlware, you can populate the req.linz.notifications
array with any Noty objects you’d like to be shown. For example:
app.use('/url', (req, res, next) => {
req.linz.notifications.push(linz.api.views.notification({ text: 'A message here.' }));
return next();
});
Once the page is rendered, Linz will pick up on this notification and display it for the user.
req.flash¶
As described above, using the req.flash
API for notifications is a handy way to provide the user information about the state of the action they’ve just performed. However, it should always be used in conjunction with res.redirect('back')
.
To use this API, use req.flash
like so:
req.flash('linz-notification', linz.api.views.notification({ text: 'Message here.' }));
The first parameter passed to req.flash
must be the string 'linz-notification'
, otherwise Linz will ignore it. The second parameter passed must be a Noty options object. Linz provides a handy API (views.notification(noty)) defaulting some of the options.
Request namespace¶
Linz adds to the Express req
object, an object which you can use to access Linz information about the incoming request.
The Linz namespace exists at:
req.linz
And is a copy of the object you receive when requiring Linz, for example require('linz')
.
It has the keys:
notifications
which is an array of notifications Linz will display.cache
which is an internal cache that Linz uses.
Depending on which view is currently being requested, you’ll also get extra information.
The Linz namespace can be used whenever Linz passes you req
and becomes a very handy API to get more infromation about the request currently being served.
Model form¶
The model form, both create and edit views, also recieve:
model
which is a reference to the current model, the basic Mongoose version of the model.model.linz
which is a reference to the current model, which extra Linz-specific information included.model.linz.form
which is a reference to the model form DSL.
Model list¶
The model list view also recieves:
model
which is a reference to the current model, the basic Mongoose version of the model.model.linz
which is a reference to the current model, which extra Linz-specific information included.model.linz.list
which is a reference to the model List DSL.
Model overview¶
The model overview view also recieves:
model
which is a reference to the current model, the basic Mongoose version of the model.model.linz
which is a reference to the current model, which extra Linz-specific information included.model.linz.overview
which is a reference to the model overview DSL.
Error handling¶
Linz consistently manages all errors by passing the error through to next. This allows a consistent approach to error handling and provides a centralised approach to intercept those errors if desired.
Linz provides an error handling middleware at linz.api.middleware.error
. You should mount this to your Express app as the last piece of error middleware. It will:
- Log
err.stack
toconsole.error
. - Store the error at
req.linz.error
. - Return the error in the context of JSON, or display the error in a view.
Recording errors¶
You can use some middleware to trap all errors that happen within Linz. For example:
module.exports = (req, res, next) => {
res.on('finish', () => {
if (req.complete && req.linz && req.linz.error) {
console.error(req.linz.error);
}
});
return next();
};
JSON errors¶
If an error occurs within Linz, in the context of a JSON request, Linz will decorate the error with two properties:
- json will be true.
- statusCode will be 500.
If the Linz error handling middleware captures an error with those properties, will return the error via JSON.
Cell renderers¶
Linz tries to provide as many customisation options as possible. One of those is in the form of a what we call a cell renderer.
Cell renderers can be used within record overviews and model indexes. They’re used to represent data to the user in a human friendly way.
You can do many things with cell renderers that will improve the user experience. For example, you could take latitude and longitude values and render a map, providing visual context about location information specific to the record.
Built-in cell renderers¶
There are already many built-in cell renderers. They can all be accessed in the following namespace linz.formtools.cellRenderers
.
The following shows how to define a specific cell renderer for a list field:
list: {
fields: {
websiteUrl: {
label: 'Website url',
renderer: linz.formtools.cellRenderers.url
}
}
}
The following provides a description of each built-in cell renderer:
date
used withdate
field types to render a date, as per thedate format
setting.overviewLink
can be used to provide a link in the list, to the overview for a particular record.array
can be used to format an array in the formatvalue 1, value 2, value 3
.boolean
can be used to format a boolean in the formatYes
orNo
.reference
can be used to render the title for aref
field type.url
can be used withurl
field types to render an<a>
tag linking to the URL stored as the value of the field.documentarray
can be used with an embedded document to render a table showing embedded documents.text
can be used to render text, or any value as it is.default
is used by Linz as the default cell renderer if a specific type can’t be matched. It attempts to support arrays, dates, numbers, booleans and url field types.
Custom cell renderers¶
You can quite easily create and use your own cell renderer. All cell renderers must have the same signature:
function renderer (value, record, fieldName, model, callback)
The value
is the value that needs to be rendered. The record
is a copy of the entire record that the value
belongs to. The fieldName
is the name of the field that is being rendered. The model
is a reference to the model that the record
belongs to.
The callback
is a function that accepts the standard Node.js callback signature:
function callback (err, result)
The result
should be HTML. The HTML will be used directly, without manipulation. As it is HTML, you can provide inline JavaScript and CSS as required to add functionality your cell renderer.
The following is an example of a cell renderer that will look up data for a reference field and render the title:
function renderReference (val, record, fieldName, model, callback) {
// Make sure we have the neccessary value, without erroring.
if (!val || typeof val === 'string' || typeof val === 'number') {
return callback(null, val);
}
// Retrieve the related documents title.
linz.mongoose.models[val.ref].findById(val._id, (err, doc) => {
if (err) {
return callback(err);
}
return callback(null, (doc && doc.title) ? doc.title : `${val} (missing)`);
});
};
Forgot password process¶
Linz has the capability to support a forgot password process. It can be used to allow the user to reset their password.
If enabled, it will render a Forgot your password? link on the log in page, which will facilitate the ability for a user to reset their password. It uses an email as proof of user record ownership. If the user can access a link sent to an email identified with a user record, then they can reset the password.
To enable this process, you need to:
- Have a user model that stores an email address.
- Define a
sendPasswordResetEmail
Mongoose static on your user model. - Define a
verifyPasswordResetHash
Mongoose method on your user model. - Define an
updatePassword
Mongoose static on your user model.
The process works as follows:
- User clicks the Forgot your password? link on the log in page.
- The user is directed to the Forgot your password page, and prompted to enter their email address.
- The
sendPasswordResetEmail
static is executed with the email address they provided. - The
sendPasswordResetEmail
static should generate a unique hash for the user, and send an email to the user containing a link to reset their password. - The user will receive the email, and click on the link.
- The link will be verified by executing the
verifyPasswordResetHash
method. - If the password reset hash can be be verified, the user will be provided the opportunity to enter a new password meeting the conditions of the
admin password pattern
setting. - The new password will be provided to the
updatePassword
static to store the updated password against the user record.
More succintly:
- Send a password reset email.
- Verify ownership of the email.
- Collect a new password and update their user record.
Send a password reset email¶
This part of the process entails:
- Retreiving a user record based on an email address.
- Generating a unique hash for the user record.
- Creating a link for the user to continue the process.
- Sending an email to the email address provided.
These actions should take place within the sendPasswordResetEmail
executed by Linz.
The sendPasswordResetEmail
static¶
The sendPasswordResetEmail
static should have the following signature:
function sendPasswordResetEmail (userEmail, req, res, callback)
It needs to be a Mongoose static on your user model.
userEmail
is the email address provided by the user who is trying to reset their password, req
is the current request object, res
is the current response object and callback
is the function to execute when you’ve completed the neccessary steps.
The callback
accepts the standard Node.js signature:
function callback (err)
If an Error
is provided, Linz will render the error, otherwise it will consider the process complete.
Retrieving a user record based on an email address¶
Use the userEmail
argument to search your user model for a corresponding record. If a record can’t be found, return an Error
to the callback
.
Make sure you take into consideration the following scenarios:
- Multiple user records associated with the same email address.
- No user record associated with the email address.
Genearing a unique hash for the user record¶
Once you have the user record, generate a unique hash for the user record. We recommend including the username
, _id
, email
and dateModified
.
The hash you generate must be verifyable by generating the same hash, at a later time, with the same information in the database (i.e. username
, _id
, email
and dateModified
).
A good Node.js package to consider to generate a hash is the bcrypt.js package.
Creating a link to verify email address ownership¶
Once you have the unique hash, and the records _id
value you can use linz.api.url.getAdminPasswordResetLink(id, hash)
to generate a url. Pass in the _id
and hash and Linz will safely add those to the url it returns.
Send an email¶
Once you have the link, you simply need to send it to the email address with instructions on what to do next; click on the link.
This is something you’ll have to implement yourself. Linz does not provide any capabilities to send emails. Linz is based on Express though, so you have all of it’s templating capabilities at hand. See using template engines with Express.
Verifying ownership of the email address¶
This part of the process involves verifying ownership of the email address. The user will receive the email, and click on the link. We want to make sure the link hasn’t been tampered with and that we can generate the same hash that was provided in the link.
Linz will retrieve the hash from the url and pass it to the verifyPasswordResetHash
method.
It must be a Mongoose method on your user model.
Your verifyPasswordResetHash
Mongoose method should have the following signature:
function verifyPasswordResetHash (candidateHash, callback)
The candidateHash
is the hash value that was retreived from the Url. The callback
is a standard Node.js callback:
function callback (err, result)
The result
should be a boolean value.
Your verifyPasswordResetHash
method should go through the same process to create the hash as it did in the first process. It should then verify that the candidateHash
is the same as your freshly generated hash using the data from your database.
If the candidateHash
checks out and you can successfully match it, return true
to the callback.
Updating the users password¶
If the hash
was verified, the user is provided an opportunity to enter a new password. The new password must meet the requirements of the admin password
setting.
The new password is provided to the updatePassword
Mongoose static on your user model. The updatePassword
static should have the following signature:
function updatePassword (id, newPassword, req, res, callback)
id
is the _id
of the user model record. newPassword
is the new password provided by the user. req
is the current request object. res
is the current response object. callback
is a standard Node.js callback:
function callback (err)
If an Error
is provided, Linz will render the error, otherwise it will consider the process complete.
The user will be notified that their password has been updated, and prompted to log into Linz again.
Security¶
Linz comes with a number of security features out of the box including:
- CSRF Protection
CSRF Protection¶
CSRF protection helps prevent unauthorized commands that are transmitted from a user that the web application trusts.
You can read more about CSRF on OWASP.
Customising CSRF¶
Linz uses the csurf module to provide CSRF protection.
To customise the options you can supply Linz with the option 'csrf options: {}'
. It accepts an object with the same properties as the csurf module:
linz.init({
'options': {
'csrf options': {},
},
});
Custom error handler¶
CSRF errors throw an error with the code err.code === 'EBADCSRFTOKEN'
. You can use this in your error handlers to display a custom message.
For example, here is a snippet from the linz error middleware:
module.exports = function (err, req, res, next) {
if (err.code === 'EBADCSRFTOKEN') {
err.message = (!req.body._csrf || req.body._csrf === 'undefined') ? 'No CSRF token was provided.' : 'The wrong CSRF token was provided.';
}
...
}
Adding CSRF protection to a custom form¶
The csurf module exposes the csrf token via req.csrfToken()
.
When implementing a custom page with a form, make sure to pass the following hidden input:
<input type="hidden" name="_csrf" value="{{csrfToken}}">
This is handled automatically for you when using linz.api.model.generateFormString()
. Just make sure to add the csrfToken option:
linz.api.model.generateFormString(linz.api.model.get('user'), { csrfToken: req.csrfToken() });
Getting started with Linz development¶
The linz repository is a complete environment for hacking on Linz. Using Node.js Docker, and a few commands, you’ll have a complete development environment up and running in no time.
Visit the linz repository for more information on how to get started hacking on Linz.
A note on documentation¶
Documentation is now a primary concern for the project. All PRs should be accompanied with updated documentation that describes in detail how to use a new feature, new capability, updates to an existing DSL or a new DSL.
mini-twitter¶
Mini Twitter is a complete working example of Linz. Head over to the mini-twitter GitHub repository to download mini-twitter and check out Linz.
Linz definitions¶
The following are a list of words you’ll see used many times in the Linz documentation. You can familiarize yourself with our terminology here.
- DSL
- Domain specific language. Linz uses these frequently to take instruction as to how certain functionality should be scaffolded. There is a DSL for Models, Configurations and Schemas.