chore: update nginx docker compose

This commit is contained in:
2026-01-22 22:23:37 +01:00
parent c3ac31917a
commit 325b1b7c10
15 changed files with 606 additions and 638 deletions
+1 -1
View File
@@ -191,7 +191,7 @@
- name: Set permissions for .env file
ansible.builtin.file:
path: "{{ docker_dir }}/docker/.env"
path: "{{ docker_dir }}/.env"
owner: "{{ docker_user }}"
group: "{{ docker_group }}"
mode: "0640"
+3 -2
View File
@@ -32,7 +32,8 @@ FROM alpine:latest
RUN apk --no-cache add ca-certificates tzdata wget
# Créer l'utilisateur avec UID/GID fixes
RUN addgroup -g 1000 app && adduser -u 1000 -S app -G app
RUN addgroup -g 101 app && adduser -u 101 -S app -G app
WORKDIR /app
@@ -41,7 +42,7 @@ COPY --from=builder /app/server .
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# Copier l'entrypoint
COPY docker/docker/backend/entrypoint.sh .
COPY docker/backend/entrypoint.sh .
RUN chmod +x entrypoint.sh
# ✅ Créer le dossier uploads et donner ownership AVANT de changer d'utilisateur
+11 -6
View File
@@ -4,8 +4,8 @@ services:
# =========================================================
backend:
build:
context: ../..
dockerfile: docker/docker/backend/Dockerfile
context: ..
dockerfile: docker/backend/Dockerfile
container_name: gestion-backend
restart: unless-stopped
environment:
@@ -26,7 +26,8 @@ services:
- TOMTOM_API_KEY=${TOMTOM_API_KEY}
- API_PORT=${API_PORT:-8080}
volumes:
- backend_uploads:/app/uploads
# ✅ Volume partagé pour les uploads (même volume que le frontend)
- frontend_uploads:/app/uploads
networks:
- gestion-network
depends_on:
@@ -40,8 +41,8 @@ services:
# =========================================================
frontend:
build:
context: ../..
dockerfile: docker/docker/frontend/Dockerfile
context: ..
dockerfile: docker/frontend/Dockerfile
container_name: gestion-frontend
restart: unless-stopped
environment:
@@ -53,6 +54,9 @@ services:
- BACKEND_PORT=${API_PORT:-8080}
ports:
- "80:80"
volumes:
# ✅ Volume partagé pour les uploads (accessible via nginx)
- frontend_uploads:/usr/share/nginx/html/uploads
networks:
- gestion-network
@@ -128,5 +132,6 @@ volumes:
driver: local
redis_data:
driver: local
backend_uploads:
# ✅ Volume partagé entre backend et frontend
frontend_uploads:
driver: local
-134
View File
@@ -1,134 +0,0 @@
services:
# =========================================================
# Backend Go
# =========================================================
backend:
build:
context: ..
dockerfile: docker/backend/Dockerfile
container_name: gestion-backend
restart: unless-stopped
environment:
- DB_HOST=${DB_HOST:-postgres}
- DB_PORT=${DB_PORT:-5432}
- DB_USER=${DB_USER:-postgres}
- DB_PASSWORD=${DB_PASSWORD}
- DB_NAME=${DB_NAME:-gestion_db}
- DB_SSLMODE=${DB_SSLMODE:-disable}
- SESSION_SECRET=${SESSION_SECRET}
- USER_JWT_SECRET=${USER_JWT_SECRET}
- USER_JWT_SECRET_OLD=${USER_JWT_SECRET_OLD}
- ADMIN_JWT_SECRET=${ADMIN_JWT_SECRET}
- ADMIN_JWT_SECRET_OLD=${ADMIN_JWT_SECRET_OLD}
- REDIS_HOST=${REDIS_HOST:-redis}
- REDIS_PORT=${REDIS_PORT:-6379}
- REDIS_PASSWORD=${REDIS_PASSWORD}
- TOMTOM_API_KEY=${TOMTOM_API_KEY}
- API_PORT=${API_PORT:-8080}
ports:
- "${API_PORT:-8080}:8080"
volumes:
- backend_uploads:/app/uploads
networks:
- gestion-network
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
# =========================================================
# Frontend Nginx + ModSecurity
# =========================================================
frontend:
build:
context: ..
dockerfile: docker/frontend/Dockerfile
container_name: gestion-frontend
restart: unless-stopped
environment:
- MODSEC_RULE_ENGINE=On
- PARANOIA=2
- ANOMALY_INBOUND=5
- ANOMALY_OUTBOUND=4
- BACKEND_PORT=${API_PORT:-8080}
ports:
- "80:80"
networks:
- gestion-network
# =========================================================
# PostgreSQL
# =========================================================
postgres:
image: postgres:16-alpine
container_name: gestion-postgres
restart: unless-stopped
environment:
- POSTGRES_USER=${DB_USER:-postgres}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=${DB_NAME:-gestion_db}
- PGDATA=/var/lib/postgresql/data/pgdata
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "${DB_PORT:-5432}:5432"
networks:
- gestion-network
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-gestion_db}",
]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
# =========================================================
# Redis
# =========================================================
redis:
image: redis:7-alpine
container_name: gestion-redis
restart: unless-stopped
command: >
redis-server
--requirepass ${REDIS_PASSWORD}
--appendonly yes
--appendfsync everysec
--maxmemory 256mb
--maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
ports:
- "${REDIS_PORT:-6379}:6379"
networks:
- gestion-network
healthcheck:
test:
[
"CMD",
"redis-cli",
"--no-auth-warning",
"-a",
"${REDIS_PASSWORD}",
"ping",
]
interval: 10s
timeout: 3s
retries: 5
start_period: 10s
networks:
gestion-network:
driver: bridge
volumes:
postgres_data:
driver: local
redis_data:
driver: local
backend_uploads:
driver: local
+2 -2
View File
@@ -36,8 +36,8 @@ COPY --from=builder /build/dist /usr/share/nginx/html
# Logs ModSecurity
RUN mkdir -p /var/log/modsec && chown -R nginx:nginx /var/log/modsec
COPY docker/docker/frontend/nginx.conf /etc/nginx/conf.d/app.conf
COPY docker/docker/frontend/custom-rules.conf /etc/nginx/modsec/custom-rules.conf
COPY docker/frontend/nginx.conf /etc/nginx/conf.d/app.conf
COPY docker/frontend/custom-rules.conf /etc/nginx/modsec/custom-rules.conf
RUN echo "Include /etc/nginx/modsec/custom-rules.conf" > /etc/nginx/modsec/custom-includes.conf
RUN rm -f /etc/nginx/templates/conf.d/default.conf.template || true
-49
View File
@@ -1,49 +0,0 @@
# =========================================================
# Stage 1: Build Frontend
# =========================================================
FROM node:20-alpine AS builder
WORKDIR /build
COPY frontend-prep/package*.json ./
RUN npm ci --only=production
COPY frontend-prep .
RUN npm run build && ls -la dist/
# =========================================================
# Stage 2: Nginx + ModSecurity
# =========================================================
FROM owasp/modsecurity-crs:nginx-alpine
USER root
# Copier le frontend build
COPY --from=builder /build/dist /usr/share/nginx/html
# Configs Nginx / ModSecurity
COPY docker/frontend/nginx.conf.prod /etc/nginx/conf.d/app.conf
COPY docker/frontend/main.conf /etc/nginx/modsec/main.conf
COPY docker/frontend/ban-on-crs-scores-delivery-api.conf /etc/nginx/modsec/ban-on-crs-scores-delivery-api.conf
COPY docker/frontend/crs-setup.conf /etc/nginx/modsec/crs-setup.conf
# Supprimer template par défaut
RUN rm -f /etc/nginx/templates/conf.d/default.conf.template || true
# Variables ModSecurity
ENV MODSEC_ENGINE=On \
PARANOIA=2 \
ANOMALY_INBOUND=5 \
ANOMALY_OUTBOUND=4
# Permissions
RUN chown -R nginx:nginx /usr/share/nginx/html \
&& mkdir -p /etc/letsencrypt \
&& chown -R nginx:nginx /etc/letsencrypt
USER nginx
EXPOSE 80 443
STOPSIGNAL SIGQUIT
CMD ["nginx", "-g", "daemon off;"]
+7
View File
@@ -6,6 +6,13 @@ SecRule REQUEST_URI "@streq /api/v2/admin/protected/products" \
ctl:ruleRemoveById=920120,\
ctl:ruleRemoveById=920121"
SecRule REQUEST_URI "@beginsWith /uploads/" \
"id:1000,\
phase:1,\
pass,\
nolog,\
ctl:ruleEngine=Off"
SecRule IP:BANNED "@eq 1" \
"id:100000,phase:1,deny,status:403,log,\
msg:'IP is banned'"
+59 -3
View File
@@ -67,12 +67,68 @@ server {
add_header Pragma "no-cache" always;
add_header Expires "0" always;
}
location /uploads/ {
# ✅ IMPORTANT : alias doit se terminer par / ET le chemin aussi
alias /usr/share/nginx/html/uploads/;
location ~* \.(css|js|jpg|jpeg|png|gif|svg|ico|woff2?|ttf|eot)$ {
expires 1y;
# ✅ Désactiver ModSecurity pour les uploads
modsecurity off;
# Cache headers
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
location ^~ /uploads/images/ {
alias /usr/share/nginx/html/uploads/images/;
# ✅ Désactiver ModSecurity
modsecurity off;
# ✅ CORS pour images publiques
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
# Cache agressif
expires 30d;
add_header Cache-Control "public, immutable" always;
# Types MIME
types {
image/jpeg jpg jpeg;
image/png png;
image/gif gif;
image/webp webp;
image/svg+xml svg;
}
default_type image/jpeg;
}
# =========================================================
# ✅ UPLOADS VIDEOS (priorité maximale avec ^~)
# =========================================================
location ^~ /uploads/videos/ {
alias /usr/share/nginx/html/uploads/videos/;
# ✅ Désactiver ModSecurity
modsecurity off;
# ✅ CORS pour vidéos publiques
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
# Cache agressif
expires 30d;
add_header Cache-Control "public, immutable" always;
# Types MIME pour vidéos
types {
video/mp4 mp4;
video/webm webm;
video/ogg ogv;
}
default_type video/mp4;
}
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
@@ -114,14 +114,14 @@ const ProductDetailsModal: React.FC<ProductDetailsModalProps> = ({
<div className="media-viewer">
{currentMedia?.type === "image" ? (
<img
src={`http://localhost:8080${currentMedia.url}`}
src={`${currentMedia.url}`}
alt={product.name}
className="media-display"
/>
) : currentMedia?.type === "video" ? (
<div className="video-container">
<video
src={`http://localhost:8080${currentMedia.url}`}
src={`${currentMedia.url}`}
controls
className="media-display"
>
@@ -179,7 +179,7 @@ const ProductDetailsModal: React.FC<ProductDetailsModalProps> = ({
>
{media.type === "image" ? (
<img
src={`http://localhost:8080${media.url}`}
src={`{product.media[0].url}${media.url}`}
alt={`Media ${index + 1}`}
/>
) : (
+144 -56
View File
@@ -6,11 +6,22 @@
// ✅ Upload séparé des médias
// ✅ Boutons au lieu de select (comme AdminUsers)
import React, { useState } from 'react';
import { X, Plus, Trash2, Upload, DollarSign, Image as ImageIcon } from 'lucide-react';
import { updateProductAdmin, deleteProductMediaAdmin, uploadProductMediaAdmin } from '../api/api_admin';
import type { Product, ProductPrice } from '../api/api_admin_types';
import './ProductModal.css';
import React, { useState } from "react";
import {
X,
Plus,
Trash2,
Upload,
DollarSign,
Image as ImageIcon,
} from "lucide-react";
import {
updateProductAdmin,
deleteProductMediaAdmin,
uploadProductMediaAdmin,
} from "../api/api_admin";
import type { Product, ProductPrice } from "../api/api_admin_types";
import "./ProductModal.css";
interface ProductEditModalProps {
product: Product;
@@ -18,7 +29,11 @@ interface ProductEditModalProps {
onSuccess: () => void;
}
const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, onSuccess }) => {
const ProductEditModal: React.FC<ProductEditModalProps> = ({
product,
onClose,
onSuccess,
}) => {
// ============================================
// 📝 STATE
// ============================================
@@ -29,7 +44,7 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
const [prices, setPrices] = useState<ProductPrice[]>(
product.prices && product.prices.length > 0
? product.prices
: [{ quantity: 1, price: 0 }]
: [{ quantity: 1, price: 0 }],
);
const [existingMedia, setExistingMedia] = useState(product.media || []);
const [newMediaFiles, setNewMediaFiles] = useState<File[]>([]);
@@ -39,9 +54,9 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
// ✅ Options de catégories
const categoryOptions = [
{ value: 'weed&hash', label: 'Weed & Hash' },
{ value: 'zipette&co', label: 'Zipette & Co' },
{ value: 'gros&semi', label: 'Gros & Semi' }
{ value: "weed&hash", label: "Weed & Hash" },
{ value: "zipette&co", label: "Zipette & Co" },
{ value: "gros&semi", label: "Gros & Semi" },
];
// ============================================
@@ -57,7 +72,11 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
}
};
const updatePrice = (index: number, field: 'quantity' | 'price', value: number) => {
const updatePrice = (
index: number,
field: "quantity" | "price",
value: number,
) => {
const newPrices = [...prices];
newPrices[index][field] = value;
setPrices(newPrices);
@@ -70,7 +89,9 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
if (e.target.files) {
const newFiles = Array.from(e.target.files);
setNewMediaFiles([...newMediaFiles, ...newFiles]);
console.log(`📁 ${newFiles.length} nouveau(x) fichier(s) ajouté(s)`);
console.log(
`📁 ${newFiles.length} nouveau(x) fichier(s) ajouté(s)`,
);
}
};
@@ -80,7 +101,7 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
const markMediaForDeletion = (mediaId: number) => {
setMediaToDelete([...mediaToDelete, mediaId]);
setExistingMedia(existingMedia.filter(m => m.id !== mediaId));
setExistingMedia(existingMedia.filter((m) => m.id !== mediaId));
console.log(`🗑️ Média ${mediaId} marqué pour suppression`);
};
@@ -88,14 +109,15 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
// ✅ VALIDATION
// ============================================
const validateForm = (): string | null => {
if (!name.trim()) return 'Le nom est requis';
if (!description.trim()) return 'La description est requise';
if (stock < 0) return 'Le stock ne peut pas être négatif';
if (prices.length === 0) return 'Au moins un prix est requis';
if (!name.trim()) return "Le nom est requis";
if (!description.trim()) return "La description est requise";
if (stock < 0) return "Le stock ne peut pas être négatif";
if (prices.length === 0) return "Au moins un prix est requis";
for (const price of prices) {
if (price.quantity <= 0) return 'Toutes les quantités doivent être positives';
if (price.price <= 0) return 'Tous les prix doivent être positifs';
if (price.quantity <= 0)
return "Toutes les quantités doivent être positives";
if (price.price <= 0) return "Tous les prix doivent être positifs";
}
return null;
@@ -114,7 +136,7 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
}
if (!product.id) {
setError('ID du produit manquant');
setError("ID du produit manquant");
return;
}
@@ -122,20 +144,28 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
setLoading(true);
setError(null);
console.log('📤 [EDIT_MODAL] Début de la mise à jour du produit:', product.id);
console.log(
"📤 [EDIT_MODAL] Début de la mise à jour du produit:",
product.id,
);
// ============================================
// ÉTAPE 1: Supprimer les médias marqués
// ============================================
if (mediaToDelete.length > 0) {
console.log(`🗑️ Suppression de ${mediaToDelete.length} média(s)...`);
console.log(
`🗑️ Suppression de ${mediaToDelete.length} média(s)...`,
);
for (const mediaId of mediaToDelete) {
try {
await deleteProductMediaAdmin(product.id, mediaId);
console.log(`✅ Média ${mediaId} supprimé`);
} catch (err) {
console.warn(`⚠️ Erreur suppression média ${mediaId}:`, err);
console.warn(
`⚠️ Erreur suppression média ${mediaId}:`,
err,
);
}
}
}
@@ -148,13 +178,13 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
category,
description: description.trim(),
stock,
prices: prices.map(p => ({
prices: prices.map((p) => ({
quantity: p.quantity,
price: p.price
}))
price: p.price,
})),
};
console.log('📝 [EDIT_MODAL] Données de mise à jour:', updateData);
console.log("📝 [EDIT_MODAL] Données de mise à jour:", updateData);
// ============================================
// ÉTAPE 3: Mettre à jour le produit
@@ -162,29 +192,43 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
const response = await updateProductAdmin(product.id, updateData);
if (!response.success) {
console.error('❌ [EDIT_MODAL] Erreur API:', response.error);
setError(response.error || 'Erreur lors de la mise à jour du produit');
console.error("❌ [EDIT_MODAL] Erreur API:", response.error);
setError(
response.error ||
"Erreur lors de la mise à jour du produit",
);
setLoading(false);
return;
}
console.log('✅ [EDIT_MODAL] Produit mis à jour avec succès');
console.log("✅ [EDIT_MODAL] Produit mis à jour avec succès");
// ============================================
// ÉTAPE 4: Upload des nouveaux médias
// ============================================
if (newMediaFiles.length > 0) {
console.log(`📤 [EDIT_MODAL] Upload de ${newMediaFiles.length} nouveau(x) média(s)...`);
console.log(
`📤 [EDIT_MODAL] Upload de ${newMediaFiles.length} nouveau(x) média(s)...`,
);
for (const file of newMediaFiles) {
try {
const fileType = file.type.startsWith('image/') ? 'image' : 'video';
const uploadResponse = await uploadProductMediaAdmin(product.id, file, fileType);
const fileType = file.type.startsWith("image/")
? "image"
: "video";
const uploadResponse = await uploadProductMediaAdmin(
product.id,
file,
fileType,
);
if (uploadResponse.success) {
console.log(`✅ Média uploadé: ${file.name}`);
} else {
console.warn(`⚠️ Erreur upload ${file.name}:`, uploadResponse.error);
console.warn(
`⚠️ Erreur upload ${file.name}:`,
uploadResponse.error,
);
}
} catch (err) {
console.warn(`⚠️ Erreur upload ${file.name}:`, err);
@@ -192,14 +236,15 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
}
}
console.log('🎉 [EDIT_MODAL] Toutes les modifications appliquées');
console.log("🎉 [EDIT_MODAL] Toutes les modifications appliquées");
// Fermer et rafraîchir
onSuccess();
} catch (error) {
console.error('❌ [EDIT_MODAL] Erreur:', error);
setError(error instanceof Error ? error.message : 'Erreur inconnue');
console.error("❌ [EDIT_MODAL] Erreur:", error);
setError(
error instanceof Error ? error.message : "Erreur inconnue",
);
} finally {
setLoading(false);
}
@@ -215,7 +260,11 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
{/* Header */}
<div className="modal-header">
<h2>Modifier le Produit</h2>
<button className="close-modal" onClick={onClose} disabled={loading}>
<button
className="close-modal"
onClick={onClose}
disabled={loading}
>
<X size={24} />
</button>
</div>
@@ -249,12 +298,18 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
<div className="form-group">
<label>Catégorie *</label>
<div className="filter-buttons">
{categoryOptions.map(option => (
{categoryOptions.map((option) => (
<button
key={option.value}
type="button"
className={category === option.value ? 'active' : ''}
onClick={() => setCategory(option.value)}
className={
category === option.value
? "active"
: ""
}
onClick={() =>
setCategory(option.value)
}
disabled={loading}
>
{option.label}
@@ -284,7 +339,9 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
min="0"
step="0.1"
value={stock}
onChange={(e) => setStock(parseFloat(e.target.value) || 0)}
onChange={(e) =>
setStock(parseFloat(e.target.value) || 0)
}
placeholder="100"
disabled={loading}
required
@@ -317,7 +374,14 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
min="1"
step="1"
value={price.quantity}
onChange={(e) => updatePrice(index, 'quantity', parseInt(e.target.value) || 1)}
onChange={(e) =>
updatePrice(
index,
"quantity",
parseInt(e.target.value) ||
1,
)
}
disabled={loading}
required
/>
@@ -332,7 +396,15 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
min="0"
step="0.01"
value={price.price}
onChange={(e) => updatePrice(index, 'price', parseFloat(e.target.value) || 0)}
onChange={(e) =>
updatePrice(
index,
"price",
parseFloat(
e.target.value,
) || 0,
)
}
disabled={loading}
required
/>
@@ -343,7 +415,9 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
<button
type="button"
className="remove-price-btn"
onClick={() => removePriceRow(index)}
onClick={() =>
removePriceRow(index)
}
disabled={loading}
title="Supprimer ce prix"
>
@@ -361,10 +435,13 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
<h3>Médias Actuels</h3>
<div className="existing-media-grid">
{existingMedia.map((media) => (
<div key={media.id} className="existing-media-item">
{media.type === 'image' ? (
<div
key={media.id}
className="existing-media-item"
>
{media.type === "image" ? (
<img
src={`http://localhost:8080${media.url}`}
src={`${media.url}`}
alt="Media"
className="media-thumbnail"
/>
@@ -377,7 +454,9 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
<button
type="button"
className="delete-media-btn"
onClick={() => markMediaForDeletion(media.id!)}
onClick={() =>
markMediaForDeletion(media.id!)
}
disabled={loading}
title="Supprimer ce média"
>
@@ -394,10 +473,16 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
<h3>Ajouter de Nouveaux Médias</h3>
<div className="upload-zone">
<label htmlFor="media-upload" className="upload-label">
<label
htmlFor="media-upload"
className="upload-label"
>
<Upload size={32} />
<p>Cliquez pour ajouter des fichiers</p>
<span>Images (JPG, PNG, WebP) ou Vidéos (MP4, WebM)</span>
<span>
Images (JPG, PNG, WebP) ou Vidéos (MP4,
WebM)
</span>
</label>
<input
id="media-upload"
@@ -406,7 +491,7 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
multiple
onChange={handleFileSelect}
disabled={loading}
style={{ display: 'none' }}
style={{ display: "none" }}
/>
</div>
@@ -415,9 +500,12 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
{newMediaFiles.map((file, index) => (
<div key={index} className="file-item">
<div className="file-info">
<span className="file-name">{file.name}</span>
<span className="file-name">
{file.name}
</span>
<span className="file-size">
{(file.size / 1024).toFixed(1)} KB
{(file.size / 1024).toFixed(1)}{" "}
KB
</span>
</div>
<button
@@ -451,7 +539,7 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
onClick={handleSubmit}
disabled={loading}
>
{loading ? 'Mise à jour...' : 'Mettre à Jour'}
{loading ? "Mise à jour..." : "Mettre à Jour"}
</button>
</div>
</div>
@@ -514,7 +514,7 @@ const AdminProduct: React.FC = () => {
product.media.length > 0 ? (
<div className="product-image">
<img
src={`http://localhost:8080${product.media[0].url}`}
src={`${product.media[0].url}`}
alt={
product.name
}
+2 -4
View File
@@ -85,9 +85,7 @@ function UserAccueil() {
(mediaItem) => mediaItem && mediaItem.type === "video",
);
return videoMedia?.url
? `http://localhost:8080${videoMedia.url}`
: undefined;
return videoMedia?.url ? `${videoMedia.url}` : undefined;
};
const getProductImage = (product: Product): string => {
// Pas de media ? Image placeholder
@@ -101,7 +99,7 @@ function UserAccueil() {
// Vérifier si c'est un objet avec type "image" et url
if (mediaItem && mediaItem.type === "image" && mediaItem.url) {
return `http://localhost:8080${mediaItem.url}`;
return `${mediaItem.url}`;
}
}
+2 -4
View File
@@ -111,7 +111,7 @@ function Cart() {
for (let i = 0; i < product.media.length; i++) {
const mediaItem = product.media[i];
if (mediaItem && mediaItem.type === "image" && mediaItem.url) {
return `http://localhost:8080${mediaItem.url}`;
return `${mediaItem.url}`;
}
}
@@ -136,9 +136,7 @@ function Cart() {
(mediaItem) => mediaItem && mediaItem.type === "video",
);
return videoMedia?.url
? `http://localhost:8080${videoMedia.url}`
: undefined;
return videoMedia?.url ? `${videoMedia.url}` : undefined;
};
// ============================================
@@ -117,7 +117,7 @@ function OrderDetails() {
for (let i = 0; i < product.media.length; i++) {
const mediaItem = product.media[i];
if (mediaItem && mediaItem.type === "image" && mediaItem.url) {
return `http://localhost:8080${mediaItem.url}`;
return `${mediaItem.url}`;
}
}
@@ -142,9 +142,7 @@ function OrderDetails() {
(mediaItem) => mediaItem && mediaItem.type === "video",
);
return videoMedia?.url
? `http://localhost:8080${videoMedia.url}`
: undefined;
return videoMedia?.url ? `${videoMedia.url}` : undefined;
};
// HANDLERS VIDÉO
+1 -1
View File
@@ -5,7 +5,7 @@
# Version avec validation GPS + Simulation déplacement livreur
# ============================================
BASE_URL="https://d437671454fb1f66-90-50-148-138.serveousercontent.com"
BASE_URL="https://c80319bf2e28244c-90-50-148-138.serveousercontent.com"
ADMIN_TOKEN=""
CABINE_TOKEN=""
LIVREUR_TOKEN=""