Merge pull request #2 from UBARSTUPAR/feat/V3-uber

Feat/v3 uber
This commit is contained in:
2026-05-09 15:18:32 +02:00
committed by GitHub
35 changed files with 742 additions and 2174 deletions
+18 -20
View File
@@ -2,13 +2,13 @@ name: Backend - Build & Lint
on:
push:
branches: [main, pre-prod]
branches: [main]
paths:
- "backend/**"
- "backend/**/**"
pull_request:
branches: [main, pre-prod]
branches: [main]
paths:
- "backend/**"
- "backend/**/**"
jobs:
lint:
@@ -54,26 +54,31 @@ jobs:
working-directory: backend/gestion
run: go build -v ./...
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: backend-binary
path: backend/gestion/gestion
retention-days: 7
docker:
name: Docker Build & Push
needs: build
runs-on: ubuntu-latest
if: >
(github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/pre-prod')) ||
(github.event_name == 'pull_request' && (github.base_ref == 'main' || github.base_ref == 'pre-prod'))
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build & push backend (runtime)
uses: docker/build-push-action@v6
with:
@@ -81,9 +86,7 @@ jobs:
file: docker/backend/Dockerfile
target: runtime
push: true
tags: xor1234/backend-mln:${{ (github.ref == 'refs/heads/main' || github.base_ref == 'main') && 'latest' || 'pre-prod' }}
cache-from: type=gha,scope=backend-runtime
cache-to: type=gha,mode=max,scope=backend-runtime
tags: xor1234/backend-mln:latest
- name: Build & push WAF
uses: docker/build-push-action@v6
@@ -92,17 +95,12 @@ jobs:
file: docker/backend/Dockerfile
target: waf
push: true
tags: xor1234/backend-mln:${{ (github.ref == 'refs/heads/main' || github.base_ref == 'main') && 'waf' || 'waf-pre-prod' }}
cache-from: type=gha,scope=backend-waf
cache-to: type=gha,mode=max,scope=backend-waf
tags: xor1234/backend-mln:waf
deploy:
name: Deploy to server
name: SSH Deploy
needs: docker
runs-on: ubuntu-latest
if: >
(github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/pre-prod')) ||
(github.event_name == 'pull_request' && (github.base_ref == 'main' || github.base_ref == 'pre-prod'))
steps:
- name: SSH deploy
+4 -5
View File
@@ -32,10 +32,9 @@ jobs:
working-directory: frontend-admin
run: npx tsc --noEmit
build-apk-pre-prod:
build-apk:
needs: typecheck
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/pre-prod'
steps:
- uses: actions/checkout@v4
@@ -63,13 +62,13 @@ jobs:
jq '.expo.extra.eas.projectId = "${{ secrets.EXPO_PROJECT_ID }}"' app.json > app.tmp.json
mv app.tmp.json app.json
- name: Build production APK
- name: Build APK
working-directory: frontend-admin
env:
EAS_BUILD_NO_EXPO_GO_WARNING: true
run: eas build --platform android --profile pre-production --non-interactive
run: eas build --platform android ${{ (github.ref == 'refs/heads/main' || github.base_ref == 'main') && '--profile production' || '--profile preview' }} --non-interactive
- name: Download production APK
- name: Download APK
working-directory: frontend-admin
run: |
APK_URL=$(eas build:list --platform android --status finished --limit 1 --json --non-interactive | jq -r '.[0].artifacts.buildUrl')
+3 -4
View File
@@ -32,10 +32,9 @@ jobs:
working-directory: mobile
run: npx tsc --noEmit
build-apk-prod:
build-apk:
needs: typecheck
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/pre-prod'
steps:
- uses: actions/checkout@v4
@@ -67,11 +66,11 @@ jobs:
working-directory: mobile
run: cat app.json
- name: Build production APK
- name: Build APK
working-directory: mobile
env:
EAS_BUILD_NO_EXPO_GO_WARNING: true
run: eas build --platform android --profile production --non-interactive
run: eas build --platform android ${{ (github.ref == 'refs/heads/main' || github.base_ref == 'main') && '--profile production' || '--profile preview' }} --non-interactive
- name: Download production APK
working-directory: mobile
-3
View File
@@ -98,9 +98,6 @@ jobs:
name: Deploy to server
needs: docker
runs-on: ubuntu-latest
if: >
(github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/pre-prod')) ||
(github.event_name == 'pull_request' && (github.base_ref == 'main' || github.base_ref == 'pre-prod'))
steps:
- name: SSH deploy
+8 -1
View File
@@ -374,10 +374,12 @@ func (d *Database) GetClientByUsername(username string) (*models.Client, error)
MustChangePassword bool `gorm:"column:must_change_password"`
PointsExtraJSON []byte `gorm:"column:points_extra"`
CreatedAt time.Time `gorm:"column:created_at"`
TwoFAEnabled bool `gorm:"column:two_fa_enabled"`
}
err := d.GDB.Raw(`
SELECT id, username, password, nom, prenom, telephone, command, amende,
must_change_password, COALESCE(points_extra, '{}'::jsonb) as points_extra, created_at
must_change_password, COALESCE(points_extra, '{}'::jsonb) as points_extra, created_at,
two_fa_enabled
FROM clients WHERE username = ?`, username).Scan(&row).Error
if err != nil {
return nil, fmt.Errorf("erreur lors de la récupération du client: %w", err)
@@ -397,6 +399,7 @@ func (d *Database) GetClientByUsername(username string) (*models.Client, error)
Amende: row.Amende,
MustChangePassword: row.MustChangePassword,
CreatedAt: row.CreatedAt,
TwoFAEnabled: row.TwoFAEnabled,
}
client.PointsExtra = map[string]int{}
if len(row.PointsExtraJSON) > 0 {
@@ -406,6 +409,10 @@ func (d *Database) GetClientByUsername(username string) (*models.Client, error)
return client, nil
}
func (d *Database) SetClientTwoFAEnabled(clientID int, enabled bool) error {
return d.GDB.Model(&models.Client{}).Where("id = ?", clientID).Update("two_fa_enabled", enabled).Error
}
func (d *Database) GetClientPenaltiesInfo(username string) (map[string]interface{}, error) {
amende, err := d.GetClientAmende(username)
if err != nil {
+8
View File
@@ -66,6 +66,7 @@ func DefaultSettings() models.AppSettings {
},
},
},
ShopName: "Milieu-Nantais",
DeliveryMode: models.DeliveryModeConfig{
Mode: "single",
CategoryRoutes: []models.CategoryRoute{},
@@ -81,6 +82,7 @@ func DefaultSettings() models.AppSettings {
"44860", "44220", "44118", "44710", "44690", "44119",
}},
},
Telegram2FAEnabled: false,
}
}
@@ -149,6 +151,10 @@ func (d *Database) GetSettings() (models.AppSettings, error) {
if err := json.Unmarshal([]byte(row.Value), &mode); err == nil {
settings.DeliveryMode = mode
}
case "telegram_2fa_enabled":
settings.Telegram2FAEnabled = row.Value == "true"
case "shop_name":
settings.ShopName = row.Value
}
}
return settings, nil
@@ -226,7 +232,9 @@ func (d *Database) UpdateSettings(s models.AppSettings) error {
{"telegram_bot_token", s.TelegramBotToken},
{"telegram_bot_username", s.TelegramBotUsername},
{"telegram_notifications_enabled", boolStr(s.TelegramNotificationsEnabled)},
{"telegram_2fa_enabled", boolStr(s.Telegram2FAEnabled)},
{"delivery_mode", string(deliveryModeJSON)},
{"shop_name", s.ShopName},
}
upsert := `INSERT INTO app_settings (key, value) VALUES (?, ?)
+34
View File
@@ -15,6 +15,7 @@ func (d *Database) MigrateAddTelegramColumns() {
migrations := []string{
`ALTER TABLE clients ADD COLUMN IF NOT EXISTS telegram_chat_id BIGINT`,
`ALTER TABLE users ADD COLUMN IF NOT EXISTS telegram_chat_id BIGINT`,
`ALTER TABLE clients ADD COLUMN IF NOT EXISTS two_fa_enabled BOOLEAN NOT NULL DEFAULT FALSE`,
}
for _, q := range migrations {
if err := d.GDB.Exec(q).Error; err != nil {
@@ -127,3 +128,36 @@ func (d *Database) GetUserByTelegramChatID(chatID int64) (username, role string,
return "", "", fmt.Errorf("aucun compte lié à ce chat_id")
}
// ── 2FA sessions ─────────────────────────────────────────────────────────────
const twoFASessionTTL = 5 * time.Minute
type twoFASessionData struct {
Username string `json:"username"`
Code string `json:"code"`
}
func Store2FASession(sessionToken, username, code string) error {
data, err := json.Marshal(twoFASessionData{Username: username, Code: code})
if err != nil {
return err
}
return Redis.Set(RedisCtx, "2fa:session:"+sessionToken, data, twoFASessionTTL).Err()
}
// Verify2FASession valide le code et retourne le username. GETDEL = atomique (anti-replay).
func Verify2FASession(sessionToken, code string) (string, error) {
val, err := Redis.GetDel(RedisCtx, "2fa:session:"+sessionToken).Bytes()
if err != nil {
return "", fmt.Errorf("session invalide ou expirée")
}
var d twoFASessionData
if err := json.Unmarshal(val, &d); err != nil {
return "", fmt.Errorf("données corrompues")
}
if d.Code != code {
return "", fmt.Errorf("code incorrect")
}
return d.Username, nil
}
+155 -4
View File
@@ -1,8 +1,11 @@
package handlers
import (
"crypto/rand"
"fmt"
"gestion/db"
"gestion/models"
"gestion/services"
"gestion/utils"
"log"
"net/http"
@@ -76,7 +79,7 @@ func RegisterClient(c *gin.Context) {
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("❌ [REGISTER_CLIENT] Erreur binding: %v", err)
c.JSON(http.StatusBadRequest, gin.H{
"error": "Données invalides",
"error": "Données invalides",
})
return
}
@@ -242,8 +245,16 @@ func AdminCreateClient(c *gin.Context) {
})
}
func cryptoRandInt() int {
b := make([]byte, 4)
rand.Read(b)
return int(b[0])<<24 | int(b[1])<<16 | int(b[2])<<8 | int(b[3])
}
// LoginClient authentifie un client
func LoginClient(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
var req models.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("❌ [LOGIN_CLIENT] Erreur binding: %v", err)
@@ -251,8 +262,6 @@ func LoginClient(c *gin.Context) {
return
}
database := c.MustGet("database").(*db.Database)
client, err := database.GetClientByUsername(req.Username)
if err != nil || client == nil {
log.Printf("❌ [LOGIN_CLIENT] Client non trouvé: %s", req.Username)
@@ -266,6 +275,30 @@ func LoginClient(c *gin.Context) {
return
}
settings, err := database.GetSettings()
if err != nil {
log.Printf("❌ [LOGIN_CLIENT] Erreur récupération des paramètres: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur interne"})
return
}
if settings.Telegram2FAEnabled && client.TwoFAEnabled {
chatID, linked, _ := database.GetClientTelegramChatID(client.Username)
if linked {
code := fmt.Sprintf("%06d", cryptoRandInt()%1000000)
sessionToken := uuid.New().String()
if err := db.Store2FASession(sessionToken, client.Username, code); err == nil {
msg := fmt.Sprintf("🔐 Code de vérification : <b>%s</b>\n\nValable 5 minutes.", code)
services.TelegramBot.SendMessage(chatID, msg)
c.JSON(http.StatusOK, gin.H{
"requires_2fa": true,
"session_token": sessionToken,
})
return
}
}
}
token, err := generateClientToken(client)
if err != nil {
log.Printf("❌ [LOGIN_CLIENT] Erreur génération token: %v", err)
@@ -280,7 +313,6 @@ func LoginClient(c *gin.Context) {
return
}
// Créer la session Redis
sessionID := uuid.New().String()
if err := database.CreateClientSession(client.ID, client.Username, sessionID); err != nil {
log.Printf("⚠️ [LOGIN_CLIENT] Erreur session Redis: %v", err)
@@ -303,6 +335,125 @@ func LoginClient(c *gin.Context) {
})
}
func Verify2FAClient(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
var req struct {
SessionToken string `json:"session_token" binding:"required"`
Code string `json:"code" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
return
}
username, err := db.Verify2FASession(req.SessionToken, req.Code)
if err != nil {
log.Printf("❌ [2FA] Échec vérification: %v", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
client, err := database.GetClientByUsername(username)
if err != nil || client == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur interne"})
return
}
token, err := generateClientToken(client)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur génération token"})
return
}
expiresAt := time.Now().Add(clientTokenDuration)
if err := database.SaveToken(client.ID, "client", token, expiresAt); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement token"})
return
}
sessionID := uuid.New().String()
if err := database.CreateClientSession(client.ID, client.Username, sessionID); err != nil {
log.Printf("⚠️ [2FA] Erreur session Redis: %v", err)
}
c.JSON(http.StatusOK, models.LoginResponse{
AccessToken: token,
TokenType: "Bearer",
ExpiresIn: int(clientTokenDuration.Seconds()),
User: gin.H{
"id": client.ID,
"username": client.Username,
"nom": client.Nom,
"prenom": client.Prenom,
"telephone": client.Telephone,
"role": "client",
"session_id": sessionID,
"must_change_password": client.MustChangePassword,
},
})
}
func GetClient2FAStatus(c *gin.Context) {
clientID := c.GetInt("client_id")
database := c.MustGet("database").(*db.Database)
client, err := database.GetClientByID(clientID)
if err != nil || client == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Client non trouvé"})
return
}
_, tgLinked, _ := database.GetClientTelegramChatID(client.Username)
settings, _ := database.GetSettings()
c.JSON(http.StatusOK, gin.H{
"two_fa_enabled": client.TwoFAEnabled,
"telegram_linked": tgLinked,
"admin_2fa_enabled": settings.Telegram2FAEnabled,
})
}
func ToggleClient2FA(c *gin.Context) {
clientID := c.GetInt("client_id")
database := c.MustGet("database").(*db.Database)
var req struct {
Enabled bool `json:"enabled"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
return
}
client, err := database.GetClientByID(clientID)
if err != nil || client == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Client non trouvé"})
return
}
if req.Enabled {
_, linked, _ := database.GetClientTelegramChatID(client.Username)
if !linked {
c.JSON(http.StatusBadRequest, gin.H{"error": "Telegram non lié — impossible d'activer la 2FA"})
return
}
settings, _ := database.GetSettings()
if !settings.Telegram2FAEnabled {
c.JSON(http.StatusBadRequest, gin.H{"error": "La 2FA n'est pas activée par l'administrateur"})
return
}
}
if err := database.SetClientTwoFAEnabled(clientID, req.Enabled); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur mise à jour"})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "two_fa_enabled": req.Enabled})
}
// ChangePassword permet à un client de changer son mot de passe
func ChangePassword(c *gin.Context) {
var req struct {
+16 -14
View File
@@ -30,20 +30,22 @@ func GetPublicSettings(c *gin.Context) {
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"penalties_enabled": settings.PenaltiesEnabled,
"show_amende_score": settings.ShowAmendeScore,
"points_enabled": settings.PointsEnabled,
"points_separated": len(settings.PointsPools) > 1,
"pool_names": poolNames,
"pool_keys": poolKeys,
"referral_enabled": settings.ReferralEnabled,
"referral_amount": settings.ReferralAmount,
"delivery_schedule": settings.DeliverySchedule,
"crypto_payment_enabled": settings.CryptoPaymentEnabled,
"crypto_only": settings.CryptoOnly,
"nowpayments_currencies": settings.NowPaymentsCurrencies,
"telegram_notifications_enabled": settings.TelegramNotificationsEnabled,
"success": true,
"penalties_enabled": settings.PenaltiesEnabled,
"show_amende_score": settings.ShowAmendeScore,
"points_enabled": settings.PointsEnabled,
"points_separated": len(settings.PointsPools) > 1,
"pool_names": poolNames,
"pool_keys": poolKeys,
"referral_enabled": settings.ReferralEnabled,
"referral_amount": settings.ReferralAmount,
"delivery_schedule": settings.DeliverySchedule,
"crypto_payment_enabled": settings.CryptoPaymentEnabled,
"crypto_only": settings.CryptoOnly,
"nowpayments_currencies": settings.NowPaymentsCurrencies,
"telegram_notifications_enabled": settings.TelegramNotificationsEnabled,
"shop_name": settings.ShopName,
"two_fa_enabled": settings.Telegram2FAEnabled,
})
}
+7
View File
@@ -242,13 +242,20 @@ func UnlinkClientTelegram(c *gin.Context) {
return
}
clientID := c.GetInt("client_id")
database := c.MustGet("database").(*db.Database)
if err := database.DeleteClientTelegramChatID(username); err != nil {
log.Printf("❌ [TELEGRAM_UNLINK] Erreur pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur déliaison"})
return
}
// Désactiver la 2FA si Telegram est délié
if clientID > 0 {
_ = database.SetClientTwoFAEnabled(clientID, false)
}
log.Printf("✅ [TELEGRAM_UNLINK] Compte client %s délié", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
+1
View File
@@ -21,6 +21,7 @@ type Client struct {
Parrain string `gorm:"column:parrain" json:"parrain"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
TwoFAEnabled bool `gorm:"column:two_fa_enabled;default:false" json:"two_fa_enabled"`
}
func (Client) TableName() string { return "clients" }
+20 -18
View File
@@ -61,22 +61,24 @@ type DeliveryModeConfig struct {
// AppSettings contient les paramètres globaux de l'application
type AppSettings struct {
PenaltiesEnabled bool `json:"penalties_enabled"`
ShowAmendeScore bool `json:"show_amende_score"` // afficher le score d'amendes aux clients/cabine
PenaltyTiers []PenaltyTier `json:"penalty_tiers"` // barème des amendes (liste configurable)
PointsEnabled bool `json:"points_enabled"` // afficher/activer le système de points
PointsPools []PointsPool `json:"points_pools"` // types de points personnalisés
ReferralEnabled bool `json:"referral_enabled"` // activer/désactiver le système de parrainage
ReferralAmount float64 `json:"referral_amount"` // montant crédité par parrainage
CryptoPaymentEnabled bool `json:"crypto_payment_enabled"` // activer/désactiver le paiement crypto
CryptoOnly bool `json:"crypto_only"` // forcer le paiement crypto uniquement (pas d'espèces)
NowPaymentsAPIKey string `json:"nowpayments_api_key"` // clé API NowPayments
NowPaymentsIPNSecret string `json:"nowpayments_ipn_secret"` // secret IPN NowPayments
NowPaymentsCurrencies []string `json:"nowpayments_currencies"` // cryptos acceptées (ex: ["btc","eth","ltc"])
DeliverySchedule DeliverySchedule `json:"delivery_schedule"` // horaires de livraison par jour
PostalZones []PostalZone `json:"postal_zones"` // zones de livraison avec minimum de commande
TelegramBotToken string `json:"telegram_bot_token"` // token du bot Telegram (BotFather)
TelegramBotUsername string `json:"telegram_bot_username"` // username du bot (sans @)
TelegramNotificationsEnabled bool `json:"telegram_notifications_enabled"` // activer/désactiver les notifications Telegram
DeliveryMode DeliveryModeConfig `json:"delivery_mode"` // mode d'assignation des livreurs
PenaltiesEnabled bool `json:"penalties_enabled"`
ShowAmendeScore bool `json:"show_amende_score"` // afficher le score d'amendes aux clients/cabine
PenaltyTiers []PenaltyTier `json:"penalty_tiers"` // barème des amendes (liste configurable)
PointsEnabled bool `json:"points_enabled"` // afficher/activer le système de points
PointsPools []PointsPool `json:"points_pools"` // types de points personnalisés
ReferralEnabled bool `json:"referral_enabled"` // activer/désactiver le système de parrainage
ReferralAmount float64 `json:"referral_amount"` // montant crédité par parrainage
CryptoPaymentEnabled bool `json:"crypto_payment_enabled"` // activer/désactiver le paiement crypto
CryptoOnly bool `json:"crypto_only"` // forcer le paiement crypto uniquement (pas d'espèces)
NowPaymentsAPIKey string `json:"nowpayments_api_key"` // clé API NowPayments
NowPaymentsIPNSecret string `json:"nowpayments_ipn_secret"` // secret IPN NowPayments
NowPaymentsCurrencies []string `json:"nowpayments_currencies"` // cryptos acceptées (ex: ["btc","eth","ltc"])
DeliverySchedule DeliverySchedule `json:"delivery_schedule"` // horaires de livraison par jour
PostalZones []PostalZone `json:"postal_zones"` // zones de livraison avec minimum de commande
TelegramBotToken string `json:"telegram_bot_token"` // token du bot Telegram (BotFather)
TelegramBotUsername string `json:"telegram_bot_username"` // username du bot (sans @)
TelegramNotificationsEnabled bool `json:"telegram_notifications_enabled"` // activer/désactiver les notifications Telegram
DeliveryMode DeliveryModeConfig `json:"delivery_mode"` // mode d'assignation des livreurs
ShopName string `json:"shop_name"` // nom affiché dans la sidebar du site client
Telegram2FAEnabled bool `json:"telegram_2fa_enabled"` // activer/désactiver l'authentification à deux facteurs
}
+5
View File
@@ -34,6 +34,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
{
authGroupV1.POST("/login", middleware.LoginRateLimitMiddleware, handlers.LoginClient)
authGroupV1.POST("/logout", handlers.LogoutClient)
authGroupV1.POST("/2fa/verify", middleware.LoginRateLimitMiddleware, handlers.Verify2FAClient)
}
// Route change-password (auth client requise)
@@ -107,6 +108,10 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
cartGroupV1.GET("/profile", handlers.GetMyProfile) // ✅ Récupérer mon profil
cartGroupV1.PUT("/profile/update", handlers.UpdateMyProfile) // ✅ Modifier mon profil
// 🔐 2FA CLIENT
cartGroupV1.GET("/two-fa/status", handlers.GetClient2FAStatus)
cartGroupV1.POST("/two-fa/toggle", handlers.ToggleClient2FA)
// 🎁 PARRAINAGE CLIENT
cartGroupV1.GET("/referral/balance", handlers.GetMyReferralBalance)
cartGroupV1.GET("/parrain", handlers.GetMyParrainInfo)
-4
View File
@@ -16,10 +16,6 @@ import (
"github.com/redis/go-redis/v9"
)
// ============================================
// CONSTANTES
// ============================================
const (
NominatimBaseURL = "https://nominatim.openstreetmap.org/search"
EarthRadiusKm = 6371.0
+23
View File
@@ -138,6 +138,27 @@ services:
- "7007:7007"
restart: unless-stopped
clamav:
deploy:
resources:
limits:
memory: 1g
cpus: 0.5
image: clamav/clamav:latest
container_name: gestion-clamav
restart: unless-stopped
volumes:
- backend_uploads:/app/uploads:ro
- clamav_data:/var/lib/clamav
networks:
- gestion-network
healthcheck:
test: ["CMD", "clamdcheck.sh"]
interval: 60s
timeout: 10s
retries: 3
start_period: 120s
networks:
gestion-network:
driver: bridge
@@ -152,3 +173,5 @@ volumes:
driver: local
backend_uploads:
driver: local
clamav_data:
driver: local
+3 -3
View File
@@ -15,7 +15,7 @@
"simulator": true
},
"env": {
"API_URL": "https://5.181.0.112.nip.io"
"API_URL": "https://mln-uber.club"
}
},
"preview": {
@@ -24,7 +24,7 @@
"buildType": "apk"
},
"env": {
"API_URL": "https://5.181.0.112.nip.io"
"API_URL": "https://mln-uber.club"
}
},
"production": {
@@ -34,7 +34,7 @@
"buildType": "apk"
},
"env": {
"API_URL": "https://5.181.0.112.nip.io"
"API_URL": "https://mln-uber.club"
}
}
}
+4 -421
View File
@@ -17,11 +17,9 @@
"axios": "^1.13.4",
"expo": "~54.0.34",
"expo-build-properties": "~1.0.10",
"expo-device": "~8.0.10",
"expo-font": "~14.0.11",
"expo-image-picker": "~17.0.11",
"expo-location": "~19.0.8",
"expo-notifications": "~0.32.17",
"expo-status-bar": "~3.0.9",
"jwt-decode": "^4.0.0",
"react": "19.1.0",
@@ -81,7 +79,6 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.6",
@@ -1369,7 +1366,6 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
"peer": true,
"engines": {
"node": ">=6.9.0"
}
@@ -2049,11 +2045,6 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/@ide/backoff": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz",
"integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g=="
},
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
@@ -2567,7 +2558,6 @@
"version": "7.1.28",
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.28.tgz",
"integrity": "sha512-d1QDn+KNHfHGt3UIwOZvupvdsDdiHYZBEj7+wL2yDVo3tMezamYy60H9s3EnNVE1Ae1ty0trc7F2OKqo/RmsdQ==",
"peer": true,
"dependencies": {
"@react-navigation/core": "^7.14.0",
"escape-string-regexp": "^4.0.0",
@@ -2767,7 +2757,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz",
"integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==",
"devOptional": true,
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -2982,18 +2971,6 @@
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
},
"node_modules/assert": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
"integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
"dependencies": {
"call-bind": "^1.0.2",
"is-nan": "^1.3.2",
"object-is": "^1.1.5",
"object.assign": "^4.1.4",
"util": "^0.12.5"
}
},
"node_modules/async-limiter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
@@ -3004,20 +2981,6 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"dependencies": {
"possible-typed-array-names": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/axios": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
@@ -3224,11 +3187,6 @@
"@babel/core": "^7.0.0"
}
},
"node_modules/badgin": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz",
"integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw=="
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -3367,7 +3325,6 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -3454,23 +3411,6 @@
"node": ">=8"
}
},
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -3483,21 +3423,6 @@
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
@@ -3936,22 +3861,6 @@
"node": ">=10"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-lazy-prop": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
@@ -3961,22 +3870,6 @@
"node": ">=8"
}
},
"node_modules/define-properties": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dependencies": {
"define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -4077,6 +3970,7 @@
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"optional": true,
"peer": true,
"dependencies": {
"iconv-lite": "^0.6.2"
}
@@ -4086,6 +3980,7 @@
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"optional": true,
"peer": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
@@ -4217,7 +4112,6 @@
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.34.tgz",
"integrity": "sha512-XkVHguZZDC8BcTQxHAd14/TQFbDp1Wt0Z/KApO9t68Ll5A127hLCPzU+a9gytfCIiyL/V1IpF1vIcOLKEVAoNQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.20.0",
"@expo/cli": "54.0.24",
@@ -4265,14 +4159,6 @@
}
}
},
"node_modules/expo-application": {
"version": "7.0.8",
"resolved": "https://registry.npmjs.org/expo-application/-/expo-application-7.0.8.tgz",
"integrity": "sha512-qFGyxk7VJbrNOQWBbE09XUuGuvkOgFS9QfToaK2FdagM2aQ+x3CvGV2DuVgl/l4ZxPgIf3b/MNh9xHpwSwn74Q==",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-asset": {
"version": "12.0.13",
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.13.tgz",
@@ -4324,42 +4210,6 @@
"react-native": "*"
}
},
"node_modules/expo-device": {
"version": "8.0.10",
"resolved": "https://registry.npmjs.org/expo-device/-/expo-device-8.0.10.tgz",
"integrity": "sha512-jd5BxjaF7382JkDMaC+P04aXXknB2UhWaVx5WiQKA05ugm/8GH5uaz9P9ckWdMKZGQVVEOC8MHaUADoT26KmFA==",
"dependencies": {
"ua-parser-js": "^0.7.33"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-device/node_modules/ua-parser-js": {
"version": "0.7.41",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz",
"integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/ua-parser-js"
},
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
},
{
"type": "github",
"url": "https://github.com/sponsors/faisalman"
}
],
"bin": {
"ua-parser-js": "script/cli.js"
},
"engines": {
"node": "*"
}
},
"node_modules/expo-file-system": {
"version": "19.0.22",
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.22.tgz",
@@ -4374,7 +4224,6 @@
"version": "14.0.11",
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz",
"integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==",
"peer": true,
"dependencies": {
"fontfaceobserver": "^2.1.0"
},
@@ -4450,26 +4299,6 @@
"react-native": "*"
}
},
"node_modules/expo-notifications": {
"version": "0.32.17",
"resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.32.17.tgz",
"integrity": "sha512-lwwzn7tImuzTzn9PAglZlS2VfZEvsfFGJTK9Eb8I4cqkGh2DI23YJFJH+WPEIu4QhDvk5JeBjklenJ8IZbmA4A==",
"license": "MIT",
"dependencies": {
"@expo/image-utils": "^0.8.8",
"@ide/backoff": "^1.0.0",
"abort-controller": "^3.0.0",
"assert": "^2.0.0",
"badgin": "^1.1.5",
"expo-application": "~7.0.8",
"expo-constants": "~18.0.13"
},
"peerDependencies": {
"expo": "*",
"react": "*",
"react-native": "*"
}
},
"node_modules/expo-server": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.6.tgz",
@@ -4817,20 +4646,6 @@
"resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz",
"integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg=="
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"dependencies": {
"is-callable": "^1.2.7"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
@@ -4889,14 +4704,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/generator-function": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
"integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -5055,17 +4862,6 @@
"node": ">=8"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -5295,37 +5091,11 @@
"loose-envify": "^1.0.0"
}
},
"node_modules/is-arguments": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
"dependencies": {
"call-bound": "^1.0.2",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-arrayish": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="
},
"node_modules/is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
@@ -5362,39 +5132,6 @@
"node": ">=8"
}
},
"node_modules/is-generator-function": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
"integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
"dependencies": {
"call-bound": "^1.0.4",
"generator-function": "^2.0.0",
"get-proto": "^1.0.1",
"has-tostringtag": "^1.0.2",
"safe-regex-test": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-nan": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
"integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
"dependencies": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -5411,37 +5148,6 @@
"node": ">=8"
}
},
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"dependencies": {
"call-bound": "^1.0.2",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-typed-array": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"dependencies": {
"which-typed-array": "^1.1.16"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
@@ -6760,48 +6466,6 @@
"node": ">=0.10.0"
}
},
"node_modules/object-is": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/object.assign": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
"integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.3",
"define-properties": "^1.2.1",
"es-object-atoms": "^1.0.0",
"has-symbols": "^1.1.0",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@@ -7138,14 +6802,6 @@
"node": ">=4.0.0"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/postcss": {
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
@@ -7354,7 +7010,6 @@
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -7372,7 +7027,6 @@
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@@ -7400,7 +7054,6 @@
"version": "0.81.5",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz",
"integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==",
"peer": true,
"dependencies": {
"@jest/create-cache-key-function": "^29.7.0",
"@react-native/assets-registry": "0.81.5",
@@ -7527,7 +7180,6 @@
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
"integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==",
"peer": true,
"peerDependencies": {
"react": "*",
"react-native": "*"
@@ -7537,7 +7189,6 @@
"version": "4.16.0",
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz",
"integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==",
"peer": true,
"dependencies": {
"react-freeze": "^1.0.0",
"react-native-is-edge-to-edge": "^1.2.1",
@@ -7552,7 +7203,6 @@
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
"integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.6",
"@react-native/normalize-colors": "^0.74.1",
@@ -7582,7 +7232,6 @@
"version": "13.15.0",
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.15.0.tgz",
"integrity": "sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==",
"peer": true,
"dependencies": {
"escape-string-regexp": "^4.0.0",
"invariant": "2.2.4"
@@ -7596,7 +7245,6 @@
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",
"integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==",
"peer": true,
"dependencies": {
"@babel/plugin-transform-arrow-functions": "^7.0.0-0",
"@babel/plugin-transform-class-properties": "^7.0.0-0",
@@ -7720,7 +7368,6 @@
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -7973,27 +7620,12 @@
],
"license": "MIT"
},
"node_modules/safe-regex-test": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"is-regex": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"optional": true
"optional": true,
"peer": true
},
"node_modules/sax": {
"version": "1.4.4",
@@ -8109,22 +7741,6 @@
"node": ">= 0.8"
}
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
@@ -8615,7 +8231,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -8825,18 +8440,6 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
"dependencies": {
"inherits": "^2.0.3",
"is-arguments": "^1.0.4",
"is-generator-function": "^1.0.7",
"is-typed-array": "^1.1.3",
"which-typed-array": "^1.1.2"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -8954,26 +8557,6 @@
"node": ">= 8"
}
},
"node_modules/which-typed-array": {
"version": "1.1.20",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
"integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
"dependencies": {
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"for-each": "^0.3.5",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/wonka": {
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.6.tgz",
+5 -8
View File
@@ -9,9 +9,9 @@ import type {
Alert,
} from "./types";
const V2 = "https://5.181.0.112.nip.io/api/v2";
const CABINE_URL = "https://5.181.0.112.nip.io/api/v1/cabine";
const V1_PUBLIC = "https://5.181.0.112.nip.io/api/v1";
const V2 = "https://mln-uber.club/api/v2";
const CABINE_URL = "https://mln-uber.club/api/v1/cabine";
const V1_PUBLIC = "https://mln-uber.club/api/v1";
export const loginAdmin = async (
username: string,
@@ -52,10 +52,6 @@ export const logoutAdmin = async (): Promise<void> => {
}
};
// ============================================
// CLIENTS
// ============================================
export const getAllClients = async (): Promise<ClientResponse[]> => {
const { data } = await apiClient.get(`${V2}/admin/protected/all/clients`);
return data.clients || [];
@@ -90,7 +86,6 @@ export const updateUserByAdmin = async (
// ============================================
// COMMANDES
// ============================================
export const getAllCommands = async (status?: string, username?: string) => {
let url = `${V2}/admin/protected/orders`;
@@ -963,7 +958,9 @@ export interface AppSettings {
telegram_bot_token: string;
telegram_bot_username: string;
telegram_notifications_enabled: boolean;
telegram_2fa_enabled: boolean;
delivery_mode: DeliveryModeConfig;
shop_name: string;
}
export const getSettings = async (): Promise<{
+2 -6
View File
@@ -6,12 +6,8 @@ import type {
Alert,
} from "./types";
const API = "https://5.181.0.112.nip.io/api/v1/cabine";
const V2 = "https://5.181.0.112.nip.io/api/v2";
// ============================================
// ITEMS
// ============================================
const API = "https://mln-uber.club/api/v1/cabine";
const V2 = "https://mln-uber.club/api/v2";
export const getCommandItems = async (commandId: number) => {
const { data } = await apiClient.get(`${API}/commands/${commandId}/items`);
+1 -1
View File
@@ -7,7 +7,7 @@ import type {
Alert,
} from "./types";
const API = "https://5.181.0.112.nip.io/api/v1/livreur";
const API = "https://mln-uber.club/api/v1/livreur";
// ============================================
// STATUT
+1 -1
View File
@@ -2,7 +2,7 @@ import axios from "axios";
import { getToken, getAdminToken } from "../auth/tokenStorage";
// Change this to your server IP/domain
export const API_BASE_URL = "https://5.181.0.112.nip.io";
export const API_BASE_URL = "https://mln-uber.club";
const apiClient = axios.create({
baseURL: API_BASE_URL,
@@ -677,7 +677,9 @@ export default function SettingsScreen() {
telegram_bot_token: "",
telegram_bot_username: "",
telegram_notifications_enabled: false,
telegram_2fa_enabled: false,
delivery_mode: { mode: "single" as const, category_routes: [] },
shop_name: "Milieu-Nantais",
});
const [showApiKey, setShowApiKey] = useState(false);
const [showIpnSecret, setShowIpnSecret] = useState(false);
@@ -939,6 +941,27 @@ export default function SettingsScreen() {
return (
<View style={s.container}>
<ScrollView contentContainerStyle={s.content}>
{/* Personnalisation */}
<View style={s.section}>
<Text style={s.sectionTitle}>Personnalisation</Text>
<View style={[s.row, s.rowFirst]}>
<View style={s.rowLeft}>
<Text style={s.rowLabel}>Nom du shop</Text>
<Text style={s.rowDesc}>Affiché dans la sidebar du site client</Text>
</View>
</View>
<View style={{ paddingHorizontal: spacing.l, paddingBottom: spacing.l }}>
<TextInput
style={s.input}
value={settings.shop_name}
onChangeText={(v) => setSettings((p) => ({ ...p, shop_name: v }))}
placeholder="Ex: Milieu-Nantais"
placeholderTextColor={colors.textMuted}
autoCorrect={false}
/>
</View>
</View>
{/* Amendes */}
<View style={s.section}>
<Text style={s.sectionTitle}>Amendes</Text>
@@ -1412,6 +1435,18 @@ export default function SettingsScreen() {
thumbColor="#fff"
/>
</View>
<View style={s.row}>
<View style={s.rowLeft}>
<Text style={s.rowLabel}>Authentification 2FA</Text>
<Text style={s.rowDesc}>Permettre aux clients d'activer la double authentification via Telegram lors de la connexion</Text>
</View>
<Switch
value={settings.telegram_2fa_enabled}
onValueChange={(v) => setSettings((prev) => ({ ...prev, telegram_2fa_enabled: v }))}
trackColor={{ false: colors.border, true: colors.accent }}
thumbColor="#fff"
/>
</View>
<View style={{ paddingHorizontal: spacing.l, paddingBottom: spacing.l, gap: spacing.m }}>
<Text style={s.rowDesc}>
Configurez le bot Telegram pour envoyer des notifications aux utilisateurs qui ont lié leur compte.
+1 -2
View File
@@ -3,8 +3,7 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
+82 -5
View File
@@ -41,6 +41,8 @@ export interface AuthResponse {
access_token?: string; // ✅ CRITICAL!
token_type?: string;
expires_in?: number;
requires_2fa?: boolean;
session_token?: string;
user?: {
id: number;
username: string;
@@ -210,6 +212,15 @@ export const loginUser = async (
const data = await safeJson(response);
console.log("📋 [LOGIN] Réponse:", data);
// 2FA requis — retourner sans token
if (data.requires_2fa) {
return {
success: true,
requires_2fa: true,
session_token: data.session_token,
};
}
// ✅ Vérifier access_token
if (!data.access_token) {
console.error("❌ [LOGIN] Pas de access_token");
@@ -1219,11 +1230,14 @@ export const calculateOrderTotal = (order: Record<string, unknown>): number => {
// Fallback: calculer depuis les items si présents
if (Array.isArray(order.items) && order.items.length > 0) {
return (order.items as Record<string, unknown>[]).reduce((sum: number, item) => {
const price = Number(item.prix || item.price || 0);
const quantity = Number(item.quantite || item.quantity || 1);
return sum + price * quantity;
}, 0);
return (order.items as Record<string, unknown>[]).reduce(
(sum: number, item) => {
const price = Number(item.prix || item.price || 0);
const quantity = Number(item.quantite || item.quantity || 1);
return sum + price * quantity;
},
0,
);
}
return 0;
@@ -1863,6 +1877,8 @@ export interface PublicSettings {
crypto_payment_enabled: boolean;
crypto_only: boolean;
nowpayments_currencies: string[];
shop_name: string;
two_fa_enabled: boolean;
}
export const getPublicSettings = async (): Promise<PublicSettings> => {
@@ -1877,6 +1893,8 @@ export const getPublicSettings = async (): Promise<PublicSettings> => {
crypto_payment_enabled: false,
crypto_only: false,
nowpayments_currencies: [],
shop_name: "Milieu-Nantais",
two_fa_enabled: false,
};
try {
const response = await fetch(`${API_URL}/app-settings`);
@@ -1898,12 +1916,71 @@ export const getPublicSettings = async (): Promise<PublicSettings> => {
nowpayments_currencies: Array.isArray(data.nowpayments_currencies)
? data.nowpayments_currencies
: [],
shop_name: data.shop_name || "Milieu-Nantais",
two_fa_enabled: data.two_fa_enabled ?? false,
};
} catch {
return defaults;
}
};
export const verify2FA = async (
sessionToken: string,
code: string,
): Promise<AuthResponse> => {
try {
const response = await fetch(`${API_URL}/auth/2fa/verify`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ session_token: sessionToken, code }),
});
const data = await safeJson(response);
if (!response.ok) {
return { success: false, message: data.error || "Code invalide" };
}
sessionStorage.setItem("token", data.access_token);
syncUsernameFromJWT();
return {
success: true,
access_token: data.access_token,
token_type: data.token_type,
expires_in: data.expires_in,
user: data.user,
};
} catch (error) {
return { success: false, message: error instanceof Error ? error.message : "Erreur" };
}
};
export const get2FAStatus = async (): Promise<{ two_fa_enabled: boolean; telegram_linked: boolean; admin_2fa_enabled: boolean }> => {
const token = sessionStorage.getItem("token");
try {
const response = await fetch(`${API_URL}/two-fa/status`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) return { two_fa_enabled: false, telegram_linked: false, admin_2fa_enabled: false };
return await safeJson(response);
} catch {
return { two_fa_enabled: false, telegram_linked: false, admin_2fa_enabled: false };
}
};
export const toggle2FA = async (enabled: boolean): Promise<{ success: boolean; error?: string }> => {
const token = sessionStorage.getItem("token");
try {
const response = await fetch(`${API_URL}/two-fa/toggle`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
body: JSON.stringify({ enabled }),
});
const data = await safeJson(response);
if (!response.ok) return { success: false, error: data.error };
return { success: true };
} catch {
return { success: false, error: "Erreur réseau" };
}
};
export interface CryptoPaymentStatus {
command_id: number;
client_order_number?: number;
+2
View File
@@ -58,6 +58,8 @@ export interface LoginResponse {
token_type?: string;
expires_in?: number;
user?: UserResponse;
requires_2fa?: boolean;
session_token?: string;
}
/**
+6 -2
View File
@@ -39,6 +39,7 @@ function Navbar() {
const [unreadCount, setUnreadCount] = useState(0);
const [showNotifPanel, setShowNotifPanel] = useState(false);
const [referralEnabled, setReferralEnabled] = useState(true);
const [shopName, setShopName] = useState("Milieu-Nantais");
const seenKeysRef = useRef<Set<string>>(new Set());
const isFirstLoadRef = useRef(true);
const notifPanelRef = useRef<HTMLDivElement>(null);
@@ -74,7 +75,10 @@ function Navbar() {
}, [fetchNotifications]);
useEffect(() => {
getPublicSettings().then((s) => setReferralEnabled(s.referral_enabled));
getPublicSettings().then((s) => {
setReferralEnabled(s.referral_enabled);
if (s.shop_name) setShopName(s.shop_name);
});
}, []);
useEffect(() => {
@@ -235,7 +239,7 @@ function Navbar() {
<FontAwesomeIcon icon={faShoppingCart} />
</div>
<div>
<p className="sidebar-brand-name">Milieu-Nantais</p>
<p className="sidebar-brand-name">{shopName}</p>
<p className="sidebar-brand-sub">Mon espace</p>
</div>
</div>
+36 -16
View File
@@ -382,11 +382,14 @@ div[data-category="gros&semi"] .success-message {
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(8px);
border-radius: 50%;
width: 40px;
height: 40px;
width: 48px;
height: 48px;
min-width: 48px;
min-height: 48px;
display: flex;
align-items: center;
justify-content: center;
overflow: visible;
z-index: 3;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
@@ -406,8 +409,11 @@ div[data-category="gros&semi"] .success-message {
.media-indicator svg {
color: #ffffff;
width: 18px;
height: 18px;
width: 24px;
height: 24px;
min-width: 24px;
min-height: 24px;
flex-shrink: 0;
animation: cameraPulse 2s ease-in-out infinite;
}
@@ -601,12 +607,15 @@ div[data-category="gros&semi"] .details-button:hover {
backdrop-filter: blur(10px);
border: 2px solid rgba(255, 255, 255, 0.3);
color: white;
width: 40px;
height: 40px;
width: 48px;
height: 48px;
min-width: 48px;
min-height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
overflow: visible;
cursor: pointer;
z-index: 10;
transition: all 0.3s ease;
@@ -623,8 +632,11 @@ div[data-category="gros&semi"] .details-button:hover {
}
.video-close-btn svg {
width: 18px;
height: 18px;
width: 24px;
height: 24px;
min-width: 24px;
min-height: 24px;
flex-shrink: 0;
}
.video-player {
@@ -668,13 +680,17 @@ div[data-category="gros&semi"] .details-button:hover {
.video-close-btn {
top: 10px;
right: 10px;
width: 36px;
height: 36px;
width: 44px;
height: 44px;
min-width: 44px;
min-height: 44px;
}
.video-close-btn svg {
width: 16px;
height: 16px;
width: 20px;
height: 20px;
min-width: 20px;
min-height: 20px;
}
.video-player {
@@ -699,15 +715,19 @@ div[data-category="gros&semi"] .details-button:hover {
}
.media-indicator {
width: 36px;
height: 49px;
width: 44px;
height: 44px;
min-width: 44px;
min-height: 44px;
top: 10px;
right: 10px;
}
.media-indicator svg {
width: 16px;
height: 16px;
width: 20px;
height: 20px;
min-width: 20px;
min-height: 20px;
}
}
+5 -3
View File
@@ -1,4 +1,5 @@
import { useState } from "react";
import { createPortal } from "react-dom";
import { useNavigate } from "react-router-dom";
import { useCart } from "../context/useCart";
import { Camera, Info, X } from "lucide-react";
@@ -218,8 +219,8 @@ function ProductCard({
)}
</div>
{/* ✨ Modal vidéo */}
{showVideo && videoUrl && (
{/* ✨ Modal vidéo — rendu via Portal pour éviter le clipping du transform:scale sur .product-card */}
{showVideo && videoUrl && createPortal(
<div className="video-modal" onClick={handleCloseVideo}>
<div
className="video-modal-content"
@@ -242,7 +243,8 @@ function ProductCard({
vidéos.
</video>
</div>
</div>
</div>,
document.body
)}
</div>
);
+95 -18
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import { Lock, Mail, Eye, EyeOff, User } from "lucide-react";
import { Lock, Mail, Eye, EyeOff, User, Shield } from "lucide-react";
import "./Login.css";
import { loginUser, syncUsernameFromJWT } from "../../api/api";
import { loginUser, verify2FA, syncUsernameFromJWT } from "../../api/api";
import type { LoginRequest } from "../../api/api_types";
import { useNavigate } from "react-router-dom";
@@ -21,6 +21,10 @@ const LoginClient = () => {
const [isLoading, setIsLoading] = useState(false);
const [apiError, setApiError] = useState<string>("");
const [twoFAStep, setTwoFAStep] = useState(false);
const [sessionToken, setSessionToken] = useState("");
const [twoFACode, setTwoFACode] = useState("");
/**
* Valider le formulaire
*/
@@ -79,30 +83,19 @@ const LoginClient = () => {
hasToken: !!result.access_token,
});
if (result.success && result.access_token) {
console.log("✅ [LOGIN] Connexion réussie!");
// ✅ La synchronisation JWT est AUTOMATIQUE dans loginUser()
// Pas besoin de le faire ici
console.log("✅ [LOGIN] Token et username synchronisés");
// ✅ Vérifier la synchronisation
if (result.success && result.requires_2fa) {
setSessionToken(result.session_token || "");
setTwoFAStep(true);
} else if (result.success && result.access_token) {
const syncedUsername = syncUsernameFromJWT();
console.log("✅ [LOGIN] Username synchronisé:", syncedUsername);
// ✅ Redirection
if (result.user?.must_change_password) {
console.log("✅ [LOGIN] Première connexion - changement de mot de passe requis");
navigate("/user/change-password");
} else {
console.log("✅ [LOGIN] Redirection vers /user/accueil");
navigate("/user/accueil");
}
} else {
// ❌ Erreur API
const errorMessage =
result.message || "Identifiants incorrects";
console.error("❌ [LOGIN] Erreur API:", errorMessage);
const errorMessage = result.message || "Identifiants incorrects";
setApiError(errorMessage);
setErrors({ username: errorMessage });
}
@@ -117,6 +110,30 @@ const LoginClient = () => {
}
};
const handle2FASubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!twoFACode.trim()) return;
setIsLoading(true);
setApiError("");
try {
const result = await verify2FA(sessionToken, twoFACode.trim());
if (result.success && result.access_token) {
syncUsernameFromJWT();
if (result.user?.must_change_password) {
navigate("/user/change-password");
} else {
navigate("/user/accueil");
}
} else {
setApiError(result.message || "Code invalide");
}
} catch {
setApiError("Erreur de vérification");
} finally {
setIsLoading(false);
}
};
/**
* Gérer les changements d'input
*/
@@ -141,6 +158,66 @@ const LoginClient = () => {
}
};
if (twoFAStep) {
return (
<div className="login-container">
<div className="login-content">
<div className="login-header">
<div className="login-logo">
<Shield className="w-8 h-8 text-white" />
</div>
<h1 className="login-title">Vérification 2FA</h1>
<p className="login-subtitle">
Entrez le code envoyé sur votre Telegram
</p>
</div>
<div className="login-card">
<form className="login-form" onSubmit={handle2FASubmit}>
{apiError && (
<div className="error-banner"> {apiError}</div>
)}
<div className="form-group">
<label htmlFor="twoFACode" className="form-label">
Code de vérification
</label>
<div className="input-wrapper">
<input
type="text"
id="twoFACode"
value={twoFACode}
onChange={(e) => setTwoFACode(e.target.value)}
className="form-input"
placeholder="000000"
maxLength={6}
disabled={isLoading}
autoComplete="one-time-code"
style={{ letterSpacing: "0.3em", textAlign: "center", fontSize: "1.5rem" }}
/>
</div>
</div>
<button
type="submit"
disabled={isLoading || twoFACode.length < 6}
className="submit-button"
style={{ opacity: isLoading ? 0.6 : 1, cursor: isLoading ? "not-allowed" : "pointer" }}
>
{isLoading ? "Vérification..." : "Confirmer"}
</button>
<button
type="button"
onClick={() => { setTwoFAStep(false); setTwoFACode(""); setApiError(""); }}
className="submit-button"
style={{ marginTop: "0.5rem", background: "transparent", border: "1px solid #555", color: "#aaa" }}
>
Retour
</button>
</form>
</div>
</div>
</div>
);
}
return (
<div className="login-container">
<div className="login-content">
+55 -2
View File
@@ -1,11 +1,11 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import Navbar from '../../components/Navbar';
import { isUserAuthenticated, extractUsernameFromToken, getMyProfile, updateMyProfile, getTelegramStatus, generateTelegramLinkToken, unlinkTelegram } from '../../api/api';
import { isUserAuthenticated, extractUsernameFromToken, getMyProfile, updateMyProfile, getTelegramStatus, generateTelegramLinkToken, unlinkTelegram, get2FAStatus, toggle2FA, getPublicSettings } from '../../api/api';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faUser, faMapMarkerAlt, faPhone, faCommentDots,
faSave, faCheckCircle, faExclamationTriangle, faPaperPlane, faUnlink, faTimes, faLock,
faSave, faCheckCircle, faExclamationTriangle, faPaperPlane, faUnlink, faTimes, faLock, faShieldAlt,
} from '@fortawesome/free-solid-svg-icons';
import './ProfilePage.css';
@@ -37,6 +37,11 @@ export default function ProfilePage() {
const [tgEnabled, setTgEnabled] = useState(false);
const [tgLoading, setTgLoading] = useState(false);
// 2FA
const [twoFAEnabled, setTwoFAEnabled] = useState(false);
const [twoFAAdminEnabled, setTwoFAAdminEnabled] = useState(false);
const [twoFALoading, setTwoFALoading] = useState(false);
// Modal confirmation infos par défaut
const [showSaveModal, setShowSaveModal] = useState(false);
@@ -50,6 +55,12 @@ export default function ProfilePage() {
// Statut Telegram
getTelegramStatus().then((s) => { setTgLinked(s.linked); setTgEnabled(s.enabled); });
// Statut 2FA
Promise.all([get2FAStatus(), getPublicSettings()]).then(([status, pub]) => {
setTwoFAEnabled(status.two_fa_enabled);
setTwoFAAdminEnabled(pub.two_fa_enabled);
});
// Charger depuis backend
getMyProfile().then((res) => {
if (res.success && res.client) {
@@ -99,9 +110,23 @@ export default function ProfilePage() {
if (!window.confirm('Délier votre compte Telegram ? Vous ne recevrez plus de notifications.')) return;
await unlinkTelegram();
setTgLinked(false);
setTwoFAEnabled(false);
showSuccess('Compte Telegram délié');
};
const handleToggle2FA = async () => {
const newVal = !twoFAEnabled;
setTwoFALoading(true);
const res = await toggle2FA(newVal);
setTwoFALoading(false);
if (res.success) {
setTwoFAEnabled(newVal);
showSuccess(newVal ? 'Double authentification activée' : 'Double authentification désactivée');
} else {
showError(res.error || 'Erreur lors de la modification');
}
};
const saveContact = async () => {
setSavingContact(true);
const res = await updateMyProfile({ nom: nom.trim(), prenom: prenom.trim(), telephone: telephone.trim() });
@@ -276,6 +301,34 @@ export default function ProfilePage() {
)}
</div>
)}
{/* Section 2FA — visible uniquement si l'admin l'a activé ET Telegram est lié */}
{twoFAAdminEnabled && tgLinked && (
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon icon={faShieldAlt} className="profile-card-icon" style={{ color: '#6366f1' }} />
Double authentification (2FA)
</h2>
<p className="profile-hint">
À chaque connexion, un code vous sera envoyé sur Telegram avant d'accéder à votre compte.
</p>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginTop: '0.8rem' }}>
<button
className={`profile-btn ${twoFAEnabled ? 'profile-btn--danger' : 'profile-btn--telegram'}`}
onClick={handleToggle2FA}
disabled={twoFALoading}
>
<FontAwesomeIcon icon={faShieldAlt} />
{twoFALoading ? ' ...' : twoFAEnabled ? ' Désactiver la 2FA' : ' Activer la 2FA'}
</button>
{twoFAEnabled && (
<span style={{ color: '#10b981', fontSize: '0.9rem' }}>
<FontAwesomeIcon icon={faCheckCircle} /> Activée
</span>
)}
</div>
</div>
)}
</div>
{showSaveModal && (
+6 -1606
View File
File diff suppressed because it is too large Load Diff
+28 -1
View File
@@ -12,7 +12,7 @@ import type {
import { getToken } from "../auth/tokenStorage";
import { extractUsernameFromToken } from "../auth/jwtUtils";
const V1 = "https://5.181.0.112.nip.io/api/v1";
const V1 = "https://mln-uber.club/api/v1";
export const getJwtUsername = async (): Promise<string | null> => {
const token = await getToken();
@@ -787,6 +787,7 @@ export interface PublicSettings {
crypto_only: boolean;
nowpayments_currencies: string[];
telegram_notifications_enabled: boolean;
two_fa_enabled: boolean;
}
export const getPublicSettings = async (): Promise<PublicSettings> => {
@@ -801,6 +802,7 @@ export const getPublicSettings = async (): Promise<PublicSettings> => {
crypto_only: false,
nowpayments_currencies: [],
telegram_notifications_enabled: false,
two_fa_enabled: false,
};
try {
const { data } = await apiClient.get(`${V1}/app-settings`);
@@ -821,6 +823,7 @@ export const getPublicSettings = async (): Promise<PublicSettings> => {
: [],
telegram_notifications_enabled:
data.telegram_notifications_enabled ?? false,
two_fa_enabled: data.two_fa_enabled ?? false,
};
} catch {
return defaults;
@@ -889,6 +892,30 @@ export const unlinkTelegram = async (): Promise<void> => {
}
};
export const get2FAStatus = async (): Promise<{
two_fa_enabled: boolean;
telegram_linked: boolean;
admin_2fa_enabled: boolean;
}> => {
try {
const { data } = await apiClient.get(`${V1}/two-fa/status`);
return data;
} catch {
return { two_fa_enabled: false, telegram_linked: false, admin_2fa_enabled: false };
}
};
export const toggle2FA = async (
enabled: boolean,
): Promise<{ success: boolean; error?: string }> => {
try {
const { data } = await apiClient.post(`${V1}/two-fa/toggle`, { enabled });
return { success: data.success ?? true };
} catch (e: any) {
return { success: false, error: e?.response?.data?.error || "Erreur" };
}
};
export const calculateOrderTotal = (order: any): number => {
if (typeof order.total === "number" && order.total > 0) return order.total;
if (typeof order.total_prix === "number" && order.total_prix > 0)
+1 -3
View File
@@ -2,7 +2,7 @@ import axios from "axios";
import { getToken, getAdminToken } from "../auth/tokenStorage";
// Change this to your server IP/domain
export const API_BASE_URL = "https://5.181.0.112.nip.io";
export const API_BASE_URL = "https://mln-uber.club";
const apiClient = axios.create({
baseURL: API_BASE_URL,
@@ -12,9 +12,7 @@ const apiClient = axios.create({
},
});
// Request interceptor: attach JWT token
apiClient.interceptors.request.use(async (config) => {
// Determine which token to use based on URL
const isAdminRoute =
config.url?.includes("/api/v2/") ||
config.url?.includes("/api/v1/cabine/") ||
@@ -45,7 +45,7 @@ export default function OrderHistoryScreen() {
const [orders, setOrders] = useState<CompletedOrder[]>([]);
const [stats, setStats] = useState<ClientStats | null>(null);
const [penalties, setPenalties] = useState<PenaltyInfo | null>(null);
const [appSettings, setAppSettings] = useState<PublicSettings>({ penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true, referral_enabled: false, pool_names: ['Pool 1', 'Pool 2'], crypto_payment_enabled: false, nowpayments_currencies: [], crypto_only: false, telegram_notifications_enabled: false });
const [appSettings, setAppSettings] = useState<PublicSettings>({ penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true, referral_enabled: false, pool_names: ['Pool 1', 'Pool 2'], crypto_payment_enabled: false, nowpayments_currencies: [], crypto_only: false, telegram_notifications_enabled: false, two_fa_enabled: false });
const [referralBalance, setReferralBalance] = useState(0);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
+71 -2
View File
@@ -6,6 +6,7 @@ import {
ScrollView,
StyleSheet,
TouchableOpacity,
Switch,
Alert,
Modal,
KeyboardAvoidingView,
@@ -17,7 +18,7 @@ import { Feather } from "@expo/vector-icons";
import { useFocusEffect } from "@react-navigation/native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { Ionicons } from "@expo/vector-icons";
import { getMyProfile, updateMyProfile, getTelegramStatus, generateTelegramLinkToken, unlinkTelegram, changePassword } from "../../api/api";
import { getMyProfile, updateMyProfile, getTelegramStatus, generateTelegramLinkToken, unlinkTelegram, changePassword, get2FAStatus, toggle2FA, getPublicSettings } from "../../api/api";
import TextInput from "../../components/ui/TextInput";
import Button from "../../components/ui/Button";
import { useTheme } from "../../context/ThemeContext";
@@ -49,6 +50,11 @@ export default function ProfileScreen() {
const [telegramEnabled, setTelegramEnabled] = useState(false);
const [telegramLoading, setTelegramLoading] = useState(false);
// 2FA
const [twoFAEnabled, setTwoFAEnabled] = useState(false);
const [twoFAAdminEnabled, setTwoFAAdminEnabled] = useState(false);
const [twoFALoading, setTwoFALoading] = useState(false);
// Modal confirmation infos par défaut
const [showSaveModal, setShowSaveModal] = useState(false);
@@ -64,15 +70,19 @@ export default function ProfileScreen() {
const loadData = useCallback(async () => {
setLoadingProfile(true);
const [savedAddress, savedPhone, savedSignal, profileRes, tgStatus] = await Promise.all([
const [savedAddress, savedPhone, savedSignal, profileRes, tgStatus, twoFAStatus, pubSettings] = await Promise.all([
AsyncStorage.getItem(STORAGE_ADDRESS),
AsyncStorage.getItem(STORAGE_PHONE),
AsyncStorage.getItem(STORAGE_SIGNAL),
getMyProfile(),
getTelegramStatus(),
get2FAStatus(),
getPublicSettings(),
]);
setTelegramLinked(tgStatus.linked);
setTelegramEnabled(tgStatus.enabled);
setTwoFAEnabled(twoFAStatus.two_fa_enabled);
setTwoFAAdminEnabled(pubSettings.two_fa_enabled);
if (savedAddress !== null) setDefaultAddress(savedAddress);
if (savedPhone !== null) setDefaultPhone(savedPhone);
@@ -136,6 +146,7 @@ export default function ProfileScreen() {
onPress: async () => {
await unlinkTelegram();
setTelegramLinked(false);
setTwoFAEnabled(false);
},
},
],
@@ -170,6 +181,17 @@ export default function ProfileScreen() {
}
};
const handleToggle2FA = async (value: boolean) => {
setTwoFALoading(true);
const res = await toggle2FA(value);
setTwoFALoading(false);
if (res.success) {
setTwoFAEnabled(value);
} else {
Alert.alert("Erreur", res.error || "Impossible de modifier la 2FA");
}
};
const saveContact = async () => {
setSavingContact(true);
const res = await updateMyProfile({ nom: nom.trim(), prenom: prenom.trim(), telephone: telephone.trim() });
@@ -327,6 +349,17 @@ export default function ProfileScreen() {
},
pwdInputIcon: { marginRight: spacing.s },
pwdInput: { flex: 1, fontSize: fontSize.sm },
twoFARow: {
flexDirection: "row" as const,
alignItems: "center" as const,
justifyContent: "space-between" as const,
paddingTop: spacing.xs,
},
twoFALabel: {
color: colors.textPrimary,
fontSize: fontSize.sm,
fontWeight: fontWeight.semibold,
},
}), [colors]);
if (loadingProfile) {
@@ -505,6 +538,42 @@ export default function ProfileScreen() {
</View>
)}
{/* Carte 2FA — visible uniquement si l'admin l'a activé ET Telegram est lié */}
{twoFAAdminEnabled && telegramLinked && (
<View style={styles.card}>
<View style={styles.cardTitle}>
<Ionicons name="shield-checkmark-outline" size={18} color="#6366f1" />
<Text style={styles.cardTitleText}>Double authentification (2FA)</Text>
</View>
<Text style={styles.hint}>
À chaque connexion, un code à 6 chiffres vous sera envoyé sur Telegram avant d'accéder à votre compte.
</Text>
<View style={styles.twoFARow}>
<View style={{ flex: 1 }}>
<Text style={styles.twoFALabel}>
{twoFAEnabled ? "Activée" : "Désactivée"}
</Text>
{twoFAEnabled && (
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs, marginTop: 2 }}>
<Ionicons name="checkmark-circle" size={13} color="#10b981" />
<Text style={{ color: "#10b981", fontSize: fontSize.xs }}>Protection activée</Text>
</View>
)}
</View>
{twoFALoading ? (
<ActivityIndicator size="small" color="#6366f1" />
) : (
<Switch
value={twoFAEnabled}
onValueChange={handleToggle2FA}
trackColor={{ false: colors.border, true: "#6366f155" }}
thumbColor={twoFAEnabled ? "#6366f1" : colors.textMuted}
/>
)}
</View>
</View>
)}
</ScrollView>
<Modal