feat: guest + user permissions
This commit is contained in:
parent
aa57ea920e
commit
75eb277401
@ -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>
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
|
@ -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 !== ``'
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -234,7 +234,7 @@ export default {
|
||||
|
||||
if (!token.type) { return }
|
||||
|
||||
console.info(token)
|
||||
// console.info(token)
|
||||
},
|
||||
/**
|
||||
* Update scroll sync
|
||||
|
@ -1,6 +1,6 @@
|
||||
mutation($strategies: [AuthenticationStrategyInput]) {
|
||||
mutation($strategies: [AuthenticationStrategyInput]!, $config: AuthenticationConfigInput) {
|
||||
authentication {
|
||||
updateStrategies(strategies: $strategies) {
|
||||
updateStrategies(strategies: $strategies, config: $config) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
|
@ -23,6 +23,7 @@
|
||||
// @import 'node_modules/diff2html/dist/diff2html.min';
|
||||
|
||||
@import 'pages/new';
|
||||
@import 'pages/unauthorized';
|
||||
@import 'pages/welcome';
|
||||
@import 'pages/error';
|
||||
|
||||
|
81
client/scss/pages/_unauthorized.scss
Normal file
81
client/scss/pages/_unauthorized.scss
Normal 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;
|
||||
}
|
||||
}
|
14
client/static/svg/icon-delete-shield.svg
Normal file
14
client/static/svg/icon-delete-shield.svg
Normal 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 |
33
client/static/svg/icon-safety-float.svg
Normal file
33
client/static/svg/icon-safety-float.svg
Normal 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 |
3
client/static/svg/motif-diagonals.svg
Normal file
3
client/static/svg/motif-diagonals.svg
Normal 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 |
@ -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: {},
|
||||
|
@ -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" ]
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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 })
|
||||
}
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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')
|
@ -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: {} }
|
||||
])
|
||||
}
|
@ -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,
|
||||
|
@ -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!
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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('🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺')
|
||||
})
|
||||
}
|
||||
|
13
server/views/unauthorized.pug
Normal file
13
server/views/unauthorized.pug
Normal 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')
|
Loading…
Reference in New Issue
Block a user