├── .nvmrc ├── backend ├── lib │ ├── client │ │ └── search.js │ ├── team │ │ ├── search.js │ │ └── index.js │ ├── i18n │ │ └── index.js │ ├── cns │ │ ├── index.js │ │ ├── cns.model.js │ │ └── helpers │ │ │ ├── duration.js │ │ │ └── ticket.js │ ├── db │ │ ├── contribution.js │ │ ├── ticketing-user.js │ │ ├── counter.js │ │ ├── contract.js │ │ ├── ticketing-user-role.js │ │ ├── ticketing-user-contract.js │ │ ├── ticketing-glossary.js │ │ ├── organization.js │ │ ├── schemas │ │ │ ├── ticketingUser.js │ │ │ └── contribution.js │ │ ├── custom-filter.js │ │ ├── index.js │ │ ├── client.js │ │ ├── team.js │ │ └── software.js │ ├── listeners │ │ ├── user │ │ │ ├── index.js │ │ │ ├── listener.js │ │ │ └── denormalize.js │ │ ├── contract │ │ │ ├── index.js │ │ │ ├── listener.js │ │ │ └── denormalize.js │ │ ├── software │ │ │ ├── index.js │ │ │ ├── denormalize.js │ │ │ └── listener.js │ │ ├── ticket │ │ │ ├── index.js │ │ │ └── listener.js │ │ ├── organization │ │ │ ├── index.js │ │ │ ├── listener.js │ │ │ └── denormalize.js │ │ └── index.js │ ├── filter │ │ ├── helpers.js │ │ └── index.js │ ├── helpers.js │ ├── access-control │ │ ├── policies.js │ │ └── index.js │ ├── ticket │ │ └── search.js │ ├── index.js │ ├── ticketing-glossary.js │ ├── config │ │ └── index.js │ ├── contract │ │ └── search.js │ ├── limesurvey │ │ └── limesurvey.js │ ├── ticketing-user.js │ ├── software │ │ └── search.js │ ├── user │ │ └── search.js │ └── organization │ │ └── search.js ├── templates │ └── email │ │ ├── user.created │ │ └── attachments │ │ ├── contract.expired │ │ ├── attachments │ │ └── html.pug │ │ ├── ticket.created │ │ └── attachments │ │ ├── ticket.updated │ │ └── attachments │ │ ├── contract.creditconsumed │ │ ├── attachments │ │ └── html.pug │ │ ├── attachments │ │ ├── github.png │ │ ├── logo.png │ │ ├── facebook.png │ │ ├── twitter.png │ │ ├── youtube.png │ │ ├── instagram.png │ │ ├── newaccount.png │ │ ├── Linagora-logo.png │ │ ├── logo-08000linux.png │ │ ├── contractexpiration.png │ │ └── contractcreditconsumed.png │ │ └── includes │ │ └── attachments.pug ├── webserver │ ├── api │ │ ├── dashboard │ │ │ ├── controller.js │ │ │ └── index.js │ │ ├── filter │ │ │ ├── controller.js │ │ │ └── index.js │ │ ├── glossary │ │ │ ├── index.js │ │ │ ├── controller.js │ │ │ └── middleware.js │ │ ├── lininfosec │ │ │ └── index.js │ │ ├── constants.js │ │ ├── role │ │ │ ├── index.js │ │ │ ├── middleware.js │ │ │ └── controller.js │ │ ├── index.js │ │ ├── custom-filter │ │ │ └── middleware.js │ │ ├── contract │ │ │ └── middleware │ │ │ │ ├── permission.js │ │ │ │ └── demand.js │ │ ├── organization │ │ │ ├── index.js │ │ │ └── middleware.js │ │ ├── team │ │ │ ├── middleware.js │ │ │ └── controller.js │ │ ├── utils.js │ │ ├── client │ │ │ ├── middleware.js │ │ │ └── controller.js │ │ ├── contribution │ │ │ └── middleware.js │ │ ├── software │ │ │ └── middleware.js │ │ └── helpers.js │ └── application.js └── ws │ └── index.js ├── .gitignore ├── test ├── config │ ├── db.json │ ├── mocks │ │ ├── injector.js │ │ ├── ng-mock-component.js │ │ └── module.js │ ├── default.json │ ├── servers-conf.js │ └── karma.conf.js ├── unit-backend │ ├── fixtures │ │ ├── errors.js │ │ └── logger-noop.js │ ├── all.js │ └── lib │ │ ├── user.js │ │ └── filter.js ├── midway-backend │ ├── fixtures │ │ ├── logger.js │ │ └── deployments.js │ └── webserver │ │ └── api │ │ ├── user │ │ └── get-role.js │ │ └── software │ │ └── get-by-name.js └── unit-storage │ ├── fixtures │ └── logger.js │ ├── lib │ └── db │ │ ├── contribution.js │ │ ├── team.js │ │ ├── ticketing-user-role.js │ │ ├── contract.js │ │ └── software.js │ └── all.js ├── bin ├── .eslintrc.js ├── commands │ ├── elasticsearch.js │ ├── role.js │ └── elasticsearch_cmds │ │ └── setup.js ├── cli.js └── lib │ ├── contract.js │ ├── software.js │ ├── utils.js │ ├── organization.js │ ├── index.js │ ├── db.js │ ├── ticketing-user-role.js │ ├── commons.js │ └── user.js ├── config ├── db.json ├── esn │ └── default.production.json └── elasticsearch │ ├── software.json │ ├── organizations.json │ └── contracts.json ├── doc ├── REST_API │ └── swagger │ │ ├── parameters │ │ ├── team.js │ │ ├── user.js │ │ ├── client.js │ │ ├── contract.js │ │ ├── custom-filter.js │ │ ├── software.js │ │ ├── ticket.js │ │ └── contribution.js │ │ ├── definitions │ │ ├── filter.js │ │ ├── client.js │ │ ├── users.js │ │ ├── custom-filter.js │ │ ├── team.js │ │ ├── software.js │ │ ├── contribution.js │ │ └── ticket.js │ │ └── responses │ │ ├── filter.js │ │ ├── client.js │ │ ├── team.js │ │ ├── custom-filter.js │ │ └── software.js ├── cli.md └── dev.md ├── Dockerfile ├── .eslintrc.json ├── tasks ├── linters.js └── tests.js ├── package.json └── index.js /.nvmrc: -------------------------------------------------------------------------------- 1 | 8 -------------------------------------------------------------------------------- /backend/lib/client/search.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/lib/team/search.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/templates/email/user.created/attachments: -------------------------------------------------------------------------------- 1 | ../attachments -------------------------------------------------------------------------------- /backend/templates/email/contract.expired/attachments: -------------------------------------------------------------------------------- 1 | ../attachments -------------------------------------------------------------------------------- /backend/templates/email/ticket.created/attachments: -------------------------------------------------------------------------------- 1 | ../attachments -------------------------------------------------------------------------------- /backend/templates/email/ticket.updated/attachments: -------------------------------------------------------------------------------- 1 | ../attachments -------------------------------------------------------------------------------- /backend/templates/email/contract.creditconsumed/attachments: -------------------------------------------------------------------------------- 1 | ../attachments -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | *.log 4 | tmp 5 | .vscode 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /backend/templates/email/attachments/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartSLA/smartsla-backend/HEAD/backend/templates/email/attachments/github.png -------------------------------------------------------------------------------- /backend/templates/email/attachments/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartSLA/smartsla-backend/HEAD/backend/templates/email/attachments/logo.png -------------------------------------------------------------------------------- /test/config/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "connectionString": "mongodb://mongo/tests", 3 | "connectionOptions": { 4 | "auto_reconnect": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /backend/templates/email/attachments/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartSLA/smartsla-backend/HEAD/backend/templates/email/attachments/facebook.png -------------------------------------------------------------------------------- /backend/templates/email/attachments/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartSLA/smartsla-backend/HEAD/backend/templates/email/attachments/twitter.png -------------------------------------------------------------------------------- /backend/templates/email/attachments/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartSLA/smartsla-backend/HEAD/backend/templates/email/attachments/youtube.png -------------------------------------------------------------------------------- /backend/templates/email/attachments/instagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartSLA/smartsla-backend/HEAD/backend/templates/email/attachments/instagram.png -------------------------------------------------------------------------------- /backend/templates/email/attachments/newaccount.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartSLA/smartsla-backend/HEAD/backend/templates/email/attachments/newaccount.png -------------------------------------------------------------------------------- /test/unit-backend/fixtures/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | send(res, code, error) { 5 | res.json(code, error); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /backend/templates/email/attachments/Linagora-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartSLA/smartsla-backend/HEAD/backend/templates/email/attachments/Linagora-logo.png -------------------------------------------------------------------------------- /backend/templates/email/attachments/logo-08000linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartSLA/smartsla-backend/HEAD/backend/templates/email/attachments/logo-08000linux.png -------------------------------------------------------------------------------- /backend/templates/email/attachments/contractexpiration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartSLA/smartsla-backend/HEAD/backend/templates/email/attachments/contractexpiration.png -------------------------------------------------------------------------------- /backend/templates/email/attachments/contractcreditconsumed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartSLA/smartsla-backend/HEAD/backend/templates/email/attachments/contractcreditconsumed.png -------------------------------------------------------------------------------- /bin/.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | rules: { 5 | 'no-process-env': 0, 6 | 'arrow-body-style': 0, 7 | 'no-console': 0 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /test/midway-backend/fixtures/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const log = console.warn; 4 | 5 | module.exports = { 6 | log: log, 7 | warn: log, 8 | error: log, 9 | debug: log, 10 | info: log 11 | }; 12 | -------------------------------------------------------------------------------- /test/unit-storage/fixtures/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const log = console.warn; 4 | 5 | module.exports = { 6 | log: log, 7 | warn: log, 8 | error: log, 9 | debug: log, 10 | info: log 11 | }; 12 | -------------------------------------------------------------------------------- /config/db.json: -------------------------------------------------------------------------------- 1 | {"connectionOptions":{"db":{"w":1,"fsync":true,"native_parser":true},"server":{"socketOptions":{"keepAlive":10000,"connectTimeoutMS":10000},"auto_reconnect":true,"poolSize":10}},"connectionString":"mongodb://localhost:27017/esn"} 2 | -------------------------------------------------------------------------------- /backend/lib/i18n/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(dependencies) { 4 | const i18n = dependencies('i18n'); 5 | 6 | i18n.setDefaultConfiguration({ directory: __dirname + '/locales' }); 7 | 8 | return i18n; 9 | }; 10 | -------------------------------------------------------------------------------- /backend/lib/cns/index.js: -------------------------------------------------------------------------------- 1 | module.exports = dependencies => { 2 | const { computeCns } = require('./cns'); 3 | const { exportData } = require('./export-csv')(dependencies); 4 | 5 | return { 6 | computeCns, 7 | exportData 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/parameters/team.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * parameter: 4 | * team_id: 5 | * name: id 6 | * in: path 7 | * description: unique identifier respresenting the team 8 | * type: string 9 | * required: true 10 | */ 11 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/parameters/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * parameter: 4 | * user_id: 5 | * name: id 6 | * in: path 7 | * description: unique identifier respresenting the users 8 | * type: string 9 | * required: true 10 | */ 11 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/parameters/client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * parameter: 4 | * client_id: 5 | * name: id 6 | * in: path 7 | * description: unique identifier respresenting client 8 | * type: string 9 | * required: true 10 | */ 11 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/parameters/contract.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * parameter: 4 | * contract_id: 5 | * name: id 6 | * in: path 7 | * description: unique identifier respresenting contract 8 | * type: string 9 | * required: true 10 | */ 11 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/parameters/custom-filter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * parameter: 4 | * filter_id: 5 | * name: id 6 | * in: path 7 | * description: unique identifier respresenting the filter 8 | * type: string 9 | * required: true 10 | */ 11 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/parameters/software.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * parameter: 4 | * software_id: 5 | * name: id 6 | * in: path 7 | * description: unique identifier respresenting the software 8 | * type: string 9 | * required: true 10 | */ 11 | -------------------------------------------------------------------------------- /bin/commands/elasticsearch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | command: 'elasticsearch', 5 | desc: 'Elasticsearch Management', 6 | builder: yargs => yargs.commandDir('elasticsearch_cmds').demandCommand(1, 'Please specify a command'), 7 | handler: () => {} 8 | }; 9 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/parameters/ticket.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * parameter: 4 | * ticket_number: 5 | * name: id 6 | * in: path 7 | * description: sequential number respresenting ticket identifier 8 | * type: number 9 | * required: true 10 | */ 11 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/definitions/filter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * definition: 4 | * filter: 5 | * type: Object 6 | * description: filter object 7 | * properties: 8 | * _id: 9 | * type: string 10 | * name: 11 | * type: string 12 | */ 13 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/parameters/contribution.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * parameter: 4 | * contribution_id: 5 | * name: id 6 | * in: path 7 | * description: unique identifier respresenting the contribution 8 | * type: string 9 | * required: true 10 | */ 11 | -------------------------------------------------------------------------------- /backend/lib/db/contribution.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const mongoose = dependencies('db').mongo.mongoose; 5 | const ContributionSchema = require('./schemas/contribution')(dependencies); 6 | 7 | return mongoose.model('Contribution', ContributionSchema); 8 | }; 9 | -------------------------------------------------------------------------------- /backend/lib/db/ticketing-user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const mongoose = dependencies('db').mongo.mongoose; 5 | const TicketingUserSchema = require('./schemas/ticketingUser')(dependencies); 6 | 7 | return mongoose.model('TicketingUser', TicketingUserSchema); 8 | }; 9 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('yargs') 4 | .usage('Usage: $0 [options]') 5 | .commandDir('./commands') 6 | .demand(1, 'You need to specify a command') 7 | .alias('help', 'h') 8 | .help() 9 | .version() 10 | .epilogue('for more information, go to https://open-paas.org') 11 | .argv; 12 | -------------------------------------------------------------------------------- /backend/webserver/api/dashboard/controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(dependencies, lib) { 4 | return { 5 | get 6 | }; 7 | 8 | function get(req, res) { 9 | lib.dashboard.processDashboardQuery(req) 10 | .then(results => { 11 | res.status(200).json(results); 12 | }); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /test/unit-backend/fixtures/logger-noop.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const noop = function() {}; 4 | 5 | /** 6 | * 7 | * @return {{log: Function, warn: Function, error: Function, debug: Function, info: Function}} 8 | */ 9 | module.exports = { 10 | log: noop, 11 | warn: noop, 12 | error: noop, 13 | debug: noop, 14 | info: noop 15 | }; 16 | -------------------------------------------------------------------------------- /backend/lib/db/counter.js: -------------------------------------------------------------------------------- 1 | module.exports = dependencies => { 2 | const mongoose = dependencies('db').mongo.mongoose; 3 | 4 | const CounterSchema = new mongoose.Schema({ 5 | _id: { type: String, required: true, unique: true }, 6 | seq: { type: Number, required: true } 7 | }); 8 | 9 | return mongoose.model('Counter', CounterSchema); 10 | }; 11 | -------------------------------------------------------------------------------- /bin/lib/contract.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { dependencies } = require('./utils'); 4 | 5 | require('../../backend/lib/db/contract')(dependencies); 6 | const contractLibModule = require('../../backend/lib/contract')(dependencies); 7 | 8 | module.exports = { 9 | listByCursor 10 | }; 11 | 12 | function listByCursor() { 13 | return contractLibModule.listByCursor(); 14 | } 15 | -------------------------------------------------------------------------------- /bin/lib/software.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { dependencies } = require('./utils'); 4 | 5 | require('../../backend/lib/db/software')(dependencies); 6 | const softwareLibModule = require('../../backend/lib/software')(dependencies); 7 | 8 | module.exports = { 9 | listByCursor 10 | }; 11 | 12 | function listByCursor() { 13 | return softwareLibModule.listByCursor(); 14 | } 15 | -------------------------------------------------------------------------------- /bin/lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mongoose = require('mongoose'); 4 | const deps = { 5 | db: { 6 | mongo: { 7 | mongoose 8 | } 9 | }, 10 | pubsub: { 11 | local: { 12 | topic: () => {} 13 | } 14 | } 15 | }; 16 | 17 | module.exports = { 18 | dependencies 19 | }; 20 | 21 | function dependencies(name) { 22 | return deps[name]; 23 | } 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM docker-registry.linagora.com:5000/openpaas-releases/openpaas-esn:1.6.6 3 | COPY package.json index.js /var/www/node_modules/smartsla-backend/ 4 | COPY backend/ /var/www/node_modules/smartsla-backend/backend/ 5 | COPY config/esn/default.production.json /var/www/config/default.production.json 6 | RUN cd /var/www/node_modules/smartsla-backend && npm install --production 7 | -------------------------------------------------------------------------------- /bin/lib/organization.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { dependencies } = require('./utils'); 4 | 5 | require('../../backend/lib/db/organization')(dependencies); 6 | const organizationLibModule = require('../../backend/lib/organization')(dependencies); 7 | 8 | module.exports = { 9 | listByCursor 10 | }; 11 | 12 | function listByCursor() { 13 | return organizationLibModule.listByCursor(); 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "linagora-esn", 3 | "rules": { 4 | "consistent-this": ["warn", "self"], 5 | "no-unused-vars": ["error", { "vars": "all", "args": "after-used" }], 6 | "no-console": ["error", { "allow": ["warn", "error"] }], 7 | "no-process-env": "off", 8 | "arrow-parens": ["error", "as-needed"] 9 | }, 10 | "parserOptions": { 11 | "ecmaVersion": 2017 12 | } 13 | } -------------------------------------------------------------------------------- /backend/lib/listeners/user/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const logger = dependencies('logger'); 5 | const listener = require('./listener')(dependencies); 6 | 7 | return { 8 | registerListener 9 | }; 10 | 11 | function registerListener() { 12 | logger.info('Subscribing to user update for indexing'); 13 | listener.register(); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/lib/listeners/contract/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const logger = dependencies('logger'); 5 | const listener = require('./listener')(dependencies); 6 | 7 | return { 8 | registerListener 9 | }; 10 | 11 | function registerListener() { 12 | logger.info('Subscribing to contract update for indexing'); 13 | listener.register(); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/lib/listeners/software/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const logger = dependencies('logger'); 5 | const listener = require('./listener')(dependencies); 6 | 7 | return { 8 | registerListener 9 | }; 10 | 11 | function registerListener() { 12 | logger.info('Subscribing to software update for indexing'); 13 | listener.register(); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/lib/listeners/ticket/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const logger = dependencies('logger'); 5 | const listener = require('./listener')(dependencies); 6 | 7 | return { 8 | registerListener 9 | }; 10 | 11 | function registerListener() { 12 | logger.info('Subscribing to ticket event for activity streams'); 13 | listener.register(); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/webserver/api/filter/controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(dependencies, lib) { 4 | return { 5 | list 6 | }; 7 | 8 | /** 9 | * List filters 10 | * @param {Request} req 11 | * @param {Response} res 12 | */ 13 | function list(req, res) { 14 | lib.filter.list(req) 15 | .then(filters => { 16 | res.status(200).json(filters); 17 | }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /backend/lib/listeners/organization/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const logger = dependencies('logger'); 5 | const listener = require('./listener')(dependencies); 6 | 7 | return { 8 | registerListener 9 | }; 10 | 11 | function registerListener() { 12 | logger.info('Subscribing to organizations updates for indexing'); 13 | listener.register(); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/lib/db/contract.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const mongoose = dependencies('db').mongo.mongoose; 5 | const TicketingUserContractModel = mongoose.model('TicketingUserContract'); 6 | const { ContractSchema } = require('./schemas/contract')(dependencies); 7 | 8 | ContractSchema.post('findOneAndRemove', function(contract) { 9 | TicketingUserContractModel.remove({ contract: contract._id }).exec(); 10 | }); 11 | 12 | return mongoose.model('Contract', ContractSchema); 13 | }; 14 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/responses/filter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * response: 4 | * filters: 5 | * description: OK with filters list 6 | * schema: 7 | * type: array 8 | * items: 9 | * $ref: "#/definitions/filter" 10 | * exemples: 11 | * application/json: 12 | * [ 13 | * { 14 | * "_id": "closed", 15 | * "name": "Closed tickets" 16 | * }, 17 | * { 18 | * "_id": "open", 19 | * "name": "Open tickets" 20 | * } 21 | * ] 22 | */ 23 | -------------------------------------------------------------------------------- /bin/lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const commons = require('./commons'); 4 | const db = require('./db'); 5 | const organization = require('./organization'); 6 | const software = require('./software'); 7 | const ticketingUserRole = require('./ticketing-user-role'); 8 | const ticketingUser = require('./ticketing-user'); 9 | const contract = require('./contract'); 10 | const utils = require('./utils'); 11 | 12 | module.exports = { 13 | contract, 14 | commons, 15 | db, 16 | organization, 17 | software, 18 | ticketingUserRole, 19 | ticketingUser, 20 | utils 21 | }; 22 | -------------------------------------------------------------------------------- /backend/lib/filter/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | populateFilteryQueryTemplate, 5 | parseQuery 6 | }; 7 | 8 | function populateFilteryQueryTemplate(query, params) { 9 | let queryString = JSON.stringify(query); 10 | 11 | params.map(param => { 12 | queryString = queryString.replace(`%${param.key}%`, param.value); 13 | }); 14 | 15 | return JSON.parse(queryString); 16 | } 17 | 18 | function parseQuery(query) { 19 | const queryString = JSON.stringify(query); 20 | 21 | return (queryString.match(/%.*?%/g) || []).map(x => x.replace(/[%%]/g, '')); 22 | } 23 | -------------------------------------------------------------------------------- /backend/webserver/api/dashboard/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(dependencies, lib, router) { 4 | const authorizationMW = dependencies('authorizationMW'); 5 | const controller = require('./controller')(dependencies, lib); 6 | const { flipFeature } = require('../helpers')(dependencies, lib); 7 | const userMiddleware = require('../user/middleware')(dependencies, lib); 8 | 9 | router.get('/dashboard', 10 | authorizationMW.requiresAPILogin, 11 | flipFeature('isDashboardEnabled'), 12 | userMiddleware.loadTicketingUser, 13 | controller.get 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /backend/webserver/application.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const cors = require('cors'); 5 | 6 | // This is you own express application 7 | // eslint-disable-next-line no-unused-vars 8 | module.exports = function(dependencies) { 9 | 10 | const application = express(); 11 | 12 | application.all('/api/*', cors({ 13 | origin: true, 14 | credentials: true, 15 | exposedHeaders: ['X-ESN-Items-Count'] 16 | })); 17 | // Every express new configuration are appended here. 18 | // This needs to be initialized before the body parser 19 | 20 | return application; 21 | }; 22 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/definitions/client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * definition: 4 | * client_content: 5 | * description: Client object 6 | * type: object 7 | * properties: 8 | * _id: 9 | * type: string 10 | * name: 11 | * type: string 12 | * active: 13 | * type: boolean 14 | * logo: 15 | * type: string 16 | * accessCode: 17 | * type: string 18 | * accessHelp: 19 | * type: string 20 | * timestamps: 21 | * type: object 22 | * properties: 23 | * creation: 24 | * type: string 25 | */ 26 | -------------------------------------------------------------------------------- /backend/lib/listeners/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const organization = require('./organization')(dependencies); 5 | const software = require('./software')(dependencies); 6 | const contract = require('./contract')(dependencies); 7 | const ticket = require('./ticket')(dependencies); 8 | const user = require('./user')(dependencies); 9 | 10 | return { 11 | init 12 | }; 13 | 14 | function init() { 15 | organization.registerListener(); 16 | software.registerListener(); 17 | contract.registerListener(); 18 | ticket.registerListener(); 19 | user.registerListener(); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /config/esn/default.production.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": [ 3 | "linagora.esn.account", 4 | "linagora.esn.core.webserver", 5 | "linagora.esn.core.wsserver", 6 | "linagora.esn.cron", 7 | "linagora.esn.digest.daily", 8 | "linagora.esn.jobqueue", 9 | "linagora.esn.graceperiod", 10 | "linagora.esn.messaging.email", 11 | "linagora.esn.oauth.consumer", 12 | "linagora.esn.profile", 13 | "linagora.esn.user.status", 14 | "linagora.esn.controlcenter", 15 | "linagora.esn.unifiedinbox", 16 | "linagora.esn.admin", 17 | "linagora.esn.resource", 18 | "linagora.esn.sync", 19 | "smartsla-backend" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /test/config/mocks/injector.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Inject angular things globally, for use in frontend unit tests. Add your 5 | * instance names to the INJECTIONS array and don't forget to modify linter config 6 | */ 7 | (function(global) { 8 | // Define things that should be globally injected here 9 | var INJECTIONS = ['$q']; 10 | 11 | // The rest of the code is boilerplate 12 | angular.module('esn.test.injector', []).run(INJECTIONS.concat([function() { 13 | for (var i = 0, len = arguments.length; i < len; i++) { 14 | global[INJECTIONS[i]] = arguments[i]; 15 | } 16 | }])); 17 | beforeEach(angular.mock.module('esn.test.injector')); 18 | })(this); 19 | -------------------------------------------------------------------------------- /backend/lib/db/ticketing-user-role.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { validateUserRole } = require('../helpers'); 4 | 5 | module.exports = dependencies => { 6 | const mongoose = dependencies('db').mongo.mongoose; 7 | 8 | const TicketingUserRoleSchema = new mongoose.Schema({ 9 | user: { type: mongoose.Schema.ObjectId, ref: 'User', required: true }, 10 | role: { type: String, required: true, validate: [validateUserRole, 'Invalid TicketingUser role'] }, 11 | timestamps: { 12 | creation: {type: Date, default: Date.now} 13 | }, 14 | schemaVersion: { type: Number, default: 1 } 15 | }); 16 | 17 | return mongoose.model('TicketingUserRole', TicketingUserRoleSchema); 18 | }; 19 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/definitions/users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * definition: 4 | * user_content: 5 | * type: object 6 | * description: user_object 7 | * properties: 8 | * _id: 9 | * type: string 10 | * identifier: 11 | * type: string 12 | * name: 13 | * type: string 14 | * email: 15 | * type: string 16 | * phone: 17 | * type: string 18 | * role: 19 | * type: string 20 | * type: 21 | * type: string 22 | * user: 23 | * type: string 24 | * timestamps: 25 | * type: object 26 | * properties: 27 | * createdAt: 28 | * type: string 29 | */ 30 | -------------------------------------------------------------------------------- /test/config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "webserver": { 3 | "enabled": true, 4 | "ip": "127.0.0.1", 5 | "ipv6": "::1", 6 | "port": 8081, 7 | "virtualhosts": [], 8 | "startupBufferTimeout": 500 9 | }, 10 | "wsserver": { 11 | "enabled": false, 12 | "port": 8081 13 | }, 14 | "db": { 15 | "attemptsLimit": 100 16 | }, 17 | "log": { 18 | "file": { 19 | "enabled": false 20 | }, 21 | "console": { 22 | "enabled": true, 23 | "level": "error" 24 | } 25 | }, 26 | "auth": { 27 | "strategies": ["mongo"], 28 | "apiStrategies": ["basic-mongo"] 29 | }, 30 | "core": { 31 | "config": { 32 | "db": "db.json" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/webserver/api/glossary/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (dependencies, lib, router) => { 4 | const authorizationMW = dependencies('authorizationMW'); 5 | const controller = require('./controller')(dependencies, lib); 6 | const { 7 | canListGlossary, 8 | canCreateGlossary, 9 | validateGlossaryCreation 10 | } = require('./middleware')(dependencies, lib); 11 | 12 | router.get('/glossaries', 13 | authorizationMW.requiresAPILogin, 14 | canListGlossary, 15 | controller.list 16 | ); 17 | 18 | router.post('/glossaries', 19 | authorizationMW.requiresAPILogin, 20 | canCreateGlossary, 21 | validateGlossaryCreation, 22 | controller.create 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /backend/webserver/api/lininfosec/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(dependencies, lib, router) { 4 | const controller = require('./controller')(dependencies, lib); 5 | 6 | /** 7 | * @swagger 8 | * /ticketing/api/lininfosec: 9 | * post: 10 | * tags: 11 | * - LinInfoSec 12 | * responses: 13 | * 200: 14 | * $ref: "#/responses/lininfosec" 15 | * 401: 16 | * $ref: "#/responses/cm_401" 17 | * 403: 18 | * $ref: "#/responses/cm_403" 19 | * 404: 20 | * $ref: "#/responses/cm_404" 21 | * 500: 22 | * $ref: "#/responses/cm_500" 23 | */ 24 | router.post('/lininfosec', 25 | // TO DO: Add the authentification!! 26 | controller.create 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /test/config/servers-conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const DEFAULT_PORTS = { 4 | express: 23455 5 | }; 6 | 7 | module.exports = { 8 | host: process.env.HOSTNAME || process.env.DOCKER_HOST || 'localhost', 9 | 10 | express: { 11 | port: process.env.PORT_EXPRESS || DEFAULT_PORTS.express 12 | }, 13 | 14 | redis: { 15 | host: 'redis', 16 | port: 6379, 17 | url: 'redis://redis:6379' 18 | }, 19 | 20 | mongodb: { 21 | host: 'mongo', 22 | port: 27017, 23 | connectionString: 'mongodb://mongo/tests' 24 | }, 25 | 26 | elasticsearch: { 27 | host: 'elasticsearch', 28 | port: 9200, 29 | interval_index: 1200 30 | }, 31 | rabbitmq: { 32 | host: 'rabbitmq', 33 | port: 5672, 34 | url: 'amqp://rabbitmq:5672' 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /backend/lib/db/ticketing-user-contract.js: -------------------------------------------------------------------------------- 1 | // { ticketingUserId(ObjectId), contractId(ObjectId), role(String) } 2 | module.exports = dependencies => { 3 | const mongoose = dependencies('db').mongo.mongoose; 4 | 5 | const TicketingUserContractSchema = new mongoose.Schema({ 6 | user: { type: mongoose.Schema.ObjectId, required: true, ref: 'User' }, 7 | contract: { type: mongoose.Schema.ObjectId, required: true, ref: 'Contract' }, 8 | role: { type: String, default: 'viewer' }, 9 | timestamps: { 10 | createdAt: { type: Date, default: Date.now }, 11 | updatedAt: { type: Date, default: Date.now } 12 | }, 13 | schemaVersion: { type: Number, default: 1 } 14 | }); 15 | 16 | return mongoose.model('TicketingUserContract', TicketingUserContractSchema); 17 | }; 18 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/definitions/custom-filter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * definition: 4 | * filter_item: 5 | * type: object 6 | * description: filter entry 7 | * properties: 8 | * category: 9 | * type: string 10 | * value: 11 | * type: string 12 | * filter_content: 13 | * type: object 14 | * description: filter object 15 | * properties: 16 | * _id: 17 | * type: string 18 | * user: 19 | * type: string 20 | * name: 21 | * type: string 22 | * items: 23 | * type: array 24 | * items: 25 | * $ref: "#/definitions/filter_item" 26 | * timestamps: 27 | * type: object 28 | * properties: 29 | * creation: 30 | * type: string 31 | */ 32 | -------------------------------------------------------------------------------- /bin/lib/db.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Q = require('q'); 4 | const mongoose = require('mongoose'); 5 | const commons = require('./commons'); 6 | 7 | module.exports = { 8 | connect, 9 | disconnect 10 | }; 11 | 12 | function connect(config) { 13 | const defer = Q.defer(); 14 | 15 | mongoose.connect(config.connectionString, function(err) { 16 | if (err) { 17 | return defer.reject(err); 18 | } 19 | 20 | commons.logInfo('Connected to MongoDB at', config.connectionString); 21 | defer.resolve(); 22 | }); 23 | 24 | return defer.promise; 25 | } 26 | 27 | function disconnect() { 28 | const defer = Q.defer(); 29 | 30 | commons.logInfo('Disconnecting from MongoDB'); 31 | mongoose.disconnect(function() { 32 | defer.resolve(); 33 | }); 34 | 35 | return defer.promise; 36 | } 37 | -------------------------------------------------------------------------------- /backend/lib/listeners/contract/listener.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const { EVENTS, INDICES } = require('../../constants'); 5 | const coreESListeners = dependencies('coreElasticsearch').listeners; 6 | const denormalize = require('./denormalize')(dependencies); 7 | 8 | return { 9 | getOptions, 10 | register 11 | }; 12 | 13 | function getOptions() { 14 | return { 15 | events: { 16 | add: EVENTS.CONTRACT.created, 17 | update: EVENTS.CONTRACT.updated 18 | }, 19 | denormalize: denormalize.denormalize, 20 | getId: denormalize.getId, 21 | type: INDICES.CONTRACT.type, 22 | index: INDICES.CONTRACT.name 23 | }; 24 | } 25 | 26 | function register() { 27 | coreESListeners.addListener(getOptions()); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /backend/lib/listeners/software/denormalize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const mongoose = dependencies('db').mongo.mongoose; 5 | const Software = mongoose.model('Software'); 6 | 7 | return { 8 | denormalize, 9 | getId 10 | }; 11 | 12 | function getId(software) { 13 | return software._id; 14 | } 15 | 16 | function denormalize(software) { 17 | const options = {virtuals: true, depopulate: true, transform: transform}; 18 | 19 | function transform(doc, ret) { 20 | const hideKeys = ['__v', '_id', 'schemaVersion']; 21 | 22 | ret.id = getId(ret); 23 | hideKeys.forEach(key => { delete ret[key]; }); 24 | } 25 | 26 | return software instanceof Software ? software.toObject(options) : new Software(software).toObject(options); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /backend/lib/listeners/software/listener.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const { EVENTS, INDICES } = require('../../constants'); 5 | const coreESListeners = dependencies('coreElasticsearch').listeners; 6 | const denormalize = require('./denormalize')(dependencies); 7 | 8 | return { 9 | getOptions, 10 | register 11 | }; 12 | 13 | function getOptions() { 14 | return { 15 | events: { 16 | add: EVENTS.SOFTWARE.created, 17 | update: EVENTS.SOFTWARE.updated 18 | }, 19 | denormalize: denormalize.denormalize, 20 | getId: denormalize.getId, 21 | type: INDICES.SOFTWARE.type, 22 | index: INDICES.SOFTWARE.name 23 | }; 24 | } 25 | 26 | function register() { 27 | coreESListeners.addListener(getOptions()); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /backend/lib/db/ticketing-glossary.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { validateGlossaryCategory } = require('../helpers'); 4 | 5 | module.exports = dependencies => { 6 | const mongoose = dependencies('db').mongo.mongoose; 7 | const Schema = mongoose.Schema; 8 | 9 | const TicketingGlossarySchema = new Schema({ 10 | word: { type: String, required: true }, 11 | category: { type: String, required: true, validate: [validateGlossaryCategory, 'Invalid glossary category'] }, 12 | timestamps: { 13 | creation: {type: Date, default: Date.now} 14 | }, 15 | schemaVersion: { type: Number, default: 1 } 16 | }); 17 | 18 | // uniqueness of pair (word, category) 19 | TicketingGlossarySchema.index({ word: 1, category: 1 }, { unique: true }); 20 | 21 | return mongoose.model('TicketingGlossary', TicketingGlossarySchema); 22 | }; 23 | -------------------------------------------------------------------------------- /backend/lib/listeners/organization/listener.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const { EVENTS, INDICES } = require('../../constants'); 5 | const coreESListeners = dependencies('coreElasticsearch').listeners; 6 | const denormalize = require('./denormalize')(dependencies); 7 | 8 | return { 9 | getOptions, 10 | register 11 | }; 12 | 13 | function getOptions() { 14 | return { 15 | events: { 16 | add: EVENTS.ORGANIZATION.created, 17 | update: EVENTS.ORGANIZATION.updated 18 | }, 19 | denormalize: denormalize.denormalize, 20 | getId: denormalize.getId, 21 | type: INDICES.ORGANIZATION.type, 22 | index: INDICES.ORGANIZATION.name 23 | }; 24 | } 25 | 26 | function register() { 27 | coreESListeners.addListener(getOptions()); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /backend/lib/listeners/user/listener.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { EVENTS, INDICES } = require('../../constants'); 4 | 5 | module.exports = dependencies => { 6 | const coreESListeners = dependencies('coreElasticsearch').listeners; 7 | const denormalize = require('./denormalize')(dependencies); 8 | 9 | return { 10 | getOptions, 11 | register 12 | }; 13 | 14 | function getOptions() { 15 | return { 16 | events: { 17 | add: EVENTS.USER.created, 18 | update: EVENTS.USER.updated, 19 | remove: EVENTS.USER.deleted 20 | }, 21 | denormalize: denormalize.denormalize, 22 | getId: denormalize.getId, 23 | type: INDICES.USER.type, 24 | index: INDICES.USER.name 25 | }; 26 | } 27 | 28 | function register() { 29 | coreESListeners.addListener(getOptions()); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /backend/lib/listeners/organization/denormalize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | 5 | const mongoose = dependencies('db').mongo.mongoose; 6 | const Organization = mongoose.model('Organization'); 7 | 8 | return { 9 | denormalize, 10 | getId 11 | }; 12 | 13 | function getId(organization) { 14 | return organization._id; 15 | } 16 | 17 | function denormalize(organization) { 18 | const options = {virtuals: true, depopulate: true, transform: transform}; 19 | 20 | function transform(doc, ret) { 21 | const hideKeys = ['__v', '_id', 'schemaVersion']; 22 | 23 | ret.id = getId(ret); 24 | hideKeys.forEach(key => { delete ret[key]; }); 25 | } 26 | 27 | return organization instanceof Organization ? organization.toObject(options) : new Organization(organization).toObject(options); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /backend/lib/db/organization.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const mongoose = dependencies('db').mongo.mongoose; 5 | const Schema = mongoose.Schema; 6 | 7 | const OrganizationSchema = new mongoose.Schema({ 8 | parent: { type: Schema.ObjectId, ref: 'Organization' }, 9 | shortName: { type: String, required: true, unique: true }, 10 | fullName: { type: String }, 11 | type: { type: String }, 12 | address: { type: String }, 13 | manager: { type: Schema.ObjectId, ref: 'User' }, 14 | contract: { type: Schema.ObjectId, ref: 'Contract' }, 15 | users: [{ type: Schema.ObjectId, ref: 'User' }], 16 | schemaVersion: {type: Number, default: 1}, 17 | description: { type: String }, 18 | creation: { type: Date, default: Date.now } 19 | }); 20 | 21 | return mongoose.model('Organization', OrganizationSchema); 22 | }; 23 | -------------------------------------------------------------------------------- /backend/lib/cns/cns.model.js: -------------------------------------------------------------------------------- 1 | const { convertIsoDurationInDaysHoursMinutes, getHoursValue } = require('./helpers/duration'); 2 | 3 | class Cns { 4 | } 5 | 6 | class CnsValue { 7 | constructor(engagement, workingHours, isNonBusinessHours) { 8 | this.engagement = engagement; 9 | this.workingHours = workingHours; 10 | this.isNonBusinessHours = isNonBusinessHours; 11 | this.elapsedMinutes = 0; 12 | this.percentageElapsed = 0; 13 | this.suspendedMinutes = 0; 14 | } 15 | 16 | getEngagementInHours() { 17 | if (!this.engagement) { 18 | return 0; 19 | } 20 | 21 | const { days, hours } = convertIsoDurationInDaysHoursMinutes(this.engagement); 22 | 23 | return getHoursValue(this.workingHours, days, hours); 24 | } 25 | 26 | getValueInHours() { 27 | return this.elapsedMinutes / 60; 28 | } 29 | } 30 | 31 | module.exports = { Cns, CnsValue }; 32 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/definitions/team.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * definition: 4 | * team_content: 5 | * type: object 6 | * description: team object 7 | * properties: 8 | * name: 9 | * type: string 10 | * motto: 11 | * type: string 12 | * email: 13 | * type: string 14 | * manager: 15 | * type: string 16 | * alertSystemActive: 17 | * type: boolean 18 | * hash: 19 | * type: string 20 | * testAlertSystemActive: 21 | * type: boolean 22 | * alertStartHour: 23 | * type: string 24 | * testAlertStartHour: 25 | * type: string 26 | * contracts: 27 | * type: array 28 | * items: 29 | * type: string 30 | * timestamps: 31 | * type: object 32 | * properties: 33 | * creation: 34 | * type: string 35 | */ 36 | -------------------------------------------------------------------------------- /backend/webserver/api/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | TICKET_SCOPES: { 5 | MINE: 'mine' 6 | }, 7 | TICKET_ACTIONS: { 8 | updateState: 'updateState', 9 | set: 'set', 10 | unset: 'unset' 11 | }, 12 | DEFAULT_TIMEZONE: 'Europe/Paris', 13 | CONTRIBUTION_STATUS_LIST: [ 14 | 'develop', 15 | 'reversed', 16 | 'published', 17 | 'integrated', 18 | 'rejected' 19 | ], 20 | TICKETING_USER_TYPES: { 21 | EXPERT: 'expert', 22 | BENEFICIARY: 'beneficiary' 23 | }, 24 | LININFOSEC: { 25 | DEFAULT_MEETINGID: '1234', // TO DEFINE 26 | DEFAULT_CALLNUMBER: '0600000000', // TO DEFINE 27 | TICKET_STATUS: 'new', 28 | TYPE: 'softwareVulnerability', 29 | NVD_PATH: 'https://nvd.nist.gov/vuln/detail/' 30 | }, 31 | LININFOSEC_SEVERITY_TYPES: { 32 | MAJOR: 'Major', 33 | MINOR: 'Minor', 34 | NONE: 'None' 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /backend/webserver/api/role/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (dependencies, lib, router) => { 2 | const authorizationMW = dependencies('authorizationMW'); 3 | const middleware = require('./middleware')(dependencies, lib); 4 | const controller = require('./controller')(dependencies, lib); 5 | 6 | router.get('/roles', 7 | authorizationMW.requiresAPILogin, 8 | middleware.canList, 9 | controller.list 10 | ); 11 | 12 | router.post('/roles', 13 | authorizationMW.requiresAPILogin, 14 | middleware.canCreateRoles, 15 | controller.createRoles 16 | ); 17 | 18 | router.post('/roles/:id', 19 | authorizationMW.requiresAPILogin, 20 | middleware.loadRole, 21 | middleware.canUpdateRole, 22 | controller.updateRole 23 | ); 24 | 25 | router.delete('/roles/:id', 26 | authorizationMW.requiresAPILogin, 27 | middleware.loadRole, 28 | middleware.canDeleteRole, 29 | controller.deleteRole 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /backend/lib/cns/helpers/duration.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment-timezone'); 2 | 3 | module.exports = { convertIsoDurationInDaysHoursMinutes, convertIsoDurationInMinutes, getHoursValue }; 4 | 5 | function convertIsoDurationInDaysHoursMinutes(duration) { 6 | const momentDuration = moment.duration(duration); 7 | const days = Math.trunc(momentDuration.asDays()); 8 | const hours = momentDuration.hours(); 9 | const minutes = momentDuration.minutes(); 10 | 11 | return { days, hours, minutes }; 12 | } 13 | 14 | function convertIsoDurationInMinutes(duration, workingHours) { 15 | if (!duration || !workingHours) { 16 | return 0; 17 | } 18 | const { days, hours, minutes } = convertIsoDurationInDaysHoursMinutes(duration); 19 | 20 | return days * workingHours * 60 + hours * 60 + minutes; 21 | } 22 | 23 | function getHoursValue(workingHours, days, hours = 0, minutes = 0) { 24 | return days * workingHours + hours + minutes / 60; 25 | } 26 | -------------------------------------------------------------------------------- /backend/webserver/api/filter/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(dependencies, lib, router) { 4 | const authorizationMW = dependencies('authorizationMW'); 5 | const userMiddleware = require('../user/middleware')(dependencies, lib); 6 | const controller = require('./controller')(dependencies, lib); 7 | 8 | /** 9 | * @swagger 10 | * /ticketing/api/filters: 11 | * get: 12 | * tags: 13 | * - Filter 14 | * description: Get filters list. 15 | * responses: 16 | * 200: 17 | * $ref: "#/responses/filters" 18 | * 401: 19 | * $ref: "#/responses/cm_401" 20 | * 403: 21 | * $ref: "#/responses/cm_403" 22 | * 404: 23 | * $ref: "#/responses/cm_404" 24 | * 500: 25 | * $ref: "#/responses/cm_500" 26 | */ 27 | router.get('/filters', 28 | authorizationMW.requiresAPILogin, 29 | userMiddleware.loadTicketingUser, 30 | controller.list 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /bin/commands/role.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { commons, db, ticketingUserRole } = require('../lib'); 4 | const { TICKETING_USER_ROLES } = require('../../backend/lib/constants'); 5 | 6 | module.exports = { 7 | command: 'role', 8 | desc: 'Roles management', 9 | builder: { 10 | email: { 11 | alias: 'e', 12 | describe: 'user email' 13 | }, 14 | role: { 15 | alias: 'r', 16 | describe: 'expectation role', 17 | choices: Object.values(TICKETING_USER_ROLES) 18 | } 19 | }, 20 | handler: argv => { 21 | const { email, role } = argv; 22 | 23 | return exec(email, role) 24 | .then(null, commons.logError) 25 | .finally(commons.exit); 26 | } 27 | }; 28 | 29 | function exec(email, role) { 30 | return db.connect(commons.getDBOptions()) 31 | .then(() => ticketingUserRole.create(email, role)) 32 | .then(() => commons.logInfo(`User role has been set to ${role}`)) 33 | .catch(err => commons.logError(err)); 34 | } 35 | -------------------------------------------------------------------------------- /backend/webserver/api/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | 5 | module.exports = (dependencies, lib) => { 6 | 7 | const router = express.Router(); 8 | 9 | require('./contract')(dependencies, lib, router); 10 | require('./contribution')(dependencies, lib, router); 11 | require('./organization')(dependencies, lib, router); 12 | require('./user')(dependencies, lib, router); 13 | require('./software')(dependencies, lib, router); 14 | require('./glossary')(dependencies, lib, router); 15 | require('./ticket')(dependencies, lib, router); 16 | require('./team')(dependencies, lib, router); 17 | require('./client')(dependencies, lib, router); 18 | require('./custom-filter')(dependencies, lib, router); 19 | require('./role')(dependencies, lib, router); 20 | require('./dashboard')(dependencies, lib, router); 21 | require('./filter')(dependencies, lib, router); 22 | require('./lininfosec')(dependencies, lib, router); 23 | 24 | return router; 25 | }; 26 | -------------------------------------------------------------------------------- /backend/lib/db/schemas/ticketingUser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const mongoose = dependencies('db').mongo.mongoose; 5 | 6 | const TicketingUserSchema = new mongoose.Schema({ 7 | identifier: { type: String }, 8 | name: { type: String, required: true }, 9 | email: { type: String, unique: true, required: true }, 10 | phone: { type: String }, 11 | jobTitle: {type: String}, 12 | client: { type: mongoose.Schema.ObjectId, ref: 'Client'}, 13 | // The role must be kept here in case the type is "expert" just because we do not have any other place to store it 14 | role: { type: String }, 15 | timestamps: { 16 | createdAt: { type: Date, default: Date.now } 17 | }, 18 | // "beneficiary" (ie customer) or "expert" (ie support) 19 | type: { type: String, default: 'beneficiary' }, 20 | user: { type: mongoose.Schema.ObjectId, ref: 'User', required: true }, 21 | schemaVersion: { type: Number, default: 1 } 22 | }); 23 | 24 | return TicketingUserSchema; 25 | }; 26 | -------------------------------------------------------------------------------- /backend/lib/listeners/contract/denormalize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const mongoose = dependencies('db').mongo.mongoose; 5 | const Contract = mongoose.model('Contract'); 6 | 7 | return { 8 | denormalize, 9 | getId 10 | }; 11 | 12 | function getId(contract) { 13 | return contract._id; 14 | } 15 | 16 | function denormalize(contract) { 17 | const options = { virtuals: true, depopulate: true, transform: transform }; 18 | 19 | function transform(doc, ret) { 20 | const hideKeys = ['__v', '_id', 'schemaVersion']; 21 | 22 | ret.id = getId(ret); 23 | 24 | hideKeys.forEach(key => { delete ret[key]; }); 25 | } 26 | 27 | const software = contract.software; 28 | const organization = contract.organization; 29 | 30 | contract = contract instanceof Contract ? contract.toObject(options) : new Contract(contract).toObject(options); 31 | contract = Object.assign(contract, { software, organization }); 32 | 33 | return contract; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/definitions/software.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * definition: 4 | * software_link: 5 | * type: object 6 | * description: software external link 7 | * properties: 8 | * name: 9 | * type: string 10 | * url: 11 | * type: string 12 | * software_content: 13 | * type: object 14 | * description: software object 15 | * properties: 16 | * _id: 17 | * type: string 18 | * name: 19 | * type: string 20 | * summary: 21 | * type: string 22 | * description: 23 | * type: string 24 | * licence: 25 | * type: string 26 | * technology: 27 | * type: string 28 | * group: 29 | * type: string 30 | * logo: 31 | * type: string 32 | * externalLinks: 33 | * type: array 34 | * items: 35 | * $ref: "#/definitions/software_link" 36 | * timestamps: 37 | * type: object 38 | * properties: 39 | * creation: 40 | * type: string 41 | */ 42 | -------------------------------------------------------------------------------- /backend/lib/db/custom-filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const mongoose = dependencies('db').mongo.mongoose; 5 | 6 | const ValueSchema = new mongoose.Schema({ 7 | name: { type: String }, 8 | id: { type: String } 9 | }, { _id: false }); 10 | 11 | const ItemSchema = new mongoose.Schema({ 12 | category: { type: String, required: true }, 13 | value: ValueSchema 14 | }, { _id: false }); 15 | 16 | const CustomFilterSchema = new mongoose.Schema({ 17 | user: { type: mongoose.Schema.ObjectId, ref: 'User', required: true }, 18 | name: { type: String, required: true }, 19 | objectType: { type: String, enum: ['REQUEST', 'CONTRIBUTION'] }, 20 | items: [ItemSchema], 21 | timestamps: { 22 | creation: { type: Date, default: Date.now } 23 | }, 24 | schemaVersion: { type: Number, default: 1 } 25 | }); 26 | 27 | const FilterModel = mongoose.model('CustomFilter', CustomFilterSchema); 28 | 29 | return FilterModel; 30 | }; 31 | -------------------------------------------------------------------------------- /backend/webserver/api/glossary/controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(dependencies, lib) { 4 | const { send500Error } = require('../utils')(dependencies); 5 | 6 | return { 7 | create, 8 | list 9 | }; 10 | 11 | /** 12 | * Create a glossary. 13 | * 14 | * @param {Request} req 15 | * @param {Response} res 16 | */ 17 | function create(req, res) { 18 | return lib.glossary.create(req.body) 19 | .then(glossary => res.status(201).json(glossary)) 20 | .catch(err => send500Error(`Failed to create ${req.body.catetory}`, err, res)); 21 | } 22 | 23 | /** 24 | * List glossaries. 25 | * 26 | * @param {Request} req 27 | * @param {Response} res 28 | */ 29 | function list(req, res) { 30 | const { category } = req.query; 31 | 32 | return lib.glossary.list({ category }) 33 | .then(glossaries => { 34 | res.header('X-ESN-Items-Count', glossaries.length); 35 | res.status(200).json(glossaries); 36 | }) 37 | .catch(err => send500Error('Failed to list glossaries', err, res)); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /backend/lib/listeners/user/denormalize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const mongoose = dependencies('db').mongo.mongoose; 5 | const User = mongoose.model('User'); 6 | 7 | const publicKeys = [ 8 | 'firstname', 9 | 'lastname', 10 | 'preferredEmail', 11 | 'emails', 12 | 'domains', 13 | 'avatars', 14 | 'job_title', 15 | 'service', 16 | 'building_location', 17 | 'office_location', 18 | 'main_phone', 19 | 'description', 20 | 'role' 21 | ]; 22 | 23 | return { 24 | denormalize, 25 | getId 26 | }; 27 | 28 | function getId(user) { 29 | return user._id; 30 | } 31 | 32 | function denormalize(user) { 33 | const denormalizedUser = { 34 | role: user.role 35 | }; 36 | 37 | user = user instanceof User ? user : new User(user).toObject({ virtuals: true }); 38 | 39 | denormalizedUser.id = getId(user); 40 | publicKeys.forEach(key => { 41 | if (user[key]) { 42 | denormalizedUser[key] = user[key]; 43 | } 44 | }); 45 | 46 | return denormalizedUser; 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /backend/lib/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const CONSTANTS = require('./constants'); 5 | 6 | module.exports = { 7 | validateUserRole, 8 | validateGlossaryCategory, 9 | uniqueDemands, 10 | validateTicketState, 11 | isSuspendedTicketState 12 | }; 13 | 14 | function validateUserRole(role) { 15 | return Object.values(CONSTANTS.TICKETING_USER_ROLES).indexOf(role) > -1; 16 | } 17 | 18 | function validateGlossaryCategory(category) { 19 | return Object.values(CONSTANTS.GLOSSARY_CATEGORIES).indexOf(category) > -1; 20 | } 21 | 22 | function uniqueDemands(demands) { 23 | const unique = _.uniqBy(demands, demand => [ 24 | demand.demandType, 25 | demand.softwareType, 26 | demand.issueType 27 | ].join()); 28 | 29 | return unique.length === demands.length; 30 | } 31 | 32 | function validateTicketState(state) { 33 | return Object.values(CONSTANTS.TICKET_STATUS).indexOf(state) > -1; 34 | } 35 | 36 | function isSuspendedTicketState(state) { 37 | return [CONSTANTS.TICKET_STATUS.BYPASSED, 38 | CONSTANTS.TICKET_STATUS.CLOSED].indexOf(state) > -1; 39 | } 40 | -------------------------------------------------------------------------------- /backend/webserver/api/custom-filter/middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const composableMw = require('composable-middleware'); 4 | 5 | module.exports = function(dependencies) { 6 | const { send400Error } = require('../utils')(dependencies); 7 | 8 | return { 9 | validateFilterCreatePayload, 10 | validateFilterUpdatePayload 11 | }; 12 | 13 | function validateFilterCreatePayload(req, res, next) { 14 | const middlewares = [ 15 | validateBasicInfo 16 | ]; 17 | 18 | return composableMw(...middlewares)(req, res, next); 19 | } 20 | 21 | function validateFilterUpdatePayload(req, res, next) { 22 | const middlewares = [ 23 | validateBasicInfo 24 | ]; 25 | 26 | return composableMw(...middlewares)(req, res, next); 27 | } 28 | 29 | function validateBasicInfo(req, res, next) { 30 | const { user } = req.body; 31 | const { items } = req.body; 32 | 33 | if (!user) { 34 | return send400Error('user is required', res); 35 | } 36 | if (!items.length) { 37 | return send400Error('Custom filter is empty', res); 38 | } 39 | 40 | next(); 41 | } 42 | 43 | }; 44 | -------------------------------------------------------------------------------- /backend/lib/db/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | 5 | const TicketUserContract = require('./ticketing-user-contract')(dependencies); 6 | const Client = require('./client')(dependencies); 7 | const Contract = require('./contract')(dependencies); 8 | const Counter = require('./counter')(dependencies); 9 | const Contribution = require('./contribution')(dependencies); 10 | const CustomFilter = require('./custom-filter')(dependencies); 11 | const Organization = require('./organization')(dependencies); 12 | const Software = require('./software')(dependencies); 13 | const Team = require('./team')(dependencies); 14 | const TicketingUser = require('./ticketing-user')(dependencies); 15 | const TicketingGlossary = require('./ticketing-glossary')(dependencies); 16 | const TicketingUserRole = require('./ticketing-user-role')(dependencies); 17 | const Ticket = require('./ticket')(dependencies); 18 | 19 | return { 20 | Client, 21 | Contract, 22 | Contribution, 23 | Counter, 24 | CustomFilter, 25 | Organization, 26 | Software, 27 | Team, 28 | Ticket, 29 | TicketingGlossary, 30 | TicketingUser, 31 | TicketingUserRole, 32 | TicketUserContract 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /backend/webserver/api/contract/middleware/permission.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (dependencies, lib) => { 4 | const { validateObjectIds } = require('../../helpers')(dependencies, lib); 5 | const { 6 | send400Error, 7 | send500Error 8 | } = require('../../utils')(dependencies); 9 | 10 | return { 11 | validatePermissions 12 | }; 13 | 14 | function validatePermissions(req, res, next) { 15 | let { permissions } = req.body; 16 | 17 | if (permissions === 1 || (Array.isArray(permissions) && permissions.length === 0)) { 18 | return next(); 19 | } 20 | 21 | if (Array.isArray(permissions) && validateObjectIds(permissions)) { 22 | permissions = [...new Set(permissions)]; 23 | 24 | return lib.organization.entitiesBelongsOrganization(permissions, req.contract.organization) 25 | .then(belonged => { 26 | if (!belonged) { 27 | return send400Error('entities does not belong to contract\'s organization', res); 28 | } 29 | 30 | req.body.permissions = permissions; 31 | next(); 32 | }) 33 | .catch(err => send500Error('Unable to check permissions', err, res)); 34 | } 35 | 36 | return send400Error('permissions is invalid', res); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /backend/webserver/api/role/middleware.js: -------------------------------------------------------------------------------- 1 | module.exports = (dependencies, lib) => { 2 | const { requireAdministrator } = require('../helpers')(dependencies, lib); 3 | const { send404Error, send500Error } = require('../utils')(dependencies); 4 | 5 | return { 6 | canUpdateRole, 7 | canCreateRoles, 8 | canDeleteRole, 9 | canSetRoles, 10 | canList, 11 | loadRole 12 | }; 13 | 14 | function canCreateRoles(req, res, next) { 15 | requireAdministrator(req, res, next); 16 | } 17 | 18 | function canUpdateRole(req, res, next) { 19 | requireAdministrator(req, res, next); 20 | } 21 | 22 | function canDeleteRole(req, res, next) { 23 | requireAdministrator(req, res, next); 24 | } 25 | 26 | function loadRole(req, res, next) { 27 | lib.ticketingUserRole.get(req.params.id).then(role => { 28 | if (!role) { 29 | return send404Error('Role not found', res); 30 | } 31 | 32 | req.ticketingUserRole = role; 33 | next(); 34 | }) 35 | .catch(err => send500Error('Failed to update contract', err, res)); 36 | } 37 | 38 | function canSetRoles(req, res, next) { 39 | requireAdministrator(req, res, next); 40 | } 41 | 42 | function canList(req, res, next) { 43 | requireAdministrator(req, res, next); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /tasks/linters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | grunt.registerTask('prepare-quick-lint', function() { 5 | const done = this.async(); 6 | const spawn = require('child_process').spawn; 7 | const revision = grunt.option('r'); 8 | const gitopts = revision ? 9 | ['diff-tree', '--no-commit-id', '--name-only', '-r', revision] : 10 | ['status', '--short', '--porcelain', '--untracked-files=no']; 11 | 12 | const child = spawn('git', gitopts); 13 | let output = ''; 14 | 15 | child.stdout.on('data', data => (output += data)); 16 | child.stdout.on('end', () => { 17 | const files = []; 18 | 19 | output.split('\n').forEach(line => { 20 | const filename = revision ? line : line.substr(3); 21 | const status = revision ? '' : line.substr(0, 3).trim(); 22 | 23 | if (status !== 'D' && filename.substr(-3, 3) === '.js') { 24 | files.push(filename); 25 | } 26 | }); 27 | if (files.length) { 28 | grunt.log.ok('Running linters on files:'); 29 | grunt.log.oklns(grunt.log.wordlist(files)); 30 | } else { 31 | grunt.log.ok('No changed files'); 32 | } 33 | grunt.config.set('eslint.quick.src', files); 34 | 35 | done(); 36 | }); 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /doc/cli.md: -------------------------------------------------------------------------------- 1 | # Command Line Interface 2 | 3 | From the root directory: 4 | 5 | ```bash 6 | $ node ./bin/cli --help 7 | ``` 8 | 9 | ## Commands 10 | 11 | ### Elasticsearch 12 | 13 | **Setup** 14 | 15 | It will create the indexes on the elasticsearch instance defined from CLI options. 16 | 17 | ```bash 18 | $ node ./bin/cli elasticsearch setup --host localhost --port 9200 --type organizations 19 | ``` 20 | 21 | - host: default is localhost 22 | - port: default is 9200 23 | - type: Defines the type of index to create. Possible values: organizations, software, contracts. When not set, it will create all the required indexes. 24 | 25 | **Reindex** 26 | 27 | It will index or reindex data from the DB to ES. 28 | 29 | ```bash 30 | $ node ./bin/cli elasticsearch reindex --host localhost --port 9200 --type organizations 31 | ``` 32 | 33 | - host: default is localhost 34 | - port: default is 9200 35 | - type: the data type to reindex. Possible values: organizations, software, contracts 36 | 37 | 38 | ### Role 39 | 40 | It will set role for user which user email and role are defined from CLI options. 41 | 42 | ```bash 43 | $ node ./bin/cli role --email user@mail.com --role administrator 44 | ``` 45 | 46 | - --email, -e user email 47 | - --role, -r expectation role, choices: "administrator", "user" 48 | -------------------------------------------------------------------------------- /backend/lib/filter/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { FILTER_LIST } = require('./constants'); 4 | const { parseQuery, populateFilteryQueryTemplate } = require('./helpers'); 5 | 6 | module.exports = { 7 | list, 8 | getById 9 | }; 10 | 11 | /** 12 | * List filter 13 | * @return {Promise} - Resolve on success 14 | */ 15 | function list({ticketingUser}) { 16 | const { type } = ticketingUser || {}; 17 | const filters = FILTER_LIST.filter(filter => { 18 | if (filter.rights && !filter.rights.includes(type)) { 19 | return false; 20 | } 21 | 22 | return true; 23 | }).map(({ _id, name }) => ({ _id, name })); 24 | 25 | return Promise.resolve(filters); 26 | } 27 | 28 | /** 29 | * Get a filter by ID. 30 | * @param {String} filterId - The filter ID 31 | * @return {Promise} - Resolve the found filter 32 | */ 33 | function getById(filterId = 'open', values = {}) { 34 | const filter = FILTER_LIST.find(filter => filter._id === filterId); 35 | 36 | if (filter && filter.query) { 37 | 38 | const templateParams = parseQuery(filter.query); 39 | const paramValues = templateParams.map(param => ({ 40 | key: param, 41 | value: values[param] || '' 42 | })); 43 | 44 | filter.query = populateFilteryQueryTemplate(filter.query, paramValues); 45 | } 46 | 47 | return Promise.resolve(filter); 48 | } 49 | -------------------------------------------------------------------------------- /backend/lib/db/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const mongoose = dependencies('db').mongo.mongoose; 5 | 6 | const ClientSchema = new mongoose.Schema({ 7 | name: { type: String, required: true, unique: true }, 8 | address: { type: String }, 9 | active: { type: Boolean, default: true }, 10 | logo: mongoose.Schema.Types.ObjectId, 11 | accessCode: { type: String }, 12 | accessHelp: { type: String }, 13 | timestamps: { 14 | creation: { type: Date, default: Date.now } 15 | }, 16 | schemaVersion: { type: Number, default: 1 } 17 | }); 18 | 19 | const ClientModel = mongoose.model('Client', ClientSchema); 20 | 21 | ClientSchema.pre('save', function(next) { 22 | const self = this; 23 | 24 | // Get the document by name insensitive lowercase and uppercase 25 | ClientModel.findOne({ name: new RegExp(`^${self.name}$`, 'i') }, (err, team) => { 26 | if (err) { 27 | return next(err); 28 | } 29 | 30 | if (!self.isNew && team._id.toString() !== self._id.toString()) { 31 | return next(new Error('name is taken')); 32 | } 33 | 34 | if (self.isNew && team) { 35 | return next(new Error('name is taken')); 36 | } 37 | 38 | next(); 39 | }); 40 | }); 41 | 42 | return mongoose.model('Client', ClientSchema); 43 | }; 44 | -------------------------------------------------------------------------------- /backend/webserver/api/organization/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(dependencies, lib, router) { 4 | const { checkIdInParams } = dependencies('helperMw'); 5 | const authorizationMW = dependencies('authorizationMW'); 6 | const controller = require('./controller')(dependencies, lib); 7 | const { 8 | canCreateOrganization, 9 | canReadOrganization, 10 | canListOrganization, 11 | canUpdateOrganization, 12 | validateOrganizationCreatePayload, 13 | validateOrganizationUpdatePayload 14 | } = require('./middleware')(dependencies, lib); 15 | 16 | router.get('/organizations', 17 | authorizationMW.requiresAPILogin, 18 | canListOrganization, 19 | controller.list 20 | ); 21 | 22 | router.get('/organizations/:id', 23 | authorizationMW.requiresAPILogin, 24 | canReadOrganization, 25 | checkIdInParams('id', 'Organization'), 26 | controller.get 27 | ); 28 | 29 | router.post('/organizations', 30 | authorizationMW.requiresAPILogin, 31 | canCreateOrganization, 32 | validateOrganizationCreatePayload, 33 | controller.create 34 | ); 35 | 36 | router.put('/organizations/:id', 37 | authorizationMW.requiresAPILogin, 38 | canUpdateOrganization, 39 | checkIdInParams('id', 'Organization'), 40 | validateOrganizationUpdatePayload, 41 | controller.update 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /config/elasticsearch/software.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "analysis": { 4 | "filter": { 5 | "nGram_filter": { 6 | "type": "nGram", 7 | "min_gram": 1, 8 | "max_gram": 20, 9 | "token_chars": [ 10 | "letter", 11 | "digit", 12 | "punctuation", 13 | "symbol" 14 | ] 15 | } 16 | }, 17 | "analyzer": { 18 | "nGram_analyzer": { 19 | "type": "custom", 20 | "tokenizer": "whitespace", 21 | "filter": [ 22 | "lowercase", 23 | "asciifolding", 24 | "nGram_filter" 25 | ] 26 | }, 27 | "whitespace_analyzer": { 28 | "type": "custom", 29 | "tokenizer": "whitespace", 30 | "filter": [ 31 | "lowercase", 32 | "asciifolding" 33 | ] 34 | } 35 | } 36 | } 37 | }, 38 | "mappings": { 39 | "software": { 40 | "properties": { 41 | "name": { 42 | "type": "string", 43 | "analyzer": "nGram_analyzer", 44 | "search_analyzer": "whitespace_analyzer", 45 | "fields": { 46 | "sort": { 47 | "type": "string", 48 | "index": "not_analyzed" 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tasks/tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | grunt.registerMultiTask('splitfiles', 'split the files and run separate targets', function() { 6 | const options = this.options({ 7 | chunk: 50, 8 | common: [] 9 | }); 10 | 11 | if (!options.target) { 12 | grunt.fatal.fail('Missing target in options'); 13 | 14 | return; 15 | } 16 | 17 | const files = this.files.reduce(function(a, b) { 18 | return a.concat(b.src); 19 | }, []); 20 | const totalFiles = files.length; 21 | let chunkSize = grunt.option('chunk'); 22 | 23 | if (chunkSize === true) { 24 | chunkSize = options.chunk; 25 | } else if (typeof chunkSize === 'undefined') { 26 | chunkSize = totalFiles; 27 | } 28 | const commonFiles = grunt.file.expand(options.common); 29 | const targets = []; 30 | const configBase = options.target.replace(/:/g, '.'); 31 | 32 | for (let chunkId = 1; files.length; chunkId++) { 33 | const chunkFiles = commonFiles.concat(files.splice(0, chunkSize)); 34 | 35 | grunt.config.set(configBase + chunkId + '.options.files', chunkFiles); 36 | targets.push(options.target + chunkId); 37 | } 38 | 39 | if (targets.length > 1) { 40 | grunt.log.ok('Splitting ' + totalFiles + ' tests into ' + targets.length + ' chunks'); 41 | } 42 | grunt.task.run(targets); 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /backend/webserver/api/glossary/middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (dependencies, lib) => { 4 | const { requireAdministrator } = require('../helpers')(dependencies, lib); 5 | const { send400Error, send500Error } = require('../utils')(dependencies); 6 | 7 | return { 8 | canListGlossary, 9 | canCreateGlossary, 10 | validateGlossaryCreation 11 | }; 12 | 13 | function canListGlossary(req, res, next) { 14 | return requireAdministrator(req, res, next); 15 | } 16 | 17 | function canCreateGlossary(req, res, next) { 18 | return requireAdministrator(req, res, next); 19 | } 20 | 21 | function validateGlossaryCreation(req, res, next) { 22 | const { 23 | word, 24 | category 25 | } = req.body; 26 | 27 | if (!word) { 28 | return send400Error('word is required', res); 29 | } 30 | 31 | if (!category) { 32 | return send400Error('category is required', res); 33 | } 34 | 35 | if (!lib.helpers.validateGlossaryCategory(category)) { 36 | return send400Error('category is not supported', res); 37 | } 38 | 39 | lib.glossary.glossaryExists({ word, category }) 40 | .then(alreadyExists => { 41 | if (alreadyExists) { 42 | return send400Error(`${word} in ${category} already exists`, res); 43 | } 44 | 45 | next(); 46 | }) 47 | .catch(err => send500Error('Unable to check glossary', err, res)); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /backend/webserver/api/team/middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const composableMw = require('composable-middleware'); 4 | 5 | module.exports = (dependencies, lib) => { 6 | const { requireAdministrator } = require('../helpers')(dependencies, lib); 7 | const { send400Error } = require('../utils')(dependencies); 8 | 9 | return { 10 | canCreateTeam, 11 | canListTeam, 12 | canUpdateTeam, 13 | validateTeamCreatePayload, 14 | validateTeamUpdatePayload 15 | }; 16 | 17 | function canCreateTeam(req, res, next) { 18 | return requireAdministrator(req, res, next); 19 | } 20 | 21 | function canListTeam(req, res, next) { 22 | next(); // TODO Improve permissions 23 | } 24 | 25 | function canUpdateTeam(req, res, next) { 26 | return requireAdministrator(req, res, next); 27 | } 28 | 29 | function validateTeamCreatePayload(req, res, next) { 30 | const middlewares = [ 31 | validateBasicInfo 32 | ]; 33 | 34 | return composableMw(...middlewares)(req, res, next); 35 | } 36 | 37 | function validateTeamUpdatePayload(req, res, next) { 38 | const middlewares = [ 39 | validateBasicInfo 40 | ]; 41 | 42 | return composableMw(...middlewares)(req, res, next); 43 | } 44 | 45 | function validateBasicInfo(req, res, next) { 46 | const { name } = req.body; 47 | 48 | if (!name) { 49 | return send400Error('name is required', res); 50 | } 51 | 52 | next(); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /backend/lib/access-control/policies.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | administrator: { 5 | can: { 6 | ticket: ['create', 'read', 'update', 'edit', 'list'] 7 | } 8 | }, 9 | supporter: { 10 | can: { 11 | ticket: [ 12 | 'read', 13 | { 14 | name: 'update', 15 | when: options => { 16 | const supportTechnicianIds = options.ticket.supportTechnicians.map(supportTechnician => String(supportTechnician._id)); 17 | 18 | return supportTechnicianIds.indexOf(String(options.user._id)) !== -1 || 19 | String(options.ticket.supportManager._id) === String(options.user._id) || 20 | String(options.ticket.contract.defaultSupportManager) === String(options.user._id); 21 | } 22 | }, 23 | { 24 | name: 'edit', 25 | when: options => String(options.ticket.contract.defaultSupportManager) === String(options.user._id) 26 | }, 27 | 'list' 28 | ] 29 | } 30 | }, 31 | user: { 32 | can: { 33 | ticket: [ 34 | { 35 | name: 'read', 36 | when: options => { 37 | const requesterId = options.ticket.requester && options.ticket.requester._id ? options.ticket.requester._id : options.ticket.requester; 38 | 39 | return String(requesterId) === String(options.user._id); 40 | } 41 | }, 42 | 'list' 43 | ] 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /backend/lib/access-control/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const policies = require('./policies'); 4 | 5 | class AccessControl { 6 | constructor() { 7 | this.policies = this._flattenPolicies(policies); 8 | } 9 | 10 | can(role, resourceType, operation, options = {}) { 11 | if (!this.policies[role]) { 12 | return false; 13 | } 14 | 15 | const policy = this.policies[role]; 16 | const action = `${operation}_${resourceType}`; 17 | 18 | if (policy.can[action]) { 19 | if (typeof policy.can[action] !== 'function') { 20 | return true; 21 | } 22 | 23 | return policy.can[action](options); 24 | } 25 | 26 | return false; 27 | } 28 | 29 | _flattenPolicies(policies) { // eslint-disable-line class-methods-use-this 30 | const result = {}; 31 | 32 | Object.keys(policies).forEach(role => { 33 | result[role] = { can: {} }; 34 | Object.keys(policies[role].can).forEach(resourceType => { 35 | policies[role].can[resourceType].forEach(operation => { 36 | if (typeof operation === 'string') { 37 | result[role].can[`${operation}_${resourceType}`] = true; 38 | } else if (typeof operation.name === 'string' && typeof operation.when === 'function') { 39 | result[role].can[`${operation.name}_${resourceType}`] = operation.when; 40 | } 41 | }); 42 | }); 43 | }); 44 | 45 | return result; 46 | } 47 | } 48 | 49 | module.exports = AccessControl; 50 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/definitions/contribution.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * definition: 4 | * contribution_link: 5 | * type: object 6 | * description: an external contribution link 7 | * properties: 8 | * name: 9 | * type: string 10 | * url: 11 | * type: string 12 | * contribution_status: 13 | * type: object 14 | * description: contribution status 15 | * properties: 16 | * develop: 17 | * type: string 18 | * reversed: 19 | * type: string 20 | * published: 21 | * type: string 22 | * integrated: 23 | * type: string 24 | * rejected: 25 | * type: string 26 | * contribution_content: 27 | * type: object 28 | * description: contribution object 29 | * properties: 30 | * _id: 31 | * type: number 32 | * name: 33 | * type: string 34 | * software: 35 | * type: string 36 | * author: 37 | * type: string 38 | * version: 39 | * type: string 40 | * fixedInVersion: 41 | * type: string 42 | * status: 43 | * $ref: "#/definitions/contribution_status" 44 | * description: 45 | * type: string 46 | * deposedAt: 47 | * type: string 48 | * links: 49 | * type: array 50 | * items: 51 | * $ref: "#/definitions/contribution_link" 52 | * timestamps: 53 | * type: object 54 | * properties: 55 | * creation: 56 | * type: string 57 | */ 58 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/responses/client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * response: 4 | * client: 5 | * description: ok with client object 6 | * schema: 7 | * $ref: "#/definitions/client_content" 8 | * examples: 9 | * application/json: 10 | * { 11 | * "_id": "5e7e02fe81e3d43b645ed282", 12 | * "timestamps": { 13 | * "creation": "2020-03-27T13:43:26.892Z" 14 | * }, 15 | * "active": true, 16 | * "name": "SNCF", 17 | * "contracts": 18 | * [ 19 | * { 20 | * "_id": "5e7e034981e3d43b645ed284", 21 | * "name": "Contract SNCF 2020" 22 | * } 23 | * ] 24 | * } 25 | * clients: 26 | * description: ok with the clients list 27 | * schema: 28 | * type: array 29 | * items: 30 | * $ref: "#/definitions/client_content" 31 | * examples: 32 | * application/json: 33 | * [ 34 | * { 35 | * "timestamps": { 36 | * "creation": "2020-03-27T13:43:26.892Z" 37 | * }, 38 | * "active": true, 39 | * "_id": "5e7e02fe81e3d43b645ed282", 40 | * "name": "SNCF", 41 | * }, 42 | * { 43 | * "timestamps": { 44 | * "creation": "2020-03-26T09:17:06.188Z" 45 | * }, 46 | * "active": true, 47 | * "_id": "5e7c73123687003605f59305", 48 | * "name": "Ministère du bien être", 49 | * "address": "Paris", 50 | * } 51 | * ] 52 | */ 53 | -------------------------------------------------------------------------------- /backend/webserver/api/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const logger = dependencies('logger'); 5 | 6 | return { 7 | send200ListResponse, 8 | send200ItemCount, 9 | send500Error, 10 | send404Error, 11 | send403Error, 12 | send400Error 13 | }; 14 | 15 | function send200ListResponse(list = [], res) { 16 | res.header('X-ESN-Items-Count', list.length); 17 | res.status(200).json(list); 18 | } 19 | 20 | function send200ItemCount(length = 0, res) { 21 | res.header('X-ESN-Items-Count', length); 22 | res.status(200).end(); 23 | } 24 | 25 | function send500Error(details, err, res) { 26 | logger.error(details, err); 27 | 28 | return res.status(500).json({ 29 | error: { 30 | code: 500, 31 | message: 'Server Error', 32 | details 33 | } 34 | }); 35 | } 36 | 37 | function send404Error(details, res) { 38 | return res.status(404).json({ 39 | error: { 40 | code: 404, 41 | message: 'Not Found', 42 | details 43 | } 44 | }); 45 | } 46 | 47 | function send403Error(details, res) { 48 | return res.status(403).json({ 49 | error: { 50 | code: 403, 51 | message: 'Forbidden', 52 | details 53 | } 54 | }); 55 | } 56 | 57 | function send400Error(details, res) { 58 | return res.status(400).json({ 59 | error: { 60 | code: 400, 61 | message: 'Bad Request', 62 | details 63 | } 64 | }); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /backend/lib/cns/helpers/ticket.js: -------------------------------------------------------------------------------- 1 | module.exports = { getTicketSoftwareEngagement, getEngagementHours }; 2 | 3 | const UNDEFINED_DURATION = 'P0D'; 4 | 5 | function getTicketSoftwareEngagement(ticket, contract) { 6 | const engagements = 7 | ticket && 8 | ticket.software && 9 | ticket.software.critical && 10 | contract && 11 | contract.Engagements[ticket.software.critical] && 12 | contract.Engagements[ticket.software.critical].engagements; 13 | 14 | const foundEngagements = 15 | engagements && 16 | engagements.find(function(engagement) { 17 | return engagement.severity === ticket.severity && engagement.request === ticket.type; 18 | }); 19 | 20 | return ( 21 | foundEngagements && { 22 | supported: getEngagementHours(foundEngagements.supported, ticket.createdDuringBusinessHours), 23 | bypassed: getEngagementHours(foundEngagements.bypassed, ticket.createdDuringBusinessHours), 24 | resolved: getEngagementHours(foundEngagements.resolved, ticket.createdDuringBusinessHours) 25 | } 26 | ); 27 | } 28 | 29 | function isEngagementInBusinessHour(engagement, useBusinessHours) { 30 | return useBusinessHours || engagement.nonBusinessHours === UNDEFINED_DURATION; 31 | } 32 | 33 | function getEngagementHours(engagement = {}, useBusinessHours = true) { 34 | const businessHours = isEngagementInBusinessHour(engagement, useBusinessHours); 35 | const hours = businessHours ? engagement.businessHours : engagement.nonBusinessHours; 36 | 37 | return { 38 | hours, 39 | businessHours 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /backend/lib/db/team.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const mongoose = dependencies('db').mongo.mongoose; 5 | 6 | const TeamSchema = new mongoose.Schema({ 7 | name: { type: String, required: true, unique: true }, 8 | motto: { type: String }, 9 | email: { type: String, required: true }, 10 | manager: mongoose.Schema.Types.ObjectId, 11 | alertSystemActive: { type: Boolean, default: false }, 12 | hash: { type: String }, 13 | testAlertSystemActive: { type: Boolean, default: false }, 14 | alertStartHour: { type: String }, 15 | autoAlertStartHour: { type: String }, 16 | contracts: [mongoose.Schema.Types.ObjectId], 17 | timestamps: { 18 | creation: { type: Date, default: Date.now } 19 | }, 20 | schemaVersion: { type: Number, default: 1 } 21 | }); 22 | 23 | const TeamModel = mongoose.model('Team', TeamSchema); 24 | 25 | TeamSchema.pre('save', function(next) { 26 | const self = this; 27 | 28 | // Get the document by name insensitive lowercase and uppercase 29 | TeamModel.findOne({ name: new RegExp(`^${self.name}$`, 'i') }, (err, team) => { 30 | if (err) { 31 | return next(err); 32 | } 33 | 34 | if (!self.isNew && team._id.toString() !== self._id.toString()) { 35 | return next(new Error('name is taken')); 36 | } 37 | 38 | if (self.isNew && team) { 39 | return next(new Error('name is taken')); 40 | } 41 | 42 | next(); 43 | }); 44 | }); 45 | 46 | return mongoose.model('Team', TeamSchema); 47 | }; 48 | -------------------------------------------------------------------------------- /bin/commands/elasticsearch_cmds/setup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Q = require('q'); 4 | const { commons } = require('../../lib'); 5 | const { INDICES } = require('../../../backend/lib/constants'); 6 | 7 | module.exports = { 8 | command: 'setup', 9 | desc: 'Setup elasticSearch for ticketing module', 10 | builder: { 11 | host: { 12 | alias: 'H', 13 | describe: 'elasticsearch host to connect to', 14 | default: 'localhost' 15 | }, 16 | port: { 17 | alias: 'p', 18 | describe: 'elasticsearch port to connect to', 19 | type: 'number', 20 | default: 9200 21 | }, 22 | type: { 23 | alias: 't', 24 | describe: 'index type' 25 | }, 26 | index: { 27 | alias: 'i', 28 | describe: 'index to create' 29 | } 30 | }, 31 | handler: argv => { 32 | const { host, port, type, index } = argv; 33 | 34 | exec(host, port, type, index) 35 | .then(() => commons.logInfo('ElasticSearch has been configured')) 36 | .catch(commons.logError) 37 | .finally(commons.exit); 38 | } 39 | }; 40 | 41 | function exec(host, port, type, index) { 42 | if (type) { 43 | const esConfig = commons.getESConfiguration({ host, port, type }); 44 | 45 | index = Object.values(INDICES).find(index => (index.type === type)).name; 46 | 47 | return esConfig.setup(index, type); 48 | } 49 | 50 | return Q.all(Object.values(INDICES).map(index => { 51 | const esConfig = commons.getESConfiguration({ host, port, type: index.type }); 52 | 53 | esConfig.setup(index.name, index.type); 54 | })); 55 | } 56 | -------------------------------------------------------------------------------- /backend/lib/listeners/ticket/listener.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Q = require('q'); 4 | 5 | module.exports = dependencies => { 6 | const pubsubLocal = dependencies('pubsub').local; 7 | const pubsubGlobal = dependencies('pubsub').global; 8 | const logger = dependencies('logger'); 9 | const { EVENTS, TICKET_ACTIVITY, NOTIFICATIONS } = require('../../constants'); 10 | const activitystreams = dependencies('activitystreams'); 11 | 12 | const ticketUpdatedNotificationTopic = pubsubLocal.topic(NOTIFICATIONS.updated); 13 | 14 | return { 15 | register 16 | }; 17 | 18 | /** 19 | * Handler to store a timeline when a ticket event is fired 20 | * @param {Object} data - The data object contains verb, actor and ticket 21 | * @param {Promise} - Resolve on done 22 | */ 23 | function handler(data) { 24 | const entry = { 25 | verb: data.verb, 26 | actor: activitystreams.helpers.getUserAsActor(data.actor), 27 | object: { 28 | objectType: TICKET_ACTIVITY.OBJECT_TYPE, 29 | _id: data.ticketId 30 | }, 31 | changeset: data.changeset 32 | }; 33 | 34 | return Q.ninvoke(activitystreams, 'addTimelineEntry', entry) 35 | .then(result => { 36 | ticketUpdatedNotificationTopic.forward(pubsubGlobal, result[0]); 37 | logger.debug('timelineEntry has been saved', result); 38 | }) 39 | .catch(err => logger.error('Error while creating timelineEntry', err)); 40 | } 41 | 42 | function register() { 43 | Object.values(EVENTS.TICKET).forEach(event => pubsubLocal.topic(event).subscribe(handler)); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /backend/ws/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const NAMESPACE = '/ticketing/tickets'; 4 | const { NOTIFICATIONS } = require('../lib/constants'); 5 | 6 | let initialized = false; 7 | let ticketNamespace; 8 | 9 | module.exports = dependencies => { 10 | const logger = dependencies('logger'); 11 | const pubsub = dependencies('pubsub').global; 12 | const io = dependencies('wsserver').io; 13 | 14 | return { 15 | init 16 | }; 17 | 18 | function init() { 19 | if (initialized) { 20 | logger.warn('The ticket notification service is already initialized'); 21 | 22 | return; 23 | } 24 | 25 | function synchronizeActivitiesLists(event, data) { 26 | if (ticketNamespace) { 27 | ticketNamespace.to(String(data.object._id)).emit(event, { 28 | room: String(data.object._id), 29 | data 30 | }); 31 | } 32 | } 33 | 34 | pubsub.topic(NOTIFICATIONS.updated).subscribe(data => { 35 | logger.info('Notifying ticket update'); 36 | synchronizeActivitiesLists('ticketing:ticket:updated', data); 37 | }); 38 | 39 | ticketNamespace = io.of(NAMESPACE); 40 | ticketNamespace.on('connection', socket => { 41 | logger.info('New connection on ' + NAMESPACE); 42 | 43 | socket.on('subscribe', ticketId => { 44 | logger.info('Joining ticket room', ticketId); 45 | socket.join(ticketId); 46 | }); 47 | 48 | socket.on('unsubscribe', ticketId => { 49 | logger.info('Leaving ticket room', ticketId); 50 | socket.leave(ticketId); 51 | }); 52 | }); 53 | 54 | initialized = true; 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /backend/lib/db/software.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const mongoose = dependencies('db').mongo.mongoose; 5 | const Schema = mongoose.Schema; 6 | 7 | const ExternalLinksSchema = new Schema({ 8 | name: {type: String}, 9 | url: {type: String} 10 | }, {_id: false}); 11 | 12 | const SoftwareSchema = new mongoose.Schema({ 13 | name: { type: String, required: true, unique: true }, 14 | summary: { type: String }, 15 | description: { type: String }, 16 | licence: { type: String }, 17 | technology: { type: String }, 18 | group: { type: String }, 19 | logo: mongoose.Schema.Types.ObjectId, 20 | externalLinks: [ExternalLinksSchema], 21 | timestamps: { 22 | creation: { type: Date, default: Date.now } 23 | }, 24 | schemaVersion: { type: Number, default: 1 } 25 | }); 26 | 27 | const SoftwareModel = mongoose.model('Software', SoftwareSchema); 28 | 29 | SoftwareSchema.pre('save', function(next) { 30 | const self = this; 31 | 32 | // Get the document by name insensitive lowercase and uppercase 33 | SoftwareModel.findOne({ name: new RegExp(`^${self.name}$`, 'i') }, (err, software) => { 34 | if (err) { 35 | return next(err); 36 | } 37 | 38 | if (!self.isNew && software._id.toString() !== self._id.toString()) { 39 | return next(new Error('name is taken')); 40 | } 41 | 42 | if (self.isNew && software) { 43 | return next(new Error('name is taken')); 44 | } 45 | 46 | next(); 47 | }); 48 | }); 49 | 50 | return mongoose.model('Software', SoftwareSchema); 51 | }; 52 | -------------------------------------------------------------------------------- /backend/webserver/api/role/controller.js: -------------------------------------------------------------------------------- 1 | module.exports = (dependencies, lib) => { 2 | const coreUser = dependencies('coreUser'); 3 | const { send500Error, send400Error } = require('../utils')(dependencies); 4 | 5 | return { 6 | list, 7 | updateRole, 8 | deleteRole, 9 | createRoles 10 | }; 11 | 12 | function createRoles(req, res) { 13 | if (!req.body || !req.body.length) { 14 | return send400Error('Array of {user,role} is required', res); 15 | } 16 | 17 | lib.ticketingUserRole.createMultiple(req.body) 18 | .then(created => res.status(201).json(created)) 19 | .catch(err => send500Error('Unable to get role', err, res)); 20 | } 21 | 22 | function list(req, res) { 23 | lib.ticketingUserRole.list({ limit: -1, offset: 0 }) 24 | .then(roles => { 25 | roles = roles.map(role => ({ 26 | _id: role._id, 27 | role: role.role, 28 | user: coreUser.denormalize.denormalize(role.user) 29 | })); 30 | 31 | res.status(200).json(roles); 32 | }) 33 | .catch(err => send500Error('Unable to get role', err, res)); 34 | } 35 | 36 | function updateRole(req, res) { 37 | lib.ticketingUserRole.updateRoleById(req.ticketingUserRole._id, req.body.role) 38 | .then(() => res.status(200).send()) 39 | .catch(err => send500Error('Failed to update role', err, res)); 40 | } 41 | 42 | function deleteRole(req, res) { 43 | lib.ticketingUserRole.deleteById(req.ticketingUserRole._id) 44 | .then(() => res.status(204).send()) 45 | .catch(err => send500Error('Failed to delete role', err, res)); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /test/unit-storage/lib/db/contribution.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const expect = chai.expect; 5 | 6 | describe('The Contribution model', function() { 7 | let Contribution, ObjectId, mongoose; 8 | 9 | beforeEach(function(done) { 10 | mongoose = this.moduleHelpers.dependencies('db').mongo.mongoose; 11 | ObjectId = mongoose.Types.ObjectId; 12 | 13 | require(this.testEnv.backendPath + '/lib/db/counter')( 14 | this.moduleHelpers.dependencies 15 | ); 16 | require(this.testEnv.backendPath + '/lib/db/contribution')( 17 | this.moduleHelpers.dependencies 18 | ); 19 | Contribution = mongoose.model('Contribution'); 20 | 21 | this.connectMongoose(mongoose, done); 22 | }); 23 | 24 | afterEach(function(done) { 25 | delete mongoose.connection.models.Contribution; 26 | this.helpers.mongo.dropDatabase(err => { 27 | if (err) return done(err); 28 | this.testEnv.core.db.mongo.mongoose.connection.close(done); 29 | }); 30 | }); 31 | 32 | function saveContribution(contributionJson, callback) { 33 | const MyContribution = new Contribution(contributionJson); 34 | 35 | return MyContribution.save(callback); 36 | } 37 | 38 | describe('The Contribution model', function() { 39 | it('should store a Contribution with valid fields', function(done) { 40 | saveContribution( 41 | { 42 | name: 'Foo', 43 | software: new ObjectId(), 44 | author: new ObjectId() 45 | }, 46 | 47 | err => { 48 | expect(err).to.not.exist; 49 | done(); 50 | } 51 | ); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/responses/team.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * response: 4 | * team: 5 | * description: OK with team object 6 | * schema: 7 | * $ref: "#/definitions/team_content" 8 | * examples: 9 | * application/json: 10 | * { 11 | * "timestamps": { 12 | * "creation": "2020-03-11T15:48:03.585Z" 13 | * }, 14 | * "alertSystemActive": false, 15 | * "testAlertSystemActive": false, 16 | * "contracts": [], 17 | * "_id": "5e6908332891b92465cb1fe9", 18 | * "name": "TAO Pizza Team", 19 | * "email": "test@linagora.com", 20 | * } 21 | * teams: 22 | * description: Ok with the team list 23 | * schema: 24 | * type: array 25 | * items: 26 | * $ref: "#/definitions/team_content" 27 | * examples: 28 | * applications/json: 29 | * [ 30 | * { 31 | * "timestamps": { 32 | * "creation": "2020-03-11T15:48:03.585Z" 33 | * }, 34 | * "alertSystemActive": false, 35 | * "testAlertSystemActive": false, 36 | * "contracts": [], 37 | * "_id": "5e6908332891b92465cb1fe9", 38 | * "name": "TAO Pizza Team", 39 | * "email": "test@linagora.com", 40 | * }, 41 | * { 42 | * "timestamps": { 43 | * "creation": "2020-02-13T16:36:01.482Z" 44 | * }, 45 | * "alertSystemActive": false, 46 | * "testAlertSystemActive": false, 47 | * "contracts": [], 48 | * "_id": "5e457af17c72893da8f28a9d", 49 | * "name": "caseSensitive", 50 | * "email": "test", 51 | * } 52 | * ] 53 | */ 54 | -------------------------------------------------------------------------------- /config/elasticsearch/organizations.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "analysis": { 4 | "filter": { 5 | "nGram_filter": { 6 | "type": "nGram", 7 | "min_gram": 1, 8 | "max_gram": 20, 9 | "token_chars": [ 10 | "letter", 11 | "digit", 12 | "punctuation", 13 | "symbol" 14 | ] 15 | } 16 | }, 17 | "analyzer": { 18 | "nGram_analyzer": { 19 | "type": "custom", 20 | "tokenizer": "whitespace", 21 | "filter": [ 22 | "lowercase", 23 | "asciifolding", 24 | "nGram_filter" 25 | ] 26 | }, 27 | "whitespace_analyzer": { 28 | "type": "custom", 29 | "tokenizer": "whitespace", 30 | "filter": [ 31 | "lowercase", 32 | "asciifolding" 33 | ] 34 | } 35 | } 36 | } 37 | }, 38 | "mappings": { 39 | "organizations": { 40 | "properties": { 41 | "shortName": { 42 | "type": "string", 43 | "analyzer": "nGram_analyzer", 44 | "search_analyzer": "whitespace_analyzer", 45 | "fields": { 46 | "sort": { 47 | "type": "string", 48 | "index": "not_analyzed" 49 | } 50 | } 51 | }, 52 | "fullName": { 53 | "type": "string", 54 | "analyzer": "nGram_analyzer", 55 | "search_analyzer": "whitespace_analyzer" 56 | }, 57 | "description": { 58 | "type": "string", 59 | "analyzer": "nGram_analyzer", 60 | "search_analyzer": "whitespace_analyzer" 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/unit-backend/all.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mockery = require('mockery'); 4 | const chai = require('chai'); 5 | const path = require('path'); 6 | 7 | before(function() { 8 | chai.use(require('chai-shallow-deep-equal')); 9 | chai.use(require('sinon-chai')); 10 | chai.use(require('chai-as-promised')); 11 | this.helpers = {}; 12 | }); 13 | 14 | beforeEach(function() { 15 | mockery.enable({warnOnReplace: false, warnOnUnregistered: false, useCleanCache: true}); 16 | const depsStore = { 17 | logger: require('./fixtures/logger-noop'), 18 | errors: require('./fixtures/errors') 19 | }; 20 | const dependencies = name => depsStore[name]; 21 | 22 | const addDep = (name, dep) => { 23 | depsStore[name] = dep; 24 | }; 25 | 26 | const mockModels = mockedModels => { 27 | const types = { 28 | ObjectId: function(id) { 29 | return {id: id}; 30 | }, 31 | Mixed: '' 32 | }; 33 | 34 | const schema = function() {}; 35 | 36 | schema.Types = types; 37 | 38 | const mongooseMock = { 39 | Types: types, 40 | Schema: schema, 41 | model: function(model) { 42 | return mockedModels[model]; 43 | }, 44 | __replaceObjectId: function(newObjectId) { 45 | types.ObjectId = newObjectId; 46 | } 47 | }; 48 | 49 | mockery.registerMock('mongoose', mongooseMock); 50 | 51 | return this.moduleHelpers.addDep('db', { 52 | mongo: { 53 | mongoose: require('mongoose') 54 | } 55 | }); 56 | }; 57 | 58 | this.moduleHelpers = { 59 | backendPath: path.normalize(__dirname + '/../../backend'), 60 | addDep, 61 | dependencies, 62 | mockModels 63 | }; 64 | }); 65 | 66 | afterEach(function() { 67 | mockery.resetCache(); 68 | mockery.deregisterAll(); 69 | mockery.disable(); 70 | }); 71 | -------------------------------------------------------------------------------- /backend/webserver/api/client/middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const composableMw = require('composable-middleware'); 4 | 5 | module.exports = (dependencies, lib) => { 6 | const { requireAdministrator } = require('../helpers')(dependencies, lib); 7 | const { send400Error, send403Error } = require('../utils')(dependencies); 8 | 9 | return { 10 | canCreateClient, 11 | canListClient, 12 | canUpdateClient, 13 | clientCanBeRemoved, 14 | validateClientCreatePayload, 15 | validateClientUpdatePayload 16 | }; 17 | 18 | function canCreateClient(req, res, next) { 19 | return requireAdministrator(req, res, next); 20 | } 21 | 22 | function canListClient(req, res, next) { 23 | next(); // TODO Improve permissions 24 | } 25 | 26 | function canUpdateClient(req, res, next) { 27 | return requireAdministrator(req, res, next); 28 | } 29 | 30 | function validateClientCreatePayload(req, res, next) { 31 | const middlewares = [ 32 | validateBasicInfo 33 | ]; 34 | 35 | return composableMw(...middlewares)(req, res, next); 36 | } 37 | 38 | function validateClientUpdatePayload(req, res, next) { 39 | const middlewares = [ 40 | validateBasicInfo 41 | ]; 42 | 43 | return composableMw(...middlewares)(req, res, next); 44 | } 45 | 46 | function validateBasicInfo(req, res, next) { 47 | const { name } = req.body; 48 | 49 | if (!name) { 50 | return send400Error('name is required', res); 51 | } 52 | 53 | next(); 54 | } 55 | 56 | function clientCanBeRemoved(req, res, next) { 57 | return lib.contract.listByClient(req.params.id) 58 | .then(contracts => { 59 | if (contracts && contracts.length) { 60 | return send403Error('client cannot be deleted: related to contracts', res); 61 | } 62 | 63 | return next(); 64 | }); 65 | 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/responses/custom-filter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * response: 4 | * custom-filter: 5 | * description: Ok with the filter object 6 | * schema: 7 | * $ref: "#/definitions/filter_content" 8 | * examples: 9 | * application/json: 10 | * { 11 | * "timestamps": { 12 | * "creation": "2020-02-14T15:03:43.708Z" 13 | * }, 14 | * "_id": "5e46b6cf7c72893da8f296a7", 15 | * "name": "thunderbird", 16 | * "items": [ 17 | * { 18 | * "category": "Software", 19 | * "value": "Thunderbird" 20 | * } 21 | * ], 22 | * "user": "5bd18e5477f6bb734331c3f1", 23 | * } 24 | * custom-filters: 25 | * description: Ok with the filters list 26 | * schema: 27 | * type: array 28 | * items: 29 | * $ref: "#/definitions/filter_content" 30 | * examples: 31 | * application/json: 32 | * [ 33 | * { 34 | * "timestamps": { 35 | * "creation": "2020-02-14T15:03:43.708Z" 36 | * }, 37 | * "_id": "5e46b6cf7c72893da8f296a7", 38 | * "name": "thunderbird", 39 | * "items": [ 40 | * { 41 | * "category": "Software", 42 | * "value": "Thunderbird" 43 | * } 44 | * ], 45 | * "user": "5bd18e5477f6bb734331c3f1", 46 | * }, 47 | * { 48 | * "timestamps": { 49 | * "creation": "2020-02-13T10:20:09.636Z" 50 | * }, 51 | * "_id": "5e4522d9f00f0817f71f4bd4", 52 | * "name": "A", 53 | * "items": [ 54 | * { 55 | * "category": "Type", 56 | * "value": "Information" 57 | * } 58 | * ], 59 | * "user": "5bd18e5477f6bb734331c3f1", 60 | * } 61 | * ] 62 | */ 63 | -------------------------------------------------------------------------------- /backend/lib/ticket/search.js: -------------------------------------------------------------------------------- 1 | const { ALL_CONTRACTS } = require('../constants'); 2 | 3 | module.exports = dependencies => { 4 | const mongoose = dependencies('db').mongo.mongoose; 5 | const Ticket = mongoose.model('Ticket'); 6 | const contract = require('../contract')(dependencies); 7 | 8 | function search(req) { 9 | return buildQuery(req).then(result => ({ 10 | size: result.length, 11 | list: result 12 | })); 13 | } 14 | 15 | function buildQuery(req) { 16 | const {query, user, ticketingUser} = req; 17 | let contractIdFilter; 18 | 19 | let findOptions = { 20 | $or: [ 21 | {title: new RegExp(`${query.q}`, 'i')}, 22 | {description: new RegExp(`${query.q}`, 'i')} 23 | ] 24 | }; 25 | 26 | if (!isNaN(parseInt(query.q, 10))) { 27 | findOptions.$or = [...findOptions.$or, {_id: query.q}]; 28 | } 29 | 30 | return contract.allowedContracts({ user, ticketingUser }) 31 | .then(allowedContractIds => { 32 | 33 | if (allowedContractIds && allowedContractIds !== ALL_CONTRACTS) { 34 | contractIdFilter = allowedContractIds.map(String); 35 | } 36 | 37 | if (query.contract) { 38 | contractIdFilter = contractIdFilter ? [query.contract].filter(contract => contractIdFilter.includes(contract)) : [query.contract]; 39 | } 40 | 41 | if (contractIdFilter) { 42 | findOptions = { 43 | ...findOptions, 44 | contract: { $in: contractIdFilter.map(mongoose.Types.ObjectId) } 45 | }; 46 | } 47 | 48 | return Ticket.find(findOptions).exec(); 49 | }); 50 | } 51 | 52 | return search; 53 | }; 54 | -------------------------------------------------------------------------------- /backend/lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(dependencies) { 4 | 5 | const models = require('./db')(dependencies); 6 | const cns = require('./cns')(dependencies); 7 | const dashboard = require('./dashboard')(dependencies); 8 | const filter = require('./filter'); 9 | const user = require('./user')(dependencies); 10 | const organization = require('./organization')(dependencies); 11 | const ticketingUserRole = require('./ticketing-user-role')(dependencies); 12 | const ticketingUser = require('./ticketing-user')(dependencies); 13 | const contract = require('./contract')(dependencies); 14 | const contribution = require('./contribution')(dependencies); 15 | const helpers = require('./helpers'); 16 | const constants = require('./constants'); 17 | const listeners = require('./listeners')(dependencies); 18 | const software = require('./software')(dependencies); 19 | const team = require('./team')(dependencies); 20 | const client = require('./client')(dependencies); 21 | const glossary = require('./ticketing-glossary')(dependencies); 22 | const ticket = require('./ticket')(dependencies); 23 | const customFilter = require('./custom-filter')(dependencies); 24 | const AccessControl = require('./access-control'); 25 | const email = require('./email')(dependencies); 26 | 27 | return { 28 | cns, 29 | constants, 30 | contract, 31 | contribution, 32 | email, 33 | dashboard, 34 | filter, 35 | helpers, 36 | models, 37 | organization, 38 | software, 39 | team, 40 | client, 41 | glossary, 42 | start, 43 | user, 44 | ticketingUserRole, 45 | ticketingUser, 46 | ticket, 47 | customFilter, 48 | accessControl: new AccessControl() 49 | }; 50 | 51 | function start(callback) { 52 | listeners.init(); 53 | 54 | require('./config')(dependencies).register(); 55 | 56 | callback(); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /backend/lib/ticketing-glossary.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const mongoose = dependencies('db').mongo.mongoose; 5 | const TicketingGlossary = mongoose.model('TicketingGlossary'); 6 | 7 | return { 8 | create, 9 | findByGlossaries, 10 | list, 11 | glossaryExists 12 | }; 13 | 14 | /** 15 | * Create new glossary. 16 | * @param {Object} glossary - Glossary object 17 | * @return {Promise} - Resolve with created glossary on success 18 | */ 19 | function create(glossary) { 20 | glossary = glossary instanceof TicketingGlossary ? glossary : new TicketingGlossary(glossary); 21 | 22 | return TicketingGlossary.create(glossary); 23 | } 24 | 25 | /** 26 | * List glossaries. 27 | * @param {Object} options - The options object, may contain category 28 | * @param {Promise} - Resolve with list of glossary in alphabetic order 29 | */ 30 | function list(options = {}) { 31 | const conditions = options.category ? { category: options.category } : {}; 32 | 33 | return TicketingGlossary 34 | .find(conditions) 35 | .sort('word') 36 | .exec(); 37 | } 38 | 39 | /** 40 | * Check if glossary already exists. 41 | * @param {Object} glossary - Glossary object 42 | * @param {Promise} - Resolve true if glossary already exists, false otherwise 43 | */ 44 | function glossaryExists(glossary) { 45 | return TicketingGlossary.count(glossary).exec() 46 | .then(count => count > 0); 47 | } 48 | 49 | /** 50 | * Find by glossaries 51 | * @param {Array} glossaries - The list of glossary objects, each item has format: { word: 'word', category: 'category' } 52 | * @return {Promise} Resolve on success with the list of found glossaries 53 | */ 54 | function findByGlossaries(glossaries) { 55 | return TicketingGlossary.find({ $or: glossaries }).exec(); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /test/unit-backend/lib/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | const q = require('q'); 5 | //const expect = require('chai').expect; 6 | const sinon = require('sinon'); 7 | //const mockery = require('mockery'); 8 | const mongoose = require('mongoose'); 9 | 10 | describe('The user lib', function() { 11 | let ObjectId, moduleHelpers; 12 | let user, userId; 13 | let findByIdAndRemoveMock; 14 | 15 | beforeEach(function() { 16 | moduleHelpers = this.moduleHelpers; 17 | ObjectId = mongoose.Types.ObjectId; 18 | userId = new ObjectId(); 19 | 20 | moduleHelpers.addDep('pubsub', { 21 | local: { 22 | topic: () => {} 23 | } 24 | }); 25 | moduleHelpers.addDep('coreUser', { 26 | recordUser: (user, callback) => { 27 | user._id = userId; 28 | 29 | callback(null, user); 30 | } 31 | }); 32 | 33 | findByIdAndRemoveMock = sinon.spy(() => ({ exec: () => q.when() })); 34 | 35 | function User(user) { 36 | this.email = user.email; 37 | } 38 | 39 | User.findByIdAndRemove = findByIdAndRemoveMock; 40 | User.create = () => q.when(user); 41 | 42 | moduleHelpers.mockModels({ 43 | User 44 | }); 45 | }); 46 | 47 | //const getModule = () => require(moduleHelpers.backendPath + '/lib/user')(moduleHelpers.dependencies); 48 | 49 | it('should remove the user if failed to create user role', function(done) { 50 | const error = new Error('something wrong'); 51 | const createUserRoleMock = sinon.stub().returns(q.reject(error)); 52 | 53 | mockery.registerMock('../ticketing-user-role', () => ({ create: createUserRoleMock })); 54 | 55 | getModule().create(user) 56 | .catch(err => { 57 | expect(err.message).to.equal(error.message); 58 | expect(createUserRoleMock).to.have.been.calledWith(userRole); 59 | expect(findByIdAndRemoveMock).to.have.been.calledWith(userId); 60 | done(); 61 | }); 62 | }); 63 | });*/ 64 | -------------------------------------------------------------------------------- /bin/lib/ticketing-user-role.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MongoClient = require('mongodb').MongoClient; 4 | const trim = require('trim'); 5 | const Q = require('q'); 6 | const commons = require('./commons'); 7 | const { dependencies } = require('./utils'); 8 | 9 | require('../../backend/lib/db/ticketing-user-role')(dependencies); 10 | require('./user'); 11 | const ticUserRoleLibModule = require('../../backend/lib/ticketing-user-role')(dependencies); 12 | 13 | module.exports = { 14 | create, 15 | listByCursor 16 | }; 17 | 18 | function create(email, role) { 19 | return Q.ninvoke(MongoClient, 'connect', commons.getDBOptions().connectionString) 20 | .then(dbConnection => { 21 | const User = dbConnection.collection('users'); 22 | 23 | return Q.ninvoke(User, 'findOne', _buildFindByEmailQuery(email)) 24 | .then(user => { 25 | if (!user) { 26 | dbConnection.close(); 27 | 28 | return Q.reject(new Error('user does not exist')); 29 | } 30 | 31 | return ticUserRoleLibModule.getByUser(user._id) 32 | .then(userRole => { 33 | if (userRole) { 34 | if (userRole.role === role) { 35 | return; 36 | } 37 | 38 | userRole.role = role; 39 | 40 | return ticUserRoleLibModule.updateById(userRole._id, userRole); 41 | } 42 | 43 | userRole = { 44 | user: user._id, 45 | role 46 | }; 47 | 48 | return ticUserRoleLibModule.create(userRole); 49 | }); 50 | }) 51 | .then(() => dbConnection.close()); 52 | }); 53 | } 54 | 55 | function listByCursor() { 56 | return ticUserRoleLibModule.listByCursor(); 57 | } 58 | 59 | function _buildFindByEmailQuery(email) { 60 | return { 61 | accounts: { 62 | $elemMatch: { 63 | emails: trim(email).toLowerCase() 64 | } 65 | } 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /test/unit-storage/lib/db/team.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const expect = chai.expect; 5 | 6 | describe('The Team model', function() { 7 | let Team, mongoose; 8 | 9 | beforeEach(function(done) { 10 | mongoose = this.moduleHelpers.dependencies('db').mongo.mongoose; 11 | 12 | require(this.testEnv.backendPath + '/lib/db/team')( 13 | this.moduleHelpers.dependencies 14 | ); 15 | Team = mongoose.model('Team'); 16 | 17 | this.connectMongoose(mongoose, done); 18 | }); 19 | 20 | afterEach(function(done) { 21 | delete mongoose.connection.models.Team; 22 | this.helpers.mongo.dropDatabase(err => { 23 | if (err) return done(err); 24 | this.testEnv.core.db.mongo.mongoose.connection.close(done); 25 | }); 26 | }); 27 | 28 | function saveTeam(TeamJson, callback) { 29 | const MyTeam = new Team(TeamJson); 30 | 31 | return MyTeam.save(callback); 32 | } 33 | 34 | describe('The Team model', function() { 35 | it('should store a Team with valid fields', function(done) { 36 | saveTeam( 37 | { 38 | name: 'Team1', 39 | email: 'Foo' 40 | }, 41 | 42 | err => { 43 | expect(err).to.not.exist; 44 | done(); 45 | } 46 | ); 47 | }); 48 | 49 | it('should have a unique name value', function(done) { 50 | saveTeam( 51 | { 52 | name: 'Team1', 53 | email: 'Foo' 54 | }, 55 | 56 | err => { 57 | expect(err).to.not.exist; 58 | 59 | const anotherTeam = { 60 | name: 'Team1', 61 | email: 'Foo' 62 | }; 63 | 64 | setTimeout(function() { 65 | saveTeam(anotherTeam, err => { 66 | expect(err).to.exist; 67 | expect(err.message).to.contain('duplicate key error'); 68 | done(); 69 | }); 70 | }, 2000); 71 | } 72 | ); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/unit-storage/lib/db/ticketing-user-role.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const expect = chai.expect; 5 | 6 | describe('The TicketingUserRole model', function() { 7 | let TicketingUserRole, ObjectId, mongoose; 8 | 9 | beforeEach(function(done) { 10 | mongoose = this.moduleHelpers.dependencies('db').mongo.mongoose; 11 | ObjectId = mongoose.Types.ObjectId; 12 | 13 | require(this.testEnv.backendPath + '/lib/db/ticketing-user-role')(this.moduleHelpers.dependencies); 14 | TicketingUserRole = mongoose.model('TicketingUserRole'); 15 | 16 | this.connectMongoose(mongoose, done); 17 | }); 18 | 19 | afterEach(function(done) { 20 | delete mongoose.connection.models.TicketingUserRole; 21 | this.helpers.mongo.dropDatabase(err => { 22 | if (err) return done(err); 23 | this.testEnv.core.db.mongo.mongoose.connection.close(done); 24 | }); 25 | }); 26 | 27 | function saveTicketingUserRole(userRoleJson, callback) { 28 | const userRole = new TicketingUserRole(userRoleJson); 29 | 30 | return userRole.save(callback); 31 | } 32 | 33 | describe('The role field', function() { 34 | it('should not save TicketingUserRole if role is invalid', function(done) { 35 | const userRoleJson = { 36 | user: new ObjectId(), 37 | role: 'invalid-role' 38 | }; 39 | 40 | saveTicketingUserRole(userRoleJson, err => { 41 | expect(err).to.exist; 42 | expect(err.errors.role.message).to.equal('Invalid TicketingUser role'); 43 | done(); 44 | }); 45 | }); 46 | 47 | it('should save TicketingUserRole if all fields are valid', function(done) { 48 | const userRoleJson = { 49 | user: new ObjectId(), 50 | role: 'user' 51 | }; 52 | 53 | saveTicketingUserRole(userRoleJson, (err, savedUserRole) => { 54 | expect(err).to.not.exist; 55 | expect(savedUserRole.role).to.equal(userRoleJson.role); 56 | done(); 57 | }); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /backend/lib/config/index.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | rights: { 3 | padmin: 'rw', 4 | admin: 'rw' 5 | }, 6 | configurations: { 7 | frontendUrl: 'http://localhost:8080', 8 | mail: { 9 | value: { 10 | replyto: 'ossa-dev@linagora.com', 11 | noreply: 'noreply-dev@linagora.com', 12 | support: 'ossa-dev@linagora.com' 13 | } 14 | }, 15 | ssp: { 16 | value: { 17 | sspUrl: 'https://ssp.08000linux.com/', 18 | sspUrlReset: 'https://ssp.08000linux.com/?action=sendtoken', 19 | isSspEnabled: true 20 | } 21 | }, 22 | limesurvey: { 23 | value: { 24 | apiUrl: 'http://limesurvey.localhost:8080/admin/remotecontrol/', 25 | surveyId: 158386, 26 | username: 'username', 27 | password: 'password', 28 | limesurveyUrl: 'http://limesurvey.localhost:8080/' 29 | } 30 | }, 31 | lininfosec: { 32 | value: { 33 | apiUrl: 'http://lininfosec.localhost:9999', 34 | lininfosec_auth_token: 'LinagoraR7', 35 | author: { 36 | id: '5f3a805b7aa11a5db50a39c2', 37 | name: 'Amy WOLSH', 38 | email: 'amy.wolsh@open-paas.org', 39 | type: 'beneficiary', 40 | phone: '' 41 | } 42 | } 43 | }, 44 | features: { 45 | rights: { 46 | padmin: 'rw', 47 | admin: 'rw', 48 | user: 'r' 49 | }, 50 | value: { 51 | isLimesurveyEnabled: false, 52 | isDashboardEnabled: false, 53 | isLinInfosecEnabled: false 54 | } 55 | }, 56 | language: { 57 | rights: { 58 | user: 'rw' 59 | }, 60 | value: { 61 | defaultLanguage: 'en' 62 | } 63 | 64 | } 65 | } 66 | }; 67 | 68 | module.exports = dependencies => { 69 | const esnConfig = dependencies('esn-config'); 70 | 71 | return { 72 | register 73 | }; 74 | 75 | function register() { 76 | esnConfig.registry.register('smartsla-backend', config); 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /config/elasticsearch/contracts.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "analysis": { 4 | "filter": { 5 | "nGram_filter": { 6 | "type": "nGram", 7 | "min_gram": 1, 8 | "max_gram": 20, 9 | "token_chars": [ 10 | "letter", 11 | "digit", 12 | "punctuation", 13 | "symbol" 14 | ] 15 | } 16 | }, 17 | "analyzer": { 18 | "nGram_analyzer": { 19 | "type": "custom", 20 | "tokenizer": "whitespace", 21 | "filter": [ 22 | "lowercase", 23 | "asciifolding", 24 | "nGram_filter" 25 | ] 26 | }, 27 | "whitespace_analyzer": { 28 | "type": "custom", 29 | "tokenizer": "whitespace", 30 | "filter": [ 31 | "lowercase", 32 | "asciifolding" 33 | ] 34 | } 35 | } 36 | } 37 | }, 38 | "mappings": { 39 | "contracts": { 40 | "properties": { 41 | "title": { 42 | "type": "string", 43 | "analyzer": "nGram_analyzer", 44 | "search_analyzer": "whitespace_analyzer", 45 | "fields": { 46 | "sort": { 47 | "type": "string", 48 | "index": "not_analyzed" 49 | } 50 | } 51 | }, 52 | "organization": { 53 | "properties": { 54 | "_id": { 55 | "type": "string", 56 | "index": "not_analyzed" 57 | } 58 | } 59 | }, 60 | "software": { 61 | "type": "nested", 62 | "include_in_parent": true, 63 | "properties": { 64 | "template" : { 65 | "properties": { 66 | "_id": { 67 | "type": "string", 68 | "index": "not_analyzed" 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/unit-backend/lib/filter.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | 3 | describe('The filter module', function() { 4 | let moduleHelpers, filterModule, filterList, ticketingUser; 5 | 6 | beforeEach(function() { 7 | moduleHelpers = this.moduleHelpers; 8 | filterModule = require(moduleHelpers.backendPath + '/lib/filter'); 9 | filterList = require(moduleHelpers.backendPath + '/lib/filter/constants').FILTER_LIST; 10 | ticketingUser = { 11 | user: '5e204f99cdc2b21444f07bdd', 12 | _id: '5e204fa9cdc2b21444f07be4', 13 | type: 'expert' 14 | }; 15 | }); 16 | 17 | describe('The list function', function() { 18 | it('should return array of filters', function(done) { 19 | filterModule.list({ticketingUser}) 20 | .then(filters => { 21 | const { type } = ticketingUser; 22 | const filterListExpert = filterList.filter(filter => { 23 | if (filter.rights && !filter.rights.includes(type)) { 24 | return false; 25 | } 26 | 27 | return true; 28 | }); 29 | 30 | expect(filters).to.be.an('array'); 31 | expect(filters.length).to.be.equal(filterListExpert.length); 32 | 33 | done(); 34 | }) 35 | .catch(done); 36 | }); 37 | 38 | it('should return filters with id and name properties', function(done) { 39 | filterModule.list({ticketingUser}) 40 | .then(filters => { 41 | filters.forEach(filter => { 42 | expect(Object.keys(filter)).to.have.members(['_id', 'name']); 43 | 44 | }); 45 | 46 | done(); 47 | }) 48 | .catch(done); 49 | }); 50 | }); 51 | 52 | describe('The getById function', function() { 53 | it('should return filter with id, name, query properties', function(done) { 54 | filterModule.getById('closed') 55 | .then(filter => { 56 | expect(Object.keys(filter)).to.have.members(['_id', 'name', 'query']); 57 | 58 | done(); 59 | }) 60 | .catch(done); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /backend/webserver/api/contribution/middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const composableMw = require('composable-middleware'); 4 | 5 | module.exports = (dependencies, lib) => { 6 | const { requireAdministrator } = require('../helpers')(dependencies, lib); 7 | const { send400Error } = require('../utils')(dependencies); 8 | const { CONTRIBUTION_STATUS_LIST } = require('../constants'); 9 | 10 | return { 11 | canCreateContribution, 12 | canUpdateContribution, 13 | canRemoveContribution, 14 | validateContributionCreatePayload, 15 | validateContributionUpdatePayload, 16 | validateContributionStatusUpdatePayload 17 | }; 18 | 19 | function canCreateContribution(req, res, next) { 20 | return requireAdministrator(req, res, next); 21 | } 22 | 23 | function canUpdateContribution(req, res, next) { 24 | return requireAdministrator(req, res, next); 25 | } 26 | 27 | function canRemoveContribution(req, res, next) { 28 | return requireAdministrator(req, res, next); 29 | } 30 | 31 | function validateContributionCreatePayload(req, res, next) { 32 | const middlewares = [ 33 | validateBasicInfo 34 | ]; 35 | 36 | return composableMw(...middlewares)(req, res, next); 37 | } 38 | 39 | function validateContributionUpdatePayload(req, res, next) { 40 | const middlewares = [ 41 | validateBasicInfo 42 | ]; 43 | 44 | return composableMw(...middlewares)(req, res, next); 45 | } 46 | 47 | function validateContributionStatusUpdatePayload(req, res, next) { 48 | const middlewares = [ 49 | validateStatus 50 | ]; 51 | 52 | return composableMw(...middlewares)(req, res, next); 53 | } 54 | function validateBasicInfo(req, res, next) { 55 | const { name, software } = req.body; 56 | 57 | if (!name) { 58 | return send400Error('name is required', res); 59 | } 60 | 61 | if (!software) { 62 | return send400Error('software is required', res); 63 | } 64 | 65 | next(); 66 | } 67 | 68 | function validateStatus(req, res, next) { 69 | const { stepName } = req.body; 70 | 71 | if (!stepName || !CONTRIBUTION_STATUS_LIST.includes(stepName)) { 72 | return send400Error('invalid status', res); 73 | } 74 | 75 | next(); 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /backend/webserver/api/organization/middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (dependencies, lib) => { 4 | const { 5 | requireAdministrator, 6 | validateObjectIds 7 | } = require('../helpers')(dependencies, lib); 8 | const { send400Error } = require('../utils')(dependencies); 9 | 10 | return { 11 | canCreateOrganization, 12 | canListOrganization, 13 | canReadOrganization, 14 | canUpdateOrganization, 15 | validateOrganizationCreatePayload, 16 | validateOrganizationUpdatePayload 17 | }; 18 | 19 | function canCreateOrganization(req, res, next) { 20 | return requireAdministrator(req, res, next); 21 | } 22 | 23 | function canListOrganization(req, res, next) { 24 | return requireAdministrator(req, res, next); 25 | } 26 | 27 | function canReadOrganization(req, res, next) { 28 | return requireAdministrator(req, res, next); 29 | } 30 | 31 | function canUpdateOrganization(req, res, next) { 32 | return requireAdministrator(req, res, next); 33 | } 34 | 35 | function validateOrganizationCreatePayload(req, res, next) { 36 | const { shortName, manager } = req.body; 37 | 38 | if (!shortName) { 39 | return send400Error('shortName is required', res); 40 | } 41 | 42 | if (manager && !validateObjectIds(manager)) { 43 | return send400Error('manager is invalid', res); 44 | } 45 | 46 | lib.organization.getByShortName(shortName) 47 | .then(organization => { 48 | if (organization) { 49 | return send400Error('shortName is taken', res); 50 | } 51 | 52 | next(); 53 | }); 54 | } 55 | 56 | function validateOrganizationUpdatePayload(req, res, next) { 57 | const { shortName, manager } = req.body; 58 | 59 | if (!shortName) { 60 | return send400Error('shortName is required', res); 61 | } 62 | 63 | if (manager && !validateObjectIds(manager)) { 64 | return send400Error('manager is invalid', res); 65 | } 66 | 67 | lib.organization.getByShortName(shortName) 68 | .then(organization => { 69 | if (organization && organization._id.toString() !== req.params.id) { 70 | return send400Error('shortName is taken', res); 71 | } 72 | 73 | next(); 74 | }); 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /backend/lib/db/schemas/contribution.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dependencies => { 4 | const mongoose = dependencies('db').mongo.mongoose; 5 | const Schema = mongoose.Schema; 6 | const CounterModel = mongoose.model('Counter'); 7 | 8 | const LinkSchema = new Schema({ 9 | name: { type: String }, 10 | url: { type: String } 11 | }, { _id: false }); 12 | 13 | const StatusSchema = new Schema({ 14 | develop: { type: Date, default: null }, 15 | reversed: { type: Date, default: null }, 16 | published: { type: Date, default: null }, 17 | integrated: { type: Date, default: null }, 18 | rejected: { type: Date, default: null } 19 | }, { _id: false }); 20 | 21 | const ContributionSchema = new mongoose.Schema({ 22 | _id: { type: Number }, 23 | name: { type: String, required: true }, 24 | software: { type: mongoose.Schema.ObjectId, ref: 'Software', required: true }, 25 | author: { type: String }, 26 | type: { type: String}, 27 | version: { type: String }, 28 | fixedInVersion: { type: String }, 29 | status: { type: StatusSchema, default: StatusSchema}, 30 | description: { type: String }, 31 | deposedAt: { type: String }, 32 | links: [LinkSchema], 33 | timestamps: { 34 | creation: { type: Date, default: Date.now }, 35 | updatedAt: { type: Date } 36 | }, 37 | schemaVersion: { type: Number, default: 1 } 38 | }); 39 | 40 | ContributionSchema.pre('save', function(next) { 41 | const self = this; 42 | 43 | if (this.isNew) { 44 | CounterModel.findOneAndUpdate({ _id: 'contribution'}, { $inc: { seq: 1 } }, { upsert: true, new: true }, 45 | (err, counter) => { 46 | if (err) { 47 | return next(err); 48 | } 49 | self._id = counter.seq; 50 | 51 | next(); 52 | }); 53 | } else { 54 | next(); 55 | } 56 | }); 57 | 58 | ContributionSchema.pre('update', function(next) { 59 | const self = this; 60 | const set = self._update.$set || {}; 61 | 62 | if (set.timestamps) { 63 | set.timestamps.updatedAt = Date.now(); 64 | } else { 65 | self._update.$set = Object.assign(set, { 'timestamps.updatedAt': Date.now() }); 66 | } 67 | 68 | next(); 69 | }); 70 | 71 | return ContributionSchema; 72 | }; 73 | -------------------------------------------------------------------------------- /bin/lib/commons.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const q = require('q'); 5 | const fs = require('fs'); 6 | const ESConfiguration = require('esn-elasticsearch-configuration'); 7 | 8 | const readdir = q.denodeify(fs.readdir); 9 | 10 | module.exports = { 11 | exit, 12 | loadMongooseModels, 13 | logInfo, 14 | logError, 15 | getDBOptions, 16 | getESConfiguration, 17 | runCommand 18 | }; 19 | 20 | function log(level, ...message) { 21 | console.log('[CLI]', level, ...message); 22 | } 23 | 24 | function logInfo(...message) { 25 | log('INFO', ...message); 26 | } 27 | 28 | function logError(...message) { 29 | log('ERROR', ...message); 30 | } 31 | 32 | function getDBOptions() { 33 | const dbConfigFilePath = path.normalize(`${__dirname}/../../config/db.json`); 34 | const dbConfig = fs.readFileSync(dbConfigFilePath, 'utf8'); 35 | 36 | return JSON.parse(dbConfig); 37 | } 38 | 39 | function exit(code) { 40 | process.exit(code); // eslint-disable-line no-process-exit 41 | } 42 | 43 | function runCommand(name, command) { 44 | return command().then(() => { 45 | logInfo(`Command "${name}" terminated successfully`); 46 | 47 | exit(); 48 | }, err => { 49 | logError(`Command "${name}" returned an error: ${err}`); 50 | 51 | exit(1); 52 | }); 53 | } 54 | 55 | function loadMongooseModels() { 56 | var ESN_ROOT = path.resolve(__dirname, '../../'); 57 | var MODELS_ROOT = path.resolve(ESN_ROOT, 'backend/lib/db'); 58 | 59 | return readdir(MODELS_ROOT).then(function(files) { 60 | files.forEach(function(filename) { 61 | var file = path.resolve(MODELS_ROOT, filename); 62 | 63 | if (fs.statSync(file).isFile()) { 64 | require(file); 65 | } 66 | }); 67 | }); 68 | } 69 | 70 | function getESConfiguration(options) { 71 | const elasticsearchConfigPath = path.normalize(`${__dirname}/../../config/elasticsearch/`); 72 | const esOptions = { 73 | host: options.host || 'localhost', 74 | port: options.port || 9200, 75 | path: elasticsearchConfigPath 76 | }; 77 | 78 | if (options.type) { 79 | if (!fs.existsSync(path.resolve([elasticsearchConfigPath, options.type, '.json'].join('')))) { 80 | delete esOptions.path; 81 | } 82 | } 83 | 84 | return new ESConfiguration(esOptions); 85 | } 86 | -------------------------------------------------------------------------------- /test/unit-storage/lib/db/contract.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const expect = chai.expect; 5 | 6 | describe('The Contract model', function() { 7 | let Contract, ObjectId, mongoose; 8 | 9 | beforeEach(function(done) { 10 | mongoose = this.moduleHelpers.dependencies('db').mongo.mongoose; 11 | ObjectId = mongoose.Types.ObjectId; 12 | 13 | require(this.testEnv.backendPath + '/lib/db/ticketing-user-contract')( 14 | this.moduleHelpers.dependencies 15 | ); 16 | require(this.testEnv.backendPath + '/lib/db/contract')( 17 | this.moduleHelpers.dependencies 18 | ); 19 | Contract = mongoose.model('Contract'); 20 | 21 | this.connectMongoose(mongoose, done); 22 | }); 23 | 24 | afterEach(function(done) { 25 | delete mongoose.connection.models.Contract; 26 | delete mongoose.connection.models.TicketingUserContract; 27 | this.helpers.mongo.dropDatabase(err => { 28 | if (err) return done(err); 29 | this.testEnv.core.db.mongo.mongoose.connection.close(done); 30 | }); 31 | }); 32 | 33 | function saveContract(ContractJson, callback) { 34 | const MyContract = new Contract(ContractJson); 35 | 36 | return MyContract.save(callback); 37 | } 38 | 39 | describe('The Contract model', function() { 40 | it('should store a contract with valid fields', function(done) { 41 | saveContract( 42 | { 43 | endDate: '2000-12-31', 44 | client: 'Foo', 45 | clientId: new ObjectId(), 46 | name: 'Bar', 47 | startDate: '2000-01-01', 48 | timezone: 'Europe/Paris' 49 | }, 50 | 51 | err => { 52 | expect(err).to.not.exist; 53 | done(); 54 | } 55 | ); 56 | }); 57 | it('should not store a contract when name field is missing', function(done) { 58 | saveContract( 59 | { 60 | endDate: '2000-12-31', 61 | client: 'Bar', 62 | clientId: new ObjectId(), 63 | startDate: '2000-01-01', 64 | timezone: 'Europe/Paris' 65 | }, 66 | 67 | err => { 68 | expect(err).to.exist; 69 | expect(err.message).to.contain('`name` is required'); 70 | done(); 71 | } 72 | ); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /backend/templates/email/includes/attachments.pug: -------------------------------------------------------------------------------- 1 | - let attachmentsMessage = content.latestEvent.attachments.length > 1 ? `See attachments` : 'See attachment'; 2 | table.icons_block(width='100%' border='0' cellpadding='0' cellspacing='0' role='presentation' style='mso-table-lspace: 0pt; mso-table-rspace: 0pt;') 3 | tr 4 | td(style='vertical-align: middle; color: #000000; font-family: inherit; font-size: 14px; text-align: left;') 5 | table(width='100%' cellpadding='0' cellspacing='0' role='presentation' style='mso-table-lspace: 0pt; mso-table-rspace: 0pt;') 6 | tr 7 | td(style='vertical-align: middle; text-align: left;') 8 | //if vml 9 | table(align='left' cellpadding='0' cellspacing='0' role='presentation' style='display:inline-block;padding-left:0px;padding-right:0px;mso-table-lspace: 0pt;mso-table-rspace: 0pt;') 10 | // [if !vml] { 22 | if (err) { 23 | return done(err); 24 | } 25 | 26 | user1 = models.users[1]; 27 | user2 = models.users[2]; 28 | 29 | lib.ticketingUserRole.create({ 30 | user: user1._id, 31 | role: 'administrator' 32 | }) 33 | .then(() => done()) 34 | .catch(err => done(err)); 35 | }); 36 | }); 37 | 38 | afterEach(function(done) { 39 | helpers.mongo.dropDatabase(err => done(err)); 40 | }); 41 | 42 | it('should respond 401 if not logged in', function(done) { 43 | helpers.api.requireLogin(app, 'get', '/ticketing/api/user/role', done); 44 | }); 45 | 46 | it('should respond 404 if user not found', function(done) { 47 | helpers.api.loginAsUser(app, user2.emails[0], password, helpers.callbacks.noErrorAnd(requestAsMember => { 48 | const req = requestAsMember(request(app).get('/ticketing/api/user/role')); 49 | 50 | req.expect(404) 51 | .end(helpers.callbacks.noErrorAnd(res => { 52 | expect(res.body).to.deep.equal({ 53 | error: { code: 404, message: 'Not Found', details: 'User not found' } 54 | }); 55 | done(); 56 | })); 57 | })); 58 | }); 59 | 60 | it('should respond 200 with user role', function(done) { 61 | helpers.api.loginAsUser(app, user1.emails[0], password, helpers.callbacks.noErrorAnd(requestAsMember => { 62 | const req = requestAsMember(request(app).get('/ticketing/api/user/role')); 63 | 64 | req.expect(200) 65 | .end(helpers.callbacks.noErrorAnd(res => { 66 | expect(res.body).to.equal('administrator'); 67 | done(); 68 | })); 69 | })); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/definitions/ticket.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * definition: 4 | * ticket_event: 5 | * description: a ticket event 6 | * type: object 7 | * properties: 8 | * id: 9 | * type: string 10 | * name: 11 | * type: string 12 | * type: 13 | * type: string 14 | * id_name_email_type: 15 | * description: Object containing name, id, type and email of user 16 | * type: object 17 | * properties: 18 | * id: 19 | * type: number 20 | * name: 21 | * type: string 22 | * email: 23 | * type: string 24 | * type: 25 | * type: string 26 | * ticket_content: 27 | * description: Ticket object 28 | * type: object 29 | * properties: 30 | * _id: 31 | * type: number 32 | * assignedTo: 33 | * $ref: "#/definitions/id_name_email_type" 34 | * author: 35 | * $ref: "#/definitions/id_name_email_type" 36 | * beneficiary: 37 | * $ref: "#/definitions/id_name_email_type" 38 | * callNumber: 39 | * type: string 40 | * contract: 41 | * type: string 42 | * createdDuringBusinessHours: 43 | * type: boolean 44 | * description: 45 | * type: string 46 | * events: 47 | * type: array 48 | * items: 49 | * $ref: "#/definitions/ticket_event" 50 | * meetingId: 51 | * type: string 52 | * participants: 53 | * type: array 54 | * items: 55 | * type: string 56 | * relatedRequests: 57 | * type: array 58 | * items: 59 | * type: object 60 | * relatedContributions: 61 | * type: array 62 | * items: 63 | * $ref: "#/definitions/contribution_content" 64 | * responsible: 65 | * $ref: "#/definitions/id_name_email_type" 66 | * severity: 67 | * type: string 68 | * software: 69 | * $ref: "#/definitions/contract_software" 70 | * status: 71 | * type: string 72 | * team: 73 | * type: object 74 | * timestamps: 75 | * type: object 76 | * properties: 77 | * createdAt: 78 | * type: string 79 | * updatedAt: 80 | * type: string 81 | * title: 82 | * type: string 83 | * type: 84 | * type: string 85 | */ 86 | -------------------------------------------------------------------------------- /backend/lib/contract/search.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const Q = require('q'); 5 | const { DEFAULT_LIST_OPTIONS, INDICES } = require('../constants'); 6 | 7 | module.exports = dependencies => { 8 | const coreElasticsearch = dependencies('coreElasticsearch'); 9 | 10 | return search; 11 | 12 | /** 13 | * Search contracts in system. 14 | * 15 | * @param {object} options - Hash with: 16 | * - 'limit' and 'offset' for pagination 17 | * - 'search' for filtering terms 18 | * Search can be a single string, an array of strings which will be joined, or a space separated string list. 19 | * In the case of array or space separated string, a AND search will be performed with the input terms. 20 | * @return {Promise} Resolve on success with result: { total_count: number, list: [Contract1, Contract2, ...] } 21 | */ 22 | function search(options) { 23 | return Q.nfcall(_search, options); 24 | } 25 | 26 | function _search(options = {}, callback) { 27 | options.limit = +options.limit || DEFAULT_LIST_OPTIONS.LIMIT; 28 | options.offset = +options.offset || DEFAULT_LIST_OPTIONS.OFFSET; 29 | 30 | if (!options.search) { 31 | return callback(new Error('query.search is mandatory')); 32 | } 33 | 34 | return coreElasticsearch.client((err, elascticsearchClient) => { 35 | if (err) { 36 | return callback(err); 37 | } 38 | 39 | const elasticsearchQuery = { 40 | sort: [ 41 | {'title.sort': 'asc'} 42 | ], 43 | query: { 44 | bool: { 45 | must: { 46 | match: { 47 | title: options.search 48 | } 49 | } 50 | } 51 | } 52 | }; 53 | 54 | return elascticsearchClient.search({ 55 | index: INDICES.CONTRACT.name, 56 | type: INDICES.CONTRACT.type, 57 | from: options.offset, 58 | size: options.limit, 59 | body: elasticsearchQuery 60 | }, (err, response) => { 61 | if (err) { 62 | return callback(err); 63 | } 64 | 65 | const list = response.hits.hits; 66 | const contracts = list.map(hit => _.extend(hit._source, { _id: hit._source.id })); 67 | 68 | return callback(null, { 69 | total_count: response.hits.total, 70 | list: contracts 71 | }); 72 | }); 73 | }); 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /backend/lib/limesurvey/limesurvey.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | module.exports = dependencies => { 4 | const EsnConfig = dependencies('esn-config').EsnConfig; 5 | const logger = dependencies('logger'); 6 | 7 | /** 8 | * Get limesurvey configuration. 9 | * 10 | * @return {Promise} resolve on success 11 | */ 12 | function getConfig() { 13 | return new EsnConfig('smartsla-backend') 14 | .get('limesurvey') 15 | .then(config => { 16 | if (config && config.apiUrl) { 17 | return config; 18 | } 19 | 20 | logger.warn('No "limesurvey" configuration has been found'); 21 | 22 | throw new Error('No "limesurvey" configuration has been found'); 23 | }); 24 | } 25 | 26 | /** 27 | * Generate a unqiue secure token for the issue 28 | * @return {String} - Participants token 29 | */ 30 | function generateToken(ticketId) { 31 | return `${Date.now()}_${ticketId}`; 32 | } 33 | 34 | /** 35 | * Get session key 36 | * @return {Promise} - Resolve on success 37 | */ 38 | function getSessionKey(apiUrl, surveyId, credentials) { 39 | return axios.post(apiUrl, { 40 | method: 'get_session_key', 41 | params: credentials, 42 | id: surveyId 43 | }); 44 | } 45 | 46 | /** 47 | * Add participants to the survey 48 | * @return {Promise} - Resolve on success 49 | */ 50 | function createSurvey(ticketId) { 51 | return getConfig().then(config => { 52 | const { surveyId, apiUrl, username, password} = config; 53 | const generatedToken = generateToken(ticketId); 54 | let sessionKey = null; 55 | 56 | return new Promise(resolve => { 57 | getSessionKey(apiUrl, surveyId, [username, password]) 58 | .then(({ data }) => { 59 | sessionKey = data.result; 60 | axios 61 | .post(apiUrl, { 62 | method: 'add_participants', 63 | params: [sessionKey, surveyId, [{ token: generatedToken }], false] 64 | }) 65 | .then(() => resolve({ id: surveyId, token: generatedToken })); 66 | }) 67 | .catch(e => { 68 | logger.warn('Unable to reach limesurvey server', e); 69 | resolve(); 70 | }); 71 | }); 72 | }) 73 | .catch(err => err); 74 | } 75 | 76 | return { 77 | createSurvey 78 | }; 79 | }; 80 | -------------------------------------------------------------------------------- /test/unit-storage/lib/db/software.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const expect = chai.expect; 5 | 6 | describe('The sofware model', function() { 7 | let Software, mongoose; 8 | 9 | beforeEach(function(done) { 10 | mongoose = this.moduleHelpers.dependencies('db').mongo.mongoose; 11 | 12 | require(this.testEnv.backendPath + '/lib/db/software')( 13 | this.moduleHelpers.dependencies 14 | ); 15 | Software = mongoose.model('Software'); 16 | 17 | this.connectMongoose(mongoose, done); 18 | }); 19 | 20 | afterEach(function(done) { 21 | delete mongoose.connection.models.Software; 22 | this.helpers.mongo.dropDatabase(err => { 23 | if (err) return done(err); 24 | this.testEnv.core.db.mongo.mongoose.connection.close(done); 25 | }); 26 | }); 27 | 28 | function saveSoftware(sofwareJson, callback) { 29 | const sofware = new Software(sofwareJson); 30 | 31 | return sofware.save(callback); 32 | } 33 | 34 | describe('The name field', function() { 35 | it('should not store a sofware which has an already taken name', function(done) { 36 | saveSoftware( 37 | { 38 | name: 'foo', 39 | category: 'bar', 40 | versions: ['1'] 41 | }, 42 | err => { 43 | expect(err).to.not.exist; 44 | 45 | const software = { 46 | name: 'foo', 47 | category: 'bar', 48 | versions: ['1'] 49 | }; 50 | 51 | setTimeout(function() { 52 | saveSoftware(software, err => { 53 | expect(err).to.exist; 54 | expect(err.message).to.contain('duplicate key error'); 55 | done(); 56 | }); 57 | }, 2000); 58 | } 59 | ); 60 | }); 61 | 62 | /* it('should store the sofware which has name is taken by itself', function(done) { 63 | saveSoftware({ 64 | name: 'foo', 65 | category: 'bar', 66 | versions: ['1'] 67 | }, (err, createdSofware) => { 68 | expect(err).to.not.exist; 69 | const software = Object.assign(createdSofware, { category: 'bazzz' }); 70 | 71 | saveSoftware(software, (err, storedSoftware) => { 72 | expect(err).to.not.exist; 73 | expect(storedSoftware.category).to.equal(software.category); 74 | done(); 75 | }); 76 | }); 77 | }); */ 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /backend/webserver/api/software/middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const composableMw = require('composable-middleware'); 4 | 5 | module.exports = (dependencies, lib) => { 6 | const { requireAdministrator } = require('../helpers')(dependencies, lib); 7 | const { send400Error, send500Error } = require('../utils')(dependencies); 8 | 9 | return { 10 | canCreateSoftware, 11 | canListSoftware, 12 | canUpdateSoftware, 13 | validateSoftwareCreatePayload, 14 | validateSoftwareUpdatePayload 15 | }; 16 | 17 | function canCreateSoftware(req, res, next) { 18 | return requireAdministrator(req, res, next); 19 | } 20 | 21 | function canListSoftware(req, res, next) { 22 | next(); // TODO Improve permissions 23 | } 24 | 25 | function canUpdateSoftware(req, res, next) { 26 | return requireAdministrator(req, res, next); 27 | } 28 | 29 | function validateSoftwareCreatePayload(req, res, next) { 30 | const middlewares = [ 31 | validateBasicInfo, 32 | uniqueSoftwareNameToCreate 33 | ]; 34 | 35 | return composableMw(...middlewares)(req, res, next); 36 | } 37 | 38 | function validateSoftwareUpdatePayload(req, res, next) { 39 | const middlewares = [ 40 | validateBasicInfo, 41 | uniqueSoftwareNameToUpdate 42 | ]; 43 | 44 | return composableMw(...middlewares)(req, res, next); 45 | } 46 | 47 | function validateBasicInfo(req, res, next) { 48 | const { name } = req.body; 49 | 50 | if (!name) { 51 | return send400Error('name is required', res); 52 | } 53 | 54 | next(); 55 | } 56 | 57 | function uniqueSoftwareNameToCreate(req, res, next) { 58 | const { name } = req.body; 59 | 60 | lib.software.getByName(name) 61 | .then(software => { 62 | if (software) { 63 | return send400Error('name is taken', res); 64 | } 65 | 66 | next(); 67 | }, err => send500Error('Error while checking softwaretest/unit-storage/lib/db/software.js name', err, res)); 68 | } 69 | 70 | function uniqueSoftwareNameToUpdate(req, res, next) { 71 | const { name } = req.body; 72 | 73 | lib.software.getByName(name) 74 | .then(software => { 75 | if (software && software._id.toString() !== req.params.id) { 76 | return send400Error('name is taken', res); 77 | } 78 | 79 | next(); 80 | }, err => send500Error('Error while checking software name', err, res)); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /doc/dev.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Install 4 | Make sure you have OpenPaaS installed from [here](https://ci.linagora.com/linagora/lgs/openpaas/esn) 5 | 6 | ### Clone module repository 7 | 8 | ```bash 9 | git clone https://ci.linagora.com/linagora/lgs/smartsla/smartsla-backend 10 | ``` 11 | 12 | ### Add the module to OpenPaaS 13 | 14 | Add the line `smartsla-backend` to `esn/config/default.json` in modules section 15 | 16 | ### Install dependencies 17 | 18 | In the `smartsla-backend` folder: 19 | 20 | ```bash 21 | npm install 22 | bower install 23 | ``` 24 | 25 | ### Create module npm link in `esn`: 26 | 27 | In the `smartsla-backend` folder: 28 | 29 | ```bash 30 | npm link 31 | ``` 32 | 33 | In the `esn` folder: 34 | 35 | ```bash 36 | npm link smartsla-backend 37 | ``` 38 | 39 | ### Launch OpenPaaS 40 | 41 | In the `esn` folder: 42 | 43 | ```bash 44 | grunt dev 45 | ``` 46 | 47 | ## Test 48 | 49 | ### Local 50 | 51 | Make sure you have [docker](https://docs.docker.com/engine/installation/) installed 52 | 53 | #### Setup 54 | This step is required for `test-unit-storage` and `test-midway-backend`. 55 | 56 | ##### Create docker services for test 57 | ```bash 58 | docker run --name tic-redis-for-test -d -p 172.17.0.1:6379:6379 redis 59 | docker run --name tic-mongo-for-test -d -p 172.17.0.1:27017:27017 mongo:3.2.0 60 | docker run --name tic-rabbit-for-test -d -p 172.17.0.1:5672:5672 rabbitmq:3.6.5-management 61 | docker run --name tic-es-for-test -d -p 172.17.0.1:9200:9200 elasticsearch:2.3.2 62 | ``` 63 | 64 | ##### Edit file `/etc/hosts`: 65 | Add this line: `172.17.0.1 redis rabbitmq elasticsearch mongo` 66 | 67 | Don't forget stop those services when test done. 68 | 69 | ```bash 70 | docker stop tic-redis-for-test tic-mongo-for-test tic-rabbit-for-test tic-es-for-test 71 | ``` 72 | 73 | Then you can start them for next test without create services. 74 | ```bash 75 | docker start tic-redis-for-test tic-mongo-for-test tic-rabbit-for-test tic-es-for-test 76 | ``` 77 | 78 | #### Test 79 | 80 | - Test unit frontend: 81 | ```bash 82 | grunt test-unit-frontend 83 | ``` 84 | - Test unit backend: 85 | ```bash 86 | grunt test-unit-backend 87 | ``` 88 | - Test unit storage: 89 | ```bash 90 | grunt test-unit-storage 91 | ``` 92 | - Test midway-backend: 93 | ```bash 94 | grunt test-midway-backend 95 | ``` 96 | - Test linters: 97 | ```bash 98 | grunt linters 99 | ``` 100 | - Test all: 101 | ```bash 102 | grunt test --chunk=1 103 | ``` 104 | -------------------------------------------------------------------------------- /test/midway-backend/fixtures/deployments.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | ticketingModule, 5 | ticketingUsers 6 | }; 7 | 8 | function ticketingUsers() { 9 | return [ 10 | { 11 | firstname: 'admin', 12 | lastname: 'admin', 13 | accounts: [{ 14 | type: 'email', 15 | emails: ['admin@tic.org'], 16 | hosted: true 17 | }], 18 | main_phone: '3333', 19 | role: 'administrator', 20 | password: 'secret' 21 | }, 22 | { 23 | firstname: 'supporter', 24 | lastname: 'supporter', 25 | accounts: [{ 26 | type: 'email', 27 | emails: ['supporter@tic.org'], 28 | hosted: true 29 | }], 30 | main_phone: '4444', 31 | role: 'supporter', 32 | password: 'secret' 33 | }, 34 | { 35 | firstname: 'user1', 36 | lastname: 'user1', 37 | accounts: [{ 38 | type: 'email', 39 | emails: ['user1@tic.org'], 40 | hosted: true 41 | }], 42 | main_phone: '5555', 43 | password: 'secret' 44 | }, 45 | { 46 | firstname: 'user2', 47 | lastname: 'user2', 48 | accounts: [{ 49 | type: 'email', 50 | emails: ['user2@tic.org'], 51 | hosted: true 52 | }], 53 | main_phone: '6666', 54 | password: 'secret' 55 | } 56 | ]; 57 | } 58 | 59 | function ticketingModule() { 60 | return { 61 | domain: { 62 | name: 'ticketing-domain', 63 | hostnames: [ 64 | 'localhost', 65 | '127.0.0.1' 66 | ], 67 | company_name: 'linagora' 68 | }, 69 | users: [ 70 | { 71 | password: 'secret', 72 | firstname: 'Domain ', 73 | lastname: 'Administrator', 74 | accounts: [{ 75 | type: 'email', 76 | hosted: true, 77 | emails: ['itadmin@ticketing.net'] 78 | }] 79 | }, 80 | { 81 | password: 'secret', 82 | firstname: 'John', 83 | lastname: 'Doe', 84 | accounts: [{ 85 | type: 'email', 86 | hosted: true, 87 | emails: ['jdoe@ticketing.net'] 88 | }] 89 | }, 90 | { 91 | password: 'secret', 92 | firstname: 'Foo', 93 | lastname: 'Bar', 94 | accounts: [{ 95 | type: 'email', 96 | hosted: true, 97 | emails: ['foobar@ticketing.net'] 98 | }] 99 | } 100 | ] 101 | }; 102 | } 103 | -------------------------------------------------------------------------------- /doc/REST_API/swagger/responses/software.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * response: 4 | * software: 5 | * description: Ok with the software object 6 | * schema: 7 | * $ref: "#/definitions/software_content" 8 | * examples: 9 | * applications/json: 10 | * { 11 | * "timestamps": { 12 | * "creation": "2020-03-26T09:36:41.373Z" 13 | * }, 14 | * "_id": "5e7c77a93687003605f593fa", 15 | * "name": "Moodle OpenHackademy", 16 | * "summary": "Application Moodle et paramétrages/plugins spécifiques pour le site OpenHackademy du ministère du bien être", 17 | * "description": "Application d'apprentissage en ligne basée sur Moodle", 18 | * "licence": "GNU General Public License (GPL)", 19 | * "technology": "PHP", 20 | * "externalLinks": [], 21 | * "group": " Serveurs d'applications" 22 | * } 23 | * software_list: 24 | * description: 25 | * schema: 26 | * type: array 27 | * items: 28 | * $ref: "#/definitions/software_content" 29 | * examples: 30 | * application/json: 31 | * [ 32 | * { 33 | * "timestamps": { 34 | * "creation": "2020-03-26T09:36:41.373Z" 35 | * }, 36 | * "_id": "5e7c77a93687003605f593fa", 37 | * "name": "Moodle OpenHackademy", 38 | * "summary": "Application Moodle et paramétrages/plugins spécifiques pour le site OpenHackademy du ministère du bien être", 39 | * "description": "Application d'apprentissage en ligne basée sur Moodle", 40 | * "licence": "GNU General Public License (GPL)", 41 | * "technology": "PHP", 42 | * "externalLinks": [], 43 | * "group": " Serveurs d'applications" 44 | * }, 45 | * { 46 | * "timestamps": { 47 | * "creation": "2020-03-26T09:34:43.253Z" 48 | * }, 49 | * "_id": "5e7c77333687003605f593f8", 50 | * "name": "Drupal", 51 | * "description": "Drupal est un système de gestion de contenu libre et open-source publié sous la licence publique générale GNU et écrit en PHP.", 52 | * "licence": "GNU General Public License (GPL)", 53 | * "technology": "PHP", 54 | * "group": "Outils et langages", 55 | * "externalLinks": [ 56 | * { 57 | * "name": "lien de la communauté", 58 | * "url": "https://www.drupal.org" 59 | * } 60 | * ], 61 | * "summary": "CMS leader" 62 | * } 63 | * ] 64 | */ 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smartsla-backend", 3 | "version": "1.6.6-dev", 4 | "description": "OpenPaaS backend module for SmartSLA", 5 | "main": "index.js", 6 | "devDependencies": { 7 | "@linagora/grunt-i18n-checker": "2.0.5", 8 | "@linagora/grunt-lint-pattern": "0.1.4", 9 | "@linagora/i18n-checker": "2.0.3", 10 | "@linagora/karma-ng-jade2module-preprocessor": "0.5.3", 11 | "chai": "3.5.0", 12 | "chai-as-promised": "5.3.0", 13 | "chai-shallow-deep-equal": "1.4.0", 14 | "eslint": "3.6.0", 15 | "eslint-config-airbnb-base": "8.0.0", 16 | "eslint-config-linagora-esn": "1.0.2", 17 | "eslint-plugin-import": "1.16.0", 18 | "grunt": "^1.1.0", 19 | "grunt-contrib-clean": "1.0.0", 20 | "grunt-contrib-concat": "1.0.1", 21 | "grunt-contrib-watch": "^1.1.0", 22 | "grunt-eslint": "19.0.0", 23 | "grunt-karma": "2.0.0", 24 | "grunt-mocha-cli": "3.0.0", 25 | "grunt-puglint": "1.0.0", 26 | "grunt-swagger-generate": "github:linagora/grunt-swagger-generate", 27 | "karma": "1.7.1", 28 | "karma-chrome-launcher": "2.0.0", 29 | "karma-coverage": "1.1.1", 30 | "karma-firefox-launcher": "1.0.0", 31 | "karma-mocha": "1.1.1", 32 | "karma-phantomjs-launcher": "1.0.2", 33 | "karma-spec-reporter": "0.0.26", 34 | "linagora-rse": "linagora/openpaas-esn#master", 35 | "mocha": "3.1.2", 36 | "mockery": "1.7.0", 37 | "mongoose": "^5.9.18", 38 | "rewire": "2.5.2", 39 | "sinon": "1.17.5", 40 | "sinon-chai": "2.8.0", 41 | "socket.io": "1.2.1", 42 | "supertest": "2.0.0", 43 | "time-grunt": "1.4.0" 44 | }, 45 | "scripts": { 46 | "test": "grunt test" 47 | }, 48 | "repository": { 49 | "type": "git", 50 | "url": "https://ci.linagora.com/linagora/lgs/linagora/lgs/smartsla/smartsla-backend.git" 51 | }, 52 | "author": "Linagora R&D", 53 | "license": "AGPL-3.0", 54 | "engines": { 55 | "node": ">=6.5.0" 56 | }, 57 | "dependencies": { 58 | "async": "2.6.0", 59 | "awesome-module": "1.1.0", 60 | "axios": "^0.19.1", 61 | "composable-middleware": "0.3.0", 62 | "cors": "^2.8.5", 63 | "deep-object-diff": "^1.1.0", 64 | "email-addresses": "3.0.1", 65 | "esn-elasticsearch-configuration": "^1.4.7", 66 | "express": "^4.17.1", 67 | "glob-all": "^3.2.1", 68 | "html-to-text": "5.1.1", 69 | "json2csv": "4.5.4", 70 | "lodash": "^4.17.15", 71 | "moment-business": "^3.0.1", 72 | "moment-timezone": "^0.5.27", 73 | "mongodb": "^3.5.8", 74 | "q": "1.4.1", 75 | "trim": "0.0.1", 76 | "yargs": "^15.3.1" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /backend/lib/ticketing-user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { DEFAULT_LIST_OPTIONS, TICKETING_CONTRACT_ROLES } = require('./constants'); 4 | 5 | module.exports = dependencies => { 6 | const mongoose = dependencies('db').mongo.mongoose; 7 | const TicketingUser = mongoose.model('TicketingUser'); 8 | 9 | return { 10 | create, 11 | list, 12 | getByUser, 13 | listByType, 14 | listByUserIds, 15 | removeById, 16 | updateUserById, 17 | listByClientId 18 | }; 19 | 20 | /** 21 | * Create a TicketingUser 22 | * @param {Object} ticketingUserRole - The ticketingUserRole object 23 | * @param {Promise} - Resolve on success 24 | */ 25 | function create(ticketingUser) { 26 | ticketingUser = ticketingUser instanceof TicketingUser ? ticketingUser : new TicketingUser(ticketingUser); 27 | 28 | return TicketingUser.create(ticketingUser); 29 | } 30 | 31 | /** 32 | * List TicketingUser 33 | * @param {Object} options - The options object, may contain offset and limit 34 | * @param {Promise} - Resolve on success 35 | */ 36 | function list(options = {}) { 37 | return TicketingUser 38 | .find() 39 | .skip(+options.offset || DEFAULT_LIST_OPTIONS.OFFSET) 40 | .limit(+options.limit || DEFAULT_LIST_OPTIONS.LIMIT) 41 | .sort('-timestamps.creation') 42 | .exec(); 43 | } 44 | 45 | function listByType(type) { 46 | return TicketingUser.find({ type }).exec(); 47 | } 48 | 49 | function listByUserIds(userIds) { 50 | return TicketingUser.find({ user: { $in: userIds }}).exec(); 51 | } 52 | 53 | /** 54 | * Get TicketingUser by user ID 55 | * @param {String} userId - ID of user object 56 | * @return {Promise} - Resolve on success 57 | */ 58 | function getByUser(userId) { 59 | return TicketingUser 60 | .findOne({ user: userId }); 61 | } 62 | 63 | /** 64 | * Remove user by ID 65 | */ 66 | function removeById(userId) { 67 | return TicketingUser.remove({ user: userId }).exec(); 68 | } 69 | 70 | /** 71 | * Update TicketingUser by Id 72 | * @param {String} userId - The user identifier 73 | * @param {Object} user - The user object 74 | */ 75 | function updateUserById(userId, user) { 76 | return TicketingUser 77 | .findByIdAndUpdate( 78 | userId, 79 | { $set: user }, 80 | { new: true } 81 | ) 82 | .exec(); 83 | } 84 | 85 | function listByClientId(clientId) { 86 | return TicketingUser.find({ 87 | client: { $in: clientId }, 88 | role: TICKETING_CONTRACT_ROLES.CONTRACT_MANAGER 89 | }).exec(); 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /backend/webserver/api/contract/middleware/demand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const composableMw = require('composable-middleware'); 4 | const _ = require('lodash'); 5 | 6 | module.exports = (dependencies, lib) => { 7 | const GLOSSARY_CATEGORIES = lib.constants.GLOSSARY_CATEGORIES; 8 | const { 9 | send400Error, 10 | send500Error 11 | } = require('../../utils')(dependencies); 12 | 13 | return { 14 | validateDemand 15 | }; 16 | 17 | function validateDemand(req, res, next) { 18 | const { demandType } = req.body; 19 | 20 | if (!demandType) { 21 | return send400Error('Demand type is required', res); 22 | } 23 | 24 | const middlewares = [ 25 | checkGlossariesAvailable, 26 | checkDemandExist 27 | ]; 28 | 29 | return composableMw(middlewares)(req, res, next); 30 | } 31 | 32 | function checkGlossariesAvailable(req, res, next) { 33 | const { 34 | demandType, 35 | softwareType, 36 | issueType 37 | } = req.body; 38 | 39 | const glossaries = [{ 40 | word: demandType, 41 | category: GLOSSARY_CATEGORIES.DEMAND_TYPE 42 | }]; 43 | 44 | if (softwareType) { 45 | glossaries.push({ 46 | word: softwareType, 47 | category: GLOSSARY_CATEGORIES.SOFTWARE_TYPE 48 | }); 49 | } 50 | 51 | if (issueType) { 52 | glossaries.push({ 53 | word: issueType, 54 | category: GLOSSARY_CATEGORIES.ISSUE_TYPE 55 | }); 56 | } 57 | 58 | lib.glossary.findByGlossaries(glossaries) 59 | .then(foundGlossaries => { 60 | if (foundGlossaries.length === glossaries.length) { 61 | return next(); 62 | } 63 | 64 | let notExistGlossaryCategories = glossaries.map(glossary => { 65 | if (!_.find(foundGlossaries, glossary)) { 66 | return glossary.category; 67 | } 68 | }).filter(Boolean); 69 | let message; 70 | 71 | if (notExistGlossaryCategories.length === 1) { 72 | message = `${notExistGlossaryCategories[0]} is unavailable`; 73 | } else { 74 | notExistGlossaryCategories = notExistGlossaryCategories.join(', '); 75 | message = `${notExistGlossaryCategories} are unavailable`; 76 | } 77 | 78 | return send400Error(message, res); 79 | }) 80 | .catch(err => send500Error('Unable to check demand', err, res)); 81 | } 82 | 83 | function checkDemandExist(req, res, next) { 84 | const demands = [...req.contract.demands, req.body]; 85 | 86 | if (!lib.helpers.uniqueDemands(demands)) { 87 | return send400Error('Demand already exists', res); 88 | } 89 | 90 | next(); 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /test/config/mocks/ng-mock-component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable */ 4 | 5 | /** 6 | * @ngdoc service 7 | * @name $componentController 8 | * @description 9 | * A service that can be used to create instances of component controllers. 10 | *
11 | * Be aware that the controller will be instantiated and attached to the scope as specified in 12 | * the component definition object. If you do not provide a `$scope` object in the `locals` param 13 | * then the helper will create a new isolated scope as a child of `$rootScope`. 14 | *
15 | * @param {string} componentName the name of the component whose controller we want to instantiate 16 | * @param {Object} locals Injection locals for Controller. 17 | * @param {Object=} bindings Properties to add to the controller before invoking the constructor. This is used 18 | * to simulate the `bindToController` feature and simplify certain kinds of tests. 19 | * @param {string=} ident Override the property name to use when attaching the controller to the scope. 20 | * @return {Object} Instance of requested controller. 21 | */ 22 | angular.mock.$ComponentControllerProvider = ['$compileProvider', function($compileProvider) { 23 | this.$get = ['$controller', '$injector', '$rootScope', function($controller, $injector, $rootScope) { 24 | return function $componentController(componentName, locals, bindings, ident) { 25 | // get all directives associated to the component name 26 | var directives = $injector.get(componentName + 'Directive'); 27 | // look for those directives that are components 28 | var candidateDirectives = directives.filter(function(directiveInfo) { 29 | // components have controller, controllerAs and restrict:'E' 30 | return directiveInfo.controller && directiveInfo.controllerAs && directiveInfo.restrict === 'E'; 31 | }); 32 | // check if valid directives found 33 | if (candidateDirectives.length === 0) { 34 | throw new Error('No component found'); 35 | } 36 | if (candidateDirectives.length > 1) { 37 | throw new Error('Too many components found'); 38 | } 39 | // get the info of the component 40 | var directiveInfo = candidateDirectives[0]; 41 | // create a scope if needed 42 | locals = locals || {}; 43 | locals.$scope = locals.$scope || $rootScope.$new(true); 44 | return $controller(directiveInfo.controller, locals, bindings, ident || directiveInfo.controllerAs); 45 | }; 46 | }]; 47 | }]; 48 | 49 | angular.module('ngMock').provider({ 50 | $componentController: angular.mock.$ComponentControllerProvider 51 | }); 52 | -------------------------------------------------------------------------------- /test/unit-storage/all.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const path = require('path'); 5 | const mockery = require('mockery'); 6 | const testConfig = require('../config/servers-conf'); 7 | const basePath = path.resolve(__dirname + '/../../node_modules/linagora-rse'); 8 | const backendPath = path.normalize(__dirname + '/../../backend'); 9 | const MODULE_NAME = 'smartsla-backend'; 10 | let rse; 11 | 12 | before(function(done) { 13 | chai.use(require('chai-shallow-deep-equal')); 14 | chai.use(require('sinon-chai')); 15 | chai.use(require('chai-as-promised')); 16 | 17 | this.testEnv = { 18 | serversConfig: testConfig, 19 | basePath: basePath, 20 | backendPath: backendPath, 21 | fixtures: path.resolve(basePath, 'test/midway-backend/fixtures'), 22 | mongoUrl: testConfig.mongodb.connectionString, 23 | initCore(callback) { 24 | rse.core.init(() => { callback && process.nextTick(callback); }); 25 | } 26 | }; 27 | 28 | process.env.NODE_CONFIG = 'test/config'; 29 | process.env.NODE_ENV = 'test'; 30 | process.env.REDIS_HOST = 'redis'; 31 | process.env.REDIS_PORT = 6379; 32 | process.env.AMQP_HOST = 'rabbitmq'; 33 | process.env.ES_HOST = 'elasticsearch'; 34 | 35 | this.connectMongoose = function(mongoose, done) { 36 | mongoose.Promise = require('q').Promise; // http://mongoosejs.com/docs/promises.html 37 | mongoose.connect(this.testEnv.mongoUrl, done); 38 | }; 39 | 40 | rse = require('linagora-rse'); 41 | this.helpers = {}; 42 | 43 | this.testEnv.core = rse.core; 44 | this.testEnv.moduleManager = rse.moduleManager; 45 | rse.test.helpers(this.helpers, this.testEnv); 46 | rse.test.moduleHelpers(this.helpers, this.testEnv); 47 | rse.test.apiHelpers(this.helpers, this.testEnv); 48 | 49 | const manager = this.testEnv.moduleManager.manager; 50 | const nodeModulesPath = path.normalize( 51 | path.join(__dirname, '../../node_modules/') 52 | ); 53 | const nodeModulesLoader = manager.loaders.filesystem(nodeModulesPath, true); 54 | const loader = manager.loaders.code(require('../../index.js'), true); 55 | 56 | manager.appendLoader(nodeModulesLoader); 57 | manager.appendLoader(loader); 58 | loader.load(MODULE_NAME, done); 59 | }); 60 | 61 | beforeEach(function() { 62 | mockery.enable({warnOnReplace: false, warnOnUnregistered: false, useCleanCache: true}); 63 | const depsStore = { 64 | db: require('linagora-rse/backend/core/db') 65 | }; 66 | const dependencies = function(name) { 67 | return depsStore[name]; 68 | }; 69 | 70 | const addDep = (name, dep) => { 71 | depsStore[name] = dep; 72 | }; 73 | 74 | this.moduleHelpers = { 75 | dependencies, 76 | addDep 77 | }; 78 | }); 79 | -------------------------------------------------------------------------------- /test/config/karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MODULE_DIR_NAME = '/smartsla-backend'; 4 | 5 | module.exports = function(config) { 6 | config.set({ 7 | basePath: '../../', 8 | files: [ 9 | 'frontend/components/chai/chai.js', 10 | 'node_modules/chai-shallow-deep-equal/chai-shallow-deep-equal.js', 11 | 'frontend/components/jquery/dist/jquery.min.js', 12 | 'frontend/components/angular/angular.min.js', 13 | 'frontend/components/angular-ui-router/release/angular-ui-router.min.js', 14 | 'frontend/components/angular-mocks/angular-mocks.js', 15 | 'frontend/components/dynamic-directive/dist/dynamic-directive.min.js', 16 | 'frontend/components/angular-component/dist/angular-component.min.js', 17 | 'frontend/components/restangular/dist/restangular.min.js', 18 | 'frontend/components/lodash/dist/lodash.min.js', 19 | 'frontend/components/sinon-chai/lib/sinon-chai.js', 20 | 'frontend/components/sinon-1.15.4/index.js', 21 | 'test/config/mocks/**/*.js', 22 | 'frontend/app/**/*.module.js', 23 | 'frontend/app/**/*.js', 24 | 'frontend/app/**/*.pug' 25 | ], 26 | exclude: [ 27 | 'frontend/app/**/*.run.js' 28 | ], 29 | frameworks: ['mocha'], 30 | colors: true, 31 | singleRun: true, 32 | autoWatch: true, 33 | browsers: ['PhantomJS', 'Chrome', 'Firefox'], 34 | reporters: ['coverage', 'spec'], 35 | preprocessors: { 36 | 'frontend/app/**/*.js': ['coverage'], 37 | '**/*.pug': ['ng-jade2module'] 38 | }, 39 | 40 | plugins: [ 41 | 'karma-phantomjs-launcher', 42 | 'karma-chrome-launcher', 43 | 'karma-firefox-launcher', 44 | 'karma-mocha', 45 | 'karma-coverage', 46 | 'karma-spec-reporter', 47 | '@linagora/karma-ng-jade2module-preprocessor' 48 | ], 49 | 50 | coverageReporter: { type: 'text', dir: '/tmp' }, 51 | 52 | ngJade2ModulePreprocessor: { 53 | stripPrefix: 'frontend', 54 | prependPrefix: MODULE_DIR_NAME, 55 | cacheIdFromPath: function(filepath) { 56 | var cacheId = filepath.replace(/.pug$/, '.html').replace(/^frontend/, '/ticketing'); 57 | 58 | return cacheId; 59 | }, 60 | // setting this option will create only a single module that contains templates 61 | // from all the files, so you can load them all with module('templates') 62 | jadeRenderOptions: { 63 | basedir: require('path').resolve(__dirname, '../../node_modules/linagora-rse/frontend/views') 64 | }, 65 | jadeRenderLocals: { 66 | __: function(str) { 67 | return str; 68 | } 69 | }, 70 | moduleName: 'jadeTemplates' 71 | } 72 | 73 | }); 74 | }; 75 | -------------------------------------------------------------------------------- /backend/lib/software/search.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const Q = require('q'); 5 | const { DEFAULT_LIST_OPTIONS, INDICES } = require('../constants'); 6 | 7 | module.exports = dependencies => { 8 | const coreElasticsearch = dependencies('coreElasticsearch'); 9 | 10 | return search; 11 | 12 | /** 13 | * Search software in system. 14 | * 15 | * @param {object} options - Hash with: 16 | * - 'limit' and 'offset' for pagination 17 | * - 'search' for filtering terms 18 | * - 'excludedIds' for excluded document ids 19 | * Search can be a single string, an array of strings which will be joined, or a space separated string list. 20 | * In the case of array or space separated string, a AND search will be performed with the input terms. 21 | * @return {Promise} Resolve on success with result: { total_count: number, list: [Software1, Software2, ...] } 22 | */ 23 | function search(options) { 24 | return Q.nfcall(_search, options); 25 | } 26 | 27 | function _search(options = {}, callback) { 28 | options.limit = +options.limit || DEFAULT_LIST_OPTIONS.LIMIT; 29 | options.offset = +options.offset || DEFAULT_LIST_OPTIONS.OFFSET; 30 | 31 | if (!options.search) { 32 | return callback(new Error('query.search is mandatory')); 33 | } 34 | 35 | return coreElasticsearch.client((err, elascticsearchClient) => { 36 | if (err) { 37 | return callback(err); 38 | } 39 | 40 | const elasticsearchQuery = { 41 | sort: [ 42 | {'name.sort': 'asc'} 43 | ], 44 | query: { 45 | bool: { 46 | filter: { 47 | bool: _getElasticsearchFilter(options) 48 | }, 49 | must: { 50 | match: { 51 | name: options.search 52 | } 53 | } 54 | } 55 | } 56 | }; 57 | 58 | return elascticsearchClient.search({ 59 | index: INDICES.SOFTWARE.name, 60 | type: INDICES.SOFTWARE.type, 61 | from: options.offset, 62 | size: options.limit, 63 | body: elasticsearchQuery 64 | }, (err, response) => { 65 | if (err) { 66 | return callback(err); 67 | } 68 | 69 | const list = response.hits.hits; 70 | const software = list.map(hit => _.extend(hit._source, { _id: hit._source.id })); 71 | 72 | return callback(null, { 73 | total_count: response.hits.total, 74 | list: software 75 | }); 76 | }); 77 | }); 78 | 79 | function _getElasticsearchFilter(options) { 80 | const filter = {}; 81 | 82 | if (options.excludedIds) { 83 | filter.must_not = [{ 84 | ids: { 85 | values: options.excludedIds 86 | } 87 | }]; 88 | } 89 | 90 | return filter; 91 | } 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /backend/lib/user/search.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const Q = require('q'); 5 | const { DEFAULT_LIST_OPTIONS, INDICES } = require('../constants'); 6 | 7 | module.exports = dependencies => { 8 | const coreElasticsearch = dependencies('coreElasticsearch'); 9 | 10 | return search; 11 | 12 | /** 13 | * Search users in system. 14 | * 15 | * @param {object} options - Hash with: 16 | * - 'limit' and 'offset' for pagination 17 | * - 'search' for filtering terms 18 | * - 'role' for filtering users by role 19 | * Search can be a single string, an array of strings which will be joined, or a space separated string list. 20 | * In the case of array or space separated string, a AND search will be performed with the input terms. 21 | * @return {Promise} Resolve on success with result: { total_count: number, list: [User1, User2, ...] } 22 | */ 23 | function search(options) { 24 | return Q.nfcall(_search, options); 25 | } 26 | 27 | function _search(options = {}, callback) { 28 | options.limit = +options.limit || DEFAULT_LIST_OPTIONS.LIMIT; 29 | options.offset = +options.offset || DEFAULT_LIST_OPTIONS.OFFSET; 30 | 31 | if (!options.search) { 32 | return callback(new Error('query.search is mandatory')); 33 | } 34 | 35 | return coreElasticsearch.client((err, elascticsearchClient) => { 36 | if (err) { 37 | return callback(err); 38 | } 39 | 40 | const terms = (options.search instanceof Array) ? options.search.join(' ') : options.search; 41 | const elasticsearchQuery = { 42 | sort: [ 43 | {'firstname.sort': 'asc'} 44 | ], 45 | query: { 46 | bool: { 47 | filter: { 48 | bool: _getElasticsearchFilters(options) 49 | }, 50 | must: { 51 | multi_match: { 52 | query: terms, 53 | type: 'cross_fields', 54 | fields: ['firstname', 'lastname', 'accounts.emails'], 55 | operator: 'and' 56 | } 57 | } 58 | } 59 | } 60 | }; 61 | 62 | return elascticsearchClient.search({ 63 | index: INDICES.USER.name, 64 | type: INDICES.USER.type, 65 | from: options.offset, 66 | size: options.limit, 67 | body: elasticsearchQuery 68 | }, (err, response) => { 69 | if (err) { 70 | return callback(err); 71 | } 72 | 73 | const list = response.hits.hits; 74 | const users = list.map(hit => _.extend(hit._source, { _id: hit._source.id })); 75 | 76 | return callback(null, { 77 | total_count: response.hits.total, 78 | list: users 79 | }); 80 | }); 81 | }); 82 | } 83 | 84 | function _getElasticsearchFilters(options) { 85 | if (!options || !options.role) { 86 | return; 87 | } 88 | 89 | return { 90 | must: [{ 91 | term: { 92 | role: options.role 93 | } 94 | }] 95 | }; 96 | } 97 | }; 98 | -------------------------------------------------------------------------------- /backend/templates/email/contract.expired/html.pug: -------------------------------------------------------------------------------- 1 | - var contractUrl = new URL(`administration/contracts/${content.ticket.contract}`, content.frontendUrl).toString(); 2 | - var contractNameLinked = `${content.contractName}` 3 | 4 | include ../includes/header.pug 5 | table.row.row-3(align='center' width='100%' border='0' cellpadding='0' cellspacing='0' role='presentation' style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #eeeeee;') 6 | tbody 7 | tr 8 | td 9 | table.row-content.stack(align='center' border='0' cellpadding='0' cellspacing='0' role='presentation' style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #ffffff; color: #000000; width: 750px;border-radius: 30px 30px 0 0;' width='750') 10 | tbody 11 | tr 12 | td.column.column-1(width='100%' style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; font-weight: 400; text-align: left; vertical-align: top; padding-top: 5px; padding-bottom: 5px; border-top: 0px; border-right: 0px; border-bottom: 0px; border-left: 0px;') 13 | tbody 14 | tr 15 | td.column.column-1(width='100%' style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; font-weight: 400; text-align: left; vertical-align: top; padding-top: 5px; padding-bottom: 5px; border-top: 0px; border-right: 0px; border-bottom: 0px; border-left: 0px;') 16 | tbody 17 | tr 18 | td.column.column-1(width='100%' style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; font-weight: 400; text-align: left; vertical-align: top; padding-top: 5px; padding-bottom: 5px; border-top: 0px; border-right: 0px; border-bottom: 0px; border-left: 0px;') 19 | table.image_block(width='100%' border='0' cellpadding='0' cellspacing='0' role='presentation' style='mso-table-lspace: 0pt; mso-table-rspace: 0pt;') 20 | tr 21 | td(style='width:100%;padding-right:0px;padding-left:0px;') 22 | div(align='center' style='line-height:10px') 23 | img(src="cid:contractexpiration" width='470' height='440' alt="contract expiration image" title="I'm an image") 24 | table.text_block(width='100%' border='0' cellpadding='10' cellspacing='0' role='presentation' style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;') 25 | tr 26 | td 27 | div(style='font-family: sans-serif') 28 | .txtTinyMce-wrapper(style='font-size: 12px; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; mso-line-height-alt: 14.399999999999999px; color: #0556a5; line-height: 1.2;') 29 | p(style='margin: 0; font-size: 14px; text-align: center;') !{translate('The contract {{{contractNameLinked}}} is expired.', {contractNameLinked: contractNameLinked})} 30 | include ../includes/footer.pug -------------------------------------------------------------------------------- /backend/templates/email/contract.creditconsumed/html.pug: -------------------------------------------------------------------------------- 1 | - var contractUrl = new URL(`administration/contracts/${content.ticket.contract}`, content.frontendUrl).toString(); 2 | - var contractNameLinked = `${content.contractName}` 3 | 4 | 5 | include ../includes/header.pug 6 | table.row.row-3(align='center' width='100%' border='0' cellpadding='0' cellspacing='0' role='presentation' style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #eeeeee;') 7 | tbody 8 | tr 9 | td 10 | table.row-content.stack(align='center' border='0' cellpadding='0' cellspacing='0' role='presentation' style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #ffffff; color: #000000; width: 750px;border-radius: 30px 30px 0 0;' width='750') 11 | tbody 12 | tr 13 | td.column.column-1(width='100%' style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; font-weight: 400; text-align: left; vertical-align: top; padding-top: 5px; padding-bottom: 5px; border-top: 0px; border-right: 0px; border-bottom: 0px; border-left: 0px;') 14 | tbody 15 | tr 16 | td.column.column-1(width='100%' style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; font-weight: 400; text-align: left; vertical-align: top; padding-top: 5px; padding-bottom: 5px; border-top: 0px; border-right: 0px; border-bottom: 0px; border-left: 0px;') 17 | tbody 18 | tr 19 | td.column.column-1(width='100%' style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; font-weight: 400; text-align: left; vertical-align: top; padding-top: 5px; padding-bottom: 5px; border-top: 0px; border-right: 0px; border-bottom: 0px; border-left: 0px;') 20 | table.image_block(width='100%' border='0' cellpadding='0' cellspacing='0' role='presentation' style='mso-table-lspace: 0pt; mso-table-rspace: 0pt;') 21 | tr 22 | td(style='width:100%;padding-right:0px;padding-left:0px;') 23 | div(align='center' style='line-height:10px') 24 | img(src="cid:contractcreditconsumed" width='410' height='528' alt="contract credit consumed image" title="I'm an image") 25 | table.text_block(width='100%' border='0' cellpadding='10' cellspacing='0' role='presentation' style='mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;') 26 | tr 27 | td 28 | div(style='font-family: sans-serif') 29 | .txtTinyMce-wrapper(style='font-size: 12px; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; mso-line-height-alt: 14.399999999999999px; color: #0556a5; line-height: 1.2;') 30 | p(style='margin: 0; font-size: 14px; text-align: center;') !{translate('The contract {{{contractNameLinked}}} has consumed all its credits.', {contractNameLinked: contractNameLinked})} 31 | include ../includes/footer.pug 32 | -------------------------------------------------------------------------------- /test/config/mocks/module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global _: false */ 4 | 5 | angular.module('esn.router', ['ui.router']) 6 | .factory('session', function($q) { 7 | return { 8 | ready: $q.when(), 9 | user: {}, 10 | domain: {}, 11 | userIsDomainAdministrator: function() { 12 | return false; 13 | } 14 | }; 15 | }); 16 | angular.module('esn.http', []) 17 | .factory('httpErrorHandler', function() { 18 | return { 19 | redirectToLogin: angular.noop 20 | }; 21 | }); 22 | angular.module('esn.async-action', []) 23 | .factory('asyncAction', function() { 24 | return function(message, action) { 25 | return action(); 26 | }; 27 | }) 28 | .factory('rejectWithErrorNotification', function() { 29 | return function() { 30 | return $q.reject(); 31 | }; 32 | }); 33 | angular.module('esn.scroll', []) 34 | .factory('elementScrollService', function() { 35 | return {}; 36 | }); 37 | angular.module('esn.attendee', []) 38 | .constant('ESN_ATTENDEE_DEFAULT_TEMPLATE_URL', '') 39 | .factory('attendeeService', function() { 40 | return { 41 | getAttendeeCandidates: function() {} 42 | }; 43 | }); 44 | angular.module('esn.core', []) 45 | .constant('_', _) 46 | .filter('bytes', function() { 47 | return function(input) { return input; }; 48 | }); 49 | angular.module('ui.select', {}); 50 | angular.module('ngSanitize', {}); 51 | angular.module('ngTagsInput', []) 52 | .provider('tagsInputConfig', function() { 53 | this.setDefaults = function() {}; 54 | this.$get = function() {}; 55 | this.setActiveInterpolation = function() {}; 56 | }); 57 | angular.module('esn.i18n', []) 58 | .filter('esnI18n', function() { 59 | return function(input) { return input; }; 60 | }) 61 | .factory('esnI18nService', function() { 62 | return { 63 | translate: function(input) { 64 | return { 65 | toString: function() { 66 | return input; 67 | } 68 | }; 69 | } 70 | }; 71 | }); 72 | angular.module('esn.notification', []) 73 | .factory('notificationFactory', function() { 74 | return { 75 | weakError: function() {} 76 | }; 77 | }); 78 | angular.module('esn.session', []); 79 | angular.module('esn.domain', []) 80 | .factory('domainAPI', function() { 81 | return {}; 82 | }); 83 | angular.module('esn.template', []) 84 | .provider('esnTemplate', function() { 85 | this.setSuccessTemplate = function() {}; 86 | this.$get = function() {}; 87 | }); 88 | angular.module('esn.module-registry', []) 89 | .factory('esnModuleRegistry', function() { 90 | return { 91 | add: function() {} 92 | }; 93 | }); 94 | angular.module('esn.file', []) 95 | .factory('fileUploadService', function() { 96 | return { 97 | get: function() {} 98 | }; 99 | }); 100 | angular.module('esn.attachment', []); 101 | angular.module('esn.websocket', []) 102 | .factory('livenotification', function() { 103 | return {}; 104 | }); 105 | -------------------------------------------------------------------------------- /backend/lib/organization/search.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const Q = require('q'); 5 | const { DEFAULT_LIST_OPTIONS, INDICES } = require('../constants'); 6 | 7 | module.exports = dependencies => { 8 | 9 | const coreElasticsearch = dependencies('coreElasticsearch'); 10 | 11 | return search; 12 | 13 | /** 14 | * Search organization in system. 15 | * 16 | * @param {object} options - Hash with: 17 | * - 'limit' and 'offset' for pagination 18 | * - 'search' for filtering terms 19 | * - 'parent' for search only organizations or entities 20 | * Search can be a single string, an array of strings which will be joined, or a space separated string list. 21 | * In the case of array or space separated string, a AND search will be performed with the input terms. 22 | * @return {Promise} Resolve on success with result: { total_count: number, list: [Organization1, Organization2, ...] } 23 | */ 24 | function search(options) { 25 | return Q.nfcall(_search, options); 26 | } 27 | 28 | function _search(options, callback) { 29 | options.limit = +options.limit || DEFAULT_LIST_OPTIONS.LIMIT; 30 | options.offset = +options.offset || DEFAULT_LIST_OPTIONS.OFFSET; 31 | 32 | if (!options.search) { 33 | return callback(new Error('query.search is mandatory')); 34 | } 35 | 36 | return coreElasticsearch.client((err, elascticsearchClient) => { 37 | if (err) { 38 | return callback(err); 39 | } 40 | 41 | const terms = (options.search instanceof Array) ? options.search.join(' ') : options.search; 42 | 43 | const elasticsearchQuery = { 44 | sort: [ 45 | {'shortName.sort': 'asc'} 46 | ], 47 | query: { 48 | bool: { 49 | filter: { 50 | bool: _getElasticsearchFilter(options.parent) 51 | }, 52 | must: { 53 | multi_match: { 54 | query: terms, 55 | type: 'cross_fields', 56 | fields: ['shortName', 'fullName', 'description'], 57 | operator: 'and' 58 | } 59 | } 60 | } 61 | } 62 | }; 63 | 64 | return elascticsearchClient.search({ 65 | index: INDICES.ORGANIZATION.name, 66 | type: INDICES.ORGANIZATION.type, 67 | from: options.offset, 68 | size: options.limit, 69 | body: elasticsearchQuery 70 | }, (err, response) => { 71 | if (err) { 72 | return callback(err); 73 | } 74 | 75 | const list = response.hits.hits; 76 | const organizations = list.map(function(hit) { return _.extend(hit._source, { _id: hit._source.id }); }); 77 | 78 | return callback(null, { 79 | total_count: response.hits.total, 80 | list: organizations 81 | }); 82 | }); 83 | }); 84 | } 85 | 86 | function _getElasticsearchFilter(parent) { 87 | if (!parent) { 88 | return { 89 | must: [{ 90 | missing: { 91 | field: 'parent' 92 | } 93 | }] 94 | }; 95 | } 96 | 97 | return { 98 | must: [{ 99 | exists: { 100 | field: 'parent' 101 | } 102 | }] 103 | }; 104 | } 105 | }; 106 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const AwesomeModule = require('awesome-module'); 4 | const Dependency = AwesomeModule.AwesomeModuleDependency; 5 | 6 | const MODULE_NAME = 'smartsla'; 7 | const AWESOME_MODULE_NAME = `${MODULE_NAME}-backend`; 8 | 9 | const myAwesomeModule = new AwesomeModule(AWESOME_MODULE_NAME, { 10 | dependencies: [ 11 | new Dependency(Dependency.TYPE_NAME, 'linagora.esn.core.logger', 'logger'), 12 | new Dependency(Dependency.TYPE_NAME, 'linagora.esn.core.webserver.wrapper', 'webserver-wrapper'), 13 | new Dependency(Dependency.TYPE_NAME, 'linagora.esn.core.db', 'db'), 14 | new Dependency(Dependency.TYPE_NAME, 'linagora.esn.core.user', 'coreUser'), 15 | new Dependency(Dependency.TYPE_NAME, 'linagora.esn.core.domain', 'coreDomain'), 16 | new Dependency(Dependency.TYPE_NAME, 'linagora.esn.core.collaboration', 'collaboration'), 17 | new Dependency(Dependency.TYPE_NAME, 'linagora.esn.core.elasticsearch', 'coreElasticsearch'), 18 | new Dependency(Dependency.TYPE_NAME, 'linagora.esn.core.esn-config', 'esn-config'), 19 | new Dependency(Dependency.TYPE_NAME, 'linagora.esn.core.email', 'email'), 20 | new Dependency(Dependency.TYPE_NAME, 'linagora.esn.core.pubsub', 'pubsub'), 21 | new Dependency(Dependency.TYPE_NAME, 'linagora.esn.core.webserver.middleware.authorization', 'authorizationMW'), 22 | new Dependency(Dependency.TYPE_NAME, 'linagora.esn.core.webserver.middleware.domain', 'domainMW'), 23 | new Dependency(Dependency.TYPE_NAME, 'linagora.esn.core.webserver.middleware.helper', 'helperMw'), 24 | new Dependency(Dependency.TYPE_NAME, 'linagora.esn.core.webserver.denormalize.user', 'coreUserDenormalizer'), 25 | new Dependency(Dependency.TYPE_NAME, 'linagora.esn.core.i18n', 'i18n'), 26 | new Dependency(Dependency.TYPE_NAME, 'linagora.esn.core.filestore', 'filestore'), 27 | new Dependency(Dependency.TYPE_NAME, 'linagora.esn.core.activitystreams', 'activitystreams'), 28 | new Dependency(Dependency.TYPE_NAME, 'linagora.esn.core.wsserver', 'wsserver'), 29 | new Dependency(Dependency.TYPE_NAME, 'linagora.esn.core.availability', 'availability') 30 | ], 31 | 32 | states: { 33 | lib: function(dependencies, callback) { 34 | const moduleLib = require('./backend/lib')(dependencies); 35 | const module = require('./backend/webserver/api')(dependencies, moduleLib); 36 | 37 | const lib = { 38 | api: { 39 | module: module 40 | }, 41 | lib: moduleLib 42 | }; 43 | 44 | return callback(null, lib); 45 | }, 46 | 47 | deploy: function(dependencies, callback) { 48 | const webserverWrapper = dependencies('webserver-wrapper'); 49 | 50 | // Register the webapp 51 | const app = require('./backend/webserver/application')(dependencies, this); 52 | 53 | // Register every exposed endpoints 54 | app.use('/api', this.api.module); 55 | webserverWrapper.addApp('ticketing', app); 56 | 57 | return callback(); 58 | }, 59 | 60 | start: function(dependencies, callback) { 61 | require('./backend/ws')(dependencies).init(); 62 | 63 | this.lib.start(callback); 64 | } 65 | } 66 | }); 67 | 68 | module.exports = Object.freeze({ 69 | name: AWESOME_MODULE_NAME 70 | }); 71 | 72 | /** 73 | * The main AwesomeModule describing the application. 74 | * @type {AwesomeModule} 75 | */ 76 | module.exports = myAwesomeModule; 77 | -------------------------------------------------------------------------------- /bin/lib/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const emailAddresses = require('email-addresses'); 4 | const mongoose = require('mongoose'); 5 | const ObjectId = mongoose.Schema.Types.ObjectId; 6 | const Mixed = mongoose.Schema.Types.Mixed; 7 | 8 | function validateEmail(email) { 9 | return emailAddresses.parseOneAddress(email) !== null; 10 | } 11 | 12 | function validateEmails(emails) { 13 | if (!emails || !emails.length) { 14 | return true; 15 | } 16 | let valid = true; 17 | 18 | emails.forEach(function(email) { 19 | if (!validateEmail(email)) { 20 | valid = false; 21 | } 22 | }); 23 | 24 | return valid; 25 | } 26 | 27 | function validateAccounts(accounts) { 28 | return accounts && accounts.length; 29 | } 30 | 31 | var UserAccountSchema = new mongoose.Schema({ 32 | _id: false, 33 | type: { type: String, enum: ['email', 'oauth'] }, 34 | hosted: { type: Boolean, default: false }, 35 | emails: { type: [String], unique: true, partialFilterExpression: { $type: 'array' }, validate: validateEmails }, 36 | preferredEmailIndex: { type: Number, default: 0 }, 37 | timestamps: { 38 | creation: { type: Date, default: Date.now } 39 | }, 40 | data: { type: Mixed } 41 | }); 42 | 43 | var MemberOfDomainSchema = new mongoose.Schema({ 44 | domain_id: { type: mongoose.Schema.Types.ObjectId, ref: 'Domain', required: true }, 45 | joined_at: { type: Date, default: Date.now }, 46 | status: { type: String, lowercase: true, trim: true } 47 | }, { _id: false }); 48 | 49 | var UserSchema = new mongoose.Schema({ 50 | firstname: { type: String, trim: true }, 51 | lastname: { type: String, trim: true }, 52 | password: { type: String }, 53 | job_title: { type: String, trim: true }, 54 | service: { type: String, trim: true }, 55 | building_location: { type: String, trim: true }, 56 | office_location: { type: String, trim: true }, 57 | main_phone: { type: String, trim: true }, 58 | description: { type: String, trim: true }, 59 | timestamps: { 60 | creation: { type: Date, default: Date.now } 61 | }, 62 | domains: { type: [MemberOfDomainSchema] }, 63 | login: { 64 | disabled: { type: Boolean, default: false }, 65 | failures: { 66 | type: [Date] 67 | }, 68 | success: { type: Date } 69 | }, 70 | schemaVersion: { type: Number, default: 2 }, 71 | avatars: [ObjectId], 72 | currentAvatar: ObjectId, 73 | accounts: { type: [UserAccountSchema], required: true, validate: validateAccounts } 74 | }); 75 | 76 | UserSchema.virtual('preferredEmail').get(function() { 77 | return this.accounts 78 | .filter(function(account) { 79 | return account.type === 'email'; 80 | }) 81 | .slice() // Because sort mutates the array 82 | .sort(function(a, b) { 83 | return b.hosted - a.hosted; 84 | }) 85 | .reduce(function(foundPreferredEmail, account) { 86 | return foundPreferredEmail || account.emails[account.preferredEmailIndex]; 87 | }, null); 88 | }); 89 | 90 | UserSchema.virtual('preferredDomainId').get(function() { 91 | return this.domains.length ? this.domains[0].domain_id : ''; 92 | }); 93 | 94 | UserSchema.virtual('emails').get(function() { 95 | var emails = []; 96 | 97 | this.accounts.forEach(function(account) { 98 | account.emails && account.emails.forEach(function(email) { 99 | emails.push(email); 100 | }); 101 | }); 102 | 103 | return emails; 104 | }); 105 | module.exports = mongoose.model('User', UserSchema); 106 | -------------------------------------------------------------------------------- /test/midway-backend/webserver/api/software/get-by-name.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('supertest'); 4 | const path = require('path'); 5 | const expect = require('chai').expect; 6 | 7 | describe('GET /ticketing/api/software?name=', function() { 8 | let app, lib, helpers; 9 | let user1, user2; 10 | const password = 'secret'; 11 | 12 | beforeEach(function(done) { 13 | helpers = this.helpers; 14 | app = this.app; 15 | lib = this.lib; 16 | 17 | const deployOptions = { 18 | fixtures: path.normalize(`${__dirname}/../../../fixtures/deployments`) 19 | }; 20 | 21 | helpers.api.applyDomainDeployment('ticketingModule', deployOptions, (err, models) => { 22 | if (err) { 23 | return done(err); 24 | } 25 | 26 | user1 = models.users[1]; 27 | user2 = models.users[2]; 28 | 29 | lib.ticketingUserRole.create({ 30 | user: user1._id, 31 | role: 'administrator' 32 | }) 33 | .then(() => { 34 | lib.ticketingUserRole.create({ 35 | user: user2._id, 36 | role: 'user' 37 | }); 38 | }) 39 | .then(() => done()) 40 | .catch(err => done(err)); 41 | }); 42 | }); 43 | 44 | afterEach(function(done) { 45 | helpers.mongo.dropDatabase(err => done(err)); 46 | }); 47 | 48 | it('should respond 401 if not logged in', function(done) { 49 | helpers.api.requireLogin(app, 'get', '/ticketing/api/software?name=a', done); 50 | }); 51 | 52 | it('should respond 403 if user is not an administrator', function(done) { 53 | helpers.api.loginAsUser(app, user2.emails[0], password, helpers.callbacks.noErrorAnd(requestAsMember => { 54 | const req = requestAsMember(request(app).get('/ticketing/api/software?name=a')); 55 | 56 | req.expect(403) 57 | .end(helpers.callbacks.noErrorAnd(res => { 58 | expect(res.body).to.deep.equal({ 59 | error: { code: 403, message: 'Forbidden', details: 'User is not the administrator' } 60 | }); 61 | done(); 62 | })); 63 | })); 64 | }); 65 | 66 | it('should respond 200 with an empty array if no software found', function(done) { 67 | helpers.api.loginAsUser(app, user1.emails[0], password, helpers.callbacks.noErrorAnd(requestAsMember => { 68 | const req = requestAsMember(request(app).get('/ticketing/api/software?name=a')); 69 | 70 | req.expect(200) 71 | .end(helpers.callbacks.noErrorAnd(res => { 72 | expect(res.headers['x-esn-items-count']).to.exist; 73 | expect(res.headers['x-esn-items-count']).to.equal('0'); 74 | expect(res.body).to.shallowDeepEqual([]); 75 | done(); 76 | })); 77 | })); 78 | }); 79 | 80 | it('should respond 200 with an array of found software', function(done) { 81 | helpers.api.loginAsUser(app, user1.emails[0], password, helpers.callbacks.noErrorAnd(requestAsMember => { 82 | const softwareA = { name: 'foo', category: 'test' }; 83 | const req = requestAsMember(request(app).get(`/ticketing/api/software?name=${softwareA.name}`)); 84 | 85 | lib.software.create(softwareA) 86 | .then(() => { 87 | req.expect(200) 88 | .end(helpers.callbacks.noErrorAnd(res => { 89 | expect(res.headers['x-esn-items-count']).to.exist; 90 | expect(res.headers['x-esn-items-count']).to.equal('1'); 91 | expect(res.body[0].name).to.shallowDeepEqual(softwareA.name); 92 | done(); 93 | })); 94 | }) 95 | .catch(err => done(err || 'should resolve')); 96 | })); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /backend/webserver/api/client/controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(dependencies, lib) { 4 | const { send404Error, send500Error } = require('../utils')(dependencies); 5 | 6 | return { 7 | create, 8 | get, 9 | list, 10 | update, 11 | remove 12 | }; 13 | 14 | /** 15 | * Create a client 16 | * 17 | * @param {Request} req 18 | * @param {Response} res 19 | */ 20 | function create(req, res) { 21 | return lib.client.create(req.body) 22 | .then(createdClient => res.status(201).json(createdClient)) 23 | .catch(err => send500Error('Failed to create client', err, res)); 24 | } 25 | 26 | /** 27 | * List clients 28 | * 29 | * @param {Request} req 30 | * @param {Response} res 31 | */ 32 | function list(req, res) { 33 | let getClient; 34 | let errorMessage; 35 | 36 | if (req.query.search) { 37 | const options = { 38 | limit: +req.query.limit, 39 | offset: +req.query.offset, 40 | search: req.query.search, 41 | excludedIds: req.query.excludedIds 42 | }; 43 | 44 | errorMessage = 'Error while searching client'; 45 | getClient = lib.client.search(options); 46 | } else if (req.query.name) { 47 | errorMessage = `Failed to get client ${req.query.name}`; 48 | getClient = lib.client.getByName(req.query.name) 49 | .then(client => { 50 | const list = client ? [client] : []; 51 | 52 | return { 53 | total_count: list.length, 54 | list 55 | }; 56 | }); 57 | } else { 58 | const options = { 59 | limit: +req.query.limit, 60 | offset: +req.query.offset 61 | }; 62 | 63 | errorMessage = 'Failed to list client'; 64 | getClient = lib.client.list(options) 65 | .then(client => ({ 66 | total_count: client.length, 67 | list: client 68 | })); 69 | } 70 | 71 | return getClient 72 | .then(result => { 73 | res.header('X-ESN-Items-Count', result.total_count); 74 | res.status(200).json(result.list); 75 | }) 76 | .catch(err => send500Error(errorMessage, err, res)); 77 | } 78 | 79 | /** 80 | * Get a client 81 | * 82 | * @param {Request} req 83 | * @param {Response} res 84 | */ 85 | function get(req, res) { 86 | return lib.client.getById(req.params.id) 87 | .then(client => res.status(200).json(client)) 88 | .catch(err => send404Error(err.message, res)); 89 | } 90 | 91 | /** 92 | * Update a client 93 | * 94 | * @param {Request} req 95 | * @param {Response} res 96 | */ 97 | function update(req, res) { 98 | return lib.client.updateById(req.params.id, req.body) 99 | .then(numberOfUpdatedDocs => { 100 | if (numberOfUpdatedDocs) { 101 | return res.status(204).end(); 102 | } 103 | 104 | return send404Error('client not found', res); 105 | }) 106 | .catch(err => send500Error('Failed to update client', err, res)); 107 | } 108 | 109 | /** 110 | * Delete a client 111 | * 112 | * @param {Request} req 113 | * @param {Response} res 114 | */ 115 | function remove(req, res) { 116 | return lib.client.removeById(req.params.id) 117 | .then(deletedClient => { 118 | if (deletedClient) { 119 | return res.status(204).end(); 120 | } 121 | 122 | return send404Error('client not found', res); 123 | }) 124 | .catch(err => send500Error('Failed to delete client', err, res)); 125 | } 126 | }; 127 | -------------------------------------------------------------------------------- /backend/webserver/api/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (dependencies, lib) => { 4 | const { send400Error, send403Error, send500Error } = require('./utils')(dependencies); 5 | const mongoose = dependencies('db').mongo.mongoose; 6 | const ObjectId = mongoose.Types.ObjectId; 7 | const coreUserDenormalizer = dependencies('coreUserDenormalizer'); 8 | const EsnConfig = dependencies('esn-config').EsnConfig; 9 | 10 | return { 11 | flipFeature, 12 | loadUserRole, 13 | validateObjectIds, 14 | isAdministrator, 15 | requireAdministrator, 16 | buildUserDisplayName, 17 | requireContractManagerOrAdmin 18 | }; 19 | 20 | function flipFeature(featureName) { 21 | return function(req, res, next) { 22 | new EsnConfig('smartsla-backend') 23 | .get('features') 24 | .then(config => { 25 | if (config[featureName]) { 26 | next(); 27 | } else { 28 | send403Error('Feature not enabled', res); 29 | } 30 | }); 31 | }; 32 | } 33 | 34 | function isAdministrator(user, res) { 35 | if (!user || !user._id) { 36 | return send400Error('Missing user', res); 37 | } 38 | 39 | return Promise.all([ 40 | coreUserDenormalizer.denormalize(user, { includeIsPlatformAdmin: true }), 41 | isTicketingAdmin(user._id) 42 | ]) 43 | .then(([user, isTicketingAdmin]) => (user.isPlatformAdmin || isTicketingAdmin)); 44 | 45 | function isTicketingAdmin(userId) { 46 | return lib.ticketingUserRole.userIsAdministrator(userId); 47 | } 48 | } 49 | 50 | function requireAdministrator(req, res, next) { 51 | return isAdministrator(req.user, res) 52 | .then(isAdmin => { 53 | 54 | if (isAdmin) { 55 | return next(); 56 | } 57 | 58 | return send403Error('User does not have the necessary permission', res); 59 | }) 60 | .catch(err => send500Error('Unable to check administrator permission', err, res)); 61 | } 62 | 63 | function loadUserRole(req, res, next) { 64 | if (!req.user || !req.user._id) { 65 | return send400Error('Missing user', res); 66 | } 67 | 68 | return lib.ticketingUserRole.getByUser(req.user._id) 69 | .then(ticketingUserRole => { 70 | if (!ticketingUserRole) { 71 | return send403Error('User not found', res); 72 | } 73 | 74 | req.user.role = ticketingUserRole.role; 75 | next(); 76 | }) 77 | .catch(err => send500Error('Unable to load user\'s role', err, res)); 78 | } 79 | 80 | function validateObjectIds(ids) { 81 | ids = Array.isArray(ids) ? ids : [ids]; 82 | 83 | return !ids.some(id => !_validateObjectId(id)); 84 | } 85 | 86 | function _validateObjectId(id) { 87 | return ObjectId.isValid(String(id)); 88 | } 89 | 90 | function buildUserDisplayName(user) { 91 | return (user.firstname && user.lastname) ? user.firstname + ' ' + user.lastname : user.preferredEmail; 92 | } 93 | 94 | function requireContractManagerOrAdmin(req, res, next) { 95 | const { role } = req.ticketingUser; 96 | 97 | if ( 98 | role === lib.constants.TICKETING_CONTRACT_ROLES.CONTRACT_MANAGER || 99 | role === lib.constants.TICKETING_CONTRACT_ROLES.OPERATIONAL_MANAGER || 100 | role === lib.constants.TICKETING_CONTRACT_ROLES.VIEWER || 101 | role === lib.constants.TICKETING_CONTRACT_ROLES.CUSTOMER || 102 | role === lib.constants.EXPERT_ROLE.EXPERT 103 | ) { 104 | return next(); 105 | } 106 | 107 | return requireAdministrator(req, res, next); 108 | } 109 | }; 110 | -------------------------------------------------------------------------------- /backend/lib/team/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(dependencies) { 4 | const mongoose = dependencies('db').mongo.mongoose; 5 | const pubsubLocal = dependencies('pubsub').local; 6 | const Team = mongoose.model('Team'); 7 | const { DEFAULT_LIST_OPTIONS, EVENTS } = require('../constants'); 8 | 9 | const teamCreatedTopic = pubsubLocal.topic(EVENTS.TEAM.created); 10 | const teamUpdatedTopic = pubsubLocal.topic(EVENTS.TEAM.updated); 11 | const teamDeletedTopic = pubsubLocal.topic(EVENTS.TEAM.deleted); 12 | 13 | return { 14 | create, 15 | getById, 16 | getByName, 17 | list, 18 | listByCursor, 19 | updateById, 20 | removeById 21 | }; 22 | 23 | /** 24 | * Create a team 25 | * @param {Object} Team - The team object 26 | * @param {Promise} - Resolve on success 27 | */ 28 | function create(team) { 29 | team = team instanceof Team ? team : new Team(team); 30 | 31 | return Team.create(team) 32 | .then(createdTeam => { 33 | teamCreatedTopic.publish(createdTeam); 34 | 35 | return createdTeam; 36 | }); 37 | } 38 | 39 | /** 40 | * List team 41 | * @param {Object} options - The options object, may contain offset and limit 42 | * @param {Promise} - Resolve on success 43 | */ 44 | function list(options = {}) { 45 | 46 | return Team 47 | .find() 48 | .skip(+options.offset || DEFAULT_LIST_OPTIONS.OFFSET) 49 | .limit(+options.limit || DEFAULT_LIST_OPTIONS.LIMIT) 50 | .sort('-timestamps.creation') 51 | .exec(); 52 | } 53 | 54 | /** 55 | * Update a team by ID 56 | * @param {String} teamId - The team ID 57 | * @param {Object} modified - The modified team object 58 | * @param {Promise} - Resolve on success with the number of documents selected for update 59 | */ 60 | function updateById(teamId, modified) { 61 | return Team.update({ _id: teamId }, { $set: modified }).exec() 62 | .then(updatedResult => { 63 | // updatedResult: { "ok" : 1, "nModified" : 1, "n" : 1 } 64 | // updatedResult.n: The number of documents selected for update 65 | // http://mongoosejs.com/docs/api.html#model_Model.update 66 | if (updatedResult.n) { 67 | modified._id = modified._id || teamId; 68 | teamUpdatedTopic.publish(modified); 69 | } 70 | 71 | return updatedResult.n; 72 | }); 73 | } 74 | 75 | /** 76 | * Get a team by name insensitive lowercase and uppercase 77 | * @param {String} name - The team name 78 | * @param {Promise} - Resolve on success 79 | */ 80 | function getByName(name) { 81 | return Team.findOne({ name: new RegExp(`^${name}$`, 'i') }); 82 | } 83 | 84 | /** 85 | * Get a team by ID 86 | * @param {String} teamId - The team ID 87 | * @param {Promise} - Resolve on success 88 | */ 89 | function getById(teamId) { 90 | return Team 91 | .findById(teamId) 92 | .exec(); 93 | } 94 | 95 | /** 96 | * List team using cursor 97 | * @param {Promise} - Resolve on success with a cursor object 98 | */ 99 | function listByCursor() { 100 | return Team.find().cursor(); 101 | } 102 | 103 | /** 104 | * Remove team by ID 105 | */ 106 | function removeById(teamId) { 107 | return Team 108 | .findByIdAndRemove(teamId) 109 | .then(deletedTeam => { 110 | if (deletedTeam) { 111 | teamDeletedTopic.publish(deletedTeam); 112 | } 113 | 114 | return deletedTeam; 115 | }); 116 | } 117 | 118 | }; 119 | -------------------------------------------------------------------------------- /backend/webserver/api/team/controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(dependencies, lib) { 4 | const { send404Error, send500Error } = require('../utils')(dependencies); 5 | 6 | return { 7 | create, 8 | get, 9 | list, 10 | update, 11 | remove 12 | }; 13 | 14 | /** 15 | * Create a team 16 | * 17 | * @param {Request} req 18 | * @param {Response} res 19 | */ 20 | function create(req, res) { 21 | return lib.team.create(req.body) 22 | .then(createdTeam => res.status(201).json(createdTeam)) 23 | .catch(err => send500Error('Failed to create team', err, res)); 24 | } 25 | 26 | /** 27 | * list teams 28 | * 29 | * @param {Request} req 30 | * @param {Response} res 31 | */ 32 | function list(req, res) { 33 | let getTeam; 34 | let errorMessage; 35 | 36 | if (req.query.search) { 37 | const options = { 38 | limit: +req.query.limit, 39 | offset: +req.query.offset, 40 | search: req.query.search, 41 | excludedIds: req.query.excludedIds 42 | }; 43 | 44 | errorMessage = 'Error while searching team'; 45 | getTeam = lib.team.search(options); 46 | } else if (req.query.name) { 47 | errorMessage = `Failed to get team ${req.query.name}`; 48 | getTeam = lib.team.getByName(req.query.name) 49 | .then(team => { 50 | const list = team ? [team] : []; 51 | 52 | return { 53 | total_count: list.length, 54 | list 55 | }; 56 | }); 57 | } else { 58 | const options = { 59 | limit: +req.query.limit, 60 | offset: +req.query.offset 61 | }; 62 | 63 | errorMessage = 'Failed to list team'; 64 | getTeam = lib.team.list(options) 65 | .then(team => ({ 66 | total_count: team.length, 67 | list: team 68 | })); 69 | } 70 | 71 | return getTeam 72 | .then(result => { 73 | res.header('X-ESN-Items-Count', result.total_count); 74 | res.status(200).json(result.list); 75 | }) 76 | .catch(err => send500Error(errorMessage, err, res)); 77 | } 78 | 79 | /** 80 | * Get a Team 81 | * 82 | * @param {Request} req 83 | * @param {Response} res 84 | */ 85 | function get(req, res) { 86 | return lib.team.getById(req.params.id) 87 | .then(team => { 88 | team = team.toObject(); 89 | 90 | return res.status(200).json(team); 91 | 92 | }) 93 | .catch(err => send500Error('Failed to get software', err, res)); 94 | } 95 | 96 | /** 97 | * Update a team 98 | * 99 | * @param {Request} req 100 | * @param {Response} res 101 | */ 102 | function update(req, res) { 103 | return lib.team.updateById(req.params.id, req.body) 104 | .then(numberOfUpdatedDocs => { 105 | if (numberOfUpdatedDocs) { 106 | return res.status(204).end(); 107 | } 108 | 109 | return send404Error('team not found', res); 110 | }) 111 | .catch(err => send500Error('Failed to update team', err, res)); 112 | } 113 | 114 | /** 115 | * Delete a team 116 | * 117 | * @param {Request} req 118 | * @param {Response} res 119 | */ 120 | function remove(req, res) { 121 | return lib.team.removeById(req.params.id) 122 | .then(deletedTeam => { 123 | if (deletedTeam) { 124 | return res.status(204).end(); 125 | } 126 | 127 | return send404Error('team not found', res); 128 | }) 129 | .catch(err => send500Error('Failed to delete team', err, res)); 130 | } 131 | }; 132 | --------------------------------------------------------------------------------