Files
FEDEO/src/routes/helpdesk.ts
2025-10-31 16:27:56 +01:00

308 lines
11 KiB
TypeScript

// 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"
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
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
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 conversation_id = req.params.id
const { data, error } = await server.supabase
.from('helpdesk_conversations')
.select('*, helpdesk_contacts(*)')
.eq('tenant_id', tenant_id)
.eq('id', conversation_id)
.single()
if (error) return res.status(404).send({ error: 'Conversation not found' })
return res.send(data)
})
// 🔄 4. Konversation Status ändern
server.patch('/helpdesk/conversations/:id/status', async (req, res) => {
const conversation_id = req.params.id
const { status } = req.body
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 conversation_id = req.params.id
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 conversation_id = req.params.id
const { text } = req.body
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
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
// 🔒 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)
}
// Speichern in Supabase
const { data, error } = await server.supabase
.from("helpdesk_channel_instances")
.insert({
tenant_id,
type_id,
name,
config: safeConfig,
is_active,
})
.select()
.single()
if (error) throw error
// sensible Felder aus Response entfernen
if (data.config?.imap) {
delete data.config.imap.host
delete data.config.imap.user
delete data.config.imap.pass
}
if (data.config?.smtp) {
delete data.config.smtp.host
delete data.config.smtp.user
delete data.config.smtp.pass
}
reply.send({
message: "E-Mail-Channel erfolgreich erstellt",
channel: data,
})
} 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 { data: conv, error: convErr } = await server.supabase
.from("helpdesk_conversations")
.select(`
id,
tenant_id,
subject,
channel_instance_id,
helpdesk_contacts(email),
helpdesk_channel_instances(config, name),
ticket_number
`)
.eq("id", conversationId)
.single()
console.log(conv)
if (convErr || !conv) {
reply.status(404).send({ error: "Konversation nicht gefunden" })
return
}
const contact = conv.helpdesk_contacts
const channel = conv.helpdesk_channel_instances
console.log(contact)
if (!contact?.email) {
reply.status(400).send({ error: "Kein Empfänger gefunden" })
return
}
// 🔐 SMTP-Daten entschlüsseln
try {
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.ticket_number} | ${conv.subject}` || `${conv.ticket_number} | Antwort vom FEDEO Helpdesk`,
text,
}
const info = await transporter.sendMail(mailOptions)
console.log(`[Helpdesk SMTP] Gesendet an ${contact.email}: ${info.messageId}`)
// 💾 Nachricht speichern
const { error: insertErr } = await server.supabase
.from("helpdesk_messages")
.insert({
tenant_id: conv.tenant_id,
conversation_id: conversationId,
direction: "outgoing",
payload: { type: "text", text },
external_message_id: info.messageId,
received_at: new Date().toISOString(),
})
if (insertErr) throw insertErr
// 🔁 Konversation aktualisieren
await server.supabase
.from("helpdesk_conversations")
.update({ last_message_at: new Date().toISOString() })
.eq("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