├── .gitignore ├── README.md ├── app.js ├── config ├── config.example.js └── locale.js ├── controllers ├── index.js ├── message.js ├── ticket.js └── user.js ├── gulpfile.js ├── libs └── i18next.js ├── models ├── file.js ├── index.js ├── mail.js ├── message.js ├── project.js ├── ticket.js └── user.js ├── package.json ├── public ├── css │ ├── .gitignore │ ├── _adaptive.less │ ├── _common.less │ ├── _reset.less │ ├── _var.less │ ├── pages │ │ ├── _include.less │ │ ├── add-ticket.less │ │ ├── text-page.less │ │ └── ticket-list.less │ ├── react-select.less │ ├── style.css │ └── style.less ├── favicon.ico ├── images │ ├── base64 │ │ ├── arr.png │ │ ├── arrow-up.png │ │ ├── search-active.png │ │ ├── search.png │ │ └── select-plus.png │ ├── bg.jpg │ ├── bg.png │ ├── space.jpg │ ├── sprite.png │ └── sprite.psd ├── js │ ├── actions.js │ ├── actions │ │ ├── root.js │ │ └── tickets.js │ ├── application.js │ ├── bundle.js │ ├── components │ │ ├── copyright.js │ │ ├── file-drop.js │ │ ├── home.js │ │ ├── home │ │ │ ├── add-ticket.js │ │ │ ├── add-ticket │ │ │ │ └── add-form.js │ │ │ └── login.js │ │ ├── ie-check.js │ │ ├── language-switcher.js │ │ ├── root.js │ │ ├── tickets.js │ │ └── tickets │ │ │ ├── detail.js │ │ │ ├── detail │ │ │ ├── header.js │ │ │ ├── messages.js │ │ │ ├── messages │ │ │ │ ├── answer-form.js │ │ │ │ └── message.js │ │ │ └── tags.js │ │ │ ├── list.js │ │ │ ├── list │ │ │ ├── content.js │ │ │ ├── content │ │ │ │ └── ticket.js │ │ │ ├── header.js │ │ │ └── header │ │ │ │ ├── sort-item.js │ │ │ │ └── tags-filter.js │ │ │ ├── sidebar.js │ │ │ └── sidebar │ │ │ ├── folders.js │ │ │ ├── folders │ │ │ └── index.js │ │ │ ├── projects.js │ │ │ └── projects │ │ │ └── index.js │ ├── constants │ │ ├── action-types.js │ │ └── folder-type.js │ ├── i18n.js │ ├── locale.js │ ├── mixins │ │ └── form-mixin.js │ ├── reducers │ │ ├── index.js │ │ ├── root.js │ │ └── tickets.js │ └── store │ │ └── configure-store.js ├── locales │ ├── _lang_.example.json │ ├── en.json │ └── ru.json ├── pictures │ ├── logo-big.jpg │ └── logo.png └── tickets │ └── .gitignore ├── services ├── config.js ├── i18n.js └── template-string.js ├── temp └── .gitignore └── views ├── _layout ├── footer.ejs └── header.ejs ├── agreement-en.ejs ├── agreement.ejs ├── error.ejs └── index.ejs /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /node_modules 3 | /config/config.js 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HelpDesk 2 | 3 | ### Лицензия 4 | GPL v3 http://www.gnu.org/licenses/gpl-3.0.ru.html 5 | 6 | ### Установка 7 | ```sh 8 | mkdir helpdesk 9 | cd ./helpdesk 10 | git clone https://github.com/SibirixScrum/HelpDesk.git ./ 11 | npm i 12 | ``` 13 | 14 | ### Настройка 15 | ```sh 16 | cd ./config 17 | cp ./config.example.js ./config.js 18 | mcedit ./config.js 19 | ``` 20 | 21 | ### Основые опции 22 | - connectString: 'mongodb://localhost/helpdesk' — база в mongo. Будет создана при первом запуске, если еще не существует. 23 | - exports.projects — настройки проектов. 24 | - responsible: 'tester@example.com' — почта администратора проекта. Аккаунт создается автоматически. Пароль отправляется на почту. 25 | - exports.socketIo: 'SECRETKEY' — секретный ключ для шифрования куков. 26 | - exports.session.secret: 'SECRETKEY' — секретный ключ для шифрования сессий. 27 | - exports.locales=['ru', 'en'] — доступные языки: английский и русский. 28 | - en.json — английский язык. 29 | - ru.json — русский язык. 30 | 31 | ### Запуск 32 | ```sh 33 | node app.js 34 | ``` 35 | 36 | --- 37 | 38 | # HelpDesk 39 | 40 | ### Licence 41 | GPL v3 http://www.gnu.org/licenses/gpl-3.0.en.html 42 | 43 | ### Installation 44 | ```sh 45 | mkdir helpdesk 46 | cd ./helpdesk 47 | git clone https://github.com/SibirixScrum/HelpDesk.git ./ 48 | npm i 49 | ``` 50 | 51 | ### Configuring 52 | ```sh 53 | cd ./config 54 | cp ./config.example.js ./config.js 55 | mcedit ./config.js 56 | ``` 57 | 58 | ### Basic configurations 59 | - connectString: 'mongodb://localhost/helpdesk' — MongoDB database will be created (in case there isn’t any done yet by the time the program is launched for the first time). 60 | - exports.projects — projects settings. 61 | - responsible: 'tester@example.com' — email of project Administrator. The account is created automatically, the password is sent to the email. 62 | - exports.socketIo: 'SECRETKEY' — a private key for cookies encryption. 63 | - exports.session.secret: 'SECRETKEY' — a private key for sessions encryption. 64 | - exports.locales=['ru', 'en'] — available languages: English and Russian. 65 | - en.json — English. 66 | - ru.json — Russian. 67 | 68 | 69 | ### Launch 70 | ```sh 71 | node app.js 72 | ``` 73 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // Node modules 2 | var bodyParser = require('body-parser'); 3 | var cookieParser = require('cookie-parser'); 4 | var express = require('express'); 5 | var favicon = require('serve-favicon'); 6 | var fs = require('fs'); 7 | var http = require('http'); 8 | var https = require('https'); 9 | var logger = require('morgan'); 10 | var mongoose = require('mongoose'); 11 | var mongoStore = require('connect-mongodb'); 12 | var path = require('path'); 13 | var session = require('express-session'); 14 | var jwt = require('jsonwebtoken'); 15 | var socketIo = require('socket.io'); 16 | var socketioJwt = require('socketio-jwt'); 17 | 18 | const i18next = require('./libs/i18next'); 19 | const middleware = require('i18next-express-middleware'); 20 | const i18nFsBackend = require('i18next-node-fs-backend'); 21 | const intervalPlural = require('i18next-intervalplural-postprocessor'); 22 | 23 | // Читаем конфиг 24 | var extend = require('extend'); 25 | var configExample = require('./config/config.example'); 26 | 27 | let lang = require('./config/locale'); 28 | 29 | try { 30 | var config = require('./config/config'); 31 | } catch (e) { 32 | throw new Error('Not found config file! Copy ./config/config.example.js to ./config/config.js and edit it.'); 33 | } 34 | 35 | global.config = config = extend(true, configExample, config, { 36 | get: function(name, def) { 37 | var namePath = name.split('.'); 38 | var result = this; 39 | 40 | namePath.forEach(function(key){ 41 | if (undefined === result) { 42 | return false; 43 | } 44 | 45 | result = result[key]; 46 | return true; 47 | }); 48 | 49 | if (undefined === result) { 50 | return def; 51 | } 52 | 53 | return result; 54 | } 55 | }); 56 | 57 | // инициализация i18n 58 | i18next 59 | .use(i18nFsBackend) 60 | .use(middleware.LanguageDetector) 61 | .use(intervalPlural) 62 | .init({ 63 | backend: { 64 | loadPath: path.join(__dirname, 'public') + '/locales/{{lng}}.json', 65 | }, 66 | fallbackLng: lang.fallbackLng, 67 | lowerCaseLng: true, 68 | preload: lang.locales, 69 | interpolation: { 70 | escapeValue: false 71 | }, 72 | 73 | parseMissingKeyHandler: function(key, options) { 74 | const regex = /^(\w+\.\w+\.)/; 75 | 76 | if (regex.exec(key) !== null) { 77 | let resultKey = key.replace(regex, ''); 78 | return i18next.t(resultKey, options); 79 | } 80 | 81 | return key; 82 | 83 | }, 84 | 85 | detection: config.detection 86 | }); 87 | 88 | global.defaultTranslator = i18next.getFixedT(lang.defaultLanguage); 89 | 90 | var models = require('./models/'); 91 | 92 | models.setCallback(function() { 93 | 94 | models.project.validateProjectsConfig(); 95 | models.project.checkResponsible(); 96 | 97 | var indexController = require('./controllers/index'); 98 | var ticketController = require('./controllers/ticket'); 99 | var messageController = require('./controllers/message'); 100 | var userController = require('./controllers/user'); 101 | 102 | var app = express(); 103 | 104 | app.set('port', config.port || 3000); 105 | app.set('views', path.join(__dirname, 'views')); 106 | app.set('view engine', 'ejs'); 107 | 108 | // Использование сессий 109 | app.use(session({ 110 | cookie: {maxAge: config.session.maxAge}, 111 | secret: config.session.secret, 112 | store: new mongoStore({db: mongoose.connection.db}), 113 | resave: true, 114 | saveUninitialized: true 115 | })); 116 | 117 | app.use( 118 | middleware.handle(i18next, { 119 | removeLngFromUrl: false 120 | }) 121 | ); 122 | 123 | //app.use(favicon(__dirname + '/public/favicon.ico')); 124 | app.use(logger('dev')); 125 | app.use(bodyParser.json({ limit: '10mb' })); 126 | app.use(bodyParser.urlencoded()); 127 | app.use(cookieParser()); 128 | app.use(express.static(path.join(__dirname, 'public'))); 129 | 130 | app.use('/ticket', ticketController); 131 | app.use('/message', messageController); 132 | app.use('/user', userController); 133 | app.use('/', indexController); 134 | 135 | /// catch 404 and forwarding to error handler 136 | app.use(function(req, res, next) { 137 | var err = new Error('Not Found'); 138 | err.status = 404; 139 | next(err); 140 | }); 141 | 142 | /// error handlers 143 | 144 | // development error handler 145 | // will print stacktrace 146 | if (app.get('env') === 'development') { 147 | app.use(function(err, req, res, next) { 148 | res.status(err.status || 500); 149 | 150 | if (req.headers['accept'] == 'application/json' || req.headers['x-requested-with'] && (req.headers['x-requested-with'].toLowerCase() == 'xmlhttprequest')) { 151 | res.end(JSON.stringify({result: false, message: err.message, error: err})); 152 | } else { 153 | res.render('error', { 154 | message: err.message, 155 | error: err 156 | }); 157 | } 158 | }); 159 | } 160 | 161 | // production error handler 162 | // no stacktraces leaked to user 163 | app.use(function(err, req, res, next) { 164 | res.status(err.status || 500); 165 | 166 | if (req.headers['x-requested-with'] && (req.headers['x-requested-with'].toLowerCase() == 'xmlhttprequest')) { 167 | res.end(JSON.stringify({result: false, message: err.message, error: err})); 168 | } else { 169 | res.render('error', { 170 | message: err.message, 171 | error: err 172 | }); 173 | } 174 | }); 175 | 176 | var httpServer; 177 | 178 | if (config.ssl && config.ssl.enabled) { 179 | var privateKey = fs.readFileSync(config.ssl.key); 180 | var certificate = fs.readFileSync(config.ssl.cert); 181 | var credentials = {key: privateKey, cert: certificate}; 182 | httpServer = https.createServer(credentials, app); 183 | } else { 184 | httpServer = http.createServer(app); 185 | } 186 | 187 | var io = socketIo.listen(httpServer); 188 | io.sockets 189 | .on('connection', socketioJwt.authorize({ 190 | secret: global.config.socketIo.secret, 191 | timeout: global.config.socketIo.timeout 192 | })).on('authenticated', function(socket) { 193 | var userEmail = socket.decoded_token; 194 | var projectsList = models.project.getResponsibleProjectsList(userEmail); 195 | 196 | if (projectsList.length) { 197 | // ТП 198 | for (var i = 0; i < projectsList.length; i++) { 199 | socket.join(projectsList[i].code); 200 | } 201 | } else { 202 | // Клиент 203 | socket.join(userEmail); 204 | } 205 | } 206 | ); 207 | 208 | global.io = io; 209 | 210 | models.mail.startCheckTimeout(); 211 | 212 | httpServer.listen(app.get('port'), function () { 213 | console.log('Express server listening on port ' + app.get('port')); 214 | }); 215 | }); 216 | -------------------------------------------------------------------------------- /config/config.example.js: -------------------------------------------------------------------------------- 1 | exports.port = '3000'; 2 | exports.serverName = 'localhost:' + exports.port; 3 | 4 | exports.appRoot = __dirname + '/../'; // Relative to this file. Usually no need to change this 5 | exports.tmpDir = exports.appRoot + 'temp/'; // Uploaded files temp folder 6 | 7 | /** 8 | * Database, mongodb 9 | */ 10 | exports.db = { 11 | connectString: 'mongodb://localhost/helpdesk' 12 | }; 13 | 14 | /** 15 | * SocketIO auth timeouts 16 | */ 17 | exports.socketIo = { 18 | expire: 60 * 5, // in seconds 19 | timeout: 15000, // in milliseconds 20 | secret: 'SECRET' // todo Fill secret key 21 | }; 22 | 23 | /** 24 | * Session storage (in mongodb) 25 | */ 26 | exports.session = { 27 | maxAge: 30 * 60 * 1000, // in seconds 28 | secret: 'SECRET' // todo Fill secret key 29 | }; 30 | 31 | /** 32 | * Project list 33 | */ 34 | exports.projects = [ 35 | { 36 | code: 'HELPDESK', // Project code 37 | domain: 'localhost:3000', // Project domain (each project should have it's own domain) 38 | name: 'projects.helpdesk.name', // Project name 39 | title: 'projects.helpdesk.formTitle', // Title for form 40 | letters: 'HD', // Short project code, used in ticket numbers 41 | responsible: 'example@example.com', // Email of responsible. Auto-creates user account with this email and sends password to it 42 | email: { // Mail parsing service 43 | login: "", // todo Fill email login 44 | password: "", // todo Fill email password 45 | host: 'imap.gmail.com', 46 | port: 993, 47 | tls: true, 48 | sign: 'Helpdesk', 49 | checkInterval: 30, // in seconds 50 | smtpHost: 'smtp.gmail.com', 51 | smtpPort: 465, 52 | smtpSecure: true, 53 | keepAlive: false // Persistent connection 54 | }, 55 | files: [ // Array of files for project, use for documentation 56 | /*{ 57 | name: 'Displayed text', 58 | path: '/docs/manual.pdf' // File path, relative to www/public/ 59 | }, { 60 | name: '', 61 | path: '' 62 | },*/ 63 | ], 64 | filesPos: 'right', // show project files on right or bottom of ticket form 65 | footer: true, // bool 66 | bodyClass: false // false or string 67 | }/* 68 | , { 69 | code: 'PROJECT2', 70 | ... 71 | }*/ 72 | ]; 73 | 74 | exports.projectColors = [ 75 | 'ff0036', 'ffae00', 'ffe400', '96ff00', '00a2ff', '005aff', '8400ff', 76 | 'ff0078', 'ff9600', 'f6ff00', 'ccf801', '00ccff', '065cec', 'a800ff', 77 | 'ff00c6', 'ff7e00', 'd2ff00', 'e3eb00', '00eaff', '0957d9', 'b716fa' 78 | ]; 79 | 80 | // Time to mark tickets with red color 81 | exports.markDateRed = 60 * 60 * 24 * 3; // 3 days 82 | exports.files = { 83 | maxSize: 5 * 1024 * 1024, // In bytes 84 | maxCount: 10, 85 | extensions: [ 86 | 'jpg', 'jpeg', 'gif', 'png', 'bmp', // images 87 | 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'ods', 'odt', 'txt', // Documents 88 | 'rar', 'zip', '7z', 'tgz', 'gz', 'bz2' // archives 89 | ] 90 | }; 91 | 92 | exports.tickets = { 93 | editor: { 94 | allowedTags: "