chore: fix updates

This commit is contained in:
2026-03-01 17:30:08 +01:00
parent 2fad63c28d
commit bb8dee3406
9 changed files with 226 additions and 19 deletions
+25 -6
View File
@@ -1,10 +1,14 @@
name: Frontend Client - EAS Update
name: Frontend Client - EAS Build
on:
push:
branches: [main]
paths:
- "mobile/**"
pull_request:
branches: [main]
paths:
- "mobile/**"
jobs:
typecheck:
@@ -28,7 +32,7 @@ jobs:
working-directory: mobile
run: npx tsc --noEmit
eas-update:
build-apk-prod:
needs: typecheck
runs-on: ubuntu-latest
@@ -58,10 +62,25 @@ jobs:
jq '.expo.extra.eas.projectId = "${{ secrets.EXPO_PROJECT_ID_CLIENT }}"' app.json > app.tmp.json
mv app.tmp.json app.json
- name: Map channel to branch
- name: Debug app.json
working-directory: mobile
run: eas channel:edit production --branch main --non-interactive
run: cat app.json
- name: Publish OTA update
- name: Build production APK
working-directory: mobile
run: eas update --channel production --platform android --message "${{ github.event.head_commit.message }}" --non-interactive
env:
EAS_BUILD_NO_EXPO_GO_WARNING: true
run: eas build --platform android --profile production --non-interactive
- name: Download production APK
working-directory: mobile
run: |
APK_URL=$(eas build:list --platform android --status finished --limit 1 --json --non-interactive | jq -r '.[0].artifacts.buildUrl')
curl -L -o client-panel-prod.apk "$APK_URL"
- name: Upload production APK artifact
uses: actions/upload-artifact@v4
with:
name: client-panel-android-prod-apk
path: mobile/client-panel-prod.apk
retention-days: 14
-1
View File
@@ -1,3 +1,2 @@
suppr la page register
rajouter la modification de mot de passe
les notif sur l'app web
+24 -1
View File
@@ -178,6 +178,28 @@ func (d *Database) UpdateClientPassword(clientID int, hashedPassword string) err
return nil
}
// UpdateClientPasswordAndClearFlag met à jour le mot de passe et remet must_change_password à false
func (d *Database) UpdateClientPasswordAndClearFlag(clientID int, hashedPassword string) error {
query := `UPDATE clients SET password = $1, must_change_password = FALSE, updated_at = CURRENT_TIMESTAMP WHERE id = $2`
result, err := d.Exec(query, hashedPassword, clientID)
if err != nil {
return fmt.Errorf("erreur lors de la mise à jour du mot de passe: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("erreur lors de la vérification: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("client non trouvé")
}
log.Printf("✅ Mot de passe client mis à jour + must_change_password=false (ID: %d)", clientID)
return nil
}
// GetClientStats récupère les statistiques d'un client
func (d *Database) GetClientStats(clientID int) (map[string]interface{}, error) {
client, err := d.GetClientByID(clientID)
@@ -369,7 +391,7 @@ 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, created_at
query := `SELECT id, username, password, nom, prenom, telephone, command, point, point_zipette, amende, must_change_password, created_at
FROM clients WHERE username = $1`
err := d.QueryRow(query, username).Scan(
@@ -383,6 +405,7 @@ func (d *Database) GetClientByUsername(username string) (*models.Client, error)
&client.Point,
&client.PointZipette,
&client.Amende,
&client.MustChangePassword,
&client.CreatedAt,
)
+5
View File
@@ -71,6 +71,11 @@ func InitDB() *Database {
log.Println("✅ Tables créées avec succès")
// Migration: ajouter colonne must_change_password si elle n'existe pas
if _, err = database.Exec(`ALTER TABLE clients ADD COLUMN IF NOT EXISTS must_change_password BOOLEAN NOT NULL DEFAULT TRUE`); err != nil {
log.Fatalf("❌ Erreur migration must_change_password: %v", err)
}
// Lancer le nettoyage périodique des tokens expirés
go database.cleanExpiredTokensPeriodically()
+53 -7
View File
@@ -327,17 +327,63 @@ func LoginClient(c *gin.Context) {
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,
"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,
},
})
}
// ChangePassword permet à un client de changer son mot de passe
// PUT /api/v1/auth/change-password
func ChangePassword(c *gin.Context) {
var req struct {
CurrentPassword string `json:"current_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=8"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides", "details": err.Error()})
return
}
clientID := c.GetInt("client_id")
database := c.MustGet("database").(*db.Database)
client, err := database.GetClientByID(clientID)
if err != nil {
log.Printf("❌ [CHANGE_PASSWORD] Client non trouvé: ID=%d", clientID)
c.JSON(http.StatusNotFound, gin.H{"error": "Client non trouvé"})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(client.Password), []byte(req.CurrentPassword)); err != nil {
log.Printf("❌ [CHANGE_PASSWORD] Mot de passe actuel invalide: ID=%d", clientID)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Mot de passe actuel incorrect"})
return
}
hashed, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil {
log.Printf("❌ [CHANGE_PASSWORD] Erreur bcrypt: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur traitement mot de passe"})
return
}
if err := database.UpdateClientPasswordAndClearFlag(clientID, string(hashed)); err != nil {
log.Printf("❌ [CHANGE_PASSWORD] Erreur update: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur mise à jour mot de passe"})
return
}
log.Printf("✅ [CHANGE_PASSWORD] Mot de passe changé: ID=%d", clientID)
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Mot de passe mis à jour avec succès"})
}
// LogoutClient déconnecte un client
// POST /api/v1/auth/logout
func LogoutClient(c *gin.Context) {
+103
View File
@@ -0,0 +1,103 @@
package handlers
import (
"encoding/json"
"gestion/db"
"log"
"net/http"
"github.com/gin-gonic/gin"
)
// GetClientNotifications retourne les notifications du client connecté
// GET /api/v1/notifications
func GetClientNotifications(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
notifKey := "notifications:" + username
// Récupérer toutes les notifications (max 50)
results, err := db.Redis.LRange(db.RedisCtx, notifKey, 0, 49).Result()
if err != nil {
log.Printf("❌ [GET_NOTIFICATIONS] Erreur Redis: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur récupération notifications"})
return
}
type Notification struct {
CommandID int `json:"command_id"`
Type string `json:"type"`
Message string `json:"message"`
CreatedAt string `json:"created_at"`
Read bool `json:"read"`
}
notifications := make([]Notification, 0, len(results))
unreadCount := 0
for _, raw := range results {
var n Notification
if err := json.Unmarshal([]byte(raw), &n); err != nil {
continue
}
notifications = append(notifications, n)
if !n.Read {
unreadCount++
}
}
log.Printf("✅ [GET_NOTIFICATIONS] %d notifications pour %s (%d non lues)", len(notifications), username, unreadCount)
c.JSON(http.StatusOK, gin.H{
"notifications": notifications,
"unread_count": unreadCount,
"total": len(notifications),
})
}
// MarkNotificationsRead marque toutes les notifications comme lues
// POST /api/v1/notifications/read
func MarkNotificationsRead(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
notifKey := "notifications:" + username
// Récupérer toutes les notifications
results, err := db.Redis.LRange(db.RedisCtx, notifKey, 0, -1).Result()
if err != nil {
log.Printf("❌ [MARK_NOTIFICATIONS_READ] Erreur Redis LRange: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lecture notifications"})
return
}
// Réécrire chaque notification avec read=true
markedCount := 0
for i, raw := range results {
var n map[string]interface{}
if err := json.Unmarshal([]byte(raw), &n); err != nil {
continue
}
if read, ok := n["read"].(bool); ok && read {
continue
}
n["read"] = true
updated, _ := json.Marshal(n)
db.Redis.LSet(db.RedisCtx, notifKey, int64(i), string(updated))
markedCount++
}
log.Printf("✅ [MARK_NOTIFICATIONS_READ] %d notifications marquées lues pour %s", markedCount, username)
c.JSON(http.StatusOK, gin.H{
"success": true,
"marked_count": markedCount,
})
}
+4 -3
View File
@@ -14,7 +14,8 @@ type Client struct {
Point int `json:"point"`
PointZipette int `json:"points_zipette"`
Amende float64 `json:"amende"`
CancellationsCount int `json:"cancellations_count"` // ✅ NOUVEAU
LastPenaltyReason string `json:"last_penalty_reason"`
CreatedAt time.Time `json:"created_at"`
CancellationsCount int `json:"cancellations_count"` // ✅ NOUVEAU
LastPenaltyReason string `json:"last_penalty_reason"`
MustChangePassword bool `json:"must_change_password"`
CreatedAt time.Time `json:"created_at"`
}
+11
View File
@@ -37,6 +37,13 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
authGroupV1.POST("/logout", handlers.LogoutClient)
}
// Route change-password (auth client requise)
authProtectedV1 := router.Group("/api/v1/auth")
authProtectedV1.Use(middleware.ClientMiddleware)
{
authProtectedV1.PUT("/change-password", handlers.ChangePassword)
}
// ============================================
// 📦 PRODUITS (v1) - PUBLIC (SANS middleware!)
// ============================================
@@ -84,6 +91,10 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
// ⭐⭐ PÉNALITÉS CLIENT
cartGroupV1.GET("/penalties", handlers.GetMyPenalties) // Voir mes pénalités
// 🔔 NOTIFICATIONS CLIENT
cartGroupV1.GET("/notifications", handlers.GetClientNotifications)
cartGroupV1.POST("/notifications/read", handlers.MarkNotificationsRead)
// 👤 PROFIL CLIENT - MODIFICATION PAR LE CLIENT
cartGroupV1.PUT("/profile/update", handlers.UpdateMyProfile) // ✅ Modifier mon profil
}
+1 -1
View File
@@ -82,7 +82,7 @@ export default function Toast({
</Animated.View>
);
}
//
const styles = StyleSheet.create({
container: {
position: "absolute",