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; | ||||
| } | ||||