chore: update nginx docker compose
This commit is contained in:
@@ -191,7 +191,7 @@
|
|||||||
|
|
||||||
- name: Set permissions for .env file
|
- name: Set permissions for .env file
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ docker_dir }}/docker/.env"
|
path: "{{ docker_dir }}/.env"
|
||||||
owner: "{{ docker_user }}"
|
owner: "{{ docker_user }}"
|
||||||
group: "{{ docker_group }}"
|
group: "{{ docker_group }}"
|
||||||
mode: "0640"
|
mode: "0640"
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ FROM alpine:latest
|
|||||||
RUN apk --no-cache add ca-certificates tzdata wget
|
RUN apk --no-cache add ca-certificates tzdata wget
|
||||||
|
|
||||||
# Créer l'utilisateur avec UID/GID fixes
|
# 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
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ COPY --from=builder /app/server .
|
|||||||
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
||||||
|
|
||||||
# Copier l'entrypoint
|
# Copier l'entrypoint
|
||||||
COPY docker/docker/backend/entrypoint.sh .
|
COPY docker/backend/entrypoint.sh .
|
||||||
RUN chmod +x entrypoint.sh
|
RUN chmod +x entrypoint.sh
|
||||||
|
|
||||||
# ✅ Créer le dossier uploads et donner ownership AVANT de changer d'utilisateur
|
# ✅ Créer le dossier uploads et donner ownership AVANT de changer d'utilisateur
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ services:
|
|||||||
# =========================================================
|
# =========================================================
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: ../..
|
context: ..
|
||||||
dockerfile: docker/docker/backend/Dockerfile
|
dockerfile: docker/backend/Dockerfile
|
||||||
container_name: gestion-backend
|
container_name: gestion-backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -26,7 +26,8 @@ services:
|
|||||||
- TOMTOM_API_KEY=${TOMTOM_API_KEY}
|
- TOMTOM_API_KEY=${TOMTOM_API_KEY}
|
||||||
- API_PORT=${API_PORT:-8080}
|
- API_PORT=${API_PORT:-8080}
|
||||||
volumes:
|
volumes:
|
||||||
- backend_uploads:/app/uploads
|
# ✅ Volume partagé pour les uploads (même volume que le frontend)
|
||||||
|
- frontend_uploads:/app/uploads
|
||||||
networks:
|
networks:
|
||||||
- gestion-network
|
- gestion-network
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -40,8 +41,8 @@ services:
|
|||||||
# =========================================================
|
# =========================================================
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ../..
|
context: ..
|
||||||
dockerfile: docker/docker/frontend/Dockerfile
|
dockerfile: docker/frontend/Dockerfile
|
||||||
container_name: gestion-frontend
|
container_name: gestion-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -53,6 +54,9 @@ services:
|
|||||||
- BACKEND_PORT=${API_PORT:-8080}
|
- BACKEND_PORT=${API_PORT:-8080}
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
# ✅ Volume partagé pour les uploads (accessible via nginx)
|
||||||
|
- frontend_uploads:/usr/share/nginx/html/uploads
|
||||||
networks:
|
networks:
|
||||||
- gestion-network
|
- gestion-network
|
||||||
|
|
||||||
@@ -128,5 +132,6 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
redis_data:
|
redis_data:
|
||||||
driver: local
|
driver: local
|
||||||
backend_uploads:
|
# ✅ Volume partagé entre backend et frontend
|
||||||
|
frontend_uploads:
|
||||||
driver: local
|
driver: local
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -36,8 +36,8 @@ COPY --from=builder /build/dist /usr/share/nginx/html
|
|||||||
# Logs ModSecurity
|
# Logs ModSecurity
|
||||||
RUN mkdir -p /var/log/modsec && chown -R nginx:nginx /var/log/modsec
|
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/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/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 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
|
RUN rm -f /etc/nginx/templates/conf.d/default.conf.template || true
|
||||||
|
|||||||
@@ -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;"]
|
|
||||||
@@ -6,6 +6,13 @@ SecRule REQUEST_URI "@streq /api/v2/admin/protected/products" \
|
|||||||
ctl:ruleRemoveById=920120,\
|
ctl:ruleRemoveById=920120,\
|
||||||
ctl:ruleRemoveById=920121"
|
ctl:ruleRemoveById=920121"
|
||||||
|
|
||||||
|
SecRule REQUEST_URI "@beginsWith /uploads/" \
|
||||||
|
"id:1000,\
|
||||||
|
phase:1,\
|
||||||
|
pass,\
|
||||||
|
nolog,\
|
||||||
|
ctl:ruleEngine=Off"
|
||||||
|
|
||||||
SecRule IP:BANNED "@eq 1" \
|
SecRule IP:BANNED "@eq 1" \
|
||||||
"id:100000,phase:1,deny,status:403,log,\
|
"id:100000,phase:1,deny,status:403,log,\
|
||||||
msg:'IP is banned'"
|
msg:'IP is banned'"
|
||||||
|
|||||||
@@ -67,12 +67,68 @@ server {
|
|||||||
add_header Pragma "no-cache" always;
|
add_header Pragma "no-cache" always;
|
||||||
add_header Expires "0" 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)$ {
|
# ✅ Désactiver ModSecurity pour les uploads
|
||||||
expires 1y;
|
modsecurity off;
|
||||||
|
|
||||||
|
# Cache headers
|
||||||
|
expires 30d;
|
||||||
add_header Cache-Control "public, immutable";
|
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-Frame-Options "SAMEORIGIN" always;
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
|||||||
@@ -114,14 +114,14 @@ const ProductDetailsModal: React.FC<ProductDetailsModalProps> = ({
|
|||||||
<div className="media-viewer">
|
<div className="media-viewer">
|
||||||
{currentMedia?.type === "image" ? (
|
{currentMedia?.type === "image" ? (
|
||||||
<img
|
<img
|
||||||
src={`http://localhost:8080${currentMedia.url}`}
|
src={`${currentMedia.url}`}
|
||||||
alt={product.name}
|
alt={product.name}
|
||||||
className="media-display"
|
className="media-display"
|
||||||
/>
|
/>
|
||||||
) : currentMedia?.type === "video" ? (
|
) : currentMedia?.type === "video" ? (
|
||||||
<div className="video-container">
|
<div className="video-container">
|
||||||
<video
|
<video
|
||||||
src={`http://localhost:8080${currentMedia.url}`}
|
src={`${currentMedia.url}`}
|
||||||
controls
|
controls
|
||||||
className="media-display"
|
className="media-display"
|
||||||
>
|
>
|
||||||
@@ -179,7 +179,7 @@ const ProductDetailsModal: React.FC<ProductDetailsModalProps> = ({
|
|||||||
>
|
>
|
||||||
{media.type === "image" ? (
|
{media.type === "image" ? (
|
||||||
<img
|
<img
|
||||||
src={`http://localhost:8080${media.url}`}
|
src={`{product.media[0].url}${media.url}`}
|
||||||
alt={`Media ${index + 1}`}
|
alt={`Media ${index + 1}`}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -6,11 +6,22 @@
|
|||||||
// ✅ Upload séparé des médias
|
// ✅ Upload séparé des médias
|
||||||
// ✅ Boutons au lieu de select (comme AdminUsers)
|
// ✅ Boutons au lieu de select (comme AdminUsers)
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from "react";
|
||||||
import { X, Plus, Trash2, Upload, DollarSign, Image as ImageIcon } from 'lucide-react';
|
import {
|
||||||
import { updateProductAdmin, deleteProductMediaAdmin, uploadProductMediaAdmin } from '../api/api_admin';
|
X,
|
||||||
import type { Product, ProductPrice } from '../api/api_admin_types';
|
Plus,
|
||||||
import './ProductModal.css';
|
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 {
|
interface ProductEditModalProps {
|
||||||
product: Product;
|
product: Product;
|
||||||
@@ -18,7 +29,11 @@ interface ProductEditModalProps {
|
|||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, onSuccess }) => {
|
const ProductEditModal: React.FC<ProductEditModalProps> = ({
|
||||||
|
product,
|
||||||
|
onClose,
|
||||||
|
onSuccess,
|
||||||
|
}) => {
|
||||||
// ============================================
|
// ============================================
|
||||||
// 📝 STATE
|
// 📝 STATE
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -29,7 +44,7 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
|
|||||||
const [prices, setPrices] = useState<ProductPrice[]>(
|
const [prices, setPrices] = useState<ProductPrice[]>(
|
||||||
product.prices && product.prices.length > 0
|
product.prices && product.prices.length > 0
|
||||||
? product.prices
|
? product.prices
|
||||||
: [{ quantity: 1, price: 0 }]
|
: [{ quantity: 1, price: 0 }],
|
||||||
);
|
);
|
||||||
const [existingMedia, setExistingMedia] = useState(product.media || []);
|
const [existingMedia, setExistingMedia] = useState(product.media || []);
|
||||||
const [newMediaFiles, setNewMediaFiles] = useState<File[]>([]);
|
const [newMediaFiles, setNewMediaFiles] = useState<File[]>([]);
|
||||||
@@ -39,9 +54,9 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
|
|||||||
|
|
||||||
// ✅ Options de catégories
|
// ✅ Options de catégories
|
||||||
const categoryOptions = [
|
const categoryOptions = [
|
||||||
{ value: 'weed&hash', label: 'Weed & Hash' },
|
{ value: "weed&hash", label: "Weed & Hash" },
|
||||||
{ value: 'zipette&co', label: 'Zipette & Co' },
|
{ value: "zipette&co", label: "Zipette & Co" },
|
||||||
{ value: 'gros&semi', label: 'Gros & Semi' }
|
{ 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];
|
const newPrices = [...prices];
|
||||||
newPrices[index][field] = value;
|
newPrices[index][field] = value;
|
||||||
setPrices(newPrices);
|
setPrices(newPrices);
|
||||||
@@ -70,7 +89,9 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
|
|||||||
if (e.target.files) {
|
if (e.target.files) {
|
||||||
const newFiles = Array.from(e.target.files);
|
const newFiles = Array.from(e.target.files);
|
||||||
setNewMediaFiles([...newMediaFiles, ...newFiles]);
|
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) => {
|
const markMediaForDeletion = (mediaId: number) => {
|
||||||
setMediaToDelete([...mediaToDelete, mediaId]);
|
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`);
|
console.log(`🗑️ Média ${mediaId} marqué pour suppression`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,14 +109,15 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
|
|||||||
// ✅ VALIDATION
|
// ✅ VALIDATION
|
||||||
// ============================================
|
// ============================================
|
||||||
const validateForm = (): string | null => {
|
const validateForm = (): string | null => {
|
||||||
if (!name.trim()) return 'Le nom est requis';
|
if (!name.trim()) return "Le nom est requis";
|
||||||
if (!description.trim()) return 'La description est requise';
|
if (!description.trim()) return "La description est requise";
|
||||||
if (stock < 0) return 'Le stock ne peut pas être négatif';
|
if (stock < 0) return "Le stock ne peut pas être négatif";
|
||||||
if (prices.length === 0) return 'Au moins un prix est requis';
|
if (prices.length === 0) return "Au moins un prix est requis";
|
||||||
|
|
||||||
for (const price of prices) {
|
for (const price of prices) {
|
||||||
if (price.quantity <= 0) return 'Toutes les quantités doivent être positives';
|
if (price.quantity <= 0)
|
||||||
if (price.price <= 0) return 'Tous les prix doivent être positifs';
|
return "Toutes les quantités doivent être positives";
|
||||||
|
if (price.price <= 0) return "Tous les prix doivent être positifs";
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -114,7 +136,7 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!product.id) {
|
if (!product.id) {
|
||||||
setError('ID du produit manquant');
|
setError("ID du produit manquant");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,20 +144,28 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
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
|
// ÉTAPE 1: Supprimer les médias marqués
|
||||||
// ============================================
|
// ============================================
|
||||||
if (mediaToDelete.length > 0) {
|
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) {
|
for (const mediaId of mediaToDelete) {
|
||||||
try {
|
try {
|
||||||
await deleteProductMediaAdmin(product.id, mediaId);
|
await deleteProductMediaAdmin(product.id, mediaId);
|
||||||
console.log(`✅ Média ${mediaId} supprimé`);
|
console.log(`✅ Média ${mediaId} supprimé`);
|
||||||
} catch (err) {
|
} 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,
|
category,
|
||||||
description: description.trim(),
|
description: description.trim(),
|
||||||
stock,
|
stock,
|
||||||
prices: prices.map(p => ({
|
prices: prices.map((p) => ({
|
||||||
quantity: p.quantity,
|
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
|
// É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);
|
const response = await updateProductAdmin(product.id, updateData);
|
||||||
|
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
console.error('❌ [EDIT_MODAL] Erreur API:', response.error);
|
console.error("❌ [EDIT_MODAL] Erreur API:", response.error);
|
||||||
setError(response.error || 'Erreur lors de la mise à jour du produit');
|
setError(
|
||||||
|
response.error ||
|
||||||
|
"Erreur lors de la mise à jour du produit",
|
||||||
|
);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
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
|
// ÉTAPE 4: Upload des nouveaux médias
|
||||||
// ============================================
|
// ============================================
|
||||||
if (newMediaFiles.length > 0) {
|
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) {
|
for (const file of newMediaFiles) {
|
||||||
try {
|
try {
|
||||||
const fileType = file.type.startsWith('image/') ? 'image' : 'video';
|
const fileType = file.type.startsWith("image/")
|
||||||
const uploadResponse = await uploadProductMediaAdmin(product.id, file, fileType);
|
? "image"
|
||||||
|
: "video";
|
||||||
|
const uploadResponse = await uploadProductMediaAdmin(
|
||||||
|
product.id,
|
||||||
|
file,
|
||||||
|
fileType,
|
||||||
|
);
|
||||||
|
|
||||||
if (uploadResponse.success) {
|
if (uploadResponse.success) {
|
||||||
console.log(`✅ Média uploadé: ${file.name}`);
|
console.log(`✅ Média uploadé: ${file.name}`);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`⚠️ Erreur upload ${file.name}:`, uploadResponse.error);
|
console.warn(
|
||||||
|
`⚠️ Erreur upload ${file.name}:`,
|
||||||
|
uploadResponse.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`⚠️ Erreur upload ${file.name}:`, 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
|
// Fermer et rafraîchir
|
||||||
onSuccess();
|
onSuccess();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ [EDIT_MODAL] Erreur:', error);
|
console.error("❌ [EDIT_MODAL] Erreur:", error);
|
||||||
setError(error instanceof Error ? error.message : 'Erreur inconnue');
|
setError(
|
||||||
|
error instanceof Error ? error.message : "Erreur inconnue",
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -215,7 +260,11 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<h2>Modifier le Produit</h2>
|
<h2>Modifier le Produit</h2>
|
||||||
<button className="close-modal" onClick={onClose} disabled={loading}>
|
<button
|
||||||
|
className="close-modal"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
<X size={24} />
|
<X size={24} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -249,12 +298,18 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
|
|||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Catégorie *</label>
|
<label>Catégorie *</label>
|
||||||
<div className="filter-buttons">
|
<div className="filter-buttons">
|
||||||
{categoryOptions.map(option => (
|
{categoryOptions.map((option) => (
|
||||||
<button
|
<button
|
||||||
key={option.value}
|
key={option.value}
|
||||||
type="button"
|
type="button"
|
||||||
className={category === option.value ? 'active' : ''}
|
className={
|
||||||
onClick={() => setCategory(option.value)}
|
category === option.value
|
||||||
|
? "active"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
setCategory(option.value)
|
||||||
|
}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
@@ -284,7 +339,9 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
|
|||||||
min="0"
|
min="0"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
value={stock}
|
value={stock}
|
||||||
onChange={(e) => setStock(parseFloat(e.target.value) || 0)}
|
onChange={(e) =>
|
||||||
|
setStock(parseFloat(e.target.value) || 0)
|
||||||
|
}
|
||||||
placeholder="100"
|
placeholder="100"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
required
|
required
|
||||||
@@ -317,7 +374,14 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
|
|||||||
min="1"
|
min="1"
|
||||||
step="1"
|
step="1"
|
||||||
value={price.quantity}
|
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}
|
disabled={loading}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -332,7 +396,15 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
|
|||||||
min="0"
|
min="0"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
value={price.price}
|
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}
|
disabled={loading}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -343,7 +415,9 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="remove-price-btn"
|
className="remove-price-btn"
|
||||||
onClick={() => removePriceRow(index)}
|
onClick={() =>
|
||||||
|
removePriceRow(index)
|
||||||
|
}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
title="Supprimer ce prix"
|
title="Supprimer ce prix"
|
||||||
>
|
>
|
||||||
@@ -361,10 +435,13 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
|
|||||||
<h3>Médias Actuels</h3>
|
<h3>Médias Actuels</h3>
|
||||||
<div className="existing-media-grid">
|
<div className="existing-media-grid">
|
||||||
{existingMedia.map((media) => (
|
{existingMedia.map((media) => (
|
||||||
<div key={media.id} className="existing-media-item">
|
<div
|
||||||
{media.type === 'image' ? (
|
key={media.id}
|
||||||
|
className="existing-media-item"
|
||||||
|
>
|
||||||
|
{media.type === "image" ? (
|
||||||
<img
|
<img
|
||||||
src={`http://localhost:8080${media.url}`}
|
src={`${media.url}`}
|
||||||
alt="Media"
|
alt="Media"
|
||||||
className="media-thumbnail"
|
className="media-thumbnail"
|
||||||
/>
|
/>
|
||||||
@@ -377,7 +454,9 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="delete-media-btn"
|
className="delete-media-btn"
|
||||||
onClick={() => markMediaForDeletion(media.id!)}
|
onClick={() =>
|
||||||
|
markMediaForDeletion(media.id!)
|
||||||
|
}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
title="Supprimer ce média"
|
title="Supprimer ce média"
|
||||||
>
|
>
|
||||||
@@ -394,10 +473,16 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
|
|||||||
<h3>Ajouter de Nouveaux Médias</h3>
|
<h3>Ajouter de Nouveaux Médias</h3>
|
||||||
|
|
||||||
<div className="upload-zone">
|
<div className="upload-zone">
|
||||||
<label htmlFor="media-upload" className="upload-label">
|
<label
|
||||||
|
htmlFor="media-upload"
|
||||||
|
className="upload-label"
|
||||||
|
>
|
||||||
<Upload size={32} />
|
<Upload size={32} />
|
||||||
<p>Cliquez pour ajouter des fichiers</p>
|
<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>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="media-upload"
|
id="media-upload"
|
||||||
@@ -406,7 +491,7 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
|
|||||||
multiple
|
multiple
|
||||||
onChange={handleFileSelect}
|
onChange={handleFileSelect}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
style={{ display: 'none' }}
|
style={{ display: "none" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -415,9 +500,12 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
|
|||||||
{newMediaFiles.map((file, index) => (
|
{newMediaFiles.map((file, index) => (
|
||||||
<div key={index} className="file-item">
|
<div key={index} className="file-item">
|
||||||
<div className="file-info">
|
<div className="file-info">
|
||||||
<span className="file-name">{file.name}</span>
|
<span className="file-name">
|
||||||
|
{file.name}
|
||||||
|
</span>
|
||||||
<span className="file-size">
|
<span className="file-size">
|
||||||
{(file.size / 1024).toFixed(1)} KB
|
{(file.size / 1024).toFixed(1)}{" "}
|
||||||
|
KB
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -451,7 +539,7 @@ const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, o
|
|||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? 'Mise à jour...' : 'Mettre à Jour'}
|
{loading ? "Mise à jour..." : "Mettre à Jour"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -514,7 +514,7 @@ const AdminProduct: React.FC = () => {
|
|||||||
product.media.length > 0 ? (
|
product.media.length > 0 ? (
|
||||||
<div className="product-image">
|
<div className="product-image">
|
||||||
<img
|
<img
|
||||||
src={`http://localhost:8080${product.media[0].url}`}
|
src={`${product.media[0].url}`}
|
||||||
alt={
|
alt={
|
||||||
product.name
|
product.name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,9 +85,7 @@ function UserAccueil() {
|
|||||||
(mediaItem) => mediaItem && mediaItem.type === "video",
|
(mediaItem) => mediaItem && mediaItem.type === "video",
|
||||||
);
|
);
|
||||||
|
|
||||||
return videoMedia?.url
|
return videoMedia?.url ? `${videoMedia.url}` : undefined;
|
||||||
? `http://localhost:8080${videoMedia.url}`
|
|
||||||
: undefined;
|
|
||||||
};
|
};
|
||||||
const getProductImage = (product: Product): string => {
|
const getProductImage = (product: Product): string => {
|
||||||
// Pas de media ? Image placeholder
|
// Pas de media ? Image placeholder
|
||||||
@@ -101,7 +99,7 @@ function UserAccueil() {
|
|||||||
|
|
||||||
// Vérifier si c'est un objet avec type "image" et url
|
// Vérifier si c'est un objet avec type "image" et url
|
||||||
if (mediaItem && mediaItem.type === "image" && mediaItem.url) {
|
if (mediaItem && mediaItem.type === "image" && mediaItem.url) {
|
||||||
return `http://localhost:8080${mediaItem.url}`;
|
return `${mediaItem.url}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ function Cart() {
|
|||||||
for (let i = 0; i < product.media.length; i++) {
|
for (let i = 0; i < product.media.length; i++) {
|
||||||
const mediaItem = product.media[i];
|
const mediaItem = product.media[i];
|
||||||
if (mediaItem && mediaItem.type === "image" && mediaItem.url) {
|
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",
|
(mediaItem) => mediaItem && mediaItem.type === "video",
|
||||||
);
|
);
|
||||||
|
|
||||||
return videoMedia?.url
|
return videoMedia?.url ? `${videoMedia.url}` : undefined;
|
||||||
? `http://localhost:8080${videoMedia.url}`
|
|
||||||
: undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ function OrderDetails() {
|
|||||||
for (let i = 0; i < product.media.length; i++) {
|
for (let i = 0; i < product.media.length; i++) {
|
||||||
const mediaItem = product.media[i];
|
const mediaItem = product.media[i];
|
||||||
if (mediaItem && mediaItem.type === "image" && mediaItem.url) {
|
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",
|
(mediaItem) => mediaItem && mediaItem.type === "video",
|
||||||
);
|
);
|
||||||
|
|
||||||
return videoMedia?.url
|
return videoMedia?.url ? `${videoMedia.url}` : undefined;
|
||||||
? `http://localhost:8080${videoMedia.url}`
|
|
||||||
: undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// HANDLERS VIDÉO
|
// HANDLERS VIDÉO
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# Version avec validation GPS + Simulation déplacement livreur
|
# 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=""
|
ADMIN_TOKEN=""
|
||||||
CABINE_TOKEN=""
|
CABINE_TOKEN=""
|
||||||
LIVREUR_TOKEN=""
|
LIVREUR_TOKEN=""
|
||||||
|
|||||||
Reference in New Issue
Block a user