fix(ui): support secure UUIDs on LAN HTTP
Avoid eager consent component crashes and registration approval failures where crypto.randomUUID is unavailable. Share the companion audio CSPRNG fallback, preserve UUIDv4/session semantics and fail closed without secure randomness. Validation: 24 focused UUID, consent mount, registration recovery and companion audio tests passed. Independent review passed; full UI typecheck/build qualification remains in progress.
This commit is contained in:
@@ -41,6 +41,7 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { secureUuid } from '@/utils/secureUuid'
|
||||
import { fileBrowserClient, type FileBrowserItem } from '@/api/filebrowser-client'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
import type { RegistrationRequest, CloudSelection } from '@/composables/useMediaRegistrationBridge'
|
||||
@@ -48,7 +49,7 @@ const props = defineProps<{ request: RegistrationRequest | null; phase: 'select'
|
||||
const emit = defineEmits<{ approve: [selection: CloudSelection]; cancel: []; resolve: [] }>()
|
||||
const modal = ref<HTMLElement | null>(null), directory = ref('/'), files = ref<FileBrowserItem[]>([])
|
||||
const selected = ref<FileBrowserItem | null>(null), loading = ref(false), localError = ref('')
|
||||
const titleId = `media-registration-${crypto.randomUUID()}`
|
||||
const titleId = `media-registration-${secureUuid()}`
|
||||
let generation = 0
|
||||
const visible = computed(() => files.value.filter(item => item.isDir || /\.(mp4|m4v|webm|mov)$/i.test(item.name)))
|
||||
const duration = computed(() => { const seconds = props.request?.intent.viewingSeconds ?? 0; return seconds % 3600 === 0 ? `${seconds / 3600} hours` : `${Math.ceil(seconds / 60)} minutes` })
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { webcrypto } from 'node:crypto'
|
||||
const files = vi.hoisted(() => ({ login: vi.fn(async () => true), listDirectory: vi.fn(async () => []) }))
|
||||
vi.mock('@/api/filebrowser-client', () => ({ fileBrowserClient: files }))
|
||||
import MediaRegistrationConsent from '../MediaRegistrationConsent.vue'
|
||||
afterEach(() => { vi.unstubAllGlobals(); vi.clearAllMocks() })
|
||||
it('mounts the inactive dashboard consent and opens its labelled dialog on LAN HTTP', async () => {
|
||||
vi.stubGlobal('crypto', { getRandomValues: webcrypto.getRandomValues.bind(webcrypto) })
|
||||
const wrapper = mount(MediaRegistrationConsent, { props: { request: null, phase: 'select', error: '' } })
|
||||
expect(files.login).not.toHaveBeenCalled()
|
||||
await wrapper.setProps({ request: { intent: { viewingSeconds: 3600, priceSats: 15 } } as any })
|
||||
await flushPromises()
|
||||
const dialog = wrapper.get('[role="dialog"]')
|
||||
const label = dialog.attributes('aria-labelledby')
|
||||
expect(label).toMatch(/^media-registration-[0-9a-f-]{36}$/)
|
||||
expect(wrapper.get('#' + label).text()).toBeTruthy()
|
||||
wrapper.unmount()
|
||||
})
|
||||
Reference in New Issue
Block a user