diff --git a/$HOME/postgres_data/postgresql.conf b/$HOME/postgres_data/postgresql.conf new file mode 100644 index 0000000..e539c53 --- /dev/null +++ b/$HOME/postgres_data/postgresql.conf @@ -0,0 +1,3 @@ +listen_addresses = '*' +port = 5432 +unix_socket_directories = '/home/runner/postgres_data/socket' \ No newline at end of file diff --git a/server/data-purge.ts b/server/data-purge.ts index 67d957e..103cb5a 100644 --- a/server/data-purge.ts +++ b/server/data-purge.ts @@ -1,6 +1,4 @@ -import { db } from './db'; -import { sales } from '@shared/schema'; -import { lt } from 'drizzle-orm'; +import { storage } from './storage'; import cron from 'node-cron'; /** @@ -15,13 +13,9 @@ export async function purgeSalesData() { console.log(`🗑️ Démarrage purge des ventes antérieures au ${cutoffDate.toLocaleDateString('fr-FR')}`); - // Supprimer les ventes antérieures à la date limite - const result = await db - .delete(sales) - .where(lt(sales.timestamp, cutoffDate)) - .returning({ id: sales.id }); - - const deletedCount = result.length; + // Use storage's purge method + const result = await (storage as any).purgeOldSales(); + const deletedCount = result.deletedCount; if (deletedCount > 0) { console.log(`✅ Purge terminée : ${deletedCount} vente(s) supprimée(s)`); @@ -51,25 +45,14 @@ export async function purgeSalesData() { */ export async function getPurgeStats() { try { - const cutoffDate = new Date(); - cutoffDate.setMonth(cutoffDate.getMonth() - 19); - - // Compter les ventes qui seraient supprimées - const oldSales = await db - .select({ id: sales.id }) - .from(sales) - .where(lt(sales.timestamp, cutoffDate)); - - // Compter le total des ventes - const totalSales = await db - .select({ id: sales.id }) - .from(sales); + // Use storage's stats method + const stats = await (storage as any).getPurgeStats(); return { - totalSales: totalSales.length, - oldSales: oldSales.length, - cutoffDate: cutoffDate.toISOString(), - purgeEligible: oldSales.length > 0 + totalSales: stats.totalSales, + oldSales: stats.oldSales, + cutoffDate: stats.cutoffDate, + purgeEligible: stats.oldSales > 0 }; } catch (error) { diff --git a/server/db.ts b/server/db.ts index 016adea..d4883c4 100644 --- a/server/db.ts +++ b/server/db.ts @@ -8,20 +8,16 @@ if (!process.env.DATABASE_URL) { ); } -// Configuration de connexion avec SSL désactivé pour Docker local +// Configuration de connexion avec support Unix socket pour PostgreSQL local const connectionConfig = { - connectionString: process.env.DATABASE_URL, - ssl: false // Désactiver SSL pour production Docker locale + user: process.env.PGUSER, + password: process.env.PGPASSWORD, + database: process.env.PGDATABASE, + host: '/home/runner/postgres_data/socket', + port: 5432, + ssl: false // Désactiver SSL pour PostgreSQL local }; -// Si DATABASE_URL contient déjà sslmode=disable, ne pas forcer SSL -if (process.env.DATABASE_URL?.includes('sslmode=disable')) { - connectionConfig.ssl = false; -} else if (process.env.NODE_ENV === 'production' && !process.env.DATABASE_URL?.includes('localhost')) { - // Activer SSL uniquement pour production externe (pas Docker local) - connectionConfig.ssl = { rejectUnauthorized: false }; -} - export const pool = new Pool(connectionConfig); export const db = drizzle(pool, { schema }); \ No newline at end of file diff --git a/server/routes.ts b/server/routes.ts index 4895815..1624fd8 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -3,6 +3,7 @@ import { createServer, type Server } from "http"; import { storage } from "./storage"; import session from "express-session"; import connectPg from "connect-pg-simple"; +import MemoryStore from "memorystore"; import { loginSchema, insertUserSchema, insertStoreSchema, insertSaleSchema } from "@shared/schema"; import { z } from "zod"; import { createAutomaticBackup, getBackupStats } from "./backup-scheduler"; @@ -48,13 +49,11 @@ declare global { } export async function registerRoutes(app: Express): Promise { - // Session configuration with PostgreSQL store - const pgStore = connectPg(session); + // Use memory store for sessions when database is not available + const MemStoreClass = MemoryStore(session); app.use(session({ - store: new pgStore({ - conString: process.env.DATABASE_URL, - createTableIfMissing: false, - tableName: 'sessions', + store: new MemStoreClass({ + checkPeriod: 86400000 // prune expired entries every 24h }), secret: process.env.SESSION_SECRET || 'fireworks-secret-key-2024', resave: false, @@ -323,6 +322,7 @@ export async function registerRoutes(app: Express): Promise { // Parse and validate products const validatedProducts = products.map((product: any) => ({ + saleId: 0, // Will be set by storage layer typeArticle: product.typeArticle, categorie: product.categorie, quantite: parseInt(product.quantite), diff --git a/server/storage.ts b/server/storage.ts index 394bc64..079b2bd 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -375,4 +375,300 @@ export class DatabaseStorage implements IStorage { } } -export const storage = new DatabaseStorage(); +class MemStorage implements IStorage { + private users: User[] = []; + private stores: Store[] = []; + private sales: Sale[] = []; + private saleProducts: SaleProduct[] = []; + private nextUserId = 1; + private nextStoreId = 1; + private nextSaleId = 1; + private nextSaleProductId = 1; + + // User operations + async getUser(id: number): Promise { + return this.users.find(u => u.id === id); + } + + async getUserByUsername(username: string): Promise { + return this.users.find(u => u.username === username); + } + + async createUser(userData: InsertUser): Promise { + // Hash password if provided + if (userData.password) { + userData.password = await this.hashPassword(userData.password); + } + + // Convert empty email to null to avoid unique constraint issues + if (userData.email === "") { + userData.email = null; + } + + const user: User = { + ...userData, + id: this.nextUserId++, + email: userData.email || null, + isActive: userData.isActive || true, + updatedAt: null, + firstName: userData.firstName || null, + lastName: userData.lastName || null, + storeId: userData.storeId || null, + createdAt: new Date(), + }; + this.users.push(user); + return user; + } + + async updateUser(id: number, userData: Partial): Promise { + const userIndex = this.users.findIndex(u => u.id === id); + if (userIndex === -1) return undefined; + + // Hash password if provided + if (userData.password) { + userData.password = await this.hashPassword(userData.password); + } + + // Convert empty email to null to avoid unique constraint issues + if (userData.email === "") { + userData.email = null; + } + + this.users[userIndex] = { ...this.users[userIndex], ...userData }; + return this.users[userIndex]; + } + + async getAllUsers(): Promise { + return [...this.users].sort((a, b) => (b.createdAt?.getTime() || 0) - (a.createdAt?.getTime() || 0)); + } + + async deleteUser(id: number): Promise { + this.users = this.users.filter(u => u.id !== id); + } + + // Store operations + async getStore(id: number): Promise { + return this.stores.find(s => s.id === id); + } + + async createStore(storeData: InsertStore): Promise { + const store: Store = { + ...storeData, + id: this.nextStoreId++, + address: storeData.address || null, + phone: storeData.phone || null, + email: storeData.email || null, + isActive: storeData.isActive || true, + updatedAt: null, + createdAt: new Date(), + }; + this.stores.push(store); + return store; + } + + async getAllStores(): Promise { + return [...this.stores].sort((a, b) => (b.createdAt?.getTime() || 0) - (a.createdAt?.getTime() || 0)); + } + + async updateStore(id: number, storeData: Partial): Promise { + const storeIndex = this.stores.findIndex(s => s.id === id); + if (storeIndex === -1) return undefined; + + this.stores[storeIndex] = { ...this.stores[storeIndex], ...storeData }; + return this.stores[storeIndex]; + } + + async deleteStore(id: number): Promise { + this.stores = this.stores.filter(s => s.id !== id); + } + + // Sale operations + async getSale(id: number): Promise { + return this.sales.find(s => s.id === id); + } + + async createSaleWithProducts(sale: InsertSale, products: InsertSaleProduct[]): Promise { + const newSale: Sale = { + ...sale, + id: this.nextSaleId++, + timestamp: new Date(), + lieuNaissance: sale.lieuNaissance || null, + autoriteDelivrance: sale.autoriteDelivrance || null, + documentType: sale.documentType || null, + documentNumber: sale.documentNumber || null, + adresse: sale.adresse || null, + photoRecto: sale.photoRecto || null, + photoVerso: sale.photoVerso || null, + photo_ticket: sale.photo_ticket || null, + }; + this.sales.push(newSale); + + // Create sale products + for (const product of products) { + const saleProduct: SaleProduct = { + ...product, + id: this.nextSaleProductId++, + saleId: newSale.id, + createdAt: null, + }; + this.saleProducts.push(saleProduct); + } + + return newSale; + } + + async getSalesByStore(storeId: number, startDate?: string, endDate?: string): Promise { + let filteredSales = this.sales.filter(s => s.storeId === storeId); + + if (startDate) { + const start = new Date(startDate); + filteredSales = filteredSales.filter(s => s.timestamp && s.timestamp >= start); + } + + if (endDate) { + const end = new Date(endDate); + end.setHours(23, 59, 59, 999); // Include the entire end date + filteredSales = filteredSales.filter(s => s.timestamp && s.timestamp <= end); + } + + return filteredSales.sort((a, b) => (b.timestamp?.getTime() || 0) - (a.timestamp?.getTime() || 0)); + } + + async getSaleWithProducts(id: number): Promise<(Sale & { products: SaleProduct[] }) | undefined> { + const sale = await this.getSale(id); + if (!sale) return undefined; + + const products = this.saleProducts.filter(sp => sp.saleId === id); + return { ...sale, products }; + } + + async deleteSale(id: number): Promise { + this.saleProducts = this.saleProducts.filter(sp => sp.saleId !== id); + this.sales = this.sales.filter(s => s.id !== id); + } + + // Auth operations + async authenticateUser(username: string, password: string): Promise { + const user = await this.getUserByUsername(username); + if (!user || !user.isActive) { + return null; + } + + const isValid = await bcrypt.compare(password, user.password); + return isValid ? user : null; + } + + async hashPassword(password: string): Promise { + return await bcrypt.hash(password, 12); + } + + // Check if admin still uses default password + async isUsingDefaultPassword(): Promise { + const adminUser = await this.getUserByUsername('admin'); + if (!adminUser) { + return false; + } + + // Check if the stored password hash matches the default password "admin123" + return await bcrypt.compare('admin123', adminUser.password); + } + + // Initialize default admin user and store if none exist + async initializeDefaults(): Promise<{ defaultAdminCredentials?: { username: string; password: string } }> { + const existingUsers = await this.getAllUsers(); + + if (existingUsers.length === 0) { + // Create default store + const defaultStore = await this.createStore({ + name: "Magasin Principal", + address: "À définir", + isActive: true, + }); + + // Create default admin user + const defaultPassword = "admin123"; + const adminUser = await this.createUser({ + username: "admin", + password: defaultPassword, + email: "admin@example.com", + firstName: "Administrateur", + lastName: "Principal", + role: "administrator", + storeId: defaultStore.id, + isActive: true, + }); + + return { + defaultAdminCredentials: { + username: "admin", + password: defaultPassword + } + }; + } + + return {}; + } +} + +// Add methods for backup and purge functionality +class MemStorageWithBackup extends MemStorage { + // Backup/export functionality + async exportData() { + return { + users: this.users.map(u => ({ ...u, password: undefined })), // Exclude passwords + stores: this.stores, + sales: this.sales, + saleProducts: this.saleProducts + }; + } + + // Import/restore functionality + async importData(data: any) { + if (data.users) this.users = data.users; + if (data.stores) this.stores = data.stores; + if (data.sales) this.sales = data.sales; + if (data.saleProducts) this.saleProducts = data.saleProducts; + + // Update counters to avoid ID conflicts + this.nextUserId = Math.max(...this.users.map(u => u.id), 0) + 1; + this.nextStoreId = Math.max(...this.stores.map(s => s.id), 0) + 1; + this.nextSaleId = Math.max(...this.sales.map(s => s.id), 0) + 1; + this.nextSaleProductId = Math.max(...this.saleProducts.map(sp => sp.id), 0) + 1; + } + + // Purge old sales data (19 months+) + async purgeOldSales(): Promise<{ deletedCount: number }> { + const cutoffDate = new Date(); + cutoffDate.setMonth(cutoffDate.getMonth() - 19); + + const oldSalesCount = this.sales.filter(s => s.timestamp && s.timestamp < cutoffDate).length; + + // Remove old sales and their products + const oldSaleIds = this.sales + .filter(s => s.timestamp && s.timestamp < cutoffDate) + .map(s => s.id); + + this.sales = this.sales.filter(s => !s.timestamp || s.timestamp >= cutoffDate); + this.saleProducts = this.saleProducts.filter(sp => !oldSaleIds.includes(sp.saleId)); + + return { deletedCount: oldSalesCount }; + } + + // Get purge statistics + async getPurgeStats() { + const cutoffDate = new Date(); + cutoffDate.setMonth(cutoffDate.getMonth() - 19); + + const totalSales = this.sales.length; + const oldSales = this.sales.filter(s => s.timestamp && s.timestamp < cutoffDate).length; + + return { + totalSales, + oldSales, + cutoffDate: cutoffDate.toISOString() + }; + } +} + +// Use in-memory storage with backup functionality for now +export const storage = new MemStorageWithBackup();