// modules/helpdesk/helpdesk.routes.ts import { FastifyPluginAsync } from 'fastify' import { createConversation, getConversations, updateConversationStatus } from '../modules/helpdesk/helpdesk.conversation.service.js' import { addMessage, getMessages } from '../modules/helpdesk/helpdesk.message.service.js' import { getOrCreateContact } from '../modules/helpdesk/helpdesk.contact.service.js' import {decrypt, encrypt} from "../utils/crypt"; import nodemailer from "nodemailer" import { eq } from "drizzle-orm"; import { helpdesk_channel_instances, helpdesk_contacts, helpdesk_conversations, helpdesk_messages, } from "../../db/schema"; const helpdeskRoutes: FastifyPluginAsync = async (server) => { // 📩 1. Liste aller Konversationen server.get('/helpdesk/conversations', async (req, res) => { const tenant_id = req.user?.tenant_id if (!tenant_id) return res.status(401).send({ error: 'Unauthorized' }) const { status } = req.query as {status: string} const conversations = await getConversations(server, tenant_id, { status }) return res.send(conversations) }) // 🆕 2. Neue Konversation erstellen server.post('/helpdesk/conversations', async (req, res) => { const tenant_id = req.user?.tenant_id if (!tenant_id) return res.status(401).send({ error: 'Unauthorized' }) const { contact, channel_instance_id, subject, message } = req.body as { contact: object channel_instance_id: string subject: string message: string } if (!contact || !channel_instance_id) { return res.status(400).send({ error: 'Missing contact or channel_instance_id' }) } // 1. Konversation erstellen const conversation = await createConversation(server, { tenant_id, contact, channel_instance_id, subject, }) // 2. Falls erste Nachricht vorhanden → hinzufügen if (message) { await addMessage(server, { tenant_id, conversation_id: conversation.id, direction: 'incoming', payload: { type: 'text', text: message }, }) } return res.status(201).send(conversation) }) // 🧭 3. Einzelne Konversation abrufen server.get('/helpdesk/conversations/:id', async (req, res) => { const tenant_id = req.user?.tenant_id const {id: conversation_id} = req.params as {id: string} const rows = await server.db .select({ conversation: helpdesk_conversations, contact: helpdesk_contacts }) .from(helpdesk_conversations) .leftJoin(helpdesk_contacts, eq(helpdesk_contacts.id, helpdesk_conversations.contactId)) .where(eq(helpdesk_conversations.id, conversation_id)) const data = rows[0] if (!data || data.conversation.tenantId !== tenant_id) return res.status(404).send({ error: 'Conversation not found' }) return res.send({ ...data.conversation, channel_instance_id: data.conversation.channelInstanceId, contact_id: data.conversation.contactId, contact_person_id: data.conversation.contactPersonId, created_at: data.conversation.createdAt, customer_id: data.conversation.customerId, last_message_at: data.conversation.lastMessageAt, tenant_id: data.conversation.tenantId, ticket_number: data.conversation.ticketNumber, helpdesk_contacts: data.contact, }) }) // 🔄 4. Konversation Status ändern server.patch('/helpdesk/conversations/:id/status', async (req, res) => { const {id: conversation_id} = req.params as { id: string } const { status } = req.body as { status: string } const updated = await updateConversationStatus(server, conversation_id, status) return res.send(updated) }) // 💬 5. Nachrichten abrufen server.get('/helpdesk/conversations/:id/messages', async (req, res) => { const {id:conversation_id} = req.params as { id: string } const messages = await getMessages(server, conversation_id) return res.send(messages) }) // 💌 6. Nachricht hinzufügen (z. B. Antwort eines Agents) server.post('/helpdesk/conversations/:id/messages', async (req, res) => { console.log(req.user) const tenant_id = req.user?.tenant_id const author_user_id = req.user?.user_id const {id: conversation_id} = req.params as { id: string } const { text } = req.body as { text: string } if (!text) return res.status(400).send({ error: 'Missing message text' }) const message = await addMessage(server, { tenant_id, conversation_id, author_user_id, direction: 'outgoing', payload: { type: 'text', text }, }) return res.status(201).send(message) }) // 👤 7. Kontakt suchen oder anlegen server.post('/helpdesk/contacts', async (req, res) => { const tenant_id = req.user?.tenant_id const { email, phone, display_name } = req.body as { email: string; phone: string, display_name: string } const contact = await getOrCreateContact(server, tenant_id, { email, phone, display_name }) return res.status(201).send(contact) }) server.post("/helpdesk/channels", { schema: { body: { type: "object", required: ["type_id", "name", "config"], properties: { type_id: { type: "string" }, name: { type: "string" }, config: { type: "object" }, is_active: { type: "boolean", default: true }, }, }, }, handler: async (req, reply) => { const { type_id, name, config, is_active = true } = req.body as { type_id: string, name: string, config: { imap:{ host: string | object, user: string | object, pass: string | object, }, smtp:{ host: string | object, user: string | object, pass: string | object, } }, is_active: boolean } // 🔒 Tenant aus Auth-Context const tenant_id = req.user?.tenant_id if (!tenant_id) { return reply.status(401).send({ error: "Kein Tenant im Benutzerkontext gefunden." }) } if (type_id !== "email") { return reply.status(400).send({ error: "Nur Typ 'email' wird aktuell unterstützt." }) } try { const safeConfig = { ...config } // 🔐 IMAP-Daten verschlüsseln if (safeConfig.imap) { if (safeConfig.imap.host) safeConfig.imap.host = encrypt(safeConfig.imap.host) if (safeConfig.imap.user) safeConfig.imap.user = encrypt(safeConfig.imap.user) if (safeConfig.imap.pass) safeConfig.imap.pass = encrypt(safeConfig.imap.pass) } // 🔐 SMTP-Daten verschlüsseln if (safeConfig.smtp) { if (safeConfig.smtp.host) safeConfig.smtp.host = encrypt(safeConfig.smtp.host) if (safeConfig.smtp.user) safeConfig.smtp.user = encrypt(safeConfig.smtp.user) if (safeConfig.smtp.pass) safeConfig.smtp.pass = encrypt(safeConfig.smtp.pass) } const inserted = await server.db .insert(helpdesk_channel_instances) .values({ tenantId: tenant_id, typeId: type_id, name, config: safeConfig, isActive: is_active, }) .returning() const data = inserted[0] if (!data) throw new Error("Konnte Channel nicht erstellen") const responseConfig: any = data.config // sensible Felder aus Response entfernen if (responseConfig?.imap) { delete responseConfig.imap.host delete responseConfig.imap.user delete responseConfig.imap.pass } if (responseConfig?.smtp) { delete responseConfig.smtp.host delete responseConfig.smtp.user delete responseConfig.smtp.pass } reply.send({ message: "E-Mail-Channel erfolgreich erstellt", channel: { ...data, config: responseConfig }, }) } catch (err) { console.error("Fehler bei Channel-Erstellung:", err) reply.status(500).send({ error: err.message }) } }, }) server.post("/helpdesk/conversations/:id/reply", { schema: { body: { type: "object", required: ["text"], properties: { text: { type: "string" }, }, }, }, handler: async (req, reply) => { const conversationId = (req.params as any).id const { text } = req.body as { text: string } // 🔹 Konversation inkl. Channel + Kontakt laden const rows = await server.db .select({ conversation: helpdesk_conversations, contact: helpdesk_contacts, channel: helpdesk_channel_instances, }) .from(helpdesk_conversations) .leftJoin(helpdesk_contacts, eq(helpdesk_contacts.id, helpdesk_conversations.contactId)) .leftJoin(helpdesk_channel_instances, eq(helpdesk_channel_instances.id, helpdesk_conversations.channelInstanceId)) .where(eq(helpdesk_conversations.id, conversationId)) .limit(1) const conv = rows[0] console.log(conv) if (!conv) { reply.status(404).send({ error: "Konversation nicht gefunden" }) return } const contact = conv.contact as unknown as {email: string} const channel = conv.channel as unknown as {name: string, config: any} console.log(contact) if (!contact?.email) { reply.status(400).send({ error: "Kein Empfänger gefunden" }) return } // 🔐 SMTP-Daten entschlüsseln try { // @ts-ignore const smtp = channel?.config?.smtp const host = typeof smtp.host === "object" ? decrypt(smtp.host) : smtp.host const user = typeof smtp.user === "object" ? decrypt(smtp.user) : smtp.user const pass = typeof smtp.pass === "object" ? decrypt(smtp.pass) : smtp.pass // 🔧 Transporter const transporter = nodemailer.createTransport({ host, port: smtp.port || 465, secure: smtp.secure ?? true, auth: { user, pass }, }) // 📩 Mail senden const mailOptions = { from: `"${channel?.name}" <${user}>`, to: contact.email, subject: `${conv.conversation.ticketNumber} | ${conv.conversation.subject}` || `${conv.conversation.ticketNumber} | Antwort vom FEDEO Helpdesk`, text, } const info = await transporter.sendMail(mailOptions) console.log(`[Helpdesk SMTP] Gesendet an ${contact.email}: ${info.messageId}`) // 💾 Nachricht speichern await server.db .insert(helpdesk_messages) .values({ tenantId: conv.conversation.tenantId, conversationId: conversationId, direction: "outgoing", payload: { type: "text", text }, externalMessageId: info.messageId, receivedAt: new Date(), }) // 🔁 Konversation aktualisieren await server.db .update(helpdesk_conversations) .set({ lastMessageAt: new Date() }) .where(eq(helpdesk_conversations.id, conversationId)) reply.send({ message: "E-Mail erfolgreich gesendet", messageId: info.messageId, }) } catch (err: any) { console.error("Fehler beim SMTP-Versand:", err) reply.status(500).send({ error: err.message }) } }, }) } export default helpdeskRoutes