From af74b39b22a7779c2f634387186bfc74c9704f9f Mon Sep 17 00:00:00 2001 From: Xor290 Date: Tue, 17 Mar 2026 09:18:23 +0100 Subject: [PATCH] chore: update --- backend/gestion/db/db_clients.go | 100 +++++++++--------- backend/gestion/db/db_init.go | 34 +++++- backend/gestion/handlers/commands.go | 27 ++++- backend/gestion/handlers/history.go | 21 ++-- backend/gestion/handlers/product.go | 20 ++-- docker/backend/nginx.conf | 6 +- .../src/screens/admin/ProductsScreen.tsx | 11 +- .../src/screens/admin/UsersScreen.tsx | 12 +-- .../src/screens/cabine/UsersScreen.tsx | 12 +-- frontend-admin/src/utils/pushTokenUtils.ts | 9 +- 10 files changed, 149 insertions(+), 103 deletions(-) diff --git a/backend/gestion/db/db_clients.go b/backend/gestion/db/db_clients.go index 20510fc4..892eacdc 100644 --- a/backend/gestion/db/db_clients.go +++ b/backend/gestion/db/db_clients.go @@ -399,7 +399,8 @@ func (d *Database) GetClientByTelephone(telephone string) (*models.Client, error // GetClientByUsername récupère un client par son username func (d *Database) GetClientByUsername(username string) (*models.Client, error) { client := &models.Client{} - query := `SELECT id, username, password, nom, prenom, telephone, command, point, point_zipette, amende, must_change_password, created_at + var pointsExtraJSON []byte + query := `SELECT id, username, password, nom, prenom, telephone, command, point, point_zipette, amende, must_change_password, COALESCE(points_extra, '{}'::jsonb), created_at FROM clients WHERE username = $1` err := d.QueryRow(query, username).Scan( @@ -414,6 +415,7 @@ func (d *Database) GetClientByUsername(username string) (*models.Client, error) &client.PointZipette, &client.Amende, &client.MustChangePassword, + &pointsExtraJSON, &client.CreatedAt, ) @@ -424,6 +426,11 @@ func (d *Database) GetClientByUsername(username string) (*models.Client, error) return nil, fmt.Errorf("erreur lors de la récupération du client: %w", err) } + client.PointsExtra = map[string]int{} + if len(pointsExtraJSON) > 0 { + json.Unmarshal(pointsExtraJSON, &client.PointsExtra) + } + return client, nil } @@ -949,27 +956,38 @@ func (d *Database) CalculateAndAddPointsForCommandTx(tx *sql.Tx, commandID int, log.Printf("📊 [CalcPointsTx] Pool[%d] (%s): %.2f€", i, pools[i].Name, t) } - // ✅ ÉTAPE 2: Calculer les points par pool et mettre à jour les colonnes DB - // Pool[0] → colonne `point`, Pool[1] → colonne `point_zipette` + // ✅ ÉTAPE 2: Calculer les points pour tous les pools var totalPoints int var pointCategory string - var result sql.Result - pts0 := CalcPointsFromTiers(poolTotals[0], pools[0].Tiers) - pts1 := 0 - if len(pools) >= 2 { - pts1 = CalcPointsFromTiers(poolTotals[1], pools[1].Tiers) + poolPts := make([]int, len(pools)) + var categoryParts []string + for i, pool := range pools { + poolPts[i] = CalcPointsFromTiers(poolTotals[i], pool.Tiers) + totalPoints += poolPts[i] + if poolPts[i] > 0 { + categoryParts = append(categoryParts, pool.Name) + } } - totalPoints = pts0 + pts1 - // Pools supplémentaires (index 2+) → points_extra JSONB - for i := 2; i < len(pools); i++ { - ptsExtra := CalcPointsFromTiers(poolTotals[i], pools[i].Tiers) - if ptsExtra == 0 { + log.Printf("💰 [CalcPointsTx] points par pool: %v, total=%d", poolPts, totalPoints) + + if totalPoints == 0 { + return 0, "", nil + } + + if len(categoryParts) > 0 { + pointCategory = strings.Join(categoryParts, " & ") + } else { + pointCategory = "points" + } + + // Écrire tous les pools dans points_extra[pool.Key] (stockage dynamique) + // + maintenir les colonnes legacy point/point_zipette pour la compatibilité admin + for i, pool := range pools { + if poolPts[i] == 0 { continue } - totalPoints += ptsExtra - poolKey := pools[i].Key _, err = tx.Exec(` UPDATE clients SET points_extra = jsonb_set( @@ -978,50 +996,30 @@ func (d *Database) CalculateAndAddPointsForCommandTx(tx *sql.Tx, commandID int, to_jsonb(COALESCE((points_extra->>$2)::int, 0) + $3) ), updated_at = CURRENT_TIMESTAMP WHERE username = $1 - `, username, poolKey, ptsExtra) + `, username, pool.Key, poolPts[i]) if err != nil { - log.Printf("❌ [CalcPointsTx] Erreur UPDATE points_extra pool[%d]: %v", i, err) + log.Printf("❌ [CalcPointsTx] Erreur UPDATE points_extra pool[%d] (%s): %v", i, pool.Key, err) return 0, "", fmt.Errorf("erreur mise à jour points pool[%d]: %w", i, err) } - log.Printf("💰 [CalcPointsTx] pool[%d] (%s): +%d pts → points_extra", i, pools[i].Name, ptsExtra) + log.Printf("💰 [CalcPointsTx] pool[%d] (%s / key=%s): +%d pts", i, pool.Name, pool.Key, poolPts[i]) } - log.Printf("💰 [CalcPointsTx] pool[0]=%d pts, pool[1]=%d pts, total=%d pts", pts0, pts1, totalPoints) - - if totalPoints == 0 { - return 0, "", nil + // Maintenir colonnes legacy pour affichage admin + pts0 := poolPts[0] + pts1 := 0 + if len(pools) >= 2 { + pts1 = poolPts[1] } - - var categoryParts []string - if pts0 > 0 { - categoryParts = append(categoryParts, pools[0].Name) - } - if pts1 > 0 { - categoryParts = append(categoryParts, pools[1].Name) - } - if len(categoryParts) > 0 { - pointCategory = strings.Join(categoryParts, " & ") + var legacyErr error + var result sql.Result + if pts1 == 0 { + result, legacyErr = tx.Exec(`UPDATE clients SET point = point + $1, updated_at = CURRENT_TIMESTAMP WHERE username = $2`, pts0, username) } else { - pointCategory = "points" + result, legacyErr = tx.Exec(`UPDATE clients SET point = point + $1, point_zipette = point_zipette + $2, updated_at = CURRENT_TIMESTAMP WHERE username = $3`, pts0, pts1, username) } - - if len(pools) == 1 || pts1 == 0 { - result, err = tx.Exec(` - UPDATE clients - SET point = point + $1, updated_at = CURRENT_TIMESTAMP - WHERE username = $2 - `, pts0, username) - } else { - result, err = tx.Exec(` - UPDATE clients - SET point = point + $1, point_zipette = point_zipette + $2, updated_at = CURRENT_TIMESTAMP - WHERE username = $3 - `, pts0, pts1, username) - } - - if err != nil { - log.Printf("❌ [CalcPointsTx] Erreur UPDATE points: %v", err) - return 0, "", fmt.Errorf("erreur mise à jour points: %w", err) + if legacyErr != nil { + log.Printf("❌ [CalcPointsTx] Erreur UPDATE colonnes legacy: %v", legacyErr) + return 0, "", fmt.Errorf("erreur mise à jour points: %w", legacyErr) } affected, _ := result.RowsAffected() if affected == 0 { diff --git a/backend/gestion/db/db_init.go b/backend/gestion/db/db_init.go index b7b1f21a..093788e6 100644 --- a/backend/gestion/db/db_init.go +++ b/backend/gestion/db/db_init.go @@ -2,6 +2,7 @@ package db import ( "database/sql" + "encoding/json" "fmt" "log" "os" @@ -161,11 +162,42 @@ func InitDB() *Database { log.Fatalf("❌ Erreur migration commandes.referral_used: %v", err) } - // Migration: points extra pour les pools de points supplémentaires (pool[2+]) + // Migration: points extra pour tous les pools de points (stockage dynamique par clé) if _, err = database.Exec(`ALTER TABLE clients ADD COLUMN IF NOT EXISTS points_extra JSONB NOT NULL DEFAULT '{}'::jsonb`); err != nil { log.Fatalf("❌ Erreur migration clients.points_extra: %v", err) } + // Migration: copier point/point_zipette → points_extra[pool.Key] selon les clés admin + { + var poolsJSON string + _ = database.QueryRow(`SELECT value FROM app_settings WHERE key = 'points_pools'`).Scan(&poolsJSON) + if poolsJSON != "" { + var pools []struct { + Key string `json:"key"` + } + if jsonErr := json.Unmarshal([]byte(poolsJSON), &pools); jsonErr == nil { + if len(pools) > 0 && pools[0].Key != "" { + k0 := pools[0].Key + if _, mErr := database.Exec(`UPDATE clients SET points_extra = points_extra || jsonb_build_object($1::text, point) WHERE point > 0 AND NOT (points_extra ? $1)`, k0); mErr != nil { + log.Printf("⚠️ Migration points_extra pool[0] (%s): %v", k0, mErr) + } else { + log.Printf("✅ Migration points_extra pool[0] key=%s", k0) + } + } + if len(pools) > 1 && pools[1].Key != "" { + k1 := pools[1].Key + if _, mErr := database.Exec(`UPDATE clients SET points_extra = points_extra || jsonb_build_object($1::text, point_zipette) WHERE point_zipette > 0 AND NOT (points_extra ? $1)`, k1); mErr != nil { + log.Printf("⚠️ Migration points_extra pool[1] (%s): %v", k1, mErr) + } else { + log.Printf("✅ Migration points_extra pool[1] key=%s", k1) + } + } + } + } else { + log.Printf("ℹ️ Migration points_extra: aucun pool configuré (app_settings vide)") + } + } + // Lancer le nettoyage périodique des tokens expirés go database.cleanExpiredTokensPeriodically() diff --git a/backend/gestion/handlers/commands.go b/backend/gestion/handlers/commands.go index a8797dbc..6299941b 100644 --- a/backend/gestion/handlers/commands.go +++ b/backend/gestion/handlers/commands.go @@ -735,7 +735,25 @@ func GetClientCommandsHistory(c *gin.Context) { "count": len(commands), } - if err == nil && client != nil { // ✅ VÉRIFIE AUSSI que client != nil + if err == nil && client != nil { + // Récupérer les pools configurés par l'admin + poolNames := []string{} + poolKeys := []string{} + if settings, sErr := database.GetSettings(); sErr == nil && len(settings.PointsPools) > 0 { + for _, p := range settings.PointsPools { + poolNames = append(poolNames, p.Name) + poolKeys = append(poolKeys, p.Key) + } + } + + // Construire pool_points depuis points_extra[pool.Key] (stockage dynamique) + poolPoints := make([]int, len(poolKeys)) + for i, key := range poolKeys { + if key != "" { + poolPoints[i] = client.PointsExtra[key] + } + } + resp["client_stats"] = gin.H{ "username": client.Username, "nom": client.Nom, @@ -743,12 +761,13 @@ func GetClientCommandsHistory(c *gin.Context) { "telephone": client.Telephone, "total_commands": client.Command, "points": client.Point, - "points_zipette": client.PointZipette, + "pool_points": poolPoints, + "pool_names": poolNames, "penalties": client.Amende, } - log.Printf("✅ [HISTORY] Stats client: point=%d, point_zipette=%d", - client.Point, client.PointZipette) + log.Printf("📊 [HISTORY] pool_names=%v pool_keys=%v pool_points=%v extra=%v", + poolNames, poolKeys, poolPoints, client.PointsExtra) } else { log.Printf("⚠️ [HISTORY] client_stats NON ajouté - err=%v, client=%v", err, client) } diff --git a/backend/gestion/handlers/history.go b/backend/gestion/handlers/history.go index 3e1a425d..fe791077 100644 --- a/backend/gestion/handlers/history.go +++ b/backend/gestion/handlers/history.go @@ -50,12 +50,15 @@ func GetMyCompletedOrders(c *gin.Context) { // ✅ Récupérer les infos client pour statistiques client, err := database.GetClientByUsername(usernameStr) - // ✅ Récupérer les noms des pools de points + // ✅ Récupérer les noms et clés des pools de points poolNames := []string{"Pool 1", "Pool 2"} + var poolKeys []string if settings, sErr := database.GetSettings(); sErr == nil && len(settings.PointsPools) > 0 { poolNames = make([]string, len(settings.PointsPools)) + poolKeys = make([]string, len(settings.PointsPools)) for i, p := range settings.PointsPools { poolNames[i] = p.Name + poolKeys[i] = p.Key } } @@ -66,15 +69,17 @@ func GetMyCompletedOrders(c *gin.Context) { } if err == nil && client != nil { - // Construire le tableau générique des valeurs par pool - poolPoints := make([]int, len(poolNames)) - if len(poolPoints) > 0 { - poolPoints[0] = client.Point - } - if len(poolPoints) > 1 { - poolPoints[1] = client.PointZipette + // Construire le tableau depuis points_extra[poolKey] pour tous les pools (stockage dynamique) + poolPoints := make([]int, len(poolKeys)) + for i, key := range poolKeys { + if key != "" { + poolPoints[i] = client.PointsExtra[key] + } } + log.Printf("📊 [HISTORY] pool_names=%v pool_keys=%v pool_points=%v extra=%v", + poolNames, poolKeys, poolPoints, client.PointsExtra) + response["client_stats"] = gin.H{ "username": client.Username, "total_commands": client.Command, diff --git a/backend/gestion/handlers/product.go b/backend/gestion/handlers/product.go index d8878242..92160ff7 100644 --- a/backend/gestion/handlers/product.go +++ b/backend/gestion/handlers/product.go @@ -814,29 +814,21 @@ func UploadMedia(c *gin.Context) { } // ✅ VÉRIFIER LE TYPE MIME RÉEL - fileHeader, err := file.Open() + detectedMime, err := validateFileMimeType(file) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lecture fichier"}) - return - } - defer fileHeader.Close() - - buffer := make([]byte, 512) - _, err = fileHeader.Read(buffer) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lecture fichier"}) + log.Printf("❌ [UploadMedia] Type MIME invalide: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "Type de fichier non autorisé"}) return } - mimeType := http.DetectContentType(buffer) - log.Printf("📋 [UploadMedia] Type MIME détecté: %s", mimeType) + log.Printf("📋 [UploadMedia] Type MIME détecté: %s", detectedMime) // Vérifier que le MIME correspond au type déclaré - if fileType == "image" && !strings.HasPrefix(mimeType, "image/") { + if fileType == "image" && !strings.HasPrefix(detectedMime, "image/") { c.JSON(http.StatusBadRequest, gin.H{"error": "Le fichier n'est pas une image valide"}) return } - if fileType == "video" && !strings.HasPrefix(mimeType, "video/") { + if fileType == "video" && !strings.HasPrefix(detectedMime, "video/") { c.JSON(http.StatusBadRequest, gin.H{"error": "Le fichier n'est pas une vidéo valide"}) return } diff --git a/docker/backend/nginx.conf b/docker/backend/nginx.conf index 500ddbb8..0ce4b572 100644 --- a/docker/backend/nginx.conf +++ b/docker/backend/nginx.conf @@ -37,7 +37,7 @@ server { ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; - client_max_body_size 10M; + client_max_body_size 100M; modsecurity on; modsecurity_rules_file /etc/nginx/modsec/custom-rules.conf; @@ -67,8 +67,8 @@ server { proxy_set_header Connection ""; proxy_connect_timeout 60s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; } add_header X-Frame-Options "SAMEORIGIN" always; diff --git a/frontend-admin/src/screens/admin/ProductsScreen.tsx b/frontend-admin/src/screens/admin/ProductsScreen.tsx index 56a26eef..3c419a19 100644 --- a/frontend-admin/src/screens/admin/ProductsScreen.tsx +++ b/frontend-admin/src/screens/admin/ProductsScreen.tsx @@ -352,6 +352,7 @@ export default function ProductsScreen() { // 3) Upload new media pour les produits existants (mode edit) if (editingProduct && pendingMedia.length > 0) { setUploadingMedia(true); + const uploadErrors: string[] = []; for (const m of pendingMedia) { try { await uploadProductMediaAdmin( @@ -361,11 +362,17 @@ export default function ProductsScreen() { m.mimeType, m.mediaType, ); - } catch { - /* ignore individual failures */ + } catch (uploadErr: any) { + const msg = uploadErr?.response?.data?.error || uploadErr?.message || `Erreur upload ${m.mediaType}`; + uploadErrors.push(msg); } } setUploadingMedia(false); + if (uploadErrors.length > 0) { + setFormError(`Erreur upload média: ${uploadErrors.join(", ")}`); + await loadData(); + return; + } } showSuccess( diff --git a/frontend-admin/src/screens/admin/UsersScreen.tsx b/frontend-admin/src/screens/admin/UsersScreen.tsx index 90011d5c..9a11d15e 100644 --- a/frontend-admin/src/screens/admin/UsersScreen.tsx +++ b/frontend-admin/src/screens/admin/UsersScreen.tsx @@ -792,15 +792,9 @@ export default function UsersScreen() { Cmd {pointsEnabled && poolNames.map((name, i) => { - let value: number; - let color: string; - if (i === 0) { value = item.clientData!.point; color = colors.success; } - else if (i === 1) { value = item.clientData!.points_zipette; color = colors.info; } - else { - const key = poolKeys[i] ?? ""; - value = item.clientData!.points_extra?.[key] ?? 0; - color = colors.warning; - } + const key = poolKeys[i] ?? ""; + const value = key ? (item.clientData!.points_extra?.[key] ?? 0) : 0; + const color = i === 0 ? colors.success : i === 1 ? colors.info : colors.warning; return ( {value} diff --git a/frontend-admin/src/screens/cabine/UsersScreen.tsx b/frontend-admin/src/screens/cabine/UsersScreen.tsx index 5ee06ff6..2335c8cf 100644 --- a/frontend-admin/src/screens/cabine/UsersScreen.tsx +++ b/frontend-admin/src/screens/cabine/UsersScreen.tsx @@ -137,15 +137,9 @@ export default function UsersScreen() { {appSettings.points_enabled && appSettings.pool_names.map((name, i) => { - let value: number; - let color: string; - if (i === 0) { value = item.point; color = colors.success; } - else if (i === 1) { value = item.points_zipette; color = colors.info; } - else { - const key = appSettings.pool_keys[i] ?? ""; - value = item.points_extra?.[key] ?? 0; - color = colors.warning; - } + const key = appSettings.pool_keys[i] ?? ""; + const value = key ? (item.points_extra?.[key] ?? 0) : 0; + const color = i === 0 ? colors.success : i === 1 ? colors.info : colors.warning; return ( {value} diff --git a/frontend-admin/src/utils/pushTokenUtils.ts b/frontend-admin/src/utils/pushTokenUtils.ts index f0b205b0..959fac1d 100644 --- a/frontend-admin/src/utils/pushTokenUtils.ts +++ b/frontend-admin/src/utils/pushTokenUtils.ts @@ -28,6 +28,11 @@ export async function getExpoPushToken(): Promise { }); } - const tokenData = await Notifications.getExpoPushTokenAsync(); - return tokenData.data; + try { + const tokenData = await Notifications.getExpoPushTokenAsync(); + return tokenData.data; + } catch { + // Google Play Services absent (ex: GrapheneOS) — push non disponible + return null; + } }