fix(frontend): fix eula handling to prevent being stuck on /eula

If initial EULA request fails, we will show AppUnavailable instead of sending to /eula. If you navigate directly /eula and its already accepted, navigate away to the Home page.

Signed-off-by: Edward Sammut Alessi <edward.sammutalessi@siderolabs.com>
This commit is contained in:
Edward Sammut Alessi 2026-04-23 17:19:54 +02:00
parent 725f41d4ee
commit 921389a59c
No known key found for this signature in database
GPG Key ID: 65558E016966977A
5 changed files with 75 additions and 6 deletions

View File

@ -11,6 +11,8 @@ import { createApp } from 'vue'
import { handleHotUpdate } from 'vue-router/auto-routes'
import { Runtime } from '@/api/common/omni.pb'
import type { RequestError } from '@/api/fetch.pb'
import { Code } from '@/api/google/rpc/code.pb'
import type { Resource } from '@/api/grpc'
import { initState, ResourceService } from '@/api/grpc'
import type { AuthConfigSpec, EulaAcceptanceSpec } from '@/api/omni/specs/auth.pb'
@ -65,7 +67,13 @@ const setupApp = async () => {
withRuntime(Runtime.Omni),
)
eulaAccepted.value = true
} catch {
} catch (e) {
if ((e as RequestError)?.code !== Code.NOT_FOUND) {
console.error('failed to get eula state', e)
createApp(AppUnavailable).mount('#app')
return
}
eulaAccepted.value = false
}

View File

@ -8,12 +8,19 @@ import { Runtime } from '@/api/common/omni.pb'
import type { fetchOption } from '@/api/fetch.pb'
import { type Resource, ResourceService } from '@/api/grpc'
import type { GetRequest } from '@/api/omni/resources/resources.pb'
import { withAbortController, withContext, withRuntime, withSelectors } from '@/api/options'
import {
withAbortController,
withContext,
withRuntime,
withSelectors,
withSkipRequestSignature,
} from '@/api/options'
import type { WatchContext } from '@/api/watch'
interface GetOptionsCommon {
resource: GetRequest
selectors?: string[]
skipSignature?: boolean
skip?: boolean
}
@ -54,6 +61,10 @@ export function useResourceGet<TSpec = unknown, TStatus = unknown>(
const fetchOptions: fetchOption[] = []
if (options.skipSignature) {
fetchOptions.push(withSkipRequestSignature())
}
if (options.runtime) {
fetchOptions.push(withRuntime(options.runtime))
}

View File

@ -8,12 +8,19 @@ import { Runtime } from '@/api/common/omni.pb'
import type { fetchOption } from '@/api/fetch.pb'
import { type Resource, ResourceService } from '@/api/grpc'
import type { ListRequest } from '@/api/omni/resources/resources.pb'
import { withAbortController, withContext, withRuntime, withSelectors } from '@/api/options'
import {
withAbortController,
withContext,
withRuntime,
withSelectors,
withSkipRequestSignature,
} from '@/api/options'
import type { WatchContext } from '@/api/watch'
interface ListOptionsCommon {
resource: ListRequest
selectors?: string[]
skipSignature?: boolean
skip?: boolean
}
@ -54,6 +61,10 @@ export function useResourceList<TSpec = unknown, TStatus = unknown>(
const fetchOptions: fetchOption[] = []
if (options.skipSignature) {
fetchOptions.push(withSkipRequestSignature())
}
if (options.runtime) {
fetchOptions.push(withRuntime(options.runtime))
}

View File

@ -5,7 +5,8 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { delay, http, HttpResponse } from 'msw'
import type { CreateRequest, CreateResponse } from '@/api/omni/resources/resources.pb'
import { Code } from '@/api/google/rpc/code.pb'
import type { CreateRequest, CreateResponse, GetRequest } from '@/api/omni/resources/resources.pb'
import { DefaultNamespace, EulaAcceptanceID, EulaAcceptanceType } from '@/api/resources'
import Eula from './eula.vue'
@ -21,6 +22,29 @@ export const Default: Story = {
parameters: {
msw: {
handlers: [
http.post<never, GetRequest>('/omni.resources.ResourceService/Get', async ({ request }) => {
const { id, type, namespace } = await request.clone().json()
if (
id !== EulaAcceptanceID ||
type !== EulaAcceptanceType ||
namespace !== DefaultNamespace
) {
return
}
return HttpResponse.json(
{
body: {
code: Code.NOT_FOUND,
message:
"failed to get: resource EulaAcceptances.omni.sidero.dev(default/eula@undefined) doesn't exist",
},
},
{ status: 404 },
)
}),
http.post<never, CreateRequest, CreateResponse>(
'/omni.resources.ResourceService/Create',
async ({ request }) => {

View File

@ -5,7 +5,7 @@ Use of this software is governed by the Business Source License
included in the LICENSE file.
-->
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, ref, watchEffect } from 'vue'
import { useRouter } from 'vue-router'
import { Runtime } from '@/api/common/omni.pb'
@ -17,6 +17,7 @@ import TCheckbox from '@/components/Checkbox/TCheckbox.vue'
import PageContainer from '@/components/PageContainer/PageContainer.vue'
import TInput from '@/components/TInput/TInput.vue'
import { eulaAccepted } from '@/methods'
import { useResourceGet } from '@/methods/useResourceGet'
import { showError } from '@/notification'
import { withRuntime, withSkipRequestSignature } from '../api/options'
@ -34,6 +35,20 @@ const accepting = ref(false)
const invalidForm = computed(() => !accepted.value || !name.value.trim() || !email.value.trim())
const { data, loading } = useResourceGet<EulaAcceptanceSpec>({
runtime: Runtime.Omni,
resource: {
namespace: DefaultNamespace,
type: EulaAcceptanceType,
id: EulaAcceptanceID,
},
skipSignature: true,
})
watchEffect(() => {
if (data.value) router.replace({ name: 'Home' })
})
const accept = async () => {
if (accepting.value) return
@ -68,7 +83,7 @@ const accept = async () => {
</script>
<template>
<PageContainer class="flex h-full items-center justify-center">
<PageContainer v-if="!loading && !data" class="flex h-full items-center justify-center">
<form
class="flex w-full max-w-2xl flex-col gap-6 rounded-md bg-naturals-n3 px-8 py-8 drop-shadow-md"
@submit.prevent="accept"