Add configuration interface for enterprise task webhook

Implement a new UI component and backend routes for configuring and testing enterprise task webhooks, including database schema and migration scripts.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: cfbfe47c-0c9a-4ebf-8a41-2937407eccff
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/cfbfe47c-0c9a-4ebf-8a41-2937407eccff/vzUkIk6
This commit is contained in:
michaelschal committed 2025-09-03 13:15:50 +00:00
1 parent 6ce9ec90a4
commit f938fdb95d
9 files changed
+582 -7

No files matched your search

+141 -2
View File
@@ -68,6 +68,139 @@ export async function registerRoutes(app: Express): Promise<Server> {
// Auth middleware
await setupAuth(app);
// Routes pour configuration webhook BAP
app.get('/api/webhook-bap-config', isAuthenticated, async (req: any, res) => {
try {
// Vérifier que l'utilisateur est admin
const userId = req.user?.claims?.sub || req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Utilisateur non authentifié' });
}
const user = await storage.getUser(userId);
if (!user || user.role !== 'admin') {
return res.status(403).json({ error: 'Accès refusé - Admin uniquement' });
}
const config = await storage.getWebhookBapConfig();
res.json(config || null);
} catch (error: any) {
console.error('❌ Erreur récupération config webhook BAP:', error);
res.status(500).json({ error: 'Erreur serveur', details: error.message });
}
});
app.post('/api/webhook-bap-config', isAuthenticated, async (req: any, res) => {
try {
// Vérifier que l'utilisateur est admin
const userId = req.user?.claims?.sub || req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Utilisateur non authentifié' });
}
const user = await storage.getUser(userId);
if (!user || user.role !== 'admin') {
return res.status(403).json({ error: 'Accès refusé - Admin uniquement' });
}
// Valider les données avec Zod
const validatedData = insertWebhookBapConfigSchema.parse(req.body);
// Vérifier si une configuration existe déjà
const existingConfig = await storage.getWebhookBapConfig();
let config: any;
if (existingConfig) {
// Mettre à jour la configuration existante
config = await storage.updateWebhookBapConfig(existingConfig.id, validatedData);
} else {
// Créer une nouvelle configuration
config = await storage.createWebhookBapConfig(validatedData);
}
console.log('✅ Configuration webhook BAP sauvegardée:', {
id: config.id,
name: config.name,
isActive: config.isActive
});
res.json(config);
} catch (error: any) {
console.error('❌ Erreur sauvegarde config webhook BAP:', error);
res.status(500).json({ error: 'Erreur serveur', details: error.message });
}
});
app.post('/api/webhook-bap-config/test', isAuthenticated, async (req: any, res) => {
try {
// Vérifier que l'utilisateur est admin
const userId = req.user?.claims?.sub || req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Utilisateur non authentifié' });
}
const user = await storage.getUser(userId);
if (!user || user.role !== 'admin') {
return res.status(403).json({ error: 'Accès refusé - Admin uniquement' });
}
const { webhookUrl } = req.body;
if (!webhookUrl) {
return res.status(400).json({ error: 'URL webhook requise' });
}
console.log('🔍 Test webhook BAP:', { url: webhookUrl });
// Tester la connectivité avec le webhook
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 secondes
const testResponse = await fetch(webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
test: true,
timestamp: new Date().toISOString(),
message: 'Test de connectivité depuis LogiFlow'
}),
signal: controller.signal
});
clearTimeout(timeoutId);
console.log('🌐 Réponse test webhook:', { status: testResponse.status, ok: testResponse.ok });
if (!testResponse.ok) {
throw new Error(`HTTP ${testResponse.status}: ${testResponse.statusText}`);
}
const result = await testResponse.text();
res.json({
success: true,
status: testResponse.status,
message: 'Webhook accessible',
response: result.substring(0, 200) // Limiter la réponse
});
} catch (error: any) {
console.error('❌ Erreur test webhook BAP:', error);
let errorMessage = 'Échec du test de connectivité';
if (error.name === 'AbortError') {
errorMessage = 'Timeout - Le webhook ne répond pas';
} else if (error.message) {
errorMessage = error.message;
}
res.status(500).json({ error: errorMessage, details: error.message });
}
});
// Route BAP pour envoi webhook n8n
app.post('/api/bap/send-webhook', isAuthenticated, async (req: any, res) => {
try {
@@ -143,8 +276,14 @@ export async function registerRoutes(app: Express): Promise<Server> {
fileSize: fileBuffer.length
});
// Envoyer vers le webhook n8n
const webhookUrl = 'https://workflow.ffnancy.fr/webhook/a3d03176-b72f-412d-8fb9-f920b9fbab4d';
// Récupérer l'URL du webhook depuis la configuration
const webhookConfig = await storage.getWebhookBapConfig();
if (!webhookConfig || !webhookConfig.isActive) {
console.error('❌ BAP: Configuration webhook non trouvée ou inactive');
return res.status(500).json({ error: 'Configuration webhook BAP non disponible' });
}
const webhookUrl = webhookConfig.webhookUrl;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 secondes
+63
View File
@@ -66,6 +66,9 @@ import {
type InsertWeatherData,
type WeatherSettings,
type InsertWeatherSettings,
webhookBapConfig,
type WebhookBapConfig,
type InsertWebhookBapConfig,
} from "@shared/schema";
import { db } from "./db";
import { eq, and, inArray, desc, sql, gte, lte, lt, gt, or, isNull, isNotNull, asc, ne } from "drizzle-orm";
@@ -219,6 +222,11 @@ export interface IStorage {
updateWeatherData(id: number, data: Partial<InsertWeatherData>): Promise<WeatherData>;
deleteOldWeatherData(daysToKeep: number): Promise<void>;
clearWeatherCache(): Promise<void>;
// Webhook BAP Configuration
getWebhookBapConfig(): Promise<WebhookBapConfig | undefined>;
createWebhookBapConfig(config: InsertWebhookBapConfig): Promise<WebhookBapConfig>;
updateWebhookBapConfig(id: number, config: Partial<InsertWebhookBapConfig>): Promise<WebhookBapConfig>;
}
export class DatabaseStorage implements IStorage {
@@ -2211,6 +2219,29 @@ export class DatabaseStorage implements IStorage {
await db.delete(weatherData);
console.log('🧹 Weather cache cleared due to location change');
}
// Webhook BAP Configuration
async getWebhookBapConfig(): Promise<WebhookBapConfig | undefined> {
const [config] = await db.select().from(webhookBapConfig).limit(1);
return config;
}
async createWebhookBapConfig(configData: InsertWebhookBapConfig): Promise<WebhookBapConfig> {
const [config] = await db
.insert(webhookBapConfig)
.values(configData)
.returning();
return config;
}
async updateWebhookBapConfig(id: number, configData: Partial<InsertWebhookBapConfig>): Promise<WebhookBapConfig> {
const [config] = await db
.update(webhookBapConfig)
.set({ ...configData, updatedAt: new Date() })
.where(eq(webhookBapConfig.id, id))
.returning();
return config;
}
}
// MemStorage class for development
@@ -3859,6 +3890,38 @@ export class MemStorage implements IStorage {
console.log('🧹 DEV: Weather cache cleared due to location change');
}
// Webhook BAP Configuration
async getWebhookBapConfig(): Promise<WebhookBapConfig | undefined> {
// En développement, retourner une config par défaut
return {
id: 1,
name: "Configuration BAP",
webhookUrl: "https://workflow.ffnancy.fr/webhook/a3d03176-b72f-412d-8fb9-f920b9fbab4d",
description: "Configuration par défaut pour développement",
isActive: true,
createdAt: new Date(),
updatedAt: new Date()
};
}
async createWebhookBapConfig(config: InsertWebhookBapConfig): Promise<WebhookBapConfig> {
return {
id: 1,
...config,
createdAt: new Date(),
updatedAt: new Date()
};
}
async updateWebhookBapConfig(id: number, config: Partial<InsertWebhookBapConfig>): Promise<WebhookBapConfig> {
const existing = await this.getWebhookBapConfig();
return {
...existing!,
...config,
updatedAt: new Date()
};
}
// Announcement operations
async createAnnouncement(announcementData: InsertAnnouncement): Promise<Announcement> {
const announcementStorage = getAnnouncementStorage(