├── --bundles-- ├── app.bundle ├── login.bundle ├── portal.bundle └── setup.bundle ├── .bundleignore ├── .github ├── FUNDING.yml └── workflows │ └── docker-image.yml ├── .gitignore ├── Dockerfile ├── bundle.sh ├── config ├── controllers ├── api.js ├── socket.js └── verify.js ├── database-2023-09-15.sql ├── database-2024-03-21.sql ├── database-2024-10-22.sql ├── database-2024-10-30.sql ├── database.sql ├── definitions ├── func.js └── init.js ├── docker-compose.yaml ├── index.js ├── license.txt ├── package.json ├── plugins ├── login │ ├── index.html │ ├── index.js │ └── schemas │ │ └── login.js ├── portal │ ├── index.html │ ├── index.js │ └── public │ │ ├── default.css │ │ ├── default.js │ │ ├── forms │ │ ├── account.html │ │ ├── feedback.html │ │ ├── intro.html │ │ ├── notifications.html │ │ ├── password.html │ │ ├── sessions.html │ │ └── welcome.html │ │ ├── pages │ │ └── welcome.html │ │ ├── password.png │ │ └── sounds │ │ ├── alert.mp3 │ │ ├── badges.mp3 │ │ ├── beep.mp3 │ │ ├── confirm.mp3 │ │ ├── done.mp3 │ │ ├── fail.mp3 │ │ ├── message.mp3 │ │ ├── notifications.mp3 │ │ └── success.mp3 └── setup │ ├── index.html │ ├── index.js │ ├── public │ ├── default.css │ ├── forms │ │ ├── app.html │ │ ├── assign.html │ │ ├── feedback.html │ │ ├── group.html │ │ ├── import.html │ │ └── user.html │ ├── import.png │ └── pages │ │ ├── apps.html │ │ ├── dashboard.html │ │ ├── feedback.html │ │ ├── groups.html │ │ ├── settings.html │ │ └── users.html │ └── schemas │ ├── apps.js │ ├── dashboard.js │ ├── feedback.js │ ├── groups.js │ ├── settings.js │ └── users.js ├── public ├── favicon.ico ├── icon.png ├── iframe.js ├── img │ ├── icon.png │ └── photo.jpg ├── ui-uilogin-tdt3qv.min.js ├── ui-uiportal-1phb0ln.min.js └── ui-uisetup-2amosi.min.js ├── readme.md ├── resources ├── en.resource ├── es.resource ├── fr.resource └── sk.resource ├── schemas └── account.js └── views └── mail ├── feedback.html ├── layout.html ├── reset.html ├── unread.html └── welcome.html /--bundles--/login.bundle: -------------------------------------------------------------------------------- 1 | /plugins/ : # 2 | /plugins/login/ : # 3 | /plugins/login/schemas/ : # 4 | /plugins/login/index.html : H4sIAAAAAAAAE91ZX2/bOBJ/lj/FVIdd2UBkO2mD7clW6iB1FgGKJJukh+sd7oGSaIsbilRJyo7PyHc/kJRsOVYcX9FN9w56sEkO5z9/MxJHS4oWvFBtz+s8tlrDNx+vzu6+XI8hVRk9aQ2rH4ySk5YzVERRfDJqf+JTwjrgw2h5dnV53mUow4/Dnl1uOcMMKwRxioTEKnQLNfHfu9BbraRK5T7+WpBZ6P7d/3zqn/EsR4pEFLsQc6YwU6F7MQ4P+/VtWkroTrjIkPITrHCsCGe1HQpTnKec4ZDx7Y0zguc5F6q2YU4SlYYJnpEY+2ZwAIQRRRD1ZYwoDg8PoJBYmBGKqOZ8ABUnf0JUGPMZFtvSBI+4kjVZiNKDCaeUz0tiStg9CExDV6oFxTLFWLmQCjwJ3dHS+ylOmPfYkznqZoSNDv/ajaUst8pYkFyBFPEzpL9L92TYs2TbG27G11fdglAdxccnhFqVk5bjBIJzBcuW4zi+L1BCChnAcf4waDnOY8txulGhFGewhIiLBIsA+gPIkJgSZv5GKL6fCl6wxI855SKAv4x/0c8AUkymqQrgXT9/GECOkoSwaQB9ODITFXm/3x9AXAipRzknTGExgAlnyp+gjNBFAKeCIDoAShj2K65v32kmMywUiRH1ESVTFkBGkoTiAfBCaWqjomElyb9xAIdmk8IPOrFiLpDOrAAYZ3gASiAmiZ1BlEK/+1YOwKRLAIf9/k8DqHuEwHK1eLxiW+oRY2uG9ZQvrM6GrMYjSHVSwRJ4jmKiFgH0u+83KP5pssyTRZQR5f0LltaaeemEiNOkKQYzJNq+HXTWnj4/P98UnxCpkz3R0d2O4/mxfuANyfQhQEwNyhxYR7qvnw2Kaum0r5/NpTLGjGs3UT7HWnf+4MsUJXxehaFRQe3sHazreyZESOXHKaHJKmt9xXOf4olapbj1kB11VoZFXCme7aSsi6KoWZIJ936idpLWZHVlpnNyuTpVR++enKr3erxxRGy219P/qEzAYa8EgGHPYv4w4snipNVyhgXxY57lnGGmSoz7dPqPL5BhKdHUAveETEs0C440rtT3PMeEcg206+0Cfy2wVDI4HFDEpgWa4mC0rP4+brNt5IofcLwvqRBcpIglFIt9ldYVIOIPa63tucZJcDjIkcBMBXPCEo31MUVShi5hMyJNhdM8DdOIsARypNLQjXmWcdbNNx2Z8nkwQ7TAYRh6Bq09F4x7QzchMqdoEUSUx/eDDD3Y+hW87WsQLYG4D6hQfFADIJ3BK51SkiSYuRrunWFCZtV8jhimdtoomtNiSlipqtbR6FISNPpHi50QWjFpJjIuWxlUZaxN9SnKO9ZTdntNO+tqyFa8HWdIsum6GpKYM+/RBUSVndDi9EQpyTrqUDvK1tMmISg3zcV61RlSFGF6smY47NmZNYNeQmarYTlaDRs8QFheKLd07IcuzhCh6/irRY4DMzfQh4KU6UVRjFNOdc0dtcfGF9jXVICSRGApOy4keIIKbb438lbhztyTUXu8Sbp1QvZWNkdSzrlInuhbTb+kckXX2VTvuprePofbQZrIwyPI1PFmLtQITItloAASpJCv/4bulNtzZqZIoiFHYuWeDEm1TxFQxI+RwMoCMWTiWGMDORm1bzT1Wv/NoDvDN76/WxeIdmsTC4wU3ludj5x5ClI0w4AYoDjmBVMfnqrl+/9NXs4QJYlWoop23b9lm2MpbQOyimG5tuXyD72KsKrcW+bd40Vl0u3Fr5dwcdkZ9iy/muqNSdGYxE2TZs6imQW9yklmQePxN4KzyaA/CTiX2fw8OO+Pu7th97ug7m7QfRFzN9J8M7N/HODuC7N121NM8+0zT/VpjwoaVUfjCy9gTigFgWNMZhgIk0oU5k1YwoQLMOFXhE1hwYs1zkK0AGNat/PETanQ4WhQycKr2xiqPZG1bBT2hLJbMmU+YdqM7AmA1WH1T4Oqa1B9OfN2QOofhahxiuN7PyYi1l1nWbzGt+O7LWBtxtVXRVAbn5ch9Pg1ILRKltY63/7v0TJ6DgamgiT+UTMMbDReL+Otntx809uBsZq4o7H01J4+O/Fcw7qJFt+u4g8uCY0h+kNjst3H1wzdbOmbzN4oMp2dLfz3DlUlVeCcLr6H+oCmiDBjxI3mWW/y9w5WrQbUEPBDE+7Bzz+D+bMyZXvGGNdA+CYMmygbsW/Lz/aTTfXpxkcUC7XxOr1ZSOZIMMKmVQ2pAiwh4fqjHWRIxWm384xX6nXB+V9pOL6loK8iTCbBS2ENw7Apej+kJzi7GZ/ejeH07Ozq8+XLzcHTGO/XEjR8V6tuHVqOM0MCbJMAISwfB3qu1jRACOX3L3398HF83p3jaMVMckYXEIISBTY7JwUzDTHY9Ghj2rE3Gbfju7ZX4+sdwOnd3c1HTdGxNxstx7n+9PnXi8u2t/rS5R1AxbKNH/THZakZao7lsGsdD+GasBRpTMt4gimEUFHrfBuY1WoG/Y4e2t711e0d9FBOekZuD8Y3N1c33oFlUNNCYJlzJnElxNFfUfVKV98fQQi/fR7fXJx/aXs97wAuT//W/VpgsehYoY/217pZD54Ybd9mX99oI/d5o1fGruRj1fZGZZ31DmBZGmYCfTe+aXslwvVkEcdYSu8APPsOVb4+Jbq7N+Ud5kSle79NdTvenr4s+9rXd6YV/CreFNULEqRIQoQxAys9gZJ0UlC66MKphUh9gauPZ1WH5kiC1MiquHUzrndKO329vrtsDXv2rmLYM7fW/wEwFxRx2x4AAA== 5 | /plugins/login/index.js : H4sIAAAAAAAAE3WP3UrEMBCFr+1TDHiRrkzNAxQFkeKNNstaH2BMZ3cDMSnJdK2I7y5tL/xB5+4cvg/O8DTEJPnShSzkPVzBfgxWXAzlBt6Ls5156ppSVXdNB/PpC4Wgzn08uKBd6HlSm/oL25rHhdM0OL1CAFBV13C/hKX630icWX4aS7UYWsOfkk1Mwvq7tFaz9VEXhWlLlZj6N4W/vrs1D1vTNm1305ldqUa37kNQPLFFHy15RhckUWCxOePJ8etznJBTiulIofec8IVzpsMMDqPgibzrSRjtMcbMyEE4IY0S9857hSBp5Hnapv4Ew3O5Fn4BAAA= 6 | /plugins/login/schemas/login.js : H4sIAAAAAAAAE9VVXU/bMBR9bn/FnVRhB2XetGl7KIpEVWCqBO00QDxOJr5pLRK7sh0QAv77ZJskLRTotpftJYqv75fPPceeHl6MxmeT2ZSSYz2X6kPpvySFu35P8QqHQE7lXIG39XtSLWs3BLKLFZfl8NB/U9hdcmtvtBHDU2ekmnvPZX1ZSrsYgjM1pv0ez53Uagjc3qocilqFNR2kUGmBZQJ3/X6vN2CPcTRa97zxmhtYGl3IEiEDfsOlg4PR2YgZ5IISvWTusvxZWzQkYYXEUlhKpEh9+6nBAg2qHNPQciqtkJZflihSaaXybV1jKm2uVSFNhYIk7GaBBikJAeSxQRZW7V5z4na7MTC74J++fKXj2fSIWV66pI2R1mClr1FkR6Pj00OSMDRGG0r26URd81IKyA0KVE7y0iYkYUujK2mRDiIQsgD67hEJttKyB6/nwZMxjc84ynNdKwfSgtIOOl+S7Hlng642yv8+NKm7zA1EbyRu3bbJ2WD9Rs7WbXPOk9FkynjtFizw1POnLSFSGDChFdLExz70Hzxs06cEN2jRrRH8h7dAN9LNPP9HWF1yNa/5HCO9N9M516U22xH5FVI2U/H8KXStxAuU/B9449EW3HHI4O4hdO5XzOkrVJDB0fl0zPIF5le2rui388kB/fwxsCj6eZQ8R7Iw+BC/MrFKC1ncrs8sDeUSJkV3FvEMvmar6aNr6rngw1ATf7Y1C2QQ7pqwCFFhKa0fM+zswMlockxXRp8C2afrlE9ICsTvteJ4LJHCdDb73mmsIR/c38eqqwYSoR8wW+c5WktfU2FukDtck+E4mIDHoa7JMHjExyWF33h82mcgnly4WFYMD0LxbQW8MutAkqfy/DudHb4Po+JCGLTWk5mX/hK4hdpimI0n3TPuxCpSQAaerwH7aGuPCRlMZxfdRgMVZG++Wl3QFhqJjpvo2OxZf3/YVj+NWWknC5lzD7p9QV1SWTTuqbrieNYweeGi3UYOF1jmusJOBzfR0BRqdBDj/lAFvwATkPfrawkAAA== 7 | -------------------------------------------------------------------------------- /.bundleignore: -------------------------------------------------------------------------------- 1 | /databases/ 2 | /bundle.sh 3 | /guest.json 4 | /license.txt 5 | /package.json 6 | /readme.todo 7 | /public/photos/ 8 | /public/backgrounds/ 9 | /public/tmp/ 10 | /resources/sk.resource 11 | .DS_Store 12 | .bundleignore -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | docker: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - 11 | name: Checkout 12 | uses: actions/checkout@v2 13 | with: 14 | repository: 'totaljs/openplatform' 15 | ref: 'master' 16 | token: ${{ secrets.ACCESS_TOKEN }} 17 | - 18 | name: Set up QEMU 19 | uses: docker/setup-qemu-action@v1 20 | - 21 | name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v1 23 | - 24 | name: Login to DockerHub 25 | uses: docker/login-action@v1 26 | with: 27 | username: ${{ secrets.DOCKERHUB_USERNAME }} 28 | password: ${{ secrets.DOCKERHUB_TOKEN }} 29 | - 30 | name: docker_build 31 | uses: docker/build-push-action@v2 32 | with: 33 | context: . 34 | platforms: linux/amd64,linux/arm64 35 | tags: totalplatform/openplatform:latest 36 | push: true 37 | secrets: | 38 | github_token=${{ secrets.ACCESS_TOKEN }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | logs/ 3 | databases/ 4 | debug.pid 5 | sftp-config.json 6 | node_modules 7 | .DS_Store 8 | /index.js.map 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | MAINTAINER totalplatform "info@totaljs.com" 3 | 4 | VOLUME /www/ 5 | WORKDIR /www/ 6 | RUN mkdir -p /www/ 7 | RUN mkdir -p /www/controllers/ 8 | RUN mkdir -p /www/definitions/ 9 | RUN mkdir -p /www/plugins/ 10 | RUN mkdir -p /www/public/ 11 | RUN mkdir -p /www/resources/ 12 | RUN mkdir -p /www/schemas/ 13 | RUN mkdir -p /www/views/ 14 | 15 | COPY controllers/ ./controllers/ 16 | COPY definitions/ ./definitions/ 17 | COPY plugins/ ./plugins/ 18 | COPY public/ ./public/ 19 | COPY resources/ ./resources/ 20 | COPY schemas/ ./schemas/ 21 | COPY views/ ./views/ 22 | COPY database.sql . 23 | COPY index.js . 24 | COPY config . 25 | COPY license.txt . 26 | COPY package.json . 27 | 28 | RUN npm install 29 | EXPOSE 8000 30 | 31 | CMD [ "npm", "start" ] -------------------------------------------------------------------------------- /bundle.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('total5'); 4 | 5 | var path = '--bundles--'; 6 | 7 | function buildplugin(name, callback) { 8 | console.log('| |--', name + '.bundle'); 9 | Total.backup(path + '/' + name + '.bundle', PATH.root(), callback, function(path, isdir) { 10 | return path === '/' || path === '/plugins/' || (path.indexOf('plugins/' + name) !== -1); 11 | }); 12 | } 13 | 14 | console.log('|-- Total.js bundle compiler'); 15 | console.time('|-- Compilation'); 16 | 17 | console.log('| |--', 'app.bundle'); 18 | Total.backup(path + '/app.bundle', PATH.root(), function() { 19 | F.Fs.readdir(PATH.root('plugins'), function(err, response) { 20 | response.wait(function(key, next) { 21 | buildplugin(key, next); 22 | }, function() { 23 | console.timeEnd('|-- Compilation'); 24 | }); 25 | }); 26 | }, function(path, isdir) { 27 | 28 | if (!isdir) 29 | return path.split('/').length > 2; 30 | 31 | var p = path.split('/').trim(); 32 | 33 | if (!p[0] || (p.length === 1 && p[0] === 'plugins')) 34 | return true; 35 | 36 | var allowed = ['controllers', 'definitions', 'modules', 'public', 'resources', 'schemas', 'views']; 37 | 38 | for (var m of allowed) { 39 | if (path.indexOf(m) === 1) 40 | return true; 41 | } 42 | 43 | return false; 44 | }); -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | // database : postgresql://user:pass@localhost:5432/database 2 | database (env) : DATABASE -------------------------------------------------------------------------------- /controllers/api.js: -------------------------------------------------------------------------------- 1 | exports.install = function() { 2 | 3 | ROUTE('+GET /logout/ --> Account/logout'); 4 | ROUTE('-GET /auth/ --> Account/token'); 5 | 6 | ROUTE('+API /api/ -session --> Account/session'); 7 | ROUTE('+API /api/ -account --> Account/read'); 8 | ROUTE('+API /api/ +account_update --> Account/update'); 9 | ROUTE('+API /api/ -run/{appid} --> Account/run'); 10 | ROUTE('+API /api/ -apps --> Account/apps'); 11 | ROUTE('+API /api/ +reorder --> Account/reorder'); 12 | ROUTE('+API /api/ -notifications --> Account/notifications'); 13 | ROUTE('+API /api/ -notifications_clear --> Account/notifications_clear'); 14 | ROUTE('+API /api/ -sessions --> Account/sessions'); 15 | ROUTE('+API /api/ -sessions_remove/{id} --> Account/sessions_remove'); 16 | ROUTE('+API /api/ +password --> Account/password'); 17 | ROUTE('+API /api/ +feedback --> Account/feedback'); 18 | 19 | ROUTE('GET /users/', users); 20 | 21 | ROUTE('POST /upload/base64/ <2MB', upload); 22 | ROUTE('FILE /files/*.jpg', files); 23 | }; 24 | 25 | async function upload($) { 26 | 27 | if (BLOCKED($, 20)) { 28 | $.invalid(401); 29 | return; 30 | } 31 | 32 | if (!$.user && $.query.token !== CONF.servicetoken) { 33 | $.invalid(401); 34 | return; 35 | } 36 | 37 | BLOCKED($, null); 38 | 39 | var name = $.body.filename || $.body.name; 40 | if (!name) { 41 | $.invalid('Invalid file name'); 42 | return; 43 | } 44 | 45 | var file = $.body.file.parseDataURI(); 46 | if (!file) { 47 | $.invalid('Invalid data'); 48 | return; 49 | } 50 | 51 | var type = file.type; 52 | if (!type || type !== 'image/jpeg') { 53 | $.invalid('Invalid file type'); 54 | return; 55 | } 56 | 57 | var buffer = file.buffer; 58 | if (!buffer) { 59 | $.invalid('Invalid file data'); 60 | return; 61 | } 62 | 63 | var id = UID(); 64 | await FILESTORAGE('files').save(id, 'base64.jpg', buffer); 65 | $.json('/files/' + FUNC.checksum(id) + '.jpg'); 66 | } 67 | 68 | function files($) { 69 | 70 | if ($.split.length !== 2) { 71 | $.invalid(404); 72 | return; 73 | } 74 | 75 | var id = $.split[1]; 76 | 77 | id = id.substring(0, id.lastIndexOf('.')); 78 | var arr = id.split('X'); 79 | 80 | if (FUNC.checksum(arr[0]) === id) 81 | $.filefs('files', arr[0]); 82 | else 83 | $.invalid(404); 84 | 85 | } 86 | 87 | function users($) { 88 | 89 | if (!CONF.allow_token || !CONF.token) { 90 | $.invalid('Public API is disabled'); 91 | return; 92 | } 93 | 94 | if (BLOCKED($, 20)) { 95 | $.invalid(401); 96 | return; 97 | } 98 | 99 | var token = $.query.token || $.headers['x-token']; 100 | 101 | if (token !== CONF.token) { 102 | $.invalid(401); 103 | return; 104 | } 105 | 106 | BLOCKED($, null); 107 | 108 | var db = DB(); 109 | 110 | db.query('SELECT a.id,a.name,a.icon,a.color FROM op.tbl_group a').set('groups'); 111 | db.query('SELECT a.id,a.reference,a.language,a.gender,a.photo,a.name,a.search,a.email,a.color,a.interface,a.sounds,a.notifications,a.sa,a.isdisabled,a.isinactive,a.isonline,a.dtlogged,a.dtcreated,a.dtupdated,ARRAY(SELECT x.groupid FROM op.tbl_user_group x WHERE x.userid=a.id) AS groups FROM op.tbl_user a WHERE a.isremoved=FALSE ORDER BY dtcreated ASC').set('users'); 112 | 113 | db.callback($.successful(function(response) { 114 | for (var m of response.users) { 115 | if (!m.color) 116 | m.color = CONF.color; 117 | if (m.photo) 118 | m.photo = CONF.url + m.photo; 119 | } 120 | $.json(response); 121 | })); 122 | } -------------------------------------------------------------------------------- /controllers/socket.js: -------------------------------------------------------------------------------- 1 | const RECONNECT_TIMEOUT = 10000; 2 | 3 | exports.install = function() { 4 | ROUTE('SOCKET /sync/', socket); 5 | }; 6 | 7 | function socket($) { 8 | 9 | MAIN.ws = $; 10 | $.autodestroy(() => MAIN.ws = null); 11 | 12 | let pending = []; 13 | let timeout = null; 14 | 15 | var resend = function() { 16 | 17 | if (timeout || !pending.length) 18 | return; 19 | 20 | let msg = pending.shift(); 21 | if (msg) { 22 | $.send(msg.text, client => client != msg.client); 23 | setImmediate(resend); 24 | } 25 | }; 26 | 27 | var resend_force = function() { 28 | timeout = null; 29 | setImmediate(resend); 30 | }; 31 | 32 | $.on('open', function(client) { 33 | client.confirmed = CONF.sync && client.query.token == CONF.sync_token; 34 | if (!client.confirmed) 35 | setTimeout(client => client.close(4001), 500, client); 36 | }); 37 | 38 | $.on('close', function(client) { 39 | if (client.confirmed) { 40 | timeout && clearTimeout(timeout); 41 | timeout = setTimeout(resend_force, RECONNECT_TIMEOUT); 42 | } 43 | }); 44 | 45 | $.on('message', function(client, message) { 46 | if (client.confirmed) { 47 | if (timeout) 48 | pending.push({ client: client, text: message }); 49 | else 50 | $.send(message, conn => conn !== client); 51 | } 52 | }); 53 | } -------------------------------------------------------------------------------- /controllers/verify.js: -------------------------------------------------------------------------------- 1 | const ERR_INVALID = 'Invalid token'; 2 | const SchemaNotification = 'body:String, path:String, icon:Icon, color:Color'.toJSONSchema(); 3 | 4 | exports.install = function() { 5 | 6 | // 3rd party apps 7 | ROUTE('GET /verify/', verify); 8 | ROUTE('POST /notify/', notify); 9 | ROUTE('GET /session/', session); 10 | 11 | }; 12 | 13 | async function verify($) { 14 | 15 | var reqtoken = $.query.token; 16 | var restoken = $.headers['x-token']; 17 | 18 | $.status = 401; 19 | 20 | var index = reqtoken.lastIndexOf('~'); 21 | var sign = reqtoken.substring(index + 1); 22 | 23 | reqtoken = reqtoken.substring(0, index); 24 | 25 | if (!restoken || !reqtoken) { 26 | $.invalid('Invalid token'); 27 | return; 28 | } 29 | 30 | // Internal in-memory cache 31 | if (MAIN.cache[reqtoken]) { 32 | $.status = 200; 33 | $.jsonstring(MAIN.cache[reqtoken]); 34 | return; 35 | } 36 | 37 | var reqarr = reqtoken.split('X'); 38 | if (FUNC.checksum(reqarr[0] + 'X' + reqarr[1]) !== reqtoken) { 39 | $.invalid(ERR_INVALID); 40 | return; 41 | } 42 | 43 | var session = await DATA.read('op.tbl_app_session').id(reqarr[0]).error('Invalid token session').promise($); 44 | if (session.reqtoken !== sign || session.restoken !== restoken) { 45 | $.invalid(ERR_INVALID); 46 | return; 47 | } 48 | 49 | var app = await DATA.read('op.tbl_app').fields('id,allow,reqtoken,restoken,isexternal').id(session.appid).error('Invalid token session').promise($); 50 | if (app.allow && app.allow.length) { 51 | if (!app.allow.includes($.ip)) { 52 | $.invalid('Not allowed IP address'); 53 | return; 54 | } 55 | } 56 | 57 | var user = await DATA.read('op.tbl_user').fields('id,email,name,photo,sa,gender,reference,language,color,interface,darkmode,sounds,notifications,dtbirth,dtcreated,dtupdated,isonline,isdisabled,isinactive').id(session.userid).where('isremoved=FALSE').error('User not found').promise($); 58 | 59 | if (user.isdisabled) { 60 | $.invalid('User has been disabled'); 61 | return; 62 | } 63 | 64 | if (user.isinactive) { 65 | $.invalid('User is inactive'); 66 | return; 67 | } 68 | 69 | var userappid = user.id + app.id; 70 | var userapp = await DATA.read('op.tbl_user_app').fields('notify').id(userappid).promise(); 71 | 72 | if (!userapp) { 73 | userapp = FUNC.makenotify(app, user.id); 74 | userapp.id = userappid; 75 | userapp.appid = session.appid; 76 | userapp.userid = user.id; 77 | await DATA.insert('op.tbl_user_app', userapp).promise(); 78 | } 79 | 80 | if (!userapp.notify) { 81 | userapp = FUNC.makenotify(app, user.id); 82 | userapp.dtupdated = NOW; 83 | await DATA.modify('op.tbl_user_app', userapp).id(userappid).promise(); 84 | } 85 | 86 | // Clean useless fields 87 | user.isinactive = undefined; 88 | user.isdisabled = undefined; 89 | user.isonline = undefined; 90 | 91 | if (user.photo) 92 | user.photo = CONF.url + user.photo; 93 | 94 | if (!user.language) 95 | user.language = CONF.language; 96 | 97 | if (!user.color) 98 | user.color = CONF.color; 99 | 100 | FUNC.permissions(user.id, async function(data) { 101 | 102 | user.openplatformid = CONF.id; 103 | user.openplatform = CONF.url; 104 | user.appid = session.appid; 105 | user.ssid = FUNC.checksum(session.id + 'X' + session.sessionid); 106 | 107 | if (user.notifications) 108 | user.notify = userapp.notify; 109 | 110 | user.permissions = data.permissions[app.id] || EMPTYARRAY; 111 | user.iframe = app.isexternal ? false : true; 112 | user.groups = data.groups; 113 | 114 | user.platform = {}; 115 | 116 | if (CONF.icon) 117 | user.platform.logo = CONF.url + CONF.icon; 118 | 119 | user.platform.name = CONF.name; 120 | user.platform.groups = await DATA.find('op.tbl_group').fields('id,name').sort('name').promise($); 121 | user.platform.apps = await DATA.find('op.tbl_app').fields('id,name,icon,color,reference').sort('name').where('isremoved=FALSE and isdisabled=FALSE').in('id', data.apps).promise($); 122 | 123 | $.transform('verify', user, function(err, user) { 124 | 125 | // Compress data 126 | for (var key in user) { 127 | var val = user[key]; 128 | if (val == null || val === '') 129 | user[key] = undefined; 130 | } 131 | 132 | MAIN.cache[reqtoken] = JSON.stringify(user); 133 | $.status = 200; 134 | $.jsonstring(MAIN.cache[reqtoken]); 135 | 136 | }); 137 | 138 | }); 139 | } 140 | 141 | async function notify($) { 142 | 143 | var reqtoken = $.query.token; 144 | var restoken = $.headers['x-token']; 145 | var id; 146 | 147 | $.response.status = 401; 148 | 149 | if (!restoken || !reqtoken) { 150 | $.invalid(ERR_INVALID); 151 | return; 152 | } 153 | 154 | var tmp = SchemaNotification.transform($.body); 155 | if (tmp.error) { 156 | $.invalid(tmp.error); 157 | return; 158 | } 159 | 160 | $.body = tmp.response; 161 | 162 | if (MAIN.cache[reqtoken]) { 163 | $.status = 200; 164 | id = makenotification($, MAIN.cache[reqtoken]); 165 | $.success(id); 166 | return; 167 | } 168 | 169 | var reqtokenerr = 'err' + reqtoken; 170 | 171 | if (MAIN.cache[reqtokenerr]) { 172 | $.invalid(MAIN.cache[reqtokenerr]); 173 | return; 174 | } 175 | 176 | var reqarr = reqtoken.split('X'); 177 | 178 | if (FUNC.checksum(reqarr[0] + 'X' + reqarr[1]) !== reqtoken) { 179 | MAIN.cache[reqtokenerr] = ERR_INVALID; 180 | $.invalid(ERR_INVALID); 181 | return; 182 | } 183 | 184 | var userapp = await DATA.read('op.tbl_user_app').fields('userid,appid,notifytoken,notifications').id(reqarr[0]).promise($); 185 | 186 | if (!userapp || restoken !== userapp.notifytoken) { 187 | MAIN.cache[reqtokenerr] = ERR_INVALID; 188 | $.invalid(ERR_INVALID); 189 | return; 190 | } 191 | 192 | if (!userapp.notifications) { 193 | MAIN.cache[reqtokenerr] = 'Not allowed notifications'; 194 | $.invalid(MAIN.cache[reqtokenerr]); 195 | return; 196 | } 197 | 198 | var user = await DATA.read('op.tbl_user').fields('name,email,notifications,isremoved,isconfirmed,isdisabled,isinactive').id(userapp.userid).promise($); 199 | 200 | if (!user || user.isremoved || !user.isconfirmed || user.isdisabled || user.isinactive) { 201 | MAIN.cache[reqtokenerr] = ERR_INVALID; 202 | $.invalid(ERR_INVALID); 203 | return; 204 | } 205 | 206 | if (!user.notifications) { 207 | MAIN.cache[reqtokenerr] = 'Not allowed notifications'; 208 | $.invalid(MAIN.cache[reqtokenerr]); 209 | return; 210 | } 211 | 212 | var app = await DATA.read('op.tbl_app').id(userapp.appid).fields('allow,name,icon,color').promise(); 213 | if (app.allow && app.allow.length && app.allow.indexOf($.ip) === -1) { 214 | MAIN.cache[reqtokenerr] = 'Not allowed IP address'; 215 | $.invalid(MAIN.cache[reqtokenerr]); 216 | return; 217 | } 218 | 219 | userapp.user = user.name; 220 | userapp.email = user.email; 221 | userapp.name = app.name; 222 | userapp.icon = app.icon; 223 | userapp.color = app.color; 224 | 225 | $.status = 200; 226 | userapp.notifytoken = undefined; 227 | MAIN.cache[reqtoken] = userapp; 228 | id = makenotification($, userapp); 229 | 230 | if (id) 231 | $.success(id); 232 | else 233 | $.invalid('@(Invalid data)'); 234 | } 235 | 236 | function makenotificationunread(user) { 237 | user.unread++; 238 | } 239 | 240 | NEWPUBLISH('Notifications.create', 'id,userid,appid,body,path,app,user,email,color,icon,dtcreated'); 241 | 242 | async function makenotification($, userapp) { 243 | 244 | var data = $.body; 245 | var model = {}; 246 | 247 | model.id = UID(); 248 | model.userid = userapp.userid; 249 | model.appid = userapp.appid; 250 | model.body = data.body || ''; 251 | model.path = data.path; 252 | model.name = userapp.name; 253 | model.color = data.color || userapp.color; 254 | model.icon = data.icon || userapp.icon; 255 | model.dtcreated = NOW = new Date(); 256 | 257 | model = await $.transform('notify', model); 258 | 259 | await DATA.insert('op.tbl_notification', model).promise(); 260 | 261 | if (model.body) 262 | await DATA.query('UPDATE op.tbl_user SET unread=unread+1 WHERE id={0}'.format(PG_ESCAPE(userapp.userid))).promise(); 263 | 264 | MAIN.auth.update(model.userid, makenotificationunread); 265 | 266 | if (CONF.$tms) { 267 | model.app = model.name; 268 | model.user = userapp.name; 269 | model.email = userapp.email; 270 | model.name = undefined; 271 | PUBLISH('Notifications.create', model); 272 | } 273 | 274 | return model.id; 275 | } 276 | 277 | async function session($) { 278 | 279 | var session = $.query.ssid || $.query.openplatformid || $.query.token || $.query.session; 280 | 281 | if (!session) { 282 | $.invalid('@(Invalid token)'); 283 | return; 284 | } 285 | 286 | var arr = session.split('X'); 287 | 288 | if (FUNC.checksum(arr[0] + 'X' + arr[1]) !== session) { 289 | $.invalid('@(Invalid token)'); 290 | return; 291 | } 292 | 293 | var user = await DATA.query('SELECT b.id,b.language,b.name,b.color,b.sa,a.isonline FROM op.tbl_session a INNER JOIN op.tbl_user b ON b.id=a.userid AND b.isremoved=FALSE AND b.isdisabled=FALSE AND b.isinactive=FALSE WHERE a.id={0} AND dtexpire>=NOW()'.format(PG_ESCAPE(arr[1]))).first().promise($); 294 | if (user) { 295 | user = await $.transform('session', user); 296 | $.json(user); 297 | } else 298 | $.invalid('@(Invalid token)'); 299 | } -------------------------------------------------------------------------------- /database-2023-09-15.sql: -------------------------------------------------------------------------------- 1 | --- The script is targeted only for older existing instances 2 | ALTER TABLE op.tbl_app ADD COLUMN "isscrollbar" bool DEFAULT false; -------------------------------------------------------------------------------- /database-2024-03-21.sql: -------------------------------------------------------------------------------- 1 | --- The script is targeted only for older existing instances 2 | ALTER TABLE op.tbl_app ADD COLUMN "isexternal" bool DEFAULT false; -------------------------------------------------------------------------------- /database-2024-10-22.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO op.cl_config (id, value, type, name) VALUES('newtab', 'true', 'boolean', 'Open external and bookmarks in new tab/window'); -------------------------------------------------------------------------------- /database-2024-10-30.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO op.cl_config (id, value, type, name) VALUES('sync', 'false', 'boolean', 'WebSocket synchronization endpoint for 3rd party apps'); 2 | INSERT INTO op.cl_config (id, value, type, name) VALUES('sync_token', md5(random()::text), 'string', 'WebSocket synchronization token'); -------------------------------------------------------------------------------- /database.sql: -------------------------------------------------------------------------------- 1 | --------------------------------------- 2 | -- TABLES 3 | --------------------------------------- 4 | 5 | CREATE SCHEMA op; 6 | 7 | --------------------------------------- 8 | -- TABLES 9 | --------------------------------------- 10 | 11 | CREATE TABLE "op"."cl_config" ( 12 | "id" text NOT NULL, 13 | "value" text, 14 | "type" text, 15 | "name" text, 16 | "dtupdated" timestamp DEFAULT now(), 17 | PRIMARY KEY ("id") 18 | ); 19 | 20 | CREATE TABLE "op"."tbl_group" ( 21 | "id" text NOT NULL, 22 | "checksum" text, 23 | "reference" text, 24 | "name" text, 25 | "color" text, 26 | "icon" text, 27 | "isdisabled" bool DEFAULT false, 28 | "isprocessed" bool DEFAULT false, 29 | "dtprocessed" timestamp, 30 | "dtcreated" timestamp DEFAULT now(), 31 | "dtupdated" timestamp, 32 | PRIMARY KEY ("id") 33 | ); 34 | 35 | CREATE TABLE "op"."tbl_user" ( 36 | "id" text NOT NULL, 37 | "token" text, 38 | "checksum" text, 39 | "reference" text, 40 | "language" text, 41 | "gender" text, 42 | "photo" text, 43 | "name" text, 44 | "search" text, 45 | "email" text, 46 | "password" text, 47 | "color" text, 48 | "interface" text, 49 | "unread" int4 DEFAULT 0, 50 | "darkmode" int2 DEFAULT 0, 51 | "logged" int4 DEFAULT 0, 52 | "sounds" bool DEFAULT true, 53 | "notifications" bool DEFAULT true, 54 | "sa" bool DEFAULT false, 55 | "isreset" bool DEFAULT false, 56 | "isdisabled" bool DEFAULT false, 57 | "isconfirmed" bool DEFAULT false, 58 | "isinactive" bool DEFAULT false, 59 | "isonline" bool DEFAULT false, 60 | "isprocessed" bool DEFAULT false, 61 | "isremoved" bool DEFAULT false, 62 | "cache" json, 63 | "cachefilter" _text, 64 | "dtbirth" date, 65 | "dtnotified" timestamp, 66 | "dtlogged" timestamp, 67 | "dtprocessed" timestamp, 68 | "dtpassword" timestamp, 69 | "dtcreated" timestamp DEFAULT now(), 70 | "dtremoved" timestamp, 71 | "dtupdated" timestamp, 72 | PRIMARY KEY ("id") 73 | ); 74 | 75 | COMMENT ON COLUMN "op"."tbl_user"."checksum" IS 'It can help with synchronization users from difference sources'; 76 | COMMENT ON COLUMN "op"."tbl_user"."isprocessed" IS 'Was the record processed by 3rd party system?'; 77 | 78 | CREATE TABLE "op"."tbl_session" ( 79 | "id" text NOT NULL, 80 | "userid" text, 81 | "ua" text, 82 | "ip" text, 83 | "isonline" bool DEFAULT false, 84 | "isreset" bool DEFAULT false, 85 | "logged" int4 DEFAULT 0, 86 | "device" text, 87 | "dtlogged" timestamp, 88 | "dtexpire" timestamp, 89 | "dtcreated" timestamp DEFAULT now(), 90 | CONSTRAINT "tbl_session_userid_fkey" FOREIGN KEY ("userid") REFERENCES "op"."tbl_user"("id") ON DELETE CASCADE ON UPDATE CASCADE, 91 | PRIMARY KEY ("id") 92 | ); 93 | 94 | CREATE TABLE "op"."tbl_app" ( 95 | "id" text NOT NULL, 96 | "checksum" text, 97 | "reference" text, 98 | "name" text, 99 | "color" text, 100 | "icon" text, 101 | "meta" text, 102 | "url" text, 103 | "reqtoken" text, 104 | "restoken" text, 105 | "allow" text, 106 | "cache" json, 107 | "sortindex" int2 DEFAULT 0, 108 | "notifications" bool DEFAULT true, 109 | "isnewtab" bool DEFAULT false, 110 | "isexternal" bool DEFAULT false, 111 | "isbookmark" bool DEFAULT false, 112 | "isprocessed" bool DEFAULT false, 113 | "isdisabled" bool DEFAULT false, 114 | "isscrollbar" bool DEFAULT false, 115 | "isremoved" bool DEFAULT false, 116 | "logged" int4 DEFAULT 0, 117 | "dtlogged" timestamp, 118 | "dtprocessed" timestamp, 119 | "dtupdated" timestamp, 120 | "dtcreated" timestamp DEFAULT now(), 121 | "dtremoved" timestamp, 122 | PRIMARY KEY ("id") 123 | ); 124 | 125 | COMMENT ON COLUMN "op"."tbl_app"."allow" IS 'Allows only specific IP addresses'; 126 | 127 | CREATE TABLE "op"."tbl_app_permission" ( 128 | "id" text NOT NULL, 129 | "appid" text, 130 | "name" text, 131 | "value" text, 132 | "sortindex" int2 DEFAULT 0, 133 | CONSTRAINT "tbl_app_permission_appid_fkey" FOREIGN KEY ("appid") REFERENCES "op"."tbl_app"("id") ON DELETE CASCADE ON UPDATE CASCADE, 134 | PRIMARY KEY ("id") 135 | ); 136 | 137 | CREATE TABLE "op"."tbl_group_permission" ( 138 | "id" text NOT NULL, 139 | "appid" text, 140 | "permissionid" text, 141 | "groupid" text, 142 | CONSTRAINT "tbl_group_permission_appid_fkey" FOREIGN KEY ("appid") REFERENCES "op"."tbl_app"("id") ON DELETE CASCADE ON UPDATE CASCADE, 143 | CONSTRAINT "tbl_group_permission_permissionid_fkey" FOREIGN KEY ("permissionid") REFERENCES "op"."tbl_app_permission"("id") ON DELETE CASCADE ON UPDATE CASCADE, 144 | CONSTRAINT "tbl_group_permission_groupid_fkey" FOREIGN KEY ("groupid") REFERENCES "op"."tbl_group"("id") ON DELETE CASCADE ON UPDATE CASCADE, 145 | PRIMARY KEY ("id") 146 | ); 147 | 148 | CREATE TABLE "op"."tbl_app_session" ( 149 | "id" text NOT NULL, 150 | "sessionid" text, 151 | "userid" text, 152 | "appid" text, 153 | "device" text, 154 | "ip" text, 155 | "url" text, 156 | "reqtoken" text, 157 | "restoken" text, 158 | "dtexpire" timestamp, 159 | "dtcreated" timestamp DEFAULT now(), 160 | CONSTRAINT "tbl_app_session_sessionid_fkey" FOREIGN KEY ("sessionid") REFERENCES "op"."tbl_session"("id") ON DELETE CASCADE ON UPDATE CASCADE, 161 | CONSTRAINT "tbl_app_session_appid_fkey" FOREIGN KEY ("appid") REFERENCES "op"."tbl_app"("id") ON DELETE CASCADE ON UPDATE CASCADE, 162 | CONSTRAINT "tbl_app_session_userid_fkey" FOREIGN KEY ("userid") REFERENCES "op"."tbl_user"("id") ON DELETE CASCADE ON UPDATE CASCADE, 163 | PRIMARY KEY ("id") 164 | ); 165 | 166 | COMMENT ON COLUMN "op"."tbl_app_session"."reqtoken" IS 'Pregenerated for verification purpose only'; 167 | COMMENT ON COLUMN "op"."tbl_app_session"."restoken" IS 'Pregenerated for verification purpose only'; 168 | 169 | CREATE TABLE "op"."tbl_notification" ( 170 | "id" text NOT NULL, 171 | "userid" text, 172 | "appid" text, 173 | "color" text, 174 | "icon" text, 175 | "name" text, 176 | "body" text, 177 | "path" text, 178 | "isread" bool DEFAULT false, 179 | "dtcreated" timestamp DEFAULT now(), 180 | CONSTRAINT "tbl_notification_userid_fkey" FOREIGN KEY ("userid") REFERENCES "op"."tbl_user"("id") ON DELETE CASCADE ON UPDATE CASCADE, 181 | CONSTRAINT "tbl_notification_appid_fkey" FOREIGN KEY ("appid") REFERENCES "op"."tbl_app"("id") ON DELETE CASCADE ON UPDATE CASCADE, 182 | PRIMARY KEY ("id") 183 | ); 184 | 185 | CREATE TABLE "op"."tbl_user_app" ( 186 | "id" text NOT NULL, 187 | "userid" text, 188 | "appid" text, 189 | "notify" text, 190 | "notifytoken" text, 191 | "unread" int4 DEFAULT 0, 192 | "badges" int4 DEFAULT 0, 193 | "sortindex" int2 DEFAULT 0, 194 | "isfavorite" bool DEFAULT false, 195 | "notifications" bool DEFAULT true, 196 | "muted" timestamp, 197 | "dtupdated" timestamp DEFAULT now(), 198 | CONSTRAINT "tbl_user_app_userid_fkey" FOREIGN KEY ("userid") REFERENCES "op"."tbl_user"("id") ON DELETE CASCADE ON UPDATE CASCADE, 199 | CONSTRAINT "tbl_user_app_appid_fkey" FOREIGN KEY ("appid") REFERENCES "op"."tbl_app"("id") ON DELETE CASCADE ON UPDATE CASCADE, 200 | PRIMARY KEY ("id") 201 | ); 202 | 203 | CREATE TABLE "op"."tbl_user_group" ( 204 | "id" text NOT NULL, 205 | "userid" text, 206 | "groupid" text, 207 | CONSTRAINT "tbl_user_group_groupid_fkey" FOREIGN KEY ("groupid") REFERENCES "op"."tbl_group"("id") ON DELETE CASCADE ON UPDATE CASCADE, 208 | CONSTRAINT "tbl_user_group_userid_fkey" FOREIGN KEY ("userid") REFERENCES "op"."tbl_user"("id") ON DELETE CASCADE ON UPDATE CASCADE, 209 | PRIMARY KEY ("id") 210 | ); 211 | 212 | COMMENT ON COLUMN "op"."tbl_user_group"."id" IS 'userid + groupid'; 213 | 214 | CREATE TABLE "op"."tbl_visitor" ( 215 | "id" int4 NOT NULL, 216 | "maxlogged" int4 DEFAULT 0, 217 | "desktop" int4 DEFAULT 0, 218 | "mobile" int4 DEFAULT 0, 219 | "tablet" int4 DEFAULT 0, 220 | "date" date, 221 | PRIMARY KEY ("id") 222 | ); 223 | 224 | CREATE TABLE "op"."tbl_feedback" ( 225 | "id" text NOT NULL, 226 | "userid" text, 227 | "appid" text, 228 | "account" text, 229 | "email" text, 230 | "app" text, 231 | "ua" text, 232 | "ip" text, 233 | "body" text, 234 | "rating" int2 DEFAULT 0, 235 | "updatedby" text, 236 | "iscomplete" bool DEFAULT false, 237 | "dtcreated" timestamp DEFAULT now(), 238 | "dtupdated" timestamp, 239 | CONSTRAINT "tbl_feedback_appid_fkey" FOREIGN KEY ("appid") REFERENCES "op"."tbl_app"("id") ON DELETE CASCADE ON UPDATE CASCADE, 240 | CONSTRAINT "tbl_feedback_userid_fkey" FOREIGN KEY ("userid") REFERENCES "op"."tbl_user"("id") ON DELETE CASCADE ON UPDATE CASCADE, 241 | PRIMARY KEY ("id") 242 | ); 243 | 244 | --------------------------------------- 245 | -- INDEXES 246 | --------------------------------------- 247 | 248 | CREATE INDEX "tbl_user_idx" ON "op"."tbl_user" USING BTREE ("email"); 249 | CREATE INDEX "tbl_session_idx" ON "op"."tbl_session" USING BTREE ("userid"); 250 | CREATE INDEX "tbl_notification_idx" ON "op"."tbl_notification" USING BTREE ("userid","dtcreated"); 251 | CREATE INDEX "tbl_user_group_idx" ON "op"."tbl_user_group" USING BTREE ("userid","groupid"); 252 | 253 | --------------------------------------- 254 | -- VIEWS 255 | --------------------------------------- 256 | 257 | CREATE OR REPLACE VIEW op.view_user AS 258 | SELECT 259 | a.id, 260 | a.name, 261 | a.language, 262 | a.gender, 263 | a.photo, 264 | a.search, 265 | a.email, 266 | a.color, 267 | a.interface, 268 | a.unread, 269 | a.darkmode, 270 | a.logged, 271 | a.sounds, 272 | a.notifications, 273 | a.sa, 274 | a.isconfirmed, 275 | a.isdisabled, 276 | a.isinactive, 277 | a.dtbirth, 278 | a.dtlogged, 279 | a.dtpassword, 280 | a.dtcreated, 281 | a.dtupdated, 282 | a.dtnotified, 283 | array_to_string(ARRAY(SELECT b.name FROM op.tbl_group b WHERE (b.id IN (SELECT c.groupid FROM op.tbl_user_group c WHERE c.userid = a.id))), ', '::text) AS groups, 284 | a.isonline 285 | FROM op.tbl_user a 286 | WHERE a.isremoved = false; 287 | 288 | CREATE OR REPLACE VIEW op.view_group AS 289 | SELECT 290 | a.id, 291 | a.reference, 292 | a.name, 293 | a.color, 294 | a.icon, 295 | a.isdisabled, 296 | a.dtcreated, 297 | a.dtupdated, 298 | (SELECT count(1) AS count FROM op.tbl_user_group b WHERE b.groupid = a.id) AS users 299 | FROM op.tbl_group a; 300 | 301 | --------------------------------------- 302 | -- DATA 303 | --------------------------------------- 304 | 305 | INSERT INTO "op"."cl_config" ("id", "value", "type", "name") VALUES 306 | ('app_session_expire', '1 day', 'string', 'App session expiration'), 307 | ('auth_cookie', '{cookie}', 'string', 'Cookie name'), 308 | ('auth_cookie_expire', '1 month', 'string', 'Cookie expiration'), 309 | ('auth_cookie_options', '{"httponly":true,"security":"lax"}', 'object', 'Cookie settings'), 310 | ('auth_expire', '5 minutes', 'string', 'Session expiration'), 311 | ('auth_secret', '{secret}', 'string', 'Cookie secret'), 312 | ('auth_strict', 'false', 'boolean', 'Strict session'), 313 | ('$tms', 'false', 'boolean', 'Allow TMS'), 314 | ('allow_token', 'false', 'boolean', 'Allow API'), 315 | ('newtab', 'false', 'boolean', 'Open external and bookmarks in new tab/window'), 316 | ('cdn', 'https://cdn.componentator.com/', 'string', 'CDN'), 317 | ('color', '#4285F4', 'string', 'Color'), 318 | ('icon', '/icon.png', 'string', 'Icon'), 319 | ('id', '{id}', 'string', 'ID'), 320 | ('language', 'eu', 'string', 'A default language'), 321 | ('mail_from', '', 'string', 'Sender address'), 322 | ('mail_smtp', '', 'string', 'SMTP server'), 323 | ('mail_smtp_options', '{"port":465,"secure":true,"user":"","password":""}', 'object', 'SMTP options'), 324 | ('name', 'OpenPlatform', 'string', 'Name'), 325 | ('salt', '{salt}', 'string', 'Salt for passwords'), 326 | ('saltchecksum', '{saltchecksum}', 'string', 'Salf for checksums'), 327 | ('secret', '{secret}', 'string', 'Secret for tokens'), 328 | ('secret_tms', '{tms}', 'string', 'TMS token'), 329 | ('sync', 'false', 'boolean', 'WebSocket synchronization endpoint for 3rd party apps'), 330 | ('sync_token', md5(random()::text), 'string', 'WebSocket synchronization token'), 331 | ('token', '{maintoken}', 'string', 'Secret token'), 332 | ('url', '{url}', 'string', 'URL address'); 333 | 334 | -- DEFAULT GROUP 335 | INSERT INTO "op"."tbl_group" ("id", "name", "dtcreated") VALUES('{groupid}', 'Admin', NOW()); 336 | 337 | -- DEFAULT USER 338 | INSERT INTO "op"."tbl_user" ("id", "token", "name", "search", "email", "password", "color", "sa", "isconfirmed", "dtcreated") VALUES('{userid}', '{token}', 'John Connor', 'john conor', 'info@totaljs.com', '{password}', '#4285F4', 't', 't', NOW()); 339 | 340 | -- ASSIGN A DEFAULT GROUP TO THE USER 341 | INSERT INTO "op"."tbl_user_group" ("id", "userid", "groupid") VALUES('{userid}{groupid}', '{userid}', '{groupid}'); -------------------------------------------------------------------------------- /definitions/func.js: -------------------------------------------------------------------------------- 1 | FUNC.checksum = function(val) { 2 | return val + 'X' + val.makeid(CONF.saltchecksum); 3 | }; 4 | 5 | FUNC.permissions = async function(userid, callback) { 6 | 7 | var db = DB(); 8 | var user = await db.read('op.tbl_user').fields('cache').id(userid).promise(); 9 | 10 | if (user && user.cache) { 11 | callback(user.cache); 12 | return; 13 | } 14 | 15 | var groups = await db.query('SELECT a.id FROM op.tbl_group a WHERE a.isdisabled=FALSE AND a.id IN (SELECT b.groupid FROM op.tbl_user_group b WHERE b.userid={0})'.format(PG_ESCAPE(userid))).promise(); 16 | var apermissions = await db.find('op.tbl_group_permission').fields('permissionid,appid').in('groupid', groups, 'id').promise(); 17 | var allowed = []; 18 | var cache = {}; 19 | 20 | for (let m of apermissions) { 21 | if (m.permissionid) 22 | allowed.push(m); 23 | else if (!cache[m.appid]) 24 | cache[m.appid] = []; 25 | } 26 | 27 | var permissions = EMPTYARRAY; 28 | 29 | if (allowed.length) 30 | permissions = await db.find('op.tbl_app_permission').fields('id,appid,value').in('id', allowed, 'permissionid').query('appid IN (SELECT x.id FROM op.tbl_app x WHERE x.isremoved=FALSE AND x.isdisabled=FALSE)').promise(); 31 | 32 | for (let m of permissions) { 33 | 34 | if (!cache[m.appid]) 35 | cache[m.appid] = []; 36 | 37 | if (m.value) 38 | cache[m.appid].push(m.value); 39 | 40 | } 41 | 42 | var apps = Object.keys(cache); 43 | var data = {}; 44 | 45 | data.apps = apps; 46 | data.permissions = cache; 47 | data.groups = []; 48 | 49 | for (var m of groups) 50 | data.groups.push(m.id); 51 | 52 | var filter = []; 53 | 54 | for (let m of groups) 55 | filter.push('G' + m.id); 56 | 57 | for (let m of apps) 58 | filter.push('A' + m); 59 | 60 | await db.modify('op.tbl_user', { cache: JSON.stringify(data), cachefilter: filter }).id(userid).promise(); 61 | callback(data); 62 | }; 63 | 64 | FUNC.clearcache = function(id) { 65 | FUNC.clearcacheinternal(); 66 | DB().query('UPDATE op.tbl_user SET cache=null, cachefilter=null WHERE {0}=ANY(cachefilter)'.format(PG_ESCAPE(id))); 67 | }; 68 | 69 | FUNC.clearcacheinternal = function() { 70 | MAIN.cache = {}; 71 | }; 72 | 73 | FUNC.makenotify = function(app, userid) { 74 | var obj = {}; 75 | var token = FUNC.checksum(userid + app.id + 'X' + CONF.id); 76 | obj.notify = CONF.url + '/notify/?token=' + token; 77 | obj.notifytoken = obj.notify.md5(app.reqtoken).md5(app.restoken); 78 | return obj; 79 | }; -------------------------------------------------------------------------------- /definitions/init.js: -------------------------------------------------------------------------------- 1 | require('querybuilderpg').init('', CONF.database, CONF.pooling || 1, ERROR('PostgreSQL')); 2 | 3 | MAIN.cache = {}; 4 | 5 | async function reconfigure() { 6 | 7 | var config = await DATA.find('op.cl_config').fields('id,value,type').promise(); 8 | 9 | LOADCONFIG(config); 10 | 11 | if (!CONF.id) 12 | CONF.id = Date.now().toString(36); 13 | 14 | if (!CONF.icon) 15 | CONF.icon = '/img/icon.png'; 16 | 17 | if (!CONF.color) 18 | CONF.color = '#4285F4'; 19 | 20 | var hostname = CONF.url; 21 | if (hostname) { 22 | if (hostname[hostname.length - 1] === '/') 23 | hostname = hostname.substring(0, hostname.length - 1); 24 | CONF.url = hostname; 25 | } 26 | 27 | CONF.ismail = CONF.mail_smtp && CONF.mail_from ? true : false; 28 | EMIT('configure'); 29 | } 30 | 31 | ON('service', async function(counter) { 32 | 33 | // 2 minutes 34 | // Clear internal cache 35 | if (counter % 2 === 0) 36 | FUNC.clearcacheinternal(); 37 | 38 | // Make stats 39 | if (counter % 3 === 0) 40 | makestats(); 41 | 42 | // 8 hours 43 | // Auto reconfiguration from DB 44 | if (counter % 480 === 0) 45 | reconfigure(); 46 | 47 | // 12 hours 48 | // Remove expired sessions 49 | if (counter % 720 === 0) { 50 | DATA.remove('op.tbl_app_session').where('dtexpire0 AND (dtnotified IS NULL OR (dtnotified + '3 days') <= NOW()) RETURNING id,language,name,color,email,unread").promise(); 58 | for (var m of users) { 59 | if (!m.color) 60 | m.color = CONF.color; 61 | MAIL(m.email, '@(Unread notifications) ({0})'.format(m.unread), 'mail/unread', m, m.language || CONF.language || ''); 62 | } 63 | } 64 | 65 | }); 66 | 67 | NEWPUBLISH('Stats.create', 'online:Number,date:Date,device'); 68 | async function makestats() { 69 | 70 | var online = await DATA.query('SELECT COUNT(1)::int4 AS count FROM op.tbl_session WHERE isonline=TRUE').promise(); 71 | 72 | online = online[0].count; 73 | 74 | if (online) { 75 | 76 | var devices = await DATA.query('SELECT device, COUNT(1)::int4 AS count FROM op.tbl_session WHERE isonline=TRUE GROUP BY device').promise(); 77 | var model = {}; 78 | 79 | model['>maxlogged'] = online; 80 | 81 | for (var m of devices) 82 | model['>' + m.device] = m.count; 83 | 84 | model.id = +NOW.format('yyyyMMdd'); 85 | model.date = NOW; 86 | 87 | await DATA.modify('op.tbl_visitor', model, true).id(model.id).promise(); 88 | PUBLISH('Stats.create', model); 89 | } 90 | } 91 | 92 | // Authorization 93 | function auth() { 94 | 95 | NEWPUBLISH('Session.create', 'id,sessionid,photo,name,language,sa:Boolean,color:Color,isreset:Boolean,sounds:Boolean,notifications:Boolean,unread:Number'); 96 | 97 | var options = {}; 98 | var onprofile = (userid, callback) => DATA.one('op.tbl_user').fields('id,photo,name,language,sa,color,interface,isreset,darkmode,sounds,notifications,unread').id(userid).where('isconfirmed=TRUE AND isdisabled=FALSE AND isinactive=FALSE AND isremoved=FALSE').callback(callback); 99 | 100 | function authconfig() { 101 | options.secret = CONF.auth_secret; 102 | options.cookie = CONF.auth_cookie; 103 | options.options = CONF.auth_cookie_options; 104 | options.expire = CONF.auth_expire || '5 minutes'; 105 | options.strict = CONF.auth_strict == null || CONF.auth_strict == true; 106 | options.ddos = CONF.auth_ddos || 10; 107 | } 108 | 109 | options.onsession = function(session, $) { 110 | if ($.url === '/setup/') { 111 | if (session.data.sa) 112 | $.success(session.data); 113 | else 114 | $.invalid(); 115 | return true; 116 | } 117 | }; 118 | 119 | options.onread = async function(meta, next) { 120 | 121 | // meta.sessionid {String} 122 | // meta.userid {String} 123 | // meta.ua {String} 124 | // next(err, USER_DATA) {Function} callback function 125 | 126 | var session = await DATA.one('op.tbl_session').fields('id,userid').id(meta.sessionid).query('dtexpire>NOW()').promise(); 127 | if (session) { 128 | onprofile(meta.userid, function(err, user) { 129 | if (user) { 130 | next(err, user); 131 | DATA.modify('op.tbl_session', { isonline: true, dtlogged: NOW, '+logged': 1, ua: meta.ua }).id(meta.sessionid); 132 | DATA.modify('op.tbl_user', { isreset: false, isonline: true, dtlogged: NOW, '+logged': 1 }).id(meta.userid); 133 | user.sessionid = meta.sessionid; 134 | PUBLISH('Session.create', user); 135 | } else 136 | next(err || 404); 137 | }); 138 | } else 139 | next(404); 140 | }; 141 | 142 | options.onfree = function(meta) { 143 | var mod = { isonline: false }; 144 | meta.sessions && DATA.modify('op.tbl_session', mod).query('isonline=TRUE').id(meta.sessions); 145 | meta.users && DATA.modify('op.tbl_user', mod).query('isonline=TRUE').id(meta.users); 146 | }; 147 | 148 | DATA.query('UPDATE op.tbl_session SET isonline=FALSE WHERE isonline=TRUE'); 149 | DATA.query('UPDATE op.tbl_user SET isonline=FALSE WHERE isonline=TRUE'); 150 | 151 | AUTH(options); 152 | DEF.onLocalize = $ => ($.user ? $.user.language : $.query.language) || CONF.language || ''; 153 | 154 | ON('configure', authconfig); 155 | 156 | MAIN.auth = {}; 157 | MAIN.auth.update = function(userid, fn) { 158 | options.update(userid, fn); 159 | }; 160 | 161 | MAIN.auth.login = function($, userid, callback) { 162 | 163 | var obj = {}; 164 | obj.id = UID(); 165 | obj.userid = userid; 166 | obj.ua = $.controller.ua; 167 | obj.ip = $.ip; 168 | obj.device = $.mobile ? 'mobile' : 'desktop'; 169 | obj.dtexpire = NOW.add(CONF.auth_cookie_expire || '1 month'); 170 | obj.dtcreated = NOW; 171 | 172 | DB().insert('op.tbl_session', obj).callback(function() { 173 | options.authcookie($, obj.id, userid, CONF.auth_cookie_expire); 174 | callback && callback(null, obj); 175 | }); 176 | 177 | }; 178 | 179 | MAIN.auth.logout = function($, callback) { 180 | DATA.remove('op.tbl_session').id($.sessionid).callback(function() { 181 | options.logout($); 182 | callback && callback(); 183 | }); 184 | }; 185 | 186 | MAIN.auth.refresh = userid => options.refresh(userid); 187 | MAIN.reconfigure = reconfigure; 188 | 189 | } 190 | 191 | async function init() { 192 | 193 | var op_tables = await DATA.query("SELECT FROM pg_tables WHERE schemaname='op' AND tablename='cl_config' LIMIT 1").promise(); 194 | 195 | if (op_tables.length) { 196 | PAUSESERVER('Database'); 197 | reconfigure(); 198 | auth(); 199 | return; 200 | } 201 | 202 | // DB is empty 203 | F.Fs.readFile(PATH.root('database.sql'), async function(err, buffer) { 204 | 205 | var data = {}; 206 | data.id = U.random_string(15).toLowerCase(); 207 | data.cookie = U.random_string(5).toLowerCase(); 208 | data.secret = GUID(25); 209 | data.salt = GUID(25); 210 | data.saltchecksum = CONF.saltchecksum = GUID(25); 211 | data.secret_tms = GUID(30); 212 | data.maintoken = GUID(30); 213 | data.tms = GUID(35); 214 | data.url = CONF.url || ''; 215 | data.token = FUNC.checksum(GUID(30)); 216 | data.userid = UID(); 217 | data.groupid = UID(); 218 | data.password = 'admin'.sha256(data.salt); 219 | 220 | // Temporary 221 | CONF.welcome = true; 222 | 223 | var sql = buffer.toString('utf8').arg(data); 224 | 225 | // Run SQL 226 | await DATA.query(sql).promise(); 227 | reconfigure(); 228 | auth(); 229 | 230 | PAUSESERVER('Database'); 231 | 232 | }); 233 | 234 | } 235 | 236 | PAUSESERVER('Database'); 237 | 238 | // Docker 239 | if (process.env.DATABASE) 240 | setTimeout(init, 3000); 241 | else 242 | init(); 243 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | postgres: 5 | container_name: postgres 6 | image: postgres:latest 7 | ports: 8 | - 5432:5432 9 | volumes: 10 | - pgdata:/var/lib/postgresql/data/ 11 | environment: 12 | - POSTGRES_USER=total 13 | - POSTGRES_PASSWORD=platform 14 | - POSTGRES_DB=openplatform 15 | healthcheck: 16 | test: ["CMD", "pg_isready", "-U", "total", "-d", "openplatform"] 17 | interval: 3s 18 | timeout: 3s 19 | retries: 5 20 | 21 | openplatform: 22 | container_name: openplatform 23 | image: totalplatform/openplatform:latest 24 | ports: 25 | - 8000:8000 26 | volumes: 27 | - openplatform:/www/databases/ 28 | environment: 29 | - DATABASE=postgresql://total:platform@postgres:5432/openplatform 30 | depends_on: 31 | postgres: 32 | condition: service_healthy 33 | 34 | volumes: 35 | pgdata: 36 | driver: local 37 | openplatform: 38 | driver: local -------------------------------------------------------------------------------- /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 2017-2024 (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. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OpenPlatform", 3 | "version": "0.0.1", 4 | "main": "index.js", 5 | "dependencies": { 6 | "total5": "latest", 7 | "querybuilderpg": "latest" 8 | }, 9 | "scripts": { 10 | "start": "node index.js 8000" 11 | }, 12 | "keywords": [ 13 | "openplatform" 14 | ], 15 | "author": "Total.js", 16 | "license": "MIT" 17 | } -------------------------------------------------------------------------------- /plugins/login/index.html: -------------------------------------------------------------------------------- 1 | @{layout('')} 2 | 3 | 4 | 5 | 6 | @(Login) - @{CONF.name} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 158 | 159 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /plugins/login/index.js: -------------------------------------------------------------------------------- 1 | exports.install = function() { 2 | ROUTE('-GET /*', '#login/index'); 3 | ROUTE('-POST /api/login/ --> Login/login'); 4 | ROUTE('-POST /api/reset/ --> Login/reset'); 5 | // ROUTE('-POST /api/create/ --> Login/create'); 6 | }; 7 | 8 | ON('ready', function() { 9 | COMPONENTATOR('uilogin', 'exec,locale,intranetcss,viewbox,errorhandler,message,input,validate,choose,enter,autofill', true); 10 | }); -------------------------------------------------------------------------------- /plugins/login/schemas/login.js: -------------------------------------------------------------------------------- 1 | NEWACTION('Login/login', { 2 | name: 'Sign in', 3 | input: '*email:Email, *password:String', 4 | publish: true, 5 | action: async function($, model) { 6 | 7 | $.publish(model); 8 | 9 | var profile = await DATA.read('op.tbl_user').fields('id,name,reference,email,isdisabled,isinactive,isconfirmed').where('email', model.email).where('password', model.password.sha256(CONF.salt)).where('isremoved=FALSE').error('@(Invalid credentials)').promise($); 10 | 11 | if (!profile.isconfirmed) { 12 | $.invalid('@(Account is not confirmed)'); 13 | return; 14 | } 15 | 16 | if (profile.isdisabled) { 17 | $.invalid('@(Account is disabled)'); 18 | return; 19 | } 20 | 21 | if (profile.isinactive) { 22 | $.invalid('@(Account is inactive)'); 23 | return; 24 | } 25 | 26 | MAIN.auth.login($, profile.id, $.done()); 27 | } 28 | }); 29 | 30 | NEWACTION('Login/reset', { 31 | name: 'Reset password', 32 | input: '*email:Email', 33 | publish: true, 34 | action: async function($, model) { 35 | 36 | $.publish(model); 37 | 38 | var profile = await DATA.read('op.tbl_user').fields('id,language,name,isdisabled,isinactive,color').where('email', model.email).where('isremoved=FALSE').error('@(Account not found)').promise($); 39 | 40 | if (profile.isdisabled) { 41 | $.invalid('@(Account is disabled)'); 42 | return; 43 | } 44 | 45 | if (profile.isinactive) { 46 | $.invalid('@(Account is inactive)'); 47 | return; 48 | } 49 | 50 | var data = {}; 51 | 52 | data.token = FUNC.checksum(GUID(30)); 53 | data.isreset = true; 54 | 55 | await DATA.modify('op.tbl_user', data).id(profile.id).promise($); 56 | 57 | profile.token = data.token; 58 | 59 | if (!profile.color) 60 | profile.color = CONF.color; 61 | 62 | CONF.ismail && MAIL(model.email, '@(Reset password)', 'mail/reset', profile, NOOP, profile.language || CONF.language || ''); 63 | $.success(); 64 | } 65 | }); 66 | 67 | NEWACTION('Login/create', { 68 | name: 'Create account', 69 | input: '*name:String, *email:Email, *password:String', 70 | publish: 'id,name,email,dtcreated:Date', 71 | action: async function($, model) { 72 | 73 | await DATA.check('op.tbl_user').where('email', model.email).where('isremoved=FALSE').error('@(E-mail address is already used)', true).promise($); 74 | 75 | model.id = UID(); 76 | model.dtcreated = NOW; 77 | model.password = model.password.sha256(CONF.salt); 78 | model.token = FUNC.checksum(GUID(30)); 79 | model.color = CONF.color; 80 | model.sounds = true; 81 | model.notifications = true; 82 | 83 | await DATA.insert('op.tbl_user', model).promise($); 84 | $.publish(model); 85 | 86 | CONF.ismail && MAIL(model.email, '@(Welcome)', 'mail/welcome', model, NOOP, model.language || CONF.language || ''); 87 | $.success(); 88 | } 89 | }); -------------------------------------------------------------------------------- /plugins/portal/index.js: -------------------------------------------------------------------------------- 1 | const REG_REDIRECT = /\/(api|auth|setup|notify|verify|upload)\//i; 2 | 3 | exports.install = function() { 4 | ROUTE('+GET /*', index); 5 | }; 6 | 7 | ON('ready', function() { 8 | COMPONENTATOR('uiportal', 'exec,locale,menu,notify,features,shortcuts,loading,importer,tangular-initials,viewbox,page,tablegrid,errorhandler,ready,box,intranetcss,input,validate,preview,colorselector,miniform,approve,intro,message,rating,sounds,windows,detach,movable,datepicker,noscrollbar', true); 9 | }); 10 | 11 | function index($) { 12 | 13 | if (REG_REDIRECT.test($.url)) { 14 | $.redirect('/'); 15 | return; 16 | } 17 | 18 | let redirect = $.query.redirect; 19 | 20 | if (redirect) { 21 | 22 | // external redirect 23 | // try to find app 24 | 25 | let beg = redirect.indexOf('/', 10); 26 | let app = beg === -1 ? redirect : redirect.substring(0, beg); 27 | 28 | $.query.redirect = ''; 29 | 30 | DATA.read('op.tbl_app').fields('id').search('url', app, 'beg').where('isremoved=FALSE AND isdisabled=FALSE AND (isexternal=TRUE OR isnewtab=TRUE)').error(404).callback(function(err, response) { 31 | 32 | if (err) { 33 | index($); 34 | return; 35 | } 36 | 37 | ACTION('Account/run', null, $).params({ appid: response.id }).user($.user).callback(function(err, response) { 38 | 39 | if (err) { 40 | index($); 41 | return; 42 | } 43 | 44 | var token = response.url.match(/(openplatform|ssid)=.*?(&|$)/)[0]; 45 | 46 | if (token[token.length - 1] === '&') 47 | token = token.substring(0, token.length - 1); 48 | 49 | redirect = redirect + (redirect.includes('&') ? '&' : '?') + token; 50 | $.redirect(redirect); 51 | }); 52 | 53 | }); 54 | 55 | return; 56 | } 57 | 58 | var plugins = []; 59 | 60 | for (let key in PLUGINS) { 61 | let item = PLUGINS[key]; 62 | item.portal && plugins.push(item.portal); 63 | } 64 | 65 | var view = $.view('#portal/index'); 66 | view.repository.plugins = plugins.length ? plugins.join('\n') : ''; 67 | } 68 | -------------------------------------------------------------------------------- /plugins/portal/public/default.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --radius: 5px; 3 | } 4 | 5 | html,body { padding: 0; margin: 0; overflow: hidden; background-color: #F7F7F7; height: 100%; } 6 | header { height: 53px; padding: 0 5px; position: absolute; bottom: 3px; left: 0; right: 0; color: #888; } 7 | header > div { position: relative; } 8 | header .logo { float: left; line-height: 45px; width: 50px; height: 50px; text-align: center; } 9 | header .logo img { height: 26px; image-rendering: -webkit-optimize-contrast; } 10 | header .user { float: right; width: 60px; text-align: right; margin: 4px 0 0; text-align: center; } 11 | header .user .initials { width: 38px; height: 38px; line-height: 38px; font-size: 14px; position: relative; display: inline-block; cursor: pointer; border-radius: 100px; } 12 | header .user .photo { width: 38px; height: 38px; line-height: 38px; font-size: 12px; position: relative; display: inline-block; cursor: pointer; border-radius: 100px; border: 1px solid #F0F0F0; image-rendering: -webkit-optimize-contrast; image-rendering: optimizeQuality; } 13 | header .openplatform { line-height: 48px; height: 49px; padding: 0 13px; float: left; font-size: 14px; color: #000; max-width: 250px; } 14 | header .openplatform b { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; display: block; } 15 | header .openplatform i { font-size: 18px; margin-right: 8px; float: left; line-height: 48px; } 16 | header .openplatform img { width: 32px; height: 32px; float: left; border-radius: 8px; margin: 7px 8px 0 0; border: 1px solid #E9E9E9; image-rendering: -webkit-optimize-contrast; image-rendering: optimizeQuality; } 17 | header .search { float: right; line-height: 0; font-size: 18px; height: 50px; padding: 4px 0 0 0; width: 40px; margin-right: 5px; } 18 | header .search > div { width: 38px; height: 38px; cursor: pointer; border: 1px solid transparent; line-height: 38px; text-align: center; border-radius: 100px; } 19 | header .search > div:hover { background-color: #EBEBEB; border-color: #EBECF2; color: #000; } 20 | header .notifications { float: right; line-height: 0; font-size: 18px; height: 50px; padding: 4px 0 0 0; width: 40px; } 21 | header .notifications > ui-bind { border-radius: 100px; background-color: #E73323; color: #FFF; font-size: 10px; width: 16px; height: 16px; position: absolute; line-height: 14px; text-align: center; margin: 22px 0 0 20px; z-index: 1; } 22 | header .notifications > div { width: 38px; height: 38px; cursor: pointer; border: 1px solid transparent; line-height: 38px; text-align: center; border-radius: 100px; } 23 | header .notifications > div:hover { background-color: #EBEBEB; border-color: #EBECF2; color: #000; } 24 | header .apps { padding: 5px 120px 0 0; margin-left: 250px; margin-right: 100px; text-align: center; } 25 | header .apps ui-bind { position: relative; display: inline-block; margin-left: 10px; } 26 | header .apps ui-bind button { margin-left: 2px; } 27 | header .apps button { position: relative; display: inline-block; width: 35px; height: 35px; text-align: center; line-height: 32px; color: #A0A0A0; font-size: 18px; border: 0; background-color: transparent; } 28 | header .apps button span { position: absolute; font-size: 6px; color: #68B25B; margin: 32px 0 0 6px; } 29 | header .apps button:hover { color: #000; } 30 | header .apps button.selected { color: #000; } 31 | header .apps button.startmenubutton { color: var(--color); transition-duration: 0.3s; background-color: #FFF; border-radius: 8px; box-shadow: 1px 1px 3px rgba(0,0,0,0.1); text-align: center; padding: 0; margin: 0; } 32 | header .apps button.startmenubutton:after { content: ''; background: var(--color); display: block; position: absolute; padding-top: 100%; padding-left: 100%; margin-left: 0; margin-top: -95%; opacity: 0; transition: all 0.3s; border-radius: 8px; } 33 | header .apps button.startmenubutton:active:after { padding: 0; margin: 0; opacity: 1; transition: 0s; } 34 | 35 | .jc-xs header .search { display: none; } 36 | 37 | .thirdparty { position: absolute; left: 15px; right: 15px; top: 15px; bottom: 50px; } 38 | .ui-content { background-color: #FFF; border: 1px solid #E7E7E7; border-radius: 5px; overflow: hidden; } 39 | .ui-preview img { border-radius: 100px; image-rendering: -webkit-optimize-contrast; image-rendering: optimizeQuality; } 40 | .img-responsive { image-rendering: -webkit-optimize-contrast; image-rendering: optimizeQuality;} 41 | .ml5 { margin-left: 5px; } 42 | .block { display: block; } 43 | 44 | .menu { padding: 10px 0; } 45 | .menu > a, .menu > button { display: block; width: 38px; line-height: 38px; height: 38px; text-align: center; font-size: 18px; border-radius: var(--radius); color: #999; margin: 0 auto; margin-bottom: 5px; border: 1px solid transparent; } 46 | .menu hr { margin: 10px 0; border-color: #EBEBEB; } 47 | .menu > a:hover, .menu > button:hover, .menu > a.selected, .menu > button.selected { background-color: #EBEBEB; color: #000; border-color: #EBECF2; } 48 | .menu > a .running { font-size: 6px; color: #68B25B; position: absolute; margin: 15px 0 0 24px; } 49 | .menu.bottom { border-top: 1px solid #EBEBEB; } 50 | main { margin-left: 80px; } 51 | 52 | .masked { opacity: 0; pointer-events: none; z-index: -1; } 53 | .ui-apps { position: relative; border-radius: 5px; } 54 | 55 | .ui-iframe-item { border-radius: 5px; overflow: hidden; } 56 | .ui-iframe-item iframe { width: 100%; height: 100%; border: 0; margin: 0; padding: 0; border-radius: 0 0 5px 5px; display: block; } 57 | .ui-iframe-focus { z-index: 1; } 58 | .ui-iframe-header { height: 24px; border-bottom: 1px solid #EBEBEB; background-color: #F8F8F8; padding-right: 5px; border-radius: 5px 5px 0 0; } 59 | .ui-iframe-header span { float: right; width: 25px; height: 24px; text-align: center; line-height: 23px; cursor: pointer; } 60 | .ui-iframe-header span:hover { opacity: 0.8; } 61 | .ui-iframe-header .close { color: #E73323; font-weight: bold; font-size: 13px !important; } 62 | .ui-iframe-header .refresh { color: #A0A0A0; font-size: 11px; } 63 | .ui-iframe-header .detach { color: #A0A0A0; } 64 | .ui-iframe-header .minimalize { color: #A0A0A0; } 65 | .jc-xs .ui-iframe-header .minimalize, .jc-xs .ui-iframe-header .detach { display: none; } 66 | .ui-iframe-header .feedback { color: #A0A0A0; font-size: 12px; } 67 | .ui-iframe-header div i { margin-right: 5px; } 68 | .ui-iframe-header div { margin: 0 30px 0 0; padding-left: 8px; font-size: 11px; color: #A0A0A0; line-height: 23px; border-radius: 5px 5px 0 0; } 69 | .ui-iframe-loading { background-color: rgba(255,255,255, .9); width: 100%; height: 100%; position: absolute; top: 0; left: 0; display: flex; justify-content: center; align-items: center; } 70 | .ui-iframe-loading i { font-size: 50px; color: var(--color); } 71 | .ui-windows-item .ui-iframe-header { display: none; } 72 | .ui-windows-item .ui-iframe-item { border-radius: 0; } 73 | 74 | .ui-time { position: absolute; left: 30px; top: 30px; font-size: 12px; color: #999; } 75 | .ui-time i { margin: 0 6px 0 0; } 76 | .ui-time span { vertical-align: middle; } 77 | 78 | h2 { margin: 0 0 20px; font: bold 24px Arial; } 79 | .link { color: var(--color); } 80 | .color { color: var(--color) !important; } 81 | .feedback { color: #8A2094 !important; } 82 | 83 | .empty { margin: 30px; } 84 | .empty > i { float: left; width: 36px; height: 36px; line-height: 36px; font-size: 18px; text-align: center; background-color: #F0F0F0; border-radius: var(--radius); margin-right: 10px; } 85 | .empty > div { font-size: 12px; font-weight: bold; padding-top: 2px; } 86 | .empty > summary { font-size: 11px; color: #999; border: 0; background: transparent; } 87 | 88 | .ui-parts { position: absolute; z-index: 1; } 89 | .ui-parts-item { display: none; } 90 | .ui-parts-focused { display: block; } 91 | 92 | .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%; } 93 | .button i { width: 15px; text-align: center; margin-right: 5px; } 94 | .button:hover { opacity: 0.8; } 95 | .button[name='submit'] { font-weight: bold; background-color: var(--color); color: #FFF; } 96 | .button:disabled { background-color: #F5F5F5 !important; border-color: #E0E0E0 !important; color: #A0A0A0 !important; cursor: not-allowed; box-shadow: none; } 97 | .button:disabled i { color: #A0A0A0 !important; } 98 | .button:first-child { border-top-left-radius: var(--radius); border-bottom-left-radius: var(--radius); } 99 | .button:last-child { border-top-right-radius: var(--radius); border-bottom-right-radius: var(--radius); } 100 | .button.small { height: 24px; padding: 0 8px; line-height: 14px; font-size: 12px; } 101 | 102 | .ui-windows-body { overflow: hidden; } 103 | .ui-windows-item { box-shadow: 0 10px 20px rgb(0 0 0 / 15%); } 104 | .ui-windows-title span { font-size: 12px; font-weight: normal; color: #777; } 105 | 106 | @media (max-width: 600px) { 107 | .jc-ios header { bottom: 5px; } 108 | header .openplatform img { display: none; } 109 | header .openplatform { max-width: 150px; } 110 | header .apps { position: absolute; left: 0; margin: 0; padding: 0; left: 50%; margin-left: -30px; top: 5px; } 111 | header .apps > ui-bind { display: none; } 112 | header button.startmenubutton { width: 60px; } 113 | header .apps button.startmenubutton:after { padding-top: 60%; padding-left: 100%; margin-top: -58%; } 114 | .ui-time { display: none; } 115 | } -------------------------------------------------------------------------------- /plugins/portal/public/forms/account.html: -------------------------------------------------------------------------------- 1 | 41 | 42 | -------------------------------------------------------------------------------- /plugins/portal/public/forms/feedback.html: -------------------------------------------------------------------------------- 1 | 34 | 35 | -------------------------------------------------------------------------------- /plugins/portal/public/forms/intro.html: -------------------------------------------------------------------------------- 1 | 23 | 24 | -------------------------------------------------------------------------------- /plugins/portal/public/forms/notifications.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 43 | 44 | -------------------------------------------------------------------------------- /plugins/portal/public/forms/password.html: -------------------------------------------------------------------------------- 1 | 39 | 40 | -------------------------------------------------------------------------------- /plugins/portal/public/forms/sessions.html: -------------------------------------------------------------------------------- 1 | 38 | 39 | -------------------------------------------------------------------------------- /plugins/portal/public/forms/welcome.html: -------------------------------------------------------------------------------- 1 | 20 | 21 | -------------------------------------------------------------------------------- /plugins/portal/public/pages/welcome.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 49 | 50 | -------------------------------------------------------------------------------- /plugins/portal/public/password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totaljs/openplatform/cf1029c45384e6a2df86b2131342598c9a7cfee5/plugins/portal/public/password.png -------------------------------------------------------------------------------- /plugins/portal/public/sounds/alert.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totaljs/openplatform/cf1029c45384e6a2df86b2131342598c9a7cfee5/plugins/portal/public/sounds/alert.mp3 -------------------------------------------------------------------------------- /plugins/portal/public/sounds/badges.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totaljs/openplatform/cf1029c45384e6a2df86b2131342598c9a7cfee5/plugins/portal/public/sounds/badges.mp3 -------------------------------------------------------------------------------- /plugins/portal/public/sounds/beep.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totaljs/openplatform/cf1029c45384e6a2df86b2131342598c9a7cfee5/plugins/portal/public/sounds/beep.mp3 -------------------------------------------------------------------------------- /plugins/portal/public/sounds/confirm.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totaljs/openplatform/cf1029c45384e6a2df86b2131342598c9a7cfee5/plugins/portal/public/sounds/confirm.mp3 -------------------------------------------------------------------------------- /plugins/portal/public/sounds/done.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totaljs/openplatform/cf1029c45384e6a2df86b2131342598c9a7cfee5/plugins/portal/public/sounds/done.mp3 -------------------------------------------------------------------------------- /plugins/portal/public/sounds/fail.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totaljs/openplatform/cf1029c45384e6a2df86b2131342598c9a7cfee5/plugins/portal/public/sounds/fail.mp3 -------------------------------------------------------------------------------- /plugins/portal/public/sounds/message.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totaljs/openplatform/cf1029c45384e6a2df86b2131342598c9a7cfee5/plugins/portal/public/sounds/message.mp3 -------------------------------------------------------------------------------- /plugins/portal/public/sounds/notifications.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totaljs/openplatform/cf1029c45384e6a2df86b2131342598c9a7cfee5/plugins/portal/public/sounds/notifications.mp3 -------------------------------------------------------------------------------- /plugins/portal/public/sounds/success.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totaljs/openplatform/cf1029c45384e6a2df86b2131342598c9a7cfee5/plugins/portal/public/sounds/success.mp3 -------------------------------------------------------------------------------- /plugins/setup/index.html: -------------------------------------------------------------------------------- 1 | @{layout('')} 2 | 3 | 4 | 5 | 6 | @{CONF.name} - @(Setup) 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | @{import('meta', 'head', '/_setup/default.css', 'favicon.ico')} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 99 | 100 |
101 | 102 | 103 | @{json(model, 'pluginsdata')} 104 | 105 | 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /plugins/setup/index.js: -------------------------------------------------------------------------------- 1 | exports.install = function() { 2 | 3 | ROUTE('+GET /setup/', setup); 4 | 5 | // Users 6 | ROUTE('+API /setup/ -users --> Users/list'); 7 | ROUTE('+API /setup/ -users_read/{id} --> Users/read'); 8 | ROUTE('+API /setup/ +users_create --> Users/create'); 9 | ROUTE('+API /setup/ +users_update/{id} --> Users/update'); 10 | ROUTE('+API /setup/ -users_remove/{id} --> Users/remove'); 11 | ROUTE('+API /setup/ -users_assign/{id} --> Users/assign'); 12 | 13 | // Groups 14 | ROUTE('+API /setup/ -groups --> Groups/list'); 15 | ROUTE('+API /setup/ -groups_read/{id} --> Groups/read'); 16 | ROUTE('+API /setup/ +groups_create --> Groups/create'); 17 | ROUTE('+API /setup/ +groups_update/{id} --> Groups/update'); 18 | ROUTE('+API /setup/ -groups_remove/{id} --> Groups/remove'); 19 | ROUTE('+API /setup/ -groups_apps --> Groups/apps'); 20 | 21 | // Apps 22 | ROUTE('+API /setup/ -apps --> Apps/list'); 23 | ROUTE('+API /setup/ -apps_read/{id} --> Apps/read'); 24 | ROUTE('+API /setup/ +apps_create --> Apps/create'); 25 | ROUTE('+API /setup/ +apps_update/{id} --> Apps/update'); 26 | ROUTE('+API /setup/ -apps_remove/{id} --> Apps/remove'); 27 | ROUTE('+API /setup/ -apps_download --> Apps/download'); 28 | 29 | // Settings 30 | ROUTE('+API /setup/ -settings --> Settings/read'); 31 | ROUTE('+API /setup/ +settings_save --> Settings/save'); 32 | ROUTE('+API /setup/ +settings_test --> Settings/test'); 33 | ROUTE('+API /setup/ -resources --> Settings/resources'); 34 | 35 | // Feedback 36 | ROUTE('+API /setup/ -feedback --> Feedback/list'); 37 | ROUTE('+API /setup/ -feedback_read/{id} --> Feedback/read'); 38 | ROUTE('+API /setup/ +feedback_update/{id} --> Feedback/update'); 39 | ROUTE('+API /setup/ -feedback_remove/{id} --> Feedback/remove'); 40 | 41 | ROUTE('+API /setup/ -dashboard --> Dashboard/stats'); 42 | }; 43 | 44 | ON('ready', function() { 45 | COMPONENTATOR('uisetup', 'exec,intranetcss,navlayout,importer,page,box,input,datagrid,loading,approve,notify,errorhandler,aselected,localize,locale,validate,directory,icons,colorpicker,edit,viewbox,preview,choose,selection,colorselector,menu,clipboard,miniform,message,datepicker,uibuilder,prompt,uistudio', true); 46 | }); 47 | 48 | function setup($) { 49 | 50 | var plugins = []; 51 | 52 | for (var key in PLUGINS) { 53 | var item = PLUGINS[key]; 54 | if (item.type == 'setup' && ($.user.sa || !item.visible || item.visible($.user))) { 55 | var obj = {}; 56 | obj.id = item.id; 57 | obj.url = '/{0}/'.format(item.id); 58 | obj.sortindex = item.position; 59 | obj.name = TRANSLATE($.user.language || '', item.name); 60 | obj.icon = item.icon; 61 | obj.color = item.color; 62 | obj.type = item.type; 63 | obj.import = item.import; 64 | obj.hidden = item.hidden; 65 | plugins.push(obj); 66 | } 67 | } 68 | 69 | plugins.quicksort('position'); 70 | $.view('#setup/index', plugins); 71 | } -------------------------------------------------------------------------------- /plugins/setup/public/default.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --radius: 5px; 3 | } 4 | 5 | html,body { overflow: hidden; height: 100%; margin: 0; padding: 0; } 6 | 7 | header { height: 50px; border-bottom: 1px solid #E0E0E0; } 8 | header label { float: left; line-height: 50px; font-size: 15px; color: #000; font-weight: bold; margin: 0 15px; } 9 | header label i { margin-right: 8px; } 10 | header .toolbar { float: right; margin: 12px 12px 0 0; } 11 | 12 | .nav { background: transparent; margin: 0; } 13 | .nav hr { margin: 8px 5px; border-color: #F0F0F0; } 14 | .nav nav > div, .nav nav > a { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } 15 | .color { color: var(--color); } 16 | 17 | .selection { width: 16px; height: 16px; border: 1px solid #D0D0D0; border-radius: var(--radius); font-size: 8px; float: left; text-align: center; margin-right: 8px; background-color: #FFF; line-height: 14px; color: #000; } 18 | .selection i { display: none; line-height: 14px; } 19 | .selection.selected i { display: block; } 20 | 21 | .ui-preview img { border-radius: 8px; } 22 | 23 | .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%; } 24 | .button i { width: 15px; text-align: center; margin-right: 5px; } 25 | .button:hover { opacity: 0.8; } 26 | .button[name='submit'] { font-weight: bold; background-color: var(--color); color: #FFF; } 27 | .button:disabled { background-color: #F5F5F5 !important; border-color: #E0E0E0 !important; color: #A0A0A0 !important; cursor: not-allowed; box-shadow: none; } 28 | .button:disabled i { color: #A0A0A0 !important; } 29 | .button:first-child { border-top-left-radius: var(--radius); border-bottom-left-radius: var(--radius); } 30 | .button:last-child { border-top-right-radius: var(--radius); border-bottom-right-radius: var(--radius); } 31 | .button.small { height: 24px; padding: 0 8px; line-height: 14px; font-size: 12px; } 32 | 33 | .dg-value .icon { width: 14px; text-align: center; margin-right: 7px; } 34 | .highlight { background-color: rgba(0,0,0,0.03); } 35 | .left { text-align: left; } 36 | .nmr { margin-right: 0; } 37 | 38 | .ui-navlayout > section { border-right: 1px solid #E0E0E0; background-color: #FFF; } 39 | .jc-xs .ui-navlayout > section, .jc-sm .ui-navlayout > section { border-right: 0; z-index: 10 !important; } 40 | 41 | .mobilemenu { width: 48px; height: 48px; line-height: 46px; font-size: 18px; border-radius: 100%; background-color: #FFF; box-shadow: 0 3px 20px rgba(0,0,0,0.15); text-align: center; position: absolute; left: 50%; margin-left: -24px; bottom: 20px; display: none; z-index: 11; } 42 | .jc-xs .mobilemenu, .jc-sm .mobilemenu { display: block; } 43 | -------------------------------------------------------------------------------- /plugins/setup/public/forms/assign.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /plugins/setup/public/forms/feedback.html: -------------------------------------------------------------------------------- 1 | 68 | 69 | -------------------------------------------------------------------------------- /plugins/setup/public/forms/group.html: -------------------------------------------------------------------------------- 1 | 20 | 21 | 87 | 88 | 126 | -------------------------------------------------------------------------------- /plugins/setup/public/forms/import.html: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /plugins/setup/public/forms/user.html: -------------------------------------------------------------------------------- 1 | 71 | 72 | -------------------------------------------------------------------------------- /plugins/setup/public/import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totaljs/openplatform/cf1029c45384e6a2df86b2131342598c9a7cfee5/plugins/setup/public/import.png -------------------------------------------------------------------------------- /plugins/setup/public/pages/apps.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 | 7 | 8 | 9 |
10 |
11 | 12 |
13 | 14 | 31 | 32 |
33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /plugins/setup/public/pages/dashboard.html: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 |
25 | 26 |
27 | 28 | 113 | 114 |
115 | 116 | 134 | -------------------------------------------------------------------------------- /plugins/setup/public/pages/feedback.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 | 7 |
8 |
9 | 10 |
11 | 12 | 29 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /plugins/setup/public/pages/groups.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 | 7 | 8 |
9 |
10 | 11 |
12 | 13 | 24 | 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /plugins/setup/public/pages/settings.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 7 | 8 |
9 |
10 |
11 |
12 |
13 | 14 |
15 |
@(Icon 150x150)
16 |
17 |
18 | 19 |
20 |
21 | @(Name) 22 |
23 |
24 |
25 | @(Language) 26 |
27 |
28 | @(Color) 29 |
30 |
31 |
32 |
33 | @(URL address) 34 |
35 |
36 | @(Open external and bookmarks in new tab/window) 37 | @(Enable WebSocket data synchronization for 3rd party apps) 38 |
39 | 40 | 47 | 48 |
49 | 50 |
51 |

@(This configuration allows external apps to read all groups and user profiles through the REST endpoint. The token below must be set in the X-Token header or via "?token=" query parameter.)

52 | 53 |
54 | @(Enable API) 55 |
56 | 57 | 64 | 65 |
66 | 67 |
68 |

@(The Total.js Message Service allows you to capture data from the OpenPlatform using the Total.js Flow Visual Programming Interface.)

69 | 70 |
71 | @(Enable TMS) 72 |
73 | 74 | 81 | 82 |
83 | 84 |
85 |
86 | 87 |
88 | 89 |
90 | 91 |
92 |
93 | @(Sender address) 94 |
95 |
96 | @(SMTP server) 97 |
98 |
99 | 100 |
101 | @(SMTP options) 102 |
@(SMTP options have to be in valid JSON format, according to the Total.js documentation). @(Documentation)
103 |
104 | 105 | 106 | 118 | 119 | 120 | 121 | 122 | 123 |
124 |
125 |
126 |
127 |
128 | 129 |
130 | 131 |
132 | 133 | -------------------------------------------------------------------------------- /plugins/setup/public/pages/users.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 | 7 | 8 | 9 |
10 |
11 | 12 |
13 | 14 | 34 | 35 |
36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /plugins/setup/schemas/apps.js: -------------------------------------------------------------------------------- 1 | NEWSCHEMA('@Apps/Permissions', 'id,*name,*value,sortindex:Number'); 2 | NEWSCHEMA('@Apps', '*name,color:Color,icon:Icon,meta:URL,*url:URL,*reqtoken,*restoken,sortindex:Number,notifications:Boolean,isbookmark:Boolean,isnewtab:Boolean,isscrollbar:Boolean,isdisabled:Boolean,isexternal:Boolean,permissions:[@Apps/Permissions]'); 3 | 4 | NEWACTION('Apps/list', { 5 | name: 'List of apps', 6 | action: function($) { 7 | DATA.find('op.tbl_app').autoquery($.query, 'id:UID,name,url,color,icon,notifications:Boolean,isexternal:Boolean,isbookmark:Boolean,isnewtab:Boolean,isdisabled:Boolean,url:URL,logged:Number,dtlogged:Date,dtcreated:Date,dtupdated:Date', 'name', 1000).where('isremoved=FALSE').callback($); 8 | } 9 | }); 10 | 11 | NEWACTION('Apps/read', { 12 | name: 'Read app', 13 | params: '*id:UID', 14 | action: async function($) { 15 | var params = $.params; 16 | var model = await DATA.read('op.tbl_app').id(params.id).where('isremoved=FALSE').error('@(App not found)').promise($); 17 | model.cache = undefined; 18 | model.permissions = await DATA.find('op.tbl_app_permission').fields('id,name,value,sortindex').where('appid', model.id).promise($); 19 | $.callback(model); 20 | } 21 | }); 22 | 23 | NEWACTION('Apps/create', { 24 | name: 'Create app', 25 | input: '@Apps', 26 | action: async function($, model) { 27 | 28 | var permissions = model.permissions || []; 29 | 30 | model.permissions = undefined; 31 | model.id = UID(); 32 | model.dtcreated = NOW; 33 | 34 | if (!model.sortindex) 35 | model.sortindex = (await DATA.count('op.tbl_app').promise()) + 1; 36 | 37 | await DATA.insert('op.tbl_app', model).promise($); 38 | 39 | for (let m of permissions) { 40 | if (!m.id || m.id[0] === '_') 41 | m.id = UID(); 42 | m.appid = model.id; 43 | await DATA.insert('op.tbl_app_permission', m).promise($); 44 | } 45 | 46 | $.success(model.id); 47 | } 48 | }); 49 | 50 | NEWACTION('Apps/update', { 51 | name: 'Update app', 52 | params: '*id:UID', 53 | input: '@Apps', 54 | action: async function($, model) { 55 | 56 | var params = $.params; 57 | var newpermissions = model.permissions || []; 58 | 59 | if (model.isbookmark) 60 | newpermissions = EMPTYARRAY; 61 | 62 | model.permissions = undefined; 63 | model.dtupdated = NOW; 64 | model.isprocessed = false; 65 | 66 | await DATA.modify('op.tbl_app', model).id(params.id).error('@(App not found)').where('isremoved=FALSE').promise($); 67 | 68 | var oldpermissions = await DATA.find('op.tbl_app_permission').where('appid', params.id).promise($); 69 | var diff = DIFFARR('id', oldpermissions, newpermissions); 70 | 71 | for (let m of diff.add) { 72 | m.id = UID(); 73 | m.appid = params.id; 74 | await DATA.insert('op.tbl_app_permission', m).promise($); 75 | } 76 | 77 | for (let m of diff.upd) { 78 | m.form.id = undefined; 79 | await DATA.modify('op.tbl_app_permission', m.form).id(m.db.id).where('appid', params.id).promise($); 80 | } 81 | 82 | for (let m of diff.rem) 83 | await DATA.remove('op.tbl_app_permission').id(m).where('appid', params.id).promise($); 84 | 85 | FUNC.clearcache('A' + params.id); 86 | $.success(params.id); 87 | } 88 | }); 89 | 90 | NEWACTION('Apps/remove', { 91 | name: 'Remove app', 92 | params: '*id:UID', 93 | action: async function($) { 94 | var params = $.params; 95 | await DATA.modify('op.tbl_app', { isprocessed: false, isremoved: true, dtremoved: NOW }).id(params.id).error('@(App not found)').where('isremoved=FALSE').promise($); 96 | FUNC.clearcache('A' + params.id); 97 | $.success(params.id); 98 | } 99 | }); 100 | 101 | NEWACTION('Apps/download', { 102 | name: 'Download meta', 103 | query: '*url:URL', 104 | action: function($) { 105 | RESTBuilder.GET($.query.url).callback($); 106 | } 107 | }); -------------------------------------------------------------------------------- /plugins/setup/schemas/dashboard.js: -------------------------------------------------------------------------------- 1 | NEWACTION('Dashboard/stats', { 2 | name: 'Stats', 3 | action: async function($) { 4 | 5 | var data = {}; 6 | var stats; 7 | 8 | data.sessions = await DATA.count('op.tbl_session').where('dtexpire>NOW()').promise($); 9 | data.total = {}; 10 | 11 | stats = (await DATA.query('SELECT MAX(maxlogged)::int4 AS maxlogged, SUM(desktop)::int4 AS desktop, SUM(mobile)::int4 AS mobile FROM op.tbl_visitor').promise($))[0] || EMPTYARRAY; 12 | 13 | data.total.maxlogged = stats.maxlogged || 0; 14 | data.total.desktop = stats.desktop || 0; 15 | data.total.mobile = stats.mobile || 0; 16 | 17 | stats = (await DATA.query('SELECT maxlogged, desktop, mobile FROM op.tbl_visitor WHERE id=' + NOW.format('yyyyMMdd')).promise($))[0] || EMPTYARRAY; 18 | 19 | data.today = {}; 20 | data.today.maxlogged = stats.maxlogged || 0; 21 | data.today.desktop = stats.desktop || 0; 22 | data.today.mobile = stats.mobile || 0; 23 | 24 | data.feedback = await DATA.count('op.tbl_feedback').where('iscomplete=FALSE').promise($); 25 | data.online = await DATA.count('op.tbl_session').where('isonline=TRUE').promise($); 26 | data.users = await DATA.count('op.tbl_user').where('isremoved=FALSE').promise($); 27 | 28 | var consumption = F.consumption; 29 | 30 | data.memory = consumption.memory; 31 | data.date = consumption.date; 32 | data.clients = MAIN.ws ? MAIN.ws.online : 0; 33 | 34 | $.callback(data); 35 | } 36 | }); -------------------------------------------------------------------------------- /plugins/setup/schemas/feedback.js: -------------------------------------------------------------------------------- 1 | NEWACTION('Feedback/list', { 2 | name: 'Feedback list', 3 | action: function($) { 4 | DATA.list('op.tbl_feedback').autoquery($.query, 'id:UID,account,email,ua,ip,app,updatedby,iscomplete:Boolean,rating:Number,dtcreated:Date,dtupdated:Date', 'dtcreated_desc', 100).callback($); 5 | } 6 | }); 7 | 8 | NEWACTION('Feedback/read', { 9 | name: 'Read feedback', 10 | params: '*id:UID', 11 | action: async function($) { 12 | var params = $.params; 13 | DATA.read('op.tbl_feedback').id(params.id).error('@(Feedback not found)').callback($); 14 | } 15 | }); 16 | 17 | NEWACTION('Feedback/update', { 18 | name: 'Update feedback', 19 | params: '*id:UID', 20 | input: 'iscomplete:Boolean', 21 | action: async function($, model) { 22 | var params = $.params; 23 | model.updatedby = $.user.name; 24 | model.dtupdated = NOW; 25 | DATA.modify('op.tbl_feedback', model).id(params.id).error('@(Feedback not found)').callback($.done()); 26 | } 27 | }); 28 | 29 | NEWACTION('Feedback/remove', { 30 | name: 'Remove feedback', 31 | params: '*id:UID', 32 | action: async function($) { 33 | var params = $.params; 34 | DATA.remove('op.tbl_feedback').id(params.id).error('@(Feedback not found)').callback($.done()); 35 | } 36 | }); -------------------------------------------------------------------------------- /plugins/setup/schemas/groups.js: -------------------------------------------------------------------------------- 1 | NEWSCHEMA('@Group', '*name,color:Color,icon:Icon,reference,permissions:[String]'); 2 | 3 | NEWACTION('Groups/list', { 4 | name: 'List of groups', 5 | action: function($) { 6 | DATA.find('op.view_group').autoquery($.query, 'id:UID,name,color,icon,users:Number,dtcreated:Date,dtupdated:Date', 'dtcreated_desc', 100).callback($); 7 | } 8 | }); 9 | 10 | NEWACTION('Groups/read', { 11 | name: 'Read group', 12 | params: '*id:UID', 13 | action: async function($) { 14 | 15 | var params = $.params; 16 | var model = await DATA.read('op.tbl_group').id(params.id).error('@(Group not found)').promise($); 17 | var permissions = await DATA.find('op.tbl_group_permission').fields('permissionid,appid').where('groupid', model.id).promise($); 18 | model.permissions = []; 19 | 20 | for (let m of permissions) 21 | model.permissions.push(m.permissionid || ('_' + m.appid)); 22 | 23 | $.callback(model); 24 | } 25 | }); 26 | 27 | NEWACTION('Groups/create', { 28 | name: 'Create group', 29 | input: '@Group', 30 | action: async function($, model) { 31 | 32 | var permissions = model.permissions || EMPTYARRAY; 33 | model.permissions = undefined; 34 | model.id = UID(); 35 | model.dtcreated = NOW; 36 | 37 | await DATA.insert('op.tbl_group', model).promise($); 38 | 39 | var apps = []; 40 | 41 | for (let m of permissions) { 42 | if (m[0] === '_') 43 | apps.push(m.substring(1)); 44 | } 45 | 46 | for (let m of apps) 47 | permissions.splice(permissions.indexOf('_' + m), 1); 48 | 49 | var apermissions = await DATA.find('op.tbl_app_permission').fields('id,appid').in('id', permissions).promise($); 50 | 51 | for (let m of apermissions) { 52 | if (apps.includes(m.appid)) { 53 | m.permissionid = m.id; 54 | m.id = model.id + m.id; 55 | m.groupid = model.id; 56 | await DATA.insert('op.tbl_group_permission', m).promise($); 57 | } 58 | } 59 | 60 | // Permission for opening app 61 | for (let m of apps) { 62 | let tmp ={}; 63 | tmp.id = UID(); 64 | tmp.appid = m; 65 | tmp.groupid = model.id; 66 | await DATA.insert('op.tbl_group_permission', tmp).promise($); 67 | } 68 | 69 | $.success(model.id); 70 | } 71 | }); 72 | 73 | NEWACTION('Groups/update', { 74 | name: 'Update group', 75 | params: '*id:UID', 76 | input: '@Group', 77 | action: async function($, model) { 78 | 79 | var params = $.params; 80 | var permissions = model.permissions || EMPTYARRAY; 81 | 82 | model.permissions = undefined; 83 | model.dtupdated = NOW; 84 | model.isprocessed = false; 85 | 86 | var apps = []; 87 | 88 | for (let m of permissions) { 89 | if (m[0] === '_') 90 | apps.push(m.substring(1)); 91 | } 92 | 93 | for (let m of apps) 94 | permissions.splice(permissions.indexOf('_' + m), 1); 95 | 96 | await DATA.modify('op.tbl_group', model).id(params.id).error('@(Group not found)').promise($); 97 | await DATA.remove('op.tbl_group_permission').where('groupid', params.id).promise($); 98 | 99 | var apermissions = await DATA.find('op.tbl_app_permission').fields('id,appid').in('id', permissions).promise($); 100 | var aapps = await DATA.find('op.tbl_app').fields('id').in('id', apps).promise($); 101 | 102 | apps = []; 103 | for (var m of aapps) 104 | apps.push(m.id); 105 | 106 | for (let m of apermissions) { 107 | if (apps.includes(m.appid)) { 108 | m.permissionid = m.id; 109 | m.id = params.id + m.id; 110 | m.groupid = params.id; 111 | await DATA.insert('op.tbl_group_permission', m).promise($); 112 | } 113 | } 114 | 115 | // Permission for opening app 116 | for (let m of apps) { 117 | let tmp = {}; 118 | tmp.id = UID(); 119 | tmp.appid = m; 120 | tmp.groupid = params.id; 121 | await DATA.insert('op.tbl_group_permission', tmp).promise($); 122 | } 123 | 124 | FUNC.clearcache('G' + params.id); 125 | $.success(params.id); 126 | } 127 | }); 128 | 129 | NEWACTION('Groups/remove', { 130 | name: 'Remove group', 131 | params: '*id:UID', 132 | action: async function($) { 133 | var params = $.params; 134 | await DATA.remove('op.tbl_group').id(params.id).error('@(Group not found)').promise($); 135 | FUNC.clearcache('G' + params.id); 136 | $.success(params.id); 137 | } 138 | }); 139 | 140 | NEWACTION('Groups/apps', { 141 | name: 'Read all apps with permissions', 142 | action: async function($) { 143 | 144 | var items = await DATA.find('op.tbl_app').fields('id,name,icon,color').where('isremoved=FALSE').sort('name').promise($); 145 | var permissions = await DATA.find('op.tbl_app_permission').fields('id,appid,name').promise($); 146 | 147 | for (var item of items) 148 | item.permissions = permissions.findAll('appid', item.id); 149 | 150 | $.callback(items); 151 | } 152 | }); 153 | -------------------------------------------------------------------------------- /plugins/setup/schemas/settings.js: -------------------------------------------------------------------------------- 1 | NEWACTION('Settings/read', { 2 | name: 'Read settings', 3 | action: async function($) { 4 | var items = await DATA.find('op.cl_config').fields('id,value,type').in('id', 'icon,name,url,mail_smtp,mail_smtp_options,language,mail_from,$tms,secret_tms,allow_token,token,newtab,sync,sync_token,color'.split(',')).promise($); 5 | var model = {}; 6 | 7 | for (var m of items) { 8 | var value = m.value; 9 | switch (m.type) { 10 | case 'boolean': 11 | value = value == 'true'; 12 | break; 13 | case 'number': 14 | value = value.parseFloat(); 15 | break; 16 | case 'date': 17 | value = value.parseDate(); 18 | break; 19 | } 20 | model[m.id] = value; 21 | } 22 | 23 | $.callback(model); 24 | } 25 | }); 26 | 27 | NEWACTION('Settings/save', { 28 | name: 'Save settings', 29 | input: 'name:String, url:URL, color:Color, language:Lower, mail_smtp:String, mail_smtp_options:JSON, mail_from:Email, icon:String, $tms:Boolean, secret_tms:String, allow_token:Boolean, token:String, newtab:Boolean, sync:Boolean, sync_token:String', 30 | action: async function($, model) { 31 | 32 | for (var key in model) 33 | await DATA.modify('op.cl_config', { value: model[key] }).id(key).promise(); 34 | 35 | if (CONF.url !== model.url) { 36 | await DATA.query('UPDATE op.tbl_user_app SET notify=NULL, notifytoken=NULL WHERE notify IS NOT NULL OR notifytoken IS NOT NULL'); 37 | await DATA.query('DELETE FROM op.tbl_app_session'); 38 | } 39 | 40 | MAIN.reconfigure(); 41 | $.success(); 42 | } 43 | }); 44 | 45 | NEWACTION('Settings/test', { 46 | name: 'Test SMTP settings', 47 | input: '*mail_smtp, mail_smtp_options:JSON, mail_from:Email', 48 | action: async function($, model) { 49 | var options = (model.mail_smtp_options || '').parseJSON() || {}; 50 | options.server = model.mail_smtp; 51 | Mail.try(options, $.done(true)); 52 | } 53 | }); 54 | 55 | NEWACTION('Settings/resources', { 56 | name: 'Loads list of resources', 57 | action: function($) { 58 | $.callback(Object.keys(F.resources)); 59 | } 60 | }); -------------------------------------------------------------------------------- /plugins/setup/schemas/users.js: -------------------------------------------------------------------------------- 1 | NEWSCHEMA('@User', 'language:lower,gender:{male|female},photo,*name,*email:Email,password,darkmode:Number,sounds:Boolean,notifications:Boolean,sa:Boolean,isconfirmed:Boolean,isdisabled:Boolean,isinactive:Boolean,iswelcome:Boolean,ispassword:Boolean,groups:[UID]'); 2 | 3 | NEWACTION('Users/list', { 4 | name: 'List of users', 5 | action: function($) { 6 | var builder = DATA.list('op.view_user'); 7 | builder.autoquery($.query, 'id:UID,groups,photo,language,gender,name,sa:Boolean,isonline:Boolean,unread:Number,isdisabled:Boolean,isconfirmed:Boolean,isinactive:Boolean,email,logged:Number,dtlogged:Date,dtcreated:Date,dtupdated:Date', 'dtcreated_desc', 100); 8 | builder.callback($); 9 | } 10 | }); 11 | 12 | NEWACTION('Users/read', { 13 | name: 'Read user', 14 | params: '*id:UID', 15 | action: async function($) { 16 | 17 | var params = $.params; 18 | var model = await DATA.read('op.tbl_user').id(params.id).where('isremoved=FALSE').promise($); 19 | 20 | delete model.token; 21 | delete model.password; 22 | 23 | var groups = await DATA.find('op.tbl_user_group').fields('groupid').where('userid', model.id).promise(); 24 | model.groups = []; 25 | 26 | for (let m of groups) 27 | model.groups.push(m.groupid); 28 | 29 | $.callback(model); 30 | } 31 | }); 32 | 33 | NEWACTION('Users/create', { 34 | name: 'Create user', 35 | input: '@User', 36 | action: async function($, model) { 37 | 38 | if (model.ispassword && !model.password) { 39 | $.invalid('password'); 40 | return; 41 | } 42 | 43 | var groups = model.groups || EMPTYARRAY; 44 | var iswelcome = model.iswelcome; 45 | 46 | if (model.ispassword) 47 | model.password = model.password.sha256(CONF.salt); 48 | else 49 | delete model.password; 50 | 51 | model.ispassword = undefined; 52 | model.iswelcome = undefined; 53 | model.groups = undefined; 54 | 55 | if (!model.language) 56 | model.language = null; 57 | 58 | model.id = UID(); 59 | model.search = model.name.toSearch(); 60 | model.dtcreated = NOW = new Date(); 61 | model.token = FUNC.checksum(GUID(30)); 62 | 63 | if (!model.password) 64 | model.isreset = true; 65 | 66 | await DATA.insert('op.tbl_user', model).promise($); 67 | 68 | for (let m of groups) { 69 | let tmp = {}; 70 | tmp.id = model.id + m; 71 | tmp.userid = model.id; 72 | tmp.groupid = m; 73 | await DATA.insert('op.tbl_user_group', tmp).promise($); 74 | } 75 | 76 | if (iswelcome) { 77 | if (!model.color) 78 | model.color = CONF.color; 79 | 80 | CONF.ismail && MAIL(model.email, '@(Welcome)', 'mail/welcome', model, NOOP, model.language || CONF.language || ''); 81 | } 82 | 83 | $.success(model.id); 84 | } 85 | }); 86 | 87 | NEWACTION('Users/update', { 88 | name: 'Update user', 89 | params: '*id:UID', 90 | input: '@User', 91 | action: async function($, model) { 92 | 93 | var params = $.params; 94 | 95 | if (model.ispassword && !model.password) { 96 | $.invalid('password'); 97 | return; 98 | } 99 | 100 | await DATA.check('op.tbl_user').where('email', model.email).where('id', '<>', params.id).where('isremoved=FALSE').error('@(E-mail address is already used)', true).promise($); 101 | 102 | var groups = model.groups || EMPTYARRAY; 103 | var iswelcome = model.iswelcome == true; 104 | 105 | if (model.ispassword) 106 | model.password = model.password.sha256(CONF.salt); 107 | else 108 | delete model.password; 109 | 110 | model.ispassword = undefined; 111 | model.iswelcome = undefined; 112 | model.groups = undefined; 113 | 114 | model.search = model.name.toSearch(); 115 | model.dtupdated = NOW; 116 | 117 | if (iswelcome) 118 | model.token = FUNC.checksum(GUID(30)); 119 | 120 | if (!model.language) 121 | model.language = null; 122 | 123 | model.isprocessed = false; 124 | model.cache = null; 125 | model.cachefilter = null; 126 | 127 | await DATA.modify('op.tbl_user', model).id(params.id).error('@(User account not found)').where('isremoved=FALSE').promise($); 128 | await DATA.remove('op.tbl_user_group').where('userid', params.id).promise(); 129 | 130 | // Read all groups from DB due to JSON imports 131 | groups = await DATA.find('op.tbl_group').in('id', groups).promise($); 132 | 133 | for (let m of groups) { 134 | let tmp = {}; 135 | tmp.id = params.id + m.id; 136 | tmp.userid = params.id; 137 | tmp.groupid = m.id; 138 | await DATA.insert('op.tbl_user_group', tmp).promise($); 139 | } 140 | 141 | if (iswelcome) { 142 | if (!model.color) 143 | model.color = CONF.color; 144 | CONF.ismail && MAIL(model.email, '@(Welcome)', 'mail/welcome', model, NOOP, model.language || CONF.language || ''); 145 | } 146 | 147 | $.success(params.id); 148 | } 149 | }); 150 | 151 | NEWACTION('Users/remove', { 152 | name: 'Remove user', 153 | params: '*id:UID', 154 | action: async function($) { 155 | var params = $.params; 156 | await DATA.modify('op.tbl_user', { isprocessed: false, isremoved: true, dtremoved: NOW }).id(params.id).error('@(User account not found)').where('isremoved=FALSE').promise($); 157 | $.success(params.id); 158 | } 159 | }); 160 | 161 | NEWACTION('Users/assign', { 162 | name: 'Assign a group', 163 | params: '*id:UID', 164 | query: '*groupid:String', 165 | action: async function($) { 166 | 167 | var params = $.params; 168 | var remove = $.query.groupid[0] === '-'; 169 | var groupid = $.query.groupid.substring(1); 170 | var id = params.id + groupid; 171 | 172 | var is = await DATA.check('op.tbl_user_group').id(id).promise($); 173 | if (is) { 174 | if (remove) { 175 | await DATA.remove('op.tbl_user_group').id(id).promise($); 176 | FUNC.clearcache('G' + groupid); 177 | } 178 | } else { 179 | if (!remove) { 180 | await DATA.insert('op.tbl_user_group', { id: id, userid: params.id, groupid: groupid }).promise($); 181 | await DATA.modify('op.tbl_user', { cache: null, cachefilter: null }).id(params.id).promise($); 182 | } 183 | } 184 | 185 | $.success(); 186 | } 187 | }); 188 | 189 | NEWACTION('Users/logout', { 190 | name: 'Logout user', 191 | params: '*id:UID', 192 | action: async function($) { 193 | var params = $.params; 194 | await DATA.modify('op.tbl_user', { isonline: false }).id(params.id).error('@(User account not found)').where('isremoved=FALSE').promise($); 195 | await DATA.remove('op.tbl_session').where('userid', params.id).promise($); 196 | $.success(params.id); 197 | } 198 | }); 199 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totaljs/openplatform/cf1029c45384e6a2df86b2131342598c9a7cfee5/public/favicon.ico -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totaljs/openplatform/cf1029c45384e6a2df86b2131342598c9a7cfee5/public/icon.png -------------------------------------------------------------------------------- /public/iframe.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var W = window; 4 | var obj = {}; 5 | var focused; 6 | 7 | obj.iframe = W.top !== W; 8 | obj.callbacks = {}; 9 | obj.callbackindex = 0; 10 | obj.events = {}; 11 | obj.ready = false; 12 | obj.version = 1; 13 | obj.origin = location.href; 14 | 15 | (function() { 16 | var arr = location.search.substring(1).split('&'); 17 | for (var i = 0; i < arr.length; i++) { 18 | var name = arr[i]; 19 | if (name.substring(0, 13) === 'openplatform=') { 20 | var tmp = decodeURIComponent(name.substring(13)); 21 | obj.token = name.substring(13); 22 | obj.accesstoken = decodeURIComponent(tmp.substring(tmp.indexOf('token=') + 6)); 23 | break; 24 | } 25 | } 26 | })(); 27 | 28 | function sendfocus() { 29 | 30 | var dt = Date.now(); 31 | 32 | if (!focused || focused < dt) 33 | obj.focus(true); 34 | 35 | focused = dt + 1000; 36 | } 37 | 38 | obj.on = function(name, fn) { 39 | if (obj.events[name]) 40 | obj.events[name].push(fn); 41 | else 42 | obj.events[name] = [fn]; 43 | }; 44 | 45 | obj.play = function(name) { 46 | var msg = {}; 47 | msg.TYPE = 'play'; 48 | msg.data = name; 49 | obj.send(msg); 50 | }; 51 | 52 | obj.success = function(body) { 53 | var msg = {}; 54 | msg.TYPE = 'success'; 55 | msg.data = body; 56 | obj.send(msg); 57 | }; 58 | 59 | obj.warning = function(body) { 60 | var msg = {}; 61 | msg.TYPE = 'warning'; 62 | msg.data = body; 63 | obj.send(msg); 64 | }; 65 | 66 | obj.open = function(id, path) { 67 | var msg = {}; 68 | msg.TYPE = 'open'; 69 | msg.app = id; 70 | msg.path = path; 71 | obj.send(msg); 72 | }; 73 | 74 | obj.error = function(body) { 75 | var msg = {}; 76 | msg.TYPE = 'error'; 77 | msg.data = body; 78 | obj.send(msg); 79 | }; 80 | 81 | obj.focus = function(auto) { 82 | var msg = {}; 83 | msg.TYPE = 'focus'; 84 | msg.data = auto; 85 | obj.send(msg); 86 | if (!auto) 87 | W.focus(); 88 | }; 89 | 90 | obj.copy = function(text) { 91 | var msg = {}; 92 | msg.TYPE = 'clipboard'; 93 | msg.data = text; 94 | obj.send(msg); 95 | W.focus(); 96 | }; 97 | 98 | obj.path = function(path) { 99 | var msg = {}; 100 | msg.TYPE = 'path'; 101 | msg.data = path; 102 | obj.send(msg); 103 | W.focus(); 104 | }; 105 | 106 | obj.refresh = function() { 107 | var msg = {}; 108 | msg.TYPE = 'refresh'; 109 | obj.send(msg); 110 | }; 111 | 112 | obj.refresh_account = function() { 113 | var msg = {}; 114 | msg.TYPE = 'refresh_account'; 115 | obj.send(msg); 116 | }; 117 | 118 | obj.restart = function() { 119 | var msg = {}; 120 | msg.TYPE = 'restart'; 121 | obj.send(msg); 122 | }; 123 | 124 | obj.feedback = function() { 125 | var msg = {}; 126 | msg.TYPE = 'feedback'; 127 | obj.send(msg); 128 | }; 129 | 130 | obj.close = function() { 131 | var msg = {}; 132 | msg.TYPE = 'close'; 133 | obj.send(msg); 134 | }; 135 | 136 | obj.send = function(msg) { 137 | 138 | if (msg.callback) { 139 | var key = (obj.callbackindex++) + ''; 140 | obj.callbacks[key] = { fn: msg.callback, ts: Date.now() }; 141 | msg.callbackid = key; 142 | delete msg.callback; 143 | } 144 | 145 | msg.totalplatform = true; 146 | msg.origin = location.origin + location.pathname; 147 | W.parent.postMessage(JSON.stringify(msg), '*'); 148 | }; 149 | 150 | obj.emitforce = function(name, a, b, c, d) { 151 | var arr = obj.events[name]; 152 | if (arr) { 153 | for (var m of arr) 154 | m(a, b, c, d); 155 | } 156 | }; 157 | 158 | obj.emit = function(name, a, b, c, d) { 159 | setTimeout(obj.emitforce, 1, name, a, b, c, d); 160 | }; 161 | 162 | obj.tokenize = function(url) { 163 | var index = url.indexOf('?'); 164 | return index === -1 ? (url + ('?openplatform=' + obj.token)) : (url.substring(0, index + 1) + ('openplatform=' + obj.token + '&' + url.substring(index + 1))); 165 | }; 166 | 167 | function callbackexec(msg) { 168 | var key = msg.callbackid; 169 | var fn = obj.callbacks[key]; 170 | if (fn) { 171 | delete obj.callbacks[key]; 172 | fn.fn(msg.data, msg.error); 173 | } 174 | } 175 | 176 | if (obj.iframe) { 177 | W.addEventListener('message', function(e) { 178 | 179 | if (typeof(e.data) !== 'string') 180 | return; 181 | 182 | try { 183 | 184 | var msg = JSON.parse(e.data); 185 | if (msg.callbackid) 186 | setTimeout(callbackexec, 2, msg); 187 | 188 | if (msg.TYPE === 'init') { 189 | obj.ready = true; 190 | obj.emit('init'); 191 | return; 192 | } 193 | 194 | if (msg.TYPE === 'event') { 195 | var arr = obj.events[msg.name]; 196 | if (arr) { 197 | for (var i = 0; i < arr.length; i++) 198 | arr[i].apply(W, msg.args); 199 | } 200 | } 201 | 202 | if (msg.TYPE === 'focus' || msg.TYPE === 'path') { 203 | if (msg.data) { 204 | obj.href = msg.data; 205 | obj.emit('path', msg.data, true); 206 | } 207 | W.focus(); 208 | return; 209 | } 210 | 211 | if (msg.TYPE === 'mobilemenu') { 212 | obj.emit('mobilemenu'); 213 | return; 214 | } 215 | 216 | if (msg.TYPE === 'appearance') { 217 | APP.sounds = msg.data.sounds !== false; 218 | APP.notifications = msg.data.notifications !== false; 219 | APP.color = msg.data.color; 220 | obj.emit('appearance', msg.data); 221 | W.APPEARANCE && W.APPEARANCE({ color: msg.data.color }); 222 | return; 223 | } 224 | 225 | } catch (e) { 226 | console.error(e); 227 | // unhandled error 228 | } 229 | 230 | }, false); 231 | 232 | document.addEventListener('touchstart', sendfocus, { passive: true }); 233 | document.addEventListener('click', sendfocus); 234 | document.addEventListener('keydown', function(e) { 235 | var is = false; 236 | if (e.keyCode === 112) { 237 | // F1 238 | is = true; 239 | var msg = {}; 240 | msg.TYPE = 'quicksearch'; 241 | obj.send(msg); 242 | } else if (e.keyCode === 116) { 243 | // F5 244 | setTimeout(function() { 245 | location.reload(true); 246 | }, 200); 247 | is = true; 248 | } else if (e.keyCode === 9 && (e.altKey || e.ctrlKey || e.metaKey)) { 249 | // CTRL/ALT/CMD + TAB 250 | is = true; 251 | var msg = {}; 252 | msg.TYPE = 'nextwindow'; 253 | obj.send(msg); 254 | } 255 | 256 | if (is) { 257 | e.returnValue = false; 258 | e.keyCode = 0; 259 | return false; 260 | } 261 | 262 | }); 263 | } 264 | 265 | W.APP = obj; 266 | 267 | setTimeout(function() { 268 | var msg = {}; 269 | msg.TYPE = 'init'; 270 | obj.send(msg); 271 | W.APP_INIT && W.APP_INIT(); 272 | }, 2); 273 | 274 | })(); -------------------------------------------------------------------------------- /public/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totaljs/openplatform/cf1029c45384e6a2df86b2131342598c9a7cfee5/public/img/icon.png -------------------------------------------------------------------------------- /public/img/photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totaljs/openplatform/cf1029c45384e6a2df86b2131342598c9a7cfee5/public/img/photo.jpg -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # OpenPlatform v5 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 | OpenPlatform is a simple enterprise-ready platform for running, integrating and managing multiple web applications. 8 | 9 | ![OpenPlatform](https://docs.totaljs.com/download/xav3001kb41d-1si7hid-640x492-1.gif) 10 | 11 | ## Installation 12 | 13 | WARNING: Please do not execute `database.sql`, this script will use OpenPlatform internally. 14 | 15 | __Manual installation__: 16 | 17 | - Install latest version of [__Node.js platform__](https://nodejs.org/en/) 18 | - Install PostgreSQL 19 | - [Download __Source-Code__](https://github.com/totaljs/openplatform) 20 | - Create a database for the OpenPlatform 21 | - Install NPM dependencies via terminal `$ npm install` in the root of application 22 | - Update connection strings in `/config` file 23 | - Run it `$ node index.js` 24 | - Open `http://127.0.0.1:8000` in your web browser 25 | - __IMPORTANT__: Then open settings and configure the platform 26 | 27 | __Docker Hub__: 28 | 29 | ```bash 30 | docker pull totalplatform/openplatform 31 | docker run --env DATABASE='postgresql://user:pass@hostname/database' -p 8000:8000 totalplatform/openplatform 32 | ```` 33 | 34 | __Docker Compose__: 35 | 36 | ```bash 37 | git clone https://github.com/totaljs/openplatform.git 38 | cd openplatform 39 | docker compose up 40 | ```` 41 | 42 | ## Default credentials 43 | 44 | ```html 45 | login : info@totaljs.com 46 | password : admin 47 | ``` 48 | 49 | ## Good to know 50 | 51 | __Don't forget:__ every added third-party app must be assigned to a group. -------------------------------------------------------------------------------- /resources/en.resource: -------------------------------------------------------------------------------- 1 | Tagsor7 : Invalid data 2 | T14lakc0 : Invalid token 3 | T1gkjkgn : Unread notifications 4 | Tm0qe9a : E-mail address is already used 5 | T13oi6c6 : App not found 6 | Tlcnga : App has been temporary disabled 7 | T1bjrc82 : Account not found 8 | T1kod7hb : Account is disabled 9 | T1glqo0e : Account is inactive 10 | T178h6ff : Session not found 11 | T1dwzbwy : You must enter old password 12 | T39vorh : Invalid old password 13 | T1wxl291 : Feedback 14 | T17tfm1 : Login 15 | T14ranob : Enter e-mail address 16 | T1w7l82b : E-mail address 17 | Tdrftoj : Enter password 18 | Tl71ry3 : Password 19 | Tb4wo6k : Reset password 20 | Tykg00t : Don't have an account? 21 | T1aeqyco : SIGN IN 22 | Tf821nc : You will receive instructions for resetting your password by email. 23 | Tibgsfj : Sign-in form 24 | T1acy0f : RESET 25 | Tigv5rn : Enter name 26 | Tjsvyke : Account name 27 | Tt35tog : Enter your password 28 | T1cyekzk : Enter your password again 29 | T1sksnqp : Reply password 30 | T9sze77 : Passwords do not match. 31 | Trjmw2x : CREATE ACCOUNT 32 | Tqmy8m2 : You received an email with instructions for resetting your password. 33 | T3qscoh : Your account has been created successfully. A confirmation message was sent to your email address. 34 | T14w27d4 : Search 35 | T1c7py9 : Today 36 | Tkg5z0p : Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday 37 | T12tbn1 : Clear 38 | T1maqqy3 : January,February,March,April,May,Juny,July,August,September,October,November,December 39 | Ty97he0 : Notifications 40 | T5zhbb1 : Start menu 41 | T12tjh4 : Close 42 | Txspnlf : Detach 43 | Tqbr8s3 : Minimalize 44 | T19hc5a3 : Refresh 45 | Twnfthw : Report a problem 46 | T1bhvtp : Setup 47 | T16zabwn : Close {0} 48 | Ttmatki : Close all open applications 49 | T1vv3iit : feedback 50 | T12cvb0z : Open sessions 51 | Tizbdg9 : My account 52 | T1twvzjd : profile 53 | Tvggo92 : Reload account 54 | Thy8ul7 : refresh 55 | T11qckoa : Logout 56 | Treh6vn : Account has been reloaded 57 | T1297zig : Restart {0} 58 | Tzjt9hb : Close {0} 59 | T41pkh7 : Change password 60 | T1qonvrk : Open in new tab 61 | T19oinzj : Restart 62 | T2cba : now 63 | Tnhjmc9 : # minutes ago,# minute ago,# minutes ago,# minutes ago 64 | T3egjfd : # hours ago,# hour ago,# hours ago,# hours ago 65 | T9wgord : # days ago,# day ago,# days ago,# days ago 66 | T1mqtx3t : # months ago,# month ago,# months ago,# months ago 67 | To0b6h : # years ago,# year ago,# years ago,# years ago 68 | Tx9d2fu : Cancel 69 | T1y4 : OK 70 | Tft8p6c : Dashboard 71 | T1cu3mw : Users 72 | T17vyq : Apps 73 | Tzex2pg : Groups 74 | Tosmo8z : Settings 75 | T1whz : Yes 76 | T1y9 : No 77 | T706k13 : # pages,# page,# pages,# pages 78 | Tmm5bl3 : # items,# item,# items,# items 79 | Tysvd08 : Filter 80 | T11sitq : Apply 81 | T1ay23z : Reset 82 | Tmaxa8e : This message you are reading contains feedback from a platform user. I would appreciate your review and response. 83 | T1k0jv : URL: 84 | T17vx5 : App: 85 | T1cu3lb : User: 86 | T1hbr8uc : Device: 87 | T1in0cqv : E-mail address: 88 | T1jvf5v3 : Reference: 89 | T1sghbn7 : Message: 90 | Tnf4szb : Reset your password 91 | Tga1kxw : If you have lost your password or wish to reset it, use the link below to get started with @{CONF.name}. 92 | T3e79n1 : You can safely ignore this email if you did not request a password reset. Account passwords can only be reset by someone who has access to your email. 93 | Tnigz7n : Please be reminded that you have unread notifications (@{model.unread}) in the @{CONF.name}. If you would like to review them, please sign in to the portal. 94 | T1r2awam : Read notifications 95 | T1bx8wg2 : Welcome 96 | Tsz897w : Dear @{model.name}, welcome to the @{'%name'}. We're excited that you've joined the platform. Please confirm your account via the link below. 97 | T1dgvp2l : Confirm account 98 | T102cmcz : Invalid credentials 99 | Tlrm4jz : Account is not confirmed 100 | Tep427u : Feedback not found 101 | T4w8eic : Group not found 102 | T180ypcd : User account not found 103 | T1v0doob : Account photo 200x200 104 | T13xh18 : Email 105 | Tlv3xuz : Enable notifications 106 | Twvcu3l : Enable sounds 107 | T14mox7c : SUBMIT 108 | T1ppiecu : Account has been changed successfully 109 | Tfd0in7 : Send us your feedback or report a problem. Whenever a message is reported, we thoroughly investigate it. You will receive an email response from us. 110 | T80zqrg : Rate us: 111 | Ti7tjwq : Application: 112 | T17bmnl3 : Message 113 | Tb6sf2n : Thank you for reaching out and providing us with valuable feedback. 114 | T1fytv : Next 115 | T19s4i : Done 116 | Tn6oih5 : Welcome to . We are very thrilled to welcome you on board. 117 | Teuofo : That's all 118 | To92iba : You can update your profile now. 119 | T84f2oz : Time to relax! 120 | T16dbrot : You don't have any notifications 121 | T1nh15nd : Enter your old password 122 | Tujl5zo : Old password 123 | T18z50iy : We recommend using various chars like numbers, special characters, and lower/upper chars. 124 | Tbhyivq : Passwords cannot be reused. 125 | Tliwp51 : Password reply 126 | Tf7qnvg : Password has been changed successfully 127 | T1scnbkv : My open sessions 128 | Tvezzy0 : Auto-cleaner 129 | T1e4t50a : The program removes all open sessions except the one you are currently in. 130 | T17i0zgc : Are you sure you want to cancel selected session? 131 | Th2px7i : Are you sure you want to clear all unused sessions? 132 | T1qrqsk5 : Done, unused sessions have been removed 133 | T73iban : Welcome to . You are the platform's first super user. 134 | T4ucl30 : The first step is to update the main OpenPlatform configuration through the setup interface. Then you can register users and apps. 135 | T8259n1 : Account 136 | Tgeudzi : Copy settings 137 | T8zw3zk : Paste settings 138 | T1rgn0a8 : Synchronize 139 | T10a9v45 : Import 140 | Tqns537 : Enter application name 141 | T1fvl7 : Name 142 | Tttxmoz : URL address 143 | T7n6th0 : Origin: 144 | T74vdr9 : Set as a bookmark 145 | T1xgd2c1 : The app will be used as a bookmark to the external web app 146 | Tgrkjmo : Security 147 | T14wcv43 : Edit token 148 | Tc9wkjs : Request token 149 | Tvwxchm : Response token 150 | T1k8gb7p : You can allow only specific IP addresses to access sensitive user information for this app. 151 | Tfz4xv2 : 11.11.11.11 152 | Tkt0rcx : Allowed IP addresses 153 | T1fjh48b : Enter IP addresses separated by the comma 154 | Twdcm3g : Additional settings 155 | T1cq6x : Icon 156 | T12ve4j : Color 157 | Tjtx8mu : Open the app always externally in a new tab 158 | Truotm0 : Allow opening the app in a new tab 159 | T62mms : Enable explicit scrolling in the iframe 160 | T4xiins : Disable application 161 | T1eo1 : Add 162 | T1abzfj8 : Permissions 163 | T1usp52l : The app doesn't have any defined permissions 164 | Ts1qoa2 : Update app 165 | T19h9suc : Register app 166 | Ttk2mlu : Token has been copied into the clipboard. 167 | T1unypd : value 168 | T1219pnu : The app metadata has been exported to the clipboard. 169 | Txh3l5w : Copied 170 | T13f1p3l : Pasted 171 | Tymsnrx : Invalid clipboard data 172 | T1c2nfs9 : Change token 173 | T1ekbk05 : Assign groups 174 | T1uvyeal : Choose groups 175 | T1o15c9f : Add groups 176 | Tshmg9c : Remove groups 177 | T133axy1 : Are you sure you want to update selected users? 178 | T1608rft : Update 179 | T18jgseg : Created 180 | T1kpnv : User 181 | T1eyp : App 182 | Txsr3hy : Device 183 | T1eim7ln : IP address 184 | T151r95x : Solved 185 | Ts2wqv0 : Updated by 186 | T1hg9934 : Mark as solved 187 | T1yrok3o : The feedback is not solved yet 188 | Tk4ctwy : The system doesn't send email after solving. 189 | T10cah3n : Enter a group name 190 | Thuap0r : Reference 191 | T19zj9rb : Disable group 192 | T1v8kwlm : Allowed apps 193 | Txupfgo : Update group 194 | T3rra0r : Create group 195 | T1w396bq : Import app 196 | Tqxymaz : Enter the URL for the OpenPlatform app metadata. 197 | T13pqvju : https://www.yourdomain.com/openplatform.json 198 | Tzs3c11 : IMPORT 199 | Tswy1e1 : User photo 200x200 200 | T1f8kt : Male 201 | Tyqofos : Female 202 | T1us6m12 : Enter account name 203 | Txeubzz : E-mail 204 | T1h2tt0h : Default 205 | T19ewry0 : Language 206 | T30e032 : User email address is confirmed 207 | Tw0pwkw : Disable user account 208 | T6bmgs9 : User account is inactive 209 | T1irlupz : Super privileges 210 | T1yehj0z : Send a Welcome mail message 211 | T8kpxfn : Enter new password 212 | Th4z2qq : Update user 213 | Tigj0b3 : Create user 214 | T1oonxs3 : Register 215 | Txijsik : Create 216 | T14f871g : Remove 217 | T1sr : ID 218 | T4hb798 : disabled 219 | Tx5y7ue : bookmark 220 | T14x2tor : external 221 | T5jsqzg : Disabled 222 | Ty8frkm : Bookmark 223 | T15zkdez : External 224 | T1kpnh : Used 225 | T14wzruf : Last usage 226 | Tnnin4r : Updated 227 | T1cmsyog : Are you sure you want to remove selected apps? 228 | T13f7ws : Done. 229 | T19hez4 : Open: 230 | Trw6u07 : # sessions,# session,# sessions,# sessions 231 | T17eh95w : Used memory 232 | T1b6t9xe : Online users 233 | T185y3gq : Registered users 234 | Tbyb14r : Open feedback 235 | T15i86i3 : Subscribers 236 | T1l7n05e : Today statistics 237 | T1qw2huo : Max. logged users 238 | T1vtxcb8 : Desktop computers 239 | Tj9ic8v : Mobile devices 240 | T11qu4db : Total statistics 241 | T1gtii : Open 242 | T14d5cxp : Rating 243 | T1t3 : IP 244 | T1b3mt7h : Are you sure you want to remove selected feedback? 245 | Tgdx3fy : Are you sure you want to remove selected groups? 246 | T1qe2tcp : Icon 150x150 247 | Ttgx4hm : Open external and bookmarks in new tab/window 248 | T1p2sk0u : Enable WebSocket data synchronization for 3rd party apps 249 | T1l3aq7x : Synchronization Access token 250 | Tv0c3o5 : Generate 251 | T1q0gr7y : WebSocket endpoint 252 | Tpp7bbq : Allow reading for all users and groups 253 | T1t8blnf : This configuration allows external apps to read all groups and user profiles through the REST endpoint. The token below must be set in the X-Token header or via "?token=" query parameter. 254 | T1s93dvh : Enable API 255 | Tbupqhp : Access token 256 | Ttv3f05 : Endpoint 257 | T16y5rh : Total.js TMS 258 | T1hw0tk2 : The Total.js Message Service allows you to capture data from the OpenPlatform using the Total.js Flow Visual Programming Interface. 259 | T1s93rwd : Enable TMS 260 | Tvywxyz : TMS endpoint 261 | T1jsmq : Test 262 | T1e7nx65 : SMTP settings 263 | T1tbx2cp : Sender address 264 | Titpmq5 : SMTP server 265 | Tww2dw4 : SMTP options 266 | T189bd17 : SMTP options have to be in valid JSON format, according to the Total.js documentation 267 | T13gs4nu : Documentation 268 | T1p9z8wo : SMTP server is not configured yet. 269 | T20b5iu : SMTP server is configured correctly. 270 | T7bylzj : SMTP response: 271 | Txgsqvt : Settings have been saved successfully 272 | T1x0qenp : URL address has been copied 273 | Twl9frj : Assign 274 | T1mv43c6 : unconfirmed 275 | T1iagm6r : online 276 | T1j67nz : admin 277 | T11qceci : Logged 278 | T11ksof : Admin 279 | T1350pkj : Online 280 | Tjud4qn : Confirmed 281 | T1h67ij : Inactive 282 | Tphxixg : Are you sure you want to remove selected users? -------------------------------------------------------------------------------- /resources/sk.resource: -------------------------------------------------------------------------------- 1 | T1bjrc82 : Účet sa nenašiel 2 | T1kod7hb : Účet je zablokovaný 3 | T1glqo0e : Účet je neaktívny 4 | Tm0qe9a : E-mailová adresa je už použitá 5 | T13oi6c6 : Aplikácia sa nenašla 6 | T14lakc0 : Neplatný token 7 | T178h6ff : Relácia sa nenašla 8 | T1dwzbwy : Musíte zadať staré heslo 9 | T39vorh : Neplatné staré heslo 10 | T1wxl291 : Spätná väzba 11 | T17tfm1 : Prihlásenie 12 | T14ranob : Zadajte e-mailovú adresu 13 | T1w7l82b : E-mailová adresa 14 | Tdrftoj : Zadajte heslo 15 | Tykg00t : Nemáte ešte účet? 16 | T1aeqyco : PRIHLÁSIŤ 17 | Tf821nc : Pokyny na obnovenie hesla dostanete e-mailom. 18 | Tibgsfj : Prihlasovací formulár 19 | T1acy0f : RESETOVAŤ 20 | Tigv5rn : Zadajte názov 21 | Tt35tog : Zadajte heslo 22 | T1cyekzk : Zadajte heslo znova 23 | T1sksnqp : Znova heslo 24 | Trjmw2x : Vytvoriť účet 25 | Tqmy8m2 : Dostali ste e-mail s pokynmi na obnovenie hesla. 26 | T3qscoh : Váš účet bol úspešne vytvorený. Na vašu e-mailovú adresu bola odoslaná potvrdzujúca správa. 27 | Tl71ry3 : Heslo 28 | Tb4wo6k : Resetovať heslo 29 | Tjsvyke : Názov účtu 30 | T9sze77 : Heslá sa nezhodujú. 31 | Ty97he0 : Notifikácie 32 | T5zhbb1 : Štart menu 33 | T16zabwn : Zatvoriť {0} 34 | Ttmatki : Zatvoriť otvorené aplikácie 35 | Twnfthw : Nahlásiť problém 36 | T1vv3iit : spätná väzba 37 | T12cvb0z : Otvorené relácie 38 | T1twvzjd : profil 39 | Tvggo92 : Načítať znova účet 40 | Thy8ul7 : aktualizovať 41 | T11qckoa : Odhlásiť sa 42 | Treh6vn : Účet bol aktualizoný 43 | T1297zig : Reštartovať {0} 44 | Tzjt9hb : Zatvoriť {0} 45 | T1qonvrk : Otvoriť v novej karte 46 | T19oinzj : Reštartovať 47 | T12tjh4 : Zatvoriť 48 | T14w27d4 : Vyhľadať 49 | Tizbdg9 : Môj účet 50 | T41pkh7 : Zmeniť heslo 51 | T1bhvtp : Nastavenie 52 | Tft8p6c : Prehľad 53 | T1cu3mw : Používatelia 54 | T17vyq : Aplikácie 55 | Tzex2pg : Skupiny 56 | Tosmo8z : Nastavenia 57 | T1whz : Áno 58 | T1y9 : Nie 59 | Tmaxa8e : Táto správa, ktorú čítate, obsahuje spätnú väzbu od používateľa platformy. Ocenil by som vašu recenziu a reakciu. 60 | T1k0jv : URL: 61 | T17vx5 : Aplikácia: 62 | T1cu3lb : Používateľ: 63 | T1hbr8uc : Zariadenie: 64 | T1in0cqv : E-mailová adresa: 65 | T1jvf5v3 : Referencia: 66 | T1sghbn7 : Správa: 67 | Tnf4szb : Resetovať heslo 68 | Tga1kxw : Ak ste stratili svoje heslo do @{CONF.name} alebo ho chcete obnoviť, začnite pomocou odkazu nižšie. 69 | T3e79n1 : Tento e-mail môžete pokojne ignorovať, ak ste nepožiadali o obnovenie hesla. Heslá účtu môže obnoviť iba niekto, kto má prístup k vášmu e-mailu. 70 | T1bx8wg2 : Vitajte 71 | Tsz897w : Dobrý deň @{model.name}, vitajte v @{'%name'}. Sme radi, že ste sa pripojili k platforme. Potvrďte svoj účet prostredníctvom odkazu nižšie. 72 | T1dgvp2l : Potvrdiť účet 73 | T102cmcz : Nesprávne prihlasovacie údaje 74 | Tlrm4jz : Účet nie je potvrdený 75 | Tep427u : Spätná väzba sa nenašla 76 | T4w8eic : Skupina sa nenašla 77 | T180ypcd : Používaľ sa nenašiel 78 | T1v0doob : Fotografia účtu 200x200 79 | T1ppiecu : Účet bol úspešne aktualizovaný 80 | T13xh18 : Email 81 | Tlv3xuz : Povoliť notifikácie 82 | Twvcu3l : Povoliť zvuky 83 | T14mox7c : ODOSLAŤ 84 | Tx9d2fu : Zrušiť 85 | Tfd0in7 : Pošlite nám svoj názor alebo nahláste problém. Vždy, keď je správa nahlásená, dôkladne ju prešetríme. Dostanete od nás e-mailovú odpoveď. 86 | T80zqrg : Zadajte hodnotenie: 87 | Ti7tjwq : Aplikácia: 88 | Tb6sf2n : Ďakujeme, že ste sa na nás obrátili a poskytli nám cennú spätnú väzbu. 89 | T17bmnl3 : Správa 90 | T1fytv : Ďalej 91 | T19s4i : Hotovo 92 | Tn6oih5 : Vitajte v . Sme veľmi radi, že Vás môžeme privítať na palube. 93 | Teuofo : To je všetko 94 | To92iba : Teraz si môžete upraviť Váš profil. 95 | T12tbn1 : Vyčistiť 96 | T84f2oz : Čas na relax! 97 | T16dbrot : Nemáte žiadne notifikácie 98 | T1nh15nd : Zadajte Vaše staré heslo 99 | Tujl5zo : Staré heslo 100 | T18z50iy : Odporúčame používať rôzne znaky, ako sú čísla, špeciálne znaky a spodné/horné znaky. 101 | Tbhyivq : Heslá nie je možné znova použiť. 102 | Tliwp51 : Heslo znova 103 | Tf7qnvg : Heslo bolo úspešne zmenené 104 | T1scnbkv : Moje otvorené relácie 105 | Tvezzy0 : Auto-čistenie 106 | T1e4t50a : Program odstráni všetky otvorené relácie okrem tej, v ktorej sa práve nachádzate. 107 | T17i0zgc : Naozaj chcete zrušiť vybranú reláciu? 108 | Th2px7i : Naozaj chcete vymazať všetky nepoužívané relácie? 109 | T1qrqsk5 : Hotovo, nepoužívané relácie boli odstránené 110 | T73iban : Vitajte v . Ste prvým super používateľom platformy. 111 | T4ucl30 : Prvým krokom je aktualizácia hlavnej konfigurácie OpenPlatform cez nastavovacie rozhranie. Potom môžete zaregistrovať používateľov a aplikácie. 112 | T8259n1 : Účet 113 | Tgeudzi : Kopírovať nastavenie 114 | T8zw3zk : Prilepiť nastavenie 115 | Txh3l5w : Skopírované 116 | T13f1p3l : Prilepené 117 | Tymsnrx : Neplatné údaje v clipboarde 118 | Tqns537 : Zadajten zázov aplikácie 119 | Twdcm3g : Ďalšie nastavenie 120 | T1cq6x : Ikona 121 | T12ve4j : Farba 122 | T1fvl7 : Názov 123 | T7n6th0 : Pôvod: 124 | Tgrkjmo : Bezpečnosť 125 | Tc9wkjs : Request token 126 | Tvwxchm : Response token 127 | T1k8gb7p : Prístup k citlivým informáciám pre túto aplikáciu môžete povoliť iba konkrétnym adresám IP. 128 | Tfz4xv2 : 11.11.11.11 129 | Tkt0rcx : Povolené IP adresy 130 | T1fjh48b : Zadajte IP adresy oddelené čiarkami 131 | Truotm0 : Povoliť otvorenie aplikácie na samostatnej karte 132 | T4xiins : Zablokovať aplikáciu 133 | T1abzfj8 : Povolenia 134 | T1usp52l : Aplikácia nemá žiadne definované povolenia 135 | Ts1qoa2 : Aktualizovať aplikáciu 136 | T19h9suc : Zaregistrovať aplikáciu 137 | T1unypd : hodnota 138 | T1eo1 : Pridať 139 | T1ekbk05 : Prideliť skupiny 140 | T1o15c9f : Pridať skupiny 141 | Tshmg9c : Odstrániť skupiny 142 | T133axy1 : Naozaj chcete aktualizovať vybratých používateľov? 143 | T1608rft : Aktualizovať 144 | T1uvyeal : Zvoľte skupiny 145 | T18jgseg : Vytvorené 146 | T1kpnv : Používateľ 147 | T1eim7ln : IP adresa 148 | T1hg9934 : Označiť ako vyriešené 149 | T1yrok3o : Spätná väzba zatiaľ nie je vyriešená 150 | Tk4ctwy : Systém po vyriešení neodošle e-mail. 151 | T1eyp : Aplikácia 152 | Txsr3hy : Zariadenie 153 | T151r95x : Vyriešené 154 | Ts2wqv0 : Aktualizoval 155 | T19zj9rb : Zablokovať skupinu 156 | T1v8kwlm : Povolené aplikácie 157 | Tyedbj7 : Odblokovať 158 | Txupfgo : Aktualizovať skupiny 159 | T3rra0r : Vytvoriť skupinu 160 | T1w396bq : Importovať aplikáciu 161 | Tqxymaz : Zadajte adresu URL pre metadáta aplikácie OpenPlatform. 162 | T13pqvju : https://www.yourdomain.com/openplatform.json 163 | Tzs3c11 : IMPORTOVAŤ 164 | Tttxmoz : URL adresa 165 | Tswy1e1 : Fotka používateľa 200x200 166 | T1f8kt : Muž 167 | Tyqofos : Žena 168 | T1us6m12 : Zadajte názov účtu 169 | Txeubzz : E-mail 170 | T30e032 : E-mailová adresa používateľa je potvrdená 171 | Tw0pwkw : Zablokovať používateľský účet 172 | T6bmgs9 : Používateľský účet je neaktívny 173 | T1irlupz : Super privilégiá 174 | T1yehj0z : Poslať uvítaciu e-mailovú správu 175 | T8kpxfn : Zadajte nové heslo 176 | Th4z2qq : Aktualizovať používateľa 177 | Tigj0b3 : Vytvoriť používateľa 178 | T10a9v45 : Importovať 179 | T4hb798 : zablokovať 180 | T1kpnh : Použité 181 | T14wzruf : Naposledy 182 | T1cmsyog : Naozaj chcete odstrániť vybraté aplikácie? 183 | T19hc5a3 : Obnoviť 184 | T14f871g : Odstrániť 185 | T1sr : ID 186 | Tnnin4r : Zmenené 187 | T13f7ws : Hotovo. 188 | T5jsqzg : Zablokované 189 | T19hez4 : Otvorené: 190 | Trw6u07 : # relácií,# relácia,# relácie,# relácií 191 | T17eh95w : Použitá RAM 192 | T185y3gq : Registrovaní používatelia 193 | Tbyb14r : Otvorené spätné väzby 194 | T1l7n05e : Dnešné štatistiky 195 | T1qw2huo : Max. prihlásených 196 | T1vtxcb8 : Stolové počítače 197 | Tj9ic8v : Mobilné zariadenia 198 | T11qu4db : Celkové štatistiky 199 | T1350pkj : Online 200 | T1gtii : Otvorené 201 | T14d5cxp : Hodnotenie 202 | T1t3 : IP 203 | T1b3mt7h : Naozaj chcete odstrániť vybratú spätnú väzbu? 204 | Tgdx3fy : Naozaj chcete odstrániť vybraté skupiny? 205 | T1qe2tcp : Piktogram 150x150 206 | T1jsmq : Test 207 | T1e7nx65 : SMTP nastavenie 208 | T1tbx2cp : E-mail odosielateľa 209 | Titpmq5 : SMTP server 210 | Tww2dw4 : SMTP možnosti 211 | T189bd17 : Možnosti SMTP musia byť v platnom formáte JSON podľa dokumentácie Total.js 212 | T13gs4nu : Dokumentácia 213 | T1p9z8wo : Server SMTP ešte nie je nakonfigurovaný. 214 | T20b5iu : Server SMTP je správne nakonfigurovaný. 215 | T7bylzj : Odpoveď SMTP: 216 | Txgsqvt : Nastavenia boli úspešne uložené 217 | Twl9frj : Prideliť 218 | T1iagm6r : online 219 | T1j67nz : admin 220 | T11qceci : Prihlásený 221 | T11ksof : Admin 222 | T1h67ij : Neaktívny 223 | Tphxixg : Naozaj chcete odstrániť vybratých používateľov? 224 | Txspnlf : Oddeliť 225 | T1gkjkgn : Neprečítané správy 226 | Tnigz7n : Pripomíname Vám, že máte neprečítané upozornenia (@{model.unread}) v @{CONF.name}. Ak si ich chcete pozrieť, prihláste sa na portál. 227 | T1r2awam : Prečítať správy 228 | T706k13 : # strán,# strana,# strany,# strán 229 | Tmm5bl3 : # položiek,# položka,# položky,# položiek 230 | Tagsor7 : Neplatné data 231 | T2cba : teraz 232 | T16y5rh : Total.js TMS 233 | T1hw0tk2 : Total.js Message Service Vám povolí zachytiť data z OpenPlatformy pomocou Total.js Flow - vizuálne programovateľné rozhranie. 234 | T1s93rwd : Povoliť TMS 235 | Tbupqhp : Prístupový token 236 | Tv0c3o5 : Vygenerovať 237 | Tvywxyz : TMS koncový bod 238 | T1x0qenp : URL adresa bola skopírovaná 239 | Tnhjmc9 : pred # minútami,pred # minútou,pred # minútami,pred # minútami 240 | T3egjfd : pred # hodinami,pred # hodinou,pred # hodinami,pred # hodinami 241 | T9wgord : pred # dňami,pred # dňom,pred # dňami,pred # dňami 242 | T1mqtx3t : pred # mesiacmi,pred # mesiacom,pred # mesiacmi,pred # mesiacmi 243 | Tlcnga : Aplikácia bola dočasne zablokovaná 244 | T74vdr9 : Nastaviť ako záložku 245 | T1xgd2c1 : Aplikácia bude použitá ako záložka pre externú aplikáciu 246 | Tx5y7ue : záložka 247 | Ty8frkm : Záložka 248 | To0b6h : # rokov,# rok,# roky,# rokov 249 | Tysvd08 : Filter 250 | T11sitq : Použiť 251 | T1ay23z : Resetovať 252 | T10cah3n : Zadajte názov skupiny 253 | T1b6t9xe : Online používatelia 254 | T1mv43c6 : Nepotvdené 255 | Tjud4qn : Potvrdené 256 | T1rgn0a8 : Synchronizovať 257 | T62mms : Povoliť explicitné posúvanie v rámci iframe 258 | Ttk2mlu : Token bol skopírovaný do schránky. 259 | T1219pnu : Metadáta aplikácie boli exportované do schránky. 260 | Tpp7bbq : Povoliť čítanie všetkých používateľov a skupín 261 | T1t8blnf : Táto konfigurácia umožňuje externým aplikáciám čítať všetky skupiny a používateľské profily cez koncový bod REST. 262 | Ttv3f05 : Token nižšie musí byť nastavený v hlavičke X-Token alebo prostredníctvom parametra dopytu "?token=" 263 | T1s93dvh : Povoliť API 264 | T14e78xi : Neplatná skupina 265 | Thvmnq2 : Do skupiny 266 | Tgtcv77 : Neplatný názov povolenia 267 | T7gzhej : Do role 268 | T1c7py9 : Dnes 269 | Tkg5z0p : Nedeľa,Pondelok,Utorok,Streda,Štvrtok,Piatok,Sobota 270 | T1maqqy3 : Január,Február,Marec,Apríl,Máj,Jún,Júl,August,September,Október,November,December 271 | Thuap0r : Referencia 272 | T19ewry0 : Jazyk 273 | T1h2tt0h : Predvolený -------------------------------------------------------------------------------- /schemas/account.js: -------------------------------------------------------------------------------- 1 | NEWACTION('Account/session', { 2 | name: 'Read session data', 3 | action: async function($) { 4 | $.callback($.user); 5 | } 6 | }); 7 | 8 | NEWACTION('Account/read', { 9 | name: 'Read account data', 10 | action: async function($) { 11 | 12 | var profile = await DATA.read('op.tbl_user').fields('id,email,name,gender,dtbirth,photo,language,color,interface,unread,darkmode,logged,sounds,notifications,sa,dtlogged,isdisabled,isinactive,isremoved,isconfirmed').id($.user.id).promise($); 13 | 14 | if (!profile || profile.isdisabled || profile.isinactive || profile.isremoved || !profile.isconfirmed) { 15 | MAIN.auth.logout($, () => $.invalid(401)); 16 | return; 17 | } 18 | 19 | profile.isdisabled = undefined; 20 | profile.isinactive = undefined; 21 | profile.isremoved = undefined; 22 | profile.isconfirmed = undefined; 23 | profile.isreset = $.user.isreset; 24 | 25 | if (CONF.welcome) { 26 | profile.welcome = true; 27 | CONF.welcome = false; 28 | } 29 | 30 | if (!CONF.url) { 31 | CONF.url = $.controller.hostname(); 32 | DATA.modify('op.cl_config', { value: CONF.url }).id('url'); 33 | } 34 | 35 | profile = await $.transform('profile', profile); 36 | $.callback(profile); 37 | } 38 | }); 39 | 40 | NEWACTION('Account/update', { 41 | name: 'Update account', 42 | input: 'photo:String, *name:String, *email:Email, language:Lower, notifications:Boolean, sounds:Boolean, interface:String, color:Color, darkmode:Number', 43 | publish: '+id', 44 | action: async function($, model) { 45 | 46 | await DATA.check('op.tbl_user').where('email', model.email).where('id', '<>', $.user.id).where('isremoved=FALSE').error('@(E-mail address is already used)', true).promise($); 47 | model.dtupdated = NOW; 48 | await DATA.modify('op.tbl_user', model).id($.user.id).promise($); 49 | 50 | MAIN.auth.refresh($.user.id); 51 | $.success(); 52 | 53 | model.id = $.user.id; 54 | $.publish(model); 55 | } 56 | }); 57 | 58 | NEWACTION('Account/apps', { 59 | name: 'List of apps', 60 | action: function($) { 61 | FUNC.permissions($.user.id, async function(data) { 62 | if (data && data.apps.length) { 63 | 64 | var apps = await DATA.find('op.tbl_app').fields('id,name,icon,color,isnewtab,isbookmark,isexternal,isscrollbar,sortindex').in('id', data.apps).where('isremoved=FALSE AND isdisabled=FALSE').promise($); 65 | var userapps = await DATA.find('op.tbl_user_app').where('userid', $.user.id).in('appid', data.apps).query('appid IN (SELECT x.id FROM op.tbl_app x WHERE x.isremoved=FALSE AND x.isdisabled=FALSE)').promise($); 66 | 67 | for (var app of userapps) { 68 | var origin = apps.findItem('id', app.appid); 69 | origin.isfavorite = app.isfavorite; 70 | origin.notifications = app.notifications; 71 | origin.muted = app.muted; 72 | if (app.sortindex !== 0) 73 | origin.sortindex = app.sortindex; 74 | } 75 | 76 | $.callback(apps); 77 | 78 | } else 79 | $.callback(EMPTYARRAY); 80 | }); 81 | } 82 | }); 83 | 84 | NEWACTION('Account/reorder', { 85 | name: 'Reorder apps', 86 | input: '*id:[UID]', 87 | action: function($, model) { 88 | 89 | FUNC.permissions($.user.id, async function(data) { 90 | 91 | var builder = []; 92 | var userapps = await DATA.find('op.tbl_user_app').fields('id,appid,sortindex').where('userid', $.user.id).in('appid', data.apps).promise($); 93 | 94 | for (var i = 0; i < model.id.length; i++) { 95 | 96 | var id = model.id[i]; 97 | 98 | // Check the app existence 99 | if (!data.apps.includes(id)) 100 | continue; 101 | 102 | var ua = userapps.findItem('appid', id); 103 | 104 | if (ua) { 105 | // modify 106 | id && builder.push('UPDATE op.tbl_user_app SET sortindex={0}, dtupdated=NOW() WHERE id=\'{1}\';'.format(i + 1, ua.id)); 107 | } else { 108 | // create 109 | await DATA.insert('op.tbl_user_app', { id: $.user.id + id, userid: $.user.id, appid: id, notifications: true, sortindex: i + 1, dtupdated: NOW }).promise($); 110 | } 111 | 112 | } 113 | 114 | if (builder.length) 115 | await DATA.query(builder.join('\n')).promise($); 116 | 117 | $.success(); 118 | }); 119 | 120 | } 121 | }); 122 | 123 | NEWACTION('Account/run', { 124 | name: 'Run app', 125 | params: '*appid:UID', 126 | publish: 'id,sessionid,appid,userid,name,color,icon,user,device,ip,dtcreated:Date,dtexpire:Date', 127 | action: function($) { 128 | FUNC.permissions($.user.id, async function(data) { 129 | 130 | var params = $.params; 131 | 132 | if (!data.apps.includes(params.appid)) { 133 | $.invalid('@(App not found)'); 134 | return; 135 | } 136 | 137 | var app = await DATA.read('op.tbl_app').fields('id,url,icon,color,name,reqtoken,restoken,isdisabled,isbookmark,isexternal').id(params.appid).error('@(App not found)').where('isremoved=FALSE').promise($); 138 | 139 | if (app.isdisabled) { 140 | $.invalid('@(App has been temporary disabled)'); 141 | return; 142 | } 143 | 144 | var session = {}; 145 | session.id = Date.now().toString(36) + GUID(10); 146 | session.sessionid = $.sessionid || $.user.sessionid; 147 | session.userid = $.user.id; 148 | session.appid = app.id; 149 | session.ip = $.ip; 150 | session.device = $.mobile ? 'mobile' : 'desktop'; 151 | session.dtcreated = NOW; 152 | session.dtexpire = NOW.add(CONF.app_session_expire || '1 day'); 153 | 154 | if (!app.isbookmark) { 155 | 156 | session.url = CONF.url + '/verify/?token=' + FUNC.checksum(session.id + 'X' + CONF.id); 157 | session.reqtoken = session.url.md5(app.reqtoken).toLowerCase(); 158 | session.restoken = session.reqtoken.md5(app.restoken); 159 | 160 | // Remove previous one (due to security) 161 | // await DATA.remove('op.tbl_app_session').where('appid', app.id).where('sessionid', session.sessionid).promise($); 162 | } 163 | 164 | // Register a new session 165 | await DATA.insert('op.tbl_app_session', session).promise($); 166 | await DATA.query('UPDATE op.tbl_app SET logged=logged+1, dtlogged=NOW() WHERE id=' + PG_ESCAPE(app.id)).promise(); 167 | 168 | if (app.isbookmark) 169 | app.url = QUERIFY(app.url, { ssid: FUNC.checksum(session.id + 'X' + session.sessionid) }); 170 | else 171 | app.url = QUERIFY(app.url, { openplatform: session.url + '~' + session.reqtoken }); 172 | 173 | app.reqtoken = undefined; 174 | app.restoken = undefined; 175 | 176 | $.callback(app); 177 | 178 | if (CONF.$tms) { 179 | session.reqtoken = undefined; 180 | session.restoken = undefined; 181 | session.url = app.url; 182 | session.icon = app.icon; 183 | session.color = app.color; 184 | session.name = app.name; 185 | session.user = $.user.name; 186 | $.publish(session); 187 | } 188 | 189 | }); 190 | } 191 | }); 192 | 193 | NEWACTION('Account/token', { 194 | name: 'Login by token', 195 | query: '*token:String', 196 | action: async function($) { 197 | 198 | var token = $.query.token; 199 | var arr = token.split('X'); 200 | 201 | if (FUNC.checksum(arr[0]) !== token) { 202 | $.invalid('@(Invalid token)'); 203 | return; 204 | } 205 | 206 | var profile = await DATA.read('op.tbl_user').fields('id,language,name,isdisabled,isinactive,isconfirmed,isreset').where('token', token).where('isremoved=FALSE').error('@(Account not found)').promise($); 207 | 208 | $.language = profile.language; 209 | 210 | if (profile.isdisabled) { 211 | $.invalid('@(Account is disabled)'); 212 | return; 213 | } 214 | 215 | if (profile.isinactive) { 216 | $.invalid('@(Account is inactive)'); 217 | return; 218 | } 219 | 220 | if (!profile.isconfirmed) 221 | await DATA.modify('op.tbl_user', { isconfirmed: true }).id(profile.id).promise($); 222 | 223 | MAIN.auth.login($, profile.id, () => $.redirect('/' + (profile.isconfirmed && profile.isreset ? '?reset=1' : '?welcome=1'))); 224 | } 225 | }); 226 | 227 | NEWACTION('Account/logout', { 228 | name: 'Sign out', 229 | publish: 'id,name,sessionid', 230 | action: function($) { 231 | $.publish($.user); 232 | MAIN.auth.logout($, () => $.redirect('/')); 233 | } 234 | }); 235 | 236 | NEWACTION('Account/notifications', { 237 | name: 'Read all notifications', 238 | action: function($) { 239 | var userid = $.user.id; 240 | 241 | DATA.find('op.tbl_notification').fields('id,appid,name,icon,path,body,color,isread,dtcreated').where('userid', userid).sort('dtcreated', true).take(50).callback($); 242 | 243 | if ($.user.unread) { 244 | userid = PG_ESCAPE(userid); 245 | DATA.query('UPDATE op.tbl_notification SET isread=TRUE WHERE userid={0} AND isread=FALSE'.format(userid)); 246 | DATA.query('UPDATE op.tbl_user SET unread=0, dtnotified=NULL WHERE id={0} AND unread>0'.format(userid)); 247 | } 248 | 249 | } 250 | }); 251 | 252 | NEWACTION('Account/notifications_clear', { 253 | name: 'Clear all notifications', 254 | action: function($) { 255 | DATA.query('DELETE FROM op.tbl_notification WHERE userid=' + PG_ESCAPE($.user.id)); 256 | MAIN.auth.update($.user.id, user => user.unread = 0); 257 | $.success(); 258 | } 259 | }); 260 | 261 | NEWACTION('Account/sessions', { 262 | name: 'Read user open sessions', 263 | action: async function($) { 264 | var items = await DATA.find('op.tbl_session').where('userid', $.user.id).sort('dtcreated', true).promise($); 265 | for (var item of items) 266 | item.current = item.id === $.sessionid; 267 | $.callback(items); 268 | } 269 | }); 270 | 271 | NEWACTION('Account/sessions_remove', { 272 | name: 'Remove session', 273 | params: '*id:String', 274 | action: function($) { 275 | var params = $.params; 276 | DATA.remove('op.tbl_session').id(params.id).where('userid', $.user.id).error('@(Session not found)').callback($.done()); 277 | MAIN.auth.refresh($.user.id); 278 | } 279 | }); 280 | 281 | NEWACTION('Account/password', { 282 | name: 'Change password', 283 | input: 'oldpassword:String, *password:String', 284 | publish: 'id,name,email', 285 | action: async function($, model) { 286 | 287 | var user = $.user; 288 | 289 | if (!user.isreset && !model.oldpassword) { 290 | $.invalid('@(You must enter old password)'); 291 | return; 292 | } 293 | 294 | if (!user.isreset) 295 | await DATA.read('op.tbl_user').fields('id').id(user.id).where('password', model.oldpassword.sha256(CONF.salt)).error('@(Invalid old password)').promise($); 296 | 297 | await DATA.modify('op.tbl_user', { dtpassword: NOW, password: model.password.sha256(CONF.salt) }).id(user.id).promise($); 298 | 299 | $.publish($.user); 300 | $.success(); 301 | } 302 | }); 303 | 304 | NEWACTION('Account/feedback', { 305 | name: 'Create a feedback', 306 | input: 'appid:UID, rating:Number, *body:String', 307 | publish: '+ip,userid,appid,app,email,account,ua,dtcreated:Date', 308 | action: async function($, model) { 309 | 310 | model.id = UID(); 311 | model.userid = $.user.id; 312 | model.dtcreated = NOW; 313 | model.ua = $.ua; 314 | model.ip = $.ip; 315 | 316 | var user = await DATA.read('op.tbl_user').fields('name,email,reference').id($.user.id).promise($); 317 | var app = model.appid ? await DATA.read('op.tbl_app').fields('name').id(model.appid).promise($) : null; 318 | 319 | model.email = user.email; 320 | model.account = user.name; 321 | model.app = app ? app.name : ''; 322 | 323 | await DATA.insert('op.tbl_feedback', model).promise($); 324 | 325 | var admin = await DATA.find('op.tbl_user').fields('email,language').where('sa=TRUE AND isremoved=FALSE AND isconfirmed=TRUE AND isinactive=FALSE AND isdisabled=FALSE').promise($); 326 | 327 | if (CONF.ismail) { 328 | for (var m of admin) 329 | MAIL(m.email, '@(Feedback)', 'mail/feedback', model, NOOP, m.language || CONF.language || '').reply(user.email); 330 | } 331 | 332 | $.publish(model); 333 | $.success(); 334 | } 335 | }); -------------------------------------------------------------------------------- /views/mail/feedback.html: -------------------------------------------------------------------------------- 1 | @{layout('mail/layout')} 2 | 3 |
4 |
@(Feedback)
5 |
@(This message you are reading contains feedback from a platform user. I would appreciate your review and response.)
6 |
7 |
8 |
9 |
@{'%name'}
10 |
@(URL:) @{'%url'}
11 | @{if model.app} 12 |
@(App:) @{model.app}
13 | @{fi} 14 |
15 |
@(User:) @{model.account}
16 |
@(Device:) @{model.ua}
17 |
@(E-mail address:) @{model.email}
18 | @{if model.reference} 19 |
@(Reference:) @{model.reference}
20 | @{fi} 21 |
22 |
@(Message:)
23 |
@{model.body}
24 |
-------------------------------------------------------------------------------- /views/mail/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mail Template 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 37 | 38 |
14 | 15 | 16 | 20 | 21 | 22 | 27 | 28 | 29 | 34 | 35 |
17 |
18 |
19 |
23 |
24 | @{body} 25 |
26 |
30 |
31 | © @{NOW.getFullYear()} @{CONF.name} 32 |
33 |
36 |
39 | 40 | -------------------------------------------------------------------------------- /views/mail/reset.html: -------------------------------------------------------------------------------- 1 | @{layout('mail/layout')} 2 | 3 |
4 |
@(Reset your password)
5 | @(If you have lost your password or wish to reset it, use the link below to get started with @{CONF.name}.) 6 |
@(You can safely ignore this email if you did not request a password reset. Account passwords can only be reset by someone who has access to your email.)
7 |
8 |
9 | @(Reset password) 10 |
-------------------------------------------------------------------------------- /views/mail/unread.html: -------------------------------------------------------------------------------- 1 | @{layout('mail/layout')} 2 | 3 |
4 |
Hello @{model.name}!
5 |
@(Please be reminded that you have unread notifications (@{model.unread}) in the @{CONF.name}. If you would like to review them, please sign in to the portal.)
6 |
@{CONF.url}
7 |
8 |
9 |
10 | @(Read notifications) 11 |
-------------------------------------------------------------------------------- /views/mail/welcome.html: -------------------------------------------------------------------------------- 1 | @{layout('mail/layout')} 2 | 3 |
@(Welcome)
4 |
5 |
@(Dear @{model.name}, welcome to the @{'%name'}. We're excited that you've joined the platform. Please confirm your account via the link below.)
6 |
7 | @(Confirm account) 8 |
9 | --------------------------------------------------------------------------------