Files
shopdb-flask/plugins/printers/frontend/views/ZabbixSettings.vue
Proudlock f9f1d39f08 Consolidate settings into 8 hubs and fix security vulnerabilities
Settings Consolidation (37+ pages to 8 hubs):
- Asset Type Hub: Unified type management with plugin descriptors
- Model Catalog Hub: Vendors, types, models, operating systems
- Asset Configuration Hub: Statuses, relationships, fields, identifiers
- Security Hub: Authentication, tokens, users, audit
- Site Configuration Hub: Site settings, floor map, branding, imports
- Integrations Hub: ServiceNow, Zabbix, Dell warranty
- Locations Hub: Locations, business units, employees
- Network Infrastructure Hub: VLANs, subnets

Plugin Descriptor Pattern:
- Add get_asset_type_descriptor hook to BasePlugin
- Implement descriptors in 6 plugins (printers, computers, machines, network, measuringtools, locations)
- Add /api/pluginui/asset-type-descriptors and core-type-descriptors endpoints
- Generic SubtypesTab component renders CRUD UI from descriptor schema

Security Fixes:
- Fix stored XSS via javascript: URIs in Models and Support Teams
- Add URL scheme validation (allow only http/https) in create/update endpoints
- Fix plugin contract violations in endpoints plugin (use shopdb.api imports)
- Add missing requiresAdmin guard to printer model supplies route
- Add user-facing error messages to API token load failures

Integration Improvements:
- Add persistent tab navigation to ServiceNow, Zabbix, and Dell Warranty settings
- Standardize integration settings UI across all three pages
- Fix tab visibility when navigating between integrations

Tested with 94-agent orchestrated review for ADR compliance and security.
2026-09-11 10:45:12 -04:00

152 lines
4.2 KiB
Vue

<template>
<div>
<div class="page-header">
<h2>Integrations</h2>
<p class="header-description">Configure external service connections and APIs</p>
</div>
<div class="integration-tabs">
<router-link
:to="{ name: 'servicenow-settings' }"
class="integration-tab"
:class="{ active: $route.name === 'servicenow-settings' }"
>
ServiceNow
</router-link>
<router-link
:to="{ name: 'zabbix-settings' }"
class="integration-tab"
:class="{ active: $route.name === 'zabbix-settings' }"
>
Zabbix Supplies
</router-link>
<router-link
:to="{ name: 'dell-warranty-settings' }"
class="integration-tab"
:class="{ active: $route.name === 'dell-warranty-settings' }"
>
Dell Warranty
</router-link>
</div>
<div class="section-card">
<div class="setting-group">
<p class="setting-description">
Connect to Zabbix for real-time printer supply monitoring. When enabled, supply levels
are fetched from Zabbix API and displayed on printer detail pages.
</p>
<div class="setting-row">
<label class="toggle-label">
<span>Enable Zabbix Integration</span>
<button
class="toggle-btn"
:class="{ active: settings.zabbix_enabled }"
@click="toggleSetting('zabbix_enabled')"
:disabled="saving"
>
<span class="toggle-slider"></span>
</button>
</label>
</div>
<div class="setting-row" v-if="settings.zabbix_enabled">
<label>
<span>Zabbix API URL</span>
<input
type="url"
v-model="settings.zabbix_url"
placeholder="http://zabbix.example.com:8080"
@blur="saveSetting('zabbix_url', settings.zabbix_url)"
:disabled="saving"
>
</label>
</div>
<div class="setting-row" v-if="settings.zabbix_enabled">
<label>
<span>Zabbix API Token</span>
<input
type="password"
v-model="settings.zabbix_token"
placeholder="Enter API token"
@blur="saveSetting('zabbix_token', settings.zabbix_token)"
:disabled="saving"
>
</label>
</div>
<div class="status-indicator" v-if="settings.zabbix_enabled">
<span class="status-dot" :class="zabbixStatus"></span>
<span>{{ zabbixMessage }}</span>
</div>
</div>
</div>
<div v-if="error" class="error-message">{{ error }}</div>
<div v-if="success" class="settings-success">{{ success }}</div>
</div>
</template>
<script setup>
import { onMounted, computed } from 'vue'
import { useSystemSettings } from '@/composables/systemSettings'
const {
settings, saving, error, success,
loadSettings, saveSetting, toggleSetting,
} = useSystemSettings()
// Per-page connection status (stays with the page, not the composable).
const zabbixStatus = computed(() => {
if (!settings.zabbix_enabled) return 'inactive'
if (!settings.zabbix_url || !settings.zabbix_token) return 'warning'
return 'pending'
})
const zabbixMessage = computed(() => {
if (!settings.zabbix_enabled) return 'Disabled'
if (!settings.zabbix_url) return 'URL not configured'
if (!settings.zabbix_token) return 'Token not configured'
return 'Configured (connectivity checked on first use)'
})
onMounted(loadSettings)
</script>
<style scoped>
.header-description {
margin: 0.5rem 0 0;
color: var(--text-light);
font-size: 0.95rem;
}
.integration-tabs {
display: flex;
gap: 0.25rem;
border-bottom: 1px solid var(--border-color);
background: var(--bg-secondary);
margin: 2rem 0;
}
.integration-tab {
padding: 0.75rem 1.5rem;
text-decoration: none;
color: var(--text-light);
border-bottom: 2px solid transparent;
transition: all 0.15s;
font-weight: 500;
font-size: 0.95rem;
}
.integration-tab:hover {
color: var(--text);
background: var(--hover-bg);
}
.integration-tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
</style>