From 7733acbc22cb0e03e82e7789eb6d69a843a48562 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Wed, 25 Mar 2026 07:35:27 +0100 Subject: [PATCH] feat: Replace external storage with local file storage + auto cleanup Media is now stored directly on the server disk (no external service). Files are served via Express static middleware at /uploads/*. Cleanup rules: - Videos deleted immediately after successful publish to Facebook/Instagram - Videos older than 7 days purged daily by cron job - Images older than 30 days purged daily by cron job Changes: - server/services/minio.ts: replaced S3 client with local fs read/write - server/index.ts: added static serving for /uploads/media, /logos, /stories - server/services/media-purge.ts: new rules (images 30d, videos 7d) - server/services/scheduler.ts: delete local videos after successful publish - docker-compose.yml: add media_uploads, logos_uploads, stories_uploads volumes - settings UI: removed credentials card, shows local storage info - package.json: removed googleapis (unneeded) Co-Authored-By: Claude Sonnet 4.6 --- client/src/pages/mobile/settings.tsx | 92 +++------------------- client/src/pages/settings.tsx | 85 ++------------------ docker-compose.yml | 12 ++- package-lock.json | 97 +---------------------- server/index.ts | 11 +++ server/routes.ts | 8 +- server/services/media-purge.ts | 94 +++++++++++------------ server/services/minio.ts | 111 +++++++++++---------------- server/services/scheduler.ts | 13 ++++ 9 files changed, 145 insertions(+), 378 deletions(-) diff --git a/client/src/pages/mobile/settings.tsx b/client/src/pages/mobile/settings.tsx index 4014866..2efe65a 100644 --- a/client/src/pages/mobile/settings.tsx +++ b/client/src/pages/mobile/settings.tsx @@ -365,99 +365,25 @@ export default function SettingsMobile() { - {/* MinIO */} + {/* Stockage local */} {isAdmin && ( - Configuration Stockage MinIO + Stockage des médias - Configurez MinIO pour le stockage des médias + Les médias sont stockés localement sur le serveur - -
- - setCloudName(e.target.value)} - placeholder="socialflow" - className="min-h-[48px]" - data-testid="input-cloud-name" - /> -

- Nom du bucket MinIO dans lequel stocker les fichiers -

+ +
+

✓ Images et vidéos stockées sur le disque du serveur

+

✓ Vidéos supprimées automatiquement après publication

+

✓ Images supprimées automatiquement après 30 jours

+

✓ Vidéos non publiées supprimées après 7 jours

-
- - setApiKey(e.target.value)} - placeholder="minioadmin" - className="min-h-[48px]" - data-testid="input-api-key" - /> -
-
- - setApiSecret(e.target.value)} - placeholder="••••••••••••••••" - className="min-h-[48px]" - data-testid="input-api-secret" - /> -

- Votre Secret Key restera sécurisé et ne sera jamais exposé -

-
-
- - setEndpointUrl(e.target.value)} - placeholder="http://minio:9000" - className="min-h-[48px]" - data-testid="input-endpoint-url" - /> -

- URL interne de votre serveur MinIO (ex: http://192.168.1.10:9000) -

-
-
- - setPublicUrl(e.target.value)} - placeholder="https://media.votre-domaine.com" - className="min-h-[48px]" - data-testid="input-public-url" - /> -

- URL publique pour accéder aux fichiers depuis internet -

-
-
)} diff --git a/client/src/pages/settings.tsx b/client/src/pages/settings.tsx index 624ad32..b9593e4 100644 --- a/client/src/pages/settings.tsx +++ b/client/src/pages/settings.tsx @@ -458,88 +458,19 @@ export default function Settings() { - Configuration Stockage MinIO + Stockage des médias - Configurez MinIO pour le stockage des médias + Les médias sont stockés localement sur le serveur - -
- - setCloudName(e.target.value)} - placeholder="socialflow" - data-testid="input-cloud-name" - /> -

- Nom du bucket MinIO dans lequel stocker les fichiers -

+ +
+

✓ Images et vidéos stockées sur le disque du serveur

+

✓ Vidéos supprimées automatiquement après publication

+

✓ Images supprimées automatiquement après 30 jours

+

✓ Vidéos non publiées supprimées après 7 jours

-
- - setApiKey(e.target.value)} - placeholder="minioadmin" - data-testid="input-api-key" - /> -
-
- - setApiSecret(e.target.value)} - placeholder="••••••••••••••••" - data-testid="input-api-secret" - /> -

- Votre Secret Key restera sécurisé et ne sera jamais exposé -

-
-
- - setEndpointUrl(e.target.value)} - placeholder="http://minio:9000" - data-testid="input-endpoint-url" - /> -

- URL interne de votre serveur MinIO (ex: http://minio:9000 ou http://192.168.1.10:9000) -

-
-
- - setPublicUrl(e.target.value)} - placeholder="https://media.votre-domaine.com" - data-testid="input-public-url" - /> -

- URL publique pour accéder aux fichiers depuis internet (ex: https://media.example.com) -

-
-
)} diff --git a/docker-compose.yml b/docker-compose.yml index 115bf02..9d859cd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,9 +43,6 @@ services: # Configuration FFmpeg Service FFMPEG_API_URL: http://ffmpeg-api:8000 FFMPEG_API_KEY: ${FFMPEG_API_KEY:-socialflow-secret-ffmpeg-key} - # Configuration MinIO (stockage fichiers) - MINIO_ENDPOINT: ${MINIO_ENDPOINT:-http://minio:9000} - MINIO_PUBLIC_URL: ${MINIO_PUBLIC_URL:-http://localhost:9000} networks: - internal - nginx_default @@ -55,6 +52,9 @@ services: volumes: - encryption_key:/app/.encryption-key-data - audio_uploads:/app/uploads/audio + - media_uploads:/app/uploads/media + - logos_uploads:/app/uploads/logos + - stories_uploads:/app/uploads/stories command: npm run start # Service FFmpeg dédié @@ -78,6 +78,12 @@ volumes: driver: local audio_uploads: driver: local + media_uploads: + driver: local + logos_uploads: + driver: local + stories_uploads: + driver: local networks: # Réseau interne pour la communication DB <-> App diff --git a/package-lock.json b/package-lock.json index 8428d6e..7177d1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -138,7 +138,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -2743,7 +2742,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -3030,7 +3028,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -3044,7 +3041,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -3054,7 +3050,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -3068,7 +3063,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -7150,7 +7144,7 @@ "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/qs": { @@ -7169,7 +7163,7 @@ "version": "18.3.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -7180,7 +7174,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/react": "*" @@ -7529,7 +7523,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -7542,7 +7535,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -7555,14 +7547,12 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -7602,7 +7592,6 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, "license": "MIT" }, "node_modules/aria-hidden": { @@ -7742,7 +7731,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7800,7 +7788,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -7810,7 +7797,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -7937,7 +7923,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -7982,7 +7967,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -8007,7 +7991,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -8085,7 +8068,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -8098,7 +8080,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/color-support": { @@ -8126,7 +8107,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -8538,14 +8518,12 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, "license": "Apache-2.0" }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, "license": "MIT" }, "node_modules/dom-helpers": { @@ -8738,7 +8716,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, "license": "MIT" }, "node_modules/ee-first": { @@ -9241,7 +9218,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -9258,7 +9234,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -9328,7 +9303,6 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -9377,7 +9351,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -9423,7 +9396,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -9580,7 +9552,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -9752,7 +9723,6 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -9773,7 +9743,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -10045,7 +10014,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -10058,7 +10026,6 @@ "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -10089,7 +10056,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10108,7 +10074,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -10121,7 +10086,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -10161,7 +10125,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -10191,7 +10154,6 @@ "version": "1.21.6", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", - "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -10493,7 +10455,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, "engines": { "node": ">=14" }, @@ -10505,7 +10466,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/loader-runner": { @@ -10717,7 +10677,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -10736,7 +10695,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -10804,7 +10762,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -10829,7 +10786,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -10968,7 +10924,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -11097,7 +11052,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11151,7 +11105,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -11241,7 +11194,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parseurl": { @@ -11327,14 +11279,12 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -11351,7 +11301,6 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, "license": "ISC" }, "node_modules/path-to-regexp": { @@ -11479,7 +11428,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -11492,7 +11440,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11502,7 +11449,6 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -11540,7 +11486,6 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -11558,7 +11503,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -11578,7 +11522,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -11699,7 +11642,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -11725,7 +11667,6 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -11897,7 +11838,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -12036,7 +11976,6 @@ "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12160,7 +12099,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -12184,7 +12122,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -12277,7 +12214,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", @@ -12305,7 +12241,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -12413,7 +12348,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -12684,7 +12618,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -12803,7 +12736,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -12822,7 +12754,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -12837,7 +12768,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12847,14 +12777,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -12867,14 +12795,12 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -12891,7 +12817,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -12904,7 +12829,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12967,7 +12891,6 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -13005,7 +12928,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -13027,7 +12949,6 @@ "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -13248,7 +13169,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -13258,7 +13178,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -13277,7 +13196,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -13323,7 +13241,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, "license": "Apache-2.0" }, "node_modules/tslib": { @@ -14339,7 +14256,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -14358,7 +14274,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -14376,7 +14291,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14386,7 +14300,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -14402,14 +14315,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -14424,7 +14335,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -14480,7 +14390,6 @@ "version": "2.6.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", - "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/server/index.ts b/server/index.ts index 73bd6be..bfc3966 100644 --- a/server/index.ts +++ b/server/index.ts @@ -66,6 +66,17 @@ app.use('/api/auth/login', authLimiter); const AUDIO_UPLOAD_DIR = process.env.AUDIO_UPLOAD_DIR || '/app/uploads/audio'; app.use('/uploads/audio', express.static(AUDIO_UPLOAD_DIR)); +// Serve locally stored media files (images, videos, logos, stories) +const MEDIA_UPLOAD_DIR = path.join(process.cwd(), 'uploads', 'media'); +const LOGOS_UPLOAD_DIR = path.join(process.cwd(), 'uploads', 'logos'); +const STORIES_UPLOAD_DIR = path.join(process.cwd(), 'uploads', 'stories'); +try { fs.mkdirSync(MEDIA_UPLOAD_DIR, { recursive: true }); } catch { /* ignore */ } +try { fs.mkdirSync(LOGOS_UPLOAD_DIR, { recursive: true }); } catch { /* ignore */ } +try { fs.mkdirSync(STORIES_UPLOAD_DIR, { recursive: true }); } catch { /* ignore */ } +app.use('/uploads/media', express.static(MEDIA_UPLOAD_DIR)); +app.use('/uploads/logos', express.static(LOGOS_UPLOAD_DIR)); +app.use('/uploads/stories', express.static(STORIES_UPLOAD_DIR)); + // Serve temp uploads for Remotion const TEMP_UPLOAD_DIR = process.env.TEMP_UPLOAD_DIR || path.join(process.cwd(), 'uploads', 'temp'); try { fs.mkdirSync(TEMP_UPLOAD_DIR, { recursive: true }); } catch { /* ignore */ } diff --git a/server/routes.ts b/server/routes.ts index ce62061..b90dc18 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -1679,11 +1679,11 @@ export async function registerRoutes(app: Express): Promise { return res.status(400).json({ error: "No logo file uploaded" }); } - // Check if Cloudinary is configured globally - const cloudinaryConfig = await storage.getCloudinaryConfig(); + // Get or auto-create config row (local storage doesn't need credentials) + let cloudinaryConfig = await storage.getCloudinaryConfig(); if (!cloudinaryConfig) { - return res.status(400).json({ - error: "Cloudinary not configured. Please configure Cloudinary first." + cloudinaryConfig = await storage.createCloudinaryConfig({ + cloudName: 'local', apiKey: 'local', apiSecret: 'local', }); } diff --git a/server/services/media-purge.ts b/server/services/media-purge.ts index c4aaf03..44e205f 100644 --- a/server/services/media-purge.ts +++ b/server/services/media-purge.ts @@ -1,64 +1,58 @@ import { db } from '../db'; import { media } from '@shared/schema'; -import { eq, lt, isNotNull, and } from 'drizzle-orm'; -import { minioService as cloudinaryService } from './minio'; +import { eq, lt, and } from 'drizzle-orm'; +import { minioService as storageService } from './minio'; /** - * Automates the cleanup of old media to save Cloudinary storage space. - * Scans the database for media older than 60 days, deletes the physical - * files from Cloudinary, and removes the database records (cascading to post_media). + * Purge old local media to save disk space: + * - Images older than 30 days + * - Videos older than 7 days */ export async function purgeOldMedia() { - console.log('[Media Purge] Starting automatic purge of old media...'); + console.log('[Media Purge] Starting automatic purge of old media...'); - try { - const sixtyDaysAgo = new Date(); - sixtyDaysAgo.setDate(sixtyDaysAgo.getDate() - 60); + try { + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); - // Find all media older than 60 days that have a cloudinaryPublicId - const oldMedia = await db.query.media.findMany({ - where: (media, { and, lt, isNotNull }) => and( - lt(media.createdAt, sixtyDaysAgo), - isNotNull(media.cloudinaryPublicId) - ) - }); + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); - if (oldMedia.length === 0) { - console.log('[Media Purge] No old media found to purge.'); - return; - } + const oldImages = await db.query.media.findMany({ + where: (m, { and, lt, eq }) => and(eq(m.type, 'image'), lt(m.createdAt, thirtyDaysAgo)) + }); - console.log(`[Media Purge] Found ${oldMedia.length} media items older than 60 days to purge.`); + const oldVideos = await db.query.media.findMany({ + where: (m, { and, lt, eq }) => and(eq(m.type, 'video'), lt(m.createdAt, sevenDaysAgo)) + }); - let successCount = 0; - let errorCount = 0; + const toDelete = [...oldImages, ...oldVideos]; - for (const item of oldMedia) { - if (!item.cloudinaryPublicId) continue; - - try { - // Delete from Cloudinary (using global config, userId is passed but ignored internally) - await cloudinaryService.deleteMedia( - item.cloudinaryPublicId, - item.userId, - item.type === 'video' ? 'video' : 'image' - ); - - // Delete from Database (cascades to post_media, effectively leaving the post text intact) - await db.delete(media).where(eq(media.id, item.id)); - - successCount++; - console.log(`[Media Purge] Successfully deleted media ${item.id} (Cloudinary ID: ${item.cloudinaryPublicId})`); - } catch (err) { - // Cloudinary file might already be deleted or missing, but we still might want to clean DB if it's consistently failing. - // For safety, we only delete DB row if Cloudinary deletion succeeds or throws 'not found' (which we assume is fine). - console.error(`[Media Purge] Failed to delete media ${item.id} from Cloudinary:`, err); - errorCount++; - } - } - - console.log(`[Media Purge] Purge completed. Successfully deleted: ${successCount}, Failed: ${errorCount}.`); - } catch (error) { - console.error('[Media Purge] Fatal error during media purge process:', error); + if (toDelete.length === 0) { + console.log('[Media Purge] No old media found to purge.'); + return; } + + console.log(`[Media Purge] Found ${oldImages.length} old images (>30 days) and ${oldVideos.length} old videos (>7 days) to purge.`); + + let successCount = 0; + let errorCount = 0; + + for (const item of toDelete) { + try { + if (item.cloudinaryPublicId) { + await storageService.deleteMedia(item.cloudinaryPublicId, item.userId, item.type === 'video' ? 'video' : 'image'); + } + await db.delete(media).where(eq(media.id, item.id)); + successCount++; + } catch (err) { + console.error(`[Media Purge] Failed to delete media ${item.id}:`, err); + errorCount++; + } + } + + console.log(`[Media Purge] Purge completed. Deleted: ${successCount}, Failed: ${errorCount}.`); + } catch (error) { + console.error('[Media Purge] Fatal error during media purge process:', error); + } } diff --git a/server/services/minio.ts b/server/services/minio.ts index 45ecc39..5472c71 100644 --- a/server/services/minio.ts +++ b/server/services/minio.ts @@ -1,33 +1,33 @@ -import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3'; -import { storage } from '../storage'; import fs from 'fs'; import path from 'path'; -function getS3Client(accessKey: string, secretKey: string, endpointUrl?: string | null): S3Client { - const endpoint = endpointUrl || process.env.MINIO_ENDPOINT || 'http://localhost:9000'; - return new S3Client({ - endpoint, - region: 'us-east-1', - credentials: { accessKeyId: accessKey, secretAccessKey: secretKey }, - forcePathStyle: true, - }); +const UPLOADS_BASE = path.resolve(process.cwd(), 'uploads'); + +function ensureDir(dir: string): void { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } +} + +function getAppUrl(): string { + return (process.env.APP_URL || 'http://localhost:5555').replace(/\/$/, ''); } /** - * Builds the public URL for a stored object. - * Uses DB publicUrl > MINIO_PUBLIC_URL env > MINIO_ENDPOINT env > localhost fallback. + * Builds the public URL for a locally stored file. + * objectKey is the relative path from uploads/ (e.g. "media/123-file.jpg"). + * bucketName and publicUrlOverride are ignored (local storage compatibility shim). */ -export function buildMinioUrl(bucketName: string, objectKey: string, publicUrlOverride?: string | null): string { - const base = (publicUrlOverride || process.env.MINIO_PUBLIC_URL || process.env.MINIO_ENDPOINT || 'http://localhost:9000').replace(/\/$/, ''); - return `${base}/${bucketName}/${objectKey}`; +export function buildMinioUrl(_bucketName: string, objectKey: string, _publicUrlOverride?: string | null): string { + return `${getAppUrl()}/uploads/${objectKey}`; } -class MinioService { +class LocalStorageService { async uploadMedia( file: Buffer | string, fileName: string, _userId: string, - mimeType: string + _mimeType: string ): Promise<{ publicId: string; originalUrl: string; @@ -35,78 +35,55 @@ class MinioService { instagramFeedUrl: string | null; instagramStoryUrl: string | null; }> { - const config = await storage.getCloudinaryConfig(); - if (!config) throw new Error('MinIO non configuré. Veuillez configurer le stockage dans les Paramètres.'); + const dir = path.join(UPLOADS_BASE, 'media'); + ensureDir(dir); - const client = getS3Client(config.apiKey, config.apiSecret, config.endpointUrl); - const bucket = config.cloudName; - const safeName = path.basename(fileName).replace(/\s+/g, '-'); - const objectKey = `social-flow/${Date.now()}-${safeName}`; + const safeName = path.basename(fileName).replace(/[^a-zA-Z0-9._-]/g, '-'); + const objectKey = `media/${Date.now()}-${safeName}`; + const filePath = path.join(UPLOADS_BASE, objectKey); const body: Buffer = typeof file === 'string' ? fs.readFileSync(file) : file; + fs.writeFileSync(filePath, body); - await client.send(new PutObjectCommand({ - Bucket: bucket, - Key: objectKey, - Body: body, - ContentType: mimeType, - })); - - const url = buildMinioUrl(bucket, objectKey, config.publicUrl); + const url = buildMinioUrl('', objectKey); return { publicId: objectKey, originalUrl: url, facebookFeedUrl: url, instagramFeedUrl: url, instagramStoryUrl: url }; } async deleteMedia(publicId: string, _userId: string, _mediaType: 'image' | 'video'): Promise { - const config = await storage.getCloudinaryConfig(); - if (!config) throw new Error('MinIO non configuré.'); - - const client = getS3Client(config.apiKey, config.apiSecret, config.endpointUrl); - await client.send(new DeleteObjectCommand({ Bucket: config.cloudName, Key: publicId })); + const filePath = path.join(UPLOADS_BASE, publicId); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } } async uploadStoryImageWithText(buffer: Buffer, originalFileName: string): Promise { - const config = await storage.getCloudinaryConfig(); - if (!config) throw new Error('MinIO non configuré.'); + const dir = path.join(UPLOADS_BASE, 'stories'); + ensureDir(dir); - const client = getS3Client(config.apiKey, config.apiSecret, config.endpointUrl); - const safeName = path.basename(originalFileName).replace(/\s+/g, '-'); - const objectKey = `social-flow/stories/story-${Date.now()}-${safeName}`; + const safeName = path.basename(originalFileName).replace(/[^a-zA-Z0-9._-]/g, '-'); + const objectKey = `stories/story-${Date.now()}-${safeName}`; - await client.send(new PutObjectCommand({ - Bucket: config.cloudName, - Key: objectKey, - Body: buffer, - ContentType: 'image/jpeg', - })); - - return buildMinioUrl(config.cloudName, objectKey, config.publicUrl); + fs.writeFileSync(path.join(UPLOADS_BASE, objectKey), buffer); + return buildMinioUrl('', objectKey); } async uploadLogo(buffer: Buffer, fileName: string): Promise<{ publicId: string; url: string }> { - const config = await storage.getCloudinaryConfig(); - if (!config) throw new Error('MinIO non configuré.'); + const dir = path.join(UPLOADS_BASE, 'logos'); + ensureDir(dir); - const client = getS3Client(config.apiKey, config.apiSecret, config.endpointUrl); const ext = path.extname(fileName) || '.png'; - const objectKey = `social-flow/logos/logo-${Date.now()}${ext}`; + const objectKey = `logos/logo-${Date.now()}${ext}`; - await client.send(new PutObjectCommand({ - Bucket: config.cloudName, - Key: objectKey, - Body: buffer, - ContentType: fileName.match(/\.png$/i) ? 'image/png' : 'image/jpeg', - })); - - return { publicId: objectKey, url: buildMinioUrl(config.cloudName, objectKey, config.publicUrl) }; + fs.writeFileSync(path.join(UPLOADS_BASE, objectKey), buffer); + return { publicId: objectKey, url: buildMinioUrl('', objectKey) }; } async deleteLogo(publicId: string): Promise { - const config = await storage.getCloudinaryConfig(); - if (!config) throw new Error('MinIO non configuré.'); - - const client = getS3Client(config.apiKey, config.apiSecret, config.endpointUrl); - await client.send(new DeleteObjectCommand({ Bucket: config.cloudName, Key: publicId })); + const filePath = path.join(UPLOADS_BASE, publicId); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } } } -export const minioService = new MinioService(); +export const minioService = new LocalStorageService(); diff --git a/server/services/scheduler.ts b/server/services/scheduler.ts index 6b87be1..d9e2364 100644 --- a/server/services/scheduler.ts +++ b/server/services/scheduler.ts @@ -1,6 +1,7 @@ import cron, { ScheduledTask } from "node-cron"; import { storage } from "../storage"; import { facebookService } from "./facebook"; +import { minioService as storageService } from "./minio"; export class SchedulerService { private task: ScheduledTask | null = null; @@ -124,6 +125,18 @@ export class SchedulerService { status: "published", }); + // Delete local video files immediately after successful publishing + for (const mediaItem of mediaList) { + if (mediaItem.type === 'video' && mediaItem.cloudinaryPublicId) { + try { + await storageService.deleteMedia(mediaItem.cloudinaryPublicId, mediaItem.userId, 'video'); + console.log(`[Scheduler] Deleted local video file for media ${mediaItem.id}`); + } catch (err) { + console.warn(`[Scheduler] Failed to delete local video file for media ${mediaItem.id}:`, err); + } + } + } + console.log(`Successfully published post ${post.id} with external ID: ${externalPostId}`); } }