Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: remove tokeninfo endpoint usage #10869

Merged
merged 2 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

57 changes: 20 additions & 37 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,28 @@ 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) {
// FIXME: check for error codes as soon as the server supports them
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 +181,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 @@ -282,11 +269,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 @@ -297,8 +282,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 @@ -324,9 +308,8 @@ export default defineComponent({
wrongPasswordMessage,
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