diff --git a/apps/pusher_app_prem/appserver/static/git_pusher.js b/apps/pusher_app_prem/appserver/static/git_pusher.js
old mode 100644
new mode 100755
index b65c4ae9..977b7fef
--- a/apps/pusher_app_prem/appserver/static/git_pusher.js
+++ b/apps/pusher_app_prem/appserver/static/git_pusher.js
@@ -1,565 +1,544 @@
// ============================================
-// CHARGER LES DASHBOARDS DYNAMIQUEMENT
+// GIT PUSHER - MAIN JAVASCRIPT
+// Version 2.0 avec système de licence par fichier
// ============================================
-// Charger les applications
-function loadAvailableApps() {
- console.log("loadAvailableApps called");
-
- const apiUrl = '/en-US/splunkd/__raw/services/apps/local?output_mode=json&count=0';
-
- fetch(apiUrl)
- .then(response => {
- if (!response.ok) {
- throw new Error('HTTP ' + response.status);
- }
- return response.json();
- })
- .then(data => {
- console.log("Apps Response: Found " + (data.entry ? data.entry.length : 0) + " apps");
-
- if (data.entry && data.entry.length > 0) {
- const apps = data.entry
- .filter(item => {
- // Filtrer les apps système
- const isHidden = item.content && item.content.is_visible === 0;
- const appName = item.name;
- // Exclure les apps système
- const systemApps = ['launcher', 'splunk_monitoring_console', 'introspection'];
- return !isHidden && !systemApps.includes(appName);
- })
- .map(item => ({
- id: item.name,
- name: item.content.label || item.name,
- description: item.content.description || ''
- }))
- .sort((a, b) => a.name.localeCompare(b.name));
-
- console.log("Filtered apps: " + apps.length);
- populateAppsList(apps);
- } else {
- console.warn("No apps found");
- showAppsEmpty();
- }
- })
- .catch(error => {
- console.error("API Error:", error);
- showAppsEmpty();
- });
-}
+// Configuration
+const GIT_PUSHER_CONFIG = {
+ serverUrl: window.location.protocol + '//' + window.location.hostname + ':9999',
+ credentialsKey: 'git_pusher_credentials',
+ version: '2.0.0'
+};
-function populateAppsList(apps) {
- console.log("populateAppsList called with", apps.length, "apps");
-
- const container = document.getElementById('dashboard-list');
-
- if (!container) {
- console.error("dashboard-list container not found");
- return;
- }
+// État global
+let selectedApps = [];
+let isProcessing = false;
- if (!apps || apps.length === 0) {
- showAppsEmpty();
- return;
- }
-
- let html = '
';
+// ============================================
+// INITIALISATION
+// ============================================
- apps.forEach((app, index) => {
- const checkboxId = 'app-' + index;
- html += '';
- html += '
';
- html += '
' + app.name + ' ' + app.id + ' ';
- if (app.description) {
- html += '' + app.description + '
';
+require([
+ 'jquery',
+ 'splunkjs/mvc',
+ 'splunkjs/mvc/searchmanager',
+ 'splunkjs/mvc/simplexml/ready!'
+], function($, mvc, SearchManager) {
+
+ console.log("Git Pusher v2.0 initializing...");
+
+ // Initialiser le système de licence
+ if (typeof initializeLicense === 'function') {
+ initializeLicense();
+ } else {
+ console.warn("License system not loaded");
}
- html += '';
- html += '
';
- });
+
+ // Charger les credentials sauvegardés
+ loadSavedCredentials();
+
+ // Récupérer les résultats de recherche pour les apps
+ const searchManager = mvc.Components.get('dsearch');
+
+ if (searchManager) {
+ searchManager.on('search:done', function() {
+ const results = searchManager.data('results');
+ if (results) {
+ results.on('data', function() {
+ const rows = results.data().rows;
+ const fields = results.data().fields;
+ renderAppsList(rows, fields);
+ });
+ }
+ });
+ }
+
+ // Exposer les fonctions globalement
+ window.pushDashboards = pushDashboards;
+ window.resetForm = resetForm;
+ window.toggleSelectAll = toggleSelectAll;
+});
+
+// Attacher l'événement au bouton après chargement du DOM
+document.addEventListener('DOMContentLoaded', function() {
+ setTimeout(function() {
+ var pushBtn = document.getElementById('push-btn');
+ if (pushBtn) {
+ pushBtn.onclick = function() {
+ if (typeof pushDashboards === 'function') {
+ pushDashboards();
+ } else if (typeof window.pushDashboards === 'function') {
+ window.pushDashboards();
+ }
+ };
+ console.log("Push button event attached");
+ }
+ }, 2000); // Attendre 2 secondes que tout soit chargé
+});
- container.innerHTML = html;
- console.log('Successfully populated ' + apps.length + ' apps');
-
- // Ajouter les event listeners
- addCheckboxListeners();
-}
+// ============================================
+// RENDU DE LA LISTE DES APPLICATIONS
+// ============================================
-function addCheckboxListeners() {
- console.log("addCheckboxListeners called");
-
- const selectAllCheckbox = document.getElementById('select-all');
- if (selectAllCheckbox) {
- selectAllCheckbox.addEventListener('change', function() {
- toggleSelectAll(this);
- });
- }
-
- const appCheckboxes = document.querySelectorAll('#dashboard-list input[type="checkbox"][data-app]');
- appCheckboxes.forEach(checkbox => {
- checkbox.addEventListener('change', function() {
- console.log("App checkbox changed");
+function renderAppsList(rows, fields) {
+ const container = document.getElementById('dashboard-list');
+ if (!container) return;
+
+ console.log("Fields:", fields);
+ console.log("First row:", rows[0]);
+
+ // Trouver les index des colonnes
+ const nameIdx = fields.indexOf('name');
+ const labelIdx = fields.indexOf('label');
+ const descIdx = fields.indexOf('description');
+
+ console.log("Index - name:", nameIdx, "label:", labelIdx);
+
+ // Générer le HTML
+ let html = `
+
+ `;
+
+ let validApps = 0;
+
+ rows.forEach((row, index) => {
+ // Récupérer le nom - essayer plusieurs méthodes
+ let name = '';
+ if (nameIdx >= 0 && row[nameIdx]) {
+ name = row[nameIdx];
+ } else if (labelIdx >= 0 && row[labelIdx]) {
+ // Si name est null, utiliser label comme fallback temporaire
+ // On va chercher le vrai nom via l'API
+ name = row[labelIdx];
+ } else if (row[0]) {
+ name = row[0];
+ } else if (row[1]) {
+ name = row[1]; // Deuxième élément si premier est null
+ }
+
+ const label = (labelIdx >= 0 && row[labelIdx]) ? row[labelIdx] : name;
+
+ console.log(`Row ${index}: name="${name}", label="${label}"`);
+
+ // Ignorer si pas de nom ou apps système
+ if (!name || name.startsWith('splunk_') || name === 'learned' || name === 'launcher') {
+ return;
+ }
+
+ validApps++;
+
+ html += `
+
+
+
+ ${name}
+ ${label !== name ? ' - ' + label : ''}
+
+
+ `;
});
- });
-}
-
-function showAppsEmpty() {
- const container = document.getElementById('dashboard-list');
- if (container) {
- container.innerHTML = 'No apps found
';
- }
- console.log("Displayed empty state");
+
+ container.innerHTML = html;
+
+ console.log(`Rendered ${validApps} valid applications out of ${rows.length}`);
}
-// Attendre que le DOM soit chargé
-function initScript() {
- console.log("initScript called");
-
- // INITIALISER LA LICENCE EN PREMIER
- initializeLicense();
+// ============================================
+// GESTION DE LA SÉLECTION
+// ============================================
- // Charger les applications
- loadAvailableApps();
-
- // Charger les credentials sauvegardés
- loadSavedCredentials();
-
- // Attacher les event listeners au bouton
- const pushBtn = document.getElementById('push-btn');
- if (pushBtn) {
- console.log("Push button found, attaching listener");
- pushBtn.addEventListener('click', function(e) {
- e.preventDefault();
- console.log("Push button clicked!");
- pushDashboards();
+function updateSelectedApps() {
+ const checkboxes = document.querySelectorAll('#dashboard-list input[type="checkbox"][data-app-id]');
+ selectedApps = [];
+
+ checkboxes.forEach(cb => {
+ if (cb.checked) {
+ selectedApps.push({
+ id: cb.getAttribute('data-app-id'),
+ label: cb.getAttribute('data-app-label')
+ });
+ }
});
- } else {
- console.warn("Push button not found");
- }
-
- // Attacher l'event listener pour sauvegarder les credentials
- const saveCheckbox = document.getElementById('save-credentials');
- if (saveCheckbox) {
- saveCheckbox.addEventListener('change', function() {
- if (this.checked) {
- saveCredentials();
- } else {
- clearSavedCredentials();
- }
+
+ // Mettre à jour le "Select All"
+ const selectAll = document.getElementById('select-all');
+ if (selectAll) {
+ const allChecked = Array.from(checkboxes).every(cb => cb.checked);
+ const someChecked = Array.from(checkboxes).some(cb => cb.checked);
+ selectAll.checked = allChecked;
+ selectAll.indeterminate = someChecked && !allChecked;
+ }
+
+ console.log(`Selected ${selectedApps.length} apps`);
+}
+
+function toggleSelectAll(checked) {
+ const checkboxes = document.querySelectorAll('#dashboard-list input[type="checkbox"][data-app-id]');
+ checkboxes.forEach(cb => {
+ cb.checked = checked;
});
- }
+ updateSelectedApps();
}
-function loadSavedCredentials() {
- console.log("Loading saved credentials...");
-
- try {
- const savedUrl = getCookie('git_pusher_url');
- const savedToken = getCookie('git_pusher_token');
- const savedBranch = getCookie('git_pusher_branch');
-
- if (savedUrl) {
- document.getElementById('git-url').value = decodeURIComponent(savedUrl);
- console.log("Loaded saved URL from cookie");
+// ============================================
+// PUSH VERS GIT
+// ============================================
+
+async function pushDashboards() {
+ console.log("Starting push process...");
+
+ // Vérifier si déjà en cours
+ if (isProcessing) {
+ console.log("Push already in progress");
+ return;
+ }
+
+ // Récupérer les apps sélectionnées directement depuis le DOM
+ var apps = [];
+ var checkboxes = document.querySelectorAll('#dashboard-list input[type="checkbox"][data-app-id]:checked');
+ checkboxes.forEach(function(cb) {
+ apps.push({
+ id: cb.getAttribute('data-app-id'),
+ label: cb.getAttribute('data-app-label') || cb.getAttribute('data-app-id')
+ });
+ });
+
+ console.log("Apps selected from DOM:", apps);
+
+ // Mettre à jour selectedApps
+ selectedApps = apps;
+
+ // Vérifier si déjà en cours
+ if (isProcessing) {
+ console.log("Push already in progress");
+ return;
}
- if (savedToken) {
- document.getElementById('git-token').value = decodeURIComponent(savedToken);
- console.log("Loaded saved token from cookie");
+ // Vérifier la licence AVANT tout
+ if (typeof checkLicenseBeforePush === 'function') {
+ const licenseOk = await checkLicenseBeforePush();
+ if (!licenseOk) {
+ console.log("License check failed");
+ return;
+ }
}
- if (savedBranch) {
- document.getElementById('git-branch').value = decodeURIComponent(savedBranch);
+ // Récupérer les valeurs du formulaire
+ const gitUrl = document.getElementById('git-url')?.value?.trim();
+ const gitBranch = document.getElementById('git-branch')?.value?.trim() || 'main';
+ const gitToken = document.getElementById('git-token')?.value?.trim();
+ const commitMessage = document.getElementById('commit-message')?.value?.trim();
+ const saveCredentials = document.getElementById('save-credentials')?.checked;
+
+ // Mettre à jour la liste des apps sélectionnées
+ updateSelectedApps();
+
+ // Validation
+ if (!gitUrl) {
+ showMessage('error', 'Please enter a Git repository URL');
+ return;
}
- if (savedUrl && savedToken) {
- document.getElementById('save-credentials').checked = true;
+ if (!gitToken) {
+ showMessage('error', 'Please enter a Git token or password');
+ return;
}
- } catch (e) {
- console.warn("Could not load saved credentials:", e);
- }
-}
-
-function saveCredentials() {
- console.log("Saving credentials...");
-
- try {
- const gitUrl = document.getElementById('git-url').value;
- const gitToken = document.getElementById('git-token').value;
- const gitBranch = document.getElementById('git-branch').value;
-
- if (gitUrl && gitToken) {
- setCookie('git_pusher_url', gitUrl, 30);
- setCookie('git_pusher_token', gitToken, 30);
- setCookie('git_pusher_branch', gitBranch, 30);
- console.log("Credentials saved to cookies");
- showSuccess("Credentials saved locally");
- } else {
- showError("Please fill in URL and Token before saving");
+
+ if (!commitMessage) {
+ showMessage('error', 'Please enter a commit message');
+ return;
+ }
+
+ if (selectedApps.length === 0) {
+ showMessage('error', 'Please select at least one application to deploy');
+ return;
+ }
+
+ // Sauvegarder les credentials si demandé
+ if (saveCredentials) {
+ saveCredentialsToStorage(gitUrl, gitBranch, gitToken);
+ }
+
+ // Démarrer le push
+ isProcessing = true;
+ showLoading(true);
+ hideMessages();
+
+ try {
+ // Récupérer l'utilisateur courant
+ const currentUser = await getCurrentUser();
+
+ // Construire les paramètres
+ const params = new URLSearchParams({
+ git_url: gitUrl,
+ git_branch: gitBranch,
+ git_token: gitToken,
+ commit_message: commitMessage,
+ apps: JSON.stringify(selectedApps),
+ user: currentUser
+ });
+
+ console.log(`Pushing ${selectedApps.length} apps to ${gitUrl}`);
+
+ // Appeler le serveur
+ const response = await fetch(`${GIT_PUSHER_CONFIG.serverUrl}/push?${params.toString()}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ const result = await response.json();
+ console.log("Push result:", result);
+
+ if (result.status === 'success') {
+ showMessage('success', `✅ Successfully deployed ${result.apps_pushed || selectedApps.length} application(s) to Git!`);
+
+ // Reset la sélection après succès
+ setTimeout(() => {
+ toggleSelectAll(false);
+ }, 2000);
+ } else if (result.error_code === 'LICENSE_ERROR') {
+ showMessage('error', `🔐 ${result.message}`);
+
+ // Afficher le modal de licence
+ if (typeof showLicenseModal === 'function') {
+ showLicenseModal(result.message, result.error_code);
+ }
+ } else if (result.error_code === 'APP_LIMIT') {
+ showMessage('error', `📦 ${result.message}`);
+ } else {
+ showMessage('error', result.message || 'Unknown error occurred');
+ }
+
+ } catch (error) {
+ console.error("Push error:", error);
+ showMessage('error', `Connection error: ${error.message}. Is the Git Pusher server running?`);
+ } finally {
+ isProcessing = false;
+ showLoading(false);
}
- } catch (e) {
- console.error("Error saving credentials:", e);
- showError("Could not save credentials");
- }
-}
-
-function clearSavedCredentials() {
- console.log("Clearing saved credentials...");
-
- try {
- deleteCookie('git_pusher_url');
- deleteCookie('git_pusher_token');
- deleteCookie('git_pusher_branch');
- console.log("Credentials cleared from cookies");
- showSuccess("Credentials cleared");
- } catch (e) {
- console.error("Error clearing credentials:", e);
- }
}
-// Fonctions utilitaires pour les cookies
-function setCookie(name, value, days) {
- const d = new Date();
- d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
- const expires = "expires=" + d.toUTCString();
- document.cookie = name + "=" + encodeURIComponent(value) + ";" + expires + ";path=/";
- console.log("Cookie set: " + name);
-}
+// ============================================
+// UTILITAIRES UI
+// ============================================
-function getCookie(name) {
- const nameEQ = name + "=";
- const ca = document.cookie.split(';');
- for (let i = 0; i < ca.length; i++) {
- let c = ca[i].trim();
- if (c.indexOf(nameEQ) === 0) {
- return c.substring(nameEQ.length);
+function showLoading(show) {
+ const loading = document.getElementById('loading');
+ const pushBtn = document.getElementById('push-btn');
+
+ if (loading) {
+ loading.classList.toggle('active', show);
+ }
+
+ if (pushBtn) {
+ pushBtn.disabled = show;
+ pushBtn.textContent = show ? '⏳ Deploying...' : '✈️ Deploy to Git';
}
- }
- return "";
}
-function deleteCookie(name) {
- setCookie(name, "", -1);
- console.log("Cookie deleted: " + name);
+function showMessage(type, text) {
+ hideMessages();
+
+ const successMsg = document.getElementById('success-msg');
+ const errorMsg = document.getElementById('error-msg');
+ const successText = document.getElementById('success-text');
+ const errorText = document.getElementById('error-text');
+
+ if (type === 'success' && successMsg && successText) {
+ successText.textContent = text;
+ successMsg.classList.add('active');
+
+ // Auto-hide après 5 secondes
+ setTimeout(() => {
+ successMsg.classList.remove('active');
+ }, 5000);
+ } else if (type === 'error' && errorMsg && errorText) {
+ errorText.textContent = text;
+ errorMsg.classList.add('active');
+ }
}
-if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', function() {
- console.log("DOM Ready - Initializing script...");
- setTimeout(function() {
- initScript();
- }, 1000);
- });
-} else {
- console.log("DOM already ready - Initializing script...");
- setTimeout(function() {
- initScript();
- }, 1000);
+function hideMessages() {
+ const successMsg = document.getElementById('success-msg');
+ const errorMsg = document.getElementById('error-msg');
+
+ if (successMsg) successMsg.classList.remove('active');
+ if (errorMsg) errorMsg.classList.remove('active');
}
-function getFormKeyValue() {
- try {
- // Chercher dans les meta tags du DOM
- const metaTag = document.querySelector('meta[name="splunk_form_key"]');
- if (metaTag) {
- return metaTag.getAttribute('content');
- }
+function resetForm(clearCredentials = false) {
+ // Reset les champs
+ const commitMessage = document.getElementById('commit-message');
+ if (commitMessage) commitMessage.value = '';
- // Chercher dans les cookies
- const cookies = document.cookie.split(';');
- for (let cookie of cookies) {
- const [name, value] = cookie.trim().split('=');
- if (name === 'splunk_form_key') {
- return decodeURIComponent(value);
- }
+ if (clearCredentials) {
+ const gitUrl = document.getElementById('git-url');
+ const gitToken = document.getElementById('git-token');
+ const saveCredentials = document.getElementById('save-credentials');
+
+ if (gitUrl) gitUrl.value = '';
+ if (gitToken) gitToken.value = '';
+ if (saveCredentials) saveCredentials.checked = false;
+
+ // Supprimer les credentials sauvegardés
+ localStorage.removeItem(GIT_PUSHER_CONFIG.credentialsKey);
}
- console.warn("Could not find form key, proceeding without it");
- return '';
- } catch (e) {
- console.error("Error getting form key:", e);
- return '';
- }
+ // Reset la sélection
+ toggleSelectAll(false);
+
+ // Cacher les messages
+ hideMessages();
+
+ console.log("Form reset" + (clearCredentials ? " (with credentials)" : ""));
}
-function loadAvailableApps() {
- console.log("loadAvailableApps called");
-
- const apiUrl = '/en-US/splunkd/__raw/services/apps/local?output_mode=json&count=0';
-
- fetch(apiUrl)
- .then(response => {
- if (!response.ok) {
- throw new Error('HTTP ' + response.status);
- }
- return response.json();
- })
- .then(data => {
- console.log("Apps Response: Found " + (data.entry ? data.entry.length : 0) + " apps");
-
- if (data.entry && data.entry.length > 0) {
- const apps = data.entry
- .filter(item => {
- // Filtrer les apps système
- const isHidden = item.content && item.content.is_visible === 0;
- const appName = item.name;
- // Exclure les apps système
- const systemApps = ['launcher', 'splunk_monitoring_console', 'introspection'];
- return !isHidden && !systemApps.includes(appName);
- })
- .map(item => ({
- id: item.name,
- name: item.content.label || item.name,
- description: item.content.description || ''
- }))
- .sort((a, b) => a.name.localeCompare(b.name));
-
- console.log("Filtered apps: " + apps.length);
- populateAppsList(apps);
- } else {
- console.warn("No apps found");
- showAppsEmpty();
+// ============================================
+// GESTION DES CREDENTIALS
+// ============================================
+
+function saveCredentialsToStorage(gitUrl, gitBranch, gitToken) {
+ try {
+ const credentials = {
+ gitUrl: gitUrl,
+ gitBranch: gitBranch,
+ // Note: En production, envisager une solution plus sécurisée
+ gitToken: btoa(gitToken), // Encodage basique (pas sécurisé, juste pour l'obfuscation)
+ savedAt: new Date().toISOString()
+ };
+
+ localStorage.setItem(GIT_PUSHER_CONFIG.credentialsKey, JSON.stringify(credentials));
+ console.log("Credentials saved");
+ } catch (error) {
+ console.error("Error saving credentials:", error);
}
- })
- .catch(error => {
- console.error("API Error:", error);
- showAppsEmpty();
- });
}
-function populateAppsList(apps) {
- console.log("populateAppsList called with", apps.length, "apps");
-
- const container = document.getElementById('dashboard-list');
-
- if (!container) {
- console.error("dashboard-list container not found");
- return;
- }
-
- if (!apps || apps.length === 0) {
- showAppsEmpty();
- return;
- }
-
- let html = '';
- html += ' ';
- html += 'Select All (' + apps.length + ' apps) ';
- html += '
';
-
- apps.forEach((app, index) => {
- const checkboxId = 'app-' + index;
- html += '';
- html += '
';
- html += '
' + app.name + ' ' + app.id + ' ';
- if (app.description) {
- html += '' + app.description + '
';
+function loadSavedCredentials() {
+ try {
+ const saved = localStorage.getItem(GIT_PUSHER_CONFIG.credentialsKey);
+ if (!saved) return;
+
+ const credentials = JSON.parse(saved);
+
+ const gitUrl = document.getElementById('git-url');
+ const gitBranch = document.getElementById('git-branch');
+ const gitToken = document.getElementById('git-token');
+ const saveCredentials = document.getElementById('save-credentials');
+
+ if (gitUrl && credentials.gitUrl) {
+ gitUrl.value = credentials.gitUrl;
+ }
+
+ if (gitBranch && credentials.gitBranch) {
+ gitBranch.value = credentials.gitBranch;
+ }
+
+ if (gitToken && credentials.gitToken) {
+ gitToken.value = atob(credentials.gitToken);
+ }
+
+ if (saveCredentials) {
+ saveCredentials.checked = true;
+ }
+
+ console.log("Credentials loaded from storage");
+ } catch (error) {
+ console.error("Error loading credentials:", error);
}
- html += '';
- html += '
';
- });
-
- container.innerHTML = html;
- console.log('Successfully populated ' + apps.length + ' apps');
}
-function showAppsEmpty() {
- const container = document.getElementById('dashboard-list');
- if (container) {
- container.innerHTML = 'No apps found
';
- }
- console.log("Displayed empty state");
-}
+// ============================================
+// RÉCUPÉRATION DE L'UTILISATEUR SPLUNK
+// ============================================
-function toggleSelectAll(checkbox) {
- const checkboxes = document.querySelectorAll('#dashboard-list input[type="checkbox"][data-app]');
- checkboxes.forEach(cb => cb.checked = checkbox.checked);
+async function getCurrentUser() {
+ try {
+ const response = await fetch('/en-US/splunkd/__raw/services/authentication/current-context?output_mode=json');
+ const data = await response.json();
+ return data.entry?.[0]?.content?.username || 'unknown';
+ } catch (error) {
+ console.error("Error getting current user:", error);
+ return 'unknown';
+ }
}
// ============================================
-// POUSSER LES DASHBOARDS VERS GIT
+// VÉRIFICATION DU SERVEUR
// ============================================
-function pushDashboards() {
-
- // Vérifier la licence
- if (!checkLicenseBeforePush()) {
- return;
- }
- console.log("pushDashboards called");
-
- const gitUrl = document.getElementById('git-url').value;
- const gitBranch = document.getElementById('git-branch').value;
- const gitToken = document.getElementById('git-token').value;
- const commitMessage = document.getElementById('commit-message').value;
-
- console.log("Git URL:", gitUrl);
- console.log("Git Branch:", gitBranch);
- console.log("Commit Message:", commitMessage);
-
- const checkboxes = document.querySelectorAll('#dashboard-list input[type="checkbox"]:not(#select-all):checked');
- const selectedApps = Array.from(checkboxes).map(cb => ({
- id: cb.value,
- name: cb.getAttribute('data-name')
- }));
-
- console.log("Selected apps:", selectedApps);
-
- // Validation
- if (!gitUrl.trim()) {
- console.warn("Validation failed: No Git URL");
- showError('Please enter a Git repository URL');
- return;
- }
-
- if (!gitToken.trim()) {
- console.warn("Validation failed: No Git token");
- showError('Please enter your Git token or password');
- return;
- }
-
- if (!commitMessage.trim()) {
- console.warn("Validation failed: No commit message");
- showError('Please enter a commit message');
- return;
- }
-
- if (selectedApps.length === 0) {
- console.warn("Validation failed: No apps selected");
- showError('Please select at least one application');
- return;
- }
-
- console.log("Validation passed, showing loading state...");
-
- // Afficher le loading
- document.getElementById('loading').style.display = 'block';
- document.getElementById('success-msg').style.display = 'none';
- document.getElementById('error-msg').style.display = 'none';
- document.getElementById('push-btn').disabled = true;
-
- // Sauvegarder les credentials si la case est cochée
- if (document.getElementById('save-credentials').checked) {
+async function checkServerHealth() {
try {
- setCookie('git_pusher_url', gitUrl, 30);
- setCookie('git_pusher_token', gitToken, 30);
- setCookie('git_pusher_branch', gitBranch, 30);
- console.log("Credentials auto-saved to cookies");
- } catch (e) {
- console.warn("Could not auto-save credentials:", e);
+ const response = await fetch(`${GIT_PUSHER_CONFIG.serverUrl}/health`, {
+ method: 'GET',
+ timeout: 5000
+ });
+ const data = await response.json();
+ return data.status === 'ok';
+ } catch (error) {
+ console.error("Server health check failed:", error);
+ return false;
}
- }
-
- // Préparer les données - passer les apps au lieu des dashboards
- const payload = {
- git_url: gitUrl,
- git_branch: gitBranch,
- git_token: gitToken,
- apps: selectedApps,
- commit_message: commitMessage,
- timestamp: new Date().toISOString(),
- user: getCurrentUser()
- };
-
- console.log("Payload prepared:", payload);
-
- // Appeler le script Python via serveur
- callPushScript(payload);
-}
-
-function callPushScript(payload) {
- console.log("callPushScript called");
- console.log("Payload:", payload);
-
- // Construire l'URL vers le serveur Python sur le port 9999 en HTTP
- const hostname = window.location.hostname;
- const baseUrl = `http://${hostname}:9999`;
-
- const url = new URL('/push', baseUrl);
-
- // Ajouter les paramètres en query string
- url.searchParams.append('git_url', payload.git_url);
- url.searchParams.append('git_branch', payload.git_branch);
- url.searchParams.append('git_token', payload.git_token);
- url.searchParams.append('commit_message', payload.commit_message);
-
- // Encoder correctement les apps en JSON
- const appsJson = JSON.stringify(payload.apps);
- console.log("Apps JSON:", appsJson);
- url.searchParams.append('apps', appsJson);
-
- url.searchParams.append('user', payload.user);
-
- console.log("Calling:", url.toString());
-
- fetch(url.toString(), {
- method: 'POST',
- mode: 'no-cors'
- })
- .then(response => {
- // Avec no-cors, on ne peut pas lire response.json()
- // Donc on suppose que si la requête arrive au serveur, c'est bon
- console.log("Request sent successfully");
- document.getElementById('loading').style.display = 'none';
- document.getElementById('push-btn').disabled = false;
- showSuccess('Push request sent! Check server logs for details.');
- resetForm();
- return;
- })
- .catch(error => {
- console.error('Fetch error:', error);
- document.getElementById('loading').style.display = 'none';
- document.getElementById('push-btn').disabled = false;
- showError('Network error: ' + error.message);
- });
}
-function getCurrentUser() {
- try {
- // Essayer plusieurs méthodes
- if (Splunk && Splunk.util && typeof Splunk.util.getCurrentUser === 'function') {
- return Splunk.util.getCurrentUser();
+// Vérifier la santé du serveur au démarrage
+setTimeout(async () => {
+ const healthy = await checkServerHealth();
+ if (!healthy) {
+ console.warn("Git Pusher server may not be running");
+ } else {
+ console.log("Git Pusher server is healthy");
}
-
- // Fallback: chercher dans le DOM ou retourner 'unknown'
- return 'unknown_user';
- } catch (e) {
- console.warn("Could not get current user:", e);
- return 'unknown_user';
- }
-}
+}, 1000);
-function showSuccess(message) {
- const successMsg = document.getElementById('success-msg');
- document.getElementById('success-text').textContent = message;
- successMsg.style.display = 'block';
- setTimeout(() => {
- successMsg.style.display = 'none';
- }, 5000);
-}
+// ============================================
+// EXPORT POUR DEBUG
+// ============================================
-function showError(message) {
- const errorMsg = document.getElementById('error-msg');
- document.getElementById('error-text').textContent = 'X ' + message;
- errorMsg.style.display = 'block';
-}
+window.GitPusher = {
+ config: GIT_PUSHER_CONFIG,
+ getSelectedApps: () => selectedApps,
+ checkServer: checkServerHealth,
+ version: GIT_PUSHER_CONFIG.version
+};
-function resetForm(showConfirm = false) {
- document.getElementById('git-url').value = '';
- document.getElementById('git-branch').value = 'main';
- document.getElementById('git-token').value = '';
- document.getElementById('commit-message').value = '';
- document.querySelectorAll('#dashboard-list input[type="checkbox"]').forEach(cb => cb.checked = false);
-
- // Demander si l'utilisateur veut aussi effacer les credentials sauvegardés
- // SEULEMENT si showConfirm = true (c'est-à-dire si l'utilisateur a cliqué sur Reset)
- if (showConfirm && document.getElementById('save-credentials').checked) {
- const confirmClear = confirm('Do you want to clear saved credentials?');
- if (confirmClear) {
- clearSavedCredentials();
- document.getElementById('save-credentials').checked = false;
+// ============================================
+// FIX: Attacher les événements aux boutons
+// ============================================
+(function attachButtonEvents() {
+ function tryAttach() {
+ var pushBtn = document.getElementById('push-btn');
+ if (pushBtn) {
+ pushBtn.addEventListener('click', function(e) {
+ e.preventDefault();
+ console.log("Push button clicked");
+ pushDashboards();
+ });
+ console.log("✓ Push button event attached");
+ } else {
+ // Réessayer dans 500ms
+ setTimeout(tryAttach, 500);
+ }
+ }
+
+ // Démarrer après un délai
+ if (document.readyState === 'complete') {
+ setTimeout(tryAttach, 1000);
+ } else {
+ window.addEventListener('load', function() {
+ setTimeout(tryAttach, 1000);
+ });
}
- }
-}
\ No newline at end of file
+})();
\ No newline at end of file
diff --git a/apps/pusher_app_prem/appserver/static/license_file_management.js b/apps/pusher_app_prem/appserver/static/license_file_management.js
new file mode 100755
index 00000000..f59a6676
--- /dev/null
+++ b/apps/pusher_app_prem/appserver/static/license_file_management.js
@@ -0,0 +1,613 @@
+// ============================================
+// SYSTÈME DE GESTION DE LICENCE FICHIER .LIC
+// ============================================
+
+const LICENSE_FILE_KEY = 'git_pusher_license_file';
+const LICENSE_STATUS_ENDPOINT = '/custom/git_pusher/license_status';
+
+let currentLicenseStatus = null;
+
+function initializeLicenseSystem() {
+ console.log("Initializing file-based license system...");
+
+ // Vérifier le statut de la licence
+ checkLicenseStatus();
+}
+
+function checkLicenseStatus() {
+ console.log("Checking license status...");
+
+ // Appeler le backend pour vérifier la licence
+ fetch(LICENSE_STATUS_ENDPOINT)
+ .then(response => response.json())
+ .then(data => {
+ console.log("License status:", data);
+ currentLicenseStatus = data;
+
+ if (data.licensed) {
+ // Licence valide
+ displayLicenseBadge(data.license_info, data.warnings);
+ } else {
+ // Pas de licence ou invalide
+ showLicenseUploadModal(data.errors);
+ }
+ })
+ .catch(error => {
+ console.error("Error checking license:", error);
+ // En cas d'erreur, afficher le modal
+ showLicenseUploadModal(["Impossible de vérifier la licence"]);
+ });
+}
+
+function displayLicenseBadge(licenseInfo, warnings) {
+ console.log("Displaying license badge...");
+
+ const container = document.getElementById('license-badge-container');
+ if (!container) {
+ console.error("license-badge-container not found");
+ return;
+ }
+
+ // Créer le badge
+ const badge = document.createElement('div');
+ badge.id = 'license-badge';
+ badge.style.cssText = `
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ padding: 12px 20px;
+ border-radius: 8px;
+ font-size: 12px;
+ font-weight: 600;
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
+ cursor: pointer;
+ transition: all 0.3s ease;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ text-align: center;
+ min-width: 200px;
+ `;
+
+ // Déterminer le texte et la couleur selon le type
+ let badgeText = '✓ Licence Activée';
+ let badgeColor = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
+
+ if (licenseInfo.type === 'trial') {
+ badgeText = `⏱️ Essai: ${licenseInfo.days_remaining} jours`;
+ badgeColor = 'linear-gradient(135deg, #ff9800 0%, #f57c00 100%)';
+ } else if (licenseInfo.type === 'standard') {
+ badgeText = `✓ Standard (${licenseInfo.days_remaining}j)`;
+ } else if (licenseInfo.type === 'enterprise') {
+ badgeText = `✓ Enterprise (${licenseInfo.days_remaining}j)`;
+ badgeColor = 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)';
+ }
+
+ // Avertissement si expire bientôt
+ if (licenseInfo.days_remaining < 30) {
+ badgeColor = 'linear-gradient(135deg, #ff9800 0%, #f57c00 100%)';
+ badgeText = `⚠️ Expire dans ${licenseInfo.days_remaining}j`;
+ }
+
+ badge.style.background = badgeColor;
+ badge.textContent = badgeText;
+
+ // Clic pour afficher les détails
+ badge.onclick = function() {
+ showLicenseDetailsModal(licenseInfo, warnings);
+ };
+
+ container.appendChild(badge);
+
+ // Hover effect
+ badge.addEventListener('mouseenter', function() {
+ this.style.transform = 'translateY(-3px)';
+ this.style.boxShadow = '0 6px 25px rgba(102, 126, 234, 0.5)';
+ });
+
+ badge.addEventListener('mouseleave', function() {
+ this.style.transform = 'translateY(0)';
+ this.style.boxShadow = '0 4px 15px rgba(102, 126, 234, 0.3)';
+ });
+}
+
+function showLicenseUploadModal(errors = []) {
+ console.log("Showing license upload modal");
+
+ // Créer le modal
+ const modal = document.createElement('div');
+ modal.id = 'license-modal';
+ modal.style.cssText = `
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.8);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10000;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ `;
+
+ const content = document.createElement('div');
+ content.style.cssText = `
+ background: white;
+ border-radius: 16px;
+ padding: 40px;
+ max-width: 550px;
+ width: 90%;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
+ animation: slideIn 0.3s ease;
+ `;
+
+ let errorsHtml = '';
+ if (errors.length > 0) {
+ errorsHtml = `
+
+
+ ⚠️ Problèmes détectés:
+
+
+ ${errors.map(e => `${e} `).join('')}
+
+
+ `;
+ }
+
+ content.innerHTML = `
+
+
🔐 Git Pusher
+
Activation de licence requise
+
+
+ ${errorsHtml}
+
+
+
+ 📋 Hostname détecté: Chargement...
+
+
+
+
+
+ 📂 Choisissez votre fichier de licence (.lic)
+
+
+
+
📄
+
+ Glissez votre fichier .lic ici
+
+
+ ou cliquez pour parcourir
+
+
+
+
+
+
+
+ 💡 Vous n'avez pas de licence? Contactez-nous pour obtenir votre fichier .lic personnalisé
+
+
+
+
+
+
+ 📤 Installer la licence
+
+ ⏱️ Essai gratuit
+
+ `;
+
+ modal.appendChild(content);
+ document.body.appendChild(modal);
+
+ // Ajouter l'animation CSS
+ const style = document.createElement('style');
+ style.textContent = `
+ @keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateY(-20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ }
+ `;
+ document.head.appendChild(style);
+
+ // Récupérer et afficher le hostname
+ getHostname().then(hostname => {
+ document.getElementById('detected-hostname').textContent = hostname;
+ });
+
+ // Configurer le drag & drop et file input
+ setupFileUpload();
+}
+
+function setupFileUpload() {
+ const dropZone = document.getElementById('drop-zone');
+ const fileInput = document.getElementById('license-file-input');
+ const fileInfo = document.getElementById('file-info');
+ const uploadBtn = document.getElementById('upload-btn');
+
+ let selectedFile = null;
+
+ // Clic sur la zone pour ouvrir le sélecteur
+ dropZone.onclick = () => fileInput.click();
+
+ // Hover effect
+ dropZone.addEventListener('mouseenter', function() {
+ this.style.background = '#eef1ff';
+ this.style.borderColor = '#5568d3';
+ });
+
+ dropZone.addEventListener('mouseleave', function() {
+ this.style.background = '#f8f9ff';
+ this.style.borderColor = '#667eea';
+ });
+
+ // Drag & drop
+ dropZone.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ dropZone.style.background = '#e3e7ff';
+ dropZone.style.borderColor = '#5568d3';
+ });
+
+ dropZone.addEventListener('dragleave', () => {
+ dropZone.style.background = '#f8f9ff';
+ dropZone.style.borderColor = '#667eea';
+ });
+
+ dropZone.addEventListener('drop', (e) => {
+ e.preventDefault();
+ dropZone.style.background = '#f8f9ff';
+ dropZone.style.borderColor = '#667eea';
+
+ const files = e.dataTransfer.files;
+ if (files.length > 0) {
+ handleFileSelection(files[0]);
+ }
+ });
+
+ // Sélection de fichier
+ fileInput.addEventListener('change', (e) => {
+ if (e.target.files.length > 0) {
+ handleFileSelection(e.target.files[0]);
+ }
+ });
+
+ function handleFileSelection(file) {
+ selectedFile = file;
+
+ // Vérifier l'extension
+ if (!file.name.endsWith('.lic')) {
+ showLicenseMessage('⚠️ Veuillez sélectionner un fichier .lic', 'warning');
+ return;
+ }
+
+ // Afficher les infos du fichier
+ document.getElementById('file-name').textContent = file.name;
+ document.getElementById('file-size').textContent = formatFileSize(file.size);
+ fileInfo.style.display = 'block';
+
+ // Activer le bouton
+ uploadBtn.disabled = false;
+ uploadBtn.style.cursor = 'pointer';
+ uploadBtn.style.opacity = '1';
+
+ // Stocker le fichier pour l'upload
+ window.selectedLicenseFile = file;
+
+ showLicenseMessage('✓ Fichier prêt à être installé', 'success');
+ }
+}
+
+function formatFileSize(bytes) {
+ if (bytes < 1024) return bytes + ' B';
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
+}
+
+function uploadLicenseFile() {
+ const file = window.selectedLicenseFile;
+
+ if (!file) {
+ showLicenseMessage('❌ Aucun fichier sélectionné', 'error');
+ return;
+ }
+
+ showLicenseMessage('⏳ Installation de la licence...', 'info');
+
+ // Lire le fichier
+ const reader = new FileReader();
+
+ reader.onload = function(e) {
+ const fileContent = e.target.result;
+
+ // Envoyer au backend pour validation et installation
+ fetch('/custom/git_pusher/install_license', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ license_file: fileContent,
+ filename: file.name
+ })
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ showLicenseMessage('✅ Licence installée avec succès !', 'success');
+
+ setTimeout(() => {
+ closeLicenseModal();
+ // Recharger pour afficher le badge
+ checkLicenseStatus();
+ }, 1500);
+ } else {
+ showLicenseMessage('❌ ' + (data.message || 'Erreur d\'installation'), 'error');
+
+ if (data.errors && data.errors.length > 0) {
+ const errorList = data.errors.map(e => `• ${e}`).join('\n');
+ console.error('License errors:', errorList);
+ }
+ }
+ })
+ .catch(error => {
+ console.error('Upload error:', error);
+ showLicenseMessage('❌ Erreur de connexion au serveur', 'error');
+ });
+ };
+
+ reader.onerror = function() {
+ showLicenseMessage('❌ Erreur de lecture du fichier', 'error');
+ };
+
+ reader.readAsText(file);
+}
+
+function requestTrial() {
+ showLicenseMessage('⏳ Demande d\'essai en cours...', 'info');
+
+ // Appeler le backend pour créer une licence d'essai
+ fetch('/custom/git_pusher/request_trial', {
+ method: 'POST'
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ showLicenseMessage('✅ Licence d\'essai activée (7 jours) !', 'success');
+
+ setTimeout(() => {
+ closeLicenseModal();
+ checkLicenseStatus();
+ }, 1500);
+ } else {
+ showLicenseMessage('❌ ' + (data.message || 'Erreur'), 'error');
+ }
+ })
+ .catch(error => {
+ console.error('Trial request error:', error);
+ showLicenseMessage('❌ Erreur de connexion', 'error');
+ });
+}
+
+function showLicenseDetailsModal(licenseInfo, warnings) {
+ // Créer un modal pour afficher les détails de la licence
+ const modal = document.createElement('div');
+ modal.id = 'license-details-modal';
+ modal.style.cssText = `
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10001;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ `;
+
+ const content = document.createElement('div');
+ content.style.cssText = `
+ background: white;
+ border-radius: 16px;
+ padding: 40px;
+ max-width: 500px;
+ width: 90%;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
+ `;
+
+ let warningsHtml = '';
+ if (warnings && warnings.length > 0) {
+ warningsHtml = `
+
+ ${warnings.map(w => `
${w}
`).join('')}
+
+ `;
+ }
+
+ const featuresHtml = Object.entries(licenseInfo.features || {})
+ .map(([feature, enabled]) => {
+ const icon = enabled ? '✅' : '❌';
+ return `${icon} ${feature}
`;
+ }).join('');
+
+ content.innerHTML = `
+
+
🔐 Informations de licence
+
+
+
+
+
🆔 ID de licence:
+
${licenseInfo.license_id}
+
+
+
+
👤 Client:
+
${licenseInfo.customer}
+
+
+
+
🏷️ Type:
+
${licenseInfo.type.toUpperCase()}
+
+
+
+
📅 Expire le:
+
${new Date(licenseInfo.expires).toLocaleDateString('fr-FR')}
+
+
+
+
⏰ Jours restants:
+
${licenseInfo.days_remaining} jours
+
+
+
+
+
✨ Fonctionnalités:
+
+ ${featuresHtml}
+
+
+
+ ${warningsHtml}
+
+
+ 🔄 Changer de licence
+
+ Fermer
+
+ `;
+
+ modal.appendChild(content);
+ document.body.appendChild(modal);
+}
+
+function closeDetailsModal() {
+ const modal = document.getElementById('license-details-modal');
+ if (modal) {
+ modal.remove();
+ }
+}
+
+function showLicenseMessage(message, type) {
+ const messageEl = document.getElementById('license-message');
+ if (!messageEl) return;
+
+ messageEl.style.display = 'block';
+ messageEl.textContent = message;
+
+ if (type === 'success') {
+ messageEl.style.background = '#d4edda';
+ messageEl.style.color = '#155724';
+ messageEl.style.border = '1px solid #c3e6cb';
+ } else if (type === 'error') {
+ messageEl.style.background = '#f8d7da';
+ messageEl.style.color = '#721c24';
+ messageEl.style.border = '1px solid #f5c6cb';
+ } else if (type === 'warning') {
+ messageEl.style.background = '#fff3cd';
+ messageEl.style.color = '#856404';
+ messageEl.style.border = '1px solid #ffeaa7';
+ } else if (type === 'info') {
+ messageEl.style.background = '#d1ecf1';
+ messageEl.style.color = '#0c5460';
+ messageEl.style.border = '1px solid #bee5eb';
+ }
+}
+
+function closeLicenseModal() {
+ const modal = document.getElementById('license-modal');
+ if (modal) {
+ modal.remove();
+ }
+}
+
+function getHostname() {
+ return new Promise((resolve) => {
+ fetch('/en-US/splunkd/__raw/services/server/info?output_mode=json')
+ .then(r => r.json())
+ .then(d => {
+ const hostname = d.entry?.[0]?.content?.host || 'unknown';
+ resolve(hostname);
+ })
+ .catch(() => resolve('unknown'));
+ });
+}
+
+function checkLicenseBeforePush() {
+ if (!currentLicenseStatus || !currentLicenseStatus.licensed) {
+ alert('⚠️ Aucune licence valide détectée. Veuillez installer une licence.');
+ showLicenseUploadModal([]);
+ return false;
+ }
+
+ return true;
+}
+
+// Initialiser au chargement de la page
+document.addEventListener('DOMContentLoaded', function() {
+ initializeLicenseSystem();
+});
diff --git a/apps/pusher_app_prem/appserver/static/license_validation.js b/apps/pusher_app_prem/appserver/static/license_validation.js
old mode 100644
new mode 100755
index b668a4a8..ad0093db
--- a/apps/pusher_app_prem/appserver/static/license_validation.js
+++ b/apps/pusher_app_prem/appserver/static/license_validation.js
@@ -1,368 +1,598 @@
// ============================================
-// SYSTÈME DE VALIDATION DE LICENCE
+// SYSTÈME DE VALIDATION DE LICENCE - VERSION 2.0
+// Avec support fichier .lic
// ============================================
-const LICENSE_STORAGE_KEY = 'git_pusher_license';
+const LICENSE_API_URL = window.location.protocol + '//' + window.location.hostname + ':9999';
-function initializeLicense() {
- console.log("Initializing license system...");
-
- // Vérifier si une licence est déjà stockée
- const storedLicense = getCookie(LICENSE_STORAGE_KEY);
-
- if (storedLicense) {
- // Valider la licence stockée
- validateStoredLicense(storedLicense);
- // Afficher les infos de licence
- displayLicenseInfo(storedLicense);
- } else {
- // Afficher la page de licence
- showLicenseModal();
- }
+// État global de la licence
+let currentLicense = null;
+let currentHostname = null;
+
+// ============================================
+// INITIALISATION
+// ============================================
+
+async function initializeLicense() {
+ console.log("Initializing license system v2.0...");
+
+ try {
+ // Récupérer le statut de la licence depuis le serveur
+ const response = await fetch(`${LICENSE_API_URL}/license/status`);
+ const data = await response.json();
+
+ console.log("License status:", data);
+
+ currentHostname = data.hostname;
+
+ if (data.status === 'valid' && data.license) {
+ // Licence valide
+ currentLicense = data.license;
+ displayLicenseInfo(data.license);
+ hideLicenseModal();
+ } else {
+ // Pas de licence ou licence invalide
+ showLicenseModal(data.error, data.error_code, data.hostname);
+ }
+ } catch (error) {
+ console.error("Error checking license:", error);
+ // En cas d'erreur réseau, afficher le modal
+ showLicenseModal("Impossible de vérifier la licence. Le serveur est-il démarré?", "CONNECTION_ERROR");
+ }
}
+// ============================================
+// AFFICHAGE DU BADGE DE LICENCE
+// ============================================
+
function displayLicenseInfo(license) {
- console.log("Displaying license info...");
-
- // Chercher le container du badge
- const container = document.getElementById('license-badge-container');
- if (!container) {
- console.error("license-badge-container not found");
- return;
- }
-
- // Créer le badge
- const badge = document.createElement('div');
- badge.id = 'license-badge';
- badge.style.cssText = `
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- padding: 12px 20px;
- border-radius: 8px;
- font-size: 12px;
- font-weight: 600;
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
- cursor: pointer;
- transition: all 0.3s ease;
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- text-align: center;
- min-width: 200px;
- `;
-
- let badgeText = '✓ Licence Activée';
-
- // Si c'est une licence d'essai
- if (license.startsWith('TRIAL-')) {
- const daysRemaining = getTrialDaysRemaining(license);
+ console.log("Displaying license info:", license);
+
+ const container = document.getElementById('license-badge-container');
+ if (!container) {
+ console.error("license-badge-container not found");
+ return;
+ }
+
+ // Supprimer l'ancien badge s'il existe
+ const oldBadge = document.getElementById('license-badge');
+ if (oldBadge) oldBadge.remove();
+
+ // Créer le badge
+ const badge = document.createElement('div');
+ badge.id = 'license-badge';
+
+ // Déterminer le style selon le type et les jours restants
+ let badgeStyle = '';
+ let badgeText = '';
+ let badgeIcon = '✓';
+
+ const daysRemaining = license.days_remaining || 0;
+ const licenseType = license.type_name || license.type || 'Unknown';
if (daysRemaining <= 0) {
- badgeText = '⏱️ Essai expiré';
- badge.style.background = 'linear-gradient(135deg, #f44336 0%, #da190b 100%)';
- } else if (daysRemaining <= 2) {
- badgeText = `⚠️ ${daysRemaining} jour${daysRemaining > 1 ? 's' : ''} restant${daysRemaining > 1 ? 's' : ''}`;
- badge.style.background = 'linear-gradient(135deg, #ff9800 0%, #f57c00 100%)';
+ // Expirée
+ badgeStyle = 'background: linear-gradient(135deg, #f44336 0%, #da190b 100%);';
+ badgeText = '⚠ Licence expirée';
+ badgeIcon = '⚠';
+ } else if (daysRemaining <= 7) {
+ // Expire bientôt
+ badgeStyle = 'background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);';
+ badgeText = `⚠ ${licenseType} - ${daysRemaining}j restants`;
+ badgeIcon = '⚠';
+ } else if (license.type === 'trial') {
+ // Essai
+ badgeStyle = 'background: linear-gradient(135deg, #9c27b0 0%, #7b1fa2 100%);';
+ badgeText = `⏱ Essai - ${daysRemaining}j restants`;
+ badgeIcon = '⏱';
} else {
- badgeText = `⏱️ Essai: ${daysRemaining} jours`;
+ // Licence normale valide
+ badgeStyle = 'background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%);';
+ badgeText = `✓ ${licenseType}`;
+ badgeIcon = '✓';
}
- }
-
- badge.textContent = badgeText;
- badge.onclick = function() {
- alert('Licence: ' + license.substring(0, 50) + '...\n\nClique sur le logo pour gérer ta licence.');
- };
-
- container.appendChild(badge);
-
- // Ajouter un hover effect
- badge.addEventListener('mouseenter', function() {
- this.style.transform = 'translateY(-3px)';
- this.style.boxShadow = '0 6px 25px rgba(102, 126, 234, 0.5)';
- });
-
- badge.addEventListener('mouseleave', function() {
- this.style.transform = 'translateY(0)';
- this.style.boxShadow = '0 4px 15px rgba(102, 126, 234, 0.3)';
- });
-}
-
-function getTrialDaysRemaining(trialLicense) {
- // Extraire le timestamp du license (format: TRIAL-timestamp)
- const parts = trialLicense.split('-');
- if (parts.length !== 2) return 0;
-
- const timestamp = parseInt(parts[1]);
- if (isNaN(timestamp)) return 0;
-
- // Créer la date de création
- const createdDate = new Date(timestamp);
-
- // Ajouter 7 jours
- const expirationDate = new Date(createdDate.getTime() + (7 * 24 * 60 * 60 * 1000));
-
- // Calculer les jours restants
- const now = new Date();
- const daysRemaining = Math.ceil((expirationDate - now) / (1000 * 60 * 60 * 24));
-
- console.log("Trial created:", createdDate);
- console.log("Trial expires:", expirationDate);
- console.log("Days remaining:", daysRemaining);
-
- return Math.max(0, daysRemaining);
-}
-
-function showLicenseModal() {
- console.log("Showing license modal");
-
- // Créer le modal HTML
- const modal = document.createElement('div');
- modal.id = 'license-modal';
- modal.style.cssText = `
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.7);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 10000;
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- `;
-
- const content = document.createElement('div');
- content.style.cssText = `
- background: white;
- border-radius: 16px;
- padding: 40px;
- max-width: 500px;
- width: 90%;
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
- `;
-
- content.innerHTML = `
-
-
🔐 Git Pusher
-
Activation de licence requise
-
-
-
-
- 📋 Hostname détecté: Chargement...
-
-
-
-
-
- Entrez votre clé de licence
-
-
-
- Vous n'avez pas de licence? Cliquez ici
-
-
-
-
-
-
- Activer la licence
- Essai gratuit (7 jours)
-
- `;
-
- modal.appendChild(content);
- document.body.appendChild(modal);
-
- // Afficher le hostname
- getHostname().then(hostname => {
- document.getElementById('detected-hostname').textContent = hostname;
- });
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ text-align: center;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ `;
+
+ badge.innerHTML = `
+ ${badgeIcon}
+ ${badgeText}
+ `;
+
+ // Click pour voir les détails
+ badge.onclick = function() {
+ showLicenseDetails(license);
+ };
+
+ // Hover effect
+ badge.addEventListener('mouseenter', function() {
+ this.style.transform = 'translateY(-2px)';
+ this.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.3)';
+ });
+
+ badge.addEventListener('mouseleave', function() {
+ this.style.transform = 'translateY(0)';
+ this.style.boxShadow = '0 4px 15px rgba(0, 0, 0, 0.2)';
+ });
+
+ container.appendChild(badge);
+}
+
+// ============================================
+// MODAL DE DÉTAILS DE LICENCE
+// ============================================
+
+function showLicenseDetails(license) {
+ const modal = document.createElement('div');
+ modal.id = 'license-details-modal';
+ modal.style.cssText = `
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10000;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ `;
+
+ const features = (license.features || []).map(f => `${f} `).join('');
+
+ const limits = license.limits || {};
+ const maxApps = limits.max_apps === -1 ? 'Illimité' : limits.max_apps;
+ const maxPushes = limits.max_pushes_per_day === -1 ? 'Illimité' : limits.max_pushes_per_day + '/jour';
+
+ modal.innerHTML = `
+
+
+
📋 Détails de la licence
+ ×
+
+
+
+
+
+
Type
+
${license.type_name || license.type}
+
+
+
ID
+
${license.license_id}
+
+
+
Expire
+
${license.expires}
+
+
+
Jours restants
+
${license.days_remaining}
+
+
+
+
+
+
Client
+
${license.customer?.name || 'N/A'} (${license.customer?.email || 'N/A'})
+
+
+
+
Hostname
+
${license.hostname || currentHostname}
+
+
+
+
Limites
+
Apps: ${maxApps} | Pushes: ${maxPushes}
+
+
+
+
Fonctionnalités
+
${features || 'Aucune '}
+
+
+
+ 🔄 Changer de licence
+ Fermer
+
+
+ `;
+
+ document.body.appendChild(modal);
}
-function showGeneratorInfo() {
- alert(`Pour générer une clé de licence, exécutez sur le serveur Splunk:
+// ============================================
+// MODAL D'UPLOAD DE LICENCE
+// ============================================
-python /opt/splunk/etc/apps/pusher_app/bin/license_generator.py
+function showLicenseModal(error = null, errorCode = null, hostname = null) {
+ console.log("Showing license modal", { error, errorCode, hostname });
+
+ // Supprimer l'ancien modal s'il existe
+ hideLicenseModal();
+ const detailsModal = document.getElementById('license-details-modal');
+ if (detailsModal) detailsModal.remove();
+
+ const modal = document.createElement('div');
+ modal.id = 'license-modal';
+ modal.style.cssText = `
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10000;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ `;
+
+ // Message d'erreur si présent
+ let errorHtml = '';
+ if (error) {
+ let errorStyle = 'background: #ffebee; color: #c62828; border-left: 4px solid #f44336;';
+ if (errorCode === 'NO_LICENSE') {
+ errorStyle = 'background: #fff3e0; color: #e65100; border-left: 4px solid #ff9800;';
+ }
+ errorHtml = `
+
+ ${error}
+
+ `;
+ }
+
+ modal.innerHTML = `
+
+
+
🔐 Git Pusher
+
Activation de licence requise
+
+
+ ${errorHtml}
+
+
+
+ 📋 Hostname Splunk:
+ ${hostname || 'Chargement...'}
+
+
+ Communiquez ce hostname pour obtenir votre licence.
+
+
+
+
+
+ 📄 Fichier de licence (.lic)
+
+
+
+
📁
+
+ Glissez votre fichier .lic ici
+ ou cliquez pour sélectionner
+
+
+
+
+
+
+ ✕
+
+
+
+
+
+
Activer la licence
+
+
+
+ ${currentLicense ? `
+
+
+ Annuler
+
+
+ ` : ''}
+
+ `;
+
+ document.body.appendChild(modal);
+
+ // Récupérer le hostname si pas fourni
+ if (!hostname) {
+ fetch(`${LICENSE_API_URL}/license/hostname`)
+ .then(r => r.json())
+ .then(d => {
+ const codeEl = modal.querySelector('code');
+ if (codeEl) codeEl.textContent = d.hostname || 'Inconnu';
+ currentHostname = d.hostname;
+ })
+ .catch(() => {});
+ }
+}
-Cela générera une clé basée sur votre hostname.`);
+function hideLicenseModal() {
+ const modal = document.getElementById('license-modal');
+ if (modal) modal.remove();
}
-function getHostname() {
- return new Promise((resolve) => {
- fetch('/en-US/splunkd/__raw/services/server/info?output_mode=json')
- .then(r => r.json())
- .then(d => {
- const hostname = d.entry?.[0]?.content?.host || 'unknown';
- resolve(hostname);
- })
- .catch(() => resolve('unknown'));
- });
+// ============================================
+// GESTION DU FICHIER
+// ============================================
+
+let selectedLicenseContent = null;
+
+function handleDragOver(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ event.currentTarget.style.borderColor = '#667eea';
+ event.currentTarget.style.background = '#f0f4ff';
}
-function validateLicenseInput() {
- const licenseInput = document.getElementById('license-input').value.trim();
-
- if (!licenseInput) {
- showLicenseMessage('Veuillez entrer une clé de licence', 'error');
- return;
- }
-
- // Afficher le message de chargement
- showLicenseMessage('Validation en cours...', 'info');
-
- // Simuler la validation (en production, faire un appel à un serveur)
- // Pour l'instant, on accepte juste n'importe quelle licence
- if (licenseInput.length > 20) {
- // Stocker la licence
- setCookie(LICENSE_STORAGE_KEY, licenseInput, 365);
-
- showLicenseMessage('✓ Licence activée avec succès!', 'success');
-
- setTimeout(() => {
- closeLicenseModal();
- // Afficher les infos de licence
- displayLicenseInfo(licenseInput);
- }, 1500);
- } else {
- showLicenseMessage('Format de licence invalide', 'error');
- }
+function handleDragLeave(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ event.currentTarget.style.borderColor = '#ccc';
+ event.currentTarget.style.background = '#fafafa';
}
-function skipLicense() {
- // Créer une licence d'essai avec timestamp (format: TRIAL-timestamp)
- const trialLicense = 'TRIAL-' + Date.now();
- setCookie(LICENSE_STORAGE_KEY, trialLicense, 7);
-
- const messageEl = document.getElementById('license-message');
- messageEl.style.display = 'block';
- messageEl.style.background = '#fff3cd';
- messageEl.style.color = '#856404';
- messageEl.style.border = '1px solid #ffeaa7';
- messageEl.textContent = '⏱️ Mode essai activé pour 7 jours';
-
- setTimeout(() => {
- closeLicenseModal();
- // Afficher les infos de licence
- displayLicenseInfo(trialLicense);
- }, 1500);
+function handleDrop(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ event.currentTarget.style.borderColor = '#ccc';
+ event.currentTarget.style.background = '#fafafa';
+
+ const files = event.dataTransfer.files;
+ if (files.length > 0) {
+ processFile(files[0]);
+ }
}
-function showLicenseMessage(message, type) {
- const messageEl = document.getElementById('license-message');
- messageEl.style.display = 'block';
- messageEl.textContent = message;
-
- if (type === 'success') {
- messageEl.style.background = '#d4edda';
- messageEl.style.color = '#155724';
- messageEl.style.border = '1px solid #c3e6cb';
- } else if (type === 'error') {
- messageEl.style.background = '#f8d7da';
- messageEl.style.color = '#721c24';
- messageEl.style.border = '1px solid #f5c6cb';
- } else if (type === 'info') {
- messageEl.style.background = '#d1ecf1';
- messageEl.style.color = '#0c5460';
- messageEl.style.border = '1px solid #bee5eb';
- }
+function handleFileSelect(event) {
+ const files = event.target.files;
+ if (files.length > 0) {
+ processFile(files[0]);
+ }
}
-function validateStoredLicense(license) {
- console.log("Validating stored license...");
-
- // Pour l'instant, accepter simplement la licence stockée
- // En production, faire une validation serveur
- if (license && license.length > 5) {
- console.log("License is valid");
- return true;
- }
-
- // Si invalide, afficher le modal à nouveau
- showLicenseModal();
- return false;
+function processFile(file) {
+ console.log("Processing file:", file.name);
+
+ if (!file.name.endsWith('.lic')) {
+ showLicenseMessage('Veuillez sélectionner un fichier .lic', 'error');
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onload = function(e) {
+ selectedLicenseContent = e.target.result;
+
+ // Afficher le nom du fichier
+ document.getElementById('selected-file-info').style.display = 'block';
+ document.getElementById('selected-file-name').textContent = '📄 ' + file.name;
+
+ // Activer le bouton
+ const btn = document.getElementById('activate-btn');
+ btn.disabled = false;
+ btn.style.opacity = '1';
+ btn.style.cursor = 'pointer';
+
+ showLicenseMessage('Fichier prêt à être activé', 'info');
+ };
+ reader.onerror = function() {
+ showLicenseMessage('Erreur de lecture du fichier', 'error');
+ };
+ reader.readAsText(file);
}
-function closeLicenseModal() {
- const modal = document.getElementById('license-modal');
- if (modal) {
- modal.remove();
- }
+function clearSelectedFile() {
+ selectedLicenseContent = null;
+ document.getElementById('selected-file-info').style.display = 'none';
+ document.getElementById('license-file-input').value = '';
+
+ const btn = document.getElementById('activate-btn');
+ btn.disabled = true;
+ btn.style.opacity = '0.5';
+
+ const msgEl = document.getElementById('license-message');
+ msgEl.style.display = 'none';
}
-function checkLicenseBeforePush() {
- const license = getCookie(LICENSE_STORAGE_KEY);
-
- if (!license) {
- alert('Veuillez d\'abord activer une licence');
- showLicenseModal();
- return false;
- }
-
- // Vérifier si c'est une licence d'essai expirée
- if (license.startsWith('TRIAL-')) {
- // À implémenter : vérifier la date
- }
-
- return true;
+// ============================================
+// UPLOAD ET ACTIVATION
+// ============================================
+
+async function uploadLicense() {
+ if (!selectedLicenseContent) {
+ showLicenseMessage('Veuillez sélectionner un fichier de licence', 'error');
+ return;
+ }
+
+ showLicenseMessage('⏳ Validation en cours...', 'info');
+
+ const btn = document.getElementById('activate-btn');
+ btn.disabled = true;
+ btn.textContent = 'Validation...';
+
+ try {
+ const response = await fetch(`${LICENSE_API_URL}/license/upload`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ license_content: selectedLicenseContent
+ })
+ });
+
+ const result = await response.json();
+ console.log("Upload result:", result);
+
+ if (result.success) {
+ showLicenseMessage('✓ Licence activée avec succès!', 'success');
+ currentLicense = result.license_info;
+
+ setTimeout(() => {
+ hideLicenseModal();
+ displayLicenseInfo(result.license_info);
+ }, 1500);
+ } else {
+ showLicenseMessage(result.error || 'Erreur d\'activation', 'error');
+ btn.disabled = false;
+ btn.textContent = 'Activer la licence';
+ }
+ } catch (error) {
+ console.error("Upload error:", error);
+ showLicenseMessage('Erreur de connexion au serveur', 'error');
+ btn.disabled = false;
+ btn.textContent = 'Activer la licence';
+ }
}
// ============================================
-// FONCTIONS UTILITAIRES DE COOKIE
+// UTILITAIRES
// ============================================
-function setCookie(name, value, days) {
- const d = new Date();
- d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
- const expires = "expires=" + d.toUTCString();
- document.cookie = name + "=" + encodeURIComponent(value) + ";" + expires + ";path=/";
- console.log("Cookie set: " + name);
+function showLicenseMessage(message, type) {
+ const msgEl = document.getElementById('license-message');
+ if (!msgEl) return;
+
+ msgEl.style.display = 'block';
+ msgEl.textContent = message;
+
+ switch (type) {
+ case 'success':
+ msgEl.style.background = '#e8f5e9';
+ msgEl.style.color = '#2e7d32';
+ msgEl.style.border = '1px solid #a5d6a7';
+ break;
+ case 'error':
+ msgEl.style.background = '#ffebee';
+ msgEl.style.color = '#c62828';
+ msgEl.style.border = '1px solid #ef9a9a';
+ break;
+ case 'info':
+ msgEl.style.background = '#e3f2fd';
+ msgEl.style.color = '#1565c0';
+ msgEl.style.border = '1px solid #90caf9';
+ break;
+ }
+}
+
+function showContactInfo() {
+ alert(`Pour obtenir une licence Git Pusher:
+
+1. Copiez votre hostname Splunk affiché ci-dessus
+2. Contactez-nous avec:
+ - Votre hostname
+ - Votre email
+ - Le type de licence souhaité
+
+Email: support@gitpusher.com
+Site: https://gitpusher.com`);
}
-function getCookie(name) {
- const nameEQ = name + "=";
- const ca = document.cookie.split(';');
- for (let i = 0; i < ca.length; i++) {
- let c = ca[i].trim();
- if (c.indexOf(nameEQ) === 0) {
- return decodeURIComponent(c.substring(nameEQ.length));
+// ============================================
+// VÉRIFICATION AVANT PUSH
+// ============================================
+
+async function checkLicenseBeforePush() {
+ try {
+ const response = await fetch(`${LICENSE_API_URL}/license/status`);
+ const data = await response.json();
+
+ if (data.status !== 'valid') {
+ alert('⚠️ ' + (data.error || 'Licence invalide ou absente'));
+ showLicenseModal(data.error, data.error_code, data.hostname);
+ return false;
+ }
+
+ // Vérifier les limites
+ const limits = data.license?.limits || {};
+ const usage = data.usage || {};
+
+ if (limits.max_pushes_per_day > 0 && usage.pushes_today >= limits.max_pushes_per_day) {
+ alert(`⚠️ Limite quotidienne atteinte (${limits.max_pushes_per_day} pushes/jour)`);
+ return false;
+ }
+
+ return true;
+ } catch (error) {
+ console.error("License check error:", error);
+ alert('⚠️ Impossible de vérifier la licence');
+ return false;
}
- }
- return "";
}
-function deleteCookie(name) {
- setCookie(name, "", -1);
- console.log("Cookie deleted: " + name);
-}
\ No newline at end of file
+// ============================================
+// EXPORT POUR UTILISATION EXTERNE
+// ============================================
+
+window.LicenseManager = {
+ init: initializeLicense,
+ check: checkLicenseBeforePush,
+ showModal: showLicenseModal,
+ getLicense: () => currentLicense,
+ getHostname: () => currentHostname
+};
diff --git a/apps/pusher_app_prem/bin/README b/apps/pusher_app_prem/bin/README
old mode 100644
new mode 100755
diff --git a/apps/pusher_app_prem/bin/__pycache__/license_validator.cpython-39.pyc b/apps/pusher_app_prem/bin/__pycache__/license_validator.cpython-39.pyc
new file mode 100644
index 00000000..fe7ac122
Binary files /dev/null and b/apps/pusher_app_prem/bin/__pycache__/license_validator.cpython-39.pyc differ
diff --git a/apps/pusher_app_prem/bin/git_pusher.pid b/apps/pusher_app_prem/bin/git_pusher.pid
index 81d2a971..679e2eb3 100644
--- a/apps/pusher_app_prem/bin/git_pusher.pid
+++ b/apps/pusher_app_prem/bin/git_pusher.pid
@@ -1 +1 @@
-919562
+212462
diff --git a/apps/pusher_app_prem/bin/git_pusher.py b/apps/pusher_app_prem/bin/git_pusher.py
old mode 100644
new mode 100755
index 6f0c0ceb..f312e389
--- a/apps/pusher_app_prem/bin/git_pusher.py
+++ b/apps/pusher_app_prem/bin/git_pusher.py
@@ -1,5 +1,11 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
+"""
+Git Pusher - Main Server
+Serveur HTTP pour pousser les applications Splunk vers Git
+
+Avec système de licence par fichier .lic
+"""
import sys
import os
@@ -12,6 +18,29 @@ from datetime import datetime
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import parse_qs, urlparse
+# Importer le validateur de licence
+# En production, ce fichier sera dans le même dossier
+try:
+ from license_validator import (
+ validate_license,
+ save_license_file,
+ check_limits,
+ increment_usage,
+ get_splunk_hostname,
+ get_usage_stats,
+ parse_license_content
+ )
+except ImportError:
+ # Fallback pour le développement
+ print("Warning: license_validator not found, running without license checks")
+ def validate_license(): return {"valid": True, "type": "dev", "days_remaining": 999}
+ def save_license_file(c): return {"success": True}
+ def check_limits(): return {"allowed": True}
+ def increment_usage(): return {}
+ def get_splunk_hostname(): return "dev-host"
+ def get_usage_stats(): return {}
+ def parse_license_content(c): return {}
+
# Configuration du logging
log_dir = '/opt/splunk/var/log/splunk'
os.makedirs(log_dir, exist_ok=True)
@@ -30,65 +59,209 @@ logger = logging.getLogger('git_pusher')
class GitPusherRequestHandler(BaseHTTPRequestHandler):
"""Handler pour les requêtes HTTP"""
+ def send_cors_headers(self):
+ """Envoyer les headers CORS complets"""
+ # Permettre toutes les origines
+ origin = self.headers.get('Origin', '*')
+ self.send_header('Access-Control-Allow-Origin', origin)
+ self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, PUT, DELETE')
+ self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, Origin')
+ self.send_header('Access-Control-Allow-Credentials', 'true')
+ self.send_header('Access-Control-Max-Age', '86400') # Cache preflight 24h
+
def do_OPTIONS(self):
"""Traiter les requêtes OPTIONS (CORS preflight)"""
+ logger.info(f"OPTIONS request from {self.headers.get('Origin', 'unknown')}")
self.send_response(200)
- self.send_header('Access-Control-Allow-Origin', '*')
- self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
- self.send_header('Access-Control-Allow-Headers', 'Content-Type')
+ self.send_cors_headers()
self.end_headers()
+ # Important: ne rien écrire dans le body pour OPTIONS
+ return
+
+ def do_GET(self):
+ """Traiter les requêtes GET"""
+ self.send_response(200)
+ self.send_header('Content-type', 'application/json')
+ self.send_cors_headers()
+ self.end_headers()
+
+ try:
+ parsed_url = urlparse(self.path)
+ path = parsed_url.path
+
+ # ============================================
+ # ENDPOINTS LICENCE
+ # ============================================
+
+ if path == '/license' or path == '/license/status':
+ # Récupérer le statut de la licence
+ validation = validate_license()
+ usage = get_usage_stats()
+ hostname = get_splunk_hostname()
+
+ response = {
+ "status": "valid" if validation.get("valid") else "invalid",
+ "hostname": hostname,
+ "license": validation if validation.get("valid") else None,
+ "error": validation.get("error") if not validation.get("valid") else None,
+ "error_code": validation.get("error_code") if not validation.get("valid") else None,
+ "usage": usage
+ }
+ self.wfile.write(json.dumps(response).encode())
+
+ elif path == '/license/hostname':
+ # Juste le hostname
+ response = {"hostname": get_splunk_hostname()}
+ self.wfile.write(json.dumps(response).encode())
+
+ elif path == '/health':
+ # Health check
+ response = {
+ "status": "ok",
+ "service": "git_pusher",
+ "timestamp": datetime.now().isoformat()
+ }
+ self.wfile.write(json.dumps(response).encode())
+
+ else:
+ response = {"error": "Unknown endpoint", "path": path}
+ self.wfile.write(json.dumps(response).encode())
+
+ except Exception as e:
+ logger.error(f"GET error: {e}")
+ self.wfile.write(json.dumps({"error": str(e)}).encode())
def do_POST(self):
"""Traiter les requêtes POST"""
- # Envoyer les headers CORS EN PREMIER
self.send_response(200)
self.send_header('Content-type', 'application/json')
- self.send_header('Access-Control-Allow-Origin', '*')
- self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
- self.send_header('Access-Control-Allow-Headers', 'Content-Type')
+ self.send_cors_headers()
self.end_headers()
try:
- logger.info(f"POST request to {self.path}")
-
- # Parser l'URL et les paramètres
parsed_url = urlparse(self.path)
+ path = parsed_url.path
query_params = parse_qs(parsed_url.query)
- logger.info(f"Query params keys: {list(query_params.keys())}")
+ logger.info(f"POST request to {path}")
+ # ============================================
+ # ENDPOINTS LICENCE
+ # ============================================
+
+ if path == '/license/upload':
+ # Uploader une nouvelle licence
+ content_length = int(self.headers.get('Content-Length', 0))
+ body = self.rfile.read(content_length).decode('utf-8')
+
+ try:
+ data = json.loads(body)
+ license_content = data.get('license_content', '')
+ except:
+ license_content = body
+
+ if not license_content:
+ response = {"success": False, "error": "Contenu de licence vide"}
+ else:
+ response = save_license_file(license_content)
+
+ self.wfile.write(json.dumps(response).encode())
+ return
+
+ elif path == '/license/delete':
+ # Supprimer la licence
+ license_path = "/opt/splunk/etc/apps/pusher_app_prem/local/license.lic"
+ if os.path.exists(license_path):
+ os.remove(license_path)
+ response = {"success": True, "message": "Licence supprimée"}
+ else:
+ response = {"success": False, "error": "Aucune licence à supprimer"}
+
+ self.wfile.write(json.dumps(response).encode())
+ return
+
+ # ============================================
+ # ENDPOINT PUSH GIT
+ # ============================================
+
+ elif path == '/push' or path.startswith('/services/'):
+ # Vérifier la licence avant le push
+ license_check = check_limits()
+ if not license_check.get("allowed"):
+ response = {
+ "status": "error",
+ "error_code": "LICENSE_ERROR",
+ "message": license_check.get("error", "Licence invalide ou limite atteinte")
+ }
+ self.wfile.write(json.dumps(response).encode())
+ return
+
+ # Traiter le push Git
+ self.handle_git_push(query_params)
+ return
+
+ else:
+ # Ancien comportement pour compatibilité
+ # Vérifier la licence
+ license_check = check_limits()
+ if not license_check.get("allowed"):
+ response = {
+ "status": "error",
+ "error_code": "LICENSE_ERROR",
+ "message": license_check.get("error", "Licence invalide ou limite atteinte")
+ }
+ self.wfile.write(json.dumps(response).encode())
+ return
+
+ self.handle_git_push(query_params)
+
+ except Exception as e:
+ logger.error(f"POST error: {str(e)}", exc_info=True)
+ response = {
+ "status": "error",
+ "message": f"Error: {str(e)}"
+ }
+ self.wfile.write(json.dumps(response).encode())
+
+ def handle_git_push(self, query_params):
+ """Gérer le push Git"""
+ try:
# Extraire les paramètres
git_url = query_params.get('git_url', [''])[0]
git_branch = query_params.get('git_branch', ['main'])[0]
git_token = query_params.get('git_token', [''])[0]
commit_message = query_params.get('commit_message', [''])[0]
-
- # Accepter soit 'apps' soit 'dashboards'
apps_json = query_params.get('apps', query_params.get('dashboards', ['[]']))[0]
user = query_params.get('user', ['unknown'])[0]
- logger.info(f"Parameters received: git_url={git_url}, branch={git_branch}, user={user}")
- logger.info(f"Raw apps_json: '{apps_json}'")
+ logger.info(f"Parameters: git_url={git_url}, branch={git_branch}, user={user}")
# Parser les apps
try:
- # parse_qs décode déjà, mais au cas où
- if isinstance(apps_json, str):
- apps = json.loads(apps_json)
- else:
- apps = apps_json
+ apps = json.loads(apps_json) if isinstance(apps_json, str) else apps_json
except (json.JSONDecodeError, TypeError) as e:
- logger.error(f"JSON parse error: {e} - trying to parse: {apps_json}")
+ logger.error(f"JSON parse error: {e}")
apps = []
- logger.info(f"Parsed apps: {len(apps)} items - {apps}")
+ logger.info(f"Parsed apps: {len(apps)} items")
+
+ # Vérifier les limites d'apps
+ license_info = validate_license()
+ max_apps = license_info.get("limits", {}).get("max_apps", -1)
+ if max_apps > 0 and len(apps) > max_apps:
+ response = {
+ "status": "error",
+ "error_code": "APP_LIMIT",
+ "message": f"Votre licence permet {max_apps} apps max. Vous en avez sélectionné {len(apps)}."
+ }
+ self.wfile.write(json.dumps(response).encode())
+ return
- # Valider
+ # Valider les paramètres
if not git_url or not git_token or not commit_message or not apps:
- logger.warning(f"Validation failed: git_url={bool(git_url)}, git_token={bool(git_token)}, commit_message={bool(commit_message)}, apps={len(apps)}")
response = {
- 'status': 'error',
- 'message': 'Missing required parameters'
+ "status": "error",
+ "message": "Missing required parameters"
}
self.wfile.write(json.dumps(response).encode())
return
@@ -101,14 +274,12 @@ class GitPusherRequestHandler(BaseHTTPRequestHandler):
# Préparer l'URL Git avec le token
git_url_with_token = self.prepare_git_url(git_url, git_token)
- logger.info(f"Git URL prepared (token inserted)")
- logger.debug(f"Git URL with token: {git_url_with_token}")
logger.info("Cloning repository...")
self.clone_repository(temp_dir, git_url_with_token, git_branch)
- # Récupérer TOUTES les applications (dossiers complets)
+ # Récupérer les applications
logger.info("Fetching applications from Splunk...")
- dashboard_contents = self.fetch_apps_directories(apps)
+ app_directories = self.fetch_apps_directories(apps)
# Créer le dossier apps
apps_dir = os.path.join(temp_dir, 'apps')
@@ -116,41 +287,40 @@ class GitPusherRequestHandler(BaseHTTPRequestHandler):
# Copier les applications
logger.info("Copying applications to repository...")
- for app_data in dashboard_contents:
+ for app_data in app_directories:
app_name = app_data['name']
app_path = app_data['path']
dest_path = os.path.join(apps_dir, app_name)
if os.path.exists(app_path):
- logger.info(f"Copying app {app_name} from {app_path}")
- # Supprimer le dossier s'il existe déjà
if os.path.exists(dest_path):
- logger.info(f"Removing existing app directory: {dest_path}")
shutil.rmtree(dest_path)
- # Copier le dossier
shutil.copytree(app_path, dest_path)
logger.info(f"Copied app: {app_name}")
else:
logger.warning(f"App path not found: {app_path}")
# Configurer git
- logger.info("Configuring git...")
subprocess.run(['git', 'config', 'user.email', 'splunk@splunk.local'],
cwd=temp_dir, capture_output=True)
subprocess.run(['git', 'config', 'user.name', 'Splunk Git Pusher'],
cwd=temp_dir, capture_output=True)
# Commit et push
- logger.info("Adding files...")
subprocess.run(['git', 'add', '-A'], cwd=temp_dir, capture_output=True)
- full_message = f"{commit_message}\n\nPushed by: {user}\nTimestamp: {datetime.now().isoformat()}"
- logger.info("Committing...")
+ # Ajouter les infos de licence au commit
+ license_info = validate_license()
+ full_message = f"{commit_message}\n\n"
+ full_message += f"Pushed by: {user}\n"
+ full_message += f"License: {license_info.get('license_id', 'N/A')} ({license_info.get('type_name', 'N/A')})\n"
+ full_message += f"Timestamp: {datetime.now().isoformat()}"
+
result = subprocess.run(['git', 'commit', '-m', full_message],
cwd=temp_dir, capture_output=True, text=True)
if result.returncode != 0:
- logger.warning(f"Commit may have failed or had no changes: {result.stderr}")
+ logger.warning(f"Commit warning: {result.stderr}")
logger.info("Pushing...")
result = subprocess.run(['git', 'push', 'origin', git_branch],
@@ -159,11 +329,15 @@ class GitPusherRequestHandler(BaseHTTPRequestHandler):
if result.returncode != 0:
raise Exception(f"Push failed: {result.stderr}")
+ # Incrémenter les stats d'utilisation
+ increment_usage()
+
logger.info("Push successful!")
response = {
- 'status': 'success',
- 'message': f'Successfully pushed {len(dashboard_contents)} dashboards from {len(apps)} application(s) to Git',
- 'dashboards_pushed': len(dashboard_contents)
+ "status": "success",
+ "message": f"Successfully pushed {len(app_directories)} application(s) to Git",
+ "apps_pushed": len(app_directories),
+ "license_type": license_info.get("type_name", "N/A")
}
self.wfile.write(json.dumps(response).encode())
@@ -172,36 +346,28 @@ class GitPusherRequestHandler(BaseHTTPRequestHandler):
shutil.rmtree(temp_dir, ignore_errors=True)
except Exception as e:
- logger.error(f"Error: {str(e)}", exc_info=True)
+ logger.error(f"Git push error: {str(e)}", exc_info=True)
response = {
- 'status': 'error',
- 'message': f'Error: {str(e)}'
+ "status": "error",
+ "message": f"Error: {str(e)}"
}
self.wfile.write(json.dumps(response).encode())
def log_message(self, format, *args):
- """Éviter les logs HTTP par défaut"""
logger.debug(format % args)
@staticmethod
def prepare_git_url(git_url, token):
- """Préparer l'URL Git avec le token inséré"""
- logger.info(f"Preparing git URL with token")
-
- # Si l'URL contient déjà un token (format: https://user:token@host/repo)
- # on le remplace
+ """Préparer l'URL Git avec le token"""
if '@' in git_url:
- # Extraire la partie sans le token
protocol = git_url.split('://')[0]
rest = git_url.split('://', 1)[1]
host_and_path = rest.split('@', 1)[1] if '@' in rest else rest
return f"{protocol}://{token}@{host_and_path}"
- # Si l'URL est juste https://host/repo (sans credentials)
if git_url.startswith('https://') or git_url.startswith('http://'):
protocol = git_url.split('://')[0]
host_and_path = git_url.split('://', 1)[1]
- # Insérer le token au format user:token@host ou juste token@host
return f"{protocol}://{token}@{host_and_path}"
return git_url
@@ -224,7 +390,7 @@ class GitPusherRequestHandler(BaseHTTPRequestHandler):
@staticmethod
def fetch_apps_directories(apps):
- """Récupérer les dossiers complets des applications"""
+ """Récupérer les dossiers des applications"""
logger.info(f"Fetching directories for {len(apps)} applications")
splunk_home = '/opt/splunk'
@@ -233,10 +399,14 @@ class GitPusherRequestHandler(BaseHTTPRequestHandler):
app_directories = []
for app in apps:
- app_id = app.get('id') or app.get('app_id')
- app_path = os.path.join(apps_base_path, app_id)
+ app_id = app.get('id') or app.get('app_id') or app.get('name')
- logger.info(f"Checking app directory: {app_path}")
+ # Vérifier que app_id n'est pas None
+ if not app_id:
+ logger.warning(f"Skipping app with no ID: {app}")
+ continue
+
+ app_path = os.path.join(apps_base_path, app_id)
if os.path.isdir(app_path):
app_directories.append({
@@ -246,95 +416,29 @@ class GitPusherRequestHandler(BaseHTTPRequestHandler):
for dirpath, dirnames, filenames in os.walk(app_path)
for filename in filenames)
})
- logger.info(f"Found app: {app_id} at {app_path}")
+ logger.info(f"Found app: {app_id}")
else:
logger.warning(f"App directory not found: {app_path}")
- logger.info(f"Successfully found {len(app_directories)} application directories")
return app_directories
- """Récupérer TOUS les dashboards de chaque application"""
- logger.info(f"Fetching dashboards from {len(apps)} applications")
-
- import urllib.request
- import urllib.error
- import ssl
- import base64
-
- # Ignorer les certificats SSL auto-signés
- ssl_context = ssl.create_default_context()
- ssl_context.check_hostname = False
- ssl_context.verify_mode = ssl.CERT_NONE
-
- dashboard_contents = []
-
- # Lire le fichier de configuration Splunk pour obtenir les credentials
- # Ou utiliser des credentials par défaut
- splunk_username = os.environ.get('SPLUNK_USERNAME', 'admin')
- splunk_password = os.environ.get('SPLUNK_PASSWORD', 'changeme')
-
- # Créer l'authentification Basic
- credentials = base64.b64encode(f"{splunk_username}:{splunk_password}".encode()).decode()
-
- for app in apps:
- app_id = app.get('id') or app.get('app_id')
- logger.info(f"Fetching all dashboards from app: {app_id}")
-
- try:
- # Récupérer la liste de TOUS les dashboards de cette app
- api_url = f"https://127.0.0.1:8089/servicesNS/-/{app_id}/data/ui/views?output_mode=json&count=0"
-
- logger.debug(f"API URL: {api_url}")
-
- req = urllib.request.Request(api_url)
- req.add_header('Authorization', f'Basic {credentials}')
-
- with urllib.request.urlopen(req, timeout=15, context=ssl_context) as response:
- api_data = json.loads(response.read().decode('utf-8'))
-
- if 'entry' in api_data and len(api_data['entry']) > 0:
- for entry in api_data['entry']:
- try:
- dashboard_id = entry.get('name')
- content = entry.get('content', {})
-
- # eai:data contient le XML complet du dashboard
- dashboard_xml = content.get('eai:data', '')
-
- if dashboard_xml:
- dashboard_contents.append({
- 'id': f"{app_id}_{dashboard_id}",
- 'app': app_id,
- 'content': dashboard_xml,
- 'name': dashboard_id
- })
- logger.debug(f"Fetched: {dashboard_id} from {app_id}")
- except Exception as e:
- logger.error(f"Error processing dashboard entry: {str(e)}")
-
- logger.info(f"Found {len([d for d in dashboard_contents if d['app'] == app_id])} dashboards in {app_id}")
- else:
- logger.warning(f"No dashboards found in app {app_id}")
-
- except urllib.error.HTTPError as e:
- logger.error(f"HTTP {e.code} when fetching app {app_id}: {e.reason}")
- except urllib.error.URLError as e:
- logger.error(f"Cannot reach Splunk API for app {app_id}: {e.reason}")
- except Exception as e:
- logger.error(f"Error fetching dashboards from {app_id}: {str(e)}")
-
- logger.info(f"Successfully fetched {len(dashboard_contents)} dashboards total")
- return dashboard_contents
def start_server(port=9999):
"""Démarrer le serveur HTTP"""
server = HTTPServer(('0.0.0.0', port), GitPusherRequestHandler)
- logger.info(f"Git Pusher server listening on 0.0.0.0:{port} (HTTP)")
+ logger.info(f"Git Pusher server listening on 0.0.0.0:{port}")
+
+ # Afficher le statut de la licence au démarrage
+ license_status = validate_license()
+ if license_status.get("valid"):
+ logger.info(f"License: {license_status.get('type_name')} - {license_status.get('days_remaining')} days remaining")
+ else:
+ logger.warning(f"License: {license_status.get('error', 'Invalid')}")
+
server.serve_forever()
if __name__ == '__main__':
- # Démarrer le serveur en background
port = 9999
logger.info(f"Starting Git Pusher on port {port}")
start_server(port)
\ No newline at end of file
diff --git a/apps/pusher_app_prem/bin/license_endpoints.py b/apps/pusher_app_prem/bin/license_endpoints.py
new file mode 100755
index 00000000..e71d4f8d
--- /dev/null
+++ b/apps/pusher_app_prem/bin/license_endpoints.py
@@ -0,0 +1,191 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Git Pusher - License Endpoints
+Endpoints REST pour gérer les licences
+"""
+
+import sys
+import os
+import json
+import tempfile
+from http.server import BaseHTTPRequestHandler
+
+# Ajouter le chemin du module
+sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
+
+from license_validator import LicenseValidator, get_license_status, LICENSE_FILE_PATH
+
+class LicenseHandler(BaseHTTPRequestHandler):
+ """Handler pour les requêtes de licence"""
+
+ def do_OPTIONS(self):
+ """Gérer les requêtes OPTIONS (CORS preflight)"""
+ self.send_response(200)
+ self.send_headers()
+ self.end_headers()
+
+ def do_GET(self):
+ """Gérer les requêtes GET"""
+ if self.path == '/custom/git_pusher/license_status':
+ self.handle_license_status()
+ else:
+ self.send_error(404)
+
+ def do_POST(self):
+ """Gérer les requêtes POST"""
+ if self.path == '/custom/git_pusher/install_license':
+ self.handle_install_license()
+ elif self.path == '/custom/git_pusher/request_trial':
+ self.handle_request_trial()
+ else:
+ self.send_error(404)
+
+ def send_headers(self):
+ """Envoyer les headers CORS"""
+ self.send_header('Content-type', 'application/json')
+ self.send_header('Access-Control-Allow-Origin', '*')
+ self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
+ self.send_header('Access-Control-Allow-Headers', 'Content-Type')
+
+ def handle_license_status(self):
+ """Vérifier le statut de la licence"""
+ try:
+ status = get_license_status()
+
+ self.send_response(200)
+ self.send_headers()
+ self.end_headers()
+
+ self.wfile.write(json.dumps(status).encode())
+
+ except Exception as e:
+ self.send_response(500)
+ self.send_headers()
+ self.end_headers()
+
+ self.wfile.write(json.dumps({
+ 'licensed': False,
+ 'errors': [f'Erreur serveur: {str(e)}']
+ }).encode())
+
+ def handle_install_license(self):
+ """Installer un fichier de licence uploadé"""
+ try:
+ # Lire le body
+ content_length = int(self.headers['Content-Length'])
+ body = self.rfile.read(content_length)
+ data = json.loads(body.decode())
+
+ license_content = data.get('license_file')
+ filename = data.get('filename', 'uploaded.lic')
+
+ if not license_content:
+ raise Exception("Aucun contenu de licence fourni")
+
+ # Créer un fichier temporaire
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.lic', delete=False) as temp_file:
+ temp_file.write(license_content)
+ temp_path = temp_file.name
+
+ try:
+ # Valider et installer
+ validator = LicenseValidator()
+ result = validator.install_license(temp_path, LICENSE_FILE_PATH)
+
+ self.send_response(200)
+ self.send_headers()
+ self.end_headers()
+
+ self.wfile.write(json.dumps(result).encode())
+
+ finally:
+ # Nettoyer le fichier temporaire
+ if os.path.exists(temp_path):
+ os.remove(temp_path)
+
+ except Exception as e:
+ self.send_response(400)
+ self.send_headers()
+ self.end_headers()
+
+ self.wfile.write(json.dumps({
+ 'success': False,
+ 'message': f'Erreur: {str(e)}'
+ }).encode())
+
+ def handle_request_trial(self):
+ """Créer une licence d'essai"""
+ try:
+ from datetime import datetime, timedelta
+ import socket
+ import hashlib
+
+ # Générer une licence d'essai basique (7 jours)
+ hostname = socket.gethostname()
+ trial_license = {
+ "license": {
+ "version": "1.0",
+ "license_id": "TRIAL-" + hashlib.md5(
+ f"{hostname}{datetime.now()}".encode()
+ ).hexdigest()[:8].upper(),
+ "customer": {
+ "name": "Trial User",
+ "hostname": hostname
+ },
+ "validity": {
+ "issued": datetime.now().isoformat(),
+ "expires": (datetime.now() + timedelta(days=7)).isoformat(),
+ "days": 7
+ },
+ "limits": {
+ "max_pushes": 50,
+ "max_apps": None
+ },
+ "features": {
+ "git_push": True,
+ "multi_branch": False,
+ "auto_commit": False,
+ "webhooks": False
+ },
+ "type": "trial"
+ },
+ "signature": "TRIAL-NO-SIGNATURE",
+ "checksum": "TRIAL"
+ }
+
+ # Créer le dossier si nécessaire
+ os.makedirs(os.path.dirname(LICENSE_FILE_PATH), exist_ok=True)
+
+ # Sauvegarder
+ with open(LICENSE_FILE_PATH, 'w') as f:
+ json.dump(trial_license, f, indent=2)
+
+ self.send_response(200)
+ self.send_headers()
+ self.end_headers()
+
+ self.wfile.write(json.dumps({
+ 'success': True,
+ 'message': 'Licence d\'essai créée (7 jours, 50 pushes max)',
+ 'license_info': {
+ 'license_id': trial_license['license']['license_id'],
+ 'type': 'trial',
+ 'expires': trial_license['license']['validity']['expires'],
+ 'days_remaining': 7
+ }
+ }).encode())
+
+ except Exception as e:
+ self.send_response(500)
+ self.send_headers()
+ self.end_headers()
+
+ self.wfile.write(json.dumps({
+ 'success': False,
+ 'message': f'Erreur: {str(e)}'
+ }).encode())
+
+ def log_message(self, format, *args):
+ """Override pour éviter les logs HTTP par défaut"""
+ pass
diff --git a/apps/pusher_app_prem/bin/license_generator.py b/apps/pusher_app_prem/bin/license_generator.py
deleted file mode 100644
index 8a353809..00000000
--- a/apps/pusher_app_prem/bin/license_generator.py
+++ /dev/null
@@ -1,173 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-Git Pusher - License Generator
-Génère des clés de licence basées sur le hostname Splunk
-"""
-
-import hashlib
-import hmac
-import base64
-import socket
-from datetime import datetime, timedelta
-import json
-
-# Secret key pour générer les licences (À CHANGER !)
-SECRET_KEY = "git_pusher_license_secret_2024"
-
-def get_hostname():
- """Récupérer le hostname du serveur"""
- return socket.gethostname()
-
-def generate_license(hostname, days_valid=365, max_pushes=None):
- """
- Générer une clé de licence
-
- Args:
- hostname: nom d'hôte Splunk
- days_valid: nombre de jours de validité
- max_pushes: nombre maximum de pushes (None = illimité)
-
- Returns:
- license_key: clé de licence formatée
- """
-
- # Créer la date d'expiration
- expiration_date = datetime.now() + timedelta(days=days_valid)
- expiration_str = expiration_date.strftime("%Y-%m-%d")
-
- # Créer le payload
- payload = {
- "hostname": hostname,
- "expiration": expiration_str,
- "max_pushes": max_pushes,
- "issued": datetime.now().strftime("%Y-%m-%d")
- }
-
- # Convertir en JSON et encoder en base64
- payload_json = json.dumps(payload, separators=(',', ':'))
- payload_b64 = base64.b64encode(payload_json.encode()).decode()
-
- # Créer la signature HMAC
- signature = hmac.new(
- SECRET_KEY.encode(),
- payload_b64.encode(),
- hashlib.sha256
- ).hexdigest()[:16] # Prendre les 16 premiers caractères
-
- # Formater la clé de licence
- license_key = f"{signature}-{payload_b64}"
-
- return license_key, payload
-
-def validate_license(license_key, hostname):
- """
- Valider une clé de licence
-
- Args:
- license_key: clé à valider
- hostname: hostname Splunk actuel
-
- Returns:
- dict: {valid: bool, error: str, expiration: str, max_pushes: int}
- """
-
- try:
- # Séparer signature et payload
- parts = license_key.split('-', 1)
- if len(parts) != 2:
- return {
- 'valid': False,
- 'error': 'Format de clé invalide'
- }
-
- signature, payload_b64 = parts
-
- # Vérifier la signature
- expected_signature = hmac.new(
- SECRET_KEY.encode(),
- payload_b64.encode(),
- hashlib.sha256
- ).hexdigest()[:16]
-
- if signature != expected_signature:
- return {
- 'valid': False,
- 'error': 'Signature invalide - clé corrompue ou falsifiée'
- }
-
- # Décoder le payload
- try:
- payload_json = base64.b64decode(payload_b64).decode()
- payload = json.loads(payload_json)
- except Exception as e:
- return {
- 'valid': False,
- 'error': f'Erreur de décodage: {str(e)}'
- }
-
- # Vérifier le hostname
- if payload.get('hostname') != hostname:
- return {
- 'valid': False,
- 'error': f'Cette licence est pour {payload.get("hostname")}, pas {hostname}'
- }
-
- # Vérifier l'expiration
- expiration = datetime.strptime(payload.get('expiration'), '%Y-%m-%d')
- if datetime.now() > expiration:
- return {
- 'valid': False,
- 'error': f'Licence expirée le {payload.get("expiration")}'
- }
-
- return {
- 'valid': True,
- 'expiration': payload.get('expiration'),
- 'max_pushes': payload.get('max_pushes'),
- 'days_remaining': (expiration - datetime.now()).days
- }
-
- except Exception as e:
- return {
- 'valid': False,
- 'error': f'Erreur de validation: {str(e)}'
- }
-
-if __name__ == '__main__':
- import sys
-
- hostname = get_hostname()
- print("=" * 60)
- print("Git Pusher - License Generator")
- print("=" * 60)
- print(f"\nHostname détecté: {hostname}")
-
- if len(sys.argv) > 1 and sys.argv[1] == 'validate':
- # Mode validation
- license_key = sys.argv[2] if len(sys.argv) > 2 else input("Entrez la clé de licence: ")
- result = validate_license(license_key, hostname)
-
- print("\nRésultat de validation:")
- print(json.dumps(result, indent=2, ensure_ascii=False))
- else:
- # Mode génération
- days = int(sys.argv[1]) if len(sys.argv) > 1 else 365
- max_pushes = int(sys.argv[2]) if len(sys.argv) > 2 else None
-
- license_key, payload = generate_license(hostname, days, max_pushes)
-
- print(f"\n📋 Payload:")
- print(json.dumps(payload, indent=2, ensure_ascii=False))
-
- print(f"\n🔑 Clé de licence générée:")
- print(license_key)
-
- print(f"\n✓ Valide pour: {days} jours")
- if max_pushes:
- print(f"✓ Pushes limités à: {max_pushes}")
-
- # Tester la validation
- print(f"\n✔️ Test de validation:")
- result = validate_license(license_key, hostname)
- print(json.dumps(result, indent=2, ensure_ascii=False))
\ No newline at end of file
diff --git a/apps/pusher_app_prem/bin/license_validator.py b/apps/pusher_app_prem/bin/license_validator.py
new file mode 100755
index 00000000..eca87391
--- /dev/null
+++ b/apps/pusher_app_prem/bin/license_validator.py
@@ -0,0 +1,485 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+Git Pusher - License Validator (Server-side)
+Ce fichier doit être déployé sur le serveur Splunk dans l'application
+
+Emplacement: /opt/splunk/etc/apps/pusher_app_prem/bin/license_validator.py
+"""
+
+import hashlib
+import hmac
+import base64
+import json
+import os
+import socket
+from datetime import datetime
+from http.server import HTTPServer, BaseHTTPRequestHandler
+import logging
+
+# Configuration du logging
+log_dir = '/opt/splunk/var/log/splunk'
+os.makedirs(log_dir, exist_ok=True)
+
+logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ handlers=[
+ logging.FileHandler(os.path.join(log_dir, 'license_validator.log')),
+ logging.StreamHandler()
+ ]
+)
+logger = logging.getLogger('license_validator')
+
+# ============================================
+# CONFIGURATION - DOIT CORRESPONDRE AU GÉNÉRATEUR !
+# ============================================
+
+# IMPORTANT: Cette clé DOIT être identique à celle du générateur
+SECRET_KEY = "git_pusher_super_secret_key_2024_change_me_in_production"
+
+# Chemin vers le fichier de licence
+LICENSE_FILE_PATH = "/opt/splunk/etc/apps/pusher_app_prem/local/license.lic"
+
+# Fichier de cache pour les stats d'utilisation
+USAGE_STATS_PATH = "/opt/splunk/etc/apps/pusher_app_prem/local/usage_stats.json"
+
+
+def get_splunk_hostname():
+ """Récupérer le hostname du serveur Splunk"""
+ # Essayer d'abord via l'API Splunk
+ try:
+ import urllib.request
+ import ssl
+
+ ssl_context = ssl.create_default_context()
+ ssl_context.check_hostname = False
+ ssl_context.verify_mode = ssl.CERT_NONE
+
+ splunk_username = os.environ.get('SPLUNK_USERNAME', 'admin')
+ splunk_password = os.environ.get('SPLUNK_PASSWORD', '2312Jocpam!?')
+ credentials = base64.b64encode(f"{splunk_username}:{splunk_password}".encode()).decode()
+
+ req = urllib.request.Request("https://127.0.0.1:8089/services/server/info?output_mode=json")
+ req.add_header('Authorization', f'Basic {credentials}')
+
+ with urllib.request.urlopen(req, timeout=5, context=ssl_context) as response:
+ data = json.loads(response.read().decode('utf-8'))
+ hostname = data.get('entry', [{}])[0].get('content', {}).get('host', '')
+ if hostname:
+ return hostname.lower().strip()
+ except Exception as e:
+ logger.warning(f"Could not get hostname via Splunk API: {e}")
+
+ # Fallback sur le hostname système
+ return socket.gethostname().lower().strip()
+
+
+def create_signature(data_str):
+ """Créer une signature HMAC-SHA256"""
+ signature = hmac.new(
+ SECRET_KEY.encode('utf-8'),
+ data_str.encode('utf-8'),
+ hashlib.sha256
+ ).hexdigest()
+ return signature
+
+
+def verify_signature(data_str, signature):
+ """Vérifier une signature"""
+ expected = create_signature(data_str)
+ return hmac.compare_digest(expected, signature)
+
+
+def read_license_file(filepath=None):
+ """Lire et parser un fichier de licence"""
+ if filepath is None:
+ filepath = LICENSE_FILE_PATH
+
+ try:
+ if not os.path.exists(filepath):
+ return None
+
+ with open(filepath, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ # Extraire la partie base64 (ignorer les commentaires)
+ lines = content.strip().split('\n')
+ base64_lines = [l for l in lines if not l.startswith('#') and l.strip()]
+ base64_content = ''.join(base64_lines)
+
+ # Décoder
+ decoded = base64.b64decode(base64_content).decode('utf-8')
+ license_file = json.loads(decoded)
+
+ return license_file
+
+ except Exception as e:
+ logger.error(f"Error reading license file: {e}")
+ return None
+
+
+def parse_license_content(content):
+ """Parser le contenu d'un fichier de licence (pour l'upload)"""
+ try:
+ # Extraire la partie base64 (ignorer les commentaires)
+ lines = content.strip().split('\n')
+ base64_lines = [l for l in lines if not l.startswith('#') and l.strip()]
+ base64_content = ''.join(base64_lines)
+
+ # Décoder
+ decoded = base64.b64decode(base64_content).decode('utf-8')
+ license_file = json.loads(decoded)
+
+ return license_file
+
+ except Exception as e:
+ logger.error(f"Error parsing license content: {e}")
+ return None
+
+
+def validate_license(license_file=None, current_hostname=None):
+ """
+ Valider une licence
+
+ Args:
+ license_file: Contenu du fichier de licence (dict) - si None, lit le fichier par défaut
+ current_hostname: Hostname actuel - si None, détecte automatiquement
+
+ Returns:
+ dict avec {valid: bool, error: str, license_info: dict}
+ """
+ try:
+ # Lire la licence si non fournie
+ if license_file is None:
+ license_file = read_license_file()
+ if license_file is None:
+ return {
+ "valid": False,
+ "error": "Aucun fichier de licence trouvé",
+ "error_code": "NO_LICENSE"
+ }
+
+ # Récupérer le hostname si non fourni
+ if current_hostname is None:
+ current_hostname = get_splunk_hostname()
+
+ license_data = license_file.get("license", {})
+ signature = license_file.get("signature", "")
+
+ # Recréer le JSON pour vérifier la signature
+ license_json = json.dumps(license_data, separators=(',', ':'), sort_keys=True)
+
+ # Vérifier la signature
+ if not verify_signature(license_json, signature):
+ return {
+ "valid": False,
+ "error": "Signature invalide - fichier corrompu ou modifié",
+ "error_code": "INVALID_SIGNATURE"
+ }
+
+ # Vérifier le hostname
+ expected_hostname = license_data.get("binding", {}).get("hostname", "").lower()
+ current_hostname_clean = current_hostname.lower().strip()
+
+ if current_hostname_clean != expected_hostname:
+ return {
+ "valid": False,
+ "error": f"Cette licence est pour '{expected_hostname}', pas '{current_hostname_clean}'",
+ "error_code": "HOSTNAME_MISMATCH",
+ "expected_hostname": expected_hostname,
+ "current_hostname": current_hostname_clean
+ }
+
+ # Vérifier l'expiration
+ expires_timestamp = license_data.get("dates", {}).get("expires_timestamp", 0)
+ if datetime.now().timestamp() > expires_timestamp:
+ expires_date = license_data.get("dates", {}).get("expires", "unknown")
+ return {
+ "valid": False,
+ "error": f"Licence expirée le {expires_date}",
+ "error_code": "EXPIRED"
+ }
+
+ # Calculer les jours restants
+ days_remaining = (expires_timestamp - datetime.now().timestamp()) / (24 * 3600)
+
+ return {
+ "valid": True,
+ "license_id": license_data.get("license_id"),
+ "type": license_data.get("type"),
+ "type_name": license_data.get("type_name"),
+ "customer": license_data.get("customer", {}),
+ "expires": license_data.get("dates", {}).get("expires"),
+ "days_remaining": int(days_remaining),
+ "limits": license_data.get("limits", {}),
+ "features": license_data.get("features", []),
+ "hostname": expected_hostname
+ }
+
+ except Exception as e:
+ logger.error(f"Validation error: {e}")
+ return {
+ "valid": False,
+ "error": f"Erreur de validation: {str(e)}",
+ "error_code": "VALIDATION_ERROR"
+ }
+
+
+def save_license_file(content):
+ """
+ Sauvegarder un nouveau fichier de licence
+
+ Args:
+ content: Contenu du fichier .lic
+
+ Returns:
+ dict avec {success: bool, error: str}
+ """
+ try:
+ # Créer le dossier local si nécessaire
+ local_dir = os.path.dirname(LICENSE_FILE_PATH)
+ os.makedirs(local_dir, exist_ok=True)
+
+ # Parser d'abord pour valider
+ license_file = parse_license_content(content)
+ if license_file is None:
+ return {
+ "success": False,
+ "error": "Format de fichier de licence invalide"
+ }
+
+ # Valider la licence
+ validation = validate_license(license_file)
+ if not validation.get("valid"):
+ return {
+ "success": False,
+ "error": validation.get("error", "Licence invalide")
+ }
+
+ # Sauvegarder
+ with open(LICENSE_FILE_PATH, 'w', encoding='utf-8') as f:
+ f.write(content)
+
+ logger.info(f"License saved: {validation.get('license_id')}")
+
+ return {
+ "success": True,
+ "license_info": validation
+ }
+
+ except Exception as e:
+ logger.error(f"Error saving license: {e}")
+ return {
+ "success": False,
+ "error": str(e)
+ }
+
+
+def get_usage_stats():
+ """Récupérer les statistiques d'utilisation"""
+ try:
+ if os.path.exists(USAGE_STATS_PATH):
+ with open(USAGE_STATS_PATH, 'r') as f:
+ return json.load(f)
+ except:
+ pass
+
+ return {
+ "pushes_today": 0,
+ "pushes_total": 0,
+ "last_push_date": None,
+ "apps_pushed": []
+ }
+
+
+def increment_usage():
+ """Incrémenter le compteur d'utilisation"""
+ stats = get_usage_stats()
+ today = datetime.now().strftime("%Y-%m-%d")
+
+ # Reset si nouveau jour
+ if stats.get("last_push_date") != today:
+ stats["pushes_today"] = 0
+ stats["last_push_date"] = today
+
+ stats["pushes_today"] += 1
+ stats["pushes_total"] += 1
+
+ try:
+ os.makedirs(os.path.dirname(USAGE_STATS_PATH), exist_ok=True)
+ with open(USAGE_STATS_PATH, 'w') as f:
+ json.dump(stats, f)
+ except Exception as e:
+ logger.error(f"Error saving usage stats: {e}")
+
+ return stats
+
+
+def check_limits():
+ """
+ Vérifier si les limites de la licence sont respectées
+
+ Returns:
+ dict avec {allowed: bool, error: str}
+ """
+ validation = validate_license()
+
+ if not validation.get("valid"):
+ return {
+ "allowed": False,
+ "error": validation.get("error")
+ }
+
+ limits = validation.get("limits", {})
+ stats = get_usage_stats()
+
+ # Vérifier pushes par jour
+ max_pushes = limits.get("max_pushes_per_day", -1)
+ if max_pushes > 0 and stats.get("pushes_today", 0) >= max_pushes:
+ return {
+ "allowed": False,
+ "error": f"Limite quotidienne atteinte ({max_pushes} pushes/jour)"
+ }
+
+ return {
+ "allowed": True,
+ "license_info": validation,
+ "usage": stats
+ }
+
+
+# ============================================
+# API REST POUR L'INTERFACE WEB
+# ============================================
+
+class LicenseAPIHandler(BaseHTTPRequestHandler):
+ """Handler pour les requêtes de l'API licence"""
+
+ def send_cors_headers(self):
+ """Envoyer les headers CORS complets"""
+ origin = self.headers.get('Origin', '*')
+ self.send_header('Access-Control-Allow-Origin', origin)
+ self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, PUT, DELETE')
+ self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, Origin')
+ self.send_header('Access-Control-Allow-Credentials', 'true')
+ self.send_header('Access-Control-Max-Age', '86400')
+
+ def do_OPTIONS(self):
+ logger.info(f"OPTIONS request from {self.headers.get('Origin', 'unknown')}")
+ self.send_response(200)
+ self.send_cors_headers()
+ self.end_headers()
+ return
+
+ def do_GET(self):
+ """GET /license - Récupérer les infos de licence"""
+ self.send_response(200)
+ self.send_header('Content-type', 'application/json')
+ self.send_cors_headers()
+ self.end_headers()
+
+ try:
+ if '/license/status' in self.path or self.path == '/license':
+ validation = validate_license()
+ usage = get_usage_stats()
+ hostname = get_splunk_hostname()
+
+ response = {
+ "status": "valid" if validation.get("valid") else "invalid",
+ "hostname": hostname,
+ "license": validation if validation.get("valid") else None,
+ "error": validation.get("error") if not validation.get("valid") else None,
+ "error_code": validation.get("error_code") if not validation.get("valid") else None,
+ "usage": usage
+ }
+
+ elif '/license/hostname' in self.path:
+ response = {
+ "hostname": get_splunk_hostname()
+ }
+
+ else:
+ response = {"error": "Unknown endpoint"}
+
+ self.wfile.write(json.dumps(response).encode())
+
+ except Exception as e:
+ logger.error(f"GET error: {e}")
+ self.wfile.write(json.dumps({"error": str(e)}).encode())
+
+ def do_POST(self):
+ """POST /license/upload - Uploader une nouvelle licence"""
+ self.send_response(200)
+ self.send_header('Content-type', 'application/json')
+ self.send_cors_headers()
+ self.end_headers()
+
+ try:
+ content_length = int(self.headers.get('Content-Length', 0))
+ body = self.rfile.read(content_length).decode('utf-8')
+
+ if '/license/upload' in self.path:
+ # Le body contient le contenu du fichier .lic
+ data = json.loads(body)
+ license_content = data.get('license_content', '')
+
+ if not license_content:
+ response = {"success": False, "error": "Contenu de licence vide"}
+ else:
+ response = save_license_file(license_content)
+
+ elif '/license/delete' in self.path:
+ # Supprimer la licence
+ if os.path.exists(LICENSE_FILE_PATH):
+ os.remove(LICENSE_FILE_PATH)
+ response = {"success": True, "message": "Licence supprimée"}
+ else:
+ response = {"success": False, "error": "Aucune licence à supprimer"}
+
+ else:
+ response = {"error": "Unknown endpoint"}
+
+ self.wfile.write(json.dumps(response).encode())
+
+ except Exception as e:
+ logger.error(f"POST error: {e}")
+ self.wfile.write(json.dumps({"error": str(e)}).encode())
+
+ def log_message(self, format, *args):
+ logger.debug(format % args)
+
+
+def start_license_server(port=9998):
+ """Démarrer le serveur API licence (optionnel, peut être intégré au serveur principal)"""
+ server = HTTPServer(('127.0.0.1', port), LicenseAPIHandler)
+ logger.info(f"License API server listening on 127.0.0.1:{port}")
+ server.serve_forever()
+
+
+# ============================================
+# CLI POUR TESTS
+# ============================================
+
+if __name__ == '__main__':
+ import sys
+
+ if len(sys.argv) > 1:
+ if sys.argv[1] == "status":
+ result = validate_license()
+ print(json.dumps(result, indent=2, ensure_ascii=False))
+
+ elif sys.argv[1] == "hostname":
+ print(f"Hostname: {get_splunk_hostname()}")
+
+ elif sys.argv[1] == "server":
+ start_license_server()
+
+ else:
+ print("Usage:")
+ print(" python license_validator.py status # Vérifier la licence")
+ print(" python license_validator.py hostname # Afficher le hostname")
+ print(" python license_validator.py server # Démarrer l'API")
+ else:
+ result = validate_license()
+ print(json.dumps(result, indent=2, ensure_ascii=False))
\ No newline at end of file
diff --git a/apps/pusher_app_prem/bin/start_git_pusher.sh b/apps/pusher_app_prem/bin/start_git_pusher.sh
old mode 100644
new mode 100755
index 951e1a0e..d205939a
--- a/apps/pusher_app_prem/bin/start_git_pusher.sh
+++ b/apps/pusher_app_prem/bin/start_git_pusher.sh
@@ -1,5 +1,249 @@
#!/bin/bash
-export SPLUNK_USERNAME=admin
-export SPLUNK_PASSWORD='2312Jocpam!?'
-python3 /opt/splunk/etc/apps/pusher_app/bin/git_pusher.py > /opt/splunk/var/log/splunk/git_pusher_startup.log 2>&1 &
-echo $! > /opt/splunk/etc/apps/pusher_app/bin/git_pusher.pid
\ No newline at end of file
+# ============================================
+# Git Pusher - Start Script
+# Version 2.0 avec système de licence
+# ============================================
+
+# Configuration
+SPLUNK_HOME=${SPLUNK_HOME:-/opt/splunk}
+APP_HOME="${SPLUNK_HOME}/etc/apps/pusher_app_prem"
+BIN_DIR="${APP_HOME}/bin"
+LOG_DIR="${SPLUNK_HOME}/var/log/splunk"
+PID_FILE="${BIN_DIR}/git_pusher.pid"
+
+# Couleurs pour les logs
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# Fonction de logging
+log_info() {
+ echo -e "${GREEN}[INFO]${NC} $1"
+}
+
+log_warn() {
+ echo -e "${YELLOW}[WARN]${NC} $1"
+}
+
+log_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+# Vérifier si le serveur est déjà en cours d'exécution
+check_running() {
+ if [ -f "$PID_FILE" ]; then
+ PID=$(cat "$PID_FILE")
+ if ps -p $PID > /dev/null 2>&1; then
+ return 0 # Running
+ fi
+ fi
+ return 1 # Not running
+}
+
+# Démarrer le serveur
+start_server() {
+ log_info "Starting Git Pusher server..."
+
+ # Vérifier si déjà en cours
+ if check_running; then
+ log_warn "Git Pusher is already running (PID: $(cat $PID_FILE))"
+ return 1
+ fi
+
+ # Créer le répertoire de logs
+ mkdir -p "$LOG_DIR"
+
+ # Variables d'environnement pour l'authentification Splunk
+ # IMPORTANT: Modifiez ces valeurs ou utilisez des variables d'environnement
+ export SPLUNK_USERNAME=${SPLUNK_USERNAME:-admin}
+ export SPLUNK_PASSWORD=${SPLUNK_PASSWORD:-changeme}
+
+ # Démarrer le serveur Python
+ cd "$BIN_DIR"
+ python3 git_pusher.py > "${LOG_DIR}/git_pusher_startup.log" 2>&1 &
+
+ # Sauvegarder le PID
+ echo $! > "$PID_FILE"
+
+ # Attendre un peu et vérifier
+ sleep 2
+
+ if check_running; then
+ log_info "Git Pusher started successfully (PID: $(cat $PID_FILE))"
+ log_info "Server listening on port 9999"
+
+ # Vérifier le statut de la licence
+ HOSTNAME=$(hostname)
+ log_info "Hostname: $HOSTNAME"
+
+ if [ -f "${APP_HOME}/local/license.lic" ]; then
+ log_info "License file found"
+ else
+ log_warn "No license file found at ${APP_HOME}/local/license.lic"
+ log_warn "The application will require license activation"
+ fi
+
+ return 0
+ else
+ log_error "Failed to start Git Pusher"
+ log_error "Check logs at ${LOG_DIR}/git_pusher.log"
+ return 1
+ fi
+}
+
+# Arrêter le serveur
+stop_server() {
+ log_info "Stopping Git Pusher server..."
+
+ if [ -f "$PID_FILE" ]; then
+ PID=$(cat "$PID_FILE")
+
+ if ps -p $PID > /dev/null 2>&1; then
+ kill $PID
+ sleep 2
+
+ # Force kill si nécessaire
+ if ps -p $PID > /dev/null 2>&1; then
+ log_warn "Force killing process..."
+ kill -9 $PID
+ fi
+
+ rm -f "$PID_FILE"
+ log_info "Git Pusher stopped"
+ return 0
+ else
+ log_warn "Process not running, cleaning up PID file"
+ rm -f "$PID_FILE"
+ return 0
+ fi
+ else
+ log_warn "PID file not found, Git Pusher may not be running"
+ return 1
+ fi
+}
+
+# Redémarrer le serveur
+restart_server() {
+ log_info "Restarting Git Pusher server..."
+ stop_server
+ sleep 1
+ start_server
+}
+
+# Afficher le statut
+show_status() {
+ echo "============================================"
+ echo "Git Pusher Status"
+ echo "============================================"
+
+ if check_running; then
+ PID=$(cat "$PID_FILE")
+ echo -e "Status: ${GREEN}RUNNING${NC}"
+ echo "PID: $PID"
+ echo "Port: 9999"
+ else
+ echo -e "Status: ${RED}STOPPED${NC}"
+ fi
+
+ echo ""
+ echo "Paths:"
+ echo " App Home: $APP_HOME"
+ echo " Bin Dir: $BIN_DIR"
+ echo " Log Dir: $LOG_DIR"
+ echo ""
+
+ # Statut de la licence
+ echo "License:"
+ if [ -f "${APP_HOME}/local/license.lic" ]; then
+ echo -e " File: ${GREEN}Present${NC}"
+ # Essayer de lire quelques infos
+ if command -v python3 &> /dev/null; then
+ python3 -c "
+import sys
+sys.path.insert(0, '$BIN_DIR')
+from license_validator import validate_license
+result = validate_license()
+if result.get('valid'):
+ print(f\" Type: {result.get('type_name', 'N/A')}\")
+ print(f\" Expires: {result.get('expires', 'N/A')}\")
+ print(f\" Days remaining: {result.get('days_remaining', 'N/A')}\")
+else:
+ print(f\" Status: Invalid - {result.get('error', 'Unknown error')}\")
+" 2>/dev/null || echo " Unable to read license details"
+ fi
+ else
+ echo -e " File: ${YELLOW}Not found${NC}"
+ fi
+
+ echo ""
+ echo "Hostname: $(hostname)"
+ echo "============================================"
+}
+
+# Afficher les logs
+show_logs() {
+ LOG_FILE="${LOG_DIR}/git_pusher.log"
+
+ if [ -f "$LOG_FILE" ]; then
+ if [ "$1" == "-f" ]; then
+ tail -f "$LOG_FILE"
+ else
+ tail -n 50 "$LOG_FILE"
+ fi
+ else
+ log_warn "Log file not found at $LOG_FILE"
+ fi
+}
+
+# Menu d'aide
+show_help() {
+ echo "Git Pusher - Server Management Script"
+ echo ""
+ echo "Usage: $0 {start|stop|restart|status|logs|help}"
+ echo ""
+ echo "Commands:"
+ echo " start Start the Git Pusher server"
+ echo " stop Stop the Git Pusher server"
+ echo " restart Restart the Git Pusher server"
+ echo " status Show the current status"
+ echo " logs Show recent logs (use -f for follow)"
+ echo " help Show this help message"
+ echo ""
+ echo "Environment variables:"
+ echo " SPLUNK_USERNAME Splunk admin username (default: admin)"
+ echo " SPLUNK_PASSWORD Splunk admin password (default: changeme)"
+ echo " SPLUNK_HOME Splunk installation directory (default: /opt/splunk)"
+}
+
+# Main
+case "$1" in
+ start)
+ start_server
+ ;;
+ stop)
+ stop_server
+ ;;
+ restart)
+ restart_server
+ ;;
+ status)
+ show_status
+ ;;
+ logs)
+ show_logs "$2"
+ ;;
+ help|--help|-h)
+ show_help
+ ;;
+ *)
+ # Par défaut, démarrer le serveur (pour compatibilité)
+ if [ -z "$1" ]; then
+ start_server
+ else
+ echo "Unknown command: $1"
+ show_help
+ exit 1
+ fi
+ ;;
+esac
diff --git a/apps/pusher_app_prem/certs/server.crt b/apps/pusher_app_prem/certs/server.crt
old mode 100644
new mode 100755
diff --git a/apps/pusher_app_prem/certs/server.key b/apps/pusher_app_prem/certs/server.key
old mode 100644
new mode 100755
diff --git a/apps/pusher_app_prem/default/app.conf b/apps/pusher_app_prem/default/app.conf
old mode 100644
new mode 100755
diff --git a/apps/pusher_app_prem/default/data/ui/nav/default.xml b/apps/pusher_app_prem/default/data/ui/nav/default.xml
old mode 100644
new mode 100755
diff --git a/apps/pusher_app_prem/default/data/ui/views/README b/apps/pusher_app_prem/default/data/ui/views/README
old mode 100644
new mode 100755
diff --git a/apps/pusher_app_prem/local/app.conf b/apps/pusher_app_prem/local/app.conf
old mode 100644
new mode 100755
diff --git a/apps/pusher_app_prem/local/data/ui/views/git_pusher_-_deploy_applications.xml b/apps/pusher_app_prem/local/data/ui/views/git_pusher_-_deploy_applications.xml
index 292fbe8f..237cacba 100644
--- a/apps/pusher_app_prem/local/data/ui/views/git_pusher_-_deploy_applications.xml
+++ b/apps/pusher_app_prem/local/data/ui/views/git_pusher_-_deploy_applications.xml
@@ -13,7 +13,7 @@
- | rest /services/apps/local | search disabled=0 | fields name, label, description | sort label
+ | rest /services/apps/local | search disabled=0 | table title, label, description | rename title as name | sort label
-4h@h
now
diff --git a/apps/pusher_app_prem/local/data/ui/views/git_pusher_-_push_applications_to_git.xml b/apps/pusher_app_prem/local/data/ui/views/git_pusher_-_push_applications_to_git.xml
old mode 100644
new mode 100755
diff --git a/apps/pusher_app_prem/local/data/ui/views/git_pusher_-_push_dashboards_to_git.xml b/apps/pusher_app_prem/local/data/ui/views/git_pusher_-_push_dashboards_to_git.xml
old mode 100644
new mode 100755
diff --git a/apps/pusher_app_prem/local/usage_stats.json b/apps/pusher_app_prem/local/usage_stats.json
new file mode 100644
index 00000000..73139971
--- /dev/null
+++ b/apps/pusher_app_prem/local/usage_stats.json
@@ -0,0 +1 @@
+{"pushes_today": 9, "pushes_total": 9, "last_push_date": "2026-01-31", "apps_pushed": []}
\ No newline at end of file
diff --git a/apps/pusher_app_prem/metadata/default.meta b/apps/pusher_app_prem/metadata/default.meta
old mode 100644
new mode 100755
diff --git a/apps/pusher_app_prem/metadata/local.meta b/apps/pusher_app_prem/metadata/local.meta
index d8e71ee3..cf1356db 100644
--- a/apps/pusher_app_prem/metadata/local.meta
+++ b/apps/pusher_app_prem/metadata/local.meta
@@ -23,4 +23,4 @@ access = read : [ * ], write : [ * ]
export = none
owner = admin
version = 10.0.2
-modtime = 1769373002.573917000
+modtime = 1769889828.514647000