Since on the server side for frontend Countly uses Express.js adding new functionality is extremely easy.
And with EJS templating you can easily pass data to your templates and render it.
File that will handle frontend requests should be named app.js and located in your plugins directory api folder as {plugin}/frontend/app.js
Your app.js needs to export object with single public method init, to which Countly will pass database connection, express app instance and express reference, so you could add any request handling or middleware as you want.
Countly path prefix
Countly might be installed in subdirectory of a domain, in that case countlyConfig will have a path property defined. You need to consider it when registering request pass with express app
Example app.js file could look like:
var plugin = {},
countlyConfig = require('../../../frontend/express/config');
(function(plugin) {
plugin.init = function(app, countlyDb, express) {
//add your middleware or process requests here
app.get(countlyConfig.path + '/ourplugin', function(req, res, next) {
//get url parameters
var parts = req.url.split("/");
var id = parts[parts.length - 1];
//read data from db using countlyDB
countlyDb.collection('ourplugin').findOne({
'_id': id
}, function(err, plugindata) {
//if no data available
if (err || !att) res.send('404: Page not Found', 404);
else {
//render template with data
res.render('../../../plugins/ourplugin/frontend/public/templates/default', {
path: countlyConfig.path || "",
cdn: countlyConfig.cdn || "",
data: plugindata
});
}
});
});
};
}(plugin));
module.exports = plugin;
This example will render a template located at {countly}/plugins/ourplugin/frontend/public/templates/default.html which should be an EJS template where you can process and display prepopulated plugin data.
Intercepting requests
Request registration to plugins are passed before any other request is registered by the core, meaning any request will be firstly passed to plugin allowing you to either override it or modify it.
And in some cases you would want to intercept request made to core either to perform some additional tasks based on request data or to modify request, etc. For that purpose you can register request with express app and use next function to pass request processing to Countly core.
//add your middleware or process requests here
app.get(countlyConfig.path+'/login', function(req, res, next) {
//do something with request data
//let Countly handle natural login flow
next();
});
Changing Countly configurations
Some Countly configurations are used in frontend and are even passed to browser side and used in templates.
You can change them in your plugin. Each req request object additionally contains config property with Countly's frontend/express/config.js contents that you can modify for each specific request separately
Checking if user is authenticated
Some pages should be accessed only by authenticated users. To check if user is authenticated, simply check if he has any session uid defined.
Additionally you might want to check if user is global admin, by checking sessions gadm property
app.get(countlyConfig.path+'/mypage', function (req, res, next) {
//check if user is authenticated
if (req.session.uid) {
if(req.session.gadm){
//user is global admin
}
else{
//user is simple user
}
}
else
//redirect to login page
res.redirect(countlyConfig.path+'/login');
});
Handling POST requests
Countly uses body parser middleware by default, so handling post requests is as easy as get requests
app.post(countlyConfig.path+'/mypage', function (req, res, next) {
//data we received with post
console.log(req.body)
//render something or redirect user here
res.end();
return true;
});
File uploads are as easy to handle
app.post(countlyConfig.path+'/mypage', function (req, res) {
//your file data is located at req.files.fieldname
var tmp_path = req.files.fieldname.path,
target_path = __dirname + "/public/images/image.png",
type = req.files.app_image.type;
// lets check file type
if (type != "image/png" && type != "image/gif" && type != "image/jpeg") {
//nothing that we would want, delete file
fs.unlink(tmp_path, function () {});
res.send(false);
return true;
}
//else we have an image, lets copy it
fs.rename(tmp_path, target_path, function (err) {
//and remove uploaded file
fs.unlink(tmp_path, function () {});
//output that we are finished
res.send("uploaded");
});
});
Static paths
In most cases when creating UI for your plugin, you would have your plugin specific css and javascript files, as well as templates that should be accessible from the web to a frontend client.
Countly core automatically adds your plugins public directories to static path list thus everything that is located at {countly}/plugins/yourplugin/frontend/public directory will be automatically available for whole world to see.
These public resources could be accessed in your templates or loaded via javascript by using your plugin name as subpath.
So if you want to access css file which is located at {countly}/plugins/yourplugin/frontend/public/stylesheets/main.css you would have to use url: http://yourdomain.com/yourplugin/stylesheets/main.css
Additionally you can specify your own static paths if needed using staticPaths method on your frontend plugin.
You can use the same method to override some existing static files, like redirect main css request to some other themed css file.
Note that you should use app.use inside staticPaths method, because it is invoked before router middleware
var plugin = {},
countlyConfig = require('../../../frontend/express/config');
(function (plugin) {
plugin.staticPaths = function(app, countlyDb, express){
//redirect static file path to another
app.use(countlyConfig.path+'/stylesheets/main.css', function (req, res, next) {
res.redirect(countlyConfig.path+'/ourplugin/stylesheets/main.css');
});
//add your own new static paths
app.use(countlyConfig.path+"/myfolder", express.static(__dirname + "/myfolder"));
};
}(plugin));
module.exports = plugin;
Frontend methods
Most of the cases can be handled by Express.js methods of intercepting requests before Countly core processes it. But in some cases you might want to get notified about some specific actions or do changes inside Countly flows.
For example, you don't want to listen to all /login calls, but you may need to know when user successfully logged in. Or you may want to modify javascript object which is exposed to dashboard or passed to EJS template. Or you might want to cancel CSRF for some frontend requests.
To accomplish that you can define methods for frontend part of your plugin object, that will be called in some specific cases.
Each method gets object with:
- req - request object
- res - response object
- next - express js next callback
- data - some additional info
Method name | When it is called | Notes |
---|---|---|
staticPaths | On server process start before assigning static path files | Can be used to add additional static paths if needed, or overwrite existing static paths |
skipCSRF | On each request before performing CSRF check | Return false if you want to skip CSRF check for this request |
userLogout | When user successfully logged out | Contains uid and email in data |
renderDashboard | When dashboard is rendered | Data contains: member - user data adminApps - id of apps user is admin of userApps - id of apps user is user of countlyGlobal - global object ot be exposed in browser toDashboard - object to be passed to EJS template |
passwordRequest | When user requested to change password | Data contains: email of the user requested |
passwordReset | When user resseted password | Data contains: member document |
setup | When first user created account when setting up Countly | Data contains: member document |
loginSuccessful | When user successfully logged in | Data contains: member document |
loginFailed | When user login failed | Data contains: username password |
apikeySuccessful | When user requested api key | Data contains: member document |
apikeyFailed | When user requested api key, but authentication failed | Data contains: username |
mobileloginSuccessful | When user logs in through mobile | Data contains: member document |
mobileloginFailed | When authentication fails through mobile | Data contains: username |
iconUpload | When user uploads app icon | Data contains: app_image_id |
userSettings | When user changes user settings | Data contains: member document |
Example code of using frontend methods
var plugin = {},
countlyConfig = require('../../../frontend/express/config');
(function (plugin) {
plugin.init = function(app, countlyDb){
//add new request handles here as expected
app.post(countlyConfig.path+'/nocsrf', function (req, res, next) {
res.write(true);
})
};
plugin.staticPaths = function(app, countlyDb, express){
//redirect static file path to another
app.use(countlyConfig.path+'/stylesheets/main.css', function (req, res, next) {
res.redirect(countlyConfig.path+'/ourplugin/stylesheets/main.css');
});
//add your own new static paths
app.use(countlyConfig.path+"/myfolder", express.static(__dirname + "/myfolder"));
};
//let's skip csrf for our nocsrf request
plugin.skipCSRF = function(ob){
if(ob.req.path == countlyConfig.path+"/nocsrf")
return true;
return false;
};
//let's add additional object to expose on dashboard
plugin.renderDashboard = function(ob){
//this can be accessed on browser side as
//countlyGlobal.myValue
ob.data.countlyGlobal.myValue = 42;
}
}(plugin));
module.exports = plugin;