Skip to content
Open
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
67 changes: 67 additions & 0 deletions packages/web-app-ocm/src/composables/useInvitationAcceptance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ref, unref } from 'vue'
import { useClientService, useMessages, useRouter, useRoute } from '@ownclouders/web-pkg'
import { useGettext } from 'vue3-gettext'

export const useInvitationAcceptance = () => {
const { showErrorMessage } = useMessages()
const clientService = useClientService()
const router = useRouter()
const route = useRoute()
const { $gettext } = useGettext()

const isAccepting = ref(false)

const errorPopup = (error: Error) => {
console.error(error)
showErrorMessage({
title: $gettext('Error'),
desc: $gettext('An error occurred'),
errors: [error]
})
}

const validateParameters = (token: string, providerDomain: string) => {
if (!token || !providerDomain) {
const error = new Error($gettext('Missing required parameters: token and providerDomain'))
errorPopup(error)
return false
}
return true
}

const acceptInvitation = async (token: string, providerDomain: string): Promise<boolean> => {
if (!validateParameters(token, providerDomain)) {
return false
}

isAccepting.value = true

try {
await clientService.httpAuthenticated.post('/sciencemesh/accept-invite', {
token,
providerDomain
})

// Remove query params
const { token: currentToken, providerDomain: currentProvider, ...query } = unref(route).query
await router.replace({
name: 'open-cloud-mesh-invitations',
query
})

return true
} catch (error) {
errorPopup(error)
return false
} finally {
isAccepting.value = false
}
}

return {
isAccepting,
acceptInvitation,
errorPopup,
validateParameters
}
}
182 changes: 182 additions & 0 deletions packages/web-app-ocm/src/composables/useWayf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { ref } from 'vue'
import { useClientService, useMessages } from '@ownclouders/web-pkg'
import { useGettext } from 'vue3-gettext'
import {
WayfFederation,
WayfProvider,
FederationsApiResponse,
DiscoverResponse
} from '../types/wayf'

export const useWayf = () => {
const { showErrorMessage } = useMessages()
const clientService = useClientService()
const { $gettext } = useGettext()

const federations = ref<WayfFederation>({})
const isLoadingFederations = ref(false)
const isDiscovering = ref(false)

const getCurrentHostname = (): string => {
return window.location.hostname
}

const stripProtocolAndPort = (domain: string): string => {
try {
const url = new URL(domain.startsWith('http') ? domain : `https://${domain}`)
return url.hostname
} catch {
return domain.replace(/^https?:\/\//, '').split(':')[0]
}
}

const isSelfDomain = (domain: string): boolean => {
const currentHost = stripProtocolAndPort(getCurrentHostname())
const checkHost = stripProtocolAndPort(domain)
return currentHost === checkHost
}

const loadFederations = async () => {
isLoadingFederations.value = true
try {
const { data } = await clientService.httpUnAuthenticated.get<FederationsApiResponse>(
'/sciencemesh/federations'
)

const federationMap: WayfFederation = {}

data.forEach((federation) => {
const providers = federation.servers
.filter((server) => !isSelfDomain(server.url))
.map((server) => ({
name: server.displayName,
fqdn: stripProtocolAndPort(server.url),
inviteAcceptDialog: server.inviteAcceptDialog || ''
}))

if (providers.length > 0) {
federationMap[federation.federation] = providers
}
})

federations.value = federationMap
} catch (error) {
console.error('Failed to load federations:', error)
showErrorMessage({
title: $gettext('Error'),
desc: $gettext('Failed to load federations'),
errors: [error]
})
} finally {
isLoadingFederations.value = false
}
}

const discoverProvider = async (domain: string): Promise<DiscoverResponse | null> => {
isDiscovering.value = true
try {
const { data } = await clientService.httpUnAuthenticated.post<DiscoverResponse>(
'/sciencemesh/discover',
{ domain }
)
return data
} catch (error) {
console.error('Failed to discover provider:', error)
showErrorMessage({
title: $gettext('Error'),
desc: $gettext('Failed to discover provider'),
errors: [error]
})
return null
} finally {
isDiscovering.value = false
}
}

const navigateToProvider = (
provider: WayfProvider,
token: string,
providerDomain: string
): void => {
if (isSelfDomain(provider.fqdn)) {
showErrorMessage({
title: $gettext('Error'),
desc: $gettext('You cannot select your own instance')
})
return
}

let targetUrl: string
const inviteDialogPath = provider.inviteAcceptDialog || '/open-cloud-mesh/accept-invite'

if (inviteDialogPath.startsWith('http://') || inviteDialogPath.startsWith('https://')) {
// Absolute URL
targetUrl = inviteDialogPath
} else {
// Relative path
const cleanPath = inviteDialogPath.startsWith('/') ? inviteDialogPath : `/${inviteDialogPath}`
targetUrl = `https://${provider.fqdn}${cleanPath}`
}

const url = new URL(targetUrl)
url.searchParams.set('token', token)
url.searchParams.set('providerDomain', providerDomain)

window.location.href = url.toString()
}

const navigateToManualProvider = async (
input: string,
token: string,
providerDomain: string
): Promise<void> => {
const domain = stripProtocolAndPort(input)

if (isSelfDomain(domain)) {
showErrorMessage({
title: $gettext('Error'),
desc: $gettext('You cannot select your own instance')
})
return
}

const discoveryResult = await discoverProvider(domain)
if (!discoveryResult) {
return
}

const provider: WayfProvider = {
name: domain,
fqdn: domain,
inviteAcceptDialog: discoveryResult.inviteAcceptDialog || ''
}

navigateToProvider(provider, token, providerDomain)
}

const filterProviders = (providers: WayfProvider[], query: string): WayfProvider[] => {
if (!query) {
return providers
}

const lowerQuery = query.toLowerCase()
return providers.filter(
(provider) =>
provider.name.toLowerCase().includes(lowerQuery) ||
provider.fqdn.toLowerCase().includes(lowerQuery)
)
}

return {
federations,
isLoadingFederations,
isDiscovering,
loadFederations,
discoverProvider,
navigateToProvider,
navigateToManualProvider,
isSelfDomain,
filterProviders,
getCurrentHostname
}
}
20 changes: 20 additions & 0 deletions packages/web-app-ocm/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import App from './views/App.vue'
import Wayf from './views/Wayf.vue'
import { ApplicationInformation, defineWebApplication, useRouter } from '@ownclouders/web-pkg'
import translations from '../l10n/translations.json'
import { extensions } from './extensions'
Expand All @@ -20,6 +21,25 @@ const routes: RouteRecordRaw[] = [
patchCleanPath: true,
title: 'Invitations'
}
},
{
path: '/wayf',
name: 'open-cloud-mesh-wayf',
component: Wayf,
meta: {
patchCleanPath: true,
title: 'Where Are You From',
authContext: 'anonymous' // No authentication required
}
},
{
path: '/accept-invite',
name: 'open-cloud-mesh-accept-invite',
component: App,
meta: {
patchCleanPath: true,
title: 'Accept Invitation'
}
}
]

Expand Down
34 changes: 34 additions & 0 deletions packages/web-app-ocm/src/types/wayf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Frontend types
export interface WayfProvider {
name: string
fqdn: string
inviteAcceptDialog: string // Can be empty, relative path, or absolute URL
}

export interface WayfFederation {
[federationName: string]: WayfProvider[]
}

// Backend API response types
export interface FederationServerResponse {
displayName: string
url: string
inviteAcceptDialog: string
}

export interface FederationResponse {
federation: string
servers: FederationServerResponse[]
}

export type FederationsApiResponse = FederationResponse[]

export interface DiscoverRequest {
domain: string
}

export interface DiscoverResponse {
inviteAcceptDialog: string
provider?: string
apiVersion?: string
}
Loading