feat: register validation + create + admin improvements
This commit is contained in:
parent
17244a0cb3
commit
901dbb98e0
@ -9,3 +9,7 @@ insert_final_newline = true
|
|||||||
|
|
||||||
[*.{jade,pug,md}]
|
[*.{jade,pug,md}]
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
1
Makefile
1
Makefile
@ -30,6 +30,7 @@ docker-dev-down: ## Shutdown dockerized dev environment
|
|||||||
docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . down
|
docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . down
|
||||||
|
|
||||||
docker-dev-rebuild: ## Rebuild dockerized dev image
|
docker-dev-rebuild: ## Rebuild dockerized dev image
|
||||||
|
rm -rf ./node_modules
|
||||||
docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . build --no-cache --force-rm
|
docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . build --no-cache --force-rm
|
||||||
|
|
||||||
docker-build: ## Run assets generation build in docker
|
docker-build: ## Run assets generation build in docker
|
||||||
|
@ -163,10 +163,12 @@ Vue.component('admin', () => import(/* webpackChunkName: "admin" */ './component
|
|||||||
Vue.component('editor', () => import(/* webpackPrefetch: -100, webpackChunkName: "editor" */ './components/editor.vue'))
|
Vue.component('editor', () => import(/* webpackPrefetch: -100, webpackChunkName: "editor" */ './components/editor.vue'))
|
||||||
Vue.component('history', () => import(/* webpackChunkName: "history" */ './components/history.vue'))
|
Vue.component('history', () => import(/* webpackChunkName: "history" */ './components/history.vue'))
|
||||||
Vue.component('page-source', () => import(/* webpackChunkName: "source" */ './components/source.vue'))
|
Vue.component('page-source', () => import(/* webpackChunkName: "source" */ './components/source.vue'))
|
||||||
|
Vue.component('loader', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/loader.vue'))
|
||||||
Vue.component('login', () => import(/* webpackPrefetch: true, webpackChunkName: "login" */ './components/login.vue'))
|
Vue.component('login', () => import(/* webpackPrefetch: true, webpackChunkName: "login" */ './components/login.vue'))
|
||||||
Vue.component('nav-header', () => import(/* webpackMode: "eager" */ './components/common/nav-header.vue'))
|
Vue.component('nav-header', () => import(/* webpackMode: "eager" */ './components/common/nav-header.vue'))
|
||||||
Vue.component('page-selector', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/page-selector.vue'))
|
Vue.component('page-selector', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/page-selector.vue'))
|
||||||
Vue.component('profile', () => import(/* webpackChunkName: "profile" */ './components/profile.vue'))
|
Vue.component('profile', () => import(/* webpackChunkName: "profile" */ './components/profile.vue'))
|
||||||
|
Vue.component('register', () => import(/* webpackChunkName: "register" */ './components/register.vue'))
|
||||||
Vue.component('v-card-chin', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/v-card-chin.vue'))
|
Vue.component('v-card-chin', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/v-card-chin.vue'))
|
||||||
|
|
||||||
Vue.component('nav-footer', () => import(/* webpackChunkName: "theme-page" */ './themes/' + process.env.CURRENT_THEME + '/components/nav-footer.vue'))
|
Vue.component('nav-footer', () => import(/* webpackChunkName: "theme-page" */ './themes/' + process.env.CURRENT_THEME + '/components/nav-footer.vue'))
|
||||||
|
@ -151,6 +151,18 @@
|
|||||||
multiple
|
multiple
|
||||||
chips
|
chips
|
||||||
)
|
)
|
||||||
|
template(v-if='strategy.key === `local`')
|
||||||
|
v-divider.mt-3
|
||||||
|
v-subheader.pl-0 Security
|
||||||
|
.pr-3
|
||||||
|
v-switch.ml-3(
|
||||||
|
:disabled='true'
|
||||||
|
v-model='strategy.recaptcha'
|
||||||
|
label='Use reCAPTCHA by Google'
|
||||||
|
color='primary'
|
||||||
|
hint='Protects against spam robots and malicious registrations.'
|
||||||
|
persistent-hint
|
||||||
|
)
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
.subheading Sponsors
|
.subheading Sponsors
|
||||||
v-spacer
|
v-spacer
|
||||||
v-btn(outline, small, href='https://opencollective.com/wikijs/order/1273') Become a Sponsor
|
v-btn(outline, small, href='https://opencollective.com/wikijs/order/1273') Become a Sponsor
|
||||||
v-list(two-line, dense)
|
v-list(two-line)
|
||||||
template(v-for='(sponsor, idx) in sponsors')
|
template(v-for='(sponsor, idx) in sponsors')
|
||||||
v-list-tile(:key='sponsor.id')
|
v-list-tile(:key='sponsor.id')
|
||||||
v-list-tile-avatar
|
v-list-tile-avatar
|
||||||
@ -89,6 +89,58 @@
|
|||||||
v-btn(icon, :href='backer.website', target='_blank')
|
v-btn(icon, :href='backer.website', target='_blank')
|
||||||
v-icon(color='grey') public
|
v-icon(color='grey') public
|
||||||
v-divider(v-if='idx < backers.length - 1')
|
v-divider(v-if='idx < backers.length - 1')
|
||||||
|
v-toolbar(color='primary', dense, dark)
|
||||||
|
.subheading Special Thanks
|
||||||
|
v-list(two-line)
|
||||||
|
v-list-tile
|
||||||
|
v-list-tile-avatar
|
||||||
|
img(src='https://static.requarks.io/logo/algolia.svg', alt='Algolia')
|
||||||
|
v-list-tile-content
|
||||||
|
v-list-tile-title Algolia
|
||||||
|
v-list-tile-sub-title Algolia is a powerful search-as-a-service solution, made easy to use with API clients, UI libraries, and pre-built integrations.
|
||||||
|
v-list-tile-action
|
||||||
|
v-btn(icon, href='https://www.algolia.com/', target='_blank')
|
||||||
|
v-icon(color='grey') public
|
||||||
|
v-divider
|
||||||
|
v-list-tile
|
||||||
|
v-list-tile-avatar
|
||||||
|
img(src='https://static.requarks.io/logo/browserstack.svg', alt='Browserstack')
|
||||||
|
v-list-tile-content
|
||||||
|
v-list-tile-title BrowserStack
|
||||||
|
v-list-tile-sub-title BrowserStack is a cloud web and mobile testing platform that enables developers to test their websites and mobile applications.
|
||||||
|
v-list-tile-action
|
||||||
|
v-btn(icon, href='https://www.browserstack.com/', target='_blank')
|
||||||
|
v-icon(color='grey') public
|
||||||
|
v-divider
|
||||||
|
v-list-tile
|
||||||
|
v-list-tile-avatar
|
||||||
|
img(src='https://static.requarks.io/logo/cloudflare.svg', alt='Cloudflare')
|
||||||
|
v-list-tile-content
|
||||||
|
v-list-tile-title Cloudflare
|
||||||
|
v-list-tile-sub-title Providing content delivery network services, DDoS mitigation, Internet security and distributed domain name server services.
|
||||||
|
v-list-tile-action
|
||||||
|
v-btn(icon, href='https://www.cloudflare.com/', target='_blank')
|
||||||
|
v-icon(color='grey') public
|
||||||
|
v-divider
|
||||||
|
v-list-tile
|
||||||
|
v-list-tile-avatar
|
||||||
|
img(src='https://static.requarks.io/logo/digitalocean.svg', alt='DigitalOcean')
|
||||||
|
v-list-tile-content
|
||||||
|
v-list-tile-title DigitalOcean
|
||||||
|
v-list-tile-sub-title Providing developers and businesses a reliable, easy-to-use cloud computing platform of virtual servers (Droplets), object storage (Spaces), and more.
|
||||||
|
v-list-tile-action
|
||||||
|
v-btn(icon, href='https://m.do.co/c/5f7445bfa4d0', target='_blank')
|
||||||
|
v-icon(color='grey') public
|
||||||
|
v-divider
|
||||||
|
v-list-tile
|
||||||
|
v-list-tile-avatar
|
||||||
|
img(src='/svg/logo-icons8.svg', alt='Icons8')
|
||||||
|
v-list-tile-content
|
||||||
|
v-list-tile-title Icons8
|
||||||
|
v-list-tile-sub-title All the Icons You Need. Guaranteed.
|
||||||
|
v-list-tile-action
|
||||||
|
v-btn(icon, href='https://icons8.com', target='_blank')
|
||||||
|
v-icon(color='grey') public
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
avatar
|
avatar
|
||||||
:key='rdr.key'
|
:key='rdr.key'
|
||||||
@click='selectRenderer(rdr.key)'
|
@click='selectRenderer(rdr.key)'
|
||||||
:class='currentRenderer.key === rdr.key ? "blue lighten-5" : ""'
|
:class='currentRenderer.key === rdr.key ? (darkMode ? `grey darken-4-l4` : `blue lighten-5`) : ``'
|
||||||
)
|
)
|
||||||
v-list-tile-avatar
|
v-list-tile-avatar
|
||||||
v-icon(:color='currentRenderer.key === rdr.key ? "primary" : "grey"') {{rdr.icon}}
|
v-icon(:color='currentRenderer.key === rdr.key ? "primary" : "grey"') {{rdr.icon}}
|
||||||
@ -120,6 +120,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { DepGraph } from 'dependency-graph'
|
import { DepGraph } from 'dependency-graph'
|
||||||
|
import { get } from 'vuex-pathify'
|
||||||
|
|
||||||
import { StatusIndicator } from 'vue-status-indicator'
|
import { StatusIndicator } from 'vue-status-indicator'
|
||||||
|
|
||||||
@ -136,6 +137,9 @@ export default {
|
|||||||
currentRenderer: {}
|
currentRenderer: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
darkMode: get('site/dark'),
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
renderers(newValue, oldValue) {
|
renderers(newValue, oldValue) {
|
||||||
_.delay(() => {
|
_.delay(() => {
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
v-list-tile-content
|
v-list-tile-content
|
||||||
v-list-tile-title Email
|
v-list-tile-title Email
|
||||||
v-list-tile-sub-title {{ user.email }}
|
v-list-tile-sub-title {{ user.email }}
|
||||||
v-list-tile-action
|
v-list-tile-action(v-if='!user.isSystem')
|
||||||
v-btn(icon, color='grey', flat)
|
v-btn(icon, color='grey', flat)
|
||||||
v-icon edit
|
v-icon edit
|
||||||
v-divider
|
v-divider
|
||||||
@ -50,7 +50,7 @@
|
|||||||
v-list-tile-action
|
v-list-tile-action
|
||||||
v-btn(icon, color='grey', flat)
|
v-btn(icon, color='grey', flat)
|
||||||
v-icon edit
|
v-icon edit
|
||||||
v-card.mt-3
|
v-card.mt-3(v-if='!user.isSystem')
|
||||||
v-toolbar(color='primary', dense, dark, flat)
|
v-toolbar(color='primary', dense, dark, flat)
|
||||||
v-icon.mr-2 lock_outline
|
v-icon.mr-2 lock_outline
|
||||||
span Authentication
|
span Authentication
|
||||||
|
51
client/components/common/loader.vue
Normal file
51
client/components/common/loader.vue
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<template lang='pug'>
|
||||||
|
v-dialog(v-model='value', persistent, max-width='350')
|
||||||
|
v-card.loader-dialog.radius-7(:color='color', dark)
|
||||||
|
v-card-text.text-xs-center.py-4
|
||||||
|
atom-spinner.is-inline(
|
||||||
|
:animation-duration='1000'
|
||||||
|
:size='60'
|
||||||
|
color='#FFF'
|
||||||
|
)
|
||||||
|
.subheading {{ title }}
|
||||||
|
.caption {{ subtitle }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { AtomSpinner } from 'epic-spinners'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AtomSpinner
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: 'blue darken-3'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: 'Working...'
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
type: String,
|
||||||
|
default: 'Please wait'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang='scss'>
|
||||||
|
.loader-dialog {
|
||||||
|
.atom-spinner.is-inline {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.caption {
|
||||||
|
color: rgba(255,255,255,.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
79
client/components/common/password-strength.vue
Normal file
79
client/components/common/password-strength.vue
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
.password-strength
|
||||||
|
v-progress-linear(
|
||||||
|
:color='passwordStrengthColor'
|
||||||
|
v-model='passwordStrength'
|
||||||
|
height='2'
|
||||||
|
)
|
||||||
|
.caption(v-if='!hideText', :class='passwordStrengthColor + "--text"') {{passwordStrengthText}}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import zxcvbn from 'zxcvbn'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
hideText: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
passwordStrength: 0,
|
||||||
|
passwordStrengthColor: 'grey',
|
||||||
|
passwordStrengthText: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(newValue) {
|
||||||
|
this.checkPasswordStrength(newValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkPasswordStrength: _.debounce(function (pwd) {
|
||||||
|
if (!pwd || pwd.length < 1) {
|
||||||
|
this.passwordStrength = 0
|
||||||
|
this.passwordStrengthColor = 'grey'
|
||||||
|
this.passwordStrengthText = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const strength = zxcvbn(pwd)
|
||||||
|
this.passwordStrength = _.round((strength.score + 1 ) / 5 * 100)
|
||||||
|
if (this.passwordStrength <= 20) {
|
||||||
|
this.passwordStrengthColor = 'red'
|
||||||
|
this.passwordStrengthText = 'Very Weak'
|
||||||
|
} else if (this.passwordStrength <= 40) {
|
||||||
|
this.passwordStrengthColor = 'orange'
|
||||||
|
this.passwordStrengthText = 'Weak'
|
||||||
|
} else if (this.passwordStrength <= 60) {
|
||||||
|
this.passwordStrengthColor = 'teal'
|
||||||
|
this.passwordStrengthText = 'Average'
|
||||||
|
} else if (this.passwordStrength <= 80) {
|
||||||
|
this.passwordStrengthColor = 'green'
|
||||||
|
this.passwordStrengthText = 'Strong'
|
||||||
|
} else {
|
||||||
|
this.passwordStrengthColor = 'green'
|
||||||
|
this.passwordStrengthText = 'Very Strong'
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
|
||||||
|
.password-strength > .caption {
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
margin: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
@ -30,16 +30,6 @@
|
|||||||
v-content
|
v-content
|
||||||
component(:is='currentEditor')
|
component(:is='currentEditor')
|
||||||
editor-modal-properties(v-model='dialogProps')
|
editor-modal-properties(v-model='dialogProps')
|
||||||
v-dialog(v-model='dialogProgress', persistent, max-width='350')
|
|
||||||
v-card(color='blue darken-3', dark)
|
|
||||||
v-card-text.text-xs-center.py-4
|
|
||||||
atom-spinner.is-inline(
|
|
||||||
:animation-duration='1000'
|
|
||||||
:size='60'
|
|
||||||
color='#FFF'
|
|
||||||
)
|
|
||||||
.subheading {{ $t('editor:save.processing') }}
|
|
||||||
.caption.blue--text.text--lighten-3 {{ $t('editor:save.pleaseWait') }}
|
|
||||||
v-dialog(v-model='dialogEditorSelector', persistent, max-width='700')
|
v-dialog(v-model='dialogEditorSelector', persistent, max-width='700')
|
||||||
v-card.radius-7(color='blue darken-3', dark)
|
v-card.radius-7(color='blue darken-3', dark)
|
||||||
v-card-text.text-xs-center.py-4
|
v-card-text.text-xs-center.py-4
|
||||||
@ -88,6 +78,7 @@
|
|||||||
.caption.grey--text.text--darken-1 Drag-n-drop
|
.caption.grey--text.text--darken-1 Drag-n-drop
|
||||||
.caption.blue--text.text--lighten-2 This cannot be changed once the page is created.
|
.caption.blue--text.text--lighten-2 This cannot be changed once the page is created.
|
||||||
|
|
||||||
|
loader(v-model='dialogProgress', :title='$t(`editor:save.processing`)', :subtitle='$t(`editor:save.pleaseWait`)')
|
||||||
v-snackbar(
|
v-snackbar(
|
||||||
:color='notification.style'
|
:color='notification.style'
|
||||||
bottom,
|
bottom,
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
v-toolbar(color='primary', flat, dense, dark)
|
v-toolbar(color='primary', flat, dense, dark)
|
||||||
v-spacer
|
v-spacer
|
||||||
.subheading(v-if='screen === "tfa"') {{ $t('auth:tfa.subtitle') }}
|
.subheading(v-if='screen === "tfa"') {{ $t('auth:tfa.subtitle') }}
|
||||||
.subheading(v-else-if='selectedStrategy.key !== "local"') Login using {{ selectedStrategy.title }}
|
.subheading(v-else-if='selectedStrategy.key !== "local"') {{ $t('auth:loginUsingStrategy', { strategy: selectedStrategy.title }) }}
|
||||||
.subheading(v-else) {{ $t('auth:loginRequired') }}
|
.subheading(v-else) {{ $t('auth:loginRequired') }}
|
||||||
v-spacer
|
v-spacer
|
||||||
v-card-text.text-xs-center
|
v-card-text.text-xs-center
|
||||||
@ -80,12 +80,12 @@
|
|||||||
v-spacer
|
v-spacer
|
||||||
v-card-actions.pb-3(v-if='selectedStrategy.key === "local"')
|
v-card-actions.pb-3(v-if='selectedStrategy.key === "local"')
|
||||||
v-spacer
|
v-spacer
|
||||||
a.caption(href='') Forgot your password?
|
a.caption(href='') {{ $t('auth:forgotPasswordLink') }}
|
||||||
v-spacer
|
v-spacer
|
||||||
template(v-if='isSocialShown')
|
template(v-if='isSocialShown')
|
||||||
v-divider
|
v-divider
|
||||||
v-card-text.grey.lighten-4.text-xs-center
|
v-card-text.grey.lighten-4.text-xs-center
|
||||||
.pb-2.body-2.text-xs-center.grey--text.text--darken-2 or login using...
|
.pb-2.body-2.text-xs-center.grey--text.text--darken-2 {{ $t('auth:orLoginUsingStrategy') }}
|
||||||
v-tooltip(top, v-for='strategy in strategies', :key='strategy.key')
|
v-tooltip(top, v-for='strategy in strategies', :key='strategy.key')
|
||||||
.social-login-btn.mr-2(
|
.social-login-btn.mr-2(
|
||||||
slot='activator'
|
slot='activator'
|
||||||
@ -99,8 +99,11 @@
|
|||||||
v-divider
|
v-divider
|
||||||
v-card-actions.py-3(:class='isSocialShown ? "" : "grey lighten-4"')
|
v-card-actions.py-3(:class='isSocialShown ? "" : "grey lighten-4"')
|
||||||
v-spacer
|
v-spacer
|
||||||
.caption Don't have an account yet? #[a.caption(href='') Create an account]
|
i18next.caption(path='auth:switchToRegister.text', tag='div')
|
||||||
|
a.caption(href='/register', place='link') {{ $t('auth:switchToRegister.link') }}
|
||||||
v-spacer
|
v-spacer
|
||||||
|
|
||||||
|
loader(v-model='isLoading', :color='loaderColor', :title='loaderTitle', :subtitle='$t(`auth:pleaseWait`)')
|
||||||
nav-footer(color='grey darken-4')
|
nav-footer(color='grey darken-4')
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -128,6 +131,8 @@ export default {
|
|||||||
securityCode: '',
|
securityCode: '',
|
||||||
loginToken: '',
|
loginToken: '',
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
loaderColor: 'grey darken-4',
|
||||||
|
loaderTitle: 'Working...',
|
||||||
isShown: false
|
isShown: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -173,18 +178,20 @@ export default {
|
|||||||
if (this.username.length < 2) {
|
if (this.username.length < 2) {
|
||||||
this.$store.commit('showNotification', {
|
this.$store.commit('showNotification', {
|
||||||
style: 'red',
|
style: 'red',
|
||||||
message: 'Enter a valid email / username.',
|
message: this.$t('auth:invalidEmailUsername'),
|
||||||
icon: 'warning'
|
icon: 'warning'
|
||||||
})
|
})
|
||||||
this.$refs.iptEmail.focus()
|
this.$refs.iptEmail.focus()
|
||||||
} else if (this.password.length < 2) {
|
} else if (this.password.length < 2) {
|
||||||
this.$store.commit('showNotification', {
|
this.$store.commit('showNotification', {
|
||||||
style: 'red',
|
style: 'red',
|
||||||
message: 'Enter a valid password.',
|
message: this.$t('auth:invalidPassword'),
|
||||||
icon: 'warning'
|
icon: 'warning'
|
||||||
})
|
})
|
||||||
this.$refs.iptPassword.focus()
|
this.$refs.iptPassword.focus()
|
||||||
} else {
|
} else {
|
||||||
|
this.loaderColor = 'grey darken-4'
|
||||||
|
this.loaderTitle = this.$t('auth:signingIn')
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
try {
|
try {
|
||||||
let resp = await this.$apollo.mutate({
|
let resp = await this.$apollo.mutate({
|
||||||
@ -205,23 +212,20 @@ export default {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.iptTFA.focus()
|
this.$refs.iptTFA.focus()
|
||||||
})
|
})
|
||||||
|
this.isLoading = false
|
||||||
} else {
|
} else {
|
||||||
this.$store.commit('showNotification', {
|
this.loaderColor = 'green darken-1'
|
||||||
message: 'Login Successful! Redirecting...',
|
this.loaderTitle = this.$t('auth:loginSuccess')
|
||||||
style: 'success',
|
|
||||||
icon: 'check'
|
|
||||||
})
|
|
||||||
Cookies.set('jwt', respObj.jwt, { expires: 365 })
|
Cookies.set('jwt', respObj.jwt, { expires: 365 })
|
||||||
_.delay(() => {
|
_.delay(() => {
|
||||||
window.location.replace('/') // TEMPORARY - USE RETURNURL
|
window.location.replace('/') // TEMPORARY - USE RETURNURL
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
this.isLoading = false
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(respObj.responseResult.message)
|
throw new Error(respObj.responseResult.message)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Authentication is unavailable.')
|
throw new Error(this.$t('auth:genericError'))
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
@ -270,7 +274,7 @@ export default {
|
|||||||
throw new Error(respObj.responseResult.message)
|
throw new Error(respObj.responseResult.message)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Authentication is unavailable.')
|
throw new Error(this.$t('auth:genericError'))
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
@ -289,7 +293,6 @@ export default {
|
|||||||
query: strategiesQuery,
|
query: strategiesQuery,
|
||||||
update: (data) => data.authentication.strategies,
|
update: (data) => data.authentication.strategies,
|
||||||
watchLoading (isLoading) {
|
watchLoading (isLoading) {
|
||||||
this.isLoading = isLoading
|
|
||||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh')
|
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
303
client/components/register.vue
Normal file
303
client/components/register.vue
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
v-app
|
||||||
|
.register
|
||||||
|
v-container(grid-list-lg)
|
||||||
|
v-layout(row, wrap)
|
||||||
|
v-flex(
|
||||||
|
xs12
|
||||||
|
offset-sm1, sm10
|
||||||
|
offset-md2, md8
|
||||||
|
offset-lg3, lg6
|
||||||
|
offset-xl4, xl4
|
||||||
|
)
|
||||||
|
transition(name='zoom')
|
||||||
|
v-card.elevation-5.md2(v-show='isShown')
|
||||||
|
v-toolbar(color='indigo', flat, dense, dark)
|
||||||
|
v-spacer
|
||||||
|
.subheading {{ $t('auth:registerTitle') }}
|
||||||
|
v-spacer
|
||||||
|
v-card-text.text-xs-center
|
||||||
|
h1.display-1.indigo--text.py-2 {{ siteTitle }}
|
||||||
|
.body-2 {{ $t('auth:registerSubTitle') }}
|
||||||
|
v-text-field.md2.mt-3(
|
||||||
|
solo
|
||||||
|
flat
|
||||||
|
prepend-icon='email'
|
||||||
|
background-color='grey lighten-4'
|
||||||
|
hide-details
|
||||||
|
ref='iptEmail'
|
||||||
|
v-model='email'
|
||||||
|
:placeholder='$t("auth:fields.email")'
|
||||||
|
color='indigo'
|
||||||
|
)
|
||||||
|
v-text-field.md2.mt-2(
|
||||||
|
solo
|
||||||
|
flat
|
||||||
|
prepend-icon='vpn_key'
|
||||||
|
background-color='grey lighten-4'
|
||||||
|
ref='iptPassword'
|
||||||
|
v-model='password'
|
||||||
|
:append-icon='hidePassword ? "visibility" : "visibility_off"'
|
||||||
|
@click:append='() => (hidePassword = !hidePassword)'
|
||||||
|
:type='hidePassword ? "password" : "text"'
|
||||||
|
:placeholder='$t("auth:fields.password")'
|
||||||
|
color='indigo'
|
||||||
|
loading
|
||||||
|
)
|
||||||
|
password-strength(slot='progress', v-model='password')
|
||||||
|
v-text-field.md2.mt-2(
|
||||||
|
solo
|
||||||
|
flat
|
||||||
|
prepend-icon='vpn_key'
|
||||||
|
background-color='grey lighten-4'
|
||||||
|
hide-details
|
||||||
|
ref='iptVerifyPassword'
|
||||||
|
v-model='verifyPassword'
|
||||||
|
@click:append='() => (hidePassword = !hidePassword)'
|
||||||
|
type='password'
|
||||||
|
:placeholder='$t("auth:fields.verifyPassword")'
|
||||||
|
color='indigo'
|
||||||
|
)
|
||||||
|
v-text-field.md2.mt-2(
|
||||||
|
solo
|
||||||
|
flat
|
||||||
|
prepend-icon='person'
|
||||||
|
background-color='grey lighten-4'
|
||||||
|
hide-details
|
||||||
|
ref='iptName'
|
||||||
|
v-model='name'
|
||||||
|
:placeholder='$t("auth:fields.name")'
|
||||||
|
@keyup.enter='register'
|
||||||
|
color='indigo'
|
||||||
|
)
|
||||||
|
v-card-actions.pb-4
|
||||||
|
v-spacer
|
||||||
|
v-btn.md2(
|
||||||
|
block
|
||||||
|
large
|
||||||
|
dark
|
||||||
|
color='indigo'
|
||||||
|
@click='register'
|
||||||
|
round
|
||||||
|
:loading='isLoading'
|
||||||
|
) {{ $t('auth:actions.register') }}
|
||||||
|
v-spacer
|
||||||
|
v-divider
|
||||||
|
v-card-actions.py-3.grey.lighten-4
|
||||||
|
v-spacer
|
||||||
|
i18next.caption(path='auth:switchToLogin.text', tag='div')
|
||||||
|
a.caption(href='/login', place='link') {{ $t('auth:switchToLogin.link') }}
|
||||||
|
v-spacer
|
||||||
|
|
||||||
|
loader(v-model='isLoading', :color='loaderColor', :title='loaderTitle', :subtitle='$t(`auth:pleaseWait`)')
|
||||||
|
nav-footer(color='grey darken-4', dark-color='grey darken-4')
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* global siteConfig */
|
||||||
|
|
||||||
|
import _ from 'lodash'
|
||||||
|
import Cookies from 'js-cookie'
|
||||||
|
import validate from 'validate.js'
|
||||||
|
import PasswordStrength from './common/password-strength.vue'
|
||||||
|
|
||||||
|
import registerMutation from 'gql/register/register-mutation-create.gql'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
i18nOptions: { namespaces: 'auth' },
|
||||||
|
components: {
|
||||||
|
PasswordStrength
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
verifyPassword: '',
|
||||||
|
name: '',
|
||||||
|
hidePassword: true,
|
||||||
|
isLoading: false,
|
||||||
|
isShown: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
siteTitle () {
|
||||||
|
return siteConfig.title
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.isShown = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.iptEmail.focus()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* REGISTER
|
||||||
|
*/
|
||||||
|
async register () {
|
||||||
|
const validation = validate({
|
||||||
|
email: this.email,
|
||||||
|
password: this.password,
|
||||||
|
verifyPassword: this.verifyPassword,
|
||||||
|
name: this.name
|
||||||
|
}, {
|
||||||
|
email: {
|
||||||
|
presence: {
|
||||||
|
message: this.$t('auth:missingEmail'),
|
||||||
|
allowEmpty: false
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
message: this.$t('auth:invalidEmail')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
presence: {
|
||||||
|
message: this.$t('auth:missingPassword'),
|
||||||
|
allowEmpty: false
|
||||||
|
},
|
||||||
|
length: {
|
||||||
|
minimum: 6,
|
||||||
|
tooShort: this.$t('auth:passwordTooShort')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verifyPassword: {
|
||||||
|
equality: {
|
||||||
|
attribute: 'password',
|
||||||
|
message: this.$t('auth:passwordNotMatch')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
presence: {
|
||||||
|
message: this.$t('auth:missingName'),
|
||||||
|
allowEmpty: false
|
||||||
|
},
|
||||||
|
length: {
|
||||||
|
minimum: 2,
|
||||||
|
maximum: 255,
|
||||||
|
tooShort: this.$t('auth:nameTooShort'),
|
||||||
|
tooLong: this.$t('auth:nameTooLong')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, { fullMessages: false })
|
||||||
|
|
||||||
|
if (validation) {
|
||||||
|
if(validation.email) {
|
||||||
|
this.$store.commit('showNotification', {
|
||||||
|
style: 'red',
|
||||||
|
message: validation.email[0],
|
||||||
|
icon: 'warning'
|
||||||
|
})
|
||||||
|
this.$refs.iptEmail.focus()
|
||||||
|
} else if (validation.password) {
|
||||||
|
this.$store.commit('showNotification', {
|
||||||
|
style: 'red',
|
||||||
|
message: validation.password[0],
|
||||||
|
icon: 'warning'
|
||||||
|
})
|
||||||
|
this.$refs.iptPassword.focus()
|
||||||
|
} else if (validation.verifyPassword) {
|
||||||
|
this.$store.commit('showNotification', {
|
||||||
|
style: 'red',
|
||||||
|
message: validation.verifyPassword[0],
|
||||||
|
icon: 'warning'
|
||||||
|
})
|
||||||
|
this.$refs.iptVerifyPassword.focus()
|
||||||
|
} else {
|
||||||
|
this.$store.commit('showNotification', {
|
||||||
|
style: 'red',
|
||||||
|
message: validation.name[0],
|
||||||
|
icon: 'warning'
|
||||||
|
})
|
||||||
|
this.$refs.iptName.focus()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isLoading = true
|
||||||
|
try {
|
||||||
|
let resp = await this.$apollo.mutate({
|
||||||
|
mutation: registerMutation,
|
||||||
|
variables: {
|
||||||
|
email: this.email,
|
||||||
|
password: this.password,
|
||||||
|
name: this.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (_.has(resp, 'data.authentication.register')) {
|
||||||
|
let respObj = _.get(resp, 'data.authentication.register', {})
|
||||||
|
if (respObj.responseResult.succeeded === true) {
|
||||||
|
this.$store.commit('showNotification', {
|
||||||
|
message: 'Account created successfully! Redirecting...',
|
||||||
|
style: 'success',
|
||||||
|
icon: 'check'
|
||||||
|
})
|
||||||
|
Cookies.set('jwt', respObj.jwt, { expires: 365 })
|
||||||
|
_.delay(() => {
|
||||||
|
window.location.replace('/')
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
throw new Error(respObj.responseResult.message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Registration is unavailable at this time.')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
this.$store.commit('showNotification', {
|
||||||
|
style: 'red',
|
||||||
|
message: err.message,
|
||||||
|
icon: 'warning'
|
||||||
|
})
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.register {
|
||||||
|
background-color: mc('indigo', '900');
|
||||||
|
background-image: url('../static/svg/motif-blocks.svg');
|
||||||
|
background-repeat: repeat;
|
||||||
|
background-size: 200px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
animation: loginBgReveal 20s linear infinite;
|
||||||
|
|
||||||
|
@include keyframes(loginBgReveal) {
|
||||||
|
0% {
|
||||||
|
background-position-x: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position-x: 800px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
background-image: url('../static/svg/motif-overlay.svg');
|
||||||
|
background-attachment: fixed;
|
||||||
|
background-size: cover;
|
||||||
|
opacity: .5;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .container {
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-family: 'Varela Round' !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-text-field.centered input {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
13
client/graph/register/register-mutation-create.gql
Normal file
13
client/graph/register/register-mutation-create.gql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
mutation($email: String!, $password: String!, $name: String!) {
|
||||||
|
authentication {
|
||||||
|
register(email: $email, password: $password, name: $name) {
|
||||||
|
responseResult {
|
||||||
|
succeeded
|
||||||
|
errorCode
|
||||||
|
slug
|
||||||
|
message
|
||||||
|
}
|
||||||
|
jwt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
Before Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB |
18
client/static/svg/logo-icons8.svg
Normal file
18
client/static/svg/logo-icons8.svg
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="80pt" height="80pt" viewBox="0 0 80 80" version="1.1">
|
||||||
|
<g id="surface1">
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:#78909C;fill-opacity:1;" d="M 16.667969 73.332031 C 15.667969 73.332031 15 72.667969 15 71.667969 C 15 70.667969 15.667969 70 16.667969 70 C 17.5 70 18.332031 69.832031 18.332031 68.167969 L 18.332031 61.667969 C 18.332031 60.667969 19 60 20 60 C 21 60 21.667969 60.667969 21.667969 61.667969 L 21.667969 68.167969 C 21.667969 71.332031 19.667969 73.332031 16.667969 73.332031 Z "/>
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:#CFD8DC;fill-opacity:1;" d="M 67.832031 25 C 68.167969 24 68.332031 22.832031 68.332031 21.667969 C 68.332031 16.167969 63.832031 11.667969 58.332031 11.667969 C 55.5 11.667969 53 12.832031 51.167969 14.832031 C 49.667969 10 45.167969 6.667969 40 6.667969 C 34.667969 6.667969 30.332031 10.167969 28.832031 15 C 27.332031 14 25.332031 13.332031 23.332031 13.332031 C 17.832031 13.332031 13.332031 17.832031 13.332031 23.332031 C 13.332031 23.832031 13.332031 24.5 13.5 25 Z "/>
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:#32C24D;fill-opacity:1;" d="M 10 21.667969 L 70 21.667969 L 70 58.332031 L 10 58.332031 Z "/>
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:#FFFFFF;fill-opacity:1;" d="M 43.332031 40 C 43.332031 41.832031 41.832031 43.332031 40 43.332031 C 38.167969 43.332031 36.667969 41.832031 36.667969 40 C 36.667969 38.167969 38.167969 36.667969 40 36.667969 C 41.832031 36.667969 43.332031 38.167969 43.332031 40 Z "/>
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:#FFFFFF;fill-opacity:1;" d="M 56.667969 40 C 56.667969 41.832031 55.167969 43.332031 53.332031 43.332031 C 51.5 43.332031 50 41.832031 50 40 C 50 38.167969 51.5 36.667969 53.332031 36.667969 C 55.167969 36.667969 56.667969 38.167969 56.667969 40 Z "/>
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:#FFFFFF;fill-opacity:1;" d="M 30 40 C 30 41.832031 28.5 43.332031 26.667969 43.332031 C 24.832031 43.332031 23.332031 41.832031 23.332031 40 C 23.332031 38.167969 24.832031 36.667969 26.667969 36.667969 C 28.5 36.667969 30 38.167969 30 40 Z "/>
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:#546E7A;fill-opacity:1;" d="M 18.332031 58.332031 L 21.667969 58.332031 L 21.667969 61.667969 L 18.332031 61.667969 Z "/>
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:#78909C;fill-opacity:1;" d="M 26.667969 73.332031 C 25.667969 73.332031 25 72.667969 25 71.667969 C 25 70.667969 25.667969 70 26.667969 70 C 27.5 70 28.332031 69.832031 28.332031 68.167969 L 28.332031 61.667969 C 28.332031 60.667969 29 60 30 60 C 31 60 31.667969 60.667969 31.667969 61.667969 L 31.667969 68.167969 C 31.667969 71.332031 29.667969 73.332031 26.667969 73.332031 Z "/>
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:#546E7A;fill-opacity:1;" d="M 28.332031 58.332031 L 31.667969 58.332031 L 31.667969 61.667969 L 28.332031 61.667969 Z "/>
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:#78909C;fill-opacity:1;" d="M 48.332031 73.332031 C 47.332031 73.332031 46.667969 72.667969 46.667969 71.667969 C 46.667969 70.667969 47.332031 70 48.332031 70 C 49.167969 70 50 69.832031 50 68.167969 L 50 61.667969 C 50 60.667969 50.667969 60 51.667969 60 C 52.667969 60 53.332031 60.667969 53.332031 61.667969 L 53.332031 68.167969 C 53.332031 71.332031 51.332031 73.332031 48.332031 73.332031 Z "/>
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:#546E7A;fill-opacity:1;" d="M 50 58.332031 L 53.332031 58.332031 L 53.332031 61.667969 L 50 61.667969 Z "/>
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:#78909C;fill-opacity:1;" d="M 58.332031 73.332031 C 57.332031 73.332031 56.667969 72.667969 56.667969 71.667969 C 56.667969 70.667969 57.332031 70 58.332031 70 C 59.167969 70 60 69.832031 60 68.167969 L 60 61.667969 C 60 60.667969 60.667969 60 61.667969 60 C 62.667969 60 63.332031 60.667969 63.332031 61.667969 L 63.332031 68.167969 C 63.332031 71.332031 61.332031 73.332031 58.332031 73.332031 Z "/>
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:#546E7A;fill-opacity:1;" d="M 60 58.332031 L 63.332031 58.332031 L 63.332031 61.667969 L 60 61.667969 Z "/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.1 KiB |
@ -30,8 +30,6 @@ services:
|
|||||||
|
|
||||||
adminer:
|
adminer:
|
||||||
image: adminer:latest
|
image: adminer:latest
|
||||||
environment:
|
|
||||||
ADMINER_DESIGN: pappu687
|
|
||||||
logging:
|
logging:
|
||||||
driver: "none"
|
driver: "none"
|
||||||
networks:
|
networks:
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
"test": "eslint --format codeframe --ext .js,.vue . && pug-lint server/views && jest",
|
"test": "eslint --format codeframe --ext .js,.vue . && pug-lint server/views && jest",
|
||||||
"docker:dev:up": "docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . up -d && docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . exec wiki yarn dev",
|
"docker:dev:up": "docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . up -d && docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . exec wiki yarn dev",
|
||||||
"docker:dev:down": "docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . down",
|
"docker:dev:down": "docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . down",
|
||||||
"docker:dev:rebuild": "docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . build --no-cache --force-rm",
|
"docker:dev:rebuild": "rmdir node_modules /s /q && docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . build --no-cache --force-rm",
|
||||||
"docker:build": "docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . run wiki yarn build && docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . down"
|
"docker:build": "docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . run wiki yarn build && docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . down"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -59,6 +59,7 @@
|
|||||||
"connect-redis": "3.4.0",
|
"connect-redis": "3.4.0",
|
||||||
"cookie-parser": "1.4.3",
|
"cookie-parser": "1.4.3",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
|
"custom-error-instance": "2.1.1",
|
||||||
"dependency-graph": "0.7.2",
|
"dependency-graph": "0.7.2",
|
||||||
"diff": "3.5.0",
|
"diff": "3.5.0",
|
||||||
"diff2html": "2.5.0",
|
"diff2html": "2.5.0",
|
||||||
@ -154,6 +155,7 @@
|
|||||||
"subscriptions-transport-ws": "0.9.15",
|
"subscriptions-transport-ws": "0.9.15",
|
||||||
"uslug": "1.0.4",
|
"uslug": "1.0.4",
|
||||||
"uuid": "3.3.2",
|
"uuid": "3.3.2",
|
||||||
|
"validate.js": "0.12.0",
|
||||||
"validator": "10.9.0",
|
"validator": "10.9.0",
|
||||||
"validator-as-promised": "1.0.2",
|
"validator-as-promised": "1.0.2",
|
||||||
"winston": "3.1.0",
|
"winston": "3.1.0",
|
||||||
@ -284,7 +286,8 @@
|
|||||||
"webpack-subresource-integrity": "1.3.0",
|
"webpack-subresource-integrity": "1.3.0",
|
||||||
"whatwg-fetch": "3.0.0",
|
"whatwg-fetch": "3.0.0",
|
||||||
"write-file-webpack-plugin": "4.4.1",
|
"write-file-webpack-plugin": "4.4.1",
|
||||||
"xterm": "3.8.0"
|
"xterm": "3.8.0",
|
||||||
|
"zxcvbn": "4.4.2"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
|
@ -18,6 +18,13 @@ router.get('/logout', function (req, res) {
|
|||||||
res.redirect('/')
|
res.redirect('/')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register form
|
||||||
|
*/
|
||||||
|
router.get('/register', function (req, res, next) {
|
||||||
|
res.render('register')
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JWT Public Endpoints
|
* JWT Public Endpoints
|
||||||
*/
|
*/
|
||||||
|
@ -38,7 +38,7 @@ module.exports = {
|
|||||||
AuthenticationMutation: {
|
AuthenticationMutation: {
|
||||||
async login(obj, args, context) {
|
async login(obj, args, context) {
|
||||||
try {
|
try {
|
||||||
let authResult = await WIKI.models.users.login(args, context)
|
const authResult = await WIKI.models.users.login(args, context)
|
||||||
return {
|
return {
|
||||||
...authResult,
|
...authResult,
|
||||||
responseResult: graphHelper.generateSuccess('Login success')
|
responseResult: graphHelper.generateSuccess('Login success')
|
||||||
@ -49,7 +49,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
async loginTFA(obj, args, context) {
|
async loginTFA(obj, args, context) {
|
||||||
try {
|
try {
|
||||||
let authResult = await WIKI.models.users.loginTFA(args, context)
|
const authResult = await WIKI.models.users.loginTFA(args, context)
|
||||||
return {
|
return {
|
||||||
...authResult,
|
...authResult,
|
||||||
responseResult: graphHelper.generateSuccess('TFA success')
|
responseResult: graphHelper.generateSuccess('TFA success')
|
||||||
@ -58,6 +58,22 @@ module.exports = {
|
|||||||
return graphHelper.generateError(err)
|
return graphHelper.generateError(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async register(obj, args, context) {
|
||||||
|
try {
|
||||||
|
await WIKI.models.users.register(args, context)
|
||||||
|
const authResult = await WIKI.models.users.login({
|
||||||
|
username: args.email,
|
||||||
|
password: args.password,
|
||||||
|
strategy: 'local'
|
||||||
|
}, context)
|
||||||
|
return {
|
||||||
|
jwt: authResult.jwt,
|
||||||
|
responseResult: graphHelper.generateSuccess('Registration success')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return graphHelper.generateError(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
async updateStrategies(obj, args, context) {
|
async updateStrategies(obj, args, context) {
|
||||||
try {
|
try {
|
||||||
for (let str of args.strategies) {
|
for (let str of args.strategies) {
|
||||||
|
@ -36,6 +36,12 @@ type AuthenticationMutation {
|
|||||||
securityCode: String!
|
securityCode: String!
|
||||||
): DefaultResponse
|
): DefaultResponse
|
||||||
|
|
||||||
|
register(
|
||||||
|
email: String!
|
||||||
|
password: String!
|
||||||
|
name: String!
|
||||||
|
): AuthenticationRegisterResponse
|
||||||
|
|
||||||
updateStrategies(
|
updateStrategies(
|
||||||
strategies: [AuthenticationStrategyInput]
|
strategies: [AuthenticationStrategyInput]
|
||||||
): DefaultResponse @auth(requires: ["manage:system"])
|
): DefaultResponse @auth(requires: ["manage:system"])
|
||||||
@ -69,6 +75,11 @@ type AuthenticationLoginResponse {
|
|||||||
tfaLoginToken: String
|
tfaLoginToken: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthenticationRegisterResponse {
|
||||||
|
responseResult: ResponseStatus
|
||||||
|
jwt: String
|
||||||
|
}
|
||||||
|
|
||||||
input AuthenticationStrategyInput {
|
input AuthenticationStrategyInput {
|
||||||
isEnabled: Boolean!
|
isEnabled: Boolean!
|
||||||
key: String!
|
key: String!
|
||||||
|
@ -1,30 +1,44 @@
|
|||||||
class BaseError extends Error {
|
const CustomError = require('custom-error-instance')
|
||||||
constructor (message) {
|
|
||||||
super(message)
|
|
||||||
this.name = this.constructor.name
|
|
||||||
Error.captureStackTrace(this, this.constructor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AuthGenericError extends BaseError { constructor (message = 'An unexpected error occured during login.') { super(message) } }
|
|
||||||
class AuthLoginFailed extends BaseError { constructor (message = 'Invalid email / username or password.') { super(message) } }
|
|
||||||
class AuthProviderInvalid extends BaseError { constructor (message = 'Invalid authentication provider.') { super(message) } }
|
|
||||||
class AuthTFAFailed extends BaseError { constructor (message = 'Incorrect TFA Security Code.') { super(message) } }
|
|
||||||
class AuthTFAInvalid extends BaseError { constructor (message = 'Invalid TFA Security Code or Login Token.') { super(message) } }
|
|
||||||
class BruteInstanceIsInvalid extends BaseError { constructor (message = 'Invalid Brute Force Instance.') { super(message) } }
|
|
||||||
class BruteTooManyAttempts extends BaseError { constructor (message = 'Too many attempts! Try again later.') { super(message) } }
|
|
||||||
class LocaleInvalidNamespace extends BaseError { constructor (message = 'Invalid locale or namespace.') { super(message) } }
|
|
||||||
class UserCreationFailed extends BaseError { constructor (message = 'An unexpected error occured during user creation.') { super(message) } }
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
BaseError,
|
AuthGenericError: CustomError('AuthGenericError', {
|
||||||
AuthGenericError,
|
message: 'An unexpected error occured during login.',
|
||||||
AuthLoginFailed,
|
code: 1001
|
||||||
AuthProviderInvalid,
|
}),
|
||||||
AuthTFAFailed,
|
AuthLoginFailed: CustomError('AuthLoginFailed', {
|
||||||
AuthTFAInvalid,
|
message: 'Invalid email / username or password.',
|
||||||
BruteInstanceIsInvalid,
|
code: 1002
|
||||||
BruteTooManyAttempts,
|
}),
|
||||||
LocaleInvalidNamespace,
|
AuthProviderInvalid: CustomError('AuthProviderInvalid', {
|
||||||
UserCreationFailed
|
message: 'Invalid authentication provider.',
|
||||||
|
code: 1003
|
||||||
|
}),
|
||||||
|
AuthAccountAlreadyExists: CustomError('AuthAccountAlreadyExists', {
|
||||||
|
message: 'An account already exists using this email address.',
|
||||||
|
code: 1004
|
||||||
|
}),
|
||||||
|
AuthTFAFailed: CustomError('AuthTFAFailed', {
|
||||||
|
message: 'Incorrect TFA Security Code.',
|
||||||
|
code: 1005
|
||||||
|
}),
|
||||||
|
AuthTFAInvalid: CustomError('AuthTFAInvalid', {
|
||||||
|
message: 'Invalid TFA Security Code or Login Token.',
|
||||||
|
code: 1006
|
||||||
|
}),
|
||||||
|
BruteInstanceIsInvalid: CustomError('BruteInstanceIsInvalid', {
|
||||||
|
message: 'Invalid Brute Force Instance.',
|
||||||
|
code: 1007
|
||||||
|
}),
|
||||||
|
BruteTooManyAttempts: CustomError('BruteTooManyAttempts', {
|
||||||
|
message: 'Too many attempts! Try again later.',
|
||||||
|
code: 1008
|
||||||
|
}),
|
||||||
|
LocaleInvalidNamespace: CustomError('LocaleInvalidNamespace', {
|
||||||
|
message: 'Invalid locale or namespace.',
|
||||||
|
code: 1009
|
||||||
|
}),
|
||||||
|
UserCreationFailed: CustomError('UserCreationFailed', {
|
||||||
|
message: 'An unexpected error occured during user creation.',
|
||||||
|
code: 1010
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -292,4 +292,23 @@ module.exports = class User extends Model {
|
|||||||
}
|
}
|
||||||
throw new WIKI.Error.AuthTFAInvalid()
|
throw new WIKI.Error.AuthTFAInvalid()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async register ({ email, password, name }, context) {
|
||||||
|
const usr = await WIKI.models.users.query().findOne({ email, providerKey: 'local' })
|
||||||
|
if (!usr) {
|
||||||
|
await WIKI.models.users.query().insert({
|
||||||
|
provider: 'local',
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
password,
|
||||||
|
locale: 'en',
|
||||||
|
defaultEditor: 'markdown',
|
||||||
|
tfaIsActive: false,
|
||||||
|
isSystem: false
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
throw new WIKI.Error.AuthAccountAlreadyExists()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
5
server/views/register.pug
Normal file
5
server/views/register.pug
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
extends master.pug
|
||||||
|
|
||||||
|
block body
|
||||||
|
#root.is-fullscreen
|
||||||
|
register
|
Loading…
Reference in New Issue
Block a user