163 lines
5.3 KiB
Vue
163 lines
5.3 KiB
Vue
<template>
|
|
<v-card class="span-12 panel-pad">
|
|
<h2 class="section-title mb-1">Alert webhook</h2>
|
|
<p class="quiet text-body-2 mb-3">
|
|
Send every newly raised alert to an external HTTP endpoint as a JSON object (one POST per alert).
|
|
Delivery runs on the alert cycle alongside (or instead of) email. Leave the secret blank to keep the stored value.
|
|
</p>
|
|
<div class="form-grid">
|
|
<v-switch v-model="form.webhook_enabled" label="Enable webhook delivery" color="primary" density="compact" hide-details />
|
|
<div />
|
|
<v-text-field v-model="form.webhook_url" class="full" label="Webhook URL" placeholder="https://example.com/hooks/deprecore" />
|
|
<v-text-field
|
|
v-model="form.webhook_secret"
|
|
type="password"
|
|
:label="settings.webhook_secret_set ? 'Signing secret (unchanged)' : 'Signing secret (optional)'"
|
|
hint="If set, each request is signed with HMAC-SHA256 in the X-DepreCore-Signature header"
|
|
persistent-hint
|
|
autocomplete="new-password"
|
|
/>
|
|
<div class="d-flex align-center">
|
|
<v-btn
|
|
v-if="settings.webhook_secret_set"
|
|
variant="text"
|
|
size="small"
|
|
color="error"
|
|
prepend-icon="mdi-key-remove"
|
|
@click="clearSecret"
|
|
>Clear secret</v-btn>
|
|
</div>
|
|
</div>
|
|
<div class="toolbar-row mt-3">
|
|
<v-btn variant="tonal" prepend-icon="mdi-webhook" :loading="testing" :disabled="!form.webhook_url" @click="sendTest">Send test event</v-btn>
|
|
<v-spacer />
|
|
<v-btn color="primary" prepend-icon="mdi-content-save" :loading="saving" @click="save">Save webhook</v-btn>
|
|
</div>
|
|
<v-alert v-if="status" class="mt-3" density="compact" :type="statusType">{{ status }}</v-alert>
|
|
|
|
<v-divider class="my-4" />
|
|
<div class="quiet text-caption mb-1">Example payload delivered per alert:</div>
|
|
<pre class="payload-sample">{{ samplePayload }}</pre>
|
|
</v-card>
|
|
</template>
|
|
|
|
<script>
|
|
import { apiRequest } from '../services/api';
|
|
|
|
export default {
|
|
props: {
|
|
token: { type: String, required: true }
|
|
},
|
|
data() {
|
|
return {
|
|
settings: {},
|
|
form: {
|
|
webhook_enabled: false,
|
|
webhook_url: '',
|
|
webhook_secret: ''
|
|
},
|
|
saving: false,
|
|
testing: false,
|
|
status: '',
|
|
statusType: 'success',
|
|
samplePayload: JSON.stringify({
|
|
event: 'alert',
|
|
id: 142,
|
|
type: 'pm_overdue',
|
|
severity: 'critical',
|
|
title: 'PM overdue: Lift quarterly service',
|
|
message: 'Lift quarterly service on FA-1042 was due 2026-05-20.',
|
|
due_date: '2026-05-20',
|
|
status: 'open',
|
|
reference_type: 'asset_pm',
|
|
reference_id: 88,
|
|
asset_id: 1042,
|
|
asset_code: 'FA-1042',
|
|
created_at: '2026-06-04T12:00:00.000Z',
|
|
sent_at: '2026-06-04T12:05:00.000Z'
|
|
}, null, 2)
|
|
};
|
|
},
|
|
mounted() {
|
|
this.load();
|
|
},
|
|
methods: {
|
|
async api(path, options = {}) {
|
|
return apiRequest(path, this.token, options);
|
|
},
|
|
async load() {
|
|
const data = await (await this.api('/api/notifications/settings')).json();
|
|
this.settings = data.settings;
|
|
this.form = {
|
|
webhook_enabled: data.settings.webhook_enabled,
|
|
webhook_url: data.settings.webhook_url,
|
|
webhook_secret: ''
|
|
};
|
|
},
|
|
async save() {
|
|
this.saving = true;
|
|
this.status = '';
|
|
try {
|
|
const payload = {
|
|
webhook_enabled: this.form.webhook_enabled,
|
|
webhook_url: this.form.webhook_url
|
|
};
|
|
if (this.form.webhook_secret) payload.webhook_secret = this.form.webhook_secret;
|
|
const data = await (await this.api('/api/notifications/settings', { method: 'PUT', body: JSON.stringify(payload) })).json();
|
|
this.settings = data.settings;
|
|
this.form.webhook_secret = '';
|
|
this.statusType = 'success';
|
|
this.status = 'Webhook settings saved.';
|
|
} catch (error) {
|
|
this.statusType = 'error';
|
|
this.status = error.message;
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
async clearSecret() {
|
|
this.status = '';
|
|
try {
|
|
const data = await (await this.api('/api/notifications/settings', { method: 'PUT', body: JSON.stringify({ webhook_secret_clear: true }) })).json();
|
|
this.settings = data.settings;
|
|
this.form.webhook_secret = '';
|
|
this.statusType = 'success';
|
|
this.status = 'Signing secret cleared.';
|
|
} catch (error) {
|
|
this.statusType = 'error';
|
|
this.status = error.message;
|
|
}
|
|
},
|
|
async sendTest() {
|
|
this.testing = true;
|
|
this.status = '';
|
|
try {
|
|
// Persist the current URL/secret first so the test hits what the user typed.
|
|
await this.save();
|
|
const result = await (await this.api('/api/notifications/webhook-test', { method: 'POST', body: JSON.stringify({}) })).json();
|
|
this.statusType = 'success';
|
|
this.status = `Test event delivered (HTTP ${result.status}).`;
|
|
} catch (error) {
|
|
this.statusType = 'error';
|
|
this.status = `Test failed: ${error.message}`;
|
|
} finally {
|
|
this.testing = false;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.payload-sample {
|
|
margin: 0;
|
|
padding: 12px 14px;
|
|
border-radius: 8px;
|
|
background: rgba(127, 127, 127, 0.1);
|
|
font-size: 0.78rem;
|
|
line-height: 1.45;
|
|
overflow-x: auto;
|
|
white-space: pre;
|
|
}
|
|
</style>
|