diff --git a/src/frontend/app-images/default-avatar.jpg b/src/frontend/app-images/default-avatar.jpg new file mode 100644 index 0000000..1a0e507 Binary files /dev/null and b/src/frontend/app-images/default-avatar.jpg differ diff --git a/src/frontend/app-images/favicons/android-chrome-192x192.png b/src/frontend/app-images/favicons/android-chrome-192x192.png new file mode 100644 index 0000000..7bf92cc Binary files /dev/null and b/src/frontend/app-images/favicons/android-chrome-192x192.png differ diff --git a/src/frontend/app-images/favicons/android-chrome-512x512.png b/src/frontend/app-images/favicons/android-chrome-512x512.png new file mode 100644 index 0000000..499abf5 Binary files /dev/null and b/src/frontend/app-images/favicons/android-chrome-512x512.png differ diff --git a/src/frontend/app-images/favicons/apple-touch-icon.png b/src/frontend/app-images/favicons/apple-touch-icon.png new file mode 100644 index 0000000..31e8c9d Binary files /dev/null and b/src/frontend/app-images/favicons/apple-touch-icon.png differ diff --git a/src/frontend/app-images/favicons/browserconfig.xml b/src/frontend/app-images/favicons/browserconfig.xml new file mode 100644 index 0000000..e781b88 --- /dev/null +++ b/src/frontend/app-images/favicons/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #f0ad00 + + + diff --git a/src/frontend/app-images/favicons/favicon-16x16.png b/src/frontend/app-images/favicons/favicon-16x16.png new file mode 100644 index 0000000..0072a79 Binary files /dev/null and b/src/frontend/app-images/favicons/favicon-16x16.png differ diff --git a/src/frontend/app-images/favicons/favicon-32x32.png b/src/frontend/app-images/favicons/favicon-32x32.png new file mode 100644 index 0000000..f8762e7 Binary files /dev/null and b/src/frontend/app-images/favicons/favicon-32x32.png differ diff --git a/src/frontend/app-images/favicons/favicon.ico b/src/frontend/app-images/favicons/favicon.ico new file mode 100644 index 0000000..6090f0f Binary files /dev/null and b/src/frontend/app-images/favicons/favicon.ico differ diff --git a/src/frontend/app-images/favicons/manifest.json b/src/frontend/app-images/favicons/manifest.json new file mode 100644 index 0000000..c0a1e16 --- /dev/null +++ b/src/frontend/app-images/favicons/manifest.json @@ -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" +} \ No newline at end of file diff --git a/src/frontend/app-images/favicons/mstile-150x150.png b/src/frontend/app-images/favicons/mstile-150x150.png new file mode 100644 index 0000000..14687cc Binary files /dev/null and b/src/frontend/app-images/favicons/mstile-150x150.png differ diff --git a/src/frontend/app-images/favicons/safari-pinned-tab.svg b/src/frontend/app-images/favicons/safari-pinned-tab.svg new file mode 100644 index 0000000..8333c04 --- /dev/null +++ b/src/frontend/app-images/favicons/safari-pinned-tab.svg @@ -0,0 +1,32 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + + + diff --git a/src/frontend/fonts b/src/frontend/fonts new file mode 120000 index 0000000..84b6a8e --- /dev/null +++ b/src/frontend/fonts @@ -0,0 +1 @@ +../../node_modules/tabler-ui/dist/assets/fonts \ No newline at end of file diff --git a/src/frontend/images b/src/frontend/images new file mode 120000 index 0000000..6f1cb6a --- /dev/null +++ b/src/frontend/images @@ -0,0 +1 @@ +../../node_modules/tabler-ui/dist/assets/images \ No newline at end of file diff --git a/src/frontend/js/app/api.js b/src/frontend/js/app/api.js new file mode 100644 index 0000000..d6c4764 --- /dev/null +++ b/src/frontend/js/app/api.js @@ -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'); + } + } +}; diff --git a/src/frontend/js/app/cache.js b/src/frontend/js/app/cache.js new file mode 100644 index 0000000..c34d674 --- /dev/null +++ b/src/frontend/js/app/cache.js @@ -0,0 +1,10 @@ +'use strict'; + +const UserModel = require('../models/user'); + +let cache = { + User: new UserModel.Model() +}; + +module.exports = cache; + diff --git a/src/frontend/js/app/controller.js b/src/frontend/js/app/controller.js new file mode 100644 index 0000000..e217d1d --- /dev/null +++ b/src/frontend/js/app/controller.js @@ -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(); + } +}; diff --git a/src/frontend/js/app/dashboard/main.ejs b/src/frontend/js/app/dashboard/main.ejs new file mode 100644 index 0000000..40816a2 --- /dev/null +++ b/src/frontend/js/app/dashboard/main.ejs @@ -0,0 +1 @@ +Hi \ No newline at end of file diff --git a/src/frontend/js/app/dashboard/main.js b/src/frontend/js/app/dashboard/main.js new file mode 100644 index 0000000..6694281 --- /dev/null +++ b/src/frontend/js/app/dashboard/main.js @@ -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' +}); + diff --git a/src/frontend/js/app/main.js b/src/frontend/js/app/main.js new file mode 100644 index 0000000..4ef25c6 --- /dev/null +++ b/src/frontend/js/app/main.js @@ -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; diff --git a/src/frontend/js/app/profile/main.ejs b/src/frontend/js/app/profile/main.ejs new file mode 100644 index 0000000..f1e5d5c --- /dev/null +++ b/src/frontend/js/app/profile/main.ejs @@ -0,0 +1,33 @@ +
+ +
+
+
+

My Profile

+
+
+
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/src/frontend/js/app/profile/main.js b/src/frontend/js/app/profile/main.js new file mode 100644 index 0000000..5f4ea1a --- /dev/null +++ b/src/frontend/js/app/profile/main.js @@ -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; + } +}); + diff --git a/src/frontend/js/app/router.js b/src/frontend/js/app/router.js new file mode 100644 index 0000000..91825e7 --- /dev/null +++ b/src/frontend/js/app/router.js @@ -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; + } +}); diff --git a/src/frontend/js/app/tokens.js b/src/frontend/js/app/tokens.js new file mode 100644 index 0000000..fb85e9f --- /dev/null +++ b/src/frontend/js/app/tokens.js @@ -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; diff --git a/src/frontend/js/app/ui/footer/main.ejs b/src/frontend/js/app/ui/footer/main.ejs new file mode 100644 index 0000000..eeea09e --- /dev/null +++ b/src/frontend/js/app/ui/footer/main.ejs @@ -0,0 +1,14 @@ +
+
+
+ +
+
+
+ v<%- getVersion() %> © 2018 jc21.com. Theme by Tabler +
+
diff --git a/src/frontend/js/app/ui/footer/main.js b/src/frontend/js/app/ui/footer/main.js new file mode 100644 index 0000000..531ad81 --- /dev/null +++ b/src/frontend/js/app/ui/footer/main.js @@ -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; + } + } +}); diff --git a/src/frontend/js/app/ui/header/main.ejs b/src/frontend/js/app/ui/header/main.ejs new file mode 100644 index 0000000..eba23e3 --- /dev/null +++ b/src/frontend/js/app/ui/header/main.ejs @@ -0,0 +1,28 @@ +
+
+ +   Docker Registry + + + +
+
diff --git a/src/frontend/js/app/ui/header/main.js b/src/frontend/js/app/ui/header/main.js new file mode 100644 index 0000000..74f492f --- /dev/null +++ b/src/frontend/js/app/ui/header/main.js @@ -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'; + } + } +}); diff --git a/src/frontend/js/app/ui/main.ejs b/src/frontend/js/app/ui/main.ejs new file mode 100644 index 0000000..5d8b656 --- /dev/null +++ b/src/frontend/js/app/ui/main.ejs @@ -0,0 +1,18 @@ +
+ + + +
+
+ +
+
+
+ + \ No newline at end of file diff --git a/src/frontend/js/app/ui/main.js b/src/frontend/js/app/ui/main.js new file mode 100644 index 0000000..b013cd4 --- /dev/null +++ b/src/frontend/js/app/ui/main.js @@ -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(); + } +}); diff --git a/src/frontend/js/app/ui/menu/main.ejs b/src/frontend/js/app/ui/menu/main.ejs new file mode 100644 index 0000000..d4b975c --- /dev/null +++ b/src/frontend/js/app/ui/menu/main.ejs @@ -0,0 +1,56 @@ +
+
+ +
+
\ No newline at end of file diff --git a/src/frontend/js/app/ui/menu/main.js b/src/frontend/js/app/ui/menu/main.js new file mode 100644 index 0000000..b9fdf17 --- /dev/null +++ b/src/frontend/js/app/ui/menu/main.js @@ -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 +}); diff --git a/src/frontend/js/index.js b/src/frontend/js/index.js new file mode 100644 index 0000000..bfaa017 --- /dev/null +++ b/src/frontend/js/index.js @@ -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(); +}); diff --git a/src/frontend/js/lib/helpers.js b/src/frontend/js/lib/helpers.js new file mode 100644 index 0000000..984b513 --- /dev/null +++ b/src/frontend/js/lib/helpers.js @@ -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, '$2'); + } + +}; diff --git a/src/frontend/js/lib/marionette.js b/src/frontend/js/lib/marionette.js new file mode 100644 index 0000000..810defe --- /dev/null +++ b/src/frontend/js/lib/marionette.js @@ -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; diff --git a/src/frontend/js/login.js b/src/frontend/js/login.js new file mode 100644 index 0000000..0094e2a --- /dev/null +++ b/src/frontend/js/login.js @@ -0,0 +1,5 @@ +const App = require('./login/main'); + +$(document).ready(() => { + App.start(); +}); diff --git a/src/frontend/js/login/main.js b/src/frontend/js/login/main.js new file mode 100644 index 0000000..80d2866 --- /dev/null +++ b/src/frontend/js/login/main.js @@ -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; diff --git a/src/frontend/js/login/ui/login.ejs b/src/frontend/js/login/ui/login.ejs new file mode 100644 index 0000000..991d42f --- /dev/null +++ b/src/frontend/js/login/ui/login.ejs @@ -0,0 +1,28 @@ +
+
+ +
+
diff --git a/src/frontend/js/login/ui/login.js b/src/frontend/js/login/ui/login.js new file mode 100644 index 0000000..07ee8eb --- /dev/null +++ b/src/frontend/js/login/ui/login.js @@ -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'); + } + } +}); diff --git a/src/frontend/js/models/user.js b/src/frontend/js/models/user.js new file mode 100644 index 0000000..41250d3 --- /dev/null +++ b/src/frontend/js/models/user.js @@ -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 + }) +}; diff --git a/src/frontend/scss/styles.scss b/src/frontend/scss/styles.scss new file mode 100644 index 0000000..ac3bf72 --- /dev/null +++ b/src/frontend/scss/styles.scss @@ -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; +} \ No newline at end of file