/** * 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 } })