Compare commits

...

4 Commits

Author SHA1 Message Date
oguzhankoral 07109540a7 live log 2025-10-27 15:58:48 +03:00
oguzhankoral 5de27d36b2 feat: handle exceptions 2025-10-27 15:28:28 +03:00
oguzhankoral d8ed362baa feat: enable auth with registered app 2025-10-27 14:58:59 +03:00
oguzhankoral ab8ebf762e feat: auth in dui 2025-10-26 09:14:13 +03:00
5 changed files with 159 additions and 2 deletions
+20 -1
View File
@@ -13,7 +13,8 @@
@clear="showCustomServerInput = false"
/>
</div>
<FormButton full-width @click="startAccountAddFlow()">Sign In</FormButton>
<FormButton v-if="canAddAccount" full-width @click="logIn()">Log in</FormButton>
<FormButton v-else full-width @click="startAccountAddFlow()">Sign in</FormButton>
<FormButton
text
size="sm"
@@ -52,6 +53,8 @@ import { useAccountStore } from '~~/store/accounts'
import { useHostAppStore } from '~/store/hostApp'
import { ToastNotificationType } from '@speckle/ui-components'
import { useMixpanel } from '~/lib/core/composables/mixpanel'
import { useAuthManager } from '~/lib/authn/useAuthManager'
import type { BaseBridge } from '~/lib/bridge/base'
const accountStore = useAccountStore()
const hostApp = useHostAppStore()
@@ -63,6 +66,11 @@ const isAddingAccount = ref(false)
const showHelp = ref(false)
const showCustomServerInput = ref(false)
const { $accountBinding } = useNuxtApp()
const canAddAccount = ['AddAccount', 'addAccount'].some((name) =>
($accountBinding as unknown as BaseBridge).availableMethodNames.includes(name)
)
const accountCheckerIntervalFn = useIntervalFn(
async () => {
const previousAccountCount = accountStore.accounts.length
@@ -79,6 +87,17 @@ const accountCheckerIntervalFn = useIntervalFn(
{ immediate: false }
)
const { generateChallenge } = useAuthManager()
const logIn = () => {
const challenge = generateChallenge()
const serverUrl = customServerUrl.value
? new URL(customServerUrl.value).origin
: 'https://app.speckle.systems'
const authUrl = `${serverUrl}/authn/verify/sdui/${challenge}`
window.location.href = authUrl
}
const startAccountAddFlow = () => {
isAddingAccount.value = true
accountCheckerIntervalFn.resume()
+7 -1
View File
@@ -5,7 +5,7 @@
>
Welcome to Speckle
</h1>
<div v-if="isDesktopServiceAvailable">
<div v-if="isDesktopServiceAvailable || canAddAccount">
<AccountsSignInFlow />
</div>
<div v-else>
@@ -37,10 +37,16 @@
<script setup lang="ts">
import { useAccountStore } from '~~/store/accounts'
import { useDesktopService } from '~/lib/core/composables/desktopService'
import type { BaseBridge } from '~/lib/bridge/base'
const accountStore = useAccountStore()
const { pingDesktopService } = useDesktopService()
const { $accountBinding } = useNuxtApp()
const canAddAccount = ['AddAccount', 'addAccount'].some((name) =>
($accountBinding as unknown as BaseBridge).availableMethodNames.includes(name)
)
const isDesktopServiceAvailable = ref(false) // this should be false default because there is a delay if /ping is not successful.
onMounted(async () => {
+22
View File
@@ -0,0 +1,22 @@
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const CHALLENGE_KEY = 'speckle_challenge'
export function useAuthManager() {
const generateChallenge = (): string => {
let result = ''
for (let i = 0; i < 12; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
localStorage.setItem(CHALLENGE_KEY, result) // <-- persist it
return result
}
const getChallenge = (): string | null => {
return localStorage.getItem(CHALLENGE_KEY)
}
return {
getChallenge,
generateChallenge
}
}
@@ -7,6 +7,7 @@ export const IAccountBindingKey = 'accountsBinding'
export interface IAccountBinding extends IBinding<IAccountBindingEvents> {
getAccounts: () => Promise<Account[]>
addAccount: (accountId: string, account: Account) => Promise<void>
removeAccount: (accountId: string) => Promise<void>
}
@@ -15,6 +16,7 @@ export type Account = {
id: string
isDefault: boolean
token: string
refreshToken: string
serverInfo: {
name: string
url: string
@@ -54,6 +56,10 @@ export class MockedAccountBinding implements IAccountBinding {
]) as Account[]
}
public async addAccount(accountId: string, account: Account) {
return await console.log('no way dude', accountId, account)
}
public async removeAccount(accountId: string) {
return await console.log('no way dude', accountId)
}
+104
View File
@@ -0,0 +1,104 @@
<template>
<div class="flex items-center justify-center"><InfiniteLoading /></div>
</template>
<script setup lang="ts">
import { md5 } from '@speckle/shared'
import { ToastNotificationType } from '@speckle/ui-components'
import { useRoute, useRouter } from 'vue-router'
import { useAuthManager } from '~/lib/authn/useAuthManager'
import type { Account } from '~/lib/bindings/definitions/IAccountBinding'
import { useHostAppStore } from '~/store/hostApp'
const route = useRoute()
const router = useRouter()
const { getChallenge } = useAuthManager()
const { $accountBinding } = useNuxtApp()
const hostApp = useHostAppStore()
onMounted(async () => {
try {
const origin = window.location.origin
console.log(origin)
const accessCode = route.query.access_code as string | undefined
console.log(route.query)
if (accessCode) {
const challenge = getChallenge()
const body = {
appId: 'sdui',
appSecret: 'sdui',
accessCode,
challenge
}
// Exchange the access code for a real token (optional)
const response = await fetch(new URL('/auth/token', origin), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
if (!response.ok) {
const errorText = await response.text()
hostApp.setNotification({
title: 'Log In',
type: ToastNotificationType.Danger,
description: `Token exchange failed with status ${response.status}: ${errorText}`
})
// Stop processing and redirect immediately on failure
return router.replace('/')
}
const { token, refreshToken } = (await response.json()) as {
token: string
refreshToken: string
}
const graphqlQuery = {
query:
'query { activeUser { id name email company avatar } serverInfo { name company adminContact description version } }'
}
const userAndServerInfoResponse = await fetch(new URL('/graphql', origin), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}` // Add the token as a Bearer token
},
body: JSON.stringify(graphqlQuery)
})
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const userAndServerInfo = await userAndServerInfoResponse.json()
const accountId = md5(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
userAndServerInfo.data.activeUser.email + origin
).toUpperCase()
const account: Account = {
id: accountId,
token,
refreshToken,
isDefault: true,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
serverInfo: { url: origin, ...userAndServerInfo.data.serverInfo },
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
userInfo: userAndServerInfo.data.activeUser
}
await $accountBinding.addAccount(accountId, account)
} else {
throw new Error('No access code is found.')
}
} catch (error) {
hostApp.setNotification({
type: ToastNotificationType.Danger,
title: 'Failed to add your Speckle account.',
description: error as string
})
} finally {
router.replace('/')
}
})
</script>