Skip to content

Commit

Permalink
Merge pull request #11992 from owncloud/feat/ocm-invite-token
Browse files Browse the repository at this point in the history
fix(ocm): base64 encoded invite token
  • Loading branch information
JammingBen authored Dec 4, 2024
2 parents 40b3628 + 690dec7 commit 319bdda
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 192 deletions.
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

0 comments on commit 319bdda

Please sign in to comment.