Add system settings, audit logging, user management, and dark mode fixes

System Settings:
- Add SystemSettings.vue with Zabbix integration, SMTP/email config, SAML SSO settings
- Add Setting model with key-value storage and typed values
- Add settings API with caching

Audit Logging:
- Add AuditLog model tracking user, IP, action, entity changes
- Add comprehensive audit logging to all CRUD operations:
  - Machines, Computers, Equipment, Network devices, VLANs, Subnets
  - Printers, USB devices (including checkout/checkin)
  - Applications, Settings, Users/Roles
- Track old/new values for all field changes
- Mask sensitive values (passwords, tokens) in logs

User Management:
- Add UsersList.vue with full user CRUD
- Add Role management with granular permissions
- Add 41 predefined permissions across 10 categories
- Add users API with roles and permissions endpoints

Reports:
- Add TonerReport.vue for printer supply monitoring

Dark Mode Fixes:
- Fix map position section in PCForm, PrinterForm
- Fix alert-warning in KnowledgeBaseDetail
- All components now use CSS variables for theming

CLI Commands:
- Add flask seed permissions
- Add flask seed settings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-02-04 22:16:56 -05:00
parent 9efdb5f52d
commit e18c7c2d87
40 changed files with 4221 additions and 39 deletions

View File

@@ -6,6 +6,8 @@ from typing import Dict, List, Optional
import requests
from flask import current_app
from shopdb.extensions import cache
logger = logging.getLogger(__name__)
@@ -14,20 +16,70 @@ class ZabbixService:
Zabbix API service for real-time printer supply lookups.
Queries Zabbix by IP address to get current supply levels.
No caching - always returns live data.
Use getsuppliesbyip_cached() for cached lookups or
getsuppliesbyip() for live data.
Configuration:
ZABBIX_ENABLED: Set to True to enable Zabbix integration (default: False)
ZABBIX_URL: Zabbix API URL (e.g., http://zabbix.example.com:8080)
ZABBIX_TOKEN: Zabbix API authentication token
"""
CACHE_TTL = 600 # 10 minutes
REACHABLE_CHECK_TTL = 60 # Check reachability every 60 seconds
def __init__(self):
self._url = None
self._token = None
self._enabled = None
@property
def isenabled(self) -> bool:
"""Check if Zabbix integration is enabled."""
# Check database setting first, fall back to env var
from shopdb.core.models import Setting
db_enabled = Setting.get('zabbix_enabled')
if db_enabled is not None:
return bool(db_enabled)
# Fall back to env var for backwards compatibility
return current_app.config.get('ZABBIX_ENABLED', False)
@property
def isconfigured(self) -> bool:
"""Check if Zabbix is configured."""
self._url = current_app.config.get('ZABBIX_URL')
self._token = current_app.config.get('ZABBIX_TOKEN')
"""Check if Zabbix is enabled and configured."""
if not self.isenabled:
return False
# Check database settings first, fall back to env vars
from shopdb.core.models import Setting
self._url = Setting.get('zabbix_url') or current_app.config.get('ZABBIX_URL')
self._token = Setting.get('zabbix_token') or current_app.config.get('ZABBIX_TOKEN')
return bool(self._url and self._token)
@property
def isreachable(self) -> bool:
"""Check if Zabbix is reachable (cached for 60 seconds)."""
if not self.isenabled or not self.isconfigured:
return False
cache_key = 'zabbix_reachable'
cached = cache.get(cache_key)
if cached is not None:
return cached
# Quick connectivity check with 500ms timeout
try:
response = requests.get(
f"{self._url}/api_jsonrpc.php",
timeout=0.5
)
reachable = response.status_code in (200, 401, 403, 405)
except requests.RequestException:
reachable = False
cache.set(cache_key, reachable, timeout=self.REACHABLE_CHECK_TTL)
logger.debug(f"Zabbix reachability check: {reachable}")
return reachable
def _apicall(self, method: str, params: Dict) -> Optional[Dict]:
"""Make a Zabbix API call."""
if not self.isconfigured:
@@ -46,7 +98,7 @@ class ZabbixService:
f"{self._url}/api_jsonrpc.php",
json=payload,
headers={'Content-Type': 'application/json'},
timeout=10
timeout=0.5 # 500ms timeout - fail fast if Zabbix is slow/unreachable
)
response.raise_for_status()
data = response.json()
@@ -131,3 +183,21 @@ class ZabbixService:
"""Get Zabbix host ID for an IP address."""
host = self.gethostbyip(ip)
return host['hostid'] if host else None
def getsuppliesbyip_cached(self, ip: str) -> Optional[List[Dict]]:
"""Get printer supply levels with caching (10-minute TTL)."""
cache_key = f'zabbix_supplies_{ip}'
result = cache.get(cache_key)
if result is not None:
return result
result = self.getsuppliesbyip(ip)
if result is not None:
cache.set(cache_key, result, timeout=self.CACHE_TTL)
return result
def clearcache(self, ip: str = None):
"""Clear cached supply data for one IP or all."""
if ip:
cache.delete(f'zabbix_supplies_{ip}')
cache.delete('printers_low_supplies')