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