Unverified Commit c009cc13 authored by Nicolas Giard's avatar Nicolas Giard Committed by GitHub

feat: new login experience (#2139)

* feat: multiple auth instances * fix: auth setup + strategy initialization * feat: admin auth - add strategy * feat: redirect on login - group setting * feat: oauth2 generic - props definitions * feat: new login UI (wip) * feat: new login UI (wip) * feat: admin security login settings * feat: tabset editor indicators + print view improvements * fix: code styling
parent 1c4829f7
module.exports = {
classPrefix: 'mdz-',
options: ['setClasses'],
'feature-detects': [
'css/backdropfilter'
]
}
...@@ -129,7 +129,7 @@ ...@@ -129,7 +129,7 @@
v-list-item-avatar(size='24', tile): v-icon mdi-heart-outline v-list-item-avatar(size='24', tile): v-icon mdi-heart-outline
v-list-item-title {{ $t('admin:contribute.title') }} v-list-item-title {{ $t('admin:contribute.title') }}
v-content(:class='$vuetify.theme.dark ? "grey darken-5" : "grey lighten-5"') v-main(:class='$vuetify.theme.dark ? "grey darken-5" : "grey lighten-5"')
transition(name='admin-router') transition(name='admin-router')
router-view router-view
......
<template lang="pug"> <template lang="pug">
v-card(flat) v-card(flat)
v-card-text v-container.px-3.pb-3.pt-3(fluid, grid-list-md)
v-text-field( v-layout(row, wrap)
outlined v-flex(xs12, v-if='group.isSystem')
v-model='group.name' v-alert.radius-7.mb-0(
label='Group Name'
counter='255'
prepend-icon='mdi-account-group'
)
v-alert.radius-7(
v-if='group.isSystem'
color='orange darken-2' color='orange darken-2'
:class='$vuetify.theme.dark ? "grey darken-4" : "orange lighten-5"' :class='$vuetify.theme.dark ? "grey darken-4" : "orange lighten-5"'
outlined outlined
:value='true' :value='true'
icon='mdi-lock-outline' icon='mdi-lock-outline'
) This is a system group. Some permissions cannot be modified. ) This is a system group. Some permissions cannot be modified.
v-container.px-3.pb-3.pt-0(fluid, grid-list-md)
v-layout(row, wrap)
v-flex(xs12, md6, lg4, v-for='pmGroup in permissions', :key='pmGroup.category') v-flex(xs12, md6, lg4, v-for='pmGroup in permissions', :key='pmGroup.category')
v-card.md2(flat, :class='$vuetify.theme.dark ? "grey darken-3-d5" : "grey lighten-5"') v-card.md2(flat, :class='$vuetify.theme.dark ? "grey darken-3-d5" : "grey lighten-5"')
.overline.px-5.pt-5.pb-3.grey--text.text--darken-2 {{pmGroup.category}} .overline.px-5.pt-5.pb-3.grey--text.text--darken-2 {{pmGroup.category}}
......
<template lang="pug"> <template lang="pug">
v-card(flat) v-card(flat)
v-card-text(v-if='group.id === 1') v-card-text(v-if='group.id === 1')
v-alert.radius-7( v-alert.radius-7.mb-0(
:class='$vuetify.theme.dark ? "grey darken-4" : "orange lighten-5"' :class='$vuetify.theme.dark ? "grey darken-4" : "orange lighten-5"'
color='orange darken-2' color='orange darken-2'
outlined outlined
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
v-icon mdi-arrow-left v-icon mdi-arrow-left
v-dialog(v-model='deleteGroupDialog', max-width='500', v-if='!group.isSystem') v-dialog(v-model='deleteGroupDialog', max-width='500', v-if='!group.isSystem')
template(v-slot:activator='{ on }') template(v-slot:activator='{ on }')
v-btn.ml-2(color='red', icon, outlined, v-on='on') v-btn.ml-3(color='red', icon, outlined, v-on='on')
v-icon(color='red') mdi-trash-can-outline v-icon(color='red') mdi-trash-can-outline
v-card v-card
.dialog-header.is-red Delete Group? .dialog-header.is-red Delete Group?
...@@ -21,11 +21,14 @@ ...@@ -21,11 +21,14 @@
v-spacer v-spacer
v-btn(text, @click='deleteGroupDialog = false') Cancel v-btn(text, @click='deleteGroupDialog = false') Cancel
v-btn(color='red', dark, @click='deleteGroup') Delete v-btn(color='red', dark, @click='deleteGroup') Delete
v-btn.ml-2(color='success', large, depressed, @click='updateGroup') v-btn.ml-3(color='success', large, depressed, @click='updateGroup')
v-icon(left) mdi-check v-icon(left) mdi-check
span Update Group span Update Group
v-card.mt-3 v-card.mt-3
v-tabs.grad-tabs(v-model='tab', :color='$vuetify.theme.dark ? `blue` : `primary`', fixed-tabs, show-arrows, icons-and-text) v-tabs.grad-tabs(v-model='tab', :color='$vuetify.theme.dark ? `blue` : `primary`', fixed-tabs, show-arrows, icons-and-text)
v-tab(key='settings')
span Settings
v-icon mdi-cog-box
v-tab(key='permissions') v-tab(key='permissions')
span Permissions span Permissions
v-icon mdi-lock-pattern v-icon mdi-lock-pattern
...@@ -36,6 +39,44 @@ ...@@ -36,6 +39,44 @@
span Users span Users
v-icon mdi-account-group v-icon mdi-account-group
v-tab-item(key='settings', :transition='false', :reverse-transition='false')
v-card(flat)
template(v-if='group.id <= 2')
v-card-text
v-alert.radius-7.mb-0(
color='orange darken-2'
:class='$vuetify.theme.dark ? "grey darken-4" : "orange lighten-5"'
outlined
:value='true'
icon='mdi-lock-outline'
) This is a system group and its settings cannot be modified.
v-divider
v-card-text
v-text-field(
outlined
v-model='group.name'
label='Group Name'
hide-details
prepend-icon='mdi-account-group'
style='max-width: 600px;'
:disabled='group.id <= 2'
)
template(v-if='group.id > 2')
v-divider
v-card-text
v-text-field(
outlined
v-model='group.redirectOnLogin'
label='Redirect on Login'
persistent-hint
hint='The path / URL where the user will be redirected upon successful login.'
prepend-icon='mdi-arrow-top-left-thick'
append-icon='mdi-folder-search'
@click:append='selectPage'
style='max-width: 850px;'
:counter='255'
)
v-tab-item(key='permissions', :transition='false', :reverse-transition='false') v-tab-item(key='permissions', :transition='false', :reverse-transition='false')
group-permissions(v-model='group', @refresh='refresh') group-permissions(v-model='group', @refresh='refresh')
...@@ -44,21 +85,23 @@ ...@@ -44,21 +85,23 @@
v-tab-item(key='users', :transition='false', :reverse-transition='false') v-tab-item(key='users', :transition='false', :reverse-transition='false')
group-users(v-model='group', @refresh='refresh') group-users(v-model='group', @refresh='refresh')
v-card-chin v-card-chin
v-spacer v-spacer
.caption.grey--text.pr-2 Group ID #[strong {{group.id}}] .caption.grey--text.pr-2 Group ID #[strong {{group.id}}]
page-selector(mode='select', v-model='selectPageModal', :open-handler='selectPageHandle', path='home', :locale='currentLang')
</template> </template>
<script> <script>
import _ from 'lodash' import _ from 'lodash'
import gql from 'graphql-tag'
import GroupPermissions from './admin-groups-edit-permissions.vue' import GroupPermissions from './admin-groups-edit-permissions.vue'
import GroupRules from './admin-groups-edit-rules.vue' import GroupRules from './admin-groups-edit-rules.vue'
import GroupUsers from './admin-groups-edit-users.vue' import GroupUsers from './admin-groups-edit-users.vue'
import groupQuery from 'gql/admin/groups/groups-query-single.gql' /* global siteConfig */
import deleteGroupMutation from 'gql/admin/groups/groups-mutation-delete.gql'
import updateGroupMutation from 'gql/admin/groups/groups-mutation-update.gql'
export default { export default {
components: { components: {
...@@ -74,20 +117,55 @@ export default { ...@@ -74,20 +117,55 @@ export default {
isSystem: false, isSystem: false,
permissions: [], permissions: [],
pageRules: [], pageRules: [],
users: [] users: [],
redirectOnLogin: '/'
}, },
deleteGroupDialog: false, deleteGroupDialog: false,
tab: null tab: null,
selectPageModal: false,
currentLang: siteConfig.lang
} }
}, },
methods: { methods: {
selectPage () {
this.selectPageModal = true
},
selectPageHandle ({ path, locale }) {
this.group.redirectOnLogin = `/${locale}/${path}`
},
async updateGroup() { async updateGroup() {
try { try {
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: updateGroupMutation, mutation: gql`
mutation (
$id: Int!
$name: String!
$redirectOnLogin: String!
$permissions: [String]!
$pageRules: [PageRuleInput]!
) {
groups {
update(
id: $id
name: $name
redirectOnLogin: $redirectOnLogin
permissions: $permissions
pageRules: $pageRules
) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
`,
variables: { variables: {
id: this.group.id, id: this.group.id,
name: this.group.name, name: this.group.name,
redirectOnLogin: this.group.redirectOnLogin,
permissions: this.group.permissions, permissions: this.group.permissions,
pageRules: this.group.pageRules pageRules: this.group.pageRules
}, },
...@@ -108,7 +186,20 @@ export default { ...@@ -108,7 +186,20 @@ export default {
this.deleteGroupDialog = false this.deleteGroupDialog = false
try { try {
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: deleteGroupMutation, mutation: gql`
mutation ($id: Int!) {
groups {
delete(id: $id) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
`,
variables: { variables: {
id: this.group.id id: this.group.id
}, },
...@@ -132,7 +223,34 @@ export default { ...@@ -132,7 +223,34 @@ export default {
}, },
apollo: { apollo: {
group: { group: {
query: groupQuery, query: gql`
query ($id: Int!) {
groups {
single(id: $id) {
id
name
redirectOnLogin
isSystem
permissions
pageRules {
id
path
roles
match
deny
locales
}
users {
id
name
email
}
createdAt
updatedAt
}
}
}
`,
variables() { variables() {
return { return {
id: _.toSafeInteger(this.$route.params.id) id: _.toSafeInteger(this.$route.params.id)
......
...@@ -8,7 +8,9 @@ ...@@ -8,7 +8,9 @@
.headline.blue--text.text--darken-2.animated.fadeInLeft Groups .headline.blue--text.text--darken-2.animated.fadeInLeft Groups
.subtitle-1.grey--text.animated.fadeInLeft.wait-p4s Manage groups and their permissions .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s Manage groups and their permissions
v-spacer v-spacer
v-btn.animated.fadeInDown.wait-p2s.mr-3(color='grey', outlined, @click='refresh', icon) v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/groups', target='_blank')
v-icon mdi-help-circle
v-btn.animated.fadeInDown.wait-p2s.mx-3(color='grey', outlined, @click='refresh', icon)
v-icon mdi-refresh v-icon mdi-refresh
v-dialog(v-model='newGroupDialog', max-width='500') v-dialog(v-model='newGroupDialog', max-width='500')
template(v-slot:activator='{ on }') template(v-slot:activator='{ on }')
......
...@@ -93,25 +93,25 @@ ...@@ -93,25 +93,25 @@
.caption Defines the duration for which the server should only deliver content through HTTPS. .caption Defines the duration for which the server should only deliver content through HTTPS.
.caption It's a good idea to start with small values and make sure that nothing breaks on your wiki before moving to longer values. .caption It's a good idea to start with small values and make sure that nothing breaks on your wiki before moving to longer values.
v-divider.mt-3 //- v-divider.mt-3
v-switch( //- v-switch(
inset //- inset
label='Enforce CSP' //- label='Enforce CSP'
color='red darken-2' //- color='red darken-2'
v-model='config.securityCSP' //- v-model='config.securityCSP'
persistent-hint //- persistent-hint
hint='Restricts scripts to pre-approved content sources.' //- hint='Restricts scripts to pre-approved content sources.'
disabled //- disabled
) //- )
v-textarea.mt-5( //- v-textarea.mt-5(
label='CSP Directives' //- label='CSP Directives'
outlined //- outlined
v-model='config.securityCSPDirectives' //- v-model='config.securityCSPDirectives'
prepend-icon='mdi-subdirectory-arrow-right' //- prepend-icon='mdi-subdirectory-arrow-right'
persistent-hint //- persistent-hint
hint='One directive per line.' //- hint='One directive per line.'
disabled //- disabled
) //- )
v-flex(lg6 xs12) v-flex(lg6 xs12)
v-card.animated.fadeInUp.wait-p2s v-card.animated.fadeInUp.wait-p2s
...@@ -142,6 +142,62 @@ ...@@ -142,6 +142,62 @@
:suffix='$t(`admin:security.maxUploadBatchSuffix`)' :suffix='$t(`admin:security.maxUploadBatchSuffix`)'
style='max-width: 450px;' style='max-width: 450px;'
) )
v-card.mt-3.animated.fadeInUp.wait-p2s
v-toolbar(flat, color='primary', dark, dense)
.subtitle-1 {{$t('admin:security.login')}}
//- v-card-info(color='blue')
//- span {{$t('admin:security.loginInfo')}}
.overline.grey--text.pa-4 {{$t('admin:security.loginScreen')}}
.px-4.pb-3
v-text-field(
outlined
:label='$t(`admin:security.loginBgUrl`)'
v-model='config.authLoginBgUrl'
:hint='$t(`admin:security.loginBgUrlHint`)'
persistent-hint
prepend-icon='mdi-image-area'
append-icon='mdi-folder-image'
@click:append='browseLoginBg'
)
v-switch(
inset
:label='$t(`admin:security.bypassLogin`)'
color='red darken-2'
v-model='config.authAutoLogin'
prepend-icon='mdi-fast-forward'
persistent-hint
:hint='$t(`admin:security.bypassLoginHint`)'
)
v-divider.mt-3
.overline.grey--text.pa-4 {{$t('admin:security.jwt')}}
.px-4.pb-3
v-text-field(
v-model='config.authJwtAudience'
outlined
prepend-icon='mdi-account-group-outline'
:label='$t(`admin:auth.jwtAudience`)'
:hint='$t(`admin:auth.jwtAudienceHint`)'
persistent-hint
)
v-text-field.mt-3(
v-model='config.authJwtExpiration'
outlined
prepend-icon='mdi-clock-outline'
:label='$t(`admin:auth.tokenExpiration`)'
:hint='$t(`admin:auth.tokenExpirationHint`)'
persistent-hint
)
v-text-field.mt-3(
v-model='config.authJwtRenewablePeriod'
outlined
prepend-icon='mdi-update'
:label='$t(`admin:auth.tokenRenewalPeriod`)'
:hint='$t(`admin:auth.tokenRenewalPeriodHint`)'
persistent-hint
)
component(:is='activeModal')
</template> </template>
<script> <script>
...@@ -149,7 +205,17 @@ import _ from 'lodash' ...@@ -149,7 +205,17 @@ import _ from 'lodash'
import { sync } from 'vuex-pathify' import { sync } from 'vuex-pathify'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import editorStore from '../../store/editor'
/* global WIKI */
WIKI.$store.registerModule('editor', editorStore)
export default { export default {
i18nOptions: { namespaces: 'editor' },
components: {
editorModalMedia: () => import(/* webpackChunkName: "editor", webpackMode: "lazy" */ '../editor/editor-modal-media.vue')
},
data() { data() {
return { return {
config: { config: {
...@@ -163,7 +229,12 @@ export default { ...@@ -163,7 +229,12 @@ export default {
securityHSTS: false, securityHSTS: false,
securityHSTSDuration: 0, securityHSTSDuration: 0,
securityCSP: false, securityCSP: false,
securityCSPDirectives: '' securityCSPDirectives: '',
authAutoLogin: false,
authLoginBgUrl: '',
authJwtAudience: 'urn:wiki.js',
authJwtExpiration: '30m',
authJwtRenewablePeriod: '14d'
}, },
hstsDurations: [ hstsDurations: [
{ value: 300, text: '5 minutes' }, { value: 300, text: '5 minutes' },
...@@ -184,6 +255,11 @@ export default { ...@@ -184,6 +255,11 @@ export default {
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: gql` mutation: gql`
mutation ( mutation (
$authAutoLogin: Boolean
$authLoginBgUrl: String
$authJwtAudience: String
$authJwtExpiration: String
$authJwtRenewablePeriod: String
$uploadMaxFileSize: Int $uploadMaxFileSize: Int
$uploadMaxFiles: Int $uploadMaxFiles: Int
$securityOpenRedirect: Boolean $securityOpenRedirect: Boolean
...@@ -198,6 +274,11 @@ export default { ...@@ -198,6 +274,11 @@ export default {
) { ) {
site { site {
updateConfig( updateConfig(
authAutoLogin: $authAutoLogin,
authLoginBgUrl: $authLoginBgUrl,
authJwtAudience: $authJwtAudience,
authJwtExpiration: $authJwtExpiration,
authJwtRenewablePeriod: $authJwtRenewablePeriod,
uploadMaxFileSize: $uploadMaxFileSize, uploadMaxFileSize: $uploadMaxFileSize,
uploadMaxFiles: $uploadMaxFiles, uploadMaxFiles: $uploadMaxFiles,
securityOpenRedirect: $securityOpenRedirect, securityOpenRedirect: $securityOpenRedirect,
...@@ -221,6 +302,11 @@ export default { ...@@ -221,6 +302,11 @@ export default {
} }
`, `,
variables: { variables: {
authAutoLogin: _.get(this.config, 'authAutoLogin', false),
authLoginBgUrl: _.get(this.config, 'authLoginBgUrl', ''),
authJwtAudience: _.get(this.config, 'authJwtAudience', ''),
authJwtExpiration: _.get(this.config, 'authJwtExpiration', ''),
authJwtRenewablePeriod: _.get(this.config, 'authJwtRenewablePeriod', ''),
uploadMaxFileSize: _.toSafeInteger(_.get(this.config, 'uploadMaxFileSize', 0)), uploadMaxFileSize: _.toSafeInteger(_.get(this.config, 'uploadMaxFileSize', 0)),
uploadMaxFiles: _.toSafeInteger(_.get(this.config, 'uploadMaxFiles', 0)), uploadMaxFiles: _.toSafeInteger(_.get(this.config, 'uploadMaxFiles', 0)),
securityOpenRedirect: _.get(this.config, 'securityOpenRedirect', false), securityOpenRedirect: _.get(this.config, 'securityOpenRedirect', false),
...@@ -245,14 +331,31 @@ export default { ...@@ -245,14 +331,31 @@ export default {
} catch (err) { } catch (err) {
this.$store.commit('pushGraphError', err) this.$store.commit('pushGraphError', err)
} }
},
browseLoginBg () {
this.$store.set('editor/editorKey', 'common')
this.activeModal = 'editorModalMedia'
} }
}, },
mounted () {
this.$root.$on('editorInsert', opts => {
this.config.loginBgUrl = opts.path
})
},
beforeDestroy() {
this.$root.$off('editorInsert')
},
apollo: { apollo: {
config: { config: {
query: gql` query: gql`
{ {
site { site {
config { config {
authAutoLogin
authLoginBgUrl
authJwtAudience
authJwtExpiration
authJwtRenewablePeriod
uploadMaxFileSize uploadMaxFileSize
uploadMaxFiles uploadMaxFiles
securityOpenRedirect securityOpenRedirect
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
v-card-text.pt-5 v-card-text.pt-5
v-select( v-select(
:items='providers' :items='providers'
item-text='title' item-text='displayName'
item-value='key' item-value='key'
outlined outlined
prepend-icon='mdi-domain' prepend-icon='mdi-domain'
...@@ -230,19 +230,15 @@ export default { ...@@ -230,19 +230,15 @@ export default {
query: gql` query: gql`
query { query {
authentication { authentication {
strategies( activeStrategies {
isEnabled: true
) {
key key
title displayName
icon
color
} }
} }
} }
`, `,
fetchPolicy: 'network-only', fetchPolicy: 'network-only',
update: (data) => data.authentication.strategies, update: (data) => data.authentication.activeStrategies,
watchLoading (isLoading) { watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-strategies-refresh') this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-strategies-refresh')
} }
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
label='Identity Provider' label='Identity Provider'
:items='strategies' :items='strategies'
v-model='filterStrategy' v-model='filterStrategy'
item-text='title' item-text='displayName'
item-value='key' item-value='key'
style='max-width: 300px;' style='max-width: 300px;'
dense dense
...@@ -162,13 +162,9 @@ export default { ...@@ -162,13 +162,9 @@ export default {
query: gql` query: gql`
query { query {
authentication { authentication {
strategies( activeStrategies {
isEnabled: true
) {
key key
title displayName
icon
color
} }
} }
} }
...@@ -177,8 +173,8 @@ export default { ...@@ -177,8 +173,8 @@ export default {
update: (data) => { update: (data) => {
return _.concat({ return _.concat({
key: 'all', key: 'all',
title: 'All Providers' displayName: 'All Providers'
}, data.authentication.strategies) }, data.authentication.activeStrategies)
}, },
watchLoading (isLoading) { watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-strategies-refresh') this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-strategies-refresh')
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
top top
multi-line multi-line
v-model='notificationState' v-model='notificationState'
:timeout='6000'
) )
.text-left .text-left
v-icon.mr-3(dark) mdi-{{ notification.icon }} v-icon.mr-3(dark) mdi-{{ notification.icon }}
...@@ -26,16 +27,15 @@ export default { ...@@ -26,16 +27,15 @@ export default {
<style lang='scss'> <style lang='scss'>
.nav-notify { .nav-notify {
// top: 60px; top: -64px;
padding-top: 0;
z-index: 999; z-index: 999;
.v-snack__wrapper { .v-snack__wrapper {
border-top-left-radius: 0; border-top-left-radius: 0;
border-top-right-radius: 0; border-top-right-radius: 0;
}
.v-snack__content {
position: relative; position: relative;
margin-top: 0;
&::after { &::after {
content: ''; content: '';
......
...@@ -245,6 +245,7 @@ import mermaid from 'mermaid' ...@@ -245,6 +245,7 @@ import mermaid from 'mermaid'
// Helpers // Helpers
import katexHelper from './common/katex' import katexHelper from './common/katex'
import tabsetHelper from './markdown/tabset'
// ======================================== // ========================================
// INIT // INIT
...@@ -433,6 +434,7 @@ export default { ...@@ -433,6 +434,7 @@ export default {
this.$store.set('editor/content', newContent) this.$store.set('editor/content', newContent)
this.previewHTML = DOMPurify.sanitize(md.render(newContent)) this.previewHTML = DOMPurify.sanitize(md.render(newContent))
this.$nextTick(() => { this.$nextTick(() => {
tabsetHelper.format()
this.renderMermaidDiagrams() this.renderMermaidDiagrams()
Prism.highlightAllUnder(this.$refs.editorPreview) Prism.highlightAllUnder(this.$refs.editorPreview)
Array.from(this.$refs.editorPreview.querySelectorAll('pre.line-numbers')).forEach(pre => pre.classList.add('prismjs')) Array.from(this.$refs.editorPreview.querySelectorAll('pre.line-numbers')).forEach(pre => pre.classList.add('prismjs'))
...@@ -856,6 +858,44 @@ $editor-height-mobile: calc(100vh - 112px - 16px); ...@@ -856,6 +858,44 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
p.line { p.line {
overflow-wrap: break-word; overflow-wrap: break-word;
} }
.tabset {
background-color: mc('teal', '700');
color: mc('teal', '100') !important;
padding: 5px 12px;
font-size: 14px;
font-weight: 500;
border-radius: 5px 0 0 0;
font-style: italic;
&::after {
display: none;
}
&-header {
background-color: mc('teal', '500');
color: #FFF !important;
padding: 5px 12px;
font-size: 14px;
font-weight: 500;
margin-top: 0 !important;
&::after {
display: none;
}
}
&-content {
border-left: 5px solid mc('teal', '500');
background-color: mc('teal', '50');
padding: 0 15px 15px;
overflow: hidden;
@at-root .theme--dark & {
background-color: rgba(mc('teal', '500'), .1);
}
}
}
} }
} }
......
import cash from 'cash-dom'
import _ from 'lodash'
export default {
format () {
for (let i = 1; i < 6; i++) {
cash(`.editor-markdown-preview-content h${i}.tabset`).each((idx, elm) => {
elm.innerHTML = 'Tabset ( rendered upon saving )'
cash(elm).nextUntil(_.times(i, t => `h${t + 1}`).join(', '), `h${i + 1}`).each((hidx, hd) => {
hd.classList.add('tabset-header')
cash(hd).nextUntil(_.times(i + 1, t => `h${t + 1}`).join(', ')).wrapAll('<div class="tabset-content"></div>')
})
})
}
}
}
mutation($strategies: [AuthenticationStrategyInput]!, $config: AuthenticationConfigInput) {
authentication {
updateStrategies(strategies: $strategies, config: $config) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
mutation ($id: Int!) {
groups {
delete(id: $id) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
mutation ($id: Int!, $name: String!, $permissions: [String]!, $pageRules: [PageRuleInput]!) {
groups {
update(id: $id, name: $name, permissions: $permissions, pageRules: $pageRules) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
query ($id: Int!) {
groups {
single(id: $id) {
id
name
isSystem
permissions
pageRules {
id
path
roles
match
deny
locales
}
users {
id
name
email
}
createdAt
updatedAt
}
}
}
...@@ -14,6 +14,8 @@ switch (window.document.documentElement.lang) { ...@@ -14,6 +14,8 @@ switch (window.document.documentElement.lang) {
break break
} }
require('modernizr')
require('./scss/app.scss') require('./scss/app.scss')
import(/* webpackChunkName: "theme" */ './themes/' + siteConfig.theme + '/scss/app.scss') import(/* webpackChunkName: "theme" */ './themes/' + siteConfig.theme + '/scss/app.scss')
......
/*! modernizr 3.6.0 (Custom Build) | MIT *
* https://modernizr.com/download/?-setclasses !*/
!function(n,e,s){function o(n){var e=r.className,s=Modernizr._config.classPrefix||"";if(c&&(e=e.baseVal),Modernizr._config.enableJSClass){var o=new RegExp("(^|\\s)"+s+"no-js(\\s|$)");e=e.replace(o,"$1"+s+"js$2")}Modernizr._config.enableClasses&&(e+=" "+s+n.join(" "+s),c?r.className.baseVal=e:r.className=e)}function a(n,e){return typeof n===e}function i(){var n,e,s,o,i,l,r;for(var c in f)if(f.hasOwnProperty(c)){if(n=[],e=f[c],e.name&&(n.push(e.name.toLowerCase()),e.options&&e.options.aliases&&e.options.aliases.length))for(s=0;s<e.options.aliases.length;s++)n.push(e.options.aliases[s].toLowerCase());for(o=a(e.fn,"function")?e.fn():e.fn,i=0;i<n.length;i++)l=n[i],r=l.split("."),1===r.length?Modernizr[r[0]]=o:(!Modernizr[r[0]]||Modernizr[r[0]]instanceof Boolean||(Modernizr[r[0]]=new Boolean(Modernizr[r[0]])),Modernizr[r[0]][r[1]]=o),t.push((o?"":"no-")+r.join("-"))}}var t=[],f=[],l={_version:"3.6.0",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(n,e){var s=this;setTimeout(function(){e(s[n])},0)},addTest:function(n,e,s){f.push({name:n,fn:e,options:s})},addAsyncTest:function(n){f.push({name:null,fn:n})}},Modernizr=function(){};Modernizr.prototype=l,Modernizr=new Modernizr;var r=e.documentElement,c="svg"===r.nodeName.toLowerCase();i(),o(t),delete l.addTest,delete l.addAsyncTest;for(var u=0;u<Modernizr._q.length;u++)Modernizr._q[u]();n.Modernizr=Modernizr}(window,document);
\ No newline at end of file
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
...@@ -13,7 +13,8 @@ const state = { ...@@ -13,7 +13,8 @@ const state = {
searchIsFocused: false, searchIsFocused: false,
searchIsLoading: false, searchIsLoading: false,
searchRestrictLocale: false, searchRestrictLocale: false,
searchRestrictPath: false searchRestrictPath: false,
printView: false
} }
export default { export default {
......
<template lang="pug"> <template lang="pug">
v-app(v-scroll='upBtnScroll', :dark='$vuetify.theme.dark', :class='$vuetify.rtl ? `is-rtl` : `is-ltr`') v-app(v-scroll='upBtnScroll', :dark='$vuetify.theme.dark', :class='$vuetify.rtl ? `is-rtl` : `is-ltr`')
nav-header nav-header(v-if='!printView')
v-navigation-drawer( v-navigation-drawer(
v-if='navMode !== `NONE`' v-if='navMode !== `NONE` && !printView'
:class='$vuetify.theme.dark ? `grey darken-4-d4` : `primary`' :class='$vuetify.theme.dark ? `grey darken-4-d4` : `primary`'
dark dark
app app
...@@ -171,7 +171,8 @@ ...@@ -171,7 +171,8 @@
) )
v-tooltip(bottom) v-tooltip(bottom)
template(v-slot:activator='{ on }') template(v-slot:activator='{ on }')
v-btn(icon, tile, v-on='on', @click='print'): v-icon(color='grey') mdi-printer v-btn(icon, tile, v-on='on', @click='print')
v-icon(:color='printView ? `primary` : `grey`') mdi-printer
span {{$t('common:page.printFormat')}} span {{$t('common:page.printFormat')}}
v-spacer v-spacer
...@@ -264,7 +265,7 @@ ...@@ -264,7 +265,7 @@
.caption {{$t('common:page.unpublishedWarning')}} .caption {{$t('common:page.unpublishedWarning')}}
.contents(ref='container') .contents(ref='container')
slot(name='contents') slot(name='contents')
.comments-container#discussion(v-if='commentsEnabled && commentsPerms.read') .comments-container#discussion(v-if='commentsEnabled && commentsPerms.read && !printView')
.comments-header .comments-header
v-icon.mr-2(dark) mdi-comment-text-outline v-icon.mr-2(dark) mdi-comment-text-outline
span {{$t('common:comments.title')}} span {{$t('common:comments.title')}}
...@@ -297,7 +298,7 @@ import Tabset from './tabset.vue' ...@@ -297,7 +298,7 @@ import Tabset from './tabset.vue'
import NavSidebar from './nav-sidebar.vue' import NavSidebar from './nav-sidebar.vue'
import Prism from 'prismjs' import Prism from 'prismjs'
import mermaid from 'mermaid' import mermaid from 'mermaid'
import { get } from 'vuex-pathify' import { get, sync } from 'vuex-pathify'
import _ from 'lodash' import _ from 'lodash'
import ClipboardJS from 'clipboard' import ClipboardJS from 'clipboard'
import Vue from 'vue' import Vue from 'vue'
...@@ -490,7 +491,8 @@ export default { ...@@ -490,7 +491,8 @@ export default {
hasAnyPagePermissions () { hasAnyPagePermissions () {
return this.hasAdminPermission || this.hasWritePagesPermission || this.hasManagePagesPermission || return this.hasAdminPermission || this.hasWritePagesPermission || this.hasManagePagesPermission ||
this.hasDeletePagesPermission || this.hasReadSourcePermission || this.hasReadHistoryPermission this.hasDeletePagesPermission || this.hasReadSourcePermission || this.hasReadHistoryPermission
} },
printView: sync('site/printView')
}, },
created() { created() {
this.$store.set('page/authorId', this.authorId) this.$store.set('page/authorId', this.authorId)
...@@ -566,7 +568,14 @@ export default { ...@@ -566,7 +568,14 @@ export default {
this.upBtnShown = scrollOffset > window.innerHeight * 0.33 this.upBtnShown = scrollOffset > window.innerHeight * 0.33
}, },
print () { print () {
if (this.printView) {
this.printView = false
} else {
this.printView = true
this.$nextTick(() => {
window.print() window.print()
})
}
}, },
pageEdit () { pageEdit () {
this.$root.$emit('pageEdit') this.$root.$emit('pageEdit')
......
...@@ -991,4 +991,8 @@ ...@@ -991,4 +991,8 @@
} }
} }
} }
.comments-container {
display: none;
}
} }
...@@ -173,6 +173,10 @@ module.exports = { ...@@ -173,6 +173,10 @@ module.exports = {
outputPath: 'fonts/' outputPath: 'fonts/'
} }
}] }]
},
{
loader: 'webpack-modernizr-loader',
test: /\.modernizrrc\.js$/
} }
] ]
}, },
...@@ -253,7 +257,8 @@ module.exports = { ...@@ -253,7 +257,8 @@ module.exports = {
// Duplicates fixes: // Duplicates fixes:
'apollo-link': path.join(process.cwd(), 'node_modules/apollo-link'), 'apollo-link': path.join(process.cwd(), 'node_modules/apollo-link'),
'apollo-utilities': path.join(process.cwd(), 'node_modules/apollo-utilities'), 'apollo-utilities': path.join(process.cwd(), 'node_modules/apollo-utilities'),
'uc.micro': path.join(process.cwd(), 'node_modules/uc.micro') 'uc.micro': path.join(process.cwd(), 'node_modules/uc.micro'),
'modernizr$': path.resolve(process.cwd(), 'client/.modernizrrc.js')
}, },
extensions: [ extensions: [
'.js', '.js',
......
...@@ -224,6 +224,7 @@ ...@@ -224,6 +224,7 @@
"babel-plugin-transform-imports": "2.0.0", "babel-plugin-transform-imports": "2.0.0",
"cache-loader": "4.1.0", "cache-loader": "4.1.0",
"canvas-confetti": "1.2.0", "canvas-confetti": "1.2.0",
"cash-dom": "8.0.0",
"chart.js": "2.9.3", "chart.js": "2.9.3",
"clean-webpack-plugin": "3.0.0", "clean-webpack-plugin": "3.0.0",
"clipboard": "2.0.6", "clipboard": "2.0.6",
...@@ -318,6 +319,7 @@ ...@@ -318,6 +319,7 @@
"webpack-dev-middleware": "3.7.2", "webpack-dev-middleware": "3.7.2",
"webpack-hot-middleware": "2.25.0", "webpack-hot-middleware": "2.25.0",
"webpack-merge": "4.2.2", "webpack-merge": "4.2.2",
"webpack-modernizr-loader": "5.0.0",
"webpack-subresource-integrity": "1.4.1", "webpack-subresource-integrity": "1.4.1",
"webpackbar": "4.0.0", "webpackbar": "4.0.0",
"whatwg-fetch": "3.0.0", "whatwg-fetch": "3.0.0",
......
...@@ -53,6 +53,12 @@ defaults: ...@@ -53,6 +53,12 @@ defaults:
theme: 'default' theme: 'default'
iconset: 'md' iconset: 'md'
darkMode: false darkMode: false
auth:
autoLogin: false
loginBgUrl: ''
audience: 'urn:wiki.js'
tokenExpiration: '30m'
tokenRenewal: '14d'
features: features:
featurePageRatings: true featurePageRatings: true
featurePageComments: true featurePageComments: true
......
...@@ -78,9 +78,8 @@ module.exports = { ...@@ -78,9 +78,8 @@ module.exports = {
const enabledStrategies = await WIKI.models.authentication.getStrategies() const enabledStrategies = await WIKI.models.authentication.getStrategies()
for (let idx in enabledStrategies) { for (let idx in enabledStrategies) {
const stg = enabledStrategies[idx] const stg = enabledStrategies[idx]
if (!stg.isEnabled) { continue }
try { try {
const strategy = require(`../modules/authentication/${stg.key}/authentication.js`) const strategy = require(`../modules/authentication/${stg.strategyKey}/authentication.js`)
stg.config.callbackURL = `${WIKI.config.host}/login/${stg.key}/callback` stg.config.callbackURL = `${WIKI.config.host}/login/${stg.key}/callback`
strategy.init(passport, stg.config) strategy.init(passport, stg.config)
...@@ -90,9 +89,9 @@ module.exports = { ...@@ -90,9 +89,9 @@ module.exports = {
...strategy, ...strategy,
...stg ...stg
} }
WIKI.logger.info(`Authentication Strategy ${stg.key}: [ OK ]`) WIKI.logger.info(`Authentication Strategy ${stg.displayName}: [ OK ]`)
} catch (err) { } catch (err) {
WIKI.logger.error(`Authentication Strategy ${stg.key}: [ FAILED ]`) WIKI.logger.error(`Authentication Strategy ${stg.displayName} (${stg.key}): [ FAILED ]`)
WIKI.logger.error(err) WIKI.logger.error(err)
} }
} }
......
exports.up = async knex => {
await knex('authentication').where('isEnabled', false).del()
await knex.schema
.alterTable('authentication', table => {
table.dropColumn('isEnabled')
table.integer('order').unsigned().notNullable().defaultTo(0)
table.string('strategyKey').notNullable().defaultTo('')
table.string('displayName').notNullable().defaultTo('')
})
// Fix pre-2.5 strategies
const strategies = await knex('authentication')
let idx = 1
for (const strategy of strategies) {
await knex('authentication').where('key', strategy.key).update({
strategyKey: strategy.key,
order: (strategy.key === 'local') ? 0 : idx++
})
}
}
exports.down = knex => { }
exports.up = async knex => {
await knex.schema
.alterTable('groups', table => {
table.string('redirectOnLogin').notNullable().defaultTo('/')
})
}
exports.down = knex => { }
exports.up = async knex => {
await knex('authentication').where('isEnabled', false).del()
await knex.schema
.alterTable('authentication', table => {
table.dropColumn('isEnabled')
table.integer('order').unsigned().notNullable().defaultTo(0)
table.string('strategyKey').notNullable().defaultTo('')
table.string('displayName').notNullable().defaultTo('')
})
// Fix pre-2.5 strategies
const strategies = await knex('authentication')
let idx = 1
for (const strategy of strategies) {
await knex('authentication').where('key', strategy.key).update({
strategyKey: strategy.key,
order: (strategy.key === 'local') ? 0 : idx++
})
}
}
exports.down = knex => { }
exports.up = async knex => {
await knex.schema
.alterTable('groups', table => {
table.string('redirectOnLogin').notNullable().defaultTo('/')
})
}
exports.down = knex => { }
...@@ -34,16 +34,28 @@ module.exports = { ...@@ -34,16 +34,28 @@ module.exports = {
apiState () { apiState () {
return WIKI.config.api.isEnabled return WIKI.config.api.isEnabled
}, },
async strategies () {
return WIKI.data.authentication.map(stg => ({
...stg,
isAvailable: stg.isAvailable === true,
props: _.sortBy(_.transform(stg.props, (res, value, key) => {
res.push({
key,
value: JSON.stringify(value)
})
}, []), 'key')
}))
},
/** /**
* Fetch active authentication strategies * Fetch active authentication strategies
*/ */
async strategies (obj, args, context, info) { async activeStrategies (obj, args, context, info) {
let strategies = await WIKI.models.authentication.getStrategies(args.isEnabled) let strategies = await WIKI.models.authentication.getStrategies()
strategies = strategies.map(stg => { strategies = strategies.map(stg => {
const strategyInfo = _.find(WIKI.data.authentication, ['key', stg.key]) || {} const strategyInfo = _.find(WIKI.data.authentication, ['key', stg.strategyKey]) || {}
return { return {
...strategyInfo,
...stg, ...stg,
strategy: strategyInfo,
config: _.sortBy(_.transform(stg.config, (res, value, key) => { config: _.sortBy(_.transform(stg.config, (res, value, key) => {
const configData = _.get(strategyInfo.props, key, false) const configData = _.get(strategyInfo.props, key, false)
if (configData) { if (configData) {
...@@ -174,16 +186,18 @@ module.exports = { ...@@ -174,16 +186,18 @@ module.exports = {
*/ */
async updateStrategies (obj, args, context) { async updateStrategies (obj, args, context) {
try { try {
WIKI.config.auth = { // WIKI.config.auth = {
audience: _.get(args, 'config.audience', WIKI.config.auth.audience), // audience: _.get(args, 'config.audience', WIKI.config.auth.audience),
tokenExpiration: _.get(args, 'config.tokenExpiration', WIKI.config.auth.tokenExpiration), // tokenExpiration: _.get(args, 'config.tokenExpiration', WIKI.config.auth.tokenExpiration),
tokenRenewal: _.get(args, 'config.tokenRenewal', WIKI.config.auth.tokenRenewal) // tokenRenewal: _.get(args, 'config.tokenRenewal', WIKI.config.auth.tokenRenewal)
} // }
await WIKI.configSvc.saveToDb(['auth']) // await WIKI.configSvc.saveToDb(['auth'])
for (let str of args.strategies) { const previousStrategies = await WIKI.models.authentication.getStrategies()
await WIKI.models.authentication.query().patch({ for (const str of args.strategies) {
isEnabled: str.isEnabled, const newStr = {
displayName: str.displayName,
order: str.order,
config: _.reduce(str.config, (result, value, key) => { config: _.reduce(str.config, (result, value, key) => {
_.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null)) _.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null))
return result return result
...@@ -191,8 +205,32 @@ module.exports = { ...@@ -191,8 +205,32 @@ module.exports = {
selfRegistration: str.selfRegistration, selfRegistration: str.selfRegistration,
domainWhitelist: { v: str.domainWhitelist }, domainWhitelist: { v: str.domainWhitelist },
autoEnrollGroups: { v: str.autoEnrollGroups } autoEnrollGroups: { v: str.autoEnrollGroups }
}
if (_.some(previousStrategies, ['key', str.key])) {
await WIKI.models.authentication.query().patch({
key: str.key,
strategyKey: str.strategyKey,
...newStr
}).where('key', str.key) }).where('key', str.key)
} else {
await WIKI.models.authentication.query().insert({
key: str.key,
strategyKey: str.strategyKey,
...newStr
})
}
} }
for (const str of _.differenceBy(previousStrategies, args.strategies, 'key')) {
const hasUsers = await WIKI.models.users.query().count('* as total').where({ providerKey: str.key }).first()
if (_.toSafeInteger(hasUsers.total) > 0) {
throw new Error(`Cannot delete ${str.displayName} as 1 or more users are still using it.`)
} else {
await WIKI.models.authentication.query().delete().where('key', str.key)
}
}
await WIKI.auth.activateStrategies() await WIKI.auth.activateStrategies()
WIKI.events.outbound.emit('reloadAuthStrategies') WIKI.events.outbound.emit('reloadAuthStrategies')
return { return {
......
...@@ -102,8 +102,13 @@ module.exports = { ...@@ -102,8 +102,13 @@ module.exports = {
throw new gql.GraphQLError('Some Page Rules contains unsafe or exponential time regex.') throw new gql.GraphQLError('Some Page Rules contains unsafe or exponential time regex.')
} }
if (_.isEmpty(args.redirectOnLogin)) {
args.redirectOnLogin = '/'
}
await WIKI.models.groups.query().patch({ await WIKI.models.groups.query().patch({
name: args.name, name: args.name,
redirectOnLogin: args.redirectOnLogin,
permissions: JSON.stringify(args.permissions), permissions: JSON.stringify(args.permissions),
pageRules: JSON.stringify(args.pageRules) pageRules: JSON.stringify(args.pageRules)
}).where('id', args.id) }).where('id', args.id)
......
...@@ -171,9 +171,10 @@ module.exports = { ...@@ -171,9 +171,10 @@ module.exports = {
* FETCH TAGS * FETCH TAGS
*/ */
async tags (obj, args, context, info) { async tags (obj, args, context, info) {
const pages = await WIKI.models.pages.query().column([ const pages = await WIKI.models.pages.query()
.column([
'path', 'path',
{ locale: 'localeCode' }, { locale: 'localeCode' }
]) ])
.withGraphJoined('tags') .withGraphJoined('tags')
const allTags = _.filter(pages, r => { const allTags = _.filter(pages, r => {
...@@ -181,8 +182,7 @@ module.exports = { ...@@ -181,8 +182,7 @@ module.exports = {
path: r.path, path: r.path,
locale: r.locale locale: r.locale
}) })
}) }).flatMap(r => r.tags)
.flatMap(r => r.tags)
return _.orderBy(_.uniqBy(allTags, 'id'), ['tag'], ['asc']) return _.orderBy(_.uniqBy(allTags, 'id'), ['tag'], ['asc'])
}, },
/** /**
...@@ -190,9 +190,10 @@ module.exports = { ...@@ -190,9 +190,10 @@ module.exports = {
*/ */
async searchTags (obj, args, context, info) { async searchTags (obj, args, context, info) {
const query = _.trim(args.query) const query = _.trim(args.query)
const pages = await WIKI.models.pages.query().column([ const pages = await WIKI.models.pages.query()
.column([
'path', 'path',
{ locale: 'localeCode' }, { locale: 'localeCode' }
]) ])
.withGraphJoined('tags') .withGraphJoined('tags')
.modifyGraph('tags', builder => { .modifyGraph('tags', builder => {
...@@ -212,9 +213,7 @@ module.exports = { ...@@ -212,9 +213,7 @@ module.exports = {
path: r.path, path: r.path,
locale: r.locale locale: r.locale
}) })
}) }).flatMap(r => r.tags).map(t => t.tag)
.flatMap(r => r.tags)
.map(t => t.tag)
return _.uniq(allTags).slice(0, 5) return _.uniq(allTags).slice(0, 5)
}, },
/** /**
...@@ -271,7 +270,7 @@ module.exports = { ...@@ -271,7 +270,7 @@ module.exports = {
* FETCH PAGE LINKS * FETCH PAGE LINKS
*/ */
async links (obj, args, context, info) { async links (obj, args, context, info) {
let results; let results
if (WIKI.config.db.type === 'mysql' || WIKI.config.db.type === 'mariadb' || WIKI.config.db.type === 'sqlite') { if (WIKI.config.db.type === 'mysql' || WIKI.config.db.type === 'mariadb' || WIKI.config.db.type === 'sqlite') {
results = await WIKI.models.knex('pages') results = await WIKI.models.knex('pages')
......
...@@ -21,6 +21,11 @@ module.exports = { ...@@ -21,6 +21,11 @@ module.exports = {
...WIKI.config.seo, ...WIKI.config.seo,
...WIKI.config.features, ...WIKI.config.features,
...WIKI.config.security, ...WIKI.config.security,
authAutoLogin: WIKI.config.auth.autoLogin,
authLoginBgUrl: WIKI.config.auth.loginBgUrl,
authJwtAudience: WIKI.config.auth.audience,
authJwtExpiration: WIKI.config.auth.tokenExpiration,
authJwtRenewablePeriod: WIKI.config.auth.tokenRenewal,
uploadMaxFileSize: WIKI.config.uploads.maxFileSize, uploadMaxFileSize: WIKI.config.uploads.maxFileSize,
uploadMaxFiles: WIKI.config.uploads.maxFiles uploadMaxFiles: WIKI.config.uploads.maxFiles
} }
...@@ -60,6 +65,14 @@ module.exports = { ...@@ -60,6 +65,14 @@ module.exports = {
analyticsId: _.get(args, 'analyticsId', WIKI.config.seo.analyticsId) analyticsId: _.get(args, 'analyticsId', WIKI.config.seo.analyticsId)
} }
WIKI.config.auth = {
autoLogin: _.get(args, 'authAutoLogin', WIKI.config.auth.autoLogin),
loginBgUrl: _.get(args, 'authLoginBgUrl', WIKI.config.auth.loginBgUrl),
audience: _.get(args, 'authJwtAudience', WIKI.config.auth.audience),
tokenExpiration: _.get(args, 'authJwtExpiration', WIKI.config.auth.tokenExpiration),
tokenRenewal: _.get(args, 'authJwtRenewablePeriod', WIKI.config.auth.tokenRenewal)
}
WIKI.config.features = { WIKI.config.features = {
featurePageRatings: _.get(args, 'featurePageRatings', WIKI.config.features.featurePageRatings), featurePageRatings: _.get(args, 'featurePageRatings', WIKI.config.features.featurePageRatings),
featurePageComments: _.get(args, 'featurePageComments', WIKI.config.features.featurePageComments), featurePageComments: _.get(args, 'featurePageComments', WIKI.config.features.featurePageComments),
...@@ -83,7 +96,7 @@ module.exports = { ...@@ -83,7 +96,7 @@ module.exports = {
maxFiles: _.get(args, 'uploadMaxFiles', WIKI.config.uploads.maxFiles) maxFiles: _.get(args, 'uploadMaxFiles', WIKI.config.uploads.maxFiles)
} }
await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'contentLicense', 'seo', 'logoUrl', 'features', 'security', 'uploads']) await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'contentLicense', 'seo', 'logoUrl', 'auth', 'features', 'security', 'uploads'])
if (WIKI.config.security.securityTrustProxy) { if (WIKI.config.security.securityTrustProxy) {
WIKI.app.enable('trust proxy') WIKI.app.enable('trust proxy')
......
...@@ -19,9 +19,8 @@ type AuthenticationQuery { ...@@ -19,9 +19,8 @@ type AuthenticationQuery {
apiState: Boolean! @auth(requires: ["manage:system", "manage:api"]) apiState: Boolean! @auth(requires: ["manage:system", "manage:api"])
strategies( strategies: [AuthenticationStrategy] @auth(requires: ["manage:system"])
isEnabled: Boolean activeStrategies: [AuthenticationActiveStrategy]
): [AuthenticationStrategy]
} }
# ----------------------------------------------- # -----------------------------------------------
...@@ -68,7 +67,6 @@ type AuthenticationMutation { ...@@ -68,7 +67,6 @@ type AuthenticationMutation {
updateStrategies( updateStrategies(
strategies: [AuthenticationStrategyInput]! strategies: [AuthenticationStrategyInput]!
config: AuthenticationConfigInput
): DefaultResponse @auth(requires: ["manage:system"]) ): DefaultResponse @auth(requires: ["manage:system"])
regenerateCertificates: DefaultResponse @auth(requires: ["manage:system"]) regenerateCertificates: DefaultResponse @auth(requires: ["manage:system"])
...@@ -81,9 +79,8 @@ type AuthenticationMutation { ...@@ -81,9 +79,8 @@ type AuthenticationMutation {
# ----------------------------------------------- # -----------------------------------------------
type AuthenticationStrategy { type AuthenticationStrategy {
isEnabled: Boolean!
key: String! key: String!
props: [String] props: [KeyValuePair] @auth(requires: ["manage:system"])
title: String! title: String!
description: String description: String
isAvailable: Boolean isAvailable: Boolean
...@@ -92,6 +89,13 @@ type AuthenticationStrategy { ...@@ -92,6 +89,13 @@ type AuthenticationStrategy {
color: String color: String
website: String website: String
icon: String icon: String
}
type AuthenticationActiveStrategy {
key: String!
strategy: AuthenticationStrategy!
displayName: String!
order: Int!
config: [KeyValuePair] @auth(requires: ["manage:system"]) config: [KeyValuePair] @auth(requires: ["manage:system"])
selfRegistration: Boolean! selfRegistration: Boolean!
domainWhitelist: [String]! @auth(requires: ["manage:system"]) domainWhitelist: [String]! @auth(requires: ["manage:system"])
...@@ -112,20 +116,16 @@ type AuthenticationRegisterResponse { ...@@ -112,20 +116,16 @@ type AuthenticationRegisterResponse {
} }
input AuthenticationStrategyInput { input AuthenticationStrategyInput {
isEnabled: Boolean!
key: String! key: String!
strategyKey: String!
config: [KeyValuePairInput] config: [KeyValuePairInput]
displayName: String!
order: Int!
selfRegistration: Boolean! selfRegistration: Boolean!
domainWhitelist: [String]! domainWhitelist: [String]!
autoEnrollGroups: [Int]! autoEnrollGroups: [Int]!
} }
input AuthenticationConfigInput {
audience: String!
tokenExpiration: String!
tokenRenewal: String!
}
type AuthenticationApiKey { type AuthenticationApiKey {
id: Int! id: Int!
name: String! name: String!
......
...@@ -37,6 +37,7 @@ type GroupMutation { ...@@ -37,6 +37,7 @@ type GroupMutation {
update( update(
id: Int! id: Int!
name: String! name: String!
redirectOnLogin: String!
permissions: [String]! permissions: [String]!
pageRules: [PageRuleInput]! pageRules: [PageRuleInput]!
): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"]) ): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"])
...@@ -78,6 +79,7 @@ type Group { ...@@ -78,6 +79,7 @@ type Group {
id: Int! id: Int!
name: String! name: String!
isSystem: Boolean! isSystem: Boolean!
redirectOnLogin: String
permissions: [String]! permissions: [String]!
pageRules: [PageRule] pageRules: [PageRule]
users: [UserMinimal] users: [UserMinimal]
......
...@@ -33,6 +33,11 @@ type SiteMutation { ...@@ -33,6 +33,11 @@ type SiteMutation {
company: String company: String
contentLicense: String contentLicense: String
logoUrl: String logoUrl: String
authAutoLogin: Boolean
authLoginBgUrl: String
authJwtAudience: String
authJwtExpiration: String
authJwtRenewablePeriod: String
featurePageRatings: Boolean featurePageRatings: Boolean
featurePageComments: Boolean featurePageComments: Boolean
featurePersonalWikis: Boolean featurePersonalWikis: Boolean
...@@ -65,6 +70,11 @@ type SiteConfig { ...@@ -65,6 +70,11 @@ type SiteConfig {
company: String! company: String!
contentLicense: String! contentLicense: String!
logoUrl: String! logoUrl: String!
authAutoLogin: Boolean
authLoginBgUrl: String
authJwtAudience: String
authJwtExpiration: String
authJwtRenewablePeriod: String
featurePageRatings: Boolean! featurePageRatings: Boolean!
featurePageComments: Boolean! featurePageComments: Boolean!
featurePersonalWikis: Boolean! featurePersonalWikis: Boolean!
......
...@@ -17,11 +17,10 @@ module.exports = class Authentication extends Model { ...@@ -17,11 +17,10 @@ module.exports = class Authentication extends Model {
static get jsonSchema () { static get jsonSchema () {
return { return {
type: 'object', type: 'object',
required: ['key', 'isEnabled'], required: ['key'],
properties: { properties: {
key: {type: 'string'}, key: {type: 'string'},
isEnabled: {type: 'boolean'},
selfRegistration: {type: 'boolean'} selfRegistration: {type: 'boolean'}
} }
} }
...@@ -35,8 +34,8 @@ module.exports = class Authentication extends Model { ...@@ -35,8 +34,8 @@ module.exports = class Authentication extends Model {
return WIKI.models.authentication.query().findOne({ key }) return WIKI.models.authentication.query().findOne({ key })
} }
static async getStrategies(isEnabled) { static async getStrategies() {
const strategies = await WIKI.models.authentication.query().where(_.isBoolean(isEnabled) ? { isEnabled } : {}) const strategies = await WIKI.models.authentication.query().orderBy('order')
return _.sortBy(strategies.map(str => ({ return _.sortBy(strategies.map(str => ({
...str, ...str,
domainWhitelist: _.get(str.domainWhitelist, 'v', []), domainWhitelist: _.get(str.domainWhitelist, 'v', []),
...@@ -45,7 +44,7 @@ module.exports = class Authentication extends Model { ...@@ -45,7 +44,7 @@ module.exports = class Authentication extends Model {
} }
static async getStrategiesForLegacyClient() { static async getStrategiesForLegacyClient() {
const strategies = await WIKI.models.authentication.query().select('key', 'selfRegistration').where({ isEnabled: true }) const strategies = await WIKI.models.authentication.query().select('key', 'selfRegistration')
let formStrategies = [] let formStrategies = []
let socialStrategies = [] let socialStrategies = []
...@@ -77,64 +76,42 @@ module.exports = class Authentication extends Model { ...@@ -77,64 +76,42 @@ module.exports = class Authentication extends Model {
} }
static async refreshStrategiesFromDisk() { static async refreshStrategiesFromDisk() {
let trx
try { try {
const dbStrategies = await WIKI.models.authentication.query() const dbStrategies = await WIKI.models.authentication.query()
// -> Fetch definitions from disk // -> Fetch definitions from disk
const authDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/authentication')) const authDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/authentication'))
let diskStrategies = [] WIKI.data.authentication = []
for (let dir of authDirs) { for (let dir of authDirs) {
const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/authentication', dir, 'definition.yml'), 'utf8') const defRaw = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/authentication', dir, 'definition.yml'), 'utf8')
diskStrategies.push(yaml.safeLoad(def)) const def = yaml.safeLoad(defRaw)
WIKI.data.authentication.push({
...def,
props: commonHelper.parseModuleProps(def.props)
})
} }
WIKI.data.authentication = diskStrategies.map(strategy => ({
...strategy,
props: commonHelper.parseModuleProps(strategy.props)
}))
let newStrategies = [] for (const strategy of dbStrategies) {
for (let strategy of WIKI.data.authentication) { const strategyDef = _.find(WIKI.data.authentication, ['key', strategy.strategyKey])
if (!_.some(dbStrategies, ['key', strategy.key])) { strategy.config = _.transform(strategyDef.props, (result, value, key) => {
newStrategies.push({
key: strategy.key,
isEnabled: false,
config: _.transform(strategy.props, (result, value, key) => {
_.set(result, key, value.default)
return result
}, {}),
selfRegistration: false,
domainWhitelist: { v: [] },
autoEnrollGroups: { v: [] }
})
} else {
const strategyConfig = _.get(_.find(dbStrategies, ['key', strategy.key]), 'config', {})
await WIKI.models.authentication.query().patch({
config: _.transform(strategy.props, (result, value, key) => {
if (!_.has(result, key)) { if (!_.has(result, key)) {
_.set(result, key, value.default) _.set(result, key, value.default)
} }
return result return result
}, strategyConfig) }, strategy.config)
// Fix pre-2.5 strategies displayName
if (!strategy.displayName) {
await WIKI.models.authentication.query().patch({
displayName: strategyDef.title
}).where('key', strategy.key) }).where('key', strategy.key)
} }
} }
if (newStrategies.length > 0) {
trx = await WIKI.models.Objection.transaction.start(WIKI.models.knex) WIKI.logger.info(`Loaded ${WIKI.data.authentication.length} authentication strategies: [ OK ]`)
for (let strategy of newStrategies) {
await WIKI.models.authentication.query(trx).insert(strategy)
}
await trx.commit()
WIKI.logger.info(`Loaded ${newStrategies.length} new authentication strategies: [ OK ]`)
} else {
WIKI.logger.info(`No new authentication strategies found: [ SKIPPED ]`)
}
} catch (err) { } catch (err) {
WIKI.logger.error(`Failed to scan or load new authentication providers: [ FAILED ]`) WIKI.logger.error(`Failed to scan or load new authentication providers: [ FAILED ]`)
WIKI.logger.error(err) WIKI.logger.error(err)
if (trx) {
trx.rollback()
}
} }
} }
} }
...@@ -14,6 +14,8 @@ module.exports = class Group extends Model { ...@@ -14,6 +14,8 @@ module.exports = class Group extends Model {
properties: { properties: {
id: {type: 'integer'}, id: {type: 'integer'},
name: {type: 'string'}, name: {type: 'string'},
isSystem: {type: 'boolean'},
redirectOnLogin: {type: 'string'},
createdAt: {type: 'string'}, createdAt: {type: 'string'},
updatedAt: {type: 'string'} updatedAt: {type: 'string'}
} }
......
key: local key: local
title: Local title: Local Database
description: Built-in authentication for Wiki.js description: Built-in authentication for Wiki.js
author: requarks.io author: requarks.io
logo: https://static.requarks.io/logo/wikijs.svg logo: https://static.requarks.io/logo/wikijs.svg
......
...@@ -5,9 +5,54 @@ author: requarks.io ...@@ -5,9 +5,54 @@ author: requarks.io
logo: https://static.requarks.io/logo/oauth2.svg logo: https://static.requarks.io/logo/oauth2.svg
color: grey darken-4 color: grey darken-4
website: https://oauth.net/2/ website: https://oauth.net/2/
isAvailable: true
useForm: false useForm: false
props: props:
clientId: String clientId:
clientSecret: String type: String
authorizationURL: String title: Client ID
tokenURL: String hint: Application Client ID
order: 1
clientSecret:
type: String
title: Client Secret
hint: Application Client Secret
order: 2
authorizationURL:
type: String
title: Authorization Endpoint URL
hint: The full URL to the authorization endpoint, used to get an authorization code.
order: 3
tokenURL:
type: String
title: Token Endpoint URL
hint: The full URL to the token endpoint, used to get an access token.
order: 4
mappingUID:
title: Unique ID Field Mapping
type: String
default: 'id'
hint: The field storing the user unique identifier, e.g. "id" or "_id".
maxWidth: 500
order: 20
mappingEmail:
title: Email Field Mapping
type: String
default: 'email'
hint: The field storing the user email, e.g. "email" or "mail".
maxWidth: 500
order: 21
mappingDisplayName:
title: Display Name Field Mapping
type: String
default: 'name'
hint: The field storing the user display name, e.g. "name", "displayName" or "username".
maxWidth: 500
order: 22
mappingPicture:
title: Avatar Picture Field Mapping
type: String
default: 'pictureUrl'
hint: The field storing the user avatar picture, e.g. "pictureUrl" or "avatarUrl".
maxWidth: 500
order: 23
...@@ -253,9 +253,17 @@ module.exports = () => { ...@@ -253,9 +253,17 @@ module.exports = () => {
throw new Error('Incorrect groups auto-increment configuration! Should start at 0 and increment by 1. Contact your database administrator.') throw new Error('Incorrect groups auto-increment configuration! Should start at 0 and increment by 1. Contact your database administrator.')
} }
// Load authentication strategies + enable local // Load local authentication strategy
await WIKI.models.authentication.refreshStrategiesFromDisk() await WIKI.models.authentication.query().insert({
await WIKI.models.authentication.query().patch({ isEnabled: true }).where('key', 'local') key: 'local',
config: {},
selfRegistration: false,
domainWhitelist: {v: []},
autoEnrollGroups: {v: []},
order: 0,
strategyKey: 'local',
displayName: 'Local'
})
// Load editors + enable default // Load editors + enable default
await WIKI.models.editors.refreshEditorsFromDisk() await WIKI.models.editors.refreshEditorsFromDisk()
......
This diff was suppressed by a .gitattributes entry.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment