diff --git a/README.md b/README.md index 9538464..57dd67b 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,17 @@ STJÓRNA was created to have an easy product management with the possibility to STJÓRNA is islandic and means something like manage or store stuff. The two main goal of STJÓRNA are to be very simple in the setup and configuration effort. The second goal was to publish and share the stored data over an REST API with other applications, maybe your website. +So it is like a CMS for changing data like products or just images on your website. +The implementation on the client side is very easy and do not require much effort. - Availability of REST API for third-party applications +- Easy setup, you will be ready in minutes - Language support for German and English - Possibility to export all your data as a JSON or Excel file - Open Source software - hosted on Github +- Optional Matomo Tracking over the REST API to monitor loading activity on categories and products + +![Screenshot Stjorna](https://stjorna.secanis.ch/docs/images/stjorna_dashboard.png) ## Get Started @@ -49,13 +55,14 @@ cd server; npm run test Do not forget to set the NodeJS production mode: `process.env.NODE_ENV = 'production'`! -### Cofiguration +### Cofiguration & Setup -For more information please have a look on our website: [https://stjorna.secanis.ch](https://stjorna.secanis.ch). +Call `https:///setup` to configure Stjorna initially. +For more information please have a look on our documentation: [https://stjorna.secanis.ch](https://stjorna.secanis.ch). ## Contribution It would be very nice, when you give us a feedback or when you create issues if you detect problems or bugs. If you want to fix it yourself or you have an idea for something new, please create a PR, that would help us a lot. -Happy Coding <3 ... \ No newline at end of file +Happy Coding <3 ... diff --git a/server/api/routes_categories.js b/server/api/routes_categories.js index 1bdbb35..27f46f4 100644 --- a/server/api/routes_categories.js +++ b/server/api/routes_categories.js @@ -1,9 +1,12 @@ const dbHelper = require('../lib/database_helper.js'); +const trackingHelper = require('../lib/tracking_helper.js'); const logger = require('../lib/logging_helper.js').logger; -const prepareAndSaveImage = require('../lib/image_helper.js').prepareAndSaveImage; +const prepareAndSaveImage = require('../lib/image_helper.js') + .prepareAndSaveImage; module.exports = (router) => { - router.route('/v1/categories') + router + .route('/v1/categories') /** * @api {get} /api/v1/categories Get Category List * @apiName GetCategoryList @@ -16,17 +19,30 @@ module.exports = (router) => { .get((req, res) => { let categories; if (req.query.apikey || req.headers['x-stjorna-apikey']) { - categories = dbHelper.db.get('categories').filter({ active: true }).value(); + categories = dbHelper.db + .get('categories') + .filter({ active: true }) + .value(); } else { categories = dbHelper.db.get('categories').value(); } logger.log('debug', `category - load category list`); + if (categories) { + trackingHelper.apiTrack(req, 'list categories', { + c_n: 'categories', + c_t: req.originalUrl, + }); res.send(categories); } else { - logger.error(`category - error occured: couldn't load your categories`); - res.status(400).send({ 'message': `Couldn't load your categories`, 'status': 'error' }); + logger.error( + `category - error occured: couldn't load your categories` + ); + res.status(400).send({ + message: `Couldn't load your categories`, + status: 'error', + }); } }) /** @@ -46,7 +62,11 @@ module.exports = (router) => { // prepare and save image let imagePath = req.body.imageUrl || ''; if (req.body.image && req.body.image.includes('data:image')) { - imagePath = prepareAndSaveImage(req.body.image, '/categories', req.headers['x-stjorna-userid']); + imagePath = prepareAndSaveImage( + req.body.image, + '/categories', + req.headers['x-stjorna-userid'] + ); } let newItem = { @@ -59,32 +79,45 @@ module.exports = (router) => { created: new Date().getTime(), createdUser: req.body.createdUser, updated: new Date().getTime(), - updatedUser: null + updatedUser: null, }; - logger.log('debug', `category - add product with ID: ${newItem._id}`); - dbHelper.db.get('categories') + logger.log( + 'debug', + `category - add product with ID: ${newItem._id}` + ); + dbHelper.db + .get('categories') .push(newItem) .write() .then(() => { - let item = dbHelper.db.get('categories').find({ _id: newItem._id }).value(); + let item = dbHelper.db + .get('categories') + .find({ _id: newItem._id }) + .value(); if (item) { res.send(item); } else { - logger.error(`category - error occured: couldn't add category`); - res.status(400).send({ 'message': `Couldn't add category`, 'status': 'error' }); + logger.error( + `category - error occured: couldn't add category` + ); + res.status(400).send({ + message: `Couldn't add category`, + status: 'error', + }); } }); }); - router.route('/v1/categories/:id') + router + .route('/v1/categories/:id') /** * @api {get} /api/v1/categories/:id Get Category * @apiName GetCategory * @apiGroup Category * @apiPermission token/apikey * @apiVersion 1.0.0 - * + * * @apiParam {String} id unique Category ID. * * @apiSuccess {String} _id Category unique ID @@ -99,13 +132,30 @@ module.exports = (router) => { * @apiSuccess {String} updatedUser UserID which user has updatged the item. */ .get((req, res) => { - let item = dbHelper.db.get('categories').filter({ _id: req.params.id }).value()[0]; - logger.log('debug', `category - get category with ID: ${req.params.id}`); + let item = dbHelper.db + .get('categories') + .filter({ _id: req.params.id }) + .value()[0]; + logger.log( + 'debug', + `category - get category with ID: ${req.params.id}` + ); + if (item) { + trackingHelper.apiTrack(req, 'get category', { + c_n: 'category', + c_p: item.name, + c_t: req.originalUrl, + }); res.send(item); } else { - logger.error(`category - error occured: couldn't load category '${req.params.id}'`); - res.status(400).send({ 'message': `Couldn't load category '${req.params.id}'`, 'status': 'error' }); + logger.error( + `category - error occured: couldn't load category '${req.params.id}'` + ); + res.status(400).send({ + message: `Couldn't load category '${req.params.id}'`, + status: 'error', + }); } }) /** @@ -115,7 +165,7 @@ module.exports = (router) => { * @apiGroup Category * @apiPermission loggedin * @apiVersion 1.0.0 - * + * * @apiParam {String} id unique category ID. * * @apiSuccess {Object} Category Returns the updated category by ID. @@ -127,7 +177,11 @@ module.exports = (router) => { // prepare and save image let imagePath = req.body.imageUrl || ''; if (req.body.image && req.body.image.includes('data:image')) { - imagePath = prepareAndSaveImage(req.body.image, '/categories', req.headers['x-stjorna-userid']); + imagePath = prepareAndSaveImage( + req.body.image, + '/categories', + req.headers['x-stjorna-userid'] + ); } let newItem = { @@ -137,21 +191,33 @@ module.exports = (router) => { image: '', imageUrl: imagePath || '', updated: new Date().getTime(), - updatedUser: req.body.updatedUser + updatedUser: req.body.updatedUser, }; - logger.log('debug', `category - update category with ID: ${req.params.id}`); - dbHelper.db.get('categories') + logger.log( + 'debug', + `category - update category with ID: ${req.params.id}` + ); + dbHelper.db + .get('categories') .find({ _id: req.params.id }) .assign(newItem) .write() .then(() => { - let item = dbHelper.db.get('categories').filter({ _id: req.params.id }).value()[0]; + let item = dbHelper.db + .get('categories') + .filter({ _id: req.params.id }) + .value()[0]; if (item && item.updated === newItem.updated) { res.send(item); } else { - logger.error(`category - error occured: couldn't update category '${req.params.id}'`); - res.status(400).send({ 'message': `Couldn't update category '${req.params.id}'`, 'status': 'error' }); + logger.error( + `category - error occured: couldn't update category '${req.params.id}'` + ); + res.status(400).send({ + message: `Couldn't update category '${req.params.id}'`, + status: 'error', + }); } }); }) @@ -162,84 +228,143 @@ module.exports = (router) => { * @apiGroup Category * @apiPermission loggedin * @apiVersion 1.0.0 - * + * * @apiParam {String} id unique category ID. * * @apiSuccess {Object} Message Returns the status of the deleted category. */ .delete((req, res) => { - let productsWithCategory = dbHelper.db.get('products') - .filter({ category: req.params.id }) - .value(); - + let productsWithCategory = dbHelper.db + .get('products') + .filter({ category: req.params.id }) + .value(); + if (productsWithCategory && productsWithCategory.length > 0) { - logger.error(`category - error occured: couldn't remove category '${req.params.id}', because of existing products with this category`); - logger.warn(`category - you have ${productsWithCategory.length} existing products on category ${req.params.id}`); - res.status(400).send({ 'message': `Couldn't remove category '${req.params.id}', because of existing products with this category`, 'status': 'warning' }); + logger.error( + `category - error occured: couldn't remove category '${req.params.id}', because of existing products with this category` + ); + logger.warn( + `category - you have ${productsWithCategory.length} existing products on category ${req.params.id}` + ); + res.status(400).send({ + message: `Couldn't remove category '${req.params.id}', because of existing products with this category`, + status: 'warning', + }); } else { - logger.log('debug', `category - delete category with ID: ${req.params.id}`); - dbHelper.db.get('categories') + logger.log( + 'debug', + `category - delete category with ID: ${req.params.id}` + ); + dbHelper.db + .get('categories') .remove({ _id: req.params.id }) .write() .then(() => { - let item = dbHelper.db.get('categories').filter({ _id: req.params.id }).value()[0]; + let item = dbHelper.db + .get('categories') + .filter({ _id: req.params.id }) + .value()[0]; if (!item) { - res.send({ 'message': 'successfully removed', 'status': 'ok' }); + res.send({ + message: 'successfully removed', + status: 'ok', + }); } else { - logger.error(`category - error occured: couldn't remove category '${req.params.id}'`); - res.status(400).send({ 'message': `Couldn't remove category '${req.params.id}'`, 'status': 'error' }); + logger.error( + `category - error occured: couldn't remove category '${req.params.id}'` + ); + res.status(400).send({ + message: `Couldn't remove category '${req.params.id}'`, + status: 'error', + }); } }); } }); - router.route('/v1/categories/:id/products') + router + .route('/v1/categories/:id/products') /** * @api {get} /api/v1/categories/:id/products Get Products by Category * @apiName GetProductsByCategory * @apiGroup Category * @apiPermission token/apikey * @apiVersion 1.0.0 - * + * * @apiParam {String} id Category ID. * * @apiSuccess {Object[Product]} Product Returns a list of products. */ .get((req, res) => { - let products = dbHelper.db.get('products') + let products = dbHelper.db + .get('products') .filter({ category: req.params.id, active: true }) .value(); + const category = dbHelper.db + .get('categories') + .filter({ _id: req.params.id }) + .value() + .shift(); + if (products) { + trackingHelper.apiTrack(req, 'products by category', { + c_n: 'productsByCategory', + c_p: category.name, + c_t: req.originalUrl, + }); res.send(products); } else { - logger.error(`category - error occured: couldn't load your products by category '${req.params.id}'`); - res.status(400).send({ 'message': `Couldn't load your products by category '${req.params.id}'`, 'status': 'error' }); + logger.error( + `category - error occured: couldn't load your products by category '${req.params.id}'` + ); + res.status(400).send({ + message: `Couldn't load your products by category '${req.params.id}'`, + status: 'error', + }); } }); - router.route('/v1/categories/:id/services') - /** - * @api {get} /api/v1/categories/:id/services Get Services by Category - * @apiName GetServicesByCategory - * @apiGroup Category - * @apiPermission token/apikey - * @apiVersion 1.1.0 - * - * @apiParam {String} id Category ID. - * - * @apiSuccess {Object[Service]} Service Returns a list of services. - */ + router + .route('/v1/categories/:id/services') + /** + * @api {get} /api/v1/categories/:id/services Get Services by Category + * @apiName GetServicesByCategory + * @apiGroup Category + * @apiPermission token/apikey + * @apiVersion 1.1.0 + * + * @apiParam {String} id Category ID. + * + * @apiSuccess {Object[Service]} Service Returns a list of services. + */ .get((req, res) => { - let services = dbHelper.db.get('services') + let services = dbHelper.db + .get('services') .filter({ category: req.params.id, active: true }) .value(); + const category = dbHelper.db + .get('categories') + .filter({ _id: req.params.id }) + .value() + .shift(); + if (services) { + trackingHelper.apiTrack(req, 'services by category', { + c_n: 'servicesByCategory', + c_p: category.name, + c_t: req.originalUrl, + }); res.send(services); } else { - logger.error(`error occured: couldn't load your services by category '${req.params.id}'`); - res.status(400).send({ 'message': `Couldn't load your services by category '${req.params.id}'`, 'status': 'error' }); + logger.error( + `error occured: couldn't load your services by category '${req.params.id}'` + ); + res.status(400).send({ + message: `Couldn't load your services by category '${req.params.id}'`, + status: 'error', + }); } }); -}; \ No newline at end of file +}; diff --git a/server/api/routes_data.js b/server/api/routes_data.js index 29cc9d1..bec7cf0 100644 --- a/server/api/routes_data.js +++ b/server/api/routes_data.js @@ -1,17 +1,17 @@ -const fs = require('fs'); - const logger = require('../lib/logging_helper.js').logger; const fileHelper = require('../lib/file_helper.js'); +const trackingHelper = require('../lib/tracking_helper.js'); module.exports = (router) => { - router.route('/data/uploads/:userid/:additionalPath?') + router + .route('/data/uploads/:userid/:additionalPath?') /** * @api {get} /api/data/uploads/:userid/:additionalPath? Get Image List * @apiName GetListImages * @apiGroup Data * @apiPermission token/apikey * @apiVersion 1.0.0 - * + * * @apiParam {String} userid users unique ID. * @apiParam {String} [additionalPath] additonal path to a binary file * @@ -23,29 +23,48 @@ module.exports = (router) => { additionalPath = `/${req.params.additionalPath}`; } // check if there are wanted "autentication" methods for binary data - if ((req.query.userid || req.headers['x-stjorna-userid'])) { - logger.log('debug', `data - try to serve path: ${process.env.STJORNA_SERVER_STORAGE}/uploads/${req.params.userid}${additionalPath}`); - fileHelper.getFolderContent(`${process.env.STJORNA_SERVER_STORAGE}/uploads/${req.params.userid}${additionalPath}`, (err, data) => { - if (!err) { - res.send(data); - } else { - logger.error(`data - error occured: ${err.message}`); - res.status(500).send({ 'message': err.message, 'status': 'error' }); - } + if (req.query.userid || req.headers['x-stjorna-userid']) { + logger.log( + 'debug', + `data - try to serve path: ${process.env.STJORNA_SERVER_STORAGE}/uploads/${req.params.userid}${additionalPath}` + ); + trackingHelper.apiTrack(req, 'get imagelist', { + c_n: 'imagelist', + c_p: additionalPath, }); + fileHelper.getFolderContent( + `${process.env.STJORNA_SERVER_STORAGE}/uploads/${req.params.userid}${additionalPath}`, + (err, data) => { + if (!err) { + res.send(data); + } else { + logger.error( + `data - error occured: ${err.message}` + ); + res.status(500).send({ + message: err.message, + status: 'error', + }); + } + } + ); } else { - res.status(401).send({ 'message': 'no permissions for this ressource.', 'status': 'error' }); + res.status(401).send({ + message: 'no permissions for this ressource.', + status: 'error', + }); } }); - router.route('/data/uploads/:userid/:additionalPath?/:image') + router + .route('/data/uploads/:userid/:additionalPath?/:image') /** * @api {get} /api/data/uploads/:userid/:additionalPath?/:image Get Image * @apiName GetImage * @apiGroup Data * @apiPermission token/apikey * @apiVersion 1.0.0 - * + * * @apiParam {String} userid users unique ID. * @apiParam {String} [additionalPath] additonal path to a binary file * @apiParam {String} image image file name to load @@ -54,19 +73,32 @@ module.exports = (router) => { */ .get((req, res) => { // check if there are wanted "autentication" methods for binary data - if ((req.query.userid || req.headers['x-stjorna-userid'])) { + if (req.query.userid || req.headers['x-stjorna-userid']) { let additionalPath = ''; if (req.params.additionalPath) { additionalPath = `/${req.params.additionalPath}`; } try { - res.sendFile(`${process.env.STJORNA_SERVER_STORAGE}/uploads/${req.params.userid}${additionalPath}/${req.params.image}`); - } catch(err) { + trackingHelper.apiTrack(req, 'get image', { + c_n: 'image', + c_p: req.params.image, + c_t: `${additionalPath}/${req.params.image}`, + }); + res.sendFile( + `${process.env.STJORNA_SERVER_STORAGE}/uploads/${req.params.userid}${additionalPath}/${req.params.image}` + ); + } catch (err) { logger.error(`data - error occured: ${err.message}`); - res.status(500).send({ 'message': err.message, 'status': 'error' }); + res.status(500).send({ + message: err.message, + status: 'error', + }); } } else { - res.status(401).send({ 'message': 'no permissions for this ressource.', 'status': 'error' }); + res.status(401).send({ + message: 'no permissions for this ressource.', + status: 'error', + }); } }); -}; \ No newline at end of file +}; diff --git a/server/api/routes_products.js b/server/api/routes_products.js index f042cd6..63ad7ae 100644 --- a/server/api/routes_products.js +++ b/server/api/routes_products.js @@ -1,9 +1,12 @@ const dbHelper = require('../lib/database_helper.js'); +const trackingHelper = require('../lib/tracking_helper.js'); const logger = require('../lib/logging_helper.js').logger; -const prepareAndSaveImage = require('../lib/image_helper.js').prepareAndSaveImage; +const prepareAndSaveImage = require('../lib/image_helper.js') + .prepareAndSaveImage; module.exports = (router) => { - router.route('/v1/products') + router + .route('/v1/products') /** * @api {get} /api/v1/products Get Product List * @apiName GetProductsList @@ -16,17 +19,27 @@ module.exports = (router) => { .get((req, res) => { let products; if (req.query.apikey || req.headers['x-stjorna-apikey']) { - products = dbHelper.db.get('products').find({ active: true }).value(); + products = dbHelper.db + .get('products') + .find({ active: true }) + .value(); } else { products = dbHelper.db.get('products').value(); } logger.log('debug', `product - load product list`); if (products) { + trackingHelper.apiTrack(req, 'list products', { + c_n: 'products', + c_t: req.originalUrl, + }); res.send(products); } else { logger.error(`error occured: could not load your products`); - res.status(400).send({ 'message': `Couldn't load your products`, 'status': 'error' }); + res.status(400).send({ + message: `Couldn't load your products`, + status: 'error', + }); } }) @@ -47,7 +60,11 @@ module.exports = (router) => { // prepare and save image let imagePath = req.body.imageUrl; if (req.body.image && req.body.image.includes('data:image')) { - imagePath = prepareAndSaveImage(req.body.image, '/products', req.headers['x-stjorna-userid']); + imagePath = prepareAndSaveImage( + req.body.image, + '/products', + req.headers['x-stjorna-userid'] + ); } let newItem = { @@ -62,33 +79,44 @@ module.exports = (router) => { created: new Date().getTime(), createdUser: req.body.createdUser, updated: new Date().getTime(), - updatedUser: null + updatedUser: null, }; - logger.log('debug', `product - add product with ID: ${newItem._id}`); + logger.log( + 'debug', + `product - add product with ID: ${newItem._id}` + ); - dbHelper.db.get('products') + dbHelper.db + .get('products') .push(newItem) .write() .then(() => { - let item = dbHelper.db.get('products').find({ _id: newItem._id }).value(); + let item = dbHelper.db + .get('products') + .find({ _id: newItem._id }) + .value(); if (item) { res.send(item); } else { logger.error(`error occured: could not add product`); - res.status(400).send({ 'message': `Couldn't add product`, 'status': 'error' }); + res.status(400).send({ + message: `Couldn't add product`, + status: 'error', + }); } }); }); - router.route('/v1/products/:id') + router + .route('/v1/products/:id') /** * @api {get} /api/v1/products/:id Get Product * @apiName GetProduct * @apiGroup Product * @apiPermission token/apikey * @apiVersion 1.0.0 - * + * * @apiParam {String} id unique Product ID. * * @apiSuccess {Object} Product Returns a specific Product by ID. @@ -105,13 +133,29 @@ module.exports = (router) => { * @apiSuccess {String} updatedUser UserID which user has updatged the item. */ .get((req, res) => { - let product = dbHelper.db.get('products').find({ _id: req.params.id }).value(); - logger.log('debug', `product - get product with ID: ${req.params.id }`); + let product = dbHelper.db + .get('products') + .find({ _id: req.params.id }) + .value(); + logger.log( + 'debug', + `product - get product with ID: ${req.params.id}` + ); if (product) { + trackingHelper.apiTrack(req, 'get product', { + c_n: 'product', + c_p: product.name, + c_t: req.originalUrl, + }); res.send(product); } else { - logger.error(`error occured: could not load product ${req.params.id}`); - res.status(400).send({ 'message': `Couldn't load product ${req.params.id}`, 'status': 'error' }); + logger.error( + `error occured: could not load product ${req.params.id}` + ); + res.status(400).send({ + message: `Couldn't load product ${req.params.id}`, + status: 'error', + }); } }) /** @@ -121,19 +165,23 @@ module.exports = (router) => { * @apiGroup Product * @apiPermission loggedin * @apiVersion 1.0.0 - * + * * @apiParam {String} id unique Product ID. * * @apiSuccess {Object} Product Returns the updated Product by ID. */ - .post( (req, res) => { + .post((req, res) => { if (req.body.active) { req.body.active = JSON.parse(req.body.active); } // prepare and save image let imagePath = req.body.imageUrl; if (req.body.image && req.body.image.includes('data:image')) { - imagePath = prepareAndSaveImage(req.body.image, '/products', req.headers['x-stjorna-userid']); + imagePath = prepareAndSaveImage( + req.body.image, + '/products', + req.headers['x-stjorna-userid'] + ); } let newItem = { @@ -145,21 +193,33 @@ module.exports = (router) => { image: '', imageUrl: imagePath, updated: new Date().getTime(), - updatedUser: req.body.updatedUser + updatedUser: req.body.updatedUser, }; - logger.log('debug', `product - update product with ID: ${req.params.id}`); - dbHelper.db.get('products') + logger.log( + 'debug', + `product - update product with ID: ${req.params.id}` + ); + dbHelper.db + .get('products') .find({ _id: req.params.id }) .assign(newItem) .write() .then(() => { - let item = dbHelper.db.get('products').find({ _id: req.params.id }).value(); + let item = dbHelper.db + .get('products') + .find({ _id: req.params.id }) + .value(); if (item && item.updated === newItem.updated) { res.send(item); } else { - logger.error(`error occured: could not update product '${req.params.id}'`); - res.status(400).send({ 'message': `Couldn't update product '${req.params.id}'`, 'status': 'error' }); + logger.error( + `error occured: could not update product '${req.params.id}'` + ); + res.status(400).send({ + message: `Couldn't update product '${req.params.id}'`, + status: 'error', + }); } }); }) @@ -170,24 +230,39 @@ module.exports = (router) => { * @apiGroup Product * @apiPermission loggedin * @apiVersion 1.0.0 - * + * * @apiParam {String} id unique Product ID. * * @apiSuccess {Object} Message Returns the status of the deleted Product. */ - .delete( (req, res) => { - logger.log('debug', `product - delete product with ID: ${req.params.id}`); - dbHelper.db.get('products') + .delete((req, res) => { + logger.log( + 'debug', + `product - delete product with ID: ${req.params.id}` + ); + dbHelper.db + .get('products') .remove({ _id: req.params.id }) .write() .then(() => { - let item = dbHelper.db.get('products').find({ _id: req.params.id }).value(); + let item = dbHelper.db + .get('products') + .find({ _id: req.params.id }) + .value(); if (!item) { - res.send({ 'message': 'successfully removed', 'status': 'ok' }); + res.send({ + message: 'successfully removed', + status: 'ok', + }); } else { - logger.error(`error occured: could not remove products '${req.params.id}'`); - res.status(400).send({ 'message': `Couldn't remove products '${req.params.id}'`, 'status': 'error' }); + logger.error( + `error occured: could not remove products '${req.params.id}'` + ); + res.status(400).send({ + message: `Couldn't remove products '${req.params.id}'`, + status: 'error', + }); } }); }); -}; \ No newline at end of file +}; diff --git a/server/api/routes_services.js b/server/api/routes_services.js index 7c663c0..315dd10 100644 --- a/server/api/routes_services.js +++ b/server/api/routes_services.js @@ -1,9 +1,12 @@ const dbHelper = require('../lib/database_helper.js'); +const trackingHelper = require('../lib/tracking_helper.js'); const logger = require('../lib/logging_helper.js').logger; -const prepareAndSaveImage = require('../lib/image_helper.js').prepareAndSaveImage; +const prepareAndSaveImage = require('../lib/image_helper.js') + .prepareAndSaveImage; module.exports = (router) => { - router.route('/v1/services') + router + .route('/v1/services') /** * @api {get} /api/v1/services Get Service List * @apiName GetServicesList @@ -16,17 +19,27 @@ module.exports = (router) => { .get((req, res) => { let services; if (req.query.apikey || req.headers['x-stjorna-apikey']) { - services = dbHelper.db.get('services').find({ active: true }).value(); + services = dbHelper.db + .get('services') + .find({ active: true }) + .value(); } else { services = dbHelper.db.get('services').value(); } logger.log('debug', `service - load service list`); if (services) { + trackingHelper.apiTrack(req, 'list services', { + c_n: 'services', + c_t: req.originalUrl, + }); res.send(services); } else { logger.error(`error occured: couldn't load your services`); - res.status(400).send({ 'message': `Couldn't load your services`, 'status': 'error' }); + res.status(400).send({ + message: `Couldn't load your services`, + status: 'error', + }); } }) @@ -47,7 +60,11 @@ module.exports = (router) => { // prepare and save image let imagePath = req.body.imageUrl; if (req.body.image && req.body.image.includes('data:image')) { - imagePath = prepareAndSaveImage(req.body.image, '/services', req.headers['x-stjorna-userid']); + imagePath = prepareAndSaveImage( + req.body.image, + '/services', + req.headers['x-stjorna-userid'] + ); } let service = { @@ -62,53 +79,74 @@ module.exports = (router) => { created: new Date().getTime(), createdUser: req.body.createdUser, updated: new Date().getTime(), - updatedUser: null + updatedUser: null, }; - dbHelper.db.get('services') + dbHelper.db + .get('services') .push(service) .write() .then(() => { - let item = dbHelper.db.get('services').find({ _id: service._id }).value(); + let item = dbHelper.db + .get('services') + .find({ _id: service._id }) + .value(); if (item) { res.send(item); } else { logger.error(`error occured: couldn't add service`); - res.status(400).send({ 'message': `Couldn't add service`, 'status': 'error' }); + res.status(400).send({ + message: `Couldn't add service`, + status: 'error', + }); } }); }); - router.route('/v1/services/:id') - /** - * @api {get} /api/v1/services/:id Get service - * @apiName GetService - * @apiGroup Service - * @apiPermission token/apikey - * @apiVersion 1.1.0 - * - * @apiParam {String} id unique Service ID. - * - * @apiSuccess {Object} Service Returns a specific service by ID. - * @apiSuccess {String} _id service unique ID - * @apiSuccess {String} name Service name - * @apiSuccess {String} description Service description (larger text) - * @apiSuccess {String} category Category unique ID - * @apiSuccess {Boolean} active Is the Service active over the remote api. - * @apiSuccess {String} image Base64 image string, normally empty. - * @apiSuccess {String} imageUrl Image url, when an image is uploaded. - * @apiSuccess {Number} created Timestamp when the item was created. - * @apiSuccess {String} createdUser UserID which user has created the item. - * @apiSuccess {Number} updated Timestamp when the item was updated. - * @apiSuccess {String} updatedUser UserID which user has updatged the item. - */ + router + .route('/v1/services/:id') + /** + * @api {get} /api/v1/services/:id Get service + * @apiName GetService + * @apiGroup Service + * @apiPermission token/apikey + * @apiVersion 1.1.0 + * + * @apiParam {String} id unique Service ID. + * + * @apiSuccess {Object} Service Returns a specific service by ID. + * @apiSuccess {String} _id service unique ID + * @apiSuccess {String} name Service name + * @apiSuccess {String} description Service description (larger text) + * @apiSuccess {String} category Category unique ID + * @apiSuccess {Boolean} active Is the Service active over the remote api. + * @apiSuccess {String} image Base64 image string, normally empty. + * @apiSuccess {String} imageUrl Image url, when an image is uploaded. + * @apiSuccess {Number} created Timestamp when the item was created. + * @apiSuccess {String} createdUser UserID which user has created the item. + * @apiSuccess {Number} updated Timestamp when the item was updated. + * @apiSuccess {String} updatedUser UserID which user has updatged the item. + */ .get((req, res) => { - let service = dbHelper.db.get('services').find({ _id: req.params.id }).value(); + let service = dbHelper.db + .get('services') + .find({ _id: req.params.id }) + .value(); if (service) { + trackingHelper.apiTrack(req, 'get service', { + c_n: 'service', + c_p: service.name, + c_t: req.originalUrl, + }); res.send(service); } else { - logger.error(`error occured: couldn't load service ${req.params.id}`); - res.status(400).send({ 'message': `Couldn't load service ${req.params.id}`, 'status': 'error' }); + logger.error( + `error occured: couldn't load service ${req.params.id}` + ); + res.status(400).send({ + message: `Couldn't load service ${req.params.id}`, + status: 'error', + }); } }) /** @@ -123,14 +161,18 @@ module.exports = (router) => { * * @apiSuccess {Object} Service Returns the updated Service by ID. */ - .post( (req, res) => { + .post((req, res) => { if (req.body.active) { req.body.active = JSON.parse(req.body.active); } // prepare and save image let imagePath = req.body.imageUrl; if (req.body.image && req.body.image.includes('data:image')) { - imagePath = prepareAndSaveImage(req.body.image, '/services', req.headers['x-stjorna-userid']); + imagePath = prepareAndSaveImage( + req.body.image, + '/services', + req.headers['x-stjorna-userid'] + ); } let newItem = { @@ -142,20 +184,29 @@ module.exports = (router) => { image: '', imageUrl: imagePath, updated: new Date().getTime(), - updatedUser: req.body.updatedUser + updatedUser: req.body.updatedUser, }; - dbHelper.db.get('services') + dbHelper.db + .get('services') .find({ _id: req.params.id }) .assign(newItem) .write() .then(() => { - let item = dbHelper.db.get('services').find({ _id: req.params.id }).value(); + let item = dbHelper.db + .get('services') + .find({ _id: req.params.id }) + .value(); if (item && item.updated === newItem.updated) { res.send(item); } else { - logger.error(`error occured: couldn't update service '${req.params.id}'`); - res.status(400).send({ 'message': `Couldn't update service '${req.params.id}'`, 'status': 'error' }); + logger.error( + `error occured: couldn't update service '${req.params.id}'` + ); + res.status(400).send({ + message: `Couldn't update service '${req.params.id}'`, + status: 'error', + }); } }); }) @@ -171,18 +222,30 @@ module.exports = (router) => { * * @apiSuccess {Object} Message Returns the status of the deleted Service. */ - .delete( (req, res) => { - dbHelper.db.get('services') + .delete((req, res) => { + dbHelper.db + .get('services') .remove({ _id: req.params.id }) .write() .then(() => { - let item = dbHelper.db.get('services').find({ _id: req.params.id }).value(); + let item = dbHelper.db + .get('services') + .find({ _id: req.params.id }) + .value(); if (!item) { - res.send({ 'message': 'successfully removed', 'status': 'ok' }); + res.send({ + message: 'successfully removed', + status: 'ok', + }); } else { - logger.error(`error occured: couldn't remove service '${req.params.id}'`); - res.status(400).send({ 'message': `Couldn't remove service '${req.params.id}'`, 'status': 'error' }); + logger.error( + `error occured: couldn't remove service '${req.params.id}'` + ); + res.status(400).send({ + message: `Couldn't remove service '${req.params.id}'`, + status: 'error', + }); } }); }); -}; \ No newline at end of file +}; diff --git a/server/lib/database_helper.js b/server/lib/database_helper.js index 6e6af5d..233a8be 100644 --- a/server/lib/database_helper.js +++ b/server/lib/database_helper.js @@ -62,12 +62,12 @@ function initialize() { // set database defaults and print size of dataset module.exports.db.defaults(datasets).write().then(() => { getAllDataSetMembers().forEach((constellation) => { - logger.info(`constellation.${constellation} records: ${getSizeOfDataSet(constellation)}`); + logger.info(`dataset constellation.${constellation} records: ${getSizeOfDataSet(constellation)}`); }); }); }); break; - + default: logger.error(`could not initialize database. unkown database type set: ${process.env.STJORNA_DATABASE_TYPE}`); break; diff --git a/server/lib/tracking_helper.js b/server/lib/tracking_helper.js new file mode 100644 index 0000000..f6aa79f --- /dev/null +++ b/server/lib/tracking_helper.js @@ -0,0 +1,62 @@ +const PiwikTracker = require('piwik-tracker'); +const logger = require('../lib/logging_helper.js').logger; + +let matomo; + +function initialize() { + const matomoPageId = process.env.STJORNA_MATOMOID; + const matomoUrl = process.env.STJORNA_MATOMOURL; + const matomoToken = process.env.STJORNA_MATOMOTOKEN; + + if (matomoPageId && matomoUrl) { + logger.info(`setup matomo tracking to instance: ${matomoUrl}`); + logger.info(`setup matomo tracking for page: ${matomoPageId}`); + if (matomoToken) { + logger.info(`matomo token was set, enabled IP tracking`); + } else { + logger.warn('no matomo token configured, skip IP tracking'); + } + matomo = new PiwikTracker(matomoPageId, matomoUrl); + } else { + logger.warn( + 'no matomo instance configured, skip api tracking setup' + ); + } +} + +function getRemoteAddr(req) { + if (req.ip) return req.ip; + if (req._remoteAddress) return req._remoteAddress; + var sock = req.socket; + if (sock.socket) return sock.socket.remoteAddress; + return sock.remoteAddress; +} + +function apiTrack(req, action, contentInfo) { + const matomoToken = process.env.STJORNA_MATOMOTOKEN; + let trackObj = { + url: `${req.header.origin}${req.baseUrl}${req.url}`, + action_name: action, + ua: req.header('User-Agent'), + lang: req.header('Accept-Language'), + c_n: contentInfo.c_n, + c_p: contentInfo.c_p, + c_t: contentInfo.c_t, + cvar: JSON.stringify({ + '1': ['API version', 'v1'], + '2': ['HTTP method', req.method], + }), + }; + + if (matomoToken) { + trackObj['token_auth'] = matomoToken; + trackObj['cip'] = getRemoteAddr(req); + } + + matomo.track(trackObj); +} + +module.exports = { + initialize, + apiTrack, +}; diff --git a/server/migration/migration.js b/server/migration/migration.js index c71d456..e425e4e 100644 --- a/server/migration/migration.js +++ b/server/migration/migration.js @@ -8,22 +8,22 @@ function executeMigrations() { try { const configFile = fs.readFileSync(`${process.env.STJORNA_SERVER_STORAGE}/config.json`); if (JSON.parse(configFile).installed) { - logger.info(`>> execute migration for folder: ${directoryPath}`); + logger.info(`execute migration for folder: ${directoryPath}`); const files = fs.readdirSync(directoryPath); files .filter(file => file !== 'migration.js' && file.endsWith('.js')) .some(file => { - logger.info(` >> execute migration: ${file.replace(/(\.[a-zA-Z0-9]+)/, '')}`); + logger.debug(` execute migration: ${file.replace(/(\.[a-zA-Z0-9]+)/, '')}`); require(path.join(__dirname + `/${file}`)); - }); + }); } else { - logger.warn('>> can not execute migration, STJORNA is not yet installed'); + logger.warn('can not execute migration, STJORNA is not yet installed'); } } catch(ex) { - logger.debug('>> can not execute migration, STJORNA is not yet installed'); + logger.debug('can not execute migration, STJORNA is not yet installed'); } } module.exports = { executeMigrations -}; \ No newline at end of file +}; diff --git a/server/package-lock.json b/server/package-lock.json index e94bea9..8b39ace 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -5068,6 +5068,11 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" }, + "piwik-tracker": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/piwik-tracker/-/piwik-tracker-1.1.2.tgz", + "integrity": "sha512-YKLrl7ThpTeGiWSPVcDjWO51FSvick91Edvb7kLWpBYKkKNA6PrhfdyhuU2eZVuiHhnDJcGM0ccnbxOCoOyWeA==" + }, "pixelmatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", diff --git a/server/package.json b/server/package.json index da2a58c..302b37b 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "stjorna", - "version": "2.4.4", + "version": "2.5.0", "description": "STJÓRNA product management", "main": "server.js", "scripts": { @@ -29,10 +29,11 @@ "jimp": "^0.12.1", "jsonwebtoken": "^8.5.1", "lowdb": "^1.0.0", + "piwik-tracker": "^1.1.2", + "snyk": "^1.330.0", "uniqid": "^5.2.0", "util": "^0.12.3", - "winston": "^3.2.1", - "snyk": "^1.330.0" + "winston": "^3.2.1" }, "devDependencies": { "apidoc": "^0.22.1", diff --git a/server/server.js b/server/server.js index e8c3bd0..2884efb 100644 --- a/server/server.js +++ b/server/server.js @@ -14,6 +14,9 @@ const logger = require('./lib/logging_helper.js'); // initialize databases require('./lib/database_helper').initialize(); +// initialize tracking +require('./lib/tracking_helper').initialize(); + // initialize cron jobs require('./cronjobs/cleanup_uploads')(); @@ -52,9 +55,9 @@ migration.executeMigrations(); // start application module.exports = app.listen(process.env.STJORNA_SERVER_PORT); -logger.logger.info(`>> app : ${appInfo.name}:${appInfo.version}`); -logger.logger.info(`>> port : ${process.env.STJORNA_SERVER_PORT}`); +logger.logger.info(`app : ${appInfo.name}:${appInfo.version}`); +logger.logger.info(`port : ${process.env.STJORNA_SERVER_PORT}`); // print configuration -logger.logger.info(`>> production mode enabled: ${stjornaEnv.isProduction()}`); -logger.logger.info(`>> configuration from ${process.env.STJORNA_SERVER_STORAGE}/config.json`); -logger.logger.info(`>> cleanup interval ${process.env.STJORNA_CRON_CLEANUP_INTERVAL}`); +logger.logger.info(`production mode enabled: ${stjornaEnv.isProduction()}`); +logger.logger.info(`configuration from ${process.env.STJORNA_SERVER_STORAGE}/config.json`); +logger.logger.info(`cleanup interval ${process.env.STJORNA_CRON_CLEANUP_INTERVAL}`); diff --git a/server/test/api/data.spec.js b/server/test/api/data.spec.js index 0892e56..aa029ee 100644 --- a/server/test/api/data.spec.js +++ b/server/test/api/data.spec.js @@ -38,7 +38,8 @@ const service = { active: true, image: '', imageUrl: '', - createdUser: user.username + createdUser: user.username, + updatedUser: user.username, }; const service_2 = { @@ -151,7 +152,7 @@ describe('Products/Services/Categories', () => { // create, alter, get, delete service it('crud service', (done) => { - let productId; + let serviceId; chai.request(testHelper.getServer()) .put(`${apiUrl}/services`) .send(product) @@ -167,28 +168,28 @@ describe('Products/Services/Categories', () => { res.body.should.have.property('updatedUser').eql(null); res.body.should.have.property('created'); res.body.should.have.property('updated'); - productId = res.body._id; + serviceId = res.body._id; chai.request(testHelper.getServer()) - .post(`${apiUrl}/services/${productId}`) - .send(product_2) + .post(`${apiUrl}/services/${serviceId}`) + .send(service) .end((err, res) => { res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('_id').eql(productId); + res.body.should.have.property('_id').eql(serviceId); chai.request(testHelper.getServer()) - .get(`${apiUrl}/services/${productId}`) + .get(`${apiUrl}/services/${serviceId}`) .end((err, res) => { - res.body.should.have.property('name').eql(product_2.name); - res.body.should.have.property('price').eql(product_2.price); - res.body.should.have.property('category').eql(product_2.category); - res.body.should.have.property('description').eql(product_2.description); - res.body.should.have.property('active').eql(product_2.active); + res.body.should.have.property('name').eql(service.name); + res.body.should.have.property('price').eql(service.price); + res.body.should.have.property('category').eql(service.category); + res.body.should.have.property('description').eql(service.description); + res.body.should.have.property('active').eql(service.active); res.body.should.have.property('createdUser').eql(user.username); res.body.should.have.property('updatedUser').eql(user.username) res.body.should.have.property('created'); res.body.should.have.property('updated'); chai.request(testHelper.getServer()) - .delete(`${apiUrl}/services/${productId}`) + .delete(`${apiUrl}/services/${serviceId}`) .end((err, res) => { res.should.have.status(200); res.body.should.be.a('object');