├── .github └── FUNDING.yml ├── README.md ├── builder ├── builder.js └── readme.md ├── cdn ├── cdn.js └── readme.md ├── clienterror ├── clienterror.js └── readme.md ├── code ├── code.js └── readme.md ├── console ├── console.package ├── make.sh ├── readme.md └── source │ ├── index.html │ └── index.js ├── directorylisting ├── directorylisting.js └── readme.md ├── edbms ├── edbms.js └── readme.md ├── filecache ├── filecache.js └── readme.md ├── flash ├── flash.js └── readme.md ├── flowstream ├── flowstream.js └── readme.md ├── fulltext ├── fulltext.js └── readme.md ├── helpdesk ├── helpdesk.package ├── make.sh ├── readme.md └── source │ ├── default.css │ ├── default.js │ ├── index.html │ ├── index.js │ └── mail.html ├── jsonwebtoken ├── jsonwebtoken.js └── readme.md ├── modules.json ├── monitor ├── monitor.js └── readme.md ├── nosqlexplorer ├── nosqlexplorer.js └── readme.md ├── oauth2 ├── oauth2.js └── readme.md ├── openplatform ├── openplatform.js └── readme.md ├── querybuilderapi ├── querybuilderapi.js └── readme.md ├── quickapi ├── make.sh ├── quickapi.package ├── readme.md └── source │ ├── index.html │ └── index.js ├── serverstatus ├── readme.md ├── serverstatus.js └── zabbix │ └── totaljs-server-status-template.xml ├── session ├── readme.md └── session.js ├── sshbackup ├── readme.md └── sshbackup.js ├── tape ├── readme.md └── tape.js ├── tapi ├── readme.md ├── tapi4.js └── tapi5.js ├── totalaccount ├── readme.md └── totalaccount.js ├── totaldb ├── readme.md └── totaldb.js └── totp ├── readme.md └── totp.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: totaljs 4 | open_collective: totalplatform 5 | ko_fi: totaljs 6 | liberapay: totaljs 7 | buy_me_a_coffee: totaljs 8 | custom: https://www.totaljs.com/support/ 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Total.js modules / packages 2 | 3 | - [Documentation](https://docs.totaljs.com) 4 | - [Join Total.js Telegram](https://t.me/totaljs) 5 | - [Support](https://www.totaljs.com/support/) 6 | 7 | Download modules and packages for your Total.js applications. -------------------------------------------------------------------------------- /builder/builder.js: -------------------------------------------------------------------------------- 1 | exports.install = function() { 2 | if (DEBUG) { 3 | ROUTE('GET /_build/', function() { 4 | this.file('~' + PATH.builds('app.build'), null, { 'content-type': 'application/json' }); 5 | }); 6 | ROUTE('POST /_build/', function() { 7 | require('fs').writeFile(PATH.builds('app.build'), this.body, this.done()); 8 | }, ['raw'], 1024); 9 | ROUTE('GET /builder/', function() { 10 | this.proxy('https://cdn.totaljs.com/appbuilder.html'); 11 | }); 12 | } 13 | }; -------------------------------------------------------------------------------- /builder/readme.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | __AppBuilder module__ enables Total.js AppBuilder designer in DEBUG mode. 4 | 5 | - download and copy `builder.js` into the `/modules/` directory 6 | - run app 7 | - open 8 | -------------------------------------------------------------------------------- /cdn/cdn.js: -------------------------------------------------------------------------------- 1 | // CDN downloader 2 | // The MIT License 3 | // Copyright 2022 (c) Peter Širka 4 | 5 | var Cache = {}; 6 | var Directory; 7 | 8 | exports.install = function() { 9 | Directory = PATH.public('cdn'); 10 | PATH.mkdir(Directory); 11 | ROUTE('FILE /cdn/*.html', cdn); 12 | }; 13 | 14 | function cdn(req, res) { 15 | 16 | var filename = req.split[1]; 17 | var key = filename; 18 | 19 | if (Cache[key] === 1) { 20 | setTimeout(cdn, 500, req, res); 21 | return; 22 | } 23 | 24 | var path = PATH.join(Directory, filename); 25 | 26 | if (Cache[key] === 2) { 27 | res.file(path); 28 | return; 29 | } 30 | 31 | Cache[key] = 1; 32 | PATH.exists(filename ,function(err, response) { 33 | if (response) { 34 | Cache[key] = 2; 35 | res.file(path); 36 | } else { 37 | // Download 38 | DOWNLOAD('https://cdn.componentator.com/' + filename, path, function() { 39 | Cache[key] = 2; 40 | res.file(path); 41 | }); 42 | } 43 | }); 44 | } 45 | 46 | exports.clear = function(callback) { 47 | F.Fs.readdir(Directory, function(err, files) { 48 | 49 | if (err) { 50 | callback(err); 51 | return; 52 | } 53 | 54 | for (var i = 0; i < files.length; i++) { 55 | var file = files[i]; 56 | TOUCH('/cdn/' + file); 57 | files[i] = PATH.join(Directory, file); 58 | } 59 | 60 | Cache = {}; 61 | PATH.unlink(files, () => callback(null, files.length)); 62 | }); 63 | }; -------------------------------------------------------------------------------- /cdn/readme.md: -------------------------------------------------------------------------------- 1 | # CDN downloader 2 | 3 | This module downloads UI components from the Componentator CDN, storing them in the `/public/cdn/` directory. 4 | 5 | - components can be used on the relative URL address `/cdn/*.html` 6 | 7 | __Client-Side usage:__ 8 | 9 | ```js 10 | DEF.fallback = '/cdn/j-{0}.html'; 11 | ``` 12 | 13 | __Good to know__: 14 | 15 | - How to release cache? `MODULE('cdn').clear((err, removed) => console.log(err, removed))` -------------------------------------------------------------------------------- /clienterror/clienterror.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright Peter Širka 3 | 4 | const USAGE = { errors: [], counter: 0 }; 5 | 6 | exports.name = 'clienterror'; 7 | exports.usage = () => USAGE; 8 | exports.options = { logger: true, console: true, filename: 'clienterror', url: '/$clienterror/' }; 9 | exports.version = 'v3.0.0'; 10 | 11 | var options = exports.options; 12 | 13 | exports.install = function(opt) { 14 | options = U.extend(options, opt, true); 15 | ON('controller', controller); 16 | ROUTE('POST ' + options.url, process_error, ['referer']); 17 | }; 18 | 19 | exports.uninstall = function() { 20 | OFF('controller', controller); 21 | }; 22 | 23 | function controller(controller) { 24 | !controller.robot && controller.head("".format(options.url)); 25 | } 26 | 27 | function process_error() { 28 | var self = this; 29 | var body = self.body; 30 | var ua = self.req.headers['user-agent'] || ''; 31 | var browser = ''; 32 | 33 | self.empty(); 34 | USAGE.counter++; 35 | 36 | options.logger && self.logger(options.filename, body.url, body.error, browser, self.ip); 37 | options.console && console.log('CLIENTERROR:', body.url, body.error, browser, self.ip); 38 | 39 | body.browser = ua ? ua.parseUA() : 'Unknown'; 40 | body.ip = self.ip; 41 | 42 | USAGE.errors.push(body); 43 | USAGE.errors.length > 50 && USAGE.errors.shift(); 44 | } 45 | -------------------------------------------------------------------------------- /clienterror/readme.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | __ClientError module__ captures all client-side errors, then it writes into the console or log file on the server-side. The module appends a small JS script into the `@{head}` tag. 4 | 5 | - download and copy `clienterror.js` into the `/modules/` directory __or create a definition with:__ 6 | 7 | ```javascript 8 | var options = {}; 9 | 10 | // options.logger = true; 11 | // options.console = false; 12 | // options.url = '/$clienterror/'; 13 | 14 | // The framework adds `.log` extension automatically 15 | // options.filename = 'logger'; 16 | INSTALL('module', 'https://modules.totaljs.com/latest/clienterror.js', options); 17 | ``` -------------------------------------------------------------------------------- /code/code.js: -------------------------------------------------------------------------------- 1 | const Fs = require('fs'); 2 | const Path = require('path'); 3 | const SKIP = /\/\.git\//; 4 | const VERSION = 1; 5 | 6 | var DDOS = {}; 7 | 8 | exports.install = function(options) { 9 | 10 | var url = CONF.code_url; 11 | 12 | if (options && options.url) 13 | url = options.url; 14 | 15 | if (!url) 16 | url = '/$code/'; 17 | 18 | DEBUG && ROUTE('POST ' + url + ' *CodeModule --> exec', [1000], 1024 * 5); // Max. 5 MB 19 | }; 20 | 21 | DEBUG && NEWSCHEMA('CodeModule', function(schema) { 22 | 23 | schema.define('TYPE', String, true); 24 | schema.define('path', String); 25 | schema.define('data', String); 26 | 27 | schema.addWorkflow('exec', function($, model) { 28 | 29 | if (DDOS[$.ip] > 5) { 30 | $.invalid('errork-token', 'Invalid token'); 31 | return; 32 | } 33 | 34 | if (CONF.code_token && $.headers['x-token'] !== CONF.code_token) { 35 | DDOS[$.ip] = (DDOS[$.ip] || 0) + 1; 36 | $.invalid('errork-token', 'Invalid token'); 37 | return; 38 | } 39 | 40 | switch (model.TYPE) { 41 | 42 | case 'ping': 43 | $.success(VERSION); 44 | break; 45 | 46 | // Browse files 47 | case 'browse': 48 | browse($, model); 49 | break; 50 | 51 | // Download 52 | case 'download': 53 | download($, model); 54 | break; 55 | 56 | // Creates file/directory 57 | case 'create': 58 | create($, model); 59 | break; 60 | 61 | // Loads file 62 | case 'load': 63 | load($, model); 64 | break; 65 | 66 | // Saves file 67 | case 'save': 68 | save($, model); 69 | break; 70 | 71 | // Removes file 72 | case 'remove': 73 | remove($, model); 74 | break; 75 | 76 | // Modifies file 77 | case 'modify': 78 | modify($, model); 79 | break; 80 | 81 | // Reads info about the file 82 | case 'info': 83 | info($, model); 84 | break; 85 | 86 | // Reads log file 87 | case 'log': 88 | log($, model); 89 | break; 90 | 91 | case 'rename': 92 | rename($, model); 93 | break; 94 | 95 | case 'upload': 96 | upload($, model); 97 | break; 98 | 99 | // Clears log 100 | case 'logclear': 101 | clearlog($, model); 102 | break; 103 | 104 | case 'wiki': 105 | wiki($, model); 106 | break; 107 | 108 | case 'ip': 109 | ipaddress($, model); 110 | break; 111 | 112 | default: 113 | $.invalid(400); 114 | break; 115 | } 116 | 117 | }); 118 | 119 | }); 120 | 121 | function mkdir(path, callback) { 122 | var a = '/'; 123 | path = path.split('/').trim(); 124 | path.wait(function(p, next) { 125 | a = a + p + '/'; 126 | Fs.lstat(a, function(err) { 127 | if (err) 128 | Fs.mkdir(a, next); 129 | else 130 | next(); 131 | }); 132 | }, callback); 133 | } 134 | 135 | function browse($, model) { 136 | var path = PATH.root(); 137 | var m = model.data.parseJSON() || EMPTYARRAY; 138 | var skip = m.skip ? new RegExp(m.skip) : null; 139 | var validator; 140 | 141 | if (m.type === 'localization') 142 | validator = ((path, dir) => dir ? (path.endsWith('/node_modules') || path.endsWith('/tmp') || path.endsWith('/.git') || path.endsWith('/.src') || path.endsWith('/logs')) ? false : true : true); 143 | else 144 | validator = n => !SKIP.test(n) && (!skip || !skip.test(n)); 145 | 146 | U.ls(path, function(files, directories) { 147 | 148 | for (var i = 0; i < files.length; i++) 149 | files[i] = files[i].substring(path.length); 150 | 151 | for (var i = 0; i < directories.length; i++) 152 | directories[i] = directories[i].substring(path.length); 153 | 154 | if (m.type === 'localization') { 155 | var allowed = { html: 1, js: 1 }; 156 | files = files.remove(n => allowed[U.getExtension(n)] != 1); 157 | } 158 | 159 | $.callback({ files: files, directories: directories }); 160 | 161 | }, validator); 162 | } 163 | 164 | function log($, model) { 165 | var filename = PATH.root(model.path); 166 | Fs.stat(filename, function(err, stats) { 167 | if (stats) { 168 | var start = stats.size - (1024 * 4); // Max. 4 kB 169 | if (start < 0) 170 | start = 0; 171 | var buffer = []; 172 | Fs.createReadStream(filename, { start: start < 0 ? 0 : start }).on('data', chunk => buffer.push(chunk)).on('end', function() { 173 | var buf = Buffer.concat(buffer); 174 | $.controller.plain(buf.toString('utf8')); 175 | $.cancel(); 176 | }); 177 | } else { 178 | $.controller.plain(''); 179 | $.cancel(); 180 | } 181 | }); 182 | } 183 | 184 | function clearlog($, model) { 185 | var filename = PATH.root(model.path); 186 | Fs.truncate(filename, NOOP); 187 | $.success(); 188 | } 189 | 190 | function load($, model) { 191 | var filename = PATH.root(model.path); 192 | Fs.readFile(filename, function(err, data) { 193 | 194 | if (err) { 195 | $.invalid(err); 196 | return; 197 | } 198 | 199 | var index = -1; 200 | 201 | while (true) { 202 | index += 1; 203 | if (data.length <= index || data[index] !== 0) 204 | break; 205 | } 206 | 207 | if (index !== -1) 208 | data = data.slice(index); 209 | 210 | $.controller.binary(data, U.getContentType(U.getExtension(model.path))); 211 | $.cancel(); 212 | }); 213 | } 214 | 215 | function save($, model) { 216 | 217 | // Tries to create a folder 218 | var filename = PATH.root(model.path); 219 | var name = U.getName(model.path); 220 | var directory = filename.substring(0, filename.length - name.length); 221 | 222 | Fs.mkdir(directory, { recursive: true }, function() { 223 | Fs.writeFile(filename, model.data, $.done()); 224 | }); 225 | } 226 | 227 | function remove($, model) { 228 | 229 | // A simple protection for this module 230 | if (model.path.lastIndexOf('modules/code.js') === -1) { 231 | var filename = PATH.root(model.path); 232 | try { 233 | var stats = Fs.lstatSync(filename); 234 | if (stats.isFile()) { 235 | Fs.unlink(filename, NOOP); 236 | } else { 237 | if (stats.isDirectory()) 238 | F.path.rmdir(filename); 239 | else 240 | Fs.unlink(filename, NOOP); 241 | } 242 | } catch (e) {} 243 | } 244 | 245 | $.success(); 246 | } 247 | 248 | function info($, model) { 249 | var filename = PATH.root(model.path); 250 | Fs.lstat(filename, $.callback); 251 | } 252 | 253 | function download($, model) { 254 | var filename = PATH.root(model.path); 255 | var ext = U.getExtension(model.path); 256 | Fs.lstat(filename, function(err, stats) { 257 | if (err || stats.isDirectory() || stats.isSocket()) { 258 | $.status = 400; 259 | $.invalid('400', 'error-file'); 260 | } else { 261 | $.controller.stream(U.getContentType(ext), Fs.createReadStream(filename)); 262 | $.cancel(); 263 | } 264 | }); 265 | } 266 | 267 | function rename($, model) { 268 | var data = CONVERT(model.data.parseJSON(), 'newpath:String,oldpath:String'); 269 | data.newpath = PATH.root(data.newpath); 270 | data.oldpath = PATH.root(data.oldpath); 271 | mkdir(Path.dirname(data.newpath), function() { 272 | Fs.rename(data.oldpath, data.newpath, $.done()); 273 | }); 274 | } 275 | 276 | function create($, model) { 277 | 278 | var filename = PATH.root(model.path); 279 | var data = model.data.parseJSON(); 280 | 281 | Fs.lstat(filename, function(err) { 282 | 283 | if (err) { 284 | // file not found 285 | // we can continue 286 | if (data.folder) { 287 | mkdir(filename, $.done()); 288 | } else { 289 | var name = U.getName(filename); 290 | mkdir(filename.substring(0, filename.length - name.length), function() { 291 | if (data.clone) 292 | Fs.copyFile(PATH.root(data.clone), filename, $.done()); 293 | else 294 | Fs.writeFile(filename, '', $.done()); 295 | }); 296 | } 297 | } else 298 | $.invalid('path', model.path + ' already exists'); 299 | }); 300 | } 301 | 302 | function upload($, model) { 303 | var name = U.getName(model.path); 304 | var filename = PATH.root(model.path); 305 | var directory = PATH.root(model.path.substring(0, model.length - name.length)); 306 | mkdir(directory, function() { 307 | var buf = Buffer.from(model.data, 'base64'); 308 | Fs.writeFile(filename, buf, $.done()); 309 | }); 310 | } 311 | 312 | function modify($, model) { 313 | var filename = PATH.root(model.path); 314 | var dt = new Date(); 315 | Fs.utimes(filename, dt, dt, NOOP); 316 | $.success(); 317 | } 318 | 319 | function wiki($) { 320 | 321 | var path = PATH.root(); 322 | 323 | U.ls(path, function(files) { 324 | 325 | var builder = []; 326 | 327 | files.wait(function(item, next) { 328 | 329 | if (item.substring(item.length - 3) === '.js') { 330 | Fs.readFile(item, function(err, buffer) { 331 | if (buffer) { 332 | builder.push(buffer.toString('utf8')); 333 | builder.push(''); 334 | } 335 | next(); 336 | }); 337 | } else 338 | next(); 339 | 340 | }, function() { 341 | $.controller.plain(builder.join('\n')); 342 | $.cancel(); 343 | }); 344 | 345 | }, path => (/schemas|controllers/).test(path)); 346 | 347 | } 348 | 349 | function ipaddress($) { 350 | var opt = {}; 351 | opt.url = 'https://ipecho.net/plain'; 352 | opt.callback = function(err, response) { 353 | $.controller.plain(response.body || 'undefined'); 354 | $.cancel(); 355 | }; 356 | REQUEST(opt); 357 | } 358 | 359 | DEBUG && ON('service', function(counter) { 360 | if (counter % 20 === 0) 361 | DDOS = {}; 362 | }); -------------------------------------------------------------------------------- /code/readme.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | __Requirements__: 4 | 5 | - Total.js `+v4.0.0` 6 | 7 | --- 8 | 9 | - download module `code.js` 10 | - copy it to `yourapp/modules/code.js` 11 | - update `/config` file 12 | - restart the app 13 | 14 | ## Configuration 15 | 16 | You can change the endpoint for the monitor via the `/config` file of the application like this: 17 | 18 | ```html 19 | code_url : /$code/ 20 | code_token : 123456 21 | ``` 22 | 23 | - `code_url` is an API endpoint for communicating between Total.js Code Editor and your app 24 | - `code_token` is a security element (optional) -------------------------------------------------------------------------------- /console/console.package: -------------------------------------------------------------------------------- 1 | /index.html :H4sIAAAAAAAAE3VWbW/bNhD+7PwKztsqabEkOx2GwrbSFmmGdRuSYUmxDdsw0NLZYkyRKkn5pan/++4oW3GQ1AFs3vHuueNzx2Pe3Eu+1Y0LgyDanZxMv3p3fXH712+XrHSVPD+ZHn6AF+cnvakTTsL5xfXVzfWvl9O0FVFfgeMsL7mx4LJ+4+bxqz5Lu53SuTqGj41YZf0/4w9v4wtd1dyJmYQ+y7VyoNDt/WU2Gh67KV5B1p9rU3EXF+Agd0KrIw8HEupSK8iUfuq4ErCutXFHDmtRuDIrYCVyiL0wYEIJJ7iMbc4lZKMBaywYL3HM71lko2fa2SNcLuVgrqXU672xzY2oHbMmz/ppyu/4JllovZDAa2GTXFdel0oxs+ndxwbMNn2ZnCWjvZBUQiV3tn8+TVskj+m2Epjb1kAn37g0t2hx0ut9dx+vYbYULp7pTWzFJ6EW45k2BZ4DNZO40p++sPWsdoeYVHh2z0oQi9KN2Wg4/HbCaGOmi+2TjRnPlwujG1XEuZbajNm6FA4mbI4MjZmiEko2Oqs37K1Bsies5kVBUdlwwipuFkL5JdnHttLalX6XK6qN4BaKNnxtAKM/jff1j0P6ewTWxTgb1ptJl/IrylivwMyxXmOGBGPlWvRCrI7OdkaGz+GNPB45UB24AY5evp0OhDym5zkMXLeMd+d+4Ol75Cm40MpqyW0woHVjBBh2BesAc2+cFAq8owEsH5CrAp8Sdgz1CV7atL21U1+xgjseO70ElfXf3Fe6ALnD5kF7ZBTbjL5RQAKoo6bduWrJcyi1xESz/uUG8sYB9n0BLMyrIs2dkeyU4SUAE7EEP33GG6fnOm8s3ovDkjr5gElxUh+ouyi07K24YWvLMqYarAdpvgmDg1MQJVqFwRK2hV4r5GTeKD8OQojYPdr2whASuqO/wJZ9/swgodxQiNiLFyih5wWlnWUZG71slXicEBvNJisuG4gmCLOLfOQDOib0H95yhaMnpDjs6EP5gsR8MU2kL2g9e+0RYM3+gNmNzpfgwlDqnBNcUhvtNLZsIkEtXOmz+YG9ZsHa2oCN6TeIkNBgnKYB/naOpbbuWMbxWdIwItvXbV3JPsRcqN5IF3fOhMFD3REXacE53+WJjOZSW+Tkgc09mb2jQpCIc/1WVEAvxQMjA7whw6Gnrbc7Aq3AWr54BHsoki9yhTs/31xf4SHwwQgLoH768Pt7ehWwjZXDSlLeUbQPvhYuL1lYJfQ0HZB6OU4FFtD4DsatxqNbZxA/CCZ7Hb4fLKQNgerhgB2YZ1WCpXcap22rmqDFlHXr09Noj9AjyFM8jX+Jws7vb/Gvz5CxgxE1lUxodoYoHm3OsIWXniIvt6njKPCZtyboyOsaVBF2caiS0VMUWu483eTUzq9bXYcoLLDbhtFe95MfQl199k5da+/DSL3YU+pJUgVskBzUJn59PQ+DaBy0KAZcYxQLprPztjsXiW1meFQcaeFw0Hr7/p2mz5k87P+jgqcZ+QuJY2WfD66IUWxF/51YIsc3Tgsn5luyTlCowqglygN+oVtHbbf6GXkYO5gnkuynJf2r8z/2Vm5AEAkAAA== 2 | /index.js :H4sIAAAAAAAAE51W727bNhD/LD0Fgw2lDLtyBnRf7LormiZttyYpFhdDEQQBI9G2FonUSMqu0Ppd9jJ7r91RokzZdQcsgCPx7ni/H+8fNR6Ty3dz8j5LuNA8HI/JmSxrlS1Xhnzghivyz9+ZemTkeYkrje8vlwXL8jiRxYsw5J9LqYyOs5TMCE2k0DLndNrJ17hJClSun8Wn8SnoQjQzZJVpI1UNqtu7aStrHdzncgnydhXDat+AKyWVZ2LX+0YbpoRng0tncnnz5v7dFRx9Rr6Q+acP5xNCM5EZOnK8Jh3Brb/p1fXrT/4mlqYULcI1U0SWBg6rrb5z8/PpiFQqB9vxjy2VMcBUmiuQwVvJtN5IleIKXaGnDX/QMnnkBnyJKs+nXqiBCstzUCwqkSBgBLh6QL6EYfAR8lLWVjBydAawOfj9+uP8PKJvzueEkqFTxUBsRNYZ39y31MA4+OP81c312W/n86hn1nG6ZxZ2RG7pn1oKemcRvGz53JBXEJSKl1ykEQUtHJmpZVVwYZBbEHhZj1lZ5nXkSfaMtz6Uq4JjYFZ/FM5q9wCt7HuQbU0dQ0T1UUBU7uGh6Ftw11cRdpNRMs+5PUJlVqDdeoVQiW+UguXTT4UXzOlh7Honnx4c1CcK2otY8UKu+Xuobi64Os7SMToom6ipVKxyzfMFYBjoFTy0X/SoQhk+Y3ArU64Bp45g++zFQX8MOlsAoBKSAWy6oCR5BtFtcINsQXZ1DU1Injzp2sG1YpNVa9q3HRI68fun2xCvmF5Fg9jIG6MysQSaJzMIn0WO/6q4qkH3yEXrOmg1zBgG58BMGFXxqa9Lcql5NGhkipvKpiAItiH+ws5OY+m5iWZLqB+OgmvNlvwwIiNIb1EwkXqh2eM1CD1seDeqhv/2CBcxX7M8ci6Q2pYkzCQrEvH2mBdNnUW8UTfUtl51uM4xdcltI9iN7fCMcy6WZkVmEEkXcTeVIWvOSq+yhbFxwqJKbU3wDXnNDO+kUDwgTk28kKpgJqI1/D29vHyapuTt20lRTLSmA8wviTDByAdXg4mdl9C1wLJtXiy4hut0R7WsIP+AgjKMY1ehTSTc3RE/yBTvPLDEiHRWuySilU1iL04NAa93ZGXKCo9K8UoN4FgkQnkGotMpPJ7vxkobR5AOh16qM5vc1tEQPBHryrqHzFYcXHU+brO7qdvYKmdt7zWp9vyglNpSxemQiaaut4fbK5HyRSZ4euijUx1xhCRtkmb2IReN20FHslECCNW2Iyn5+pXshKIqHmBm9YUPEmYdE3QvMNaz19pIgufwwdSz+vXm+ipusLJF3dGxdJsOapPW7wD/9m3GN87DGMURfem+GDIIx2c6Iv81ucgv5P9PLPwC2etPnOjRbsY31WPnRCeLBSu4nXb0B/cFiGE96fHwBR2H0J8tmFFram8d5/yBscj1FGpjXpSmbuplz4yeNegT6q5dp02YSHjeJK4bpNvQ82qBT7w540hbbRdd36J/WXhoZqXk5tnpT9F3aGB3/wuN6t2QfgsAAA== 3 | -------------------------------------------------------------------------------- /console/make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd source 4 | tpm create console.package 5 | mv console.package ../ -------------------------------------------------------------------------------- /console/readme.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | With this module __you can control the whole application__. The module contains real-time console with textbox for performing custom commands. 4 | 5 | - download and copy `console.package` into the `/packages/` directory __or create a definition with:__ 6 | 7 | ```javascript 8 | var options = {}; 9 | 10 | // A credential is optional: 11 | // options.user = 'login name'; 12 | // options.password = 'login password'; 13 | 14 | // options.url = '/$console/'; 15 | 16 | INSTALL('package', 'https://cdn.totaljs.com/console.package', options); 17 | ``` 18 | 19 | ![Console](https://blog.totaljs.com/download/1707070825Tvew8b.jpg) -------------------------------------------------------------------------------- /console/source/index.html: -------------------------------------------------------------------------------- 1 | @{layout('')} 2 | 3 | 4 | 5 | 6 | CONSOLE 7 | 8 | 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 24 |

25 | 	
26 | 27 |
28 | 29 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /console/source/index.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright Peter Širka 3 | 4 | exports.id = 'console'; 5 | exports.version = 'v4.0.0'; 6 | 7 | const history = []; 8 | const console_log = console.log; 9 | const console_error = console.error; 10 | const console_warn = console.warn; 11 | const MSG_INIT = { TYPE: 'init', history: history }; 12 | const MSG_BODY = { TYPE: 'add' }; 13 | 14 | var options = { history: 50, url: '/$console/', user: '', password: '' }; 15 | var websocket = null; 16 | 17 | exports.install = function(opts) { 18 | 19 | U.copy(opts, options); 20 | 21 | ROUTE('GET ' + options.url, view_console); 22 | WEBSOCKET(options.url, websocket_action, ['json']); 23 | 24 | console.log = function() { 25 | prepend('log', arguments); 26 | console_log.apply(console_log, arguments); 27 | }; 28 | 29 | console.error = function() { 30 | prepend('error', arguments); 31 | console_error.apply(console_error, arguments); 32 | }; 33 | 34 | console.warn = function() { 35 | prepend('warn', arguments); 36 | console_warn.apply(console_warn, arguments); 37 | }; 38 | 39 | ON('controller', auth); 40 | }; 41 | 42 | exports.uninstall = function() { 43 | console.log = console_log; 44 | console.error = console_error; 45 | console.warn = console_warn; 46 | F.removeListener('controller', auth); 47 | }; 48 | 49 | function websocket_action() { 50 | 51 | var self = this; 52 | 53 | websocket = self; 54 | 55 | self.autodestroy(() => websocket = null); 56 | 57 | self.on('open', function(client) { 58 | 59 | if (options.user && options.password) { 60 | if ((options.user + ':' + options.password).hash().toString() !== client.query.token) { 61 | client.attacker = true; 62 | client.close(); 63 | return; 64 | } 65 | } 66 | 67 | client.send(MSG_INIT); 68 | }); 69 | 70 | self.on('message', function(client, command) { 71 | 72 | if (client.attacker) 73 | return; 74 | 75 | try 76 | { 77 | F.eval(command); 78 | } catch (e) { 79 | F.error(e); 80 | } 81 | }); 82 | } 83 | 84 | function prepend(type, arg) { 85 | history.length === options.history && history.shift(); 86 | var dt = new Date(); 87 | var str = dt.format('yyyy-MM-dd HH:mm:ss') + ' (' + type + '): ' + append.apply(null, arg); 88 | history.push(str); 89 | if (websocket) { 90 | MSG_BODY.body = str; 91 | websocket.send(MSG_BODY); 92 | } 93 | } 94 | 95 | function append() { 96 | 97 | var output = ''; 98 | 99 | for (var i = 0; i < arguments.length; i++) { 100 | 101 | if (i) 102 | output += ' '; 103 | 104 | var value = arguments[i]; 105 | 106 | if (value === null) { 107 | output += 'null'; 108 | continue; 109 | } 110 | 111 | if (value === undefined) { 112 | output += 'undefined'; 113 | continue; 114 | } 115 | 116 | var type = typeof(value); 117 | 118 | if (type === 'string' || type === 'number' || type === 'boolean') 119 | output += value.toString(); 120 | else 121 | output += JSON.stringify(value); 122 | } 123 | 124 | return output; 125 | } 126 | 127 | function view_console() { 128 | this.view('@console/index', options.user && options.password ? (options.user + ':' + options.password).hash().toString() : ''); 129 | } 130 | 131 | function auth(controller) { 132 | 133 | if (controller.name !== '#console' || !options.user || !options.password) 134 | return; 135 | 136 | var user = controller.baa(); 137 | if (user.empty) { 138 | controller.baa('Console:'); 139 | controller.cancel(); 140 | return; 141 | } 142 | 143 | if (user.user !== options.user || user.password !== options.password) { 144 | controller.throw401(); 145 | controller.cancel(); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /directorylisting/directorylisting.js: -------------------------------------------------------------------------------- 1 | const Fs = require('fs'); 2 | const Path = require('path'); 3 | 4 | exports.install = function(options) { 5 | var url = '/'; 6 | if (options && options.url) 7 | url = U.path(options.url.replace('*', '')); 8 | ROUTE('GET ' + url + '*', directorylisting); 9 | }; 10 | 11 | function directorylisting() { 12 | var self = this; 13 | var dir = F.path.public(self.url.substring(1)); 14 | Fs.readdir(dir, function(err, items) { 15 | 16 | var directories = []; 17 | var files = []; 18 | 19 | self.url !== '/' && directories.push('..'); 20 | 21 | if (err) 22 | return render(self, directories, files); 23 | 24 | items.wait(function(item, next) { 25 | var filename = Path.join(dir, item); 26 | Fs.stat(filename, function(err, info) { 27 | if (info.isFile()) 28 | files.push('{0}{2}'.format(item, self.url + item, info.size.filesize())); 29 | else 30 | directories.push('{0}'.format(item, self.url + item)); 31 | next(); 32 | }); 33 | }, () => render(self, directories, files)); 34 | }); 35 | } 36 | 37 | function render(controller, directories, files) { 38 | controller.content('Directory listing: {0}
{1}
{2}
'.format(controller.url, directories.join(''), files.join('')), 'text/html'); 39 | } -------------------------------------------------------------------------------- /directorylisting/readme.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Directory listing. This module uses wildcard routing. 4 | 5 | - download and copy `directorylisting.js` into the `/modules/` directory __or create a definition with:__ 6 | 7 | ```javascript 8 | var options = {}; 9 | 10 | // URL from which should be enabled directory listing: 11 | // Default value: 12 | options.url = '/'; 13 | 14 | INSTALL('module', 'https://modules.totaljs.com/latest/directorylisting.js', options); 15 | ``` -------------------------------------------------------------------------------- /edbms/edbms.js: -------------------------------------------------------------------------------- 1 | var DB = {}; 2 | var pending = 0; 3 | 4 | function EDBMS(url, eb) { 5 | 6 | var t = this; 7 | t.$remap = true; 8 | // t.$raw = false; 9 | t.$errors = eb ? eb : new ErrorBuilder(); 10 | t.$commands = []; 11 | 12 | // t.$timeout; 13 | // t.$output; 14 | // t.$outputlast; 15 | // t.output; 16 | // t.$callback; 17 | 18 | t.url = DB[url || 'default'] || ''; 19 | t.response = {}; 20 | t.tmp = ''; 21 | 22 | t.$request = function() { 23 | t.$timeout = null; 24 | t.$exec(); 25 | }; 26 | } 27 | 28 | EDBMS.clear = function() { 29 | DB = {}; 30 | }; 31 | 32 | EDBMS.use = function() { 33 | // Total.js framework 34 | if (global.F) { 35 | global.F.database = function(err) { 36 | if (typeof(err) === 'function') { 37 | var db = new EDBMS(); 38 | err.call(db, db); 39 | } else 40 | return new EDBMS(err); 41 | }; 42 | } 43 | return EDBMS; 44 | }; 45 | 46 | EDBMS.url = function(name, url) { 47 | 48 | if (url == null) { 49 | url = name; 50 | name = 'default'; 51 | } 52 | 53 | if (url[url.length - 1] === '/') 54 | url = url.substring(0, url.length - 1); 55 | 56 | DB[name] = url; 57 | return EDBMS; 58 | }; 59 | 60 | EDBMS.index = function(name, indexname, callback) { 61 | 62 | if (indexname == null || typeof(indexname) === 'function') { 63 | callback = indexname; 64 | indexname = name; 65 | name = 'default'; 66 | } 67 | 68 | pending++; 69 | 70 | var url = DB[name]; 71 | RESTBuilder.HEAD(url + '/' + indexname).exec(function(err, response) { 72 | 73 | pending--; 74 | 75 | if (err) 76 | throw err; 77 | 78 | if (!response) { 79 | pending++; 80 | callback(function(model, callback) { 81 | pending--; 82 | // console.log(model); 83 | RESTBuilder.PUT(url + '/' + indexname, model).exec(callback || ERROR('Create index "' + url + '/' + indexname + '"')); 84 | }); 85 | } 86 | }); 87 | }; 88 | 89 | const ED = EDBMS.prototype; 90 | const TMP = {}; 91 | 92 | TMP.replace = function(text) { 93 | return JSON.stringify(TMP.value[text.substring(1)]); 94 | }; 95 | 96 | ED.callback = function(fn) { 97 | this.$callback = fn; 98 | return this; 99 | }; 100 | 101 | ED.must = function(err, reverse) { 102 | var self = this; 103 | self.$commands.push({ type: 'must', value: err || 'unhandled exception', reverse: reverse }); 104 | return self; 105 | }; 106 | 107 | ED.insert = function(index, type, data) { 108 | 109 | if (!index || !type || !data) 110 | throw new Error('Missing required arguments.'); 111 | 112 | var self = this; 113 | var name = index + '/' + type; 114 | return self.exec(name, data); 115 | }; 116 | 117 | ED.update = function(index, type, id, data) { 118 | 119 | if (!index || !type || !id || !data) 120 | throw new Error('Missing required arguments.'); 121 | 122 | var self = this; 123 | var name = 'PUT {0}/{1}/{2}'.format(index, type, id); 124 | return self.exec(name, data); 125 | }; 126 | 127 | ED.modify = function(index, id, data) { 128 | 129 | if (!index || !id || !data) 130 | throw new Error('Missing required arguments.'); 131 | 132 | var self = this; 133 | var name = '{0}/_update/{1}'.format(index, id); 134 | var model = {}; 135 | model.doc = data; 136 | return self.exec(name, model); 137 | }; 138 | 139 | ED.read = function(index, type, id) { 140 | 141 | if (!index || !type || !id) 142 | throw new Error('Missing required arguments.'); 143 | 144 | var self = this; 145 | var name = 'GET {0}/{1}/{2}'.format(index, type, id); 146 | return self.exec(name, null, null, true); 147 | }; 148 | 149 | ED.list = function(index) { 150 | 151 | if (!index) 152 | throw new Error('Missing required arguments.'); 153 | 154 | var self = this; 155 | var name = index + '/_search'; 156 | return self.exec(name); 157 | }; 158 | 159 | ED.delete = function(index, type, id) { 160 | 161 | if (!index || (type && !id)) 162 | throw new Error('Missing required arguments.'); 163 | 164 | var self = this; 165 | var name; 166 | 167 | if (id == null) 168 | name = '{0}/_delete_by_query'.format(index); 169 | else 170 | name = 'DELETE {0}/{1}/{2}'.format(index, type, id); 171 | 172 | return self.exec(name); 173 | }; 174 | 175 | ED.refresh = function(index) { 176 | 177 | if (!index) 178 | throw new Error('Missing required arguments.'); 179 | 180 | var self = this; 181 | self.$raw = true; 182 | var name = index + '/_refresh'; 183 | return self.exec(name); 184 | }; 185 | 186 | ED.count = function(index) { 187 | 188 | if (!index) 189 | throw new Error('Missing required arguments.'); 190 | 191 | var self = this; 192 | self.$raw = true; 193 | var name = index + '/_count'; 194 | return self.exec(name); 195 | }; 196 | 197 | ED.exec = function(name, index, data, read) { 198 | 199 | if (typeof(index) === 'object') { 200 | data = index; 201 | index = null; 202 | } 203 | 204 | if (index == null) 205 | index = name; 206 | 207 | var self = this; 208 | var builder = new ElasticQuery(); 209 | var beg = index.indexOf(' '); 210 | 211 | if (beg !== -1) { 212 | var method = index.substring(0, beg); 213 | builder.options.method = method; 214 | index = index.substring(beg + 1).trim(); 215 | } 216 | 217 | if (data) 218 | builder.options.body = data; 219 | 220 | if (read) 221 | builder.options.one = true; 222 | 223 | builder.$commandindex = self.$commands.push({ name: name, index: index, builder: builder }) - 1; 224 | self.$timeout && clearImmediate(self.$timeout); 225 | self.$timeout = setImmediate(self.$request); 226 | return builder; 227 | }; 228 | 229 | ED.output = function(val) { 230 | this.$output = val; 231 | return this; 232 | }; 233 | 234 | ED.$validate = function(cmd) { 235 | var type = typeof(cmd.value); 236 | var stop = false; 237 | switch (type) { 238 | case 'function': 239 | var val = cmd.value(self.output, self.$output); 240 | if (typeof(val) === 'string') { 241 | stop = true; 242 | self.$errors.push(val); 243 | } 244 | break; 245 | case 'string': 246 | if (self.output instanceof Array) { 247 | if (cmd.reverse) { 248 | if (self.output.length) { 249 | self.$errors.push(cmd.value); 250 | stop = true; 251 | } 252 | } else { 253 | if (!self.output.length) { 254 | self.$errors.push(cmd.value); 255 | stop = true; 256 | } 257 | } 258 | } else { 259 | if (cmd.reverse) { 260 | if (self.output) { 261 | self.$errors.push(cmd.value); 262 | stop = true; 263 | } 264 | } else { 265 | if (!self.output) { 266 | self.$errors.push(cmd.value); 267 | stop = true; 268 | } 269 | } 270 | } 271 | break; 272 | } 273 | 274 | if (stop) { 275 | self.$commands = []; 276 | self.$callback && self.$callback(self.$errors.length ? self.error : null, self.output); 277 | } else { 278 | self.$timeout && clearImmediate(self.$timeout); 279 | self.$timeout = setImmediate(self.$request); 280 | } 281 | }; 282 | 283 | ED.$exec = function() { 284 | 285 | var self = this; 286 | 287 | // Pending for indexes... 288 | if (pending > 0) { 289 | setTimeout(self.$request, 500); 290 | return self; 291 | } 292 | 293 | var cmd = self.$commands.shift(); 294 | 295 | if (cmd == null) { 296 | // end 297 | // callback 298 | self.$callback && self.$callback(self.$errors.length ? self.error : null, self.output); 299 | return self; 300 | } 301 | 302 | var c = cmd.index[0]; 303 | 304 | if (c === '/') 305 | cmd.index = cmd.index.substring(1); 306 | else if (c === '[') { 307 | var beg = cmd.index.indexOf(']'); 308 | cmd.index = DB[cmd.index.substring(1, beg)] + cmd.index.substring(beg + 1); 309 | self.url = ''; 310 | } 311 | 312 | var builder = cmd.builder; 313 | 314 | if (builder.options.refresh) 315 | cmd.index += '?refresh'; 316 | 317 | var rb = RESTBuilder.url((self.url ? (self.url + '/') : '') + cmd.index); 318 | 319 | if (builder.options.method !== 'GET') { 320 | var q = builder.create(); 321 | rb.json(q); 322 | } 323 | 324 | rb.$method = builder.options.method; 325 | rb.$keepalive = true; 326 | rb.exec(function(err, response) { 327 | 328 | if (self.$raw) { 329 | self.output = self.response[cmd.name] = response; 330 | builder.options.data && builder.options.data(err, self.response[cmd.name]); 331 | builder.options.callback && builder.options.callback(err, self.response[cmd.name]); 332 | self.$timeout && clearImmediate(self.$timeout); 333 | self.$timeout = setImmediate(self.$request); 334 | return; 335 | } 336 | 337 | if (builder.options.one) { 338 | if (!response.found) 339 | self.output = self.response[cmd.name] = null; 340 | else { 341 | var source = response._source; 342 | source.id = response._id; 343 | self.output = self.response[cmd.name] = source; 344 | } 345 | 346 | builder.options.data && builder.options.data(err, self.response[cmd.name]); 347 | builder.options.callback && builder.options.callback(err, self.response[cmd.name]); 348 | self.$timeout && clearImmediate(self.$timeout); 349 | self.$timeout = setImmediate(self.$request); 350 | } 351 | 352 | if (response.deleted > -1) { 353 | self.output = self.response[cmd.name] = { status: response.deleted === 0 ? 'noop' : 'deleted', total: response.deleted }; 354 | builder.options.data && builder.options.data(err, self.response[cmd.name]); 355 | builder.options.callback && builder.options.callback(err, self.response[cmd.name]); 356 | self.$timeout && clearImmediate(self.$timeout); 357 | self.$timeout = setImmediate(self.$request); 358 | } 359 | 360 | if (response.error) { 361 | var err = response.error.type ? (response.error.type + ': ' + response.error.reason) : response.error; 362 | self.$errors.push(err); 363 | builder.options.fail && builder.options.fail(err); 364 | builder.options.callback && builder.options.callback(err); 365 | self.$timeout && clearImmediate(self.$timeout); 366 | self.$timeout = setImmediate(self.$request); 367 | return; 368 | } 369 | 370 | if (response.result) { 371 | self.output = self.response[cmd.name] = { id: response._id, status: response.result }; 372 | builder.options.data && builder.options.data(err, self.response[cmd.name]); 373 | builder.options.callback && builder.options.callback(err, self.response[cmd.name]); 374 | self.$timeout && clearImmediate(self.$timeout); 375 | self.$timeout = setImmediate(self.$request); 376 | return; 377 | } 378 | 379 | response = response.hits; 380 | 381 | if (!response) { 382 | self.$timeout && clearImmediate(self.$timeout); 383 | self.$timeout = setImmediate(self.$request); 384 | return; 385 | } 386 | 387 | var item; 388 | var opt = builder.options; 389 | if (opt.first) { 390 | if (response.total.value) { 391 | item = response.hits[0]; 392 | if (self.$remap) { 393 | item._source.id = item._id; 394 | item = item._source; 395 | } 396 | self.output = self.response[cmd.name] = item; 397 | } else 398 | self.output = self.response[cmd.name] = null; 399 | } else { 400 | var output = {}; 401 | output.score = response.max_score; 402 | output.count = response.total.value; 403 | output.pages = output.count && opt.take ? Math.ceil(output.count / opt.take) : 0; 404 | output.page = opt.skip ? (Math.ceil(opt.skip / opt.take) + 1) : 1; 405 | output.items = response.hits; 406 | if (self.$remap) { 407 | for (var i = 0; i < output.items.length; i++) { 408 | item = output.items[i]; 409 | item._source.id = item._id; 410 | output.items[i] = item._source; 411 | } 412 | } 413 | self.output = self.response[cmd.name] = output; 414 | } 415 | 416 | builder.options.data && builder.options.data(err, self.response[cmd.name]); 417 | builder.options.callback && builder.options.callback(err, self.response[cmd.name]); 418 | self.$timeout && clearImmediate(self.$timeout); 419 | self.$timeout = setImmediate(self.$request); 420 | }); 421 | }; 422 | 423 | function ElasticQuery() { 424 | this.mapper = {}; 425 | this.items = []; 426 | this.options = { method: 'post' }; 427 | this.tmp = ''; 428 | } 429 | 430 | const EP = ElasticQuery.prototype; 431 | 432 | EP.fields = function(fields) { 433 | 434 | var self = this; 435 | 436 | if (!self.options.fields) 437 | self.options.fields = []; 438 | 439 | var arr = arguments; 440 | 441 | if (arr.length === 1 && fields.indexOf(',') !== -1) 442 | arr = fields.split(','); 443 | 444 | for (var i = 0; i < arr.length; i++) 445 | self.options.fields.push((arr[i][0] === ' ' ? arr[i].trim() : arr[i])); 446 | 447 | return self; 448 | }; 449 | 450 | EP.scope = function(path) { 451 | var self = this; 452 | self.$scope = path || ''; 453 | return self; 454 | }; 455 | 456 | EP.push = function(path, value) { 457 | 458 | var self = this; 459 | 460 | if (self.$scope) 461 | path = self.$scope + (path ? '.' : '') + path; 462 | 463 | if (value === undefined) 464 | value = NOOP; 465 | 466 | if (self.mapper[path]) 467 | self.mapper[path].push(value); 468 | else 469 | self.mapper[path] = [value]; 470 | 471 | return self; 472 | }; 473 | 474 | EP.sort = function(name, type) { 475 | var self = this; 476 | var opt = self.options; 477 | var item; 478 | 479 | if (type) { 480 | item = {}; 481 | item[name] = type; 482 | } else 483 | item = name; 484 | 485 | if (opt.sort) 486 | opt.sort.push(item); 487 | else 488 | opt.sort = [item]; 489 | 490 | return self; 491 | }; 492 | 493 | EP.skip = function(value) { 494 | var self = this; 495 | self.options.skip = value; 496 | return self; 497 | }; 498 | 499 | EP.take = function(value) { 500 | var self = this; 501 | self.options.take = value; 502 | return self; 503 | }; 504 | 505 | EP.page = function(page, limit) { 506 | var self = this; 507 | if (limit) 508 | self.options.take = limit; 509 | self.options.skip = page * self.options.take; 510 | return self; 511 | }; 512 | 513 | EP.paginate = function(page, limit, maxlimit) { 514 | 515 | var self = this; 516 | var limit2 = +(limit || 0); 517 | var page2 = (+(page || 0)) - 1; 518 | 519 | if (page2 < 0) 520 | page2 = 0; 521 | 522 | if (maxlimit && limit2 > maxlimit) 523 | limit2 = maxlimit; 524 | 525 | if (!limit2) 526 | limit2 = maxlimit; 527 | 528 | self.options.skip = page2 * limit2; 529 | self.options.take = limit2; 530 | return self; 531 | }; 532 | 533 | EP.first = function() { 534 | var self = this; 535 | self.options.first = true; 536 | self.options.take = 1; 537 | return self; 538 | }; 539 | 540 | EP.one = function() { 541 | this.options.one = true; 542 | return this; 543 | }; 544 | 545 | EP.create = function() { 546 | var self = this; 547 | var opt = self.options; 548 | 549 | if (opt.body) 550 | return opt.body; 551 | 552 | var obj = {}; 553 | var keys = Object.keys(self.mapper); 554 | var tmp; 555 | 556 | if (opt.sort) 557 | obj.sort = opt.sort; 558 | 559 | opt.take && (obj.size = opt.take); 560 | opt.skip && (obj.from = opt.skip); 561 | opt.fields && opt.fields.length && (obj._source = opt.fields); 562 | 563 | for (var i = 0; i < keys.length; i++) { 564 | 565 | var key = keys[i]; 566 | var cur, arr, p, isarr, isend; 567 | 568 | arr = key.split('.'); 569 | cur = obj; 570 | 571 | for (var j = 0; j < arr.length; j++) { 572 | 573 | p = arr[j]; 574 | isarr = p[p.length - 1] === ']'; 575 | isend = j === arr.length - 1; 576 | 577 | if (isarr) 578 | p = p.substring(0, p.length - 2); 579 | 580 | if (cur instanceof Array) { 581 | // must be ended 582 | if (!isend) 583 | throw new Error('Not allowed path for "' + key + '".'); 584 | 585 | } else if (cur[p] === undefined) 586 | cur[p] = isarr ? [] : {}; 587 | 588 | if (!isend) 589 | cur = cur[p]; 590 | } 591 | 592 | var items = self.mapper[key]; 593 | for (var j = 0; j < items.length; j++) { 594 | var item = items[j]; 595 | if (item != NOOP) { 596 | if (cur[p] instanceof Array) { 597 | cur[p].push(item); 598 | } else { 599 | if (cur instanceof Array) { 600 | tmp = {}; 601 | tmp[p] = item; 602 | cur.push(tmp); 603 | } else 604 | cur[p] = item; 605 | } 606 | } 607 | } 608 | } 609 | 610 | if (self.$extend) { 611 | for (var i = 0 ; i < self.$extend.length; i++) 612 | self.$extend[i](obj); 613 | } 614 | 615 | var body = JSON.stringify(obj); 616 | 617 | if (opt.debug) 618 | console.log('--->', body); 619 | 620 | return body; 621 | }; 622 | 623 | EP.extend = function(fn) { 624 | var self = this; 625 | if (self.$extend) 626 | self.$extend.push(fn); 627 | else 628 | self.$extend = [fn]; 629 | return self; 630 | }; 631 | 632 | EP.debug = function() { 633 | this.options.debug = true; 634 | return this; 635 | }; 636 | 637 | EP.refresh = function() { 638 | this.options.refresh = true; 639 | return this; 640 | }; 641 | 642 | EP.callback = function(callback) { 643 | this.options.callback = callback; 644 | return this; 645 | }; 646 | 647 | EP.data = function(callback) { 648 | this.options.data = callback; 649 | return this; 650 | }; 651 | 652 | EP.fail = function(callback) { 653 | this.options.fail = callback; 654 | return this; 655 | }; 656 | 657 | exports.EDBMS = EDBMS; 658 | exports.url = EDBMS.url; 659 | exports.clear = EDBMS.clear; 660 | exports.use = EDBMS.use; 661 | exports.index = EDBMS.index; 662 | 663 | global.EDB = function(name, err) { 664 | if (name && typeof(name) === 'object') { 665 | err = name; 666 | name = null; 667 | } 668 | return new EDBMS(name, err); 669 | }; -------------------------------------------------------------------------------- /edbms/readme.md: -------------------------------------------------------------------------------- 1 | # Total.js ElasticSearch Database Management System 2 | 3 | ## Initialization 4 | 5 | - make a definition file e.g. `/definitions/db.js` 6 | 7 | ```js 8 | EDBMS.url('http://localhost:9200'); 9 | ``` 10 | 11 | ## Examples 12 | 13 | ```js 14 | var db = EDB(); 15 | 16 | // Listing - Performs index/_search 17 | var builder = db.list('index'); 18 | builder.scope('query.bool.must[]'); 19 | builder.push('match', { title: 'my_search_phrase' }); 20 | builder.callback(function(err, response) { 21 | // ... 22 | }); 23 | 24 | // Read single document 25 | db.read('index', 'type', '_id').callback(function(err, response) { 26 | // ... 27 | }); 28 | 29 | // Create a new document and manual refresh 30 | db.insert('index', 'type', model).refresh().callback(function(err, response) { 31 | // ... 32 | }); 33 | 34 | // Update document 35 | db.update('index', 'type', '_id', model).callback(function(err, response) { 36 | // ... 37 | }); 38 | 39 | // Partial update 40 | db.modify('index', '_id', model).callback(function(err, response) { 41 | // ... 42 | }); 43 | 44 | // Delete document 45 | db.delete('index', 'type', 'id').callback(function(err, response) { 46 | // ... 47 | }); 48 | 49 | // Delete by query 50 | var builder = db.delete('index'); 51 | builder.scope('query.bool.must[]'); 52 | builder.push('term', { userid: 5 }); 53 | builder.callback(function(err, response) { 54 | // ... 55 | }); 56 | 57 | // Custom query 58 | // Available methods: POST (default), GET, PUT, DELETE 59 | var builder = db.exec('GET /YOUR-INDEX/TYPE/_search'); 60 | builder.scope('query.bool.must[]'); 61 | builder.push('term', { userid: 5 }); 62 | builder.callback(function(err, response) { 63 | // ... 64 | }); 65 | 66 | // Refresh index 67 | db.refresh('index').callback(function(err, response) { 68 | console.log(err, response); 69 | }); 70 | 71 | // Count of documents 72 | var builder = db.count('index'); 73 | builder.scope('query.bool.must[]'); 74 | builder.push('term', { userid: 5 }); 75 | builder.callback(function(err, response) { 76 | // ... 77 | }); 78 | 79 | // create index 80 | db.index('index', function(next) { 81 | 82 | next({ 'index-data' }); 83 | 84 | }); 85 | ``` 86 | 87 | - Author: [Tomas Novak](https://github.com/tomee03) -------------------------------------------------------------------------------- /filecache/filecache.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright Peter Širka 3 | 4 | const ERRNOTFOUND = 'File not found.'; 5 | const Fs = require('fs'); 6 | 7 | function FileCache() { 8 | this.list = {}; 9 | this.length = 0; 10 | } 11 | 12 | var FCP = FileCache.prototype; 13 | 14 | FCP.init = function() { 15 | ON('service', function(counter) { 16 | if (counter % 5 === 0) 17 | MODULE('filecache').clear(); 18 | }); 19 | }; 20 | 21 | FCP.has = function(id) { 22 | var obj = this.list[id]; 23 | return obj ? obj.expire < NOW : false; 24 | }; 25 | 26 | FCP.add = function(file, expire, id, callback) { 27 | 28 | var self = this; 29 | var type = typeof(id); 30 | 31 | if (type === 'function') { 32 | var tmp = callback; 33 | callback = id; 34 | id = tmp; 35 | type = typeof(id); 36 | } 37 | 38 | if (!id) 39 | id = GUID(20); 40 | else if (!self.list[id]) 41 | self.length++; 42 | 43 | var path = PATH.temp(id + '.filecache'); 44 | self.list[id] = { expire: expire, type: file.type, filename: file.filename, length: file.length, width: file.width, height: file.height, path: path }; 45 | 46 | if (callback) 47 | file.copy(path, () => callback(id, self.list[id])); 48 | else 49 | file.copy(path); 50 | 51 | return id; 52 | }; 53 | 54 | FCP.info = function(id) { 55 | var obj = this.list[id]; 56 | return obj == null || obj.expire < NOW ? null : obj; 57 | }; 58 | 59 | FCP.read = function(id, callback, remove) { 60 | 61 | var self = this; 62 | 63 | if (!self.list[id]) { 64 | callback(ERRNOTFOUND); 65 | return; 66 | } 67 | 68 | var obj = self.list[id]; 69 | 70 | if (obj.expire < new Date()) { 71 | self.remove(id); 72 | callback(ERRNOTFOUND); 73 | return; 74 | } 75 | 76 | var stream = Fs.createReadStream(PATH.temp(id + '.filecache')); 77 | remove && stream.on('close', () => self.remove(id)); 78 | callback(null, obj, stream); 79 | return self; 80 | }; 81 | 82 | FCP.copy = function(id, path, callback, remove) { 83 | 84 | var self = this; 85 | 86 | if (!self.list[id]) { 87 | callback && callback(ERRNOTFOUND); 88 | return; 89 | } 90 | 91 | var obj = self.list[id]; 92 | if (obj.expire < NOW) { 93 | self.remove(id); 94 | callback && callback(ERRNOTFOUND); 95 | return; 96 | } 97 | 98 | var stream = Fs.createReadStream(PATH.temp(id + '.filecache')); 99 | stream.pipe(Fs.createWriteStream(path)); 100 | remove && stream.on('close', () => self.remove(id)); 101 | callback && callback(null, path); 102 | return self; 103 | }; 104 | 105 | FCP.remove = function(id) { 106 | 107 | var self = this; 108 | 109 | if (!(id instanceof Array)) 110 | id = [id]; 111 | 112 | var arr = []; 113 | var length = id.length; 114 | 115 | for (var i = 0; i < length; i++) { 116 | 117 | var key = id[i]; 118 | var file = self.list[key]; 119 | 120 | if (!file) 121 | continue; 122 | 123 | delete self.list[key]; 124 | self.length--; 125 | arr.push(PATH.temp(key + '.filecache')); 126 | } 127 | 128 | if (arr.length === 0) 129 | return self; 130 | 131 | arr.wait((path, next) => Fs.unlink(path, next)); 132 | return self; 133 | }; 134 | 135 | FCP.clear = function() { 136 | 137 | var self = this; 138 | var arr = Object.keys(self.list); 139 | var length = arr.length; 140 | var tmp = []; 141 | var now = NOW; 142 | 143 | for (var i = 0; i < length; i++) { 144 | var obj = self.list[arr[i]]; 145 | 146 | if (obj.expire >= now) 147 | continue; 148 | 149 | delete self.list[arr[i]]; 150 | tmp.push(PATH.temp(arr[i] + '.filecache')); 151 | self.length--; 152 | } 153 | 154 | if (tmp.length > 0) 155 | framework.unlink(tmp); 156 | 157 | return self; 158 | }; 159 | 160 | FCP.usage = function() { 161 | return { count: this.length }; 162 | }; 163 | 164 | var filecache = new FileCache(); 165 | module.exports = filecache; 166 | module.exports.install = () => filecache.init(); -------------------------------------------------------------------------------- /filecache/readme.md: -------------------------------------------------------------------------------- 1 | # FileCache module 2 | 3 | - copy **filecache.js** to __/your-totaljs-website/modules/__ 4 | - __IMPORTANT:__ this module does not work with the cluster 5 | 6 | FileCache stores uploaded files for some time. The module automatically removed older files. 7 | 8 | #### Add a file: filecache.add(file, expire, [callback]) 9 | 10 | ```js 11 | // this === controller 12 | var self = this; 13 | var filecache = MODULE('filecache'); 14 | 15 | var id = filecache.add(self.files[0], new Date().add('minute', 5)); 16 | console.log(id); 17 | 18 | // or 19 | 20 | filecache.add(self.files[0], new Date().add('minute', 5), function(id, header) { 21 | console.log(id); 22 | console.log(header); 23 | // { expire: Date, type: String, filename: String, length: Number, width: Number, height: Number } 24 | }); 25 | ``` 26 | 27 | #### Read a file informations: filecache.info(id); 28 | 29 | > If the file is expired or doesn't exist then function return __null__. 30 | 31 | ```js 32 | // this === controller 33 | var self = this; 34 | var filecache = MODULE('filecache'); 35 | 36 | console.log(filechace.info('d4e2ec5edbc4eda32e48')); 37 | // { expire: Date, type: String, filename: String, path: String, length: Number, width: Number, height: Number } 38 | 39 | ``` 40 | 41 | #### Read a file: filecache.read(id, calllback, [removeAfterRead]); 42 | 43 | ```js 44 | // this === controller 45 | var self = this; 46 | var filecache = MODULE('filecache'); 47 | 48 | filecache.read('id', function(err, header, stream) { 49 | 50 | if (err) { 51 | self.view500(err); 52 | return; 53 | } 54 | 55 | // header.type {String} 56 | // header.filename {String} 57 | // header.expire {DateTime} 58 | 59 | self.stream(header.type, stream); 60 | }); 61 | ``` 62 | 63 | #### Copy file: filecache.copy(id, path, [calllback], [removeAfterRead]); 64 | 65 | ```js 66 | // this === controller 67 | var self = this; 68 | var filecache = MODULE('filecache'); 69 | 70 | filecache.copy('id', '/newpah/myfile.jpg', function(err, path) { 71 | 72 | if (err) { 73 | self.view500(err); 74 | return; 75 | } 76 | 77 | }, true); 78 | ``` 79 | 80 | #### Has a file: filecache.has(id); 81 | 82 | ```js 83 | // this === controller 84 | var self = this; 85 | var filecache = MODULE('filecache'); 86 | 87 | console.log(filecache.has('id')); // TRUE or FALSE 88 | ``` 89 | 90 | #### Remove a file: filecache.remove(id) 91 | 92 | ```js 93 | // this === controller 94 | var self = this; 95 | var filecache = MODULE('filecache'); 96 | 97 | filecache.remove('id'); 98 | 99 | // or 100 | 101 | filecache.remove(['id1', 'id2', 'id3']); 102 | ``` -------------------------------------------------------------------------------- /flash/flash.js: -------------------------------------------------------------------------------- 1 | var COOKIE = '__flash'; 2 | var flash = {}; 3 | 4 | exports.version = 'v3.0.0'; 5 | exports.id = 'flash'; 6 | exports.expire = '5 minutes'; 7 | 8 | exports.usage = function() { 9 | return flash; 10 | }; 11 | 12 | const Flash = function(name, value) { 13 | 14 | var item = flash[this.$flash]; 15 | var dt = NOW; 16 | var now = dt.getTime(); 17 | 18 | if (name === undefined && value === undefined) 19 | return item.expire < now ? [] : item.params; 20 | 21 | if (value === undefined) 22 | return item.expire < now ? undefined : item.params[name]; 23 | 24 | if (item.expire < now) 25 | item.params = {}; 26 | 27 | item.expire = dt.add(exports.expire).getTime(); 28 | 29 | if (value instanceof Array) { 30 | if (item.params[name]) 31 | item.params[name].push.apply(item.params[name], value); 32 | else 33 | item.params[name] = value; 34 | return item.params; 35 | } 36 | 37 | if (item.params[name] instanceof Array) 38 | item.params[name].push(value); 39 | else 40 | item.params[name] = [value]; 41 | 42 | return item.params; 43 | }; 44 | 45 | MIDDLEWARE('flash', function(req, res, resume, options) { 46 | 47 | var id = req.cookie(COOKIE); 48 | 49 | if (!id) { 50 | id = GUID(10); 51 | res.cookie(COOKIE, id); // a session cookie 52 | } 53 | 54 | req.flash = Flash; 55 | 56 | var expire = exports.expire; 57 | if (options && options.expire) 58 | expire = options.expire; 59 | 60 | !flash[id] && (flash[id] = { params: {}, expire: NOW.add(expire).getTime() }); 61 | req.$flash = id; 62 | resume(); 63 | }); 64 | 65 | exports.install = function() { 66 | ON('service', service); 67 | }; 68 | 69 | exports.uninstall = function() { 70 | OFF('service', service); 71 | }; 72 | 73 | // Extends controller 74 | Controller.prototype.flash = function(name, value) { 75 | return this.req.flash(name, value); 76 | }; 77 | 78 | DEF.helpers.flash = function(name) { 79 | return this.req.flash(name); 80 | }; 81 | 82 | // Cleaner 83 | function service() { 84 | var now = Date.now(); 85 | for (var m in flash) 86 | flash[m].expire < now && (delete flash[m]); 87 | } -------------------------------------------------------------------------------- /flash/readme.md: -------------------------------------------------------------------------------- 1 | # Flash messages 2 | 3 | - inspired from 4 | - cookie per session + session timeout 5 | - copy __flash.js__ to /your-totaljs-website/modules/ 6 | 7 | ### Usage 8 | 9 | ```javascript 10 | exports.install = function() { 11 | F.route('/', json_index, ['#flash']); 12 | F.route('/flash/', redirect_flash, ['#flash']); 13 | // or use global middleware 14 | // F.use('flash'); 15 | }; 16 | 17 | function json_index() { 18 | var self = this; 19 | // self.json(self.flash('info')) 20 | self.json(self.flash()); 21 | } 22 | 23 | function redirect_flash() { 24 | var self = this; 25 | // self.req.flash() 26 | self.flash('info', 'Flash is back!'); 27 | self.redirect('/'); 28 | } 29 | ``` 30 | 31 | ```html 32 | @{flash('info')} returns array 33 | ``` 34 | 35 | ## Expiration 36 | 37 | ```javascript 38 | MODULE('flash').expire = '5 minutes'; 39 | // or 40 | // MODULE('flash').expire = '5 seconds'; 41 | ``` 42 | -------------------------------------------------------------------------------- /flowstream/readme.md: -------------------------------------------------------------------------------- 1 | # FlowStream module 2 | 3 | This module integrates Total.js FlowStream and extends it by adding new features. The module uses [__FlowStream app__](https://github.com/totaljs/flowstream). 4 | 5 | ## Usage 6 | 7 | ```js 8 | var schema = { components: {}, design: {}, variables: {}, variables2: {} }; 9 | var worker_thread = true; 10 | var instance = MODULE('flowstream'); 11 | 12 | // Methods: 13 | // instance.init(meta isworker, callback(err, instance)); 14 | // instance.socket(meta, socket, check(client) => true); 15 | // instance.input([flowstreamid], [id], data); 16 | // instance.trigger(flowstreamid, id, data); 17 | // instance.refresh([flowstreamid], [type]); 18 | // instance.exec(flowstreamid, opt); 19 | // opt.id {String} 20 | // opt.ref {String} optional, custom reference 21 | // opt.uid {String} optional, custom reference 22 | // opt.repo {Object} optional, a custom message repository object 23 | // opt.vars {Object} optional, a custom message variables 24 | // opt.data {Object/String/Number} optional, data for the message 25 | // opt.timeout {Number} optional, a timeout in milliseconds 26 | // otp.callback(err, output) return processed message 27 | ``` -------------------------------------------------------------------------------- /fulltext/fulltext.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // This module performs full-text search on the text content. 3 | // Author: Peter Širka 4 | 5 | const REG_KEYWORDS = /\s|\t|\n/; 6 | 7 | function _min(d0, d1, d2, bx, ay) { 8 | return d0 < d1 || d2 < d1 ? d0 > d2 ? d2 + 1 : d0 + 1 : bx === ay ? d1 : d1 + 1; 9 | } 10 | 11 | // License: MIT 12 | // Author: Gustaf Andersson 13 | // Source: https://github.com/gustf/js-levenshtein 14 | function levelstein(a, b) { 15 | 16 | if (a === b) 17 | return 0; 18 | 19 | if (a.length > b.length) { 20 | let tmp = a; 21 | a = b; 22 | b = tmp; 23 | } 24 | 25 | let la = a.length; 26 | let lb = b.length; 27 | 28 | while (la > 0 && (a.charCodeAt(la - 1) === b.charCodeAt(lb - 1))) { 29 | la--; 30 | lb--; 31 | } 32 | 33 | let offset = 0; 34 | 35 | while (offset < la && (a.charCodeAt(offset) === b.charCodeAt(offset))) 36 | offset++; 37 | 38 | la -= offset; 39 | lb -= offset; 40 | 41 | if (la === 0 || lb < 3) 42 | return lb; 43 | 44 | let x = 0; 45 | let y; 46 | let d0; 47 | let d1; 48 | let d2; 49 | let d3; 50 | let dd; 51 | let dy; 52 | let ay; 53 | let bx0; 54 | let bx1; 55 | let bx2; 56 | let bx3; 57 | let vector = []; 58 | 59 | for (y = 0; y < la; y++) { 60 | vector.push(y + 1); 61 | vector.push(a.charCodeAt(offset + y)); 62 | } 63 | 64 | var len = vector.length - 1; 65 | 66 | for (; x < lb - 3;) { 67 | bx0 = b.charCodeAt(offset + (d0 = x)); 68 | bx1 = b.charCodeAt(offset + (d1 = x + 1)); 69 | bx2 = b.charCodeAt(offset + (d2 = x + 2)); 70 | bx3 = b.charCodeAt(offset + (d3 = x + 3)); 71 | dd = (x += 4); 72 | for (y = 0; y < len; y += 2) { 73 | dy = vector[y]; 74 | ay = vector[y + 1]; 75 | d0 = _min(dy, d0, d1, bx0, ay); 76 | d1 = _min(d0, d1, d2, bx1, ay); 77 | d2 = _min(d1, d2, d3, bx2, ay); 78 | dd = _min(d2, d3, dd, bx3, ay); 79 | vector[y] = dd; 80 | d3 = d2; 81 | d2 = d1; 82 | d1 = d0; 83 | d0 = dy; 84 | } 85 | } 86 | 87 | for (; x < lb;) { 88 | bx0 = b.charCodeAt(offset + (d0 = x)); 89 | dd = ++x; 90 | for (y = 0; y < len; y += 2) { 91 | dy = vector[y]; 92 | vector[y] = dd = _min(dy, d0, dd, bx0, vector[y + 1]); 93 | d0 = dy; 94 | } 95 | } 96 | 97 | return dd; 98 | } 99 | 100 | function parsekeywords(search) { 101 | 102 | let keywords = search.split(REG_KEYWORDS); 103 | let output = []; 104 | 105 | for (let i = 0; i < keywords.length; i++) { 106 | let keyword = keywords[i]; 107 | if (keyword.trim()) { 108 | let kw = {}; 109 | kw.text = keyword; 110 | kw.ch = search.indexOf(keyword); 111 | kw.search = kw.text.toASCII().replace(/y/g, 'i'); 112 | output.push(kw); 113 | } 114 | } 115 | 116 | return output; 117 | } 118 | 119 | function FulltextResponse() {} 120 | 121 | FulltextResponse.prototype = { 122 | get count() { 123 | return this.items.length; 124 | }, 125 | get value() { 126 | let item = this.items[0]; 127 | return item ? item.value : null; 128 | }, 129 | get values() { 130 | let items = []; 131 | for (let m of this.items) 132 | items.push(m.value); 133 | return items; 134 | } 135 | }; 136 | 137 | FulltextResponse.prototype.or = function(opt) { 138 | let t = this; 139 | return t.items.length ? t : t.search(opt); 140 | }; 141 | 142 | FulltextResponse.prototype.set = function(path, arr) { 143 | let t = this; 144 | let val = arr ? t.values : t.value; 145 | if (path.includes('.')) 146 | U.set(t.parent.response, path, val); 147 | else 148 | t.parent.response[path] = val; 149 | return t; 150 | }; 151 | 152 | FulltextResponse.prototype.search = function(opt) { 153 | 154 | let t = this; 155 | let output = []; 156 | 157 | for (let m of t.items) { 158 | 159 | let filter = {}; 160 | for (let key in opt) 161 | filter[key] = opt[key]; 162 | 163 | var from = opt.from || opt.line; 164 | 165 | filter.line = m.line; 166 | 167 | if (typeof(from) === 'string') 168 | filter.line += from.parseInt(); 169 | 170 | output.push(filter); 171 | } 172 | 173 | if (output.length) 174 | return t.parent.search(output); 175 | 176 | let response = new FulltextResponse(); 177 | response.parent = t.parent; 178 | return response; 179 | }; 180 | 181 | FulltextResponse.prototype.next = function(fn) { 182 | let t = this; 183 | fn.call(t, t); 184 | return t; 185 | }; 186 | 187 | function Fulltext() { 188 | var t = this; 189 | t.response = {}; 190 | t.cache = {}; 191 | } 192 | 193 | function preparevalue(opt, meta, val) { 194 | 195 | if (opt.limit) 196 | val = val.substring(0, opt.limit); 197 | 198 | meta.raw = val; 199 | 200 | if (opt.match) { 201 | val = val.match(opt.match); 202 | if (!val) 203 | return 0; 204 | val = val[0]; 205 | } 206 | 207 | meta.matched = val; 208 | 209 | if (opt.clean) 210 | val = val.replace(opt.clean, ''); 211 | 212 | if (opt.trim === undefined || opt.trim == true) 213 | val = val.trim(); 214 | 215 | meta.cleaned = val; 216 | 217 | if (opt.convert) { 218 | switch (opt.convert) { 219 | case 'number': 220 | val = val.parseFloat2(); 221 | break; 222 | default: 223 | val = val.parseDate(opt.convert); 224 | break; 225 | } 226 | } 227 | 228 | meta.value = val; 229 | } 230 | 231 | function findkeywords(opt, line, keywords, number) { 232 | 233 | let output = []; 234 | let counter = 0; 235 | let from = 0; 236 | let start = null; 237 | let diffcount = null; 238 | let linekeywords = line.keywords.slice(0); 239 | let diff = opt.diff ? (opt.diff + 1) : 0; 240 | 241 | for (let k of keywords) { 242 | let index = linekeywords.findIndex(opt.strict ? 'text' : 'search', opt.strict ? k.text : k.search); 243 | if (index !== -1) { 244 | let tmp2 = linekeywords[index]; 245 | linekeywords.splice(index, 1); 246 | from = Math.max(from, tmp2.ch + tmp2.text.length); 247 | start = start == null ? tmp2.ch : Math.min(start, tmp2.ch); 248 | counter++; 249 | } else if (!opt.strict && diff) { 250 | // compare levelstein 251 | for (let j = 0; j < linekeywords.length; j++) { 252 | let k2 = linekeywords[j]; 253 | if (Math.abs(k2.search.length - k.search.length) < diff) { 254 | let len = Math.ceil((k2.search.length + k.search.length) / 100 * 20); 255 | if (levelstein(k2.search, k.search) < len) { 256 | let startindex = line.text.indexOf(k2.text); 257 | from = Math.max(startindex + k2.text.length); 258 | start = start == null ? startindex : Math.min(start, startindex); 259 | diffcount++; 260 | counter++; 261 | linekeywords.splice(j, 1); 262 | break; 263 | } 264 | } 265 | } 266 | } 267 | } 268 | 269 | if (counter === keywords.length) { 270 | // Between keywords are spaces 271 | from += counter - 1; 272 | let raw = opt.parse ? line.text.substring(from) : line.text; 273 | let meta = { line: number, text: line.text, context: line.text.substring(start), ch: from, levelstein: diffcount || 0 }; 274 | preparevalue(opt, meta, raw); 275 | output.push(meta); 276 | } 277 | 278 | return output.length ? output : null; 279 | } 280 | 281 | Fulltext.prototype.set = function(path, value) { 282 | var t = this; 283 | if (path.includes('.')) 284 | U.set(t.response, path, value); 285 | else 286 | t.response[path] = value; 287 | return t; 288 | }; 289 | 290 | Fulltext.prototype.load = function(value, separator = '\n') { 291 | 292 | let t = this; 293 | let lines = separator ? value.split(separator).trim() : [value.trim()]; 294 | var output = []; 295 | 296 | for (let line of lines) { 297 | 298 | let keywords = line.split(REG_KEYWORDS).trim(); 299 | let item = {}; 300 | 301 | item.text = line; 302 | item.keywords = []; 303 | 304 | for (let i = 0; i < keywords.length; i++) { 305 | let keyword = {}; 306 | keyword.text = keywords[i]; 307 | keyword.ch = line.indexOf(keyword.text); 308 | keyword.search = keyword.text.toASCII().replace(/y/g, 'i'); 309 | item.keywords.push(keyword); 310 | } 311 | 312 | output.push(item); 313 | } 314 | 315 | t.lines = output; 316 | 317 | return t; 318 | }; 319 | 320 | Fulltext.prototype.clear = function() { 321 | var t = this; 322 | t.cache = {}; 323 | t.response = {}; 324 | return t; 325 | }; 326 | 327 | Fulltext.prototype.search = function(opt) { 328 | 329 | if (typeof(opt) === 'string') 330 | opt = { search: opt }; 331 | 332 | let t = this; 333 | let output = []; 334 | let response = new FulltextResponse(); 335 | response.parent = t; 336 | 337 | if (opt instanceof Array) { 338 | for (let m of opt) { 339 | let tmp = t.search(m); 340 | if (tmp.items.length) 341 | output.push.apply(output, tmp.items); 342 | } 343 | response.items = output; 344 | return response; 345 | } 346 | 347 | if (!opt.from) 348 | opt.from = 0; 349 | 350 | if (opt.line) 351 | opt.from = opt.line; 352 | 353 | if (!opt.to) 354 | opt.to = t.lines.length; 355 | else if (typeof(opt.to) === 'string') 356 | opt.to = opt.from + opt.to.parseInt(); 357 | 358 | if (opt.parse === undefined) 359 | opt.parse = true; 360 | 361 | if (opt.diff == null) 362 | opt.diff = 2; 363 | 364 | if (opt.search instanceof RegExp) { 365 | for (let i = opt.from; i < opt.to; i++) { 366 | let line = t.lines[i]; 367 | if (line) { 368 | let match = line.text.match(opt.search); 369 | if (match) { 370 | let meta = { line: i, text: line.text, context: match[0] }; 371 | preparevalue(opt, meta, match[0]); 372 | output.push(meta); 373 | } 374 | } 375 | } 376 | 377 | response.items = output; 378 | return response; 379 | } 380 | 381 | let keywords = t.cache[opt.search]; 382 | if (keywords == null) 383 | keywords = t.cache[opt.search] = parsekeywords(opt.search); 384 | 385 | for (let i = opt.from; i < opt.to; i++) { 386 | let line = t.lines[i]; 387 | if (line) { 388 | let tmp = findkeywords(opt, line, keywords, i); 389 | if (tmp) { 390 | for (let m of tmp) 391 | output.push(m); 392 | } 393 | } 394 | } 395 | 396 | response.items = output; 397 | return response; 398 | }; 399 | 400 | exports.load = function(value) { 401 | 402 | var fulltext = new Fulltext(); 403 | 404 | // fulltext.search(opt) 405 | // opt.search {String/RegExp} 406 | // opt.clean {RegExp} 407 | // opt.match {RegExp} 408 | // opt.line {Number} or opt.from {Number} default: 0 409 | // opt.to {Number/String} 410 | // opt.diff {Number} default: 2 411 | // opt.trim {Boolean} default: true 412 | // opt.string {Boolean} default: false 413 | // opt.value {Boolean} 414 | // opt.limit {Number} max. chars for the obtaining value 415 | // returns response {FulltextResponse} 416 | 417 | // response.parent {FullText} 418 | // response.count {Number} returns a count of results 419 | // response.values {String/Number/Date Array} returns list of values 420 | // response.value {String/Number/Date} returns the first value 421 | // response.items[0].line {Number} line number 422 | // response.items[0].text {String} 423 | // response.items[0].ch {Number} char index 424 | // response.items[0].levelstein {Number} levelstein distance 425 | // response.items[0].raw {String} a raw found text 426 | // response.items[0].matched {String} a matched value 427 | // response.items[0].cleaned {String} a matched and cleaned value 428 | // response.items[0].value {String/Number/Date} a converted value 429 | // response.search(opt); // it continues with searching from the current line 430 | // response.next(fn); // it continues with searching from the current line 431 | // response.or(opt); // it's evaluated if the response doesn't contain "response" 432 | 433 | fulltext.load(value); 434 | 435 | return fulltext; 436 | }; -------------------------------------------------------------------------------- /fulltext/readme.md: -------------------------------------------------------------------------------- 1 | # Full-text search 2 | 3 | __EXPERIMENTAL__. This module can parse data according to the full-text search. It uses a great algorithm for finding pharses. 4 | 5 | __Requirements__: 6 | 7 | - Total.js v5 8 | 9 | __Example__: 10 | 11 | ```js 12 | var parsers = []; 13 | 14 | // Alza 15 | parsers.push(function(fulltext) { 16 | 17 | let count = fulltext.search({ search: 'Alza', parse: false }).count; 18 | if (!count) { 19 | fulltext.clear(); 20 | return; 21 | } 22 | 23 | let value = fulltext.search({ search: 'Dátum vystavenia:', match: /\d{2}\.\d{2}\.\d{4}/, clean: /\s/g, convert: 'dd.MM.yyyy' }).set('created').items; 24 | let first = value[0]; 25 | first && fulltext.set('customer.name', first.text.substring(first.text.indexOf(first.matched) + first.matched.length).trim()); 26 | fulltext.search({ search: 'Variabilný symbol:' }).set('vs'); 27 | fulltext.search({ search: 'Dátum splatnosti:', match: /\d{2}\.\d{2}\.\d{4}/, clean: /\s/g, convert: 'dd.MM.yyyy' }).set('due'); 28 | fulltext.search({ search: 'Predávajúci:' }).set('issuer.name').search({ search: 'IČO:', match: /\d+/, to: '+2' }).set('issuer.id').search({ search: 'IČ DPH:', match: /[A-Z0-9]+/, to: '+2' }).set('issuer.vatid'); 29 | fulltext.search({ search: 'Názov banky:' }).search({ search: 'IČO:', match: /\d+/, from: '+1', to: '+2' }).set('customer.id').search({ search: 'DIČ:', match: /[A-Z0-9]+/, to: '+2' }).set('customer.taxid').search({ search: 'IČ DPH:', match: /[A-Z0-9]+/, to: '+2' }).set('customer.vatid'); 30 | fulltext.search({ search: 'Celkom:' }).set('total').search({ search: /\d+(\s)?%/, from: '+1', to: '+1', clean: /\s/g }).set('tax'); 31 | return fulltext.response; 32 | }); 33 | 34 | // Invoice scan: 35 | var fulltext = MODS.fulltext.load(`Q alzas rnggemesenem (HHH 36 | 37 | zarucny a dodaci list 38 | Predévajúci: Alza.sk s.r.o. 39 | 40 | Sliagska 1/D, 83102 Bratislava-Nové Mesto, ICO: 36562939, IC DPH: SK2021863811, Web: www.alza sk, Kontakt: www.alza.sk/kontakt, Tel: 41 | +421 257 101 800 42 | 43 | OR MS Bratislava lll, oddiel Sro, vioZka &islo 35889/B 44 | 45 | Darfovy doklad: Faktira Kupujiici: 46 | Datum vystavenia: ) 20.02.2024 Total Avengers s.r. o. 47 | Datum uskut. dari. plnenia: 20.02.2024 } 48 | Datum splatnosti: 05.03.2024 Viestova 6784/28 49 | Datum prevzatia 21.02.2024 97401 Banska Bystrica 50 | Spésob uhrady: Terminalom 51 | Bankovy 52 | Nazov banky: Slovenska sporitel'ia, a.s. Bratislava @ v 53 | IBAN: SK2000000000005033845424 ICO: 50679996 DIC: 2120417167 54 | BIC/SWIET: GIBASKBX IC DPH: SK2120417167 55 | Nazov banky: ESOB SK E-mail: info@totalavengers.com 56 | IBAN: SK6875000000004001969241 Tl#421903163302 57 | BIC/SWIFT: CEKOSKBX 58 | Variabilny symbol: 5012660451 59 | Kod Popis Ks Cena ks bez DPH DPH DPH% Cena Zaruka 60 | RZ351b Digitalny fotoaparat RICOH GR III Cierny 1 759,00 759,00 151,80 20 910,80 24 PX 61 | SL190r ~~ Nehmotny produkt Doprava tovaru / Na 1 217 217 0,43 20 2,60 0AL 62 | dobierku / Doruéenie na predajiiu 63 | SL083d4 Nehmotny produkt ZI'ava na dopravu 1 -2,17 -217 -0,43 20 -2,60 0AL 64 | JO194a9 +ZADARMO Stativ JOBY 1 0,00 0,00 0,00 0 0,00 0 65 | Celkom: 910,80 EUR 66 | Vyeislenie DPH v EUR: I 67 | Sadzba Zéklad DPH Zaoknihlenie: 250 EUR 68 | Celkom: 910,80 EUR 69 | 20% 759,00 151,80 70 | 0% 0,00 0,00 71 | Nehrad'te, zaplatené kartou. 72 | Identifikacia produktu: 73 | RZ351b: SN: 0141189300175, 74 | Recyklacny prispevok za ks: 75 | RZ351b: 0,06 EUR 76 | 77 | Odporicame tovar skontrolovat ihned’ po prevzati, neskorsie pripomienky k stavu odovzdaného tovaru mézu byt zamietnuté. Kupujci 78 | ziskava Viastnické prava k tovaru aZ po uplnom zaplateni kupnej ceny. V cene tovaru je zahmuty recyklacny poplatok a autorské odmeny v 79 | uzékonenej vyske. Viac informécii o podmienkach zaruky najdete vo VSeobecnych obchodnych podmienkach a Reklamacnom poriadku na 80 | www.alza. sk. 81 | 82 | Sluzby, ktoré ste zabudli kupit’ 83 | 84 | Typ sluzby: Mozno zakiipit’ 85 | Digitalni fotoaparat Ricoh GR Ill éerny: 86 | 87 | - sluzba Poji§téni prodlouzené zaruky 24 mésicli 138,14 € 05.03.2024 88 | - sluzba Zaruka okamzité vymény 109,30 € 05.03.2024 89 | - sluzba Zaruka okamzité vymény 109,30 € 05.03.2024 90 | - sluzba Poji§téni prodlouzené zaruky 12 mésicl 9184 € 05.03.2024 91 | 92 | Oslovte, prosim, obchodnikov v zelenych trickach, s vyberom tovaru Vam radi pomézu 93 | 94 | Ochranny znak El Strana 1z 2 Tlac: PDFGen 1.39 2/21/2024 8:50:33 AM 95 | 02G9686VD1C2A4A244W24A233E538232B5F 25 96 | 97 | Alzakariéra 98 | Paci sa vam Alza? Pridte pracovat pre nas! Pozrite sa na nase volné pracovné miesta. 99 | https:/fiwww.alza. sk/kariera 100 | 101 | Ochranny znak El Strana 2z 2 Tlac: PDFGen 1.39 2/21/2024 8:50:33 AM 102 | 02G9686VD1C2A4A244W24A233E538232B5F 25`); 103 | 104 | for (let parser of parsers) { 105 | var output = parser(fulltext); 106 | if (output) { 107 | console.log(output); 108 | break; 109 | } 110 | } 111 | ``` 112 | 113 | __Output__: 114 | 115 | ```js 116 | { 117 | created: 2024-02-20T00:00:00.000Z, 118 | customer: { 119 | name: 'Total Avengers s.r. o.', 120 | id: '50679996', 121 | taxid: '2120417167', 122 | vatid: 'SK2120417167' 123 | }, 124 | vs: '5012660451', 125 | due: 2024-03-05T00:00:00.000Z, 126 | issuer: { name: 'Alza.sk s.r.o.', id: '36562939', vatid: 'SK2021863811' }, 127 | total: '910,80 EUR', 128 | tax: '20%' 129 | } 130 | ``` -------------------------------------------------------------------------------- /helpdesk/helpdesk.package: -------------------------------------------------------------------------------- 1 | /default.css :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 2 | /default.js :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 3 | /index.html :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 4 | /index.js :H4sIAAAAAAAAE61X727bNhD/LD8FC2yVhHhM3LUbYDddi7jBAiRLF7efgiCQJdpmIosqSTnx0rzLXmbvtTuSkmjXTppuX2zq7nh/fvdHp91dcnL0kRzzlBWKdXZ3yYEol5JPZ5p8YJpJ8s/fXF4n5HWJTwrPb6fzhOc0FfM3nc4ikeT0w8eBOQyHpyOyT+7uB50Ouy2F1Iou8JYogBwuenSP7oWDhscLpZM8B96kKlINYpEo8U/F5K7TCcAd90wlU1pyI6NA/jzsvfgVtdFeeDFYkawkKgx3f5ixvMyYut4NVwXSSmkxh9BA6v3Ju6Nj8m44PHs/Gq3JqapENx8TSyo9Mx61CPV7L35++eoX9KwTADrAdtJI4BMSPQNq3Akc0wCGZ+f8J1omehbVhC9fVsOJBxabM1FpppxGFLau7O8TLSuGEAbBIZUoVevqkgVnN5e8yNhtF3zGG0Lyv1h4AVo3iF8pUVzqRF0rFL+dybBLnnLrMskyvFkKpTdcvScsV+wxT7/Vtad5Yx2wUJ4kZcmLaQcuz5OyQX6HhBmbJFWuaaoU+v+2SYPPQE1bb15tuXilbCYPKdR9qJhcQBeCqDsBz++jqtjQLSbHtoaKKs/BC9eC7gmgYHOxYMdcaVYwud1MrbImR6moCqhnYwHryz2TH0nvlamxPaxfr+PvPSVt6qJGAVdnrodZFukZVzEqkExXsgBXkQLeQma4FnLpOsHBWfNRb+RBaUwgir7xNt3fbPyP09Gfx1FY6w1jOgHVUUwVYB+FWaJZKhn8ZgCcaS6aQirGSXpttLVP8VZnsPYiO9ce96gT4DxVLJ8ACMgdWMpcZAxxQQ4di2zp6NkYiF9F4ZjewDP3PlcMAG6pkMywF9ajyZigelkyy4GyY1KHdpz4TFIB+oATy7Dr0JAYX7liCAI4U57hMDsaRnFN8ZAE1qF51nzOaj6GBAxrxsVnGPC2mbNCm9l/UROVyBdG0SSBKdJYLetAedlcbyGojxhvkI2pjS8CKS+pqx0WBEYd5jIafTo4gJdAZKrAxBU0yp8/J4XQfLKMatS6xOLQbcyaK/em8d3w2wS6Cze09YJuggQqviM1Ev12DCyS3PkJJ1pWagZyHtR9D+kuQVj7HsRdwst+DVjraL+tm3sbpy1OAjZMDPcxvZkxySBYbAurkGffi2KNXB16q/Gp4GkxneYsjNeAs9XSJ7CLvCHPiq5BqCb6AHlwkMeDhOBA37bQ0OOtjtrh/F9yrcoch3UzDqh7s/e+M2OPBvMg8DaeGnj7FB2aZQbhTcaJYqodUJdWIqOFUJ/zMP6foO64QtMzKW5e7u1FayN5ZfKmotBS5Ll9021bpZrJXE+aNUFo/PpMc1ZM9czmCIdipezYaQzRcZLYiYg6kE3ZvNRL3PMaLSaPpxPLNipgo+iH8GsoZaLUjZBZbHz8qedqx2jEd/K5Z46XFxiAfVmvMXZ2TJ042DaKgO+9wUPKyRtYCawFj+HQ79lIWwtrOIQjllYShngCo8ruQw3WCL2pW7um1Xvz6rcAgPYVsc7BhrRNhCQRpoVDXHtdYiXdprFBxwAEX5PmvLNjE2s3Ig+FJmHres75hZ+kB0HaHPdqCH4lu5GJDQhTy5+TTS37lKCF0H3bxK3VwUpnG6Hm7spisnVX8psX23bCpdLRptcBkxKmr0hbp4BiMonEFZ8CoNT7Bv7ZBR8/QJvQyG/EC4n0ie97FyY7AD7hU1okc2a6iPwOvg/BdwsgTxP0anVJRwuhdRKRwRHyL2aswHAsDwAA 5 | /mail.html :H4sIAAAAAAAAE71VyW7bMBA9O1/BKmhtF1GsJEUXWTIcNClSIEsP6SFHShxbbERSIOmthv+l39Iv61C0FbsJCvhS6SAuM/PeG85QyauLu8/3D98uSWFFOThINh+gbHDQSiy3JQwub86/XpPr84e77/dJz6/hpgBLSV5QbcCmwcSOwo8B6TU7kgpIgymHWaW0DUiupAWJljPObJEymPIcwnry3G2ktKA2ZGAht1zJLXcLJVSFkpBKtZcjo3ZvH8qYBmP2dQNBefnkZOyiBGIXFTj6c9vLjQlwo/WWLMkIncIRFbxcxCS4gnIKlueU3MIEgqOtFZw04yNyrjktj4ih0oQGNB/1iaB6zGUc9UmFvLkcu+EKYTLFFogUziB75AjmEI1QyhbOiFBpMRanBli/MXI8Q8N/QkjZj4mxscSU90kBfFzYmJxE0es+yWj+ONZqIlmYq1LpmMwKbqFGTXq1bKymni+npKZRL6ZBw7Wh6hKSMD7dWDyLffglcm/jcRpV8/529uqU+BVHPD55hwYboDrfiaUZngSKHcs0yPGsQAd/UyJ0YhX6zX1xxu+jBugprN9yWagDY2Rdf3HA/GBHzIbzmQv1TJlPWqY0Ax1qyvjExA7E7441XfRLLiFcJ//kYzVHVI/SGi75iAjFoDx2FUbSNCVtLrEmbHu1ttnh8qSkzqCXHWbKWiVQ0UbrzINlqmRrIlmJxIPBm8NPH87O+mTYuYUZ4cZMgHDparO6APPYTXoINthGHgyXnqCrgNXO/nA54qt/SsmVEHhQ/0PLGmo/Ba0k03WbN4KUBpoXRLiseK91YPOihvW5W1VhW1VzYlTJGTm8jNzbVLsTQ6LtMjx1VZBkmzA7upDusbvvcqRigR37u6rTHnYW+IQ3NyFj5OoqFqLb7qKebOBzf5xjqysBevX717DT2cy63fqcvFIX+4U0DJcg2eqFY90yw7+H7w4c1P2CX9eRrvW9FVLB0PWl4X5FfwC+di/KoQYAAA== 6 | -------------------------------------------------------------------------------- /helpdesk/make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | NAME=`basename "$PWD"`.package 4 | 5 | cd source 6 | tpm create "$NAME" 7 | mv "$NAME" ../ -------------------------------------------------------------------------------- /helpdesk/readme.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | __Unmaintained right now__. This is a simple __HelpDesk__ mini application. 4 | 5 | - you and your customer can create unlimited count of issues (NoSQL embedded limit) 6 | - __admin mode__ is enabled when you perform a double-click on the `bug` icon 7 | 8 | --- 9 | 10 | - download and copy `helpdesk.package` into the `/packages/` directory __or create a definition with:__ 11 | 12 | ```javascript 13 | var options = {}; 14 | 15 | // ==================================== 16 | // COMMON (OPTIONAL) 17 | // ==================================== 18 | 19 | // options.url = '/$helpdesk/'; 20 | 21 | // ==================================== 22 | // Security (OPTIONAL) 23 | // ==================================== 24 | 25 | // options.auth = ['admin:admin', 'name:password']; 26 | // options.auth = true; // HelpDesk uses "authorize" flag 27 | // options.restrictions = ['127.0.0.1', '138.201', '172.31.33']; 28 | 29 | // ==================================== 30 | // Email notifications (OPTIONAL) 31 | // ==================================== 32 | 33 | // options.customer = 'EMAIL ADDRESS'; 34 | // options.support = 'EMAIL ADDRESS'; 35 | 36 | INSTALL('package', 'https://cdn.totaljs.com/helpdesk.package', options); 37 | 38 | // OR UpToDate mechanism: 39 | UPTODATE('package', 'https://cdn.totaljs.com/helpdesk.package', options, '1 week'); 40 | ``` -------------------------------------------------------------------------------- /helpdesk/source/default.css: -------------------------------------------------------------------------------- 1 | /*auto*/ 2 | 3 | body { background-color: #E6E9ED; font-smoothing: antialiased; height: 100%; } 4 | 5 | #columns { column-width: 320px; column-gap: 20px; width: 100%; margin: 30px auto; } 6 | #columns figure { width: 100%; background: white; border: 2px solid #fcfcfc; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); margin: 0 2px 20px; padding: 15px; transition: opacity .4s ease-in-out; display: inline-block; column-break-inside: avoid; border-radius: 4px; } 7 | 8 | .operations { list-style-type: none; margin: 0 0 10px 0; padding: 0; } 9 | .operations li { position: relative; display: inline-block; cursor: pointer; cursor: pointer; color: #A0A0A0; font-size: 14px; border: 1px solid #E0E0E0; text-align: center; width: 30px; margin-right: 10px; border-radius: 4px; padding: 3px 0; background-color: white; } 10 | .operations li:hover { border-color: #D0D0D0; color: black; } 11 | 12 | .comments { list-style-type: none; margin: 10px 0 0; padding: 0; } 13 | .comments li:first-child { border-top-left-radius: 4px; border-top-right-radius: 4px; } 14 | .comments li:last-child { border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; } 15 | .comments li { font-size: 11px; border-top: 1px solid #E0E0E0; padding: 5px 8px; background-color: #FFF8D1; } 16 | .comments .customer { background-color: #F0F0F0; } 17 | .comments li span { color: gray; } 18 | .comments li div { color: black; line-height: 13px; } 19 | .comments li:first-child { border-top: 0; } 20 | .comments .fa-times { display: none; } 21 | .superadmin .comments li .fa-times { display: inline-block; color: red; font-size: 11px; margin-right: 5px; cursor: pointer; } 22 | 23 | #columns .solved { background-color: #8CC152; color: white; } 24 | #columns .solved .operations .fa-thumbs-up { color: #8CC152; } 25 | #columns .created { font-size: 12px; color: #A0A0A0; margin-bottom: 5px; } 26 | #columns .created .fa { margin-right: 5px; } 27 | #columns .solved .created { color: #d1eab4; } 28 | #columns p { line-height: 16px; margin-top: 0; } 29 | .solved .comments .customer { background-color: white; } 30 | .solved p { text-shadow: 1px 1px 1px rgba(0,0,0,0.1); } 31 | 32 | .commentform { background-color: white; padding: 5px; border-radius: 4px; margin: 15px 0 0; border: 1px solid #E0E0E0; } 33 | .commentform input { width: 100%; border: 0; background-color: transparent; outline: 0; font-size: 12px; color: black; } 34 | .commentform > div { margin-left: 40px; } 35 | .commentform > span { float: left; width: 30px; text-align: center; border-right: 1px solid #E0E0E0; color: gray; padding-right: 3px; } 36 | .superadmin .commentform { border-color: #DA858D; } 37 | .superadmin .commentform > span { border-color: #DA858D; color: #DA4453; } 38 | 39 | .taskform-sa span { color: #DA4453; } 40 | .taskform { background-color: white; padding: 5px; border-radius: 4px; margin: 20px 0 0; padding: 2px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } 41 | .taskform input { width: 100%; border: 0; background-color: transparent; outline: 0; font-size: 16px; padding: 8px 0; } 42 | .taskform > div { margin-left: 60px; } 43 | .taskform > span { float: left; width: 50px; text-align: center; border-right: 1px solid #E0E0E0; font-size: 20px; padding: 5px 0; } 44 | 45 | .ui-confirm { background-color: rgba(255,255,255,0.9); position: fixed; left: 0; right: 0; bottom: 0; top: 0; z-index: 100; width: 100%; display: table; height: 100%; transition: 0.5s all; opacity: 0; } 46 | .ui-confirm > div { display: table-cell; vertical-align: middle; text-align: center; color: white; padding: 15px; } 47 | .ui-confirm-body { padding: 30px; position: relative; display: inline-block; border-radius: 4px; max-width: 600px; text-align: left; transform: scale(0.5); transition: 0.3s all; color: gray; border: 1px solid #E0E0E0; background-color: white; box-shadow: 0 0 30px rgba(0,0,0,0.1); } 48 | .ui-confirm-message { border-bottom: 1px solid #F0F0F0; padding: 0 0 20px 0; margin-bottom: 20px; } 49 | .ui-confirm button { font-size: 12px; position: relative; display: inline-block; cursor: pointer; outline: 0; background-color: #E0E0E0; border: 0; border-radius: 4px; color: gray; padding: 0 20px; height: 30px; font-weight: bold; margin: 0 10px 0 0; } 50 | .ui-confirm button:hover { background-color: #E5E5E5; } 51 | .ui-confirm-visible { opacity: 1; } 52 | .ui-confirm-visible .ui-confirm-body { -webkit-transform: scale(1); transform: scale(1); } 53 | .ui-confirm-click { transform: scale(1.07) !important; } 54 | .hidden { display: none; } 55 | 56 | .smiles { background-image: url(https://cdn.totaljs.com/smilefy.png); width: 18px; height: 18px; position: relative; display: inline-block; vertical-align: middle; margin-top: -1px; } 57 | .smiles-0 { background-position: 0 0; } 58 | .smiles-1 { background-position: -18px 0; } 59 | .smiles-2 { background-position: -36px 0; } 60 | .smiles-3 { background-position: -54px 0; } 61 | .smiles-4 { background-position: -72px 0; } 62 | .smiles-5 { background-position: -90px 0; } 63 | .smiles-6 { background-position: -108px 0; } 64 | .smiles-7 { background-position: -126px 0; } 65 | .smiles-8 { background-position: -144px 0; } 66 | .smiles-9 { background-position: -162px 0; } 67 | .smiles-10 { background-position: -180px 0; } 68 | .smiles-11 { background-position: -198px 0; } 69 | .smiles-12 { background-position: -216px 0; } 70 | 71 | .ui-empty { padding: 50px 0; text-align: center; font-size: 14px; max-width: 500px; margin: 90px auto; background-color: white; border-radius: 4px; } 72 | .ui-empty .fa { display: block; margin-bottom: 10px; color: #67B13D; } 73 | .hidden { display: none; } 74 | .ml5 { margin-left: 5px; } 75 | 76 | @media(max-width: 767px) { 77 | .ui-empty { margin: 25px auto; } 78 | .ui-confirm button { display: block; margin: 10px 0 0; display: block; width: 100%; } 79 | } 80 | 81 | @media (max-width: 750px) { 82 | #columns { column-gap: 0; } 83 | #columns figure { width: 100%; } 84 | } -------------------------------------------------------------------------------- /helpdesk/source/default.js: -------------------------------------------------------------------------------- 1 | var url = location.pathname + '?customer=1'; 2 | 3 | $(document).ready(function() { 4 | SCHEDULE('repeater', 'manually', '5 minutes', tasks_refresh); 5 | CACHE('helpdesk.customer') && set_superadmin(); 6 | tasks_refresh(); 7 | }); 8 | 9 | function tasks_refresh() { 10 | AJAX('GET ' + url, function(response) { 11 | response.sort(function(a, b) { 12 | if (a.solved && !b.solved) 13 | return 1; 14 | if (!a.solved && b.solved) 15 | return -1; 16 | if (a.solved && b.solved) 17 | return a.datesolved > b.datesolved ? -1 : 1; 18 | return +a.id.substring(0, 14) > +b.id.substring(0, 14) ? -1 : 1; 19 | }); 20 | SET('tasks.items', response); 21 | }); 22 | } 23 | 24 | $(document).on('keydown', '.enter', function(e) { 25 | if (e.keyCode !== 13) 26 | return; 27 | var val = this.value; 28 | if (this.name === 'newtask') { 29 | this.value = ''; 30 | val && AJAX('POST ' + url, { type: 'insert', body: val }, tasks_refresh); 31 | } else if (this.name === 'comment') { 32 | this.value = ''; 33 | val && AJAX('POST ' + url, { id: $(this).closest('figure').attr('data-id'), type: 'comment', body: val }, tasks_refresh); 34 | } 35 | }); 36 | 37 | $(document).on('dblclick', '.fa-bug', set_superadmin); 38 | 39 | function set_superadmin() { 40 | var is = $('.taskform').toggleClass('taskform-sa').hasClass('taskform-sa'); 41 | var u = location.pathname; 42 | if (is) 43 | url = u; 44 | else 45 | url = u + '?customer=1'; 46 | $(document.body).toggleClass('superadmin', is); 47 | CACHE('helpdesk.customer', is, '1 year'); 48 | } 49 | 50 | COMPONENT('repeater', function() { 51 | 52 | var self = this; 53 | var recompile = false; 54 | 55 | self.readonly(); 56 | 57 | self.make = function() { 58 | var element = self.find('script'); 59 | 60 | if (!element.length) { 61 | element = self.element; 62 | self.element = self.element.parent(); 63 | } 64 | 65 | var html = element.html(); 66 | element.remove(); 67 | self.template = Tangular.compile(html); 68 | recompile = html.indexOf('data-jc="') !== -1; 69 | }; 70 | 71 | self.setter = function(value) { 72 | 73 | if (!value || !value.length) { 74 | self.empty(); 75 | return; 76 | } 77 | 78 | var builder = []; 79 | for (var i = 0, length = value.length; i < length; i++) { 80 | var item = value[i]; 81 | item.index = i; 82 | builder.push(self.template(item).replace(/\$index/g, i.toString()).replace(/\$/g, self.path + '[' + i + ']')); 83 | } 84 | 85 | self.html(builder); 86 | recompile && jC.compile(); 87 | }; 88 | }); 89 | 90 | COMPONENT('confirm', function() { 91 | var self = this; 92 | var is = false; 93 | 94 | self.readonly(); 95 | self.singleton(); 96 | 97 | self.make = function() { 98 | self.toggle('ui-confirm hidden', true); 99 | self.element.on('click', 'button', function() { 100 | self.hide($(this).attr('data-index').parseInt()); 101 | }); 102 | 103 | self.element.on('click', function(e) { 104 | if (e.target.tagName !== 'DIV') 105 | return; 106 | var el = self.element.find('.ui-confirm-body'); 107 | el.addClass('ui-confirm-click'); 108 | setTimeout(function() { 109 | el.removeClass('ui-confirm-click'); 110 | }, 300); 111 | }); 112 | }; 113 | 114 | self.confirm = function(message, buttons, fn) { 115 | self.callback = fn; 116 | 117 | var builder = []; 118 | 119 | buttons.forEach(function(item, index) { 120 | builder.push(''.format(item, index)); 121 | }); 122 | 123 | self.content('ui-confirm-warning', '
{0}
{1}'.format(message.replace(/\n/g, '
'), builder.join(''))); 124 | }; 125 | 126 | self.hide = function(index) { 127 | self.callback && self.callback(index); 128 | self.element.removeClass('ui-confirm-visible'); 129 | setTimeout2(self.id, function() { 130 | self.element.addClass('hidden'); 131 | }, 1000); 132 | }; 133 | 134 | self.content = function(cls, text) { 135 | !is && self.html('
'); 136 | self.element.find('.ui-confirm-body').empty().append(text); 137 | self.element.removeClass('hidden'); 138 | setTimeout2(self.id, function() { 139 | self.element.addClass('ui-confirm-visible'); 140 | }, 5); 141 | }; 142 | }); 143 | 144 | COMPONENT('empty', function() { 145 | 146 | var self = this; 147 | 148 | self.readonly(); 149 | 150 | self.make = function() { 151 | self.element.addClass('ui-empty'); 152 | }; 153 | 154 | self.setter = function(value) { 155 | self.element.toggleClass('hidden', value && value.length ? true : false); 156 | }; 157 | }); 158 | 159 | function urlify(str) { 160 | return str.replace(/(((https?:\/\/)|(www\.))[^\s]+)/g, function(url, b, c) { 161 | var len = url.length; 162 | var l = url.substring(len - 1); 163 | if (l === '.' || l === ',') 164 | url = url.substring(0, len - 1); 165 | else 166 | l = ''; 167 | url = c === 'www.' ? 'http://' + url : url; 168 | return '' + url + '' + l; 169 | }); 170 | } 171 | 172 | function mailify(str) { 173 | return str.replace(/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/g, function(m) { 174 | var len = m.length; 175 | var l = m.substring(len - 1); 176 | if (l === '.' || l === ',') 177 | m = m.substring(0, len - 1); 178 | else 179 | l = ''; 180 | return '' + m + '' + l; 181 | }); 182 | } 183 | 184 | function smilefy(text) { 185 | var db = { ':-)': 1, ':)': 1, ';)': 8, ':D': 0, '8)': 5, ':((': 7, ':(': 3, ':|': 2, ':P': 6, ':O': 4, ':*': 9, '+1': 10, '1': 11, '\/': 12 }; 186 | return text.replace(/(\-1|[:;8O\-)DP(|\*]|\+1){1,3}/g, function(match) { 187 | var smile = db[match.replace('-', '')]; 188 | return smile == undefined ? match : ''; 189 | }); 190 | } 191 | 192 | Tangular.register('body', function(val) { 193 | return urlify(mailify(smilefy(val))); 194 | }); -------------------------------------------------------------------------------- /helpdesk/source/index.html: -------------------------------------------------------------------------------- 1 | @{layout('')} 2 | 3 | 4 | 5 | 6 | HelpDesk: @{config.name} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 | 22 |
23 | 24 |
25 |
26 | 27 | 28 | 29 |
30 | 51 |
52 |
53 | 54 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /helpdesk/source/index.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright Peter Širka 3 | 4 | var OPT; 5 | var DDOS = {}; 6 | 7 | exports.version = 'v1.0.0'; 8 | exports.install = function(options) { 9 | 10 | // options.restrictions = ['127.0.0.1']; 11 | // options.url = '/$helpdesk/'; 12 | // options.customer = 'EMAIL ADDRESS'; 13 | // options.support = 'EMAIL ADDRESS'; 14 | // options.auth = ['petersirka:123456']; 15 | 16 | OPT = options; 17 | 18 | if (!OPT) 19 | OPT = {}; 20 | 21 | OPT.url = U.path(OPT.url || '/$helpdesk/'); 22 | 23 | // Routes 24 | 25 | if (OPT.auth === true) { 26 | F.route(OPT.url, view_index, ['authorize']); 27 | F.route(OPT.url, json_tasks, ['xhr', 'authorize']); 28 | F.route(OPT.url, json_tasks_add, ['post', 'authorize']); 29 | } else { 30 | F.route(OPT.url, view_index); 31 | F.route(OPT.url, json_tasks, ['xhr']); 32 | F.route(OPT.url, json_tasks_add, ['post']); 33 | } 34 | 35 | // Mapping 36 | F.map(OPT.url + 'default.css', '@helpdesk/default.css'); 37 | F.map(OPT.url + 'default.js', '@helpdesk/default.js'); 38 | 39 | F.on('service', service); 40 | }; 41 | 42 | exports.uninstall = function() { 43 | OPT = null; 44 | DDOS = null; 45 | F.removeListener('service', service); 46 | }; 47 | 48 | function service(counter) { 49 | if (counter % 15 === 0) 50 | DDOS = {}; 51 | } 52 | 53 | function view_index() { 54 | if (isRestricted(this)) 55 | return; 56 | this.repository.url = OPT.url; 57 | this.view('@helpdesk/index'); 58 | } 59 | 60 | function json_tasks() { 61 | if (isRestricted(this)) 62 | return; 63 | NOSQL('helpdesk').find().sort('datecreated', true).callback(this.callback()); 64 | } 65 | 66 | function json_tasks_add() { 67 | 68 | if (isRestricted(this)) 69 | return; 70 | 71 | var self = this; 72 | var model = self.body; 73 | var db = NOSQL('helpdesk'); 74 | var customer = self.query.customer === '1'; 75 | 76 | if (model.type === 'insert') { 77 | model.type = undefined; 78 | var obj = {}; 79 | obj.id = UID(); 80 | obj.datecreated = F.datetime; 81 | obj.body = model.body; 82 | obj.comments = []; 83 | obj.solved = false; 84 | obj.ip = self.ip; 85 | obj.customer = customer; 86 | 87 | db.insert(obj).callback(function() { 88 | self.json(SUCCESS(true)); 89 | customer && notify('insert', obj.id, customer); 90 | }); 91 | 92 | } else if (model.type === 'comment') { 93 | 94 | db.modify({ comments: function(val) { 95 | val.push({ datecreated: F.datetime, body: model.body, ip: self.ip, customer: customer }); 96 | return val; 97 | }}).where('id', model.id).callback(function() { 98 | self.json(SUCCESS(true)); 99 | notify('comment', model.id, customer); 100 | }); 101 | 102 | } else if (model.type === 'toggle') 103 | db.modify({ solved: n => !n, datesolved: F.datetime, ip: self.ip }).where('id', model.id).callback(() => self.json(SUCCESS(true))); 104 | else if (model.type === 'removecomment') { 105 | 106 | db.modify({ comments: function(val) { 107 | val.splice(self.body.index, 1); 108 | return val; 109 | }}).where('id', model.id).callback(() => self.json(SUCCESS(true))); 110 | 111 | } else if (model.type === 'remove') 112 | db.remove(F.path.databases('helpdesk_removed.nosql')).where('id', model.id).callback(() => self.json(SUCCESS(true))); 113 | else 114 | self.throw400(); 115 | } 116 | 117 | function isRestricted(controller) { 118 | 119 | if (OPT.auth === true) 120 | return false; 121 | 122 | if (OPT.auth && OPT.auth.length) { 123 | var user = controller.baa(); 124 | if (user.empty || OPT.auth.indexOf(user.user + ':' + user.password) === -1) { 125 | 126 | if (DDOS[controller.ip]) 127 | DDOS[controller.ip]++; 128 | else 129 | DDOS[controller.ip] = 1; 130 | 131 | if (DDOS[controller.ip] > 15) 132 | controller.throw401(); 133 | else 134 | controller.baa('Secured area'); 135 | 136 | return true; 137 | } 138 | } 139 | 140 | if (!OPT.restrictions || !OPT.restrictions.length) 141 | return false; 142 | 143 | for (var i = 0, length = OPT.restrictions.length; i < length; i++) { 144 | if (controller.ip.indexOf(OPT.restrictions[i]) === -1) { 145 | controller.throw401(); 146 | return true; 147 | } 148 | } 149 | 150 | return false; 151 | } 152 | 153 | function notify(type, id, customer) { 154 | 155 | if (customer) { 156 | if (!OPT.support) 157 | return; 158 | } else if (!OPT.customer) 159 | return; 160 | 161 | NOSQL('helpdesk').find().where('id', id).first().callback(function(err, doc) { 162 | if (err || !doc) 163 | return; 164 | doc.type = type; 165 | F.mail(customer ? OPT.support : OPT.customer, F.config.name + ': HelpDesk notification', '@helpdesk/mail', doc); 166 | }); 167 | } -------------------------------------------------------------------------------- /helpdesk/source/mail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | EMAIL LAYOUT 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 |
18 | 19 | 20 | 39 | 40 |
21 |
22 | 23 | @{if model.type === 'insert'} 24 |
★ @(New issue in HelpDesk)
25 |
@{model.body}
26 | @{fi} 27 | 28 | @{if model.type === 'comment'} 29 |
★ @(New comment)
30 |
@{model.body}
31 |
32 | @{foreach m in model.comments} 33 |
@{m.datecreated.format('@(yyyy-MM-dd HH:mm)')}@{if m.customer} @((customer))@{fi}
@{m.body}
34 | @{end} 35 | @{fi} 36 | 37 |
38 |
41 |
42 | 43 | -------------------------------------------------------------------------------- /jsonwebtoken/jsonwebtoken.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright Peter Širka 3 | 4 | exports.version = 'v1.0.0'; 5 | 6 | var jwt = require('jsonwebtoken'); 7 | 8 | F.encrypt = function(value, key) { 9 | return jwt.sign(value, key); 10 | }; 11 | 12 | F.decrypt = function(value, key) { 13 | try { 14 | return jwt.verify(value, key); 15 | } catch (e) { 16 | return null; 17 | } 18 | }; -------------------------------------------------------------------------------- /jsonwebtoken/readme.md: -------------------------------------------------------------------------------- 1 | # JsonWebToken (JWT) implementation for Total.js 2 | 3 | - `npm install jsonwebtoken` 4 | - download and copy `jsonwebtoken.js` into the `/modules/` directory __or create a definition with:__ 5 | 6 | ```javascript 7 | INSTALL('module', 'https://modules.totaljs.com/latest/jsonwebtoken.js'); 8 | ``` 9 | 10 | __Usage__: 11 | 12 | ```javascript 13 | var obj = { name: 'total.js' }; 14 | 15 | // Encrypt 16 | var encoded = F.encrypt(obj, 'KEY'); 17 | console.log('encoded', encoded); 18 | 19 | // Decrypt 20 | var decoded = F.decrypt(encoded, 'KEY')); 21 | console.log('decoded', decoded); 22 | ``` 23 | -------------------------------------------------------------------------------- /modules.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "category": "Security", 4 | "name": "OAuth 2.0", 5 | "version": "1.4.0", 6 | "total": "2.0.0", 7 | "description": "Very simple OAuth 2.0 implemenation of GitHub, Facebook, Google, DropBox, LinkedIn, Windows Live, Yahoo, Yandex and Instagram.", 8 | "author": "Peter Širka", 9 | "url": "https://github.com/totaljs/modules/tree/master/oauth2", 10 | "datecreated": "2016-01-01T00:00:00.000Z", 11 | "dateupdated": null 12 | }, 13 | { 14 | "category": "Security", 15 | "name": "DDOS Protection", 16 | "version": "1.1.0", 17 | "total": "2.0.0", 18 | "description": "Simple DDOS protection, the module counts requests according to IP address.", 19 | "author": "Peter Širka", 20 | "url": "https://github.com/totaljs/modules/tree/master/Security/ddos", 21 | "datecreated": "2016-01-01T00:00:00.000Z", 22 | "dateupdated": null, 23 | "deprecated": true 24 | }, 25 | { 26 | "category": "Security", 27 | "name": "JsonWebToken (JWT)", 28 | "version": "1.0.0", 29 | "total": "2.0.0", 30 | "description": "This JWT module rewrites internal encryption/decryption mechanism.", 31 | "author": "Peter Širka", 32 | "url": "https://github.com/totaljs/modules/tree/master/jsonwebtoken", 33 | "datecreated": "2016-01-01T00:00:00.000Z", 34 | "dateupdated": null 35 | }, 36 | { 37 | "category": "Security", 38 | "name": "Protection", 39 | "version": "1.0.0", 40 | "total": "2.0.0", 41 | "description": "The module increments internal counter identified by a key and resets the the counter at a given timeout.", 42 | "author": "Peter Širka", 43 | "url": "https://github.com/totaljs/modules/tree/master/Security/protection", 44 | "datecreated": "2016-01-01T00:00:00.000Z", 45 | "dateupdated": null, 46 | "deprecated": true 47 | }, 48 | { 49 | "category": "Security", 50 | "name": "Auth", 51 | "version": "3.0.0", 52 | "total": "2.0.0", 53 | "description": "Adds a support for authorize and unauthorize route flag. Each logged user has an own session.", 54 | "author": "Peter Širka", 55 | "url": "https://github.com/totaljs/modules/tree/master/Security/auth", 56 | "datecreated": "2016-01-01T00:00:00.000Z", 57 | "dateupdated": null, 58 | "deprecated": true 59 | }, 60 | { 61 | "category": "Miscellaneous", 62 | "name": "Client-Side Error", 63 | "version": "1.0.0", 64 | "total": "2.0.0", 65 | "description": "Catch all client-side errors. All errors are shown on server-side console.", 66 | "author": "Peter Širka", 67 | "url": "https://github.com/totaljs/modules/tree/master/clienterror", 68 | "datecreated": "2016-01-01T00:00:00.000Z", 69 | "dateupdated": null 70 | }, 71 | { 72 | "category": "Miscellaneous", 73 | "name": "Console", 74 | "version": "2.0.0", 75 | "total": "2.0.0", 76 | "description": "With this package you can control a whole Total.js application from a web browser.", 77 | "author": "Peter Širka", 78 | "url": "https://github.com/totaljs/modules/tree/master/console", 79 | "datecreated": "2016-01-01T00:00:00.000Z", 80 | "dateupdated": null 81 | }, 82 | { 83 | "category": "Miscellaneous", 84 | "name": "Directory Listing", 85 | "version": "1.0.0", 86 | "total": "2.0.0", 87 | "description": "This module enables directory listing for public directory and it shows file sizes.", 88 | "author": "Peter Širka", 89 | "url": "https://github.com/totaljs/modules/tree/master/directorylisting", 90 | "datecreated": "2016-01-01T00:00:00.000Z", 91 | "dateupdated": null 92 | }, 93 | { 94 | "category": "Miscellaneous", 95 | "name": "Monitor", 96 | "version": "1.3.0", 97 | "total": "2.0.0", 98 | "description": "This module can monitor your Total.js web application and you can use AppMonitor with it.", 99 | "author": "Peter Širka", 100 | "url": "https://github.com/totaljs/modules/tree/master/monitor", 101 | "datecreated": "2016-01-01T00:00:00.000Z", 102 | "dateupdated": null 103 | }, 104 | { 105 | "category": "Miscellaneous", 106 | "name": "Request stats", 107 | "version": "1.1.0", 108 | "total": "2.0.0", 109 | "description": "This module counts all requests (dynamic content + files) by months. Data are stored in JSON file.", 110 | "author": "Peter Širka", 111 | "url": "https://github.com/totaljs/modules/tree/master/Miscellaneous/reqstats", 112 | "datecreated": "2016-01-01T00:00:00.000Z", 113 | "dateupdated": null, 114 | "deprecated": true 115 | }, 116 | { 117 | "category": "Miscellaneous", 118 | "name": "Session", 119 | "version": "2.0.0", 120 | "total": "2.0.0", 121 | "description": "This module enables session for each visitor. Session identificator is stored in cookie.", 122 | "author": "Peter Širka", 123 | "url": "https://github.com/totaljs/modules/tree/master/session", 124 | "datecreated": "2016-01-01T00:00:00.000Z", 125 | "dateupdated": null, 126 | }, 127 | { 128 | "category": "Miscellaneous", 129 | "name": "WebCounter", 130 | "version": "3.1.0", 131 | "total": "2.0.0", 132 | "description": "Monitor your visitor. The module supports: online visitors, daily, monthly and yearly stats.", 133 | "author": "Peter Širka", 134 | "url": "https://github.com/totaljs/modules/tree/master/Miscellaneous/webcounter", 135 | "datecreated": "2016-01-01T00:00:00.000Z", 136 | "dateupdated": null, 137 | "deprecated": true 138 | }, 139 | { 140 | "category": "Miscellaneous", 141 | "name": "NoSQL explorer", 142 | "version": "1.0.0", 143 | "total": "2.0.0", 144 | "description": "Easy content sharing of NoSQL embedded database. Bind your database with NoSQL embedded database explorer.", 145 | "author": "Peter Širka", 146 | "url": "https://github.com/totaljs/modules/tree/master/nosqlexplorer", 147 | "datecreated": "2016-10-09T00:00:00.000Z", 148 | "dateupdated": null 149 | } 150 | ] 151 | -------------------------------------------------------------------------------- /monitor/monitor.js: -------------------------------------------------------------------------------- 1 | exports.install = function() { 2 | var url = CONF.monitor_url || '/$monitor/'; 3 | ROUTE('GET ' + url, send); 4 | }; 5 | 6 | function send($) { 7 | 8 | if (!F.is5) 9 | $ = this; 10 | 11 | F.Fs.readFile(process.mainModule.filename + '.json', 'utf8', function(err, response) { 12 | var json = response ? response : 'null'; 13 | if (F.is5) 14 | $.jsonstring(json); 15 | else 16 | $.content(json, 'text/json'); 17 | }); 18 | } -------------------------------------------------------------------------------- /monitor/readme.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | __Requirements__: 4 | 5 | - Total.js `+v3.4.6` 6 | 7 | --- 8 | 9 | - download module `monitor.js` 10 | - copy it to `yourapp/modules/monitor.js` 11 | - restart the app 12 | 13 | If your module is installed and applied, then register your application on [__Total.js AppMonitor__](https://platform.totaljs.com?open=monitor). 14 | 15 | ## Configuration 16 | 17 | You can change the endpoint for the monitor via the `/config` file of the application like this: 18 | 19 | ```html 20 | // Default configuration 21 | monitor_url : /$monitor/ 22 | ``` -------------------------------------------------------------------------------- /nosqlexplorer/nosqlexplorer.js: -------------------------------------------------------------------------------- 1 | const Fs = require('fs'); 2 | const EXT = /\.nosql$/; 3 | var URL = ''; 4 | 5 | exports.install = function(options) { 6 | URL = CONF.nosqlexplorer_url; 7 | 8 | if (options && options.url) 9 | URL = options.url; 10 | 11 | if (!URL) 12 | URL = '/$nosqlexplorer/'; 13 | 14 | ROUTE('GET ' + URL, nosql); 15 | F.accept('nosql', 'text/plain'); 16 | F.accept('table', 'text/plain'); 17 | }; 18 | 19 | exports.databases = function(callback) { 20 | Fs.readdir(PATH.databases(), function(err, files) { 21 | var filenames = []; 22 | files.wait(function(item, next) { 23 | 24 | if (!item.endsWith('.nosql') || !item.endsWith('.table')) 25 | return next(); 26 | 27 | Fs.stat(PATH.databases(item), function(err, info) { 28 | info.isFile() && filenames.push({ name: item, size: info.size.filesize(), url: URL + '?name=' + encodeURIComponent(item.replace(EXT, '')) }); 29 | next(); 30 | }); 31 | 32 | }, () => callback(null, filenames)); 33 | }); 34 | }; 35 | 36 | function nosql() { 37 | var self = this; 38 | var name = self.query.name; 39 | 40 | if (name) { 41 | name = PATH.databases(self.query.name); 42 | 43 | if (!name.endsWith('.nosql') && !name.endsWith('.table')) 44 | name += '.nosql'; 45 | 46 | PATH.exists(name, function(e) { 47 | if (e) 48 | self.file('~' + name); 49 | else 50 | self.throw404(); 51 | }); 52 | 53 | return; 54 | } 55 | 56 | exports.databases(function(err, files) { 57 | var links = []; 58 | 59 | for (var i = 0; i < files.length; i++) { 60 | var file = files[i]; 61 | links.push('{0}{2}'.format(file.name, 'https://nosql.totaljs.com?url=' + encodeURIComponent(self.hostname(file.url)), file.size)); 62 | } 63 | 64 | self.content('NoSQL explorer
{0}
'.format(links.join('')), 'text/html'); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /nosqlexplorer/readme.md: -------------------------------------------------------------------------------- 1 | # NoSQL embedded database explorer 2 | 3 | This module can publish a content of the NoSQL embedded databases. Bind your NoSQL embedded database with [NoSQL embedded database explorer](https://www.totaljs.com/nosql/). 4 | 5 | - download and copy `nosqlexplorer.js` into the `/modules/` directory __or create a definition with:__ 6 | 7 | ```javascript 8 | var options = {}; 9 | // options.url = '/$nosqlexplorer/'; 10 | INSTALL('module', 'https://modules.totaljs.com/latest/nosqlexplorer.js', options); 11 | ``` 12 | 13 | 14 | --- 15 | 16 | ## Configuration 17 | 18 | ```html 19 | // Change URL to `nosqlexplorer` in a configuration file 20 | nosqlexplorer_url : /$nosqlexplorer/ 21 | ``` 22 | 23 | ## Usage 24 | 25 | - Listing: 26 | - Database: 27 | 28 | __Get list of all databases manually__: 29 | 30 | ```javascript 31 | MODULE('nosqlexplorer').databases(function(err, filenames) { 32 | filenames.forEach(function(file) { 33 | console.log(file.name, file.url, file.size); 34 | }); 35 | }); 36 | ``` 37 | 38 | __NoSQL embedded database explorer:__ 39 | 40 | ```html 41 | https://nosql.totaljs.com?url=https%3A%2F%2Fwww.yourdomain.com%2F%24nosqlexplorer%2F%3Fname%3DDATABASE_NAME 42 | ``` -------------------------------------------------------------------------------- /oauth2/oauth2.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright Peter Širka 3 | 4 | const Qs = require('querystring'); 5 | const stats = { facebook: 0, google: 0, linkedin: 0, yahoo: 0, dropbox: 0, github: 0, yandex: 0, instagram: 0, vk: 0, live: 0, msgraph: 0 }; 6 | const OAUTH2_HEADER = { code: '', client_id: '', client_secret: '', redirect_uri: '', grant_type: 'authorization_code' }; 7 | const OAUTH2_BEARER = { Authorization: '', 'User-Agent': 'total.js' }; 8 | const FLAG_POST = ['post']; 9 | const FLAG_POST_JSON = ['post', 'json']; 10 | const FLAG_GET = ['get']; 11 | 12 | exports.id = 'oauth2'; 13 | exports.version = 'v1.8.0'; 14 | 15 | exports.usage = function() { 16 | return stats; 17 | }; 18 | 19 | function facebook_redirect(key, url) { 20 | return 'https://graph.facebook.com/oauth/authorize?type=web_server&client_id={0}&redirect_uri={1}&scope=email'.format(key, encodeURIComponent(url)); 21 | } 22 | 23 | function facebook_profile(key, secret, code, url, callback) { 24 | U.request('https://graph.facebook.com/oauth/access_token?client_id={0}&redirect_uri={1}&client_secret={2}&code={3}'.format(key, url, secret, code), FLAG_GET, '', function(err, data) { 25 | if (err) 26 | return callback(err); 27 | if (data.indexOf('"error"') !== -1) 28 | return callback(data); 29 | 30 | var url; 31 | var token = ''; 32 | 33 | if (data.isJSON()) { 34 | token = JSON.parse(data).access_token; 35 | url = 'https://graph.facebook.com/me?&fields=email,first_name,last_name,gender,hometown,locale,name,id,timezone,picture&access_token=' + token; 36 | } else 37 | url = 'https://graph.facebook.com/me?' + data + '&fields=email,first_name,last_name,gender,hometown,locale,name,id,timezone,picture'; 38 | 39 | U.request(url, FLAG_GET, '', process('facebook', callback, token)); 40 | }); 41 | } 42 | 43 | function openplatform_redirect(op, key, url) { 44 | return op + '/oauth/authorize/?client_id={0}&redirect_uri={1}'.format(key, encodeURIComponent(url)); 45 | } 46 | 47 | function openplatform_profile(op, key, secret, code, url, callback) { 48 | 49 | OAUTH2_HEADER.code = code; 50 | OAUTH2_HEADER.client_id = key; 51 | OAUTH2_HEADER.client_secret = secret; 52 | OAUTH2_HEADER.redirect_uri = url; 53 | 54 | U.request(op + '/oauth/token/'.format(key, url, secret, code), FLAG_POST, OAUTH2_HEADER, function(err, data) { 55 | if (err) 56 | return callback(err); 57 | 58 | if (data.indexOf('"error"') !== -1) 59 | return callback(data); 60 | 61 | var url; 62 | var token = ''; 63 | 64 | token = JSON.parse(data).access_token; 65 | url = op + '/oauth/profile/'; 66 | OAUTH2_BEARER.Authorization = 'Bearer ' + token; 67 | U.request(url, FLAG_GET, '', process('openplatform', callback, token), null, OAUTH2_BEARER); 68 | }); 69 | } 70 | 71 | function google_redirect(key, url) { 72 | return 'https://accounts.google.com/o/oauth2/auth?scope=email%20profile&redirect_uri={0}&response_type=code&client_id={1}'.format(encodeURIComponent(url), key); 73 | } 74 | 75 | function google_profile(key, secret, code, url, callback) { 76 | 77 | OAUTH2_HEADER.code = code; 78 | OAUTH2_HEADER.client_id = key; 79 | OAUTH2_HEADER.client_secret = secret; 80 | OAUTH2_HEADER.redirect_uri = url; 81 | 82 | U.request('https://www.googleapis.com/oauth2/v3/token', FLAG_POST, OAUTH2_HEADER, function(err, data) { 83 | if (!process_error(err, data, callback)) 84 | return; 85 | var token = data.parseJSON().access_token; 86 | OAUTH2_BEARER.Authorization = 'Bearer ' + token; 87 | U.request('https://www.googleapis.com/plus/v1/people/me', FLAG_GET, '', process('google', callback, token), null, OAUTH2_BEARER); 88 | }); 89 | } 90 | 91 | function linkedin_redirect(key, url) { 92 | return 'https://www.linkedin.com/uas/oauth2/authorization?response_type=code&client_id={0}&redirect_uri={1}&state=987654321'.format(key, encodeURIComponent(url)); 93 | } 94 | 95 | function linkedin_profile(key, secret, code, url, callback) { 96 | 97 | OAUTH2_HEADER.code = code; 98 | OAUTH2_HEADER.client_id = key; 99 | OAUTH2_HEADER.client_secret = secret; 100 | OAUTH2_HEADER.redirect_uri = url; 101 | 102 | U.request('https://www.linkedin.com/uas/oauth2/accessToken', FLAG_POST, OAUTH2_HEADER, function(err, data) { 103 | if (!process_error(err, data, callback)) 104 | return; 105 | var token = data.parseJSON().access_token; 106 | OAUTH2_BEARER.Authorization = 'Bearer ' + token; 107 | U.request('https://api.linkedin.com/v1/people/~:(id,first-name,last-name,headline,member-url-resources,picture-url,location,public-profile-url,email-address)?format=json', FLAG_GET, '', process('linkedin', callback, token), null, OAUTH2_BEARER); 108 | }); 109 | } 110 | 111 | function yahoo_redirect(key, url) { 112 | return 'https://api.login.yahoo.com/oauth2/request_auth?client_id={0}&redirect_uri={1}&response_type=code&language=en-us'.format(key, encodeURIComponent(url)); 113 | } 114 | 115 | function yahoo_profile(key, secret, code, url, callback) { 116 | 117 | OAUTH2_HEADER.code = code; 118 | OAUTH2_HEADER.client_id = key; 119 | OAUTH2_HEADER.client_secret = secret; 120 | OAUTH2_HEADER.redirect_uri = url; 121 | 122 | U.request('https://api.login.yahoo.com/oauth2/get_token', FLAG_POST, OAUTH2_HEADER, function(err, data) { 123 | if (!process_error(err, data, callback)) 124 | return; 125 | var token = data.parseJSON().access_token; 126 | OAUTH2_BEARER.Authorization = 'Bearer ' + token; 127 | U.request('https://social.yahooapis.com/v1/user/' + data.xoauth_yahoo_guid + '/profile?format=json', FLAG_GET, '', process('yahoo', callback, token), null, OAUTH2_BEARER); 128 | }, null, { 'Authorization': 'Basic ' + new Buffer(key + ':' + secret).toString('base64'), 'Content-Type': 'application/x-www-form-urlencoded' }); 129 | } 130 | 131 | function github_redirect(key, url) { 132 | return 'https://github.com/login/oauth/authorize?scope=user%3Aemail&redirect_uri={0}&response_type=code&client_id={1}'.format(encodeURIComponent(url), key); 133 | } 134 | 135 | function github_profile(key, secret, code, url, callback) { 136 | 137 | OAUTH2_HEADER.code = code; 138 | OAUTH2_HEADER.client_id = key; 139 | OAUTH2_HEADER.client_secret = secret; 140 | OAUTH2_HEADER.redirect_uri = url; 141 | 142 | U.request('https://github.com/login/oauth/access_token', FLAG_POST, OAUTH2_HEADER, function(err, data) { 143 | if (err) 144 | return callback(err); 145 | var token = Qs.parse(data).access_token; 146 | OAUTH2_BEARER.Authorization = 'Bearer ' + token; 147 | U.request('https://api.github.com/user', FLAG_GET, '', process('github', callback, token), null, OAUTH2_BEARER); 148 | }); 149 | } 150 | 151 | function dropbox_redirect(key, url) { 152 | return 'https://www.dropbox.com/oauth2/authorize?redirect_uri={0}&response_type=code&client_id={1}'.format(encodeURIComponent(url), key); 153 | } 154 | 155 | function dropbox_profile(key, secret, code, url, callback) { 156 | 157 | OAUTH2_HEADER.code = code; 158 | OAUTH2_HEADER.client_id = key; 159 | OAUTH2_HEADER.client_secret = secret; 160 | OAUTH2_HEADER.redirect_uri = url; 161 | 162 | U.request('https://api.dropboxapi.com/oauth2/token', FLAG_POST, OAUTH2_HEADER, function(err, data) { 163 | if (!process_error(err, data, callback)) 164 | return; 165 | var response = data.parseJSON(); 166 | var token = response.access_token, 167 | account_id = response.account_id; 168 | OAUTH2_BEARER.Authorization = 'Bearer ' + token; 169 | U.request('https://api.dropboxapi.com/2/users/get_account', FLAG_POST_JSON, { account_id }, process('dropbox', callback, token), null, OAUTH2_BEARER); 170 | }); 171 | } 172 | 173 | function live_redirect(key, url) { 174 | return 'https://login.live.com/oauth20_authorize.srf?client_id={1}&scope=wl.basic%2Cwl.signin%2Cwl.birthday%2Cwl.emails&response_type=code&redirect_uri={0}'.format(encodeURIComponent(url), encodeURIComponent(key)); 175 | } 176 | 177 | function live_profile(key, secret, code, url, callback) { 178 | 179 | OAUTH2_HEADER.code = code; 180 | OAUTH2_HEADER.client_id = key; 181 | OAUTH2_HEADER.client_secret = secret; 182 | OAUTH2_HEADER.redirect_uri = url; 183 | 184 | U.request('https://login.live.com/oauth20_token.srf', FLAG_POST, OAUTH2_HEADER, function(err, data) { 185 | if (!process_error(err, data, callback)) 186 | return; 187 | var token = data.parseJSON().access_token; 188 | OAUTH2_BEARER.Authorization = 'Bearer ' + token; 189 | U.request('https://apis.live.net/v5.0/me/', FLAG_GET, '', process('live', callback, token), null, OAUTH2_BEARER); 190 | }); 191 | } 192 | 193 | function instagram_redirect(key, url) { 194 | return 'https://api.instagram.com/oauth/authorize/?redirect_uri={0}&response_type=code&client_id={1}&scope=basic'.format(encodeURIComponent(url), key); 195 | } 196 | 197 | function instagram_profile(key, secret, code, url, callback) { 198 | 199 | OAUTH2_HEADER.code = code; 200 | OAUTH2_HEADER.client_id = key; 201 | OAUTH2_HEADER.client_secret = secret; 202 | OAUTH2_HEADER.redirect_uri = url; 203 | 204 | U.request('https://api.instagram.com/oauth/access_token', FLAG_POST, OAUTH2_HEADER, function(err, data) { 205 | if (!process_error(err, data, callback)) 206 | return; 207 | process('instagram', callback)(null, data); 208 | }); 209 | } 210 | 211 | function yandex_redirect(key, url) { 212 | return 'https://oauth.yandex.com/authorize/?response_type=code&client_id={1}'.format(encodeURIComponent(url), key); 213 | } 214 | 215 | function yandex_profile(key, secret, code, url, callback) { 216 | 217 | OAUTH2_HEADER.code = code; 218 | OAUTH2_HEADER.client_id = key; 219 | OAUTH2_HEADER.client_secret = secret; 220 | OAUTH2_HEADER.redirect_uri = url; 221 | 222 | U.request('https://oauth.yandex.com/token', FLAG_POST, OAUTH2_HEADER, function(err, data) { 223 | if (!process_error(err, data, callback)) 224 | return; 225 | var token = data.parseJSON().access_token; 226 | OAUTH2_BEARER.Authorization = 'Bearer ' + token; 227 | U.request('https://login.yandex.ru/info', FLAG_GET, '', process('yandex', callback, token), null, OAUTH2_BEARER); 228 | }); 229 | } 230 | 231 | function vk_redirect(key, url) { 232 | return 'https://oauth.vk.com/authorize?redirect_uri={0}&response_type=code&client_id={1}&scope=email'.format(encodeURIComponent(url), key); 233 | } 234 | 235 | function vk_profile(key, secret, code, url, callback) { 236 | 237 | OAUTH2_HEADER.code = code; 238 | OAUTH2_HEADER.client_id = key; 239 | OAUTH2_HEADER.client_secret = secret; 240 | OAUTH2_HEADER.redirect_uri = url; 241 | 242 | U.request('https://oauth.vk.com/access_token', FLAG_POST, OAUTH2_HEADER, function(err, data) { 243 | if (!process_error(err, data, callback)) 244 | return; 245 | data = data.parseJSON(); 246 | var token = data.access_token; 247 | U.request('https://api.vk.com/method/users.get', FLAG_GET, 'uid=' + data.user_id + '&access_token=' + token + '&fields=nickname,screen_name,photo_big,sex,country,email', process('vk', callback, token)); 248 | }); 249 | } 250 | 251 | function msgraph_redirect(key, url) { 252 | return 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id={1}&scope=openid%20offline_access%20https%3A%2F%2Fgraph.microsoft.com%2Fuser.read&response_type=code&redirect_uri={0}&response_mode=query'.format(encodeURIComponent(url), encodeURIComponent(key)); 253 | } 254 | 255 | function msgraph_profile(key, secret, code, url, callback) { 256 | 257 | OAUTH2_HEADER.code = code; 258 | OAUTH2_HEADER.client_id = key; 259 | OAUTH2_HEADER.client_secret = secret; 260 | OAUTH2_HEADER.grant_type = 'authorization_code'; 261 | OAUTH2_HEADER.redirect_uri = url; 262 | 263 | U.request('https://login.microsoftonline.com/common/oauth2/v2.0/token', FLAG_POST, OAUTH2_HEADER, function(err, data) { 264 | if (!process_error(err, data, callback)) 265 | return; 266 | var token = data.parseJSON().access_token; 267 | OAUTH2_BEARER.Authorization = 'Bearer ' + token; 268 | U.request('https://graph.microsoft.com/v1.0/me/', FLAG_GET, '', process('msgraph', callback, token), null, OAUTH2_BEARER); 269 | }); 270 | } 271 | 272 | function process_error(err, data, callback) { 273 | 274 | if (err) { 275 | callback(err); 276 | return; 277 | } 278 | 279 | if (!data.trim().isJSON()) { 280 | callback(data); 281 | return; 282 | } 283 | 284 | if (data.indexOf('"error"') !== -1) { 285 | callback(data.parseJSON()); 286 | return; 287 | } 288 | 289 | return true; 290 | } 291 | 292 | function process(name, callback, token) { 293 | return function(err, data) { 294 | 295 | stats[name]++; 296 | 297 | if (err) { 298 | callback(err, undefined, token); 299 | return; 300 | } 301 | 302 | if (!data.trim().isJSON()) { 303 | callback(data); 304 | return; 305 | } 306 | 307 | var user = data.trim().parseJSON(); 308 | 309 | if (!user) { 310 | callback(new Error('User data from OAuth2 ' + name + ' doesn\'t exist.')); 311 | return; 312 | } 313 | 314 | if (name === 'yahoo') { 315 | if (!user.profile && !user.guid) { 316 | err = user; 317 | user = null; 318 | } 319 | } else if (name === 'dropbox') { 320 | if (!user['account_id']) { 321 | err = user; 322 | user = null; 323 | } 324 | } else if (name === 'instagram') { 325 | if (!user.user || !user.user.id) { 326 | err = user; 327 | user = null; 328 | } 329 | } else if (!user.id && !user.uid) { 330 | err = user; 331 | user = null; 332 | } 333 | 334 | callback(err, user, token); 335 | }; 336 | } 337 | 338 | exports.redirect = function(type, key, url, controller, param) { 339 | switch (type) { 340 | case 'openplatform': 341 | controller.redirect(openplatform_redirect(param, key, url)); 342 | break; 343 | case 'facebook': 344 | controller.redirect(facebook_redirect(key, url)); 345 | break; 346 | case 'google': 347 | controller.redirect(google_redirect(key, url)); 348 | break; 349 | case 'yahoo': 350 | controller.redirect(yahoo_redirect(key, url)); 351 | break; 352 | case 'linkedin': 353 | controller.redirect(linkedin_redirect(key, url)); 354 | break; 355 | case 'github': 356 | controller.redirect(github_redirect(key, url)); 357 | break; 358 | case 'dropbox': 359 | controller.redirect(dropbox_redirect(key, url)); 360 | break; 361 | case 'live': 362 | controller.redirect(live_redirect(key, url)); 363 | break; 364 | case 'instagram': 365 | controller.redirect(instagram_redirect(key, url)); 366 | break; 367 | case 'yandex': 368 | controller.redirect(yandex_redirect(key, url)); 369 | break; 370 | case 'vk': 371 | controller.redirect(vk_redirect(key, url)); 372 | break; 373 | case 'msgraph': 374 | controller.redirect(msgraph_redirect(key, url)); 375 | break; 376 | } 377 | }; 378 | 379 | exports.callback = function(type, key, secret, url, controller, callback, param) { 380 | switch (type) { 381 | case 'openplatform': 382 | openplatform_profile(param, key, secret, controller.query.code, url, callback); 383 | break; 384 | case 'facebook': 385 | facebook_profile(key, secret, controller.query.code, url, callback); 386 | break; 387 | case 'google': 388 | google_profile(key, secret, controller.query.code, url, callback); 389 | break; 390 | case 'yahoo': 391 | yahoo_profile(key, secret, controller.query.code, url, callback); 392 | break; 393 | case 'linkedin': 394 | linkedin_profile(key, secret, controller.query.code, url, callback); 395 | break; 396 | case 'github': 397 | github_profile(key, secret, controller.query.code, url, callback); 398 | break; 399 | case 'dropbox': 400 | dropbox_profile(key, secret, controller.query.code, url, callback); 401 | break; 402 | case 'live': 403 | live_profile(key, secret, controller.query.code, url, callback); 404 | break; 405 | case 'instagram': 406 | instagram_profile(key, secret, controller.query.code, url, callback); 407 | break; 408 | case 'yandex': 409 | yandex_profile(key, secret, controller.query.code, url, callback); 410 | break; 411 | case 'vk': 412 | vk_profile(key, secret, controller.query.code, url, callback); 413 | break; 414 | case 'msgraph': 415 | msgraph_profile(key, secret, controller.query.code, url, callback); 416 | break; 417 | } 418 | }; 419 | 420 | exports.facebook_redirect = facebook_redirect; 421 | exports.facebook_profile = facebook_profile; 422 | exports.google_redirect = google_redirect; 423 | exports.google_profile = google_profile; 424 | exports.linkedin_redirect = linkedin_redirect; 425 | exports.linkedin_profile = linkedin_profile; 426 | exports.yahoo_redirect = yahoo_redirect; 427 | exports.yahoo_profile = yahoo_profile; 428 | exports.github_redirect = github_redirect; 429 | exports.github_profile = github_profile; 430 | exports.dropbox_redirect = dropbox_redirect; 431 | exports.dropbox_profile = dropbox_profile; 432 | exports.live_profile = live_profile; 433 | exports.live_redirect = live_redirect; 434 | exports.instagram_profile = instagram_profile; 435 | exports.instagram_redirect = instagram_redirect; 436 | exports.yandex_profile = yandex_profile; 437 | exports.yandex_redirect = yandex_redirect; 438 | exports.vk_profile = vk_profile; 439 | exports.vk_redirect = vk_redirect; 440 | exports.msgraph_profile = msgraph_profile; 441 | exports.msgraph_redirect = msgraph_redirect; 442 | exports.openplatform_profile = openplatform_profile; 443 | exports.openplatform_redirect = openplatform_redirect; 444 | -------------------------------------------------------------------------------- /oauth2/readme.md: -------------------------------------------------------------------------------- 1 | # Login with Facebook, Google, LinkedIn, Yahoo, GitHub, DropBox, Live, Instagram, Yandex, VKontakte 2 | 3 | - For testing use: [NGROK - proxy tunnel](https://ngrok.com/) 4 | example) 5 | - download and copy `oauth2.js` into the `/modules/` directory __or create a definition with:__ 6 | 7 | ```javascript 8 | INSTALL('module', 'https://cdn.totaljs.com/oauth2.js'); 9 | ``` 10 | 11 | ```javascript 12 | exports.install = function() { 13 | F.route('/login/github/', oauth_login, ['unauthorize']); 14 | F.route('/login/github/callback/', oauth_login_callback, ['unauthorize']); 15 | F.route('/login/facebook/', oauth_login, ['unauthorize']); 16 | F.route('/login/facebook/callback/', oauth_login_callback, ['unauthorize']); 17 | F.route('/login/dropbox/', oauth_login, ['unauthorize']); 18 | F.route('/login/dropbox/callback/', oauth_login_callback, ['unauthorize']); 19 | F.route('/login/linkedin/', oauth_login, ['unauthorize']); 20 | F.route('/login/linkedin/callback/', oauth_login_callback, ['unauthorize']); 21 | F.route('/login/google/', oauth_login, ['unauthorize']); 22 | F.route('/login/google/callback/', oauth_login_callback, ['unauthorize']); 23 | F.route('/login/yahoo/', oauth_login, ['unauthorize']); 24 | F.route('/login/yahoo/callback/', oauth_login_callback, ['unauthorize']); 25 | F.route('/login/live/', oauth_login, ['unauthorize']); 26 | F.route('/login/live/callback/', oauth_login_callback, ['unauthorize']); 27 | F.route('/login/instagram/', oauth_login, ['unauthorize']); 28 | F.route('/login/instagram/callback/', oauth_login_callback, ['unauthorize']); 29 | F.route('/login/msgraph/', oauth_login, ['unauthorize']); 30 | F.route('/login/msgraph/callback/', oauth_login_callback, ['unauthorize']); 31 | } 32 | 33 | // Controller action 34 | function oauth_login() { 35 | var self = this; 36 | var type = self.req.path[1]; 37 | 38 | // config: 39 | // oauth2.google.key = 40 | // oauth2.google.secret = 41 | // oauth2.github.key = 42 | // oauth2.github.secret = 43 | // ... 44 | 45 | MODULE('oauth2').redirect(type, CONFIG('oauth2.' + type + '.key'), self.host('/login/' + type + '/callback/'), self); 46 | 47 | // OpenPlatform: 48 | // MODULE('oauth2').redirect(type, CONFIG('oauth2.' + type + '.key'), self.host('/login/' + type + '/callback/'), self, OPENPLATFORM_URL_ADDRESS); 49 | } 50 | 51 | // Controller action 52 | function oauth_login_callback() { 53 | var self = this; 54 | var type = self.req.path[1]; 55 | var url = self.host('/login/' + type + '/callback/'); 56 | 57 | // config: 58 | // oauth2.google.key = 59 | // oauth2.google.secret = 60 | // oauth2.github.key = 61 | // oauth2.github.secret = 62 | // ... 63 | 64 | MODULE('oauth2').callback(type, CONFIG('oauth2.' + type + '.key'), CONFIG('oauth2.' + type + '.secret'), url, self, function(err, profile, access_token) { 65 | console.log(profile); 66 | self.json(SUCCESS(true)); 67 | }); 68 | 69 | // OpenPlatform: 70 | // MODULE('oauth2').callback(type, CONFIG('oauth2.' + type + '.key'), CONFIG('oauth2.' + type + '.secret'), url, self, function(err, profile, access_token) { 71 | // console.log(profile); 72 | // self.json(SUCCESS(true)); 73 | // }, OPENPLATFORM_URL_ADDRESS); 74 | } 75 | ``` 76 | 77 | ## Register you application 78 | 79 | - Facebook - 80 | - Google - 81 | - Yahoo - 82 | - LinkedIn - 83 | - DropBox - 84 | - GitHub - 85 | - Live (__DEPRECATED__ use `msgraph` instead) - 86 | - Instagram - 87 | - Yandex - 88 | - VKontakte - 89 | - Microsoft Graph - or __App registrations (Preview)__ experience in the Azure portal 90 | - OpenPlatform 91 | 92 | ## CALLBACK 93 | 94 | ### GOOGLE 95 | Reference: 96 | ```javascript 97 | { kind: 'plus#person', 98 | etag: '"RqKWnRU4WW46-6W3rWhLR9iFZQM/EJnOkqg0s0pcrrs0iKvN73LvCnk"', 99 | occupation: 'Web developer & Web designer', 100 | gender: 'male', 101 | emails: [ { value: 'a@a.com', type: 'account' } ], 102 | urls: 103 | [ { value: 'http://www.facebook.com/PeterSirka', 104 | type: 'otherProfile', 105 | label: 'petersirka' }, 106 | { value: 'http://twitter.com/petersirka', 107 | type: 'otherProfile', 108 | label: 'petersirka' }, 109 | { value: 'http://www.youtube.com/user/petersirka', 110 | type: 'otherProfile', 111 | label: 'http://www.youtube.com/user/petersirka' }, 112 | { value: 'https://profiles.google.com/117275945491270172609/buzz', 113 | type: 'contributor', 114 | label: 'Buzz' } ], 115 | objectType: 'person', 116 | id: '00000000000', 117 | displayName: 'Peter Širka', 118 | name: { familyName: 'Širka', givenName: 'Peter' }, 119 | url: 'https://plus.google.com/+PeterŠirka', 120 | image: 121 | { url: 'https://lh6.googleusercontent.com/-qJtxCwBkVcg/AAAAAAAAAAI/AAAAAAAAAeY/Mn8ABMy5BJE/photo.jpg?sz=50', 122 | isDefault: false }, 123 | organizations: 124 | [ { name: 'Datalan', 125 | title: '.NET Developer', 126 | type: 'work', 127 | startDate: '2014', 128 | primary: true }, 129 | { name: '858project.com', 130 | title: 'Web developer & Web designer', 131 | type: 'work', 132 | startDate: '2014', 133 | endDate: '2014', 134 | primary: false }, 135 | { name: 'Web Site Design s.r.o.', 136 | title: 'Web developer & Web designer', 137 | type: 'work', 138 | startDate: '2006', 139 | endDate: '2013', 140 | primary: false } ], 141 | isPlusUser: true, 142 | language: 'sk', 143 | circledByCount: 312, 144 | verified: false, 145 | cover: 146 | { layout: 'banner', 147 | coverPhoto: 148 | { url: 'https://lh6.googleusercontent.com/-CXhru8AWMjc/VEF47IlEwaI/AAAAAAAAA6U/dGRkNNYNDmI/s630-fcrop64=1,52770000ff78ffff/google.png', 149 | height: 357, 150 | width: 940 }, 151 | coverInfo: { topImageOffset: 0, leftImageOffset: 0 } } } 152 | ``` 153 | 154 | ### LinkedIn 155 | 156 | ```javascript 157 | { emailAddress: 'a@a.com', 158 | firstName: 'Peter', 159 | headline: 'Web Developer (node.js, total.js, ASP.NET MVC)', 160 | id: 'XXXXXX', 161 | lastName: 'Širka', 162 | location: { country: { code: 'sk' }, name: 'Slovak Republic' }, 163 | pictureUrl: 'https://media.licdn.com/mpr/mprx/0_2weeFgZ6XEbFwdjBC7UBF0HLkDrQWEjBafycF0VJUHXBnaoc8SZkWxmcE_KZoSpUDeEnHZXfWjW8', 164 | publicProfileUrl: 'https://www.linkedin.com/pub/peter-%C5%A1irka/64/973/227' } 165 | ``` 166 | 167 | ### Dropbox 168 | 169 | Reference: 170 | 171 | ```json 172 | { 173 | "account_id": "dbid:AAH4f99T0taONIb-OurWxbNQ6ywGRopQngc", 174 | "name": { 175 | "given_name": "Franz", 176 | "surname": "Ferdinand", 177 | "familiar_name": "Franz", 178 | "display_name": "Franz Ferdinand (Personal)", 179 | "abbreviated_name": "FF" 180 | }, 181 | "email": "franz@dropbox.com", 182 | "email_verified": true, 183 | "disabled": false, 184 | "is_teammate": false, 185 | "profile_photo_url": "https://dl-web.dropbox.com/account_photo/get/dbid%3AAAH4f99T0taONIb-OurWxbNQ6ywGRopQngc?vers=1453416696524\u0026size=128x128" 186 | } 187 | ``` 188 | 189 | ### GitHub 190 | 191 | ```javascript 192 | { login: 'petersirka', 193 | id: 000, 194 | avatar_url: 'https://avatars.githubusercontent.com/u/2414252?v=3', 195 | gravatar_id: '', 196 | url: 'https://api.github.com/users/petersirka', 197 | html_url: 'https://github.com/petersirka', 198 | followers_url: 'https://api.github.com/users/petersirka/followers', 199 | following_url: 'https://api.github.com/users/petersirka/following{/other_user}', 200 | gists_url: 'https://api.github.com/users/petersirka/gists{/gist_id}', 201 | starred_url: 'https://api.github.com/users/petersirka/starred{/owner}{/repo}', 202 | subscriptions_url: 'https://api.github.com/users/petersirka/subscriptions', 203 | organizations_url: 'https://api.github.com/users/petersirka/orgs', 204 | repos_url: 'https://api.github.com/users/petersirka/repos', 205 | events_url: 'https://api.github.com/users/petersirka/events{/privacy}', 206 | received_events_url: 'https://api.github.com/users/petersirka/received_events', 207 | type: 'User', 208 | site_admin: false, 209 | name: 'Peter Širka', 210 | company: 'Datalan', 211 | blog: 'https://bufferwall.com/blogs/?user=peter-sirka', 212 | location: 'Slovakia (EU), Bratislava', 213 | email: 'a@a.com', 214 | hireable: false, 215 | bio: null, 216 | public_repos: 29, 217 | public_gists: 2, 218 | followers: 121, 219 | following: 1, 220 | created_at: '2012-09-24T19:04:19Z', 221 | updated_at: '2015-03-02T11:06:56Z' } 222 | ``` 223 | 224 | ### Facebook 225 | 226 | ```javascript 227 | { id: '000', 228 | birthday: '11/06/1984', 229 | email: 'a@a.com', 230 | first_name: 'Peter', 231 | gender: 'male', 232 | hometown: { id: '000', name: 'Banská Bystrica, Slovakia' }, 233 | last_name: 'Širka', 234 | link: 'https://www.facebook.com/000/', 235 | locale: 'sk_SK', 236 | name: 'Peter Širka', 237 | timezone: 1, 238 | updated_time: '2014-09-15T16:24:13+0000', 239 | verified: true, 240 | picture: 'https://graph.facebook.com/000/picture' } 241 | ``` 242 | 243 | ### Yahoo 244 | 245 | ```javascript 246 | { profile: 247 | { guid: 'XXXX', 248 | ageCategory: 'A', 249 | created: '2015-03-03T08:21:27Z', 250 | image: 251 | { height: 192, 252 | imageUrl: 'https://s.yimg.com/dh/ap/social/profile/profile_b192.png', 253 | size: '192x192', 254 | width: 192 }, 255 | intl: 'us', 256 | jurisdiction: 'us', 257 | lang: 'en-US', 258 | memberSince: '2015-03-03T07:30:44Z', 259 | migrationSource: 1, 260 | nickname: 'Peter', 261 | notStored: true, 262 | nux: '0', 263 | profileMode: 'PUBLIC', 264 | profileStatus: 'ACTIVE', 265 | profileUrl: 'http://profile.yahoo.com/5S5G4LKFZJECMAI2IHVD5UTUII', 266 | isConnected: false, 267 | profileHidden: false, 268 | bdRestricted: true, 269 | profilePermission: 'PRIVATE', 270 | uri: 'https://social.yahooapis.com/v1/user/5S5G4LKFZJECMAI2IHVD5UTUII/profile', 271 | cache: true } } 272 | ``` 273 | 274 | ### Live 275 | 276 | __DEPRECATED__ use `msgraph` instead 277 | 278 | ```javascript 279 | { id: 'XXX', 280 | name: 'Peter Širka', 281 | first_name: 'Peter', 282 | last_name: 'Širka', 283 | link: 'https://profile.live.com/', 284 | birth_day: 6, 285 | birth_month: 11, 286 | birth_year: 1984, 287 | gender: null, 288 | emails: 289 | { preferred: 'a@a.com', 290 | account: 'a@a.com', 291 | personal: 'a@a.com', 292 | business: null }, 293 | locale: 'sk_SK', 294 | updated_time: '2015-03-03T08:38:47+0000' } 295 | ``` 296 | 297 | ### Instagram 298 | 299 | The response from Instagram __doesn't contain email__!!! 300 | 301 | ```javascript 302 | { 303 | access_token: '......', 304 | user: { 305 | username: 'petersirka', 306 | bio: '', 307 | website: '', 308 | profile_picture: 'https://instagramimages-a.akamaihd.net/profiles/anonymousUser.jpg', 309 | full_name: 'Peter Širka', 310 | id: '1635329562' 311 | } 312 | } 313 | ``` 314 | 315 | ### Yandex 316 | 317 | You must allow user account information in __Yandex.Passport API__ scope. 318 | 319 | ```javascript 320 | { first_name: 'Peter', 321 | last_name: 'Širka', 322 | display_name: 'petersirka', 323 | emails: [ 'petersirka@.....' ], 324 | default_email: 'petersirka@.....', 325 | real_name: 'Peter Širka', 326 | birthday: null, 327 | default_avatar_id: '0/0-0', 328 | login: 'petersirka', 329 | sex: null, 330 | id: '......' } 331 | ``` 332 | 333 | ### VKontakte - VK.com 334 | 335 | VK doesn't send back email address and the scope contains requirement for the email address. 336 | 337 | ```javascript 338 | { uid: 00000, 339 | first_name: 'Peter', 340 | last_name: 'Širka', 341 | nickname: '', 342 | screen_name: '....', 343 | photo_big: 'http://vk.com/images/camera_200.png' } 344 | ``` 345 | 346 | ### Microsoft Graph 347 | 348 | Reference: 349 | 350 | ```json 351 | { 352 | "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity", 353 | "businessPhones": [ 354 | "+1 412 555 0109" 355 | ], 356 | "displayName": "Megan Bowen", 357 | "givenName": "Megan", 358 | "jobTitle": "Auditor", 359 | "mail": "MeganB@M365x214355.onmicrosoft.com", 360 | "mobilePhone": null, 361 | "officeLocation": "12/1110", 362 | "preferredLanguage": "en-US", 363 | "surname": "Bowen", 364 | "userPrincipalName": "MeganB@M365x214355.onmicrosoft.com", 365 | "id": "48d31887-5fad-4d73-a9f5-3c356e68a038" 366 | } 367 | ``` 368 | -------------------------------------------------------------------------------- /openplatform/readme.md: -------------------------------------------------------------------------------- 1 | # Module: OpenPlatform 2 | 3 | This module can simplify work with OpenPlatform. It's targeted for OpenPlatform applications. 4 | 5 | - [Documentation](https://wiki.totaljs.com/openplatform/06-openplatform-module/) 6 | -------------------------------------------------------------------------------- /querybuilderapi/querybuilderapi.js: -------------------------------------------------------------------------------- 1 | // Total.js Module: QueryBuilder API 2 | // Author: Peter Širka / Total.js 3 | // Readme: https://github.com/totaljs/modules/tree/master/querybuilderapi 4 | // License: MIT 5 | 6 | // CONF.querybuilderapi {String} 7 | // CONF.querybuilderapitoken {String} 8 | 9 | exports.install = function() { 10 | 11 | ROUTE('POST {0} *QueryBuilderAPI --> exec'.format(CONF.querybuilderapi || '/db/')); 12 | 13 | }; 14 | 15 | NEWSCHEMA('QueryBuilderAPI', function(schema) { 16 | 17 | schema.define('filter', '[Object]'); 18 | schema.define('table', String); 19 | schema.define('exec', ['find', 'list', 'check', 'read', 'count', 'insert', 'update', 'remove', 'query', 'truncate', 'command'], true); 20 | schema.define('query', String); 21 | schema.define('fields', '[String]'); 22 | schema.define('returning', '[String]'); 23 | schema.define('sort', '[String]'); 24 | schema.define('skip', Number); 25 | schema.define('take', Number); 26 | schema.define('payload', Object); 27 | schema.define('upsert', Boolean); 28 | 29 | schema.action('exec', { 30 | name: 'Exec QueryBuilder', 31 | action: function($, model) { 32 | 33 | if (BLOCKED($, 10)) { 34 | $.invalid(401); 35 | return; 36 | } 37 | 38 | if ($.query.token !== CONF.querybuilderapitoken) { 39 | $.invalid(401); 40 | return; 41 | } 42 | 43 | BLOCKED($, -1); 44 | 45 | if (!model.returning.length) 46 | model.returning = null; 47 | 48 | DATA.load(model).callback(function(err, response) { 49 | if (err) 50 | $.invalid(err); 51 | else 52 | $.callback(response == undefined ? null : response); 53 | }); 54 | } 55 | }); 56 | 57 | }); -------------------------------------------------------------------------------- /querybuilderapi/readme.md: -------------------------------------------------------------------------------- 1 | # QueryBuilder API 2 | 3 | This module will enable full access to the DB for all external Total.js QueryBuilder declarations. 4 | 5 | ## Configuration 6 | 7 | - `CONF.querybuilderapi {String}` optional, endpoint (default: `/db/`) 8 | - `CONF.querybuilderapitoken {String}` __required__, a security token (compared with the URL query argument called `?token`) 9 | 10 | ### QueryBuilder declaration for external apps 11 | 12 | ```js 13 | // DB connection to the external Total.js API with the QueryBuilderAPI module 14 | NEWDB('default', function(filter, callback) { 15 | var opt = {}; 16 | opt.url = 'https://yourapp.totaljs.com/db/?token=YOUR_TOKEN'; 17 | opt.method = 'POST'; 18 | opt.type = 'json'; 19 | opt.keepalive = true; 20 | opt.body = JSON.stringify(filter); 21 | opt.callback = function(err, response) { 22 | if (err) { 23 | callback(err); 24 | } else { 25 | var data = response.body.parseJSON(); 26 | var iserr = response.status !== 200; 27 | callback(iserr ? (data instanceof Array ? data[0].error : data) : null, iserr ? null : data); 28 | } 29 | }; 30 | REQUEST(opt); 31 | }); 32 | 33 | // Then you can call data: 34 | DATA.find('tbl_something').callback(console.log); 35 | ``` -------------------------------------------------------------------------------- /quickapi/make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | NAME=`basename "$PWD"`.package 4 | 5 | cd source 6 | tpm create "$NAME" 7 | mv "$NAME" ../ -------------------------------------------------------------------------------- /quickapi/quickapi.package: -------------------------------------------------------------------------------- 1 | /index.html :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 2 | /index.js :H4sIAAAAAAAAE5VW6XIaRxD+vTxFq1LlXUrUIsnOBVbiCzlO7EiJUPKDUrmGZYBBe2VmVpjIvEteJu+V7pnZA1amKn9g+/q6p4/p6ffhw7sxvBcRTxXv9PvwOss3UiyWGq645hL+/UfIOwbPc6IUfb9YJEzEYZQlP3Q690zC5dV42OnwT3kmtQrvSS1L4Rz8+9PwJDzxh5VMpEqzOEbZvEgjjWpBltOf6sJDx0MgFDkOQnpiDsERcrsdzwkftsTHOJ0WhpFqmcUxeiUxsFwMQMuC9yBhKVtwaUm0a5oVkqLw+7MsUn1/VzbjKpLCEKTzPpM8AZGrIoFZFmcSlNDAEq57gN4VjzTXhQQ2E7lQIhLpAngsdAi/FYL1gOkjn4LGAzi3N2HO9DIoGZ8/V4F0MZLfL2/Go8B/OxqDD8fgtHpwL/j6o0hn/BNqbRsZL9JH8tpIaFrEsbUopQ0sq0hlVDyeo7ZeCsy9R1SY8CST4m8e+H8VIrrD3Po98M8gEWmhuULizejVzdvenlsDlxU6L7QrmWVNCxHPsKfOYXJLGfHmmMyAJAJ5J0P8ew4XocwIPFzzaRjzdKGXKDg+tsgGR2gsyPmO5kRYRNsypBBm65TLEBODOfpTYL79ulcoscFFKNQz+BH8jz4MwP/K73apFtYay7vMZtR4nkd2eGJuPVAENRLG0fRWTJWW2AHB6SnV0sZDJWz26ZMncLTHm9Tft19ymk1XVTo9JFw3Gff4Kdm6ktjoS6GlKiErkKSylvJ5zBbqrOZXmruTYHQbrDqOPM7YbA/NMuuiGJmKljxhlIAG6apM3KAZ/DkO39Xl9dinqrQEN2Oq14PBt/1rsakvbF1xgq5f/zT68DJoOp+c3KJsn3OMI+h3qQ3w97gZ3eT0lvg12K5lb1/XZMW2oWXacu7V0+StitiNgz3HHd/QVXY5XeHNEhIVuDw5POuhmp2VnZ0Vzg4pVzOzqmamwkVNUpmsboc1Xyc58ndcTFBrR2WTl3F7ai10tIQAzULiVz68iCkOpwNHeSSk+/Ndqjlew/6wFEwlZ3fDptFZy+gCm0cfMnnaMrk2g3fI5lnL5lWWxZylh4y+bhm9YZofsvimZWFrecjm2/ZxTCUGZgdQrsvxftz+u5b9KC0Sa/3z9eWvob2WxHwTOLDuAbTvW2i/8M0fLC74/0fcur6pGz7MC7UMHiDFHTqghuyB5LhgJJ8N7FEd1TONNzC/OGfqpZRsY1UcAVvndNupXNHo2eXTvlTbfBsMBmeBeIzPoMc1aU5Rz8yF8fSFYbWWBu6x/bYzo7t7rTGhwo6fi4LGMTQrWOHGD3y87XH3zhkG24OzExu6265lduvIyxybZTmABqZLnzlNad5w09iXRq9U2V0LtMn2toJ5PNAbI/BflC+HvnluYNgOhRDJ/fY/TiT5ZYQKAAA= 3 | -------------------------------------------------------------------------------- /quickapi/readme.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | __QuickAPI__ is a simple tool for showing internal API. 4 | 5 | - this module reads informations from routes 6 | - supports schemas 7 | - you can add a route description into the route flags as `F.route('/api/something/', action, ['post', '// description']` 8 | 9 | --- 10 | 11 | - download and copy `quickapi.package` into the `/packages/` directory __or create a definition with:__ 12 | 13 | ```javascript 14 | var options = {}; 15 | 16 | // ==================================== 17 | // COMMON (OPTIONAL) 18 | // ==================================== 19 | 20 | // options.url = '/docs/'; 21 | // options.description = 'A simple description of QuickAPI.'; 22 | 23 | // ==================================== 24 | // CONTROLLERS (OPTIONAL) 25 | // ==================================== 26 | 27 | // options.controllers = { api: true, manager: true }; 28 | 29 | INSTALL('package', 'https://cdn.totaljs.com/quickapi.package', options); 30 | 31 | // OR UpToDate mechanism: 32 | UPTODATE('package', 'https://cdn.totaljs.com/quickapi.package', options, '1 week'); 33 | ``` -------------------------------------------------------------------------------- /quickapi/source/index.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright Peter Širka 3 | 4 | var OPT; 5 | 6 | exports.version = 'v1.0.0'; 7 | exports.install = function(options) { 8 | OPT = options; 9 | 10 | if (!OPT) 11 | OPT = {}; 12 | 13 | // options.controllers = { api: true, manager: true}; 14 | // options.url = '/docs/'; 15 | // options.description = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quia, at!'; 16 | 17 | OPT.url = U.path(OPT.url || '/docs/'); 18 | ROUTE('GET ' + OPT.url, view_index); 19 | }; 20 | 21 | exports.uninstall = function() { 22 | OPT = null; 23 | }; 24 | 25 | function view_index() { 26 | var self = this; 27 | self.memorize('quickapi', '2 minutes', DEBUG, function() { 28 | var output = {}; 29 | var builder = []; 30 | 31 | for (var i = 0; i < F.routes.web.length; i++) { 32 | var item = F.routes.web[i]; 33 | 34 | if (!item.owner.startsWith('controller' + (F.is4 ? '_' : '#')) || !item.method) 35 | continue; 36 | 37 | var controller = item.owner.substring(11); 38 | if (OPT.controllers && !OPT.controllers[controller]) 39 | continue; 40 | 41 | var obj = {}; 42 | obj.url = item.urlraw; 43 | obj.method = item.method; 44 | obj.authorize = item.flags2.authorize; 45 | obj.description = item.description; 46 | obj.upload = item.flags2.upload; 47 | 48 | if (item.schema && item.schema.length && (obj.method === 'POST' || obj.method === 'PUT')) { 49 | 50 | var schema = F.is4 ? GETSCHEMA((item.schema[0] ? (item.schema[0] + '/') : '') + item.schema[1]) : GETSCHEMA(item.schema[0], item.schema[1]); 51 | if (!schema) 52 | continue; 53 | 54 | obj.schema = []; 55 | 56 | var keys = Object.keys(schema.schema); 57 | for (var j = 0; j < keys.length; j++) { 58 | var key = keys[j]; 59 | var tmp = schema.schema[key]; 60 | var type; 61 | 62 | switch (tmp.type) { 63 | case 1: 64 | type = 'Integer'; 65 | break; 66 | case 2: 67 | type = 'Float'; 68 | break; 69 | case 3: 70 | type = 'String'; 71 | break; 72 | case 4: 73 | type = 'Boolean'; 74 | break; 75 | case 5: 76 | type = 'Date'; 77 | break; 78 | case 6: 79 | type = 'Object'; 80 | break; 81 | case 7: 82 | type = 'Schema: ' + tmp.raw; 83 | break; 84 | case 8: 85 | type = 'Enum: ' + JSON.stringify(tmp.raw); 86 | break; 87 | case 9: 88 | type = 'KeyValue: ' + JSON.stringify(tmp.raw); 89 | break; 90 | } 91 | 92 | obj.schema.push({ name: key, required: tmp.required, type: type, isArray: tmp.isArray }); 93 | } 94 | } 95 | 96 | if (output[controller]) 97 | output[controller].push(obj); 98 | else 99 | output[controller] = [obj]; 100 | } 101 | 102 | var keys = Object.keys(output); 103 | for (var i = 0; i < keys.length; i++) { 104 | var key = keys[i]; 105 | output[key].quicksort('url', false, 20); 106 | builder.push({ controller: key, routes: output[key] }); 107 | } 108 | 109 | builder.quicksort('controller'); 110 | builder.description = OPT.description; 111 | self.view('@quickapi/index', builder); 112 | }); 113 | } -------------------------------------------------------------------------------- /serverstatus/readme.md: -------------------------------------------------------------------------------- 1 | # Server Status 2 | 3 | __Unmaintained right now__. 4 | 5 | ## What is it? 6 | 7 | Server Status is a monitoring module for Total.js framework. With this you can get real time traffic datas from your application. Developed by David Horvath (dacr at dacr dot hu). License: MIT. 8 | 9 | ## Install 10 | 11 | Download **serverstatus.js** and copy it to **modules** folder in your application. 12 | 13 | ## Configuration 14 | 15 | You can configure some parameters in **config** file of your application (in root directory of Total.js framework). 16 | 17 | | Attributes | Values | Default values | Descriptions | 18 | | ------------------------- | ------------------------ |--------------- |-------------------------------------------------------------------------- | 19 | | serverStatusUrl | /anything | /server-status | | 20 | | serverStatusCleanInterval | Integer | 1 | Cache cleanup interval in second (1 = request/sec, 30 = request/30sec). | 21 | | serverStatusAllowedIPs | ["IP-ADDR1", "IP-ADDR2"] | | Block access to unconfigured IPs. Simple array. | 22 | | serverStatusSecretKey | random string | | You can limit the access with secret key. | 23 | 24 | **Example:** 25 | ``` 26 | serverStatusUrl : /myStatus 27 | serverStatusCleanInterval : 1 28 | serverStatusAllowedIPs : ["127.0.0.1"] 29 | serverStatusSecretKey : MY_SECRET_KEY 30 | ``` 31 | 32 | **serverStatusAllowedIPs** and **serverStatusSecretKey** can be used also in combination. 33 | 34 | ##### Warning: If you not configure this two security attributes then status will be on display for everyone. 35 | 36 | ## Usage 37 | 38 | 39 | **Get all data in JSON:** 40 | 41 | http://yourapp:8000/server-status 42 | 43 | **Use secret key:** 44 | 45 | http://yourapp:8000/server-status?key=MY_SECRET_KEY 46 | 47 | **Get one item in plaintext:** 48 | 49 | http://yourapp:8000/server-status?item=request 50 | 51 | http://yourapp:8000/server-status?key=MY_SECRET_KEY&item=PUT 52 | 53 | 54 | **Sample output:** 55 | 56 | ```JSON 57 | { 58 | "request": 0, 59 | "requestBegin": 0, 60 | "requestEnd": 0, 61 | "GET": 0, 62 | "POST": 0, 63 | "PUT": 0, 64 | "DELETE": 0, 65 | "uploadBegin": 0, 66 | "uploadEnd": 0, 67 | "websocket": 0, 68 | "websocketBegin": 0, 69 | "websocketEnd": 0, 70 | "numberOfIPs": 0 71 | } 72 | ``` 73 | 74 | ## Use with Zabbix 75 | 76 | Download **zabbix** folder. In monitored server add this line to **zabbix_agentd.conf** (/etc/zabbix/): 77 | 78 | ``` 79 | #Total.js 80 | UserParameter=totaljs.value[*], (curl -s "http://localhost:$1/server-status?key=MY_SECRET_KEY&item=$2") 81 | ``` 82 | 83 | In Zabbix webadmin import the **totaljs-server-status-template.xml** file and configure ports. 84 | 85 | ##### Note: This is a general template. You have to be configuration Zabbix. 86 | -------------------------------------------------------------------------------- /serverstatus/serverstatus.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | *Server Status module for Total.js. 3 | *With this you can get real time traffic datas from your application. 4 | ******************************************************************************* 5 | *MIT License 6 | * 7 | *Copyright (c) 2016 David Horvath 8 | * 9 | *Permission is hereby granted, free of charge, to any person obtaining a copy 10 | *of this software and associated documentation files (the "Software"), to deal 11 | *in the Software without restriction, including without limitation the rights 12 | *to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | *copies of the Software, and to permit persons to whom the Software is 14 | *furnished to do so, subject to the following conditions: 15 | * 16 | *The above copyright notice and this permission notice shall be included in all 17 | *copies or substantial portions of the Software. 18 | * 19 | *THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | *IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | *FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | *AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | *LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | *OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | *SOFTWARE. 26 | */ 27 | 28 | 29 | exports.id = 'serverStatus'; 30 | exports.version = '1.0.1'; 31 | 32 | /****************************************************** 33 | * Init datas 34 | *****************************************************/ 35 | var now = new Date(); 36 | var serverStatusCleanInterval = CONFIG('serverStatusCleanInterval') || 1; 37 | var serverStatusUrl = CONFIG('serverStatusUrl') || '/server-status'; 38 | var serverStatusStats = { 39 | request: { 40 | count: 0, 41 | timestamp: now 42 | }, 43 | requestBegin: { 44 | count: 0, 45 | timestamp: now 46 | }, 47 | requestEnd: { 48 | count: 0, 49 | timestamp: now 50 | }, 51 | GET: { 52 | count: 0, 53 | timestamp: now 54 | }, 55 | POST: { 56 | count: 0, 57 | timestamp: now 58 | }, 59 | PUT: { 60 | count: 0, 61 | timestamp: now 62 | }, 63 | DELETE: { 64 | count: 0, 65 | timestamp: now 66 | }, 67 | uploadBegin: { 68 | count: 0, 69 | timestamp: now 70 | }, 71 | uploadEnd: { 72 | count: 0, 73 | timestamp: now 74 | }, 75 | websocket: { 76 | count: 0, 77 | timestamp: now 78 | }, 79 | websocketBegin: { 80 | count: 0, 81 | timestamp: now 82 | }, 83 | websocketEnd: { 84 | count: 0, 85 | timestamp: now 86 | }, 87 | numberOfIPs: [] 88 | }; 89 | 90 | /****************************************************** 91 | * Events & routes 92 | *****************************************************/ 93 | exports.install = function (options) { 94 | F.on('request', requestInc); 95 | F.on('request-begin', requestBeginInc); 96 | F.on('request-end', requestEndInc); 97 | F.on('upload-begin', uploadBeginInc); 98 | F.on('upload-end', uploadEndInc); 99 | F.on('websocket', websocketInc); 100 | F.on('websocket-begin', websocketBeginInc); 101 | F.on('websocket-end', websocketEndInc); 102 | F.route('/server-status', serverStatus); 103 | }; 104 | 105 | 106 | /****************************************************** 107 | * Increment counters 108 | *****************************************************/ 109 | function requestInc(req, res) { 110 | now = new Date(); 111 | if (now.diff(serverStatusStats.request.timestamp, 'seconds') > serverStatusCleanInterval) { 112 | serverStatusStats.request.count = 1; 113 | serverStatusStats.request.timestamp = now; 114 | serverStatusStats[req.method].count = 1; 115 | serverStatusStats[req.method].timestamp = now; 116 | } else { 117 | serverStatusStats.request.count += 1; 118 | serverStatusStats[req.method].count += 1; 119 | } 120 | numberOfIPs(req.ip); 121 | } 122 | 123 | function requestBeginInc(req, res) { 124 | now = new Date(); 125 | if (now.diff(serverStatusStats.requestBegin.timestamp, 'seconds') > serverStatusCleanInterval) { 126 | serverStatusStats.requestBegin.count = 1; 127 | serverStatusStats.requestBegin.timestamp = now; 128 | } else { 129 | serverStatusStats.requestBegin.count += 1; 130 | } 131 | numberOfIPs(req.ip); 132 | } 133 | 134 | function requestEndInc(req, res) { 135 | now = new Date(); 136 | if (now.diff(serverStatusStats.requestEnd.timestamp, 'seconds') > serverStatusCleanInterval) { 137 | serverStatusStats.requestEnd.count = 1; 138 | serverStatusStats.requestEnd.timestamp = now; 139 | } else { 140 | serverStatusStats.requestEnd.count += 1; 141 | } 142 | numberOfIPs(req.ip); 143 | } 144 | 145 | function uploadBeginInc(req, res) { 146 | now = new Date(); 147 | if (now.diff(serverStatusStats.uploadBegin.timestamp, 'seconds') > serverStatusCleanInterval) { 148 | serverStatusStats.uploadBegin.count = 1; 149 | serverStatusStats.uploadBegin.timestamp = now; 150 | } else { 151 | serverStatusStats.uploadBegin.count += 1; 152 | } 153 | numberOfIPs(req.ip); 154 | } 155 | 156 | function uploadEndInc(req, res) { 157 | now = new Date(); 158 | if (now.diff(serverStatusStats.uploadEnd.timestamp, 'seconds') > serverStatusCleanInterval) { 159 | serverStatusStats.uploadEnd.count = 1; 160 | serverStatusStats.uploadEnd.timestamp = now; 161 | } else { 162 | serverStatusStats.uploadEnd.count += 1; 163 | } 164 | numberOfIPs(req.ip); 165 | } 166 | 167 | function websocketInc(req, res) { 168 | now = new Date(); 169 | if (now.diff(serverStatusStats.websocket.timestamp, 'seconds') > serverStatusCleanInterval) { 170 | serverStatusStats.websocket.count = 1; 171 | serverStatusStats.websocket.timestamp = now; 172 | } else { 173 | serverStatusStats.websocket.count += 1; 174 | } 175 | numberOfIPs(req.ip); 176 | } 177 | 178 | function websocketBeginInc(req, res) { 179 | now = new Date(); 180 | if (now.diff(serverStatusStats.websocketBegin.timestamp, 'seconds') > serverStatusCleanInterval) { 181 | serverStatusStats.websocketBegin.count = 1; 182 | serverStatusStats.websocketBegin.timestamp = now; 183 | } else { 184 | serverStatusStats.websocketBegin.count += 1; 185 | } 186 | numberOfIPs(req.ip); 187 | } 188 | 189 | function websocketEndInc(req, res) { 190 | now = new Date(); 191 | if (now.diff(serverStatusStats.websocketEnd.timestamp, 'seconds') > serverStatusCleanInterval) { 192 | serverStatusStats.websocketEnd.count = 1; 193 | serverStatusStats.websocketEnd.timestamp = now; 194 | } else { 195 | serverStatusStats.websocketEnd.count += 1; 196 | } 197 | numberOfIPs(req.ip); 198 | } 199 | 200 | /****************************************************** 201 | * Update cache of users' IPs 202 | *****************************************************/ 203 | function numberOfIPs(ip) { 204 | var index = serverStatusStats.numberOfIPs.findIndex('ip', ip); 205 | now = new Date(); 206 | 207 | if (index < 0) { 208 | serverStatusStats.numberOfIPs.push({ 209 | ip: ip, 210 | timestamp: now 211 | }); 212 | } else { 213 | serverStatusStats.numberOfIPs[index].timestamp = now; 214 | } 215 | } 216 | 217 | /****************************************************** 218 | * Get server status 219 | *****************************************************/ 220 | function serverStatus() { 221 | 222 | //check allowed IPs and secret key 223 | if ((CONFIG('serverStatusSecretKey') && (CONFIG('serverStatusSecretKey') !== this.query.key)) || (CONFIG('serverStatusAllowedIPs') && JSON.parse(CONFIG('serverStatusAllowedIPs')).indexOf(this.req.ip) < 0)) { 224 | this.throw403(); 225 | return; 226 | } 227 | 228 | //make 'self' from 'this' to async functions 229 | var self = this; 230 | 231 | async(function* () { 232 | now = new Date(); 233 | 234 | //cleaning cache of users' IPs 235 | serverStatusStats.numberOfIPs.forEach(function (item, index) { 236 | if (now.diff(item.timestamp, 'seconds') > serverStatusCleanInterval) { 237 | serverStatusStats.numberOfIPs.splice(index, 1); 238 | } 239 | }); 240 | 241 | //cleaning counters 242 | for (item in serverStatusStats) { 243 | if (now.diff(serverStatusStats[item].timestamp, 'seconds') > serverStatusCleanInterval) { 244 | serverStatusStats[item].count = 0; 245 | } 246 | } 247 | 248 | })(function (err) { 249 | //response: one value 250 | if (self.query.item) { 251 | if (self.query.item === "numberOfIPs") { 252 | self.plain(serverStatusStats.numberOfIPs.length || 0); 253 | return; 254 | } 255 | self.plain(serverStatusStats[self.query.item].count.toString() + '\n'); 256 | return; 257 | } 258 | 259 | //response: all values 260 | self.json({ 261 | request: serverStatusStats.request.count, 262 | requestBegin: serverStatusStats.requestBegin.count, 263 | requestEnd: serverStatusStats.requestEnd.count, 264 | GET: serverStatusStats.GET.count, 265 | POST: serverStatusStats.POST.count, 266 | PUT: serverStatusStats.PUT.count, 267 | DELETE: serverStatusStats.DELETE.count, 268 | uploadBegin: serverStatusStats.uploadBegin.count, 269 | uploadEnd: serverStatusStats.uploadEnd.count, 270 | websocket: serverStatusStats.websocket.count, 271 | websocketBegin: serverStatusStats.websocketBegin.count, 272 | websocketEnd: serverStatusStats.websocketEnd.count, 273 | numberOfIPs: serverStatusStats.numberOfIPs.length || 0 274 | }, true); 275 | }); 276 | } 277 | -------------------------------------------------------------------------------- /session/readme.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | This module handles a user session. 4 | 5 | - download and copy `session.js` into the `/modules/` directory __or create a definition with:__ 6 | 7 | ```javascript 8 | var options = {}; 9 | 10 | // Name of cookie 11 | // options.cookie = '__ssid'; 12 | 13 | // Secret for encrypt/decrypt 14 | // options.secret = 'N84'; 15 | 16 | // Timeout 17 | // options.timeout = '5 minutes'; 18 | 19 | INSTALL('module', 'https://modules.totaljs.com/latest/session.js', options); 20 | // UNINSTALL('module', 'session'); 21 | ``` 22 | 23 | ## Usage in Controller 24 | 25 | - session is a middleware 26 | - session is automatically loaded 27 | - session is automatically saved after is response finished 28 | 29 | ```js 30 | exports.install = function(options) { 31 | 32 | // IMPORTANT: #session is a middleware 33 | // You can specify Session into all routes: 34 | ROUTE('/', some_action_in_controller, ['#session']); 35 | 36 | // or 37 | WEBSOCKET('/', some_action_in_controller, ['#session']); 38 | 39 | // or for all routes use (this is global middleware for all requests): 40 | // framework.use('session'); 41 | }; 42 | 43 | 44 | function some_action_in_controller() { 45 | var self = this; 46 | 47 | if (!self.session.counter) 48 | self.session.counter = 0; 49 | 50 | self.session.counter++; 51 | self.view('some-view'); 52 | }; 53 | 54 | ``` 55 | 56 | ## REDIS as session storage (example) 57 | 58 | > Create some definition file, example: __session-redis.js__ 59 | 60 | ```js 61 | ON('module#session', function(type, name) { 62 | 63 | var session = MODULE('session').instance; 64 | 65 | session.onRead = function(id, callback) { 66 | 67 | // id = session ID === user ID === browser ID 68 | // callback(value) = return value from the storage 69 | 70 | redis_client.get('session_' + id, function(err, reply) { 71 | callback(err ? {} : JSON.parse(reply.toString())); 72 | }); 73 | 74 | }; 75 | 76 | session.onWrite = function(id, value) { 77 | 78 | // id = session ID === user ID === browser ID 79 | // value = session state 80 | 81 | redis_client.set('session_' + id, JSON.stringify(value)); 82 | 83 | }; 84 | }); 85 | ``` -------------------------------------------------------------------------------- /session/session.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright Peter Širka 3 | 4 | const USERAGENT = 11; 5 | const VERSION = 'v2.1.0'; 6 | const EMPTY_USAGE = {}; 7 | 8 | var SUGAR = 'XY1'; 9 | var stats_read = 0; 10 | var stats_write = 0; 11 | 12 | function Session() { 13 | var t = this; 14 | t.options = null; 15 | t.onRead = (id, callback) => callback(F.cache.read2(id)); 16 | t.onWrite = (id, value) => F.cache.add(id, value, t.options.timeout); 17 | U.EventEmitter2.extend(t); 18 | } 19 | 20 | Session.prototype._read = function(req, res, next) { 21 | 22 | var self = this; 23 | var id = req.cookie(self.options.cookie) || ''; 24 | 25 | if (!id.length) { 26 | self._create(res, req, next); 27 | return self; 28 | } 29 | 30 | var obj = F.decrypt(id, self.options.secret); 31 | if (!obj) { 32 | self._create(res, req, next); 33 | return self; 34 | } 35 | 36 | if ('ssid_' + obj.sign !== self._signature(obj.id, req)) { 37 | self._create(res, req, next); 38 | return self; 39 | } 40 | 41 | req.sessionid = obj.id; 42 | req._session = self; 43 | 44 | stats_read++; 45 | 46 | self.onRead(obj.id, function(session) { 47 | self.$events && self.$events.read && self.emit('read', req.sessionid, session); 48 | req.session = session || {}; 49 | next(); 50 | }); 51 | 52 | return self; 53 | }; 54 | 55 | Session.prototype._signature = function(id, req) { 56 | return id + '|' + req.ip.replace(/\./g, '') + '|' + (req.headers['user-agent'] || '').substring(0, USERAGENT).replace(/\s|\./g, ''); 57 | }; 58 | 59 | Session.prototype._create = function(res, req, next) { 60 | 61 | var self = this; 62 | var id = U.GUID(10); 63 | var obj = { id: 'ssid_' + id, sign: self._signature(id, req) }; 64 | var json = F.encrypt(obj, self.options.secret); 65 | 66 | req.sessionid = obj.id; 67 | req._session = self; 68 | req.session = {}; 69 | res && res.statusCode && res.cookie(self.options.cookie, json); 70 | 71 | next(); 72 | return self; 73 | }; 74 | 75 | Session.prototype._write = function(id, obj) { 76 | var self = this; 77 | stats_write++; 78 | self.$events && self.$events.write && self.emit('write', id, obj); 79 | self.onWrite && self.onWrite(id, obj); 80 | return self; 81 | }; 82 | 83 | var session = new Session(); 84 | 85 | exports.name = 'session'; 86 | exports.version = VERSION; 87 | exports.instance = session; 88 | 89 | exports.usage = function() { 90 | EMPTY_USAGE.read = stats_read; 91 | EMPTY_USAGE.write = stats_write; 92 | return EMPTY_USAGE; 93 | }; 94 | 95 | exports.install = function(options) { 96 | 97 | SUGAR = (CONF.name + CONF.version + SUGAR).replace(/\s/g, ''); 98 | session.options = U.extend({ cookie: '__ssid', secret: 'N84', timeout: '5 minutes' }, options, true); 99 | 100 | MIDDLEWARE('session', function(req, res, next) { 101 | if (res.statusCode) 102 | res.once('finish', () => session._write(req.sessionid, req.session)); 103 | else 104 | res.socket.on('close', () => session._write(req.sessionid, req.session)); 105 | session._read(req, res, next); 106 | }); 107 | }; 108 | 109 | exports.uninstall = function() { 110 | UNINSTALL('middleware', 'session'); 111 | session = null; 112 | }; -------------------------------------------------------------------------------- /sshbackup/readme.md: -------------------------------------------------------------------------------- 1 | [![Support](https://www.totaljs.com/img/button-support.png)](https://www.totaljs.com/support/) [![Donate](https://www.totaljs.com/img/button-donate.png)](https://www.totaljs.com/#make-a-donation) 2 | 3 | # Simple module for backing up files to remote server over sftp (ssh) 4 | 5 | 6 | ## Installation 7 | 8 | - download the module `sshbackup.js` 9 | - add it into the modules in your application directory 10 | 11 | ```html 12 | /your-application-directory/modules/sshbackup.js 13 | ``` 14 | Install `ssh2` module from npm using `npm install ssh2` 15 | 16 | ## Initialization 17 | 18 | Paste the code bellow to some definition file. 19 | 20 | ```javascript 21 | MODULE('ssh-backup').init({ 22 | host: '1.2.3.4', 23 | port: 22, 24 | username: 'username', 25 | password: 'password' 26 | }); 27 | 28 | // or using privateKey 29 | 30 | MODULE('ssh-backup').init({ 31 | host: '1.2.3.4', 32 | port: 22, 33 | username: 'username', 34 | privateKey: require('fs').readFileSync('/path/to/the/key') 35 | }); 36 | ``` 37 | 38 | ## Usage 39 | 40 | ```javascript 41 | var source = '/local/path/to/the/file.json'; 42 | var dest = '/remote/path/to/the/file.json'; 43 | 44 | MODULE('ssh-backup').backup(source, dest, function(err){ 45 | console.log('DONE', err); 46 | }); 47 | ``` 48 | -------------------------------------------------------------------------------- /sshbackup/sshbackup.js: -------------------------------------------------------------------------------- 1 | const Fs = require('fs'); 2 | const Ssh2 = require('ssh2'); 3 | 4 | var options = {}; 5 | 6 | exports.init = function(opts) { 7 | 8 | if (!opts || !opts.host || !opts.port || !opts.username) 9 | console.log('MODULE(ssh-backup) Error: missing options'); 10 | 11 | if (!opts.privateKey && !opts.password) 12 | console.log('MODULE(ssh-backup) Error: missing privateKey or password'); 13 | 14 | options = opts; 15 | }; 16 | 17 | exports.backup = function(target, destination, callback) { 18 | 19 | if (!target || !destination) 20 | return callback('MODULE(ssh-backup) Error: missing `target` or `destination`'); 21 | 22 | var conn = new Ssh2(); 23 | 24 | conn.on('ready', function() { 25 | 26 | conn.sftp(function(err, sftp) { 27 | 28 | if (err) { 29 | callback && callback(err); 30 | return; 31 | } 32 | 33 | var writer = sftp.createWriteStream(destination); 34 | 35 | writer.on('close', function() { 36 | sftp.end(); 37 | callback && callback(); 38 | }); 39 | 40 | Fs.createReadStream(target).pipe(writer); 41 | }); 42 | }); 43 | 44 | callback && conn.on('error', callback); 45 | conn.connect(options); 46 | }; 47 | -------------------------------------------------------------------------------- /tape/readme.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | The module can store files in a single file structure with various custom object metadata. The files are stored in order. You can't change meta data because it's required to "reindex" content. You can obtain a file identifier using the "append()" method or the "ls()" method. The file identifier is collected from the file offset, meta data size, and file size. Therefore, modifying files can affect identifiers. 4 | 5 | __Requirements__: 6 | 7 | - Total.js `+v5.0.0` 8 | 9 | --- 10 | 11 | - download module `tape.js` 12 | - copy it to `yourapp/modules/tape.js` 13 | 14 | --- 15 | 16 | ```js 17 | var tape = MODS.tape.create('filename.tape'); 18 | 19 | // [Append file]: 20 | // tape.append(buffer/filename, {}, callback) 21 | // await tape.append('logo.png', { name: 'logo.png', user: 'petersirka' }); 22 | 23 | // [Listing]: 24 | // tape.ls(callback); 25 | // var list = await tape.ls(); 26 | 27 | // [Read file stream]: 28 | // tape.stream(id, callback); 29 | // var stream = await tape.stream('433687X52X152'); 30 | // stream.on('data', chunk => console.log(chunk.toString('utf8'))); 31 | 32 | // [Read file meta data]: 33 | // tape.read(id, callback); 34 | // var meta = await tape.read('433687X52X152'); 35 | // console.log(meta); 36 | 37 | // [Remove file (soft remove)]: 38 | // tape.remove(id, callback); 39 | // await tape.remove('433687X52X152'); 40 | 41 | // [Clear removed files]: 42 | // tape.clear(callback); 43 | // await tape.clear(); 44 | ``` -------------------------------------------------------------------------------- /tape/tape.js: -------------------------------------------------------------------------------- 1 | // [4 bytes] | [4 bytes] | [2 bytes] | [info data] | [file data] | [4 bytes] (due to reverse read) 2 | // size_info | size_file | state | {...json...} | ... binary data ... | total_size 3 | // size_info | size_file | state | {...json...} | ... binary data ... | total_size 4 | // size_info | size_file | state | {...json...} | ... binary data ... | total_size 5 | // size_info | size_file | state | {...json...} | ... binary data ... | total_size 6 | 7 | const Fs = Total.Fs; 8 | 9 | function Tape(filename) { 10 | let t = this; 11 | t.filename = filename; 12 | t.operation = 0; // 1: appending, 2: removing, 3: clearing 13 | } 14 | 15 | function parseid(id) { 16 | let arr = id.split('X'); 17 | return { offset: +arr[0], info: +arr[1], file: +arr[2] }; 18 | } 19 | 20 | Tape.prototype.check = function(id, callback) { 21 | 22 | let t = this; 23 | 24 | Fs.open(t.filename, 'r', function(err, fd) { 25 | 26 | let meta = typeof(id) === 'string' ? parseid(id) : id; 27 | let buffer = Buffer.alloc(14 + meta.info); 28 | 29 | Fs.read(fd, buffer, 0, buffer.length, meta.offset, function(err, bytes) { 30 | 31 | if (bytes) { 32 | 33 | Fs.close(fd, NOOP); 34 | 35 | var size_info = buffer.readInt32BE(0); 36 | var size_file = buffer.readInt32BE(4); 37 | var state = buffer.readInt8(8); 38 | var checksum = buffer.readInt32BE(10); 39 | var sum = size_file + size_info + state; 40 | 41 | if (checksum !== sum) { 42 | callback('Invalid identifier'); 43 | return; 44 | } 45 | 46 | var info = buffer.toString('utf8', 14, size_info + 14); 47 | if (info[0] === '{' && info[info.length - 1] === '}') { 48 | // correct 49 | meta.meta = info; 50 | meta.state = state; 51 | callback(null, meta); 52 | } else 53 | callback('Invalid identifier'); 54 | } 55 | 56 | }); 57 | }); 58 | }; 59 | 60 | Tape.prototype.read = function(id, callback) { 61 | 62 | let t = this; 63 | 64 | if (!callback) 65 | return new Promise((resolve, reject) => t.read(id, (err, response) => err ? reject(err) : resolve(response))); 66 | 67 | let meta = parseid(id); 68 | 69 | t.check(meta, function(err, info) { 70 | 71 | if (err) { 72 | callback(err); 73 | return; 74 | } 75 | 76 | if (!info.state) { 77 | callback('Invalid identifier'); 78 | return; 79 | } 80 | 81 | let data = info.meta.parseJSON(true); 82 | data.id = id; 83 | data.size = info.file; 84 | 85 | callback(null, data); 86 | }); 87 | }; 88 | 89 | function append(t, meta, buffer_file, callback) { 90 | 91 | let buffer_meta = Buffer.alloc(14); 92 | let buffer_info = Buffer.from(JSON.stringify(meta), 'utf8'); 93 | let buffer_total = Buffer.alloc(4); 94 | 95 | buffer_meta.writeInt32BE(buffer_info.length, 0); 96 | buffer_meta.writeInt32BE(buffer_file.length, 4); 97 | buffer_meta.writeInt8(1, 8); // 0: removed; 1: active; 2: active compressed; 98 | buffer_meta.writeInt32BE(buffer_info.length + buffer_file.length + 1, 10); // a mini checksum 99 | 100 | // Due to reverse reading 101 | buffer_total.writeInt32BE(buffer_meta.length + buffer_info.length + buffer_file.length); 102 | 103 | // 4 bytes info size, 4 bytes file size, 2 bytes state, 4 bytes checksum, info_buffer, file_buffer, 4 bytes info about total size 104 | let buffer = Buffer.concat([buffer_meta, buffer_info, buffer_file, buffer_total]); 105 | 106 | Fs.appendFile(t.filename, buffer, function() { 107 | Fs.lstat(t.filename, function(err, stat) { 108 | meta.id = (stat.size - buffer_info.length - buffer_file.length - 14) + 'X' + buffer_info.length + 'X' + buffer_file.length; 109 | meta.size = buffer_file.length; 110 | callback && callback(null, meta); 111 | t.operation = 0; 112 | }); 113 | }); 114 | 115 | } 116 | 117 | function delay_append(t, filename, meta, callback, attemp) { 118 | t.append(filename, meta, callback, attemp); 119 | } 120 | 121 | Tape.prototype.add = Tape.prototype.append = function(filename, meta, callback, attemp) { 122 | 123 | let t = this; 124 | 125 | if (!callback) { 126 | return new Promise(function(resolve, reject) { 127 | t.append(filename, meta, function(err, response) { 128 | if (err) 129 | reject(err); 130 | else 131 | resolve(response); 132 | }); 133 | }); 134 | } 135 | 136 | if (t.operation) { 137 | 138 | if (!attemp || attemp < 10) { 139 | setTimeout(delay_append, 200, t, filename, meta, callback, (attemp || 0) + 1); 140 | return; 141 | } 142 | 143 | callback('The file is locked for with another operation'); 144 | return; 145 | } 146 | 147 | t.operation = 1; 148 | 149 | if (filename instanceof Buffer) { 150 | append(t, meta, filename, callback); 151 | return; 152 | } 153 | 154 | Fs.lstat(filename, function(err, stat) { 155 | 156 | if (!meta.name) 157 | meta.name = U.getName(filename); 158 | 159 | meta.date = stat.birthtime || stat.mtime; 160 | 161 | Fs.readFile(filename, function(err, buffer_file) { 162 | 163 | if (err) { 164 | t.operation = 0; 165 | callback(err); 166 | } else 167 | append(t, meta, buffer_file, callback); 168 | 169 | }); 170 | }); 171 | 172 | }; 173 | 174 | Tape.prototype.cursor = function(onitem, ondone) { 175 | // @TODO: missing reverse mode 176 | readtape(this.filename, onitem, ondone); 177 | }; 178 | 179 | Tape.prototype.stream = function(id, callback) { 180 | 181 | let t = this; 182 | 183 | if (!callback) 184 | return new Promise((resolve, reject) => t.stream(id, (err, response) => err ? reject(err) : resolve(response))); 185 | 186 | let meta = parseid(id); 187 | 188 | t.check(meta, function(err, info) { 189 | 190 | if (err) { 191 | callback(err); 192 | return; 193 | } 194 | 195 | if (!info.state) { 196 | callback('Invalid identifier'); 197 | return; 198 | } 199 | 200 | let stream = Fs.createReadStream(t.filename, { start: meta.offset + meta.info + 10, end: meta.offset + meta.info + meta.file + 9 }); 201 | callback(null, stream); 202 | }); 203 | }; 204 | 205 | Tape.prototype.buffer = function(id, callback) { 206 | 207 | let t = this; 208 | 209 | if (!callback) 210 | return new Promise((resolve, reject) => t.buffer(id, (err, response) => err ? reject(err) : resolve(response))); 211 | 212 | let meta = parseid(id); 213 | 214 | t.check(meta, function(err, info) { 215 | 216 | if (err) { 217 | callback(err); 218 | return; 219 | } 220 | 221 | if (!info.state) { 222 | callback('Invalid identifier'); 223 | return; 224 | } 225 | 226 | let buffer = []; 227 | let stream = Fs.createReadStream(t.filename, { start: meta.offset + meta.info + 10, end: meta.offset + meta.info + meta.file + 9 }); 228 | stream.on('data', chunk => buffer.push(chunk)); 229 | stream.on('error', callback); 230 | stream.on('end', () => callback(null, Buffer.concat(buffer))); 231 | }); 232 | }; 233 | 234 | function delay_remove(t, id, callback, attemp) { 235 | t.remove(id, callback, attemp); 236 | } 237 | 238 | Tape.prototype.rm = Tape.prototype.remove = function(id, callback, attemp) { 239 | 240 | let t = this; 241 | 242 | if (!callback) 243 | return new Promise((resolve, reject) => t.remove(id, (err, response) => err ? reject(err) : resolve(response))); 244 | 245 | if (t.operation) { 246 | 247 | if (!attemp || attemp < 10) { 248 | setTimeout(delay_remove, 200, t, id, callback, (attemp || 0) + 1); 249 | return; 250 | } 251 | 252 | callback('The file is locked for with another operation'); 253 | return; 254 | } 255 | 256 | var info = parseid(id); 257 | 258 | t.check(info, function(err, meta) { 259 | 260 | if (err) { 261 | callback(err); 262 | return; 263 | } 264 | 265 | if (!meta.state) { 266 | callback('Invalid identifier'); 267 | return; 268 | } 269 | 270 | t.operation = 2; 271 | 272 | Fs.open(t.filename, 'r+', function(err, fd) { 273 | 274 | t.operation = 0; 275 | 276 | if (err) { 277 | callback(err); 278 | return; 279 | } 280 | 281 | var buffer = Buffer.alloc(6); 282 | buffer.writeInt8(0); 283 | buffer.writeInt32BE(meta.info + meta.file + 0, 2); // checksum 284 | 285 | // (4 bytes info size, 4 bytes file size) = 8, 2 bytes state, 4 bytes checksum 286 | Fs.write(fd, buffer, 0, buffer.length, info.offset + 8, function() { 287 | Fs.close(fd, NOOP); 288 | callback(null, meta); 289 | }); 290 | }); 291 | }); 292 | 293 | }; 294 | 295 | function readtape(filename, onitem, onclose) { 296 | 297 | Fs.open(filename, 'r', function(err, fd) { 298 | 299 | var read = function(offset) { 300 | 301 | let buffer = Buffer.alloc(14); 302 | 303 | Fs.read(fd, buffer, 0, buffer.length, offset, function(err, bytes) { 304 | 305 | if (!bytes) { 306 | // nothing (end of file) 307 | Fs.close(fd, NOOP); 308 | onclose && onclose(); 309 | return; 310 | } 311 | 312 | let size_info = buffer.readInt32BE(0); 313 | let size_file = buffer.readInt32BE(4); 314 | let state = buffer.readInt8(8); 315 | let checksum = buffer.readInt32BE(10); 316 | let sum = size_file + size_info + state; 317 | 318 | // Removed, skip 319 | if (checksum !== sum || !state) { 320 | read(offset + size_info + size_file + 14 + 4); 321 | return; 322 | } 323 | 324 | let buffer_info = Buffer.alloc(size_info); 325 | 326 | Fs.read(fd, buffer_info, 0, buffer_info.length, offset + 14, function() { 327 | 328 | // @TODO: missing error handling 329 | 330 | let item = buffer_info.toString('utf8').parseJSON(true); 331 | item.size = size_file; 332 | item.id = offset + 'X' + size_info + 'X' + size_file; 333 | 334 | onitem(item, function(cancel) { 335 | 336 | if (cancel) { 337 | Fs.close(fd, NOOP); 338 | onclose && onclose(); 339 | return; 340 | } 341 | 342 | read(offset + size_info + size_file + 14 + 4); 343 | }); 344 | 345 | }); 346 | 347 | }); 348 | }; 349 | 350 | read(0); 351 | }); 352 | } 353 | 354 | Tape.prototype.ls = function(callback) { 355 | 356 | let t = this; 357 | 358 | if (!callback) 359 | return new Promise((resolve, reject) => t.ls((err, response) => err ? reject(err) : resolve(response))); 360 | 361 | let arr = []; 362 | 363 | readtape(t.filename, function(item, next) { 364 | arr.push(item); 365 | next(); 366 | }, () => callback(null, arr)); 367 | 368 | }; 369 | 370 | Tape.prototype.clear = function(callback) { 371 | 372 | let t = this; 373 | 374 | if (!callback) 375 | return new Promise((resolve, reject) => t.clear((err, response) => err ? reject(err) : resolve(response))); 376 | 377 | if (t.operation) { 378 | callback('The file is locked for with another operation'); 379 | return; 380 | } 381 | 382 | t.operation = 4; 383 | Fs.unlink(t.filename, function(err) { 384 | t.operation = 0; 385 | callback(); 386 | }); 387 | }; 388 | 389 | Tape.prototype.clean = function(callback) { 390 | 391 | let t = this; 392 | 393 | if (!callback) 394 | return new Promise((resolve, reject) => t.clean((err, response) => err ? reject(err) : resolve(response))); 395 | 396 | if (t.operation) { 397 | callback('The file is locked for with another operation'); 398 | return; 399 | } 400 | 401 | t.operation = 3; 402 | 403 | let writer = Fs.createWriteStream(t.filename + '.tmp'); 404 | let removed = 0; 405 | 406 | writer.on('close', function() { 407 | Fs.rename(t.filename + '.tmp', t.filename, function(err) { 408 | t.operation = 0; 409 | callback(err, removed); 410 | }); 411 | }); 412 | 413 | Fs.open(t.filename, 'r', function(err, fd) { 414 | 415 | var read = function(offset) { 416 | 417 | let buffer = Buffer.alloc(14); 418 | 419 | Fs.read(fd, buffer, 0, buffer.length, offset, function(err, bytes) { 420 | 421 | if (!bytes) { 422 | writer.end(); 423 | Fs.close(fd, NOOP); 424 | return; 425 | } 426 | 427 | let size_info = buffer.readInt32BE(0); 428 | let size_file = buffer.readInt32BE(4); 429 | let state = buffer.readInt8(8); 430 | let checksum = buffer.readInt32BE(10); 431 | let sum = size_file + size_info + state; 432 | 433 | if (checksum !== sum || !state) { 434 | // skip item 435 | removed++; 436 | read(offset + 14 + size_info + size_file + 4); 437 | return; 438 | } 439 | 440 | // In memory copy 441 | let data = Buffer.alloc(14 + size_info + size_file + 4); 442 | 443 | Fs.read(fd, data, 0, data.length, offset, function(err, bytes) { 444 | if (bytes) { 445 | writer.write(data); 446 | read(offset + data.length); 447 | } 448 | }); 449 | 450 | }); 451 | }; 452 | 453 | read(0); 454 | }); 455 | }; 456 | 457 | exports.create = filename => new Tape(filename); -------------------------------------------------------------------------------- /tapi/readme.md: -------------------------------------------------------------------------------- 1 | # TAPI 2 | 3 | This module will list availiable Total.js API endpoints. The endpoint can be used in the Total.js Flow directly in the TAPI component. Total.js App must implement Total.js API protocol. 4 | 5 | ## Total.js 5 6 | 7 | - File: `tapi5.js` 8 | 9 | __Configuration__: 10 | 11 | - `CONF.tapi {String}` optional, meta data endpoint (default: `/tapi/`) 12 | - `CONF.$api {String}` optional, a default endpoint for the app API (default: all API endpoints) 13 | 14 | ## Total.js 4 15 | 16 | - File: `tapi4.js` 17 | 18 | __Configuration__: 19 | 20 | - `CONF.tapi {String}` optional, meta data endpoint (default: `/tapi/`) 21 | - `CONF.tapiendpoint {String}` optional, a default endpoint for the app API (default: all API endpoints) -------------------------------------------------------------------------------- /tapi/tapi4.js: -------------------------------------------------------------------------------- 1 | exports.install = function() { 2 | ROUTE('GET ' + (CONF.tapi || '/tapi/'), api); 3 | }; 4 | 5 | function api() { 6 | 7 | var $ = this; 8 | 9 | if (TEMP.TAPI) { 10 | $.json(TEMP.TAPI); 11 | return; 12 | } 13 | 14 | var items = []; 15 | var output = []; 16 | 17 | EACHSCHEMA(function(name, schema) { 18 | 19 | for (var key in schema.actions) { 20 | var action = schema.actions[key]; 21 | if (action.public) 22 | items.push({ action: key, schema: name, icon: action.icon, name: action.name, params: action.params, input: action.input, ouptut: action.ouptut, query: action.query, summary: action.summary }); 23 | } 24 | 25 | }); 26 | 27 | for (var a in F.routes.api) { 28 | var actions = F.routes.api[a]; 29 | for (var b in actions) { 30 | var action = actions[b]; 31 | for (var i = 0; i < items.length; i++) { 32 | var m = items[i]; 33 | 34 | if (CONF.tapiendpoint && action.url !== CONF.tapiendpoint) 35 | continue; 36 | 37 | if (action.action.indexOf(' ' + m.action) !== -1 && action.action.indexOf(m.schema + ' ') !== -1) { 38 | items.splice(i, 1); 39 | m.id = action.name; 40 | m.url = $.hostname(action.url); 41 | m.action = undefined; 42 | m.schema = undefined; 43 | output.push(m); 44 | break; 45 | } 46 | } 47 | } 48 | } 49 | 50 | TEMP.TAPI = output; 51 | $.json(output); 52 | } -------------------------------------------------------------------------------- /tapi/tapi5.js: -------------------------------------------------------------------------------- 1 | exports.install = function() { 2 | ROUTE('GET ' + (CONF.tapi || '/tapi/'), api); 3 | }; 4 | 5 | function api($) { 6 | 7 | if (TEMP.TAPI) { 8 | $.json(TEMP.TAPI); 9 | return; 10 | } 11 | 12 | var routes = F.sourcemap().routes; 13 | var response = []; 14 | var url = $.hostname(CONF.$api); 15 | 16 | for (var item of routes) { 17 | if (item.public && item.url === CONF.$api) 18 | response.push({ id: item.id, url: url, name: item.name, summary: item.summary, params: item.params, query: item.query, input: item.input, output: item.output }); 19 | } 20 | 21 | TEMP.TAPI = response; 22 | $.json(TEMP.TAPI); 23 | } -------------------------------------------------------------------------------- /totalaccount/readme.md: -------------------------------------------------------------------------------- 1 | # Total.js Account 2 | 3 | This module handles user profiles via Total.js Account https://account.totaljs.com. 4 | 5 | - __IMPORTANT__: it works only with the Total.js v5 6 | - the `/auth/` endpoint performs sign-in or obtains a session 7 | - the `$.user` object will be automatically filled with the user profile -------------------------------------------------------------------------------- /totalaccount/totalaccount.js: -------------------------------------------------------------------------------- 1 | const ACCOUNT = 'https://account.totaljs.com'; 2 | const COOKIE = 'ta'; 3 | 4 | var SESSIONS = {}; 5 | 6 | exports.install = function() { 7 | ROUTE('GET /auth/', auth); 8 | }; 9 | 10 | function auth($) { 11 | var token = $.query.token; 12 | if (token) { 13 | $.cookie(COOKIE, token || '', '1 month'); 14 | $.redirect('/'); 15 | } else 16 | $.redirect(ACCOUNT + '/?app=' + encodeURIComponent($.hostname('/auth/'))); 17 | } 18 | 19 | AUTH(async function($) { 20 | 21 | var token = $.cookie(COOKIE); 22 | if (token) { 23 | 24 | var response = SESSIONS[token]; 25 | if (response) { 26 | $.success(response); 27 | return; 28 | } 29 | 30 | if (BLOCKED($, 10)) 31 | return; 32 | 33 | response = SESSIONS[token] = await RESTBuilder.API(ACCOUNT + '/api/', 'account').header('x-token', token).promise($); 34 | response.expire = NOW.add('2 minutes'); 35 | $.success(response); 36 | BLOCKED($, -1); 37 | 38 | } else 39 | $.invalid(); 40 | 41 | }); 42 | 43 | ON('service', function() { 44 | for (let key in SESSIONS) { 45 | let session = SESSIONS[key]; 46 | if (session.expire < NOW) 47 | delete SESSIONS[key]; 48 | } 49 | }); -------------------------------------------------------------------------------- /totaldb/readme.md: -------------------------------------------------------------------------------- 1 | # TotalDB 2 | 3 | This module allows you communicate easily with the __Total DB__. 4 | 5 | ## Usage example 6 | 7 | ```js 8 | // TDB is a global variable 9 | 10 | var endpoint = 'https://yourtotaldb.com?token=123456' 11 | 12 | response = await TDB.read('type686', 's69a001ey41d').expand().fields('id,name,x14_name').promise(endpoint); 13 | console.log(response); 14 | 15 | response = await TDB.insert('type526', { name: 'Test XXL' }).promise(endpoint); 16 | console.log(response); 17 | 18 | response = await TDB.remove('type526', 'sp54001dk41d').promise(endpoint); 19 | console.log(response); 20 | 21 | response = await TDB.insert('type591', { name: 'A record name', x01: '123456' }).promise(endpoint); 22 | console.log(response); 23 | 24 | response = await TDB.read('type686', 's69a001ey41d').fields('id,name,x14_name').promise(endpoint); 25 | console.log(response); 26 | ``` 27 | 28 | ## Methods 29 | 30 | - `read(type, id)` reads a specific record 31 | - `first(type)` reads a specific record according to the filter 32 | - `find(type)` finds records 33 | - `list(type)` makes a list of recrods with the pagination 34 | - `insert(type, data)` inserts a specific record 35 | - `update(type, id, data)` updates a specific record 36 | - `remove(type, id)` removes a specific record 37 | 38 | A `{Response}` from the Total DB methods is the __TotalDB QueryBuilder__: 39 | 40 | - `.paginate(page, [limit])` 41 | - `.expand()` returns expanded relations 42 | - `.group(groups)` performs a group by, example: `group('attr3,name')` 43 | - `.sort(value)` sorts data, example: `sort('name asc,attr3 desc')`; 44 | - `.fields(value)` filters fields, example: `fields('name,attr4_name')` 45 | - `.where(value)` makes a simple filter `where('name=%Total%&attr3=100 - 200')` 46 | - `.filter(value)` makes advanced filter `filter('[name=Total Avengers] AND [attr3>100] AND [attr3<300]')`; 47 | - `.callback(url, callback)` a callback 48 | - `.promise(url, [$])` returns a promise for async/await -------------------------------------------------------------------------------- /totaldb/totaldb.js: -------------------------------------------------------------------------------- 1 | // query.fields = 'id,COUNT(dtcreated)'; 2 | // query.filter = '[name=Test] AND [x14_name~%Total%] OR ([x12=123456] OR [x7_name=Reject])'; 3 | // query.where = 'name=%25Test%25&x14_name=%25Total%25'; 4 | // query.filter = '[name=Test] AND [x14_name~%Total%]'; 5 | // query.group = 'id'; 6 | // query.sort = 'x14_name ASC'; 7 | // query.command = 'find'; 8 | // query.typeid = 'type3434'; 9 | // query.expand = '1'; 10 | 11 | function TotalDB(cmd) { 12 | var t = this; 13 | t.options = {}; 14 | t.command = cmd; 15 | } 16 | 17 | var TDB = TotalDB.prototype; 18 | 19 | TDB.endpoint = function(url) { 20 | this.options.url = url; 21 | return this; 22 | }; 23 | 24 | TDB.where = function(value) { 25 | var t = this; 26 | if (typeof(value) === 'object') { 27 | var builder = []; 28 | for (var key in value) 29 | builder.push(key + '=' + encodeURIComponent(value[key])); 30 | t.options.where = encodeURIComponent(builder.join('&')); 31 | } else { 32 | 33 | var output = []; 34 | var keys = value.split('&'); 35 | 36 | for (var key of keys) { 37 | var index = key.indexOf('='); 38 | if (index !== -1) 39 | output.push(key.substring(0, index) + '=' + encodeURIComponent(key.substring(index + 1))); 40 | } 41 | 42 | t.options.where = encodeURIComponent(output.join('&')); 43 | } 44 | 45 | return t; 46 | }; 47 | 48 | TDB.filter = function(value) { 49 | 50 | var t = this; 51 | 52 | if (value instanceof Array) { 53 | var builder = []; 54 | for (var m of value) 55 | builder.push((/=|<|>|~/).test(m) ? ('[' + m + ']') : m); 56 | t.options.filter = encodeURIComponent(builder.join(' ')); 57 | } else 58 | t.options.filter = encodeURIComponent(value); 59 | 60 | return t; 61 | }; 62 | 63 | TDB.fields = function(value) { 64 | this.options.fields = encodeURIComponent(value); 65 | return this; 66 | }; 67 | 68 | TDB.sort = function(value) { 69 | this.options.sort = encodeURIComponent(value); 70 | return this; 71 | }; 72 | 73 | TDB.paginate = function(page, limit) { 74 | var t = this; 75 | t.options.page = page; 76 | t.options.limit = limit; 77 | return t; 78 | }; 79 | 80 | TDB.expand = function() { 81 | this.options.expand = 1; 82 | return this; 83 | }; 84 | 85 | TDB.group = function(value) { 86 | this.options.group = encodeURIComponent(value); 87 | return this; 88 | }; 89 | 90 | TDB.exec = TDB.callback = function(url, callback) { 91 | 92 | if (typeof(url) !== 'string') { 93 | callback = url; 94 | url = this.options.url || ''; 95 | } 96 | 97 | var dburl = TEMP[url]; 98 | if (!dburl) { 99 | if (url.indexOf('/api/') === -1) { 100 | var index = url.indexOf('/', 10); 101 | TEMP[url] = dburl = url.substring(0, index) + '/api/' + url.substring(index + 1); 102 | } else 103 | TEMP[url] = dburl = url; 104 | } 105 | 106 | var t = this; 107 | var filter = []; 108 | 109 | if (t.options.fields) 110 | filter.push('fields=' + t.options.fields); 111 | 112 | if (t.options.sort) 113 | filter.push('sort=' + t.options.sort); 114 | 115 | if (t.options.where) 116 | filter.push('where=' + t.options.where); 117 | else if (t.options.filter) 118 | filter.push('filter=' + t.options.filter); 119 | 120 | if (t.options.group) 121 | filter.push('group=' + t.options.group); 122 | 123 | if (t.options.expand) 124 | filter.push('expand=1'); 125 | 126 | if (t.options.page) 127 | filter.push('page=' + t.options.page); 128 | 129 | if (t.options.limit) 130 | filter.push('limit=' + t.options.limit); 131 | 132 | var api = t.options.command + (filter.length ? ('?' + filter.join('&')) : ''); 133 | var opt = {}; 134 | 135 | opt.keepalive = true; 136 | opt.url = dburl; 137 | opt.xhr = true; 138 | opt.method = 'POST'; 139 | opt.type = 'json'; 140 | opt.dnscache = true; 141 | opt.body = JSON.stringify({ schema: api, data: t.options.data }); 142 | 143 | if (!callback || typeof(callback) !== 'function') { 144 | return new Promise(function(resolve, reject) { 145 | opt.callback = function(err, response) { 146 | var data = null; 147 | if (response.status === 200) 148 | data = response.body.parseJSON(true); 149 | else if (!err && response.status !== 200) 150 | err = response.body.parseJSON(true); 151 | if (err) { 152 | if (callback && callback.invalid) 153 | callback.invalid(err); 154 | else 155 | reject(err); 156 | } else 157 | resolve(data); 158 | }; 159 | REQUEST(opt); 160 | }); 161 | } else { 162 | opt.callback = function(err, response) { 163 | var data = null; 164 | if (response.status === 200) 165 | data = response.body.parseJSON(true); 166 | else if (!err && response.status !== 200) 167 | err = response.body.parseJSON(true); 168 | callback(err, data); 169 | }; 170 | REQUEST(opt); 171 | } 172 | }; 173 | 174 | TDB.promise = function(url) { 175 | return this.callback(url); 176 | }; 177 | 178 | exports.list = function(type) { 179 | var t = new TotalDB(); 180 | t.options.command = 'list/' + type; 181 | return t; 182 | }; 183 | 184 | exports.find = function(type) { 185 | var t = new TotalDB(); 186 | t.options.command = 'find/' + type; 187 | return t; 188 | }; 189 | 190 | exports.first = function(type) { 191 | var t = new TotalDB(); 192 | t.options.command = 'first/' + type; 193 | return t; 194 | }; 195 | 196 | exports.read = function(type, id) { 197 | var t = new TotalDB(); 198 | t.options.command = 'read/' + type + '/' + id; 199 | return t; 200 | }; 201 | 202 | exports.check = function(type) { 203 | var t = new TotalDB(); 204 | t.options.command = 'check/' + type; 205 | return t; 206 | }; 207 | 208 | exports.insert = exports.update = function(type, id, data) { 209 | var t = new TotalDB(); 210 | t.options.command = 'save/' + type; 211 | 212 | if (data) 213 | data.id = id; 214 | else 215 | data = id; 216 | 217 | t.options.data = data; 218 | return t; 219 | }; 220 | 221 | exports.custom = function(action, data) { 222 | var t = new TotalDB(); 223 | t.options.command = action; 224 | t.options.data = data; 225 | return t; 226 | }; 227 | 228 | exports.attrs = function(type) { 229 | var t = new TotalDB(); 230 | t.options.command = 'attrs/' + type; 231 | return t; 232 | }; 233 | 234 | exports.remove = function(type, id) { 235 | var t = new TotalDB(); 236 | t.options.command = 'remove/' + type + '/' + id; 237 | return t; 238 | }; 239 | 240 | global.TDB = exports; -------------------------------------------------------------------------------- /totp/readme.md: -------------------------------------------------------------------------------- 1 | # TOTP: Time-Based One-Time Password Algorithm 2 | 3 | - mobile apps: Google Authenticator or Microsoft Authenticator 4 | - download and copy `totp.js` into the `/modules/` directory __or create a definition with:__ 5 | 6 | ## Usage 7 | 8 | ```js 9 | const Totp = MODULE('totp'); 10 | ``` 11 | 12 | ### Generating secret 13 | 14 | ```js 15 | // Generates unique secret key 16 | var meta = Totp.generate(label, issuer, type); 17 | // @label {String} a label for the key 18 | // @issuer {String} a issuer 19 | // @type {String} password type (optional), can be `totp` (time-based password, default) or `hotp` (one-time password) 20 | // returns {Object} 21 | 22 | // Response 23 | meta.secret; // {String} a generated secret 24 | meta.url; // {String} an optimized url address for tokenization 25 | meta.qrcode; // {String} a link to the picture with QR code 26 | ``` 27 | 28 | ### Checking token 29 | 30 | ```js 31 | var result = Totp.totpverify(secret, token); 32 | // @result {Object} 33 | 34 | if (result == null) { 35 | console.log('NOT VALID'); 36 | } else { 37 | console.log(result); // { delta: NUMBER } 38 | } 39 | ``` -------------------------------------------------------------------------------- /totp/totp.js: -------------------------------------------------------------------------------- 1 | // Author: Peter Širka / Total Avengers 2 | // Web: https://www.totaljs.com 3 | // License: MIT 4 | 5 | const Crypto = require('crypto'); 6 | 7 | // Base32 methods are updated code from "Base32.js" library 8 | // Source: https://github.com/mikepb/base32.js 9 | function charmap(alphabet) { 10 | var mappings = {}; 11 | var chars = alphabet.split(''); 12 | for (var i = 0; i < chars.length; i++) { 13 | var c = chars[i]; 14 | if (!(c in mappings)) 15 | mappings[c] = i; 16 | } 17 | return mappings; 18 | } 19 | 20 | var CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; 21 | var CHARMAP = charmap(CHARS); 22 | CHARMAP[0] = 14; 23 | CHARMAP[1] = 8; 24 | 25 | function base32encode(arr) { 26 | var buf = []; 27 | var shift = 3; 28 | var carry = 0; 29 | 30 | for (var i = 0; i < arr.length; i++) { 31 | 32 | var byte = arr[i]; 33 | var symbol = carry | (byte >> shift); 34 | buf.push(CHARS[symbol & 0x1f]); 35 | if (shift > 5) { 36 | shift -= 5; 37 | symbol = byte >> shift; 38 | buf.push(CHARS[symbol & 0x1f]); 39 | } 40 | shift = 5 - shift; 41 | carry = byte << shift; 42 | shift = 8 - shift; 43 | } 44 | 45 | if (shift !== 3) { 46 | buf.push(CHARS[carry & 0x1f]); 47 | shift = 3; 48 | carry = 0; 49 | } 50 | 51 | return buf.join(''); 52 | } 53 | 54 | function base32decode(val) { 55 | 56 | var buf = []; 57 | var arr = val.toUpperCase().split(''); 58 | var shift = 8; 59 | var carry = 0; 60 | 61 | for (var i = 0; i < arr.length; i++) { 62 | var c = arr[i]; 63 | if (c === '=') 64 | continue; 65 | 66 | var symbol = CHARMAP[c] & 0xff; 67 | shift -= 5; 68 | 69 | if (shift > 0) { 70 | carry |= symbol << shift; 71 | } else if (shift < 0) { 72 | buf.push(carry | (symbol >> -shift)); 73 | shift += 8; 74 | carry = (symbol << shift) & 0xff; 75 | } else { 76 | buf.push(carry | symbol); 77 | shift = 8; 78 | carry = 0; 79 | } 80 | } 81 | 82 | if (shift !== 8 && carry !== 0) { 83 | buf.push(carry); 84 | shift = 8; 85 | carry = 0; 86 | } 87 | 88 | return Buffer.from(buf); 89 | } 90 | 91 | // Inspired from "Speakeasy" library 92 | // Source: https://github.com/speakeasyjs/speakeasy 93 | exports.hotp = function(key, counter) { 94 | 95 | var buffer = Buffer.from(base32decode(key), 'base32'); 96 | var buffersize = 20; 97 | var digits = 6; 98 | 99 | key = Buffer.from(Array(Math.ceil(buffersize / buffer.length) + 1).join(buffer.toString('hex')), 'hex').slice(0, buffersize); 100 | 101 | var buf = Buffer.alloc(8); 102 | var tmp = counter || 0; 103 | 104 | for (var i = 0; i < 8; i++) { 105 | // mask 0xff over number to get last 8 106 | buf[7 - i] = tmp & 0xff; 107 | // shift 8 and get ready to loop over the next batch of 8 108 | tmp = tmp >> 8; 109 | } 110 | 111 | var hmac = Crypto.createHmac('sha1', key); 112 | hmac.update(buf); 113 | var digest = hmac.digest(); 114 | 115 | // compute HOTP offset 116 | var offset = digest[digest.length - 1] & 0xf; 117 | 118 | // calculate binary code (RFC4226 5.4) 119 | var code = (digest[offset] & 0x7f) << 24 | 120 | (digest[offset + 1] & 0xff) << 16 | 121 | (digest[offset + 2] & 0xff) << 8 | 122 | (digest[offset + 3] & 0xff); 123 | 124 | // left-pad code 125 | code = new Array(digits + 1).join('0') + code.toString(10); 126 | 127 | // return length number off digits 128 | return code.substr(-digits); 129 | }; 130 | 131 | // Inspired from "Speakeasy" library 132 | // Source: https://github.com/speakeasyjs/speakeasy 133 | exports.hotpverify = function(key, token, counter, window, totp) { 134 | 135 | if (!window) 136 | window = 5; 137 | 138 | if (!counter) 139 | counter = 0; 140 | 141 | token += ''; 142 | 143 | var loopstart = totp ? counter - window : counter; 144 | var inc = 0; 145 | 146 | // Now loop through from C (C - W in case of TOTP) 147 | // to C + W to determine if there is a correct code 148 | for (var i = loopstart; i <= counter + window; ++i) { 149 | inc = i; 150 | if (exports.hotp(key, inc) === token) { 151 | // We have found a matching code, trigger callback 152 | // and pass offset 153 | return { delta: i - counter }; 154 | } 155 | } 156 | 157 | // If we get to here then no codes have matched, return null 158 | return null; 159 | }; 160 | 161 | exports.totp = function(key, time, ticks) { 162 | if (!time) 163 | time = 30; 164 | var now = ticks || Date.now(); 165 | var counter = Math.floor((now / 1000) / time); 166 | return exports.hotp(key, counter); 167 | }; 168 | 169 | exports.totpverify = function(key, token, time, ticks) { 170 | if (!time) 171 | time = 30; 172 | var now = ticks || Date.now(); 173 | var counter = Math.floor((now / 1000) / time); 174 | return exports.hotpverify(key, token, counter, 1, true); 175 | }; 176 | 177 | exports.generate = function(label, issuer, type) { 178 | 179 | // type can be "totp" (default), "hotp" 180 | 181 | var bytes = Crypto.randomBytes(20); 182 | var set = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'; 183 | var builder = []; 184 | 185 | for (var i = 0, l = bytes.length; i < l; i++) 186 | builder.push(set[Math.floor(bytes[i] / 255.0 * (set.length - 1))]); 187 | 188 | var secret = base32encode(Buffer.from(builder.join('').toUpperCase())).toString().replace(/=/g, ''); 189 | var data = {}; 190 | data.secret = secret; 191 | data.url = 'otpauth://' + (type || 'totp') + '/totaljs?secret=' + data.secret + '&issuer=' + encodeURIComponent(issuer || '');// + '&algorithm=sha1&digits=6&period=30'; 192 | data.qrcode = 'https://api.qrserver.com/v1/create-qr-code/?color=000000&bgcolor=FFFFFF&data=' + encodeURIComponent(data.url) + '&qzone=0&margin=0&size=200x200&ecc=L'; 193 | return data; 194 | }; 195 | --------------------------------------------------------------------------------