355 lines
14 KiB
TypeScript
355 lines
14 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"
|
|
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
|