/**
* POST /api/chat/image (JSON)
* { to, clientMsgId, fromName, thumbUrl, fullUrl, width, height }
*
* Records an image message. The image itself is uploaded by the CLIENT to Strapi (public media, the
* same path the mobile pages use) — this route only stores the resulting Strapi URLs on a normal chat
* message (meta { kind:'image', thumbUrl, fullUrl }) and fans it out over the socket + push. Nothing is
* stored in chat.db beyond the message row, so the DB stays small. Gated to staff; 404 when disabled.
*/
import { gateFromEvent, isStaffUser } from '../../utils/chatAccess'
import { insertMessage } from '../../utils/chatDb'
import { sendToUser, isConnected } from '../../utils/chatPresence'
import { sendPushToUser, buildChatMessagePayload } from '../../utils/pushNotifier'
import { sendFcmToUser } from '../../utils/fcmNotifier'
const IMAGE_LABEL = '📷 Bild'
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
if (config.chatEnabled !== true) { setResponseStatus(event, 404); return { error: 'chat disabled' } }
let userId: number, token: string
try {
({ userId, token } = await gateFromEvent(event))
} catch (err: any) {
setResponseStatus(event, err?.statusCode || 403)
return { error: 'forbidden' }
}
const body = await readBody(event)
const to = Number(body?.to)
const clientMsgId = String(body?.clientMsgId || '')
const fromName = String(body?.fromName || '')
const thumbUrl = String(body?.thumbUrl || '')
const fullUrl = String(body?.fullUrl || thumbUrl)
const width = Number(body?.width) || 0
const height = Number(body?.height) || 0
if (!to || to === userId) { setResponseStatus(event, 400); return { error: 'bad recipient' } }
// Only accept our own Strapi file URLs (defends the
/ sinks against arbitrary/`javascript:` URLs).
if (!/^https?:\/\/.+\/files-api\//.test(thumbUrl)) { setResponseStatus(event, 400); return { error: 'invalid image url' } }
// Recipient must be staff (mirror the WS message guard). Only reject on a hard "no".
if (!isConnected(to)) {
const staff = await isStaffUser(to, token)
if (staff === false) { setResponseStatus(event, 403); return { error: 'recipient not allowed' } }
}
const meta: any = { kind: 'image', thumbUrl, fullUrl, width, height }
const createdAt = Date.now()
const saved = insertMessage({
senderUserId: userId,
recipientUserId: to,
body: IMAGE_LABEL,
createdAt,
clientMsgId: clientMsgId || null,
meta
})
const msg = saved ?? {
id: null, senderUserId: userId, recipientUserId: to, body: IMAGE_LABEL,
createdAt, readAt: null, clientMsgId: clientMsgId || null, meta
}
const out = { type: 'message', message: msg }
sendToUser(to, out)
sendToUser(userId, out)
// Native FCM to ALL the recipient's devices regardless of presence (see voice route rationale).
sendFcmToUser(to, {
title: fromName || 'Neue Nachricht',
body: IMAGE_LABEL,
data: { senderUserId: String(userId), url: `/chat?chat=${userId}` },
}).catch((e: any) => console.error('[ChatImage] FCM failed:', e?.message))
// Web push (browsers/PWA) stays offline-only.
if (!isConnected(to)) {
const payload = buildChatMessagePayload(fromName || '', IMAGE_LABEL, userId)
sendPushToUser(to, payload).catch((e: any) => console.error('[ChatImage] offline push failed:', e?.message))
}
return { status: 200, message: msg }
})