Skip to content

Commit

Permalink
refactor: remove tokeninfo endpoint usage
Browse files Browse the repository at this point in the history
Removes the tokeninfo endpoint usage and uses the webdav response instead to retrieve all necessary information to resolve a public/internal link.

The biggest change here is that we don't get the file id anymore when resolving an internal link. To solve this we need to login the user first and then redirect to the link resolving page again (only this time with an authenticated context, hence the new route `/i/...`). After that last redirect the authenticated user can now fetch all necessary information for resolving.

See owncloud/ocis#8858 for more detail.
  • Loading branch information
JammingBen committed May 17, 2024
1 parent f3ae5bf commit e9b9ca9
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 129 deletions.
1 change: 0 additions & 1 deletion packages/web-pkg/src/cern/composables/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from './useGroupingSettings'
export * from './useLoadTokenInfo'
9 changes: 0 additions & 9 deletions packages/web-pkg/src/cern/composables/useLoadTokenInfo.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/web-runtime/src/composables/tokenInfo/index.ts

This file was deleted.

44 changes: 0 additions & 44 deletions packages/web-runtime/src/composables/tokenInfo/useLoadTokenInfo.ts

This file was deleted.

65 changes: 21 additions & 44 deletions packages/web-runtime/src/pages/resolvePublicLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,7 @@ import {
isPublicSpaceResource,
PublicSpaceResource
} from '@ownclouders/web-client'
import isEmpty from 'lodash-es/isEmpty'
import { useGettext } from 'vue3-gettext'
// full import is needed here so it can be overwritten via CERN config
import { useLoadTokenInfo } from 'web-runtime/src/composables/tokenInfo'
import { urlJoin } from '@ownclouders/web-client'
import { RouteLocationNamedRaw } from 'vue-router'
import { dirname } from 'path'
Expand Down Expand Up @@ -132,26 +129,27 @@ export default defineComponent({
return queryItemAsString(unref(detailsQuery))
})
// token info
const { loadTokenInfoTask } = useLoadTokenInfo({ clientService, authStore })
const tokenInfo = ref(null)
const isPasswordRequired = ref(false)
const isInternalLink = ref(false)
// generic public link loading
const isPasswordRequired = ref<boolean>()
const isPasswordRequiredTask = useTask(function* () {
if (!isEmpty(unref(tokenInfo))) {
return unref(tokenInfo).password_protected
}
const loadLinkMetaDataTask = useTask(function* () {
try {
let space: PublicSpaceResource = {
...unref(publicLinkSpace),
publicLinkPassword: null
}
yield clientService.webdav.getFileInfo(space)
return false
} catch (error) {
if (error.statusCode === 401) {
return true
if (error.message === "No 'Authorization: Basic' header found") {
isPasswordRequired.value = true
}
if (error.message === "No 'Authorization: Bearer' header found") {
isInternalLink.value = true
}
return
}
if (error.statusCode === 404) {
throw new Error($gettext('The resource could not be located, it may not exist anymore.'))
Expand Down Expand Up @@ -182,25 +180,13 @@ export default defineComponent({
return false
})
// resolve public link. resolve into authenticated context if possible.
const redirectToPrivateLink = (fileId: string | number) => {
return router.push({
name: 'resolvePrivateLink',
params: { fileId: `${fileId}` },
...(unref(details) && {
query: {
details: unref(details)
}
})
})
}
const resolvePublicLinkTask = useTask(function* (signal, passwordRequired: boolean) {
if (unref(isOcmLink) && !configStore.options.ocm.openRemotely) {
throw new Error($gettext('Opening files from remote is disabled'))
}
if (!isEmpty(unref(tokenInfo)) && unref(tokenInfo)?.alias_link) {
redirectToPrivateLink(unref(tokenInfo).id)
if (unref(isInternalLink)) {
router.push({ name: 'login', query: { redirectUrl: `/i/${unref(token)}` } })
return
}
Expand Down Expand Up @@ -278,16 +264,11 @@ export default defineComponent({
router.push(targetLocation)
})
const isLoading = computed<boolean>(() => {
const isLoading = computed(() => {
if (unref(errorMessage)) {
return false
}
if (
loadTokenInfoTask.isRunning ||
!loadTokenInfoTask.last ||
isPasswordRequiredTask.isRunning ||
!isPasswordRequiredTask.last
) {
if (loadLinkMetaDataTask.isRunning || !loadLinkMetaDataTask.last) {
return true
}
if (!unref(isPasswordRequired)) {
Expand All @@ -299,11 +280,9 @@ export default defineComponent({
if (resolvePublicLinkTask.isError && resolvePublicLinkTask.last.error.statusCode !== 401) {
return resolvePublicLinkTask.last.error.message
}
if (loadTokenInfoTask.isError) {
return loadTokenInfoTask.last.error.message
}
if (isPasswordRequiredTask.isError) {
return isPasswordRequiredTask.last.error.message
if (loadLinkMetaDataTask.isError) {
return loadLinkMetaDataTask.last.error.message
}
return null
})
Expand All @@ -314,8 +293,7 @@ export default defineComponent({
return
}
tokenInfo.value = await loadTokenInfoTask.perform(unref(token))
isPasswordRequired.value = await isPasswordRequiredTask.perform()
await loadLinkMetaDataTask.perform()
if (!unref(isPasswordRequired)) {
await resolvePublicLinkTask.perform(false)
}
Expand All @@ -342,9 +320,8 @@ export default defineComponent({
isLoading,
errorMessage,
footerSlogan,
loadTokenInfoTask,
resolvePublicLinkTask,
isPasswordRequiredTask
loadLinkMetaDataTask
}
}
})
Expand Down
6 changes: 6 additions & 0 deletions packages/web-runtime/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ const routes = [
component: ResolvePublicLinkPage,
meta: { title: $gettext('Public link'), authContext: 'anonymous' }
},
{
path: '/i/:token/:driveAliasAndItem(.*)?',
name: 'resolveInternalLink',
component: ResolvePublicLinkPage,
meta: { title: $gettext('Internal link'), authContext: 'user' }
},
{
path: '/o/:token/:driveAliasAndItem(.*)?',
name: 'resolvePublicOcmLink',
Expand Down
75 changes: 50 additions & 25 deletions packages/web-runtime/tests/unit/pages/resolvePublicLink.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import ResolvePublicLink from '../../../src/pages/resolvePublicLink.vue'
import { defaultPlugins, defaultComponentMocks, shallowMount, nextTicks } from 'web-test-helpers'
import { defaultPlugins, defaultComponentMocks, shallowMount } from 'web-test-helpers'
import { mockDeep } from 'vitest-mock-extended'
import { CapabilityStore, ClientService } from '@ownclouders/web-pkg'
import { SpaceResource } from '@ownclouders/web-client'
import { CapabilityStore, ClientService, useRouteParam } from '@ownclouders/web-pkg'
import { HttpError, SpaceResource } from '@ownclouders/web-client'
import { authService } from 'web-runtime/src/services/auth'
import { useLoadTokenInfo } from '../../../src/composables/tokenInfo'
import { Task } from 'vue-concurrency'
import { ref } from 'vue'

vi.mock('web-runtime/src/services/auth')
vi.mock('web-runtime/src/composables/tokenInfo')

vi.mock('@ownclouders/web-pkg', async (importOriginal) => ({
...(await importOriginal<any>()),
useRouteParam: vi.fn()
}))

const selectors = {
cardFooter: '.oc-card-footer',
Expand All @@ -30,61 +33,83 @@ describe('resolvePublicLink', () => {
describe('password required form', () => {
it('should display if password is required', async () => {
const { wrapper } = getWrapper({ passwordRequired: true })
await wrapper.vm.isPasswordRequiredTask.last
await nextTicks(4)
await wrapper.vm.loadLinkMetaDataTask.last

expect(wrapper.find('form').html()).toMatchSnapshot()
})
describe('submit button', () => {
it('should be set as disabled if "password" is empty', async () => {
const { wrapper } = getWrapper({ passwordRequired: true })
await wrapper.vm.isPasswordRequiredTask.last
await nextTicks(4)
await wrapper.vm.loadLinkMetaDataTask.last

expect(wrapper.find(selectors.submitButton).attributes().disabled).toBe('true')
})
it('should be set as enabled if "password" is not empty', async () => {
const { wrapper } = getWrapper({ passwordRequired: true })
await wrapper.vm.isPasswordRequiredTask.last
await nextTicks(4)
await wrapper.vm.loadLinkMetaDataTask.last
wrapper.vm.password = 'password'
await wrapper.vm.$nextTick()

expect(wrapper.find(selectors.submitButton).attributes().disabled).toBe('false')
})
it('should resolve the public link on click', async () => {
const resolvePublicLinkSpy = vi.spyOn(authService, 'resolvePublicLink')
const { wrapper } = getWrapper({ passwordRequired: true })
await wrapper.vm.isPasswordRequiredTask.last
await nextTicks(4)
await wrapper.vm.loadLinkMetaDataTask.last

wrapper.vm.password = 'password'
await wrapper.vm.$nextTick()
await wrapper.find(selectors.submitButton).trigger('submit')
await wrapper.vm.resolvePublicLinkTask.last

expect(resolvePublicLinkSpy).toHaveBeenCalled()
})
})
})
})
describe('internal link', () => {
it('redirects the user to the login page', async () => {
const { wrapper, mocks } = getWrapper({ isInternalLink: true })
await wrapper.vm.loadLinkMetaDataTask.last

function getWrapper({ passwordRequired = false } = {}) {
const tokenInfo = { password_protected: passwordRequired } as any
vi.mocked(useLoadTokenInfo).mockReturnValue({
loadTokenInfoTask: mockDeep<Task<any, any>>({
perform: () => tokenInfo,
isRunning: false,
isError: false
expect(mocks.$router.push).toHaveBeenCalledWith({
name: 'login',
query: { redirectUrl: '/i/token' }
})
})
})
})

function getWrapper({
passwordRequired = false,
isInternalLink = false
}: { passwordRequired?: boolean; isInternalLink?: boolean } = {}) {
const $clientService = mockDeep<ClientService>()
$clientService.webdav.getFileInfo.mockResolvedValue(
mockDeep<SpaceResource>({ driveType: 'public' })
)
const spaceResource = mockDeep<SpaceResource>({ driveType: 'public' })

// loadLinkMetaDataTask response
if (passwordRequired) {
$clientService.webdav.getFileInfo.mockRejectedValueOnce(
new HttpError("No 'Authorization: Basic' header found", undefined, 401)
)
} else if (isInternalLink) {
$clientService.webdav.getFileInfo.mockRejectedValueOnce(
new HttpError("No 'Authorization: Bearer' header found", undefined, 401)
)
} else {
$clientService.webdav.getFileInfo.mockResolvedValueOnce(spaceResource)
}

$clientService.webdav.getFileInfo.mockResolvedValueOnce(spaceResource)
const mocks = { ...defaultComponentMocks(), $clientService }

const capabilities = {
files_sharing: { federation: { incoming: true, outgoing: true } }
} satisfies Partial<CapabilityStore['capabilities']>

vi.mocked(useRouteParam).mockReturnValue(ref('token'))

return {
mocks,
wrapper: shallowMount(ResolvePublicLink, {
global: {
plugins: [...defaultPlugins({ piniaOptions: { capabilityState: { capabilities } } })],
Expand Down
5 changes: 0 additions & 5 deletions vite.cern.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,6 @@ export default defineConfig(async (args) => {
projectRootDir,
'packages/web-pkg/src/cern/components/CollapsibleOcTable.vue'
)
// token info request
;(config.resolve.alias as any)['web-runtime/src/composables/tokenInfo'] = join(
projectRootDir,
'packages/web-pkg/src/cern/composables/useLoadTokenInfo'
)
// create space component
;(config.resolve.alias as any)['../../components/AppBar/CreateSpace.vue'] = join(
projectRootDir,
Expand Down

0 comments on commit e9b9ca9

Please sign in to comment.