mirror of
https://github.com/R0m1k3/Regisflow.git
synced 2026-10-11 17:29:48 +02:00
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
This commit is contained in:
1 parent
04aff6fbbe
commit
546ba12e89
3 files changed
+345
-2
No files matched your search
@@ -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() {
|
||||
<TabsList>
|
||||
<TabsTrigger value="users">Utilisateurs</TabsTrigger>
|
||||
<TabsTrigger value="stores">Magasins</TabsTrigger>
|
||||
<TabsTrigger value="backup">Sauvegarde</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Users Tab */}
|
||||
@@ -959,6 +1047,117 @@ export default function Administration() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</TabsContent>
|
||||
|
||||
{/* Backup Tab */}
|
||||
<TabsContent value="backup" className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-semibold flex items-center gap-2">
|
||||
<Database className="h-5 w-5" />
|
||||
Gestion des Sauvegardes
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Export Backup */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Download className="h-5 w-5" />
|
||||
Exporter la Base de Données
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Téléchargez une sauvegarde complète de toutes les données (utilisateurs, magasins, ventes).
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleExportBackup}
|
||||
disabled={exportBackupMutation.isPending}
|
||||
className="w-full"
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
{exportBackupMutation.isPending ? "Export en cours..." : "Télécharger la Sauvegarde"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Import Backup */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Upload className="h-5 w-5" />
|
||||
Importer une Sauvegarde
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Restaurez les données à partir d'un fichier de sauvegarde.
|
||||
<strong className="text-red-600"> Attention : cette action remplacera toutes les données existantes.</strong>
|
||||
</p>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={importBackupMutation.isPending}
|
||||
className="w-full"
|
||||
>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
{importBackupMutation.isPending ? "Import en cours..." : "Restaurer depuis un Fichier"}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Confirmer la restauration</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Êtes-vous sûr de vouloir restaurer les données depuis un fichier de sauvegarde ?
|
||||
<br /><br />
|
||||
<strong className="text-red-600">
|
||||
⚠️ ATTENTION : Cette action supprimera définitivement toutes les données actuelles
|
||||
(utilisateurs, magasins, ventes) et les remplacera par celles du fichier de sauvegarde.
|
||||
</strong>
|
||||
<br /><br />
|
||||
Cette action est irréversible. Assurez-vous d'avoir une sauvegarde des données actuelles
|
||||
si vous souhaitez pouvoir les récupérer.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Annuler</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleImportBackup}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
Confirmer la Restauration
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Info Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Informations sur les Sauvegardes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div>
|
||||
<strong>Format des fichiers :</strong> Les sauvegardes sont au format JSON et contiennent toutes les données de l'application.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Contenu inclus :</strong> Utilisateurs, magasins, ventes, et toutes les relations entre ces données.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Sécurité :</strong> Les mots de passe sont hachés et ne peuvent pas être récupérés depuis une sauvegarde.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Recommandation :</strong> Effectuez des sauvegardes régulières et stockez-les en lieu sûr.
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -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
|
||||
- ✅ 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
|
||||
@@ -357,6 +357,142 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
Reference in new issue
Block a user