Files
Regisflow/server/routes.ts
T
michaelschal 08e3258a32 Improve photo handling by ensuring images are correctly saved in the database
Removes debug logs and ensures correct mapping of photo data from request body to database columns.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: dec54137-d5a2-4406-8ad4-5f2f9fb110bf
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0715008c-7244-40f4-befc-26c014852236/e6f3a8c1-bdaf-4251-a9d4-9326e97ede4b.jpg
2025-07-11 18:49:16 +00:00

653 lines
22 KiB
TypeScript

import type { Express, Request, Response, NextFunction } from "express";
import { createServer, type Server } from "http";
import { storage } from "./storage";
import session from "express-session";
import connectPg from "connect-pg-simple";
import { loginSchema, insertUserSchema, insertStoreSchema, insertSaleSchema } from "@shared/schema";
import { z } from "zod";
import { createAutomaticBackup, getBackupStats } from "./backup-scheduler";
import { executePurgeManually, getPurgeStats } from "./data-purge";
declare module 'express-session' {
interface SessionData {
userId?: number;
}
}
// Authentication middleware
const requireAuth = (req: Request, res: Response, next: NextFunction) => {
if (!req.session?.userId) {
return res.status(401).json({ error: "Authentication required" });
}
next();
};
// Role-based authorization middleware
const requireRole = (roles: string[]) => {
return async (req: Request, res: Response, next: NextFunction) => {
if (!req.session?.userId) {
return res.status(401).json({ error: "Authentication required" });
}
const user = await storage.getUser(req.session.userId);
if (!user || !roles.includes(user.role)) {
return res.status(403).json({ error: "Insufficient permissions" });
}
req.user = user;
next();
};
};
declare global {
namespace Express {
interface Request {
user?: any;
}
}
}
export async function registerRoutes(app: Express): Promise<Server> {
// Session configuration with PostgreSQL store
const pgStore = connectPg(session);
app.use(session({
store: new pgStore({
conString: process.env.DATABASE_URL,
createTableIfMissing: false,
tableName: 'sessions',
}),
secret: process.env.SESSION_SECRET || 'fireworks-secret-key-2024',
resave: false,
saveUninitialized: false,
cookie: {
secure: false, // Set to true in production with HTTPS
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
// Initialize default admin if needed
const initResult = await storage.initializeDefaults();
if (initResult.defaultAdminCredentials) {
console.log('\n=== DEFAULT ADMIN CREATED ===');
console.log('Username:', initResult.defaultAdminCredentials.username);
console.log('Password:', initResult.defaultAdminCredentials.password);
console.log('Please change this password after first login!');
console.log('==============================\n');
}
// Auth routes
app.post('/api/auth/login', async (req, res) => {
try {
const { username, password } = loginSchema.parse(req.body);
const user = await storage.authenticateUser(username, password);
if (!user) {
return res.status(401).json({ error: "Invalid credentials" });
}
req.session.userId = user.id;
res.json({ user: { ...user, password: undefined } });
} catch (error) {
console.error('Login error:', error);
if (error instanceof z.ZodError) {
return res.status(400).json({ error: "Invalid input", details: error.errors });
}
res.status(500).json({ error: "Login failed" });
}
});
app.get('/api/auth/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
console.error('Logout error:', err);
}
res.redirect('/');
});
});
app.post('/api/auth/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
console.error('Logout error:', err);
return res.status(500).json({ error: "Logout failed" });
}
res.json({ message: "Logged out successfully" });
});
});
// Check if default admin credentials are still in use
app.get('/api/auth/default-credentials-status', async (req, res) => {
try {
const isUsingDefault = await storage.isUsingDefaultPassword();
res.json({ showDefaultCredentials: isUsingDefault });
} catch (error) {
console.error('Default credentials check error:', error);
res.status(500).json({ error: "Failed to check default credentials status" });
}
});
// Health check endpoint for Docker
app.get('/health', async (req, res) => {
try {
// Test de connexion à la base de données
await storage.initializeDefaults();
res.status(200).json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
database: 'connected',
environment: process.env.NODE_ENV || 'development'
});
} catch (error) {
console.error('Health check failed:', error);
res.status(503).json({
status: 'unhealthy',
timestamp: new Date().toISOString(),
database: 'disconnected',
error: error instanceof Error ? error.message : 'Unknown error'
});
}
});
app.get('/api/auth/me', requireAuth, async (req, res) => {
try {
const user = await storage.getUser(req.session.userId!);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
res.json({ ...user, password: undefined });
} catch (error) {
console.error('Get user error:', error);
res.status(500).json({ error: "Failed to get user info" });
}
});
// Store routes for users
app.get('/api/stores', requireAuth, async (req, res) => {
try {
const user = await storage.getUser(req.session.userId!);
if (!user || !user.storeId) {
return res.status(404).json({ error: "User store not found" });
}
const store = await storage.getStore(user.storeId);
if (!store) {
return res.status(404).json({ error: "Store not found" });
}
res.json([store]); // Return as array to match admin endpoint format
} catch (error) {
console.error('Get user stores error:', error);
res.status(500).json({ error: "Failed to get stores" });
}
});
// Sales routes
app.get('/api/sales', requireAuth, async (req, res) => {
try {
const user = await storage.getUser(req.session.userId!);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
const { startDate, endDate, storeId } = req.query;
// Determine which store to query
let targetStoreId: number;
if (storeId) {
// storeId provided in query
targetStoreId = parseInt(storeId as string);
// For non-admin users, verify they can only access their own store
if (user.role !== 'admin' && targetStoreId !== user.storeId) {
return res.status(403).json({ error: "Access denied to this store" });
}
} else {
// No storeId provided - use user's assigned store for non-admin
if (!user.storeId) {
return res.status(400).json({ error: "User has no assigned store" });
}
targetStoreId = user.storeId;
}
const sales = await storage.getSalesByStore(
targetStoreId,
startDate as string,
endDate as string
);
res.json(sales);
} catch (error) {
console.error('Get sales error:', error);
res.status(500).json({ error: "Failed to get sales" });
}
});
app.post('/api/sales', requireAuth, async (req, res) => {
try {
const user = await storage.getUser(req.session.userId!);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
// Determine which store to use for the sale
let targetStoreId: number;
if (user.role === 'admin' && req.body.storeId) {
// Admin can create sales for any store
targetStoreId = req.body.storeId;
} else {
// Non-admin users can only create sales for their assigned store
if (!user.storeId) {
return res.status(400).json({ error: "User has no assigned store" });
}
targetStoreId = user.storeId;
}
// Photo mapping debug (can be removed in production)
// Map frontend camelCase to database snake_case for photos
const mappedBody = {
...req.body,
};
// Add photo fields with proper mapping, only if they exist and are not empty
if (req.body.photoRecto) {
mappedBody.photo_recto = req.body.photoRecto;
}
if (req.body.photoVerso) {
mappedBody.photo_verso = req.body.photoVerso;
}
if (req.body.photoTicket) {
mappedBody.photo_ticket = req.body.photoTicket;
}
// Remove camelCase versions to avoid conflicts
delete mappedBody.photoRecto;
delete mappedBody.photoVerso;
delete mappedBody.photoTicket;
const saleData = insertSaleSchema.parse({
...mappedBody,
storeId: targetStoreId,
userId: user.id
});
const sale = await storage.createSale(saleData);
res.status(201).json(sale);
} catch (error) {
console.error('Create sale error:', error);
if (error instanceof z.ZodError) {
return res.status(400).json({ error: "Invalid input", details: error.errors });
}
res.status(500).json({ error: "Failed to create sale" });
}
});
app.delete('/api/sales/:id', requireRole(['admin', 'manager']), async (req, res) => {
try {
const saleId = parseInt(req.params.id);
await storage.deleteSale(saleId);
res.json({ message: "Sale deleted successfully" });
} catch (error) {
console.error('Delete sale error:', error);
res.status(500).json({ error: "Failed to delete sale" });
}
});
// Admin routes - Users
app.get('/api/admin/users', requireRole(['admin']), async (req, res) => {
try {
const users = await storage.getAllUsers();
const safeUsers = users.map(user => ({ ...user, password: undefined }));
res.json(safeUsers);
} catch (error) {
console.error('Get users error:', error);
res.status(500).json({ error: "Failed to get users" });
}
});
app.post('/api/admin/users', requireRole(['admin']), async (req, res) => {
try {
const userData = insertUserSchema.parse(req.body);
const user = await storage.createUser(userData);
res.status(201).json({ ...user, password: undefined });
} catch (error) {
console.error('Create user error:', error);
if (error instanceof z.ZodError) {
return res.status(400).json({ error: "Invalid input", details: error.errors });
}
res.status(500).json({ error: "Failed to create user" });
}
});
app.put('/api/admin/users/:id', requireRole(['admin']), async (req, res) => {
try {
const userId = parseInt(req.params.id);
const currentUser = await storage.getUser(req.session.userId!);
const userData = insertUserSchema.partial().parse(req.body);
// Protection spécifique pour le rôle seulement
if (userData.role !== undefined) {
// Protection : empêcher un admin de changer son propre rôle par accident
if (userId === currentUser?.id && userData.role !== 'administrator') {
return res.status(400).json({
error: "Vous ne pouvez pas changer votre propre rôle d'administrateur"
});
}
// Protection : ne pas permettre de changer le rôle du dernier administrateur
if (userData.role !== 'administrator') {
const users = await storage.getAllUsers();
const adminUsers = users.filter(u => u.role === 'administrator');
const userToUpdate = users.find(u => u.id === userId);
if (userToUpdate?.role === 'administrator' && adminUsers.length <= 1) {
return res.status(400).json({
error: "Impossible de modifier le rôle du dernier administrateur"
});
}
}
}
const user = await storage.updateUser(userId, userData);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
res.json({ ...user, password: undefined });
} catch (error) {
console.error('Update user error:', error);
if (error instanceof z.ZodError) {
return res.status(400).json({ error: "Invalid input", details: error.errors });
}
res.status(500).json({ error: "Failed to update user" });
}
});
app.delete('/api/admin/users/:id', requireRole(['admin']), async (req, res) => {
try {
const userId = parseInt(req.params.id);
// Don't allow deletion of the last administrator
const users = await storage.getAllUsers();
const adminUsers = users.filter(u => u.role === 'administrator');
const userToDelete = users.find(u => u.id === userId);
if (userToDelete?.role === 'administrator' && adminUsers.length <= 1) {
return res.status(400).json({ error: "Cannot delete the last administrator" });
}
await storage.deleteUser(userId);
res.json({ message: "User deleted successfully" });
} catch (error) {
console.error('Delete user error:', error);
res.status(500).json({ error: "Failed to delete user" });
}
});
// Admin routes - Stores
app.get('/api/admin/stores', requireRole(['admin']), async (req, res) => {
try {
const stores = await storage.getAllStores();
res.json(stores);
} catch (error) {
console.error('Get stores error:', error);
res.status(500).json({ error: "Failed to get stores" });
}
});
app.post('/api/admin/stores', requireRole(['admin']), async (req, res) => {
try {
const storeData = insertStoreSchema.parse(req.body);
const store = await storage.createStore(storeData);
res.status(201).json(store);
} catch (error) {
console.error('Create store error:', error);
if (error instanceof z.ZodError) {
return res.status(400).json({ error: "Invalid input", details: error.errors });
}
res.status(500).json({ error: "Failed to create store" });
}
});
app.put('/api/admin/stores/:id', requireRole(['admin']), async (req, res) => {
try {
const storeId = parseInt(req.params.id);
const storeData = insertStoreSchema.partial().parse(req.body);
const store = await storage.updateStore(storeId, storeData);
if (!store) {
return res.status(404).json({ error: "Store not found" });
}
res.json(store);
} catch (error) {
console.error('Update store error:', error);
if (error instanceof z.ZodError) {
return res.status(400).json({ error: "Invalid input", details: error.errors });
}
res.status(500).json({ error: "Failed to update store" });
}
});
app.delete('/api/admin/stores/:id', requireRole(['admin']), async (req, res) => {
try {
const storeId = parseInt(req.params.id);
// Check if there are users assigned to this store
const users = await storage.getAllUsers();
const usersInStore = users.filter(u => u.storeId === storeId);
if (usersInStore.length > 0) {
return res.status(400).json({
error: "Cannot delete store with assigned users",
details: `${usersInStore.length} user(s) are assigned to this store`
});
}
await storage.deleteStore(storeId);
res.json({ message: "Store deleted successfully" });
} catch (error) {
console.error('Delete store error:', error);
res.status(500).json({ error: "Failed to delete store" });
}
});
// 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: any) => 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 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" });
}
});
// Admin routes - Data Purge
app.get('/api/admin/purge/stats', requireRole(['admin']), async (req, res) => {
try {
const stats = await getPurgeStats();
res.json(stats);
} catch (error) {
console.error('Get purge stats error:', error);
res.status(500).json({ error: "Failed to get purge stats" });
}
});
app.post('/api/admin/purge/execute', requireRole(['admin']), async (req, res) => {
try {
const result = await executePurgeManually();
res.json(result);
} catch (error) {
console.error('Execute purge error:', error);
res.status(500).json({ error: "Failed to execute purge" });
}
});
// Health check endpoint for production monitoring
app.get('/health', (req, res) => {
res.status(200).json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: process.env.NODE_ENV
});
});
const httpServer = createServer(app);
return httpServer;
}