client: fetch configurations from server at runtime

Permissions, regex filters, app title, email info,
and safety now fetched using server's Info API
This commit is contained in:
Shyam Sunder 2018-06-25 10:47:20 -04:00 committed by Marcin Kurczewski
parent 2bf361c64a
commit 3972b902d8
21 changed files with 137 additions and 91 deletions

View file

@ -8,6 +8,7 @@ const progress = require('./util/progress.js');
const uri = require('./util/uri.js'); const uri = require('./util/uri.js');
let fileTokens = {}; let fileTokens = {};
let remoteConfig = null;
class Api extends events.EventTarget { class Api extends events.EventTarget {
constructor() { constructor() {
@ -65,14 +66,53 @@ class Api extends events.EventTarget {
return this._wrappedRequest(url, request.delete, data, {}, options); return this._wrappedRequest(url, request.delete, data, {}, options);
} }
fetchConfig() {
if (remoteConfig === null) {
return this.get(uri.formatApiLink('info'))
.then(response => {
remoteConfig = response.config;
});
} else {
return Promise.resolve();
}
}
getName() {
return remoteConfig.name;
}
getTagNameRegex() {
return remoteConfig.tagNameRegex;
}
getPasswordRegex() {
return remoteConfig.passwordRegex;
}
getUserNameRegex() {
return remoteConfig.userNameRegex;
}
getContactEmail() {
return remoteConfig.contactEmail;
}
canSendMails() {
return !!remoteConfig.canSendMails;
}
safetyEnabled() {
return !!remoteConfig.enableSafety;
}
hasPrivilege(lookup) { hasPrivilege(lookup) {
let minViableRank = null; let minViableRank = null;
for (let privilege of Object.keys(config.privileges)) { for (let p of Object.keys(remoteConfig.privileges)) {
if (!privilege.startsWith(lookup)) { if (!p.startsWith(lookup)) {
continue; continue;
} }
const rankName = config.privileges[privilege]; const rankIndex = this.allRanks.indexOf(
const rankIndex = this.allRanks.indexOf(rankName); remoteConfig.privileges[p]);
if (minViableRank === null || rankIndex < minViableRank) { if (minViableRank === null || rankIndex < minViableRank) {
minViableRank = rankIndex; minViableRank = rankIndex;
} }

View file

@ -12,7 +12,7 @@ class HomeController {
topNavigation.setTitle('Home'); topNavigation.setTitle('Home');
this._homeView = new HomeView({ this._homeView = new HomeView({
name: config.name, name: api.getName(),
version: config.meta.version, version: config.meta.version,
buildDate: config.meta.buildDate, buildDate: config.meta.buildDate,
canListSnapshots: api.hasPrivilege('snapshots:list'), canListSnapshots: api.hasPrivilege('snapshots:list'),

View file

@ -1,6 +1,5 @@
'use strict'; 'use strict';
const config = require('../config.js');
const router = require('../router.js'); const router = require('../router.js');
const api = require('../api.js'); const api = require('../api.js');
const settings = require('../models/settings.js'); const settings = require('../models/settings.js');
@ -33,9 +32,9 @@ class PostListController {
this._headerView = new PostsHeaderView({ this._headerView = new PostsHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode, hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters, parameters: ctx.parameters,
enableSafety: config.enableSafety, enableSafety: api.safetyEnabled(),
canBulkEditTags: api.hasPrivilege('posts:bulkEdit:tags'), canBulkEditTags: api.hasPrivilege('posts:bulk-edit:tags'),
canBulkEditSafety: api.hasPrivilege('posts:bulkEdit:safety'), canBulkEditSafety: api.hasPrivilege('posts:bulk-edit:safety'),
bulkEdit: { bulkEdit: {
tags: this._bulkEditTags tags: this._bulkEditTags
}, },
@ -97,9 +96,9 @@ class PostListController {
pageRenderer: pageCtx => { pageRenderer: pageCtx => {
Object.assign(pageCtx, { Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege('posts:view'), canViewPosts: api.hasPrivilege('posts:view'),
canBulkEditTags: api.hasPrivilege('posts:bulkEdit:tags'), canBulkEditTags: api.hasPrivilege('posts:bulk-edit:tags'),
canBulkEditSafety: canBulkEditSafety:
api.hasPrivilege('posts:bulkEdit:safety'), api.hasPrivilege('posts:bulk-edit:safety'),
bulkEdit: { bulkEdit: {
tags: this._bulkEditTags, tags: this._bulkEditTags,
}, },

View file

@ -1,7 +1,6 @@
'use strict'; 'use strict';
const api = require('../api.js'); const api = require('../api.js');
const config = require('../config.js');
const router = require('../router.js'); const router = require('../router.js');
const uri = require('../util/uri.js'); const uri = require('../util/uri.js');
const misc = require('../util/misc.js'); const misc = require('../util/misc.js');
@ -31,7 +30,7 @@ class PostUploadController {
this._view = new PostUploadView({ this._view = new PostUploadView({
canUploadAnonymously: api.hasPrivilege('posts:create:anonymous'), canUploadAnonymously: api.hasPrivilege('posts:create:anonymous'),
canViewPosts: api.hasPrivilege('posts:view'), canViewPosts: api.hasPrivilege('posts:view'),
enableSafety: config.enableSafety, enableSafety: api.safetyEnabled(),
}); });
this._view.addEventListener('change', e => this._evtChange(e)); this._view.addEventListener('change', e => this._evtChange(e));
this._view.addEventListener('submit', e => this._evtSubmit(e)); this._view.addEventListener('submit', e => this._evtSubmit(e));

View file

@ -1,12 +1,12 @@
'use strict'; 'use strict';
const api = require('../api.js'); const api = require('../api.js');
const config = require('../config.js');
const topNavigation = require('../models/top_navigation.js'); const topNavigation = require('../models/top_navigation.js');
const TopNavigationView = require('../views/top_navigation_view.js'); const TopNavigationView = require('../views/top_navigation_view.js');
class TopNavigationController { class TopNavigationController {
constructor() { constructor() {
api.fetchConfig().then(() => {
this._topNavigationView = new TopNavigationView(); this._topNavigationView = new TopNavigationView();
topNavigation.addEventListener( topNavigation.addEventListener(
@ -16,6 +16,7 @@ class TopNavigationController {
api.addEventListener('logout', e => this._evtAuthChange(e)); api.addEventListener('logout', e => this._evtAuthChange(e));
this._render(); this._render();
});
} }
_evtAuthChange(e) { _evtAuthChange(e) {
@ -65,7 +66,7 @@ class TopNavigationController {
this._updateNavigationFromPrivileges(); this._updateNavigationFromPrivileges();
this._topNavigationView.render({ this._topNavigationView.render({
items: topNavigation.getAll(), items: topNavigation.getAll(),
name: config.name name: api.getName()
}); });
this._topNavigationView.activate( this._topNavigationView.activate(
topNavigation.activeItem ? topNavigation.activeItem.key : ''); topNavigation.activeItem ? topNavigation.activeItem.key : '');

View file

@ -4,7 +4,6 @@ const router = require('../router.js');
const api = require('../api.js'); const api = require('../api.js');
const uri = require('../util/uri.js'); const uri = require('../util/uri.js');
const misc = require('../util/misc.js'); const misc = require('../util/misc.js');
const config = require('../config.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const User = require('../models/user.js'); const User = require('../models/user.js');
const UserToken = require('../models/user_token.js'); const UserToken = require('../models/user_token.js');

View file

@ -1,7 +1,6 @@
'use strict'; 'use strict';
const api = require('../api.js'); const api = require('../api.js');
const config = require('../config.js');
const events = require('../events.js'); const events = require('../events.js');
const misc = require('../util/misc.js'); const misc = require('../util/misc.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
@ -26,7 +25,7 @@ class PostEditSidebarControl extends events.EventTarget {
views.replaceContent(this._hostNode, template({ views.replaceContent(this._hostNode, template({
post: this._post, post: this._post,
enableSafety: config.enableSafety, enableSafety: api.safetyEnabled(),
hasClipboard: document.queryCommandSupported('copy'), hasClipboard: document.queryCommandSupported('copy'),
canEditPostSafety: api.hasPrivilege('posts:edit:safety'), canEditPostSafety: api.hasPrivilege('posts:edit:safety'),
canEditPostSource: api.hasPrivilege('posts:edit:source'), canEditPostSource: api.hasPrivilege('posts:edit:source'),

View file

@ -21,7 +21,7 @@ class PostReadonlySidebarControl extends events.EventTarget {
views.replaceContent(this._hostNode, template({ views.replaceContent(this._hostNode, template({
post: this._post, post: this._post,
enableSafety: config.enableSafety, enableSafety: api.safetyEnabled(),
canListPosts: api.hasPrivilege('posts:list'), canListPosts: api.hasPrivilege('posts:list'),
canEditPosts: api.hasPrivilege('posts:edit'), canEditPosts: api.hasPrivilege('posts:edit'),
canViewTags: api.hasPrivilege('tags:view'), canViewTags: api.hasPrivilege('tags:view'),

View file

@ -26,37 +26,41 @@ router.enter(
next(); next();
}); });
// register controller routes
let controllers = [];
controllers.push(require('./controllers/home_controller.js'));
controllers.push(require('./controllers/help_controller.js'));
controllers.push(require('./controllers/auth_controller.js'));
controllers.push(require('./controllers/password_reset_controller.js'));
controllers.push(require('./controllers/comments_controller.js'));
controllers.push(require('./controllers/snapshots_controller.js'));
controllers.push(require('./controllers/post_detail_controller.js'));
controllers.push(require('./controllers/post_main_controller.js'));
controllers.push(require('./controllers/post_list_controller.js'));
controllers.push(require('./controllers/post_upload_controller.js'));
controllers.push(require('./controllers/tag_controller.js'));
controllers.push(require('./controllers/tag_list_controller.js'));
controllers.push(require('./controllers/tag_categories_controller.js'));
controllers.push(require('./controllers/settings_controller.js'));
controllers.push(require('./controllers/user_controller.js'));
controllers.push(require('./controllers/user_list_controller.js'));
controllers.push(require('./controllers/user_registration_controller.js'));
// 404 controller needs to be registered last
controllers.push(require('./controllers/not_found_controller.js'));
for (let controller of controllers) {
controller(router);
}
const tags = require('./tags.js'); const tags = require('./tags.js');
const api = require('./api.js'); const api = require('./api.js');
tags.refreshCategoryColorMap(); // we don't care about errors tags.refreshCategoryColorMap(); // we don't care about errors
api.loginFromCookies().then(() => {
api.fetchConfig().then(() => {
// register controller routes
let controllers = [];
controllers.push(require('./controllers/home_controller.js'));
controllers.push(require('./controllers/help_controller.js'));
controllers.push(require('./controllers/auth_controller.js'));
controllers.push(require('./controllers/password_reset_controller.js'));
controllers.push(require('./controllers/comments_controller.js'));
controllers.push(require('./controllers/snapshots_controller.js'));
controllers.push(require('./controllers/post_detail_controller.js'));
controllers.push(require('./controllers/post_main_controller.js'));
controllers.push(require('./controllers/post_list_controller.js'));
controllers.push(require('./controllers/post_upload_controller.js'));
controllers.push(require('./controllers/tag_controller.js'));
controllers.push(require('./controllers/tag_list_controller.js'));
controllers.push(require('./controllers/tag_categories_controller.js'));
controllers.push(require('./controllers/settings_controller.js'));
controllers.push(require('./controllers/user_controller.js'));
controllers.push(require('./controllers/user_list_controller.js'));
controllers.push(require('./controllers/user_registration_controller.js'));
// 404 controller needs to be registered last
controllers.push(require('./controllers/not_found_controller.js'));
for (let controller of controllers) {
controller(router);
}
}, error => {
window.alert('Could not fetch basic configuration from server');
}).then(() => {
api.loginFromCookies().then(() => {
router.start(); router.start();
}, error => { }, error => {
if (window.location.href.indexOf('login') !== -1) { if (window.location.href.indexOf('login') !== -1) {
@ -69,3 +73,4 @@ api.loginFromCookies().then(() => {
error.message); error.message);
} }
}); });
});

View file

@ -37,7 +37,7 @@ class PostList extends AbstractList {
static _decorateSearchQuery(text) { static _decorateSearchQuery(text) {
const browsingSettings = settings.get(); const browsingSettings = settings.get();
const disabledSafety = []; const disabledSafety = [];
if (config.enableSafety) { if (api.safetyEnabled()) {
for (let key of Object.keys(browsingSettings.listPosts)) { for (let key of Object.keys(browsingSettings.listPosts)) {
if (browsingSettings.listPosts[key] === false) { if (browsingSettings.listPosts[key] === false) {
disabledSafety.push(key); disabledSafety.push(key);

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const config = require('../config.js');
const events = require('../events.js'); const events = require('../events.js');
const api = require('../api.js');
class TopNavigationItem { class TopNavigationItem {
constructor(accessKey, title, url, available, imageUrl) { constructor(accessKey, title, url, available, imageUrl) {
@ -53,8 +53,10 @@ class TopNavigation extends events.EventTarget {
} }
setTitle(title) { setTitle(title) {
api.fetchConfig().then(() => {
document.oldTitle = null; document.oldTitle = null;
document.title = config.name + (title ? (' ' + title) : ''); document.title = api.getName() + (title ? (' ' + title) : '');
});
} }
showAll() { showAll() {

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const config = require('../config.js'); const api = require('../api.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const template = views.getTemplate('help'); const template = views.getTemplate('help');
@ -26,7 +26,7 @@ class HelpView {
const sourceNode = template(); const sourceNode = template();
const ctx = { const ctx = {
name: config.name, name: api.getName(),
}; };
section = section || 'about'; section = section || 'about';

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const config = require('../config.js');
const events = require('../events.js'); const events = require('../events.js');
const api = require('../api.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const template = views.getTemplate('login'); const template = views.getTemplate('login');
@ -12,15 +12,15 @@ class LoginView extends events.EventTarget {
this._hostNode = document.getElementById('content-holder'); this._hostNode = document.getElementById('content-holder');
views.replaceContent(this._hostNode, template({ views.replaceContent(this._hostNode, template({
userNamePattern: config.userNameRegex, userNamePattern: api.getUserNameRegex(),
passwordPattern: config.passwordRegex, passwordPattern: api.getPasswordRegex(),
canSendMails: config.canSendMails, canSendMails: api.canSendMails(),
})); }));
views.syncScrollPosition(); views.syncScrollPosition();
views.decorateValidator(this._formNode); views.decorateValidator(this._formNode);
this._userNameInputNode.setAttribute('pattern', config.userNameRegex); this._userNameInputNode.setAttribute('pattern', api.getUserNameRegex());
this._passwordInputNode.setAttribute('pattern', config.passwordRegex); this._passwordInputNode.setAttribute('pattern', api.getPasswordRegex());
this._formNode.addEventListener('submit', e => { this._formNode.addEventListener('submit', e => {
e.preventDefault(); e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', { this.dispatchEvent(new CustomEvent('submit', {

View file

@ -1,6 +1,5 @@
'use strict'; 'use strict';
const config = require('../config.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const template = views.getTemplate('not-found'); const template = views.getTemplate('not-found');

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const config = require('../config.js');
const events = require('../events.js'); const events = require('../events.js');
const api = require('../api.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const template = views.getTemplate('password-reset'); const template = views.getTemplate('password-reset');
@ -12,8 +12,8 @@ class PasswordResetView extends events.EventTarget {
this._hostNode = document.getElementById('content-holder'); this._hostNode = document.getElementById('content-holder');
views.replaceContent(this._hostNode, template({ views.replaceContent(this._hostNode, template({
canSendMails: config.canSendMails, canSendMails: api.canSendMails(),
contactEmail: config.contactEmail, contactEmail: api.getContactEmail(),
})); }));
views.syncScrollPosition(); views.syncScrollPosition();

View file

@ -1,6 +1,5 @@
'use strict'; 'use strict';
const config = require('../config.js');
const events = require('../events.js'); const events = require('../events.js');
const views = require('../util/views.js'); const views = require('../util/views.js');

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const config = require('../config.js');
const events = require('../events.js'); const events = require('../events.js');
const api = require('../api.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const template = views.getTemplate('user-registration'); const template = views.getTemplate('user-registration');
@ -11,8 +11,8 @@ class RegistrationView extends events.EventTarget {
super(); super();
this._hostNode = document.getElementById('content-holder'); this._hostNode = document.getElementById('content-holder');
views.replaceContent(this._hostNode, template({ views.replaceContent(this._hostNode, template({
userNamePattern: config.userNameRegex, userNamePattern: api.getUserNameRegex(),
passwordPattern: config.passwordRegex, passwordPattern: api.getPasswordRegex(),
})); }));
views.syncScrollPosition(); views.syncScrollPosition();
views.decorateValidator(this._formNode); views.decorateValidator(this._formNode);

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const config = require('../config.js');
const events = require('../events.js'); const events = require('../events.js');
const api = require('../api.js');
const misc = require('../util/misc.js'); const misc = require('../util/misc.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const TagInputControl = require('../controls/tag_input_control.js'); const TagInputControl = require('../controls/tag_input_control.js');
@ -64,7 +64,7 @@ class TagEditView extends events.EventTarget {
} }
_evtNameInput(e) { _evtNameInput(e) {
const regex = new RegExp(config.tagNameRegex); const regex = new RegExp(api.getTagNameRegex());
const list = misc.splitByWhitespace(this._namesFieldNode.value); const list = misc.splitByWhitespace(this._namesFieldNode.value);
if (!list.length) { if (!list.length) {

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const config = require('../config.js');
const events = require('../events.js'); const events = require('../events.js');
const api = require('../api.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const TagAutoCompleteControl = const TagAutoCompleteControl =
require('../controls/tag_auto_complete_control.js'); require('../controls/tag_auto_complete_control.js');
@ -14,7 +14,7 @@ class TagMergeView extends events.EventTarget {
this._tag = ctx.tag; this._tag = ctx.tag;
this._hostNode = ctx.hostNode; this._hostNode = ctx.hostNode;
ctx.tagNamePattern = config.tagNameRegex; ctx.tagNamePattern = api.getTagNameRegex();
views.replaceContent(this._hostNode, template(ctx)); views.replaceContent(this._hostNode, template(ctx));
views.decorateValidator(this._formNode); views.decorateValidator(this._formNode);

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const config = require('../config.js');
const events = require('../events.js'); const events = require('../events.js');
const api = require('../api.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const FileDropperControl = require('../controls/file_dropper_control.js'); const FileDropperControl = require('../controls/file_dropper_control.js');
@ -11,8 +11,8 @@ class UserEditView extends events.EventTarget {
constructor(ctx) { constructor(ctx) {
super(); super();
ctx.userNamePattern = config.userNameRegex + /|^$/.source; ctx.userNamePattern = api.getUserNameRegex() + /|^$/.source;
ctx.passwordPattern = config.passwordRegex + /|^$/.source; ctx.passwordPattern = api.getPasswordRegex() + /|^$/.source;
this._user = ctx.user; this._user = ctx.user;
this._hostNode = ctx.hostNode; this._hostNode = ctx.hostNode;

View file

@ -35,11 +35,15 @@ def get_info(
'diskUsage': _get_disk_usage(), 'diskUsage': _get_disk_usage(),
'serverTime': datetime.utcnow(), 'serverTime': datetime.utcnow(),
'config': { 'config': {
'name': config.config['name'],
'userNameRegex': config.config['user_name_regex'], 'userNameRegex': config.config['user_name_regex'],
'passwordRegex': config.config['password_regex'], 'passwordRegex': config.config['password_regex'],
'tagNameRegex': config.config['tag_name_regex'], 'tagNameRegex': config.config['tag_name_regex'],
'tagCategoryNameRegex': config.config['tag_category_name_regex'], 'tagCategoryNameRegex': config.config['tag_category_name_regex'],
'defaultUserRank': config.config['default_rank'], 'defaultUserRank': config.config['default_rank'],
'enableSafety': config.config['enable_safety'],
'contactEmail': config.config['contactEmail'],
'canSendMails': bool(config.config['smtp']['host']),
'privileges': 'privileges':
util.snake_case_to_lower_camel_case_keys( util.snake_case_to_lower_camel_case_keys(
config.config['privileges']), config.config['privileges']),