- Migration: ajoute sp_route/mode/is_reboot_channel/is_dynamic_dm sur teams_channels, cree table teams_channel_rules (match resp/domain/env/msg_type/hostname pattern) - Service teams_service.py: format texte plat compatible workflows existants, write_sharepoint_notification (ecrit fichier .txt dans <sp_base>/<sp_route>/), resolve_channel_for_server rules-based avec priorite reboot, send_notification orchestre resolution + envoi - Settings UI: CRUD canaux etendu (mode SP/webhook + flags reboot/dyn_dm), CRUD regles avec match conditions, sharepoint_notif_path en secret app, bouton Test ecrit fichier .txt en mode SP - Mode is_dynamic_dm: prefixe le contenu par 'TO: <email>' pour permettre un workflow PA unique qui dispatch dynamiquement aux responsables - Pas d'OAuth requis: PatchCenter ecrit fichiers, Workflows PA cote SharePoint (deja en place pour le .exe) declenchent et postent sur Teams Mode webhook conserve mais inactif tant qu'OAuth Entra ID pas mis en place chez SANEF
881 lines
55 KiB
HTML
881 lines
55 KiB
HTML
{% extends 'base.html' %}
|
||
{% block title %}Settings{% endblock %}
|
||
{% block content %}
|
||
<h2 class="text-xl font-bold text-cyber-accent mb-6">Settings</h2>
|
||
|
||
{% if saved %}
|
||
<div class="mb-4 p-3 rounded bg-green-900/30 text-cyber-green text-sm">
|
||
Section "{{ saved }}" sauvegardee.
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% macro section_header(key, title, badge_text, badge_class, extra="") %}
|
||
<button @click="open = open === '{{ key }}' ? '' : '{{ key }}'" class="w-full flex items-center justify-between p-4 hover:bg-cyber-border/20 transition-colors">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-cyber-accent font-bold">{{ title }}</span>
|
||
<span class="badge {{ badge_class }}">{{ badge_text }}</span>
|
||
{% if extra %}<span class="text-xs text-gray-500">{{ extra }}</span>{% endif %}
|
||
</div>
|
||
<span class="text-gray-500 text-lg" x-text="open === '{{ key }}' ? '▼' : '▶'"></span>
|
||
</button>
|
||
{% endmacro %}
|
||
|
||
<div x-data="{ open: '{{ saved or '' }}' }" class="space-y-2">
|
||
|
||
<!-- Qualys API -->
|
||
{% if visible.qualys %}
|
||
<div class="card overflow-hidden">
|
||
{{ section_header("qualys", "Qualys API", "Connecte", "badge-green", q_tags|string + " tags / " + q_assets|string + " assets / " + q_linked|string + " lies") }}
|
||
<div x-show="open === 'qualys'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/qualys" class="space-y-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">URL API</label>
|
||
<input type="text" name="qualys_url" value="{{ vals.qualys_url }}" placeholder="https://qualysapi.qualys.eu" class="w-full" {% if not editable.qualys %}disabled{% endif %}>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Utilisateur</label>
|
||
<input type="text" name="qualys_user" value="{{ vals.qualys_user }}" class="w-full" {% if not editable.qualys %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Mot de passe</label>
|
||
<input type="password" name="qualys_pass" value="{{ vals.qualys_pass }}" class="w-full" {% if not editable.qualys %}disabled{% endif %}>
|
||
</div>
|
||
</div>
|
||
<div class="flex gap-3 items-end">
|
||
<div class="flex-1">
|
||
<label class="text-xs text-gray-500">Proxy</label>
|
||
<input type="text" name="qualys_proxy" value="{{ vals.qualys_proxy }}" placeholder="http://proxy.sanef.fr:8080" class="w-full font-mono text-xs" {% if not editable.qualys %}disabled{% endif %}>
|
||
</div>
|
||
<label class="flex items-center gap-2 text-xs text-gray-400 pb-1">
|
||
<input type="checkbox" name="qualys_bypass_proxy" value="true" {% if vals.qualys_bypass_proxy == 'true' %}checked{% endif %} {% if not editable.qualys %}disabled{% endif %}>
|
||
Bypass proxy (accès direct)
|
||
</label>
|
||
</div>
|
||
{% if editable.qualys %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- SSH Cle privee -->
|
||
{% if visible.ssh_key %}
|
||
<div class="card overflow-hidden">
|
||
{{ section_header("ssh_key", "SSH Cle privee", "ssh_key", "badge-green") }}
|
||
<div x-show="open === 'ssh_key'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/ssh_key" class="space-y-3">
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">User SSH par defaut</label>
|
||
<input type="text" name="ssh_key_default_user" value="{{ vals.ssh_key_default_user }}" placeholder="root" class="w-full" {% if not editable.ssh_key %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Port par defaut</label>
|
||
<input type="text" name="ssh_key_default_port" value="{{ vals.ssh_key_default_port }}" placeholder="22" class="w-full" {% if not editable.ssh_key %}disabled{% endif %}>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Cle privee (PEM)</label>
|
||
<textarea name="ssh_key_private_key" rows="4" class="w-full font-mono text-xs" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" {% if not editable.ssh_key %}disabled{% endif %}>{{ vals.ssh_key_private_key }}</textarea>
|
||
</div>
|
||
<p class="text-xs text-gray-600">Surchargeable par serveur (ssh_user, ssh_port dans la fiche serveur).</p>
|
||
{% if editable.ssh_key %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- SSH Password -->
|
||
{% if visible.ssh_pwd %}
|
||
<div class="card overflow-hidden">
|
||
{{ section_header("ssh_pwd", "SSH Password", "ssh_pwd", "badge-yellow") }}
|
||
<div x-show="open === 'ssh_pwd'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/ssh_pwd" class="space-y-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">User par defaut</label>
|
||
<input type="text" name="ssh_pwd_default_user" value="{{ vals.ssh_pwd_default_user }}" class="w-full" {% if not editable.ssh_pwd %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Password par defaut</label>
|
||
<input type="password" name="ssh_pwd_default_pass" value="{{ vals.ssh_pwd_default_pass }}" class="w-full" {% if not editable.ssh_pwd %}disabled{% endif %}>
|
||
</div>
|
||
<p class="text-xs text-gray-600">Pour les environnements recette sans cle SSH. Chaque operateur peut configurer son propre compte.</p>
|
||
{% if editable.ssh_pwd %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- SSH PSMP (CyberArk) -->
|
||
{% if visible.ssh_psmp %}
|
||
<div class="card overflow-hidden">
|
||
{{ section_header("ssh_psmp", "SSH PSMP — CyberArk", "ssh_psmp", "badge-yellow") }}
|
||
<div x-show="open === 'ssh_psmp'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/ssh_psmp" class="space-y-3">
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Adresse PSMP</label>
|
||
<input type="text" name="psmp_host" value="{{ vals.psmp_host }}" placeholder="psmp.sanef.fr" class="w-full" {% if not editable.ssh_psmp %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Port PSMP</label>
|
||
<input type="text" name="psmp_port" value="{{ vals.psmp_port }}" placeholder="22" class="w-full" {% if not editable.ssh_psmp %}disabled{% endif %}>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Format user</label>
|
||
<input type="text" name="psmp_user_format" value="{{ vals.psmp_user_format }}" placeholder="{cybr_user}@{target_user}@{hostname}" class="w-full font-mono text-xs" {% if not editable.ssh_psmp %}disabled{% endif %}>
|
||
</div>
|
||
<div class="grid grid-cols-3 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Compte CyberArk</label>
|
||
<input type="text" name="psmp_cyberark_user" value="{{ vals.psmp_cyberark_user }}" placeholder="CYBP01336" class="w-full" {% if not editable.ssh_psmp %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Utilisateur cible</label>
|
||
<input type="text" name="psmp_target_user" value="{{ vals.psmp_target_user }}" placeholder="cybsecope" class="w-full" {% if not editable.ssh_psmp %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Safe par defaut</label>
|
||
<input type="text" name="psmp_default_safe" value="{{ vals.psmp_default_safe }}" class="w-full" {% if not editable.ssh_psmp %}disabled{% endif %}>
|
||
</div>
|
||
</div>
|
||
<p class="text-xs text-gray-600">Auth keyboard-interactive. Chaque operateur configure son propre compte CyberArk. MDP saisi en session.</p>
|
||
{% if editable.ssh_psmp %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- RDP PSM (CyberArk) -->
|
||
{% if visible.rdp_psm %}
|
||
<div class="card overflow-hidden">
|
||
{{ section_header("rdp_psm", "RDP PSM — CyberArk", "rdp_psm", "badge-blue") }}
|
||
<div x-show="open === 'rdp_psm'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/rdp_psm" class="space-y-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">URL PVWA</label>
|
||
<input type="text" name="rdp_psm_pvwa_url" value="{{ vals.rdp_psm_pvwa_url }}" placeholder="https://pvwa.sanef.fr" class="w-full" {% if not editable.rdp_psm %}disabled{% endif %}>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">User PVWA</label>
|
||
<input type="text" name="rdp_psm_pvwa_user" value="{{ vals.rdp_psm_pvwa_user }}" class="w-full" {% if not editable.rdp_psm %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Password PVWA</label>
|
||
<input type="password" name="rdp_psm_pvwa_pass" value="{{ vals.rdp_psm_pvwa_pass }}" class="w-full" {% if not editable.rdp_psm %}disabled{% endif %}>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Connection Component</label>
|
||
<input type="text" name="rdp_psm_component" value="{{ vals.rdp_psm_component }}" placeholder="PSM-RDP" class="w-full" {% if not editable.rdp_psm %}disabled{% endif %}>
|
||
</div>
|
||
<p class="text-xs text-gray-600">Connexion RDP via token PVWA API. Production Windows uniquement.</p>
|
||
{% if editable.rdp_psm %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- RDP Password — disabled -->
|
||
|
||
<!-- vSphere / vCenters -->
|
||
{% if visible.vsphere %}
|
||
<div class="card overflow-hidden">
|
||
<button @click="open = open === 'vsphere' ? '' : 'vsphere'" class="w-full flex items-center justify-between p-4 hover:bg-cyber-border/20 transition-colors">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-cyber-accent font-bold">vSphere / vCenters</span>
|
||
<span class="badge badge-gray">Snapshots</span>
|
||
<span class="text-xs text-gray-500">{{ vcenters|selectattr('is_active')|list|length }} actif(s)</span>
|
||
</div>
|
||
<span class="text-gray-500 text-lg" x-text="open === 'vsphere' ? '▼' : '▶'"></span>
|
||
</button>
|
||
<div x-show="open === 'vsphere'" class="border-t border-cyber-border p-4 space-y-4">
|
||
{% if editable.vsphere %}
|
||
<form method="POST" action="/settings/vsphere" class="space-y-3">
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase">Credentials vSphere (communs)</h4>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Utilisateur</label>
|
||
<input type="text" name="vsphere_user" value="{{ vals.vsphere_user }}" class="w-full">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Mot de passe</label>
|
||
<input type="password" name="vsphere_pass" value="{{ vals.vsphere_pass }}" class="w-full">
|
||
</div>
|
||
</div>
|
||
<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder credentials</button>
|
||
</form>
|
||
{% endif %}
|
||
|
||
<div>
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2">vCenters enregistres</h4>
|
||
<table class="w-full table-cyber text-sm">
|
||
<thead><tr>
|
||
<th class="text-left p-2">Nom</th>
|
||
<th class="text-left p-2">Endpoint</th>
|
||
<th class="p-2">Datacenter</th>
|
||
<th class="text-left p-2">Description</th>
|
||
<th class="p-2">Responsable</th>
|
||
<th class="p-2">Actif</th>
|
||
{% if editable.vsphere %}<th class="p-2">Action</th>{% endif %}
|
||
</tr></thead>
|
||
<tbody>
|
||
{% for vc in vcenters %}
|
||
<tr>
|
||
<td class="p-2">{{ vc.name }}</td>
|
||
<td class="p-2 font-mono text-xs text-cyber-accent">{{ vc.endpoint }}</td>
|
||
<td class="p-2 text-center text-xs">{{ vc.datacenter or '-' }}</td>
|
||
<td class="p-2 text-xs text-gray-400">{{ vc.description or '-' }}</td>
|
||
<td class="p-2 text-center text-xs">{{ vc.responsable or '-' }}</td>
|
||
<td class="p-2 text-center"><span class="badge {% if vc.is_active %}badge-green{% else %}badge-red{% endif %}">{{ 'Oui' if vc.is_active else 'Non' }}</span></td>
|
||
{% if editable.vsphere %}
|
||
<td class="p-2 text-center">
|
||
{% if vc.is_active %}
|
||
<form method="POST" action="/settings/vcenter/{{ vc.id }}/delete" style="display:inline">
|
||
<button type="submit" class="btn-sm bg-red-900/30 text-cyber-red" onclick="return confirm('Desactiver ce vCenter ?')">Desactiver</button>
|
||
</form>
|
||
{% endif %}
|
||
</td>
|
||
{% endif %}
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{% if editable.vsphere %}
|
||
<form method="POST" action="/settings/vcenter/add" class="space-y-3 pt-2 border-t border-cyber-border">
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase">Ajouter un vCenter</h4>
|
||
<div class="grid grid-cols-3 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Nom</label>
|
||
<input type="text" name="vc_name" placeholder="vCenter Senlis" class="w-full" required>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Endpoint (FQDN)</label>
|
||
<input type="text" name="vc_endpoint" placeholder="vcenter01.sanef.groupe" class="w-full font-mono text-xs" required>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Datacenter</label>
|
||
<input type="text" name="vc_datacenter" placeholder="DC-Senlis" class="w-full">
|
||
</div>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Description</label>
|
||
<input type="text" name="vc_description" placeholder="Gestion + hors-prod" class="w-full">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Responsable</label>
|
||
<input type="text" name="vc_responsable" class="w-full">
|
||
</div>
|
||
</div>
|
||
<button type="submit" class="btn-primary px-4 py-2 text-sm">Ajouter</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Teams Channels -->
|
||
{% if visible.vsphere %}
|
||
<div class="card overflow-hidden">
|
||
<button @click="open = open === 'teams' ? '' : 'teams'" class="w-full flex items-center justify-between p-4 hover:bg-cyber-border/20 transition-colors">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-cyber-accent font-bold">Canaux Microsoft Teams</span>
|
||
<span class="badge badge-gray">Notifications</span>
|
||
<span class="text-xs text-gray-500">{{ teams_channels|selectattr('is_active')|list|length }} actif(s) · {{ teams_rules|selectattr('is_active')|list|length }} règle(s){% if teams_channels|selectattr('is_default')|list|length %} · 1 défaut{% endif %}{% if teams_channels|selectattr('is_reboot_channel')|list|length %} · 1 reboot{% endif %}</span>
|
||
</div>
|
||
<span class="text-gray-500 text-lg" x-text="open === 'teams' ? '▼' : '▶'"></span>
|
||
</button>
|
||
<div x-show="open === 'teams'" class="border-t border-cyber-border p-4 space-y-6">
|
||
<p class="text-xs text-gray-500">
|
||
Deux modes pris en charge :
|
||
<strong>SharePoint</strong> (PatchCenter écrit un fichier .txt dans un dossier OneDrive sync,
|
||
un Workflow Power Automate côté SharePoint poste sur Teams — pas d'OAuth) ;
|
||
<strong>Webhook</strong> (POST direct, OAuth Entra ID requis, futur).
|
||
Le routage serveur → canal est pris en charge par les <em>règles</em>.
|
||
</p>
|
||
|
||
<div>
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2">Canaux enregistrés</h4>
|
||
<table class="w-full table-cyber text-sm">
|
||
<thead><tr>
|
||
<th class="text-left p-2">Nom</th>
|
||
<th class="p-2">Mode</th>
|
||
<th class="text-left p-2">SP route / Webhook</th>
|
||
<th class="p-2">Défaut</th>
|
||
<th class="p-2">Reboot</th>
|
||
<th class="p-2">DM dyn.</th>
|
||
<th class="p-2">Actif</th>
|
||
{% if editable.vsphere %}<th class="p-2">Actions</th>{% endif %}
|
||
</tr></thead>
|
||
<tbody>
|
||
{% for tc in teams_channels %}
|
||
<tr>
|
||
<td class="p-2 font-bold">{{ tc.name }}</td>
|
||
<td class="p-2 text-center"><span class="badge {% if tc.mode == 'sharepoint' %}badge-blue{% else %}badge-gray{% endif %}">{{ tc.mode }}</span></td>
|
||
<td class="p-2 font-mono text-[10px]" style="max-width:380px; overflow:hidden; text-overflow:ellipsis;" title="{{ tc.sp_route or tc.webhook_url or '' }}">
|
||
{% if tc.mode == 'sharepoint' %}{{ tc.sp_route or '(non défini)' }}
|
||
{% else %}{{ (tc.webhook_url or '')[:60] }}{% if tc.webhook_url and tc.webhook_url|length > 60 %}…{% endif %}
|
||
{% endif %}
|
||
</td>
|
||
<td class="p-2 text-center"><span class="badge {% if tc.is_default %}badge-green{% else %}badge-gray{% endif %}">{{ 'Oui' if tc.is_default else '-' }}</span></td>
|
||
<td class="p-2 text-center"><span class="badge {% if tc.is_reboot_channel %}badge-orange{% else %}badge-gray{% endif %}">{{ 'Oui' if tc.is_reboot_channel else '-' }}</span></td>
|
||
<td class="p-2 text-center"><span class="badge {% if tc.is_dynamic_dm %}badge-blue{% else %}badge-gray{% endif %}">{{ 'Oui' if tc.is_dynamic_dm else '-' }}</span></td>
|
||
<td class="p-2 text-center"><span class="badge {% if tc.is_active %}badge-green{% else %}badge-red{% endif %}">{{ 'Oui' if tc.is_active else 'Non' }}</span></td>
|
||
{% if editable.vsphere %}
|
||
<td class="p-2 text-center whitespace-nowrap">
|
||
<button type="button" class="btn-sm bg-cyber-blue/20 text-cyber-blue text-xs"
|
||
onclick="testTeams({{ tc.id }}, '{{ tc.name|e }}')">Test</button>
|
||
<form method="POST" action="/settings/teams-channel/{{ tc.id }}/delete" style="display:inline">
|
||
<button type="submit" class="btn-sm bg-red-900/30 text-cyber-red text-xs" onclick="return confirm('Supprimer ce canal Teams ?')">Suppr</button>
|
||
</form>
|
||
</td>
|
||
{% endif %}
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="8" class="p-3 text-gray-500 text-center">Aucun canal Teams configuré.</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{% if editable.vsphere %}
|
||
<form method="POST" action="/settings/teams-channel/add" class="space-y-3 pt-2 border-t border-cyber-border">
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase">Ajouter un canal Teams</h4>
|
||
<div class="grid grid-cols-3 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Nom</label>
|
||
<input type="text" name="tc_name" placeholder="ex: Suivi patch LAN" class="w-full" required>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Mode</label>
|
||
<select name="tc_mode" class="w-full">
|
||
<option value="sharepoint" selected>SharePoint (recommandé)</option>
|
||
<option value="webhook">Webhook (OAuth requis)</option>
|
||
</select>
|
||
</div>
|
||
<div class="flex flex-col text-xs text-gray-500 gap-1 pt-4">
|
||
<label><input type="checkbox" name="tc_is_default" value="1" class="mr-1"> Défaut</label>
|
||
<label><input type="checkbox" name="tc_is_reboot_channel" value="1" class="mr-1"> Canal reboot</label>
|
||
<label><input type="checkbox" name="tc_is_dynamic_dm" value="1" class="mr-1"> DM dynamique (TO: en 1ère ligne)</label>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Sous-dossier SharePoint (sp_route) — requis si mode = sharepoint</label>
|
||
<input type="text" name="tc_sp_route" placeholder="ex: lan_delcour, fl_prod, peage, dsi_general, dm_router" class="w-full font-mono text-xs">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Webhook URL — requis si mode = webhook (laisser vide en mode sharepoint)</label>
|
||
<input type="url" name="tc_webhook_url" placeholder="https://...api.powerplatform.com/...." class="w-full font-mono text-xs">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Description (optionnelle)</label>
|
||
<input type="text" name="tc_description" placeholder="ex: Conv groupe Patching Suivi LAN équipe Delcour" class="w-full">
|
||
</div>
|
||
<button type="submit" class="btn-primary px-4 py-2 text-sm">Ajouter le canal</button>
|
||
</form>
|
||
{% endif %}
|
||
|
||
<!-- Règles de routage -->
|
||
<div class="pt-4 border-t border-cyber-border">
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2">Règles de routage</h4>
|
||
<p class="text-xs text-gray-500 mb-3">
|
||
Les règles sont évaluées par ordre de priorité croissante (10, 20, 30…).
|
||
La première règle dont <em>toutes</em> les conditions actives matchent décide du canal.
|
||
Conditions vides = pas filtré.
|
||
Pour <em>reboot</em>, le canal flaggé "Reboot" gagne avant toute règle.
|
||
</p>
|
||
<table class="w-full table-cyber text-sm">
|
||
<thead><tr>
|
||
<th class="p-2">Prio</th>
|
||
<th class="text-left p-2">Nom</th>
|
||
<th class="text-left p-2">Match</th>
|
||
<th class="text-left p-2">→ Canal</th>
|
||
<th class="p-2">Actif</th>
|
||
{% if editable.vsphere %}<th class="p-2">Actions</th>{% endif %}
|
||
</tr></thead>
|
||
<tbody>
|
||
{% for tr in teams_rules %}
|
||
<tr>
|
||
<td class="p-2 text-center font-mono">{{ tr.priority }}</td>
|
||
<td class="p-2 font-bold">{{ tr.name }}</td>
|
||
<td class="p-2 text-xs">
|
||
{% if tr.responsable_name %}<span class="badge badge-gray">resp: {{ tr.responsable_name }}</span>{% endif %}
|
||
{% if tr.match_application_domain %}<span class="badge badge-gray">domain~{{ tr.match_application_domain }}</span>{% endif %}
|
||
{% if tr.match_env_in %}<span class="badge badge-gray">env∈{{ tr.match_env_in|join(',') }}</span>{% endif %}
|
||
{% if tr.match_msg_type_in %}<span class="badge badge-orange">msg∈{{ tr.match_msg_type_in|join(',') }}</span>{% endif %}
|
||
{% if tr.match_hostname_pattern %}<span class="badge badge-gray">host~{{ tr.match_hostname_pattern }}</span>{% endif %}
|
||
</td>
|
||
<td class="p-2">{{ tr.channel_name or '(canal supprimé)' }}</td>
|
||
<td class="p-2 text-center"><span class="badge {% if tr.is_active %}badge-green{% else %}badge-red{% endif %}">{{ 'Oui' if tr.is_active else 'Non' }}</span></td>
|
||
{% if editable.vsphere %}
|
||
<td class="p-2 text-center whitespace-nowrap">
|
||
<form method="POST" action="/settings/teams-rule/{{ tr.id }}/delete" style="display:inline">
|
||
<button type="submit" class="btn-sm bg-red-900/30 text-cyber-red text-xs" onclick="return confirm('Supprimer cette règle ?')">Suppr</button>
|
||
</form>
|
||
</td>
|
||
{% endif %}
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="6" class="p-3 text-gray-500 text-center">Aucune règle. Sans règles, seul le canal "défaut" sera utilisé.</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
|
||
{% if editable.vsphere %}
|
||
<form method="POST" action="/settings/teams-rule/add" class="space-y-3 mt-3 pt-3 border-t border-cyber-border">
|
||
<h5 class="text-xs text-cyber-accent font-bold uppercase">Ajouter une règle</h5>
|
||
<div class="grid grid-cols-3 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Priorité (asc)</label>
|
||
<input type="number" name="tr_priority" value="100" class="w-full">
|
||
</div>
|
||
<div class="col-span-2">
|
||
<label class="text-xs text-gray-500">Nom</label>
|
||
<input type="text" name="tr_name" placeholder="ex: Delcour LAN debut/fin" class="w-full" required>
|
||
</div>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Canal cible</label>
|
||
<select name="tr_channel_id" class="w-full" required>
|
||
<option value="">-- Choisir --</option>
|
||
{% for tc in teams_channels %}
|
||
<option value="{{ tc.id }}">{{ tc.name }} ({{ tc.mode }}{% if tc.sp_route %}/{{ tc.sp_route }}{% endif %})</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Match responsable (contact)</label>
|
||
<select name="tr_match_responsable_contact_id" class="w-full">
|
||
<option value="">(aucun filtre)</option>
|
||
{% for c in contacts_list %}
|
||
<option value="{{ c.id }}">{{ c.name }}{% if c.teams_upn %} — {{ c.teams_upn }}{% endif %}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Match domaine appli (substring, insensible casse)</label>
|
||
<input type="text" name="tr_match_application_domain" placeholder="ex: Flux Libre" class="w-full">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Match env (CSV — ex: Production / Test,Dev,Recette,Préprod)</label>
|
||
<input type="text" name="tr_match_env_in" placeholder="ex: Production" class="w-full">
|
||
</div>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Match msg_type (CSV — debut,fin,reboot,annulation)</label>
|
||
<input type="text" name="tr_match_msg_type_in" placeholder="ex: debut,fin" class="w-full">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Match hostname (substring, insensible casse — ex: bst)</label>
|
||
<input type="text" name="tr_match_hostname_pattern" placeholder="ex: bst" class="w-full">
|
||
</div>
|
||
</div>
|
||
<button type="submit" class="btn-primary px-4 py-2 text-sm">Ajouter la règle</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Server Clusters -->
|
||
{% if visible.vsphere %}
|
||
<div class="card overflow-hidden">
|
||
<button @click="open = open === 'clusters' ? '' : 'clusters'" class="w-full flex items-center justify-between p-4 hover:bg-cyber-border/20 transition-colors">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-cyber-accent font-bold">Groupes de serveurs (clusters)</span>
|
||
<span class="badge badge-gray">Patching</span>
|
||
<span class="text-xs text-gray-500">{{ server_clusters|length }} défini(s)</span>
|
||
</div>
|
||
<span class="text-gray-500 text-lg" x-text="open === 'clusters' ? '▼' : '▶'"></span>
|
||
</button>
|
||
<div x-show="open === 'clusters'" class="border-t border-cyber-border p-4 space-y-4">
|
||
<p class="text-xs text-gray-500">
|
||
Les clusters permettent de patcher plusieurs serveurs liés (ex : DB master + slaves,
|
||
HAProxy + backends) avec un ordre de redémarrage. Le champ
|
||
<code class="text-cyber-accent">cluster_order</code> sur chaque serveur fixe la
|
||
séquence (1 = premier patché). Stratégie <em>sequential</em> = un par un (recommandé)
|
||
ou <em>parallel</em> = tous en même temps.
|
||
</p>
|
||
|
||
<div>
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2">Clusters enregistrés</h4>
|
||
<table class="w-full table-cyber text-sm">
|
||
<thead><tr>
|
||
<th class="text-left p-2">Nom</th>
|
||
<th class="text-left p-2">Description</th>
|
||
<th class="p-2">Stratégie</th>
|
||
<th class="p-2"># Serveurs</th>
|
||
<th class="p-2">Actif</th>
|
||
{% if editable.vsphere %}<th class="p-2">Actions</th>{% endif %}
|
||
</tr></thead>
|
||
<tbody>
|
||
{% for sc in server_clusters %}
|
||
<tr>
|
||
<td class="p-2 font-bold">{{ sc.name }}</td>
|
||
<td class="p-2 text-xs text-gray-400">{{ sc.description or '-' }}</td>
|
||
<td class="p-2 text-center text-xs">{{ sc.reboot_strategy }}</td>
|
||
<td class="p-2 text-center">
|
||
<a href="#" onclick="showClusterServers({{ sc.id }}, '{{ sc.name|e }}'); return false;"
|
||
class="text-cyber-accent hover:underline">{{ sc.server_count }}</a>
|
||
</td>
|
||
<td class="p-2 text-center"><span class="badge {% if sc.is_active %}badge-green{% else %}badge-red{% endif %}">{{ 'Oui' if sc.is_active else 'Non' }}</span></td>
|
||
{% if editable.vsphere %}
|
||
<td class="p-2 text-center whitespace-nowrap">
|
||
<form method="POST" action="/settings/server-cluster/{{ sc.id }}/delete" style="display:inline">
|
||
<button type="submit" class="btn-sm bg-red-900/30 text-cyber-red text-xs"
|
||
onclick="return confirm('{{ sc.server_count }} serveur(s) rattaché(s).\nLe cluster sera désactivé si utilisé, ou supprimé si vide. OK ?');">Suppr</button>
|
||
</form>
|
||
</td>
|
||
{% endif %}
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="6" class="p-3 text-gray-500 text-center">Aucun cluster configuré.</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
<div id="cluster-servers-detail" class="mt-3 p-3 bg-cyber-border/10 rounded text-xs hidden">
|
||
<div class="flex justify-between items-center mb-2">
|
||
<span id="cluster-servers-title" class="text-cyber-accent font-bold"></span>
|
||
<button onclick="document.getElementById('cluster-servers-detail').classList.add('hidden')"
|
||
class="text-gray-500 hover:text-cyber-accent">✕</button>
|
||
</div>
|
||
<div id="cluster-servers-list"></div>
|
||
</div>
|
||
</div>
|
||
|
||
{% if editable.vsphere %}
|
||
<form method="POST" action="/settings/server-cluster/add" class="space-y-3 pt-2 border-t border-cyber-border">
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase">Ajouter un cluster</h4>
|
||
<div class="grid grid-cols-3 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Nom (unique)</label>
|
||
<input type="text" name="sc_name" placeholder="HAproxy FL" class="w-full" required>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Description</label>
|
||
<input type="text" name="sc_description" placeholder="Cluster HAproxy + backends Flux Libre" class="w-full">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Stratégie reboot</label>
|
||
<select name="sc_reboot_strategy" class="w-full">
|
||
<option value="sequential" selected>Séquentiel (un par un)</option>
|
||
<option value="parallel">Parallèle (tous ensemble)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<button type="submit" class="btn-primary px-4 py-2 text-sm">Ajouter</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Sécurité -->
|
||
{% if visible.security %}
|
||
<div class="card overflow-hidden">
|
||
{{ section_header("security", "Sécurité", "Réseau", "badge-red") }}
|
||
<div x-show="open === 'security'" class="border-t border-cyber-border p-4 space-y-4">
|
||
<!-- Réseaux autorisés -->
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase">Réseaux autorisés (nginx ACL)</h4>
|
||
<table class="w-full table-cyber text-sm">
|
||
<thead><tr>
|
||
<th class="text-left p-2">CIDR</th>
|
||
<th class="text-left p-2">Description</th>
|
||
<th class="p-2">Actif</th>
|
||
{% if editable.security %}<th class="p-2">Actions</th>{% endif %}
|
||
</tr></thead>
|
||
<tbody>
|
||
{% for n in allowed_nets %}
|
||
<tr class="{% if not n.is_active %}opacity-40{% endif %}">
|
||
<td class="p-2 font-mono text-cyber-accent">{{ n.cidr }}</td>
|
||
<td class="p-2 text-xs text-gray-400">{{ n.description or '-' }}</td>
|
||
<td class="p-2 text-center"><span class="badge {% if n.is_active %}badge-green{% else %}badge-red{% endif %}">{{ 'Oui' if n.is_active else 'Non' }}</span></td>
|
||
{% if editable.security %}
|
||
<td class="p-2 text-center">
|
||
<div class="flex gap-1 justify-center">
|
||
<form method="POST" action="/settings/network/{{ n.id }}/toggle" style="display:inline">
|
||
<button class="btn-sm bg-cyber-border text-gray-400">{{ 'Désactiver' if n.is_active else 'Activer' }}</button>
|
||
</form>
|
||
<form method="POST" action="/settings/network/{{ n.id }}/delete" style="display:inline">
|
||
<button class="btn-sm bg-red-900/30 text-cyber-red" onclick="return confirm('Supprimer ce réseau ?')">Suppr</button>
|
||
</form>
|
||
</div>
|
||
</td>
|
||
{% endif %}
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
|
||
{% if editable.security %}
|
||
<form method="POST" action="/settings/network/add" class="flex gap-3 items-end mt-2">
|
||
<div>
|
||
<label class="text-xs text-gray-500">CIDR</label>
|
||
<input type="text" name="cidr" placeholder="10.0.0.0/24" class="text-xs py-1 px-2 w-40 font-mono" required>
|
||
</div>
|
||
<div class="flex-1">
|
||
<label class="text-xs text-gray-500">Description</label>
|
||
<input type="text" name="description" placeholder="VPN nomade" class="text-xs py-1 px-2 w-full">
|
||
</div>
|
||
<button type="submit" class="btn-primary px-3 py-1 text-sm">Ajouter</button>
|
||
</form>
|
||
{% endif %}
|
||
|
||
<p class="text-xs text-gray-600 mt-2">Le fichier <code>/etc/nginx/patchcenter_acl.conf</code> est régénéré automatiquement à chaque modification. Nginx est rechargé.</p>
|
||
|
||
<!-- Paramètres sécurité -->
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase mt-4">Paramètres</h4>
|
||
<form method="POST" action="/settings/security" class="space-y-3">
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Timeout session (minutes)</label>
|
||
<input type="number" name="security_session_timeout" value="{{ vals.security_session_timeout or '60' }}" class="w-full" {% if not editable.security %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Max tentatives login (rate limit/min)</label>
|
||
<input type="number" name="security_max_login_attempts" value="{{ vals.security_max_login_attempts or '5' }}" class="w-full" {% if not editable.security %}disabled{% endif %}>
|
||
</div>
|
||
</div>
|
||
{% if editable.security %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Splunk Remote Log -->
|
||
{% if visible.splunk %}
|
||
<div class="card overflow-hidden">
|
||
{{ section_header("splunk", "Splunk — Remote Log", "HEC", "badge-yellow") }}
|
||
<div x-show="open === 'splunk'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/splunk" class="space-y-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">URL HEC (HTTP Event Collector)</label>
|
||
<input type="text" name="splunk_hec_url" value="{{ vals.splunk_hec_url }}" placeholder="https://splunk.sanef.fr:8088/services/collector" class="w-full" {% if not editable.splunk %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Token HEC</label>
|
||
<input type="password" name="splunk_hec_token" value="{{ vals.splunk_hec_token }}" class="w-full" {% if not editable.splunk %}disabled{% endif %}>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Index</label>
|
||
<input type="text" name="splunk_index" value="{{ vals.splunk_index }}" placeholder="patchcenter" class="w-full" {% if not editable.splunk %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Sourcetype</label>
|
||
<input type="text" name="splunk_sourcetype" value="{{ vals.splunk_sourcetype }}" placeholder="patchcenter:audit" class="w-full" {% if not editable.splunk %}disabled{% endif %}>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Verifier SSL (true/false)</label>
|
||
<input type="text" name="splunk_verify_ssl" value="{{ vals.splunk_verify_ssl }}" placeholder="true" class="w-full" {% if not editable.splunk %}disabled{% endif %}>
|
||
</div>
|
||
<p class="text-xs text-gray-600">Envoie les evenements de patching vers Splunk via HEC.</p>
|
||
{% if editable.splunk %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Teams Notifications -->
|
||
{% if visible.teams %}
|
||
<div class="card overflow-hidden">
|
||
{{ section_header("teams", "Teams — Notifications", "Webhook + SharePoint", "badge-blue") }}
|
||
<div x-show="open === 'teams'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/teams" class="space-y-3">
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase">Canal Teams (Webhook direct)</h4>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Webhook URL</label>
|
||
<input type="text" name="teams_webhook_url" value="{{ vals.teams_webhook_url }}" placeholder="https://outlook.office.com/webhook/..." class="w-full font-mono text-xs" {% if not editable.teams %}disabled{% endif %}>
|
||
</div>
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase mt-4">Conversation groupe (SharePoint + Power Automate)</h4>
|
||
<div>
|
||
<label class="text-xs text-gray-500">SharePoint Site URL</label>
|
||
<input type="text" name="teams_sp_site_url" value="{{ vals.teams_sp_site_url }}" placeholder="https://sanef.sharepoint.com/sites/SecOps" class="w-full font-mono text-xs" {% if not editable.teams %}disabled{% endif %}>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Library</label>
|
||
<input type="text" name="teams_sp_library" value="{{ vals.teams_sp_library }}" placeholder="Documents partages" class="w-full" {% if not editable.teams %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Folder</label>
|
||
<input type="text" name="teams_sp_folder" value="{{ vals.teams_sp_folder }}" placeholder="PatchCenter" class="w-full" {% if not editable.teams %}disabled{% endif %}>
|
||
</div>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Tenant ID (Azure AD)</label>
|
||
<input type="text" name="teams_sp_tenant_id" value="{{ vals.teams_sp_tenant_id }}" class="w-full font-mono text-xs" {% if not editable.teams %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">App Client ID</label>
|
||
<input type="text" name="teams_sp_client_id" value="{{ vals.teams_sp_client_id }}" class="w-full font-mono text-xs" {% if not editable.teams %}disabled{% endif %}>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">App Client Secret</label>
|
||
<input type="password" name="teams_sp_client_secret" value="{{ vals.teams_sp_client_secret }}" class="w-full" {% if not editable.teams %}disabled{% endif %}>
|
||
</div>
|
||
<p class="text-xs text-gray-600">Power Automate : depose JSON sur SharePoint → lit + poste dans la conversation → supprime.</p>
|
||
{% if editable.teams %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- iTop CMDB -->
|
||
{% if visible.itop %}
|
||
<div class="card overflow-hidden">
|
||
<button @click="open = open === 'itop' ? '' : 'itop'" class="w-full flex items-center justify-between p-4 hover:bg-cyber-border/20 transition-colors">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-gray-400 font-bold">iTop CMDB</span>
|
||
<span class="badge badge-green">Configuré</span>
|
||
</div>
|
||
<span class="text-gray-500 text-lg" x-text="open === 'itop' ? '▼' : '▶'"></span>
|
||
</button>
|
||
<div x-show="open === 'itop'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/itop" class="space-y-3">
|
||
{% for key, label, is_secret in sections.itop %}
|
||
<div>
|
||
<label class="text-xs text-gray-500">{{ label }}</label>
|
||
<input type="{{ 'password' if is_secret else 'text' }}" name="{{ key }}" value="{{ vals[key] }}" class="w-full" {% if not editable.itop %}disabled{% endif %}>
|
||
</div>
|
||
{% endfor %}
|
||
<div class="text-xs text-gray-600 space-y-1 mt-2">
|
||
<div>- Import serveurs + metadata</div>
|
||
<div>- Sync responsables / referents</div>
|
||
<div>- Lien applications / clusters</div>
|
||
<div>- Enrichissement domaine / environnement</div>
|
||
</div>
|
||
{% if editable.itop %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- iTop Contacts (filtre des teams à synchroniser) -->
|
||
{% if visible.itop_contacts %}
|
||
<div class="card overflow-hidden">
|
||
<button @click="open = open === 'itop_contacts' ? '' : 'itop_contacts'" class="w-full flex items-center justify-between p-4 hover:bg-cyber-border/20 transition-colors">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-gray-400 font-bold">iTop Contacts — Périmètre</span>
|
||
<span class="badge badge-blue">Filtre des teams</span>
|
||
</div>
|
||
<span class="text-gray-500 text-lg" x-text="open === 'itop_contacts' ? '▼' : '▶'"></span>
|
||
</button>
|
||
<div x-show="open === 'itop_contacts'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/itop_contacts" class="space-y-3">
|
||
{% for key, label, is_secret in sections.itop_contacts %}
|
||
<div>
|
||
<label class="text-xs text-gray-500">{{ label }}</label>
|
||
<input type="text" name="{{ key }}" value="{{ vals[key] }}" placeholder="SecOps, iPOP, Externe, DSI, Admin DSI" class="w-full font-mono text-xs" {% if not editable.itop_contacts %}disabled{% endif %}>
|
||
</div>
|
||
{% endfor %}
|
||
<div class="text-xs text-gray-600 mt-2">
|
||
Seuls les contacts appartenant à ces teams iTop seront synchronisés dans PatchCenter. Si vide, défaut : SecOps, iPOP, Externe, DSI, Admin DSI.
|
||
</div>
|
||
{% if editable.itop_contacts %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- LDAP/AD -->
|
||
{% if visible.ldap %}
|
||
<div class="card overflow-hidden">
|
||
<button @click="open = open === 'ldap' ? '' : 'ldap'" class="w-full flex items-center justify-between p-4 hover:bg-cyber-border/20 transition-colors">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-gray-400 font-bold">LDAP / Active Directory</span>
|
||
<span class="badge {% if vals.ldap_enabled == 'true' %}badge-green{% else %}badge-gray{% endif %}">{{ 'Activé' if vals.ldap_enabled == 'true' else 'Désactivé' }}</span>
|
||
<span class="text-xs text-gray-500">{{ vals.ldap_server or '' }}</span>
|
||
</div>
|
||
<span class="text-gray-500 text-lg" x-text="open === 'ldap' ? '▼' : '▶'"></span>
|
||
</button>
|
||
<div x-show="open === 'ldap'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/ldap" class="space-y-3">
|
||
{% for key, label, is_secret in sections.ldap %}
|
||
<div>
|
||
<label class="text-xs text-gray-500">{{ label }}</label>
|
||
<input type="{{ 'password' if is_secret else 'text' }}" name="{{ key }}" value="{{ vals[key] }}" class="w-full" {% if not editable.ldap %}disabled{% endif %}>
|
||
</div>
|
||
{% endfor %}
|
||
<div class="text-xs text-gray-600 mt-2">
|
||
Une fois configuré et activé, le choix <b>Local / LDAP</b> apparaîtra sur la page de connexion. Les users peuvent aussi être forcés en LDAP via le champ "Auth" dans /users.
|
||
</div>
|
||
<div class="flex gap-2 items-center">
|
||
{% if editable.ldap %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
<button type="button" onclick="testLdap()" class="btn-sm bg-cyber-border text-gray-300 px-4 py-2">Tester la connexion</button>
|
||
<span id="ldap-test-result" class="text-xs ml-2"></span>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
</div>
|
||
|
||
<script>
|
||
function testLdap() {
|
||
var out = document.getElementById('ldap-test-result');
|
||
out.textContent = 'Test en cours...';
|
||
out.className = 'text-xs ml-2 text-gray-400';
|
||
fetch('/settings/ldap/test', {method: 'POST', credentials: 'same-origin'})
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(d){
|
||
if (d.ok) { out.textContent = '✓ ' + (d.msg || 'OK'); out.className = 'text-xs ml-2 text-cyber-green'; }
|
||
else { out.textContent = '✗ ' + (d.msg || 'Erreur'); out.className = 'text-xs ml-2 text-cyber-red'; }
|
||
})
|
||
.catch(function(e){ out.textContent = '✗ ' + e.message; out.className = 'text-xs ml-2 text-cyber-red'; });
|
||
}
|
||
|
||
function showClusterServers(scId, scName) {
|
||
var detail = document.getElementById('cluster-servers-detail');
|
||
var title = document.getElementById('cluster-servers-title');
|
||
var list = document.getElementById('cluster-servers-list');
|
||
title.textContent = 'Cluster : ' + scName;
|
||
list.innerHTML = '<span class="text-gray-500">Chargement…</span>';
|
||
detail.classList.remove('hidden');
|
||
fetch('/settings/server-cluster/' + scId + '/servers', {credentials: 'same-origin'})
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(d){
|
||
if (!d.ok) { list.innerHTML = '<span class="text-cyber-red">' + (d.msg||'Erreur') + '</span>'; return; }
|
||
if (!d.servers.length) { list.innerHTML = '<span class="text-gray-500">Aucun serveur dans ce cluster.</span>'; return; }
|
||
var rows = d.servers.map(function(s){
|
||
var ord = (s.order !== null && s.order !== undefined) ? s.order : '–';
|
||
return '<tr><td class="p-1 text-center">' + ord + '</td>'
|
||
+ '<td class="p-1 font-mono text-cyber-accent">' + s.hostname + '</td>'
|
||
+ '<td class="p-1 text-gray-400">' + (s.machine_type || '-') + '</td>'
|
||
+ '<td class="p-1 text-gray-400">' + (s.etat || '-') + '</td></tr>';
|
||
}).join('');
|
||
list.innerHTML = '<table class="w-full text-xs">'
|
||
+ '<thead><tr class="text-cyber-accent border-b border-cyber-border">'
|
||
+ '<th class="p-1">Ordre</th><th class="text-left p-1">Hostname</th>'
|
||
+ '<th class="text-left p-1">Type</th><th class="text-left p-1">État</th></tr></thead>'
|
||
+ '<tbody>' + rows + '</tbody></table>';
|
||
})
|
||
.catch(function(e){ list.innerHTML = '<span class="text-cyber-red">' + e.message + '</span>'; });
|
||
}
|
||
|
||
function testTeams(tcId, tcName) {
|
||
fetch('/settings/teams-channel/' + tcId + '/test', {method: 'POST', credentials: 'same-origin'})
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(d){
|
||
if (d.ok) alert('✓ Test envoyé sur "' + tcName + '" — vérifie le canal Teams (HTTP ' + d.status + ')');
|
||
else alert('✗ Échec test "' + tcName + '" — HTTP ' + (d.status || '?') + '\n' + (d.detail || d.msg || ''));
|
||
})
|
||
.catch(function(e){ alert('✗ Erreur réseau : ' + e.message); });
|
||
}
|
||
</script>
|
||
{% endblock %}
|