From 8e09c6fce1d5c41a88df3029f2ebc5af2344171f Mon Sep 17 00:00:00 2001 From: NGPixel Date: Mon, 12 Mar 2018 00:09:54 -0400 Subject: [PATCH] refactor: updated loggers + admin UI improvements + setup fixes --- client/app.js | 2 - client/components/admin-auth.vue | 34 +++++++-- client/components/admin-general.vue | 43 ++++++++--- client/components/admin-locale.vue | 20 ++--- client/components/admin-logging.vue | 93 +++++++++++++++++++++++ client/components/admin-search.vue | 2 +- client/components/admin-storage.vue | 14 +++- client/components/admin-theme.vue | 12 +-- client/components/admin.vue | 1 + client/components/nav-header.vue | 72 ++++++++++++++++-- client/components/setup.vue | 2 +- package.json | 20 ++--- server/app/data.yml | 1 - server/core/auth.js | 5 +- server/core/logger.js | 28 ++----- server/graph/resolvers/authentication.js | 2 +- server/modules/logging/bugsnag.js | 4 +- server/modules/logging/console.js | 4 +- server/modules/logging/loggly.js | 4 +- server/modules/logging/papertrail.js | 4 +- server/modules/logging/rollbar.js | 4 +- server/modules/logging/sentry.js | 4 +- server/setup.js | 3 +- server/views/main/welcome.pug | 11 +-- server/worker.js | 4 +- yarn.lock | Bin 372947 -> 375224 bytes 26 files changed, 291 insertions(+), 102 deletions(-) create mode 100644 client/components/admin-logging.vue diff --git a/client/app.js b/client/app.js index 147728d1..28f5159b 100644 --- a/client/app.js +++ b/client/app.js @@ -1,7 +1,5 @@ 'use strict' -/* global siteConfig */ - import CONSTANTS from './constants' import Vue from 'vue' diff --git a/client/components/admin-auth.vue b/client/components/admin-auth.vue index bc94d6dd..cd1a2ef2 100644 --- a/client/components/admin-auth.vue +++ b/client/components/admin-auth.vue @@ -4,13 +4,13 @@ .pa-3.pt-4 .headline.primary--text Authentication .subheading.grey--text Configure the authentication settings of your wiki - v-tabs(color='grey lighten-4', grow, slider-color='primary', show-arrows) + v-tabs(color='grey lighten-4', fixed-tabs, slider-color='primary', show-arrows) v-tab(key='settings'): v-icon settings - v-tab(v-for='provider in providers', :key='provider.key') {{ provider.title }} + v-tab(v-for='provider in activeProviders', :key='provider.key') {{ provider.title }} v-tab-item(key='settings', :transition='false', :reverse-transition='false') v-card.pa-3 - .body-2.pb-2 Select which authentication providers are enabled: + .body-2.pb-2 Select which authentication providers to enable: v-form v-checkbox( v-for='(provider, n) in providers', @@ -32,11 +32,24 @@ v-btn(icon, @click='refresh') v-icon.grey--text refresh - v-tab-item(v-for='(provider, n) in providers', :key='provider.key', :transition='false', :reverse-transition='false') + v-tab-item(v-for='(provider, n) in activeProviders', :key='provider.key', :transition='false', :reverse-transition='false') v-card.pa-3 - .body-1(v-if='!provider.props || provider.props.length < 1') This provider has no configuration options you can modify. - v-form(v-else) - v-text-field(v-for='prop in provider.props', :key='prop', :label='prop', prepend-icon='mode_edit') + v-form + v-subheader Provider Configuration + .body-1(v-if='!provider.props || provider.props.length < 1') This provider has no configuration options you can modify. + v-text-field(v-else, v-for='prop in provider.props', :key='prop', :label='prop', prepend-icon='mode_edit') + v-divider + v-subheader Registration + v-switch.ml-3( + v-model='auths', + label='Allow self-registration', + :value='true', + color='primary', + hint='Allow any user successfully authorized by the provider to access the wiki.', + persistent-hint + ) + v-text-field(label='Limit to specific email domains', prepend-icon='mail_outline') + v-text-field(label='Assign to group', prepend-icon='people') v-divider v-btn(color='primary') v-icon(left) chevron_right @@ -52,6 +65,8 @@ + + diff --git a/client/components/admin-search.vue b/client/components/admin-search.vue index 1b384198..6dc629b2 100644 --- a/client/components/admin-search.vue +++ b/client/components/admin-search.vue @@ -4,7 +4,7 @@ .pa-3.pt-4 .headline.primary--text Search Engine .subheading.grey--text Configure the search capabilities of your wiki - v-tabs(color='grey lighten-4', grow, slider-color='primary', show-arrows) + v-tabs(color='grey lighten-4', fixed-tabs, slider-color='primary', show-arrows) v-tab(key='settings'): v-icon settings v-tab(key='db') Database v-tab(key='algolia') Algolia diff --git a/client/components/admin-storage.vue b/client/components/admin-storage.vue index e47e3ae2..2c769469 100644 --- a/client/components/admin-storage.vue +++ b/client/components/admin-storage.vue @@ -4,7 +4,7 @@ .pa-3.pt-4 .headline.primary--text Storage .subheading.grey--text Set backup and sync targets for your content - v-tabs(color='grey lighten-4', grow, slider-color='primary', show-arrows) + v-tabs(color='grey lighten-4', fixed-tabs, slider-color='primary', show-arrows) v-tab(key='settings'): v-icon settings v-tab(key='local') Local FS v-tab(key='git') Git @@ -19,7 +19,15 @@ v-tab-item(key='settings') v-card.pa-3 v-form - v-checkbox(v-for='(target, n) in targets', v-model='auths', :key='n', :label='target.text', :value='target.value', color='primary') + v-checkbox( + v-for='(target, n) in targets', + v-model='auths', + :key='n', + :label='target.text', + :value='target.value', + color='primary', + hide-details + ) v-divider v-btn(color='primary') v-icon(left) chevron_right @@ -32,7 +40,7 @@ export default { data() { return { targets: [ - { text: 'Local FS', value: 'local' }, + { text: 'Local Filesystem', value: 'local' }, { text: 'Git', value: 'auth0' }, { text: 'Amazon S3', value: 'algolia' }, { text: 'Azure Blob Storage', value: 'elasticsearch' }, diff --git a/client/components/admin-theme.vue b/client/components/admin-theme.vue index 715cfee0..b9eaf884 100644 --- a/client/components/admin-theme.vue +++ b/client/components/admin-theme.vue @@ -8,10 +8,9 @@ v-layout(row wrap) v-flex(lg6 xs12) v-card - v-toolbar(color='blue', dark, dense, flat) + v-toolbar(color='primary', dark, dense, flat) v-toolbar-title .subheading Theme - v-btn(fab, absolute, right, bottom, small, light): v-icon save v-card-text v-select(:items='themes', prepend-icon='palette', v-model='selectedTheme', label='Site Theme', persistent-hint, hint='Themes affect how content pages are displayed. Other site sections (such as the editor or admin area) are not affected.') template(slot='item', slot-scope='data') @@ -22,12 +21,15 @@ v-list-tile-sub-title(v-html='data.item.author') v-divider v-switch(v-model='darkMode', label='Dark Mode', color='primary', persistent-hint, hint='Not recommended for accessibility') + v-divider + .px-3.pb-3 + v-btn(color='primary') Save v-flex(lg6 xs12) v-card - v-toolbar(color='blue', dark, dense, flat) + v-toolbar(color='primary', dark, dense, flat) v-toolbar-title - .subheading Theme Options - v-list + .subheading --- + v-card-text --- - diff --git a/client/components/setup.vue b/client/components/setup.vue index 7a724983..b60be83b 100644 --- a/client/components/setup.vue +++ b/client/components/setup.vue @@ -40,7 +40,7 @@ v-stepper-content(step='1') v-card.text-xs-center.pa-3(flat) - img(src='svg/logo-wikijs.svg', alt='Wiki.js Logo', style='width: 300px;') + img(src='/svg/logo-wikijs.svg', alt='Wiki.js Logo', style='width: 300px;') v-container .body-2.py-2 This installation wizard will guide you through the steps needed to get your wiki up and running in no time! .body-1 diff --git a/package.json b/package.json index 497fe1b5..46a46c02 100644 --- a/package.json +++ b/package.json @@ -128,12 +128,12 @@ "request-promise": "4.2.2", "scim-query-filter-parser": "1.1.0", "semver": "5.5.0", - "sequelize": "4.35.2", + "sequelize": "4.36.0", "serve-favicon": "2.4.5", "uuid": "3.2.1", "validator": "9.4.1", "validator-as-promised": "1.0.2", - "winston": "2.4.0", + "winston": "3.0.0-rc2", "yargs": "11.0.0" }, "devDependencies": { @@ -157,8 +157,8 @@ "brace": "0.11.1", "cache-loader": "1.2.2", "clean-webpack-plugin": "0.1.19", - "colors": "1.1.2", - "copy-webpack-plugin": "4.5.0", + "colors": "1.2.0", + "copy-webpack-plugin": "4.5.1", "css-loader": "0.28.10", "cssnano": "4.0.0-rc.2", "duplicate-package-checker-webpack-plugin": "2.1.0", @@ -192,15 +192,15 @@ "sass-loader": "6.0.7", "sass-resources-loader": "1.3.3", "simple-progress-webpack-plugin": "1.1.2", - "style-loader": "0.20.2", + "style-loader": "0.20.3", "stylus": "0.54.5", "stylus-loader": "3.0.2", "twemoji-awesome": "1.0.6", - "uglifyjs-webpack-plugin": "1.2.2", + "uglifyjs-webpack-plugin": "1.2.3", "vee-validate": "2.0.5", "velocity-animate": "1.5.1", - "vue": "2.5.13", - "vue-apollo": "3.0.0-beta.4", + "vue": "2.5.15", + "vue-apollo": "3.0.0-beta.5", "vue-clipboards": "1.2.2", "vue-codemirror": "4.0.3", "vue-hot-reload-api": "2.3.0", @@ -208,8 +208,8 @@ "vue-material-design-icons": "1.2.1", "vue-router": "3.0.1", "vue-simple-breakpoints": "1.0.3", - "vue-template-compiler": "2.5.13", - "vuetify": "1.0.5", + "vue-template-compiler": "2.5.15", + "vuetify": "1.0.6", "vuex": "3.0.1", "vuex-persistedstate": "2.4.2", "webpack": "3.11.0", diff --git a/server/app/data.yml b/server/app/data.yml index 467622f3..81ea5b3a 100644 --- a/server/app/data.yml +++ b/server/app/data.yml @@ -53,7 +53,6 @@ defaults: configNamespaces: - auth - features - - git - logging - site - theme diff --git a/server/core/auth.js b/server/core/auth.js index 82a77672..73d065ed 100644 --- a/server/core/auth.js +++ b/server/core/auth.js @@ -33,12 +33,11 @@ module.exports = { // Load authentication strategies const modules = _.values(autoload(path.join(WIKI.SERVERPATH, 'modules/authentication'))) - console.info(WIKI.config.auth) _.forEach(modules, (strategy) => { - const strategyConfig = _.get(WIKI.config.auth.strategies, strategy.key, {}) + const strategyConfig = _.get(WIKI.config.auth.strategies, strategy.key, { isEnabled: false }) strategyConfig.callbackURL = `${WIKI.config.site.host}${WIKI.config.site.path}login/${strategy.key}/callback` + strategy.config = strategyConfig if (strategyConfig.isEnabled) { - console.info(strategy.title) try { strategy.init(passport, strategyConfig) } catch (err) { diff --git a/server/core/logger.js b/server/core/logger.js index 1b6b1dbe..b9432f1a 100644 --- a/server/core/logger.js +++ b/server/core/logger.js @@ -1,37 +1,25 @@ const _ = require('lodash') const cluster = require('cluster') -const fs = require('fs-extra') -const path = require('path') +const winston = require('winston') /* global WIKI */ module.exports = { loggers: {}, init() { - let winston = require('winston') - - let logger = new (winston.Logger)({ + let logger = winston.createLogger({ level: WIKI.config.logLevel, - transports: [] - }) - - logger.filters.push((level, msg) => { - let processName = (cluster.isMaster) ? 'MASTER' : `WORKER-${cluster.worker.id}` - return '[' + processName + '] ' + msg + format: winston.format.combine( + winston.format.colorize(), + winston.format.label({ label: (cluster.isMaster) ? 'MASTER' : `WORKER-${cluster.worker.id}` }), + winston.format.timestamp(), + winston.format.printf(info => `${info.timestamp} [${info.label}] ${info.level}: ${info.message}`) + ) }) _.forOwn(_.omitBy(WIKI.config.logging.loggers, s => s.enabled === false), (loggerConfig, loggerKey) => { let loggerModule = require(`../modules/logging/${loggerKey}`) loggerModule.init(logger, loggerConfig) - fs.readFile(path.join(WIKI.ROOTPATH, `assets/svg/auth-icon-${loggerKey}.svg`), 'utf8').then(iconData => { - logger.icon = iconData - }).catch(err => { - if (err.code === 'ENOENT') { - logger.icon = '[missing icon]' - } else { - logger.error(err) - } - }) this.loggers[logger.key] = loggerModule }) diff --git a/server/graph/resolvers/authentication.js b/server/graph/resolvers/authentication.js index 428e1e49..4d42121d 100644 --- a/server/graph/resolvers/authentication.js +++ b/server/graph/resolvers/authentication.js @@ -17,7 +17,7 @@ module.exports = { AuthenticationQuery: { providers(obj, args, context, info) { let prv = _.map(WIKI.auth.strategies, str => ({ - isEnabled: true, + isEnabled: str.config.isEnabled, key: str.key, props: str.props, title: str.title, diff --git a/server/modules/logging/bugsnag.js b/server/modules/logging/bugsnag.js index 990186e0..f1f67b59 100644 --- a/server/modules/logging/bugsnag.js +++ b/server/modules/logging/bugsnag.js @@ -24,9 +24,9 @@ module.exports = { callback(null, true) } - logger.add(BugsnagLogger, { + logger.add(new BugsnagLogger({ level: 'warn', key: conf.key - }) + })) } } diff --git a/server/modules/logging/console.js b/server/modules/logging/console.js index bb69154b..b483bdb8 100644 --- a/server/modules/logging/console.js +++ b/server/modules/logging/console.js @@ -11,12 +11,12 @@ module.exports = { title: 'Console', props: [], init (logger, conf) { - logger.add(winston.transports.Console, { + logger.add(new winston.transports.Console({ level: WIKI.config.logLevel, prettyPrint: true, colorize: true, silent: false, timestamp: true - }) + })) } } diff --git a/server/modules/logging/loggly.js b/server/modules/logging/loggly.js index d4156b72..33bcfda9 100644 --- a/server/modules/logging/loggly.js +++ b/server/modules/logging/loggly.js @@ -10,12 +10,12 @@ module.exports = { props: ['token', 'subdomain'], init (logger, conf) { require('winston-loggly-bulk') - logger.add(winston.transports.Loggly, { + logger.add(new winston.transports.Loggly({ token: conf.token, subdomain: conf.subdomain, tags: ['wiki-js'], level: 'warn', json: true - }) + })) } } diff --git a/server/modules/logging/papertrail.js b/server/modules/logging/papertrail.js index 7869a762..6d32e0e2 100644 --- a/server/modules/logging/papertrail.js +++ b/server/modules/logging/papertrail.js @@ -10,11 +10,11 @@ module.exports = { props: ['host', 'port'], init (logger, conf) { require('winston-papertrail').Papertrail // eslint-disable-line no-unused-expressions - logger.add(winston.transports.Papertrail, { + logger.add(new winston.transports.Papertrail({ host: conf.host, port: conf.port, level: 'warn', program: 'wiki.js' - }) + })) } } diff --git a/server/modules/logging/rollbar.js b/server/modules/logging/rollbar.js index 57d52a37..b6804f02 100644 --- a/server/modules/logging/rollbar.js +++ b/server/modules/logging/rollbar.js @@ -24,9 +24,9 @@ module.exports = { callback(null, true) } - logger.add(RollbarLogger, { + logger.add(new RollbarLogger({ level: 'warn', key: conf.key - }) + })) } } diff --git a/server/modules/logging/sentry.js b/server/modules/logging/sentry.js index 52c9aa57..4b2c2515 100644 --- a/server/modules/logging/sentry.js +++ b/server/modules/logging/sentry.js @@ -24,9 +24,9 @@ module.exports = { callback(null, true) } - logger.add(SentryLogger, { + logger.add(new SentryLogger({ level: 'warn', key: conf.key - }) + })) } } diff --git a/server/setup.js b/server/setup.js index f9578aee..25918c77 100644 --- a/server/setup.js +++ b/server/setup.js @@ -276,7 +276,6 @@ module.exports = () => { // Populate config namespaces WIKI.config.auth = WIKI.config.auth || {} WIKI.config.features = WIKI.config.features || {} - WIKI.config.git = WIKI.config.git || {} WIKI.config.logging = WIKI.config.logging || {} WIKI.config.site = WIKI.config.site || {} WIKI.config.theme = WIKI.config.theme || {} @@ -290,7 +289,7 @@ module.exports = () => { // Auth namespace _.set(WIKI.config.auth, 'public', req.body.public === 'true') - _.set(WIKI.config.auth, 'strategies.local.enabled', true) + _.set(WIKI.config.auth, 'strategies.local.isEnabled', true) _.set(WIKI.config.auth, 'strategies.local.allowSelfRegister', req.body.selfRegister === 'true') // Logging namespace diff --git a/server/views/main/welcome.pug b/server/views/main/welcome.pug index d81d1466..ddb4ccff 100644 --- a/server/views/main/welcome.pug +++ b/server/views/main/welcome.pug @@ -3,8 +3,9 @@ extends ../master.pug block body body #app.is-fullscreen - .onboarding - img(src='/svg/logo-wikijs.svg', alt='Wiki.js') - h1= t('welcome.title') - h2= t('welcome.subtitle') - a.button.is-blue(href='/e/home')= t('welcome.createhome') + v-app + .onboarding + img(src='/svg/logo-wikijs.svg', alt='Wiki.js') + h1= t('welcome.title') + h2= t('welcome.subtitle') + v-btn(color='primary', href='/e/home')= t('welcome.createhome') diff --git a/server/worker.js b/server/worker.js index d72b7cb5..99dd0e02 100644 --- a/server/worker.js +++ b/server/worker.js @@ -4,7 +4,7 @@ const Promise = require('bluebird') module.exports = Promise.join( WIKI.db.onReady, - WIKI.configSvc.loadFromDb(['features', 'git', 'logging', 'site', 'uploads']) + WIKI.configSvc.loadFromDb(['features', 'logging', 'site', 'uploads']) ).then(() => { const path = require('path') @@ -25,7 +25,7 @@ module.exports = Promise.join( const i18nBackend = require('i18next-node-fs-backend') WIKI.lang.use(i18nBackend).init({ load: 'languageOnly', - ns: ['common', 'admin', 'auth', 'errors', 'git'], + ns: ['common', 'admin', 'auth', 'errors'], defaultNS: 'common', saveMissing: false, preload: [WIKI.config.lang], diff --git a/yarn.lock b/yarn.lock index 3616915f9ae1a62df65d484398dad82ee8f32c6c..872f8a3bd1981c7f207750f92a754bae1718ef72 100644 GIT binary patch delta 3156 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 XF|g2%j)A%9tue5Ec6#m<0QLU>O^_Qy