VigilanzaTurni/server/routes.ts
marco370 0203c9694d Add vehicle management and improve user authentication and management
Introduce a new section for vehicle management, enhance user authentication with bcrypt, and implement CRUD operations for users.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 42d8028a-fa71-4ec2-938c-e43eedf7df01
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/42d8028a-fa71-4ec2-938c-e43eedf7df01/GNrPM6a
2025-10-16 17:41:22 +00:00

805 lines
27 KiB
TypeScript

import type { Express } from "express";
import { createServer, type Server } from "http";
import { storage } from "./storage";
import { setupAuth as setupReplitAuth, isAuthenticated as isAuthenticatedReplit } from "./replitAuth";
import { setupLocalAuth, isAuthenticated as isAuthenticatedLocal } from "./localAuth";
import { db } from "./db";
import { guards, certifications, sites, shifts, shiftAssignments, users, insertShiftSchema } from "@shared/schema";
import { eq } from "drizzle-orm";
import { differenceInDays } from "date-fns";
// Determina quale sistema auth usare basandosi sull'ambiente
const USE_LOCAL_AUTH = process.env.DOMAIN === "vt.alfacom.it" || !process.env.REPLIT_DOMAINS;
// Helper per estrarre user ID in modo compatibile
function getUserId(req: any): string {
if (USE_LOCAL_AUTH) {
return req.user?.id || "";
} else {
return req.user?.claims?.sub || "";
}
}
// Usa il middleware auth appropriato
const isAuthenticated = USE_LOCAL_AUTH ? isAuthenticatedLocal : isAuthenticatedReplit;
export async function registerRoutes(app: Express): Promise<Server> {
// Setup auth system appropriato
if (USE_LOCAL_AUTH) {
console.log("🔐 Usando Local Auth (vt.alfacom.it)");
await setupLocalAuth(app);
} else {
console.log("🔐 Usando Replit OIDC Auth");
await setupReplitAuth(app);
}
// ============= AUTH ROUTES =============
app.get("/api/auth/user", isAuthenticated, async (req: any, res) => {
try {
const userId = getUserId(req);
const user = await storage.getUser(userId);
res.json(user);
} catch (error) {
console.error("Error fetching user:", error);
res.status(500).json({ message: "Failed to fetch user" });
}
});
// ============= USER MANAGEMENT ROUTES =============
app.get("/api/users", isAuthenticated, async (req: any, res) => {
try {
const currentUserId = getUserId(req);
const currentUser = await storage.getUser(currentUserId);
// Only admins can view all users
if (currentUser?.role !== "admin") {
return res.status(403).json({ message: "Forbidden: Admin access required" });
}
const allUsers = await storage.getAllUsers();
res.json(allUsers);
} catch (error) {
console.error("Error fetching users:", error);
res.status(500).json({ message: "Failed to fetch users" });
}
});
app.post("/api/users", isAuthenticated, async (req: any, res) => {
try {
const currentUserId = getUserId(req);
const currentUser = await storage.getUser(currentUserId);
// Only admins can create users
if (currentUser?.role !== "admin") {
return res.status(403).json({ message: "Forbidden: Admin access required" });
}
const { email, firstName, lastName, password, role } = req.body;
if (!email || !firstName || !lastName || !password) {
return res.status(400).json({ message: "Missing required fields" });
}
// Hash password
const bcrypt = await import("bcrypt");
const passwordHash = await bcrypt.hash(password, 10);
// Generate UUID
const crypto = await import("crypto");
const userId = crypto.randomUUID();
const newUser = await storage.upsertUser({
id: userId,
email,
firstName,
lastName,
profileImageUrl: null,
passwordHash,
});
// Set role if provided
if (role) {
await storage.updateUserRole(newUser.id, role);
}
res.json(newUser);
} catch (error: any) {
console.error("Error creating user:", error);
if (error.code === '23505') { // Unique violation
return res.status(409).json({ message: "Email già esistente" });
}
res.status(500).json({ message: "Failed to create user" });
}
});
app.patch("/api/users/:id", isAuthenticated, async (req: any, res) => {
try {
const currentUserId = getUserId(req);
const currentUser = await storage.getUser(currentUserId);
// Only admins can update user roles
if (currentUser?.role !== "admin") {
return res.status(403).json({ message: "Forbidden: Admin access required" });
}
// Prevent admins from changing their own role
if (req.params.id === currentUserId) {
return res.status(403).json({ message: "Cannot change your own role" });
}
const { role } = req.body;
if (!role || !["admin", "coordinator", "guard", "client"].includes(role)) {
return res.status(400).json({ message: "Invalid role" });
}
const updated = await storage.updateUserRole(req.params.id, role);
if (!updated) {
return res.status(404).json({ message: "User not found" });
}
res.json(updated);
} catch (error) {
console.error("Error updating user role:", error);
res.status(500).json({ message: "Failed to update user role" });
}
});
app.put("/api/users/:id", isAuthenticated, async (req: any, res) => {
try {
const currentUserId = getUserId(req);
const currentUser = await storage.getUser(currentUserId);
// Only admins can update users
if (currentUser?.role !== "admin") {
return res.status(403).json({ message: "Forbidden: Admin access required" });
}
const { email, firstName, lastName, password, role } = req.body;
const existingUser = await storage.getUser(req.params.id);
if (!existingUser) {
return res.status(404).json({ message: "User not found" });
}
// Prepare update data
const updateData: any = {
id: req.params.id,
email: email || existingUser.email,
firstName: firstName || existingUser.firstName,
lastName: lastName || existingUser.lastName,
profileImageUrl: existingUser.profileImageUrl,
};
// Hash new password if provided
if (password) {
const bcrypt = await import("bcrypt");
updateData.passwordHash = await bcrypt.hash(password, 10);
}
const updated = await storage.upsertUser(updateData);
// Update role if provided and not changing own role
if (role && req.params.id !== currentUserId) {
await storage.updateUserRole(req.params.id, role);
}
res.json(updated);
} catch (error: any) {
console.error("Error updating user:", error);
if (error.code === '23505') {
return res.status(409).json({ message: "Email già esistente" });
}
res.status(500).json({ message: "Failed to update user" });
}
});
app.delete("/api/users/:id", isAuthenticated, async (req: any, res) => {
try {
const currentUserId = getUserId(req);
const currentUser = await storage.getUser(currentUserId);
// Only admins can delete users
if (currentUser?.role !== "admin") {
return res.status(403).json({ message: "Forbidden: Admin access required" });
}
// Prevent admins from deleting themselves
if (req.params.id === currentUserId) {
return res.status(403).json({ message: "Cannot delete your own account" });
}
await storage.deleteUser(req.params.id);
res.json({ success: true });
} catch (error) {
console.error("Error deleting user:", error);
res.status(500).json({ message: "Failed to delete user" });
}
});
// ============= GUARD ROUTES =============
app.get("/api/guards", isAuthenticated, async (req, res) => {
try {
const allGuards = await storage.getAllGuards();
// Fetch related data for each guard
const guardsWithDetails = await Promise.all(
allGuards.map(async (guard) => {
const certs = await storage.getCertificationsByGuard(guard.id);
const user = guard.userId ? await storage.getUser(guard.userId) : undefined;
// Update certification status based on expiry date
for (const cert of certs) {
const daysUntilExpiry = differenceInDays(new Date(cert.expiryDate), new Date());
let newStatus: "valid" | "expiring_soon" | "expired" = "valid";
if (daysUntilExpiry < 0) {
newStatus = "expired";
} else if (daysUntilExpiry <= 30) {
newStatus = "expiring_soon";
}
if (cert.status !== newStatus) {
await storage.updateCertificationStatus(cert.id, newStatus);
cert.status = newStatus;
}
}
return {
...guard,
certifications: certs,
user,
};
})
);
res.json(guardsWithDetails);
} catch (error) {
console.error("Error fetching guards:", error);
res.status(500).json({ message: "Failed to fetch guards" });
}
});
app.post("/api/guards", isAuthenticated, async (req, res) => {
try {
const guard = await storage.createGuard(req.body);
res.json(guard);
} catch (error) {
console.error("Error creating guard:", error);
res.status(500).json({ message: "Failed to create guard" });
}
});
app.patch("/api/guards/:id", isAuthenticated, async (req, res) => {
try {
const updated = await storage.updateGuard(req.params.id, req.body);
if (!updated) {
return res.status(404).json({ message: "Guard not found" });
}
res.json(updated);
} catch (error) {
console.error("Error updating guard:", error);
res.status(500).json({ message: "Failed to update guard" });
}
});
app.delete("/api/guards/:id", isAuthenticated, async (req, res) => {
try {
const deleted = await storage.deleteGuard(req.params.id);
if (!deleted) {
return res.status(404).json({ message: "Guard not found" });
}
res.json({ success: true });
} catch (error) {
console.error("Error deleting guard:", error);
res.status(500).json({ message: "Failed to delete guard" });
}
});
// ============= VEHICLE ROUTES =============
app.get("/api/vehicles", isAuthenticated, async (req, res) => {
try {
const vehicles = await storage.getAllVehicles();
res.json(vehicles);
} catch (error) {
console.error("Error fetching vehicles:", error);
res.status(500).json({ message: "Failed to fetch vehicles" });
}
});
app.post("/api/vehicles", isAuthenticated, async (req, res) => {
try {
const vehicle = await storage.createVehicle(req.body);
res.json(vehicle);
} catch (error: any) {
console.error("Error creating vehicle:", error);
if (error.code === '23505') {
return res.status(409).json({ message: "Targa già esistente" });
}
res.status(500).json({ message: "Failed to create vehicle" });
}
});
app.patch("/api/vehicles/:id", isAuthenticated, async (req, res) => {
try {
const updated = await storage.updateVehicle(req.params.id, req.body);
if (!updated) {
return res.status(404).json({ message: "Vehicle not found" });
}
res.json(updated);
} catch (error: any) {
console.error("Error updating vehicle:", error);
if (error.code === '23505') {
return res.status(409).json({ message: "Targa già esistente" });
}
res.status(500).json({ message: "Failed to update vehicle" });
}
});
app.delete("/api/vehicles/:id", isAuthenticated, async (req, res) => {
try {
const deleted = await storage.deleteVehicle(req.params.id);
if (!deleted) {
return res.status(404).json({ message: "Vehicle not found" });
}
res.json({ success: true });
} catch (error) {
console.error("Error deleting vehicle:", error);
res.status(500).json({ message: "Failed to delete vehicle" });
}
});
// ============= CERTIFICATION ROUTES =============
app.post("/api/certifications", isAuthenticated, async (req, res) => {
try {
const cert = await storage.createCertification(req.body);
res.json(cert);
} catch (error) {
console.error("Error creating certification:", error);
res.status(500).json({ message: "Failed to create certification" });
}
});
// ============= SITE ROUTES =============
app.get("/api/sites", isAuthenticated, async (req, res) => {
try {
const allSites = await storage.getAllSites();
res.json(allSites);
} catch (error) {
console.error("Error fetching sites:", error);
res.status(500).json({ message: "Failed to fetch sites" });
}
});
app.post("/api/sites", isAuthenticated, async (req, res) => {
try {
const site = await storage.createSite(req.body);
res.json(site);
} catch (error) {
console.error("Error creating site:", error);
res.status(500).json({ message: "Failed to create site" });
}
});
app.patch("/api/sites/:id", isAuthenticated, async (req, res) => {
try {
const updated = await storage.updateSite(req.params.id, req.body);
if (!updated) {
return res.status(404).json({ message: "Site not found" });
}
res.json(updated);
} catch (error) {
console.error("Error updating site:", error);
res.status(500).json({ message: "Failed to update site" });
}
});
app.delete("/api/sites/:id", isAuthenticated, async (req, res) => {
try {
const deleted = await storage.deleteSite(req.params.id);
if (!deleted) {
return res.status(404).json({ message: "Site not found" });
}
res.json({ success: true });
} catch (error) {
console.error("Error deleting site:", error);
res.status(500).json({ message: "Failed to delete site" });
}
});
// ============= SHIFT ROUTES =============
app.get("/api/shifts", isAuthenticated, async (req, res) => {
try {
const allShifts = await storage.getAllShifts();
// Fetch related data for each shift
const shiftsWithDetails = await Promise.all(
allShifts.map(async (shift) => {
const site = await storage.getSite(shift.siteId);
const assignments = await storage.getShiftAssignments(shift.id);
// Fetch guard details for each assignment
const assignmentsWithGuards = await Promise.all(
assignments.map(async (assignment) => {
const guard = await storage.getGuard(assignment.guardId);
const certs = guard ? await storage.getCertificationsByGuard(guard.id) : [];
const user = guard?.userId ? await storage.getUser(guard.userId) : undefined;
return {
...assignment,
guard: guard ? {
...guard,
certifications: certs,
user,
} : null,
};
})
);
return {
...shift,
site: site!,
assignments: assignmentsWithGuards.filter(a => a.guard !== null),
};
})
);
res.json(shiftsWithDetails);
} catch (error) {
console.error("Error fetching shifts:", error);
res.status(500).json({ message: "Failed to fetch shifts" });
}
});
app.get("/api/shifts/active", isAuthenticated, async (req, res) => {
try {
const activeShifts = await storage.getActiveShifts();
// Fetch related data for each shift
const shiftsWithDetails = await Promise.all(
activeShifts.map(async (shift) => {
const site = await storage.getSite(shift.siteId);
const assignments = await storage.getShiftAssignments(shift.id);
// Fetch guard details for each assignment
const assignmentsWithGuards = await Promise.all(
assignments.map(async (assignment) => {
const guard = await storage.getGuard(assignment.guardId);
const certs = guard ? await storage.getCertificationsByGuard(guard.id) : [];
const user = guard?.userId ? await storage.getUser(guard.userId) : undefined;
return {
...assignment,
guard: guard ? {
...guard,
certifications: certs,
user,
} : null,
};
})
);
return {
...shift,
site: site!,
assignments: assignmentsWithGuards.filter(a => a.guard !== null),
};
})
);
res.json(shiftsWithDetails);
} catch (error) {
console.error("Error fetching active shifts:", error);
res.status(500).json({ message: "Failed to fetch active shifts" });
}
});
app.post("/api/shifts", isAuthenticated, async (req, res) => {
try {
// Validate that required fields are present and dates are valid strings
if (!req.body.siteId || !req.body.startTime || !req.body.endTime) {
return res.status(400).json({ message: "Missing required fields" });
}
// Convert and validate dates
const startTime = new Date(req.body.startTime);
const endTime = new Date(req.body.endTime);
if (isNaN(startTime.getTime()) || isNaN(endTime.getTime())) {
return res.status(400).json({ message: "Invalid date format" });
}
// Validate and transform the request body
const validatedData = insertShiftSchema.parse({
siteId: req.body.siteId,
startTime,
endTime,
status: req.body.status || "planned",
});
const shift = await storage.createShift(validatedData);
res.json(shift);
} catch (error) {
console.error("Error creating shift:", error);
res.status(500).json({ message: "Failed to create shift" });
}
});
app.patch("/api/shifts/:id/status", isAuthenticated, async (req, res) => {
try {
await storage.updateShiftStatus(req.params.id, req.body.status);
res.json({ success: true });
} catch (error) {
console.error("Error updating shift status:", error);
res.status(500).json({ message: "Failed to update shift status" });
}
});
app.patch("/api/shifts/:id", isAuthenticated, async (req, res) => {
try {
const { startTime: startTimeStr, endTime: endTimeStr, ...rest } = req.body;
const updateData: any = { ...rest };
if (startTimeStr) {
const startTime = new Date(startTimeStr);
if (isNaN(startTime.getTime())) {
return res.status(400).json({ message: "Invalid start time format" });
}
updateData.startTime = startTime;
}
if (endTimeStr) {
const endTime = new Date(endTimeStr);
if (isNaN(endTime.getTime())) {
return res.status(400).json({ message: "Invalid end time format" });
}
updateData.endTime = endTime;
}
const updated = await storage.updateShift(req.params.id, updateData);
if (!updated) {
return res.status(404).json({ message: "Shift not found" });
}
res.json(updated);
} catch (error) {
console.error("Error updating shift:", error);
res.status(500).json({ message: "Failed to update shift" });
}
});
app.delete("/api/shifts/:id", isAuthenticated, async (req, res) => {
try {
const deleted = await storage.deleteShift(req.params.id);
if (!deleted) {
return res.status(404).json({ message: "Shift not found" });
}
res.json({ success: true });
} catch (error) {
console.error("Error deleting shift:", error);
res.status(500).json({ message: "Failed to delete shift" });
}
});
// ============= SHIFT ASSIGNMENT ROUTES =============
app.post("/api/shift-assignments", isAuthenticated, async (req, res) => {
try {
const assignment = await storage.createShiftAssignment(req.body);
res.json(assignment);
} catch (error) {
console.error("Error creating shift assignment:", error);
res.status(500).json({ message: "Failed to create shift assignment" });
}
});
app.delete("/api/shift-assignments/:id", isAuthenticated, async (req, res) => {
try {
await storage.deleteShiftAssignment(req.params.id);
res.json({ success: true });
} catch (error) {
console.error("Error deleting shift assignment:", error);
res.status(500).json({ message: "Failed to delete shift assignment" });
}
});
// ============= NOTIFICATION ROUTES =============
app.get("/api/notifications", isAuthenticated, async (req: any, res) => {
try {
const userId = getUserId(req);
const userNotifications = await storage.getNotificationsByUser(userId);
res.json(userNotifications);
} catch (error) {
console.error("Error fetching user notifications:", error);
res.status(500).json({ message: "Failed to fetch notifications" });
}
});
app.patch("/api/notifications/:id/read", isAuthenticated, async (req, res) => {
try {
await storage.markNotificationAsRead(req.params.id);
res.json({ success: true });
} catch (error) {
console.error("Error marking notification as read:", error);
res.status(500).json({ message: "Failed to mark notification as read" });
}
});
// ============= GUARD CONSTRAINTS ROUTES =============
app.get("/api/guard-constraints/:guardId", isAuthenticated, async (req, res) => {
try {
const constraints = await storage.getGuardConstraints(req.params.guardId);
res.json(constraints || null);
} catch (error) {
console.error("Error fetching guard constraints:", error);
res.status(500).json({ message: "Failed to fetch guard constraints" });
}
});
app.post("/api/guard-constraints", isAuthenticated, async (req, res) => {
try {
const constraints = await storage.upsertGuardConstraints(req.body);
res.json(constraints);
} catch (error) {
console.error("Error upserting guard constraints:", error);
res.status(500).json({ message: "Failed to save guard constraints" });
}
});
// ============= SITE PREFERENCES ROUTES =============
app.get("/api/site-preferences/:siteId", isAuthenticated, async (req, res) => {
try {
const preferences = await storage.getSitePreferences(req.params.siteId);
res.json(preferences);
} catch (error) {
console.error("Error fetching site preferences:", error);
res.status(500).json({ message: "Failed to fetch site preferences" });
}
});
app.post("/api/site-preferences", isAuthenticated, async (req, res) => {
try {
const preference = await storage.createSitePreference(req.body);
res.json(preference);
} catch (error) {
console.error("Error creating site preference:", error);
res.status(500).json({ message: "Failed to create site preference" });
}
});
app.delete("/api/site-preferences/:id", isAuthenticated, async (req, res) => {
try {
await storage.deleteSitePreference(req.params.id);
res.json({ success: true });
} catch (error) {
console.error("Error deleting site preference:", error);
res.status(500).json({ message: "Failed to delete site preference" });
}
});
// ============= TRAINING COURSES ROUTES =============
app.get("/api/training-courses", isAuthenticated, async (req, res) => {
try {
const guardId = req.query.guardId as string | undefined;
const courses = guardId
? await storage.getTrainingCoursesByGuard(guardId)
: await storage.getAllTrainingCourses();
res.json(courses);
} catch (error) {
console.error("Error fetching training courses:", error);
res.status(500).json({ message: "Failed to fetch training courses" });
}
});
app.post("/api/training-courses", isAuthenticated, async (req, res) => {
try {
const course = await storage.createTrainingCourse(req.body);
res.json(course);
} catch (error) {
console.error("Error creating training course:", error);
res.status(500).json({ message: "Failed to create training course" });
}
});
app.patch("/api/training-courses/:id", isAuthenticated, async (req, res) => {
try {
const updated = await storage.updateTrainingCourse(req.params.id, req.body);
if (!updated) {
return res.status(404).json({ message: "Training course not found" });
}
res.json(updated);
} catch (error) {
console.error("Error updating training course:", error);
res.status(500).json({ message: "Failed to update training course" });
}
});
app.delete("/api/training-courses/:id", isAuthenticated, async (req, res) => {
try {
await storage.deleteTrainingCourse(req.params.id);
res.json({ success: true });
} catch (error) {
console.error("Error deleting training course:", error);
res.status(500).json({ message: "Failed to delete training course" });
}
});
// ============= HOLIDAYS ROUTES =============
app.get("/api/holidays", isAuthenticated, async (req, res) => {
try {
const year = req.query.year ? parseInt(req.query.year as string) : undefined;
const holidays = await storage.getAllHolidays(year);
res.json(holidays);
} catch (error) {
console.error("Error fetching holidays:", error);
res.status(500).json({ message: "Failed to fetch holidays" });
}
});
app.post("/api/holidays", isAuthenticated, async (req, res) => {
try {
const holiday = await storage.createHoliday(req.body);
res.json(holiday);
} catch (error) {
console.error("Error creating holiday:", error);
res.status(500).json({ message: "Failed to create holiday" });
}
});
app.delete("/api/holidays/:id", isAuthenticated, async (req, res) => {
try {
await storage.deleteHoliday(req.params.id);
res.json({ success: true });
} catch (error) {
console.error("Error deleting holiday:", error);
res.status(500).json({ message: "Failed to delete holiday" });
}
});
// ============= ABSENCES ROUTES =============
app.get("/api/absences", isAuthenticated, async (req, res) => {
try {
const guardId = req.query.guardId as string | undefined;
const absences = guardId
? await storage.getAbsencesByGuard(guardId)
: await storage.getAllAbsences();
res.json(absences);
} catch (error) {
console.error("Error fetching absences:", error);
res.status(500).json({ message: "Failed to fetch absences" });
}
});
app.post("/api/absences", isAuthenticated, async (req, res) => {
try {
const absence = await storage.createAbsence(req.body);
res.json(absence);
} catch (error) {
console.error("Error creating absence:", error);
res.status(500).json({ message: "Failed to create absence" });
}
});
app.patch("/api/absences/:id", isAuthenticated, async (req, res) => {
try {
const updated = await storage.updateAbsence(req.params.id, req.body);
if (!updated) {
return res.status(404).json({ message: "Absence not found" });
}
res.json(updated);
} catch (error) {
console.error("Error updating absence:", error);
res.status(500).json({ message: "Failed to update absence" });
}
});
app.delete("/api/absences/:id", isAuthenticated, async (req, res) => {
try {
await storage.deleteAbsence(req.params.id);
res.json({ success: true });
} catch (error) {
console.error("Error deleting absence:", error);
res.status(500).json({ message: "Failed to delete absence" });
}
});
const httpServer = createServer(app);
return httpServer;
}