diff --git a/src/backend/package.json b/src/backend/package.json
index d210140..3dce267 100644
--- a/src/backend/package.json
+++ b/src/backend/package.json
@@ -21,7 +21,8 @@
"cookie-parser": "^1.4.6",
"morgan": "^1.10.0",
"multer": "^1.4.5-lts.1",
- "adm-zip": "^0.5.10"
+ "adm-zip": "^0.5.10",
+ "ssh2": "^1.16.0"
},
"devDependencies": {
"nodemon": "^3.1.0"
diff --git a/src/backend/src/config.js b/src/backend/src/config.js
index 664c1a7..34fe2e5 100644
--- a/src/backend/src/config.js
+++ b/src/backend/src/config.js
@@ -12,6 +12,7 @@ module.exports = {
url: process.env.GITEA_URL || 'http://gitea:3000',
username: process.env.GITEA_USERNAME || 'manus-admin',
password: process.env.GITEA_PASSWORD || 'Itinova69!',
+ token: process.env.GITEA_TOKEN || null,
},
// Applications config
appsBasePath: process.env.APPS_BASE_PATH || '/opt/manus-deploy/apps',
@@ -65,7 +66,7 @@ module.exports = {
recette: 'https://veille.recette.santinova-soft.org',
prod: 'https://veille.santinova-soft.org',
},
- containerName: 'veille-reglementaire',
+ containerName: 'veille-reglementaire-recette',
healthCheckUrl: 'https://veille.recette.santinova-soft.org',
port: 3000,
category: 'ITINOVA',
@@ -88,6 +89,23 @@ module.exports = {
category: 'ITINOVA',
status: 'production',
},
+ {
+ id: 'pilotage-masse-salariale',
+ name: 'Pilotage Masse Salariale',
+ description: 'Application de pilotage de la masse salariale Itinova',
+ directory: 'pilotage-masse-salariale',
+ giteaRepo: 'pilotage-masse-salariale',
+ giteaOwner: 'manus-admin',
+ urls: {
+ recette: 'https://pilotage-ms.recette.santinova-soft.org',
+ prod: 'https://pilotage-ms.santinova-soft.org',
+ },
+ containerName: 'pilotage-masse-salariale-app',
+ healthCheckUrl: 'https://pilotage-ms.recette.santinova-soft.org',
+ port: 3000,
+ category: 'ITINOVA',
+ status: 'recette',
+ },
{
id: 'sonum',
name: 'SONUM',
@@ -190,5 +208,22 @@ module.exports = {
category: 'INFRA',
status: 'production',
},
+ {
+ id: 'itinova-budget-si',
+ name: 'Gestion Budget Informatique',
+ description: 'Application de gestion du budget informatique DSI Itinova',
+ directory: 'itinova-budget-si',
+ giteaRepo: 'itinova-budget-si',
+ giteaOwner: 'manus-admin',
+ urls: {
+ recette: 'https://budget-si.recette.santinova-soft.org',
+ prod: 'https://budget-si.santinova-soft.org',
+ },
+ containerName: 'itinova-budget-si-app',
+ healthCheckUrl: 'https://budget-si.recette.santinova-soft.org',
+ port: 3000,
+ category: 'SANTINOVA',
+ status: 'recette',
+ },
],
};
diff --git a/src/backend/src/gitea.js b/src/backend/src/gitea.js
index c899c89..84f1d1b 100644
--- a/src/backend/src/gitea.js
+++ b/src/backend/src/gitea.js
@@ -3,12 +3,17 @@ const https = require('https');
const config = require('./config');
// Créer un client axios pour Gitea (ignorer les certificats auto-signés si nécessaire)
+// Utiliser le token API si disponible, sinon auth basique
+const giteaClientHeaders = config.gitea.token
+ ? { 'Authorization': `token ${config.gitea.token}` }
+ : {};
+
const giteaClient = axios.create({
baseURL: `${config.gitea.url}/api/v1`,
- auth: {
- username: config.gitea.username,
- password: config.gitea.password,
- },
+ ...(config.gitea.token
+ ? { headers: giteaClientHeaders }
+ : { auth: { username: config.gitea.username, password: config.gitea.password } }
+ ),
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
timeout: 10000,
});
diff --git a/src/backend/src/index.js b/src/backend/src/index.js
index beae15a..07b03f6 100644
--- a/src/backend/src/index.js
+++ b/src/backend/src/index.js
@@ -15,12 +15,14 @@ const webhookRoutes = require('./webhook');
const { checkAllApps, getAllStatuses } = require('./healthcheck');
const { getCommits } = require('./gitea');
const { initDynamicApps } = require('./app-creator');
+const { initSSHWebSocket } = require('./ssh');
const app = express();
const server = http.createServer(app);
// WebSocket server pour les mises à jour en temps réel
const wss = new WebSocket.Server({ server, path: '/ws' });
+initSSHWebSocket(server);
// Middleware
app.use(helmet({
diff --git a/src/backend/src/routes.js b/src/backend/src/routes.js
index 106638e..45fbc9a 100644
--- a/src/backend/src/routes.js
+++ b/src/backend/src/routes.js
@@ -503,6 +503,7 @@ const GITEA_PROD_EXTERNAL = 'https://git.santinova-soft.org';
const GITEA_USER_INV = process.env.GITEA_USERNAME || 'manus-admin';
const GITEA_PASS_REC = process.env.GITEA_PASSWORD || 'Itinova69!';
const GITEA_PASS_PRD = process.env.GITEA_PASSWORD_PROD || 'ManusGitea2026!';
+const GITEA_TOKEN_REC = process.env.GITEA_TOKEN || null;
const INVENTORY_APPS = [
{ id: 'itinova-contacts', name: 'Itinova Contacts', repoName: 'itinova-contacts' },
@@ -513,21 +514,26 @@ const INVENTORY_APPS = [
{ id: 'demat-facturation-dsi', name: 'Démat. Facturation DSI', repoName: 'demat-facturation-dsi' },
{ id: 'facturation-santinova', name: 'Facturation Santinova', repoName: 'facturation-santinova' },
{ id: 'formation-manager-itinova', name: 'Formation Manager', repoName: 'formation-manager-itinova' },
+ { id: 'pilotage-masse-salariale', name: 'Pilotage Masse Salariale', repoName: 'pilotage-masse-salariale' },
+ { id: 'itinova-budget-si', name: 'Gestion Budget Informatique', repoName: 'itinova-budget-si' },
+ { id: 'falc-generator', name: 'FALC Generator', repoName: 'falc-generator' },
{ id: 'portail-santinova', name: 'Portail Applicatif', repoName: 'portail-santinova' },
{ id: 'manus-dashboard', name: 'Dashboard Manus', repoName: 'manus-dashboard' },
];
-function curlGitea(baseUrl, owner, repo, pass, publicBaseUrl) {
+function curlGitea(baseUrl, owner, repo, pass, publicBaseUrl, token) {
return new Promise((resolve) => {
- const auth = `${GITEA_USER_INV}:${pass}`;
- const cmd = `curl -sk --max-time 6 -u "${auth}" "${baseUrl}/api/v1/repos/${owner}/${repo}"`;
+ const authHeader = token
+ ? `-H "Authorization: token ${token}"`
+ : `-u "${GITEA_USER_INV}:${pass}"`;
+ const cmd = `curl -sk --max-time 6 ${authHeader} "${baseUrl}/api/v1/repos/${owner}/${repo}"`;
exec(cmd, { timeout: 7000 }, (err, stdout) => {
if (err || !stdout) return resolve({ present: false, url: null, version: null });
try {
const data = JSON.parse(stdout);
if (!data.id) return resolve({ present: false, url: null, version: null });
// Récupérer le dernier commit
- const cmd2 = `curl -sk --max-time 6 -u "${auth}" "${baseUrl}/api/v1/repos/${owner}/${repo}/commits?limit=1"`;
+ const cmd2 = `curl -sk --max-time 6 ${authHeader} "${baseUrl}/api/v1/repos/${owner}/${repo}/commits?limit=1"`;
exec(cmd2, { timeout: 7000 }, (err2, stdout2) => {
let version = null;
try {
@@ -556,7 +562,7 @@ router.get('/inventory', authMiddleware, async (req, res) => {
const results = await Promise.all(
INVENTORY_APPS.map(async (app) => {
const [repoRecette, repoProd] = await Promise.all([
- curlGitea(GITEA_RECETTE_INTERNAL, owner, app.repoName, GITEA_PASS_REC, GITEA_RECETTE_PUBLIC),
+ curlGitea(GITEA_RECETTE_INTERNAL, owner, app.repoName, GITEA_PASS_REC, GITEA_RECETTE_PUBLIC, GITEA_TOKEN_REC),
curlGitea(GITEA_PROD_EXTERNAL, owner, app.repoName, GITEA_PASS_PRD),
]);
return { ...app, repoRecette, repoProd };
diff --git a/src/backend/src/ssh.js b/src/backend/src/ssh.js
new file mode 100644
index 0000000..34a3faf
--- /dev/null
+++ b/src/backend/src/ssh.js
@@ -0,0 +1,179 @@
+/**
+ * Module SSH Terminal - Gestion des connexions SSH via WebSocket
+ * Utilise la bibliothèque ssh2 pour établir des connexions SSH
+ */
+const { Client } = require('ssh2');
+const WebSocket = require('ws');
+const jwt = require('jsonwebtoken');
+const config = require('./config');
+
+/**
+ * Initialise le serveur WebSocket SSH sur /ws-ssh
+ * @param {http.Server} server - Le serveur HTTP Express
+ */
+function initSSHWebSocket(server) {
+ const wssSsh = new WebSocket.Server({ server, path: '/ws-ssh' });
+
+ wssSsh.on('connection', (ws, req) => {
+ // Vérifier l'authentification via le token dans l'URL
+ const url = new URL(req.url, 'http://localhost');
+ const token = url.searchParams.get('token');
+
+ if (!token) {
+ ws.send(JSON.stringify({ type: 'error', message: 'Token manquant' }));
+ ws.close();
+ return;
+ }
+
+ try {
+ jwt.verify(token, config.jwtSecret);
+ } catch (err) {
+ ws.send(JSON.stringify({ type: 'error', message: 'Token invalide' }));
+ ws.close();
+ return;
+ }
+
+ console.log('Nouvelle connexion WebSocket SSH');
+
+ let sshClient = null;
+ let sshStream = null;
+ let connected = false;
+
+ ws.on('message', (data) => {
+ try {
+ const msg = JSON.parse(data.toString());
+
+ switch (msg.type) {
+ case 'connect':
+ // Établir la connexion SSH
+ handleConnect(ws, msg, (client, stream) => {
+ sshClient = client;
+ sshStream = stream;
+ connected = true;
+ });
+ break;
+
+ case 'input':
+ // Envoyer des données au terminal SSH
+ if (sshStream && connected) {
+ sshStream.write(msg.data);
+ }
+ break;
+
+ case 'resize':
+ // Redimensionner le terminal
+ if (sshStream && connected) {
+ sshStream.setWindow(msg.rows, msg.cols, 0, 0);
+ }
+ break;
+
+ case 'disconnect':
+ // Fermer la connexion SSH
+ if (sshClient) {
+ sshClient.end();
+ }
+ break;
+
+ default:
+ console.warn('Type de message SSH inconnu:', msg.type);
+ }
+ } catch (err) {
+ console.error('Erreur parsing message SSH:', err.message);
+ }
+ });
+
+ ws.on('close', () => {
+ console.log('Connexion WebSocket SSH fermée');
+ if (sshClient) {
+ sshClient.end();
+ }
+ });
+
+ ws.on('error', (err) => {
+ console.error('Erreur WebSocket SSH:', err.message);
+ if (sshClient) {
+ sshClient.end();
+ }
+ });
+ });
+
+ console.log('Serveur WebSocket SSH initialisé sur /ws-ssh');
+ return wssSsh;
+}
+
+/**
+ * Gère la connexion SSH
+ */
+function handleConnect(ws, msg, onConnected) {
+ const { host, port, username, password, privateKey } = msg;
+
+ if (!host || !username) {
+ ws.send(JSON.stringify({ type: 'error', message: 'Hôte et utilisateur requis' }));
+ return;
+ }
+
+ const sshClient = new Client();
+
+ const connConfig = {
+ host: host,
+ port: port || 22,
+ username: username,
+ readyTimeout: 15000,
+ keepaliveInterval: 10000,
+ };
+
+ if (privateKey) {
+ connConfig.privateKey = privateKey;
+ } else if (password) {
+ connConfig.password = password;
+ } else {
+ ws.send(JSON.stringify({ type: 'error', message: 'Mot de passe ou clé privée requis' }));
+ return;
+ }
+
+ ws.send(JSON.stringify({ type: 'status', message: `Connexion à ${username}@${host}:${port || 22}...` }));
+
+ sshClient.on('ready', () => {
+ ws.send(JSON.stringify({ type: 'connected', message: `Connecté à ${host}` }));
+
+ sshClient.shell({ term: 'xterm-256color', rows: 24, cols: 80 }, (err, stream) => {
+ if (err) {
+ ws.send(JSON.stringify({ type: 'error', message: `Erreur shell: ${err.message}` }));
+ sshClient.end();
+ return;
+ }
+
+ onConnected(sshClient, stream);
+
+ // Transmettre les données du terminal SSH vers le WebSocket
+ stream.on('data', (data) => {
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.send(JSON.stringify({ type: 'output', data: data.toString('base64') }));
+ }
+ });
+
+ stream.stderr.on('data', (data) => {
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.send(JSON.stringify({ type: 'output', data: data.toString('base64') }));
+ }
+ });
+
+ stream.on('close', () => {
+ ws.send(JSON.stringify({ type: 'disconnected', message: 'Session SSH terminée' }));
+ sshClient.end();
+ });
+ });
+ });
+
+ sshClient.on('error', (err) => {
+ ws.send(JSON.stringify({ type: 'error', message: `Erreur SSH: ${err.message}` }));
+ });
+
+ sshClient.on('end', () => {
+ ws.send(JSON.stringify({ type: 'disconnected', message: 'Connexion SSH fermée' }));
+ });
+
+ sshClient.connect(connConfig);
+}
+
+module.exports = { initSSHWebSocket };
diff --git a/src/backend/src/webhook.js b/src/backend/src/webhook.js
index 6baf388..69ff5fd 100644
--- a/src/backend/src/webhook.js
+++ b/src/backend/src/webhook.js
@@ -23,6 +23,7 @@ const REPO_TO_APP_MAP = {
'veille-reglementaire': 'veille-reglementaire',
'itinova-vehicle-exchange': 'itinova-vehicle-exchange',
'manus-dashboard': 'manus-dashboard',
+ 'itinova-budget-si': 'itinova-budget-si',
};
// Déploiements en cours (évite les doubles déclenchements)
diff --git a/src/docker-compose.yml b/src/docker-compose.yml
index 68c589a..9b5bd8a 100644
--- a/src/docker-compose.yml
+++ b/src/docker-compose.yml
@@ -13,8 +13,9 @@ services:
- ADMIN_PASSWORD=Itinova69!
- GITEA_URL=https://git.recette.santinova-soft.org
- GITEA_USERNAME=manus-admin
- - GITEA_PASSWORD=Itinova69!
- - GITEA_RECETTE_URL=http://172.18.0.5:3000
+ - GITEA_PASSWORD=ManusGitea2026!
+ - GITEA_TOKEN=1e0b01c361a91d5512e13c78053ac82a66e19427
+ - GITEA_RECETTE_URL=http://gitea:3000
- GITEA_PASSWORD_PROD=ManusGitea2026!
- APPS_BASE_PATH=/opt/manus-deploy/apps
- INFRA_BASE_PATH=/opt/manus-deploy/infrastructure
diff --git a/src/frontend/src/App.jsx b/src/frontend/src/App.jsx
index 6642b20..dcbd001 100644
--- a/src/frontend/src/App.jsx
+++ b/src/frontend/src/App.jsx
@@ -8,6 +8,7 @@ import GiteaPage from './pages/GiteaPage';
import DockerPage from './pages/DockerPage';
import DocumentationPage from './pages/DocumentationPage';
import InventairePage from './pages/InventairePage';
+import TerminalPage from './pages/TerminalPage';
import Sidebar from './components/Sidebar';
import useWebSocket from './hooks/useWebSocket';
import { getMe, getApps, logout as apiLogout } from './utils/api';
@@ -111,6 +112,8 @@ export default function App() {
return
Explorez les dépôts sur{' '} - git.santinova-soft.org + git.recette.santinova-soft.org
diff --git a/src/frontend/src/pages/InventairePage.jsx b/src/frontend/src/pages/InventairePage.jsx index 79d9d73..10be926 100644 --- a/src/frontend/src/pages/InventairePage.jsx +++ b/src/frontend/src/pages/InventairePage.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Table, RefreshCw, GitBranch, CheckCircle, XCircle, ExternalLink, Tag, AlertCircle } from 'lucide-react'; +import { Table, RefreshCw, GitBranch, CheckCircle, XCircle, ExternalLink, Tag, AlertCircle, ArrowUp, ArrowDown, Minus, AlertTriangle } from 'lucide-react'; import api from '../utils/api'; const APPS_CONFIG = [ @@ -59,6 +59,27 @@ const APPS_CONFIG = [ urlRecette: 'https://formation.recette.santinova-soft.org', urlProd: 'https://formations.itinova.org', }, + { + id: 'pilotage-masse-salariale', + name: 'Pilotage Masse Salariale', + repoName: 'pilotage-masse-salariale', + urlRecette: 'https://pilotage-ms.recette.santinova-soft.org', + urlProd: null, + }, + { + id: 'itinova-budget-si', + name: 'Gestion Budget Informatique', + repoName: 'itinova-budget-si', + urlRecette: 'https://budget-si.recette.santinova-soft.org', + urlProd: null, + }, + { + id: 'falc-generator', + name: 'FALC Generator', + repoName: 'falc-generator', + urlRecette: 'https://falc.recette.santinova-soft.org', + urlProd: 'https://falc.santinova-soft.org', + }, { id: 'portail-santinova', name: 'Portail Applicatif', @@ -75,6 +96,84 @@ const APPS_CONFIG = [ }, ]; + +function SyncBadge({ versionRecette, versionProd }) { + // Extraire le SHA (7 premiers caractères avant l'espace) + const shaRec = versionRecette ? versionRecette.split(' ')[0] : null; + const shaProd = versionProd ? versionProd.split(' ')[0] : null; + // Extraire la date entre parenthèses pour comparer + const dateRec = versionRecette ? versionRecette.match(/\((.+?)\)/)?.[1] : null; + const dateProd = versionProd ? versionProd.match(/\((.+?)\)/)?.[1] : null; + + if (!shaRec && !shaProd) { + return ( + ++ Connexion SSH sécurisée aux serveurs de l'infrastructure +
+