737974b653
- Prisma-Modell DashboardImage (bytea) mit Migration 20260921120000: Tabelle, Indizes, RLS ENABLE/FORCE und tenant_isolation_policy mit Benutzerdimension - dashboard-image-rules.ts: detectImageMime ueber Magic Bytes (PNG/JPEG/GIF/ WebP), Grenzen 5 MiB je Datei und 30 je Benutzer - DashboardImagesService: list/upload/getBytes/remove, je Methode forTenant(prisma, tenantId, userId); Besitz = Mandant UND Benutzer, sonst 404 - DashboardImagesController unter dashboard/images: GET, POST (FileInterceptor image, 5 MiB, eine Datei), GET :id mit Content-Type aus dem erkannten Typ, Cache-Control private, nosniff, Content-Disposition inline ohne Dateinamen, CSP sandbox; DELETE :id - CreateWidgetDto kennt 'picture-frame' - Klassifikationsdokument: neues Paar dashboard-images.service.ts/ dashboardImage; Bereichs- und Summenzeilen nachgemessen (dashboard 12->18, settings 3->4 und bug-reports waren in der Summe nie mitgezaehlt) - Befund: Prisma-Bytes verlangt Uint8Array<ArrayBuffer>, multers Buffer wird ohne Zusicherung abgelehnt - Kopie per new Uint8Array(buffer) statt Cast Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
52 lines
2.3 KiB
TypeScript
52 lines
2.3 KiB
TypeScript
/**
|
|
* dashboard-image-rules — reine Regeln fuer das Bilderrahmen-Widget
|
|
* (quick-260921-pi9). Kein Nest, kein Prisma: Grenzen und die
|
|
* Magic-Byte-Erkennung, damit Dienst und Controller dieselben Werte
|
|
* anwenden und die Erkennung direkt an den Bytes testbar ist.
|
|
*
|
|
* Warum Magic Bytes und nicht `file.mimetype` oder die Dateiendung
|
|
* (T-PI9-01): beides behauptet der Browser, beides kann eine umbenannte
|
|
* HTML-, SVG- oder PDF-Datei tragen. Was hier NICHT als eine der vier
|
|
* Signaturen erkannt wird, kommt nicht in die Datenbank — und der erkannte
|
|
* Typ ist zugleich der Typ, mit dem die Auslieferung antwortet (T-PI9-08).
|
|
*
|
|
* Bewusst KEIN `file-type`-Paket (T-PI9-SC): vier feste Signaturen sind
|
|
* eine Handvoll Zeilen und brauchen keine Abhaengigkeit.
|
|
*/
|
|
|
|
/** Hoechstgroesse je Datei: 5 MiB (multer `limits.fileSize` je Route). */
|
|
export const DASHBOARD_IMAGE_MAX_BYTES = 5 * 1024 * 1024;
|
|
|
|
/** Hoechstzahl je Benutzer (Zaehler je Mandant+Benutzer vor dem Anlegen). */
|
|
export const DASHBOARD_IMAGE_MAX_COUNT = 30;
|
|
|
|
export type DashboardImageMime = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp';
|
|
|
|
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
const JPEG_SIGNATURE = [0xff, 0xd8, 0xff];
|
|
const GIF87A = [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]; // "GIF87a"
|
|
const GIF89A = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]; // "GIF89a"
|
|
const RIFF = [0x52, 0x49, 0x46, 0x46]; // "RIFF"
|
|
const WEBP = [0x57, 0x45, 0x42, 0x50]; // "WEBP"
|
|
|
|
function startsWith(buffer: Uint8Array, signature: number[], offset = 0): boolean {
|
|
if (buffer.length < offset + signature.length) return false;
|
|
for (let i = 0; i < signature.length; i++) {
|
|
if (buffer[offset + i] !== signature[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Erkennt PNG, JPEG, GIF und WebP an den ersten Bytes; alles andere
|
|
* (leer, Text, SVG, PDF, RIFF ohne WEBP an Stelle 8) ergibt `null`.
|
|
* Wirft nie — ein zu kurzer Puffer ist schlicht kein Bild.
|
|
*/
|
|
export function detectImageMime(buffer: Uint8Array): DashboardImageMime | null {
|
|
if (startsWith(buffer, PNG_SIGNATURE)) return 'image/png';
|
|
if (startsWith(buffer, JPEG_SIGNATURE)) return 'image/jpeg';
|
|
if (startsWith(buffer, GIF87A) || startsWith(buffer, GIF89A)) return 'image/gif';
|
|
if (startsWith(buffer, RIFF) && startsWith(buffer, WEBP, 8)) return 'image/webp';
|
|
return null;
|
|
}
|