feat: use script setup and ts in app auth components

This commit is contained in:
Dominik Pschenitschni 2021-12-08 13:22:39 +01:00 committed by Gitea
parent b03d5d80cd
commit c3c4d2a0a5
9 changed files with 278 additions and 301 deletions

View file

@ -23,6 +23,7 @@
"@sentry/vue": "6.16.1", "@sentry/vue": "6.16.1",
"@vue/compat": "3.2.26", "@vue/compat": "3.2.26",
"@vueuse/core": "7.3.0", "@vueuse/core": "7.3.0",
"@vueuse/router": "^7.3.0",
"bulma-css-variables": "0.9.33", "bulma-css-variables": "0.9.33",
"camel-case": "4.1.2", "camel-case": "4.1.2",
"codemirror": "5.64.0", "codemirror": "5.64.0",

View file

@ -1,125 +1,96 @@
<template> <template>
<ready> <ready :class="{'is-touch': isTouch}">
<div :class="{'is-touch': isTouch}"> <div :class="{'is-hidden': !online}">
<div :class="{'is-hidden': !online}"> <template v-if="authUser">
<template v-if="authUser"> <top-navigation/>
<top-navigation/> <content-auth/>
<content-auth/> </template>
</template> <content-link-share v-else-if="authLinkShare"/>
<content-link-share v-else-if="authLinkShare"/> <content-no-auth v-else/>
<content-no-auth v-else/> <notification/>
<notification/>
</div>
<transition name="fade">
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
</transition>
</div> </div>
<transition name="fade">
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
</transition>
</ready> </ready>
</template> </template>
<script> <script lang="ts" setup>
import {defineComponent} from 'vue' import {computed, watch, watchEffect, Ref} from 'vue'
import {mapState, mapGetters} from 'vuex' import {useRouter} from 'vue-router'
import {useRouteQuery} from '@vueuse/router'
import {useStore} from 'vuex'
import {useI18n} from 'vue-i18n'
import {useOnline} from '@vueuse/core'
import isTouchDevice from 'is-touch-device' import isTouchDevice from 'is-touch-device'
import {success} from '@/message'
import Notification from '@/components/misc/notification.vue'
import KeyboardShortcuts from './components/misc/keyboard-shortcuts/index.vue'
import TopNavigation from './components/home/topNavigation.vue'
import ContentAuth from './components/home/contentAuth.vue'
import ContentLinkShare from './components/home/contentLinkShare.vue'
import ContentNoAuth from './components/home/contentNoAuth.vue'
import Ready from '@/components/misc/ready.vue'
import Notification from './components/misc/notification'
import {KEYBOARD_SHORTCUTS_ACTIVE, ONLINE} from './store/mutation-types'
import KeyboardShortcuts from './components/misc/keyboard-shortcuts'
import TopNavigation from './components/home/topNavigation'
import ContentAuth from './components/home/contentAuth'
import ContentLinkShare from './components/home/contentLinkShare'
import ContentNoAuth from './components/home/contentNoAuth'
import {setLanguage} from './i18n' import {setLanguage} from './i18n'
import AccountDeleteService from '@/services/accountDelete' import AccountDeleteService from '@/services/accountDelete'
import Ready from '@/components/misc/ready' import {ONLINE} from '@/store/mutation-types'
import {useColorScheme} from '@/composables/useColorScheme' import {useColorScheme} from '@/composables/useColorScheme'
export default defineComponent({ const store = useStore()
name: 'app', const online = useOnline()
components: { watchEffect(() => store.commit(ONLINE, online.value))
ContentNoAuth,
ContentLinkShare,
ContentAuth,
TopNavigation,
KeyboardShortcuts,
Notification,
Ready,
},
beforeMount() {
this.setupOnlineStatus()
},
beforeCreate() {
setLanguage()
},
setup() {
useColorScheme()
},
created() {
// Make sure to always load the home route when running with electron
if (this.$route.fullPath.endsWith('frontend/index.html')) {
this.$router.push({name: 'home'})
}
},
watch: {
// Calling these methods in the mounted hook directly does not work.
'$route.query.accountDeletionConfirm'() {
this.setupAccountDeletionVerification()
},
'$route.query.userPasswordReset'() {
this.setupPasswortResetRedirect()
},
'$route.query.userEmailConfirm'() {
this.setupEmailVerificationRedirect()
},
},
computed: {
isTouch() {
return isTouchDevice()
},
...mapState({
online: ONLINE,
keyboardShortcutsActive: KEYBOARD_SHORTCUTS_ACTIVE,
}),
...mapGetters('auth', [
'authUser',
'authLinkShare',
]),
},
methods: {
setupOnlineStatus() {
this.$store.commit(ONLINE, navigator.onLine)
window.addEventListener('online', () => this.$store.commit(ONLINE, navigator.onLine))
window.addEventListener('offline', () => this.$store.commit(ONLINE, navigator.onLine))
},
setupPasswortResetRedirect() {
if (typeof this.$route.query.userPasswordReset === 'undefined') {
return
}
localStorage.setItem('passwordResetToken', this.$route.query.userPasswordReset) const router = useRouter()
this.$router.push({name: 'user.password-reset.reset'})
},
setupEmailVerificationRedirect() {
if (typeof this.$route.query.userEmailConfirm === 'undefined') {
return
}
localStorage.setItem('emailConfirmToken', this.$route.query.userEmailConfirm) const isTouch = computed(isTouchDevice)
this.$router.push({name: 'user.login'}) const keyboardShortcutsActive = computed(() => store.state.keyboardShortcutsActive)
},
async setupAccountDeletionVerification() {
if (typeof this.$route.query.accountDeletionConfirm === 'undefined') {
return
}
const accountDeletionService = new AccountDeleteService() const authUser = computed(() => store.getters['auth/authUser'])
await accountDeletionService.confirm(this.$route.query.accountDeletionConfirm) const authLinkShare = computed(() => store.getters['auth/authLinkShare'])
this.$message.success({message: this.$t('user.deletion.confirmSuccess')})
this.$store.dispatch('auth/refreshUserInfo') const {t} = useI18n()
},
}, // setup account deletion verification
}) const accountDeletionConfirm = useRouteQuery('accountDeletionConfirm') as Ref<null | string>
watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
if (accountDeletionConfirm === null) {
return
}
const accountDeletionService = new AccountDeleteService()
await accountDeletionService.confirm(accountDeletionConfirm)
success({message: t('user.deletion.confirmSuccess')})
store.dispatch('auth/refreshUserInfo')
}, { immediate: true })
// setup passwort reset redirect
const userPasswordReset = useRouteQuery('userPasswordReset') as Ref<null | string>
watch(userPasswordReset, (userPasswordReset) => {
if (userPasswordReset === null) {
return
}
localStorage.setItem('passwordResetToken', userPasswordReset)
router.push({name: 'user.password-reset.reset'})
}, { immediate: true })
// setup email verification redirect
const userEmailConfirm = useRouteQuery('userEmailConfirm') as Ref<null | string>
watch(userEmailConfirm, (userEmailConfirm) => {
if (userEmailConfirm === null) {
return
}
localStorage.setItem('emailConfirmToken', userEmailConfirm)
router.push({name: 'user.login'})
}, { immediate: true })
setLanguage()
useColorScheme()
</script> </script>
<style lang="scss"> <style lang="scss">

View file

@ -40,96 +40,88 @@
</div> </div>
</template> </template>
<script> <script lang="ts" setup>
import {mapState} from 'vuex' import {watch, computed} from 'vue'
import {useStore} from 'vuex'
import {useRoute, useRouter} from 'vue-router'
import {useEventListener} from '@vueuse/core'
import {CURRENT_LIST, KEYBOARD_SHORTCUTS_ACTIVE, MENU_ACTIVE} from '@/store/mutation-types' import {CURRENT_LIST, KEYBOARD_SHORTCUTS_ACTIVE, MENU_ACTIVE} from '@/store/mutation-types'
import Navigation from '@/components/home/navigation.vue' import Navigation from '@/components/home/navigation.vue'
import QuickActions from '@/components/quick-actions/quick-actions.vue' import QuickActions from '@/components/quick-actions/quick-actions.vue'
export default { const store = useStore()
name: 'contentAuth',
components: {QuickActions, Navigation},
watch: {
'$route': {
handler: 'doStuffAfterRoute',
deep: true,
},
},
created() {
this.renewTokenOnFocus()
this.loadLabels()
},
computed: mapState({
background: 'background',
menuActive: MENU_ACTIVE,
userInfo: state => state.auth.info,
authenticated: state => state.auth.authenticated,
}),
methods: {
doStuffAfterRoute() {
// this.setTitle('') // Reset the title if the page component does not set one itself
this.hideMenuOnMobile()
this.resetCurrentList()
},
resetCurrentList() {
// Reset the current list highlight in menu if the current list is not list related.
if (
this.$route.name === 'home' ||
this.$route.name === 'namespace.edit' ||
this.$route.name === 'teams.index' ||
this.$route.name === 'teams.edit' ||
this.$route.name === 'tasks.range' ||
this.$route.name === 'labels.index' ||
this.$route.name === 'migrate.start' ||
this.$route.name === 'migrate.wunderlist' ||
this.$route.name.startsWith('user.settings') ||
this.$route.name === 'namespaces.index'
) {
return this.$store.dispatch(CURRENT_LIST, null)
}
},
renewTokenOnFocus() {
// Try renewing the token every time vikunja is loaded initially
// (When opening the browser the focus event is not fired)
this.$store.dispatch('auth/renewToken')
// Check if the token is still valid if the window gets focus again to maybe renew it const background = computed(() => store.state.background)
window.addEventListener('focus', () => { const menuActive = computed(() => store.state.menuActive)
if (!this.authenticated) { function showKeyboardShortcuts() {
return store.commit(KEYBOARD_SHORTCUTS_ACTIVE, true)
}
const expiresIn = (this.userInfo !== null ? this.userInfo.exp : 0) - +new Date() / 1000
// If the token expiry is negative, it is already expired and we have no choice but to redirect
// the user to the login page
if (expiresIn < 0) {
this.$store.dispatch('auth/checkAuth')
this.$router.push({name: 'user.login'})
return
}
// Check if the token is valid for less than 60 hours and renew if thats the case
if (expiresIn < 60 * 3600) {
this.$store.dispatch('auth/renewToken')
console.debug('renewed token')
}
})
},
hideMenuOnMobile() {
if (window.innerWidth < 769) {
this.$store.commit(MENU_ACTIVE, false)
}
},
showKeyboardShortcuts() {
this.$store.commit(KEYBOARD_SHORTCUTS_ACTIVE, true)
},
loadLabels() {
this.$store.dispatch('labels/loadAllLabels')
},
},
} }
const route = useRoute()
// hide menu on mobile
watch(() => route.fullPath, () => window.innerWidth < 769 && store.commit(MENU_ACTIVE, false))
// Reset the current list highlight in menu if the current route is not list related.
watch(() => route.fullPath, () => {
if (
[
'home',
'namespace.edit',
'teams.index',
'teams.edit',
'tasks.range',
'labels.index',
'migrate.start',
'migrate.wunderlist',
'namespaces.index',
].includes(route.name) ||
route.name.startsWith('user.settings')
) {
store.dispatch(CURRENT_LIST, null)
}
})
// TODO: Reset the title if the page component does not set one itself
function useRenewTokenOnFocus() {
const router = useRouter()
const userInfo = computed(() => store.state.auth.info)
const authenticated = computed(() => store.state.auth.authenticated)
// Try renewing the token every time vikunja is loaded initially
// (When opening the browser the focus event is not fired)
store.dispatch('auth/renewToken')
// Check if the token is still valid if the window gets focus again to maybe renew it
useEventListener('focus', () => {
if (!authenticated.value) {
return
}
const expiresIn = (userInfo.value !== null ? userInfo.value.exp : 0) - +new Date() / 1000
// If the token expiry is negative, it is already expired and we have no choice but to redirect
// the user to the login page
if (expiresIn < 0) {
store.dispatch('auth/checkAuth')
router.push({name: 'user.login'})
return
}
// Check if the token is valid for less than 60 hours and renew if thats the case
if (expiresIn < 60 * 3600) {
store.dispatch('auth/renewToken')
console.debug('renewed token')
}
})
}
useRenewTokenOnFocus()
store.dispatch('labels/loadAllLabels')
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -21,23 +21,16 @@
</div> </div>
</template> </template>
<script> <script lang="ts" setup>
import {mapState} from 'vuex' import {computed} from 'vue'
import {useStore} from 'vuex'
import Logo from '@/components/home/Logo.vue' import Logo from '@/components/home/Logo.vue'
import PoweredByLink from './PoweredByLink.vue' import PoweredByLink from './PoweredByLink.vue'
export default { const store = useStore()
name: 'contentLinkShare', const currentList = computed(() => store.state.currentList)
components: { const background = computed(() => store.state.background)
Logo,
PoweredByLink,
},
computed: mapState([
'currentList',
'background',
]),
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -4,44 +4,38 @@
</no-auth-wrapper> </no-auth-wrapper>
</template> </template>
<script> <script lang="ts" setup>
import {saveLastVisited} from '@/helpers/saveLastVisited' import {watchEffect} from 'vue'
import {useRoute, useRouter} from 'vue-router'
import NoAuthWrapper from '@/components/misc/no-auth-wrapper' import NoAuthWrapper from '@/components/misc/no-auth-wrapper'
export default { import {saveLastVisited} from '@/helpers/saveLastVisited'
name: 'contentNoAuth',
components: {NoAuthWrapper}, const route = useRoute()
computed: {
routeName() { watchEffect(() => {
return this.$route.name if (!route.name) return
}, redirectToHome()
}, })
watch: {
routeName: { const router = useRouter()
handler(routeName) { function redirectToHome() {
if (!routeName) return // Check if the user is already logged in and redirect them to the home page if not
this.redirectToHome() if (
}, ![
immediate: true, 'user.login',
}, 'user.password-reset.request',
}, 'user.password-reset.reset',
methods: { 'user.register',
redirectToHome() { 'link-share.auth',
// Check if the user is already logged in and redirect them to the home page if not 'openid.auth',
if ( ].includes(route.name) &&
this.$route.name !== 'user.login' && localStorage.getItem('passwordResetToken') === null &&
this.$route.name !== 'user.password-reset.request' && localStorage.getItem('emailConfirmToken') === null
this.$route.name !== 'user.password-reset.reset' && ) {
this.$route.name !== 'user.register' && saveLastVisited(route.name, route.params)
this.$route.name !== 'link-share.auth' && router.push({name: 'user.login'})
this.$route.name !== 'openid.auth' && }
localStorage.getItem('passwordResetToken') === null &&
localStorage.getItem('emailConfirmToken') === null
) {
saveLastVisited(this.$route.name, this.$route.params)
this.$router.push({name: 'user.login'})
}
},
},
} }
</script> </script>

View file

@ -28,7 +28,7 @@ export default class AbstractService {
/** /**
* The abstract constructor. * The abstract constructor.
* @param paths An object with all paths. Default values are specified above. * @param [paths] An object with all paths. Default values are specified above.
*/ */
constructor(paths) { constructor(paths) {
this.http = axios.create({ this.http = axios.create({

View file

@ -16,83 +16,101 @@
:placeholder="$t('user.auth.passwordPlaceholder')" :placeholder="$t('user.auth.passwordPlaceholder')"
v-model="password" v-model="password"
v-focus v-focus
@keyup.enter.prevent="auth" @keyup.enter.prevent="authenticate()"
/> />
</div> </div>
</div> </div>
<x-button @click="auth" :loading="loading"> <x-button @click="authenticate()" :loading="loading">
{{ $t('user.auth.login') }} {{ $t('user.auth.login') }}
</x-button> </x-button>
<message variant="danger" class="mt-4" v-if="errorMessage !== ''"> <Message variant="danger" class="mt-4" v-if="errorMessage !== ''">
{{ errorMessage }} {{ errorMessage }}
</message> </Message>
</div> </div>
</div> </div>
</template> </template>
<script> <script lang="ts" setup>
import {mapGetters} from 'vuex' import {ref, computed} from 'vue'
import Message from '@/components/misc/message' import {useStore} from 'vuex'
import {useRoute, useRouter} from 'vue-router'
import {useI18n} from 'vue-i18n'
import {useTitle} from '@vueuse/core'
export default { import Message from '@/components/misc/message.vue'
name: 'LinkSharingAuth',
components: {Message},
data() {
return {
loading: true,
authenticateWithPassword: false,
errorMessage: '',
hash: '', const {t} = useI18n()
password: '', useTitle(t('sharing.authenticating'))
async function useAuth() {
const store = useStore()
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const authenticateWithPassword = ref(false)
const errorMessage = ref('')
const password = ref('')
const authLinkShare = computed(() => store.getters['auth/authLinkShare'])
async function authenticate() {
authenticateWithPassword.value = false
errorMessage.value = ''
if (authLinkShare.value) {
// FIXME: push to 'list.list' since authenticated?
return
} }
},
created() { // TODO: no password
this.auth()
}, loading.value = true
mounted() {
this.setTitle(this.$t('sharing.authenticating'))
},
computed: mapGetters('auth', [
'authLinkShare',
]),
methods: {
async auth() {
this.errorMessage = ''
if (this.authLinkShare) { try {
const {list_id: listId} = await store.dispatch('auth/linkShareAuth', {
hash: route.params.share,
password: password.value,
})
router.push({name: 'list.list', params: {listId}})
} catch (e) {
if (e.response?.data?.code === 13001) {
authenticateWithPassword.value = true
return return
} }
this.loading = true // TODO: Put this logic in a global errorMessage handler method which checks all auth codes
let errorMessage = t('sharing.error')
try { if (e.response?.data?.message) {
const r = await this.$store.dispatch('auth/linkShareAuth', { errorMessage = e.response.data.message
hash: this.$route.params.share,
password: this.password,
})
this.$router.push({name: 'list.list', params: {listId: r.list_id}})
} catch (e) {
if (typeof e.response.data.code !== 'undefined' && e.response.data.code === 13001) {
this.authenticateWithPassword = true
return
}
// TODO: Put this logic in a global errorMessage handler method which checks all auth codes
let errorMessage = this.$t('sharing.error')
if (e.response && e.response.data && e.response.data.message) {
errorMessage = e.response.data.message
}
if (typeof e.response.data.code !== 'undefined' && e.response.data.code === 13002) {
errorMessage = this.$t('sharing.invalidPassword')
}
this.errorMessage = errorMessage
} finally {
this.loading = false
} }
}, if (e.response?.data?.code === 13002) {
}, errorMessage = t('sharing.invalidPassword')
}
errorMessage.value = errorMessage
} finally {
loading.value = false
}
}
authenticate()
return {
loading,
authenticateWithPassword,
errorMessage,
password,
authenticate,
}
} }
const {
loading,
authenticateWithPassword,
errorMessage,
password,
authenticate,
} = useAuth()
</script> </script>

View file

@ -7,7 +7,7 @@
</div> </div>
</template> </template>
<script setup> <script lang="ts" setup>
import { ref } from 'vue' import { ref } from 'vue'
import ShowTasks from './ShowTasks' import ShowTasks from './ShowTasks'

View file

@ -3787,6 +3787,14 @@
"@vueuse/shared" "7.3.0" "@vueuse/shared" "7.3.0"
vue-demi "*" vue-demi "*"
"@vueuse/router@^7.3.0":
version "7.3.0"
resolved "https://registry.yarnpkg.com/@vueuse/router/-/router-7.3.0.tgz#8157d20636040e573379eda338b57d47f1464163"
integrity sha512-MRGaZPVV21MZOZ759LMRWTlSaRvcKh+kGw2tGCLhxkihObcrNNLl0h3N5QtBy/+eR84tf7MO7JHCL+0PJspzQg==
dependencies:
"@vueuse/shared" "7.3.0"
vue-demi "*"
"@vueuse/shared@7.3.0": "@vueuse/shared@7.3.0":
version "7.3.0" version "7.3.0"
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-7.3.0.tgz#729b2f0a83f38647896d955902e828dcbd8ed7dc" resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-7.3.0.tgz#729b2f0a83f38647896d955902e828dcbd8ed7dc"