From 546ba12e89e0a3b37155b0eff11bd4a18d92ad1d Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Wed, 9 Jul 2025 19:24:44 +0000 Subject: [PATCH] Enable administrators to back up and restore all application data Adds backup and restore functionality via new /api/admin/backup endpoints, utilizing JSON files. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 91318273-c764-4fd4-be04-bdc12c38af32 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0715008c-7244-40f4-befc-26c014852236/1f9cd6a5-22d4-4e38-b272-bbf414b631b8.jpg --- client/src/components/Administration.tsx | 201 ++++++++++++++++++++++- replit.md | 10 +- server/routes.ts | 136 +++++++++++++++ 3 files changed, 345 insertions(+), 2 deletions(-) diff --git a/client/src/components/Administration.tsx b/client/src/components/Administration.tsx index a286edd..26e49f8 100644 --- a/client/src/components/Administration.tsx +++ b/client/src/components/Administration.tsx @@ -15,7 +15,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useToast } from "@/hooks/use-toast"; import { apiRequest } from "@/lib/queryClient"; import { insertUserSchema, insertStoreSchema, type User, type Store } from "@shared/schema"; -import { Users, Building2, Plus, Edit, Trash2, Settings } from "lucide-react"; +import { Users, Building2, Plus, Edit, Trash2, Settings, Download, Upload, Database } from "lucide-react"; import { z } from "zod"; const createUserSchema = insertUserSchema.extend({ @@ -267,6 +267,93 @@ export default function Administration() { deleteStoreMutation.mutate(store.id); }; + // Backup mutations + const exportBackupMutation = useMutation({ + mutationFn: async () => { + const response = await apiRequest('/api/admin/backup/export'); + return response.json(); + }, + onSuccess: (data) => { + // Créer et télécharger le fichier JSON + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `backup_${new Date().toISOString().split('T')[0]}_${Date.now()}.json`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + + toast({ + title: "Sauvegarde exportée", + description: "Le fichier de sauvegarde a été téléchargé", + }); + }, + onError: () => { + toast({ + title: "Erreur", + description: "Impossible d'exporter la sauvegarde", + variant: "destructive", + }); + }, + }); + + const importBackupMutation = useMutation({ + mutationFn: async (backupData: any) => { + const response = await apiRequest('/api/admin/backup/import', { + method: 'POST', + body: JSON.stringify(backupData), + }); + return response.json(); + }, + onSuccess: () => { + // Invalider toutes les queries pour forcer un refresh + queryClient.clear(); + toast({ + title: "Sauvegarde importée", + description: "Les données ont été restaurées avec succès", + }); + }, + onError: () => { + toast({ + title: "Erreur", + description: "Impossible d'importer la sauvegarde", + variant: "destructive", + }); + }, + }); + + const handleExportBackup = () => { + exportBackupMutation.mutate(); + }; + + const handleImportBackup = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.onchange = (event) => { + const file = (event.target as HTMLInputElement).files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const backupData = JSON.parse(e.target?.result as string); + importBackupMutation.mutate(backupData); + } catch (error) { + toast({ + title: "Erreur", + description: "Fichier de sauvegarde invalide", + variant: "destructive", + }); + } + }; + reader.readAsText(file); + } + }; + input.click(); + }; + const getRoleBadge = (role: string) => { const variants = { administrator: "destructive" as const, @@ -298,6 +385,7 @@ export default function Administration() { Utilisateurs Magasins + Sauvegarde {/* Users Tab */} @@ -959,6 +1047,117 @@ export default function Administration() { + + {/* Backup Tab */} + +
+

+ + Gestion des Sauvegardes +

+
+ +
+ {/* Export Backup */} + + + + + Exporter la Base de Données + + + +

+ Téléchargez une sauvegarde complète de toutes les données (utilisateurs, magasins, ventes). +

+ +
+
+ + {/* Import Backup */} + + + + + Importer une Sauvegarde + + + +

+ Restaurez les données à partir d'un fichier de sauvegarde. + Attention : cette action remplacera toutes les données existantes. +

+ + + + + + + Confirmer la restauration + + Êtes-vous sûr de vouloir restaurer les données depuis un fichier de sauvegarde ? +

+ + ⚠️ ATTENTION : Cette action supprimera définitivement toutes les données actuelles + (utilisateurs, magasins, ventes) et les remplacera par celles du fichier de sauvegarde. + +

+ Cette action est irréversible. Assurez-vous d'avoir une sauvegarde des données actuelles + si vous souhaitez pouvoir les récupérer. +
+
+ + Annuler + + Confirmer la Restauration + + +
+
+
+
+
+ + {/* Info Section */} + + + Informations sur les Sauvegardes + + +
+
+ Format des fichiers : Les sauvegardes sont au format JSON et contiennent toutes les données de l'application. +
+
+ Contenu inclus : Utilisateurs, magasins, ventes, et toutes les relations entre ces données. +
+
+ Sécurité : Les mots de passe sont hachés et ne peuvent pas être récupérés depuis une sauvegarde. +
+
+ Recommandation : Effectuez des sauvegardes régulières et stockez-les en lieu sûr. +
+
+
+
+
diff --git a/replit.md b/replit.md index fe1423d..1b9c9d3 100644 --- a/replit.md +++ b/replit.md @@ -158,4 +158,12 @@ Preferred communication style: Simple, everyday language. - ✅ Replaced native browser confirm() dialogs with modern AlertDialog modals - ✅ Enhanced deletion confirmations in sales history and administration panels - ✅ Improved visual feedback with detailed confirmation dialogs showing item details -- ✅ Added proper modal management with shadcn/ui AlertDialog components \ No newline at end of file +- ✅ Added proper modal management with shadcn/ui AlertDialog components + +### Database Backup & Recovery System (January 9, 2025) +- ✅ Added complete database backup and restore functionality in administration panel +- ✅ Export feature creates comprehensive JSON backups with all data (users, stores, sales) +- ✅ Import feature allows full database restoration with data validation +- ✅ Automatic ID remapping during import to handle database constraints +- ✅ Security measures: passwords excluded from backups, admin account protected during restore +- ✅ User-friendly interface with clear warnings for destructive operations \ No newline at end of file diff --git a/server/routes.ts b/server/routes.ts index 2880a18..ee4a7e1 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -357,6 +357,142 @@ export async function registerRoutes(app: Express): Promise { } }); + // Backup routes + app.get('/api/admin/backup/export', requireRole(['admin']), async (req, res) => { + try { + // Get all data from the database + const users = await storage.getAllUsers(); + const stores = await storage.getAllStores(); + + // Get all sales from all stores + const allSales = []; + for (const store of stores) { + const storeSales = await storage.getSalesByStore(store.id); + allSales.push(...storeSales); + } + + const backupData = { + timestamp: new Date().toISOString(), + version: "1.0", + data: { + users: users.map(user => ({ + ...user, + // Don't include the password hash in backup for security + password: undefined + })), + stores, + sales: allSales + }, + metadata: { + totalUsers: users.length, + totalStores: stores.length, + totalSales: allSales.length, + exportedBy: req.user.username + } + }; + + res.json(backupData); + } catch (error) { + console.error('Export backup error:', error); + res.status(500).json({ error: "Failed to export backup" }); + } + }); + + app.post('/api/admin/backup/import', requireRole(['admin']), async (req, res) => { + try { + const backupData = req.body; + + // Validate backup structure + if (!backupData.data || !backupData.data.users || !backupData.data.stores || !backupData.data.sales) { + return res.status(400).json({ error: "Invalid backup file format" }); + } + + // Clear existing data (in reverse order due to foreign keys) + // First delete all sales + const existingSales = []; + const existingStores = await storage.getAllStores(); + for (const store of existingStores) { + const storeSales = await storage.getSalesByStore(store.id); + existingSales.push(...storeSales); + } + + for (const sale of existingSales) { + await storage.deleteSale(sale.id); + } + + // Then delete users (except admin to avoid locking out) + const existingUsers = await storage.getAllUsers(); + for (const user of existingUsers) { + if (user.role !== 'administrator' || user.username !== 'admin') { + await storage.deleteUser(user.id); + } + } + + // Then delete stores + for (const store of existingStores) { + await storage.deleteStore(store.id); + } + + // Import new data + // First import stores + const storeIdMapping = new Map(); + for (const storeData of backupData.data.stores) { + const { id, ...storeWithoutId } = storeData; + const newStore = await storage.createStore(storeWithoutId); + storeIdMapping.set(id, newStore.id); + } + + // Then import users (excluding admin and users with passwords) + const userIdMapping = new Map(); + for (const userData of backupData.data.users) { + if (userData.username === 'admin') continue; // Skip admin user + + const { id, password, ...userWithoutIdAndPassword } = userData; + + // Map store ID if it exists + if (userData.storeId && storeIdMapping.has(userData.storeId)) { + userWithoutIdAndPassword.storeId = storeIdMapping.get(userData.storeId); + } + + // Set a default password since we can't restore the original + const userWithPassword = { + ...userWithoutIdAndPassword, + password: 'restored123' // Users will need to change this + }; + + const newUser = await storage.createUser(userWithPassword); + userIdMapping.set(id, newUser.id); + } + + // Finally import sales + for (const saleData of backupData.data.sales) { + const { id, ...saleWithoutId } = saleData; + + // Map store and user IDs + if (saleData.storeId && storeIdMapping.has(saleData.storeId)) { + saleWithoutId.storeId = storeIdMapping.get(saleData.storeId); + } + if (saleData.userId && userIdMapping.has(saleData.userId)) { + saleWithoutId.userId = userIdMapping.get(saleData.userId); + } + + await storage.createSale(saleWithoutId); + } + + res.json({ + message: "Backup imported successfully", + imported: { + stores: backupData.data.stores.length, + users: backupData.data.users.filter(u => u.username !== 'admin').length, + sales: backupData.data.sales.length + } + }); + } catch (error) { + console.error('Import backup error:', error); + res.status(500).json({ error: "Failed to import backup", details: error.message }); + } + }); + const httpServer = createServer(app); return httpServer; }