mirror of
https://github.com/R0m1k3/Regisflow.git
synced 2026-10-23 23:28:18 +02:00
Enable automatic data backups to secure user information regularly
Implements automatic database backups every 12 hours using node-cron, retaining only the 10 most recent backups in server/backup-scheduler.ts. 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/674ec742-23a2-4047-910f-36599184f036.jpg
This commit is contained in:
1 parent
546ba12e89
commit
a78d648c01
9 files changed
+594
-5
No files matched your search
@@ -0,0 +1,151 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-07-09T19:27:47.907Z",
|
||||||
|
"version": "1.0",
|
||||||
|
"type": "automatic",
|
||||||
|
"data": {
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"username": "FFNANCY",
|
||||||
|
"email": "",
|
||||||
|
"firstName": "",
|
||||||
|
"lastName": "",
|
||||||
|
"role": "manager",
|
||||||
|
"storeId": 2,
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": "2025-07-09T18:55:01.413Z",
|
||||||
|
"updatedAt": "2025-07-09T18:55:01.413Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"username": "admin",
|
||||||
|
"email": "admin@example.com",
|
||||||
|
"firstName": "Administrateur",
|
||||||
|
"lastName": "Principal",
|
||||||
|
"role": "admin",
|
||||||
|
"storeId": 1,
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": "2025-07-09T18:37:53.865Z",
|
||||||
|
"updatedAt": "2025-07-09T18:37:53.865Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stores": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Houdemont",
|
||||||
|
"address": "",
|
||||||
|
"phone": "",
|
||||||
|
"email": "",
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": "2025-07-09T19:00:30.782Z",
|
||||||
|
"updatedAt": "2025-07-09T19:00:30.782Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Frouard",
|
||||||
|
"address": "À définir",
|
||||||
|
"phone": "",
|
||||||
|
"email": "",
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": "2025-07-09T18:37:53.432Z",
|
||||||
|
"updatedAt": "2025-07-09T18:37:53.432Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sales": [
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"storeId": 2,
|
||||||
|
"userId": 2,
|
||||||
|
"timestamp": "2025-07-09T19:16:46.797Z",
|
||||||
|
"vendeur": "Moi",
|
||||||
|
"dateVente": "2025-07-09",
|
||||||
|
"typeArticle": "Fusée",
|
||||||
|
"categorie": "F2",
|
||||||
|
"quantite": 10,
|
||||||
|
"gencode": "3660092323745",
|
||||||
|
"nom": " ASSOCIATION GRAND AIR",
|
||||||
|
"prenom": "Anne-Laure",
|
||||||
|
"dateNaissance": "2025-06-03",
|
||||||
|
"lieuNaissance": "Bar le duc",
|
||||||
|
"typeIdentite": "Passeport",
|
||||||
|
"numeroIdentite": "qsdfqsdgc23445",
|
||||||
|
"autoriteDelivrance": "Frouard",
|
||||||
|
"dateDelivrance": "2025-07-01",
|
||||||
|
"photoRecto": null,
|
||||||
|
"photoVerso": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"storeId": 2,
|
||||||
|
"userId": 1,
|
||||||
|
"timestamp": "2025-07-09T19:07:15.650Z",
|
||||||
|
"vendeur": "Moi",
|
||||||
|
"dateVente": "2025-07-09",
|
||||||
|
"typeArticle": "Pétard aérien",
|
||||||
|
"categorie": "F3",
|
||||||
|
"quantite": 1,
|
||||||
|
"gencode": "3660092323745",
|
||||||
|
"nom": "SCHAL",
|
||||||
|
"prenom": "Michaël",
|
||||||
|
"dateNaissance": "2025-07-02",
|
||||||
|
"lieuNaissance": "Bar le duc",
|
||||||
|
"typeIdentite": "CNI",
|
||||||
|
"numeroIdentite": "qsdfqsdgc23445",
|
||||||
|
"autoriteDelivrance": "Frouard",
|
||||||
|
"dateDelivrance": "2025-07-01",
|
||||||
|
"photoRecto": null,
|
||||||
|
"photoVerso": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"storeId": 2,
|
||||||
|
"userId": 1,
|
||||||
|
"timestamp": "2025-07-09T19:05:20.904Z",
|
||||||
|
"vendeur": "Moi",
|
||||||
|
"dateVente": "2025-07-09",
|
||||||
|
"typeArticle": "Pétard aérien",
|
||||||
|
"categorie": "F3",
|
||||||
|
"quantite": 1,
|
||||||
|
"gencode": "3660092323745",
|
||||||
|
"nom": "SCHAL",
|
||||||
|
"prenom": "Michaël",
|
||||||
|
"dateNaissance": "2025-07-02",
|
||||||
|
"lieuNaissance": "Bar le duc",
|
||||||
|
"typeIdentite": "CNI",
|
||||||
|
"numeroIdentite": "qsdfqsdgc23445",
|
||||||
|
"autoriteDelivrance": "Frouard",
|
||||||
|
"dateDelivrance": "2025-07-01",
|
||||||
|
"photoRecto": null,
|
||||||
|
"photoVerso": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"storeId": 1,
|
||||||
|
"userId": 1,
|
||||||
|
"timestamp": "2025-07-09T19:11:36.505Z",
|
||||||
|
"vendeur": "Moi",
|
||||||
|
"dateVente": "2025-07-09",
|
||||||
|
"typeArticle": "Batterie",
|
||||||
|
"categorie": "F3",
|
||||||
|
"quantite": 1,
|
||||||
|
"gencode": "3660092323745",
|
||||||
|
"nom": "SCHAL",
|
||||||
|
"prenom": "Michaël",
|
||||||
|
"dateNaissance": "2025-07-18",
|
||||||
|
"lieuNaissance": "Bar le duc",
|
||||||
|
"typeIdentite": "CNI",
|
||||||
|
"numeroIdentite": "qsdfqsdgc23445",
|
||||||
|
"autoriteDelivrance": "Frouard",
|
||||||
|
"dateDelivrance": "2025-07-01",
|
||||||
|
"photoRecto": null,
|
||||||
|
"photoVerso": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"totalUsers": 2,
|
||||||
|
"totalStores": 2,
|
||||||
|
"totalSales": 4,
|
||||||
|
"exportedBy": "system-auto-backup"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-07-09T19:28:49.721Z",
|
||||||
|
"version": "1.0",
|
||||||
|
"type": "automatic",
|
||||||
|
"data": {
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"username": "FFNANCY",
|
||||||
|
"email": "",
|
||||||
|
"firstName": "",
|
||||||
|
"lastName": "",
|
||||||
|
"role": "manager",
|
||||||
|
"storeId": 2,
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": "2025-07-09T18:55:01.413Z",
|
||||||
|
"updatedAt": "2025-07-09T18:55:01.413Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"username": "admin",
|
||||||
|
"email": "admin@example.com",
|
||||||
|
"firstName": "Administrateur",
|
||||||
|
"lastName": "Principal",
|
||||||
|
"role": "admin",
|
||||||
|
"storeId": 1,
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": "2025-07-09T18:37:53.865Z",
|
||||||
|
"updatedAt": "2025-07-09T18:37:53.865Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stores": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Houdemont",
|
||||||
|
"address": "",
|
||||||
|
"phone": "",
|
||||||
|
"email": "",
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": "2025-07-09T19:00:30.782Z",
|
||||||
|
"updatedAt": "2025-07-09T19:00:30.782Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Frouard",
|
||||||
|
"address": "À définir",
|
||||||
|
"phone": "",
|
||||||
|
"email": "",
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": "2025-07-09T18:37:53.432Z",
|
||||||
|
"updatedAt": "2025-07-09T18:37:53.432Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sales": [
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"storeId": 2,
|
||||||
|
"userId": 2,
|
||||||
|
"timestamp": "2025-07-09T19:16:46.797Z",
|
||||||
|
"vendeur": "Moi",
|
||||||
|
"dateVente": "2025-07-09",
|
||||||
|
"typeArticle": "Fusée",
|
||||||
|
"categorie": "F2",
|
||||||
|
"quantite": 10,
|
||||||
|
"gencode": "3660092323745",
|
||||||
|
"nom": " ASSOCIATION GRAND AIR",
|
||||||
|
"prenom": "Anne-Laure",
|
||||||
|
"dateNaissance": "2025-06-03",
|
||||||
|
"lieuNaissance": "Bar le duc",
|
||||||
|
"typeIdentite": "Passeport",
|
||||||
|
"numeroIdentite": "qsdfqsdgc23445",
|
||||||
|
"autoriteDelivrance": "Frouard",
|
||||||
|
"dateDelivrance": "2025-07-01",
|
||||||
|
"photoRecto": null,
|
||||||
|
"photoVerso": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"storeId": 2,
|
||||||
|
"userId": 1,
|
||||||
|
"timestamp": "2025-07-09T19:07:15.650Z",
|
||||||
|
"vendeur": "Moi",
|
||||||
|
"dateVente": "2025-07-09",
|
||||||
|
"typeArticle": "Pétard aérien",
|
||||||
|
"categorie": "F3",
|
||||||
|
"quantite": 1,
|
||||||
|
"gencode": "3660092323745",
|
||||||
|
"nom": "SCHAL",
|
||||||
|
"prenom": "Michaël",
|
||||||
|
"dateNaissance": "2025-07-02",
|
||||||
|
"lieuNaissance": "Bar le duc",
|
||||||
|
"typeIdentite": "CNI",
|
||||||
|
"numeroIdentite": "qsdfqsdgc23445",
|
||||||
|
"autoriteDelivrance": "Frouard",
|
||||||
|
"dateDelivrance": "2025-07-01",
|
||||||
|
"photoRecto": null,
|
||||||
|
"photoVerso": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"storeId": 2,
|
||||||
|
"userId": 1,
|
||||||
|
"timestamp": "2025-07-09T19:05:20.904Z",
|
||||||
|
"vendeur": "Moi",
|
||||||
|
"dateVente": "2025-07-09",
|
||||||
|
"typeArticle": "Pétard aérien",
|
||||||
|
"categorie": "F3",
|
||||||
|
"quantite": 1,
|
||||||
|
"gencode": "3660092323745",
|
||||||
|
"nom": "SCHAL",
|
||||||
|
"prenom": "Michaël",
|
||||||
|
"dateNaissance": "2025-07-02",
|
||||||
|
"lieuNaissance": "Bar le duc",
|
||||||
|
"typeIdentite": "CNI",
|
||||||
|
"numeroIdentite": "qsdfqsdgc23445",
|
||||||
|
"autoriteDelivrance": "Frouard",
|
||||||
|
"dateDelivrance": "2025-07-01",
|
||||||
|
"photoRecto": null,
|
||||||
|
"photoVerso": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"storeId": 1,
|
||||||
|
"userId": 1,
|
||||||
|
"timestamp": "2025-07-09T19:11:36.505Z",
|
||||||
|
"vendeur": "Moi",
|
||||||
|
"dateVente": "2025-07-09",
|
||||||
|
"typeArticle": "Batterie",
|
||||||
|
"categorie": "F3",
|
||||||
|
"quantite": 1,
|
||||||
|
"gencode": "3660092323745",
|
||||||
|
"nom": "SCHAL",
|
||||||
|
"prenom": "Michaël",
|
||||||
|
"dateNaissance": "2025-07-18",
|
||||||
|
"lieuNaissance": "Bar le duc",
|
||||||
|
"typeIdentite": "CNI",
|
||||||
|
"numeroIdentite": "qsdfqsdgc23445",
|
||||||
|
"autoriteDelivrance": "Frouard",
|
||||||
|
"dateDelivrance": "2025-07-01",
|
||||||
|
"photoRecto": null,
|
||||||
|
"photoVerso": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"totalUsers": 2,
|
||||||
|
"totalStores": 2,
|
||||||
|
"totalSales": 4,
|
||||||
|
"exportedBy": "system-auto-backup"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { apiRequest } from "@/lib/queryClient";
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
import { insertUserSchema, insertStoreSchema, type User, type Store } from "@shared/schema";
|
import { insertUserSchema, insertStoreSchema, type User, type Store } from "@shared/schema";
|
||||||
import { Users, Building2, Plus, Edit, Trash2, Settings, Download, Upload, Database } from "lucide-react";
|
import { Users, Building2, Plus, Edit, Trash2, Settings, Download, Upload, Database, Clock, Calendar } from "lucide-react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const createUserSchema = insertUserSchema.extend({
|
const createUserSchema = insertUserSchema.extend({
|
||||||
@@ -72,6 +72,15 @@ export default function Administration() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: backupStats } = useQuery({
|
||||||
|
queryKey: ['/api/admin/backup/stats'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await apiRequest('/api/admin/backup/stats');
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
refetchInterval: 30000, // Refresh every 30 seconds
|
||||||
|
});
|
||||||
|
|
||||||
// User mutations
|
// User mutations
|
||||||
const createUserMutation = useMutation({
|
const createUserMutation = useMutation({
|
||||||
mutationFn: async (userData: CreateUserData) => {
|
mutationFn: async (userData: CreateUserData) => {
|
||||||
@@ -324,6 +333,29 @@ export default function Administration() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createManualBackupMutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const response = await apiRequest('/api/admin/backup/create', {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['/api/admin/backup/stats'] });
|
||||||
|
toast({
|
||||||
|
title: "Sauvegarde créée",
|
||||||
|
description: `Sauvegarde automatique créée: ${data.filename}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Erreur de sauvegarde",
|
||||||
|
description: error.message || "Impossible de créer la sauvegarde",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleExportBackup = () => {
|
const handleExportBackup = () => {
|
||||||
exportBackupMutation.mutate();
|
exportBackupMutation.mutate();
|
||||||
};
|
};
|
||||||
@@ -1057,6 +1089,51 @@ export default function Administration() {
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Automatic Backup Status */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Clock className="h-5 w-5" />
|
||||||
|
Sauvegarde Automatique
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-green-600">
|
||||||
|
{backupStats?.totalBackups || 0}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">Sauvegardes disponibles</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-blue-600">12h</div>
|
||||||
|
<div className="text-sm text-muted-foreground">Intervalle automatique</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-purple-600">10</div>
|
||||||
|
<div className="text-sm text-muted-foreground">Maximum conservées</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 p-3 bg-muted rounded-lg">
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<Calendar className="h-4 w-4" />
|
||||||
|
<span>Prochaine sauvegarde automatique : 00:00 ou 12:00</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<Button
|
||||||
|
onClick={() => createManualBackupMutation.mutate()}
|
||||||
|
disabled={createManualBackupMutation.isPending}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Clock className="h-4 w-4 mr-2" />
|
||||||
|
{createManualBackupMutation.isPending ? "Création en cours..." : "Créer une Sauvegarde Manuelle"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{/* Export Backup */}
|
{/* Export Backup */}
|
||||||
<Card>
|
<Card>
|
||||||
@@ -1142,6 +1219,9 @@ export default function Administration() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-3 text-sm">
|
<div className="space-y-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<strong>Sauvegarde automatique :</strong> Le système crée automatiquement une sauvegarde toutes les 12 heures (00:00 et 12:00) et conserve les 10 plus récentes.
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Format des fichiers :</strong> Les sauvegardes sont au format JSON et contiennent toutes les données de l'application.
|
<strong>Format des fichiers :</strong> Les sauvegardes sont au format JSON et contiennent toutes les données de l'application.
|
||||||
</div>
|
</div>
|
||||||
@@ -1152,7 +1232,7 @@ export default function Administration() {
|
|||||||
<strong>Sécurité :</strong> Les mots de passe sont hachés et ne peuvent pas être récupérés depuis une sauvegarde.
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<strong>Recommandation :</strong> Effectuez des sauvegardes régulières et stockez-les en lieu sûr.
|
<strong>Recommandation :</strong> Les sauvegardes automatiques couvrent la plupart des besoins, mais vous pouvez créer des sauvegardes manuelles avant des modifications importantes.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
Generated
+17
@@ -42,6 +42,7 @@
|
|||||||
"@tanstack/react-query": "^5.60.5",
|
"@tanstack/react-query": "^5.60.5",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/memoizee": "^0.4.12",
|
"@types/memoizee": "^0.4.12",
|
||||||
|
"@types/node-cron": "^3.0.11",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
"memorystore": "^1.6.7",
|
"memorystore": "^1.6.7",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"node-cron": "^4.2.0",
|
||||||
"openid-client": "^6.6.2",
|
"openid-client": "^6.6.2",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
@@ -3498,6 +3500,12 @@
|
|||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node-cron": {
|
||||||
|
"version": "3.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz",
|
||||||
|
"integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/passport": {
|
"node_modules/@types/passport": {
|
||||||
"version": "1.0.17",
|
"version": "1.0.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz",
|
||||||
@@ -6435,6 +6443,15 @@
|
|||||||
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
|
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-cron": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-nOdP7uH7u55w7ybQq9fusXtsResok+ErzvOBydJUPBBaQ9W+EfBaBWFPgJ8sOB7FWQednDvVBJtgP5xA0bME7Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-gyp-build": {
|
"node_modules/node-gyp-build": {
|
||||||
"version": "4.8.3",
|
"version": "4.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"@tanstack/react-query": "^5.60.5",
|
"@tanstack/react-query": "^5.60.5",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/memoizee": "^0.4.12",
|
"@types/memoizee": "^0.4.12",
|
||||||
|
"@types/node-cron": "^3.0.11",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -62,6 +63,7 @@
|
|||||||
"memorystore": "^1.6.7",
|
"memorystore": "^1.6.7",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"node-cron": "^4.2.0",
|
||||||
"openid-client": "^6.6.2",
|
"openid-client": "^6.6.2",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
|
|||||||
@@ -166,4 +166,12 @@ Preferred communication style: Simple, everyday language.
|
|||||||
- ✅ Import feature allows full database restoration with data validation
|
- ✅ Import feature allows full database restoration with data validation
|
||||||
- ✅ Automatic ID remapping during import to handle database constraints
|
- ✅ Automatic ID remapping during import to handle database constraints
|
||||||
- ✅ Security measures: passwords excluded from backups, admin account protected during restore
|
- ✅ Security measures: passwords excluded from backups, admin account protected during restore
|
||||||
- ✅ User-friendly interface with clear warnings for destructive operations
|
- ✅ User-friendly interface with clear warnings for destructive operations
|
||||||
|
|
||||||
|
### Automatic Backup Scheduler (January 9, 2025)
|
||||||
|
- ✅ Implemented automatic backup system with node-cron running every 12 hours (00:00 and 12:00)
|
||||||
|
- ✅ Automatic cleanup maintaining only the 10 most recent backups to prevent disk space issues
|
||||||
|
- ✅ Backup files stored in server/backups/ directory with timestamped filenames
|
||||||
|
- ✅ Enhanced administration interface showing backup statistics and manual backup creation
|
||||||
|
- ✅ Real-time backup status monitoring with automatic stats refresh every 30 seconds
|
||||||
|
- ✅ Initial backup creation on server startup if no backups exist
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import cron from 'node-cron';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { storage } from './storage';
|
||||||
|
|
||||||
|
const BACKUP_DIR = path.join(process.cwd(), 'backups');
|
||||||
|
const MAX_BACKUPS = 10;
|
||||||
|
|
||||||
|
// Ensure backup directory exists
|
||||||
|
if (!fs.existsSync(BACKUP_DIR)) {
|
||||||
|
fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAutomaticBackup() {
|
||||||
|
try {
|
||||||
|
console.log('Creating automatic backup...');
|
||||||
|
|
||||||
|
// 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",
|
||||||
|
type: "automatic",
|
||||||
|
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: 'system-auto-backup'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create filename with timestamp
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const filename = `auto-backup-${timestamp}.json`;
|
||||||
|
const filepath = path.join(BACKUP_DIR, filename);
|
||||||
|
|
||||||
|
// Write backup to file
|
||||||
|
fs.writeFileSync(filepath, JSON.stringify(backupData, null, 2));
|
||||||
|
|
||||||
|
console.log(`✅ Automatic backup created: ${filename}`);
|
||||||
|
console.log(`📊 Backup stats: ${users.length} users, ${stores.length} stores, ${allSales.length} sales`);
|
||||||
|
|
||||||
|
// Clean up old backups
|
||||||
|
await cleanupOldBackups();
|
||||||
|
|
||||||
|
return { success: true, filename, stats: backupData.metadata };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to create automatic backup:', error);
|
||||||
|
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupOldBackups() {
|
||||||
|
try {
|
||||||
|
// Get all backup files
|
||||||
|
const files = fs.readdirSync(BACKUP_DIR)
|
||||||
|
.filter(file => file.startsWith('auto-backup-') && file.endsWith('.json'))
|
||||||
|
.map(file => ({
|
||||||
|
name: file,
|
||||||
|
path: path.join(BACKUP_DIR, file),
|
||||||
|
stat: fs.statSync(path.join(BACKUP_DIR, file))
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.stat.mtime.getTime() - a.stat.mtime.getTime()); // Sort by modification time, newest first
|
||||||
|
|
||||||
|
// If we have more than MAX_BACKUPS, delete the oldest ones
|
||||||
|
if (files.length > MAX_BACKUPS) {
|
||||||
|
const filesToDelete = files.slice(MAX_BACKUPS);
|
||||||
|
|
||||||
|
for (const file of filesToDelete) {
|
||||||
|
fs.unlinkSync(file.path);
|
||||||
|
console.log(`🗑️ Deleted old backup: ${file.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🧹 Cleaned up ${filesToDelete.length} old backup(s), keeping ${MAX_BACKUPS} most recent`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cleaning up old backups:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBackupStats() {
|
||||||
|
try {
|
||||||
|
const files = fs.readdirSync(BACKUP_DIR)
|
||||||
|
.filter(file => file.startsWith('auto-backup-') && file.endsWith('.json'));
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalBackups: files.length,
|
||||||
|
backupDirectory: BACKUP_DIR,
|
||||||
|
maxBackups: MAX_BACKUPS
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
totalBackups: 0,
|
||||||
|
backupDirectory: BACKUP_DIR,
|
||||||
|
maxBackups: MAX_BACKUPS,
|
||||||
|
error: 'Could not read backup directory'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startBackupScheduler() {
|
||||||
|
// Schedule backup every 12 hours (at 00:00 and 12:00)
|
||||||
|
const task = cron.schedule('0 0,12 * * *', async () => {
|
||||||
|
console.log('\n🕒 Scheduled backup starting...');
|
||||||
|
await createAutomaticBackup();
|
||||||
|
}, {
|
||||||
|
scheduled: false,
|
||||||
|
timezone: "Europe/Paris"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the scheduler
|
||||||
|
task.start();
|
||||||
|
|
||||||
|
console.log('📅 Automatic backup scheduler started');
|
||||||
|
console.log('⏰ Backups will run every 12 hours (00:00 and 12:00)');
|
||||||
|
console.log(`📁 Backup directory: ${BACKUP_DIR}`);
|
||||||
|
console.log(`📚 Maximum backups kept: ${MAX_BACKUPS}`);
|
||||||
|
|
||||||
|
// Create an initial backup if none exist
|
||||||
|
const stats = getBackupStats();
|
||||||
|
if (stats.totalBackups === 0) {
|
||||||
|
console.log('🎯 No existing backups found, creating initial backup...');
|
||||||
|
setTimeout(() => createAutomaticBackup(), 5000); // Wait 5 seconds for server to be fully ready
|
||||||
|
}
|
||||||
|
|
||||||
|
return task;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import express, { type Request, Response, NextFunction } from "express";
|
import express, { type Request, Response, NextFunction } from "express";
|
||||||
import { registerRoutes } from "./routes";
|
import { registerRoutes } from "./routes";
|
||||||
import { setupVite, serveStatic, log } from "./vite";
|
import { setupVite, serveStatic, log } from "./vite";
|
||||||
|
import { startBackupScheduler } from "./backup-scheduler";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@@ -66,5 +67,8 @@ app.use((req, res, next) => {
|
|||||||
reusePort: true,
|
reusePort: true,
|
||||||
}, () => {
|
}, () => {
|
||||||
log(`serving on port ${port}`);
|
log(`serving on port ${port}`);
|
||||||
|
|
||||||
|
// Start the automatic backup scheduler
|
||||||
|
startBackupScheduler();
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
+33
-2
@@ -5,6 +5,7 @@ import session from "express-session";
|
|||||||
import connectPg from "connect-pg-simple";
|
import connectPg from "connect-pg-simple";
|
||||||
import { loginSchema, insertUserSchema, insertStoreSchema, insertSaleSchema } from "@shared/schema";
|
import { loginSchema, insertUserSchema, insertStoreSchema, insertSaleSchema } from "@shared/schema";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { createAutomaticBackup, getBackupStats } from "./backup-scheduler";
|
||||||
|
|
||||||
declare module 'express-session' {
|
declare module 'express-session' {
|
||||||
interface SessionData {
|
interface SessionData {
|
||||||
@@ -483,13 +484,43 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
message: "Backup imported successfully",
|
message: "Backup imported successfully",
|
||||||
imported: {
|
imported: {
|
||||||
stores: backupData.data.stores.length,
|
stores: backupData.data.stores.length,
|
||||||
users: backupData.data.users.filter(u => u.username !== 'admin').length,
|
users: backupData.data.users.filter((u: any) => u.username !== 'admin').length,
|
||||||
sales: backupData.data.sales.length
|
sales: backupData.data.sales.length
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Import backup error:', error);
|
console.error('Import backup error:', error);
|
||||||
res.status(500).json({ error: "Failed to import backup", details: error.message });
|
res.status(500).json({ error: "Failed to import backup", details: error instanceof Error ? error.message : 'Unknown error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manual backup trigger route
|
||||||
|
app.post('/api/admin/backup/create', requireRole(['admin']), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await createAutomaticBackup();
|
||||||
|
if (result.success) {
|
||||||
|
res.json({
|
||||||
|
message: "Manual backup created successfully",
|
||||||
|
filename: result.filename,
|
||||||
|
stats: result.stats
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ error: "Failed to create backup", details: result.error });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Manual backup error:', error);
|
||||||
|
res.status(500).json({ error: "Failed to create backup" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Backup statistics route
|
||||||
|
app.get('/api/admin/backup/stats', requireRole(['admin']), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const stats = getBackupStats();
|
||||||
|
res.json(stats);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Backup stats error:', error);
|
||||||
|
res.status(500).json({ error: "Failed to get backup statistics" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user