Add endpoint software delivery: run_script and update_app push
Adds the two delivery actions to the endpoints command allowlist, each gated on a new endpoints.deploy permission ON TOP OF endpoints.command so pushing software is a heavier grant than a routine toggle. Enqueue validates update_app appversionid ownership and installer presence before queueing. New collector-authenticated download routes let the agent fetch installer packages and script bodies by digest rather than pre-staging them or sending bodies over the command wire. Wires the scriptsApi client, the siteagent generator routes, and a temporary dev-only Delivery test bench (view/route/nav) for exercising the path while the production UI is built.
This commit is contained in:
committed by
cproudlock
parent
7ac1772881
commit
4590156f30
@@ -14,6 +14,16 @@
|
||||
================================================================= -->
|
||||
<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
|
||||
@@ -75,8 +85,19 @@
|
||||
<div v-if="secret" class="card form-card secret-card">
|
||||
<h4>Save this key now</h4>
|
||||
<p class="field-hint">
|
||||
It will not be shown again. Configure it as the agent
|
||||
<code>X-API-Key</code> header value.
|
||||
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">
|
||||
@@ -196,18 +217,92 @@
|
||||
<h3>Build site agent</h3>
|
||||
<div class="card form-card">
|
||||
<p class="field-hint">
|
||||
Produce an instance-locked agent executable stamped with this server's
|
||||
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.
|
||||
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 you must drop
|
||||
<code>shopdbagent.exe</code> beside them before deploying. Stage a
|
||||
prebuilt agent under <code>instance/siteagent/</code> to have it ship
|
||||
in the 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">
|
||||
<strong>Coming soon.</strong> The build pipeline is not wired up yet -
|
||||
the C# agent runtime is still under development. Generate a key above
|
||||
and configure it manually as the agent <code>X-API-Key</code> for now.
|
||||
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>
|
||||
<button class="btn btn-secondary" type="button" disabled>
|
||||
Build agent (coming soon)
|
||||
</button>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -255,6 +350,19 @@ 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('')
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return ''
|
||||
const parsed = new Date(value)
|
||||
@@ -268,6 +376,7 @@ function applyStatus(data) {
|
||||
|
||||
onMounted(async () => {
|
||||
adoptbaseurl.value = window.location.origin
|
||||
agentbaseurl.value = window.location.origin
|
||||
try {
|
||||
const response = await api.get('/endpoints/agentkey')
|
||||
applyStatus(response.data.data || {})
|
||||
@@ -279,6 +388,15 @@ onMounted(async () => {
|
||||
} 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
|
||||
} catch (statusError) {
|
||||
console.error(statusError)
|
||||
}
|
||||
})
|
||||
|
||||
async function regenerate() {
|
||||
@@ -299,6 +417,7 @@ async function regenerate() {
|
||||
secret.value = data.secret || ''
|
||||
if (data.secret) {
|
||||
adoptcollectorkey.value = data.secret
|
||||
agentcollectorkey.value = data.secret
|
||||
}
|
||||
message.value = 'Endpoint agent key generated.'
|
||||
} catch (regenError) {
|
||||
@@ -366,6 +485,61 @@ async function downloadAdoptionTool() {
|
||||
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
|
||||
link.download = '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
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -374,7 +548,27 @@ async function downloadAdoptionTool() {
|
||||
.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; }
|
||||
|
||||
Reference in New Issue
Block a user