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

fix(ocm): base64 encoded invite token #11992

Merged
merged 1 commit into from
Dec 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
6 changes: 6 additions & 0 deletions changelog/unreleased/bugfix-ocm-invite-flow
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Bugfix: OCM invite flow

We've fixed the OCM invite flow and made it more user-friendly.

https://github.com/owncloud/web/pull/11992
https://github.com/owncloud/web/issues/11983
10 changes: 0 additions & 10 deletions packages/web-app-ocm/src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,3 @@ export type InviteSchema = z.infer<typeof inviteSchema>

export const inviteListSchema = z.array(inviteSchema)
export type InviteListSchema = z.infer<typeof inviteListSchema>

// Provider
export const providerSchema = z.object({
domain: z.string(),
full_name: z.string()
})
export type ProviderSchema = z.infer<typeof providerSchema>

export const providerListSchema = z.array(providerSchema)
export type ProviderListSchema = z.infer<typeof providerListSchema>
175 changes: 42 additions & 133 deletions packages/web-app-ocm/src/views/IncomingInvitations.vue
Original file line number Diff line number Diff line change
@@ -1,59 +1,38 @@
<template>
<div id="incoming" class="sciencemesh-app">
<div>
<div class="oc-flex oc-flex-middle oc-px-m oc-py-s">
<div class="oc-flex oc-flex-middle oc-px-m oc-pt-s">
<oc-icon name="user-received" />
<h2 class="oc-px-s" v-text="$gettext('Accept invitations')" />
<oc-contextual-helper class="oc-pl-xs" v-bind="helperContent" />
</div>
<div v-if="!providers.length" class="oc-flex oc-flex-center oc-flex-middle">
<oc-icon name="error-warning" fill-type="line" class="oc-mr-s" size="large" />
<span v-text="$gettext('The list of institutions is empty. Please contact your admin.')" />
</div>
<div v-else class="oc-flex oc-flex-column oc-flex-middle oc-flex-center oc-p-m">
<div class="oc-flex oc-flex-column oc-flex-middle oc-flex-center oc-p-m">
<div class="oc-width-1-2">
<oc-text-input
ref="tokenInput"
v-model="token"
:label="$gettext('Enter invite token')"
:clear-button-enabled="true"
class="oc-mb-m"
class="oc-mb-s"
@update:model-value="decodeInviteToken"
/>
<oc-select
v-model="provider"
:label="$gettext('Select institution of inviter')"
:options="providers"
class="oc-mb-m"
:position-fixed="true"
:loading="loading"
<div
:class="{
'oc-text-input-danger': providerError && token,
'oc-text-input-success': provider
}"
>
<template #option="{ full_name, domain }">
<div class="oc-text-break">
<span class="option">
<strong v-text="full_name" />
</span>
<span class="option" v-text="domain" />
</div>
</template>
<template #no-options> No institutions found with this name</template>
<template #selected-option="{ full_name, domain }">
<div class="options-wrapper oc-text-break">
<strong class="oc-mr-s oc-text-break" v-text="full_name" />
<small
v-oc-tooltip="domain"
v-text="domain.length > 17 ? domain.slice(0, 20) + '...' : domain"
/>
</div>
</template>
</oc-select>
<div v-if="providerError" class="oc-text-input-message">
<span
class="oc-text-input-danger"
v-text="$gettext('Unknown institution. Check invitation url or select from list')"
/>
<span v-text="$gettext('Institution:')" />
<span v-if="!token" v-text="'-'" />
<span v-else-if="provider" v-text="provider" />
<span v-else v-text="$gettext('invalid invite token')" />
</div>
</div>
<oc-button size="small" :disabled="acceptInvitationButtonDisabled" @click="acceptInvite">
<oc-button
size="small"
:disabled="acceptInvitationButtonDisabled"
class="oc-mt-s"
@click="acceptInvite"
>
<oc-icon name="add" />
<span v-text="$gettext('Accept invitation')" />
</oc-button>
Expand All @@ -63,35 +42,23 @@
</template>

<script lang="ts">
import { computed, defineComponent, onMounted, ref, unref } from 'vue'
import {
queryItemAsString,
useClientService,
useRoute,
useRouter,
useMessages,
useConfigStore
} from '@ownclouders/web-pkg'
import { computed, defineComponent, ref, unref } from 'vue'
import { useClientService, useRoute, useRouter, useMessages } from '@ownclouders/web-pkg'
import { useGettext } from 'vue3-gettext'
import { onBeforeRouteUpdate, RouteLocationNormalized } from 'vue-router'
import { ProviderSchema, providerListSchema } from '../schemas'
import { OcTextInput } from '@ownclouders/design-system/components'

export default defineComponent({
emits: ['highlightNewConnections'],
setup(props, { emit }) {
const { showErrorMessage } = useMessages()
const router = useRouter()
const route = useRoute()
const clientService = useClientService()
const configStore = useConfigStore()
const { $gettext } = useGettext()

const token = ref<string>(undefined)
const provider = ref<ProviderSchema>(undefined)
const providers = ref<ProviderSchema[]>([])
const loading = ref(true)
const decodedToken = ref<string>(undefined)
const provider = ref<string>(undefined)
const providerError = ref(false)
const tokenInput = ref<InstanceType<typeof OcTextInput>>()

const helperContent = computed(() => {
return {
Expand All @@ -103,7 +70,7 @@ export default defineComponent({
})

const acceptInvitationButtonDisabled = computed(() => {
return !unref(token) || !unref(provider) || unref(provider).full_name === 'Unknown provider'
return !unref(decodedToken) || !unref(provider)
})

const errorPopup = (error: Error) => {
Expand All @@ -117,8 +84,8 @@ export default defineComponent({
const acceptInvite = async () => {
try {
await clientService.httpAuthenticated.post('/sciencemesh/accept-invite', {
token: unref(token),
providerDomain: unref(provider).domain
token: unref(decodedToken),
providerDomain: unref(provider)
})
token.value = undefined
provider.value = undefined
Expand All @@ -134,90 +101,32 @@ export default defineComponent({
errorPopup(error)
}
}
const listProviders = async () => {
try {
const { data: allProviders } = await clientService.httpAuthenticated.get(
'/sciencemesh/list-providers',
{
schema: providerListSchema
}
)
providers.value = allProviders.filter((p) => !isMyProviderSelectedProvider(p))
} catch (error) {
errorPopup(error)
} finally {
loading.value = false
}
}
const scrollToForm = () => {
const el = document.getElementById('sciencemesh-accept-invites')
if (el) {
el.scrollIntoView()
}
}
const isMyProviderSelectedProvider = (p: ProviderSchema) => {
// the protocol is not important, we just need the host and port, it's there to make it compatible with URL
const toURL = (purl: string) =>
new URL(purl.split('://').length === 1 ? `https://${purl}` : purl)
const { host: configStoreHost, port: configStorePort } = toURL(configStore.serverUrl)
const { host: providerSchemaHost, port: providerSchemaPort } = toURL(p.domain)

return [
// ensure that the config store host is not empty, minimal check
!!configStoreHost,
// ensure that the provider schema host is not empty, minimal check
!!providerSchemaHost,
// check if the host is the same
configStoreHost === providerSchemaHost,
// also check the port, multiple instances can run on the same host but not on the same port...
configStorePort === providerSchemaPort
].every((c) => c)
}

const handleParams = (to: RouteLocationNormalized) => {
const tokenQuery = to.query.token
if (tokenQuery) {
token.value = queryItemAsString(tokenQuery)
unref(tokenInput).focus()
scrollToForm()
}
const providerDomainQuery = to.query.providerDomain
if (providerDomainQuery) {
const matchedProvider = unref(providers)?.find(
(p) => p.domain === queryItemAsString(providerDomainQuery)
)
if (matchedProvider) {
provider.value = matchedProvider
providerError.value = false
} else {
provider.value = {
full_name: 'Unknown provider',
domain: queryItemAsString(providerDomainQuery)
}
providerError.value = true
const decodeInviteToken = (value: string) => {
try {
const decoded = atob(value)
if (!decoded.includes('@')) {
throw new Error()
}
const [token, serverUrl] = decoded.split('@')
provider.value = serverUrl
decodedToken.value = token
providerError.value = false
} catch (e) {
provider.value = ''
decodedToken.value = ''
providerError.value = true
}
}

const route = useRoute()
onMounted(async () => {
await listProviders()
handleParams(unref(route))
})
onBeforeRouteUpdate((to) => {
handleParams(to)
})

return {
tokenInput,
helperContent,
token,
provider,
providers,
loading,
providerError,
acceptInvitationButtonDisabled,
acceptInvite
acceptInvite,
decodeInviteToken
}
}
})
Expand Down
Loading