From f68bb2625e5f619f365fdd421aeda2b8af879aab Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 1 Apr 2023 12:14:52 +0200 Subject: [PATCH 01/11] feat: persist link share auth rule in url hash This allows sharing links to a task directly. We're using hashes instead of query parameters because hash values are usually not logged in access logs. With this change, when a user uses a link share, the link share hash will be appended to all urls while browsing. When a link share hash is encountered in the current url and the user is not authenticated, they will be redirected to the link share auth page, get authenticated and then get redirected to whatever url they were previously on. --- src/composables/useRedirectToLastVisited.ts | 19 ++++++++--- src/router/index.ts | 37 +++++++++++++++++++-- src/views/sharing/LinkSharingAuth.vue | 20 ++++++++++- 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/composables/useRedirectToLastVisited.ts b/src/composables/useRedirectToLastVisited.ts index 838028cb68..ffd3711a90 100644 --- a/src/composables/useRedirectToLastVisited.ts +++ b/src/composables/useRedirectToLastVisited.ts @@ -5,16 +5,24 @@ export function useRedirectToLastVisited() { const router = useRouter() - function redirectIfSaved() { + function getRedirectRoute() { const last = getLastVisited() if (last !== null) { - router.push({ + clearLastVisited() + return { name: last.name, params: last.params, query: last.query, - }) - clearLastVisited() - return + } + } + + return null + } + + function redirectIfSaved() { + const lastRoute = getRedirectRoute() + if (lastRoute) { + router.push(lastRoute) } router.push({name: 'home'}) @@ -22,5 +30,6 @@ export function useRedirectToLastVisited() { return { redirectIfSaved, + getRedirectRoute, } } \ No newline at end of file diff --git a/src/router/index.ts b/src/router/index.ts index a685542cec..869a7fb601 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -6,6 +6,7 @@ import {saveProjectView, getProjectView} from '@/helpers/projectView' import {parseDateOrString} from '@/helpers/time/parseDateOrString' import {getNextWeekDate} from '@/helpers/time/getNextWeekDate' import {setTitle} from '@/helpers/setTitle' +import {getToken} from '@/helpers/auth' import {useProjectStore} from '@/stores/projects' import {useAuthStore} from '@/stores/auth' @@ -71,6 +72,8 @@ const NewProjectComponent = () => import('@/views/project/NewProject.vue') const EditTeamComponent = () => import('@/views/teams/EditTeam.vue') const NewTeamComponent = () => import('@/views/teams/NewTeam.vue') +const linkShareHashPrefix = '#linkshare=' + const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), scrollBehavior(to, from, savedPosition) { @@ -80,7 +83,7 @@ const router = createRouter({ } // Scroll to anchor should still work - if (to.hash) { + if (to.hash && !to.hash.startsWith(linkShareHashPrefix)) { return {el: to.hash} } @@ -482,8 +485,36 @@ export async function getAuthForRoute(route: RouteLocation) { } } -router.beforeEach(async (to) => { - return getAuthForRoute(to) +router.beforeEach(async (to, from) => { + + if(from.hash && from.hash.startsWith(linkShareHashPrefix)) { + to.hash = from.hash + } + + if (to.hash.startsWith(linkShareHashPrefix)) { + const currentAuthToken = getToken() + if (currentAuthToken === null) { + saveLastVisited(to.name as string, to.params, to.query) + return { + name: 'link-share.auth', + params: { + share: to.hash.replace(linkShareHashPrefix, ''), + }, + } + } + } + + const newRoute = await getAuthForRoute(to) + if(newRoute) { + return { + ...newRoute, + hash: to.hash, + } + } + + if(!to.fullPath.endsWith(to.hash)) { + return to.fullPath + to.hash + } }) export default router \ No newline at end of file diff --git a/src/views/sharing/LinkSharingAuth.vue b/src/views/sharing/LinkSharingAuth.vue index dab64eb77d..302223dec4 100644 --- a/src/views/sharing/LinkSharingAuth.vue +++ b/src/views/sharing/LinkSharingAuth.vue @@ -43,9 +43,11 @@ import {PROJECT_VIEWS, type ProjectView} from '@/types/ProjectView' import {useBaseStore} from '@/stores/base' import {useAuthStore} from '@/stores/auth' +import {useRedirectToLastVisited} from '@/composables/useRedirectToLastVisited' const {t} = useI18n({useScope: 'global'}) useTitle(t('sharing.authenticating')) +const {getRedirectRoute} = useRedirectToLastVisited() function useAuth() { const baseStore = useBaseStore() @@ -59,6 +61,7 @@ function useAuth() { const password = ref('') const authLinkShare = computed(() => authStore.authLinkShare) + const linkShareHashPrefix = '#linkshare=' async function authenticate() { authenticateWithPassword.value = false @@ -87,7 +90,22 @@ function useAuth() { ? route.query.view : 'list' - router.push({name: `project.${view}`, params: {projectId}}) + const hash = linkShareHashPrefix + route.params.share + + const last = getRedirectRoute() + if (last) { + router.push({ + ...last, + hash, + }) + return + } + + router.push({ + name: `project.${view}`, + params: {projectId}, + hash, + }) } catch (e: any) { if (e.response?.data?.code === 13001) { authenticateWithPassword.value = true From 59b05e9836946ed8b9dbb3926fc694641d8508a1 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 1 Apr 2023 14:12:47 +0200 Subject: [PATCH 02/11] chore: rename getRedirectRoute --- src/composables/useRedirectToLastVisited.ts | 6 +++--- src/views/sharing/LinkSharingAuth.vue | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/composables/useRedirectToLastVisited.ts b/src/composables/useRedirectToLastVisited.ts index ffd3711a90..901b4d9351 100644 --- a/src/composables/useRedirectToLastVisited.ts +++ b/src/composables/useRedirectToLastVisited.ts @@ -5,7 +5,7 @@ export function useRedirectToLastVisited() { const router = useRouter() - function getRedirectRoute() { + function getLastVisitedRoute() { const last = getLastVisited() if (last !== null) { clearLastVisited() @@ -20,7 +20,7 @@ export function useRedirectToLastVisited() { } function redirectIfSaved() { - const lastRoute = getRedirectRoute() + const lastRoute = getLastVisitedRoute() if (lastRoute) { router.push(lastRoute) } @@ -30,6 +30,6 @@ export function useRedirectToLastVisited() { return { redirectIfSaved, - getRedirectRoute, + getLastVisitedRoute, } } \ No newline at end of file diff --git a/src/views/sharing/LinkSharingAuth.vue b/src/views/sharing/LinkSharingAuth.vue index 302223dec4..f9c8c4c1b4 100644 --- a/src/views/sharing/LinkSharingAuth.vue +++ b/src/views/sharing/LinkSharingAuth.vue @@ -47,7 +47,7 @@ import {useRedirectToLastVisited} from '@/composables/useRedirectToLastVisited' const {t} = useI18n({useScope: 'global'}) useTitle(t('sharing.authenticating')) -const {getRedirectRoute} = useRedirectToLastVisited() +const {getLastVisitedRoute} = useRedirectToLastVisited() function useAuth() { const baseStore = useBaseStore() @@ -92,7 +92,7 @@ function useAuth() { const hash = linkShareHashPrefix + route.params.share - const last = getRedirectRoute() + const last = getLastVisitedRoute() if (last) { router.push({ ...last, From 61baf02e26b292e3f02816a483eb7d92fb49d8ab Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 1 Apr 2023 14:15:54 +0200 Subject: [PATCH 03/11] chore: import const instead of redeclaring it --- src/helpers/linkShareHash.ts | 1 + src/router/index.ts | 11 +++++------ src/views/sharing/LinkSharingAuth.vue | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 src/helpers/linkShareHash.ts diff --git a/src/helpers/linkShareHash.ts b/src/helpers/linkShareHash.ts new file mode 100644 index 0000000000..707c450f9c --- /dev/null +++ b/src/helpers/linkShareHash.ts @@ -0,0 +1 @@ +export const LINK_SHARE_HASH_PREFIX = '#linkshare=' diff --git a/src/router/index.ts b/src/router/index.ts index 869a7fb601..64f0ee1c8e 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -7,6 +7,7 @@ import {parseDateOrString} from '@/helpers/time/parseDateOrString' import {getNextWeekDate} from '@/helpers/time/getNextWeekDate' import {setTitle} from '@/helpers/setTitle' import {getToken} from '@/helpers/auth' +import {LINK_SHARE_HASH_PREFIX} from '@/helpers/linkShareHash' import {useProjectStore} from '@/stores/projects' import {useAuthStore} from '@/stores/auth' @@ -72,8 +73,6 @@ const NewProjectComponent = () => import('@/views/project/NewProject.vue') const EditTeamComponent = () => import('@/views/teams/EditTeam.vue') const NewTeamComponent = () => import('@/views/teams/NewTeam.vue') -const linkShareHashPrefix = '#linkshare=' - const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), scrollBehavior(to, from, savedPosition) { @@ -83,7 +82,7 @@ const router = createRouter({ } // Scroll to anchor should still work - if (to.hash && !to.hash.startsWith(linkShareHashPrefix)) { + if (to.hash && !to.hash.startsWith(LINK_SHARE_HASH_PREFIX)) { return {el: to.hash} } @@ -487,18 +486,18 @@ export async function getAuthForRoute(route: RouteLocation) { router.beforeEach(async (to, from) => { - if(from.hash && from.hash.startsWith(linkShareHashPrefix)) { + if(from.hash && from.hash.startsWith(LINK_SHARE_HASH_PREFIX)) { to.hash = from.hash } - if (to.hash.startsWith(linkShareHashPrefix)) { + if (to.hash.startsWith(LINK_SHARE_HASH_PREFIX)) { const currentAuthToken = getToken() if (currentAuthToken === null) { saveLastVisited(to.name as string, to.params, to.query) return { name: 'link-share.auth', params: { - share: to.hash.replace(linkShareHashPrefix, ''), + share: to.hash.replace(LINK_SHARE_HASH_PREFIX, ''), }, } } diff --git a/src/views/sharing/LinkSharingAuth.vue b/src/views/sharing/LinkSharingAuth.vue index f9c8c4c1b4..026d151b2b 100644 --- a/src/views/sharing/LinkSharingAuth.vue +++ b/src/views/sharing/LinkSharingAuth.vue @@ -40,6 +40,7 @@ import {useTitle} from '@vueuse/core' import Message from '@/components/misc/message.vue' import {PROJECT_VIEWS, type ProjectView} from '@/types/ProjectView' +import {LINK_SHARE_HASH_PREFIX} from '@/helpers/linkShareHash' import {useBaseStore} from '@/stores/base' import {useAuthStore} from '@/stores/auth' @@ -61,7 +62,6 @@ function useAuth() { const password = ref('') const authLinkShare = computed(() => authStore.authLinkShare) - const linkShareHashPrefix = '#linkshare=' async function authenticate() { authenticateWithPassword.value = false @@ -90,7 +90,7 @@ function useAuth() { ? route.query.view : 'list' - const hash = linkShareHashPrefix + route.params.share + const hash = LINK_SHARE_HASH_PREFIX + route.params.share const last = getLastVisitedRoute() if (last) { From b9f0635d9fcc764c7ee188c95ce59ac358f735cf Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 1 Apr 2023 14:17:05 +0200 Subject: [PATCH 04/11] feat: rename link share hash prefix --- src/helpers/linkShareHash.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/linkShareHash.ts b/src/helpers/linkShareHash.ts index 707c450f9c..eff521d05b 100644 --- a/src/helpers/linkShareHash.ts +++ b/src/helpers/linkShareHash.ts @@ -1 +1 @@ -export const LINK_SHARE_HASH_PREFIX = '#linkshare=' +export const LINK_SHARE_HASH_PREFIX = '#share=' From 7c964c29d487b5bcd2c125f81731e3b37374641a Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 1 Apr 2023 14:18:38 +0200 Subject: [PATCH 05/11] fix: return redirect --- src/views/sharing/LinkSharingAuth.vue | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/views/sharing/LinkSharingAuth.vue b/src/views/sharing/LinkSharingAuth.vue index 026d151b2b..98f801cd57 100644 --- a/src/views/sharing/LinkSharingAuth.vue +++ b/src/views/sharing/LinkSharingAuth.vue @@ -94,14 +94,13 @@ function useAuth() { const last = getLastVisitedRoute() if (last) { - router.push({ + return router.push({ ...last, hash, }) - return } - router.push({ + return router.push({ name: `project.${view}`, params: {projectId}, hash, From 06a1ff6f4bea4cc7447d528423de54f14583dca4 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 1 Apr 2023 14:19:28 +0200 Subject: [PATCH 06/11] chore: reduce nesting --- src/router/index.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/router/index.ts b/src/router/index.ts index 64f0ee1c8e..17e5c9f1eb 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -489,17 +489,14 @@ router.beforeEach(async (to, from) => { if(from.hash && from.hash.startsWith(LINK_SHARE_HASH_PREFIX)) { to.hash = from.hash } - - if (to.hash.startsWith(LINK_SHARE_HASH_PREFIX)) { - const currentAuthToken = getToken() - if (currentAuthToken === null) { - saveLastVisited(to.name as string, to.params, to.query) - return { - name: 'link-share.auth', - params: { - share: to.hash.replace(LINK_SHARE_HASH_PREFIX, ''), - }, - } + + if (to.hash.startsWith(linkShareHashPrefix) && getToken() === null) { + saveLastVisited(to.name as string, to.params, to.query) + return { + name: 'link-share.auth', + params: { + share: to.hash.replace(LINK_SHARE_HASH_PREFIX, ''), + }, } } From 0ce150af237985dda0cf44f24179ebae332e7585 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 1 Apr 2023 15:17:09 +0200 Subject: [PATCH 07/11] chore: move const --- src/{helpers => constants}/linkShareHash.ts | 0 src/router/index.ts | 2 +- src/views/sharing/LinkSharingAuth.vue | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/{helpers => constants}/linkShareHash.ts (100%) diff --git a/src/helpers/linkShareHash.ts b/src/constants/linkShareHash.ts similarity index 100% rename from src/helpers/linkShareHash.ts rename to src/constants/linkShareHash.ts diff --git a/src/router/index.ts b/src/router/index.ts index 17e5c9f1eb..cac41f1248 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -7,7 +7,7 @@ import {parseDateOrString} from '@/helpers/time/parseDateOrString' import {getNextWeekDate} from '@/helpers/time/getNextWeekDate' import {setTitle} from '@/helpers/setTitle' import {getToken} from '@/helpers/auth' -import {LINK_SHARE_HASH_PREFIX} from '@/helpers/linkShareHash' +import {LINK_SHARE_HASH_PREFIX} from '@/constants/linkShareHash' import {useProjectStore} from '@/stores/projects' import {useAuthStore} from '@/stores/auth' diff --git a/src/views/sharing/LinkSharingAuth.vue b/src/views/sharing/LinkSharingAuth.vue index 98f801cd57..b48b437d85 100644 --- a/src/views/sharing/LinkSharingAuth.vue +++ b/src/views/sharing/LinkSharingAuth.vue @@ -40,7 +40,7 @@ import {useTitle} from '@vueuse/core' import Message from '@/components/misc/message.vue' import {PROJECT_VIEWS, type ProjectView} from '@/types/ProjectView' -import {LINK_SHARE_HASH_PREFIX} from '@/helpers/linkShareHash' +import {LINK_SHARE_HASH_PREFIX} from '@/constants/linkShareHash' import {useBaseStore} from '@/stores/base' import {useAuthStore} from '@/stores/auth' From a33e2f6c00f35f36aabb6b4d6e823396d29cdf3d Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 1 Apr 2023 17:16:51 +0200 Subject: [PATCH 08/11] chore: follow the happy path --- src/composables/useRedirectToLastVisited.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/composables/useRedirectToLastVisited.ts b/src/composables/useRedirectToLastVisited.ts index 901b4d9351..9fc2f8fd78 100644 --- a/src/composables/useRedirectToLastVisited.ts +++ b/src/composables/useRedirectToLastVisited.ts @@ -7,25 +7,25 @@ export function useRedirectToLastVisited() { function getLastVisitedRoute() { const last = getLastVisited() - if (last !== null) { - clearLastVisited() - return { - name: last.name, - params: last.params, - query: last.query, - } + if (last === null) { + return null } - return null + clearLastVisited() + return { + name: last.name, + params: last.params, + query: last.query, + } } function redirectIfSaved() { const lastRoute = getLastVisitedRoute() if (lastRoute) { - router.push(lastRoute) + return router.push(lastRoute) } - router.push({name: 'home'}) + return router.push({name: 'home'}) } return { From c2ffe3a9dcfd1e067b8d92e1d69183c2a8acfa8f Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 1 Apr 2023 17:56:30 +0200 Subject: [PATCH 09/11] feat: check link share auth from store instead --- src/components/misc/ready.vue | 4 +++- src/router/index.ts | 23 +++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/components/misc/ready.vue b/src/components/misc/ready.vue index 2c7a5713e8..836df26b34 100644 --- a/src/components/misc/ready.vue +++ b/src/components/misc/ready.vue @@ -56,11 +56,13 @@ import {useOnline} from '@/composables/useOnline' import {getAuthForRoute} from '@/router' import {useBaseStore} from '@/stores/base' +import {useAuthStore} from '@/stores/auth' const router = useRouter() const route = useRoute() const baseStore = useBaseStore() +const authStore = useAuthStore() const ready = computed(() => baseStore.ready) const online = useOnline() @@ -72,7 +74,7 @@ async function load() { try { await baseStore.loadApp() baseStore.setReady(true) - const redirectTo = await getAuthForRoute(route) + const redirectTo = await getAuthForRoute(route, authStore) if (typeof redirectTo !== 'undefined') { await router.push(redirectTo) } diff --git a/src/router/index.ts b/src/router/index.ts index cac41f1248..6caa65743b 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -6,7 +6,6 @@ import {saveProjectView, getProjectView} from '@/helpers/projectView' import {parseDateOrString} from '@/helpers/time/parseDateOrString' import {getNextWeekDate} from '@/helpers/time/getNextWeekDate' import {setTitle} from '@/helpers/setTitle' -import {getToken} from '@/helpers/auth' import {LINK_SHARE_HASH_PREFIX} from '@/constants/linkShareHash' import {useProjectStore} from '@/stores/projects' @@ -444,8 +443,7 @@ const router = createRouter({ ], }) -export async function getAuthForRoute(route: RouteLocation) { - const authStore = useAuthStore() +export async function getAuthForRoute(to: RouteLocation, authStore) { if (authStore.authUser || authStore.authLinkShare) { return } @@ -466,31 +464,32 @@ export async function getAuthForRoute(route: RouteLocation) { 'user.register', 'link-share.auth', 'openid.auth', - ].includes(route.name as string) && + ].includes(to.name as string) && localStorage.getItem('passwordResetToken') === null && localStorage.getItem('emailConfirmToken') === null && - !(route.name === 'home' && (typeof route.query.userPasswordReset !== 'undefined' || typeof route.query.userEmailConfirm !== 'undefined')) + !(to.name === 'home' && (typeof to.query.userPasswordReset !== 'undefined' || typeof to.query.userEmailConfirm !== 'undefined')) ) { - saveLastVisited(route.name as string, route.params, route.query) + saveLastVisited(to.name as string, to.params, to.query) return {name: 'user.login'} } - if(localStorage.getItem('passwordResetToken') !== null && route.name !== 'user.password-reset.reset') { + if(localStorage.getItem('passwordResetToken') !== null && to.name !== 'user.password-reset.reset') { return {name: 'user.password-reset.reset'} } - if(localStorage.getItem('emailConfirmToken') !== null && route.name !== 'user.login') { + if(localStorage.getItem('emailConfirmToken') !== null && to.name !== 'user.login') { return {name: 'user.login'} } } router.beforeEach(async (to, from) => { - + const authStore = useAuthStore() + if(from.hash && from.hash.startsWith(LINK_SHARE_HASH_PREFIX)) { to.hash = from.hash } - if (to.hash.startsWith(linkShareHashPrefix) && getToken() === null) { + if (to.hash.startsWith(LINK_SHARE_HASH_PREFIX) && !authStore.authLinkShare) { saveLastVisited(to.name as string, to.params, to.query) return { name: 'link-share.auth', @@ -499,8 +498,8 @@ router.beforeEach(async (to, from) => { }, } } - - const newRoute = await getAuthForRoute(to) + + const newRoute = await getAuthForRoute(to, authStore) if(newRoute) { return { ...newRoute, From 20660564c16283c77029bc3c3125c6c3febde47e Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 4 Apr 2023 11:35:45 +0200 Subject: [PATCH 10/11] feat: change the link share hash name --- src/constants/linkShareHash.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/linkShareHash.ts b/src/constants/linkShareHash.ts index eff521d05b..fef54a9cee 100644 --- a/src/constants/linkShareHash.ts +++ b/src/constants/linkShareHash.ts @@ -1 +1 @@ -export const LINK_SHARE_HASH_PREFIX = '#share=' +export const LINK_SHARE_HASH_PREFIX = '#share-auth-token=' From 34182b8bbb7a7e8eeb0ce698dc6da79785d05fc9 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 11 Apr 2023 18:40:22 +0200 Subject: [PATCH 11/11] fix: follow the happy path --- src/composables/useRedirectToLastVisited.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/composables/useRedirectToLastVisited.ts b/src/composables/useRedirectToLastVisited.ts index 9fc2f8fd78..29e9ad48c2 100644 --- a/src/composables/useRedirectToLastVisited.ts +++ b/src/composables/useRedirectToLastVisited.ts @@ -21,11 +21,11 @@ export function useRedirectToLastVisited() { function redirectIfSaved() { const lastRoute = getLastVisitedRoute() - if (lastRoute) { - return router.push(lastRoute) + if (!lastRoute) { + return router.push({name: 'home'}) } - return router.push({name: 'home'}) + return router.push(lastRoute) } return {