Commit f856da07 authored by Nicolas Giard's avatar Nicolas Giard

feat: user menu + jwt certs + UI fixes

parent 74aa09f3
...@@ -48,6 +48,8 @@ window.Hammer = Hammer ...@@ -48,6 +48,8 @@ window.Hammer = Hammer
moment.locale(siteConfig.lang) moment.locale(siteConfig.lang)
store.commit('user/REFRESH_AUTH')
// ==================================== // ====================================
// Initialize Apollo Client (GraphQL) // Initialize Apollo Client (GraphQL)
// ==================================== // ====================================
......
...@@ -155,6 +155,12 @@ export default { ...@@ -155,6 +155,12 @@ export default {
<style lang='scss'> <style lang='scss'>
.admin {
&.theme--light {
background-color: lighten(mc('grey', '200'), 2%);
}
}
.admin-router { .admin-router {
&-enter-active, &-leave-active { &-enter-active, &-leave-active {
transition: opacity .25s ease; transition: opacity .25s ease;
...@@ -173,7 +179,7 @@ export default { ...@@ -173,7 +179,7 @@ export default {
background-color: rgba(mc('theme', 'primary'), .1); background-color: rgba(mc('theme', 'primary'), .1);
.v-icon { .v-icon {
color: mc('theme', 'primary') color: mc('theme', 'primary');
} }
} }
} }
...@@ -181,6 +187,11 @@ export default { ...@@ -181,6 +187,11 @@ export default {
.theme--dark { .theme--dark {
.admin-sidebar .v-list__tile--active { .admin-sidebar .v-list__tile--active {
background-color: rgba(0,0,0, .2); background-color: rgba(0,0,0, .2);
color: mc('blue', '500') !important;
.v-icon {
color: mc('blue', '500');
}
} }
} }
......
...@@ -67,7 +67,7 @@ ...@@ -67,7 +67,7 @@
) )
v-tab-item(v-for='(strategy, n) in activeStrategies', :key='strategy.key', :transition='false', :reverse-transition='false') v-tab-item(v-for='(strategy, n) in activeStrategies', :key='strategy.key', :transition='false', :reverse-transition='false')
v-card.pa-3(flat, tile) v-card.wiki-form.pa-3(flat, tile)
v-form v-form
.authlogo .authlogo
img(:src='strategy.logo', :alt='strategy.title') img(:src='strategy.logo', :alt='strategy.title')
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
v-radio-group(v-model='selectedEditor') v-radio-group(v-model='selectedEditor')
v-radio(v-for='(editor, n) in editors', :key='n', :label='editor.text', :value='editor.value', color='primary') v-radio(v-for='(editor, n) in editors', :key='n', :label='editor.text', :value='editor.value', color='primary')
v-tab-item(key='code', :transition='false', :reverse-transition='false') v-tab-item(key='code', :transition='false', :reverse-transition='false')
v-card.pa-3(flat, tile) v-card.wiki-form.pa-3(flat, tile)
v-form v-form
v-subheader Editor Configuration v-subheader Editor Configuration
.body-1.ml-3 This editor has no configuration options you can modify. .body-1.ml-3 This editor has no configuration options you can modify.
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
v-layout(row wrap) v-layout(row wrap)
v-flex(lg6 xs12) v-flex(lg6 xs12)
v-form v-form
v-card v-card.wiki-form
v-toolbar(color='primary', dark, dense, flat) v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title v-toolbar-title
.subheading {{ $t('admin:general.siteInfo') }} .subheading {{ $t('admin:general.siteInfo') }}
...@@ -23,7 +23,6 @@ ...@@ -23,7 +23,6 @@
.px-3.pb-3 .px-3.pb-3
v-text-field( v-text-field(
outline outline
background-color='grey lighten-2'
label='Site Title' label='Site Title'
required required
:counter='50' :counter='50'
...@@ -35,21 +34,18 @@ ...@@ -35,21 +34,18 @@
.px-3.pb-3 .px-3.pb-3
v-text-field( v-text-field(
outline outline
background-color='grey lighten-2'
label='Site Description' label='Site Description'
:counter='255' :counter='255'
prepend-icon='public' prepend-icon='public'
) )
v-text-field( v-text-field(
outline outline
background-color='grey lighten-2'
label='Site Keywords' label='Site Keywords'
:counter='255' :counter='255'
prepend-icon='public' prepend-icon='public'
) )
v-select( v-select(
outline outline
background-color='grey lighten-2'
label='Meta Robots' label='Meta Robots'
chips chips
tags tags
...@@ -62,7 +58,6 @@ ...@@ -62,7 +58,6 @@
.px-3.pb-3 .px-3.pb-3
v-text-field( v-text-field(
outline outline
background-color='grey lighten-2'
label='Google Analytics ID' label='Google Analytics ID'
:counter='255' :counter='255'
prepend-icon='public' prepend-icon='public'
...@@ -74,7 +69,6 @@ ...@@ -74,7 +69,6 @@
.px-3.pb-3 .px-3.pb-3
v-text-field( v-text-field(
outline outline
background-color='grey lighten-2'
label='Company / Organization Name' label='Company / Organization Name'
v-model='company' v-model='company'
:counter='255' :counter='255'
...@@ -83,7 +77,7 @@ ...@@ -83,7 +77,7 @@
hint='Name to use when displaying copyright notice in the footer. Leave empty to hide.' hint='Name to use when displaying copyright notice in the footer. Leave empty to hide.'
) )
v-flex(lg6 xs12) v-flex(lg6 xs12)
v-card v-card.wiki-form
v-toolbar(color='primary', dark, dense, flat) v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title v-toolbar-title
.subheading {{ $t('admin:general.siteBranding') }} .subheading {{ $t('admin:general.siteBranding') }}
...@@ -116,7 +110,7 @@ ...@@ -116,7 +110,7 @@
hint='Uncheck this box if you don\'t want Henry, Wiki.js mascot, to be displayed on client-facing pages.' hint='Uncheck this box if you don\'t want Henry, Wiki.js mascot, to be displayed on client-facing pages.'
) )
v-card.mt-3 v-card.wiki-form.mt-3
v-toolbar(color='primary', dark, dense, flat) v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title v-toolbar-title
.subheading Features .subheading Features
...@@ -141,7 +135,7 @@ ...@@ -141,7 +135,7 @@
<script> <script>
import { sync } from 'vuex-pathify' import { get, sync } from 'vuex-pathify'
export default { export default {
data() { data() {
...@@ -155,6 +149,7 @@ export default { ...@@ -155,6 +149,7 @@ export default {
} }
}, },
computed: { computed: {
darkMode: get('site/dark'),
siteTitle: sync('site/title'), siteTitle: sync('site/title'),
company: sync('site/company') company: sync('site/company')
}, },
......
...@@ -14,14 +14,13 @@ ...@@ -14,14 +14,13 @@
v-form.pt-3 v-form.pt-3
v-layout(row wrap) v-layout(row wrap)
v-flex(lg6 xs12) v-flex(lg6 xs12)
v-card v-card.wiki-form
v-toolbar(color='primary', dark, dense, flat) v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title v-toolbar-title
.subheading {{ $t('admin:locale.settings') }} .subheading {{ $t('admin:locale.settings') }}
v-card-text v-card-text
v-select( v-select(
outline outline
background-color='grey lighten-2'
:items='installedLocales' :items='installedLocales'
prepend-icon='language' prepend-icon='language'
v-model='selectedLocale' v-model='selectedLocale'
...@@ -49,7 +48,7 @@ ...@@ -49,7 +48,7 @@
:hint='namespacing ? $t("admin:locale.autoUpdate.hintWithNS") : $t("admin:locale.autoUpdate.hint")' :hint='namespacing ? $t("admin:locale.autoUpdate.hintWithNS") : $t("admin:locale.autoUpdate.hint")'
) )
v-card.mt-3 v-card.wiki-form.mt-3
v-toolbar(color='primary', dark, dense, flat) v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title v-toolbar-title
.subheading {{ $t('admin:locale.namespacing') }} .subheading {{ $t('admin:locale.namespacing') }}
...@@ -71,6 +70,7 @@ ...@@ -71,6 +70,7 @@
.caption.grey--text {{ $t('admin:locale.namespacingPrefixWarning.subtitle') }} .caption.grey--text {{ $t('admin:locale.namespacingPrefixWarning.subtitle') }}
v-divider.mt-3.mb-4 v-divider.mt-3.mb-4
v-select( v-select(
outline
:disabled='!namespacing' :disabled='!namespacing'
:items='installedLocales' :items='installedLocales'
prepend-icon='language' prepend-icon='language'
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
) )
v-tab-item(v-for='(logger, n) in activeLoggers', :key='logger.key', :transition='false', :reverse-transition='false') v-tab-item(v-for='(logger, n) in activeLoggers', :key='logger.key', :transition='false', :reverse-transition='false')
v-card.pa-3(flat, tile) v-card.wiki-form.pa-3(flat, tile)
v-form v-form
.loggerlogo .loggerlogo
img(:src='logger.logo', :alt='logger.title') img(:src='logger.logo', :alt='logger.title')
......
...@@ -61,40 +61,51 @@ ...@@ -61,40 +61,51 @@
v-list-tile-avatar: v-icon power_input v-list-tile-avatar: v-icon power_input
v-list-tile-title {{$t('navigation.divider')}} v-list-tile-title {{$t('navigation.divider')}}
v-flex v-flex
v-card(v-if='current.kind === "link"') v-card.wiki-form(v-if='current.kind === "link"')
v-toolbar(dense, color='blue', flat, dark) v-toolbar(dense, color='blue', flat, dark)
.subheading {{$t('navigation.edit', { kind: $t('navigation.link') })}} .subheading {{$t('navigation.edit', { kind: $t('navigation.link') })}}
v-card-text v-card-text
v-text-field( v-text-field(
outline outline
background-color='grey lighten-2'
:label='$t("navigation.label")' :label='$t("navigation.label")'
prepend-icon='title' prepend-icon='title'
v-model='current.label' v-model='current.label'
) )
v-text-field( v-text-field(
outline outline
background-color='grey lighten-2'
:label='$t("navigation.icon")' :label='$t("navigation.icon")'
prepend-icon='casino' prepend-icon='casino'
v-model='current.icon' v-model='current.icon'
) )
v-select( v-select(
outline outline
background-color='grey lighten-2'
:label='$t("navigation.targetType")' :label='$t("navigation.targetType")'
prepend-icon='near_me' prepend-icon='near_me'
:items='navTypes' :items='navTypes'
v-model='current.targetType' v-model='current.targetType'
) )
v-text-field( v-text-field(
v-if='current.targetType === "external"' v-if='current.targetType === `external`'
outline outline
background-color='grey lighten-2'
:label='$t("navigation.target")' :label='$t("navigation.target")'
prepend-icon='near_me' prepend-icon='near_me'
v-model='current.target' v-model='current.target'
) )
v-btn(
v-else-if='current.targetType === "page"'
color='indigo'
dark
)
v-icon(left) search
span Select Page...
v-text-field(
v-else-if='current.targetType === `search`'
outline
:label='$t("navigation.navType.searchQuery")'
prepend-icon='search'
v-model='current.target'
)
v-card-chin v-card-chin
v-spacer v-spacer
v-btn(color='red', outline, @click='deleteItem(current)') v-btn(color='red', outline, @click='deleteItem(current)')
......
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
v-divider.my-0(v-if='n < core.children.length - 1') v-divider.my-0(v-if='n < core.children.length - 1')
v-flex(lg9, xs12) v-flex(lg9, xs12)
v-card v-card.wiki-form
v-toolbar( v-toolbar(
color='grey darken-1' color='grey darken-1'
dark dark
...@@ -84,7 +84,6 @@ ...@@ -84,7 +84,6 @@
v-select( v-select(
v-if='cfg.value.type === "string" && cfg.value.enum' v-if='cfg.value.type === "string" && cfg.value.enum'
outline outline
background-color='grey lighten-2'
:items='cfg.value.enum' :items='cfg.value.enum'
:key='cfg.key' :key='cfg.key'
:label='cfg.value.title' :label='cfg.value.title'
...@@ -105,7 +104,6 @@ ...@@ -105,7 +104,6 @@
v-text-field( v-text-field(
v-else v-else
outline outline
background-color='grey lighten-2'
:key='cfg.key' :key='cfg.key'
:label='cfg.value.title' :label='cfg.value.title'
v-model='cfg.value.value' v-model='cfg.value.value'
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
v-form.pt-3 v-form.pt-3
v-layout(row wrap) v-layout(row wrap)
v-flex(lg6 xs12) v-flex(lg6 xs12)
v-card v-card.wiki-form
v-toolbar(color='primary', dark, dense, flat) v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title v-toolbar-title
.subheading Theme .subheading Theme
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
persistent-hint persistent-hint
hint='Not recommended for accessibility. May not be supported by all themes.' hint='Not recommended for accessibility. May not be supported by all themes.'
) )
v-card.mt-3 v-card.wiki-form.mt-3
v-toolbar(color='primary', dark, dense, flat) v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title v-toolbar-title
.subheading Code Injection .subheading Code Injection
......
<template lang="pug"> <template lang="pug">
.criterias-item .criterias-item
//- Type //- Type
v-select(solo, :items='filteredCriteriaTypes', v-model='item.type', placeholder='Rule Type', ref='typeSelect') v-select(
solo
:items='filteredCriteriaTypes'
v-model='item.type'
placeholder='Rule Type'
ref='typeSelect'
hide-details
)
template(slot='item', slot-scope='data') template(slot='item', slot-scope='data')
v-list-tile-avatar v-list-tile-avatar
v-avatar(:color='data.item.color', size='40', tile): v-icon(color='white') {{ data.item.icon }} v-avatar(:color='data.item.color', size='40', tile): v-icon(color='white') {{ data.item.icon }}
...@@ -10,7 +17,15 @@ ...@@ -10,7 +17,15 @@
v-list-tile-sub-title.caption(v-html='data.item.description') v-list-tile-sub-title.caption(v-html='data.item.description')
//- Operator //- Operator
v-select(solo, :items='filteredCriteriaOperators', v-model='item.operator', placeholder='Operator', :disabled='!item.type', :class='!item.type ? "blue-grey lighten-4" : ""') v-select(
solo
:items='filteredCriteriaOperators'
v-model='item.operator'
placeholder='Operator'
:disabled='!item.type'
:class='!item.type ? "blue-grey lighten-4" : ""'
hide-details
)
template(slot='item', slot-scope='data') template(slot='item', slot-scope='data')
v-list-tile-avatar v-list-tile-avatar
v-avatar.white--text(color='blue', size='30', tile) {{ data.item.icon }} v-avatar.white--text(color='blue', size='30', tile) {{ data.item.icon }}
...@@ -18,12 +33,58 @@ ...@@ -18,12 +33,58 @@
v-list-tile-title(v-html='data.item.text') v-list-tile-title(v-html='data.item.text')
//- Value //- Value
v-select(v-if='item.type === "country"', solo, :items='countries', v-model='item.value', placeholder='Countries...', multiple, item-text='name', item-value='code') v-select(
v-text-field(v-else-if='item.type === "path"', solo, v-model='item.value', label='Path (e.g. /section)') v-if='item.type === "country"'
v-text-field(v-else-if='item.type === "date"', solo, @click.native.stop='dateActivator = true', v-model='item.value', label='YYYY-MM-DD', readonly) solo
v-text-field(v-else-if='item.type === "time"', solo, @click.native.stop='timeActivator = true', v-model='item.value', label='HH:MM', readonly) :items='countries'
v-select(v-else-if='item.type === "group"', solo, :items='groups', v-model='item.value', placeholder='Group...', item-text='name', item-value='id') v-model='item.value'
v-text-field.blue-grey.lighten-4(v-else, solo, disabled) placeholder='Countries...'
multiple
item-text='name'
item-value='code'
hide-details
)
v-text-field(
v-else-if='item.type === "path"'
solo
v-model='item.value'
label='Path (e.g. /section)'
hide-details
)
v-text-field(
v-else-if='item.type === "date"'
solo
@click.native.stop='dateActivator = true'
v-model='item.value'
label='YYYY-MM-DD'
readonly
hide-details
)
v-text-field(
v-else-if='item.type === "time"'
solo
@click.native.stop='timeActivator = true'
v-model='item.value'
label='HH:MM'
readonly
hide-details
)
v-select(
v-else-if='item.type === "group"'
solo
:items='groups'
v-model='item.value'
placeholder='Group...'
item-text='name'
item-value='id'
hide-details
)
v-text-field.blue-grey.lighten-4(
v-else
solo
disabled
hide-details
)
v-dialog(lazy, v-model='dateActivator', width='290px', ref='dateDialog') v-dialog(lazy, v-model='dateActivator', width='290px', ref='dateDialog')
v-date-picker(v-model='item.value', scrollable, color='primary') v-date-picker(v-model='item.value', scrollable, color='primary')
......
...@@ -87,29 +87,46 @@ ...@@ -87,29 +87,46 @@
icon icon
) )
v-icon(color='grey') search v-icon(color='grey') search
v-tooltip(bottom) v-tooltip(bottom, v-if='isAuthenticated && isAdmin')
v-btn.btn-animate-rotate(icon, href='/a', slot='activator') v-btn.btn-animate-rotate(icon, href='/a', slot='activator')
v-icon(color='grey') settings v-icon(color='grey') settings
span Admin span Admin
v-menu(offset-y, min-width='300') v-menu(offset-y, min-width='300')
v-tooltip(bottom, slot='activator') v-tooltip(bottom, slot='activator')
v-btn.btn-animate-grow(icon, slot='activator', outline, color='grey darken-3') v-btn.btn-animate-grow(icon, slot='activator', outline, :color='isAuthenticated ? `blue` : `grey darken-3`')
v-icon(color='grey') account_circle v-icon(color='grey') account_circle
span Account span Account
v-list.py-0(:light='!$vuetify.dark') v-list.py-0
v-list-tile.py-3(avatar) template(v-if='isAuthenticated')
v-list-tile.py-3.grey(avatar, :class='$vuetify.dark ? `darken-4-l5` : `lighten-5`')
v-list-tile-avatar v-list-tile-avatar
v-avatar.red(:size='40'): span.white--text.subheading JD v-avatar.blue(v-if='picture.kind === `initials`', :size='40')
span.white--text.subheading {{picture.initials}}
v-avatar(v-else-if='picture.kind === `image`', :size='40')
v-img(:src='picture.url')
v-list-tile-content v-list-tile-content
v-list-tile-title John Doe v-list-tile-title {{name}}
v-list-tile-sub-title john.doe@example.com v-list-tile-sub-title {{email}}
v-divider.my-0
v-list-tile(href='/w')
v-list-tile-action: v-icon(color='blue') web
v-list-tile-title My Wiki
v-divider.my-0 v-divider.my-0
v-list-tile(href='/p') v-list-tile(href='/p')
v-list-tile-action: v-icon(color='red') person v-list-tile-action: v-icon(color='blue') person
v-list-tile-title Profile v-list-tile-title Profile
v-divider.my-0
v-list-tile(@click='logout') v-list-tile(@click='logout')
v-list-tile-action: v-icon(color='red') exit_to_app v-list-tile-action: v-icon(color='red') exit_to_app
v-list-tile-title Logout v-list-tile-title Logout
template(v-else)
v-list-tile(href='/login')
v-list-tile-action: v-icon(color='grey') person
v-list-tile-title Login
v-divider.my-0
v-list-tile(href='/register')
v-list-tile-action: v-icon(color='grey') person_add
v-list-tile-title Register
page-selector(mode='create', v-model='newPageModal') page-selector(mode='create', v-model='newPageModal')
</template> </template>
...@@ -143,7 +160,33 @@ export default { ...@@ -143,7 +160,33 @@ export default {
isLoading: get('isLoading'), isLoading: get('isLoading'),
title: get('site/title'), title: get('site/title'),
path: get('page/path'), path: get('page/path'),
mode: get('page/mode') mode: get('page/mode'),
name: get('user/name'),
email: get('user/email'),
pictureUrl: get('user/pictureUrl'),
isAuthenticated: get('user/authenticated'),
permissions: get('user/permissions'),
picture() {
if (this.pictureUrl && this.pictureUrl.length > 1) {
return {
kind: 'image',
url: this.pictureUrl
}
} else {
const nameParts = this.name.toUpperCase().split(' ')
let initials = _.head(nameParts).charAt(0)
if (nameParts.length > 1) {
initials += _.last(nameParts).charAt(0)
}
return {
kind: 'initials',
initials
}
}
},
isAdmin() {
return _.includes(this.permissions, 'manage:system')
}
}, },
created() { created() {
if (this.hideSearch || this.dense || this.$vuetify.breakpoint.smAndDown) { if (this.hideSearch || this.dense || this.$vuetify.breakpoint.smAndDown) {
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
v-list-tile v-list-tile
v-list-tile-avatar: v-icon delete v-list-tile-avatar: v-icon delete
v-list-tile-title Delete Page v-list-tile-title Delete Page
v-card(tile) v-card.wiki-form(tile)
v-card-text v-card-text
v-subheader.pl-0 Page Info v-subheader.pl-0 Page Info
v-text-field( v-text-field(
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
@import 'components/v-btn'; @import 'components/v-btn';
@import 'components/v-data-table'; @import 'components/v-data-table';
@import 'components/v-dialog'; @import 'components/v-dialog';
@import 'components/vue-tree-navigation'; @import 'components/v-form';
@import 'layout/md2'; @import 'layout/md2';
......
.wiki-form {
&.theme--light {
background-color: mc('grey', '50');
}
.v-text-field--outline {
.v-input__slot {
background-color: #FFF !important;
border-color: mc('grey', '300') !important;
border-radius: 7px;
@at-root .theme--dark & {
background-color: lighten(mc('grey', '900'), 5%) !important;
border-color: mc('grey', '700') !important;
.v-label.v-label--active.primary--text {
color: mc('blue', '500') !important;
}
}
}
&.v-input--is-focused .v-input__slot {
border-color: mc('blue', '500') !important;
}
@at-root .theme--dark & {
.v-icon.primary--text {
color: mc('blue', '500') !important;
}
}
}
}
.TreeNavigation.treenav {
font-size: 13px;
li {
padding-left: 24px;
}
a {
text-decoration: none;
color: mc('grey', '800');
}
.NavigationLevel__parent {
// font-weight: 600;
}
.NavigationItem {
padding: 3px 0;
}
.NavigationItem--active {
color: #42b883;
}
.NavigationToggle__icon {
border-color: mc('blue', '600');
}
}
...@@ -6,6 +6,7 @@ import { make } from 'vuex-pathify' // eslint-disable-line import/no-duplicates ...@@ -6,6 +6,7 @@ import { make } from 'vuex-pathify' // eslint-disable-line import/no-duplicates
import page from './page' import page from './page'
import site from './site' import site from './site'
import user from './user'
Vue.use(Vuex) Vue.use(Vuex)
...@@ -51,6 +52,7 @@ export default new Vuex.Store({ ...@@ -51,6 +52,7 @@ export default new Vuex.Store({
actions: { }, actions: { },
modules: { modules: {
page, page,
site site,
user
} }
}) })
import { make } from 'vuex-pathify'
import jwt from 'jsonwebtoken'
import Cookies from 'js-cookie'
const state = {
id: 0,
email: '',
name: '',
pictureUrl: '',
localeCode: '',
defaultEditor: '',
permissions: [],
iat: 0,
exp: 0,
authenticated: false
}
export default {
namespaced: true,
state,
mutations: {
...make.mutations(state),
REFRESH_AUTH(state) {
const jwtCookie = Cookies.get('jwt')
if (jwtCookie) {
try {
const jwtData = jwt.decode(jwtCookie)
state.id = jwtData.id
state.email = jwtData.email
state.name = jwtData.name
state.pictureUrl = jwtData.pictureUrl
state.localeCode = jwtData.localeCode
state.defaultEditor = jwtData.defaultEditor
state.permissions = jwtData.permissions
state.iat = jwtData.iat
state.exp = jwtData.exp
state.authenticated = true
} catch (err) {
console.debug('Invalid JWT. Silent authentication skipped.')
}
}
}
}
}
...@@ -83,19 +83,14 @@ ...@@ -83,19 +83,14 @@
) )
.pb-2.caption.grey--text 5 votes .pb-2.caption.grey--text 5 votes
v-divider v-divider
template(v-if='tags.length')
v-list.grey(dense, :class='darkMode ? `darken-3-d3` : `lighten-3`') v-list.grey(dense, :class='darkMode ? `darken-3-d3` : `lighten-3`')
v-subheader.pl-4.teal--text Tags v-subheader.pl-4.teal--text Tags
v-list-tile template(v-for='(tag, idx) in tags')
v-list-tile(:href='`/t/` + tag.slug')
v-list-tile-avatar: v-icon(color='teal') label v-list-tile-avatar: v-icon(color='teal') label
v-list-tile-title Astrophysics v-list-tile-title {{tag.title}}
v-divider(inset) v-divider(inset, v-if='idx < tags.length - 1')
v-list-tile
v-list-tile-avatar: v-icon(color='teal') label
v-list-tile-title Space
v-divider(inset)
v-list-tile
v-list-tile-avatar: v-icon(color='teal') label
v-list-tile-title Planets
v-divider v-divider
v-toolbar(:color='darkMode ? `grey darken-3` : `grey lighten-4`', flat, dense) v-toolbar(:color='darkMode ? `grey darken-3` : `grey lighten-4`', flat, dense)
v-spacer v-spacer
......
...@@ -136,6 +136,7 @@ ...@@ -136,6 +136,7 @@
"passport-slack": "0.0.7", "passport-slack": "0.0.7",
"passport-twitch": "1.0.3", "passport-twitch": "1.0.3",
"passport-windowslive": "1.0.2", "passport-windowslive": "1.0.2",
"pem-jwk": "1.5.1",
"pg": "7.6.1", "pg": "7.6.1",
"pg-hstore": "2.3.2", "pg-hstore": "2.3.2",
"pm2": "3.2.2", "pm2": "3.2.2",
......
/* global WIKI */ /* global WIKI */
const Promise = require('bluebird')
const express = require('express') const express = require('express')
const router = express.Router() const router = express.Router()
const ExpressBrute = require('express-brute')
const ExpressBruteRedisStore = require('express-brute-redis')
const jwt = require('jsonwebtoken')
const moment = require('moment')
const _ = require('lodash')
/**
* Setup Express-Brute
*/
const EBstore = new ExpressBruteRedisStore({
client: WIKI.redis
})
const bruteforce = new ExpressBrute(EBstore, {
freeRetries: 5,
minWait: 60 * 1000,
maxWait: 5 * 60 * 1000,
refreshTimeoutOnRequest: false,
failCallback (req, res, next, nextValidRequestDate) {
req.flash('alert', {
class: 'error',
title: WIKI.lang.t('auth:errors.toomanyattempts'),
message: WIKI.lang.t('auth:errors.toomanyattemptsmsg', { time: moment(nextValidRequestDate).fromNow() }),
iconClass: 'fa-times'
})
res.redirect('/login')
}
})
/** /**
* Login form * Login form
...@@ -38,72 +10,6 @@ router.get('/login', function (req, res, next) { ...@@ -38,72 +10,6 @@ router.get('/login', function (req, res, next) {
res.render('login') res.render('login')
}) })
router.post('/login', bruteforce.prevent, function (req, res, next) {
new Promise((resolve, reject) => {
// [1] LOCAL AUTHENTICATION
WIKI.auth.passport.authenticate('local', { session: false }, function (err, user, info) {
if (err) { return reject(err) }
if (!user) { return reject(new Error('INVALID_LOGIN')) }
resolve(user)
})(req, res, next)
}).catch({ message: 'INVALID_LOGIN' }, err => {
if (_.has(WIKI.config.auth.strategy, 'ldap')) {
// [2] LDAP AUTHENTICATION
return new Promise((resolve, reject) => {
WIKI.auth.passport.authenticate('ldapauth', { session: false }, function (err, user, info) {
if (err) { return reject(err) }
if (info && info.message) { return reject(new Error(info.message)) }
if (!user) { return reject(new Error('INVALID_LOGIN')) }
resolve(user)
})(req, res, next)
})
} else {
throw err
}
}).then((user) => {
// LOGIN SUCCESS
return req.logIn(user, { session: false }, function (err) {
if (err) { return next(err) }
req.brute.reset(function () {
return res.redirect('/')
})
})
}).catch(err => {
// LOGIN FAIL
if (err.message === 'INVALID_LOGIN') {
req.flash('alert', {
title: WIKI.lang.t('auth:errors.invalidlogin'),
message: WIKI.lang.t('auth:errors.invalidloginmsg')
})
return res.redirect('/login')
} else {
req.flash('alert', {
title: WIKI.lang.t('auth:errors.loginerror'),
message: err.message
})
return res.redirect('/login')
}
})
})
/**
* Social Login
*/
router.get('/login/ms', WIKI.auth.passport.authenticate('windowslive', { scope: ['wl.signin', 'wl.basic', 'wl.emails'] }))
router.get('/login/google', WIKI.auth.passport.authenticate('google', { scope: ['profile', 'email'] }))
router.get('/login/facebook', WIKI.auth.passport.authenticate('facebook', { scope: ['public_profile', 'email'] }))
router.get('/login/github', WIKI.auth.passport.authenticate('github', { scope: ['user:email'] }))
router.get('/login/slack', WIKI.auth.passport.authenticate('slack', { scope: ['identity.basic', 'identity.email'] }))
router.get('/login/azure', WIKI.auth.passport.authenticate('azure_ad_oauth2'))
router.get('/login/ms/callback', WIKI.auth.passport.authenticate('windowslive', { failureRedirect: '/login', successRedirect: '/' }))
router.get('/login/google/callback', WIKI.auth.passport.authenticate('google', { failureRedirect: '/login', successRedirect: '/' }))
router.get('/login/facebook/callback', WIKI.auth.passport.authenticate('facebook', { failureRedirect: '/login', successRedirect: '/' }))
router.get('/login/github/callback', WIKI.auth.passport.authenticate('github', { failureRedirect: '/login', successRedirect: '/' }))
router.get('/login/slack/callback', WIKI.auth.passport.authenticate('slack', { failureRedirect: '/login', successRedirect: '/' }))
router.get('/login/azure/callback', WIKI.auth.passport.authenticate('azure_ad_oauth2', { failureRedirect: '/login', successRedirect: '/' }))
/** /**
* Logout * Logout
*/ */
...@@ -112,4 +18,14 @@ router.get('/logout', function (req, res) { ...@@ -112,4 +18,14 @@ router.get('/logout', function (req, res) {
res.redirect('/') res.redirect('/')
}) })
/**
* JWT Public Endpoints
*/
router.get('/.well-known/jwk.json', function (req, res, next) {
res.json(WIKI.config.certs.jwk)
})
router.get('/.well-known/jwk.pem', function (req, res, next) {
res.send(WIKI.config.certs.public)
})
module.exports = router module.exports = router
...@@ -45,7 +45,7 @@ module.exports = { ...@@ -45,7 +45,7 @@ module.exports = {
// Load JWT // Load JWT
passport.use('jwt', new passportJWT.Strategy({ passport.use('jwt', new passportJWT.Strategy({
jwtFromRequest: securityHelper.extractJWT, jwtFromRequest: securityHelper.extractJWT,
secretOrKey: WIKI.config.sessionSecret, secretOrKey: WIKI.config.certs.public,
audience: 'urn:wiki.js', // TODO: use value from admin audience: 'urn:wiki.js', // TODO: use value from admin
issuer: 'urn:wiki.js' issuer: 'urn:wiki.js'
}, (jwtPayload, cb) => { }, (jwtPayload, cb) => {
......
...@@ -253,7 +253,11 @@ module.exports = class User extends Model { ...@@ -253,7 +253,11 @@ module.exports = class User extends Model {
localeCode: user.localeCode, localeCode: user.localeCode,
defaultEditor: user.defaultEditor, defaultEditor: user.defaultEditor,
permissions: ['manage:system'] permissions: ['manage:system']
}, WIKI.config.sessionSecret, { }, {
key: WIKI.config.certs.private,
passphrase: WIKI.config.sessionSecret
}, {
algorithm: 'RS256',
expiresIn: '30m', expiresIn: '30m',
audience: 'urn:wiki.js', // TODO: use value from admin audience: 'urn:wiki.js', // TODO: use value from admin
issuer: 'urn:wiki.js' issuer: 'urn:wiki.js'
......
...@@ -25,6 +25,7 @@ module.exports = () => { ...@@ -25,6 +25,7 @@ module.exports = () => {
const _ = require('lodash') const _ = require('lodash')
const cfgHelper = require('./helpers/config') const cfgHelper = require('./helpers/config')
const crypto = Promise.promisifyAll(require('crypto')) const crypto = Promise.promisifyAll(require('crypto'))
const pem2jwk = require('pem-jwk').pem2jwk
// ---------------------------------------- // ----------------------------------------
// Define Express App // Define Express App
...@@ -90,6 +91,7 @@ module.exports = () => { ...@@ -90,6 +91,7 @@ module.exports = () => {
} }
// Create directory structure // Create directory structure
WIKI.logger.info('Creating data directories...')
const dataPath = path.join(process.cwd(), 'data') const dataPath = path.join(process.cwd(), 'data')
await fs.ensureDir(dataPath) await fs.ensureDir(dataPath)
await fs.ensureDir(path.join(dataPath, 'cache')) await fs.ensureDir(path.join(dataPath, 'cache'))
...@@ -110,6 +112,26 @@ module.exports = () => { ...@@ -110,6 +112,26 @@ module.exports = () => {
_.set(WIKI.config, 'theming.darkMode', false) _.set(WIKI.config, 'theming.darkMode', false)
_.set(WIKI.config, 'title', 'Wiki.js') _.set(WIKI.config, 'title', 'Wiki.js')
// Generate certificates
WIKI.logger.info('Generating certificates...')
const certs = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'pkcs1',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs1',
format: 'pem',
cipher: 'aes-256-cbc',
passphrase: WIKI.config.sessionSecret
}
})
_.set(WIKI.config, 'certs.jwk', pem2jwk(certs.publicKey))
_.set(WIKI.config, 'certs.public', certs.publicKey)
_.set(WIKI.config, 'certs.private', certs.privateKey)
// Save config to DB // Save config to DB
WIKI.logger.info('Persisting config to DB...') WIKI.logger.info('Persisting config to DB...')
await WIKI.configSvc.saveToDb([ await WIKI.configSvc.saveToDb([
...@@ -120,7 +142,8 @@ module.exports = () => { ...@@ -120,7 +142,8 @@ module.exports = () => {
'sessionSecret', 'sessionSecret',
'telemetry', 'telemetry',
'theming', 'theming',
'title' 'title',
'certs'
]) ])
// Create default locale // Create default locale
......
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