wip: feat: use navigation guards #899
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 ContentNoAuth from './components/home/contentNoAuth'
|
||||||
import {setLanguage} from './i18n'
|
import {setLanguage} from './i18n'
|
||||||
import AccountDeleteService from '@/services/accountDelete'
|
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({
|
export default defineComponent({
|
||||||
name: 'app',
|
name: 'app',
|
||||||
|
@ -47,11 +80,11 @@ export default defineComponent({
|
||||||
KeyboardShortcuts,
|
KeyboardShortcuts,
|
||||||
Notification,
|
Notification,
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeRouteEnter(to) {
|
||||||
this.setupOnlineStatus()
|
setupOnlineStatus()
|
||||||
this.setupPasswortResetRedirect()
|
const redirect = setupRedirects(to)
|
||||||
this.setupEmailVerificationRedirect()
|
if (redirect) return redirect
|
||||||
this.setupAccountDeletionVerification()
|
setupAccountDeletionVerification(to)
|
||||||
},
|
},
|
||||||
beforeCreate() {
|
beforeCreate() {
|
||||||
// FIXME: async action in beforeCreate, might be not finished when component mounts
|
// FIXME: async action in beforeCreate, might be not finished when component mounts
|
||||||
|
@ -63,12 +96,6 @@ export default defineComponent({
|
||||||
|
|
||||||
setLanguage()
|
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: {
|
computed: {
|
||||||
isTouch() {
|
isTouch() {
|
||||||
return isTouchDevice()
|
return isTouchDevice()
|
||||||
|
@ -82,38 +109,5 @@ export default defineComponent({
|
||||||
'authLinkShare',
|
'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>
|
</script>
|
||||||
|
|
|
@ -95,28 +95,7 @@ export default {
|
||||||
this.$store.dispatch('auth/renewToken')
|
this.$store.dispatch('auth/renewToken')
|
||||||
|
|
||||||
// Check if the token is still valid if the window gets focus again to maybe renew it
|
// Check if the token is still valid if the window gets focus again to maybe renew it
|
||||||
window.addEventListener('focus', () => {
|
window.addEventListener('focus', this.$store.dispatch('auth/checkToken'))
|
||||||
|
|
||||||
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')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
hideMenuOnMobile() {
|
hideMenuOnMobile() {
|
||||||
if (window.innerWidth < 769) {
|
if (window.innerWidth < 769) {
|
||||||
|
|
|
@ -19,7 +19,6 @@
|
||||||
import {mapState} from 'vuex'
|
import {mapState} from 'vuex'
|
||||||
|
|
||||||
import logoUrl from '@/assets/logo-full.svg'
|
import logoUrl from '@/assets/logo-full.svg'
|
||||||
import { saveLastVisited } from '@/helpers/saveLastVisited'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'contentNoAuth',
|
name: 'contentNoAuth',
|
||||||
|
@ -29,39 +28,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
routeName() {
|
...mapState('config', ['motd']),
|
||||||
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'})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -52,7 +52,7 @@ export const getCurrentLanguage = () => {
|
||||||
return savedLanguage
|
return savedLanguage
|
||||||
}
|
}
|
||||||
|
|
||||||
let browserLanguage = navigator.language || navigator.userLanguage
|
const browserLanguage = navigator.language || navigator.userLanguage
|
||||||
|
|
||||||
for (let k in availableLanguages) {
|
for (let k in availableLanguages) {
|
||||||
if (browserLanguage[k] === browserLanguage || k.startsWith(browserLanguage + '-')) {
|
if (browserLanguage[k] === browserLanguage || k.startsWith(browserLanguage + '-')) {
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import {store} from '@/store'
|
||||||
|
import { saveLastVisited } from '@/helpers/saveLastVisited'
|
||||||
|
|
||||||
import HomeComponent from '../views/Home'
|
import HomeComponent from '../views/Home'
|
||||||
import NotFoundComponent from '../views/404'
|
import NotFoundComponent from '../views/404'
|
||||||
|
@ -95,6 +97,9 @@ const router = createRouter({
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'user.login',
|
name: 'user.login',
|
||||||
component: LoginComponent,
|
component: LoginComponent,
|
||||||
|
meta: {
|
||||||
|
accessibleWithoutAuth: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/get-password-reset',
|
path: '/get-password-reset',
|
||||||
|
@ -520,4 +525,49 @@ router.resolve({
|
||||||
params: { pathMatch: ['not', 'found'] },
|
params: { pathMatch: ['not', 'found'] },
|
||||||
}).href // '/not/found'
|
}).href // '/not/found'
|
||||||
|
|
||||||
|
|
||||||
|
router.beforeEach((to) => {
|
||||||
|
if (
|
||||||
|
to.matched.some(record => record.meta.requiresAuth) &&
|
||||||
|
// this route requires auth, check if logged in
|
||||||
|
// if not, redirect to login page.
|
||||||
|
!auth.loggedIn()
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
path: '/login',
|
||||||
|
query: { redirect: to.fullPath },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function canUserAccess(to) {
|
||||||
|
// Check if the user is already logged in and redirect them to the home page if not
|
||||||
|
if (
|
||||||
|
(to.matched.some(record => record.meta.accessibleWithoutAuth))
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
export default router
|
|
@ -2,6 +2,11 @@ import {HTTPFactory} from '@/http-common'
|
||||||
import {LOADING} from '../mutation-types'
|
import {LOADING} from '../mutation-types'
|
||||||
import UserModel from '../../models/user'
|
import UserModel from '../../models/user'
|
||||||
import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
|
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 = {
|
const AUTH_TYPES = {
|
||||||
'UNKNOWN': 0,
|
'UNKNOWN': 0,
|
||||||
|
@ -16,6 +21,24 @@ const defaultSettings = settings => {
|
||||||
return 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 {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: () => ({
|
state: () => ({
|
||||||
|
@ -24,7 +47,6 @@ export default {
|
||||||
info: null,
|
info: null,
|
||||||
needsTotpPasscode: false,
|
needsTotpPasscode: false,
|
||||||
avatarUrl: '',
|
avatarUrl: '',
|
||||||
lastUserInfoRefresh: null,
|
|
||||||
settings: {},
|
settings: {},
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
|
@ -142,14 +164,10 @@ export default {
|
||||||
const HTTP = HTTPFactory()
|
const HTTP = HTTPFactory()
|
||||||
ctx.commit(LOADING, true, {root: true})
|
ctx.commit(LOADING, true, {root: true})
|
||||||
|
|
||||||
const data = {
|
|
||||||
code: code,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete an eventually preexisting old token
|
// Delete an eventually preexisting old token
|
||||||
removeToken()
|
removeToken()
|
||||||
try {
|
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
|
// Save the token to local storage for later use
|
||||||
saveToken(response.data.token, true)
|
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.
|
// 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.
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,12 +253,30 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async checkToken(ctx) {
|
||||||
|
if (!ctx.state.authenticated) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// Renews the api token and saves it to local storage
|
||||||
renewToken(ctx) {
|
async 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) {
|
if (!ctx.state.authenticated) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -250,7 +291,6 @@ export default {
|
||||||
ctx.dispatch('logout')
|
ctx.dispatch('logout')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 5000)
|
|
||||||
},
|
},
|
||||||
logout(ctx) {
|
logout(ctx) {
|
||||||
removeToken()
|
removeToken()
|
||||||
|
|
|
@ -124,40 +124,38 @@ export default {
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeRouteEnter(to, from, next) {
|
||||||
|
// Check if the user is already logged in, if so, redirect them to the homepage
|
||||||
|
if (!this.authenticated) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = getLastVisited()
|
||||||
|
if (last !== null) {
|
||||||
|
clearLastVisited()
|
||||||
|
next(last)
|
||||||
|
} else {
|
||||||
|
next({name: 'home'})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
const HTTP = HTTPFactory()
|
const HTTP = HTTPFactory()
|
||||||
// Try to verify the email
|
// Try to verify the email
|
||||||
// FIXME: Why is this here? Can we find a better place for this?
|
// FIXME: Why is this here? Can we find a better place for this?
|
||||||
let emailVerifyToken = localStorage.getItem('emailConfirmToken')
|
const emailVerifyToken = localStorage.getItem('emailConfirmToken')
|
||||||
if (emailVerifyToken) {
|
if (!emailVerifyToken) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const cancel = this.setLoading()
|
const cancel = this.setLoading()
|
||||||
HTTP.post('user/confirm', {token: emailVerifyToken})
|
HTTP.post('user/confirm', {token: emailVerifyToken})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
localStorage.removeItem('emailConfirmToken')
|
localStorage.removeItem('emailConfirmToken')
|
||||||
this.confirmedEmailSuccess = true
|
this.confirmedEmailSuccess = true
|
||||||
cancel()
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
|
||||||
this.errorMessage = e.response.data.message
|
this.errorMessage = e.response.data.message
|
||||||
})
|
})
|
||||||
}
|
.finally(() => cancel())
|
||||||
|
|
||||||
// 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'})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.hasApiUrl = window.API_URL !== ''
|
this.hasApiUrl = window.API_URL !== ''
|
||||||
this.setTitle(this.$t('user.auth.login'))
|
this.setTitle(this.$t('user.auth.login'))
|
||||||
},
|
},
|
||||||
|
|
Reference in New Issue
Block a user