This commit is contained in:
Jamie Curnow 2018-06-20 08:48:14 +10:00
parent 80d78cbf25
commit b38b988da4
40 changed files with 1419 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View 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
View File

@ -0,0 +1 @@
../../node_modules/tabler-ui/dist/assets/fonts

1
src/frontend/images Symbolic link
View File

@ -0,0 +1 @@
../../node_modules/tabler-ui/dist/assets/images

224
src/frontend/js/app/api.js Normal file
View 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');
}
}
};

View File

@ -0,0 +1,10 @@
'use strict';
const UserModel = require('../models/user');
let cache = {
User: new UserModel.Model()
};
module.exports = cache;

View 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();
}
};

View File

@ -0,0 +1 @@
Hi

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

View 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>

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

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

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

View 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() %> &copy; 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>

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

View 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"> &nbsp; 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>

View 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';
}
}
});

View 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>

View 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();
}
});

View 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>

View 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
View 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();
});

View 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>');
}
};

View 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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
'\'': '&#39;',
'/': '&#x2F;'
};
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
View File

@ -0,0 +1,5 @@
const App = require('./login/main');
$(document).ready(() => {
App.start();
});

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

View 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>

View 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');
}
}
});

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

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