From cb965eaa2753c087484899edc52d0d77681daab1 Mon Sep 17 00:00:00 2001 From: Surya Paolo Date: Wed, 24 Dec 2025 00:26:38 +0100 Subject: [PATCH] - Parte 3 : Viaggi - Chat --- .env.dev.pcb | 3 +- .env.dev.riso | 1 + .env.development | 3 +- .env.prod.pcb | 3 +- .env.prod.riso | 3 +- src/controllers/UserController.js | 21 +- src/controllers/chatController.js | 842 ++++++++++-------- src/controllers/feedbackController.js | 26 +- src/controllers/geocodingController.js | 633 ++++++++----- src/controllers/geocodingController_OLD.js | 522 +++++++++++ src/controllers/rideController.js | 60 +- src/controllers/rideRequestController.js | 22 +- src/models/Chat.js | 25 +- src/models/Ride.js | 6 +- src/models/user.js | 8 - src/router/api_router.js | 59 +- src/router/users_router.js | 42 +- src/routes/geoRoutes.js | 37 + .../{trasportiRoutes.js => viaggiRoutes.js} | 227 ++--- src/server/setupExpress.js | 2 +- 20 files changed, 1752 insertions(+), 793 deletions(-) create mode 100644 src/controllers/geocodingController_OLD.js create mode 100644 src/routes/geoRoutes.js rename src/routes/{trasportiRoutes.js => viaggiRoutes.js} (82%) diff --git a/.env.dev.pcb b/.env.dev.pcb index 076722d..63cfe18 100644 --- a/.env.dev.pcb +++ b/.env.dev.pcb @@ -43,4 +43,5 @@ MIAB_ADMIN_EMAIL=admin@lamiaposta.org MIAB_ADMIN_PASSWORD=passpao1pabox@1A DS_API_KEY="sk-222e3addb3d8455d8b0516d93906eec7" SERVER_A_URL="http://51.77.156.69:3000" -API_KEY_MSSQL="m68yADSr123MIVIDA@154$DSAGVOK" \ No newline at end of file +API_KEY_MSSQL="m68yADSr123MIVIDA@154$DSAGVOK" +ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0=" \ No newline at end of file diff --git a/.env.dev.riso b/.env.dev.riso index f19d428..0763d97 100644 --- a/.env.dev.riso +++ b/.env.dev.riso @@ -39,3 +39,4 @@ AUTH_NEW_SITES=123123123 SCRIPTS_DIR=admin_scripts CLOUDFLARE_TOKENS=[{"label":"Paolo.arena77@gmail.com","value":"M9EM309v8WFquJKpYgZCw-TViM2wX6vB3wlK6GD0"},{"label":"gruppomacro.com","value":"bqmzGShoX7WqOBzkXocoECyBkPq3GfqcM5t6VFd8"}] DS_API_KEY="sk-222e3addb3d8455d8b0516d93906eec7" +ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0=" \ No newline at end of file diff --git a/.env.development b/.env.development index 232ed0c..4de267c 100644 --- a/.env.development +++ b/.env.development @@ -44,4 +44,5 @@ OLLAMA_DEFAULT_MODEL=llama3.2:3b GROK_API="xai-PcNM5obgPaETtmnfDWPZk235D75ZgxENU2QmeqPfMQCHh9dwCDVeRRe0oVVA2YOpiUDh1uJieZsMasja" REPLICATE_API_TOKEN="r8_AVhM6igwvoOnUA65cHVZdhEDfTqBVk94WTB0u" FAL_KEY="7d251c88-21b5-4b55-8b3e-4bafd910f99f:b81c0a36a25b052f26eb8ac226c7efff" -HF_TOKEN="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" \ No newline at end of file +HF_TOKEN="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" +ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0=" \ No newline at end of file diff --git a/.env.prod.pcb b/.env.prod.pcb index d833b55..62b439a 100644 --- a/.env.prod.pcb +++ b/.env.prod.pcb @@ -41,4 +41,5 @@ MIAB_HOST=box.lamiaposta.org MIAB_ADMIN_EMAIL=admin@lamiaposta.org MIAB_ADMIN_PASSWORD=passpao1pabox@1A SERVER_A_URL="http://51.77.156.69:3000" -API_KEY_MSSQL="m68yADSr123MIVIDA@154$DSAGVOK" \ No newline at end of file +API_KEY_MSSQL="m68yADSr123MIVIDA@154$DSAGVOK" +ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0=" \ No newline at end of file diff --git a/.env.prod.riso b/.env.prod.riso index 67b5f6d..28f394d 100644 --- a/.env.prod.riso +++ b/.env.prod.riso @@ -38,4 +38,5 @@ SCRIPTS_DIR=admin_scripts CLOUDFLARE_TOKENS=[{"label":"Paolo.arena77@gmail.com","value":"M9EM309v8WFquJKpYgZCw-TViM2wX6vB3wlK6GD0"},{"label":"gruppomacro.com","value":"bqmzGShoX7WqOBzkXocoECyBkPq3GfqcM5t6VFd8"}] MIAB_HOST=box.lamiaposta.org MIAB_ADMIN_EMAIL=admin@lamiaposta.org -MIAB_ADMIN_PASSWORD=passpao1pabox@1A \ No newline at end of file +MIAB_ADMIN_PASSWORD=passpao1pabox@1A +ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0=" \ No newline at end of file diff --git a/src/controllers/UserController.js b/src/controllers/UserController.js index e51eb7c..e05a078 100644 --- a/src/controllers/UserController.js +++ b/src/controllers/UserController.js @@ -55,7 +55,11 @@ class UserController { } // Send response with tokens - res.header('x-auth', result.token).header('x-refrtok', result.refreshToken).header('x-browser-random', result.browser_random).send(result.user); + res + .header('x-auth', result.token) + .header('x-refrtok', result.refreshToken) + .header('x-browser-random', result.browser_random) + .send(result.user); } catch (error) { console.error('Error in registration:', error.message); res.status(400).send({ @@ -103,11 +107,15 @@ class UserController { } // Send response with tokens - res.header('x-auth', result.token).header('x-refrtok', result.refreshToken).header('x-browser-random', result.browser_random).send({ - usertosend: result.user, - code: server_constants.RIS_CODE_OK, - subsExistonDb: result.subsExistonDb, - }); + res + .header('x-auth', result.token) + .header('x-refrtok', result.refreshToken) + .header('x-browser-random', result.browser_random) + .send({ + usertosend: result.user, + code: server_constants.RIS_CODE_OK, + subsExistonDb: result.subsExistonDb, + }); } catch (error) { console.error('Error in login:', error.message); res.status(400).send({ @@ -487,6 +495,7 @@ class UserController { const { User } = require('../models/user'); return User.isCollaboratore(user.perm); } + } module.exports = UserController; diff --git a/src/controllers/chatController.js b/src/controllers/chatController.js index e7a72a1..23238b9 100644 --- a/src/controllers/chatController.js +++ b/src/controllers/chatController.js @@ -2,95 +2,79 @@ const Chat = require('../models/Chat'); const Message = require('../models/Message'); const { User } = require('../models/user'); -/** - * @desc Ottieni tutte le chat dell'utente - * @route GET /api/trasporti/chats - * @access Private - */ -const getMyChats = async (req, res) => { +// ===== GET USER CHATS ===== +exports.getUserChats = async (req, res) => { try { const userId = req.user._id; const idapp = req.user.idapp; - const { page = 1, limit = 20 } = req.query; - - if (!idapp) { - return res.status(400).json({ - success: false, - message: 'idapp è obbligatorio' - }); - } - - const skip = (parseInt(page) - 1) * parseInt(limit); + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 20; + const skip = (page - 1) * limit; + // ✅ Trova chat dove l'utente è partecipante E non l'ha cancellata const chats = await Chat.find({ idapp, participants: userId, isActive: true, - blockedBy: { $ne: userId } + deletedBy: { $ne: userId }, // ✅ Escludi chat cancellate }) - .populate('participants', 'username name surname profile.img') - .populate('rideId', 'departure destination dateTime status') + .populate('participants', 'username name surname profile') + .populate({ + path: 'rideId', + select: 'departure destination departureDate departureTime status', + }) .sort({ updatedAt: -1 }) .skip(skip) - .limit(parseInt(limit)); + .limit(limit) + .lean(); - // Aggiungi conteggio non letti per ogni chat - const chatsWithUnread = chats.map(chat => { - const chatObj = chat.toObject(); - chatObj.unreadCount = chat.getUnreadForUser(userId); - - // Trova l'altro partecipante (per chat dirette) - if (chat.type === 'direct') { - chatObj.otherParticipant = chat.participants.find( - p => p._id.toString() !== userId.toString() - ); + const enrichedChats = chats.map((chat) => { + let unreadCount = 0; + + if (chat.unreadCount) { + if (chat.unreadCount instanceof Map) { + unreadCount = chat.unreadCount.get(userId.toString()) || 0; + } else if (typeof chat.unreadCount === 'object') { + // Dopo .lean(), la Map diventa un oggetto plain + unreadCount = chat.unreadCount[userId.toString()] || 0; + } } - - return chatObj; - }); - - const total = await Chat.countDocuments({ - idapp, - participants: userId, - isActive: true + return { + ...chat, + unreadCount, + }; }); res.json({ success: true, - data: chatsWithUnread, + data: enrichedChats, pagination: { - page: parseInt(page), - limit: parseInt(limit), - total, - pages: Math.ceil(total / parseInt(limit)) - } + page, + limit, + hasMore: chats.length === limit, + }, }); - } catch (error) { - console.error('Errore recupero chat:', error); + console.error('Error fetching chats:', error); res.status(500).json({ success: false, message: 'Errore nel recupero delle chat', - error: error.message + error: error.message, }); } }; -/** - * @desc Ottieni o crea una chat diretta con un utente - * @route POST /api/trasporti/chats/direct - * @access Private - */ -const getOrCreateDirectChat = async (req, res) => { +// ===== GET OR CREATE DIRECT CHAT ===== +exports.getOrCreateDirectChat = async (req, res) => { try { const userId = req.user._id; - const idapp = req.user.idapp; const { otherUserId, rideId } = req.body; + const idapp = req.user.idapp; - if (!idapp || !otherUserId) { + if (!otherUserId) { return res.status(400).json({ success: false, - message: 'idapp e otherUserId sono obbligatori' + message: 'otherUserId è richiesto', }); } @@ -99,382 +83,613 @@ const getOrCreateDirectChat = async (req, res) => { if (!otherUser) { return res.status(404).json({ success: false, - message: 'Utente non trovato' + message: 'Utente non trovato', }); } - // Non puoi chattare con te stesso - if (userId.toString() === otherUserId) { - return res.status(400).json({ - success: false, - message: 'Non puoi creare una chat con te stesso' + // Cerca chat esistente + let chat = await Chat.findOne({ + idapp, + type: 'direct', + participants: { $all: [userId, otherUserId], $size: 2 }, + }); + + if (!chat) { + // Crea nuova chat + chat = new Chat({ + idapp, + type: 'direct', + participants: [userId, otherUserId], + rideId: rideId || null, + unreadCount: new Map(), }); + await chat.save(); + } else if (rideId && !chat.rideId) { + // Aggiungi rideId se non presente + chat.rideId = rideId; + await chat.save(); } - const chat = await Chat.findOrCreateDirect(idapp, userId, otherUserId, rideId); + // ✅ Se la chat era stata cancellata da uno dei due, rimuovilo da deletedBy + if (chat.deletedBy && chat.deletedBy.length > 0) { + const wasDeleted = chat.deletedBy.some((id) => id.toString() === userId.toString()); - await chat.populate('participants', 'username name surname profile.img'); - await chat.populate('rideId', 'departure destination dateTime'); + if (wasDeleted) { + chat.deletedBy = chat.deletedBy.filter((id) => id.toString() !== userId.toString()); + await chat.save(); + } + } + // Popola i partecipanti + await chat.populate('participants', 'username name surname profile'); + if (chat.rideId) { + await chat.populate('rideId', 'departure destination departureDate'); + } + + // Aggiungi unread count const chatObj = chat.toObject(); - chatObj.otherParticipant = chat.participants.find( - p => p._id.toString() !== userId.toString() - ); chatObj.unreadCount = chat.getUnreadForUser(userId); res.json({ success: true, - data: chatObj + data: chatObj, }); - } catch (error) { - console.error('Errore creazione chat:', error); + console.error('Error getting/creating direct chat:', error); res.status(500).json({ success: false, message: 'Errore nella creazione della chat', - error: error.message + error: error.message, }); } }; -/** - * @desc Ottieni una chat per ID - * @route GET /api/trasporti/chats/:id - * @access Private - */ -const getChatById = async (req, res) => { +// ===== GET CHAT BY ID ===== +exports.getChatById = async (req, res) => { try { - const { id } = req.params; const userId = req.user._id; + const { chatId } = req.params; - const chat = await Chat.findById(id) - .populate('participants', 'username name surname profile.img profile.Cell') - .populate('rideId', 'departure destination dateTime status type'); + const chat = await Chat.findById(chatId) + .populate('participants', 'username name surname profile') + .populate({ + path: 'rideId', + select: 'departure destination departureDate departureTime status', + }) + .lean(); if (!chat) { return res.status(404).json({ success: false, - message: 'Chat non trovata' + message: 'Chat non trovata', }); } // Verifica che l'utente sia partecipante - if (!chat.hasParticipant(userId)) { + const isParticipant = chat.participants.some((p) => { + const pId = p._id ? p._id.toString() : p.toString(); + return pId === userId.toString(); + }); + + if (!isParticipant) { return res.status(403).json({ success: false, - message: 'Non sei autorizzato ad accedere a questa chat' + message: 'Non autorizzato', }); } - // Marca come letta - await chat.markAsRead(userId); + // Verifica se l'utente ha cancellato questa chat + const wasDeleted = chat.deletedBy?.some((id) => id.toString() === userId.toString()); - const chatObj = chat.toObject(); - chatObj.unreadCount = 0; // Appena marcata come letta - - if (chat.type === 'direct') { - chatObj.otherParticipant = chat.participants.find( - p => p._id.toString() !== userId.toString() - ); + if (wasDeleted) { + return res.status(404).json({ + success: false, + message: 'Chat non trovata', + }); } + // Aggiungi unread count per l'utente corrente + let unreadCount = 0; + if (chat.unreadCount) { + if (chat.unreadCount instanceof Map) { + unreadCount = chat.unreadCount.get(userId.toString()) || 0; + } else if (typeof chat.unreadCount === 'object') { + unreadCount = chat.unreadCount[userId.toString()] || 0; + } + } + + const chatObj = { + ...chat, + unreadCount, + }; + res.json({ success: true, - data: chatObj + data: chatObj, }); - } catch (error) { - console.error('Errore recupero chat:', error); + console.error('Error getting chat by ID:', error); res.status(500).json({ success: false, message: 'Errore nel recupero della chat', - error: error.message + error: error.message, }); } }; -/** - * @desc Ottieni i messaggi di una chat - * @route GET /api/trasporti/chats/:id/messages - * @access Private - */ -const getChatMessages = async (req, res) => { +// ===== GET CHAT MESSAGES ===== +exports.getChatMessages = async (req, res) => { try { - const { id } = req.params; - const idapp = req.user.idapp; - const { before, after, limit = 50 } = req.query; const userId = req.user._id; + const { chatId } = req.params; + const idapp = req.user.idapp; + const page = parseInt(req.query.page) || 1; + const { before, after, limit = 50 } = req.query; + const skip = (page - 1) * limit; - // Verifica accesso alla chat - const chat = await Chat.findById(id); + // Verifica chat e partecipazione + const chat = await Chat.findById(chatId); if (!chat) { return res.status(404).json({ success: false, - message: 'Chat non trovata' + message: 'Chat non trovata', }); - } + } if (!chat.hasParticipant(userId)) { return res.status(403).json({ success: false, - message: 'Non sei autorizzato' + message: 'Non autorizzato', }); } - const messages = await Message.getByChat(idapp, id, { - limit: parseInt(limit), - before, - after - }); + const query = { + chatId, + idapp, + isDeleted: { $ne: true }, + }; - // Marca messaggi come letti - await Promise.all( - messages - .filter(m => m.senderId && m.senderId._id.toString() !== userId.toString()) - .map(m => m.markAsReadBy(userId)) - ); + // clearedBefore + let clearedDate = null; + if (chat.clearedBefore) { + if (chat.clearedBefore instanceof Map) { + clearedDate = chat.clearedBefore.get(userId.toString()); + } else if (typeof chat.clearedBefore === 'object') { + clearedDate = chat.clearedBefore[userId.toString()]; + } + } - // Aggiorna unread count nella chat + if (clearedDate) { + query.createdAt = { $gt: new Date(clearedDate) }; + } + + // ✅ Paginazione: before (messaggi più vecchi) + if (before) { + query.createdAt = { + ...query.createdAt, + $lt: new Date(before), + }; + } + + // ✅ Polling: after (messaggi più nuovi) + if (after) { + query.createdAt = { + ...query.createdAt, + $gt: new Date(after), // Messaggi DOPO questo timestamp + }; + } + + // ✅ Ordina in base alla direzione + const sortOrder = after ? 1 : -1; // after: asc, before: desc + + const messages = await Message.find(query) + .sort({ createdAt: sortOrder }) + .limit(parseInt(limit)) + .populate('senderId', 'username name surname profile.img profile.avatar') + .populate({ + path: 'replyTo', + select: 'text senderId', + populate: { + path: 'senderId', + select: 'username name', + }, + }) + .lean(); + + // ✅ Se usato after, i messaggi sono già in ordine cronologico + // Se usato before, invertili + if (!after) { + messages.reverse(); + } + + // Marca i messaggi come letti await chat.markAsRead(userId); res.json({ success: true, - data: messages.reverse(), // Ordine cronologico - hasMore: messages.length === parseInt(limit) + data: messages.reverse(), + pagination: { + page, + limit, + hasMore: messages.length === limit, + }, }); - } catch (error) { - console.error('Errore recupero messaggi:', error); + console.error('Error fetching messages:', error); res.status(500).json({ success: false, message: 'Errore nel recupero dei messaggi', - error: error.message + error: error.message, }); } }; -/** - * @desc Invia un messaggio - * @route POST /api/trasporti/chats/:id/messages - * @access Private - */ -const sendMessage = async (req, res) => { +// ===== SEND MESSAGE ===== +exports.sendMessage = async (req, res) => { try { - const { id } = req.params; - const idapp = req.user.idapp; - const { text, type = 'text', metadata, replyTo } = req.body; const userId = req.user._id; + const idapp = req.user.idapp; + const { chatId } = req.params; + const { text, type = 'text', metadata } = req.body; - if (!idapp || !text) { - return res.status(400).json({ - success: false, - message: 'idapp e text sono obbligatori' - }); - } - - // Verifica accesso alla chat - const chat = await Chat.findById(id); + // Verifica chat + const chat = await Chat.findById(chatId); if (!chat) { return res.status(404).json({ success: false, - message: 'Chat non trovata' + message: 'Chat non trovata', }); } if (!chat.hasParticipant(userId)) { return res.status(403).json({ success: false, - message: 'Non sei autorizzato' + message: 'Non autorizzato', }); } - // Verifica che la chat non sia bloccata if (chat.isBlockedFor(userId)) { return res.status(403).json({ success: false, - message: 'Non puoi inviare messaggi in questa chat' + message: 'Non puoi inviare messaggi in questa chat', }); } - // Crea il messaggio + // ✅ IMPORTANTE: Se qualcuno aveva cancellato la chat, rimuovilo da deletedBy + // così la chat riappare nella sua lista + if (chat.deletedBy && chat.deletedBy.length > 0) { + const otherParticipants = chat.participants.filter((p) => p.toString() !== userId.toString()); + + let needsSave = false; + otherParticipants.forEach((participantId) => { + const wasDeleted = chat.deletedBy.some((id) => id.toString() === participantId.toString()); + if (wasDeleted) { + chat.deletedBy = chat.deletedBy.filter((id) => id.toString() !== participantId.toString()); + needsSave = true; + } + }); + + if (needsSave) { + await chat.save(); + } + } + + // Crea messaggio const message = new Message({ idapp, - chatId: id, + chatId: chat._id, senderId: userId, text, type, - metadata: metadata || {}, - replyTo: replyTo || null, - readBy: [{ userId, readAt: new Date() }] // Il mittente l'ha già letto + metadata, + readBy: [userId], }); await message.save(); + await message.populate('senderId', 'username name surname profile'); - // Popola per la risposta - await message.populate('senderId', 'username name surname profile.img'); - if (replyTo) { - await message.populate('replyTo', 'text senderId'); - } + // Aggiorna chat + await chat.updateLastMessage(message); + await chat.incrementUnread(userId); - // TODO: Inviare notifica push agli altri partecipanti - - res.status(201).json({ + res.json({ success: true, - data: message + data: message, }); - } catch (error) { - console.error('Errore invio messaggio:', error); + console.error('Error sending message:', error); res.status(500).json({ success: false, - message: 'Errore nell\'invio del messaggio', - error: error.message + message: "Errore nell'invio del messaggio", + error: error.message, }); } }; -/** - * @desc Marca tutti i messaggi di una chat come letti - * @route PUT /api/trasporti/chats/:id/read - * @access Private - */ -const markChatAsRead = async (req, res) => { +// ===== MARK MESSAGES AS READ ===== +exports.markAsRead = async (req, res) => { try { - const { id } = req.params; const userId = req.user._id; + const { chatId } = req.params; - const chat = await Chat.findById(id); + const chat = await Chat.findById(chatId); if (!chat) { return res.status(404).json({ success: false, - message: 'Chat non trovata' + message: 'Chat non trovata', }); } if (!chat.hasParticipant(userId)) { return res.status(403).json({ success: false, - message: 'Non sei autorizzato' + message: 'Non autorizzato', }); } + // Marca come letti await chat.markAsRead(userId); - // Marca tutti i messaggi come letti + // Aggiorna anche i singoli messaggi await Message.updateMany( { - chatId: id, + chatId: chat._id, senderId: { $ne: userId }, - 'readBy.userId': { $ne: userId } + readBy: { $ne: userId }, }, { - $push: { readBy: { userId, readAt: new Date() } } + $addToSet: { readBy: userId }, } ); res.json({ success: true, - message: 'Chat marcata come letta' + message: 'Messaggi marcati come letti', }); - } catch (error) { - console.error('Errore marca come letto:', error); + console.error('Error marking as read:', error); res.status(500).json({ success: false, - message: 'Errore', - error: error.message + message: 'Errore nella marcatura dei messaggi', + error: error.message, }); } }; -/** - * @desc Blocca/sblocca una chat - * @route PUT /api/trasporti/chats/:id/block - * @access Private - */ -const toggleBlockChat = async (req, res) => { +// ===== DELETE CHAT (SOFT DELETE) ===== +exports.deleteChat = async (req, res) => { try { - const { id } = req.params; - const { block } = req.body; const userId = req.user._id; + const { chatId } = req.params; - const chat = await Chat.findById(id); + const chat = await Chat.findById(chatId); if (!chat) { return res.status(404).json({ success: false, - message: 'Chat non trovata' + message: 'Chat non trovata', }); } if (!chat.hasParticipant(userId)) { return res.status(403).json({ success: false, - message: 'Non sei autorizzato' + message: 'Non autorizzato', }); } - if (block) { - if (!chat.blockedBy.includes(userId)) { - chat.blockedBy.push(userId); + // ✅ Soft delete: aggiungi userId a deletedBy + if (!chat.deletedBy) { + chat.deletedBy = []; + } + + const alreadyDeleted = chat.deletedBy.some((id) => id.toString() === userId.toString()); + + if (!alreadyDeleted) { + chat.deletedBy.push(userId); + } + + // ✅ Salva il timestamp di quando l'utente ha cancellato + // così quando riappare la chat, vedrà solo messaggi nuovi + if (!chat.clearedBefore) { + chat.clearedBefore = new Map(); + } + chat.clearedBefore.set(userId.toString(), new Date()); + + await chat.save(); + + res.json({ + success: true, + message: 'Chat eliminata', + }); + } catch (error) { + console.error('Error deleting chat:', error); + res.status(500).json({ + success: false, + message: "Errore nell'eliminazione della chat", + error: error.message, + }); + } +}; + +// ===== TOGGLE MUTE CHAT ===== +exports.toggleMuteChat = async (req, res) => { + try { + const userId = req.user._id; + const { chatId } = req.params; + const { mute } = req.body; + + const chat = await Chat.findById(chatId); + if (!chat) { + return res.status(404).json({ + success: false, + message: 'Chat non trovata', + }); + } + + if (!chat.hasParticipant(userId)) { + return res.status(403).json({ + success: false, + message: 'Non autorizzato', + }); + } + + if (!chat.mutedBy) { + chat.mutedBy = []; + } + + if (mute) { + // Aggiungi a mutedBy se non presente + const alreadyMuted = chat.mutedBy.some((id) => id.toString() === userId.toString()); + if (!alreadyMuted) { + chat.mutedBy.push(userId); } } else { - chat.blockedBy = chat.blockedBy.filter( - id => id.toString() !== userId.toString() - ); + // Rimuovi da mutedBy + chat.mutedBy = chat.mutedBy.filter((id) => id.toString() !== userId.toString()); } await chat.save(); res.json({ success: true, - message: block ? 'Chat bloccata' : 'Chat sbloccata', - data: { blocked: block } + message: mute ? 'Chat silenziata' : 'Notifiche attivate', + data: { muted: mute }, }); - } catch (error) { - console.error('Errore blocco chat:', error); + console.error('Error toggling mute:', error); res.status(500).json({ success: false, - message: 'Errore', - error: error.message + message: "Errore nell'aggiornamento", + error: error.message, + }); + } +}; + +// ===== GET UNREAD COUNT ===== +exports.getUnreadCount = async (req, res) => { + try { + const userId = req.user._id; + const idapp = req.user.idapp; + + const chats = await Chat.find({ + idapp, + participants: userId, + isActive: true, + deletedBy: { $ne: userId }, + }).lean(); + + let totalUnread = 0; + chats.forEach((chat) => { + const unread = chat.unreadCount?.get(userId.toString()) || 0; + totalUnread += unread; + }); + + res.json({ + success: true, + data: { + totalUnread, + chatCount: chats.length, + }, + }); + } catch (error) { + console.error('Error getting unread count:', error); + res.status(500).json({ + success: false, + message: 'Errore nel conteggio messaggi non letti', + error: error.message, }); } }; /** - * @desc Muta/smuta notifiche di una chat - * @route PUT /api/trasporti/chats/:id/mute + * @desc Marca una chat come letta + * @route PUT /api/viaggi/chats/:chatId/read * @access Private */ -const toggleMuteChat = async (req, res) => { +exports.markChatAsRead = async (req, res) => { try { - const { id } = req.params; - const { mute } = req.body; + const { chatId } = req.params; const userId = req.user._id; - const chat = await Chat.findById(id); + const chat = await Chat.findById(chatId); if (!chat) { return res.status(404).json({ success: false, - message: 'Chat non trovata' + message: 'Chat non trovata', }); } if (!chat.hasParticipant(userId)) { return res.status(403).json({ success: false, - message: 'Non sei autorizzato' + message: 'Non sei autorizzato', }); } - if (mute) { - if (!chat.mutedBy.includes(userId)) { - chat.mutedBy.push(userId); + // Reset unread count + if (!chat.unreadCount) { + chat.unreadCount = new Map(); + } + chat.unreadCount.set(userId.toString(), 0); + chat.markModified('unreadCount'); + await chat.save(); + + // Marca messaggi come letti + await Message.updateMany( + { + chatId, + senderId: { $ne: userId }, + readBy: { $ne: userId }, + }, + { + $addToSet: { readBy: userId }, } - } else { - chat.mutedBy = chat.mutedBy.filter( - id => id.toString() !== userId.toString() - ); + ); + + res.json({ + success: true, + message: 'Chat marcata come letta', + }); + } catch (error) { + console.error('Errore marca come letto:', error); + res.status(500).json({ + success: false, + message: 'Errore', + error: error.message, + }); + } +}; + +/** + * @desc Muta/smuta notifiche + * @route PUT /api/viaggi/chats/:chatId/mute + * @access Private + */ +exports.toggleMuteChat = async (req, res) => { + try { + const { chatId } = req.params; + const { mute } = req.body; + const userId = req.user._id; + + const chat = await Chat.findById(chatId); + if (!chat) { + return res.status(404).json({ success: false, message: 'Chat non trovata' }); + } + + if (!chat.hasParticipant(userId)) { + return res.status(403).json({ success: false, message: 'Non sei autorizzato' }); + } + + if (!chat.mutedBy) { + chat.mutedBy = []; + } + + const isMuted = chat.mutedBy.some((mid) => mid.toString() === userId.toString()); + + if (mute && !isMuted) { + chat.mutedBy.push(userId); + } else if (!mute && isMuted) { + chat.mutedBy = chat.mutedBy.filter((mid) => mid.toString() !== userId.toString()); } await chat.save(); @@ -482,80 +697,20 @@ const toggleMuteChat = async (req, res) => { res.json({ success: true, message: mute ? 'Notifiche disattivate' : 'Notifiche attivate', - data: { muted: mute } + data: { muted: mute }, }); - } catch (error) { console.error('Errore mute chat:', error); - res.status(500).json({ - success: false, - message: 'Errore', - error: error.message - }); - } -}; - -/** - * @desc Conta messaggi non letti totali - * @route GET /api/trasporti/chats/unread/count - * @access Private - */ -const getUnreadCount = async (req, res) => { - try { - const userId = req.user._id; - const idapp = req.user.idapp; - - if (!idapp) { - return res.status(400).json({ - success: false, - message: 'idapp è obbligatorio' - }); - } - - const chats = await Chat.find({ - idapp, - participants: userId, - isActive: true - }); - - let totalUnread = 0; - const chatUnreads = []; - - for (const chat of chats) { - const unread = chat.getUnreadForUser(userId); - totalUnread += unread; - if (unread > 0) { - chatUnreads.push({ - chatId: chat._id, - unread - }); - } - } - - res.json({ - success: true, - data: { - total: totalUnread, - chats: chatUnreads - } - }); - - } catch (error) { - console.error('Errore conteggio non letti:', error); - res.status(500).json({ - success: false, - message: 'Errore', - error: error.message - }); + res.status(500).json({ success: false, message: 'Errore', error: error.message }); } }; /** * @desc Elimina un messaggio (soft delete) - * @route DELETE /api/trasporti/chats/:chatId/messages/:messageId + * @route DELETE /api/viaggi/chats/:chatId/messages/:messageId * @access Private */ -const deleteMessage = async (req, res) => { +exports.deleteMessage = async (req, res) => { try { const { chatId, messageId } = req.params; const userId = req.user._id; @@ -563,115 +718,72 @@ const deleteMessage = async (req, res) => { const message = await Message.findById(messageId); if (!message) { - return res.status(404).json({ - success: false, - message: 'Messaggio non trovato' - }); + return res.status(404).json({ success: false, message: 'Messaggio non trovato' }); } if (message.chatId.toString() !== chatId) { - return res.status(400).json({ - success: false, - message: 'Messaggio non appartiene a questa chat' - }); + return res.status(400).json({ success: false, message: 'Messaggio non appartiene a questa chat' }); } - // Solo il mittente può eliminare if (message.senderId.toString() !== userId.toString()) { - return res.status(403).json({ - success: false, - message: 'Non sei autorizzato a eliminare questo messaggio' - }); + return res.status(403).json({ success: false, message: 'Non sei autorizzato' }); } - await message.softDelete(); - - res.json({ - success: true, - message: 'Messaggio eliminato' - }); + message.isDeleted = true; + message.deletedAt = new Date(); + message.text = 'Messaggio eliminato'; + await message.save(); + res.json({ success: true, message: 'Messaggio eliminato' }); } catch (error) { console.error('Errore eliminazione messaggio:', error); - res.status(500).json({ - success: false, - message: 'Errore', - error: error.message - }); + res.status(500).json({ success: false, message: 'Errore', error: error.message }); } }; /** - * @desc Elimina una chat (soft delete) - * @route DELETE /api/trasporti/chats/:id + * @desc Blocca/sblocca una chat + * @route PUT /api/viaggi/chats/:chatId/block * @access Private */ -const deleteChat = async (req, res) => { +exports.toggleBlockChat = async (req, res) => { try { - const { id } = req.params; + const { chatId } = req.params; + const { block } = req.body; const userId = req.user._id; - const chat = await Chat.findById(id); - + const chat = await Chat.findById(chatId); if (!chat) { - return res.status(404).json({ - success: false, - message: 'Chat non trovata' - }); + return res.status(404).json({ success: false, message: 'Chat non trovata' }); } - // Verifica che l'utente sia partecipante if (!chat.hasParticipant(userId)) { - return res.status(403).json({ - success: false, - message: 'Non sei autorizzato a eliminare questa chat' - }); + return res.status(403).json({ success: false, message: 'Non sei autorizzato' }); } - // Soft delete: segna come non attiva per questo utente - // Se vuoi hard delete, usa: await chat.deleteOne(); - - // Opzione 1: Soft delete (chat rimane ma nascosta per questo utente) - if (!chat.deletedBy) { - chat.deletedBy = []; + if (!chat.blockedBy) { + chat.blockedBy = []; } - - if (!chat.deletedBy.includes(userId)) { - chat.deletedBy.push(userId); + + const isBlocked = chat.blockedBy.some((bid) => bid.toString() === userId.toString()); + + if (block && !isBlocked) { + chat.blockedBy.push(userId); + } else if (!block && isBlocked) { + chat.blockedBy = chat.blockedBy.filter((bid) => bid.toString() !== userId.toString()); } - - // Se tutti i partecipanti hanno eliminato, marca come non attiva - if (chat.deletedBy.length === chat.participants.length) { - chat.isActive = false; - } - + await chat.save(); res.json({ success: true, - message: 'Chat eliminata' + message: block ? 'Chat bloccata' : 'Chat sbloccata', + data: { blocked: block }, }); - } catch (error) { - console.error('Errore eliminazione chat:', error); - res.status(500).json({ - success: false, - message: 'Errore nell\'eliminazione della chat', - error: error.message - }); + console.error('Errore blocco chat:', error); + res.status(500).json({ success: false, message: 'Errore', error: error.message }); } }; -module.exports = { - getMyChats, - getOrCreateDirectChat, - getChatById, - getChatMessages, - sendMessage, - markChatAsRead, - toggleBlockChat, - toggleMuteChat, - getUnreadCount, - deleteMessage, - deleteChat, -}; \ No newline at end of file +module.exports = exports; diff --git a/src/controllers/feedbackController.js b/src/controllers/feedbackController.js index 55cba6f..e4872ae 100644 --- a/src/controllers/feedbackController.js +++ b/src/controllers/feedbackController.js @@ -5,7 +5,7 @@ const { User } = require('../models/user'); /** * @desc Crea un feedback per un viaggio - * @route POST /api/trasporti/feedback + * @route POST /api/viaggi/feedback * @access Private */ const createFeedback = async (req, res) => { @@ -144,7 +144,7 @@ const createFeedback = async (req, res) => { /** * @desc Ottieni i feedback ricevuti da un utente - * @route GET /api/trasporti/feedback/user/:userId + * @route GET /api/viaggi/feedback/user/:userId * @access Public */ const getUserFeedback = async (req, res) => { @@ -206,13 +206,13 @@ const getUserFeedback = async (req, res) => { /** * @desc Ottieni statistiche feedback per un utente - * @route GET /api/trasporti/feedback/user/:userId/stats + * @route GET /api/viaggi/feedback/user/:userId/stats * @access Public */ const getUserFeedbackStats = async (req, res) => { try { const { userId } = req.params; - const { idapp } = req.query; + const idapp = req.user.idapp; if (!idapp) { return res.status(400).json({ @@ -246,13 +246,13 @@ const getUserFeedbackStats = async (req, res) => { /** * @desc Ottieni i feedback per un viaggio - * @route GET /api/trasporti/feedback/ride/:rideId + * @route GET /api/viaggi/feedback/ride/:rideId * @access Public (con info limitate) / Private (info complete) */ const getRideFeedback = async (req, res) => { try { const { rideId } = req.params; - const { idapp } = req.query; + const idapp = req.user.idapp; const userId = req.user?._id; if (!idapp) { @@ -337,14 +337,14 @@ const getRideFeedback = async (req, res) => { /** * @desc Verifica se l'utente può lasciare un feedback - * @route GET /api/trasporti/feedback/can-leave/:rideId/:toUserId + * @route GET /api/viaggi/feedback/can-leave/:rideId/:toUserId * @access Private * @note NUOVA FUNZIONE - Era mancante! */ const canLeaveFeedback = async (req, res) => { try { const { rideId, toUserId } = req.params; - const { idapp } = req.query; + const idapp = req.user.idapp; const fromUserId = req.user._id; if (!idapp) { @@ -476,7 +476,7 @@ const canLeaveFeedback = async (req, res) => { /** * @desc Rispondi a un feedback ricevuto - * @route POST /api/trasporti/feedback/:id/response + * @route POST /api/viaggi/feedback/:id/response * @access Private */ const respondToFeedback = async (req, res) => { @@ -542,7 +542,7 @@ const respondToFeedback = async (req, res) => { /** * @desc Segna un feedback come utile - * @route POST /api/trasporti/feedback/:id/helpful + * @route POST /api/viaggi/feedback/:id/helpful * @access Private */ const markAsHelpful = async (req, res) => { @@ -605,7 +605,7 @@ const markAsHelpful = async (req, res) => { /** * @desc Segnala un feedback inappropriato - * @route POST /api/trasporti/feedback/:id/report + * @route POST /api/viaggi/feedback/:id/report * @access Private */ const reportFeedback = async (req, res) => { @@ -679,7 +679,7 @@ const reportFeedback = async (req, res) => { /** * @desc Ottieni i miei feedback dati - * @route GET /api/trasporti/feedback/my/given + * @route GET /api/viaggi/feedback/my/given * @access Private */ const getMyGivenFeedback = async (req, res) => { @@ -729,7 +729,7 @@ const getMyGivenFeedback = async (req, res) => { /** * @desc Ottieni i miei feedback ricevuti - * @route GET /api/trasporti/feedback/my/received + * @route GET /api/viaggi/feedback/my/received * @access Private */ const getMyReceivedFeedback = async (req, res) => { diff --git a/src/controllers/geocodingController.js b/src/controllers/geocodingController.js index 02247ea..849cea9 100644 --- a/src/controllers/geocodingController.js +++ b/src/controllers/geocodingController.js @@ -1,39 +1,43 @@ /** - * Controller per Geocoding usando servizi Open Source - * - Nominatim (OpenStreetMap) per geocoding/reverse - * - OSRM per routing - * - Photon per autocomplete + * Controller per Geocoding usando OpenRouteService + * Documentazione: https://openrouteservice.org/dev/#/api-docs */ const https = require('https'); -const http = require('http'); -// Configurazione servizi -const NOMINATIM_BASE = 'https://nominatim.openstreetmap.org'; -const PHOTON_BASE = 'https://photon.komoot.io'; -const OSRM_BASE = 'https://router.project-osrm.org'; - -// User-Agent richiesto da Nominatim -const USER_AGENT = 'FreePlanetApp/1.0'; +// Configurazione OpenRouteService +const ORS_BASE = 'https://api.openrouteservice.org'; +const ORS_API_KEY = process.env.ORS_API_KEY || 'YOUR_API_KEY_HERE'; /** - * Helper per fare richieste HTTP/HTTPS + * Helper per fare richieste HTTPS a OpenRouteService */ -const makeRequest = (url) => { +const makeRequest = (url, method = 'GET', body = null) => { return new Promise((resolve, reject) => { - const client = url.startsWith('https') ? https : http; - - const req = client.get(url, { + const urlObj = new URL(url); + + const options = { + hostname: urlObj.hostname, + path: urlObj.pathname + urlObj.search, + method, headers: { - 'User-Agent': USER_AGENT, - 'Accept': 'application/json' - } - }, (res) => { + Authorization: ORS_API_KEY, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }; + + const req = https.request(options, (res) => { let data = ''; - res.on('data', chunk => data += chunk); + res.on('data', (chunk) => (data += chunk)); res.on('end', () => { try { - resolve(JSON.parse(data)); + const parsed = JSON.parse(data); + if (res.statusCode >= 400) { + reject(new Error(parsed.error?.message || `HTTP ${res.statusCode}`)); + } else { + resolve(parsed); + } } catch (e) { reject(new Error('Errore parsing risposta')); } @@ -41,338 +45,436 @@ const makeRequest = (url) => { }); req.on('error', reject); - req.setTimeout(10000, () => { + req.setTimeout(15000, () => { req.destroy(); reject(new Error('Timeout richiesta')); }); + + if (body) { + req.write(JSON.stringify(body)); + } + req.end(); }); }; /** - * @desc Autocomplete città (Photon API) - * @route GET /api/trasporti/geo/autocomplete + * @desc Autocomplete città (ORS Geocode Autocomplete) + * @route GET /api/geo/autocomplete */ const autocomplete = async (req, res) => { try { - const { q, limit = 5, lang = 'it' } = req.query; + const { q, limit = 5, lang = 'it', country = 'IT' } = req.query; if (!q || q.length < 2) { return res.status(400).json({ success: false, - message: 'Query deve essere almeno 2 caratteri' + message: 'Query deve essere almeno 2 caratteri', }); } - // Photon API - gratuito e veloce - const url = `${PHOTON_BASE}/api/?q=${encodeURIComponent(q)}&limit=${limit}&lang=${lang}&osm_tag=place:city&osm_tag=place:town&osm_tag=place:village`; + const params = new URLSearchParams({ + text: q, + size: limit, + lang, + 'boundary.country': country, + layers: 'locality,county,region', // Solo città/comuni + }); + const url = `${ORS_BASE}/geocode/autocomplete?${params}`; const data = await makeRequest(url); - // Formatta risultati - const results = data.features.map(feature => ({ + const results = data.features.map((feature) => ({ + id: feature.properties.id, city: feature.properties.name, - province: feature.properties.county || feature.properties.state, - region: feature.properties.state, + locality: feature.properties.locality, + county: feature.properties.county, + region: feature.properties.region, country: feature.properties.country, - postalCode: feature.properties.postcode, + postalCode: feature.properties.postalcode, coordinates: { lat: feature.geometry.coordinates[1], - lng: feature.geometry.coordinates[0] + lng: feature.geometry.coordinates[0], }, - displayName: [ - feature.properties.name, - feature.properties.county, - feature.properties.state, - feature.properties.country - ].filter(Boolean).join(', '), - type: feature.properties.osm_value || 'place' + displayName: feature.properties.label, + type: feature.properties.layer, + confidence: feature.properties.confidence, })); res.status(200).json({ success: true, - data: results + count: results.length, + data: results, }); - } catch (error) { console.error('Errore autocomplete:', error); res.status(500).json({ success: false, message: 'Errore durante la ricerca', - error: error.message + error: error.message, }); } }; /** - * @desc Geocoding - indirizzo a coordinate (Nominatim) - * @route GET /api/trasporti/geo/geocode + * @desc Geocoding - indirizzo a coordinate (ORS Geocode Search) + * @route GET /api/geo/geocode */ const geocode = async (req, res) => { try { - const { address, city, country = 'Italy' } = req.query; + const { address, city, country = 'IT', limit = 5, lang = 'it' } = req.query; - const searchQuery = [address, city, country].filter(Boolean).join(', '); + const searchQuery = [address, city].filter(Boolean).join(', '); if (!searchQuery) { return res.status(400).json({ success: false, - message: 'Fornisci un indirizzo o città da cercare' + message: 'Fornisci un indirizzo o città da cercare', }); } - const url = `${NOMINATIM_BASE}/search?format=json&q=${encodeURIComponent(searchQuery)}&limit=5&addressdetails=1`; + const params = new URLSearchParams({ + text: searchQuery, + size: limit, + lang, + 'boundary.country': country, + }); + const url = `${ORS_BASE}/geocode/search?${params}`; const data = await makeRequest(url); - if (!data || data.length === 0) { + if (!data.features || data.features.length === 0) { return res.status(404).json({ success: false, - message: 'Nessun risultato trovato' + message: 'Nessun risultato trovato', }); } - const results = data.map(item => ({ - displayName: item.display_name, - city: item.address.city || item.address.town || item.address.village || item.address.municipality, - address: item.address.road ? `${item.address.road}${item.address.house_number ? ' ' + item.address.house_number : ''}` : null, - province: item.address.county || item.address.province, - region: item.address.state, - country: item.address.country, - postalCode: item.address.postcode, + const results = data.features.map((feature) => ({ + id: feature.properties.id, + displayName: feature.properties.label, + name: feature.properties.name, + street: feature.properties.street, + houseNumber: feature.properties.housenumber, + city: feature.properties.locality || feature.properties.county, + county: feature.properties.county, + region: feature.properties.region, + country: feature.properties.country, + postalCode: feature.properties.postalcode, coordinates: { - lat: parseFloat(item.lat), - lng: parseFloat(item.lon) + lat: feature.geometry.coordinates[1], + lng: feature.geometry.coordinates[0], }, - type: item.type, - importance: item.importance + type: feature.properties.layer, + confidence: feature.properties.confidence, })); res.status(200).json({ success: true, - data: results + count: results.length, + data: results, }); - } catch (error) { console.error('Errore geocoding:', error); res.status(500).json({ success: false, message: 'Errore durante il geocoding', - error: error.message + error: error.message, }); } }; /** - * @desc Reverse geocoding - coordinate a indirizzo (Nominatim) - * @route GET /api/trasporti/geo/reverse + * @desc Reverse geocoding - coordinate a indirizzo (ORS Reverse) + * @route GET /api/geo/reverse */ const reverseGeocode = async (req, res) => { try { - const { lat, lng } = req.query; + const { lat, lng, lang = 'it' } = req.query; if (!lat || !lng) { return res.status(400).json({ success: false, - message: 'Coordinate lat e lng richieste' + message: 'Coordinate lat e lng richieste', }); } - const url = `${NOMINATIM_BASE}/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1`; + const params = new URLSearchParams({ + 'point.lat': lat, + 'point.lon': lng, + lang, + size: '1', + layers: 'address,street,locality', + }); + const url = `${ORS_BASE}/geocode/reverse?${params}`; const data = await makeRequest(url); - if (!data || data.error) { + if (!data.features || data.features.length === 0) { return res.status(404).json({ success: false, - message: 'Nessun risultato trovato' + message: 'Nessun risultato trovato', }); } + const feature = data.features[0]; + const result = { - displayName: data.display_name, - city: data.address.city || data.address.town || data.address.village || data.address.municipality, - address: data.address.road ? `${data.address.road}${data.address.house_number ? ' ' + data.address.house_number : ''}` : null, - province: data.address.county || data.address.province, - region: data.address.state, - country: data.address.country, - postalCode: data.address.postcode, + displayName: feature.properties.label, + name: feature.properties.name, + street: feature.properties.street, + houseNumber: feature.properties.housenumber, + city: feature.properties.locality || feature.properties.county, + county: feature.properties.county, + region: feature.properties.region, + country: feature.properties.country, + postalCode: feature.properties.postalcode, coordinates: { lat: parseFloat(lat), - lng: parseFloat(lng) - } + lng: parseFloat(lng), + }, + distance: feature.properties.distance, // distanza dal punto esatto }; res.status(200).json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Errore reverse geocoding:', error); res.status(500).json({ success: false, message: 'Errore durante il reverse geocoding', - error: error.message + error: error.message, }); } }; /** - * @desc Calcola percorso tra due punti (OSRM) - * @route GET /api/trasporti/geo/route + * @desc Calcola percorso tra due o più punti (ORS Directions) + * @route POST /api/geo/route + * @body { coordinates: [[lng,lat], [lng,lat], ...], profile: 'driving-car' } */ const getRoute = async (req, res) => { try { - const { - startLat, startLng, - endLat, endLng, - waypoints // formato: "lat1,lng1;lat2,lng2;..." + const { + startLat, + startLng, + endLat, + endLng, + waypoints, // formato: "lat1,lng1;lat2,lng2;..." + profile = 'driving-car', // driving-car, driving-hgv, cycling-regular, foot-walking + language = 'it', + units = 'km', } = req.query; if (!startLat || !startLng || !endLat || !endLng) { return res.status(400).json({ success: false, - message: 'Coordinate di partenza e arrivo richieste' + message: 'Coordinate di partenza e arrivo richieste', }); } - // Costruisci stringa coordinate - let coordinates = `${startLng},${startLat}`; - + // Costruisci array coordinate [lng, lat] (formato GeoJSON) + const coordinates = [[parseFloat(startLng), parseFloat(startLat)]]; + if (waypoints) { const waypointsList = waypoints.split(';'); - waypointsList.forEach(wp => { - const [lat, lng] = wp.split(','); - coordinates += `;${lng},${lat}`; + waypointsList.forEach((wp) => { + const [lat, lng] = wp.split(',').map(parseFloat); + coordinates.push([lng, lat]); }); } - - coordinates += `;${endLng},${endLat}`; - const url = `${OSRM_BASE}/route/v1/driving/${coordinates}?overview=full&geometries=polyline&steps=true`; + coordinates.push([parseFloat(endLng), parseFloat(endLat)]); - const data = await makeRequest(url); + // Richiesta POST a ORS Directions + const url = `${ORS_BASE}/v2/directions/${profile}`; - if (!data || data.code !== 'Ok' || !data.routes || data.routes.length === 0) { + const body = { + coordinates, + language, + units, + geometry: true, + instructions: true, + maneuvers: true, + }; + + const data = await makeRequest(url, 'POST', body); + + if (!data.routes || data.routes.length === 0) { return res.status(404).json({ success: false, - message: 'Impossibile calcolare il percorso' + message: 'Impossibile calcolare il percorso', }); } const route = data.routes[0]; - - // Estrai città attraversate (dalle istruzioni) - const citiesAlongRoute = []; - if (route.legs) { - route.legs.forEach(leg => { - if (leg.steps) { - leg.steps.forEach(step => { - if (step.name && step.name.length > 0) { - // Qui potresti fare reverse geocoding per ottenere città - // Per ora usiamo i nomi delle strade principali - } - }); - } - }); - } + const summary = route.summary; const result = { - distance: Math.round(route.distance / 1000 * 10) / 10, // km - duration: Math.round(route.duration / 60), // minuti - polyline: route.geometry, // Polyline encoded - legs: route.legs.map(leg => ({ - distance: Math.round(leg.distance / 1000 * 10) / 10, - duration: Math.round(leg.duration / 60), - summary: leg.summary, - steps: leg.steps ? leg.steps.slice(0, 10).map(s => ({ // Limita step - instruction: s.maneuver ? s.maneuver.instruction : '', - name: s.name, - distance: Math.round(s.distance), - duration: Math.round(s.duration / 60) - })) : [] - })) + distance: Math.round(summary.distance * 10) / 10, // km + duration: Math.round(summary.duration / 60), // minuti + durationFormatted: formatDuration(summary.duration), + bbox: data.bbox, // Bounding box + geometry: route.geometry, // Polyline encoded + segments: route.segments.map((segment) => ({ + distance: Math.round(segment.distance * 10) / 10, + duration: Math.round(segment.duration / 60), + steps: segment.steps.map((step) => ({ + instruction: step.instruction, + name: step.name, + distance: Math.round(step.distance * 100) / 100, + duration: Math.round(step.duration / 60), + type: step.type, + maneuver: step.maneuver, + })), + })), }; res.status(200).json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Errore calcolo percorso:', error); res.status(500).json({ success: false, message: 'Errore durante il calcolo del percorso', - error: error.message + error: error.message, + }); + } +}; + +/** + * @desc Calcola matrice distanze tra più punti (ORS Matrix) + * @route POST /api/geo/matrix + */ +const getMatrix = async (req, res) => { + try { + const { locations, profile = 'driving-car' } = req.body; + + if (!locations || locations.length < 2) { + return res.status(400).json({ + success: false, + message: 'Almeno 2 location richieste', + }); + } + + // Formato locations: [[lng, lat], [lng, lat], ...] + const url = `${ORS_BASE}/v2/matrix/${profile}`; + + const body = { + locations, + metrics: ['distance', 'duration'], + units: 'km', + }; + + const data = await makeRequest(url, 'POST', body); + + const result = { + distances: data.distances, // Matrice distanze in km + durations: data.durations, // Matrice durate in secondi + sources: data.sources, + destinations: data.destinations, + }; + + res.status(200).json({ + success: true, + data: result, + }); + } catch (error) { + console.error('Errore calcolo matrice:', error); + res.status(500).json({ + success: false, + message: 'Errore durante il calcolo della matrice', + error: error.message, }); } }; /** * @desc Suggerisci città intermedie su un percorso - * @route GET /api/trasporti/geo/suggest-waypoints + * @route GET /api/geo/suggest-waypoints */ const suggestWaypoints = async (req, res) => { try { - const { startLat, startLng, endLat, endLng } = req.query; + const { startLat, startLng, endLat, endLng, count = 3 } = req.query; if (!startLat || !startLng || !endLat || !endLng) { return res.status(400).json({ success: false, - message: 'Coordinate di partenza e arrivo richieste' + message: 'Coordinate di partenza e arrivo richieste', }); } // Prima ottieni il percorso - const routeUrl = `${OSRM_BASE}/route/v1/driving/${startLng},${startLat};${endLng},${endLat}?overview=full&geometries=geojson`; + const routeUrl = `${ORS_BASE}/v2/directions/driving-car`; + const routeBody = { + coordinates: [ + [parseFloat(startLng), parseFloat(startLat)], + [parseFloat(endLng), parseFloat(endLat)], + ], + geometry: true, + }; - const routeData = await makeRequest(routeUrl); + const routeData = await makeRequest(routeUrl, 'POST', routeBody); - if (!routeData || routeData.code !== 'Ok') { + if (!routeData.routes || routeData.routes.length === 0) { return res.status(404).json({ success: false, - message: 'Impossibile calcolare il percorso' + message: 'Impossibile calcolare il percorso', }); } - // Prendi punti lungo il percorso (ogni ~50km circa) - const coordinates = routeData.routes[0].geometry.coordinates; - const totalPoints = coordinates.length; - const step = Math.max(1, Math.floor(totalPoints / 6)); // ~5 punti intermedi + // Decodifica polyline per ottenere punti + const geometry = routeData.routes[0].geometry; + const decodedPoints = decodePolyline(geometry); + + // Seleziona punti equidistanti lungo il percorso + const totalPoints = decodedPoints.length; + const step = Math.floor(totalPoints / (parseInt(count) + 1)); const sampledPoints = []; - for (let i = step; i < totalPoints - step; i += step) { - sampledPoints.push(coordinates[i]); + for (let i = 1; i <= count; i++) { + const index = Math.min(step * i, totalPoints - 1); + sampledPoints.push(decodedPoints[index]); } // Fai reverse geocoding per ogni punto const cities = []; const seenCities = new Set(); - for (const point of sampledPoints.slice(0, 5)) { // Limita a 5 richieste + for (const point of sampledPoints) { try { - const reverseUrl = `${NOMINATIM_BASE}/reverse?format=json&lat=${point[1]}&lon=${point[0]}&addressdetails=1&zoom=10`; + const params = new URLSearchParams({ + 'point.lat': point[1], + 'point.lon': point[0], + lang: 'it', + size: '1', + layers: 'locality,county', + }); + + const reverseUrl = `${ORS_BASE}/geocode/reverse?${params}`; const data = await makeRequest(reverseUrl); - - if (data && data.address) { - const cityName = data.address.city || data.address.town || data.address.village; + + if (data.features && data.features.length > 0) { + const feature = data.features[0]; + const cityName = feature.properties.locality || feature.properties.county; + if (cityName && !seenCities.has(cityName.toLowerCase())) { seenCities.add(cityName.toLowerCase()); cities.push({ city: cityName, - province: data.address.county || data.address.province, - region: data.address.state, + county: feature.properties.county, + region: feature.properties.region, coordinates: { lat: point[1], - lng: point[0] - } + lng: point[0], + }, + displayName: feature.properties.label, }); } } - - // Rate limiting - aspetta 1 secondo tra le richieste (requisito Nominatim) - await new Promise(resolve => setTimeout(resolve, 1100)); } catch (e) { console.log('Errore reverse per punto:', e.message); } @@ -380,128 +482,196 @@ const suggestWaypoints = async (req, res) => { res.status(200).json({ success: true, - data: cities + count: cities.length, + data: cities, }); - } catch (error) { console.error('Errore suggerimento waypoints:', error); res.status(500).json({ success: false, message: 'Errore durante il suggerimento delle tappe', - error: error.message + error: error.message, }); } }; /** - * @desc Cerca città italiane (ottimizzato per Italia) - * @route GET /api/trasporti/geo/cities/it + * @desc Cerca città italiane (ottimizzato) + * @route GET /api/geo/cities/it */ const searchItalianCities = async (req, res) => { try { - const { q, limit = 10 } = req.query; + const { q, limit = 10, region } = req.query; if (!q || q.length < 2) { return res.status(400).json({ success: false, - message: 'Query deve essere almeno 2 caratteri' + message: 'Query deve essere almeno 2 caratteri', }); } - // Usa Nominatim con filtro Italia - const url = `${NOMINATIM_BASE}/search?format=json&q=${encodeURIComponent(q)}&countrycodes=it&limit=${limit}&addressdetails=1&featuretype=city`; + const params = new URLSearchParams({ + text: q, + size: limit, + lang: 'it', + 'boundary.country': 'IT', + layers: 'locality,county', + }); + // Filtro opzionale per regione + if (region) { + params.append('region', region); + } + + const url = `${ORS_BASE}/geocode/search?${params}`; const data = await makeRequest(url); - const results = data - .filter(item => - item.address && - (item.address.city || item.address.town || item.address.village) - ) - .map(item => ({ - city: item.address.city || item.address.town || item.address.village, - province: item.address.county || item.address.province, - region: item.address.state, - postalCode: item.address.postcode, + const results = data.features + .filter((f) => f.properties.locality || f.properties.county) + .map((feature) => ({ + city: feature.properties.locality || feature.properties.name, + county: feature.properties.county, + region: feature.properties.region, + postalCode: feature.properties.postalcode, coordinates: { - lat: parseFloat(item.lat), - lng: parseFloat(item.lon) + lat: feature.geometry.coordinates[1], + lng: feature.geometry.coordinates[0], }, - displayName: `${item.address.city || item.address.town || item.address.village}, ${item.address.county || item.address.state}` + displayName: `${feature.properties.locality || feature.properties.name}, ${feature.properties.region}`, + confidence: feature.properties.confidence, })); // Rimuovi duplicati - const unique = results.filter((v, i, a) => - a.findIndex(t => t.city.toLowerCase() === v.city.toLowerCase()) === i + const unique = results.filter( + (v, i, a) => a.findIndex((t) => t.city?.toLowerCase() === v.city?.toLowerCase()) === i ); res.status(200).json({ success: true, - data: unique + count: unique.length, + data: unique, }); - } catch (error) { console.error('Errore ricerca città italiane:', error); res.status(500).json({ success: false, message: 'Errore durante la ricerca', - error: error.message + error: error.message, }); } }; /** - * @desc Calcola distanza e durata tra due punti - * @route GET /api/trasporti/geo/distance + * @desc Calcola distanza e durata tra due punti (semplificato) + * @route GET /api/geo/distance */ const getDistance = async (req, res) => { try { - const { startLat, startLng, endLat, endLng } = req.query; + const { startLat, startLng, endLat, endLng, profile = 'driving-car' } = req.query; if (!startLat || !startLng || !endLat || !endLng) { return res.status(400).json({ success: false, - message: 'Tutte le coordinate sono richieste' + message: 'Tutte le coordinate sono richieste', }); } - const url = `${OSRM_BASE}/route/v1/driving/${startLng},${startLat};${endLng},${endLat}?overview=false`; + const url = `${ORS_BASE}/v2/directions/${profile}`; - const data = await makeRequest(url); + const body = { + coordinates: [ + [parseFloat(startLng), parseFloat(startLat)], + [parseFloat(endLng), parseFloat(endLat)], + ], + geometry: false, + instructions: false, + }; - if (!data || data.code !== 'Ok' || !data.routes || data.routes.length === 0) { + const data = await makeRequest(url, 'POST', body); + + if (!data.routes || data.routes.length === 0) { return res.status(404).json({ success: false, - message: 'Impossibile calcolare la distanza' + message: 'Impossibile calcolare la distanza', }); } - const route = data.routes[0]; + const summary = data.routes[0].summary; res.status(200).json({ success: true, data: { - distance: Math.round(route.distance / 1000 * 10) / 10, // km - duration: Math.round(route.duration / 60), // minuti - durationFormatted: formatDuration(route.duration) - } + distance: Math.round(summary.distance * 10) / 10, // km + duration: Math.round(summary.duration / 60), // minuti + durationFormatted: formatDuration(summary.duration), + profile, + }, }); - } catch (error) { console.error('Errore calcolo distanza:', error); res.status(500).json({ success: false, message: 'Errore durante il calcolo della distanza', - error: error.message + error: error.message, }); } }; -// Helper per formattare durata +/** + * @desc Ottieni isocrone (aree raggiungibili in X minuti) + * @route GET /api/geo/isochrone + */ +const getIsochrone = async (req, res) => { + try { + const { lat, lng, minutes = 30, profile = 'driving-car' } = req.query; + + if (!lat || !lng) { + return res.status(400).json({ + success: false, + message: 'Coordinate richieste', + }); + } + + const url = `${ORS_BASE}/v2/isochrones/${profile}`; + + const body = { + locations: [[parseFloat(lng), parseFloat(lat)]], + range: [parseInt(minutes) * 60], // secondi + range_type: 'time', + }; + + const data = await makeRequest(url, 'POST', body); + + res.status(200).json({ + success: true, + data: { + type: 'FeatureCollection', + features: data.features, + center: { lat: parseFloat(lat), lng: parseFloat(lng) }, + minutes: parseInt(minutes), + }, + }); + } catch (error) { + console.error('Errore calcolo isocrone:', error); + res.status(500).json({ + success: false, + message: 'Errore durante il calcolo isocrone', + error: error.message, + }); + } +}; + +// ============================================ +// HELPER FUNCTIONS +// ============================================ + +/** + * Formatta durata in formato leggibile + */ const formatDuration = (seconds) => { const hours = Math.floor(seconds / 3600); const minutes = Math.round((seconds % 3600) / 60); - + if (hours === 0) { return `${minutes} min`; } else if (minutes === 0) { @@ -511,12 +681,55 @@ const formatDuration = (seconds) => { } }; +/** + * Decodifica polyline encoded (formato Google/ORS) + */ +const decodePolyline = (encoded) => { + const points = []; + let index = 0; + let lat = 0; + let lng = 0; + + while (index < encoded.length) { + let b; + let shift = 0; + let result = 0; + + do { + b = encoded.charCodeAt(index++) - 63; + result |= (b & 0x1f) << shift; + shift += 5; + } while (b >= 0x20); + + const dlat = result & 1 ? ~(result >> 1) : result >> 1; + lat += dlat; + + shift = 0; + result = 0; + + do { + b = encoded.charCodeAt(index++) - 63; + result |= (b & 0x1f) << shift; + shift += 5; + } while (b >= 0x20); + + const dlng = result & 1 ? ~(result >> 1) : result >> 1; + lng += dlng; + + points.push([lng / 1e5, lat / 1e5]); // [lng, lat] formato GeoJSON + } + + return points; +}; + module.exports = { autocomplete, geocode, reverseGeocode, getRoute, + getMatrix, suggestWaypoints, searchItalianCities, - getDistance -}; \ No newline at end of file + getDistance, + getIsochrone, +}; diff --git a/src/controllers/geocodingController_OLD.js b/src/controllers/geocodingController_OLD.js new file mode 100644 index 0000000..01e89fc --- /dev/null +++ b/src/controllers/geocodingController_OLD.js @@ -0,0 +1,522 @@ +/** + * Controller per Geocoding usando servizi Open Source + * - Nominatim (OpenStreetMap) per geocoding/reverse + * - OSRM per routing + * - Photon per autocomplete + */ + +const https = require('https'); +const http = require('http'); + +// Configurazione servizi +const NOMINATIM_BASE = 'https://nominatim.openstreetmap.org'; +const PHOTON_BASE = 'https://photon.komoot.io'; +const OSRM_BASE = 'https://router.project-osrm.org'; + +// User-Agent richiesto da Nominatim +const USER_AGENT = 'FreePlanetApp/1.0'; + +/** + * Helper per fare richieste HTTP/HTTPS + */ +const makeRequest = (url) => { + return new Promise((resolve, reject) => { + const client = url.startsWith('https') ? https : http; + + const req = client.get(url, { + headers: { + 'User-Agent': USER_AGENT, + 'Accept': 'application/json' + } + }, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + resolve(JSON.parse(data)); + } catch (e) { + reject(new Error('Errore parsing risposta')); + } + }); + }); + + req.on('error', reject); + req.setTimeout(10000, () => { + req.destroy(); + reject(new Error('Timeout richiesta')); + }); + }); +}; + +/** + * @desc Autocomplete città (Photon API) + * @route GET /api/viaggi/geo/autocomplete + */ +const autocomplete = async (req, res) => { + try { + const { q, limit = 5, lang = 'it' } = req.query; + + if (!q || q.length < 2) { + return res.status(400).json({ + success: false, + message: 'Query deve essere almeno 2 caratteri' + }); + } + + // Photon API - gratuito e veloce + const url = `${PHOTON_BASE}/api/?q=${encodeURIComponent(q)}&limit=${limit}&lang=${lang}&osm_tag=place:city&osm_tag=place:town&osm_tag=place:village`; + + const data = await makeRequest(url); + + // Formatta risultati + const results = data.features.map(feature => ({ + city: feature.properties.name, + province: feature.properties.county || feature.properties.state, + region: feature.properties.state, + country: feature.properties.country, + postalCode: feature.properties.postcode, + coordinates: { + lat: feature.geometry.coordinates[1], + lng: feature.geometry.coordinates[0] + }, + displayName: [ + feature.properties.name, + feature.properties.county, + feature.properties.state, + feature.properties.country + ].filter(Boolean).join(', '), + type: feature.properties.osm_value || 'place' + })); + + res.status(200).json({ + success: true, + data: results + }); + + } catch (error) { + console.error('Errore autocomplete:', error); + res.status(500).json({ + success: false, + message: 'Errore durante la ricerca', + error: error.message + }); + } +}; + +/** + * @desc Geocoding - indirizzo a coordinate (Nominatim) + * @route GET /api/viaggi/geo/geocode + */ +const geocode = async (req, res) => { + try { + const { address, city, country = 'Italy' } = req.query; + + const searchQuery = [address, city, country].filter(Boolean).join(', '); + + if (!searchQuery) { + return res.status(400).json({ + success: false, + message: 'Fornisci un indirizzo o città da cercare' + }); + } + + const url = `${NOMINATIM_BASE}/search?format=json&q=${encodeURIComponent(searchQuery)}&limit=5&addressdetails=1`; + + const data = await makeRequest(url); + + if (!data || data.length === 0) { + return res.status(404).json({ + success: false, + message: 'Nessun risultato trovato' + }); + } + + const results = data.map(item => ({ + displayName: item.display_name, + city: item.address.city || item.address.town || item.address.village || item.address.municipality, + address: item.address.road ? `${item.address.road}${item.address.house_number ? ' ' + item.address.house_number : ''}` : null, + province: item.address.county || item.address.province, + region: item.address.state, + country: item.address.country, + postalCode: item.address.postcode, + coordinates: { + lat: parseFloat(item.lat), + lng: parseFloat(item.lon) + }, + type: item.type, + importance: item.importance + })); + + res.status(200).json({ + success: true, + data: results + }); + + } catch (error) { + console.error('Errore geocoding:', error); + res.status(500).json({ + success: false, + message: 'Errore durante il geocoding', + error: error.message + }); + } +}; + +/** + * @desc Reverse geocoding - coordinate a indirizzo (Nominatim) + * @route GET /api/viaggi/geo/reverse + */ +const reverseGeocode = async (req, res) => { + try { + const { lat, lng } = req.query; + + if (!lat || !lng) { + return res.status(400).json({ + success: false, + message: 'Coordinate lat e lng richieste' + }); + } + + const url = `${NOMINATIM_BASE}/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1`; + + const data = await makeRequest(url); + + if (!data || data.error) { + return res.status(404).json({ + success: false, + message: 'Nessun risultato trovato' + }); + } + + const result = { + displayName: data.display_name, + city: data.address.city || data.address.town || data.address.village || data.address.municipality, + address: data.address.road ? `${data.address.road}${data.address.house_number ? ' ' + data.address.house_number : ''}` : null, + province: data.address.county || data.address.province, + region: data.address.state, + country: data.address.country, + postalCode: data.address.postcode, + coordinates: { + lat: parseFloat(lat), + lng: parseFloat(lng) + } + }; + + res.status(200).json({ + success: true, + data: result + }); + + } catch (error) { + console.error('Errore reverse geocoding:', error); + res.status(500).json({ + success: false, + message: 'Errore durante il reverse geocoding', + error: error.message + }); + } +}; + +/** + * @desc Calcola percorso tra due punti (OSRM) + * @route GET /api/viaggi/geo/route + */ +const getRoute = async (req, res) => { + try { + const { + startLat, startLng, + endLat, endLng, + waypoints // formato: "lat1,lng1;lat2,lng2;..." + } = req.query; + + if (!startLat || !startLng || !endLat || !endLng) { + return res.status(400).json({ + success: false, + message: 'Coordinate di partenza e arrivo richieste' + }); + } + + // Costruisci stringa coordinate + let coordinates = `${startLng},${startLat}`; + + if (waypoints) { + const waypointsList = waypoints.split(';'); + waypointsList.forEach(wp => { + const [lat, lng] = wp.split(','); + coordinates += `;${lng},${lat}`; + }); + } + + coordinates += `;${endLng},${endLat}`; + + const url = `${OSRM_BASE}/route/v1/driving/${coordinates}?overview=full&geometries=polyline&steps=true`; + + const data = await makeRequest(url); + + if (!data || data.code !== 'Ok' || !data.routes || data.routes.length === 0) { + return res.status(404).json({ + success: false, + message: 'Impossibile calcolare il percorso' + }); + } + + const route = data.routes[0]; + + // Estrai città attraversate (dalle istruzioni) + const citiesAlongRoute = []; + if (route.legs) { + route.legs.forEach(leg => { + if (leg.steps) { + leg.steps.forEach(step => { + if (step.name && step.name.length > 0) { + // Qui potresti fare reverse geocoding per ottenere città + // Per ora usiamo i nomi delle strade principali + } + }); + } + }); + } + + const result = { + distance: Math.round(route.distance / 1000 * 10) / 10, // km + duration: Math.round(route.duration / 60), // minuti + polyline: route.geometry, // Polyline encoded + legs: route.legs.map(leg => ({ + distance: Math.round(leg.distance / 1000 * 10) / 10, + duration: Math.round(leg.duration / 60), + summary: leg.summary, + steps: leg.steps ? leg.steps.slice(0, 10).map(s => ({ // Limita step + instruction: s.maneuver ? s.maneuver.instruction : '', + name: s.name, + distance: Math.round(s.distance), + duration: Math.round(s.duration / 60) + })) : [] + })) + }; + + res.status(200).json({ + success: true, + data: result + }); + + } catch (error) { + console.error('Errore calcolo percorso:', error); + res.status(500).json({ + success: false, + message: 'Errore durante il calcolo del percorso', + error: error.message + }); + } +}; + +/** + * @desc Suggerisci città intermedie su un percorso + * @route GET /api/viaggi/geo/suggest-waypoints + */ +const suggestWaypoints = async (req, res) => { + try { + const { startLat, startLng, endLat, endLng } = req.query; + + if (!startLat || !startLng || !endLat || !endLng) { + return res.status(400).json({ + success: false, + message: 'Coordinate di partenza e arrivo richieste' + }); + } + + // Prima ottieni il percorso + const routeUrl = `${OSRM_BASE}/route/v1/driving/${startLng},${startLat};${endLng},${endLat}?overview=full&geometries=geojson`; + + const routeData = await makeRequest(routeUrl); + + if (!routeData || routeData.code !== 'Ok') { + return res.status(404).json({ + success: false, + message: 'Impossibile calcolare il percorso' + }); + } + + // Prendi punti lungo il percorso (ogni ~50km circa) + const coordinates = routeData.routes[0].geometry.coordinates; + const totalPoints = coordinates.length; + const step = Math.max(1, Math.floor(totalPoints / 6)); // ~5 punti intermedi + + const sampledPoints = []; + for (let i = step; i < totalPoints - step; i += step) { + sampledPoints.push(coordinates[i]); + } + + // Fai reverse geocoding per ogni punto + const cities = []; + const seenCities = new Set(); + + for (const point of sampledPoints.slice(0, 5)) { // Limita a 5 richieste + try { + const reverseUrl = `${NOMINATIM_BASE}/reverse?format=json&lat=${point[1]}&lon=${point[0]}&addressdetails=1&zoom=10`; + const data = await makeRequest(reverseUrl); + + if (data && data.address) { + const cityName = data.address.city || data.address.town || data.address.village; + if (cityName && !seenCities.has(cityName.toLowerCase())) { + seenCities.add(cityName.toLowerCase()); + cities.push({ + city: cityName, + province: data.address.county || data.address.province, + region: data.address.state, + coordinates: { + lat: point[1], + lng: point[0] + } + }); + } + } + + // Rate limiting - aspetta 1 secondo tra le richieste (requisito Nominatim) + await new Promise(resolve => setTimeout(resolve, 1100)); + } catch (e) { + console.log('Errore reverse per punto:', e.message); + } + } + + res.status(200).json({ + success: true, + data: cities + }); + + } catch (error) { + console.error('Errore suggerimento waypoints:', error); + res.status(500).json({ + success: false, + message: 'Errore durante il suggerimento delle tappe', + error: error.message + }); + } +}; + +/** + * @desc Cerca città italiane (ottimizzato per Italia) + * @route GET /api/viaggi/geo/cities/it + */ +const searchItalianCities = async (req, res) => { + try { + const { q, limit = 10 } = req.query; + + if (!q || q.length < 2) { + return res.status(400).json({ + success: false, + message: 'Query deve essere almeno 2 caratteri' + }); + } + + // Usa Nominatim con filtro Italia + const url = `${NOMINATIM_BASE}/search?format=json&q=${encodeURIComponent(q)}&countrycodes=it&limit=${limit}&addressdetails=1&featuretype=city`; + + const data = await makeRequest(url); + + const results = data + .filter(item => + item.address && + (item.address.city || item.address.town || item.address.village) + ) + .map(item => ({ + city: item.address.city || item.address.town || item.address.village, + province: item.address.county || item.address.province, + region: item.address.state, + postalCode: item.address.postcode, + coordinates: { + lat: parseFloat(item.lat), + lng: parseFloat(item.lon) + }, + displayName: `${item.address.city || item.address.town || item.address.village}, ${item.address.county || item.address.state}` + })); + + // Rimuovi duplicati + const unique = results.filter((v, i, a) => + a.findIndex(t => t.city.toLowerCase() === v.city.toLowerCase()) === i + ); + + res.status(200).json({ + success: true, + data: unique + }); + + } catch (error) { + console.error('Errore ricerca città italiane:', error); + res.status(500).json({ + success: false, + message: 'Errore durante la ricerca', + error: error.message + }); + } +}; + +/** + * @desc Calcola distanza e durata tra due punti + * @route GET /api/viaggi/geo/distance + */ +const getDistance = async (req, res) => { + try { + const { startLat, startLng, endLat, endLng } = req.query; + + if (!startLat || !startLng || !endLat || !endLng) { + return res.status(400).json({ + success: false, + message: 'Tutte le coordinate sono richieste' + }); + } + + const url = `${OSRM_BASE}/route/v1/driving/${startLng},${startLat};${endLng},${endLat}?overview=false`; + + const data = await makeRequest(url); + + if (!data || data.code !== 'Ok' || !data.routes || data.routes.length === 0) { + return res.status(404).json({ + success: false, + message: 'Impossibile calcolare la distanza' + }); + } + + const route = data.routes[0]; + + res.status(200).json({ + success: true, + data: { + distance: Math.round(route.distance / 1000 * 10) / 10, // km + duration: Math.round(route.duration / 60), // minuti + durationFormatted: formatDuration(route.duration) + } + }); + + } catch (error) { + console.error('Errore calcolo distanza:', error); + res.status(500).json({ + success: false, + message: 'Errore durante il calcolo della distanza', + error: error.message + }); + } +}; + +// Helper per formattare durata +const formatDuration = (seconds) => { + const hours = Math.floor(seconds / 3600); + const minutes = Math.round((seconds % 3600) / 60); + + if (hours === 0) { + return `${minutes} min`; + } else if (minutes === 0) { + return `${hours} h`; + } else { + return `${hours} h ${minutes} min`; + } +}; + +module.exports = { + autocomplete, + geocode, + reverseGeocode, + getRoute, + suggestWaypoints, + searchItalianCities, + getDistance +}; \ No newline at end of file diff --git a/src/controllers/rideController.js b/src/controllers/rideController.js index b315188..64b29b9 100644 --- a/src/controllers/rideController.js +++ b/src/controllers/rideController.js @@ -1,15 +1,15 @@ const Ride = require('../models/Ride'); -const User = require('../models/user'); +const { User } = require('../models/user'); const RideRequest = require('../models/RideRequest'); /** * @desc Crea un nuovo viaggio (offerta o richiesta) - * @route POST /api/trasporti/rides + * @route POST /api/viaggi/rides * @access Private */ const createRide = async (req, res) => { try { - const { idapp } = req.body; + const idapp = req.user.idapp; const userId = req.user._id; const { @@ -111,9 +111,7 @@ const createRide = async (req, res) => { // Aggiorna profilo utente come driver se è un'offerta if (type === 'offer') { - await User.findByIdAndUpdate(userId, { - 'profile.driverProfile.isDriver': true - }); + await User.findByIdAndUpdate(userId, { $set: { 'profile.driverProfile.isDriver': true } }, { new: true }); } // Popola i dati per la risposta @@ -137,12 +135,12 @@ const createRide = async (req, res) => { /** * @desc Ottieni lista viaggi con filtri - * @route GET /api/trasporti/rides + * @route GET /api/viaggi/rides * @access Public */ const getRides = async (req, res) => { try { - const { idapp } = req.query; + const idapp = req.query.idapp; const { type, @@ -272,15 +270,14 @@ const getRides = async (req, res) => { /** * @desc Ottieni singolo viaggio per ID - * @route GET /api/trasporti/rides/:id + * @route GET /api/viaggi/rides/:id * @access Public */ const getRideById = async (req, res) => { try { const { id } = req.params; - const { idapp } = req.query; - const ride = await Ride.findOne({ _id: id, idapp }) + const ride = await Ride.findOne({ _id: id }) .populate('userId', 'username name surname profile.img profile.Biografia profile.driverProfile') .populate('confirmedPassengers.userId', 'username name surname profile.img') .populate('contribution.contribTypes.contribTypeId'); @@ -313,14 +310,15 @@ const getRideById = async (req, res) => { /** * @desc Aggiorna un viaggio - * @route PUT /api/trasporti/rides/:id + * @route PUT /api/viaggi/rides/:id * @access Private */ const updateRide = async (req, res) => { try { const { id } = req.params; const userId = req.user._id; - const { idapp, ...updateData } = req.body; + const idapp = req.user.idapp; + const { ...updateData } = req.body; // Trova il viaggio const ride = await Ride.findOne({ _id: id, idapp }); @@ -333,7 +331,7 @@ const updateRide = async (req, res) => { } // Verifica proprietario - if (ride.userId.toString() !== userId) { + if (!ride.userId.equals(userId)) { return res.status(403).json({ success: false, message: 'Non sei autorizzato a modificare questo viaggio' @@ -400,14 +398,15 @@ const updateRide = async (req, res) => { /** * @desc Cancella un viaggio - * @route DELETE /api/trasporti/rides/:id + * @route DELETE /api/viaggi/rides/:id * @access Private */ const deleteRide = async (req, res) => { try { const { id } = req.params; const userId = req.user._id; - const { idapp, reason } = req.body; + const idapp = req.user.idapp; + const { reason } = req.body; const ride = await Ride.findOne({ _id: id, idapp }); @@ -419,7 +418,7 @@ const deleteRide = async (req, res) => { } // Verifica proprietario - if (ride.userId.toString() !== userId) { + if (!ride.userId.equals(userId)) { return res.status(403).json({ success: false, message: 'Non sei autorizzato a cancellare questo viaggio' @@ -463,13 +462,14 @@ const deleteRide = async (req, res) => { /** * @desc Ottieni viaggi dell'utente corrente - * @route GET /api/trasporti/rides/my + * @route GET /api/viaggi/rides/my * @access Private */ const getMyRides = async (req, res) => { try { const userId = req.user._id; - const { idapp, type, role, status, page = 1, limit = 20 } = req.query; + const idapp = req.query.idapp; + const { type, role, status, page = 1, limit = 20 } = req.query; let query = { idapp }; @@ -539,12 +539,12 @@ const getMyRides = async (req, res) => { /** * @desc Cerca viaggi con match intelligente - * @route GET /api/trasporti/rides/search + * @route GET /api/viaggi/rides/search * @access Public */ const searchRides = async (req, res) => { try { - const { idapp } = req.query; + const idapp = req.query.idapp; const { from, @@ -659,14 +659,14 @@ const searchRides = async (req, res) => { /** * @desc Completa un viaggio - * @route POST /api/trasporti/rides/:id/complete + * @route POST /api/viaggi/rides/:id/complete * @access Private */ const completeRide = async (req, res) => { try { const { id } = req.params; const userId = req.user._id; - const { idapp } = req.body; + const idapp = req.user.idapp; const ride = await Ride.findOne({ _id: id, idapp }); @@ -677,7 +677,7 @@ const completeRide = async (req, res) => { }); } - if (ride.userId.toString() !== userId) { + if (!ride.userId.equals(userId)) { return res.status(403).json({ success: false, message: 'Solo il conducente può completare il viaggio' @@ -723,12 +723,12 @@ const completeRide = async (req, res) => { /** * @desc Ottieni statistiche viaggi per homepage widget - * @route GET /api/trasporti/rides/stats + * @route GET /api/viaggi/rides/stats * @access Private */ const getRidesStats = async (req, res) => { try { - const { idapp } = req.query; + const idapp = req.query.idapp; const userId = req.user._id; const now = new Date(); @@ -800,7 +800,7 @@ const getRidesStats = async (req, res) => { /** * Get aggregated data for dashboard widgets - * GET /api/trasporti/widget/data + * GET /api/viaggi/widget/data */ const getWidgetData = async (req, res) => { try { @@ -981,7 +981,7 @@ const getWidgetData = async (req, res) => { /** * Get comprehensive statistics summary for user - * GET /api/trasporti/stats/summary + * GET /api/viaggi/stats/summary */ const getStatsSummary = async (req, res) => { try { @@ -1420,7 +1420,7 @@ const getStatsSummary = async (req, res) => { /** * Get city suggestions for autocomplete - * GET /api/trasporti/cities/suggestions?q=query + * GET /api/viaggi/cities/suggestions?q=query */ const getCitySuggestions = async (req, res) => { try { @@ -1679,7 +1679,7 @@ const getCitySuggestions = async (req, res) => { /** * Get recent cities from user's trip history - * GET /api/trasporti/cities/recent + * GET /api/viaggi/cities/recent */ const getRecentCities = async (req, res) => { try { diff --git a/src/controllers/rideRequestController.js b/src/controllers/rideRequestController.js index c4278b3..56c65cb 100644 --- a/src/controllers/rideRequestController.js +++ b/src/controllers/rideRequestController.js @@ -5,7 +5,7 @@ const Message = require('../models/Message'); /** * @desc Crea una richiesta di passaggio - * @route POST /api/trasporti/requests + * @route POST /api/viaggi/requests * @access Private */ const createRequest = async (req, res) => { @@ -154,7 +154,7 @@ const createRequest = async (req, res) => { /** * @desc Ottieni le richieste per un viaggio (per il conducente) - * @route GET /api/trasporti/requests/ride/:rideId + * @route GET /api/viaggi/requests/ride/:rideId * @access Private */ const getRequestsForRide = async (req, res) => { @@ -207,7 +207,7 @@ const getRequestsForRide = async (req, res) => { /** * @desc Ottieni le mie richieste (come passeggero) - * @route GET /api/trasporti/requests/my + * @route GET /api/viaggi/requests/my * @access Private */ const getMyRequests = async (req, res) => { @@ -266,13 +266,13 @@ const getMyRequests = async (req, res) => { /** * @desc Ottieni richieste pendenti (per il conducente) - * @route GET /api/trasporti/requests/pending + * @route GET /api/viaggi/requests/pending * @access Private */ const getPendingRequests = async (req, res) => { try { const userId = req.user._id; - const { idapp } = req.query; + const idapp = req.user.idapp; if (!idapp) { return res.status(400).json({ @@ -307,7 +307,7 @@ const getPendingRequests = async (req, res) => { /** * @desc Accetta una richiesta di passaggio - * @route PUT /api/trasporti/requests/:id/accept + * @route PUT /api/viaggi/requests/:id/accept * @access Private (solo conducente) */ const acceptRequest = async (req, res) => { @@ -403,7 +403,7 @@ const acceptRequest = async (req, res) => { /** * @desc Rifiuta una richiesta di passaggio - * @route PUT /api/trasporti/requests/:id/reject + * @route PUT /api/viaggi/requests/:id/reject * @access Private (solo conducente) */ const rejectRequest = async (req, res) => { @@ -477,7 +477,7 @@ const rejectRequest = async (req, res) => { /** * @desc Cancella una richiesta (dal passeggero) - * @route PUT /api/trasporti/requests/:id/cancel + * @route PUT /api/viaggi/requests/:id/cancel * @access Private */ const cancelRequest = async (req, res) => { @@ -550,7 +550,7 @@ const cancelRequest = async (req, res) => { /** * @desc Ottieni una singola richiesta - * @route GET /api/trasporti/requests/:id + * @route GET /api/viaggi/requests/:id * @access Private */ const getRequestById = async (req, res) => { @@ -604,7 +604,7 @@ const getRequestById = async (req, res) => { /** * @desc Ottieni richieste ricevute (io come conducente) - * @route GET /api/trasporti/requests/received + * @route GET /api/viaggi/requests/received * @access Private */ const getReceivedRequests = async (req, res) => { @@ -684,7 +684,7 @@ const getReceivedRequests = async (req, res) => { }; /** * @desc Ottieni richieste inviate (io come passeggero) - * @route GET /api/trasporti/requests/sent + * @route GET /api/viaggi/requests/sent * @access Private */ const getSentRequests = async (req, res) => { diff --git a/src/models/Chat.js b/src/models/Chat.js index 613a82b..edf5e17 100644 --- a/src/models/Chat.js +++ b/src/models/Chat.js @@ -79,6 +79,16 @@ const ChatSchema = new Schema( ref: 'User', }, ], + deletedBy: [ + { + type: Schema.Types.ObjectId, + ref: 'User', + }, + ], + clearedBefore: { + type: Map, + of: Date, + }, metadata: { type: Schema.Types.Mixed, }, @@ -114,19 +124,17 @@ ChatSchema.methods.getUnreadForUser = function (userId) { // ✅ FIX: incrementUnread (assicura conversione corretta) ChatSchema.methods.incrementUnread = function (excludeUserId) { const excludeIdStr = excludeUserId.toString(); - + this.participants.forEach((participantId) => { // Gestisci sia ObjectId che oggetti popolati - const id = participantId._id - ? participantId._id.toString() - : participantId.toString(); - + const id = participantId._id ? participantId._id.toString() : participantId.toString(); + if (id !== excludeIdStr) { const current = this.unreadCount.get(id) || 0; this.unreadCount.set(id, current + 1); } }); - + return this.save(); }; @@ -151,7 +159,7 @@ ChatSchema.methods.updateLastMessage = function (message) { // ✅ FIX: Gestisce sia ObjectId che oggetti User popolati ChatSchema.methods.hasParticipant = function (userId) { const userIdStr = userId.toString(); - + return this.participants.some((p) => { // Se p è un oggetto popolato (ha _id), usa p._id // Altrimenti p è già un ObjectId @@ -164,14 +172,13 @@ ChatSchema.methods.hasParticipant = function (userId) { // ✅ FIX: Metodo isBlockedFor (stesso problema) ChatSchema.methods.isBlockedFor = function (userId) { const userIdStr = userId.toString(); - + return this.blockedBy.some((id) => { const blockedId = id._id ? id._id.toString() : id.toString(); return blockedId === userIdStr; }); }; - // Metodo statico per trovare o creare una chat diretta ChatSchema.statics.findOrCreateDirect = async function (idapp, userId1, userId2, rideId = null) { // Cerca chat esistente tra i due utenti diff --git a/src/models/Ride.js b/src/models/Ride.js index 9d5b1df..b43a0fb 100644 --- a/src/models/Ride.js +++ b/src/models/Ride.js @@ -55,6 +55,10 @@ const LocationSchema = new Schema( // Schema per i waypoint (tappe intermedie) const WaypointSchema = new Schema({ + /*_id: { + type: String, + required: false + },*/ location: { type: LocationSchema, required: true, @@ -70,7 +74,7 @@ const WaypointSchema = new Schema({ type: Number, // minuti di sosta default: 0, }, -}); +}, { _id: false }); // 👈 AGGIUNGI QUESTO // Schema per la ricorrenza del viaggio const RecurrenceSchema = new Schema( diff --git a/src/models/user.js b/src/models/user.js index 70c1726..0e5a304 100755 --- a/src/models/user.js +++ b/src/models/user.js @@ -606,7 +606,6 @@ const UserSchema = new mongoose.Schema( { type: { type: String, - enum: ['auto', 'moto', 'furgone', 'minibus', 'altro'], default: 'auto', }, brand: { @@ -640,7 +639,6 @@ const UserSchema = new mongoose.Schema( features: [ { type: String, - enum: ['aria_condizionata', 'wifi', 'presa_usb', 'bluetooth', 'bagagliaio_grande', 'seggiolino_bimbi'], }, ], photos: [ @@ -704,7 +702,6 @@ const UserSchema = new mongoose.Schema( }, responseTime: { type: String, - enum: ['within_hour', 'within_day', 'within_days'], default: 'within_day', }, totalKmShared: { @@ -743,22 +740,18 @@ const UserSchema = new mongoose.Schema( // Preferenze di viaggio smoking: { type: String, - enum: ['yes', 'no', 'outside_only'], default: 'no', }, pets: { type: String, - enum: ['no', 'small', 'medium', 'large', 'all'], default: 'small', }, music: { type: String, - enum: ['no_music', 'quiet', 'moderate', 'loud', 'passenger_choice'], default: 'moderate', }, conversation: { type: String, - enum: ['quiet', 'moderate', 'chatty'], default: 'moderate', }, @@ -835,7 +828,6 @@ const UserSchema = new mongoose.Schema( }, type: { type: String, - enum: ['home', 'work', 'other'], default: 'other', }, }, diff --git a/src/router/api_router.js b/src/router/api_router.js index fe7f488..47f5e2c 100644 --- a/src/router/api_router.js +++ b/src/router/api_router.js @@ -13,7 +13,6 @@ const PageView = require('../models/PageView'); const fal = require('@fal-ai/client'); - const imageGenerator = require('../services/imageGenerator'); // Assicurati che il percorso sia corretto const posterEditor = require('../services/PosterEditor'); // <--- Importa la nuova classe @@ -33,9 +32,8 @@ const { MyElem } = require('../models/myelem'); const axios = require('axios'); -const trasportiRoutes = require('../routes/trasportiRoutes'); -router.use('/trasporti', trasportiRoutes); - +const viaggiRoutes = require('../routes/viaggiRoutes'); +router.use('/viaggi', viaggiRoutes); // Importa le routes video const videoRoutes = require('../routes/videoRoutes'); @@ -43,7 +41,6 @@ const videoRoutes = require('../routes/videoRoutes'); // Monta le routes video router.use('/video', videoRoutes); - router.use('/templates', authenticate, templatesRouter); router.use('/posters', authenticate, postersRouter); router.use('/assets', authenticate, assetsRouter); @@ -523,9 +520,16 @@ router.post('/chatbot', authenticate, async (req, res) => { }); router.post('/generateposter', async (req, res) => { - const { - titolo, data, ora, luogo, descrizione, contatti, fotoDescrizione, stile, - provider = 'hf' // Default a HF (Gratis) + const { + titolo, + data, + ora, + luogo, + descrizione, + contatti, + fotoDescrizione, + stile, + provider = 'hf', // Default a HF (Gratis) } = req.body; // 1. Prompt per l'AI: Chiediamo SOLO lo sfondo, VIETIAMO il testo. @@ -547,21 +551,50 @@ router.post('/generateposter', async (req, res) => { data, ora, luogo, - contatti + contatti, }); - res.json({ - success: true, + res.json({ + success: true, imageUrl: finalPosterBase64, // Restituisce l'immagine completa in base64 - step: 'AI + Canvas Composition' + step: 'AI + Canvas Composition', }); - } catch (err) { console.error('Errore:', err.message); res.status(500).json({ error: err.message }); } }); +router.get('/users/search', authenticate, async (req, res) => { + try { + const { User } = require('../models/user'); + + const { q, idapp } = req.query; + + if (!q || q.length < 2) { + return res.status(400).json({ success: false, message: 'Query too short' }); + } + + const query = q.trim(); + + const users = await User.find({ + idapp, + $or: [ + { name: { $regex: query, $options: 'i' } }, + { surname: { $regex: query, $options: 'i' } }, + { username: { $regex: query, $options: 'i' } }, + ], + _id: { $ne: req.user?._id }, // escludi l'utente corrente se autenticato + }) + .select('_id name surname username profile') // solo campi necessari + .limit(10); // evita overload + + res.json({ success: true, data: users }); + } catch (error) { + console.error('User search error:', error); + res.status(500).json({ success: false, message: 'Server error' }); + } +}); module.exports = router; diff --git a/src/router/users_router.js b/src/router/users_router.js index 4917dd8..b98626f 100755 --- a/src/router/users_router.js +++ b/src/router/users_router.js @@ -317,7 +317,11 @@ router.post('/', async (req, res) => { await telegrambot.askConfirmationUser(myuser.idapp, shared_consts.CallFunz.REGISTRATION, myuser); const { token, refreshToken, browser_random } = await myuser.generateAuthToken(req, browser_random); - res.header('x-auth', token).header('x-refrtok', refreshToken).header('x-browser-random', browser_random).send(myuser); + res + .header('x-auth', token) + .header('x-refrtok', refreshToken) + .header('x-browser-random', browser_random) + .send(myuser); return true; } } @@ -368,7 +372,11 @@ router.post('/', async (req, res) => { // if (!tools.testing()) { await sendemail.sendEmail_Registration(user.lang, user.email, user, user.idapp, user.linkreg); // } - res.header('x-auth', ris.token).header('x-refrtok', ris.refreshToken).header('x-browser-random', ris.browser_random).send(user); + res + .header('x-auth', ris.token) + .header('x-refrtok', ris.refreshToken) + .header('x-browser-random', ris.browser_random) + .send(user); return true; }); }) @@ -411,7 +419,9 @@ router.patch('/:id', authenticate, (req, res) => { if (!User.isAdmin(req.user.perm)) { // If without permissions, exit - return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' }); + return res + .status(server_constants.RIS_CODE_ERR_UNAUTHORIZED) + .send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' }); } User.findByIdAndUpdate(id, { $set: body }) @@ -512,7 +522,7 @@ router.post('/profile', authenticate, (req, res) => { try { // Check if ìs a Notif to read - if (idnotif) { + if (idnotif) { SendNotif.setNotifAsRead(idapp, usernameOrig, idnotif); } @@ -591,9 +601,14 @@ router.post('/panel', authenticate, async (req, res) => { idapp = req.body.idapp; locale = req.body.locale; - if (!req.user || !User.isAdmin(req.user.perm) && !User.isManager(req.user.perm) && !User.isFacilitatore(req.user.perm)) { + if ( + !req.user || + (!User.isAdmin(req.user.perm) && !User.isManager(req.user.perm) && !User.isFacilitatore(req.user.perm)) + ) { // If without permissions, exit - return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' }); + return res + .status(server_constants.RIS_CODE_ERR_UNAUTHORIZED) + .send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' }); } try { @@ -669,7 +684,7 @@ router.post('/newtok', async (req, res) => { } const recFound = await User.findByRefreshTokenAnyAccess(refreshToken); - + if (!recFound) { return res.status(403).send({ error: 'Refresh token non valido' }); } @@ -953,7 +968,9 @@ router.post('/friends/cmd', authenticate, async (req, res) => { usernameDest !== usernameLogged && (cmd === shared_consts.FRIENDSCMD.SETFRIEND || cmd === shared_consts.FRIENDSCMD.SETHANDSHAKE) ) { - return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' }); + return res + .status(server_constants.RIS_CODE_ERR_UNAUTHORIZED) + .send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' }); } } @@ -1119,7 +1136,10 @@ async function eseguiDbOpUser(idapp, mydata, locale, req, res) { } else if (mydata.dbop === 'noNameSurname') { await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noNameSurname': mydata.value } }); } else if (mydata.dbop === 'telegram_verification_skipped') { - await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.telegram_verification_skipped': mydata.value } }); + await User.findOneAndUpdate( + { _id: mydata._id }, + { $set: { 'profile.telegram_verification_skipped': mydata.value } } + ); } else if (mydata.dbop === 'pwdLikeAdmin') { await User.setPwdComeQuellaDellAdmin(mydata); } else if (mydata.dbop === 'ripristinaPwdPrec') { @@ -1129,10 +1149,10 @@ async function eseguiDbOpUser(idapp, mydata, locale, req, res) { } else if (mydata.dbop === 'noComune') { await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noComune': mydata.value } }); } else if (mydata.dbop === 'verifiedemail') { - await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'verified_email': mydata.value } }); + await User.findOneAndUpdate({ _id: mydata._id }, { $set: { verified_email: mydata.value } }); } else if (mydata.dbop === 'resendVerificationEmail') { // Invia la email di Verifica email - const ris = await sendemail.sendEmail_ReVerifyingEmail(mydata, idapp); + const ris = await sendemail.sendEmail_ReVerifyingEmail(mydata, idapp); } else if (mydata.dbop === 'noCircIta') { await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noCircIta': mydata.value } }); } else if (mydata.dbop === 'insert_circuito_ita') { diff --git a/src/routes/geoRoutes.js b/src/routes/geoRoutes.js new file mode 100644 index 0000000..1560234 --- /dev/null +++ b/src/routes/geoRoutes.js @@ -0,0 +1,37 @@ +const express = require('express'); +const router = express.Router(); +const { + autocomplete, + geocode, + reverseGeocode, + getRoute, + getMatrix, + suggestWaypoints, + searchItalianCities, + getDistance, + getIsochrone +} = require('../controllers/geocodingController'); + +// Rate limiting opzionale +const rateLimit = require('express-rate-limit'); + +const geoLimiter = rateLimit({ + windowMs: 60 * 1000, // 1 minuto + max: 60, // 60 richieste per minuto + message: { success: false, message: 'Troppe richieste, riprova tra poco' } +}); + +router.use(geoLimiter); + +// Routes +router.get('/autocomplete', autocomplete); +router.get('/geocode', geocode); +router.get('/reverse', reverseGeocode); +router.get('/route', getRoute); +router.post('/matrix', getMatrix); +router.get('/suggest-waypoints', suggestWaypoints); +router.get('/cities/it', searchItalianCities); +router.get('/distance', getDistance); +router.get('/isochrone', getIsochrone); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/trasportiRoutes.js b/src/routes/viaggiRoutes.js similarity index 82% rename from src/routes/trasportiRoutes.js rename to src/routes/viaggiRoutes.js index 3837591..7be50d9 100644 --- a/src/routes/trasportiRoutes.js +++ b/src/routes/viaggiRoutes.js @@ -14,7 +14,12 @@ const rideController = require('../controllers/rideController'); const rideRequestController = require('../controllers/rideRequestController'); const chatController = require('../controllers/chatController'); const feedbackController = require('../controllers/feedbackController'); -const geocodingController = require('../controllers/geocodingController'); +// const geocodingController = require('../controllers/geocodingController'); + +const geoRoutes = require('./geoRoutes'); // 👈 Importa geoRoutes + +router.use('/geo', geoRoutes); // 👈 Monta come sub-router + // Middleware di autenticazione (usa il tuo esistente) const { authenticate } = require('../middleware/authenticate'); @@ -24,42 +29,42 @@ const { authenticate } = require('../middleware/authenticate'); // ============================================================ /** - * @route POST /api/trasporti/rides + * @route POST /api/viaggi/rides * @desc Crea nuovo viaggio (offerta o richiesta) * @access Private */ router.post('/rides', authenticate, rideController.createRide); /** - * @route GET /api/trasporti/rides + * @route GET /api/viaggi/rides * @desc Ottieni lista viaggi con filtri * @access Public */ router.get('/rides', rideController.getRides); /** - * @route GET /api/trasporti/rides/search + * @route GET /api/viaggi/rides/search * @desc Ricerca viaggi avanzata * @access Public */ router.get('/rides/search', rideController.searchRides); /** - * @route GET /api/trasporti/rides/stats + * @route GET /api/viaggi/rides/stats * @desc Statistiche per widget homepage * @access Private */ router.get('/rides/stats', authenticate, rideController.getRidesStats); /** - * @route GET /api/trasporti/rides/my + * @route GET /api/viaggi/rides/my * @desc I miei viaggi (come driver e passenger) * @access Private */ router.get('/rides/my', authenticate, rideController.getMyRides); /** - * @route GET /api/trasporti/rides/match + * @route GET /api/viaggi/rides/match * @desc Match automatico offerta/richiesta * @access Private * @note ⚠️ IMPORTANTE: Questa route DEVE stare PRIMA di /rides/:id @@ -67,28 +72,28 @@ router.get('/rides/my', authenticate, rideController.getMyRides); //router.get('/rides/match', authenticate, rideController.findMatches); /** - * @route GET /api/trasporti/rides/:id + * @route GET /api/viaggi/rides/:id * @desc Dettaglio singolo viaggio * @access Public */ router.get('/rides/:id', rideController.getRideById); /** - * @route PUT /api/trasporti/rides/:id + * @route PUT /api/viaggi/rides/:id * @desc Aggiorna viaggio * @access Private (solo owner) */ router.put('/rides/:id', authenticate, rideController.updateRide); /** - * @route DELETE /api/trasporti/rides/:id + * @route DELETE /api/viaggi/rides/:id * @desc Cancella viaggio * @access Private (solo owner) */ router.delete('/rides/:id', authenticate, rideController.deleteRide); /** - * @route POST /api/trasporti/rides/:id/complete + * @route POST /api/viaggi/rides/:id/complete * @desc Completa un viaggio * @access Private (solo driver) */ @@ -99,28 +104,28 @@ router.post('/rides/:id/complete', authenticate, rideController.completeRide); // ============================================================ /** - * @route GET /api/trasporti/widget/data + * @route GET /api/viaggi/widget/data * @desc Dati completi per widget homepage * @access Private */ router.get('/widget/data', authenticate, rideController.getWidgetData); /** - * @route GET /api/trasporti/stats/summary + * @route GET /api/viaggi/stats/summary * @desc Stats rapide per header widget (offerte, richieste, match) * @access Public */ router.get('/stats/summary', authenticate, rideController.getStatsSummary); /** - * @route GET /api/trasporti/cities/suggestions + * @route GET /api/viaggi/cities/suggestions * @desc Suggerimenti città per autocomplete (basato su viaggi esistenti) * @access Public */ router.get('/cities/suggestions', rideController.getCitySuggestions); /** - * @route GET /api/trasporti/cities/recents + * @route GET /api/viaggi/cities/recents * @desc città recenti per autocomplete * @access Public */ @@ -131,56 +136,56 @@ router.get('/cities/recent', authenticate, rideController.getRecentCities); // ============================================================ /** - * @route POST /api/trasporti/requests + * @route POST /api/viaggi/requests * @desc Crea richiesta passaggio per un viaggio * @access Private */ router.post('/requests', authenticate, rideRequestController.createRequest); /** - * @route GET /api/trasporti/requests/received + * @route GET /api/viaggi/requests/received * @desc Richieste ricevute (sono conducente) * @access Private */ router.get('/requests/received', authenticate, rideRequestController.getReceivedRequests); /** - * @route GET /api/trasporti/requests/sent + * @route GET /api/viaggi/requests/sent * @desc Richieste inviate (sono passeggero) * @access Private */ router.get('/requests/sent', authenticate, rideRequestController.getSentRequests); /** - * @route GET /api/trasporti/requests/ride/:rideId + * @route GET /api/viaggi/requests/ride/:rideId * @desc Richieste per un viaggio specifico * @access Private (solo owner del viaggio) */ router.get('/requests/ride/:rideId', authenticate, rideRequestController.getRequestsForRide); /** - * @route GET /api/trasporti/requests/:id + * @route GET /api/viaggi/requests/:id * @desc Dettaglio singola richiesta * @access Private (driver o passenger) */ router.get('/requests/:id', authenticate, rideRequestController.getRequestById); /** - * @route POST /api/trasporti/requests/:id/accept + * @route POST /api/viaggi/requests/:id/accept * @desc Accetta richiesta passaggio * @access Private (solo driver) */ router.post('/requests/:id/accept', authenticate, rideRequestController.acceptRequest); /** - * @route POST /api/trasporti/requests/:id/reject + * @route POST /api/viaggi/requests/:id/reject * @desc Rifiuta richiesta passaggio * @access Private (solo driver) */ router.post('/requests/:id/reject', authenticate, rideRequestController.rejectRequest); /** - * @route POST /api/trasporti/requests/:id/cancel + * @route POST /api/viaggi/requests/:id/cancel * @desc Cancella richiesta/prenotazione * @access Private (driver o passenger) */ @@ -191,72 +196,72 @@ router.post('/requests/:id/cancel', authenticate, rideRequestController.cancelRe // ============================================================ /** - * @route GET /api/trasporti/chats + * @route GET /api/viaggi/chats * @desc Lista tutte le mie chat * @access Private */ -router.get('/chats', authenticate, chatController.getMyChats); +router.get('/chats', authenticate, chatController.getUserChats); /** - * @route GET /api/trasporti/chats/unread/count + * @route GET /api/viaggi/chats/unread/count * @desc Conta messaggi non letti totali * @access Private */ router.get('/chats/unread/count', authenticate, chatController.getUnreadCount); /** - * @route POST /api/trasporti/chats/direct + * @route POST /api/viaggi/chats/direct * @desc Ottieni o crea chat diretta con utente * @access Private */ router.post('/chats/direct', authenticate, chatController.getOrCreateDirectChat); /** - * @route GET /api/trasporti/chats/:id + * @route GET /api/viaggi/chats/:id * @desc Dettaglio chat * @access Private (solo partecipanti) */ -router.get('/chats/:id', authenticate, chatController.getChatById); +router.get('/chats/:chatId', authenticate, chatController.getChatById); /** - * @route GET /api/trasporti/chats/:id/messages + * @route GET /api/viaggi/chats/:id/messages * @desc Messaggi di una chat * @access Private (solo partecipanti) */ -router.get('/chats/:id/messages', authenticate, chatController.getChatMessages); +router.get('/chats/:chatId/messages', authenticate, chatController.getChatMessages); /** - * @route POST /api/trasporti/chats/:id/messages + * @route POST /api/viaggi/chats/:id/messages * @desc Invia messaggio * @access Private (solo partecipanti) */ -router.post('/chats/:id/messages', authenticate, chatController.sendMessage); +router.post('/chats/:chatId/messages', authenticate, chatController.sendMessage); /** - * @route PUT /api/trasporti/chats/:id/read + * @route PUT /api/viaggi/chats/:id/read * @desc Segna chat come letta * @access Private (solo partecipanti) * @fix Corretto: markAsRead → markChatAsRead */ -router.put('/chats/:id/read', authenticate, chatController.markChatAsRead); +router.put('/chats/:chatId/read', authenticate, chatController.markChatAsRead); /** - * @route PUT /api/trasporti/chats/:id/block + * @route PUT /api/viaggi/chats/:id/block * @desc Blocca/sblocca chat * @access Private (solo partecipanti) */ -router.put('/chats/:id/block', authenticate, chatController.toggleBlockChat); +router.put('/chats/:chatId/block', authenticate, chatController.toggleBlockChat); /** - * @route PUT /api/trasporti/chats/:id/mute + * @route PUT /api/viaggi/chats/:id/mute * @desc Muta/smuta notifiche di una chat * @access Private (solo partecipanti) * @fix Aggiunta route mancante */ -router.put('/chats/:id/mute', authenticate, chatController.toggleMuteChat); +router.put('/chats/:chatId/mute', authenticate, chatController.toggleMuteChat); /** - * @route DELETE /api/trasporti/chats/:chatId/messages/:messageId + * @route DELETE /api/viaggi/chats/:chatId/messages/:messageId * @desc Elimina messaggio * @access Private (solo mittente) * @fix Corretto: /messages/:id → /chats/:chatId/messages/:messageId @@ -264,81 +269,81 @@ router.put('/chats/:id/mute', authenticate, chatController.toggleMuteChat); router.delete('/chats/:chatId/messages/:messageId', authenticate, chatController.deleteMessage); /** - * @route DELETE /api/trasporti/chats/:id + * @route DELETE /api/viaggi/chats/:id * @desc Elimina chat (soft delete) * @access Private (solo partecipanti) */ -router.delete('/chats/:id', authenticate, chatController.deleteChat); +router.delete('/chats/:chatId', authenticate, chatController.deleteChat); // ============================================================ // ⭐ FEEDBACK - Recensioni // ============================================================ /** - * @route POST /api/trasporti/feedback + * @route POST /api/viaggi/feedback * @desc Crea feedback per un viaggio * @access Private */ router.post('/feedback', authenticate, feedbackController.createFeedback); /** - * @route GET /api/trasporti/feedback/my/received + * @route GET /api/viaggi/feedback/my/received * @desc I miei feedback ricevuti * @access Private */ router.get('/feedback/my/received', authenticate, feedbackController.getMyReceivedFeedbacks); /** - * @route GET /api/trasporti/feedback/my/given + * @route GET /api/viaggi/feedback/my/given * @desc I miei feedback lasciati * @access Private */ router.get('/feedback/my/given', authenticate, feedbackController.getMyGivenFeedbacks); /** - * @route GET /api/trasporti/feedback/user/:userId + * @route GET /api/viaggi/feedback/user/:userId * @desc Feedback di un utente * @access Public */ router.get('/feedback/user/:userId', feedbackController.getFeedbacksForUser); /** - * @route GET /api/trasporti/feedback/user/:userId/stats + * @route GET /api/viaggi/feedback/user/:userId/stats * @desc Statistiche feedback utente * @access Public */ router.get('/feedback/user/:userId/stats', feedbackController.getUserFeedbackStats); /** - * @route GET /api/trasporti/feedback/ride/:rideId + * @route GET /api/viaggi/feedback/ride/:rideId * @desc Feedback per un viaggio * @access Public */ router.get('/feedback/ride/:rideId', feedbackController.getRideFeedback); /** - * @route GET /api/trasporti/feedback/can-leave/:rideId/:toUserId + * @route GET /api/viaggi/feedback/can-leave/:rideId/:toUserId * @desc Verifica se posso lasciare feedback * @access Private */ router.get('/feedback/can-leave/:rideId/:toUserId', authenticate, feedbackController.canLeaveFeedback); /** - * @route POST /api/trasporti/feedback/:id/response + * @route POST /api/viaggi/feedback/:id/response * @desc Rispondi a un feedback * @access Private (solo destinatario) */ router.post('/feedback/:id/response', authenticate, feedbackController.respondToFeedback); /** - * @route POST /api/trasporti/feedback/:id/report + * @route POST /api/viaggi/feedback/:id/report * @desc Segnala feedback * @access Private */ router.post('/feedback/:id/report', authenticate, feedbackController.reportFeedback); /** - * @route POST /api/trasporti/feedback/:id/helpful + * @route POST /api/viaggi/feedback/:id/helpful * @desc Segna feedback come utile * @access Private */ @@ -348,68 +353,68 @@ router.post('/feedback/:id/helpful', authenticate, feedbackController.markAsHelp // 🗺️ GEO - Geocoding & Mappe (Open Source) // ============================================================ -/** - * @route GET /api/trasporti/geo/autocomplete - * @desc Autocomplete città (Photon) - * @access Public - */ -router.get('/geo/autocomplete', geocodingController.autocomplete); +// /* /** +// * @route GET /api/viaggi/geo/autocomplete +// * @desc Autocomplete città (Photon) +// * @access Public +// */ +// router.get('/geo/autocomplete', geocodingController.autocomplete); -/** - * @route GET /api/trasporti/geo/cities/it - * @desc Cerca città italiane - * @access Public - */ -router.get('/geo/cities/it', geocodingController.searchItalianCities); +// /** +// * @route GET /api/viaggi/geo/cities/it +// * @desc Cerca città italiane +// * @access Public +// */ +// router.get('/geo/cities/it', geocodingController.searchItalianCities); -/** - * @route GET /api/trasporti/geo/geocode - * @desc Indirizzo → Coordinate - * @access Public - */ -router.get('/geo/geocode', geocodingController.geocode); +// /** +// * @route GET /api/viaggi/geo/geocode +// * @desc Indirizzo → Coordinate +// * @access Public +// */ +// router.get('/geo/geocode', geocodingController.geocode); -/** - * @route GET /api/trasporti/geo/reverse - * @desc Coordinate → Indirizzo - * @access Public - */ -router.get('/geo/reverse', geocodingController.reverseGeocode); +// /** +// * @route GET /api/viaggi/geo/reverse +// * @desc Coordinate → Indirizzo +// * @access Public +// */ +// router.get('/geo/reverse', geocodingController.reverseGeocode); -/** - * @route GET /api/trasporti/geo/route - * @desc Calcola percorso tra punti - * @access Public - */ -router.get('/geo/route', geocodingController.getRoute); +// /** +// * @route GET /api/viaggi/geo/route +// * @desc Calcola percorso tra punti +// * @access Public +// */ +// router.get('/geo/route', geocodingController.getRoute); -/** - * @route GET /api/trasporti/geo/distance - * @desc Calcola distanza e durata - * @access Public - */ -router.get('/geo/distance', geocodingController.getDistance); +// /** +// * @route GET /api/viaggi/geo/distance +// * @desc Calcola distanza e durata +// * @access Public +// */ +// router.get('/geo/distance', geocodingController.getDistance); -/** - * @route GET /api/trasporti/geo/suggest-waypoints - * @desc Suggerisci città intermedie sul percorso - * @access Public - */ -router.get('/geo/suggest-waypoints', geocodingController.suggestWaypoints); +// /** +// * @route GET /api/viaggi/geo/suggest-waypoints +// * @desc Suggerisci città intermedie sul percorso +// * @access Public +// */ +// router.get('/geo/suggest-waypoints', geocodingController.suggestWaypoints); -// ============================================================ +/// ============================================================ // 🔧 UTILITY & DRIVER PROFILE // ============================================================ /** - * @route GET /api/trasporti/driver/user/:userId + * @route GET /api/viaggi/driver/user/:userId * @desc Profilo pubblico del conducente * @access Public */ router.get('/driver/user/:userId', async (req, res) => { try { const { userId } = req.params; - const { idapp } = req.query; + const idapp = req.user.idapp; const { User } = require('../models/user'); const Ride = require('../models/Ride'); @@ -502,13 +507,13 @@ router.get('/driver/user/:userId', async (req, res) => { }); /** - * @route GET /api/trasporti/driver/vehicles + * @route GET /api/viaggi/driver/vehicles * @desc Ottieni veicoli dell'utente corrente * @access Private */ router.get('/driver/vehicles', authenticate, async (req, res) => { try { - const { idapp } = req.query; + const idapp = req.user.idapp; const userId = req.user._id; // Assumo che ci sia un middleware di autenticazione if (!userId) { @@ -539,7 +544,7 @@ router.get('/driver/vehicles', authenticate, async (req, res) => { }); /** - * @route PUT /api/trasporti/driver/profile + * @route PUT /api/viaggi/driver/profile * @desc Aggiorna profilo conducente * @access Private */ @@ -585,14 +590,14 @@ router.put('/driver/profile', authenticate, async (req, res) => { }); /** - * @route POST /api/trasporti/driver/vehicles + * @route POST /api/viaggi/driver/vehicles * @desc Aggiungi veicolo * @access Private */ router.post('/driver/vehicles', authenticate, async (req, res) => { try { const userId = req.user._id; - const vehicle = req.body; + const vehicle = req.body.vehicle ? req.body.vehicle : req.body; const { User } = require('../models/user'); @@ -621,7 +626,7 @@ router.post('/driver/vehicles', authenticate, async (req, res) => { }); /** - * @route PUT /api/trasporti/driver/vehicles/:vehicleId + * @route PUT /api/viaggi/driver/vehicles/:vehicleId * @desc Aggiorna veicolo * @access Private */ @@ -667,7 +672,7 @@ router.put('/driver/vehicles/:vehicleId', authenticate, async (req, res) => { }); /** - * @route GET /api/trasporti/driver/vehicles/:vehicleId + * @route GET /api/viaggi/driver/vehicles/:vehicleId * @desc Ottieni dettagli di un veicolo specifico * @access Private */ @@ -715,7 +720,7 @@ router.get('/driver/vehicles/:vehicleId', authenticate, async (req, res) => { }); /** - * @route DELETE /api/trasporti/driver/vehicles/:vehicleId + * @route DELETE /api/viaggi/driver/vehicles/:vehicleId * @desc Rimuovi veicolo * @access Private */ @@ -745,7 +750,7 @@ router.delete('/driver/vehicles/:vehicleId', authenticate, async (req, res) => { }); /** - * @route POST /api/trasporti/driver/vehicles/:vehicleId/default + * @route POST /api/viaggi/driver/vehicles/:vehicleId/default * @desc Imposta veicolo come predefinito * @access Private */ @@ -784,13 +789,13 @@ router.post('/driver/vehicles/:vehicleId/default', authenticate, async (req, res // ============================================================ /** - * @route GET /api/trasporti/contrib-types + * @route GET /api/viaggi/contrib-types * @desc Lista tipi di contributo disponibili * @access Public */ router.get('/contrib-types', async (req, res) => { try { - const { idapp } = req.query; + const idapp = req.query.idapp; const contribTypes = await Contribtype.find({ idapp }); @@ -848,7 +853,7 @@ const uploadVehiclePhoto = multer({ }); /** - * @route POST /api/trasporti/upload/vehicle-photos + * @route POST /api/viaggi/upload/vehicle-photos * @desc Upload multiple foto veicolo (max 5) * @access Private */ @@ -913,7 +918,7 @@ router.post( ); /** - * @route POST /api/trasporti/upload/vehicle-photo + * @route POST /api/viaggi/upload/vehicle-photo * @desc Upload foto veicolo * @access Private */ @@ -977,7 +982,7 @@ router.post( ); /** - * @route DELETE /api/trasporti/upload/vehicle-photo + * @route DELETE /api/viaggi/upload/vehicle-photo * @desc Elimina foto veicolo * @access Private */ diff --git a/src/server/setupExpress.js b/src/server/setupExpress.js index ea6d243..06859b3 100644 --- a/src/server/setupExpress.js +++ b/src/server/setupExpress.js @@ -13,7 +13,7 @@ function setupExpress(app, corsOptions) { app.use(helmet()); app.use(morgan('dev')); app.use(cors(corsOptions)); - app.set('trust proxy', true); + app.set('trust proxy', (process.env.NODE_ENV === 'development') ? false : true); // parser app.use(express.json({ limit: '100mb' }));