wip: feat: use navigation guards
continuous-integration/drone/pr Build is failing
Details
continuous-integration/drone/pr Build is failing
Details
This commit is contained in:
parent
5bcd51cc5f
commit
89ff5ebee1
82
src/App.vue
82
src/App.vue
|
@ -36,6 +36,39 @@ import ContentLinkShare from './components/home/contentLinkShare'
|
|||
import ContentNoAuth from './components/home/contentNoAuth'
|
||||
import {setLanguage} from './i18n'
|
||||
import AccountDeleteService from '@/services/accountDelete'
|
||||
import {store} from '@/store'
|
||||
import {i18n} from '@/i18n'
|
||||
import {success} from '@/message'
|
||||
|
||||
function setupOnlineStatus() {
|
||||
store.commit(ONLINE, navigator.onLine)
|
||||
window.addEventListener('online', () => store.commit(ONLINE, navigator.onLine))
|
||||
window.addEventListener('offline', () => store.commit(ONLINE, navigator.onLine))
|
||||
}
|
||||
|
||||
function setupRedirects(route) {
|
||||
if (typeof route.query.userPasswordReset !== 'undefined') {
|
||||
localStorage.setItem('passwordResetToken', route.query.userPasswordReset)
|
||||
return {name: 'user.password-reset.reset'}
|
||||
} else if (typeof route.query.userEmailConfirm !== 'undefined') {
|
||||
localStorage.setItem('emailConfirmToken', route.query.userEmailConfirm)
|
||||
return {name: 'user.login'}
|
||||
} else if (route.fullPath.endsWith('frontend/index.html')) {
|
||||
// Make sure to always load the home route when running with electron
|
||||
return {name: 'home'}
|
||||
}
|
||||
}
|
||||
|
||||
async function setupAccountDeletionVerification(route) {
|
||||
if (typeof route.query.accountDeletionConfirm === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
const accountDeletionService = new AccountDeleteService()
|
||||
await accountDeletionService.confirm(route.query.accountDeletionConfirm)
|
||||
success({message: i18n.global.t('user.deletion.confirmSuccess')})
|
||||
await store.dispatch('auth/refreshUserInfo')
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'app',
|
||||
|
@ -47,11 +80,11 @@ export default defineComponent({
|
|||
KeyboardShortcuts,
|
||||
Notification,
|
||||
},
|
||||
beforeMount() {
|
||||
this.setupOnlineStatus()
|
||||
this.setupPasswortResetRedirect()
|
||||
this.setupEmailVerificationRedirect()
|
||||
this.setupAccountDeletionVerification()
|
||||
beforeRouteEnter(to) {
|
||||
setupOnlineStatus()
|
||||
const redirect = setupRedirects(to)
|
||||
if (redirect) return redirect
|
||||
setupAccountDeletionVerification(to)
|
||||
},
|
||||
beforeCreate() {
|
||||
// FIXME: async action in beforeCreate, might be not finished when component mounts
|
||||
|
@ -63,12 +96,6 @@ export default defineComponent({
|
|||
|
||||
setLanguage()
|
||||
},
|
||||
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'})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isTouch() {
|
||||
return isTouchDevice()
|
||||
|
@ -82,38 +109,5 @@ export default defineComponent({
|
|||
'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)
|
||||
this.$router.push({name: 'user.password-reset.reset'})
|
||||
},
|
||||
setupEmailVerificationRedirect() {
|
||||
if (typeof this.$route.query.userEmailConfirm === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.setItem('emailConfirmToken', this.$route.query.userEmailConfirm)
|
||||
this.$router.push({name: 'user.login'})
|
||||
},
|
||||
async setupAccountDeletionVerification() {
|
||||
if (typeof this.$route.query.accountDeletionConfirm === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
const accountDeletionService = new AccountDeleteService()
|
||||
await accountDeletionService.confirm(this.$route.query.accountDeletionConfirm)
|
||||
this.$message.success({message: this.$t('user.deletion.confirmSuccess')})
|
||||
this.$store.dispatch('auth/refreshUserInfo')
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -95,28 +95,7 @@ export default {
|
|||
this.$store.dispatch('auth/renewToken')
|
||||
|
||||
// Check if the token is still valid if the window gets focus again to maybe renew it
|
||||
window.addEventListener('focus', () => {
|
||||
|
||||
if (!this.authenticated) {
|
||||
return
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
})
|
||||
window.addEventListener('focus', this.$store.dispatch('auth/checkToken'))
|
||||
},
|
||||
hideMenuOnMobile() {
|
||||
if (window.innerWidth < 769) {
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
import {mapState} from 'vuex'
|
||||
|
||||
import logoUrl from '@/assets/logo-full.svg'
|
||||
import { saveLastVisited } from '@/helpers/saveLastVisited'
|
||||
|
||||
export default {
|
||||
name: 'contentNoAuth',
|
||||
|
@ -29,39 +28,7 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
routeName() {
|
||||
return this.$route.name
|
||||
},
|
||||
...mapState({
|
||||
motd: state => state.config.motd,
|
||||
}),
|
||||
},
|
||||
watch: {
|
||||
routeName: {
|
||||
handler(routeName) {
|
||||
if (!routeName) return
|
||||
this.redirectToHome()
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
redirectToHome() {
|
||||
// Check if the user is already logged in and redirect them to the home page if not
|
||||
if (
|
||||
this.$route.name !== 'user.login' &&
|
||||
this.$route.name !== 'user.password-reset.request' &&
|
||||
this.$route.name !== 'user.password-reset.reset' &&
|
||||
this.$route.name !== 'user.register' &&
|
||||
this.$route.name !== 'link-share.auth' &&
|
||||
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'})
|
||||
}
|
||||
},
|
||||
...mapState('config', ['motd']),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -52,7 +52,7 @@ export const getCurrentLanguage = () => {
|
|||
return savedLanguage
|
||||
}
|
||||
|
||||
let browserLanguage = navigator.language || navigator.userLanguage
|
||||
const browserLanguage = navigator.language || navigator.userLanguage
|
||||
|
||||
for (let k in availableLanguages) {
|
||||
if (browserLanguage[k] === browserLanguage || k.startsWith(browserLanguage + '-')) {
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import {store} from '@/store'
|
||||
import { saveLastVisited } from '@/helpers/saveLastVisited'
|
||||
|
||||
import HomeComponent from '../views/Home'
|
||||
import NotFoundComponent from '../views/404'
|
||||
|
@ -520,4 +522,32 @@ router.resolve({
|
|||
params: { pathMatch: ['not', 'found'] },
|
||||
}).href // '/not/found'
|
||||
|
||||
function canUserAccess(to) {
|
||||
// Check if the user is already logged in and redirect them to the home page if not
|
||||
if (
|
||||
to.name !== 'user.login' &&
|
||||
to.name !== 'user.password-reset.request' &&
|
||||
to.name !== 'user.password-reset.reset' &&
|
||||
to.name !== 'user.register' &&
|
||||
to.name !== 'link-share.auth' &&
|
||||
to.name !== 'openid.auth' &&
|
||||
localStorage.getItem('passwordResetToken') === null &&
|
||||
localStorage.getItem('emailConfirmToken') === null
|
||||
) {
|
||||
saveLastVisited(to.name, to.params)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
router.beforeEach(async (to, from) => {
|
||||
if (store.getters['auth/authUser']) {
|
||||
return
|
||||
}
|
||||
|
||||
// canUserAccess() returns `true` or `false`
|
||||
const canAccess = canUserAccess(to)
|
||||
if (!canAccess) return {name: 'user.login'}
|
||||
})
|
||||
|
||||
export default router
|
|
@ -2,6 +2,11 @@ import {HTTPFactory} from '@/http-common'
|
|||
import {LOADING} from '../mutation-types'
|
||||
import UserModel from '../../models/user'
|
||||
import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
|
||||
import { saveLastVisited } from '@/helpers/saveLastVisited'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
const lastUserInfoRefresh = useStorage('lastUserInfoRefresh', 0) // returns Ref<number>
|
||||
|
||||
|
||||
const AUTH_TYPES = {
|
||||
'UNKNOWN': 0,
|
||||
|
@ -16,6 +21,24 @@ const defaultSettings = settings => {
|
|||
return settings
|
||||
}
|
||||
|
||||
function canUserAccess(to) {
|
||||
// Check if the user is already logged in and redirect them to the home page if not
|
||||
if (
|
||||
to.name !== 'user.login' &&
|
||||
to.name !== 'user.password-reset.request' &&
|
||||
to.name !== 'user.password-reset.reset' &&
|
||||
to.name !== 'user.register' &&
|
||||
to.name !== 'link-share.auth' &&
|
||||
to.name !== 'openid.auth' &&
|
||||
localStorage.getItem('passwordResetToken') === null &&
|
||||
localStorage.getItem('emailConfirmToken') === null
|
||||
) {
|
||||
saveLastVisited(to.name, to.params)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: () => ({
|
||||
|
@ -24,7 +47,6 @@ export default {
|
|||
info: null,
|
||||
needsTotpPasscode: false,
|
||||
avatarUrl: '',
|
||||
lastUserInfoRefresh: null,
|
||||
settings: {},
|
||||
}),
|
||||
getters: {
|
||||
|
@ -142,14 +164,10 @@ export default {
|
|||
const HTTP = HTTPFactory()
|
||||
ctx.commit(LOADING, true, {root: true})
|
||||
|
||||
const data = {
|
||||
code: code,
|
||||
}
|
||||
|
||||
// Delete an eventually preexisting old token
|
||||
removeToken()
|
||||
try {
|
||||
const response = await HTTP.post(`/auth/openid/${provider}/callback`, data)
|
||||
const response = await HTTP.post(`/auth/openid/${provider}/callback`, { code })
|
||||
// Save the token to local storage for later use
|
||||
saveToken(response.data.token, true)
|
||||
|
||||
|
@ -175,7 +193,12 @@ export default {
|
|||
|
||||
// This function can be called from multiple places at the same time and shortly after one another.
|
||||
// To prevent hitting the api too frequently or race conditions, we check at most once per minute.
|
||||
if (ctx.state.lastUserInfoRefresh !== null && ctx.state.lastUserInfoRefresh > (new Date()).setMinutes((new Date()).getMinutes() + 1)) {
|
||||
const now = new Date()
|
||||
const inOneMinute = now.setMinutes(now.getMinutes() + 1)
|
||||
if (
|
||||
ctx.state.lastUserInfoRefresh !== null &&
|
||||
ctx.state.lastUserInfoRefresh > inOneMinute
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -230,27 +253,44 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
// Renews the api token and saves it to local storage
|
||||
renewToken(ctx) {
|
||||
// FIXME: Timeout to avoid race conditions when authenticated as a user (=auth token in localStorage) and as a
|
||||
// link share in another tab. Without the timeout both the token renew and link share auth are executed at
|
||||
// the same time and one might win over the other.
|
||||
setTimeout(async () => {
|
||||
if (!ctx.state.authenticated) {
|
||||
return
|
||||
}
|
||||
async checkToken(ctx) {
|
||||
if (!ctx.state.authenticated) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await refreshToken(!ctx.state.isLinkShareAuth)
|
||||
ctx.dispatch('checkAuth')
|
||||
} catch(e) {
|
||||
// Don't logout on network errors as the user would then get logged out if they don't have
|
||||
// internet for a short period of time - such as when the laptop is still reconnecting
|
||||
if (e.request.status) {
|
||||
ctx.dispatch('logout')
|
||||
}
|
||||
const expiresIn = (ctx.state.info !== null ? ctx.state.info.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) {
|
||||
ctx.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) {
|
||||
ctx.dispatch('auth/renewToken')
|
||||
console.debug('renewed token')
|
||||
}
|
||||
},
|
||||
|
||||
// Renews the api token and saves it to local storage
|
||||
async renewToken(ctx) {
|
||||
if (!ctx.state.authenticated) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await refreshToken(!ctx.state.isLinkShareAuth)
|
||||
ctx.dispatch('checkAuth')
|
||||
} catch(e) {
|
||||
// Don't logout on network errors as the user would then get logged out if they don't have
|
||||
// internet for a short period of time - such as when the laptop is still reconnecting
|
||||
if (e.request.status) {
|
||||
ctx.dispatch('logout')
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
},
|
||||
logout(ctx) {
|
||||
removeToken()
|
||||
|
|
|
@ -124,40 +124,38 @@ export default {
|
|||
errorMessage: '',
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
const HTTP = HTTPFactory()
|
||||
// Try to verify the email
|
||||
// FIXME: Why is this here? Can we find a better place for this?
|
||||
let emailVerifyToken = localStorage.getItem('emailConfirmToken')
|
||||
if (emailVerifyToken) {
|
||||
const cancel = this.setLoading()
|
||||
HTTP.post('user/confirm', {token: emailVerifyToken})
|
||||
.then(() => {
|
||||
localStorage.removeItem('emailConfirmToken')
|
||||
this.confirmedEmailSuccess = true
|
||||
cancel()
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.errorMessage = e.response.data.message
|
||||
})
|
||||
beforeRouteEnter(to, from, next) {
|
||||
// Check if the user is already logged in, if so, redirect them to the homepage
|
||||
if (!this.authenticated) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user is already logged in, if so, redirect them to the homepage
|
||||
if (this.authenticated) {
|
||||
const last = getLastVisited()
|
||||
if (last !== null) {
|
||||
this.$router.push({
|
||||
name: last.name,
|
||||
params: last.params,
|
||||
})
|
||||
clearLastVisited()
|
||||
} else {
|
||||
this.$router.push({name: 'home'})
|
||||
}
|
||||
const last = getLastVisited()
|
||||
if (last !== null) {
|
||||
clearLastVisited()
|
||||
next(last)
|
||||
} else {
|
||||
next({name: 'home'})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const HTTP = HTTPFactory()
|
||||
// Try to verify the email
|
||||
// FIXME: Why is this here? Can we find a better place for this?
|
||||
const emailVerifyToken = localStorage.getItem('emailConfirmToken')
|
||||
if (!emailVerifyToken) {
|
||||
return
|
||||
}
|
||||
const cancel = this.setLoading()
|
||||
HTTP.post('user/confirm', {token: emailVerifyToken})
|
||||
.then(() => {
|
||||
localStorage.removeItem('emailConfirmToken')
|
||||
this.confirmedEmailSuccess = true
|
||||
})
|
||||
.catch(e => {
|
||||
this.errorMessage = e.response.data.message
|
||||
})
|
||||
.finally(() => cancel())
|
||||
this.hasApiUrl = window.API_URL !== ''
|
||||
this.setTitle(this.$t('user.auth.login'))
|
||||
},
|
||||
|
|
Reference in New Issue