feat: guest + user permissions

This commit is contained in:
Nicolas Giard 2019-01-06 22:03:34 -05:00
parent aa57ea920e
commit 75eb277401
33 changed files with 463 additions and 191 deletions

View File

@ -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')
</template>

View File

@ -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',

View File

@ -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 {
<style lang='scss'>
#graphiql {
height: calc(100vh - 230px);
height: calc(100vh - 270px);
.topBar {
background-color: mc('grey', '200');
@ -136,10 +146,14 @@ export default {
background-color: initial;
box-shadow: initial;
}
.doc-explorer-title-bar, .history-title-bar {
height: auto;
}
}
#voyager {
height: calc(100vh - 250px);
height: calc(100vh - 270px);
.title-area {
display: none;
@ -147,5 +161,22 @@ export default {
.type-doc {
margin-top: 5px;
}
.doc-navigation {
> span {
overflow-y: hidden;
display: block;
}
min-height: 40px;
}
.contents {
padding-bottom: 0;
color: #666;
}
.type-info-popover {
display: none;
}
}
</style>

View File

@ -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 !== ``'

View File

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

View File

@ -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'

View File

@ -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')

View File

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

View File

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

View File

@ -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,

View File

@ -234,7 +234,7 @@ export default {
if (!token.type) { return }
console.info(token)
// console.info(token)
},
/**
* Update scroll sync

View File

@ -1,6 +1,6 @@
mutation($strategies: [AuthenticationStrategyInput]) {
mutation($strategies: [AuthenticationStrategyInput]!, $config: AuthenticationConfigInput) {
authentication {
updateStrategies(strategies: $strategies) {
updateStrategies(strategies: $strategies, config: $config) {
responseResult {
succeeded
errorCode

View File

@ -23,6 +23,7 @@
// @import 'node_modules/diff2html/dist/diff2html.min';
@import 'pages/new';
@import 'pages/unauthorized';
@import 'pages/welcome';
@import 'pages/error';

View File

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

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
<g>
<g>
<path d="M64,116c-0.5,0 -1,-0.1 -1.5,-0.4c-26,-15.4 -41.5,-42.6 -41.5,-72.7l0,-15.1c0,-1.3 0.8,-2.4 2,-2.8l40,-14.8c0.7,-0.2 1.4,-0.2 2.1,0l39.9,14.8c1.2,0.4 2,1.6 2,2.8l0,15.1c0,7.8 -1.1,15.6 -3.2,23.1c-0.4,1.6 -2.1,2.5 -3.7,2.1c-1.6,-0.4 -2.5,-2.1 -2.1,-3.7c2,-6.9 2.9,-14.1 2.9,-21.4l0,-13l-36.9,-13.8l-37,13.7l0,13c0,28 14.4,53.2 38.5,67.5c1.4,0.8 1.9,2.7 1.1,4.1c-0.6,1 -1.6,1.5 -2.6,1.5Z" style="fill:#fff;fill-rule:nonzero;"/>
<path d="M64,116c-1,0 -2,-0.5 -2.6,-1.5c-0.8,-1.4 -0.4,-3.3 1.1,-4.1c3.2,-1.9 6.3,-4 9.2,-6.3c1.3,-1 3.2,-0.8 4.2,0.5c1,1.3 0.8,3.2 -0.5,4.2c-3.1,2.5 -6.4,4.8 -9.9,6.8c-0.5,0.3 -1,0.4 -1.5,0.4Z" style="fill:#fff;fill-rule:nonzero;"/>
<path d="M64,101.6c-0.6,0 -1.3,-0.2 -1.8,-0.6c-15.9,-11.7 -26,-29 -28.6,-48.5c-0.2,-1.6 0.9,-3.1 2.6,-3.4c1.6,-0.2 3.1,0.9 3.4,2.6c2.3,17.1 10.9,32.3 24.4,43c15.9,-12.6 25,-31.4 25,-51.9l0,-4.6l-25,-9.2l-27,10c-1.6,0.6 -3.3,-0.2 -3.9,-1.8c-0.6,-1.6 0.2,-3.3 1.8,-3.9l28.1,-10.3c0.7,-0.2 1.4,-0.2 2.1,0l28,10.4c1.2,0.4 2,1.6 2,2.8l0,6.7c0,23.2 -10.6,44.4 -29.2,58.1c-0.6,0.4 -1.3,0.6 -1.9,0.6Z" style="fill:#d0d4d8;fill-rule:nonzero;"/>
</g>
<path d="M64,25.8l0,72.8c17.7,-13.1 28,-33.4 28,-55.7l0,-6.7l-28,-10.4Z" style="fill:#d0d4d8;fill-rule:nonzero;"/>
<path d="M89.857,82.858c7.805,-7.805 20.478,-7.805 28.284,0c7.805,7.805 7.805,20.479 0,28.284c-7.806,7.805 -20.479,7.805 -28.284,0c-7.806,-7.805 -7.806,-20.479 0,-28.284Z" style="fill:#ff5576;"/>
<path d="M110.9,100l-14,0c-1.7,0 -3,-1.3 -3,-3c0,-1.7 1.3,-3 3,-3l14,0c1.7,0 3,1.3 3,3c0,1.7 -1.3,3 -3,3Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve" width="128px" height="128px">
<path style="fill:#FFFFFF;" d="M64,14c-27.6,0-50,22.4-50,50s22.4,50,50,50s50-22.4,50-50S91.6,14,64,14z M64,89 c-13.8,0-25-11.2-25-25s11.2-25,25-25s25,11.2,25,25S77.8,89,64,89z"/>
<g>
<path style="fill:#FF5576;" d="M14,64c0,6.5,1.2,12.7,3.5,18.4l22.7-10.6C39.4,69.3,39,66.7,39,64c0-9,4.7-16.8,11.8-21.2L40.2,20 C24.6,28.5,14,45,14,64z"/>
</g>
<g>
<path style="fill:#FF5576;" d="M64,14c-6.5,0-12.7,1.2-18.4,3.5l10.6,22.7c2.4-0.8,5.1-1.2,7.8-1.2c9,0,16.8,4.7,21.2,11.8 L108,40.2C99.5,24.6,83,14,64,14z"/>
</g>
<path style="fill:#C3DBEA;" d="M55,37.5L52.4,32c-1.9,0.7-3.7,1.6-5.4,2.6l2.5,5.5C51.2,39,53,38.2,55,37.5z"/>
<g>
<path style="fill:#FF5576;" d="M64,89c-9,0-16.8-4.7-21.2-11.8L20,87.8c8.5,15.6,25,26.2,44,26.2c6.5,0,12.7-1.2,18.4-3.5 L71.8,87.8C69.3,88.6,66.7,89,64,89z"/>
</g>
<g>
<path style="fill:#FF5576;" d="M114,64c0-6.5-1.2-12.7-3.5-18.4L87.8,56.2c0.8,2.4,1.2,5.1,1.2,7.8c0,9-4.7,16.8-11.8,21.2 L87.8,108C103.4,99.5,114,83,114,64z"/>
</g>
<path style="fill:#C3DBEA;" d="M81.1,107.8c1.9-0.7,3.7-1.6,5.4-2.5L84,99.8c-1.7,1-3.6,1.8-5.4,2.5L81.1,107.8z"/>
<path style="fill:#FFFFFF;" d="M21.5,55.7c-0.3,0-0.5,0-0.8-0.1c-1.6-0.4-2.6-2.1-2.1-3.7c2.5-9.5,7.9-17.9,15.6-24.2 c1.3-1.1,3.2-0.9,4.2,0.4c1.1,1.3,0.9,3.2-0.4,4.2c-6.7,5.5-11.4,12.8-13.6,21.1C24,54.8,22.8,55.7,21.5,55.7z"/>
<path style="fill:#FFFFFF;" d="M20,67c-0.8,0-1.6-0.3-2.1-0.9c-0.1-0.1-0.3-0.3-0.4-0.4c-0.1-0.2-0.2-0.3-0.3-0.5 c-0.1-0.2-0.1-0.4-0.2-0.6c0-0.2-0.1-0.4-0.1-0.6c0-0.8,0.3-1.6,0.9-2.1c1.1-1.1,3.1-1.1,4.2,0c0.6,0.6,0.9,1.3,0.9,2.1 c0,0.8-0.3,1.6-0.9,2.1C21.6,66.7,20.8,67,20,67z"/>
<path style="fill:#DB3E64;" d="M33,67c-1.7,0-3-1.3-3-3c0-18.7,15.3-34,34-34c1.7,0,3,1.3,3,3s-1.3,3-3,3c-2.7,0-5.3,2.6-7.8,4.2 c0,0-1.4-1.4-2.9-0.7c-0.9,0.4-1.9,2.9-2.5,3.2l-2.9,1.5C39.8,49.1,36,53.9,36,64C36,65.7,34.7,67,33,67z"/>
<path style="fill:#FF5576;" d="M56.2,40.2L52.4,32c-1.9,0.7-3.7,1.5-5.4,2.6l3.8,8.2C52.5,41.7,54.3,40.9,56.2,40.2z"/>
<path style="fill:#DB3E64;" d="M64,111c-1.7,0-3-1.3-3-3s1.3-3,3-3c22.6,0,41-18.4,41-41c0-1.7,1.3-3,3-3s3,1.3,3,3 c0,16.2-8.2,30.5-20.7,39c-1.2,0.8-1.2,4.3-2.4,5c-0.7,0.4-2.7-1.1-3.5-0.8c-1.1,0.5-0.9,2.9-2,3.3c-1.3,0.5-3.8-1.8-5.2-1.4 C73,110.3,68.6,111,64,111z"/>
<path style="fill:#FF5576;" d="M82.4,110.5c1.9-0.7,3.7-1.6,5.4-2.5L84,99.8c-1.7,1-3.6,1.8-5.4,2.5L82.4,110.5z"/>
<path style="fill:#444B54;" d="M111,64c0,16.8-8.9,31.6-22.2,39.9c-1.3,0.8-1.8,2.4-1.1,3.8l0,0c0.8,1.6,2.8,2.2,4.3,1.3 c15-9.4,25-26,25-45c0-5.7-0.9-11.3-2.6-16.4c-0.6-1.7-2.5-2.6-4.1-1.8l0,0c-1.4,0.6-2,2.2-1.6,3.7C110.2,54,111,58.9,111,64z"/>
<path style="fill:#444B54;" d="M64,111c-16.8,0-31.6-8.9-39.9-22.2c-0.8-1.3-2.4-1.8-3.8-1.1l0,0c-1.6,0.8-2.2,2.8-1.3,4.3 c9.4,15,26,25,45,25c5.7,0,11.3-0.9,16.4-2.6c1.7-0.6,2.6-2.5,1.8-4.1l0,0c-0.6-1.4-2.2-2-3.7-1.6C74,110.2,69.1,111,64,111z"/>
<path style="fill:#444B54;" d="M42,64c0-6.9,3.2-13,8.1-17.1c1.1-0.9,1.5-2.4,0.9-3.6l0,0c-0.8-1.8-3.1-2.3-4.6-1.1 C40.1,47.3,36,55.2,36,64c0,1.9,0.2,3.7,0.5,5.5c0.4,1.9,2.5,2.9,4.2,2.1l0,0c1.2-0.6,1.9-1.9,1.7-3.2C42.1,66.9,42,65.5,42,64z"/>
<path style="fill:#444B54;" d="M64,42c6.9,0,13,3.2,17.1,8.1c0.9,1.1,2.4,1.5,3.6,0.9l0,0c1.8-0.8,2.3-3.1,1.1-4.6 C80.7,40.1,72.8,36,64,36c-1.9,0-3.7,0.2-5.5,0.5c-1.9,0.4-2.9,2.5-2.1,4.2l0,0c0.6,1.2,1.9,1.9,3.2,1.7C61.1,42.1,62.5,42,64,42z"/>
<path style="fill:#444B54;" d="M64,86c-6.9,0-13-3.2-17.1-8.1c-0.9-1.1-2.4-1.5-3.6-0.9l0,0c-1.8,0.8-2.3,3.1-1.1,4.6 C47.3,87.9,55.2,92,64,92c1.9,0,3.7-0.2,5.5-0.5c1.9-0.4,2.9-2.5,2.1-4.2l0,0c-0.6-1.2-1.9-1.9-3.2-1.7C66.9,85.9,65.5,86,64,86z"/>
<path style="fill:#444B54;" d="M86,64c0,6.9-3.2,13-8.1,17.1c-1.1,0.9-1.5,2.4-0.9,3.6l0,0c0.8,1.8,3.1,2.3,4.6,1.1 C87.9,80.7,92,72.8,92,64c0-1.9-0.2-3.7-0.5-5.5c-0.4-1.9-2.5-2.9-4.2-2.1l0,0c-1.2,0.6-1.9,1.9-1.7,3.2C85.9,61.1,86,62.5,86,64z"/>
<path style="fill:#444B54;" d="M17,64c0-16.8,8.9-31.6,22.2-39.9c1.3-0.8,1.8-2.4,1.1-3.8l0,0c-0.8-1.6-2.8-2.2-4.3-1.3 c-15,9.4-25,26-25,45c0,5.7,0.9,11.3,2.6,16.4c0.6,1.7,2.5,2.6,4.1,1.8l0,0c1.4-0.6,2-2.2,1.6-3.7C17.8,74,17,69.1,17,64z"/>
<path style="fill:#444B54;" d="M64,17c16.8,0,31.6,8.9,39.9,22.2c0.8,1.3,2.4,1.8,3.8,1.1l0,0c1.6-0.8,2.2-2.8,1.3-4.3 c-9.4-15-26-25-45-25c-5.7,0-11.3,0.9-16.4,2.6c-1.7,0.6-2.6,2.5-1.8,4.1l0,0c0.6,1.4,2.2,2,3.7,1.6C54,17.8,58.9,17,64,17z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,3 @@
<svg width="100%" height="100%" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
<path d="M40,0l-40,40l0,-20l20,-20l20,0Zm0,40l0,-20l-20,20l20,0Z" fill='#FFFFFF' fill-opacity='0.03' fill-rule='evenodd' />
</svg>

After

Width:  |  Height:  |  Size: 223 B

View File

@ -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: {},

View File

@ -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" ]

View File

@ -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:

View File

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

View File

@ -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 })
}
})

View File

@ -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<String>} 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
}
}

View File

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

View File

@ -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')

View File

@ -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: {} }
])
}

View File

@ -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,

View File

@ -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!
}

View File

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

View File

@ -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()
}
}

View File

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

View File

@ -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('🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺')
})
}

View File

@ -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')