mirror of
https://github.com/R0m1k3/Regisflow.git
synced 2026-10-22 06:38:21 +02:00
Restored to '3891ae828b859a29b86a84154ffef43740610335'
Replit-Restored-To: 3891ae828b
This commit is contained in:
1 parent
2f847dd333
commit
0821b70b7d
8 files changed
+212
-694
No files matched your search
+42
-105
@@ -2,22 +2,10 @@ import cron from 'node-cron';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { storage } from './storage';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const BACKUP_DIR = path.join(process.cwd(), 'backups');
|
||||
const MAX_BACKUPS = 10;
|
||||
|
||||
// Database connection info from environment variables
|
||||
const DB_HOST = process.env.PGHOST || 'localhost';
|
||||
const DB_PORT = process.env.PGPORT || '5432';
|
||||
const DB_NAME = process.env.PGDATABASE || 'postgres';
|
||||
const DB_USER = process.env.PGUSER || 'postgres';
|
||||
const DB_PASSWORD = process.env.PGPASSWORD || '';
|
||||
const DATABASE_URL = process.env.DATABASE_URL;
|
||||
|
||||
// Ensure backup directory exists
|
||||
if (!fs.existsSync(BACKUP_DIR)) {
|
||||
fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
||||
@@ -25,117 +13,66 @@ if (!fs.existsSync(BACKUP_DIR)) {
|
||||
|
||||
export async function createAutomaticBackup() {
|
||||
try {
|
||||
console.log('Creating automatic SQL backup...');
|
||||
console.log('Creating automatic backup...');
|
||||
|
||||
// Create filename with timestamp
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const filename = `auto-backup-${timestamp}.sql`;
|
||||
const filepath = path.join(BACKUP_DIR, filename);
|
||||
|
||||
// Create SQL dump using pg_dump
|
||||
await createSQLBackup(filepath);
|
||||
|
||||
// Get stats for logging
|
||||
// Get all data from the database
|
||||
const users = await storage.getAllUsers();
|
||||
const stores = await storage.getAllStores();
|
||||
let totalSales = 0;
|
||||
|
||||
// Get all sales from all stores
|
||||
const allSales = [];
|
||||
for (const store of stores) {
|
||||
const storeSales = await storage.getSalesByStore(store.id);
|
||||
totalSales += storeSales.length;
|
||||
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 SQL backup created: ${filename}`);
|
||||
console.log(`📊 Backup stats: ${users.length} users, ${stores.length} stores, ${totalSales} sales`);
|
||||
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: {
|
||||
totalUsers: users.length,
|
||||
totalStores: stores.length,
|
||||
totalSales: totalSales,
|
||||
exportedBy: 'system-auto-backup'
|
||||
}
|
||||
};
|
||||
return { success: true, filename, stats: backupData.metadata };
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to create automatic SQL backup:', error);
|
||||
console.error('❌ Failed to create automatic backup:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
||||
}
|
||||
}
|
||||
|
||||
export async function createSQLBackup(filepath: string): Promise<void> {
|
||||
try {
|
||||
// Use pg_dump to create SQL backup
|
||||
let command: string;
|
||||
|
||||
if (DATABASE_URL) {
|
||||
// Use DATABASE_URL if available
|
||||
command = `pg_dump "${DATABASE_URL}" --no-owner --no-privileges --clean --if-exists`;
|
||||
} else {
|
||||
// Use individual connection parameters
|
||||
command = `PGPASSWORD="${DB_PASSWORD}" pg_dump -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" --no-owner --no-privileges --clean --if-exists`;
|
||||
}
|
||||
|
||||
const { stdout, stderr } = await execAsync(command, {
|
||||
maxBuffer: 50 * 1024 * 1024, // 50MB buffer for large databases
|
||||
});
|
||||
|
||||
if (stderr && !stderr.includes('NOTICE')) {
|
||||
console.warn('pg_dump warnings:', stderr);
|
||||
}
|
||||
|
||||
// Write SQL dump to file
|
||||
fs.writeFileSync(filepath, stdout);
|
||||
|
||||
console.log(`SQL backup created: ${path.basename(filepath)}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to create SQL backup:', error);
|
||||
throw new Error(`SQL backup failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function restoreSQLBackup(filepath: string): Promise<void> {
|
||||
try {
|
||||
console.log(`Restoring SQL backup from: ${path.basename(filepath)}`);
|
||||
|
||||
// Read SQL file
|
||||
const sqlContent = fs.readFileSync(filepath, 'utf8');
|
||||
|
||||
// Use psql to restore the backup
|
||||
let command: string;
|
||||
|
||||
if (DATABASE_URL) {
|
||||
// Use DATABASE_URL if available
|
||||
command = `psql "${DATABASE_URL}"`;
|
||||
} else {
|
||||
// Use individual connection parameters
|
||||
command = `PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}"`;
|
||||
}
|
||||
|
||||
const { stderr } = await execAsync(command, {
|
||||
input: sqlContent,
|
||||
maxBuffer: 50 * 1024 * 1024, // 50MB buffer for large databases
|
||||
});
|
||||
|
||||
if (stderr && !stderr.includes('NOTICE')) {
|
||||
console.warn('psql warnings:', stderr);
|
||||
}
|
||||
|
||||
console.log(`✅ SQL backup restored successfully from: ${path.basename(filepath)}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to restore SQL backup:', error);
|
||||
throw new Error(`SQL restore failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupOldBackups() {
|
||||
try {
|
||||
// Get all backup files (now SQL files)
|
||||
// Get all backup files
|
||||
const files = fs.readdirSync(BACKUP_DIR)
|
||||
.filter(file => file.startsWith('auto-backup-') && file.endsWith('.sql'))
|
||||
.filter(file => file.startsWith('auto-backup-') && file.endsWith('.json'))
|
||||
.map(file => ({
|
||||
name: file,
|
||||
path: path.join(BACKUP_DIR, file),
|
||||
@@ -162,7 +99,7 @@ async function cleanupOldBackups() {
|
||||
export function getBackupStats() {
|
||||
try {
|
||||
const files = fs.readdirSync(BACKUP_DIR)
|
||||
.filter(file => file.startsWith('auto-backup-') && file.endsWith('.sql'));
|
||||
.filter(file => file.startsWith('auto-backup-') && file.endsWith('.json'));
|
||||
|
||||
return {
|
||||
totalBackups: files.length,
|
||||
|
||||
+118
-46
@@ -5,9 +5,7 @@ import session from "express-session";
|
||||
import connectPg from "connect-pg-simple";
|
||||
import { loginSchema, insertUserSchema, insertStoreSchema, insertSaleSchema } from "@shared/schema";
|
||||
import { z } from "zod";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { createAutomaticBackup, getBackupStats, createSQLBackup, restoreSQLBackup } from "./backup-scheduler";
|
||||
import { createAutomaticBackup, getBackupStats } from "./backup-scheduler";
|
||||
import { executePurgeManually, getPurgeStats } from "./data-purge";
|
||||
|
||||
declare module 'express-session' {
|
||||
@@ -466,65 +464,139 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// SQL Backup routes
|
||||
// Backup routes
|
||||
app.get('/api/admin/backup/export', requireRole(['admin']), async (req, res) => {
|
||||
try {
|
||||
// Create a temporary SQL backup
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const filename = `manual-backup-${timestamp}.sql`;
|
||||
const filepath = path.join(process.cwd(), 'backups', filename);
|
||||
|
||||
await createSQLBackup(filepath);
|
||||
|
||||
// Send the SQL file
|
||||
res.setHeader('Content-Type', 'application/sql');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
|
||||
const sqlContent = fs.readFileSync(filepath, 'utf8');
|
||||
res.send(sqlContent);
|
||||
|
||||
// Optionally delete the temporary file after sending
|
||||
// fs.unlinkSync(filepath);
|
||||
// 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 SQL backup error:', error);
|
||||
res.status(500).json({ error: "Failed to export SQL backup" });
|
||||
console.error('Export backup error:', error);
|
||||
res.status(500).json({ error: "Failed to export backup" });
|
||||
}
|
||||
});
|
||||
|
||||
// SQL Backup import route (receives SQL file content)
|
||||
app.post('/api/admin/backup/import', requireRole(['admin']), async (req, res) => {
|
||||
try {
|
||||
const { sqlContent } = req.body;
|
||||
const backupData = req.body;
|
||||
|
||||
if (!sqlContent || typeof sqlContent !== 'string') {
|
||||
return res.status(400).json({ error: "SQL content is required" });
|
||||
// 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" });
|
||||
}
|
||||
|
||||
// Create a temporary file for the SQL content
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const filename = `restore-${timestamp}.sql`;
|
||||
const filepath = path.join(process.cwd(), 'backups', filename);
|
||||
|
||||
// Write SQL content to temporary file
|
||||
fs.writeFileSync(filepath, sqlContent);
|
||||
|
||||
// Restore the backup
|
||||
await restoreSQLBackup(filepath);
|
||||
|
||||
// Clean up temporary file
|
||||
fs.unlinkSync(filepath);
|
||||
// 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: "SQL backup restored successfully",
|
||||
timestamp: new Date().toISOString()
|
||||
message: "Backup imported successfully",
|
||||
imported: {
|
||||
stores: backupData.data.stores.length,
|
||||
users: backupData.data.users.filter((u: any) => u.username !== 'admin').length,
|
||||
sales: backupData.data.sales.length
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Import SQL backup error:', error);
|
||||
res.status(500).json({
|
||||
error: "Failed to restore SQL backup",
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
console.error('Import backup error:', error);
|
||||
res.status(500).json({ error: "Failed to import backup", details: error instanceof Error ? error.message : 'Unknown error' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user