- Kundenbereich und Admin-Chat: Bueroklammer-Menue (Bild, Kamera, Datei), Mikrofon fuer Sprachnachrichten, Vorschau vor dem Senden mit Upload-Fortschritt, Einfuegen (Strg+V) und Hineinziehen von Dateien - Kamera: am Handy/Tablet die Kamera-App, am Rechner die Kamera im Browser mit Vorschau, Kamerawechsel und "Nochmal" - Sprachnachricht: Aufnahme mit Zeit und Pegel, vorher anhoerbar oder direkt senden, hoechstens 5 Minuten - Verlauf: Bilder (einzeln bzw. Raster, Grossansicht), Player fuer Sprachnachrichten, Datei-Kacheln mit Download - Server: jede Datei einzeln roh hochgeladen (max. 25 MB, 10 je Nachricht, max. 3 gleichzeitig), Typ aus dem Inhalt, nur erlaubte Endungen, keine Programme; Ablage unter upload-data/chat/<Kunde>/ ausserhalb von public/, Auslieferung nur mit Login; Bilder, Audio und PDF inline, alles andere als Download (nosniff, CSP sandbox) - Nicht gesendete Anhaenge werden nach 24 Stunden geloescht - Benachrichtigungs-Mail nennt nur Art und Namen der Anhaenge - Appwrite: neues Attribut portalMessages.attachments (scripts/setup-chat-collection.mjs, vor dem Deploy ausfuehren) - Datenschutzerklaerung Abschnitt 6 ergaenzt (Stand 26.09.2026), PDF neu Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
115 lines
3.8 KiB
JavaScript
115 lines
3.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Legt die Collection portalMessages (Kunde-Admin-Chat) in woms-database an.
|
|
* Idempotent: existierende Collection/Attribute/Indexe werden uebersprungen
|
|
* (also auch zum Nachruesten neuer Attribute wie `attachments` geeignet).
|
|
* Usage: APPWRITE_API_KEY=... node scripts/setup-chat-collection.mjs
|
|
*/
|
|
const endpoint = (process.env.APPWRITE_ENDPOINT || 'https://ticket.webklar.com/v1').replace(/\/$/, '')
|
|
const projectId = process.env.APPWRITE_PROJECT_ID || '6a1058610003c5a13a05'
|
|
const apiKey = process.env.APPWRITE_API_KEY
|
|
const databaseId = process.env.APPWRITE_DATABASE_ID || 'woms-database'
|
|
const COLLECTION_ID = 'portalMessages'
|
|
|
|
if (!apiKey) {
|
|
console.error('APPWRITE_API_KEY erforderlich')
|
|
process.exit(1)
|
|
}
|
|
|
|
async function aw(path, { method = 'GET', body } = {}) {
|
|
const response = await fetch(`${endpoint}${path}`, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Appwrite-Project': projectId,
|
|
'X-Appwrite-Key': apiKey,
|
|
},
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
})
|
|
const data = await response.json().catch(() => ({}))
|
|
if (!response.ok) {
|
|
const err = new Error(data.message || `Appwrite ${response.status}`)
|
|
err.status = response.status
|
|
err.type = data.type
|
|
throw err
|
|
}
|
|
return data
|
|
}
|
|
|
|
async function ensure(label, fn) {
|
|
try {
|
|
await fn()
|
|
console.log(`+ ${label}`)
|
|
} catch (e) {
|
|
if (e.status === 409 || /already exists/i.test(e.message)) {
|
|
console.log(`= ${label} (existiert)`)
|
|
} else {
|
|
throw new Error(`${label}: ${e.message}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
async function waitForAttribute(key, maxSec = 30) {
|
|
for (let i = 0; i < maxSec; i += 1) {
|
|
const attr = await aw(
|
|
`/databases/${databaseId}/collections/${COLLECTION_ID}/attributes/${key}`
|
|
).catch(() => null)
|
|
if (attr?.status === 'available') return
|
|
await new Promise((r) => setTimeout(r, 1000))
|
|
}
|
|
throw new Error(`Attribut ${key} wurde nicht verfuegbar`)
|
|
}
|
|
|
|
const base = `/databases/${databaseId}/collections`
|
|
|
|
await ensure(`Collection ${COLLECTION_ID}`, () =>
|
|
aw(base, {
|
|
method: 'POST',
|
|
body: {
|
|
collectionId: COLLECTION_ID,
|
|
name: 'Portal Messages (Kunde-Admin-Chat)',
|
|
documentSecurity: false,
|
|
permissions: [],
|
|
},
|
|
})
|
|
)
|
|
|
|
const attributes = [
|
|
{ kind: 'string', key: 'customerId', size: 64, required: true },
|
|
{ kind: 'string', key: 'sender', size: 16, required: true },
|
|
{ kind: 'string', key: 'senderName', size: 128, required: false },
|
|
{ kind: 'string', key: 'text', size: 4000, required: true },
|
|
// JSON-Liste der Anhaenge (Bilder, Dateien, Sprachnachrichten); Dateien selbst liegen auf dem Server
|
|
{ kind: 'string', key: 'attachments', size: 20000, required: false },
|
|
{ kind: 'boolean', key: 'readByAdmin', required: false },
|
|
{ kind: 'boolean', key: 'readByCustomer', required: false },
|
|
{ kind: 'datetime', key: 'createdAt', required: true },
|
|
]
|
|
|
|
for (const attr of attributes) {
|
|
const { kind, ...body } = attr
|
|
await ensure(`Attribut ${attr.key}`, () =>
|
|
aw(`${base}/${COLLECTION_ID}/attributes/${kind}`, { method: 'POST', body })
|
|
)
|
|
}
|
|
|
|
for (const attr of attributes) {
|
|
await waitForAttribute(attr.key)
|
|
}
|
|
console.log('Alle Attribute verfuegbar.')
|
|
|
|
const indexes = [
|
|
{ key: 'idx_customer_created', type: 'key', attributes: ['customerId', 'createdAt'], orders: ['ASC', 'DESC'] },
|
|
{ key: 'idx_created', type: 'key', attributes: ['createdAt'], orders: ['DESC'] },
|
|
{ key: 'idx_read_admin', type: 'key', attributes: ['readByAdmin', 'sender'], orders: ['ASC', 'ASC'] },
|
|
{ key: 'idx_customer_read', type: 'key', attributes: ['customerId', 'readByCustomer'], orders: ['ASC', 'ASC'] },
|
|
]
|
|
|
|
for (const idx of indexes) {
|
|
await ensure(`Index ${idx.key}`, () =>
|
|
aw(`${base}/${COLLECTION_ID}/indexes`, { method: 'POST', body: idx })
|
|
)
|
|
}
|
|
|
|
console.log('Fertig.')
|