Added Helpdesk
This commit is contained in:
@@ -22,6 +22,8 @@ import bankingRoutes from "./routes/banking";
|
|||||||
import exportRoutes from "./routes/exports"
|
import exportRoutes from "./routes/exports"
|
||||||
import emailAsUserRoutes from "./routes/emailAsUser";
|
import emailAsUserRoutes from "./routes/emailAsUser";
|
||||||
import authProfilesRoutes from "./routes/profiles";
|
import authProfilesRoutes from "./routes/profiles";
|
||||||
|
import helpdeskRoutes from "./routes/helpdesk";
|
||||||
|
import helpdeskInboundRoutes from "./routes/helpdesk.inbound";
|
||||||
|
|
||||||
import {sendMail} from "./utils/mailer";
|
import {sendMail} from "./utils/mailer";
|
||||||
import {loadSecrets, secrets} from "./utils/secrets";
|
import {loadSecrets, secrets} from "./utils/secrets";
|
||||||
@@ -62,6 +64,8 @@ async function main() {
|
|||||||
await app.register(authRoutes);
|
await app.register(authRoutes);
|
||||||
await app.register(healthRoutes);
|
await app.register(healthRoutes);
|
||||||
|
|
||||||
|
await app.register(helpdeskInboundRoutes);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//Geschützte Routes
|
//Geschützte Routes
|
||||||
@@ -82,6 +86,7 @@ async function main() {
|
|||||||
await subApp.register(exportRoutes);
|
await subApp.register(exportRoutes);
|
||||||
await subApp.register(emailAsUserRoutes);
|
await subApp.register(emailAsUserRoutes);
|
||||||
await subApp.register(authProfilesRoutes);
|
await subApp.register(authProfilesRoutes);
|
||||||
|
await subApp.register(helpdeskRoutes);
|
||||||
|
|
||||||
},{prefix: "/api"})
|
},{prefix: "/api"})
|
||||||
|
|
||||||
|
|||||||
38
src/modules/helpdesk/helpdesk.contact.service.ts
Normal file
38
src/modules/helpdesk/helpdesk.contact.service.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// modules/helpdesk/helpdesk.contact.service.ts
|
||||||
|
import { FastifyInstance } from 'fastify'
|
||||||
|
|
||||||
|
export async function getOrCreateContact(
|
||||||
|
server: FastifyInstance,
|
||||||
|
tenant_id: number,
|
||||||
|
{ email, phone, display_name, customer_id, contact_id }: { email?: string; phone?: string; display_name?: string; customer_id?: number; contact_id?: number }
|
||||||
|
) {
|
||||||
|
if (!email && !phone) throw new Error('Contact must have at least an email or phone')
|
||||||
|
|
||||||
|
// Bestehenden Kontakt prüfen
|
||||||
|
const { data: existing, error: findError } = await server.supabase
|
||||||
|
.from('helpdesk_contacts')
|
||||||
|
.select('*')
|
||||||
|
.eq('tenant_id', tenant_id)
|
||||||
|
.or(`email.eq.${email || ''},phone.eq.${phone || ''}`)
|
||||||
|
.maybeSingle()
|
||||||
|
|
||||||
|
if (findError) throw findError
|
||||||
|
if (existing) return existing
|
||||||
|
|
||||||
|
// Anlegen
|
||||||
|
const { data: created, error: insertError } = await server.supabase
|
||||||
|
.from('helpdesk_contacts')
|
||||||
|
.insert({
|
||||||
|
tenant_id,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
display_name,
|
||||||
|
customer_id,
|
||||||
|
contact_id
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (insertError) throw insertError
|
||||||
|
return created
|
||||||
|
}
|
||||||
81
src/modules/helpdesk/helpdesk.conversation.service.ts
Normal file
81
src/modules/helpdesk/helpdesk.conversation.service.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
// modules/helpdesk/helpdesk.conversation.service.ts
|
||||||
|
import { FastifyInstance } from 'fastify'
|
||||||
|
import { getOrCreateContact } from './helpdesk.contact.service.js'
|
||||||
|
|
||||||
|
export async function createConversation(
|
||||||
|
server: FastifyInstance,
|
||||||
|
{
|
||||||
|
tenant_id,
|
||||||
|
contact,
|
||||||
|
channel_instance_id,
|
||||||
|
subject,
|
||||||
|
customer_id = null,
|
||||||
|
contact_person_id = null
|
||||||
|
}: {
|
||||||
|
tenant_id: number
|
||||||
|
contact: { email?: string; phone?: string; display_name?: string }
|
||||||
|
channel_instance_id: string
|
||||||
|
subject?: string,
|
||||||
|
customer_id?: number,
|
||||||
|
contact_person_id?: number
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const contactRecord = await getOrCreateContact(server, tenant_id, contact)
|
||||||
|
|
||||||
|
const { data, error } = await server.supabase
|
||||||
|
.from('helpdesk_conversations')
|
||||||
|
.insert({
|
||||||
|
tenant_id,
|
||||||
|
contact_id: contactRecord.id,
|
||||||
|
channel_instance_id,
|
||||||
|
subject: subject || null,
|
||||||
|
status: 'open',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
customer_id,
|
||||||
|
contact_person_id
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getConversations(
|
||||||
|
server: FastifyInstance,
|
||||||
|
tenant_id: number,
|
||||||
|
opts?: { status?: string; limit?: number }
|
||||||
|
) {
|
||||||
|
const { status, limit = 50 } = opts || {}
|
||||||
|
|
||||||
|
let query = server.supabase.from('helpdesk_conversations').select('*, customer_id(*)').eq('tenant_id', tenant_id)
|
||||||
|
|
||||||
|
if (status) query = query.eq('status', status)
|
||||||
|
query = query.order('last_message_at', { ascending: false }).limit(limit)
|
||||||
|
|
||||||
|
const { data, error } = await query
|
||||||
|
if (error) throw error
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
customer: data.customer_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateConversationStatus(
|
||||||
|
server: FastifyInstance,
|
||||||
|
conversation_id: string,
|
||||||
|
status: string
|
||||||
|
) {
|
||||||
|
const valid = ['open', 'in_progress', 'waiting_for_customer', 'answered', 'closed']
|
||||||
|
if (!valid.includes(status)) throw new Error('Invalid status')
|
||||||
|
|
||||||
|
const { data, error } = await server.supabase
|
||||||
|
.from('helpdesk_conversations')
|
||||||
|
.update({ status })
|
||||||
|
.eq('id', conversation_id)
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
}
|
||||||
58
src/modules/helpdesk/helpdesk.message.service.ts
Normal file
58
src/modules/helpdesk/helpdesk.message.service.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// modules/helpdesk/helpdesk.message.service.ts
|
||||||
|
import { FastifyInstance } from 'fastify'
|
||||||
|
|
||||||
|
export async function addMessage(
|
||||||
|
server: FastifyInstance,
|
||||||
|
{
|
||||||
|
tenant_id,
|
||||||
|
conversation_id,
|
||||||
|
author_user_id = null,
|
||||||
|
direction = 'incoming',
|
||||||
|
payload,
|
||||||
|
raw_meta = null,
|
||||||
|
}: {
|
||||||
|
tenant_id: number
|
||||||
|
conversation_id: string
|
||||||
|
author_user_id?: string | null
|
||||||
|
direction?: 'incoming' | 'outgoing' | 'internal' | 'system'
|
||||||
|
payload: any
|
||||||
|
raw_meta?: any
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (!payload?.text) throw new Error('Message payload requires text content')
|
||||||
|
|
||||||
|
const { data: message, error } = await server.supabase
|
||||||
|
.from('helpdesk_messages')
|
||||||
|
.insert({
|
||||||
|
tenant_id,
|
||||||
|
conversation_id,
|
||||||
|
author_user_id,
|
||||||
|
direction,
|
||||||
|
payload,
|
||||||
|
raw_meta,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
|
// Letzte Nachricht aktualisieren
|
||||||
|
await server.supabase
|
||||||
|
.from('helpdesk_conversations')
|
||||||
|
.update({ last_message_at: new Date().toISOString() })
|
||||||
|
.eq('id', conversation_id)
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMessages(server: FastifyInstance, conversation_id: string) {
|
||||||
|
const { data, error } = await server.supabase
|
||||||
|
.from('helpdesk_messages')
|
||||||
|
.select('*')
|
||||||
|
.eq('conversation_id', conversation_id)
|
||||||
|
.order('created_at', { ascending: true })
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
}
|
||||||
136
src/routes/helpdesk.inbound.ts
Normal file
136
src/routes/helpdesk.inbound.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
// modules/helpdesk/helpdesk.inbound.routes.ts
|
||||||
|
import { FastifyPluginAsync } from 'fastify'
|
||||||
|
import { createConversation } from '../modules/helpdesk/helpdesk.conversation.service.js'
|
||||||
|
import { addMessage } from '../modules/helpdesk/helpdesk.message.service.js'
|
||||||
|
import { getOrCreateContact } from '../modules/helpdesk/helpdesk.contact.service.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Öffentliche Route zum Empfang eingehender Kontaktformular-Nachrichten.
|
||||||
|
* Authentifizierung: über `public_token` aus helpdesk_channel_instances
|
||||||
|
*/
|
||||||
|
|
||||||
|
function extractDomain(email) {
|
||||||
|
if (!email) return null
|
||||||
|
const parts = email.split("@")
|
||||||
|
return parts.length === 2 ? parts[1].toLowerCase() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findCustomerOrContactByEmailOrDomain(server,fromMail, tenantId) {
|
||||||
|
const sender = fromMail
|
||||||
|
const senderDomain = extractDomain(sender)
|
||||||
|
if (!senderDomain) return null
|
||||||
|
|
||||||
|
|
||||||
|
// 1️⃣ Direkter Match über contacts
|
||||||
|
const { data: contactMatch } = await server.supabase
|
||||||
|
.from("contacts")
|
||||||
|
.select("id, customer")
|
||||||
|
.eq("email", sender)
|
||||||
|
.eq("tenant", tenantId)
|
||||||
|
.maybeSingle()
|
||||||
|
|
||||||
|
if (contactMatch?.customer_id) return {
|
||||||
|
customer: contactMatch.customer,
|
||||||
|
contact: contactMatch.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2️⃣ Kunden laden, bei denen E-Mail oder Rechnungsmail passt
|
||||||
|
const { data: customers, error } = await server.supabase
|
||||||
|
.from("customers")
|
||||||
|
.select("id, infoData")
|
||||||
|
.eq("tenant", tenantId)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error(`[Helpdesk] Fehler beim Laden der Kunden:`, error.message)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3️⃣ Durch Kunden iterieren und prüfen
|
||||||
|
for (const c of customers || []) {
|
||||||
|
const info = c.infoData || {}
|
||||||
|
const email = info.email?.toLowerCase()
|
||||||
|
const invoiceEmail = info.invoiceEmail?.toLowerCase()
|
||||||
|
|
||||||
|
const emailDomain = extractDomain(email)
|
||||||
|
const invoiceDomain = extractDomain(invoiceEmail)
|
||||||
|
|
||||||
|
// exakter Match oder Domain-Match
|
||||||
|
if (
|
||||||
|
sender === email ||
|
||||||
|
sender === invoiceEmail ||
|
||||||
|
senderDomain === emailDomain ||
|
||||||
|
senderDomain === invoiceDomain
|
||||||
|
) {
|
||||||
|
return {customer: c.id, contact:null}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const helpdeskInboundRoutes: FastifyPluginAsync = async (server) => {
|
||||||
|
// Öffentliche POST-Route
|
||||||
|
server.post('/helpdesk/inbound/:public_token', async (req, res) => {
|
||||||
|
const { public_token } = req.params
|
||||||
|
const { email, phone, display_name, subject, message } = req.body
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return res.status(400).send({ error: 'Message content required' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1️⃣ Kanalinstanz anhand des Tokens ermitteln
|
||||||
|
const { data: channel, error: channelError } = await server.supabase
|
||||||
|
.from('helpdesk_channel_instances')
|
||||||
|
.select('*')
|
||||||
|
.eq('public_token', public_token)
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (channelError || !channel) {
|
||||||
|
return res.status(404).send({ error: 'Invalid channel token' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenant_id = channel.tenant_id
|
||||||
|
const channel_instance_id = channel.id
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const {customer, contact: contactPerson} = await findCustomerOrContactByEmailOrDomain(server,email, tenant_id )
|
||||||
|
|
||||||
|
|
||||||
|
// 2️⃣ Kontakt finden oder anlegen
|
||||||
|
const contact = await getOrCreateContact(server, tenant_id, {
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
display_name,
|
||||||
|
customer_id: customer,
|
||||||
|
contact_id: contactPerson,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3️⃣ Konversation erstellen
|
||||||
|
const conversation = await createConversation(server, {
|
||||||
|
tenant_id,
|
||||||
|
contact,
|
||||||
|
channel_instance_id,
|
||||||
|
subject: subject ?? 'Kontaktformular Anfrage',
|
||||||
|
customer_id: customer,
|
||||||
|
contact_person_id: contactPerson
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4️⃣ Erste Nachricht hinzufügen
|
||||||
|
await addMessage(server, {
|
||||||
|
tenant_id,
|
||||||
|
conversation_id: conversation.id,
|
||||||
|
direction: 'incoming',
|
||||||
|
payload: { type: 'text', text: message },
|
||||||
|
raw_meta: { source: 'contact_form' },
|
||||||
|
})
|
||||||
|
|
||||||
|
// (optional) Auto-Antwort oder Event hier ergänzen
|
||||||
|
|
||||||
|
return res.status(201).send({
|
||||||
|
success: true,
|
||||||
|
conversation_id: conversation.id,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default helpdeskInboundRoutes
|
||||||
306
src/routes/helpdesk.ts
Normal file
306
src/routes/helpdesk.ts
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
// 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)
|
||||||
|
`)
|
||||||
|
.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.subject || "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
|
||||||
Reference in New Issue
Block a user