Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aa724dbd0b | |||
| c37235381f | |||
| 328b58b49b | |||
| c41ca5ffbe | |||
| 16593dfc34 | |||
| 8e2f507286 | |||
| 837f34ed50 | |||
| 9d3a623fe6 | |||
| 8fc81b0b4e | |||
| 6f2f599b1b | |||
| a69de13f16 |
@@ -0,0 +1,41 @@
|
||||
# Irrelevant source files
|
||||
deployment/
|
||||
|
||||
# Build output and other temporary files
|
||||
.husky/_/
|
||||
.netlify/
|
||||
.nuxt/
|
||||
dist/
|
||||
node_modules/
|
||||
|
||||
# Version control
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# GitHub / CI metadata
|
||||
.github/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
*.env
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# IDE / editor settings
|
||||
.vscode/
|
||||
.idea/
|
||||
.zed/
|
||||
*.iml
|
||||
|
||||
# OS / editor junk
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# AI
|
||||
.claude/
|
||||
.cursor/
|
||||
|
||||
# testing
|
||||
tests/
|
||||
@@ -0,0 +1,48 @@
|
||||
name: Build Docker Container
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
PUBLISH:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
IMAGE_VERSION_TAG:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions: {} # purposefully empty by default at workflow level, explicitly overridden for specific jobs below
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-build-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
docker-build:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
name: Build Docker image
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # to be able to push images to ghcr.io. As permissions is static, it has to be granted even if PUBLISH is false
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Login to Helm Chart & Container Image Registry
|
||||
if: ${{ inputs.PUBLISH == true }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
- name: Setup Docker Builder
|
||||
uses: useblacksmith/setup-docker-builder@affa10db466676f3dfb3e54caeb228ee0691510f
|
||||
- name: Build and push
|
||||
uses: useblacksmith/build-push-action@30c71162f16ea2c27c3e21523255d209b8b538c1
|
||||
with:
|
||||
push: ${{ inputs.PUBLISH }}
|
||||
tags: ghcr.io/specklesystems/speckle-dui:${{ inputs.IMAGE_VERSION_TAG }}
|
||||
file: ./deployment/docker/Dockerfile
|
||||
network: host # to be able to connect to Tailscale and pull private base image during build
|
||||
allow: network.host # to be able to connect to Tailscale and pull private base image during build
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Get Version
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
IMAGE_VERSION_TAG:
|
||||
description: 'The image version tag under which the Helm chart and docker image should be published'
|
||||
value: ${{ jobs.get-version.outputs.VERSION }}
|
||||
|
||||
permissions: {} # purposefully empty by default at workflow level, explicitly overridden for specific jobs below
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-get-version-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
get-version:
|
||||
outputs:
|
||||
VERSION: ${{ steps.get-version.outputs.VERSION }}
|
||||
name: Get Version
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
sparse-checkout: ''
|
||||
fetch-depth: 1
|
||||
fetch-tags: 1
|
||||
persist-credentials: true # zizmor: ignore[artipacked] need to fetch tags in the next step and this ensures that git is configured & authenticated
|
||||
- run: git fetch origin 'refs/tags/*:refs/tags/*'
|
||||
- name: Get version tag
|
||||
id: get-version
|
||||
run: |
|
||||
VERSION=""
|
||||
if [[ "${GITHUB_REF_NAME}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
VERSION="${GITHUB_REF_NAME}"
|
||||
echo "VERSION=${VERSION}" >> ${GITHUB_OUTPUT}
|
||||
echo "${VERSION} is a valid semver, we shall use it. Exiting"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
LAST_RELEASE="$(git describe --always --tags $(git rev-list --tags --max-count=1) | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || true)" # get the last release tag. FIXME: Fails if a commit is tagged with more than one tag: https://stackoverflow.com/questions/8089002/git-describe-with-two-tags-on-the-same-commit/56039163#56039163
|
||||
LAST_RELEASE="${LAST_RELEASE:-0.0.0}"
|
||||
NEXT_RELEASE="$(echo "${LAST_RELEASE}" | awk -F. -v OFS=. '{$NF += 1 ; print}')"
|
||||
|
||||
if [[ "${GITHUB_REF_NAME}" == "main" ]]; then
|
||||
VERSION="${NEXT_RELEASE}-alpha.${GITHUB_RUN_NUMBER}"
|
||||
echo "VERSION=${VERSION}" >> ${GITHUB_OUTPUT}
|
||||
echo "${VERSION} will be an alpha version. Exiting"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
BRANCH_NAME_TRUNCATED="$(echo "${GITHUB_REF_NAME}" | cut -c -28 | sed 's/[^a-zA-Z0-9.-]/-/g')" # docker has a 128 character tag limit, so ensuring the branch name will be short enough
|
||||
|
||||
PADDED_RUN_NUMBER="$(printf "%06d" "${GITHUB_RUN_NUMBER}")"
|
||||
COMMIT_SHA1_TRUNCATED="$(echo "${GITHUB_SHA}" | cut -c -7)"
|
||||
|
||||
VERSION="${NEXT_RELEASE}-branch.${BRANCH_NAME_TRUNCATED}.${PADDED_RUN_NUMBER}-${COMMIT_SHA1_TRUNCATED}"
|
||||
echo "VERSION=${VERSION}" >> ${GITHUB_OUTPUT}
|
||||
echo "${VERSION} will be a branch build version. Exiting"
|
||||
exit 0
|
||||
@@ -0,0 +1,35 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
workflow_call: {}
|
||||
|
||||
permissions: {} # purposefully empty by default at workflow level, explicitly overridden for specific jobs below
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-lint-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f
|
||||
with:
|
||||
node-version: '22.14.0'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run Linter
|
||||
run: yarn lint
|
||||
+30
-35
@@ -1,44 +1,39 @@
|
||||
name: Linting
|
||||
name: Pull Request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true # other running workflows get cancelled on the same branch
|
||||
|
||||
permissions: {} # purposefully empty by default at workflow level, explicitly overridden for specific jobs below
|
||||
|
||||
jobs:
|
||||
lint-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
get-version:
|
||||
uses: ./.github/workflows/get-version.yml
|
||||
with: {}
|
||||
secrets: {}
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
lint:
|
||||
uses: ./.github/workflows/lint.yml
|
||||
with: {}
|
||||
secrets: {}
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.14.0'
|
||||
|
||||
- name: Enable Corepack and Install Correct Yarn Version
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare yarn@$(jq -r .packageManager package.json | cut -d'@' -f2) --activate
|
||||
yarn --version
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
.yarn/cache
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install Dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run Linter
|
||||
run: yarn lint
|
||||
|
||||
- name: Run generate
|
||||
run: yarn generate
|
||||
build:
|
||||
needs:
|
||||
- get-version
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
PUBLISH: false
|
||||
IMAGE_VERSION_TAG: ${{ needs.get-version.outputs.IMAGE_VERSION_TAG }}
|
||||
secrets: {}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # to be able to push images to ghcr.io, even if PUBLISH is false, as permissions is static at workflow level
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true # other running workflows get cancelled on the same branch
|
||||
|
||||
permissions: {} # purposefully empty by default at workflow level, explicitly overridden for specific jobs below
|
||||
|
||||
jobs:
|
||||
get-version:
|
||||
uses: ./.github/workflows/get-version.yml
|
||||
with: {}
|
||||
secrets: {}
|
||||
permissions:
|
||||
contents: read
|
||||
lint:
|
||||
uses: ./.github/workflows/lint.yml
|
||||
with: {}
|
||||
secrets: {}
|
||||
permissions:
|
||||
contents: read
|
||||
build:
|
||||
uses: ./.github/workflows/build.yml
|
||||
needs:
|
||||
- get-version
|
||||
- lint
|
||||
with:
|
||||
PUBLISH: true
|
||||
IMAGE_VERSION_TAG: ${{ needs.get-version.outputs.IMAGE_VERSION_TAG }}
|
||||
secrets: {}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # to be able to push images to ghcr.io
|
||||
+3
-1
@@ -15,4 +15,6 @@ dist
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
!.yarn/versions
|
||||
|
||||
.claude
|
||||
|
||||
+5
-1
@@ -32,4 +32,8 @@ venv
|
||||
|
||||
storybook-static
|
||||
.tshy
|
||||
.tshy-build
|
||||
.tshy-build
|
||||
|
||||
# Helm
|
||||
deployment/helm
|
||||
tests/deployment
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
dist/
|
||||
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div v-if="!hidden" class="flex flex-col space-y-2">
|
||||
<!-- idle: server URL + sign in button -->
|
||||
<template v-if="state === 'idle'">
|
||||
<div class="flex space-x-2">
|
||||
<FormButton
|
||||
v-if="canAddAccount"
|
||||
full-width
|
||||
color="outline"
|
||||
@click="openBrowserAuth()"
|
||||
>
|
||||
Log in with OAuth token
|
||||
</FormButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- waiting: instructions + code input -->
|
||||
<template v-if="state === 'waiting' || state === 'submitting'">
|
||||
<div class="text-foreground-2 space-y-2 border rounded-lg p-2">
|
||||
<div class="text-sm text-center">
|
||||
Check your browser: authorize the app, then copy the exchange code and paste
|
||||
it below.
|
||||
</div>
|
||||
<div class="py-2"><CommonLoadingBar :loading="state === 'waiting'" /></div>
|
||||
<FormTextInput
|
||||
v-model="exchangeCode"
|
||||
name="exchangeCode"
|
||||
:show-label="false"
|
||||
placeholder="Paste exchange code here"
|
||||
color="foundation"
|
||||
autocomplete="off"
|
||||
:disabled="state === 'submitting'"
|
||||
/>
|
||||
<FormButton
|
||||
full-width
|
||||
:disabled="!exchangeCode?.trim() || state === 'submitting'"
|
||||
@click="submitCode()"
|
||||
>
|
||||
{{ state === 'submitting' ? 'Signing in...' : 'Submit' }}
|
||||
</FormButton>
|
||||
|
||||
<div v-if="showHelp" class="p-2 rounded-md space-y-1">
|
||||
<div class="text-sm text-center">Having trouble?</div>
|
||||
<div class="flex justify-center">
|
||||
<span>
|
||||
<FormButton size="sm" text @click="retryFlow()">Retry</FormButton>
|
||||
or
|
||||
<FormButton text size="sm" @click="$openUrl('https://speckle.community')">
|
||||
Get in touch with us
|
||||
</FormButton>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- error -->
|
||||
<template v-if="state === 'error'">
|
||||
<div class="text-foreground-2 space-y-2">
|
||||
<div class="text-sm text-center text-red-500">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
<FormButton full-width @click="retryFlow()">Try again</FormButton>
|
||||
<FormButton text size="sm" full-width @click="emit('backToSignIn')">
|
||||
Back
|
||||
</FormButton>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useAuthManager } from '~/lib/authn/useAuthManager'
|
||||
import { useTokenExchange, supportsOAuthToken } from '~/lib/authn/useTokenExchange'
|
||||
import { useMixpanel } from '~/lib/core/composables/mixpanel'
|
||||
import { useAccountStore } from '~/store/accounts'
|
||||
import type { BaseBridge } from '~/lib/bridge/base'
|
||||
|
||||
const props = defineProps<{
|
||||
serverUrl: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'backToSignIn'): void
|
||||
}>()
|
||||
|
||||
const app = useNuxtApp()
|
||||
const { generateLocalChallenge } = useAuthManager()
|
||||
const { exchangeAccessCode } = useTokenExchange()
|
||||
const { trackEvent } = useMixpanel()
|
||||
const accountStore = useAccountStore()
|
||||
|
||||
const { $accountBinding } = useNuxtApp()
|
||||
const canAddAccount = ['AddAccount', 'addAccount'].some((name) =>
|
||||
($accountBinding as unknown as BaseBridge).availableMethodNames.includes(name)
|
||||
)
|
||||
|
||||
const state = ref<'idle' | 'waiting' | 'submitting' | 'error'>('idle')
|
||||
const exchangeCode = ref<string | undefined>()
|
||||
const errorMessage = ref('')
|
||||
const showHelp = ref(false)
|
||||
const hidden = ref(false)
|
||||
|
||||
const checkServerSupport = async (url: string) => {
|
||||
const serverUrl = url ? new URL(url).origin : 'https://app.speckle.systems'
|
||||
hidden.value = !(await supportsOAuthToken(serverUrl))
|
||||
}
|
||||
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
onMounted(() => checkServerSupport(props.serverUrl))
|
||||
watch(
|
||||
() => props.serverUrl,
|
||||
(url) => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => checkServerSupport(url), 500)
|
||||
}
|
||||
)
|
||||
|
||||
let currentCodeVerifier = ''
|
||||
let currentCodeChallenge = ''
|
||||
let currentServerUrl = ''
|
||||
|
||||
const openBrowserAuth = async () => {
|
||||
currentServerUrl = props.serverUrl
|
||||
? new URL(props.serverUrl).origin
|
||||
: 'https://app.speckle.systems'
|
||||
|
||||
const { codeVerifier, codeChallenge } = await generateLocalChallenge()
|
||||
currentCodeVerifier = codeVerifier
|
||||
currentCodeChallenge = codeChallenge
|
||||
const authUrl = `${currentServerUrl}/authn/verify/sdui/${codeChallenge}?returnExchangeToken=true&code_challenge_method=S256`
|
||||
app.$openUrl(authUrl)
|
||||
|
||||
state.value = 'waiting'
|
||||
exchangeCode.value = undefined
|
||||
showHelp.value = false
|
||||
|
||||
setTimeout(() => {
|
||||
if (state.value === 'waiting') {
|
||||
showHelp.value = true
|
||||
}
|
||||
}, 10_000)
|
||||
}
|
||||
|
||||
const submitCode = async () => {
|
||||
const code = exchangeCode.value?.trim()
|
||||
if (!code || !currentCodeChallenge || !currentServerUrl) return
|
||||
|
||||
state.value = 'submitting'
|
||||
try {
|
||||
await exchangeAccessCode(
|
||||
currentServerUrl,
|
||||
code,
|
||||
currentCodeChallenge,
|
||||
currentCodeVerifier
|
||||
)
|
||||
void trackEvent('DUI Account Added')
|
||||
// Refresh accounts so the watcher in Menu.vue detects the new account and closes the dialog
|
||||
await accountStore.refreshAccounts()
|
||||
} catch (error) {
|
||||
errorMessage.value =
|
||||
error instanceof Error ? error.message : 'Failed to sign in. Please try again.'
|
||||
state.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
const retryFlow = () => {
|
||||
state.value = 'idle'
|
||||
exchangeCode.value = undefined
|
||||
errorMessage.value = ''
|
||||
showHelp.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -2,56 +2,30 @@
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div v-if="isDesktopServiceAvailable">
|
||||
<div v-show="!isAddingAccount" class="text-foreground-2 space-y-2">
|
||||
<FormButton
|
||||
text
|
||||
size="sm"
|
||||
full-width
|
||||
@click="showCustomServerInput = !showCustomServerInput"
|
||||
>
|
||||
{{ showCustomServerInput ? 'Use default server' : 'Set custom server url' }}
|
||||
</FormButton>
|
||||
<div v-if="showCustomServerInput">
|
||||
<FormTextInput
|
||||
v-model="customServerUrl"
|
||||
name="name"
|
||||
:show-label="false"
|
||||
color="foundation"
|
||||
autocomplete="off"
|
||||
show-clear
|
||||
@clear="showCustomServerInput = false"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<FormButton
|
||||
color="outline"
|
||||
class="px-1"
|
||||
:icon-left="ArrowLeftIcon"
|
||||
hide-text
|
||||
@click="emit('backToSignIn')"
|
||||
/>
|
||||
|
||||
<FormButton full-width @click="startAccountAddFlow()">
|
||||
Sign in (Legacy)
|
||||
<FormButton full-width color="outline" @click="startAccountAddFlow()">
|
||||
Log in (Legacy)
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="isAddingAccount" class="text-foreground-2 mt-2 mb-4 space-y-2">
|
||||
<div
|
||||
v-show="isAddingAccount"
|
||||
class="text-foreground-2 mt-2 mb-4 space-y-2 border rounded-lg p-2"
|
||||
>
|
||||
<div class="text-sm text-center">
|
||||
Please check your browser: waiting for authorization to complete.
|
||||
</div>
|
||||
<div class="py-2"><CommonLoadingBar :loading="isAddingAccount" /></div>
|
||||
<div v-if="showHelp" class="bg-blue-500/10 p-2 rounded-md space-y-2">
|
||||
<div v-if="showHelp" class="p-2 rounded-md space-y-1">
|
||||
<div class="text-sm text-center">Having trouble?</div>
|
||||
<FormButton size="sm" full-width @click="restartFlow()">Retry</FormButton>
|
||||
<FormButton
|
||||
text
|
||||
size="sm"
|
||||
full-width
|
||||
@click="$openUrl('https://speckle.community')"
|
||||
>
|
||||
Get in touch with us
|
||||
</FormButton>
|
||||
<div class="flex justify-center">
|
||||
<span>
|
||||
<FormButton text size="sm" @click="$openUrl('https://speckle.community')">
|
||||
Get in touch with us
|
||||
</FormButton>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,6 +65,10 @@ const hostApp = useHostAppStore()
|
||||
const app = useNuxtApp()
|
||||
const { trackEvent } = useMixpanel()
|
||||
|
||||
const props = defineProps<{
|
||||
serverUrl: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'backToSignIn'): void
|
||||
}>()
|
||||
@@ -98,7 +76,6 @@ const emit = defineEmits<{
|
||||
const showCustomServerInput = ref(false)
|
||||
const isAddingAccount = ref(false)
|
||||
const isDesktopServiceAvailable = ref(false) // this should be false default because there is a delay if /ping is not successful.
|
||||
const customServerUrl = ref<string | undefined>('https://app.speckle.systems')
|
||||
const showHelp = ref(false)
|
||||
|
||||
const accountCheckerIntervalFn = useIntervalFn(
|
||||
@@ -123,9 +100,9 @@ const startAccountAddFlow = () => {
|
||||
setTimeout(() => {
|
||||
showHelp.value = true
|
||||
}, 10_000)
|
||||
const url = customServerUrl.value
|
||||
const url = props.serverUrl
|
||||
? `http://localhost:29364/auth/add-account?serverUrl=${
|
||||
new URL(customServerUrl.value).origin
|
||||
new URL(props.serverUrl).origin
|
||||
}`
|
||||
: `http://localhost:29364/auth/add-account`
|
||||
|
||||
@@ -149,11 +126,6 @@ const startAccountAddFlow = () => {
|
||||
}, 30_000)
|
||||
}
|
||||
|
||||
const restartFlow = () => {
|
||||
isAddingAccount.value = false
|
||||
showHelp.value = false
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
isDesktopServiceAvailable.value = await pingDesktopService()
|
||||
})
|
||||
|
||||
@@ -39,20 +39,24 @@
|
||||
title="Add a new account"
|
||||
fullscreen="none"
|
||||
>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<AccountsSignInFlow v-if="!showLegacy" />
|
||||
<AccountsLegacySignInFlow v-else @back-to-sign-in="showLegacy = false" />
|
||||
|
||||
<FormButton
|
||||
v-if="!showLegacy"
|
||||
text
|
||||
full-width
|
||||
size="sm"
|
||||
class="text-xs"
|
||||
@click="showLegacy = true"
|
||||
>
|
||||
Legacy Sign in
|
||||
</FormButton>
|
||||
<div class="flex flex-col space-y-4 p-2">
|
||||
<FormTextInput
|
||||
v-model="customServerUrl"
|
||||
name="Server to sign in"
|
||||
show-label
|
||||
placeholder="https://app.speckle.systems"
|
||||
color="foundation"
|
||||
autocomplete="off"
|
||||
show-clear
|
||||
/>
|
||||
<div class="space-y-2">
|
||||
<AccountsSignInFlow :server-url="customServerUrl" />
|
||||
<AccountsExchangeTokenSignInFlow :server-url="customServerUrl" />
|
||||
<AccountsLegacySignInFlow
|
||||
v-if="!canStartAuthAccount"
|
||||
:server-url="customServerUrl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CommonDialog>
|
||||
</div>
|
||||
@@ -68,10 +72,18 @@ import type { DUIAccount } from '~/store/accounts'
|
||||
import { useAccountStore } from '~/store/accounts'
|
||||
import { useMixpanel } from '~/lib/core/composables/mixpanel'
|
||||
import { useDesktopService } from '~/lib/core/composables/desktopService'
|
||||
import type { BaseBridge } from '~/lib/bridge/base'
|
||||
|
||||
const { trackEvent } = useMixpanel()
|
||||
const app = useNuxtApp()
|
||||
const { pingDesktopService } = useDesktopService()
|
||||
const { $accountBinding } = useNuxtApp()
|
||||
const canStartAuthAccount = ['AuthenticateAccount', 'authenticateAccount'].some(
|
||||
(name) =>
|
||||
($accountBinding as unknown as BaseBridge).availableMethodNames.includes(name)
|
||||
)
|
||||
|
||||
const customServerUrl = ref<string>('https://app.speckle.systems')
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -88,7 +100,7 @@ defineEmits<{
|
||||
}>()
|
||||
|
||||
const showAddNewAccount = ref(false)
|
||||
const showLegacy = ref(false)
|
||||
const signInMode = ref<'default' | 'exchange' | 'legacy'>('default')
|
||||
|
||||
const showAccountsDialog = defineModel<boolean>('open', {
|
||||
required: false,
|
||||
@@ -110,8 +122,8 @@ watch(showAccountsDialog, (newVal) => {
|
||||
|
||||
watch(showAddNewAccount, (newVal) => {
|
||||
if (newVal) {
|
||||
// reset the current/legacy state on every add account sub-dialog
|
||||
showLegacy.value = false
|
||||
// reset the sign-in mode on every add account sub-dialog
|
||||
signInMode.value = 'default'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,51 +1,53 @@
|
||||
<template>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<FormButton
|
||||
text
|
||||
size="sm"
|
||||
full-width
|
||||
@click="showCustomServerInput = !showCustomServerInput"
|
||||
>
|
||||
{{ showCustomServerInput ? 'Use default server' : 'Set custom server url' }}
|
||||
</FormButton>
|
||||
<div v-if="showCustomServerInput">
|
||||
<FormTextInput
|
||||
v-model="customServerUrl"
|
||||
name="name"
|
||||
:show-label="false"
|
||||
placeholder="https://app.speckle.systems"
|
||||
color="foundation"
|
||||
autocomplete="off"
|
||||
show-clear
|
||||
@clear="showCustomServerInput = false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormButton v-if="canAddAccount" full-width @click="logIn()">Sign in</FormButton>
|
||||
<FormButton v-if="canAddAccount" full-width @click="logIn()">Log in</FormButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useAuthManager } from '~/lib/authn/useAuthManager'
|
||||
import type { BaseBridge } from '~/lib/bridge/base'
|
||||
import { useAccountStore } from '~/store/accounts'
|
||||
import { useHostAppStore } from '~/store/hostApp'
|
||||
import { ToastNotificationType } from '@speckle/ui-components'
|
||||
|
||||
const customServerUrl = ref<string | undefined>('https://app.speckle.systems')
|
||||
const showCustomServerInput = ref(false)
|
||||
const props = defineProps<{
|
||||
serverUrl: string
|
||||
}>()
|
||||
|
||||
const accountStore = useAccountStore()
|
||||
const hostAppStore = useHostAppStore()
|
||||
const { $accountBinding } = useNuxtApp()
|
||||
const canAddAccount = ['AddAccount', 'addAccount'].some((name) =>
|
||||
($accountBinding as unknown as BaseBridge).availableMethodNames.includes(name)
|
||||
)
|
||||
const canStartAuthAccount = ['AuthenticateAccount', 'authenticateAccount'].some(
|
||||
(name) =>
|
||||
($accountBinding as unknown as BaseBridge).availableMethodNames.includes(name)
|
||||
)
|
||||
|
||||
const { generateChallenge } = useAuthManager()
|
||||
|
||||
const logIn = () => {
|
||||
const serverUrl = customServerUrl.value
|
||||
? new URL(customServerUrl.value).origin
|
||||
const logIn = async () => {
|
||||
const serverUrl = props.serverUrl
|
||||
? new URL(props.serverUrl).origin
|
||||
: 'https://app.speckle.systems'
|
||||
const challenge = generateChallenge(serverUrl)
|
||||
const authUrl = `${serverUrl}/authn/verify/sdui/${challenge}`
|
||||
window.location.href = authUrl
|
||||
if (canStartAuthAccount) {
|
||||
const acc = await $accountBinding.authenticateAccount(serverUrl)
|
||||
if (acc.token) {
|
||||
await accountStore.refreshAccounts()
|
||||
} else {
|
||||
hostAppStore.setNotification({
|
||||
title: 'Log In',
|
||||
type: ToastNotificationType.Info,
|
||||
description:
|
||||
"Log in could not completed. Make sure you have logged in successfully, otherwise try 'Log in with OAuth token'"
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const { codeChallenge } = await generateChallenge(serverUrl)
|
||||
const authUrl = `${serverUrl}/authn/verify/sdui/${codeChallenge}?code_challenge_method=S256`
|
||||
window.location.href = authUrl
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="projectIsAccesible && !projectIsAccesible"
|
||||
v-if="projectIsAccesible === false"
|
||||
class="px-2 py-4 bg-foundation dark:bg-neutral-700/10 rounded-md shadow"
|
||||
>
|
||||
<CommonAlert
|
||||
@@ -145,10 +145,25 @@ const projectNavigatorTippy = computed(() =>
|
||||
|
||||
const clientId = projectAccount.value.accountInfo.id
|
||||
|
||||
const { result: projectDetailsResult, refetch: refetchProjectDetails } = useQuery(
|
||||
const accountExists = accountStore.isAccountExistsById(props.project.accountId)
|
||||
|
||||
if (!accountExists) {
|
||||
projectIsAccesible.value = false
|
||||
}
|
||||
|
||||
const {
|
||||
result: projectDetailsResult,
|
||||
refetch: refetchProjectDetails,
|
||||
onError: onProjectDetailsError
|
||||
} = useQuery(
|
||||
projectDetailsQuery,
|
||||
() => ({ projectId: props.project.projectId }),
|
||||
() => ({ clientId, debounce: 500, fetchPolicy: 'network-only' })
|
||||
() => ({
|
||||
clientId,
|
||||
debounce: 500,
|
||||
fetchPolicy: 'network-only',
|
||||
enabled: accountExists
|
||||
})
|
||||
)
|
||||
|
||||
const removeProjectModels = async () => {
|
||||
@@ -162,6 +177,10 @@ watch(projectDetails, (newValue) => {
|
||||
projectIsAccesible.value = newValue !== undefined
|
||||
})
|
||||
|
||||
onProjectDetailsError(() => {
|
||||
projectIsAccesible.value = false
|
||||
})
|
||||
|
||||
const canLoad = computed(() => !!projectDetails.value?.permissions.canLoad.authorized)
|
||||
const canPublish = computed(
|
||||
() => !!projectDetails.value?.permissions.canPublish.authorized
|
||||
@@ -194,13 +213,13 @@ const isWorkspaceReadOnly = computed(() => {
|
||||
const { onResult: userProjectsUpdated } = useSubscription(
|
||||
userProjectsUpdatedSubscription,
|
||||
() => ({}),
|
||||
() => ({ clientId })
|
||||
() => ({ clientId, enabled: accountExists })
|
||||
)
|
||||
|
||||
const { onResult: projectUpdated } = useSubscription(
|
||||
projectUpdatedSubscription,
|
||||
() => ({ projectId: props.project.projectId }),
|
||||
() => ({ clientId })
|
||||
() => ({ clientId, enabled: accountExists })
|
||||
)
|
||||
|
||||
// to catch changes on visibility of project
|
||||
@@ -236,7 +255,7 @@ const workspaceUrl = computed(() => {
|
||||
const { onResult } = useSubscription(
|
||||
versionCreatedSubscription,
|
||||
() => ({ projectId: props.project.projectId }),
|
||||
() => ({ clientId })
|
||||
() => ({ clientId, enabled: accountExists })
|
||||
)
|
||||
|
||||
onResult((res) => {
|
||||
|
||||
@@ -36,6 +36,23 @@
|
||||
{{ isDarkTheme ? 'Light theme' : 'Dark theme' }}
|
||||
</div>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
v-if="isDisableCacheSupported"
|
||||
v-slot="{ active }"
|
||||
@click="toggleCache"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
active ? 'bg-highlight-1' : '',
|
||||
'my-1 text-body-2xs flex justify-between px-2 py-1 text-foreground cursor-pointer transition mx-1 rounded'
|
||||
]"
|
||||
>
|
||||
<span>Disable Cache</span>
|
||||
<span v-if="isCacheDisabled" class="text-primary font-bold ml-2">✓</span>
|
||||
</div>
|
||||
</MenuItem>
|
||||
|
||||
<div class="border-t border-outline-3 mt-1">
|
||||
<MenuItem v-if="app.$revitMapperBinding" v-slot="{ active }">
|
||||
<button
|
||||
@@ -120,12 +137,41 @@ import { storeToRefs } from 'pinia'
|
||||
import { XMarkIcon, Bars3Icon } from '@heroicons/vue/20/solid'
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
|
||||
import { useConfigStore } from '~/store/config'
|
||||
import { useHostAppStore } from '~/store/hostApp'
|
||||
|
||||
const app = useNuxtApp()
|
||||
|
||||
const uiConfigStore = useConfigStore()
|
||||
const { isDarkTheme, hasConfigBindings, isDevMode } = storeToRefs(uiConfigStore)
|
||||
const { toggleTheme } = uiConfigStore
|
||||
const { isDarkTheme, hasConfigBindings, isDevMode, isCacheDisabled } =
|
||||
storeToRefs(uiConfigStore)
|
||||
const { toggleTheme, toggleCache } = uiConfigStore
|
||||
|
||||
const hostAppStore = useHostAppStore()
|
||||
const { hostAppName, connectorVersion } = storeToRefs(hostAppStore)
|
||||
|
||||
const isDisableCacheSupported = computed(() => {
|
||||
const appName = hostAppName.value
|
||||
const version = connectorVersion.value
|
||||
|
||||
if (!appName || !version) return false
|
||||
|
||||
// excludes non-sharp connectors (assuming they don't have backend cache bypass)
|
||||
const nonSharpApps = ['sketchup', 'archicad', 'vectorworks']
|
||||
if (nonSharpApps.includes(appName.toLowerCase())) return false
|
||||
|
||||
// always show in dev environments
|
||||
if (version.includes('dev') || version.includes('local') || version.includes('1.0.0'))
|
||||
return true
|
||||
|
||||
// for sharp connectors, check if version is >= 3.18.0
|
||||
const targetVersion = '3.19.0'
|
||||
return (
|
||||
version.localeCompare(targetVersion, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base'
|
||||
}) >= 0
|
||||
)
|
||||
})
|
||||
|
||||
const { $showDevTools, $openUrl } = useNuxtApp()
|
||||
const showAccountsDialog = ref(false)
|
||||
|
||||
@@ -6,18 +6,22 @@
|
||||
Welcome to Speckle
|
||||
</h1>
|
||||
<div v-if="isDesktopServiceAvailable || canAddAccount">
|
||||
<AccountsSignInFlow v-if="!showLegacy" />
|
||||
<AccountsLegacySignInFlow v-else @back-to-sign-in="showLegacy = false" />
|
||||
<FormButton
|
||||
v-if="!showLegacy"
|
||||
text
|
||||
full-width
|
||||
size="sm"
|
||||
class="text-xs"
|
||||
@click="showLegacy = true"
|
||||
>
|
||||
Legacy Sign in
|
||||
</FormButton>
|
||||
<div class="flex flex-col space-y-4 p-2">
|
||||
<FormTextInput
|
||||
v-model="customServerUrl"
|
||||
name="Server to sign in"
|
||||
:show-label="false"
|
||||
placeholder="https://app.speckle.systems"
|
||||
color="foundation"
|
||||
autocomplete="off"
|
||||
show-clear
|
||||
/>
|
||||
<div class="space-y-2">
|
||||
<AccountsSignInFlow :server-url="customServerUrl" />
|
||||
<AccountsExchangeTokenSignInFlow :server-url="customServerUrl" />
|
||||
<AccountsLegacySignInFlow :server-url="customServerUrl" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="text-foreground-2 mt-2 mb-4">
|
||||
@@ -53,13 +57,13 @@ import type { BaseBridge } from '~/lib/bridge/base'
|
||||
const accountStore = useAccountStore()
|
||||
const { pingDesktopService } = useDesktopService()
|
||||
|
||||
const customServerUrl = ref<string>('https://app.speckle.systems')
|
||||
|
||||
const { $accountBinding } = useNuxtApp()
|
||||
const canAddAccount = ['AddAccount', 'addAccount'].some((name) =>
|
||||
($accountBinding as unknown as BaseBridge).availableMethodNames.includes(name)
|
||||
)
|
||||
|
||||
const showLegacy = ref(false)
|
||||
|
||||
const isDesktopServiceAvailable = ref(false) // this should be false default because there is a delay if /ping is not successful.
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -178,21 +178,25 @@ const selectVersionAndAddModel = async (
|
||||
|
||||
if (existingModel) {
|
||||
emit('close')
|
||||
// Patch the existing model card with new versions!
|
||||
await hostAppStore.patchModel(existingModel.modelCardId, {
|
||||
const patchPayload: Record<string, unknown> = {
|
||||
selectedVersionId: version.id,
|
||||
selectedVersionSourceApp: version.sourceApplication,
|
||||
selectedVersionUserId: version.authorUser?.id,
|
||||
latestVersionId: latestVersion.id,
|
||||
latestVersionSourceApp: latestVersion.sourceApplication,
|
||||
latestVersionUserId: latestVersion.authorUser?.id
|
||||
})
|
||||
}
|
||||
|
||||
// apply new settings to the existing model card if they were changed
|
||||
if (settingsWereChanged.value && receieveSettings.value) {
|
||||
patchPayload.settings = receieveSettings.value
|
||||
}
|
||||
|
||||
// patch the existing model card with new versions and settings
|
||||
await hostAppStore.patchModel(existingModel.modelCardId, patchPayload)
|
||||
await hostAppStore.receiveModel(existingModel.modelCardId, 'Wizard')
|
||||
return
|
||||
}
|
||||
|
||||
// We were tracking the source host app wrong before `getHostAppFromString`
|
||||
// i.e. we were having `Revit 2023` instead of `revit`
|
||||
const selectedVersionSourceApp = getSlugFromHostAppNameAndVersion(
|
||||
version.sourceApplication as string
|
||||
)
|
||||
|
||||
@@ -591,6 +591,13 @@ const upgradePlanButtonAction = () => {
|
||||
)
|
||||
return
|
||||
}
|
||||
// catch SSO session expired / any other unhandled permission flags
|
||||
// redirecting to the workspace root will trigger the standard web authentication flow.
|
||||
if (selectedWorkspace.value?.slug) {
|
||||
$openUrl(
|
||||
`${account.value.accountInfo.serverInfo.url}/workspaces/${selectedWorkspace.value?.slug}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const loadMore = () => {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
FROM node:22-bookworm@sha256:7e791fc54bd02fc89fd4fb39eb37e5bea753c75679c8022478d81679367d995a AS build-stage
|
||||
WORKDIR /app
|
||||
RUN corepack enable
|
||||
COPY package.json .
|
||||
COPY yarn.lock .
|
||||
COPY .yarnrc.yml .
|
||||
RUN yarn install --immutable || (cat /tmp/xfs-*/build.log && exit 1)
|
||||
COPY . .
|
||||
# NODE_ENV must be set after the dependencies are installed because @nuxt/kit is a devDependency and is required to build the application
|
||||
ARG NODE_ENV=production
|
||||
ENV NODE_ENV=${NODE_ENV}
|
||||
ENV NUXT_PUBLIC_MIXPANEL_TOKEN_ID=acd87c5a50b56df91a795e999812a3a4
|
||||
ENV NUXT_PUBLIC_MIXPANEL_API_HOST=https://analytics.speckle.systems
|
||||
RUN yarn generate
|
||||
|
||||
FROM joseluisq/static-web-server:2.40@sha256:63528bfba5d86b00572e23b4e44ed0f7a791f931df650125156d0c24f7a8f877 AS production-stage
|
||||
WORKDIR /app
|
||||
COPY --from=build-stage /app/dist /app/dist
|
||||
CMD ["--config-file", "/app/configuration.toml"]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: v2
|
||||
name: speckle-dui-chart
|
||||
description: A Helm chart for deploying the Speckle DUI3 application
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "0.1.0"
|
||||
@@ -0,0 +1,62 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "speckle-dui.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "speckle-dui.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "speckle-dui.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "speckle-dui.labels" -}}
|
||||
helm.sh/chart: {{ include "speckle-dui.chart" . }}
|
||||
{{ include "speckle-dui.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "speckle-dui.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "speckle-dui.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "speckle-dui.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "speckle-dui.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,122 @@
|
||||
kind: ConfigMap
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: {{ include "speckle-dui.fullname" . }}-configuration
|
||||
labels:
|
||||
{{- include "speckle-dui.labels" . | nindent 4 }}
|
||||
data:
|
||||
configuration.toml: |
|
||||
[general]
|
||||
|
||||
#### Address & Root dir
|
||||
host = "::"
|
||||
port = 80
|
||||
root = "/app/dist"
|
||||
|
||||
#### Logging
|
||||
log-level = "info"
|
||||
|
||||
#### Cache Control headers
|
||||
cache-control-headers = true
|
||||
|
||||
#### Auto Compression
|
||||
compression = true
|
||||
compression-level = "default"
|
||||
|
||||
#### Error pages
|
||||
# Note: If a relative path is used then it will be resolved under the root directory.
|
||||
page404 = "./404.html"
|
||||
page50x = "./50x.html"
|
||||
|
||||
#### HTTP/2 + TLS
|
||||
# Note: We expect TLS termination to be handled by a reverse proxy (e.g. Nginx, Traefik, Cloudflare, etc.)
|
||||
http2 = false
|
||||
http2-tls-cert = ""
|
||||
http2-tls-key = ""
|
||||
## we are terminating https upstream; redirect is at edge proxy (ingress/gateway)
|
||||
https-redirect = false
|
||||
https-redirect-host = "localhost"
|
||||
https-redirect-from-port = 80
|
||||
https-redirect-from-hosts = "localhost"
|
||||
|
||||
#### CORS & Security headers
|
||||
## security-headers must be disabled for iframe compatibility as they include x-frame-options: deny as default
|
||||
# security-headers = false
|
||||
## cors-allows-origins is unset as iframe embedding does not require CORS, we are not fetching from another origin via XHR/fetch, and wildcard increases attack surface.
|
||||
# cors-allow-origins = ""
|
||||
|
||||
#### Directory listing
|
||||
directory-listing = false
|
||||
|
||||
#### Directory listing sorting code
|
||||
directory-listing-order = 1
|
||||
|
||||
#### Directory listing content format
|
||||
directory-listing-format = "html"
|
||||
|
||||
#### Directory listing download format
|
||||
directory-listing-download = []
|
||||
|
||||
#### File descriptor binding
|
||||
# fd = ""
|
||||
|
||||
#### Worker threads
|
||||
threads-multiplier = 1
|
||||
|
||||
#### Grace period after a graceful shutdown
|
||||
grace-period = 0
|
||||
|
||||
#### Page fallback for 404s
|
||||
# page-fallback = ""
|
||||
|
||||
#### Log request Remote Address if available
|
||||
log-remote-address = true
|
||||
|
||||
#### Log real IP from X-Forwarded-For header if available
|
||||
log-forwarded-for = true
|
||||
|
||||
#### IPs to accept the X-Forwarded-For header from. Empty means all
|
||||
trusted-proxies = {{ .Values.security.trustedProxies | toJson }}
|
||||
|
||||
#### Redirect to trailing slash in the requested directory uri
|
||||
redirect-trailing-slash = true
|
||||
|
||||
#### Check for existing pre-compressed files
|
||||
compression-static = true
|
||||
|
||||
#### Health-check endpoint (GET or HEAD `/health`)
|
||||
health = true
|
||||
|
||||
#### Markdown content negotiation
|
||||
accept-markdown = false
|
||||
|
||||
#### Maintenance Mode
|
||||
|
||||
maintenance-mode = false
|
||||
# maintenance-mode-status = 503
|
||||
# maintenance-mode-file = "./maintenance.html"
|
||||
|
||||
[advanced]
|
||||
|
||||
#### HTTP Headers customization
|
||||
[[advanced.headers]]
|
||||
source = "/*.html"
|
||||
[advanced.headers.headers]
|
||||
# Cache-Control = "public, max-age=36000"
|
||||
Content-Security-Policy = """\
|
||||
frame-ancestors {{ if .Values.security.frameAncestors }}{{ .Values.security.frameAncestors | join " " }}{{ else }}'self'{{ end }}; \
|
||||
default-src 'self'; \
|
||||
frame-src {{ if .Values.security.frameSource }}{{ .Values.security.frameSource | join " " }}{{ else }}'self'{{ end }}; \
|
||||
script-src {{ if .Values.security.frameSource }}{{ .Values.security.frameSource | join " " }}{{ else }}'self'{{ end }} 'unsafe-inline'; \
|
||||
style-src {{ if .Values.security.frameSource }}{{ .Values.security.frameSource | join " " }}{{ else }}'self'{{ end }} 'unsafe-inline'; \
|
||||
img-src {{ if .Values.security.frameSource }}{{ .Values.security.frameSource | join " " }}{{ else }}'self'{{ end }} data: blob:; \
|
||||
connect-src {{ if .Values.security.frameAncestors }}{{ .Values.security.frameAncestors | join " " }}{{ else }}'self'{{ end }}; \
|
||||
object-src 'none'; \
|
||||
base-uri 'self'; \
|
||||
form-action {{ if .Values.security.frameAncestors }}{{ .Values.security.frameAncestors | join " " }}{{ else }}'self'{{ end }};\
|
||||
"""
|
||||
# Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"
|
||||
X-Content-Type-Options = "nosniff"
|
||||
Referrer-Policy = "strict-origin-when-cross-origin"
|
||||
Permissions-Policy = "geolocation=(), microphone=(), camera=()"
|
||||
## Purposefully do not set X-Frame-Options as this is intended to be an iframe
|
||||
@@ -0,0 +1,84 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "speckle-dui.fullname" . }}
|
||||
labels:
|
||||
{{- include "speckle-dui.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "speckle-dui.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "speckle-dui.labels" . | nindent 8 }}
|
||||
{{- with .Values.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "speckle-dui.serviceAccountName" . }}
|
||||
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||
{{- with .Values.podSecurityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
{{- with .Values.securityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80 # Needs to match port defined in deployment/docker/configuration.toml
|
||||
protocol: TCP
|
||||
{{- with .Values.livenessProbe }}
|
||||
livenessProbe:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.readinessProbe }}
|
||||
readinessProbe:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: configuration
|
||||
mountPath: /app/configuration.toml
|
||||
subPath: configuration.toml
|
||||
readOnly: true
|
||||
{{- with .Values.volumeMounts }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: configuration
|
||||
configMap:
|
||||
name: {{ include "speckle-dui.fullname" . }}-configuration
|
||||
{{- with .Values.volumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,38 @@
|
||||
{{- if .Values.httpRoute.enabled -}}
|
||||
{{- $fullName := include "speckle-dui.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.port -}}
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{- include "speckle-dui.labels" . | nindent 4 }}
|
||||
{{- with .Values.httpRoute.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
parentRefs:
|
||||
{{- with .Values.httpRoute.parentRefs }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.httpRoute.hostnames }}
|
||||
hostnames:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.httpRoute.rules }}
|
||||
- backendRefs:
|
||||
- name: {{ $fullName }}
|
||||
port: {{ $svcPort }}
|
||||
weight: 1
|
||||
{{- with .filters }}
|
||||
filters:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .matches }}
|
||||
matches:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,43 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "speckle-dui.fullname" . }}
|
||||
labels:
|
||||
{{- include "speckle-dui.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.ingress.className }}
|
||||
ingressClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- with .pathType }}
|
||||
pathType: {{ . }}
|
||||
{{- end }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "speckle-dui.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "speckle-dui.fullname" . }}
|
||||
labels: {{- include "speckle-dui.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector: {{- include "speckle-dui.selectorLabels" . | nindent 4 }}
|
||||
@@ -0,0 +1,13 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "speckle-dui.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "speckle-dui.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,150 @@
|
||||
# Default values for speckle-dui3.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
|
||||
replicaCount: 1
|
||||
|
||||
# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
|
||||
image:
|
||||
repository: ghcr.io/specklesystems/speckle-dui
|
||||
# This sets the pull policy for images.
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: ""
|
||||
|
||||
# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
|
||||
imagePullSecrets: []
|
||||
# This is to override the chart name.
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created.
|
||||
create: true
|
||||
# Automatically mount a ServiceAccount's API credentials?
|
||||
automount: false
|
||||
# Annotations to add to the service account.
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template.
|
||||
name: ""
|
||||
|
||||
# This is for setting Kubernetes Annotations to a Pod.
|
||||
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
|
||||
podAnnotations: {}
|
||||
# This is for setting Kubernetes Labels to a Pod.
|
||||
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
|
||||
podLabels: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/
|
||||
service:
|
||||
# This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types
|
||||
type: ClusterIP
|
||||
# This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports
|
||||
port: 80
|
||||
|
||||
security:
|
||||
## The IP addresses of trusted proxies, such as loadbalancers or WAFs, that may be forwarding traffic to the dashboards. This is important for correctly handling the X-Forwarded-For header and ensuring accurate client IP logging and security measures. Empty means all proxies are trusted, which may not be secure in production environments. We recommend setting this to the specific IP addresses of your trusted proxies.
|
||||
trustedProxies: []
|
||||
## A list of urls to be added as frame-ancestors of the Content-Security-Policy header. Empty means 'self', allowing embedding only from the same origin as the dashboards. We recommend setting this to the specific hostnames of your parent applications that will be embedding the dashboards in iframes.
|
||||
frameAncestors: []
|
||||
## A list of urls to be added as frame-src (and script-src, style-src, img-src) of the Content-Security-Policy header. Empty means 'self', allowing embedding of dashboards resources only from the same origin. We recommend setting this to the specific hostnames of Speckle DUI3.
|
||||
frameSource: []
|
||||
|
||||
# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
# As dashboards expect to serve all paths under the root, we recommend using a dedicated hostname for the service, e.g. dashboards.example.com, and not sharing it with other services.
|
||||
- host: chart-example.local
|
||||
paths:
|
||||
# Please retain this path, the dashboards expect to serve all paths under the root.
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
# -- Expose the service via gateway-api HTTPRoute
|
||||
# Requires Gateway API resources and suitable controller installed within the cluster
|
||||
# (see: https://gateway-api.sigs.k8s.io/guides/)
|
||||
httpRoute:
|
||||
# HTTPRoute enabled.
|
||||
enabled: false
|
||||
# HTTPRoute annotations.
|
||||
annotations: {}
|
||||
# Which Gateways this Route is attached to.
|
||||
parentRefs:
|
||||
- name: gateway
|
||||
sectionName: http
|
||||
# namespace: default
|
||||
# Hostnames matching HTTP header.
|
||||
hostnames:
|
||||
# As dashboards expect to serve all paths under the root, we recommend using a dedicated hostname for the service, e.g. dashboards.example.com, and not sharing it with other services.
|
||||
- chart-example.local
|
||||
# List of rules and filters applied.
|
||||
rules:
|
||||
- matches:
|
||||
# Please retain this path, the dashboards expect to serve all paths under the root.
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
|
||||
# Additional volumes on the output Deployment definition.
|
||||
volumes: []
|
||||
# - name: foo
|
||||
# secret:
|
||||
# secretName: mysecret
|
||||
# optional: false
|
||||
|
||||
# Additional volumeMounts on the output Deployment definition.
|
||||
volumeMounts: []
|
||||
# - name: foo
|
||||
# mountPath: "/etc/foo"
|
||||
# readOnly: true
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
@@ -1,29 +1,81 @@
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
const CHALLENGE_KEY = 'speckle_challenge'
|
||||
const CHALLENGE_URL_KEY = 'speckle_url_challenge'
|
||||
const CODE_VERIFIER_KEY = 'speckle_code_verifier'
|
||||
|
||||
function toBase64Url(buffer: Uint8Array): string {
|
||||
const base64 = btoa(String.fromCharCode(...buffer))
|
||||
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a cryptographically random base64url-encoded string.
|
||||
* 32 bytes → 43 characters after base64url encoding (within the RFC 7636 range of 43-128).
|
||||
*/
|
||||
function createCodeVerifier(): string {
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(32))
|
||||
return toBase64Url(bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes SHA-256 hash of a string and returns it as base64url.
|
||||
* This is the PKCE code_challenge derivation from a code_verifier.
|
||||
*/
|
||||
async function computeS256Challenge(codeVerifier: string): Promise<string> {
|
||||
const data = new TextEncoder().encode(codeVerifier)
|
||||
const digest = await crypto.subtle.digest('SHA-256', data)
|
||||
return toBase64Url(new Uint8Array(digest))
|
||||
}
|
||||
|
||||
export interface ChallengeData {
|
||||
/** The raw random string (code_verifier in PKCE terms) */
|
||||
codeVerifier: string
|
||||
/** SHA-256 base64url hash of codeVerifier (code_challenge for S256 method) */
|
||||
codeChallenge: string
|
||||
}
|
||||
|
||||
export function useAuthManager() {
|
||||
const generateChallenge = (url: string): 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
|
||||
/**
|
||||
* Generates a PKCE code_verifier + code_challenge pair and persists to localStorage.
|
||||
* Used by redirect-based sign-in flows (SignInFlow) that need to
|
||||
* recover the values after the browser navigates away and back.
|
||||
*/
|
||||
const generateChallenge = async (url: string): Promise<ChallengeData> => {
|
||||
const codeVerifier = createCodeVerifier()
|
||||
const codeChallenge = await computeS256Challenge(codeVerifier)
|
||||
localStorage.setItem(CHALLENGE_KEY, codeChallenge)
|
||||
localStorage.setItem(CODE_VERIFIER_KEY, codeVerifier)
|
||||
localStorage.setItem(CHALLENGE_URL_KEY, url)
|
||||
return result
|
||||
return { codeVerifier, codeChallenge }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a PKCE code_verifier + code_challenge pair without persisting to localStorage.
|
||||
* Used by flows that keep values in memory (ExchangeTokenSignInFlow)
|
||||
* so they don't overwrite the redirect flow's stored data.
|
||||
*/
|
||||
const generateLocalChallenge = async (): Promise<ChallengeData> => {
|
||||
const codeVerifier = createCodeVerifier()
|
||||
const codeChallenge = await computeS256Challenge(codeVerifier)
|
||||
return { codeVerifier, codeChallenge }
|
||||
}
|
||||
|
||||
const getChallenge = (): string | null => {
|
||||
return localStorage.getItem(CHALLENGE_KEY)
|
||||
}
|
||||
|
||||
const getCodeVerifier = (): string | null => {
|
||||
return localStorage.getItem(CODE_VERIFIER_KEY)
|
||||
}
|
||||
|
||||
const getChallengeUrl = (): string | null => {
|
||||
return localStorage.getItem(CHALLENGE_URL_KEY)
|
||||
}
|
||||
|
||||
return {
|
||||
getChallenge,
|
||||
getCodeVerifier,
|
||||
getChallengeUrl,
|
||||
generateChallenge
|
||||
generateChallenge,
|
||||
generateLocalChallenge
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { md5 } from '@speckle/shared'
|
||||
import type { Account } from '~/lib/bindings/definitions/IAccountBinding'
|
||||
|
||||
/**
|
||||
* Checks if the server supports the new /oauth/token endpoint.
|
||||
* The server exposes GET /oauth/token returning 'supported' when available.
|
||||
*/
|
||||
export async function supportsOAuthToken(serverUrl: string): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch(new URL('/oauth/token', serverUrl), { method: 'GET' })
|
||||
return res.ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function useTokenExchange() {
|
||||
const { $accountBinding } = useNuxtApp()
|
||||
|
||||
const exchangeAccessCode = async (
|
||||
rawServerUrl: string,
|
||||
accessCode: string,
|
||||
challenge: string,
|
||||
codeVerifier?: string
|
||||
): Promise<void> => {
|
||||
// Normalize to origin (strips trailing slash, path, etc.)
|
||||
// so account IDs stay consistent with connectors
|
||||
const serverUrl = new URL(rawServerUrl).origin
|
||||
const tokenHeaders = { 'Content-Type': 'application/json' }
|
||||
let tokenResponse: Response
|
||||
|
||||
// If we have a codeVerifier, try the new PKCE-based /oauth/token endpoint first
|
||||
if (codeVerifier && (await supportsOAuthToken(serverUrl))) {
|
||||
tokenResponse = await fetch(new URL('/oauth/token', serverUrl), {
|
||||
method: 'POST',
|
||||
headers: tokenHeaders,
|
||||
body: JSON.stringify({
|
||||
appId: 'sdui',
|
||||
appSecret: 'sdui',
|
||||
accessCode,
|
||||
codeVerifier
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// Fall back to legacy /auth/token with plain challenge
|
||||
tokenResponse = await fetch(new URL('/auth/token', serverUrl), {
|
||||
method: 'POST',
|
||||
headers: tokenHeaders,
|
||||
body: JSON.stringify({
|
||||
appId: 'sdui',
|
||||
appSecret: 'sdui',
|
||||
accessCode,
|
||||
challenge
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const errorText = await tokenResponse.text()
|
||||
throw new Error(
|
||||
`Token exchange failed with status ${tokenResponse.status}: ${errorText}`
|
||||
)
|
||||
}
|
||||
|
||||
const { token, refreshToken } = (await tokenResponse.json()) as {
|
||||
token: string
|
||||
refreshToken: string
|
||||
}
|
||||
|
||||
// Query user and server info
|
||||
const graphqlQuery = {
|
||||
query:
|
||||
'query { activeUser { id name email company avatar } serverInfo { name company adminContact description version } }'
|
||||
}
|
||||
|
||||
const userAndServerInfoResponse = await fetch(new URL('/graphql', serverUrl), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(graphqlQuery)
|
||||
})
|
||||
|
||||
if (!userAndServerInfoResponse.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch user info with status ${userAndServerInfoResponse.status}`
|
||||
)
|
||||
}
|
||||
|
||||
// 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 + serverUrl
|
||||
).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: serverUrl, ...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)
|
||||
}
|
||||
|
||||
return { exchangeAccessCode }
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export interface IAccountBinding extends IBinding<IAccountBindingEvents> {
|
||||
getAccounts: () => Promise<Account[]>
|
||||
addAccount: (accountId: string, account: Account) => Promise<void>
|
||||
removeAccount: (accountId: string) => Promise<void>
|
||||
authenticateAccount: (serverUrl: string) => Promise<Account>
|
||||
}
|
||||
|
||||
// An almost 1-1 mapping of what we need from the Core accounts class.
|
||||
@@ -60,6 +61,25 @@ export class MockedAccountBinding implements IAccountBinding {
|
||||
return await console.log('no way dude', accountId, account)
|
||||
}
|
||||
|
||||
public async authenticateAccount(serverUrl: string) {
|
||||
const config = useRuntimeConfig()
|
||||
return (await {
|
||||
id: 'whatever',
|
||||
isDefault: true,
|
||||
token: config.public.speckleToken,
|
||||
serverInfo: {
|
||||
name: 'test',
|
||||
url: serverUrl,
|
||||
frontend2: true
|
||||
},
|
||||
userInfo: {
|
||||
id: 'whatever',
|
||||
avatar: 'whatever',
|
||||
email: ''
|
||||
}
|
||||
}) as Account
|
||||
}
|
||||
|
||||
public async removeAccount(accountId: string) {
|
||||
return await console.log('no way dude', accountId)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ export type GlobalConfig = {
|
||||
|
||||
export type ConnectorConfig = {
|
||||
darkTheme: boolean
|
||||
disableCache?: boolean
|
||||
}
|
||||
|
||||
export type AccountsConfig = {
|
||||
@@ -48,7 +49,7 @@ export class MockedConfigBinding implements IConfigBinding {
|
||||
}
|
||||
|
||||
public async getConfig() {
|
||||
return await { darkTheme: false }
|
||||
return await { darkTheme: false, disableCache: false }
|
||||
}
|
||||
|
||||
public async getGlobalConfig() {
|
||||
|
||||
+616
-134
File diff suppressed because it is too large
Load Diff
+9
-1
@@ -10,6 +10,13 @@
|
||||
"build": "nuxt build",
|
||||
"dev:nuxt": "nuxt dev",
|
||||
"dev": "concurrently \"nuxt dev\" \"yarn gqlgen:watch\"",
|
||||
"dev:kind:up": "ctlptl apply --filename ./tests/deployment/helm/cluster-config.yaml",
|
||||
"dev:kind:down": "ctlptl delete -f ./tests/deployment/helm/cluster-config.yaml",
|
||||
"dev:kind:helm:up": "yarn run dev:kind:up && tilt up --file ./tests/deployment/helm/Tiltfile --context kind-speckle-dui",
|
||||
"dev:kind:helm:down": "tilt down --file ./tests/deployment/helm/Tiltfile --context kind-speckle-dui",
|
||||
"dev:kind:helm:ci": "tilt ci --file ./tests/deployment/helm/Tiltfile --context kind-speckle-dui --timeout 10m",
|
||||
"docker:build": "docker build -f ./deployment/docker/Dockerfile -t ghcr.io/specklesystems/speckle-dui:local .",
|
||||
"docker:run": "docker run --rm -p 8083:80 -v ./deployment/docker/configuration.toml:/app/configuration.toml:ro ghcr.io/specklesystems/speckle-dui:local",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
@@ -20,7 +27,8 @@
|
||||
"lint": "yarn lint:js && yarn lint:tsc && yarn lint:prettier && yarn lint:css",
|
||||
"lint:ci": "yarn lint:tsc && yarn lint:css",
|
||||
"gqlgen": "graphql-codegen",
|
||||
"gqlgen:watch": "graphql-codegen --watch"
|
||||
"gqlgen:watch": "graphql-codegen --watch",
|
||||
"prettier:fix": "prettier --config .prettierrc --ignore-path .prettierignore --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.14",
|
||||
|
||||
@@ -3,17 +3,16 @@
|
||||
</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 { useTokenExchange } from '~/lib/authn/useTokenExchange'
|
||||
import { useHostAppStore } from '~/store/hostApp'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { getChallenge, getChallengeUrl } = useAuthManager()
|
||||
const { $accountBinding } = useNuxtApp()
|
||||
const { getChallenge, getCodeVerifier, getChallengeUrl } = useAuthManager()
|
||||
const { exchangeAccessCode } = useTokenExchange()
|
||||
const hostApp = useHostAppStore()
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -22,69 +21,11 @@ onMounted(async () => {
|
||||
const accessCode = route.query.access_code as string | undefined
|
||||
if (accessCode && origin) {
|
||||
const challenge = getChallenge()
|
||||
const body = {
|
||||
appId: 'sdui',
|
||||
appSecret: 'sdui',
|
||||
accessCode,
|
||||
challenge
|
||||
if (!challenge) {
|
||||
throw new Error('No challenge found in storage.')
|
||||
}
|
||||
|
||||
// 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)
|
||||
const codeVerifier = getCodeVerifier() ?? undefined
|
||||
await exchangeAccessCode(origin, accessCode, challenge, codeVerifier)
|
||||
} else {
|
||||
throw new Error('No access code is found.')
|
||||
}
|
||||
@@ -92,7 +33,7 @@ onMounted(async () => {
|
||||
hostApp.setNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Failed to add your Speckle account.',
|
||||
description: error as string
|
||||
description: error instanceof Error ? error.message : (error as string)
|
||||
})
|
||||
} finally {
|
||||
router.replace('/')
|
||||
|
||||
+13
-2
@@ -8,11 +8,14 @@ export const useConfigStore = defineStore('configStore', () => {
|
||||
|
||||
const userSelectedWorkspaceId = ref<string>()
|
||||
|
||||
const config = ref<ConnectorConfig>({ darkTheme: true })
|
||||
const config = ref<ConnectorConfig>({ darkTheme: true, disableCache: false })
|
||||
|
||||
const isDarkTheme = computed(() => {
|
||||
return config.value?.darkTheme
|
||||
})
|
||||
const isCacheDisabled = computed(() => {
|
||||
return config.value?.disableCache || false
|
||||
})
|
||||
const isDevMode = ref(false)
|
||||
|
||||
const toggleTheme = () => {
|
||||
@@ -20,6 +23,11 @@ export const useConfigStore = defineStore('configStore', () => {
|
||||
$configBinding.updateConfig(config.value)
|
||||
}
|
||||
|
||||
const toggleCache = () => {
|
||||
config.value.disableCache = !config.value.disableCache
|
||||
$configBinding.updateConfig(config.value)
|
||||
}
|
||||
|
||||
const setUserSelectedWorkspace = (workspaceId: string) => {
|
||||
userSelectedWorkspaceId.value = workspaceId
|
||||
try {
|
||||
@@ -33,7 +41,8 @@ export const useConfigStore = defineStore('configStore', () => {
|
||||
|
||||
const init = async () => {
|
||||
if (!$configBinding) return
|
||||
config.value = await $configBinding.getConfig()
|
||||
const fetchedConfig = await $configBinding.getConfig()
|
||||
config.value = { disableCache: false, ...fetchedConfig }
|
||||
const workspacesConfig = await $configBinding.getWorkspacesConfig()
|
||||
if (workspacesConfig && workspacesConfig.userSelectedWorkspaceId) {
|
||||
userSelectedWorkspaceId.value = workspacesConfig.userSelectedWorkspaceId
|
||||
@@ -51,9 +60,11 @@ export const useConfigStore = defineStore('configStore', () => {
|
||||
config,
|
||||
hasConfigBindings,
|
||||
isDarkTheme,
|
||||
isCacheDisabled,
|
||||
isDevMode,
|
||||
userSelectedWorkspaceId,
|
||||
toggleTheme,
|
||||
toggleCache,
|
||||
setUserSelectedWorkspace
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
print('🚀 Deploying Speckle DUI into a Kind Cluster via Tilt...')
|
||||
|
||||
# we limit tilt to run only on the kind cluster
|
||||
allow_k8s_contexts(['kind-speckle-dui'])
|
||||
if k8s_context() != 'kind-speckle-dui':
|
||||
fail('Failing early as tilt should only ever connect to kind-speckle-dui.')
|
||||
|
||||
# Install extensions
|
||||
load('ext://helm_resource', 'helm_resource', 'helm_repo')
|
||||
load('ext://k8s_yaml_glob', 'k8s_yaml_glob')
|
||||
|
||||
docker_build('ghcr.io/specklesystems/speckle-dui',
|
||||
context='../../..',
|
||||
dockerfile='../../../deployment/docker/Dockerfile',
|
||||
ignore = ['**/.nuxt', '**/node_modules', '**/dist', '**/build', '**/.git', '**/.claude', '**/.cursor', '**/deployment/**/*', '**/tests/**/*']
|
||||
)
|
||||
|
||||
# Create namespaces
|
||||
k8s_yaml_glob('./manifests/*.namespace.yaml')
|
||||
|
||||
k8s_yaml('./manifests/coredns.configmap.yaml')
|
||||
k8s_resource(new_name='coredns',
|
||||
objects=['coredns:configmap:kube-system'],
|
||||
resource_deps=[],
|
||||
labels=['coredns'])
|
||||
|
||||
# Update CoreDNS to allow for local resolution of services internally (i.e. speckle.internal will be routed to nginx)
|
||||
local_resource('coredns-up',
|
||||
cmd='./scripts/coredns-up.sh',
|
||||
resource_deps=['coredns'],
|
||||
deps=['./manifests/coredns.configmap.yaml', './scripts/coredns-up.sh'],
|
||||
labels=['coredns'])
|
||||
|
||||
helm_repo('ingress-nginx-repo',
|
||||
'https://kubernetes.github.io/ingress-nginx')
|
||||
|
||||
#nginx should be deployed as the last dependency as it opens ports to services
|
||||
#it expects these services to exist, which are created by the helm charts above
|
||||
helm_resource('ingress-nginx',
|
||||
release_name='ingress-nginx',
|
||||
namespace='ingress-nginx',
|
||||
chart='ingress-nginx-repo/ingress-nginx',
|
||||
flags=['--version=4.8.0',
|
||||
'--values=./values/nginx.values.yaml',
|
||||
'--kube-context=kind-speckle-dui'],
|
||||
deps=['./values/nginx.values.yaml'],
|
||||
resource_deps=['ingress-nginx-repo', 'coredns'],
|
||||
labels=['speckle-dependencies'])
|
||||
|
||||
helm_resource('speckle-dui',
|
||||
release_name='speckle-dui',
|
||||
namespace='speckle-dui',
|
||||
chart='./../../../deployment/helm/speckle-dui',
|
||||
flags=['--values=./values/speckle-dui.values.yaml',
|
||||
'--kube-context=kind-speckle-dui'],
|
||||
image_deps=[
|
||||
'ghcr.io/specklesystems/speckle-dui'
|
||||
],
|
||||
image_keys=[
|
||||
('image.repository', 'image.tag')
|
||||
],
|
||||
deps=['./../../../deployment/helm/speckle-dui',
|
||||
'./values/speckle-dui.values.yaml'],
|
||||
resource_deps=['ingress-nginx', 'coredns'],
|
||||
labels=['speckle-dui'])
|
||||
@@ -0,0 +1,29 @@
|
||||
apiVersion: ctlptl.dev/v1alpha1
|
||||
kind: Registry
|
||||
name: ctlptl-registry
|
||||
port: 5000
|
||||
---
|
||||
apiVersion: ctlptl.dev/v1alpha1
|
||||
kind: Cluster
|
||||
product: kind
|
||||
registry: ctlptl-registry
|
||||
name: kind-speckle-dui
|
||||
kindV1Alpha4Cluster:
|
||||
nodes:
|
||||
- role: control-plane
|
||||
kubeadmConfigPatches:
|
||||
- |
|
||||
kind: InitConfiguration
|
||||
nodeRegistration:
|
||||
kubeletExtraArgs:
|
||||
node-labels: "ingress-ready=true"
|
||||
extraMounts: []
|
||||
extraPortMappings:
|
||||
- containerPort: 80
|
||||
hostPort: 80 # Docker requires privileged ports binding permissions https://docs.docker.com/desktop/mac/permission-requirements/#binding-privileged-ports
|
||||
protocol: TCP
|
||||
listenAddress: '127.0.0.1' #DO NOT REMOVE - this is required to prevent access from the local network or the world!!!
|
||||
- containerPort: 443
|
||||
hostPort: 443 # Docker requires privileged ports binding permissions https://docs.docker.com/desktop/mac/permission-requirements/#binding-privileged-ports
|
||||
protocol: TCP
|
||||
listenAddress: '127.0.0.1' #DO NOT REMOVE - this is required to prevent access from the local network or the world!!!
|
||||
@@ -0,0 +1,24 @@
|
||||
{ pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/63dacb46bf939521bdc93981b4cbb7ecb58427a0.tar.gz") {} }:
|
||||
|
||||
let
|
||||
corepack = pkgs.stdenv.mkDerivation {
|
||||
name = "corepack";
|
||||
buildInputs = [ pkgs.nodejs_22 ];
|
||||
phases = [ "installPhase" ];
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
corepack enable --install-directory=$out/bin
|
||||
'';
|
||||
};
|
||||
in pkgs.mkShell {
|
||||
buildInputs = [
|
||||
pkgs.docker
|
||||
pkgs.kind
|
||||
pkgs.kubectl
|
||||
pkgs.nodejs_22
|
||||
pkgs.ctlptl
|
||||
pkgs.kubernetes-helm
|
||||
pkgs.tilt
|
||||
corepack
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
apiVersion: v1
|
||||
data:
|
||||
Corefile: |
|
||||
.:53 {
|
||||
errors
|
||||
health {
|
||||
lameduck 5s
|
||||
}
|
||||
ready
|
||||
rewrite name speckle.internal ingress-nginx-controller.ingress-nginx.svc.cluster.local.
|
||||
kubernetes cluster.local in-addr.arpa ip6.arpa {
|
||||
pods insecure
|
||||
fallthrough in-addr.arpa ip6.arpa
|
||||
ttl 30
|
||||
}
|
||||
prometheus :9153
|
||||
forward . /etc/resolv.conf {
|
||||
max_concurrent 1000
|
||||
}
|
||||
cache 30
|
||||
loop
|
||||
reload
|
||||
loadbalance
|
||||
}
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: coredns
|
||||
namespace: kube-system
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: 'ingress-nginx'
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: 'speckle-dui'
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
kubectl --context="kind-speckle-dui" --namespace="kube-system" rollout restart deployment/coredns
|
||||
kubectl --context="kind-speckle-dui" --namespace="kube-system" rollout status deployment "coredns" --timeout=90s
|
||||
@@ -0,0 +1,7 @@
|
||||
controller:
|
||||
# We must set the kind cluster listen address for every port to '127.0.0.1' when hostNetwork is true
|
||||
hostNetwork: true
|
||||
admissionWebhooks:
|
||||
enabled: false
|
||||
# progressDeadlineSeconds: 600 #HACK helm chart was complaining that this was less than minReadySeconds https://github.com/kubernetes/ingress-nginx/blob/c72441585e1ab1a32df86e760613d36fa804315d/charts/ingress-nginx/templates/controller-deployment.yaml#L26
|
||||
tcp: {}
|
||||
@@ -0,0 +1,16 @@
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
annotations: {}
|
||||
hosts:
|
||||
- host: speckle.internal
|
||||
paths:
|
||||
# Please retain this path, the dashboards expect to serve all paths under the root.
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
security:
|
||||
trustedProxies: []
|
||||
frameAncestors:
|
||||
- "speckle.internal"
|
||||
frameSource:
|
||||
- "speckle.internal"
|
||||
Reference in New Issue
Block a user