Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
countly-server/frontend/express/app.js /
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
1563 lines (1425 sloc)
63.1 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Main dashboard process app.js | |
| * @module frontend/express/app | |
| */ | |
| // Set process name | |
| process.title = "countly: dashboard node " + process.argv[1]; | |
| var versionInfo = require('./version.info'), | |
| pack = require('../../package.json'), | |
| COUNTLY_VERSION = versionInfo.version, | |
| COUNTLY_COMPANY = versionInfo.company || '', | |
| COUNTLY_TYPE = versionInfo.type, | |
| COUNTLY_PAGE = versionInfo.page = (!versionInfo.title) ? "http://count.ly" : null, | |
| COUNTLY_NAME = versionInfo.title = versionInfo.title || "Countly", | |
| COUNTLY_DOCUMENTATION_LINK = (typeof versionInfo.documentationLink === "undefined") ? true : (typeof versionInfo.documentationLink === "string") ? versionInfo.documentationLink : (typeof versionInfo.documentationLink === "boolean") ? versionInfo.documentationLink : true, | |
| COUNTLY_FEEDBACK_LINK = (typeof versionInfo.feedbackLink === "undefined") ? true : (typeof versionInfo.feedbackLink === "string") ? versionInfo.feedbackLink : (typeof versionInfo.feedbackLink === "boolean") ? versionInfo.feedbackLink : true, | |
| COUNTLY_HELPCENTER_LINK = (typeof versionInfo.helpCenterLink === "undefined") ? true : (typeof versionInfo.helpCenterLink === "string") ? versionInfo.helpCenterLink : (typeof versionInfo.helpCenterLink === "boolean") ? versionInfo.helpCenterLink : true, | |
| COUNTLY_FEATUREREQUEST_LINK = (typeof versionInfo.featureRequestLink === "undefined") ? true : (typeof versionInfo.featureRequestLink === "string") ? versionInfo.featureRequestLink : (typeof versionInfo.featureRequestLink === "boolean") ? versionInfo.featureRequestLink : true, | |
| express = require('express'), | |
| SkinStore = require('./libs/connect-mongo.js'), | |
| expose = require('./libs/express-expose.js'), | |
| dollarDefender = require('./libs/dollar-defender.js')({ | |
| message: "Dollar sign is not allowed in keys", | |
| hook: function(req) { | |
| console.log("Possible Dollar sign injection", req.originalUrl, req.query, req.params, req.body); | |
| } | |
| }), | |
| crypto = require('crypto'), | |
| fs = require('fs'), | |
| path = require('path'), | |
| jimp = require('jimp'), | |
| request = require('request'), | |
| flash = require('connect-flash'), | |
| cookieParser = require('cookie-parser'), | |
| formidable = require('formidable'), | |
| session = require('express-session'), | |
| methodOverride = require('method-override'), | |
| csrf = require('csurf')(), | |
| errorhandler = require('errorhandler'), | |
| basicAuth = require('basic-auth'), | |
| bodyParser = require('body-parser'), | |
| _ = require('underscore'), | |
| countlyMail = require('../../api/parts/mgmt/mail.js'), | |
| // countlyStats = require('../../api/parts/data/stats.js'), | |
| countlyFs = require('../../api/utils/countlyFs.js'), | |
| common = require('../../api/utils/common.js'), | |
| preventBruteforce = require('./libs/preventBruteforce.js'), | |
| plugins = require('../../plugins/pluginManager.js'), | |
| countlyConfig = require('./config', 'dont-enclose'), | |
| log = require('../../api/utils/log.js')('core:app'), | |
| // url = require('url'), | |
| authorize = require('../../api/utils/authorizer.js'), //for token validations | |
| languages = require('../../frontend/express/locale.conf'), | |
| render = require('../../api/utils/render.js'), | |
| rateLimit = require("express-rate-limit"), | |
| membersUtility = require("./libs/members.js"), | |
| argon2 = require('argon2'), | |
| countlyCommon = require('../../api/lib/countly.common.js'), | |
| timezones = require('../../api/utils/timezones.js').getTimeZones; | |
| console.log("Starting Countly", "version", pack.version); | |
| var COUNTLY_NAMED_TYPE = "Countly Community Edition v" + COUNTLY_VERSION; | |
| var COUNTLY_TYPE_CE = true; | |
| var COUNTLY_TRIAL = (versionInfo.trial) ? true : false; | |
| var COUNTLY_TRACK_TYPE = "OSS"; | |
| if (versionInfo.footer) { | |
| COUNTLY_NAMED_TYPE = versionInfo.footer; | |
| COUNTLY_TYPE_CE = false; | |
| if (COUNTLY_NAMED_TYPE === "Countly Cloud") { | |
| COUNTLY_TRACK_TYPE = "Cloud"; | |
| } | |
| else if (COUNTLY_TYPE !== "777a2bf527a18e0fffe22fb5b3e322e68d9c07a6") { | |
| COUNTLY_TRACK_TYPE = "Enterprise"; | |
| } | |
| } | |
| else if (COUNTLY_TYPE !== "777a2bf527a18e0fffe22fb5b3e322e68d9c07a6") { | |
| COUNTLY_NAMED_TYPE = "Countly Enterprise Edition v" + COUNTLY_VERSION; | |
| COUNTLY_TYPE_CE = false; | |
| COUNTLY_TRACK_TYPE = "Enterprise"; | |
| } | |
| if (!countlyConfig.cookie) { | |
| countlyConfig.cookie = { | |
| httpOnly: true, | |
| maxAge: 1000 * 60 * 60 * 24, | |
| secure: countlyConfig.web.secure_cookies || false, | |
| maxAgeLogin: 1000 * 60 * 60 * 24 * 365 | |
| }; | |
| } | |
| plugins.setConfigs("frontend", { | |
| production: true, | |
| theme: countlyConfig.web.theme || "", | |
| session_timeout: 30, | |
| use_google: true, | |
| code: true, | |
| google_maps_api_key: "", | |
| offline_mode: false | |
| }); | |
| plugins.setUserConfigs("frontend", { | |
| production: false, | |
| theme: false, | |
| session_timeout: false, | |
| use_google: false, | |
| code: false, | |
| google_maps_api_key: "" | |
| }); | |
| plugins.setConfigs("security", { | |
| login_tries: 3, | |
| login_wait: 5 * 60, | |
| password_min: 8, | |
| password_char: true, | |
| password_number: true, | |
| password_symbol: true, | |
| password_expiration: 0, | |
| password_rotation: 3, | |
| password_autocomplete: true, | |
| dashboard_additional_headers: "X-Frame-Options:deny\nX-XSS-Protection:1; mode=block\nStrict-Transport-Security:max-age=31536000 ; includeSubDomains\nX-Content-Type-Options: nosniff", | |
| api_additional_headers: "X-Frame-Options:deny\nX-XSS-Protection:1; mode=block\nAccess-Control-Allow-Origin:*", | |
| dashboard_rate_limit_window: 60, | |
| dashboard_rate_limit_requests: 500 | |
| }); | |
| process.on('uncaughtException', (err) => { | |
| console.log('Caught exception: %j', err, err.stack); | |
| if (log && log.e) { | |
| log.e('Logging caught exception'); | |
| } | |
| process.exit(1); | |
| }); | |
| process.on('unhandledRejection', (reason, p) => { | |
| console.log("Unhandled rejection at: Promise ", p, " reason: ", reason); | |
| if (log && log.e) { | |
| log.e("Logging unhandled rejection"); | |
| } | |
| }); | |
| if (countlyConfig.web && countlyConfig.web.track === "all") { | |
| countlyConfig.web.track = null; | |
| } | |
| var countlyConfigOrig = JSON.parse(JSON.stringify(countlyConfig)); | |
| Promise.all([plugins.dbConnection(countlyConfig), plugins.dbConnection("countly_fs")]).then(function(dbs) { | |
| var countlyDb = dbs[0]; | |
| //reference for consistency between app and api processes | |
| membersUtility.db = common.db = countlyDb; | |
| countlyFs.setHandler(dbs[1]); | |
| //checking remote configuration | |
| membersUtility.recheckConfigs(countlyConfigOrig, countlyConfig); | |
| /** | |
| * Create sha1 hash string | |
| * @param {string} str - string to hash | |
| * @param {boolean} addSalt - should salt be added | |
| * @returns {string} hashed string | |
| **/ | |
| function sha1Hash(str, addSalt) { | |
| var salt = (addSalt) ? new Date().getTime() : ""; | |
| return crypto.createHmac('sha1', salt + "").update(str + "").digest('hex'); | |
| } | |
| /** | |
| * Create sha512 hash string | |
| * @param {string} str - string to hash | |
| * @param {boolean} addSalt - should salt be added | |
| * @returns {string} hashed string | |
| **/ | |
| function sha512Hash(str, addSalt) { | |
| var salt = (addSalt) ? new Date().getTime() : ""; | |
| return crypto.createHmac('sha512', salt + "").update(str + "").digest('hex'); | |
| } | |
| /** | |
| * Create argon2 hash string | |
| * @param {string} str - string to hash | |
| * @returns {promise} hash promise | |
| **/ | |
| function argon2Hash(str) { | |
| return argon2.hash(str); | |
| } | |
| /** | |
| * Verify argon2 hash string | |
| * @param {string} hashedStr - argon2 hashed string | |
| * @param {string} str - string for verify | |
| * @returns {promise} verify promise | |
| **/ | |
| function verifyArgon2Hash(hashedStr, str) { | |
| return argon2.verify(hashedStr, str); | |
| } | |
| /** | |
| * Is hashed string argon2? | |
| * @param {string} hashedStr | argon2 hashed string | |
| * @returns {boolean} return true if string hashed by argon2 | |
| */ | |
| function isArgon2Hash(hashedStr) { | |
| return hashedStr.includes("$argon2"); | |
| } | |
| /** | |
| * Verify member for Argon2 Hash | |
| * @param {string} username | User name | |
| * @param {password} password | Password string | |
| * @param {Function} callback | Callback function | |
| */ | |
| function verifyMemberArgon2Hash(username, password, callback) { | |
| var secret = countlyConfig.passwordSecret || ""; | |
| password = password + secret; | |
| countlyDb.collection('members').findOne({$and: [{ $or: [ {"username": username}, {"email": username}]}]}, (err, member) => { | |
| if (member) { | |
| if (isArgon2Hash(member.password)) { | |
| verifyArgon2Hash(member.password, password).then(match => { | |
| if (match) { | |
| callback(undefined, member); | |
| } | |
| else { | |
| callback("Password is wrong!"); | |
| } | |
| }).catch(function() { | |
| callback("Password is wrong!"); | |
| }); | |
| } | |
| else { | |
| var password_SHA1 = sha1Hash(password); | |
| var password_SHA5 = sha512Hash(password); | |
| if (member.password === password_SHA1 || member.password === password_SHA5) { | |
| argon2Hash(password).then(password_ARGON2 => { | |
| updateUserPasswordToArgon2(member._id, password_ARGON2); | |
| callback(undefined, member); | |
| }).catch(function() { | |
| callback("Password is wrong!"); | |
| }); | |
| } | |
| else { | |
| callback("Password is wrong!"); | |
| } | |
| } | |
| } | |
| else { | |
| callback("Username is wrong!"); | |
| } | |
| }); | |
| } | |
| /** | |
| * Update user password to new sha512 hash | |
| * @param {string} id - id of the user document | |
| * @param {string} password - password to hash | |
| **/ | |
| function updateUserPasswordToArgon2(id, password) { | |
| countlyDb.collection('members').update({ _id: id}, { $set: { password: password}}); | |
| } | |
| /** | |
| * Check if user is global admin | |
| * @param {object} req - request object | |
| * @returns {boolean} true if global admin | |
| **/ | |
| function isGlobalAdmin(req) { | |
| return (req.session.gadm); | |
| } | |
| /** | |
| * Sort array by list of | |
| * @param {array} arrayToSort - array to sort | |
| * @param {array} sortList - list of values by which to sort | |
| * @returns {array} sorted array | |
| **/ | |
| function sortBy(arrayToSort, sortList) { | |
| if (!sortList.length) { | |
| return arrayToSort; | |
| } | |
| var tmpArr = [], | |
| retArr = []; | |
| for (let i = 0; i < arrayToSort.length; i++) { | |
| var objId = arrayToSort[i]._id + ""; | |
| if (sortList.indexOf(objId) !== -1) { | |
| tmpArr[sortList.indexOf(objId)] = arrayToSort[i]; | |
| } | |
| } | |
| for (let i = 0; i < tmpArr.length; i++) { | |
| if (tmpArr[i]) { | |
| retArr[retArr.length] = tmpArr[i]; | |
| } | |
| } | |
| for (let i = 0; i < arrayToSort.length; i++) { | |
| if (retArr.indexOf(arrayToSort[i]) === -1) { | |
| retArr[retArr.length] = arrayToSort[i]; | |
| } | |
| } | |
| return retArr; | |
| } | |
| var app = express(); | |
| app = expose(app); | |
| app.enable('trust proxy'); | |
| app.set('x-powered-by', false); | |
| const limiter = rateLimit({ | |
| windowMs: parseInt(plugins.getConfig("security").dashboard_rate_limit_window) * 1000, | |
| max: parseInt(plugins.getConfig("security").dashboard_rate_limit_requests), | |
| headers: false, | |
| //limit only in production mode | |
| skip: function() { | |
| return !plugins.getConfig("frontend").production || plugins.getConfig("security").dashboard_rate_limit_requests <= 0; | |
| } | |
| }); | |
| // apply to all requests | |
| app.use(limiter); | |
| var loadedThemes = {}; | |
| var curTheme = countlyConfig.web.theme || ""; | |
| /** | |
| * Load theme files | |
| * @param {string} theme - theme name | |
| * @param {function} callback - when loading files done | |
| **/ | |
| app.loadThemeFiles = function(theme, callback) { | |
| if (!loadedThemes[theme]) { | |
| var tempThemeFiles = {css: [], js: []}; | |
| if (theme && theme.length) { | |
| var themeDir = path.resolve(__dirname, "public/themes/" + theme + "/"); | |
| fs.readdir(themeDir, function(err, list) { | |
| if (err) { | |
| if (callback) { | |
| callback(tempThemeFiles); | |
| } | |
| return ; | |
| } | |
| var ext; | |
| for (var i = 0; i < list.length; i++) { | |
| ext = list[i].split(".").pop(); | |
| if (!tempThemeFiles[ext]) { | |
| tempThemeFiles[ext] = []; | |
| } | |
| tempThemeFiles[ext].push(countlyConfig.path + '/themes/' + theme + "/" + list[i]); | |
| } | |
| if (callback) { | |
| callback(tempThemeFiles); | |
| } | |
| loadedThemes[theme] = tempThemeFiles; | |
| }); | |
| } | |
| else if (callback) { | |
| callback(tempThemeFiles); | |
| } | |
| } | |
| else if (callback) { | |
| callback(loadedThemes[theme]); | |
| } | |
| }; | |
| plugins.loadConfigs(countlyDb, function() { | |
| curTheme = plugins.getConfig("frontend").theme; | |
| app.loadThemeFiles(curTheme); | |
| app.dashboard_headers = plugins.getConfig("security").dashboard_additional_headers; | |
| }); | |
| app.engine('html', require('ejs').renderFile); | |
| app.set('views', __dirname + '/views'); | |
| app.set('view engine', 'html'); | |
| app.set('view options', {layout: false}); | |
| app.use('/stylesheets/ionicons/fonts/', function(req, res, next) { | |
| res.header("Access-Control-Allow-Origin", "*"); | |
| res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); | |
| next(); | |
| }); | |
| app.use('/fonts/', function(req, res, next) { | |
| res.header("Access-Control-Allow-Origin", "*"); | |
| res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); | |
| next(); | |
| }); | |
| app.use('*.svg', function(req, res, next) { | |
| res.setHeader('Content-Type', 'image/svg+xml; charset=UTF-8'); | |
| next(); | |
| }); | |
| /** | |
| * Add headers to request | |
| * @param {object} req - request object | |
| * @param {object} res - response object | |
| **/ | |
| function add_headers(req, res) { | |
| if (countlyConfig.web.secure_cookies) { | |
| //we can't detect if it uses https behind nginx, without specific nginx configuration, so we assume it does | |
| req.headers["x-forwarded-proto"] = "https"; | |
| } | |
| //set provided in configuration headers | |
| if (app.dashboard_headers) { | |
| var headers = app.dashboard_headers.replace(/\r\n|\r|\n/g, "\n").split("\n"); | |
| var parts; | |
| for (let i = 0; i < headers.length; i++) { | |
| if (headers[i] && headers[i].length) { | |
| parts = headers[i].split(/:(.+)?/); | |
| if (parts.length === 3) { | |
| res.header(parts[0], parts[1]); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| app.use(function(req, res, next) { | |
| add_headers(req, res); | |
| next(); | |
| }); | |
| plugins.loadAppStatic(app, countlyDb, express); | |
| app.use(cookieParser()); | |
| //server theme images | |
| app.use(function(req, res, next) { | |
| if (req.url.indexOf(countlyConfig.path + '/images/') === 0) { | |
| var urlPath = req.url.replace(countlyConfig.path, ""); | |
| var theme = req.cookies.theme || curTheme; | |
| if (theme && theme.length) { | |
| fs.exists(__dirname + '/public/themes/' + theme + urlPath, function(exists) { | |
| if (exists) { | |
| res.sendFile(__dirname + '/public/themes/' + theme + urlPath); | |
| } | |
| else { | |
| next(); | |
| } | |
| }); | |
| } | |
| else { //serve default location | |
| next(); | |
| } | |
| } | |
| else { | |
| next(); | |
| } | |
| }); | |
| //serve app images | |
| app.get(countlyConfig.path + '/appimages/*', function(req, res) { | |
| if (!req.params || !req.params[0] || req.params[0] === '') { | |
| res.sendFile(__dirname + '/public/images/default_app_icon.png'); | |
| } | |
| else { | |
| countlyFs.getStats("appimages", __dirname + '/public/' + req.path, {id: req.params[0]}, function(err, stats) { | |
| if (err || !stats || !stats.size) { | |
| res.sendFile(__dirname + '/public/images/default_app_icon.png'); | |
| } | |
| else { | |
| countlyFs.getStream("appimages", __dirname + '/public/' + req.path, {id: req.params[0]}, function(err2, stream) { | |
| if (err2 || !stream) { | |
| res.sendFile(__dirname + '/public/images/default_app_icon.png'); | |
| } | |
| else { | |
| res.writeHead(200, { | |
| 'Accept-Ranges': 'bytes', | |
| 'Cache-Control': 'public, max-age=31536000', | |
| 'Connection': 'keep-alive', | |
| 'Date': new Date().toUTCString(), | |
| 'Last-Modified': stats.mtime.toUTCString(), | |
| 'Content-Type': 'image/png', | |
| 'Content-Length': stats.size | |
| }); | |
| stream.pipe(res); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| }); | |
| //serve member images | |
| app.get(countlyConfig.path + '/memberimages/*', function(req, res) { | |
| if (!req.params || !req.params[0] || req.params[0] === '') { | |
| res.sendFile(__dirname + '/public/images/default_member_icon.png'); | |
| } | |
| else { | |
| countlyFs.getStats("memberimages", __dirname + '/public/' + req.path, {id: req.params[0]}, function(err, stats) { | |
| if (err || !stats || !stats.size) { | |
| res.sendFile(__dirname + '/public/images/default_member_icon.png'); | |
| } | |
| else { | |
| countlyFs.getStream("memberimages", __dirname + '/public/' + req.path, {id: req.params[0]}, function(err2, stream) { | |
| if (err2 || !stream) { | |
| res.sendFile(__dirname + '/public/images/default_member_icon.png'); | |
| } | |
| else { | |
| res.writeHead(200, { | |
| 'Accept-Ranges': 'bytes', | |
| 'Cache-Control': 'public, max-age=31536000', | |
| 'Connection': 'keep-alive', | |
| 'Date': new Date().toUTCString(), | |
| 'Last-Modified': stats.mtime.toUTCString(), | |
| 'Content-Type': 'image/png', | |
| 'Content-Length': stats.size | |
| }); | |
| stream.pipe(res); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| }); | |
| app.get(countlyConfig.path + "*/screenshots/*", function(req, res) { | |
| countlyFs.getStats("screenshots", __dirname + '/public/' + req.path, {id: "core"}, function(err, stats) { | |
| if (err || !stats || !stats.size) { | |
| return res.send(false); | |
| } | |
| countlyFs.getStream("screenshots", __dirname + '/public/' + req.path, {id: "core"}, function(err2, stream) { | |
| if (err2 || !stream) { | |
| return res.send(false); | |
| } | |
| res.writeHead(200, { | |
| 'Accept-Ranges': 'bytes', | |
| 'Cache-Control': 'public, max-age=31536000', | |
| 'Connection': 'keep-alive', | |
| 'Date': new Date().toUTCString(), | |
| 'Last-Modified': stats.mtime.toUTCString(), | |
| 'Content-Type': 'image/png', | |
| 'Content-Length': stats.size | |
| }); | |
| stream.pipe(res); | |
| }); | |
| }); | |
| }); | |
| var oneYear = 31557600000; | |
| app.use(countlyConfig.path, express.static(__dirname + '/public', { maxAge: oneYear })); | |
| app.use(session({ | |
| secret: countlyConfig.web.session_secret || 'countlyss', | |
| name: countlyConfig.web.session_name || 'connect.sid', | |
| cookie: countlyConfig.cookie, | |
| store: new SkinStore(countlyDb), | |
| saveUninitialized: false, | |
| resave: true, | |
| rolling: true, | |
| proxy: true, | |
| unset: "destroy" | |
| })); | |
| app.use(bodyParser.json()); // to support JSON-encoded bodies | |
| app.use(bodyParser.urlencoded({ // to support URL-encoded bodies | |
| extended: true | |
| })); | |
| app.use(function(req, res, next) { | |
| var contentType = req.headers['content-type']; | |
| if (req.method.toLowerCase() === 'post' && contentType && contentType.indexOf('multipart/form-data') >= 0) { | |
| var form = new formidable.IncomingForm(); | |
| form.uploadDir = __dirname + '/uploads'; | |
| form.parse(req, function(err, fields, files) { | |
| req.files = files; | |
| if (!req.body) { | |
| req.body = {}; | |
| } | |
| for (let i in fields) { | |
| if (typeof req.body[i] === "undefined") { | |
| req.body[i] = fields[i]; | |
| } | |
| } | |
| next(); | |
| }); | |
| } | |
| else { | |
| next(); | |
| } | |
| }); | |
| app.use(flash()); | |
| app.use(function(req, res, next) { | |
| req.template = {}; | |
| req.template.html = ""; | |
| req.template.js = ""; | |
| req.template.css = ""; | |
| req.template.form = ""; | |
| req.countly = { | |
| company: COUNTLY_COMPANY, | |
| version: COUNTLY_VERSION, | |
| type: COUNTLY_TYPE, | |
| page: COUNTLY_PAGE, | |
| title: COUNTLY_NAME, | |
| favicon: "images/favicon.png" | |
| }; | |
| plugins.loadConfigs(countlyDb, function() { | |
| var securityConf = plugins.getConfig("security"); | |
| app.dashboard_headers = securityConf.dashboard_additional_headers; | |
| add_headers(req, res); | |
| preventBruteforce.fails = Number.isInteger(securityConf.login_tries) ? securityConf.login_tries : 3; | |
| preventBruteforce.wait = securityConf.login_wait || 5 * 60; | |
| curTheme = plugins.getConfig("frontend", req.session && req.session.settings).theme; | |
| app.loadThemeFiles(req.cookies.theme || curTheme, function(themeFiles) { | |
| res.locals.flash = req.flash.bind(req); | |
| req.config = plugins.getConfig("frontend", req.session && req.session.settings); | |
| req.themeFiles = themeFiles; | |
| var _render = res.render; | |
| res.render = function(view, opts, fn, parent, sub) { | |
| if (!opts) { | |
| opts = {}; | |
| } | |
| if (!opts.path) { | |
| opts.path = countlyConfig.path || ""; | |
| } | |
| if (!opts.cdn) { | |
| opts.cdn = countlyConfig.cdn || ""; | |
| } | |
| if (!opts.themeFiles) { | |
| opts.themeFiles = themeFiles; | |
| } | |
| _render.call(res, view, opts, fn, parent, sub); | |
| }; | |
| next(); | |
| }); | |
| }); | |
| }); | |
| app.use(methodOverride()); | |
| app.use(function(req, res, next) { | |
| if (!plugins.callMethod("skipCSRF", {req: req, res: res, next: next})) { | |
| //none of the plugins requested to skip csrf for this request | |
| csrf(req, res, next); | |
| } | |
| else { | |
| //skipping csrf step, some plugin needs it without csrf | |
| next(); | |
| } | |
| }); | |
| app.use(function(req, res, next) { | |
| if (!plugins.callMethod("skipDollarCheck", {req: req, res: res, next: next})) { | |
| //none of the plugins requested to skip dollar sign check | |
| dollarDefender(req, res, next); | |
| } | |
| else { | |
| //skipping dollar sign check, some plugin needs mongo object as parameters | |
| next(); | |
| } | |
| }); | |
| //for csrf error handling. redirect to login if getting bad token while logging in(not show forbidden page) | |
| app.use(function(err, req, res, next) { // eslint-disable-line no-unused-vars | |
| var mylink = req.url.split('?'); | |
| mylink = mylink[0]; | |
| if (err.code === 'EBADCSRFTOKEN' && mylink === countlyConfig.path + "/login") { | |
| res.status(403); | |
| res.redirect(countlyConfig.path + '/login?message=login.token-expired'); | |
| } | |
| else { | |
| res.status(403).send("Forbidden Token"); | |
| } | |
| }); | |
| //prevent bruteforce attacks | |
| preventBruteforce.db = countlyDb; | |
| preventBruteforce.mail = countlyMail; | |
| for (let pathPart of ["/login", "/mobile/login"]) { | |
| const absPath = countlyConfig.path + pathPart; | |
| preventBruteforce.pathIdentifiers[absPath] = "login"; | |
| preventBruteforce.userIdentifiers[absPath] = (req) => req.body.username; | |
| } | |
| preventBruteforce.blockHooks.login = function(uid, req, res) { // eslint-disable-line no-unused-vars | |
| preventBruteforce.db.collection("members").findOne({username: uid}, function(err, member) { | |
| if (member) { | |
| preventBruteforce.mail.sendTimeBanWarning(member, preventBruteforce.db); | |
| } | |
| }); | |
| }; | |
| preventBruteforce.pathIdentifiers[countlyConfig.path + "/forgot"] = "forgot"; | |
| app.use(preventBruteforce.middleware); | |
| plugins.loadAppPlugins(app, countlyDb, express); | |
| var env = process.env.NODE_ENV || 'development'; | |
| if ('development' === env) { | |
| app.use(errorhandler(true)); | |
| } | |
| app.get(countlyConfig.path + '/', function(req, res) { | |
| res.redirect(countlyConfig.path + '/login'); | |
| }); | |
| var extendSession = function(req) { | |
| membersUtility.extendSession(req); | |
| }; | |
| var checkRequestForSession = function(req, res, next) { | |
| if (parseInt(plugins.getConfig("frontend", req.session && req.session.settings).session_timeout)) { | |
| if (req.session.uid) { | |
| if (Date.now() > req.session.expires) { | |
| membersUtility.logout(req, res); | |
| res.redirect(countlyConfig.path + '/login?message=logout.inactivity'); | |
| } | |
| else { | |
| //extend session | |
| extendSession(req, res, next); | |
| next(); | |
| } | |
| } | |
| else { | |
| next(); | |
| } | |
| } | |
| else { | |
| next(); | |
| } | |
| }; | |
| app.get(countlyConfig.path + '/ping', function(req, res) { | |
| countlyDb.collection("plugins").findOne({_id: "plugins"}, function(err) { | |
| if (err) { | |
| res.status(404).send("DB Error"); | |
| } | |
| else { | |
| res.send("Success"); | |
| } | |
| }); | |
| }); | |
| app.get(countlyConfig.path + '/configs', function(req, res) { | |
| membersUtility.recheckConfigs(countlyConfigOrig, countlyConfig); | |
| res.send("Success"); | |
| }); | |
| app.get(countlyConfig.path + '/session', function(req, res, next) { | |
| if (req.session.auth_token) { | |
| authorize.verify_return({ | |
| db: countlyDb, | |
| token: req.session.auth_token, | |
| req_path: "", | |
| callback: function(valid) { | |
| if (!valid) { | |
| //logout user | |
| res.send("logout"); | |
| } | |
| else { | |
| if (req.session.uid) { | |
| if (Date.now() > req.session.expires) { | |
| //logout user | |
| res.send("logout"); | |
| } | |
| else { | |
| //extend session | |
| if (req.query.check_session) { | |
| res.send("success"); | |
| } | |
| else { | |
| extendSession(req, res, next); | |
| res.send("success"); | |
| } | |
| } | |
| } | |
| else { | |
| res.send("login"); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| else { | |
| res.send("login"); | |
| } | |
| }); | |
| app.get(countlyConfig.path + '/dashboard', checkRequestForSession); | |
| app.post('*', checkRequestForSession); | |
| app.get(countlyConfig.path + '/logout', function(req, res) { | |
| if (req.query.message) { | |
| res.redirect(countlyConfig.path + '/login?message=' + req.query.message); | |
| } | |
| else { | |
| res.redirect(countlyConfig.path + '/login'); | |
| } | |
| }); | |
| app.post(countlyConfig.path + '/logout', function(req, res/*, next*/) { | |
| membersUtility.logout(req, res); | |
| if (req.query.message) { | |
| res.redirect(countlyConfig.path + '/login?message=' + req.query.message); | |
| } | |
| else { | |
| res.redirect(countlyConfig.path + '/login'); | |
| } | |
| }); | |
| /** | |
| * Render dashboard | |
| * @param {object} req - request object | |
| * @param {object} res - response object | |
| * @param {function} next - callback for next middleware | |
| * @param {object} member - dashboard member document | |
| * @param {array} adminOfApps - list of apps member is admin of | |
| * @param {array} userOfApps - list of apps member is user of | |
| * @param {object} countlyGlobalApps - all apps user has any access to, where key is app id and value is app document | |
| * @param {object} countlyGlobalAdminApps - all apps user has write access to, where key is app id and value is app document | |
| **/ | |
| function renderDashboard(req, res, next, member, adminOfApps, userOfApps, countlyGlobalApps, countlyGlobalAdminApps) { | |
| var configs = plugins.getConfig("frontend", member.settings); | |
| configs.export_limit = plugins.getConfig("api").export_limit; | |
| app.loadThemeFiles(configs.theme, function(theme) { | |
| if (configs._user.theme) { | |
| res.cookie("theme", configs.theme); | |
| } | |
| req.session.uid = member._id; | |
| req.session.gadm = (member.global_admin === true); | |
| req.session.email = member.email; | |
| req.session.settings = member.settings; | |
| res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'); | |
| res.header('Expires', '0'); | |
| res.header('Pragma', 'no-cache'); | |
| if (member.upgrade) { | |
| countlyDb.collection('members').update({"_id": member._id}, {$unset: {upgrade: ""}}, function() {}); | |
| } | |
| member._id += ""; | |
| delete member.password; | |
| adminOfApps = sortBy(adminOfApps, member.appSortList || []); | |
| userOfApps = sortBy(userOfApps, member.appSortList || []); | |
| var defaultApp = userOfApps[0]; | |
| var serverSideRendering = req.query.ssr; | |
| _.extend(req.config, configs); | |
| var countlyGlobal = { | |
| COUNTLY_CONTAINER: process.env.COUNTLY_CONTAINER, | |
| countlyTitle: req.countly.title, | |
| company: req.countly.company, | |
| languages: languages, | |
| countlyVersion: req.countly.version, | |
| countlyFavicon: req.countly.favicon, | |
| pluginsSHA: sha1Hash(plugins.getPlugins()), | |
| apps: countlyGlobalApps, | |
| defaultApp: defaultApp, | |
| admin_apps: countlyGlobalAdminApps, | |
| csrf_token: req.csrfToken(), | |
| auth_token: req.session.auth_token, | |
| member: member, | |
| config: req.config, | |
| security: plugins.getConfig("security"), | |
| plugins: plugins.getPlugins(), | |
| path: countlyConfig.path || "", | |
| cdn: countlyConfig.cdn || "", | |
| message: req.flash("message"), | |
| ssr: serverSideRendering, | |
| timezones: timezones | |
| }; | |
| var toDashboard = { | |
| countlyTitle: req.countly.title, | |
| languages: languages, | |
| countlyFavicon: req.countly.favicon, | |
| adminOfApps: adminOfApps, | |
| userOfApps: userOfApps, | |
| defaultApp: defaultApp, | |
| member: member, | |
| intercom: countlyConfig.web.use_intercom, | |
| track: countlyConfig.web.track || false, | |
| installed: req.session.install || false, | |
| cpus: require('os').cpus().length, | |
| countlyVersion: req.countly.version, | |
| countlyType: COUNTLY_TYPE_CE, | |
| countlyTrial: COUNTLY_TRIAL, | |
| countlyTypeName: COUNTLY_NAMED_TYPE, | |
| feedbackLink: COUNTLY_FEEDBACK_LINK, | |
| documentationLink: COUNTLY_DOCUMENTATION_LINK, | |
| helpCenterLink: COUNTLY_HELPCENTER_LINK, | |
| featureRequestLink: COUNTLY_FEATUREREQUEST_LINK, | |
| countlyTypeTrack: COUNTLY_TRACK_TYPE, | |
| frontend_app: versionInfo.frontend_app, | |
| frontend_server: versionInfo.frontend_server, | |
| production: configs.production || false, | |
| pluginsSHA: sha1Hash(plugins.getPlugins()), | |
| plugins: plugins.getPlugins(), | |
| config: req.config, | |
| path: countlyConfig.path || "", | |
| cdn: countlyConfig.cdn || "", | |
| use_google: configs.use_google || false, | |
| themeFiles: theme, | |
| inject_template: req.template, | |
| javascripts: [], | |
| stylesheets: [], | |
| offline_mode: configs.offline_mode || false | |
| }; | |
| // google services cannot work when offline mode enable | |
| if (toDashboard.offline_mode) { | |
| toDashboard.use_google = false; | |
| } | |
| if (countlyGlobal.config.offline_mode) { | |
| countlyGlobal.config.use_google = false; | |
| } | |
| var plgns = [].concat(plugins.getPlugins()); | |
| if (plgns.indexOf('push') !== -1) { | |
| plgns.splice(plgns.indexOf('push'), 1); | |
| plgns.unshift('push'); | |
| } | |
| plgns.forEach(plugin => { | |
| try { | |
| let contents = fs.readdirSync(__dirname + `/../../plugins/${plugin}/frontend/public/javascripts`) || []; | |
| toDashboard.javascripts.push.apply(toDashboard.javascripts, contents.filter(n => n.indexOf('.js') === n.length - 3).map(n => `${plugin}/javascripts/${n}`)); | |
| } | |
| catch (e) { | |
| console.log('Error while reading folder of plugin %s: %j', plugin, e.stack); | |
| } | |
| try { | |
| let contents = fs.readdirSync(__dirname + `/../../plugins/${plugin}/frontend/public/stylesheets`) || []; | |
| toDashboard.stylesheets.push.apply(toDashboard.stylesheets, contents.filter(n => n.indexOf('.css') === n.length - 4).map(n => `${plugin}/stylesheets/${n}`)); | |
| } | |
| catch (e) { | |
| console.log('Error while reading folder of plugin %s: %j', plugin, e.stack); | |
| } | |
| }); | |
| if (req.session.install) { | |
| req.session.install = null; | |
| res.clearCookie('install'); | |
| } | |
| plugins.callMethod("renderDashboard", {req: req, res: res, next: next, data: {member: member, adminApps: countlyGlobalAdminApps, userApps: countlyGlobalApps, countlyGlobal: countlyGlobal, toDashboard: toDashboard}}); | |
| res.expose(countlyGlobal, 'countlyGlobal'); | |
| res.render('dashboard', toDashboard); | |
| }); | |
| } | |
| app.get(countlyConfig.path + '/dashboard', function(req, res, next) { | |
| if (!req.session.uid) { | |
| res.redirect(countlyConfig.path + '/login'); | |
| } | |
| else { | |
| countlyDb.collection('members').findOne({"_id": countlyDb.ObjectID(req.session.uid + "")}, function(err, member) { | |
| if (member) { | |
| req.session.cookie.maxAge = countlyConfig.cookie.maxAgeLogin; | |
| var adminOfApps = [], | |
| userOfApps = [], | |
| countlyGlobalApps = {}, | |
| countlyGlobalAdminApps = {}; | |
| if (member.global_admin) { | |
| countlyDb.collection('apps').find({}).toArray(function(err2, apps) { | |
| adminOfApps = apps; | |
| userOfApps = apps; | |
| for (let i = 0; i < apps.length; i++) { | |
| apps[i].type = apps[i].type || "mobile"; | |
| countlyGlobalApps[apps[i]._id] = apps[i]; | |
| countlyGlobalApps[apps[i]._id]._id = "" + apps[i]._id; | |
| } | |
| countlyGlobalAdminApps = countlyGlobalApps; | |
| renderDashboard(req, res, next, member, adminOfApps, userOfApps, countlyGlobalApps, countlyGlobalAdminApps); | |
| }); | |
| } | |
| else { | |
| var adminOfAppIds = [], | |
| userOfAppIds = []; | |
| if (member.admin_of.length === 1 && member.admin_of[0] === "") { | |
| member.admin_of = []; | |
| } | |
| for (let i = 0; i < member.admin_of.length; i++) { | |
| if (member.admin_of[i] === "") { | |
| continue; | |
| } | |
| adminOfAppIds[adminOfAppIds.length] = countlyDb.ObjectID(member.admin_of[i]); | |
| } | |
| for (let i = 0; i < member.user_of.length; i++) { | |
| if (member.user_of[i] === "") { | |
| continue; | |
| } | |
| userOfAppIds[userOfAppIds.length] = countlyDb.ObjectID(member.user_of[i]); | |
| } | |
| countlyDb.collection('apps').find({ _id: { '$in': adminOfAppIds } }).toArray(function(err2, admin_of) { | |
| for (let i = 0; i < admin_of.length; i++) { | |
| countlyGlobalAdminApps[admin_of[i]._id] = admin_of[i]; | |
| countlyGlobalAdminApps[admin_of[i]._id]._id = "" + admin_of[i]._id; | |
| } | |
| countlyDb.collection('apps').find({ _id: { '$in': userOfAppIds } }).toArray(function(err3, user_of) { | |
| adminOfApps = admin_of; | |
| userOfApps = user_of; | |
| for (let i = 0; i < user_of.length; i++) { | |
| if (user_of[i].apn) { | |
| user_of[i].apn.forEach(a => a._id = '' + a._id); | |
| } | |
| if (user_of[i].gcm) { | |
| user_of[i].gcm.forEach(a => a._id = '' + a._id); | |
| } | |
| countlyGlobalApps[user_of[i]._id] = user_of[i]; | |
| countlyGlobalApps[user_of[i]._id]._id = "" + user_of[i]._id; | |
| countlyGlobalApps[user_of[i]._id].type = countlyGlobalApps[user_of[i]._id].type || "mobile"; | |
| } | |
| renderDashboard(req, res, next, member, adminOfApps, userOfApps, countlyGlobalApps, countlyGlobalAdminApps); | |
| }); | |
| }); | |
| } | |
| } | |
| else { | |
| membersUtility.clearReqAndRes(req, res); | |
| res.redirect(countlyConfig.path + '/login'); | |
| } | |
| }); | |
| } | |
| }); | |
| app.get(countlyConfig.path + '/setup', function(req, res) { | |
| res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'); | |
| res.header('Expires', '0'); | |
| res.header('Pragma', 'no-cache'); | |
| countlyDb.collection('members').count(function(err, memberCount) { | |
| if (!err && memberCount === 0) { | |
| res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'); | |
| res.header('Expires', '0'); | |
| res.header('Pragma', 'no-cache'); | |
| var config = plugins.getConfig("security"); | |
| res.render('setup', { languages: languages, countlyFavicon: req.countly.favicon, countlyTitle: req.countly.title, countlyPage: req.countly.page, "csrf": req.csrfToken(), path: countlyConfig.path || "", cdn: countlyConfig.cdn || "", themeFiles: req.themeFiles, inject_template: req.template, params: {}, error: {}, security: {password_min: config.password_min, password_char: config.password_char, password_number: config.password_number, password_symbol: config.password_symbol, autocomplete: config.password_autocomplete || false}}); | |
| } | |
| else if (err) { | |
| res.status(500).send('Server Error'); | |
| } | |
| else { | |
| res.redirect(countlyConfig.path + '/login'); | |
| } | |
| }); | |
| }); | |
| app.get(countlyConfig.path + '/login', function(req, res) { | |
| res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'); | |
| res.header('Expires', '0'); | |
| res.header('Pragma', 'no-cache'); | |
| if (req.session.uid) { | |
| res.redirect(countlyConfig.path + '/dashboard'); | |
| } | |
| else { | |
| countlyDb.collection('members').estimatedDocumentCount(function(err, memberCount) { | |
| if (memberCount) { | |
| if (req.query.message) { | |
| req.flash('info', req.query.message); | |
| } | |
| res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'); | |
| res.header('Expires', '0'); | |
| res.header('Pragma', 'no-cache'); | |
| var config = plugins.getConfig("security"); | |
| res.render('login', { languages: languages, countlyFavicon: req.countly.favicon, countlyTitle: req.countly.title, countlyPage: req.countly.page, "message": req.flash('info'), "csrf": req.csrfToken(), path: countlyConfig.path || "", cdn: countlyConfig.cdn || "", themeFiles: req.themeFiles, inject_template: req.template, security: {autocomplete: config.password_autocomplete || false}}); | |
| } | |
| else { | |
| res.redirect(countlyConfig.path + '/setup'); | |
| } | |
| }); | |
| } | |
| }); | |
| app.get(countlyConfig.path + '/forgot', function(req, res) { | |
| res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'); | |
| res.header('Expires', '0'); | |
| res.header('Pragma', 'no-cache'); | |
| if (req.session.uid) { | |
| res.redirect(countlyConfig.path + '/dashboard'); | |
| } | |
| else { | |
| res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'); | |
| res.header('Expires', '0'); | |
| res.header('Pragma', 'no-cache'); | |
| res.render('forgot', { languages: languages, countlyFavicon: req.countly.favicon, countlyTitle: req.countly.title, countlyPage: req.countly.page, "csrf": req.csrfToken(), "message": req.query.message || "", path: countlyConfig.path || "", cdn: countlyConfig.cdn || "", themeFiles: req.themeFiles, inject_template: req.template}); | |
| } | |
| }); | |
| app.get(countlyConfig.path + '/reset/:prid', function(req, res) { | |
| res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'); | |
| res.header('Expires', '0'); | |
| res.header('Pragma', 'no-cache'); | |
| if (req.params.prid) { | |
| req.params.prid += ""; | |
| countlyDb.collection('password_reset').findOne({prid: req.params.prid}, function(err, passwordReset) { | |
| var timestamp = Math.round(new Date().getTime() / 1000); | |
| if (passwordReset && !err) { | |
| if (timestamp > (passwordReset.timestamp + 600)) { | |
| req.flash('info', 'reset.invalid'); | |
| res.redirect(countlyConfig.path + '/forgot'); | |
| } | |
| else { | |
| res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'); | |
| res.header('Expires', '0'); | |
| res.header('Pragma', 'no-cache'); | |
| var config = plugins.getConfig("security"); | |
| res.render('reset', { languages: languages, countlyFavicon: req.countly.favicon, countlyTitle: req.countly.title, countlyPage: req.countly.page, "csrf": req.csrfToken(), "prid": req.params.prid, "message": req.query.message || "", password_min: req.query.password_min || "", path: countlyConfig.path || "", cdn: countlyConfig.cdn || "", "newinvite": passwordReset.newInvite, themeFiles: req.themeFiles, inject_template: req.template, security: {autocomplete: config.password_autocomplete || false}}); | |
| } | |
| } | |
| else { | |
| req.flash('info', 'reset.invalid'); | |
| res.redirect(countlyConfig.path + '/forgot'); | |
| } | |
| }); | |
| } | |
| else { | |
| req.flash('info', 'reset.invalid'); | |
| res.redirect(countlyConfig.path + '/forgot'); | |
| } | |
| }); | |
| app.post(countlyConfig.path + '/reset', function(req, res/*, next*/) { | |
| membersUtility.reset(req, function(result, member) { | |
| if (result === false) { | |
| if (member) { | |
| req.flash('info', 'reset.result'); | |
| res.redirect(countlyConfig.path + '/login'); | |
| } | |
| else { | |
| res.redirect(countlyConfig.path + '/reset/' + req.body.prid); | |
| } | |
| } | |
| else { | |
| res.redirect(countlyConfig.path + '/reset/' + req.body.prid + "?message=" + result + "&password_min=" + plugins.getConfig("security").password_min); | |
| } | |
| }); | |
| }); | |
| app.post(countlyConfig.path + '/forgot', function(req, res/*, next*/) { | |
| if (req.body.email) { | |
| if (countlyCommon.validateEmail(req.body.email)) { | |
| membersUtility.forgot(req, function(/*member*/) { | |
| preventBruteforce.fail("forgot", req.ip); | |
| res.redirect(countlyConfig.path + '/forgot?message=forgot.result'); | |
| }); | |
| } | |
| else { | |
| res.redirect(countlyConfig.path + '/forgot?message=forgot.result'); | |
| } | |
| } | |
| else { | |
| res.redirect(countlyConfig.path + '/forgot'); | |
| } | |
| }); | |
| app.post(countlyConfig.path + '/setup', function(req, res/*, next*/) { | |
| var params = req.body || {}; | |
| membersUtility.setup(req, function(err) { | |
| if (!err) { | |
| res.redirect(countlyConfig.path + '/dashboard'); | |
| } | |
| else if (err === "User exists") { | |
| res.redirect(countlyConfig.path + '/login'); | |
| } | |
| else if (err && err.message) { | |
| res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'); | |
| res.header('Expires', '0'); | |
| res.header('Pragma', 'no-cache'); | |
| var config = plugins.getConfig("security"); | |
| var data = { languages: languages, countlyFavicon: req.countly.favicon, countlyTitle: req.countly.title, countlyPage: req.countly.page, "csrf": req.csrfToken(), path: countlyConfig.path || "", cdn: countlyConfig.cdn || "", themeFiles: req.themeFiles, inject_template: req.template, params: {}, error: err || {}, security: {password_min: config.password_min, password_char: config.password_char, password_number: config.password_number, password_symbol: config.password_symbol, autocomplete: config.password_autocomplete || false}}; | |
| if (params.email) { | |
| data.params.email = params.email; | |
| } | |
| if (params.full_name) { | |
| data.params.full_name = params.full_name; | |
| } | |
| if (params.username) { | |
| data.params.username = params.username; | |
| } | |
| if (params.password) { | |
| data.params.password = params.password; | |
| } | |
| res.render('setup', data); | |
| } | |
| else { | |
| res.status(500).send('Server Error'); | |
| } | |
| }); | |
| }); | |
| app.post(countlyConfig.path + '/login', function(req, res/*, next*/) { | |
| membersUtility.login(req, res, function(member) { | |
| if (member) { | |
| if (member.locked) { | |
| res.redirect(countlyConfig.path + '/login?message=login.locked'); | |
| } | |
| else { | |
| res.redirect(countlyConfig.path + '/dashboard'); | |
| preventBruteforce.reset("login", req.body.username); | |
| } | |
| } | |
| else { | |
| res.redirect(countlyConfig.path + '/login?message=login.result'); | |
| if (req.body.username) { | |
| preventBruteforce.fail("login", req.body.username); | |
| } | |
| } | |
| }); | |
| }); | |
| app.get(countlyConfig.path + '/api-key', function(req, res, next) { | |
| /** | |
| * Handles unauthorized access attempt | |
| * @param {object} response - response object | |
| * @returns {void} void | |
| **/ | |
| function unauthorized(response) { | |
| response.set('WWW-Authenticate', 'Basic realm=Authorization Required'); | |
| return response.status(401).send("-1"); | |
| } | |
| var user = basicAuth(req); | |
| if (user && user.name && user.pass) { | |
| preventBruteforce.isBlocked("login", user.name, function(isBlocked, fails, err) { | |
| if (isBlocked) { | |
| if (err) { | |
| res.status(500).send('Server Error'); | |
| } | |
| else { | |
| unauthorized(res); | |
| } | |
| } | |
| else { | |
| user.name = (user.name + "").trim(); | |
| verifyMemberArgon2Hash(user.name, user.pass, (err2, member) => { | |
| if (member) { | |
| if (member.locked) { | |
| plugins.callMethod("apikeyFailed", {req: req, res: res, next: next, data: {username: user.name}}); | |
| unauthorized(res); | |
| } | |
| else { | |
| plugins.callMethod("apikeySuccessful", {req: req, res: res, next: next, data: {username: member.username}}); | |
| preventBruteforce.reset("login", user.name); | |
| countlyDb.collection('members').update({_id: member._id}, {$set: {last_login: Math.round(new Date().getTime() / 1000)}}, function() {}); | |
| res.status(200).send(member.api_key); | |
| } | |
| } | |
| else { | |
| plugins.callMethod("apikeyFailed", {req: req, res: res, next: next, data: {username: user.name}}); | |
| preventBruteforce.fail("login", user.name); | |
| unauthorized(res); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| else { | |
| plugins.callMethod("apikeyFailed", {req: req, res: res, next: next, data: {username: ""}}); | |
| unauthorized(res); | |
| } | |
| }); | |
| app.get(countlyConfig.path + '/sdks.js', function(req, res) { | |
| if (!plugins.getConfig("api").offline_mode) { | |
| var options = {uri: "http://code.count.ly/js/sdks.js", method: "GET", timeout: 4E3}; | |
| request(options, function(a, c, b) { | |
| res.set('Content-type', 'application/javascript').status(200).send(b); | |
| }); | |
| } | |
| else { | |
| res.status(403).send("Server is in offline mode, this request cannot be completed."); | |
| } | |
| }); | |
| app.post(countlyConfig.path + '/mobile/login', function(req, res, next) { | |
| if (req.body.username && req.body.password) { | |
| req.body.username = (req.body.username + "").trim(); | |
| verifyMemberArgon2Hash(req.body.username, req.body.password, (err, member) => { | |
| if (member) { | |
| if (member.locked) { | |
| plugins.callMethod("mobileloginFailed", {req: req, res: res, next: next, data: req.body}); | |
| res.render('mobile/login', { "message": "login.locked", "csrf": req.csrfToken() }); | |
| } | |
| else { | |
| plugins.callMethod("mobileloginSuccessful", {req: req, res: res, next: next, data: member}); | |
| preventBruteforce.reset("login", req.body.username); | |
| countlyDb.collection('members').update({_id: member._id}, {$set: {last_login: Math.round(new Date().getTime() / 1000)}}, function() {}); | |
| res.render('mobile/key', { "key": member.api_key || -1 }); | |
| } | |
| } | |
| else { | |
| plugins.callMethod("mobileloginFailed", {req: req, res: res, next: next, data: req.body}); | |
| preventBruteforce.fail("login", req.body.username); | |
| res.render('mobile/login', { "message": "login.result", "csrf": req.csrfToken() }); | |
| } | |
| }); | |
| } | |
| else { | |
| res.render('mobile/login', { "message": "login.result", "csrf": req.csrfToken() }); | |
| } | |
| }); | |
| app.post(countlyConfig.path + '/dashboard/settings', function(req, res) { | |
| if (!req.session.uid) { | |
| res.end(); | |
| return false; | |
| } | |
| var newAppOrder = req.body.app_sort_list; | |
| if (!newAppOrder || newAppOrder.length === 0) { | |
| res.end(); | |
| return false; | |
| } | |
| countlyDb.collection('members').update({_id: countlyDb.ObjectID(req.session.uid + "")}, {'$set': {'appSortList': newAppOrder}}, {'upsert': true}, function() { | |
| res.end(); | |
| return false; | |
| }); | |
| }); | |
| app.post(countlyConfig.path + '/apps/icon', function(req, res, next) { | |
| if (!req.files.app_image || !req.body.app_image_id) { | |
| res.end(); | |
| return true; | |
| } | |
| req.body.app_image_id = common.sanitizeFilename(req.body.app_image_id); | |
| var tmp_path = req.files.app_image.path, | |
| target_path = __dirname + '/public/appimages/' + req.body.app_image_id + ".png", | |
| type = req.files.app_image.type; | |
| if (type !== "image/png" && type !== "image/gif" && type !== "image/jpeg") { | |
| fs.unlink(tmp_path, function() {}); | |
| res.send(false); | |
| return true; | |
| } | |
| plugins.callMethod("iconUpload", {req: req, res: res, next: next, data: req.body}); | |
| try { | |
| jimp.read(tmp_path, function(err, icon) { | |
| if (err) { | |
| console.log(err, err.stack); | |
| } | |
| icon.cover(72, 72).getBuffer(jimp.MIME_PNG, function(err2, buffer) { | |
| countlyFs.saveData("appimages", target_path, buffer, {id: req.body.app_image_id + ".png", writeMode: "overwrite"}, function() { | |
| fs.unlink(tmp_path, function() {}); | |
| res.send(countlyConfig.path + "/appimages/" + req.body.app_image_id + ".png"); | |
| }); | |
| }); // save | |
| }); | |
| } | |
| catch (e) { | |
| console.log(e.stack); | |
| } | |
| }); | |
| app.post(countlyConfig.path + '/member/icon', function(req, res, next) { | |
| if (!req.files.member_image || !req.body.member_image_id) { | |
| res.end(); | |
| return true; | |
| } | |
| req.body.member_image_id = common.sanitizeFilename(req.body.member_image_id); | |
| var tmp_path = req.files.member_image.path, | |
| target_path = __dirname + '/public/memberimages/' + req.body.member_image_id + ".png", | |
| type = req.files.member_image.type; | |
| if (type !== "image/png" && type !== "image/gif" && type !== "image/jpeg") { | |
| fs.unlink(tmp_path, function() {}); | |
| res.send(false); | |
| return true; | |
| } | |
| plugins.callMethod("iconUpload", {req: req, res: res, next: next, data: req.body}); | |
| try { | |
| jimp.read(tmp_path, function(err, icon) { | |
| if (err) { | |
| console.log(err, err.stack); | |
| } | |
| icon.cover(72, 72).getBuffer(jimp.MIME_PNG, function(err2, buffer) { | |
| countlyFs.saveData("memberimages", target_path, buffer, {id: req.body.member_image_id + ".png", writeMode: "overwrite"}, function() { | |
| fs.unlink(tmp_path, function() {}); | |
| res.send(countlyConfig.path + "/memberimages/" + req.body.member_image_id + ".png"); | |
| }); | |
| }); // save | |
| }); | |
| } | |
| catch (e) { | |
| console.log(e.stack); | |
| } | |
| }); | |
| app.post(countlyConfig.path + '/user/settings', function(req, res/*, next*/) { | |
| if (!req.session.uid) { | |
| res.end(); | |
| return false; | |
| } | |
| membersUtility.settings(req, function(result, message) { | |
| if (message) { | |
| res.send(message); | |
| } | |
| else { | |
| res.send(result); | |
| } | |
| return result; | |
| }); | |
| }); | |
| app.post(countlyConfig.path + '/user/settings/lang', function(req, res) { | |
| if (!req.session.uid) { | |
| res.end(); | |
| return false; | |
| } | |
| var updatedUser = {}; | |
| if (req.body.lang) { | |
| updatedUser.lang = req.body.lang; | |
| countlyDb.collection('members').update({"_id": countlyDb.ObjectID(req.session.uid + "")}, {'$set': updatedUser}, {safe: true}, function(err, member) { | |
| if (member && !err) { | |
| res.send(true); | |
| } | |
| else { | |
| res.send(false); | |
| } | |
| }); | |
| } | |
| else { | |
| res.send(false); | |
| return false; | |
| } | |
| }); | |
| app.post(countlyConfig.path + '/user/settings/active-app', function(req, res) { | |
| if (!req.session.uid) { | |
| res.end(); | |
| return false; | |
| } | |
| var updatedUser = {}; | |
| if (req.body.appId) { | |
| updatedUser.active_app_id = req.body.appId; | |
| countlyDb.collection('members').update({ "_id": countlyDb.ObjectID(req.session.uid + "") }, { '$set': updatedUser }, { safe: true }, function(err, member) { | |
| if (member && !err) { | |
| res.send(true); | |
| } | |
| else { | |
| res.send(false); | |
| } | |
| }); | |
| } | |
| else { | |
| res.send(false); | |
| return false; | |
| } | |
| }); | |
| app.post(countlyConfig.path + '/users/check/email', function(req, res) { | |
| if (!req.session.uid || !isGlobalAdmin(req) || !req.body.email) { | |
| res.send(false); | |
| return false; | |
| } | |
| else { | |
| membersUtility.checkEmail(req.body.email, function(result) { | |
| res.send(result); | |
| }); | |
| } | |
| }); | |
| app.post(countlyConfig.path + '/users/check/username', function(req, res) { | |
| if (!req.session.uid || !req.body.username) { | |
| res.send(false); | |
| return false; | |
| } | |
| else { | |
| membersUtility.checkUsername(req.body.username, function(result) { | |
| res.send(result); | |
| }); | |
| } | |
| }); | |
| app.get(countlyConfig.path + '/render', function(req, res) { | |
| if (!req.session.uid) { | |
| return res.redirect(countlyConfig.path + '/login'); | |
| } | |
| var options = {}; | |
| var view = req.query.view || ""; | |
| var route = req.query.route || ""; | |
| var id = req.query.id || ""; | |
| options.view = view + "#" + route; | |
| options.id = id ? "#" + id : ""; | |
| var randomString = (+new Date()).toString() + (Math.random()).toString(); | |
| var imageName = "screenshot_" + sha1Hash(randomString) + ".png"; | |
| options.savePath = path.resolve(__dirname, "./public/images/screenshots/" + imageName); | |
| options.source = "core"; | |
| authorize.save({ | |
| db: countlyDb, | |
| multi: false, | |
| owner: req.session.uid, | |
| ttl: 300, | |
| purpose: "LoginAuthToken", | |
| callback: function(err2, token) { | |
| if (err2) { | |
| console.log(err2); | |
| return res.send(false); | |
| } | |
| options.token = token; | |
| render.renderView(options, function(err3) { | |
| if (err3) { | |
| return res.send(false); | |
| } | |
| return res.send(true); | |
| }); | |
| } | |
| }); | |
| }); | |
| app.get(countlyConfig.path + '/login/token/:token', function(req, res) { | |
| membersUtility.loginWithToken(req, function(member) { | |
| if (member) { | |
| var serverSideRendering = req.query.ssr || false; | |
| preventBruteforce.reset("login", member.username); | |
| var options = ""; | |
| if (serverSideRendering) { | |
| options += "ssr=" + serverSideRendering; | |
| } | |
| if (options && options.length) { | |
| options = ("?").concat(options); | |
| } | |
| res.redirect(countlyConfig.path + '/dashboard' + options); | |
| } | |
| else { | |
| res.redirect(countlyConfig.path + '/login?message=login.result'); | |
| } | |
| }); | |
| }); | |
| countlyDb.collection('apps').createIndex({"key": 1}, { unique: true }, function() {}); | |
| countlyDb.collection('members').createIndex({"api_key": 1}, { unique: true }, function() {}); | |
| countlyDb.collection('members').createIndex({ email: 1 }, { unique: true }, function() {}); | |
| countlyDb.collection('jobs').createIndex({ finished: 1 }, function() {}); | |
| countlyDb.collection('jobs').createIndex({ name: 1 }, function() {}); | |
| countlyDb.collection('long_tasks').createIndex({ manually_create: 1, start: -1 }, function() {}); | |
| app.listen(countlyConfig.web.port, countlyConfig.web.host || ''); | |
| }); |