Frontend
							
								
								
									
										
											BIN
										
									
								
								src/frontend/app-images/default-avatar.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/frontend/app-images/favicons/android-chrome-192x192.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 15 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/frontend/app-images/favicons/android-chrome-512x512.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 34 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/frontend/app-images/favicons/apple-touch-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										9
									
								
								src/frontend/app-images/favicons/browserconfig.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <browserconfig> | ||||||
|  |     <msapplication> | ||||||
|  |         <tile> | ||||||
|  |             <square150x150logo src="/images/favicon/mstile-150x150.png"/> | ||||||
|  |             <TileColor>#f0ad00</TileColor> | ||||||
|  |         </tile> | ||||||
|  |     </msapplication> | ||||||
|  | </browserconfig> | ||||||
							
								
								
									
										
											BIN
										
									
								
								src/frontend/app-images/favicons/favicon-16x16.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 958 B | 
							
								
								
									
										
											BIN
										
									
								
								src/frontend/app-images/favicons/favicon-32x32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/frontend/app-images/favicons/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 15 KiB | 
							
								
								
									
										18
									
								
								src/frontend/app-images/favicons/manifest.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,18 @@ | |||||||
|  | { | ||||||
|  |     "name": "", | ||||||
|  |     "icons": [ | ||||||
|  |         { | ||||||
|  |             "src": "/images/favicon/android-chrome-192x192.png", | ||||||
|  |             "sizes": "192x192", | ||||||
|  |             "type": "image/png" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             "src": "/images/favicon/android-chrome-512x512.png", | ||||||
|  |             "sizes": "512x512", | ||||||
|  |             "type": "image/png" | ||||||
|  |         } | ||||||
|  |     ], | ||||||
|  |     "theme_color": "#ffffff", | ||||||
|  |     "background_color": "#ffffff", | ||||||
|  |     "display": "standalone" | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								src/frontend/app-images/favicons/mstile-150x150.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 10 KiB | 
							
								
								
									
										32
									
								
								src/frontend/app-images/favicons/safari-pinned-tab.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,32 @@ | |||||||
|  | <?xml version="1.0" standalone="no"?> | ||||||
|  | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" | ||||||
|  |  "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> | ||||||
|  | <svg version="1.0" xmlns="http://www.w3.org/2000/svg" | ||||||
|  |  width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000" | ||||||
|  |  preserveAspectRatio="xMidYMid meet"> | ||||||
|  | <metadata> | ||||||
|  | Created by potrace 1.11, written by Peter Selinger 2001-2013 | ||||||
|  | </metadata> | ||||||
|  | <g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)" | ||||||
|  | fill="#000000" stroke="none"> | ||||||
|  | <path d="M2384 5110 c-1036 -74 -1922 -761 -2248 -1743 -146 -437 -170 -915 | ||||||
|  | -70 -1367 193 -869 838 -1582 1687 -1864 437 -146 915 -170 1367 -70 632 141 | ||||||
|  | 1204 533 1564 1074 222 333 358 697 412 1100 22 167 22 473 0 640 -96 722 | ||||||
|  | -473 1348 -1062 1767 -472 335 -1074 504 -1650 463z m1514 -1050 c173 -18 201 | ||||||
|  | -52 210 -250 8 -190 -30 -414 -110 -655 -112 -338 -282 -619 -509 -842 -131 | ||||||
|  | -130 -233 -203 -384 -278 -211 -105 -410 -162 -663 -188 l-114 -12 -97 -109 | ||||||
|  | c-201 -228 -505 -449 -846 -616 -210 -102 -316 -133 -338 -98 -25 39 106 345 | ||||||
|  | 248 578 166 275 296 430 548 657 36 32 47 49 47 72 l0 30 -137 -66 c-229 -109 | ||||||
|  | -615 -273 -644 -273 -15 0 -35 7 -43 16 -24 24 -20 109 8 169 38 82 425 784 | ||||||
|  | 473 860 88 136 163 193 292 221 71 15 247 18 324 5 l48 -8 49 61 c305 381 900 | ||||||
|  | 682 1434 726 50 4 96 8 101 8 6 1 52 -3 103 -8z m-558 -2245 c-21 -148 -73 | ||||||
|  | -226 -214 -319 -97 -64 -842 -472 -899 -492 -47 -17 -110 -18 -138 -4 -13 7 | ||||||
|  | -19 21 -19 44 0 29 102 278 230 563 36 81 28 76 165 88 273 25 602 139 810 | ||||||
|  | 283 l70 48 3 -65 c2 -36 -2 -102 -8 -146z"/> | ||||||
|  | <path d="M3580 3711 c-72 -23 -112 -76 -112 -151 0 -88 62 -152 150 -153 194 | ||||||
|  | -3 214 278 22 307 -19 3 -46 1 -60 -3z"/> | ||||||
|  | <path d="M3035 3392 c-68 -33 -128 -93 -158 -161 -31 -69 -29 -178 5 -252 54 | ||||||
|  | -117 159 -184 288 -184 96 1 169 33 234 106 60 66 80 129 74 228 -7 118 -59 | ||||||
|  | 201 -160 256 -44 24 -67 30 -138 33 -77 3 -90 1 -145 -26z"/> | ||||||
|  | </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 1.8 KiB | 
							
								
								
									
										1
									
								
								src/frontend/fonts
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | ../../node_modules/tabler-ui/dist/assets/fonts | ||||||
							
								
								
									
										1
									
								
								src/frontend/images
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | ../../node_modules/tabler-ui/dist/assets/images | ||||||
							
								
								
									
										224
									
								
								src/frontend/js/app/api.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,224 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const $      = require('jquery'); | ||||||
|  | const _      = require('underscore'); | ||||||
|  | const Tokens = require('./tokens'); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @param {String}  message | ||||||
|  |  * @param {*}       debug | ||||||
|  |  * @param {Integer} code | ||||||
|  |  * @constructor | ||||||
|  |  */ | ||||||
|  | const ApiError = function (message, debug, code) { | ||||||
|  |     let temp  = Error.call(this, message); | ||||||
|  |     temp.name = this.name = 'ApiError'; | ||||||
|  |     this.stack   = temp.stack; | ||||||
|  |     this.message = temp.message; | ||||||
|  |     this.debug   = debug; | ||||||
|  |     this.code    = code; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | ApiError.prototype = Object.create(Error.prototype, { | ||||||
|  |     constructor: { | ||||||
|  |         value:        ApiError, | ||||||
|  |         writable:     true, | ||||||
|  |         configurable: true | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * | ||||||
|  |  * @param   {String} verb | ||||||
|  |  * @param   {String} path | ||||||
|  |  * @param   {Object} [data] | ||||||
|  |  * @param   {Object} [options] | ||||||
|  |  * @returns {Promise} | ||||||
|  |  */ | ||||||
|  | function fetch (verb, path, data, options) { | ||||||
|  |     options = options || {}; | ||||||
|  |  | ||||||
|  |     return new Promise(function (resolve, reject) { | ||||||
|  |         let api_url = '/api/'; | ||||||
|  |         let url     = api_url + path; | ||||||
|  |         let token   = Tokens.getTopToken(); | ||||||
|  |  | ||||||
|  |         $.ajax({ | ||||||
|  |             url:         url, | ||||||
|  |             data:        typeof data === 'object' ? JSON.stringify(data) : data, | ||||||
|  |             type:        verb, | ||||||
|  |             dataType:    'json', | ||||||
|  |             contentType: 'application/json; charset=UTF-8', | ||||||
|  |             crossDomain: true, | ||||||
|  |             timeout:     (options.timeout ? options.timeout : 15000), | ||||||
|  |             xhrFields:   { | ||||||
|  |                 withCredentials: true | ||||||
|  |             }, | ||||||
|  |  | ||||||
|  |             beforeSend: function (xhr) { | ||||||
|  |                 xhr.setRequestHeader('Authorization', 'Bearer ' + (token ? token.t : null)); | ||||||
|  |             }, | ||||||
|  |  | ||||||
|  |             success: function (data, textStatus, response) { | ||||||
|  |                 let total = response.getResponseHeader('X-Dataset-Total'); | ||||||
|  |                 if (total !== null) { | ||||||
|  |                     resolve({ | ||||||
|  |                         data:       data, | ||||||
|  |                         pagination: { | ||||||
|  |                             total:  parseInt(total, 10), | ||||||
|  |                             offset: parseInt(response.getResponseHeader('X-Dataset-Offset'), 10), | ||||||
|  |                             limit:  parseInt(response.getResponseHeader('X-Dataset-Limit'), 10) | ||||||
|  |                         } | ||||||
|  |                     }); | ||||||
|  |                 } else { | ||||||
|  |                     resolve(response); | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |  | ||||||
|  |             error: function (xhr, status, error_thrown) { | ||||||
|  |                 let code = 400; | ||||||
|  |  | ||||||
|  |                 if (typeof xhr.responseJSON !== 'undefined' && typeof xhr.responseJSON.error !== 'undefined' && typeof xhr.responseJSON.error.message !== 'undefined') { | ||||||
|  |                     error_thrown = xhr.responseJSON.error.message; | ||||||
|  |                     code         = xhr.responseJSON.error.code || 500; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 reject(new ApiError(error_thrown, xhr.responseText, code)); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * | ||||||
|  |  * @param {Array} expand | ||||||
|  |  * @returns {String} | ||||||
|  |  */ | ||||||
|  | function makeExpansionString (expand) { | ||||||
|  |     let items = []; | ||||||
|  |     _.forEach(expand, function (exp) { | ||||||
|  |         items.push(encodeURIComponent(exp)); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return items.join(','); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |     status: function () { | ||||||
|  |         return fetch('get', ''); | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     Tokens: { | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * @param   {String}  identity | ||||||
|  |          * @param   {String}  secret | ||||||
|  |          * @param   {Boolean} [wipe]       Will wipe the stack before adding to it again if login was successful | ||||||
|  |          * @returns {Promise} | ||||||
|  |          */ | ||||||
|  |         login: function (identity, secret, wipe) { | ||||||
|  |             return fetch('post', 'tokens', {identity: identity, secret: secret}) | ||||||
|  |                 .then(response => { | ||||||
|  |                     if (response.token) { | ||||||
|  |                         if (wipe) { | ||||||
|  |                             Tokens.clearTokens(); | ||||||
|  |                         } | ||||||
|  |  | ||||||
|  |                         // Set storage token | ||||||
|  |                         Tokens.addToken(response.token); | ||||||
|  |                         return response.token; | ||||||
|  |                     } else { | ||||||
|  |                         Tokens.clearTokens(); | ||||||
|  |                         throw(new Error('No token returned')); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |         }, | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * @returns {Promise} | ||||||
|  |          */ | ||||||
|  |         refresh: function () { | ||||||
|  |             return fetch('get', 'tokens') | ||||||
|  |                 .then(response => { | ||||||
|  |                     if (response.token) { | ||||||
|  |                         Tokens.setCurrentToken(response.token); | ||||||
|  |                         return response.token; | ||||||
|  |                     } else { | ||||||
|  |                         Tokens.clearTokens(); | ||||||
|  |                         throw(new Error('No token returned')); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     Users: { | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * @param   {Integer|String}  user_id | ||||||
|  |          * @param   {Array}           [expand] | ||||||
|  |          * @returns {Promise} | ||||||
|  |          */ | ||||||
|  |         getById: function (user_id, expand) { | ||||||
|  |             return fetch('get', 'users/' + user_id + (typeof expand === 'object' && expand.length ? '?expand=' + makeExpansionString(expand) : '')); | ||||||
|  |         }, | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * @param   {Integer}  [offset] | ||||||
|  |          * @param   {Integer}  [limit] | ||||||
|  |          * @param   {String}   [sort] | ||||||
|  |          * @param   {Array}    [expand] | ||||||
|  |          * @param   {String}   [query] | ||||||
|  |          * @returns {Promise} | ||||||
|  |          */ | ||||||
|  |         getAll: function (offset, limit, sort, expand, query) { | ||||||
|  |             return fetch('get', 'users?offset=' + (offset ? offset : 0) + '&limit=' + (limit ? limit : 20) + (sort ? '&sort=' + sort : '') + | ||||||
|  |                 (typeof expand === 'object' && expand !== null && expand.length ? '&expand=' + makeExpansionString(expand) : '') + | ||||||
|  |                 (typeof query === 'string' ? '&query=' + query : '')); | ||||||
|  |         }, | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * @param   {Object}  data | ||||||
|  |          * @returns {Promise} | ||||||
|  |          */ | ||||||
|  |         create: function (data) { | ||||||
|  |             return fetch('post', 'users', data); | ||||||
|  |         }, | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * @param   {Object}   data | ||||||
|  |          * @param   {Integer}  data.id | ||||||
|  |          * @returns {Promise} | ||||||
|  |          */ | ||||||
|  |         update: function (data) { | ||||||
|  |             let id = data.id; | ||||||
|  |             delete data.id; | ||||||
|  |             return fetch('put', 'users/' + id, data); | ||||||
|  |         }, | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * @param   {Integer}  id | ||||||
|  |          * @returns {Promise} | ||||||
|  |          */ | ||||||
|  |         delete: function (id) { | ||||||
|  |             return fetch('delete', 'users/' + id); | ||||||
|  |         }, | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * | ||||||
|  |          * @param   {Integer}  id | ||||||
|  |          * @param   {Object}   auth | ||||||
|  |          * @returns {Promise} | ||||||
|  |          */ | ||||||
|  |         setPassword: function (id, auth) { | ||||||
|  |             return fetch('put', 'users/' + id + '/auth', auth); | ||||||
|  |         }, | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * @param   {Integer}  id | ||||||
|  |          * @returns {Promise} | ||||||
|  |          */ | ||||||
|  |         loginAs: function (id) { | ||||||
|  |             return fetch('post', 'users/' + id + '/login'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | }; | ||||||
							
								
								
									
										10
									
								
								src/frontend/js/app/cache.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const UserModel = require('../models/user'); | ||||||
|  |  | ||||||
|  | let cache = { | ||||||
|  |     User: new UserModel.Model() | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = cache; | ||||||
|  |  | ||||||
							
								
								
									
										107
									
								
								src/frontend/js/app/controller.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,107 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const Backbone = require('backbone'); | ||||||
|  | const Cache    = require('./cache'); | ||||||
|  | const Tokens   = require('./tokens'); | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @param {String} route | ||||||
|  |      * @param {Object} [options] | ||||||
|  |      * @returns {Boolean} | ||||||
|  |      */ | ||||||
|  |     navigate: function (route, options) { | ||||||
|  |         options = options || {}; | ||||||
|  |         Backbone.history.navigate(route.toString(), options); | ||||||
|  |         return true; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Login | ||||||
|  |      */ | ||||||
|  |     showLogin: function () { | ||||||
|  |         window.location = '/login'; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Users | ||||||
|  |      * | ||||||
|  |      * @param {Number}  [offset] | ||||||
|  |      * @param {Number}  [limit] | ||||||
|  |      * @param {String}  [sort] | ||||||
|  |      */ | ||||||
|  |     showUsers: function (offset, limit, sort) { | ||||||
|  |         /* | ||||||
|  |         let controller = this; | ||||||
|  |         if (Cache.User.isAdmin()) { | ||||||
|  |             require(['./main', './users/main'], (App, View) => { | ||||||
|  |                 controller.navigate('/users'); | ||||||
|  |                 App.UI.showMainLoading(); | ||||||
|  |                 let view = new View({ | ||||||
|  |                     sort:   (typeof sort !== 'undefined' && sort ? sort : Cache.Session.Users.sort), | ||||||
|  |                     offset: (typeof offset !== 'undefined' ? offset : Cache.Session.Users.offset), | ||||||
|  |                     limit:  (typeof limit !== 'undefined' && limit ? limit : Cache.Session.Users.limit) | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |                 view.on('loaded', function () { | ||||||
|  |                     App.UI.hideMainLoading(); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |                 App.UI.showAppContent(view); | ||||||
|  |             }); | ||||||
|  |         } else { | ||||||
|  |             this.showRules(); | ||||||
|  |         } | ||||||
|  |         */ | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Error | ||||||
|  |      * | ||||||
|  |      * @param {Error}   err | ||||||
|  |      * @param {String}  nice_msg | ||||||
|  |      */ | ||||||
|  |     /* | ||||||
|  |     showError: function (err, nice_msg) { | ||||||
|  |         require(['./main', './error/main'], (App, View) => { | ||||||
|  |             App.UI.showAppContent(new View({ | ||||||
|  |                 err:      err, | ||||||
|  |                 nice_msg: nice_msg | ||||||
|  |             })); | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  |     */ | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Dashboard | ||||||
|  |      */ | ||||||
|  |     showDashboard: function () { | ||||||
|  |         let controller = this; | ||||||
|  |  | ||||||
|  |         require(['./main', './dashboard/main'], (App, View) => { | ||||||
|  |             controller.navigate('/'); | ||||||
|  |             App.UI.showAppContent(new View()); | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Dashboard | ||||||
|  |      */ | ||||||
|  |     showProfile: function () { | ||||||
|  |         let controller = this; | ||||||
|  |  | ||||||
|  |         require(['./main', './profile/main'], (App, View) => { | ||||||
|  |             controller.navigate('/profile'); | ||||||
|  |             App.UI.showAppContent(new View()); | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Logout | ||||||
|  |      */ | ||||||
|  |     logout: function () { | ||||||
|  |         Tokens.dropTopToken(); | ||||||
|  |         this.showLogin(); | ||||||
|  |     } | ||||||
|  | }; | ||||||
							
								
								
									
										1
									
								
								src/frontend/js/app/dashboard/main.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | Hi | ||||||
							
								
								
									
										10
									
								
								src/frontend/js/app/dashboard/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const Mn       = require('backbone.marionette'); | ||||||
|  | const template = require('./main.ejs'); | ||||||
|  |  | ||||||
|  | module.exports = Mn.View.extend({ | ||||||
|  |     template: template, | ||||||
|  |     id:       'dashboard' | ||||||
|  | }); | ||||||
|  |  | ||||||
							
								
								
									
										167
									
								
								src/frontend/js/app/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,167 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const _          = require('underscore'); | ||||||
|  | const Backbone   = require('backbone'); | ||||||
|  | const Mn         = require('../lib/marionette'); | ||||||
|  | const Cache      = require('./cache'); | ||||||
|  | const Controller = require('./controller'); | ||||||
|  | const Router     = require('./router'); | ||||||
|  | const Api        = require('./api'); | ||||||
|  | const Tokens     = require('./tokens'); | ||||||
|  | const UI         = require('./ui/main'); | ||||||
|  |  | ||||||
|  | const App = Mn.Application.extend({ | ||||||
|  |  | ||||||
|  |     region:     '#app', | ||||||
|  |     Cache:      Cache, | ||||||
|  |     Api:        Api, | ||||||
|  |     UI:         null, | ||||||
|  |     Controller: Controller, | ||||||
|  |     version:    null, | ||||||
|  |  | ||||||
|  |     onStart: function (app, options) { | ||||||
|  |         console.log('Welcome to Nginx Proxy Manager'); | ||||||
|  |  | ||||||
|  |         // Check if token is coming through | ||||||
|  |         if (this.getParam('token')) { | ||||||
|  |             Tokens.addToken(this.getParam('token')); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Check if we are still logged in by refreshing the token | ||||||
|  |         Api.status() | ||||||
|  |             .then(result => { | ||||||
|  |                 this.version = [result.version.major, result.version.minor, result.version.revision].join('.'); | ||||||
|  |             }) | ||||||
|  |             .then(Api.Tokens.refresh) | ||||||
|  |             .then(this.bootstrap) | ||||||
|  |             .then(() => { | ||||||
|  |                 console.info('You are logged in'); | ||||||
|  |                 this.bootstrapTimer(); | ||||||
|  |                 this.refreshTokenTimer(); | ||||||
|  |  | ||||||
|  |                 this.UI = new UI(); | ||||||
|  |                 this.UI.on('render', () => { | ||||||
|  |                     new Router(options); | ||||||
|  |                     Backbone.history.start({pushState: true}); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |                 this.getRegion().show(this.UI); | ||||||
|  |             }) | ||||||
|  |             .catch(err => { | ||||||
|  |                 console.warn('Not logged in:', err.message); | ||||||
|  |                 Controller.showLogin(); | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     History: { | ||||||
|  |         replace: function (data) { | ||||||
|  |             window.history.replaceState(_.extend(window.history.state || {}, data), document.title); | ||||||
|  |         }, | ||||||
|  |  | ||||||
|  |         get: function (attr) { | ||||||
|  |             return window.history.state ? window.history.state[attr] : undefined; | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     Error: function (code, message, debug) { | ||||||
|  |         let temp  = Error.call(this, message); | ||||||
|  |         temp.name = this.name = 'AppError'; | ||||||
|  |         this.stack   = temp.stack; | ||||||
|  |         this.message = temp.message; | ||||||
|  |         this.code    = code; | ||||||
|  |         this.debug   = debug; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     showError: function () { | ||||||
|  |         let ErrorView = Mn.View.extend({ | ||||||
|  |             tagName:  'section', | ||||||
|  |             id:       'error', | ||||||
|  |             template: _.template('Error loading stuff. Please reload the app.') | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         this.getRegion().show(new ErrorView()); | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     getParam: function (name) { | ||||||
|  |         name        = name.replace(/[\[\]]/g, '\\$&'); | ||||||
|  |         let url     = window.location.href; | ||||||
|  |         let regex   = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'); | ||||||
|  |         let results = regex.exec(url); | ||||||
|  |  | ||||||
|  |         if (!results) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!results[2]) { | ||||||
|  |             return ''; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return decodeURIComponent(results[2].replace(/\+/g, ' ')); | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Get user and other base info to start prime the cache and the application | ||||||
|  |      * | ||||||
|  |      * @returns {Promise} | ||||||
|  |      */ | ||||||
|  |     bootstrap: function () { | ||||||
|  |         return Api.Users.getById('me') | ||||||
|  |             .then(response => { | ||||||
|  |                 Cache.User.set(response); | ||||||
|  |                 Tokens.setCurrentName(response.nickname || response.name); | ||||||
|  |             }); | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Bootstraps the user from time to time | ||||||
|  |      */ | ||||||
|  |     bootstrapTimer: function () { | ||||||
|  |         setTimeout(() => { | ||||||
|  |             Api.status() | ||||||
|  |                 .then(result => { | ||||||
|  |                     let version = [result.version.major, result.version.minor, result.version.revision].join('.'); | ||||||
|  |                     if (version !== this.version) { | ||||||
|  |                         document.location.reload(); | ||||||
|  |                     } | ||||||
|  |                 }) | ||||||
|  |                 .then(this.bootstrap) | ||||||
|  |                 .then(() => { | ||||||
|  |                     this.bootstrapTimer(); | ||||||
|  |                 }) | ||||||
|  |                 .catch(err => { | ||||||
|  |                     if (err.message !== 'timeout' && err.code && err.code !== 400) { | ||||||
|  |                         console.log(err); | ||||||
|  |                         console.error(err.message); | ||||||
|  |                         console.info('Not logged in?'); | ||||||
|  |                         Controller.showLogin(); | ||||||
|  |                     } else { | ||||||
|  |                         this.bootstrapTimer(); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |         }, 30 * 1000); // 30 seconds | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     refreshTokenTimer: function () { | ||||||
|  |         setTimeout(() => { | ||||||
|  |             return Api.Tokens.refresh() | ||||||
|  |                 .then(this.bootstrap) | ||||||
|  |                 .then(() => { | ||||||
|  |                     this.refreshTokenTimer(); | ||||||
|  |                 }) | ||||||
|  |                 .catch(err => { | ||||||
|  |                     if (err.message !== 'timeout' && err.code && err.code !== 400) { | ||||||
|  |                         console.log(err); | ||||||
|  |                         console.error(err.message); | ||||||
|  |                         console.info('Not logged in?'); | ||||||
|  |                         Controller.showLogin(); | ||||||
|  |                     } else { | ||||||
|  |                         this.refreshTokenTimer(); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |         }, 10 * 60 * 1000); | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const app      = new App(); | ||||||
|  | module.exports = app; | ||||||
							
								
								
									
										33
									
								
								src/frontend/js/app/profile/main.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,33 @@ | |||||||
|  | <div class="row"> | ||||||
|  |  | ||||||
|  |     <div class="col-lg-4 col-md-6 col-xs-12"> | ||||||
|  |         <div class="card"> | ||||||
|  |             <div class="card-header"> | ||||||
|  |                 <h3 class="card-title">My Profile</h3> | ||||||
|  |             </div> | ||||||
|  |             <div class="card-body"> | ||||||
|  |                 <form> | ||||||
|  |                     <div class="row"> | ||||||
|  |                         <div class="col-auto"> | ||||||
|  |                             <span class="avatar avatar-xl" style="background-image: url(<%- avatar || '/images/default-avatar.jpg' %>)"></span> | ||||||
|  |                         </div> | ||||||
|  |                         <div class="col"> | ||||||
|  |                             <div class="form-group"> | ||||||
|  |                                 <label class="form-label">Name</label> | ||||||
|  |                                 <input name="name" class="form-control" value="<%- name %>"> | ||||||
|  |                             </div> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="form-group"> | ||||||
|  |                         <label class="form-label">Email-Address</label> | ||||||
|  |                         <input name="email" class="form-control" value="<%- email %>"> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="form-footer"> | ||||||
|  |                         <button class="btn btn-primary btn-block">Save</button> | ||||||
|  |                     </div> | ||||||
|  |                 </form> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  | </div> | ||||||
							
								
								
									
										21
									
								
								src/frontend/js/app/profile/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const Mn       = require('backbone.marionette'); | ||||||
|  | const Cache    = require('../cache'); | ||||||
|  | const template = require('./main.ejs'); | ||||||
|  |  | ||||||
|  | module.exports = Mn.View.extend({ | ||||||
|  |     template: template, | ||||||
|  |     id:       'profile', | ||||||
|  |  | ||||||
|  |     templateContext: { | ||||||
|  |         getUserField: function (field, default_val) { | ||||||
|  |             return Cache.User.get(field) || default_val; | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     initialize: function () { | ||||||
|  |         this.model = Cache.User; | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  |  | ||||||
							
								
								
									
										17
									
								
								src/frontend/js/app/router.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const Mn         = require('../lib/marionette'); | ||||||
|  | const Controller = require('./controller'); | ||||||
|  |  | ||||||
|  | module.exports = Mn.AppRouter.extend({ | ||||||
|  |     appRoutes: { | ||||||
|  |         users:      'showUsers', | ||||||
|  |         profile:    'showProfile', | ||||||
|  |         logout:     'logout', | ||||||
|  |         '*default': 'showDashboard' | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     initialize: function () { | ||||||
|  |         this.controller = Controller; | ||||||
|  |     } | ||||||
|  | }); | ||||||
							
								
								
									
										128
									
								
								src/frontend/js/app/tokens.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,128 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const STORAGE_NAME = 'nginx-proxy-manager-tokens'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @returns {Array} | ||||||
|  |  */ | ||||||
|  | const getStorageTokens = function () { | ||||||
|  |     let json = window.localStorage.getItem(STORAGE_NAME); | ||||||
|  |     if (json) { | ||||||
|  |         try { | ||||||
|  |             return JSON.parse(json); | ||||||
|  |         } catch (err) { | ||||||
|  |             return []; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return []; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @param  {Array}  tokens | ||||||
|  |  */ | ||||||
|  | const setStorageTokens = function (tokens) { | ||||||
|  |     window.localStorage.setItem(STORAGE_NAME, JSON.stringify(tokens)); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const Tokens = { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @returns {Integer} | ||||||
|  |      */ | ||||||
|  |     getTokenCount: () => { | ||||||
|  |         return getStorageTokens().length; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @returns {Object}    t,n | ||||||
|  |      */ | ||||||
|  |     getTopToken: () => { | ||||||
|  |         let tokens = getStorageTokens(); | ||||||
|  |         if (tokens && tokens.length) { | ||||||
|  |             return tokens[0]; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return null; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @returns {String} | ||||||
|  |      */ | ||||||
|  |     getNextTokenName: () => { | ||||||
|  |         let tokens = getStorageTokens(); | ||||||
|  |         if (tokens && tokens.length > 1 && typeof tokens[1] !== 'undefined' && typeof tokens[1].n !== 'undefined') { | ||||||
|  |             return tokens[1].n; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return null; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * | ||||||
|  |      * @param   {String}  token | ||||||
|  |      * @param   {String}  [name] | ||||||
|  |      * @returns {Integer} | ||||||
|  |      */ | ||||||
|  |     addToken: (token, name) => { | ||||||
|  |         // Get top token and if it's the same, ignore this call | ||||||
|  |         let top = Tokens.getTopToken(); | ||||||
|  |         if (!top || top.t !== token) { | ||||||
|  |             let tokens = getStorageTokens(); | ||||||
|  |             tokens.unshift({t: token, n: name || null}); | ||||||
|  |             setStorageTokens(tokens); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return Tokens.getTokenCount(); | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @param   {String}  token | ||||||
|  |      * @returns {Boolean} | ||||||
|  |      */ | ||||||
|  |     setCurrentToken: token => { | ||||||
|  |         let tokens = getStorageTokens(); | ||||||
|  |         if (tokens.length) { | ||||||
|  |             tokens[0].t = token; | ||||||
|  |             setStorageTokens(tokens); | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return false; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @param   {String}  name | ||||||
|  |      * @returns {Boolean} | ||||||
|  |      */ | ||||||
|  |     setCurrentName: name => { | ||||||
|  |         let tokens = getStorageTokens(); | ||||||
|  |         if (tokens.length) { | ||||||
|  |             tokens[0].n = name; | ||||||
|  |             setStorageTokens(tokens); | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return false; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @returns {Integer} | ||||||
|  |      */ | ||||||
|  |     dropTopToken: () => { | ||||||
|  |         let tokens = getStorageTokens(); | ||||||
|  |         tokens.shift(); | ||||||
|  |         setStorageTokens(tokens); | ||||||
|  |         return tokens.length; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * | ||||||
|  |      */ | ||||||
|  |     clearTokens: () => { | ||||||
|  |         window.localStorage.removeItem(STORAGE_NAME); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = Tokens; | ||||||
							
								
								
									
										14
									
								
								src/frontend/js/app/ui/footer/main.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | |||||||
|  | <div class="row align-items-center flex-row-reverse"> | ||||||
|  |     <div class="col-auto ml-auto"> | ||||||
|  |         <div class="row align-items-center"> | ||||||
|  |             <div class="col-auto"> | ||||||
|  |                 <ul class="list-inline list-inline-dots mb-0"> | ||||||
|  |                     <li class="list-inline-item"><a href="https://github.com/jc21/docker-registry-ui?utm_source=docker-registry-ui">Fork me on Github</a></li> | ||||||
|  |                 </ul> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |     <div class="col-12 col-lg-auto mt-3 mt-lg-0 text-center"> | ||||||
|  |         v<%- getVersion() %> © 2018 <a href="https://jc21.com?utm_source=docker-registry-ui" target="_blank">jc21.com</a>. Theme by <a href="https://github.com/tabler/tabler?utm_source=docker-registry-ui" target="_blank">Tabler</a> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
							
								
								
									
										16
									
								
								src/frontend/js/app/ui/footer/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const Mn       = require('backbone.marionette'); | ||||||
|  | const template = require('./main.ejs'); | ||||||
|  | const App      = require('../../main'); | ||||||
|  |  | ||||||
|  | module.exports = Mn.View.extend({ | ||||||
|  |     className: 'container', | ||||||
|  |     template:  template, | ||||||
|  |  | ||||||
|  |     templateContext: { | ||||||
|  |         getVersion: function () { | ||||||
|  |             return App.version; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | }); | ||||||
							
								
								
									
										28
									
								
								src/frontend/js/app/ui/header/main.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,28 @@ | |||||||
|  | <div class="container"> | ||||||
|  |     <div class="d-flex"> | ||||||
|  |         <a class="navbar-brand" href="/"> | ||||||
|  |             <img src="/images/favicons/favicon-32x32.png" border="0">   Docker Registry | ||||||
|  |         </a> | ||||||
|  |  | ||||||
|  |         <div class="d-flex order-lg-2 ml-auto"> | ||||||
|  |             <div class="dropdown"> | ||||||
|  |                 <a href="#" class="nav-link pr-0 leading-none" data-toggle="dropdown"> | ||||||
|  |                     <span class="avatar" style="background-image: url(<%- getUserField('avatar', '/images/default-avatar.jpg') %>)"></span> | ||||||
|  |                     <span class="ml-2 d-none d-lg-block"> | ||||||
|  |                       <span class="text-default"><%- getUserField('name', 'Unknown User') %></span> | ||||||
|  |                       <small class="text-muted d-block mt-1"><%- getRole() %></small> | ||||||
|  |                     </span> | ||||||
|  |                 </a> | ||||||
|  |                 <div class="dropdown-menu dropdown-menu-right dropdown-menu-arrow"> | ||||||
|  |                     <a class="dropdown-item profile" href="/profile"> | ||||||
|  |                         <i class="dropdown-icon fe fe-user"></i> Profile | ||||||
|  |                     </a> | ||||||
|  |                     <div class="dropdown-divider"></div> | ||||||
|  |                     <a class="dropdown-item logout" href="/logout"> | ||||||
|  |                         <i class="dropdown-icon fe fe-log-out"></i> <%- getLogoutText() %> | ||||||
|  |                     </a> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
							
								
								
									
										55
									
								
								src/frontend/js/app/ui/header/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,55 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const $          = require('jquery'); | ||||||
|  | const Mn         = require('backbone.marionette'); | ||||||
|  | const Cache      = require('../../cache'); | ||||||
|  | const Controller = require('../../controller'); | ||||||
|  | const Tokens     = require('../../tokens'); | ||||||
|  | const template   = require('./main.ejs'); | ||||||
|  |  | ||||||
|  | module.exports = Mn.View.extend({ | ||||||
|  |     id:        'header', | ||||||
|  |     className: 'header', | ||||||
|  |     template:  template, | ||||||
|  |  | ||||||
|  |     ui: { | ||||||
|  |         link: 'a' | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     events: { | ||||||
|  |         'click @ui.link': function (e) { | ||||||
|  |             e.preventDefault(); | ||||||
|  |             let href = $(e.currentTarget).attr('href'); | ||||||
|  |  | ||||||
|  |             switch (href) { | ||||||
|  |                 case '/': | ||||||
|  |                     Controller.showDashboard(); | ||||||
|  |                     break; | ||||||
|  |                 case '/profile': | ||||||
|  |                     Controller.showProfile(); | ||||||
|  |                     break; | ||||||
|  |                 case '/logout': | ||||||
|  |                     Controller.logout(); | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     templateContext: { | ||||||
|  |         getUserField: function (field, default_val) { | ||||||
|  |             return Cache.User.get(field) || default_val; | ||||||
|  |         }, | ||||||
|  |  | ||||||
|  |         getRole: function () { | ||||||
|  |             return Cache.User.isAdmin() ? 'Administrator' : 'Apache Helicopter'; | ||||||
|  |         }, | ||||||
|  |  | ||||||
|  |         getLogoutText: function () { | ||||||
|  |             if (Tokens.getTokenCount() > 1) { | ||||||
|  |                 return 'Sign back in as ' + Tokens.getNextTokenName(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return 'Sign out'; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | }); | ||||||
							
								
								
									
										18
									
								
								src/frontend/js/app/ui/main.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,18 @@ | |||||||
|  | <div class="page-main"> | ||||||
|  |  | ||||||
|  |     <div class="header" id="header"> | ||||||
|  |         <!-- Header View --> | ||||||
|  |     </div> | ||||||
|  |     <div id="menu"> | ||||||
|  |         <!-- Menu View --> | ||||||
|  |     </div> | ||||||
|  |     <div class="my-3 my-md-5"> | ||||||
|  |         <div id="app-content" class="container"> | ||||||
|  |             <!-- App View --> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | <footer class="footer"> | ||||||
|  |     <!-- Footer View --> | ||||||
|  | </footer> | ||||||
							
								
								
									
										44
									
								
								src/frontend/js/app/ui/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,44 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const Mn         = require('backbone.marionette'); | ||||||
|  | const template   = require('./main.ejs'); | ||||||
|  | const HeaderView = require('./header/main'); | ||||||
|  | const MenuView   = require('./menu/main'); | ||||||
|  | const FooterView = require('./footer/main'); | ||||||
|  | const Cache      = require('../cache'); | ||||||
|  |  | ||||||
|  | module.exports = Mn.View.extend({ | ||||||
|  |     className: 'page', | ||||||
|  |     template:  template, | ||||||
|  |  | ||||||
|  |     regions: { | ||||||
|  |         header_region:      { | ||||||
|  |             el:             '#header', | ||||||
|  |             replaceElement: true | ||||||
|  |         }, | ||||||
|  |         menu_region:        { | ||||||
|  |             el:             '#menu', | ||||||
|  |             replaceElement: true | ||||||
|  |         }, | ||||||
|  |         footer_region:      '.footer', | ||||||
|  |         app_content_region: '#app-content' | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     showAppContent: function (view) { | ||||||
|  |         this.showChildView('app_content_region', view); | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     onRender: function () { | ||||||
|  |         this.showChildView('header_region', new HeaderView({ | ||||||
|  |             model: Cache.User | ||||||
|  |         })); | ||||||
|  |  | ||||||
|  |         this.showChildView('menu_region', new MenuView()); | ||||||
|  |         this.showChildView('footer_region', new FooterView()); | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     reset: function () { | ||||||
|  |         this.getRegion('header_region').reset(); | ||||||
|  |         this.getRegion('footer_region').reset(); | ||||||
|  |     } | ||||||
|  | }); | ||||||
							
								
								
									
										56
									
								
								src/frontend/js/app/ui/menu/main.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,56 @@ | |||||||
|  | <div class="container"> | ||||||
|  |     <div class="row align-items-center"> | ||||||
|  |         <div class="col-lg order-lg-first"> | ||||||
|  |             <ul class="nav nav-tabs border-0 flex-column flex-lg-row"> | ||||||
|  |                 <li class="nav-item"> | ||||||
|  |                     <a href="../index.html" class="nav-link"><i class="fe fe-home"></i> Home</a> | ||||||
|  |                 </li> | ||||||
|  |                 <li class="nav-item"> | ||||||
|  |                     <a href="javascript:void(0)" class="nav-link" data-toggle="dropdown"><i class="fe fe-box"></i> Interface</a> | ||||||
|  |                     <div class="dropdown-menu dropdown-menu-arrow"> | ||||||
|  |                         <a href="../cards.html" class="dropdown-item ">Cards design</a> | ||||||
|  |                         <a href="../charts.html" class="dropdown-item ">Charts</a> | ||||||
|  |                         <a href="../pricing-cards.html" class="dropdown-item ">Pricing cards</a> | ||||||
|  |                     </div> | ||||||
|  |                 </li> | ||||||
|  |                 <li class="nav-item dropdown"> | ||||||
|  |                     <a href="javascript:void(0)" class="nav-link" data-toggle="dropdown"><i class="fe fe-calendar"></i> Components</a> | ||||||
|  |                     <div class="dropdown-menu dropdown-menu-arrow"> | ||||||
|  |                         <a href="../maps.html" class="dropdown-item ">Maps</a> | ||||||
|  |                         <a href="../icons.html" class="dropdown-item ">Icons</a> | ||||||
|  |                         <a href="../store.html" class="dropdown-item ">Store</a> | ||||||
|  |                         <a href="../blog.html" class="dropdown-item ">Blog</a> | ||||||
|  |                         <a href="../carousel.html" class="dropdown-item ">Carousel</a> | ||||||
|  |                     </div> | ||||||
|  |                 </li> | ||||||
|  |                 <li class="nav-item dropdown"> | ||||||
|  |                     <a href="javascript:void(0)" class="nav-link" data-toggle="dropdown"><i class="fe fe-file"></i> Pages</a> | ||||||
|  |                     <div class="dropdown-menu dropdown-menu-arrow"> | ||||||
|  |                         <a href="../profile.html" class="dropdown-item ">Profile</a> | ||||||
|  |                         <a href="../login.html" class="dropdown-item ">Login</a> | ||||||
|  |                         <a href="../register.html" class="dropdown-item ">Register</a> | ||||||
|  |                         <a href="../forgot-password.html" class="dropdown-item ">Forgot password</a> | ||||||
|  |                         <a href="../400.html" class="dropdown-item ">400 error</a> | ||||||
|  |                         <a href="../401.html" class="dropdown-item ">401 error</a> | ||||||
|  |                         <a href="../403.html" class="dropdown-item ">403 error</a> | ||||||
|  |                         <a href="../404.html" class="dropdown-item ">404 error</a> | ||||||
|  |                         <a href="../500.html" class="dropdown-item ">500 error</a> | ||||||
|  |                         <a href="../503.html" class="dropdown-item ">503 error</a> | ||||||
|  |                         <a href="../email.html" class="dropdown-item ">Email</a> | ||||||
|  |                         <a href="../empty.html" class="dropdown-item ">Empty page</a> | ||||||
|  |                         <a href="../rtl.html" class="dropdown-item ">RTL mode</a> | ||||||
|  |                     </div> | ||||||
|  |                 </li> | ||||||
|  |                 <li class="nav-item dropdown"> | ||||||
|  |                     <a href="../form-elements.html" class="nav-link"><i class="fe fe-check-square"></i> Forms</a> | ||||||
|  |                 </li> | ||||||
|  |                 <li class="nav-item"> | ||||||
|  |                     <a href="../gallery.html" class="nav-link"><i class="fe fe-image"></i> Gallery</a> | ||||||
|  |                 </li> | ||||||
|  |                 <li class="nav-item"> | ||||||
|  |                     <a href="../docs/index.html" class="nav-link"><i class="fe fe-file-text"></i> Documentation</a> | ||||||
|  |                 </li> | ||||||
|  |             </ul> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
							
								
								
									
										10
									
								
								src/frontend/js/app/ui/menu/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const Mn       = require('backbone.marionette'); | ||||||
|  | const template = require('./main.ejs'); | ||||||
|  |  | ||||||
|  | module.exports = Mn.View.extend({ | ||||||
|  |     id:        'menu', | ||||||
|  |     className: 'header collapse d-lg-flex p-0', | ||||||
|  |     template:  template | ||||||
|  | }); | ||||||
							
								
								
									
										112
									
								
								src/frontend/js/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,112 @@ | |||||||
|  | // This has to exist here so that Webpack picks it up | ||||||
|  | import '../scss/styles.scss'; | ||||||
|  |  | ||||||
|  | window.tabler = { | ||||||
|  |     colors: { | ||||||
|  |         'blue':               '#467fcf', | ||||||
|  |         'blue-darkest':       '#0e1929', | ||||||
|  |         'blue-darker':        '#1c3353', | ||||||
|  |         'blue-dark':          '#3866a6', | ||||||
|  |         'blue-light':         '#7ea5dd', | ||||||
|  |         'blue-lighter':       '#c8d9f1', | ||||||
|  |         'blue-lightest':      '#edf2fa', | ||||||
|  |         'azure':              '#45aaf2', | ||||||
|  |         'azure-darkest':      '#0e2230', | ||||||
|  |         'azure-darker':       '#1c4461', | ||||||
|  |         'azure-dark':         '#3788c2', | ||||||
|  |         'azure-light':        '#7dc4f6', | ||||||
|  |         'azure-lighter':      '#c7e6fb', | ||||||
|  |         'azure-lightest':     '#ecf7fe', | ||||||
|  |         'indigo':             '#6574cd', | ||||||
|  |         'indigo-darkest':     '#141729', | ||||||
|  |         'indigo-darker':      '#282e52', | ||||||
|  |         'indigo-dark':        '#515da4', | ||||||
|  |         'indigo-light':       '#939edc', | ||||||
|  |         'indigo-lighter':     '#d1d5f0', | ||||||
|  |         'indigo-lightest':    '#f0f1fa', | ||||||
|  |         'purple':             '#a55eea', | ||||||
|  |         'purple-darkest':     '#21132f', | ||||||
|  |         'purple-darker':      '#42265e', | ||||||
|  |         'purple-dark':        '#844bbb', | ||||||
|  |         'purple-light':       '#c08ef0', | ||||||
|  |         'purple-lighter':     '#e4cff9', | ||||||
|  |         'purple-lightest':    '#f6effd', | ||||||
|  |         'pink':               '#f66d9b', | ||||||
|  |         'pink-darkest':       '#31161f', | ||||||
|  |         'pink-darker':        '#622c3e', | ||||||
|  |         'pink-dark':          '#c5577c', | ||||||
|  |         'pink-light':         '#f999b9', | ||||||
|  |         'pink-lighter':       '#fcd3e1', | ||||||
|  |         'pink-lightest':      '#fef0f5', | ||||||
|  |         'red':                '#e74c3c', | ||||||
|  |         'red-darkest':        '#2e0f0c', | ||||||
|  |         'red-darker':         '#5c1e18', | ||||||
|  |         'red-dark':           '#b93d30', | ||||||
|  |         'red-light':          '#ee8277', | ||||||
|  |         'red-lighter':        '#f8c9c5', | ||||||
|  |         'red-lightest':       '#fdedec', | ||||||
|  |         'orange':             '#fd9644', | ||||||
|  |         'orange-darkest':     '#331e0e', | ||||||
|  |         'orange-darker':      '#653c1b', | ||||||
|  |         'orange-dark':        '#ca7836', | ||||||
|  |         'orange-light':       '#feb67c', | ||||||
|  |         'orange-lighter':     '#fee0c7', | ||||||
|  |         'orange-lightest':    '#fff5ec', | ||||||
|  |         'yellow':             '#f1c40f', | ||||||
|  |         'yellow-darkest':     '#302703', | ||||||
|  |         'yellow-darker':      '#604e06', | ||||||
|  |         'yellow-dark':        '#c19d0c', | ||||||
|  |         'yellow-light':       '#f5d657', | ||||||
|  |         'yellow-lighter':     '#fbedb7', | ||||||
|  |         'yellow-lightest':    '#fef9e7', | ||||||
|  |         'lime':               '#7bd235', | ||||||
|  |         'lime-darkest':       '#192a0b', | ||||||
|  |         'lime-darker':        '#315415', | ||||||
|  |         'lime-dark':          '#62a82a', | ||||||
|  |         'lime-light':         '#a3e072', | ||||||
|  |         'lime-lighter':       '#d7f2c2', | ||||||
|  |         'lime-lightest':      '#f2fbeb', | ||||||
|  |         'green':              '#5eba00', | ||||||
|  |         'green-darkest':      '#132500', | ||||||
|  |         'green-darker':       '#264a00', | ||||||
|  |         'green-dark':         '#4b9500', | ||||||
|  |         'green-light':        '#8ecf4d', | ||||||
|  |         'green-lighter':      '#cfeab3', | ||||||
|  |         'green-lightest':     '#eff8e6', | ||||||
|  |         'teal':               '#2bcbba', | ||||||
|  |         'teal-darkest':       '#092925', | ||||||
|  |         'teal-darker':        '#11514a', | ||||||
|  |         'teal-dark':          '#22a295', | ||||||
|  |         'teal-light':         '#6bdbcf', | ||||||
|  |         'teal-lighter':       '#bfefea', | ||||||
|  |         'teal-lightest':      '#eafaf8', | ||||||
|  |         'cyan':               '#17a2b8', | ||||||
|  |         'cyan-darkest':       '#052025', | ||||||
|  |         'cyan-darker':        '#09414a', | ||||||
|  |         'cyan-dark':          '#128293', | ||||||
|  |         'cyan-light':         '#5dbecd', | ||||||
|  |         'cyan-lighter':       '#b9e3ea', | ||||||
|  |         'cyan-lightest':      '#e8f6f8', | ||||||
|  |         'gray':               '#868e96', | ||||||
|  |         'gray-darkest':       '#1b1c1e', | ||||||
|  |         'gray-darker':        '#36393c', | ||||||
|  |         'gray-light':         '#aab0b6', | ||||||
|  |         'gray-lighter':       '#dbdde0', | ||||||
|  |         'gray-lightest':      '#f3f4f5', | ||||||
|  |         'gray-dark':          '#343a40', | ||||||
|  |         'gray-dark-darkest':  '#0a0c0d', | ||||||
|  |         'gray-dark-darker':   '#15171a', | ||||||
|  |         'gray-dark-dark':     '#2a2e33', | ||||||
|  |         'gray-dark-light':    '#717579', | ||||||
|  |         'gray-dark-lighter':  '#c2c4c6', | ||||||
|  |         'gray-dark-lightest': '#ebebec' | ||||||
|  |     } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | require('tabler-core'); | ||||||
|  |  | ||||||
|  | const App = require('./app/main'); | ||||||
|  |  | ||||||
|  | $(document).ready(() => { | ||||||
|  |     App.start(); | ||||||
|  | }); | ||||||
							
								
								
									
										36
									
								
								src/frontend/js/lib/helpers.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,36 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const numeral = require('numeral'); | ||||||
|  | const moment  = require('moment'); | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @param {Integer} number | ||||||
|  |      * @returns {String} | ||||||
|  |      */ | ||||||
|  |     niceNumber: function (number) { | ||||||
|  |         return numeral(number).format('0,0'); | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @param   {String|Integer} date | ||||||
|  |      * @returns {String} | ||||||
|  |      */ | ||||||
|  |     shortTime: function (date) { | ||||||
|  |         let shorttime = ''; | ||||||
|  |  | ||||||
|  |         if (typeof date === 'number') { | ||||||
|  |             shorttime = moment.unix(date).format('H:mm A'); | ||||||
|  |         } else { | ||||||
|  |             shorttime = moment(date).format('H:mm A'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return shorttime; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     replaceSlackLinks: function (content) { | ||||||
|  |         return content.replace(/<(http[^|>]+)\|([^>]+)>/gi, '<a href="$1" target="_blank">$2</a>'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | }; | ||||||
							
								
								
									
										117
									
								
								src/frontend/js/lib/marionette.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,117 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const _       = require('underscore'); | ||||||
|  | const Mn      = require('backbone.marionette'); | ||||||
|  | const moment  = require('moment'); | ||||||
|  | const numeral = require('numeral'); | ||||||
|  |  | ||||||
|  | let render = Mn.Renderer.render; | ||||||
|  |  | ||||||
|  | Mn.Renderer.render = function (template, data, view) { | ||||||
|  |  | ||||||
|  |     data = _.clone(data); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @param   {Integer} number | ||||||
|  |      * @returns {String} | ||||||
|  |      */ | ||||||
|  |     data.niceNumber = function (number) { | ||||||
|  |         return numeral(number).format('0,0'); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @param   {Integer} seconds | ||||||
|  |      * @returns {String} | ||||||
|  |      */ | ||||||
|  |     data.secondsToTime = function (seconds) { | ||||||
|  |         let sec_num = parseInt(seconds, 10); | ||||||
|  |         let minutes = Math.floor(sec_num / 60); | ||||||
|  |         let sec     = sec_num - (minutes * 60); | ||||||
|  |  | ||||||
|  |         if (sec < 10) { | ||||||
|  |             sec = '0' + sec; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return minutes + ':' + sec; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @param   {String} date | ||||||
|  |      * @returns {String} | ||||||
|  |      */ | ||||||
|  |     data.shortDate = function (date) { | ||||||
|  |         let shortdate = ''; | ||||||
|  |  | ||||||
|  |         if (typeof date === 'number') { | ||||||
|  |             shortdate = moment.unix(date).format('YYYY-MM-DD'); | ||||||
|  |         } else { | ||||||
|  |             shortdate = moment(date).format('YYYY-MM-DD'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return moment().format('YYYY-MM-DD') === shortdate ? 'Today' : shortdate; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @param   {String} date | ||||||
|  |      * @returns {String} | ||||||
|  |      */ | ||||||
|  |     data.shortTime = function (date) { | ||||||
|  |         let shorttime = ''; | ||||||
|  |  | ||||||
|  |         if (typeof date === 'number') { | ||||||
|  |             shorttime = moment.unix(date).format('H:mm A'); | ||||||
|  |         } else { | ||||||
|  |             shorttime = moment(date).format('H:mm A'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return shorttime; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @param   {String} string | ||||||
|  |      * @returns {String} | ||||||
|  |      */ | ||||||
|  |     data.escape = function (string) { | ||||||
|  |         let entityMap = { | ||||||
|  |             '&':  '&', | ||||||
|  |             '<':  '<', | ||||||
|  |             '>':  '>', | ||||||
|  |             '"':  '"', | ||||||
|  |             '\'': ''', | ||||||
|  |             '/':  '/' | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         return String(string).replace(/[&<>"'\/]/g, function (s) { | ||||||
|  |             return entityMap[s]; | ||||||
|  |         }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @param   {String} string | ||||||
|  |      * @param   {Integer} length | ||||||
|  |      * @returns {String} | ||||||
|  |      */ | ||||||
|  |     data.trim = function (string, length) { | ||||||
|  |         if (string.length > length) { | ||||||
|  |             let trimmedString = string.substr(0, length); | ||||||
|  |             return trimmedString.substr(0, Math.min(trimmedString.length, trimmedString.lastIndexOf(' '))) + '...'; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return string; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @param   {String} name | ||||||
|  |      * @returns {String} | ||||||
|  |      */ | ||||||
|  |     data.niceVarName = function (name) { | ||||||
|  |         return name.replace('_', ' ') | ||||||
|  |             .replace(/^(.)|\s+(.)/g, function ($1) { | ||||||
|  |                 return $1.toUpperCase(); | ||||||
|  |             }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     return render.call(this, template, data, view); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = Mn; | ||||||
							
								
								
									
										5
									
								
								src/frontend/js/login.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | |||||||
|  | const App = require('./login/main'); | ||||||
|  |  | ||||||
|  | $(document).ready(() => { | ||||||
|  |     App.start(); | ||||||
|  | }); | ||||||
							
								
								
									
										17
									
								
								src/frontend/js/login/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const Mn        = require('backbone.marionette'); | ||||||
|  | const LoginView = require('./ui/login'); | ||||||
|  |  | ||||||
|  | const App = Mn.Application.extend({ | ||||||
|  |  | ||||||
|  |     region: '#login', | ||||||
|  |     UI:     null, | ||||||
|  |  | ||||||
|  |     onStart: function (/*app, options*/) { | ||||||
|  |         this.getRegion().show(new LoginView()); | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const app      = new App(); | ||||||
|  | module.exports = app; | ||||||
							
								
								
									
										28
									
								
								src/frontend/js/login/ui/login.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,28 @@ | |||||||
|  | <div class="container"> | ||||||
|  |     <div class="row"> | ||||||
|  |         <div class="col col-login mx-auto"> | ||||||
|  |             <form class="card" action="" method="post"> | ||||||
|  |                 <div class="card-body p-6"> | ||||||
|  |                     <div class="card-title">Login to your account</div> | ||||||
|  |                     <div class="form-group"> | ||||||
|  |                         <label class="form-label">Email address</label> | ||||||
|  |                         <input name="identity" type="email" class="form-control" placeholder="Enter email" required> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="form-group"> | ||||||
|  |                         <label class="form-label"> | ||||||
|  |                             Password | ||||||
|  |                         </label> | ||||||
|  |                         <input name="secret" type="password" class="form-control" placeholder="Password" required> | ||||||
|  |                         <div class="invalid-feedback secret-error"></div> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="form-footer"> | ||||||
|  |                         <button type="submit" class="btn btn-teal btn-block">Sign in</button> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </form> | ||||||
|  |             <div class="text-center text-muted"> | ||||||
|  |                 Nginx Proxy Manager v<%- getVersion() %> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
							
								
								
									
										42
									
								
								src/frontend/js/login/ui/login.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,42 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const $        = require('jquery'); | ||||||
|  | const Mn       = require('backbone.marionette'); | ||||||
|  | const template = require('./login.ejs'); | ||||||
|  | const Api      = require('../../app/api'); | ||||||
|  |  | ||||||
|  | module.exports = Mn.View.extend({ | ||||||
|  |     template:  template, | ||||||
|  |     className: 'page-single', | ||||||
|  |  | ||||||
|  |     ui: { | ||||||
|  |         form:     'form', | ||||||
|  |         identity: 'input[name="identity"]', | ||||||
|  |         secret:   'input[name="secret"]', | ||||||
|  |         error:    '.secret-error', | ||||||
|  |         button:   'button' | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     events: { | ||||||
|  |         'submit @ui.form': function (e) { | ||||||
|  |             e.preventDefault(); | ||||||
|  |             this.ui.button.addClass('btn-loading').prop('disabled', true); | ||||||
|  |             this.ui.error.hide(); | ||||||
|  |  | ||||||
|  |             Api.Tokens.login(this.ui.identity.val(), this.ui.secret.val(), true) | ||||||
|  |                 .then(() => { | ||||||
|  |                     window.location = '/'; | ||||||
|  |                 }) | ||||||
|  |                 .catch(err => { | ||||||
|  |                     this.ui.error.text(err.message).show(); | ||||||
|  |                     this.ui.button.removeClass('btn-loading').prop('disabled', false); | ||||||
|  |                 }); | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     templateContext: { | ||||||
|  |         getVersion: function () { | ||||||
|  |             return $('#login').data('version'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | }); | ||||||
							
								
								
									
										29
									
								
								src/frontend/js/models/user.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,29 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const _        = require('underscore'); | ||||||
|  | const Backbone = require('backbone'); | ||||||
|  |  | ||||||
|  | const model = Backbone.Model.extend({ | ||||||
|  |     idAttribute: 'id', | ||||||
|  |  | ||||||
|  |     defaults: function () { | ||||||
|  |         return { | ||||||
|  |             name:        '', | ||||||
|  |             nickname:    '', | ||||||
|  |             email:       '', | ||||||
|  |             is_disabled: false, | ||||||
|  |             roles:       [] | ||||||
|  |         }; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     isAdmin: function () { | ||||||
|  |         return _.indexOf(this.get('roles'), 'admin') !== -1; | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |     Model:      model, | ||||||
|  |     Collection: Backbone.Collection.extend({ | ||||||
|  |         model: model | ||||||
|  |     }) | ||||||
|  | }; | ||||||
							
								
								
									
										13
									
								
								src/frontend/scss/styles.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | |||||||
|  | @import "~tabler-ui/dist/assets/css/dashboard"; | ||||||
|  |  | ||||||
|  | /* Before any JS content is loaded */ | ||||||
|  | #app > .loader, #login > .loader, .container > .loader { | ||||||
|  |     position: absolute; | ||||||
|  |     left: 49%; | ||||||
|  |     top: 40%; | ||||||
|  |     display: block; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .no-js-warning { | ||||||
|  |     margin-top: 100px; | ||||||
|  | } | ||||||