diff --git a/client/components/admin/admin-api.vue b/client/components/admin/admin-api.vue
index 7202522f..edad343a 100644
--- a/client/components/admin/admin-api.vue
+++ b/client/components/admin/admin-api.vue
@@ -6,14 +6,14 @@
img(src='/svg/icon-rest-api.svg', alt='API', style='width: 80px;')
.admin-header-title
.headline.blue--text.text--darken-2 API
- .subheading.grey--text Manage keys to access the API
+ .subheading.grey--text Manage keys to access the API #[v-chip(label, color='primary', small).white--text coming soon]
v-spacer
- v-btn(outline, color='grey', large, @click='refresh')
+ v-btn(outline, color='grey', large, @click='refresh', disabled)
v-icon refresh
- v-btn(color='green', dark, depressed, large, @click='globalSwitch')
+ v-btn(color='green', disabled, depressed, large, @click='globalSwitch')
v-icon(left) power_settings_new
| Enable API
- v-btn(color='primary', depressed, large, @click='newKey')
+ v-btn(color='primary', depressed, large, @click='newKey', disabled)
v-icon(left) add
| New API Key
v-card.mt-3
@@ -58,7 +58,7 @@
td {{ props.item.updatedOn }}
td: v-btn(icon): v-icon.grey--text.text--darken-1 more_horiz
template(slot='no-data')
- v-alert.mt-3(icon='warning', :value='true', outline) No API have been generated yet.
+ v-alert.mt-3(icon='info', :value='true', outline, color='info') No API have been generated yet.
.text-xs-center.py-2
v-pagination(v-model='pagination.page', :length='pages')
diff --git a/client/components/admin/admin-auth.vue b/client/components/admin/admin-auth.vue
index 4250fb4f..23b8982f 100644
--- a/client/components/admin/admin-auth.vue
+++ b/client/components/admin/admin-auth.vue
@@ -207,6 +207,11 @@ export default {
await this.$apollo.mutate({
mutation: strategiesSaveMutation,
variables: {
+ config: {
+ audience: this.jwtAudience,
+ tokenExpiration: this.jwtExpiration,
+ tokenRenewal: this.jwtRenewablePeriod
+ },
strategies: this.strategies.map(str => _.pick(str, [
'isEnabled',
'key',
diff --git a/client/components/admin/admin-dev.vue b/client/components/admin/admin-dev.vue
index 750113b6..692662cd 100644
--- a/client/components/admin/admin-dev.vue
+++ b/client/components/admin/admin-dev.vue
@@ -7,8 +7,18 @@
.admin-header-title
.headline.primary--text Developer Tools
.subheading.grey--text Β―\_(γ)_/Β―
+ v-spacer
+ v-card.radius-7
+ v-card-text
+ .caption Enables extra dev options and removes many safeguards.
+ .caption.red--text Do not enable unless you know what you're doing!
+ v-switch.mt-1(
+ color='primary'
+ hide-details
+ label='Dev Mode'
+ )
- v-card.mt-3
+ v-card.mt-3.white.grey--text.text--darken-3
v-tabs(
v-model='selectedTab'
color='grey darken-2'
@@ -92,9 +102,8 @@ export default {
}, 500)
return resp
},
- query: '',
response: null,
- variables: null,
+ variables: '{}',
operationName: null,
websocketConnectionParams: null
}),
@@ -103,6 +112,7 @@ export default {
graphiQLInstance.queryEditorComponent.editor.refresh()
graphiQLInstance.variableEditorComponent.editor.refresh()
graphiQLInstance.state.variableEditorOpen = true
+ graphiQLInstance.state.docExplorerOpen = true
},
renderVoyager() {
ReactDOM.render(
@@ -120,7 +130,7 @@ export default {
diff --git a/client/components/admin/admin-general.vue b/client/components/admin/admin-general.vue
index 79928ab0..6efe8787 100644
--- a/client/components/admin/admin-general.vue
+++ b/client/components/admin/admin-general.vue
@@ -38,6 +38,8 @@
:counter='50'
v-model='config.title'
prepend-icon='public'
+ hint='Displayed in the top bar and appended to all pages meta title.'
+ persistent-hint
)
v-divider
v-subheader SEO
@@ -48,6 +50,8 @@
:counter='255'
v-model='config.description'
prepend-icon='explore'
+ hint='Default description when none is provided for a page.'
+ persistent-hint
)
v-select.mt-2(
outline
@@ -57,7 +61,7 @@
v-model='config.robots'
prepend-icon='explore'
:return-object='false'
- hint='Default: Index, Follow'
+ hint='Default: Index, Follow. Can also be set on a per-page basis.'
persistent-hint
)
v-divider
@@ -69,6 +73,8 @@
:items='analyticsServices'
v-model='config.analyticsService'
prepend-icon='timeline'
+ persistent-hint
+ hint='Automatically add tracking code for services like Google Analytics.'
)
v-text-field.mt-2(
v-if='config.analyticsService !== ``'
diff --git a/client/components/admin/admin-groups-edit-permissions.vue b/client/components/admin/admin-groups-edit-permissions.vue
index dee228da..9933da93 100644
--- a/client/components/admin/admin-groups-edit-permissions.vue
+++ b/client/components/admin/admin-groups-edit-permissions.vue
@@ -60,14 +60,14 @@ export default {
},
{
permission: 'write:pages',
- hint: 'Can view and create new pages, as specified in the Page Rules',
+ hint: 'Can create new pages, as specified in the Page Rules',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'manage:pages',
- hint: 'Can view, create, edit and move existing pages as specified in the Page Rules',
+ hint: 'Can edit and move existing pages as specified in the Page Rules',
warning: false,
restrictedForSystem: false,
disabled: false
@@ -95,7 +95,7 @@ export default {
},
{
permission: 'manage:assets',
- hint: 'Can edit and delete assets (such as images and files), as specified in the Page Rules',
+ hint: 'Can edit and delete existing assets (such as images and files), as specified in the Page Rules',
warning: false,
restrictedForSystem: false,
disabled: false
@@ -116,7 +116,7 @@ export default {
},
{
permission: 'manage:comments',
- hint: 'Can edit and delete comments, as specified in the Page Rules',
+ hint: 'Can edit and delete existing comments, as specified in the Page Rules',
warning: false,
restrictedForSystem: false,
disabled: false
diff --git a/client/components/admin/admin-locale.vue b/client/components/admin/admin-locale.vue
index 1c48d772..170d7ec9 100644
--- a/client/components/admin/admin-locale.vue
+++ b/client/components/admin/admin-locale.vue
@@ -52,6 +52,8 @@
v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title
.subheading {{ $t('admin:locale.namespacing') }}
+ v-spacer
+ v-chip(label, color='white', small).primary--text coming soon
v-card-text
v-switch(
v-model='namespacing'
diff --git a/client/components/admin/admin-logging.vue b/client/components/admin/admin-logging.vue
index 9bd8be2f..23172dc6 100644
--- a/client/components/admin/admin-logging.vue
+++ b/client/components/admin/admin-logging.vue
@@ -6,11 +6,11 @@
img(src='/svg/icon-registry-editor.svg', alt='Logging', style='width: 80px;')
.admin-header-title
.headline.primary--text Logging
- .subheading.grey--text Configure the system logger(s)
+ .subheading.grey--text Configure the system logger(s) #[v-chip(label, color='primary', small).white--text coming soon]
v-spacer
v-btn(outline, color='grey', @click='refresh', large)
v-icon refresh
- v-btn(color='black', dark, depressed, @click='toggleConsole', large)
+ v-btn(color='black', disabled, depressed, @click='toggleConsole', large)
ConsoleLineIcon.mr-3
span Live Trail
v-btn(color='success', @click='save', depressed, large)
@@ -34,6 +34,7 @@
:label='logger.title'
color='primary'
hide-details
+ disabled
)
v-tab-item(v-for='(logger, n) in activeLoggers', :key='logger.key', :transition='false', :reverse-transition='false')
diff --git a/client/components/admin/admin-search.vue b/client/components/admin/admin-search.vue
index 06c2fce3..d0abe78c 100644
--- a/client/components/admin/admin-search.vue
+++ b/client/components/admin/admin-search.vue
@@ -6,7 +6,7 @@
img(src='/svg/icon-search.svg', alt='Search Engine', style='width: 80px;')
.admin-header-title
.headline.primary--text Search Engine
- .subheading.grey--text Configure the search capabilities of your wiki
+ .subheading.grey--text Configure the search capabilities of your wiki #[v-chip(label, color='primary', small).white--text coming soon]
v-spacer
v-btn(outline, color='grey', @click='refresh', large)
v-icon refresh
diff --git a/client/components/admin/admin-storage.vue b/client/components/admin/admin-storage.vue
index 66e008c8..e562ad34 100644
--- a/client/components/admin/admin-storage.vue
+++ b/client/components/admin/admin-storage.vue
@@ -6,7 +6,7 @@
img(src='/svg/icon-cloud-storage.svg', alt='Storage', style='width: 80px;')
.admin-header-title
.headline.primary--text Storage
- .subheading.grey--text Set backup and sync targets for your content
+ .subheading.grey--text Set backup and sync targets for your content #[v-chip(label, color='primary', small).white--text coming soon]
v-spacer
v-btn(outline, color='grey', @click='refresh', large)
v-icon refresh
diff --git a/client/components/editor.vue b/client/components/editor.vue
index 6171f2d9..a380a20e 100644
--- a/client/components/editor.vue
+++ b/client/components/editor.vue
@@ -14,12 +14,12 @@
outline
color='blue'
@click.native.stop='openPropsModal'
- :class='{ "is-icon": $vuetify.breakpoint.mdAndDown, "mx-0": mode === `create`, "ml-0": mode !== `create` }'
+ :class='{ "is-icon": $vuetify.breakpoint.mdAndDown, "mx-0": !welcomeMode, "ml-0": !welcomeMode }'
)
v-icon(color='blue', :left='$vuetify.breakpoint.lgAndUp') sort_by_alpha
span.white--text(v-if='$vuetify.breakpoint.lgAndUp') {{ $t('editor:page') }}
v-btn(
- v-if='path !== `home`'
+ v-if='!welcomeMode'
outline
color='red'
:class='{ "is-icon": $vuetify.breakpoint.mdAndDown }'
@@ -62,6 +62,7 @@ import editorStore from '@/store/editor'
WIKI.$store.registerModule('editor', editorStore)
export default {
+ i18nOptions: { namespaces: 'editor' },
components: {
AtomSpinner,
editorCode: () => import(/* webpackChunkName: "editor-code", webpackMode: "lazy" */ './editor/editor-code.vue'),
@@ -127,7 +128,8 @@ export default {
darkMode: get('site/dark'),
mode: get('editor/mode'),
notification: get('notification'),
- notificationState: sync('notification@isActive')
+ notificationState: sync('notification@isActive'),
+ welcomeMode() { return this.mode === `create` && this.path === `home` }
},
watch: {
currentEditor(newValue, oldValue) {
@@ -242,6 +244,8 @@ export default {
throw new Error(_.get(resp, 'responseResult.message'))
}
}
+
+ this.initContentParsed = this.$store.get('editor/content')
} catch (err) {
this.$store.commit('showNotification', {
message: err.message,
diff --git a/client/components/editor/editor-markdown.vue b/client/components/editor/editor-markdown.vue
index 8ffeac3d..938849ab 100644
--- a/client/components/editor/editor-markdown.vue
+++ b/client/components/editor/editor-markdown.vue
@@ -234,7 +234,7 @@ export default {
if (!token.type) { return }
- console.info(token)
+ // console.info(token)
},
/**
* Update scroll sync
diff --git a/client/graph/admin/auth/auth-mutation-save-strategies.gql b/client/graph/admin/auth/auth-mutation-save-strategies.gql
index b488a535..46f34624 100644
--- a/client/graph/admin/auth/auth-mutation-save-strategies.gql
+++ b/client/graph/admin/auth/auth-mutation-save-strategies.gql
@@ -1,6 +1,6 @@
-mutation($strategies: [AuthenticationStrategyInput]) {
+mutation($strategies: [AuthenticationStrategyInput]!, $config: AuthenticationConfigInput) {
authentication {
- updateStrategies(strategies: $strategies) {
+ updateStrategies(strategies: $strategies, config: $config) {
responseResult {
succeeded
errorCode
diff --git a/client/scss/app.scss b/client/scss/app.scss
index 96ede88f..1f4bb33d 100644
--- a/client/scss/app.scss
+++ b/client/scss/app.scss
@@ -23,6 +23,7 @@
// @import 'node_modules/diff2html/dist/diff2html.min';
@import 'pages/new';
+@import 'pages/unauthorized';
@import 'pages/welcome';
@import 'pages/error';
diff --git a/client/scss/pages/_unauthorized.scss b/client/scss/pages/_unauthorized.scss
new file mode 100644
index 00000000..915d8c70
--- /dev/null
+++ b/client/scss/pages/_unauthorized.scss
@@ -0,0 +1,81 @@
+.unauthorized {
+ background: linear-gradient(to bottom, darken(mc('blue', '900'), 10%) 0%, mc('red', '500') 100%);
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ color: mc('grey', '50');
+
+ &::before {
+ content: '';
+ display:block;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ background-image: url('../static/svg/motif-diagonals.svg');
+ background-position: center center;
+ background-repeat: repeat;
+ background-size: 50px;
+ z-index: 0;
+ opacity: .75;
+ animation: onboardingBgReveal 50s linear infinite;
+
+ @include keyframes(onboardingBgReveal) {
+ 0% {
+ background-position-y: 0;
+ }
+ 100% {
+ background-position-y: -2000px;
+ }
+ }
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ background-color: transparent;
+ background-image: url('../static/svg/motif-overlay.svg');
+ background-attachment: fixed;
+ background-size: cover;
+ opacity: .5;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ }
+
+ &-content {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ z-index: 2;
+ }
+
+ img {
+ height: 250px;
+ margin-bottom: 3rem;
+ z-index: 2;
+ animation-duration: 2s;
+
+ @include until($tablet) {
+ height: 200px;
+ }
+ }
+
+ h1 {
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ z-index: 2;
+ }
+ h2 {
+ margin-bottom: 3rem;
+ z-index: 2;
+ }
+ .v-btn {
+ z-index: 2;
+ }
+}
diff --git a/client/static/svg/icon-delete-shield.svg b/client/static/svg/icon-delete-shield.svg
new file mode 100644
index 00000000..59159340
--- /dev/null
+++ b/client/static/svg/icon-delete-shield.svg
@@ -0,0 +1,14 @@
+
+
+
diff --git a/client/static/svg/icon-safety-float.svg b/client/static/svg/icon-safety-float.svg
new file mode 100644
index 00000000..2dd4dbf3
--- /dev/null
+++ b/client/static/svg/icon-safety-float.svg
@@ -0,0 +1,33 @@
+
+
+
diff --git a/client/static/svg/motif-diagonals.svg b/client/static/svg/motif-diagonals.svg
new file mode 100644
index 00000000..56ea8544
--- /dev/null
+++ b/client/static/svg/motif-diagonals.svg
@@ -0,0 +1,3 @@
+
diff --git a/client/themes/default/components/page.vue b/client/themes/default/components/page.vue
index 9f6c40db..d5b273f7 100644
--- a/client/themes/default/components/page.vue
+++ b/client/themes/default/components/page.vue
@@ -189,10 +189,8 @@ export default {
},
breadcrumbs: [
{ path: '/', name: 'Home' },
- { path: '/universe', name: 'Universe' },
- { path: '/universe/galaxy', name: 'Galaxy' },
- { path: '/universe/galaxy/solar-system', name: 'Solar System' },
- { path: '/universe/galaxy/solar-system/planet-earth', name: 'Planet Earth' }
+ { path: '/' + this.path, name: 'Breadcrumb' },
+ { path: '/' + this.path, name: 'Coming soon' }
],
scrollStyle: {
vuescroll: {},
diff --git a/dev/examples/Dockerfile b/dev/examples/Dockerfile
deleted file mode 100644
index 34b22f9c..00000000
--- a/dev/examples/Dockerfile
+++ /dev/null
@@ -1,12 +0,0 @@
-FROM requarks/wiki:latest
-
-# Replace with your email address:
-ENV WIKI_ADMIN_EMAIL admin@example.com
-
-WORKDIR /var/wiki
-
-# Replace your-config.yml with the path to your config file:
-ADD your-config.yml config.yml
-
-EXPOSE 3000
-ENTRYPOINT [ "node", "server" ]
diff --git a/dev/examples/docker-compose.yml b/dev/examples/docker-compose.yml
index 7407a580..d2721197 100644
--- a/dev/examples/docker-compose.yml
+++ b/dev/examples/docker-compose.yml
@@ -1,19 +1,53 @@
-version: '3'
+# -- DEV DOCKER-COMPOSE --
+# -- DO NOT USE IN PRODUCTION! --
+
+version: "3"
services:
- wikidb:
- image: mongo
- expose:
- - '27017'
- command: '--smallfiles --logpath=/dev/null'
- volumes:
- - ./data/mongo:/data/db
- wikijs:
- image: 'requarks/wiki:latest'
- links:
- - wikidb
- ports:
- - '80:3000'
+
+ redis:
+ image: redis:4-alpine
+ logging:
+ driver: "none"
+ networks:
+ - wikinet
+
+ db:
+ image: postgres:9-alpine
environment:
- WIKI_ADMIN_EMAIL: admin@example.com
+ POSTGRES_DB: wiki
+ POSTGRES_PASSWORD: wikijsrocks
+ POSTGRES_USER: wikijs
+ logging:
+ driver: "none"
volumes:
- - ./config.yml:/var/wiki/config.yml
+ - db-data:/var/lib/postgresql/data
+ networks:
+ - wikinet
+
+ wiki:
+ image: requarks/wiki:beta
+ depends_on:
+ - db
+ - redis
+ environment:
+ PORT: 3000 # DO NOT CHANGE! Use ports below to specify listening port.
+ DB_TYPE: postgres
+ DB_HOST: db
+ DB_PORT: 5432
+ DB_USER: wikijs
+ DB_PASS: wikijsrocks
+ DB_NAME: wiki
+ REDIS_HOST: redis
+ REDIS_PORT: 6379
+ REDIS_DB: 0
+ REDIS_PASS: ''
+ networks:
+ - wikinet
+ ports:
+ - "3000:3000" # <-- replace with "80:3000" to listen on port 80 instead
+
+networks:
+ wikinet:
+
+volumes:
+ db-data:
diff --git a/server/app/data.yml b/server/app/data.yml
index 2f2d9483..d3fdf518 100644
--- a/server/app/data.yml
+++ b/server/app/data.yml
@@ -22,14 +22,12 @@ defaults:
db: 0
password: null
# DB defaults
- defaultEditor: 'markdown'
graphEndpoint: 'https://graph.requarks.io'
lang:
code: en
autoUpdate: true
namespaces: []
namespacing: false
- public: false
telemetry:
clientId: ''
isEnabled: false
@@ -47,13 +45,6 @@ defaults:
maxAge: 600
methods: 'GET,POST'
origin: true
-configNamespaces:
- - auth
- - features
- - logging
- - site
- - theme
- - uploads
localeNamespaces:
- admin
- auth
diff --git a/server/controllers/common.js b/server/controllers/common.js
index c89c3030..2211dccf 100644
--- a/server/controllers/common.js
+++ b/server/controllers/common.js
@@ -5,6 +5,18 @@ const _ = require('lodash')
/* global WIKI */
+/**
+ * Robots.txt
+ */
+router.get('/robots.txt', (req, res, next) => {
+ res.type('text/plain')
+ if (_.includes(WIKI.config.seo.robots, 'noindex')) {
+ res.send("User-agent: *\nDisallow: /")
+ } else {
+ res.status(200).end()
+ }
+})
+
/**
* Create/Edit document
*/
@@ -17,12 +29,20 @@ router.get(['/e', '/e/*'], async (req, res, next) => {
isPrivate: false
})
if (page) {
+ if (!WIKI.auth.checkAccess(req.user, ['manage:pages'], pageArgs)) {
+ return res.render('unauthorized', { action: 'edit'})
+ }
+
_.set(res.locals, 'pageMeta.title', `Edit ${page.title}`)
_.set(res.locals, 'pageMeta.description', page.description)
page.mode = 'update'
page.isPublished = (page.isPublished === true || page.isPublished === 1) ? 'true' : 'false'
page.content = Buffer.from(page.content).toString('base64')
} else {
+ if (!WIKI.auth.checkAccess(req.user, ['write:pages'], pageArgs)) {
+ return res.render('unauthorized', { action: 'create'})
+ }
+
_.set(res.locals, 'pageMeta.title', `New Page`)
page = {
path: pageArgs.path,
@@ -56,6 +76,11 @@ router.get(['/p', '/p/*'], (req, res, next) => {
*/
router.get(['/h', '/h/*'], async (req, res, next) => {
const pageArgs = pageHelper.parsePath(req.path)
+
+ if (!WIKI.auth.checkAccess(req.user, ['read:pages'], pageArgs)) {
+ return res.render('unauthorized', { action: 'history'})
+ }
+
const page = await WIKI.models.pages.getPageFromDb({
path: pageArgs.path,
locale: pageArgs.locale,
@@ -76,6 +101,11 @@ router.get(['/h', '/h/*'], async (req, res, next) => {
*/
router.get(['/s', '/s/*'], async (req, res, next) => {
const pageArgs = pageHelper.parsePath(req.path)
+
+ if (!WIKI.auth.checkAccess(req.user, ['read:pages'], pageArgs)) {
+ return res.render('unauthorized', { action: 'source'})
+ }
+
const page = await WIKI.models.pages.getPageFromDb({
path: pageArgs.path,
locale: pageArgs.locale,
@@ -96,6 +126,15 @@ router.get(['/s', '/s/*'], async (req, res, next) => {
*/
router.get('/*', async (req, res, next) => {
const pageArgs = pageHelper.parsePath(req.path)
+
+ if (!WIKI.auth.checkAccess(req.user, ['read:pages'], pageArgs)) {
+ if (pageArgs.path === 'home') {
+ return res.redirect('/login')
+ } else {
+ return res.render('unauthorized', { action: 'view'})
+ }
+ }
+
const page = await WIKI.models.pages.getPage({
path: pageArgs.path,
locale: pageArgs.locale,
@@ -108,8 +147,10 @@ router.get('/*', async (req, res, next) => {
const sidebar = await WIKI.models.navigation.getTree({ cache: true })
res.render('page', { page, sidebar })
} else if (pageArgs.path === 'home') {
+ _.set(res.locals, 'pageMeta.title', 'Welcome')
res.render('welcome')
} else {
+ _.set(res.locals, 'pageMeta.title', 'Page Not Found')
res.status(404).render('new', { pagePath: req.path })
}
})
diff --git a/server/core/auth.js b/server/core/auth.js
index a2736378..bda065c5 100644
--- a/server/core/auth.js
+++ b/server/core/auth.js
@@ -3,6 +3,8 @@ const passportJWT = require('passport-jwt')
const fs = require('fs-extra')
const _ = require('lodash')
const path = require('path')
+const jwt = require('jsonwebtoken')
+const moment = require('moment')
const securityHelper = require('../helpers/security')
@@ -10,11 +12,16 @@ const securityHelper = require('../helpers/security')
module.exports = {
strategies: {},
+ guest: {
+ cacheExpiration: moment.utc().subtract(1, 'd')
+ },
+
+ /**
+ * Initialize the authentication module
+ */
init() {
this.passport = passport
- // Serialization user methods
-
passport.serializeUser(function (user, done) {
done(null, user.id)
})
@@ -34,6 +41,10 @@ module.exports = {
return this
},
+
+ /**
+ * Load authentication strategies
+ */
async activateStrategies() {
try {
// Unload any active strategies
@@ -46,7 +57,7 @@ module.exports = {
passport.use('jwt', new passportJWT.Strategy({
jwtFromRequest: securityHelper.extractJWT,
secretOrKey: WIKI.config.certs.public,
- audience: 'urn:wiki.js', // TODO: use value from admin
+ audience: WIKI.config.auth.audience,
issuer: 'urn:wiki.js'
}, (jwtPayload, cb) => {
cb(null, jwtPayload)
@@ -60,7 +71,7 @@ module.exports = {
const strategy = require(`../modules/authentication/${stg.key}/authentication.js`)
- stg.config.callbackURL = `${WIKI.config.host}/login/${stg.key}/callback` // TODO: config.host
+ stg.config.callbackURL = `${WIKI.config.host}/login/${stg.key}/callback`
strategy.init(passport, stg.config)
fs.readFile(path.join(WIKI.ROOTPATH, `assets/svg/auth-icon-${strategy.key}.svg`), 'utf8').then(iconData => {
@@ -79,5 +90,74 @@ module.exports = {
WIKI.logger.error(`Authentication Strategy: [ FAILED ]`)
WIKI.logger.error(err)
}
+ },
+
+ /**
+ * Authenticate current request
+ *
+ * @param {Express Request} req
+ * @param {Express Response} res
+ * @param {Express Next Callback} next
+ */
+ authenticate(req, res, next) {
+ WIKI.auth.passport.authenticate('jwt', {session: false}, async (err, user, info) => {
+ if (err) { return next() }
+
+ // Expired but still valid within N days, just renew
+ if (info instanceof Error && info.name === 'TokenExpiredError' && moment().subtract(14, 'days').isBefore(info.expiredAt)) {
+ const jwtPayload = jwt.decode(securityHelper.extractJWT(req))
+ try {
+ const newToken = await WIKI.models.users.refreshToken(jwtPayload.id)
+ user = newToken.user
+
+ // Try headers, otherwise cookies for response
+ if (req.get('content-type') === 'application/json') {
+ res.set('new-jwt', newToken.token)
+ } else {
+ res.cookie('jwt', newToken.token, { expires: moment().add(365, 'days').toDate() })
+ }
+ } catch (err) {
+ return next()
+ }
+ }
+
+ // JWT is NOT valid, set as guest
+ if (!user) {
+ if (WIKI.auth.guest.cacheExpiration ) {
+ WIKI.auth.guest = await WIKI.models.users.getGuestUser()
+ WIKI.auth.guest.cacheExpiration = moment.utc().add(1, 'm')
+ }
+ req.user = WIKI.auth.guest
+ return next()
+ }
+
+ // JWT is valid
+ req.logIn(user, { session: false }, (err) => {
+ if (err) { return next(err) }
+ next()
+ })
+ })(req, res, next)
+ },
+
+ /**
+ * Check if user has access to resource
+ *
+ * @param {User} user
+ * @param {Array} permissions
+ * @param {String|Boolean} path
+ */
+ checkAccess(user, permissions = [], path = false) {
+ // System Admin
+ if (_.includes(user.permissions, 'manage:system')) {
+ return true
+ }
+
+ // Check Global Permissions
+ if (_.intersection(user.permissions, permissions).length < 1) {
+ return false
+ }
+
+ // Check Page Rules
+ return false
}
}
diff --git a/server/core/config.js b/server/core/config.js
index e45b7e8b..dfe26ea2 100644
--- a/server/core/config.js
+++ b/server/core/config.js
@@ -52,8 +52,6 @@ module.exports = {
appconfig.port = process.env.PORT || 80
}
- appconfig.public = (appconfig.public === true || _.toLower(appconfig.public) === 'true')
-
WIKI.config = appconfig
WIKI.data = appdata
WIKI.version = require(path.join(WIKI.ROOTPATH, 'package.json')).version
diff --git a/server/db/migrations/2.0.0.js b/server/db/migrations/2.0.0-beta.1.js
similarity index 89%
rename from server/db/migrations/2.0.0.js
rename to server/db/migrations/2.0.0-beta.1.js
index 49b92a14..4ec6fd1e 100644
--- a/server/db/migrations/2.0.0.js
+++ b/server/db/migrations/2.0.0-beta.1.js
@@ -1,11 +1,14 @@
exports.up = knex => {
+ const dbCompat = {
+ charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)
+ }
return knex.schema
// =====================================
// MODEL TABLES
// =====================================
// ASSETS ------------------------------
.createTable('assets', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.increments('id').primary()
table.string('filename').notNullable()
table.string('basename').notNullable()
@@ -19,7 +22,7 @@ exports.up = knex => {
})
// ASSET FOLDERS -----------------------
.createTable('assetFolders', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.increments('id').primary()
table.string('name').notNullable()
table.string('slug').notNullable()
@@ -27,7 +30,7 @@ exports.up = knex => {
})
// AUTHENTICATION ----------------------
.createTable('authentication', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.string('key').notNullable().primary()
table.boolean('isEnabled').notNullable().defaultTo(false)
table.json('config').notNullable()
@@ -37,7 +40,7 @@ exports.up = knex => {
})
// COMMENTS ----------------------------
.createTable('comments', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.increments('id').primary()
table.text('content').notNullable()
table.string('createdAt').notNullable()
@@ -45,14 +48,14 @@ exports.up = knex => {
})
// EDITORS -----------------------------
.createTable('editors', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.string('key').notNullable().primary()
table.boolean('isEnabled').notNullable().defaultTo(false)
table.json('config').notNullable()
})
// GROUPS ------------------------------
.createTable('groups', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.increments('id').primary()
table.string('name').notNullable()
table.json('permissions').notNullable()
@@ -63,7 +66,7 @@ exports.up = knex => {
})
// LOCALES -----------------------------
.createTable('locales', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.string('code', 2).notNullable().primary()
table.json('strings')
table.boolean('isRTL').notNullable().defaultTo(false)
@@ -74,7 +77,7 @@ exports.up = knex => {
})
// LOGGING ----------------------------
.createTable('loggers', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.string('key').notNullable().primary()
table.boolean('isEnabled').notNullable().defaultTo(false)
table.string('level').notNullable().defaultTo('warn')
@@ -82,13 +85,13 @@ exports.up = knex => {
})
// NAVIGATION ----------------------------
.createTable('navigation', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.string('key').notNullable().primary()
table.json('config')
})
// PAGE HISTORY ------------------------
.createTable('pageHistory', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.increments('id').primary()
table.string('path').notNullable()
table.string('hash').notNullable()
@@ -104,7 +107,7 @@ exports.up = knex => {
})
// PAGES -------------------------------
.createTable('pages', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.increments('id').primary()
table.string('path').notNullable()
table.string('hash').notNullable()
@@ -124,7 +127,7 @@ exports.up = knex => {
})
// PAGE TREE ---------------------------
.createTable('pageTree', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.increments('id').primary()
table.string('path').notNullable()
table.integer('depth').unsigned().notNullable()
@@ -135,28 +138,28 @@ exports.up = knex => {
})
// RENDERERS ---------------------------
.createTable('renderers', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.string('key').notNullable().primary()
table.boolean('isEnabled').notNullable().defaultTo(false)
table.json('config')
})
// SEARCH ------------------------------
.createTable('searchEngines', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.string('key').notNullable().primary()
table.boolean('isEnabled').notNullable().defaultTo(false)
table.json('config')
})
// SETTINGS ----------------------------
.createTable('settings', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.string('key').notNullable().primary()
table.json('value')
table.string('updatedAt').notNullable()
})
// STORAGE -----------------------------
.createTable('storage', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.string('key').notNullable().primary()
table.boolean('isEnabled').notNullable().defaultTo(false)
table.string('mode', ['sync', 'push', 'pull']).notNullable().defaultTo('push')
@@ -164,7 +167,7 @@ exports.up = knex => {
})
// TAGS --------------------------------
.createTable('tags', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.increments('id').primary()
table.string('tag').notNullable().unique()
table.string('title')
@@ -173,7 +176,7 @@ exports.up = knex => {
})
// USER KEYS ---------------------------
.createTable('userKeys', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.increments('id').primary()
table.string('kind').notNullable()
table.string('token').notNullable()
@@ -182,7 +185,7 @@ exports.up = knex => {
})
// USERS -------------------------------
.createTable('users', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.increments('id').primary()
table.string('email').notNullable()
table.string('name').notNullable()
@@ -205,21 +208,21 @@ exports.up = knex => {
// =====================================
// PAGE HISTORY TAGS ---------------------------
.createTable('pageHistoryTags', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.increments('id').primary()
table.integer('pageId').unsigned().references('id').inTable('pageHistory').onDelete('CASCADE')
table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')
})
// PAGE TAGS ---------------------------
.createTable('pageTags', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.increments('id').primary()
table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')
})
// USER GROUPS -------------------------
.createTable('userGroups', table => {
- table.charset('utf8mb4')
+ if (dbCompat.charset) { table.charset('utf8mb4') }
table.increments('id').primary()
table.integer('userId').unsigned().references('id').inTable('users').onDelete('CASCADE')
table.integer('groupId').unsigned().references('id').inTable('groups').onDelete('CASCADE')
diff --git a/server/db/seeds/settings.js b/server/db/seeds/settings.js
deleted file mode 100644
index d8c30727..00000000
--- a/server/db/seeds/settings.js
+++ /dev/null
@@ -1,11 +0,0 @@
-exports.seed = (knex, Promise) => {
- return knex('settings')
- .insert([
- { key: 'auth', value: {} },
- { key: 'features', value: {} },
- { key: 'logging', value: {} },
- { key: 'site', value: {} },
- { key: 'theme', value: {} },
- { key: 'uploads', value: {} }
- ])
-}
diff --git a/server/graph/resolvers/authentication.js b/server/graph/resolvers/authentication.js
index b52eb7d6..fa3b13d2 100644
--- a/server/graph/resolvers/authentication.js
+++ b/server/graph/resolvers/authentication.js
@@ -70,6 +70,13 @@ module.exports = {
},
async updateStrategies(obj, args, context) {
try {
+ WIKI.config.auth = {
+ audience: _.get(args, 'config.audience', WIKI.config.auth.audience),
+ tokenExpiration: _.get(args, 'config.tokenExpiration', WIKI.config.auth.tokenExpiration),
+ tokenRenewal: _.get(args, 'config.tokenRenewal', WIKI.config.auth.tokenRenewal)
+ }
+ await WIKI.configSvc.saveToDb(['auth'])
+
for (let str of args.strategies) {
await WIKI.models.authentication.query().patch({
isEnabled: str.isEnabled,
diff --git a/server/graph/schemas/authentication.graphql b/server/graph/schemas/authentication.graphql
index e361762a..ee69dc23 100644
--- a/server/graph/schemas/authentication.graphql
+++ b/server/graph/schemas/authentication.graphql
@@ -43,7 +43,8 @@ type AuthenticationMutation {
): AuthenticationRegisterResponse
updateStrategies(
- strategies: [AuthenticationStrategyInput]
+ strategies: [AuthenticationStrategyInput]!
+ config: AuthenticationConfigInput
): DefaultResponse @auth(requires: ["manage:system"])
}
@@ -88,3 +89,9 @@ input AuthenticationStrategyInput {
domainWhitelist: [String]!
autoEnrollGroups: [Int]!
}
+
+input AuthenticationConfigInput {
+ audience: String!
+ tokenExpiration: String!
+ tokenRenewal: String!
+}
diff --git a/server/master.js b/server/master.js
index 818bd3f8..8f2e2990 100644
--- a/server/master.js
+++ b/server/master.js
@@ -67,7 +67,7 @@ module.exports = async () => {
app.use(cookieParser())
app.use(WIKI.auth.passport.initialize())
- app.use(mw.auth.jwt)
+ app.use(WIKI.auth.authenticate)
// ----------------------------------------
// SEO
@@ -138,8 +138,7 @@ module.exports = async () => {
// ----------------------------------------
app.use('/', ctrl.auth)
-
- app.use('/', mw.auth.checkPath, ctrl.common)
+ app.use('/', ctrl.common)
// ----------------------------------------
// Error handling
diff --git a/server/middlewares/auth.js b/server/middlewares/auth.js
deleted file mode 100644
index 8ee13bc4..00000000
--- a/server/middlewares/auth.js
+++ /dev/null
@@ -1,72 +0,0 @@
-const jwt = require('jsonwebtoken')
-const moment = require('moment')
-
-const securityHelper = require('../helpers/security')
-
-/* global WIKI */
-
-/**
- * Authentication middleware
- */
-module.exports = {
- jwt(req, res, next) {
- WIKI.auth.passport.authenticate('jwt', {session: false}, async (err, user, info) => {
- if (err) { return next() }
-
- // Expired but still valid within 7 days, just renew
- if (info instanceof Error && info.name === 'TokenExpiredError' && moment().subtract(14, 'days').isBefore(info.expiredAt)) {
- const jwtPayload = jwt.decode(securityHelper.extractJWT(req))
- try {
- const newToken = await WIKI.models.users.refreshToken(jwtPayload.id)
- user = newToken.user
-
- // Try headers, otherwise cookies for response
- if (req.get('content-type') === 'application/json') {
- res.set('new-jwt', newToken.token)
- } else {
- res.cookie('jwt', newToken.token, { expires: moment().add(365, 'days').toDate() })
- }
- } catch (err) {
- return next()
- }
- }
-
- // JWT is NOT valid
- if (!user) { return next() }
-
- // JWT is valid
- req.logIn(user, { session: false }, (err) => {
- if (err) { return next(err) }
- next()
- })
- })(req, res, next)
- },
- checkPath(req, res, next) {
- // Is user authenticated ?
-
- if (!req.isAuthenticated()) {
- if (WIKI.config.public !== true) {
- return res.redirect('/login')
- } else {
- // req.user = rights.guest
- res.locals.isGuest = true
- }
- } else {
- res.locals.isGuest = false
- }
-
- // Check permissions
-
- // res.locals.rights = rights.check(req)
-
- // if (!res.locals.rights.read) {
- // return res.render('error-forbidden')
- // }
-
- // Expose user data
-
- res.locals.user = req.user
-
- return next()
- }
-}
diff --git a/server/models/users.js b/server/models/users.js
index f91a74ce..dc0aae07 100644
--- a/server/models/users.js
+++ b/server/models/users.js
@@ -138,6 +138,11 @@ module.exports = class User extends Model {
return (result && _.has(result, 'delta') && result.delta === 0)
}
+ async getPermissions() {
+ const permissions = await this.$relatedQuery('groups').select('permissions').pluck('permissions')
+ this.permissions = _.uniq(_.flatten(permissions))
+ }
+
static async processProfile(profile) {
let primaryEmail = ''
if (_.isArray(profile.emails)) {
@@ -262,8 +267,8 @@ module.exports = class User extends Model {
passphrase: WIKI.config.sessionSecret
}, {
algorithm: 'RS256',
- expiresIn: '30m',
- audience: 'urn:wiki.js', // TODO: use value from admin
+ expiresIn: WIKI.config.auth.tokenExpiration,
+ audience: WIKI.config.auth.audience,
issuer: 'urn:wiki.js'
}),
user
@@ -391,4 +396,10 @@ module.exports = class User extends Model {
throw new WIKI.Error.AuthRegistrationDisabled()
}
}
+
+ static async getGuestUser () {
+ let user = await WIKI.models.users.query().findById(2)
+ user.getPermissions()
+ return user
+ }
}
diff --git a/server/setup.js b/server/setup.js
index b35cde35..81dc5fd3 100644
--- a/server/setup.js
+++ b/server/setup.js
@@ -104,8 +104,12 @@ module.exports = () => {
await fs.ensureDir(path.join(dataPath, 'uploads'))
// Set config
+ _.set(WIKI.config, 'auth', {
+ audience: 'urn:wiki.js',
+ tokenExpiration: '30m',
+ tokenRenewal: '14d'
+ })
_.set(WIKI.config, 'company', '')
- _.set(WIKI.config, 'defaultEditor', 'markdown')
_.set(WIKI.config, 'features', {
featurePageRatings: true,
featurePageComments: true,
@@ -136,7 +140,6 @@ module.exports = () => {
dkimKeySelector: '',
dkimPrivateKey: ''
})
- _.set(WIKI.config, 'public', false)
_.set(WIKI.config, 'seo', {
description: '',
robots: ['index', 'follow'],
@@ -145,7 +148,7 @@ module.exports = () => {
})
_.set(WIKI.config, 'sessionSecret', (await crypto.randomBytesAsync(32)).toString('hex'))
_.set(WIKI.config, 'telemetry', {
- isEnabled: req.body.telemetry === 'true',
+ isEnabled: req.body.telemetry === true,
clientId: WIKI.telemetry.cid
})
_.set(WIKI.config, 'theming', {
@@ -179,16 +182,15 @@ module.exports = () => {
// Save config to DB
WIKI.logger.info('Persisting config to DB...')
await WIKI.configSvc.saveToDb([
+ 'auth',
'certs',
'company',
- 'defaultEditor',
'features',
'graphEndpoint',
'host',
'lang',
'logo',
'mail',
- 'public',
'seo',
'sessionSecret',
'telemetry',
@@ -389,8 +391,10 @@ module.exports = () => {
WIKI.server.on('listening', () => {
WIKI.logger.info('HTTP Server: [ RUNNING ]')
- WIKI.logger.info('========================================')
- WIKI.logger.info(`Browse to http://localhost:${WIKI.config.port}/`)
- WIKI.logger.info('========================================')
+ WIKI.logger.info('π»π»π»π»π»π»π»π»π»π»π»π»π»π»π»π»π»π»π»π»π»π»π»π»π»π»π»π»π»')
+ WIKI.logger.info('')
+ WIKI.logger.info(`Browse to http://localhost:${WIKI.config.port}/ to complete setup!`)
+ WIKI.logger.info('')
+ WIKI.logger.info('πΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊπΊ')
})
}
diff --git a/server/views/unauthorized.pug b/server/views/unauthorized.pug
new file mode 100644
index 00000000..61d22f00
--- /dev/null
+++ b/server/views/unauthorized.pug
@@ -0,0 +1,13 @@
+extends master.pug
+
+block body
+ #root.is-fullscreen
+ v-app
+ .unauthorized
+ .unauthorized-content
+ img.animated.fadeIn(src='/svg/icon-delete-shield.svg', alt='Unauthorized')
+ .headline= t('unauthorized.title')
+ .subheading.mt-3= t('unauthorized.action.' + action)
+ v-btn.mt-5(color='red lighten-4', href='javascript:window.history.go(-1);', large, outline)
+ v-icon(left) arrow_back
+ span= t('unauthorized.goback')