Skip to content

Commit 1fae209

Browse files
author
Jannik Stehle
committed
feat(ocm): base64 encoded invite token
1 parent 3d0f235 commit 1fae209

File tree

3 files changed

+63
-155
lines changed

3 files changed

+63
-155
lines changed

packages/web-app-ocm/src/schemas.ts

-10
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,3 @@ export type InviteSchema = z.infer<typeof inviteSchema>
1111

1212
export const inviteListSchema = z.array(inviteSchema)
1313
export type InviteListSchema = z.infer<typeof inviteListSchema>
14-
15-
// Provider
16-
export const providerSchema = z.object({
17-
domain: z.string(),
18-
full_name: z.string()
19-
})
20-
export type ProviderSchema = z.infer<typeof providerSchema>
21-
22-
export const providerListSchema = z.array(providerSchema)
23-
export type ProviderListSchema = z.infer<typeof providerListSchema>

packages/web-app-ocm/src/views/IncomingInvitations.vue

+42-133
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,38 @@
11
<template>
22
<div id="incoming" class="sciencemesh-app">
33
<div>
4-
<div class="oc-flex oc-flex-middle oc-px-m oc-py-s">
4+
<div class="oc-flex oc-flex-middle oc-px-m oc-pt-s">
55
<oc-icon name="user-received" />
66
<h2 class="oc-px-s" v-text="$gettext('Accept invitations')" />
77
<oc-contextual-helper class="oc-pl-xs" v-bind="helperContent" />
88
</div>
9-
<div v-if="!providers.length" class="oc-flex oc-flex-center oc-flex-middle">
10-
<oc-icon name="error-warning" fill-type="line" class="oc-mr-s" size="large" />
11-
<span v-text="$gettext('The list of institutions is empty. Please contact your admin.')" />
12-
</div>
13-
<div v-else class="oc-flex oc-flex-column oc-flex-middle oc-flex-center oc-p-m">
9+
<div class="oc-flex oc-flex-column oc-flex-middle oc-flex-center oc-p-m">
1410
<div class="oc-width-1-2">
1511
<oc-text-input
16-
ref="tokenInput"
1712
v-model="token"
1813
:label="$gettext('Enter invite token')"
1914
:clear-button-enabled="true"
20-
class="oc-mb-m"
15+
class="oc-mb-s"
16+
@update:model-value="decodeInviteToken"
2117
/>
22-
<oc-select
23-
v-model="provider"
24-
:label="$gettext('Select institution of inviter')"
25-
:options="providers"
26-
class="oc-mb-m"
27-
:position-fixed="true"
28-
:loading="loading"
18+
<div
19+
:class="{
20+
'oc-text-input-danger': providerError && token,
21+
'oc-text-input-success': provider
22+
}"
2923
>
30-
<template #option="{ full_name, domain }">
31-
<div class="oc-text-break">
32-
<span class="option">
33-
<strong v-text="full_name" />
34-
</span>
35-
<span class="option" v-text="domain" />
36-
</div>
37-
</template>
38-
<template #no-options> No institutions found with this name</template>
39-
<template #selected-option="{ full_name, domain }">
40-
<div class="options-wrapper oc-text-break">
41-
<strong class="oc-mr-s oc-text-break" v-text="full_name" />
42-
<small
43-
v-oc-tooltip="domain"
44-
v-text="domain.length > 17 ? domain.slice(0, 20) + '...' : domain"
45-
/>
46-
</div>
47-
</template>
48-
</oc-select>
49-
<div v-if="providerError" class="oc-text-input-message">
50-
<span
51-
class="oc-text-input-danger"
52-
v-text="$gettext('Unknown institution. Check invitation url or select from list')"
53-
/>
24+
<span v-text="$gettext('Institution:')" />
25+
<span v-if="!token" v-text="'-'" />
26+
<span v-else-if="provider" v-text="provider" />
27+
<span v-else v-text="$gettext('invalid invite token')" />
5428
</div>
5529
</div>
56-
<oc-button size="small" :disabled="acceptInvitationButtonDisabled" @click="acceptInvite">
30+
<oc-button
31+
size="small"
32+
:disabled="acceptInvitationButtonDisabled"
33+
class="oc-mt-s"
34+
@click="acceptInvite"
35+
>
5736
<oc-icon name="add" />
5837
<span v-text="$gettext('Accept invitation')" />
5938
</oc-button>
@@ -63,35 +42,23 @@
6342
</template>
6443

6544
<script lang="ts">
66-
import { computed, defineComponent, onMounted, ref, unref } from 'vue'
67-
import {
68-
queryItemAsString,
69-
useClientService,
70-
useRoute,
71-
useRouter,
72-
useMessages,
73-
useConfigStore
74-
} from '@ownclouders/web-pkg'
45+
import { computed, defineComponent, ref, unref } from 'vue'
46+
import { useClientService, useRoute, useRouter, useMessages } from '@ownclouders/web-pkg'
7547
import { useGettext } from 'vue3-gettext'
76-
import { onBeforeRouteUpdate, RouteLocationNormalized } from 'vue-router'
77-
import { ProviderSchema, providerListSchema } from '../schemas'
78-
import { OcTextInput } from '@ownclouders/design-system/components'
7948
8049
export default defineComponent({
8150
emits: ['highlightNewConnections'],
8251
setup(props, { emit }) {
8352
const { showErrorMessage } = useMessages()
8453
const router = useRouter()
54+
const route = useRoute()
8555
const clientService = useClientService()
86-
const configStore = useConfigStore()
8756
const { $gettext } = useGettext()
8857
8958
const token = ref<string>(undefined)
90-
const provider = ref<ProviderSchema>(undefined)
91-
const providers = ref<ProviderSchema[]>([])
92-
const loading = ref(true)
59+
const decodedToken = ref<string>(undefined)
60+
const provider = ref<string>(undefined)
9361
const providerError = ref(false)
94-
const tokenInput = ref<InstanceType<typeof OcTextInput>>()
9562
9663
const helperContent = computed(() => {
9764
return {
@@ -103,7 +70,7 @@ export default defineComponent({
10370
})
10471
10572
const acceptInvitationButtonDisabled = computed(() => {
106-
return !unref(token) || !unref(provider) || unref(provider).full_name === 'Unknown provider'
73+
return !unref(decodedToken) || !unref(provider)
10774
})
10875
10976
const errorPopup = (error: Error) => {
@@ -117,8 +84,8 @@ export default defineComponent({
11784
const acceptInvite = async () => {
11885
try {
11986
await clientService.httpAuthenticated.post('/sciencemesh/accept-invite', {
120-
token: unref(token),
121-
providerDomain: unref(provider).domain
87+
token: unref(decodedToken),
88+
providerDomain: unref(provider)
12289
})
12390
token.value = undefined
12491
provider.value = undefined
@@ -134,90 +101,32 @@ export default defineComponent({
134101
errorPopup(error)
135102
}
136103
}
137-
const listProviders = async () => {
138-
try {
139-
const { data: allProviders } = await clientService.httpAuthenticated.get(
140-
'/sciencemesh/list-providers',
141-
{
142-
schema: providerListSchema
143-
}
144-
)
145-
providers.value = allProviders.filter((p) => !isMyProviderSelectedProvider(p))
146-
} catch (error) {
147-
errorPopup(error)
148-
} finally {
149-
loading.value = false
150-
}
151-
}
152-
const scrollToForm = () => {
153-
const el = document.getElementById('sciencemesh-accept-invites')
154-
if (el) {
155-
el.scrollIntoView()
156-
}
157-
}
158-
const isMyProviderSelectedProvider = (p: ProviderSchema) => {
159-
// the protocol is not important, we just need the host and port, it's there to make it compatible with URL
160-
const toURL = (purl: string) =>
161-
new URL(purl.split('://').length === 1 ? `https://${purl}` : purl)
162-
const { host: configStoreHost, port: configStorePort } = toURL(configStore.serverUrl)
163-
const { host: providerSchemaHost, port: providerSchemaPort } = toURL(p.domain)
164104
165-
return [
166-
// ensure that the config store host is not empty, minimal check
167-
!!configStoreHost,
168-
// ensure that the provider schema host is not empty, minimal check
169-
!!providerSchemaHost,
170-
// check if the host is the same
171-
configStoreHost === providerSchemaHost,
172-
// also check the port, multiple instances can run on the same host but not on the same port...
173-
configStorePort === providerSchemaPort
174-
].every((c) => c)
175-
}
176-
177-
const handleParams = (to: RouteLocationNormalized) => {
178-
const tokenQuery = to.query.token
179-
if (tokenQuery) {
180-
token.value = queryItemAsString(tokenQuery)
181-
unref(tokenInput).focus()
182-
scrollToForm()
183-
}
184-
const providerDomainQuery = to.query.providerDomain
185-
if (providerDomainQuery) {
186-
const matchedProvider = unref(providers)?.find(
187-
(p) => p.domain === queryItemAsString(providerDomainQuery)
188-
)
189-
if (matchedProvider) {
190-
provider.value = matchedProvider
191-
providerError.value = false
192-
} else {
193-
provider.value = {
194-
full_name: 'Unknown provider',
195-
domain: queryItemAsString(providerDomainQuery)
196-
}
197-
providerError.value = true
105+
const decodeInviteToken = (value: string) => {
106+
try {
107+
const decoded = atob(value)
108+
if (!decoded.includes('@')) {
109+
throw new Error()
198110
}
111+
const [token, serverUrl] = decoded.split('@')
112+
provider.value = serverUrl
113+
decodedToken.value = token
114+
providerError.value = false
115+
} catch (e) {
116+
provider.value = ''
117+
decodedToken.value = ''
118+
providerError.value = true
199119
}
200120
}
201121
202-
const route = useRoute()
203-
onMounted(async () => {
204-
await listProviders()
205-
handleParams(unref(route))
206-
})
207-
onBeforeRouteUpdate((to) => {
208-
handleParams(to)
209-
})
210-
211122
return {
212-
tokenInput,
213123
helperContent,
214124
token,
215125
provider,
216-
providers,
217-
loading,
218126
providerError,
219127
acceptInvitationButtonDisabled,
220-
acceptInvite
128+
acceptInvite,
129+
decodeInviteToken
221130
}
222131
}
223132
})

packages/web-app-ocm/src/views/OutgoingInvitations.vue

+21-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<div class="sciencemesh-app">
33
<div>
4-
<div class="oc-flex oc-flex-middle oc-px-m oc-py-s">
4+
<div class="oc-flex oc-flex-middle oc-px-m oc-pt-s">
55
<oc-icon name="user-shared" />
66
<h2 class="oc-px-s" v-text="$gettext('Invite users')"></h2>
77
<oc-contextual-helper class="oc-pl-xs" v-bind="helperContent" />
@@ -69,7 +69,9 @@
6969
<oc-table v-else :fields="fields" :data="sortedTokens" :highlighted="lastCreatedToken">
7070
<template #token="rowData">
7171
<div class="invite-code-wrapper oc-flex">
72-
<span class="oc-display-inline-block oc-text-truncate">{{ rowData.item.token }}</span>
72+
<div class="oc-text-truncate">
73+
<span class="oc-text-truncate">{{ encodeInviteToken(rowData.item.token) }}</span>
74+
</div>
7375
<oc-button
7476
id="oc-sciencemesh-copy-token"
7577
v-oc-tooltip="$gettext('Copy invite token')"
@@ -116,7 +118,8 @@ import {
116118
useClientService,
117119
useMessages,
118120
formatDateFromJSDate,
119-
formatRelativeDateFromJSDate
121+
formatRelativeDateFromJSDate,
122+
useConfigStore
120123
} from '@ownclouders/web-pkg'
121124
import { useGettext } from 'vue3-gettext'
122125
import { inviteListSchema, inviteSchema } from '../schemas'
@@ -138,6 +141,7 @@ export default defineComponent({
138141
setup() {
139142
const { showMessage, showErrorMessage } = useMessages()
140143
const clientService = useClientService()
144+
const configStore = useConfigStore()
141145
const { $gettext, current: currentLanguage } = useGettext()
142146
143147
const lastCreatedToken = ref('')
@@ -163,7 +167,7 @@ export default defineComponent({
163167
},
164168
{
165169
name: 'token',
166-
title: $gettext('Invite code'),
170+
title: $gettext('Invite token'),
167171
alignH: haveLinks ? 'right' : 'left',
168172
type: 'slot'
169173
},
@@ -192,6 +196,11 @@ export default defineComponent({
192196
}
193197
})
194198
199+
const encodeInviteToken = (token: string) => {
200+
const url = new URL(configStore.serverUrl)
201+
return btoa(`${token}@${url.host}`)
202+
}
203+
195204
const generateToken = async () => {
196205
const { description, recipient } = unref(formInput)
197206
if (recipient.length > 0 && !EmailValidator.validate(recipient)) {
@@ -239,9 +248,11 @@ export default defineComponent({
239248
'New token has been created and copied to your clipboard. Send it to the invitee(s).'
240249
)
241250
})
242-
lastCreatedToken.value = tokenInfo.token
251+
252+
const quickToken = encodeInviteToken(tokenInfo.token)
253+
lastCreatedToken.value = quickToken
243254
if (!recipient) {
244-
navigator.clipboard.writeText(tokenInfo.token)
255+
navigator.clipboard.writeText(quickToken)
245256
}
246257
}
247258
} catch (error) {
@@ -286,7 +297,7 @@ export default defineComponent({
286297
})
287298
}
288299
const copyToken = (rowData: { item: { link: string; token: string } }) => {
289-
navigator.clipboard.writeText(rowData.item.token)
300+
navigator.clipboard.writeText(encodeInviteToken(rowData.item.token))
290301
showMessage({
291302
title: $gettext('Invite token copied'),
292303
desc: $gettext('Invite token has been copied to your clipboard.')
@@ -347,7 +358,8 @@ export default defineComponent({
347358
fields,
348359
inputForFocusEmail,
349360
formatDate,
350-
formatDateRelative
361+
formatDateRelative,
362+
encodeInviteToken
351363
}
352364
}
353365
})
@@ -356,10 +368,7 @@ export default defineComponent({
356368
<style lang="scss">
357369
.sciencemesh-app {
358370
.invite-code-wrapper {
359-
max-width: 100%;
360-
@media (max-width: $oc-breakpoint-xlarge) {
361-
max-width: 200px;
362-
}
371+
width: 200px;
363372
}
364373
#invite-tokens-empty {
365374
height: 100%;

0 commit comments

Comments
 (0)