990 lines
35 KiB
Vue
990 lines
35 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<h2>Endpoints</h2>
|
|
</div>
|
|
|
|
<p class="setting-description">
|
|
Manage how shop-floor endpoint agents authenticate to this server, build
|
|
the agent for this site, and set what it reports and how often.
|
|
</p>
|
|
|
|
<!-- ====================================================================
|
|
Key management
|
|
================================================================= -->
|
|
<section class="settings-section">
|
|
<h3>Key management</h3>
|
|
<p class="key-warning">
|
|
STORE YOUR KEY SECURELY. IT IS SHOWN ONLY ONCE AND CANNOT BE RECOVERED.
|
|
</p>
|
|
<p class="field-hint">
|
|
Before you generate a key, open the <strong>ShopDB Secure Send</strong>
|
|
tool in a second window. As soon as the key appears, paste it into
|
|
Secure Send, generate a secure link, and send that link to yourself in
|
|
Teams so the site leader has a one-time, self-destructing backup. Never
|
|
paste the raw key into a chat message or a plain file.
|
|
</p>
|
|
<p class="field-hint">
|
|
The endpoint agent reports with a single managed key scoped to endpoint
|
|
telemetry only. It is separate from every other collector, so rotating
|
|
or revoking it stops all endpoint-agent traffic immediately without
|
|
touching the PC or GE-Enforce collectors.
|
|
</p>
|
|
|
|
<div class="card form-card">
|
|
<div v-if="message" class="settings-success">{{ message }}</div>
|
|
<div v-if="error" class="error-message">{{ error }}</div>
|
|
|
|
<div v-if="loading" class="field-hint">Loading...</div>
|
|
|
|
<template v-else>
|
|
<div class="form-group">
|
|
<label>Status</label>
|
|
<p v-if="key" class="field-hint">
|
|
Active key <code class="mono">{{ key.tokenprefix }}...</code>,
|
|
created {{ formatDate(key.createddate) }}.
|
|
<template v-if="key.lastusedat">
|
|
Last used {{ formatDate(key.lastusedat) }}.
|
|
</template>
|
|
<template v-else>Not used yet.</template>
|
|
</p>
|
|
<p v-else class="field-hint">
|
|
No endpoint agent key is active. Generate one to let agents report
|
|
with a managed credential.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Bootstrap .env fallback</label>
|
|
<p class="field-hint">
|
|
<template v-if="envkeyfallback">
|
|
<strong>On.</strong> The bootstrap COLLECTOR_API_KEY still
|
|
authorizes endpoint reports. Generating a key turns this off so
|
|
the managed key becomes the only credential.
|
|
</template>
|
|
<template v-else>
|
|
<strong>Off.</strong> Only the managed key above authorizes
|
|
endpoint reports, so revoking it is an instant kill switch.
|
|
</template>
|
|
</p>
|
|
</div>
|
|
|
|
<div class="button-row">
|
|
<button class="btn btn-primary" @click="regenerate"
|
|
:disabled="busy">
|
|
{{ busy ? 'Working...' : (key ? 'Regenerate key' : 'Generate key') }}
|
|
</button>
|
|
<button v-if="key" class="btn btn-danger" @click="revoke"
|
|
:disabled="busy">
|
|
Revoke key
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div v-if="secret" class="card form-card secret-card">
|
|
<h4>Save this key now</h4>
|
|
<p class="field-hint">
|
|
Anyone with this key can report as an endpoint agent for this site.
|
|
Treat it like a password. If it is lost you must regenerate, which
|
|
revokes the old key and forces every agent to be reconfigured.
|
|
</p>
|
|
<p class="field-hint">
|
|
<strong>Recommended:</strong> before you leave this page, open the
|
|
<strong>ShopDB Secure Send</strong> tool in a second window, paste
|
|
this key, generate a secure link, and send that link to yourself in
|
|
Teams. That gives the site leader a one-time, self-destructing copy to
|
|
fall back on instead of pasting the raw key into a chat or a file.
|
|
</p>
|
|
<p class="field-hint">
|
|
Configure it as the agent <code>X-API-Key</code> header value.
|
|
</p>
|
|
<p class="mono secret-value">{{ secret }}</p>
|
|
<button class="btn btn-secondary" @click="copySecret">
|
|
{{ copied ? 'Copied' : 'Copy to clipboard' }}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="card form-card">
|
|
<details class="collapsible">
|
|
<summary><h4>How it works</h4></summary>
|
|
<ul class="field-hint info-list">
|
|
<li>
|
|
<strong>Generate</strong> mints a fresh key and, on the first
|
|
generation, turns off the bootstrap <code>.env</code> fallback so
|
|
the managed key becomes the only accepted credential. The full
|
|
secret is shown once - store it in the agent config as the
|
|
<code>X-API-Key</code> header.
|
|
</li>
|
|
<li>
|
|
<strong>Regenerate</strong> is a hard cutover: the previous key is
|
|
revoked in the same moment the new one is minted. Every deployed
|
|
agent must be reconfigured with the new key before it can report
|
|
again.
|
|
</li>
|
|
<li>
|
|
<strong>Revoke</strong> deactivates the key and leaves it off. All
|
|
endpoint-agent reporting stops until you generate a new key. This is
|
|
the kill switch.
|
|
</li>
|
|
</ul>
|
|
</details>
|
|
|
|
<details class="collapsible">
|
|
<summary><h4>When to rotate</h4></summary>
|
|
<ul class="field-hint info-list">
|
|
<li>
|
|
<strong>An installer or agent left the secure environment.</strong>
|
|
If a build carrying the key is copied off the shop floor - onto an
|
|
engineer's laptop, a USB stick, or a machine that leaves the site -
|
|
treat the key as compromised and regenerate immediately.
|
|
</li>
|
|
<li>
|
|
<strong>A device is lost, stolen, or decommissioned</strong> while
|
|
still holding the key, and you cannot confirm the key was wiped.
|
|
</li>
|
|
<li>
|
|
<strong>Staff offboarding.</strong> Someone who could read the key
|
|
from a config or build pipeline is leaving; rotate as part of the
|
|
offboarding checklist.
|
|
</li>
|
|
<li>
|
|
<strong>Scheduled hygiene.</strong> Rotate on a routine cadence
|
|
(for example quarterly) so a silently leaked key has a bounded
|
|
lifetime.
|
|
</li>
|
|
<li>
|
|
<strong>Suspicious traffic.</strong> The Fleet view shows reports
|
|
from a host you do not recognize - revoke now, investigate, then
|
|
regenerate and redeploy to known-good endpoints.
|
|
</li>
|
|
</ul>
|
|
</details>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ====================================================================
|
|
Adoption tool
|
|
================================================================= -->
|
|
<section class="settings-section">
|
|
<h3>Adoption tool</h3>
|
|
<div class="card form-card">
|
|
<p class="field-hint">
|
|
Generate a one-shot, agentless enrollment tool. Run it once on a PC and
|
|
it creates a ShopDB PC and Endpoint entry using only the collector key -
|
|
no service, no polling, no ongoing telemetry. Use this for machines you
|
|
want inventoried without running the full endpoint agent.
|
|
</p>
|
|
<p class="field-hint">
|
|
Paste a collector key below (generate one above first; the secret is
|
|
only shown once). The key is embedded in the downloaded config and is
|
|
not stored on the server.
|
|
</p>
|
|
|
|
<label class="field-label" for="adopt-baseurl">Server base URL</label>
|
|
<input
|
|
id="adopt-baseurl"
|
|
v-model="adoptbaseurl"
|
|
class="form-control"
|
|
type="text"
|
|
placeholder="https://shopdb.example"
|
|
/>
|
|
|
|
<label class="field-label" for="adopt-key">Collector key</label>
|
|
<input
|
|
id="adopt-key"
|
|
v-model="adoptcollectorkey"
|
|
class="form-control mono"
|
|
type="text"
|
|
placeholder="Paste the endpoint agent key"
|
|
/>
|
|
|
|
<div class="button-row">
|
|
<button
|
|
class="btn btn-primary"
|
|
type="button"
|
|
:disabled="adoptbusy || !adoptbaseurl || !adoptcollectorkey"
|
|
@click="downloadAdoptionTool"
|
|
>
|
|
{{ adoptbusy ? 'Building...' : 'Download adoption tool' }}
|
|
</button>
|
|
</div>
|
|
<p v-if="adoptmessage" class="field-hint">{{ adoptmessage }}</p>
|
|
<p v-if="adopterror" class="field-error">{{ adopterror }}</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ====================================================================
|
|
Build site agent
|
|
================================================================= -->
|
|
<section class="settings-section">
|
|
<h3>Build site agent</h3>
|
|
<div class="card form-card">
|
|
<p class="field-hint">
|
|
Produce an instance-locked agent package stamped with this server's
|
|
URL, the endpoint agent key, and the reporting cadence, so it deploys
|
|
with no manual configuration on the client. The download includes the
|
|
agent, a non-secret <code>appsettings.json</code>, and an
|
|
<code>install.ps1</code> that writes the credentials to the registry
|
|
(<code>HKLM\SOFTWARE\GE\ShopDB</code>) - the key never lands in
|
|
appsettings.
|
|
</p>
|
|
<p v-if="!agentstaged" class="field-hint">
|
|
<strong>No agent binary is staged on this server.</strong> The package
|
|
will still build with config and installer, but no agent will ship
|
|
inside it. Upload a current release in the binary section below to
|
|
have it included in every download.
|
|
</p>
|
|
|
|
<label class="field-label" for="agent-baseurl">Server base URL</label>
|
|
<input
|
|
id="agent-baseurl"
|
|
v-model="agentbaseurl"
|
|
class="form-control"
|
|
type="text"
|
|
placeholder="https://shopdb.example"
|
|
/>
|
|
|
|
<label class="field-label" for="agent-key">Endpoint agent key</label>
|
|
<input
|
|
id="agent-key"
|
|
v-model="agentcollectorkey"
|
|
class="form-control mono"
|
|
type="text"
|
|
placeholder="Paste the endpoint agent key generated above"
|
|
/>
|
|
|
|
<label class="field-label" for="agent-cadence">
|
|
Reporting cadence (seconds)
|
|
</label>
|
|
<input
|
|
id="agent-cadence"
|
|
v-model.number="agentcadenceseconds"
|
|
class="form-control"
|
|
type="number"
|
|
min="60"
|
|
/>
|
|
|
|
<label v-if="certavailable" class="checkbox-row">
|
|
<input v-model="agentbundlecert" type="checkbox" />
|
|
<span>
|
|
Bundle this server's TLS certificate and trust it on the endpoint.
|
|
Enable only for self-signed or internal-CA servers; leave off when
|
|
the server uses a publicly trusted certificate.
|
|
</span>
|
|
</label>
|
|
|
|
<label class="field-label" for="agent-cert">
|
|
Server TLS certificate (optional)
|
|
</label>
|
|
<input
|
|
id="agent-cert"
|
|
ref="agentcertinput"
|
|
class="form-control"
|
|
type="file"
|
|
accept=".crt,.pem,.cer"
|
|
@change="onAgentCertChange"
|
|
/>
|
|
<p class="field-hint">
|
|
For a reverse-proxy or internal-CA server this app cannot see, upload
|
|
the server's public certificate (or issuing CA) in PEM format. It is
|
|
validated, bundled with the package, and imported into the endpoint's
|
|
Trusted Root store by <code>install.ps1</code>. Leave empty when the
|
|
server uses a publicly trusted certificate. An upload takes precedence
|
|
over the bundled server certificate above.
|
|
</p>
|
|
|
|
<div class="button-row">
|
|
<button
|
|
class="btn btn-primary"
|
|
type="button"
|
|
:disabled="agentbusy || !agentbaseurl || !agentcollectorkey"
|
|
@click="buildSiteAgent"
|
|
>
|
|
{{ agentbusy ? 'Building...' : 'Build site agent' }}
|
|
</button>
|
|
</div>
|
|
<p v-if="agentmessage" class="field-hint">{{ agentmessage }}</p>
|
|
<p v-if="agenterror" class="field-error">{{ agenterror }}</p>
|
|
|
|
<!-- ================================================================
|
|
Agent binary versions: upload a new build, keep the last few,
|
|
and roll the current pointer back to a retained version. Staging
|
|
only changes what a FUTURE download contains; it does not touch
|
|
any running endpoint. Pushing to live endpoints is a separate,
|
|
heavier grant handled elsewhere. Collapsed by default - binary
|
|
management is infrequent next to building a package. -->
|
|
<details class="collapsible binary-manager">
|
|
<summary><h4>Upload New/Legacy Agents</h4></summary>
|
|
<div
|
|
v-for="binary in binaries"
|
|
:key="binary.key"
|
|
class="card binary-block"
|
|
>
|
|
<h4 class="binary-title">{{ binary.label }}</h4>
|
|
<p class="field-hint">
|
|
Upload {{ binary.name }} updated EXE. Uploading here will update the
|
|
binaries available for your "Build Site Agent" install package.
|
|
</p>
|
|
<p class="field-hint">
|
|
Current staged version:
|
|
<strong>{{ binary.current || 'none staged yet - upload a build below' }}</strong>
|
|
</p>
|
|
|
|
<!-- Current release upload. The version is auto-detected from the
|
|
EXE's PE version resource on select; the operator can override.
|
|
This upload becomes the current staged build. -->
|
|
<label class="field-label">
|
|
Upload {{ binary.name }} binary current release
|
|
</label>
|
|
<div class="binary-upload">
|
|
<input
|
|
class="form-control"
|
|
type="file"
|
|
accept=".exe"
|
|
@change="onBinaryFileChange(binary.key, $event)"
|
|
/>
|
|
<input
|
|
v-model="binary.version"
|
|
class="form-control"
|
|
type="text"
|
|
:placeholder="binary.detecting
|
|
? 'Reading version...'
|
|
: 'Version, auto-detected from EXE'"
|
|
/>
|
|
<button
|
|
class="btn btn-secondary"
|
|
type="button"
|
|
:disabled="binary.busy || binary.detecting || !binary.file"
|
|
@click="uploadBinary(binary.key)"
|
|
>
|
|
{{ binary.busy ? 'Uploading...' : 'Upload current release' }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Rollback / legacy path. Stages a build into the version list
|
|
without displacing the current release so it is available to
|
|
roll back to. WARNING copy per intent: the version must be
|
|
confirmed from the EXE properties before uploading. -->
|
|
<label class="field-label">
|
|
Upload legacy build for rollback
|
|
</label>
|
|
<p class="field-warning">
|
|
The version is read from the EXE itself. Leave this blank to use the
|
|
binary's own file version, or enter a dotted-numeric override only
|
|
when the build carries no readable version.
|
|
</p>
|
|
<div class="binary-upload">
|
|
<input
|
|
class="form-control"
|
|
type="file"
|
|
accept=".exe"
|
|
@change="onLegacyFileChange(binary.key, $event)"
|
|
/>
|
|
<input
|
|
v-model="binary.legacyversion"
|
|
class="form-control"
|
|
type="text"
|
|
placeholder="Version, e.g. 1.5.0"
|
|
/>
|
|
<button
|
|
class="btn btn-secondary"
|
|
type="button"
|
|
:disabled="binary.busy || !binary.legacyfile"
|
|
@click="uploadLegacy(binary.key)"
|
|
>
|
|
{{ binary.busy ? 'Uploading...' : 'Upload legacy build' }}
|
|
</button>
|
|
</div>
|
|
<p v-if="binary.message" class="field-hint">{{ binary.message }}</p>
|
|
<p v-if="binary.error" class="field-error">{{ binary.error }}</p>
|
|
|
|
<!-- Version selector: pick the staged build shipped by the next
|
|
download. Ordered by version number (highest first). -->
|
|
<div v-if="binary.versions.length" class="binary-upload">
|
|
<label class="field-label" :for="`select-${binary.key}`">
|
|
Active version
|
|
</label>
|
|
<select
|
|
:id="`select-${binary.key}`"
|
|
class="form-control"
|
|
:value="binary.current || ''"
|
|
:disabled="binary.busy"
|
|
@change="setCurrentBinary(binary.key, $event.target.value)"
|
|
>
|
|
<option
|
|
v-for="entry in binary.versions"
|
|
:key="entry.version"
|
|
:value="entry.version"
|
|
>
|
|
{{ entry.version }}{{ entry.iscurrent ? ' (current)' : '' }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div v-if="binary.versions.length" class="previous-versions">
|
|
<button
|
|
class="btn-link"
|
|
type="button"
|
|
@click="binary.showprevious = !binary.showprevious"
|
|
>
|
|
{{ binary.showprevious ? 'Hide' : 'Show' }} staged versions
|
|
({{ binary.versions.length }})
|
|
</button>
|
|
<table v-if="binary.showprevious" class="versions-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Version</th>
|
|
<th>Uploaded</th>
|
|
<th>Size</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="entry in binary.versions" :key="entry.version">
|
|
<td class="mono">{{ entry.version }}</td>
|
|
<td>{{ formatDate(entry.uploadedat) }}</td>
|
|
<td>{{ formatBytes(entry.size) }}</td>
|
|
<td>
|
|
<span v-if="entry.iscurrent" class="current-tag">current</span>
|
|
<button
|
|
v-else
|
|
class="btn-link"
|
|
type="button"
|
|
:disabled="binary.busy"
|
|
@click="setCurrentBinary(binary.key, entry.version)"
|
|
>
|
|
Make current
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ====================================================================
|
|
Agent configuration
|
|
================================================================= -->
|
|
<section class="settings-section">
|
|
<h3>Agent configuration</h3>
|
|
<div class="card form-card">
|
|
<p class="field-hint">
|
|
Define what the agent collects and how often: reporting cadence,
|
|
monitored processes and services, the curated Windows event list, and
|
|
whether unknown hosts self-adopt on first contact. These settings are
|
|
stamped into the build above so a deployed agent needs no local edits.
|
|
</p>
|
|
<p class="field-hint">
|
|
Monitored processes and services are defined as reusable monitors and
|
|
attached to PC types through site policies, so a deployed agent fetches
|
|
the right set for its type.
|
|
</p>
|
|
<button
|
|
class="btn btn-secondary"
|
|
type="button"
|
|
@click="editConfiguration"
|
|
>
|
|
Edit configuration
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import api from '@/api'
|
|
|
|
const router = useRouter()
|
|
const loading = ref(true)
|
|
const busy = ref(false)
|
|
const key = ref(null)
|
|
const envkeyfallback = ref(true)
|
|
const secret = ref('')
|
|
const copied = ref(false)
|
|
const message = ref('')
|
|
const error = ref('')
|
|
|
|
// Adoption tool: build a one-shot, agentless enrollment download.
|
|
const adoptbaseurl = ref('')
|
|
const adoptcollectorkey = ref('')
|
|
const adoptbusy = ref(false)
|
|
const adoptmessage = ref('')
|
|
const adopterror = ref('')
|
|
|
|
// Site agent generator: build an instance-locked, resident endpoint agent.
|
|
const agentbaseurl = ref('')
|
|
const agentcollectorkey = ref('')
|
|
const agentcadenceseconds = ref(900)
|
|
const agentstaged = ref(false)
|
|
const certavailable = ref(false)
|
|
const agentbundlecert = ref(false)
|
|
const agentcertinput = ref(null)
|
|
const agentcertfile = ref(null)
|
|
const agentbusy = ref(false)
|
|
const agentmessage = ref('')
|
|
const agenterror = ref('')
|
|
|
|
// Versioned agent/tray binaries. Each block owns its own upload form, retained
|
|
// version list, and busy/message state so the two rows act independently. The
|
|
// 'key' matches the backend binkey (agent | tray).
|
|
const binaries = ref([
|
|
{ key: 'agent', label: 'ShopDB Agent (shopdbagent.exe)',
|
|
name: 'ShopDB Agent',
|
|
current: null, versions: [], version: '', file: '', detecting: false,
|
|
legacyversion: '', legacyfile: '',
|
|
busy: false, message: '', error: '', showprevious: false },
|
|
{ key: 'tray', label: 'Tray Agent (shopdb-endpoint-tray.exe)',
|
|
name: 'Tray Agent',
|
|
current: null, versions: [], version: '', file: '', detecting: false,
|
|
legacyversion: '', legacyfile: '',
|
|
busy: false, message: '', error: '', showprevious: false },
|
|
])
|
|
|
|
function binaryFor(binkey) {
|
|
return binaries.value.find((entry) => entry.key === binkey)
|
|
}
|
|
|
|
function formatBytes(value) {
|
|
if (!value && value !== 0) return ''
|
|
if (value < 1024) return value + ' B'
|
|
if (value < 1024 * 1024) return (value / 1024).toFixed(1) + ' KB'
|
|
return (value / (1024 * 1024)).toFixed(1) + ' MB'
|
|
}
|
|
|
|
function formatDate(value) {
|
|
if (!value) return ''
|
|
const parsed = new Date(value)
|
|
return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString()
|
|
}
|
|
|
|
function filenameFromResponse(response, fallback) {
|
|
const disposition = response.headers?.['content-disposition'] || ''
|
|
const match = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition)
|
|
return match ? decodeURIComponent(match[1]) : fallback
|
|
}
|
|
|
|
function applyStatus(data) {
|
|
key.value = data.key || null
|
|
envkeyfallback.value = !!data.envkeyfallback
|
|
}
|
|
|
|
onMounted(async () => {
|
|
adoptbaseurl.value = window.location.origin
|
|
agentbaseurl.value = window.location.origin
|
|
try {
|
|
const response = await api.get('/endpoints/agentkey')
|
|
applyStatus(response.data.data || {})
|
|
} catch (loadError) {
|
|
error.value = loadError.response?.status === 403
|
|
? 'Only an administrator can manage the endpoint agent key'
|
|
: 'Could not load the endpoint agent key status'
|
|
console.error(loadError)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
try {
|
|
const response = await api.get('/endpoints/siteagent')
|
|
const status = response.data.data || {}
|
|
agentstaged.value = !!status.agentstaged
|
|
certavailable.value = !!status.certavailable
|
|
agentbundlecert.value = !!status.certavailable
|
|
applyBinaryStatus('agent', status.agentcurrent, status.agentversions)
|
|
applyBinaryStatus('tray', status.traycurrent, status.trayversions)
|
|
} catch (statusError) {
|
|
console.error(statusError)
|
|
}
|
|
})
|
|
|
|
function applyBinaryStatus(binkey, current, versions) {
|
|
const binary = binaryFor(binkey)
|
|
if (!binary) return
|
|
binary.current = current || null
|
|
binary.versions = Array.isArray(versions) ? versions : []
|
|
}
|
|
|
|
async function regenerate() {
|
|
if (key.value &&
|
|
!window.confirm('Regenerate the key? The current key stops working '
|
|
+ 'immediately and every agent must be reconfigured.')) {
|
|
return
|
|
}
|
|
busy.value = true
|
|
message.value = ''
|
|
error.value = ''
|
|
secret.value = ''
|
|
copied.value = false
|
|
try {
|
|
const response = await api.post('/endpoints/agentkey/regenerate')
|
|
const data = response.data.data || {}
|
|
applyStatus(data)
|
|
secret.value = data.secret || ''
|
|
if (data.secret) {
|
|
adoptcollectorkey.value = data.secret
|
|
agentcollectorkey.value = data.secret
|
|
}
|
|
message.value = 'Endpoint agent key generated.'
|
|
} catch (regenError) {
|
|
error.value = regenError.response?.data?.data?.error?.message
|
|
|| 'Could not generate the endpoint agent key'
|
|
} finally {
|
|
busy.value = false
|
|
}
|
|
}
|
|
|
|
async function revoke() {
|
|
if (!window.confirm('Revoke the key? All endpoint-agent reporting stops '
|
|
+ 'until a new key is generated.')) {
|
|
return
|
|
}
|
|
busy.value = true
|
|
message.value = ''
|
|
error.value = ''
|
|
secret.value = ''
|
|
try {
|
|
const response = await api.delete('/endpoints/agentkey')
|
|
applyStatus(response.data.data || {})
|
|
message.value = 'Endpoint agent key revoked.'
|
|
} catch (revokeError) {
|
|
error.value = revokeError.response?.data?.data?.error?.message
|
|
|| 'Could not revoke the endpoint agent key'
|
|
} finally {
|
|
busy.value = false
|
|
}
|
|
}
|
|
|
|
async function copySecret() {
|
|
try {
|
|
await navigator.clipboard.writeText(secret.value)
|
|
copied.value = true
|
|
} catch (copyError) {
|
|
console.error(copyError)
|
|
}
|
|
}
|
|
|
|
async function downloadAdoptionTool() {
|
|
adoptbusy.value = true
|
|
adoptmessage.value = ''
|
|
adopterror.value = ''
|
|
try {
|
|
const response = await api.post(
|
|
'/endpoints/adoptiontool/package',
|
|
{ baseurl: adoptbaseurl.value, collectorkey: adoptcollectorkey.value },
|
|
{ responseType: 'blob' })
|
|
const url = window.URL.createObjectURL(response.data)
|
|
const link = document.createElement('a')
|
|
link.href = url
|
|
link.download = 'shopdb-adoption-tool.zip'
|
|
document.body.appendChild(link)
|
|
link.click()
|
|
link.remove()
|
|
window.URL.revokeObjectURL(url)
|
|
adoptmessage.value = 'Adoption tool downloaded.'
|
|
} catch (downloadError) {
|
|
adopterror.value = downloadError.response?.status === 403
|
|
? 'Only an administrator can build the adoption tool'
|
|
: 'Could not build the adoption tool'
|
|
console.error(downloadError)
|
|
} finally {
|
|
adoptbusy.value = false
|
|
}
|
|
}
|
|
|
|
function onAgentCertChange(event) {
|
|
const files = event.target.files
|
|
agentcertfile.value = files && files.length ? files[0] : null
|
|
}
|
|
|
|
async function readBlobError(blob) {
|
|
try {
|
|
const parsed = JSON.parse(await blob.text())
|
|
return parsed.data?.error?.message || parsed.message
|
|
|| 'Could not build the site agent'
|
|
} catch (parseError) {
|
|
return 'Could not build the site agent'
|
|
}
|
|
}
|
|
|
|
async function buildSiteAgent() {
|
|
agentbusy.value = true
|
|
agentmessage.value = ''
|
|
agenterror.value = ''
|
|
try {
|
|
const form = new FormData()
|
|
form.append('baseurl', agentbaseurl.value)
|
|
form.append('collectorkey', agentcollectorkey.value)
|
|
form.append('cadenceseconds', agentcadenceseconds.value)
|
|
form.append('bundlecert', agentbundlecert.value ? 'true' : 'false')
|
|
if (agentcertfile.value) {
|
|
form.append('certfile', agentcertfile.value)
|
|
}
|
|
const response = await api.post(
|
|
'/endpoints/siteagent/package',
|
|
form,
|
|
{ responseType: 'blob' })
|
|
const url = window.URL.createObjectURL(response.data)
|
|
const link = document.createElement('a')
|
|
link.href = url
|
|
// Honor the server's versioned download name (shopdb-endpoint-agent-<ver>.zip)
|
|
// from Content-Disposition so operators see the agent version in the file
|
|
// name; fall back to the fixed name when the header is absent.
|
|
link.download = filenameFromResponse(response, 'shopdb-endpoint-agent.zip')
|
|
document.body.appendChild(link)
|
|
link.click()
|
|
link.remove()
|
|
window.URL.revokeObjectURL(url)
|
|
agentmessage.value = 'Site agent built.'
|
|
} catch (buildError) {
|
|
if (buildError.response?.status === 400 && buildError.response?.data) {
|
|
agenterror.value = await readBlobError(buildError.response.data)
|
|
} else {
|
|
agenterror.value = buildError.response?.status === 403
|
|
? 'Only an administrator can build the site agent'
|
|
: 'Could not build the site agent'
|
|
}
|
|
console.error(buildError)
|
|
} finally {
|
|
agentbusy.value = false
|
|
}
|
|
}
|
|
|
|
async function detectVersion(binkey, file) {
|
|
const form = new FormData()
|
|
form.append('file', file)
|
|
const response = await api.post(
|
|
`/endpoints/siteagent/binaries/${binkey}/detect`, form,
|
|
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
|
return response.data.data?.version || ''
|
|
}
|
|
|
|
function onBinaryFileChange(binkey, event) {
|
|
const binary = binaryFor(binkey)
|
|
if (!binary) return
|
|
const files = event.target.files
|
|
binary.file = files && files.length ? files[0] : ''
|
|
binary.error = ''
|
|
if (!binary.file) return
|
|
binary.detecting = true
|
|
detectVersion(binkey, binary.file)
|
|
.then((version) => {
|
|
if (version) binary.version = version
|
|
})
|
|
.catch((detectError) => {
|
|
console.error(detectError)
|
|
})
|
|
.finally(() => {
|
|
binary.detecting = false
|
|
})
|
|
}
|
|
|
|
function onLegacyFileChange(binkey, event) {
|
|
const binary = binaryFor(binkey)
|
|
if (!binary) return
|
|
const files = event.target.files
|
|
binary.legacyfile = files && files.length ? files[0] : ''
|
|
}
|
|
|
|
async function uploadBinary(binkey) {
|
|
const binary = binaryFor(binkey)
|
|
if (!binary || !binary.file) return
|
|
binary.busy = true
|
|
binary.message = ''
|
|
binary.error = ''
|
|
try {
|
|
const form = new FormData()
|
|
form.append('version', binary.version)
|
|
form.append('file', binary.file)
|
|
const response = await api.post(
|
|
`/endpoints/siteagent/binaries/${binkey}`, form,
|
|
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
|
const data = response.data.data || {}
|
|
applyBinaryStatus(binkey, data.current, data.versions)
|
|
binary.version = ''
|
|
binary.file = ''
|
|
binary.message = 'Uploaded and set as current.'
|
|
} catch (uploadError) {
|
|
binary.error = uploadError.response?.data?.data?.error?.message
|
|
|| (uploadError.response?.status === 403
|
|
? 'You do not have permission to upload agent binaries'
|
|
: 'Could not upload the binary')
|
|
console.error(uploadError)
|
|
} finally {
|
|
binary.busy = false
|
|
}
|
|
}
|
|
|
|
async function uploadLegacy(binkey) {
|
|
const binary = binaryFor(binkey)
|
|
if (!binary || !binary.legacyfile) return
|
|
binary.busy = true
|
|
binary.message = ''
|
|
binary.error = ''
|
|
try {
|
|
const form = new FormData()
|
|
form.append('version', binary.legacyversion)
|
|
form.append('file', binary.legacyfile)
|
|
form.append('makecurrent', 'false')
|
|
const response = await api.post(
|
|
`/endpoints/siteagent/binaries/${binkey}`, form,
|
|
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
|
const data = response.data.data || {}
|
|
applyBinaryStatus(binkey, data.current, data.versions)
|
|
binary.legacyversion = ''
|
|
binary.legacyfile = ''
|
|
binary.message = 'Legacy build staged for rollback.'
|
|
} catch (uploadError) {
|
|
binary.error = uploadError.response?.data?.data?.error?.message
|
|
|| (uploadError.response?.status === 403
|
|
? 'You do not have permission to upload agent binaries'
|
|
: 'Could not upload the legacy build')
|
|
console.error(uploadError)
|
|
} finally {
|
|
binary.busy = false
|
|
}
|
|
}
|
|
|
|
async function setCurrentBinary(binkey, version) {
|
|
const binary = binaryFor(binkey)
|
|
if (!binary) return
|
|
if (!window.confirm(`Make version ${version} the current staged build? `
|
|
+ 'New downloads will ship this version.')) {
|
|
return
|
|
}
|
|
binary.busy = true
|
|
binary.message = ''
|
|
binary.error = ''
|
|
try {
|
|
const response = await api.put(
|
|
`/endpoints/siteagent/binaries/${binkey}/current`, { version })
|
|
const data = response.data.data || {}
|
|
applyBinaryStatus(binkey, data.current, data.versions)
|
|
binary.message = `Version ${version} is now current.`
|
|
} catch (setError) {
|
|
binary.error = setError.response?.data?.data?.error?.message
|
|
|| 'Could not change the current version'
|
|
console.error(setError)
|
|
} finally {
|
|
binary.busy = false
|
|
}
|
|
}
|
|
|
|
function editConfiguration() {
|
|
router.push({ name: 'endpoints-monitors-settings' })
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.mono { font-family: monospace; word-break: break-all; }
|
|
.form-card h3 { margin-top: 0; }
|
|
.form-card h4 { margin-top: 1rem; margin-bottom: 0.25rem; }
|
|
.form-card h4:first-child { margin-top: 0; }
|
|
.button-row { display: flex; gap: 0.5rem; }
|
|
.checkbox-row {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: flex-start;
|
|
margin: 0.75rem 0;
|
|
font-size: 0.9rem;
|
|
}
|
|
.checkbox-row input { margin-top: 0.2rem; }
|
|
.secret-card { border: 1px solid var(--color-warning, #b8860b); }
|
|
.key-warning {
|
|
margin: 0 0 0.75rem;
|
|
padding: 0.6rem 0.75rem;
|
|
border: 2px solid #ff0000;
|
|
border-radius: 4px;
|
|
background: rgba(255, 0, 0, 0.12);
|
|
color: #ff2d2d;
|
|
font-size: 1.25rem;
|
|
font-weight: 800;
|
|
letter-spacing: 0.02em;
|
|
text-align: center;
|
|
}
|
|
.secret-value { padding: 0.5rem; background: var(--color-surface-2, #1e1e1e); }
|
|
.settings-section { margin-bottom: 2rem; }
|
|
.settings-section > h3 { margin-bottom: 0.25rem; }
|
|
.info-list { margin: 0.25rem 0 0; padding-left: 1.25rem; }
|
|
.info-list li { margin-bottom: 0.5rem; }
|
|
/* Each agent is its own nested card inside the collapsible, so the two
|
|
binaries read as distinct units instead of one continuous wall of
|
|
controls separated by a hairline rule. */
|
|
.binary-block {
|
|
margin-top: 1rem;
|
|
padding: 1rem;
|
|
}
|
|
.binary-block:first-of-type { margin-top: 0.75rem; }
|
|
.binary-title {
|
|
margin: 0 0 0.5rem;
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 1px solid var(--color-border, #333);
|
|
}
|
|
.binary-upload {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
margin: 0.5rem 0;
|
|
}
|
|
.binary-upload .form-control[type="text"] { max-width: 12rem; }
|
|
.previous-versions { margin-top: 0.5rem; }
|
|
.collapsible { margin: 0.5rem 0; }
|
|
.collapsible > summary {
|
|
cursor: pointer;
|
|
list-style: none;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
user-select: none;
|
|
}
|
|
.collapsible > summary::-webkit-details-marker { display: none; }
|
|
.collapsible > summary::before {
|
|
content: "\25be";
|
|
display: inline-block;
|
|
transform: rotate(-90deg);
|
|
transition: transform 0.15s ease;
|
|
color: var(--color-text-muted, #888);
|
|
}
|
|
.collapsible[open] > summary::before { transform: rotate(0deg); }
|
|
.collapsible > summary h4 { margin: 0; }
|
|
.field-warning {
|
|
margin: 0.25rem 0 0.5rem;
|
|
padding: 0.5rem 0.75rem;
|
|
border-left: 3px solid var(--color-warning, #b8860b);
|
|
background: rgba(184, 134, 11, 0.08);
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
}
|
|
.btn-link {
|
|
background: none;
|
|
border: none;
|
|
color: var(--color-link, #4ea1ff);
|
|
cursor: pointer;
|
|
padding: 0;
|
|
font-size: 0.9rem;
|
|
text-decoration: underline;
|
|
}
|
|
.btn-link:disabled { color: var(--color-text-muted, #888); cursor: default; }
|
|
.versions-table {
|
|
width: 100%;
|
|
margin-top: 0.5rem;
|
|
border-collapse: collapse;
|
|
font-size: 0.85rem;
|
|
}
|
|
.versions-table th,
|
|
.versions-table td {
|
|
text-align: left;
|
|
padding: 0.3rem 0.5rem;
|
|
border-bottom: 1px solid var(--color-border, #333);
|
|
}
|
|
.current-tag {
|
|
font-size: 0.8rem;
|
|
color: var(--color-success, #4caf50);
|
|
font-weight: 600;
|
|
}
|
|
</style>
|