├── .github └── FUNDING.yml ├── .gitignore ├── config ├── controllers └── default.js ├── definitions ├── auth.js ├── db.js └── init.js ├── index.js ├── license.txt ├── modules └── openplatform.js ├── package.json ├── plugins ├── products │ ├── index.js │ ├── public │ │ ├── detail.html │ │ └── index.html │ └── schemas │ │ └── profiles.js ├── profiles │ ├── index.js │ ├── public │ │ ├── detail.html │ │ └── index.html │ └── schemas │ │ └── profiles.js └── setup │ ├── index.js │ ├── public │ └── index.html │ └── schemas │ └── setup.js ├── public ├── css │ └── default.css ├── favicon.ico ├── js │ └── default.js └── pages │ └── index.html ├── readme.md └── views └── index.html /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | sftp-config.json 3 | tmp/* 4 | /index.js.json 5 | /databases/ -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | database : postgresql://user:password@localhost:5432/database -------------------------------------------------------------------------------- /controllers/default.js: -------------------------------------------------------------------------------- 1 | exports.install = function() { 2 | ROUTE('+GET /*', index); 3 | }; 4 | 5 | function index($) { 6 | 7 | var plugins = []; 8 | 9 | for (var key in F.plugins) { 10 | var item = F.plugins[key]; 11 | if ($.user.sa || !item.visible || item.visible($.user)) { 12 | var obj = {}; 13 | obj.id = item.id; 14 | obj.routes = item.routes; 15 | obj.position = item.position; 16 | obj.name = TRANSLATE($.user.language || '', item.name); 17 | obj.icon = item.icon; 18 | obj.import = item.import; 19 | obj.hidden = item.hidden; 20 | plugins.push(obj); 21 | } 22 | } 23 | 24 | plugins.quicksort('position'); 25 | $.view('index', plugins); 26 | } -------------------------------------------------------------------------------- /definitions/auth.js: -------------------------------------------------------------------------------- 1 | const USER = { id: 'admin', sa: true, name: 'Admin' }; 2 | 3 | AUTH(function($) { 4 | if (CONF.op_reqtoken && CONF.op_restoken) 5 | OpenPlatform.auth($); 6 | else 7 | $.success(USER); 8 | }); -------------------------------------------------------------------------------- /definitions/db.js: -------------------------------------------------------------------------------- 1 | // require('querybuilderpg').init('', CONF.database, 1, ERROR('DB')); -------------------------------------------------------------------------------- /definitions/init.js: -------------------------------------------------------------------------------- 1 | var db = MAIN.db = MEMORIZE('data'); 2 | 3 | if (!db.config) 4 | db.config = {}; 5 | 6 | var config = db.config; 7 | 8 | if (!config.name) 9 | config.name = 'App'; 10 | 11 | if (!config.cdn) 12 | config.cdn = '//cdn.componentator.com'; 13 | 14 | // Fixed settings 15 | CONF.$customtitles = true; 16 | CONF.version = '1'; 17 | CONF.op_icon = 'ti ti-rocket'; 18 | 19 | // Loads configuration 20 | LOADCONFIG(db.config); 21 | 22 | // UI components 23 | COMPONENTATOR('ui', 'exec,locale,aselected,page,viewbox,input,importer,box,validate,loading,selected,intranetcss,notify,message,errorhandler,empty,menu,ready,fileuploader,colorpicker,approve,icons,directory,uibuilder,uistudio', true); 24 | 25 | // Permissions 26 | ON('ready', function() { 27 | for (var key in F.plugins) { 28 | var item = F.plugins[key]; 29 | if (item.permissions) 30 | OpenPlatform.permissions.push.apply(OpenPlatform.permissions, item.permissions); 31 | } 32 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // =================================================== 2 | // Total.js v5 start script 3 | // https://www.totaljs.com 4 | // =================================================== 5 | 6 | require('total5'); 7 | 8 | const options = {}; 9 | 10 | // options.ip = '127.0.0.1'; 11 | // options.port = parseInt(process.argv[2]); 12 | // options.unixsocket = PATH.join(F.tmpdir, 'app_name.socket'); 13 | // options.unixsocket777 = true; 14 | // options.config = { name: 'Total.js' }; 15 | // options.sleep = 3000; 16 | // options.inspector = 9229; 17 | // options.watch = ['private']; 18 | // options.livereload = 'https://yourhostname'; 19 | // options.watcher = true; // enables watcher for the release mode only controlled by the app `F.restart()` 20 | // options.edit = 'wss://www.yourcodeinstance.com/?id=projectname' 21 | options.release = process.argv.includes('--release'); 22 | 23 | // Service mode: 24 | options.servicemode = process.argv.includes('--service') || process.argv.includes('--servicemode'); 25 | // options.servicemode = 'definitions,modules,config'; 26 | 27 | // Cluster: 28 | // options.tz = 'utc'; 29 | // options.cluster = 'auto'; 30 | // options.limit = 10; // max 10. threads (works only with "auto" scaling) 31 | 32 | F.run(options); -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | Copyright 2023 (c) Total.js 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a 5 | copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to permit 9 | persons to whom the Software is furnished to do so, subject to the 10 | following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 18 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 21 | USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /modules/openplatform.js: -------------------------------------------------------------------------------- 1 | const EXPIRE = '2 minutes'; 2 | 3 | var Data = {}; 4 | 5 | // A temporary object for storing of sessions 6 | Data.sessions = {}; 7 | Data.permissions = []; 8 | 9 | // Meta file 10 | ROUTE('FILE /openplatform.json', function($) { 11 | 12 | var model = {}; 13 | 14 | model.name = CONF.op_name || CONF.name; 15 | model.icon = CONF.op_icon; 16 | model.color = CONF.op_color; 17 | model.url = CONF.op_url || $.hostname(); 18 | model.permissions = Data.permissions; 19 | 20 | if (CONF.op_path) 21 | model.url += CONF.op_path; 22 | 23 | $.json(model); 24 | }); 25 | 26 | // Auth method 27 | Data.auth = function($) { 28 | 29 | if (!CONF.op_reqtoken || !CONF.op_restoken) { 30 | $.invalid(); 31 | return; 32 | } 33 | 34 | var q = $.query; 35 | var a = q.openplatform || ''; 36 | 37 | if (!a) { 38 | $.invalid(); 39 | return; 40 | } 41 | 42 | var m = a.match(/token=.*?(&|$)/); 43 | if (!m) { 44 | $.invalid(); 45 | return; 46 | } 47 | 48 | // token~sign 49 | var tmp = m[0].substring(6).split('~'); 50 | var token = tmp[0]; 51 | var sign = tmp[1]; 52 | var session = Data.sessions[token]; 53 | 54 | if (session) { 55 | Data.onread && Data.onread(session); 56 | $.success(session); 57 | return; 58 | } 59 | 60 | var checksum = q.openplatform.replace('~' + sign, '').md5(CONF.op_reqtoken); 61 | 62 | if (checksum !== sign) { 63 | $.invalid(); 64 | return; 65 | } 66 | 67 | var opt = {}; 68 | 69 | opt.url = q.openplatform; 70 | opt.method = 'GET'; 71 | opt.headers = { 'x-token': sign.md5(CONF.op_restoken) }; 72 | opt.keepalive = true; 73 | opt.callback = function(err, response) { 74 | 75 | if (err || response.status !== 200) { 76 | $.invalid(); 77 | return; 78 | } 79 | 80 | session = response.body.parseJSON(true); 81 | 82 | if (session) { 83 | 84 | if (!session.permissions) 85 | session.permissions = []; 86 | 87 | session.dtexpire = NOW.add(CONF.op_expire || EXPIRE); 88 | session.token = token; 89 | session.logout = Logout; 90 | session.json = Json; 91 | session.notification = Notification; 92 | var hostname = opt.url.substring(0, opt.url.indexOf('/', 10)); 93 | session.iframe = hostname + '/iframe.js'; 94 | Data.sessions[token] = session; 95 | Data.oncreate && Data.oncreate(session); 96 | $.success(session); 97 | } else 98 | $.invalid(); 99 | }; 100 | 101 | REQUEST(opt); 102 | }; 103 | 104 | ON('service', function() { 105 | for (var key in Data.sessions) { 106 | var session = Data.sessions[key]; 107 | if (session.dtexpire < NOW) { 108 | delete Data.sessions[key]; 109 | Data.onremove && Data.onremove(session); 110 | } 111 | } 112 | }); 113 | 114 | function Json() { 115 | var obj = {}; 116 | for (var key in this) { 117 | switch (key) { 118 | case 'token': 119 | case 'dtexpired': 120 | case 'openplatformid': 121 | case 'openplatform': 122 | case 'notify': 123 | case 'notification': 124 | case 'sign': 125 | case 'json': 126 | case 'logout': 127 | break; 128 | default: 129 | obj[key] = this[key]; 130 | break; 131 | } 132 | } 133 | return obj; 134 | } 135 | 136 | function Notification(body, path, icon, color) { 137 | 138 | var model = {}; 139 | 140 | model.body = body; 141 | model.path = path; 142 | model.icon = icon; 143 | model.color = color; 144 | 145 | if (!this.sign) 146 | this.sign = this.notify.md5(CONF.op_reqtoken).md5(CONF.op_restoken); 147 | 148 | return RESTBuilder.POST(this.notify, model).header('x-token', this.sign).promise(); 149 | } 150 | 151 | function Logout() { 152 | var session = Data.sessions[this.token]; 153 | if (session) { 154 | delete Data.sessions[this.token]; 155 | Data.onremove && Data.onremove(session); 156 | } 157 | } 158 | 159 | LOCALIZE($ => ($.user ? $.user.language : $.query.language) || CONF.language || ''); 160 | AUTH(Data.auth); 161 | global.OpenPlatform = Data; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "version": "0.0.1", 4 | "main": "index.js", 5 | "dependencies": { 6 | "total5": "beta" 7 | }, 8 | "scripts": { 9 | "start": "node index.js 8000" 10 | }, 11 | "keywords": [ 12 | "openplatform", 13 | "application" 14 | ], 15 | "author": "Total.js", 16 | "license": "MIT" 17 | } -------------------------------------------------------------------------------- /plugins/products/index.js: -------------------------------------------------------------------------------- 1 | exports.icon = 'ti ti-box'; 2 | exports.name = '@(Products)'; 3 | exports.position = 1; 4 | exports.permissions = [{ id: 'products', name: 'Products' }]; 5 | exports.visible = user => user.sa || user.permissions.includes('products'); 6 | // exports.hidden = true; // hides item in the menue 7 | exports.routes = [ 8 | { url: '/products/{id}/', html: 'detail' } 9 | ]; 10 | 11 | exports.install = function() { 12 | 13 | // Profiles 14 | ROUTE('+API /api/ -products --> Products/list'); 15 | 16 | }; -------------------------------------------------------------------------------- /plugins/products/public/detail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |
7 | 8 |
9 |
10 | 11 | 24 | 25 |
26 | 27 | -------------------------------------------------------------------------------- /plugins/products/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 |
8 |
9 | 10 | 27 | 28 |
29 | 30 | -------------------------------------------------------------------------------- /plugins/products/schemas/profiles.js: -------------------------------------------------------------------------------- 1 | NEWACTION('Products/list', { 2 | name: 'List of products', 3 | permissions: 'products', 4 | action: function($) { 5 | 6 | var arr = []; 7 | for (var i = 0; i < 5; i++) 8 | arr.push(GUID()); 9 | 10 | $.callback(arr); 11 | } 12 | }); -------------------------------------------------------------------------------- /plugins/profiles/index.js: -------------------------------------------------------------------------------- 1 | exports.icon = 'ti ti-totaljs'; 2 | exports.name = '@(Profiles)'; 3 | exports.position = 2; 4 | exports.permissions = [{ id: 'profiles', name: 'Profiles' }]; 5 | exports.visible = user => user.sa || user.permissions.includes('profiles'); 6 | // exports.hidden = true; // hides item in the menue 7 | exports.routes = [ 8 | { url: '/profiles/{id}/', html: 'detail' } 9 | ]; 10 | 11 | exports.install = function() { 12 | 13 | // Profiles 14 | ROUTE('+API /api/ -profiles --> Profiles/list'); 15 | 16 | }; -------------------------------------------------------------------------------- /plugins/profiles/public/detail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |
7 | 8 |
9 |
10 | 11 | 24 | 25 |
26 | 27 | -------------------------------------------------------------------------------- /plugins/profiles/public/index.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 |
8 | 9 |
10 | 11 |
12 |
13 | 14 | 31 | 32 |
33 | 34 | -------------------------------------------------------------------------------- /plugins/profiles/schemas/profiles.js: -------------------------------------------------------------------------------- 1 | NEWACTION('Profiles/list', { 2 | name: 'List of profiles', 3 | permissions: 'profiles', 4 | action: function($) { 5 | 6 | var arr = []; 7 | for (var i = 0; i < 5; i++) 8 | arr.push(GUID()); 9 | 10 | $.callback(arr); 11 | } 12 | }); -------------------------------------------------------------------------------- /plugins/setup/index.js: -------------------------------------------------------------------------------- 1 | exports.icon = 'ti ti-cog'; 2 | exports.name = '@(Configuration)'; 3 | exports.permissions = [{ id: 'setup', name: 'Setup' }]; 4 | exports.position = 20; 5 | exports.visible = user => user.sa || user.permissions.includes('setup'); 6 | 7 | exports.install = function() { 8 | ROUTE('+API /api/ -setup_read --> Setup/read'); 9 | ROUTE('+API /api/ +setup_save --> Setup/save'); 10 | ROUTE('+API /api/ -account --> Setup/account'); 11 | }; -------------------------------------------------------------------------------- /plugins/setup/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 | 79 | 80 |
81 | 82 | -------------------------------------------------------------------------------- /plugins/setup/schemas/setup.js: -------------------------------------------------------------------------------- 1 | NEWACTION('Setup/save', { 2 | name: 'Save configuration', 3 | permissions: 'setup', 4 | input: '*name,totalapi,$tms:Boolean,secret_tms,op_reqtoken,op_restoken', 5 | action: function($, model) { 6 | COPY(model, MAIN.db.config); 7 | LOADCONFIG(model); 8 | MAIN.db.save(); 9 | $.success(); 10 | } 11 | }); 12 | 13 | NEWACTION('Setup/read', { 14 | name: 'Read configuration', 15 | permissions: 'setup', 16 | action: function($) { 17 | $.callback(MAIN.db.config); 18 | } 19 | }); 20 | 21 | NEWACTION('Setup/account', { 22 | name: 'Read account data', 23 | action: async function($) { 24 | $.callback($.user.json ? $.user.json() : $.user); 25 | } 26 | }); -------------------------------------------------------------------------------- /public/css/default.css: -------------------------------------------------------------------------------- 1 | /*auto*/ 2 | 3 | .monospace { font-family: Menlo, Consolas, monospace; } 4 | 5 | html,body { overflow: hidden; margin: 0; padding: 0; color: #303030; height: 100%; } 6 | 7 | .auto { width: 100%; margin: 0 auto; text-align: left; } 8 | .inline { position: relative; display: inline-block; } 9 | 10 | .appmenu { width: 65px; border-right: 1px solid #E0E0E0; float: left; padding: 5px 0 0; background-color: #FFF; color: #777; } 11 | .appmenu > div { border-bottom: 1px solid #E0E0E0; } 12 | .appmenu .exec, .appmenu a { height: 34px; width: 34px; font-size: 18px; line-height: 34px; text-align: center; margin: 11px 15px 0 15px; cursor: pointer; border-radius: var(--radius); display: block; color: #888; } 13 | .appmenu .selected, .appmenu .exec:hover, .appmenu a:hover { background-color: #F0F0F0; color: #000; } 14 | .appmenu hr { margin: 15px 10px; } 15 | .appmain { margin-left: 65px; } 16 | 17 | header { height: 60px; padding: 0 10px 0 0; border-bottom: 1px solid #E0E0E0; text-align: right; } 18 | header .back { float: left; margin: 12px 0 0 20px; height: 34px; line-height: 32px; width: 34px; border: 1px solid #E0E0E0; text-align: center; cursor: pointer; color: #000; background-color: #F8F8F8; border-radius: var(--radius); font-weight: bold; } 19 | header .back:hover { background-color: #F0F0F0; } 20 | header label { float: left; line-height: 59px; font-weight: bold; margin-left: 20px; font-size: 15px; } 21 | header label i { margin: 0 10px 0 0; color: var(--color); } 22 | header .toolbar { float: right; margin: 15px 10px 0 0; } 23 | header .toolbar button { height: 30px; font-size: 12px; } 24 | 25 | .mainmenu { height: 34px; width: 34px; font-size: 18px; line-height: 34px; text-align: center; margin: 8px 0 12px 15px !important; cursor: pointer; background-color: #F0F0F0; border-radius: var(--radius); color: #000 !important; } 26 | .mainmenu:hover { background-color: #F5F5F5; } 27 | 28 | .button { border: 0; margin: 0; background-color: #E7E7E7; height: 40px; padding: 0 20px; color: #000; cursor: pointer; font-family: Arial; line-height: 34px; vertical-align: middle; outline: 0; font-size: 14px; text-decoration: none; transition: all 0.3s; width: 100%; } 29 | .button i { width: 15px; text-align: center; margin-right: 5px; } 30 | .button:hover { opacity: 0.8; } 31 | .button[name='submit'] { font-weight: bold; background-color: var(--color); color: #FFF; } 32 | .button:disabled { background-color: #F5F5F5 !important; border-color: #E0E0E0 !important; color: #A0A0A0 !important; cursor: not-allowed; box-shadow: none; } 33 | .button:disabled i { color: #A0A0A0 !important; } 34 | .button:first-child { border-top-left-radius: var(--radius); border-bottom-left-radius: var(--radius); } 35 | .button:last-child { border-top-right-radius: var(--radius); border-bottom-right-radius: var(--radius); } 36 | .button.small { height: 24px; padding: 0 8px; line-height: 14px; font-size: 12px; } 37 | 38 | .divider { position: relative; margin: 25px 0; } 39 | .divider div { position: absolute; left: 0; top: -10px; right: 0; text-align: center; } 40 | .divider span { background-color: #F0F0F0; border-radius: var(--radius); padding: 5px 8px; font-size: 11px; color: #999; } 41 | .divider span i { margin-right: 5px; } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totaljs/openplatform-application/960d8c11071bb6a49b9ff03f64215b5586c8d01c/public/favicon.ico -------------------------------------------------------------------------------- /public/js/default.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totaljs/openplatform-application/960d8c11071bb6a49b9ff03f64215b5586c8d01c/public/js/default.js -------------------------------------------------------------------------------- /public/pages/index.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 21 | 22 | 23 |
24 |
25 |
26 | 27 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # OpenPlatform: Application template 2 | 3 | - [Documentation](https://docs.totaljs.com/openplatform/) 4 | - [Join Total.js Telegram](https://t.me/totaljs) 5 | - [Support](https://www.totaljs.com/support/) 6 | 7 | __Instructions__: 8 | 9 | - Download source code 10 | - Install NPM dependencies with `$ npm install` 11 | - Run the app `$ node index.js` 12 | - Register your local app in the OpenPlatform instance 13 | - Visit the app in the OpenPlatform and set tokens in the `Configuration` section 14 | 15 | [license-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat 16 | [license-url]: license.txt 17 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | @{layout('')} 2 | @{title(config.name)} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | @{import('meta', 'head', 'default.css', 'default.js', 'favicon.ico')} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 73 | 74 | @{json(model, 'pluginsdata')} 75 | 76 | @{if user.iframe} 77 | 78 | @{fi} 79 | 80 | 168 | 169 | 170 | --------------------------------------------------------------------------------