The page saved every field as 'true'/'false', the kiosk device token included, and loaded the token as a boolean. One save of Settings > USB Devices therefore stored the literal word "true" (or "false") as the token, and every kiosk provisioned with the real token got 404 on each request - reported as the kiosk no longer accepting an SSO. Generate + Save never stored the generated value either. The token now loads and saves as text. The server already keeps the stored token when it receives the mask back unchanged. The two switches still save as booleans. The parts kiosk page was not affected.
164 lines
5.7 KiB
Vue
164 lines
5.7 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<h2>USB Devices</h2>
|
|
</div>
|
|
|
|
<div class="card form-card">
|
|
<div v-if="message" class="settings-success">{{ message }}</div>
|
|
<div v-if="error" class="error-message">{{ error }}</div>
|
|
|
|
<div class="form-group">
|
|
<label class="check-row">
|
|
<input type="checkbox" v-model="values.usb_kiosk_enabled" />
|
|
<span>Allow the logged-out USB kiosk</span>
|
|
</label>
|
|
<p class="field-hint">
|
|
Turns on <code>/usb-kiosk</code>, a full-screen touch page where somebody
|
|
at the drive cabinet takes and returns a drive without signing in.
|
|
It is off by default: this is a write nobody has logged in for, so it is
|
|
a decision a site makes deliberately. While it is off, every kiosk
|
|
request is refused and the page says so.
|
|
</p>
|
|
<p class="field-hint">
|
|
The kiosk can only move a drive between checked out and checked in. It
|
|
cannot add, edit or retire one. Each action is attributed to the SSO
|
|
entered on the screen, and an SSO that matches no employee is refused -
|
|
a checkout nobody can trace to a person defeats the point of tracking
|
|
it.
|
|
</p>
|
|
<p v-if="values.usb_kiosk_enabled" class="field-hint">
|
|
<a :href="withBase('/usb-kiosk')" target="_blank">Open the kiosk</a>
|
|
- also in the sidebar under Displays.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Kiosk device token</label>
|
|
<div class="token-row">
|
|
<input v-model="values.usb_kiosk_token" type="text"
|
|
class="form-control" placeholder="not set" />
|
|
<button type="button" class="btn btn-secondary" @click="generateToken">
|
|
Generate
|
|
</button>
|
|
</div>
|
|
<p class="field-hint">
|
|
Provision each screen once by opening
|
|
<code>/usb-kiosk?token=<this value></code> on it. That browser
|
|
keeps it; the address bar does not. Turning the kiosk on is a
|
|
decision, not a credential - without this token the routes answer 404,
|
|
so nobody who merely finds the URL can move a drive and sign it with a
|
|
badge they guessed.
|
|
</p>
|
|
<p v-if="tokenJustGenerated" class="field-hint token-warn">
|
|
Copy this now and save. It is masked once stored.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="check-row">
|
|
<input type="checkbox" v-model="values.usb_kiosk_allow_return_for_others"
|
|
:disabled="!values.usb_kiosk_enabled" />
|
|
<span>Let somebody return a drive checked out to another person</span>
|
|
</label>
|
|
<p class="field-hint">
|
|
The kiosk asks first and names the holder, then records the SSO that
|
|
actually handed the drive back. Turn this off to refuse a mismatch
|
|
outright - but then a drive left behind by somebody on leave cannot be
|
|
returned at the kiosk at all, and needs a signed-in user to sort out.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button class="btn btn-primary" :disabled="saving" @click="save">
|
|
{{ saving ? 'Saving...' : 'Save' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { settingsApi } from '@/api'
|
|
import { withBase } from '@/utils/basePath'
|
|
|
|
const KEYS = ['usb_kiosk_enabled', 'usb_kiosk_allow_return_for_others',
|
|
'usb_kiosk_token']
|
|
|
|
const values = ref({
|
|
usb_kiosk_enabled: false,
|
|
usb_kiosk_allow_return_for_others: true,
|
|
usb_kiosk_token: '',
|
|
})
|
|
|
|
const tokenJustGenerated = ref(false)
|
|
|
|
// Generated in the browser so it never travels anywhere it did not need to.
|
|
function generateToken() {
|
|
const bytes = new Uint8Array(24)
|
|
crypto.getRandomValues(bytes)
|
|
values.value.usb_kiosk_token =
|
|
Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('')
|
|
tokenJustGenerated.value = true
|
|
}
|
|
const saving = ref(false)
|
|
const message = ref('')
|
|
const error = ref('')
|
|
|
|
function asBool (value) {
|
|
return String(value).toLowerCase() === 'true' || value === true || value === 1
|
|
}
|
|
|
|
async function load () {
|
|
try {
|
|
const response = await settingsApi.list({ category: 'usb' })
|
|
for (const row of (response.data.data || [])) {
|
|
if (!KEYS.includes(row.key)) continue
|
|
// The token is text (the server sends a mask once set); only the two
|
|
// switches are booleans.
|
|
values.value[row.key] = row.key === 'usb_kiosk_token'
|
|
? (row.value || '')
|
|
: asBool(row.value)
|
|
}
|
|
} catch (loadError) {
|
|
error.value = 'Could not load USB settings.'
|
|
}
|
|
}
|
|
|
|
async function save () {
|
|
saving.value = true
|
|
message.value = ''
|
|
error.value = ''
|
|
try {
|
|
for (const key of KEYS) {
|
|
// Saving the token as true/false replaced the real token with the word
|
|
// 'true', and every provisioned kiosk then got 404. The server keeps the
|
|
// stored token when it receives the mask back unchanged.
|
|
const value = key === 'usb_kiosk_token'
|
|
? String(values.value[key] ?? '')
|
|
: (values.value[key] ? 'true' : 'false')
|
|
await settingsApi.update(key, value)
|
|
}
|
|
message.value = 'Settings saved'
|
|
} catch (saveError) {
|
|
error.value = saveError.response?.data?.data?.error?.message || 'Save failed'
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(load)
|
|
</script>
|
|
|
|
<style scoped>
|
|
.token-row { display: flex; gap: 8px; align-items: center; }
|
|
.token-row .form-control { flex: 1 1 auto; }
|
|
.token-warn { color: var(--warning-dark); }
|
|
|
|
.field-hint { color: var(--text-light); font-size: 0.85rem; margin-top: 0.25rem; }
|
|
.check-row { display: flex; align-items: center; gap: 0.5rem; font-weight: 600; }
|
|
.check-row input { width: auto; }
|
|
.form-actions { margin-top: 1rem; }
|
|
</style>
|