From 7ca168c34cac9d50cd6d739670e9f73a9cc04170 Mon Sep 17 00:00:00 2001 From: EvanChal Date: Wed, 28 Jan 2026 22:31:17 +0100 Subject: [PATCH] fix --- backend/api/routers/push.py | 108 +++++++- backend/app.py | 15 ++ backend/scripts/generate_vapid_keys.py | 70 ++++++ backend/utils/security.py | 35 ++- frontend/dev-dist/sw.js.map | 2 +- .../src/components/PWAInstallTutorial.vue | 231 +++++++++++------- frontend/src/utils/axios.js | 177 ++++++++++++-- frontend/src/views/Admin.vue | 11 +- frontend/src/views/AlbumDetail.vue | 14 +- frontend/src/views/Albums.vue | 5 +- frontend/src/views/Events.vue | 20 +- frontend/src/views/MyTickets.vue | 2 +- frontend/src/views/Posts.vue | 6 +- frontend/src/views/Register.vue | 25 +- 14 files changed, 576 insertions(+), 145 deletions(-) create mode 100644 backend/scripts/generate_vapid_keys.py diff --git a/backend/api/routers/push.py b/backend/api/routers/push.py index 4884df2..262338b 100644 --- a/backend/api/routers/push.py +++ b/backend/api/routers/push.py @@ -6,18 +6,63 @@ from models.notification import PushSubscription from models.user import User from schemas.notification import PushSubscriptionCreate, VapidPublicKeyResponse from utils.security import get_current_active_user +from utils.push_service import is_push_configured, send_push_to_user +import base64 router = APIRouter() + +def get_vapid_public_key_for_web_push(): + """ + Convertit la clé publique VAPID au format attendu par Web Push. + + Les clés VAPID peuvent être au format: + 1. Base64url brut (65 octets décodés) - Format attendu par Web Push + 2. PEM/DER complet (commence par MFk...) - Doit être converti + + Web Push attend la clé publique non compressée (65 octets = 0x04 + X + Y) + """ + public_key = settings.VAPID_PUBLIC_KEY + if not public_key: + return None + + # Nettoyer la clé (enlever les éventuels espaces/newlines) + public_key = public_key.strip() + + # Si la clé commence par MFk, c'est au format DER/SubjectPublicKeyInfo + # On doit extraire les 65 derniers octets + if public_key.startswith('MFk'): + try: + # Décoder le DER + # Ajouter le padding si nécessaire + padding = 4 - len(public_key) % 4 + if padding != 4: + public_key += '=' * padding + der_bytes = base64.b64decode(public_key) + + # La clé publique non compressée est les 65 derniers octets + # (SubjectPublicKeyInfo header + la clé) + if len(der_bytes) >= 65: + raw_key = der_bytes[-65:] + # Ré-encoder en base64url sans padding + return base64.urlsafe_b64encode(raw_key).rstrip(b'=').decode('ascii') + except Exception as e: + print(f"Erreur conversion clé VAPID: {e}") + return public_key + + return public_key + + @router.get("/vapid-public-key", response_model=VapidPublicKeyResponse) -async def get_vapid_public_key(current_user: User = Depends(get_current_active_user)): +async def get_vapid_public_key_endpoint(current_user: User = Depends(get_current_active_user)): """Get the VAPID public key for push notifications.""" - if not settings.VAPID_PUBLIC_KEY: + public_key = get_vapid_public_key_for_web_push() + if not public_key: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="VAPID keys not configured on server" ) - return {"public_key": settings.VAPID_PUBLIC_KEY} + return {"public_key": public_key} @router.post("/subscribe") async def subscribe_push( @@ -66,3 +111,60 @@ async def unsubscribe_push( return {"message": "Unsubscribed successfully"} +@router.post("/test") +async def test_push_notification( + db: Session = Depends(get_db), + current_user: User = Depends(get_current_active_user) +): + """Send a test push notification to the current user.""" + if not is_push_configured(): + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Push notifications not configured. Check VAPID keys." + ) + + # Vérifier que l'utilisateur a au moins un abonnement + sub_count = db.query(PushSubscription).filter( + PushSubscription.user_id == current_user.id + ).count() + + if sub_count == 0: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Aucun appareil enregistré pour les notifications push" + ) + + # Envoyer la notification de test + sent = send_push_to_user( + db, + current_user.id, + "🔔 Test de notification", + "Les notifications push fonctionnent correctement !", + "/" + ) + + return { + "message": f"Notification de test envoyée à {sent} appareil(s)", + "devices_registered": sub_count, + "sent_successfully": sent + } + + +@router.get("/status") +async def get_push_status( + db: Session = Depends(get_db), + current_user: User = Depends(get_current_active_user) +): + """Get the push notification configuration status.""" + sub_count = db.query(PushSubscription).filter( + PushSubscription.user_id == current_user.id + ).count() + + return { + "configured": is_push_configured(), + "vapid_public_key_set": bool(settings.VAPID_PUBLIC_KEY), + "vapid_private_key_set": bool(settings.VAPID_PRIVATE_KEY), + "user_subscriptions": sub_count + } + + diff --git a/backend/app.py b/backend/app.py index 00e47ce..cd393ea 100644 --- a/backend/app.py +++ b/backend/app.py @@ -203,6 +203,21 @@ app.add_middleware( expose_headers=["Content-Range", "Accept-Ranges"], ) + +# Middleware de debug pour les requêtes POST (à désactiver en production stable) +@app.middleware("http") +async def debug_auth_middleware(request: Request, call_next): + """Log les informations d'authentification pour debug.""" + if request.method == "POST" and "/api/" in request.url.path: + auth_header = request.headers.get("authorization", "") + has_auth = bool(auth_header) + auth_preview = auth_header[:30] + "..." if len(auth_header) > 30 else auth_header + print(f"🔍 DEBUG POST {request.url.path}: auth_header={'present' if has_auth else 'MISSING'}, preview={auth_preview}") + + response = await call_next(request) + return response + + # Endpoint personnalisé pour servir les vidéos avec support Range @app.get("/uploads/{file_path:path}") async def serve_media_with_range(request: Request, file_path: str): diff --git a/backend/scripts/generate_vapid_keys.py b/backend/scripts/generate_vapid_keys.py new file mode 100644 index 0000000..d20037e --- /dev/null +++ b/backend/scripts/generate_vapid_keys.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +""" +Script pour générer des clés VAPID pour les notifications push. + +Usage: + python scripts/generate_vapid_keys.py + +Les clés générées doivent être ajoutées aux variables d'environnement: + VAPID_PUBLIC_KEY= + VAPID_PRIVATE_KEY= + VAPID_CLAIMS_EMAIL=mailto:admin@example.com +""" + +import base64 +import os + +try: + from cryptography.hazmat.primitives.asymmetric import ec + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization +except ImportError: + print("❌ cryptography n'est pas installé.") + print(" Installez-le avec: pip install cryptography") + exit(1) + + +def generate_vapid_keys(): + """Génère une paire de clés VAPID au format base64url.""" + # Générer une clé privée EC P-256 + private_key = ec.generate_private_key(ec.SECP256R1(), default_backend()) + + # Extraire la clé privée brute (32 octets) + private_numbers = private_key.private_numbers() + private_bytes = private_numbers.private_value.to_bytes(32, byteorder='big') + private_key_b64 = base64.urlsafe_b64encode(private_bytes).rstrip(b'=').decode('ascii') + + # Extraire la clé publique non compressée (65 octets = 0x04 + X + Y) + public_key = private_key.public_key() + public_bytes = public_key.public_bytes( + encoding=serialization.Encoding.X962, + format=serialization.PublicFormat.UncompressedPoint + ) + public_key_b64 = base64.urlsafe_b64encode(public_bytes).rstrip(b'=').decode('ascii') + + return public_key_b64, private_key_b64 + + +if __name__ == "__main__": + print("🔐 Génération des clés VAPID pour Web Push...\n") + + public_key, private_key = generate_vapid_keys() + + print("=" * 70) + print("Clés VAPID générées avec succès !") + print("=" * 70) + print() + print("Ajoutez ces variables à votre fichier .env ou configuration:") + print() + print(f'VAPID_PUBLIC_KEY={public_key}') + print() + print(f'VAPID_PRIVATE_KEY={private_key}') + print() + print('VAPID_CLAIMS_EMAIL=mailto:admin@lediscord.com') + print() + print("=" * 70) + print() + print("⚠️ IMPORTANT:") + print(" - Gardez la clé privée SECRÈTE") + print(" - Les clés doivent être les mêmes en dev et prod") + print(" - Après changement de clés, les utilisateurs devront se réabonner") diff --git a/backend/utils/security.py b/backend/utils/security.py index c50f881..d04346c 100644 --- a/backend/utils/security.py +++ b/backend/utils/security.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta from typing import Optional from jose import JWTError, jwt from passlib.context import CryptContext -from fastapi import Depends, HTTPException, status +from fastapi import Depends, HTTPException, status, Request from fastapi.security import OAuth2PasswordBearer from sqlalchemy.orm import Session from config.settings import settings @@ -11,7 +11,9 @@ from models.user import User from schemas.user import TokenData pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") + +# OAuth2 scheme avec auto_error=False pour pouvoir logger les erreurs +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=False) def verify_password(plain_password: str, hashed_password: str) -> bool: """Verify a plain password against a hashed password.""" @@ -45,17 +47,42 @@ def verify_token(token: str, credentials_exception) -> TokenData: except JWTError: raise credentials_exception -async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> User: +async def get_current_user( + token: Optional[str] = Depends(oauth2_scheme), + db: Session = Depends(get_db) +) -> User: """Get the current authenticated user.""" credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) - token_data = verify_token(token, credentials_exception) + + # Log si pas de token + if token is None: + print("❌ AUTH: No token provided in Authorization header") + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not authenticated - no token provided", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Log le token (premiers caractères seulement pour la sécurité) + print(f"🔐 AUTH: Token received, length={len(token)}, preview={token[:20]}...") + + try: + token_data = verify_token(token, credentials_exception) + print(f"✅ AUTH: Token valid for user_id={token_data.user_id}") + except HTTPException as e: + print(f"❌ AUTH: Token validation failed - {e.detail}") + raise + user = db.query(User).filter(User.id == token_data.user_id).first() if user is None: + print(f"❌ AUTH: User not found for id={token_data.user_id}") raise credentials_exception + + print(f"✅ AUTH: User authenticated: {user.username}") return user async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User: diff --git a/frontend/dev-dist/sw.js.map b/frontend/dev-dist/sw.js.map index d607aad..42e3519 100644 --- a/frontend/dev-dist/sw.js.map +++ b/frontend/dev-dist/sw.js.map @@ -1 +1 @@ -{"version":3,"file":"sw.js","sources":["../tmp/8a7a7c9c7bcda32c30a658026d87c311/sw.js"],"sourcesContent":["import {registerRoute as workbox_routing_registerRoute} from '/app/node_modules/workbox-routing/registerRoute.mjs';\nimport {ExpirationPlugin as workbox_expiration_ExpirationPlugin} from '/app/node_modules/workbox-expiration/ExpirationPlugin.mjs';\nimport {CacheableResponsePlugin as workbox_cacheable_response_CacheableResponsePlugin} from '/app/node_modules/workbox-cacheable-response/CacheableResponsePlugin.mjs';\nimport {CacheFirst as workbox_strategies_CacheFirst} from '/app/node_modules/workbox-strategies/CacheFirst.mjs';\nimport {NetworkFirst as workbox_strategies_NetworkFirst} from '/app/node_modules/workbox-strategies/NetworkFirst.mjs';\nimport {StaleWhileRevalidate as workbox_strategies_StaleWhileRevalidate} from '/app/node_modules/workbox-strategies/StaleWhileRevalidate.mjs';\nimport {clientsClaim as workbox_core_clientsClaim} from '/app/node_modules/workbox-core/clientsClaim.mjs';\nimport {precacheAndRoute as workbox_precaching_precacheAndRoute} from '/app/node_modules/workbox-precaching/precacheAndRoute.mjs';\nimport {cleanupOutdatedCaches as workbox_precaching_cleanupOutdatedCaches} from '/app/node_modules/workbox-precaching/cleanupOutdatedCaches.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\nimportScripts(\n \"/sw-custom.js\"\n);\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n/**\n * The precacheAndRoute() method efficiently caches and responds to\n * requests for URLs in the manifest.\n * See https://goo.gl/S9QRab\n */\nworkbox_precaching_precacheAndRoute([\n {\n \"url\": \"registerSW.js\",\n \"revision\": \"3ca0b8505b4bec776b69afdba2768812\"\n }\n], {});\nworkbox_precaching_cleanupOutdatedCaches();\n\n\n\nworkbox_routing_registerRoute(/^https:\\/\\/fonts\\.googleapis\\.com\\/.*/i, new workbox_strategies_CacheFirst({ \"cacheName\":\"google-fonts-cache\", plugins: [new workbox_expiration_ExpirationPlugin({ maxEntries: 10, maxAgeSeconds: 31536000 }), new workbox_cacheable_response_CacheableResponsePlugin({ statuses: [ 0, 200 ] })] }), 'GET');\nworkbox_routing_registerRoute(/^https:\\/\\/fonts\\.gstatic\\.com\\/.*/i, new workbox_strategies_CacheFirst({ \"cacheName\":\"gstatic-fonts-cache\", plugins: [new workbox_expiration_ExpirationPlugin({ maxEntries: 10, maxAgeSeconds: 31536000 }), new workbox_cacheable_response_CacheableResponsePlugin({ statuses: [ 0, 200 ] })] }), 'GET');\nworkbox_routing_registerRoute(({ url, request }) => {\n const urlString = url.href || url.toString();\n return /^https?:\\/\\/.*\\/api\\/.*/i.test(urlString) && request.method === \"GET\";\n }, new workbox_strategies_NetworkFirst({ \"cacheName\":\"api-cache\",\"networkTimeoutSeconds\":10, plugins: [new workbox_expiration_ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 300 })] }), 'GET');\nworkbox_routing_registerRoute(/^https?:\\/\\/.*\\/uploads\\/.*/i, new workbox_strategies_StaleWhileRevalidate({ \"cacheName\":\"uploads-cache\", plugins: [new workbox_expiration_ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 604800 }), new workbox_cacheable_response_CacheableResponsePlugin({ statuses: [ 0, 200 ] })] }), 'GET');\n\n\n\n\n"],"names":["importScripts","self","skipWaiting","workbox_core_clientsClaim","workbox_precaching_precacheAndRoute","workbox_precaching_cleanupOutdatedCaches","workbox_routing_registerRoute","workbox_strategies_CacheFirst","plugins","workbox_expiration_ExpirationPlugin","maxEntries","maxAgeSeconds","workbox_cacheable_response_CacheableResponsePlugin","statuses","url","request","urlString","href","toString","test","method","workbox_strategies_NetworkFirst","workbox_strategies_StaleWhileRevalidate"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"sw.js","sources":["../tmp/446ca58375f4cce21ff7112ab25b11c6/sw.js"],"sourcesContent":["import {registerRoute as workbox_routing_registerRoute} from '/app/node_modules/workbox-routing/registerRoute.mjs';\nimport {ExpirationPlugin as workbox_expiration_ExpirationPlugin} from '/app/node_modules/workbox-expiration/ExpirationPlugin.mjs';\nimport {CacheableResponsePlugin as workbox_cacheable_response_CacheableResponsePlugin} from '/app/node_modules/workbox-cacheable-response/CacheableResponsePlugin.mjs';\nimport {CacheFirst as workbox_strategies_CacheFirst} from '/app/node_modules/workbox-strategies/CacheFirst.mjs';\nimport {NetworkFirst as workbox_strategies_NetworkFirst} from '/app/node_modules/workbox-strategies/NetworkFirst.mjs';\nimport {StaleWhileRevalidate as workbox_strategies_StaleWhileRevalidate} from '/app/node_modules/workbox-strategies/StaleWhileRevalidate.mjs';\nimport {clientsClaim as workbox_core_clientsClaim} from '/app/node_modules/workbox-core/clientsClaim.mjs';\nimport {precacheAndRoute as workbox_precaching_precacheAndRoute} from '/app/node_modules/workbox-precaching/precacheAndRoute.mjs';\nimport {cleanupOutdatedCaches as workbox_precaching_cleanupOutdatedCaches} from '/app/node_modules/workbox-precaching/cleanupOutdatedCaches.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\nimportScripts(\n \"/sw-custom.js\"\n);\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n/**\n * The precacheAndRoute() method efficiently caches and responds to\n * requests for URLs in the manifest.\n * See https://goo.gl/S9QRab\n */\nworkbox_precaching_precacheAndRoute([\n {\n \"url\": \"registerSW.js\",\n \"revision\": \"3ca0b8505b4bec776b69afdba2768812\"\n }\n], {});\nworkbox_precaching_cleanupOutdatedCaches();\n\n\n\nworkbox_routing_registerRoute(/^https:\\/\\/fonts\\.googleapis\\.com\\/.*/i, new workbox_strategies_CacheFirst({ \"cacheName\":\"google-fonts-cache\", plugins: [new workbox_expiration_ExpirationPlugin({ maxEntries: 10, maxAgeSeconds: 31536000 }), new workbox_cacheable_response_CacheableResponsePlugin({ statuses: [ 0, 200 ] })] }), 'GET');\nworkbox_routing_registerRoute(/^https:\\/\\/fonts\\.gstatic\\.com\\/.*/i, new workbox_strategies_CacheFirst({ \"cacheName\":\"gstatic-fonts-cache\", plugins: [new workbox_expiration_ExpirationPlugin({ maxEntries: 10, maxAgeSeconds: 31536000 }), new workbox_cacheable_response_CacheableResponsePlugin({ statuses: [ 0, 200 ] })] }), 'GET');\nworkbox_routing_registerRoute(({ url, request }) => {\n const urlString = url.href || url.toString();\n return /^https?:\\/\\/.*\\/api\\/.*/i.test(urlString) && request.method === \"GET\";\n }, new workbox_strategies_NetworkFirst({ \"cacheName\":\"api-cache\",\"networkTimeoutSeconds\":10, plugins: [new workbox_expiration_ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 300 })] }), 'GET');\nworkbox_routing_registerRoute(/^https?:\\/\\/.*\\/uploads\\/.*/i, new workbox_strategies_StaleWhileRevalidate({ \"cacheName\":\"uploads-cache\", plugins: [new workbox_expiration_ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 604800 }), new workbox_cacheable_response_CacheableResponsePlugin({ statuses: [ 0, 200 ] })] }), 'GET');\n\n\n\n\n"],"names":["importScripts","self","skipWaiting","workbox_core_clientsClaim","workbox_precaching_precacheAndRoute","workbox_precaching_cleanupOutdatedCaches","workbox_routing_registerRoute","workbox_strategies_CacheFirst","plugins","workbox_expiration_ExpirationPlugin","maxEntries","maxAgeSeconds","workbox_cacheable_response_CacheableResponsePlugin","statuses","url","request","urlString","href","toString","test","method","workbox_strategies_NetworkFirst","workbox_strategies_StaleWhileRevalidate"],"mappings":"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"} \ No newline at end of file diff --git a/frontend/src/components/PWAInstallTutorial.vue b/frontend/src/components/PWAInstallTutorial.vue index 88373c2..91b9fc6 100644 --- a/frontend/src/components/PWAInstallTutorial.vue +++ b/frontend/src/components/PWAInstallTutorial.vue @@ -28,56 +28,58 @@ -
-
+
+ +
-
+
-
+
1
-
-

Appuyez sur le bouton de partage

-

- En bas de l'écran, dans la barre d'outils Safari +

+

Appuyez sur le bouton de partage

+

+ En bas de l'écran Safari, cherchez l'icône carrée avec une flèche vers le haut ⬆️

-
-
-
- - +
+
+
+ +
- -

Bouton de partage

+

+ Astuce : Si vous ne voyez pas la barre, faites défiler vers le haut pour la faire apparaître +

-
+
-
+
2
-
-

Sélectionnez "Sur l'écran d'accueil"

-

- Faites défiler le menu de partage vers le haut +

+

Cherchez "Sur l'écran d'accueil"

+

+ Faites défiler le menu vers le bas jusqu'à trouver cette option

-
+
- - + +
-

Sur l'écran d'accueil

-

Ajouter à l'écran d'accueil

+

Sur l'écran d'accueil

+

L'icône ressemble à un + dans un carré

@@ -85,97 +87,138 @@
-
+
-
+
3
-
-

Confirmez l'installation

-

- Appuyez sur "Ajouter" dans la popup de confirmation +

+

Appuyez sur "Ajouter"

+

+ En haut à droite de l'écran

-
+
- -

L'application apparaîtra sur votre écran d'accueil !

+ +

L'application apparaîtra sur votre écran d'accueil !

+ + +
+
+ ⚠️ +
+

Important pour iOS :

+

Les notifications push ne fonctionnent que si l'app est installée sur l'écran d'accueil (iOS 16.4+)

+
+
+
- -
+ +
-
+
-
+
1
-
-

Appuyez sur le menu

-

- En haut à droite de votre navigateur (⋮) +

+

Appuyez sur le menu ⋮

+

+ Les 3 points verticaux en haut à droite de Chrome

-
-
-
- - - -
+
+
+ + +
+
+
+ 2 +
+
+
+

"Installer l'application"

+

+ Ou "Ajouter à l'écran d'accueil" +

+
+
+ + +
+
+
+ 3 +
+
+
+

Confirmez avec "Installer"

+
+
+ +

L'app s'installera sur votre téléphone !

+
+ + +
+ +
+
+
+ 1 +
+
+
+

Cherchez l'icône d'installation

+

+ Dans la barre d'adresse de Chrome/Edge, cherchez l'icône 📥 ou ➕ +

+
+

+ Chrome : Icône avec un écran et une flèche à droite de la barre d'adresse +

+

+ Edge : "Installer LeDiscord" dans le menu ⋯ +

+
+
+
-
+
-
+
2
-
-

Sélectionnez "Ajouter à l'écran d'accueil"

-

- Ou "Installer l'application" selon votre navigateur -

-
+
+

Cliquez sur "Installer"

+
-
- - - -
-
-

Ajouter à l'écran d'accueil

-
+ +

L'app s'ouvrira dans sa propre fenêtre !

- - -
-
-
- 3 -
-
-
-

Confirmez l'installation

-

- Appuyez sur "Ajouter" ou "Installer" dans la popup -

-
-
- -

L'application sera installée !

-
+ + +
+
+ 💡 +
+

Si vous ne voyez pas l'option d'installation, rafraîchissez la page ou essayez avec Chrome/Edge.

@@ -231,11 +274,31 @@ const props = defineProps({ isIOS: { type: Boolean, default: false + }, + isAndroid: { + type: Boolean, + default: false + }, + isWindows: { + type: Boolean, + default: false + }, + isMac: { + type: Boolean, + default: false } }) const emit = defineEmits(['close']) +// Déterminer quel type d'instructions afficher +const instructionType = computed(() => { + if (props.isIOS) return 'ios' + if (props.isAndroid) return 'android' + if (props.isWindows || props.isMac) return 'desktop' + return 'android' // Par défaut +}) + function close() { emit('close') } diff --git a/frontend/src/utils/axios.js b/frontend/src/utils/axios.js index 6fd60e7..b4c8a26 100644 --- a/frontend/src/utils/axios.js +++ b/frontend/src/utils/axios.js @@ -123,23 +123,28 @@ instance.interceptors.response.use( } if (error.response?.status === 401) { - // Ne pas déconnecter automatiquement sur les requêtes POST/PUT avec FormData - // car les erreurs peuvent être dues à des problèmes réseau temporaires sur mobile - const isFormDataUpload = error.config?.data instanceof FormData || - (error.config?.method === 'POST' || error.config?.method === 'PUT') + const currentRoute = router.currentRoute.value + const errorDetail = error.response?.data?.detail || '' + const errorDetailLower = errorDetail.toLowerCase() + + // Vérifier si c'est une vraie erreur d'authentification + const isRealAuthError = errorDetailLower.includes('credential') || + errorDetailLower.includes('token') || + errorDetailLower.includes('not authenticated') || + errorDetailLower.includes('could not validate') || + errorDetailLower.includes('expired') + + console.warn(`🔒 401 reçu - Auth error: ${isRealAuthError}, Detail: ${errorDetail}`) // Ne pas rediriger si on est déjà sur une page d'auth - const currentRoute = router.currentRoute.value if (!currentRoute.path.includes('/login') && !currentRoute.path.includes('/register')) { - // Pour les uploads, ne déconnecter que si c'est vraiment une erreur d'authentification - // (pas juste une erreur réseau qui se manifeste comme 401) - if (!isFormDataUpload || error.response?.data?.detail?.includes('credentials') || error.response?.data?.detail?.includes('token')) { + if (isRealAuthError) { localStorage.removeItem('token') router.push('/login') toast.error('Session expirée, veuillez vous reconnecter') } else { - // Pour les uploads, juste afficher une erreur sans déconnecter - toast.error('Erreur lors de l\'upload. Veuillez réessayer.') + // Erreur 401 non liée à l'auth (rare mais possible) + toast.error('Erreur d\'autorisation') } } } else if (error.response?.status === 403) { @@ -210,27 +215,145 @@ export function getAppUrl() { return import.meta.env.VITE_APP_URL || window.location.origin } -// Fonction utilitaire pour upload de FormData via fetch natif -// (contourne les problèmes d'axios avec FormData sur certains navigateurs/mobiles) -export async function uploadFormData(endpoint, formData) { +// Fonction utilitaire pour les requêtes POST JSON via fetch natif +// (contourne les problèmes d'axios sur certains navigateurs mobiles/PWA) +export async function postJson(endpoint, data = {}) { const token = localStorage.getItem('token') const apiUrl = getApiUrl() - const response = await fetch(`${apiUrl}${endpoint}`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}` - // Ne PAS mettre Content-Type, fetch le gère automatiquement avec FormData - }, - body: formData - }) + console.log(`📤 POST JSON vers: ${apiUrl}${endpoint}`) + + try { + const response = await fetch(`${apiUrl}${endpoint}`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }) + + console.log(`📥 POST response status: ${response.status}`) - if (!response.ok) { - const errorData = await response.json().catch(() => ({})) - const error = new Error(errorData.detail || 'Erreur lors de l\'upload') - error.response = { status: response.status, data: errorData } + if (!response.ok) { + const errorData = await response.json().catch(() => ({ detail: 'Erreur inconnue' })) + console.error('❌ POST error:', errorData) + + const toast = useToast() + if (response.status === 401) { + const errorDetail = (errorData.detail || '').toLowerCase() + const isRealAuthError = errorDetail.includes('credential') || + errorDetail.includes('token') || + errorDetail.includes('not authenticated') || + errorDetail.includes('could not validate') || + errorDetail.includes('expired') + if (isRealAuthError) { + localStorage.removeItem('token') + router.push('/login') + toast.error('Session expirée, veuillez vous reconnecter') + } else { + toast.error('Erreur d\'autorisation') + } + } else if (response.status === 403) { + toast.error('Accès non autorisé') + } else if (response.status === 422) { + toast.error('Données invalides') + } else { + toast.error(errorData.detail || 'Erreur serveur') + } + + const error = new Error(errorData.detail || 'Erreur') + error.response = { status: response.status, data: errorData } + throw error + } + + return await response.json() + } catch (error) { + if (!error.response) { + console.error('❌ Network error:', error) + const toast = useToast() + toast.error('Erreur de connexion') + } + throw error + } +} + +// Fonction utilitaire pour upload de FormData via fetch natif +// (contourne les problèmes d'axios avec FormData sur certains navigateurs/mobiles) +export async function uploadFormData(endpoint, formData, options = {}) { + const token = localStorage.getItem('token') + const apiUrl = getApiUrl() + const toast = useToast() + + // Timeout par défaut de 5 minutes pour les uploads + const timeout = options.timeout || 300000 + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), timeout) + + console.log(`📤 Upload FormData vers: ${apiUrl}${endpoint}`) + + try { + const response = await fetch(`${apiUrl}${endpoint}`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}` + // Ne PAS mettre Content-Type, fetch le gère automatiquement avec FormData + }, + body: formData, + signal: controller.signal + }) + + clearTimeout(timeoutId) + console.log(`📥 Upload response status: ${response.status}`) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ detail: 'Erreur inconnue' })) + console.error('❌ Upload error:', errorData) + + // Gestion des erreurs d'authentification + if (response.status === 401) { + // Vérifier si c'est vraiment une erreur d'auth ou juste un problème réseau + const isAuthError = errorData.detail?.toLowerCase().includes('credential') || + errorData.detail?.toLowerCase().includes('token') || + errorData.detail?.toLowerCase().includes('not authenticated') + if (isAuthError) { + localStorage.removeItem('token') + router.push('/login') + toast.error('Session expirée, veuillez vous reconnecter') + } else { + toast.error('Erreur d\'authentification lors de l\'upload') + } + } else if (response.status === 413) { + toast.error('Fichier trop volumineux') + } else if (response.status === 422) { + toast.error('Données invalides: ' + (errorData.detail || 'Vérifiez le formulaire')) + } else { + toast.error(errorData.detail || 'Erreur lors de l\'upload') + } + + const error = new Error(errorData.detail || 'Erreur lors de l\'upload') + error.response = { status: response.status, data: errorData } + throw error + } + + const data = await response.json() + console.log('✅ Upload réussi') + return data + } catch (error) { + clearTimeout(timeoutId) + + if (error.name === 'AbortError') { + console.error('❌ Upload timeout') + toast.error('Délai d\'attente dépassé. Le fichier est peut-être trop volumineux.') + throw new Error('Timeout lors de l\'upload') + } + + // Erreur réseau + if (!error.response) { + console.error('❌ Network error during upload:', error) + toast.error('Erreur de connexion lors de l\'upload') + } + throw error } - - return await response.json() } diff --git a/frontend/src/views/Admin.vue b/frontend/src/views/Admin.vue index ac663df..22330e4 100644 --- a/frontend/src/views/Admin.vue +++ b/frontend/src/views/Admin.vue @@ -950,8 +950,9 @@

Cliquez sur l'image pour l'agrandir

@@ -1009,7 +1010,7 @@ >
@@ -1849,7 +1850,11 @@ function openImageModal(imageUrl) { function getMediaUrl(path) { if (!path) return '' - return path.startsWith('http') ? path : `${import.meta.env.VITE_API_URL || 'http://localhost:8002'}${path}` + if (path.startsWith('http')) return path + const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8002' + // Ensure path starts with / + const normalizedPath = path.startsWith('/') ? path : `/${path}` + return `${baseUrl}${normalizedPath}` } // Nouvelles fonctions pour les filtres et actions rapides diff --git a/frontend/src/views/AlbumDetail.vue b/frontend/src/views/AlbumDetail.vue index a28fc31..77d1313 100644 --- a/frontend/src/views/AlbumDetail.vue +++ b/frontend/src/views/AlbumDetail.vue @@ -475,6 +475,17 @@ + + + + + @@ -582,7 +593,8 @@ import { Eye, Heart, ChevronLeft, - ChevronRight + ChevronRight, + Download } from 'lucide-vue-next' import LoadingLogo from '@/components/LoadingLogo.vue' diff --git a/frontend/src/views/Albums.vue b/frontend/src/views/Albums.vue index ece03cc..78e74f9 100644 --- a/frontend/src/views/Albums.vue +++ b/frontend/src/views/Albums.vue @@ -333,7 +333,7 @@ import { ref, onMounted } from 'vue' import { useToast } from 'vue-toastification' import { useRouter } from 'vue-router' -import axios from '@/utils/axios' +import axios, { postJson } from '@/utils/axios' import { getMediaUrl, uploadFormData } from '@/utils/axios' import { formatRelativeDateInFrenchTimezone, formatShortDateInFrenchTimezone } from '@/utils/dateUtils' import { @@ -630,8 +630,7 @@ async function createAlbum() { } uploadStatus.value = 'Création de l\'album...' - const albumResponse = await axios.post('/api/albums', albumData) - const album = albumResponse.data + const album = await postJson('/api/albums', albumData) // Upload media files in batches for better performance const batchSize = 5 // Upload 5 files at a time diff --git a/frontend/src/views/Events.vue b/frontend/src/views/Events.vue index 564a43b..82fb466 100644 --- a/frontend/src/views/Events.vue +++ b/frontend/src/views/Events.vue @@ -221,23 +221,25 @@ >
-
+
- +
- +
@@ -331,7 +333,7 @@ import { ref, computed, onMounted } from 'vue' import { useToast } from 'vue-toastification' import { useRouter } from 'vue-router' -import axios from '@/utils/axios' +import axios, { postJson } from '@/utils/axios' import { getMediaUrl } from '@/utils/axios' import { formatRelativeDateInFrenchTimezone, formatShortDateInFrenchTimezone, convertFrenchTimezoneToUTC } from '@/utils/dateUtils' import { @@ -496,11 +498,11 @@ async function createEvent() { } console.log('🔵 eventData préparé:', eventData) - console.log('🔵 Envoi de la requête axios.post...') + console.log('🔵 Envoi de la requête postJson...') - const response = await axios.post('/api/events', eventData) + const data = await postJson('/api/events', eventData) - console.log('✅ Réponse reçue:', response) + console.log('✅ Réponse reçue:', data) // Refresh events list await fetchEvents() diff --git a/frontend/src/views/MyTickets.vue b/frontend/src/views/MyTickets.vue index 5db304e..fd9c29e 100644 --- a/frontend/src/views/MyTickets.vue +++ b/frontend/src/views/MyTickets.vue @@ -140,7 +140,7 @@
diff --git a/frontend/src/views/Posts.vue b/frontend/src/views/Posts.vue index cfb11ff..8b93b45 100644 --- a/frontend/src/views/Posts.vue +++ b/frontend/src/views/Posts.vue @@ -288,7 +288,7 @@ import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useAuthStore } from '@/stores/auth' import { useToast } from 'vue-toastification' -import axios from '@/utils/axios' +import axios, { postJson } from '@/utils/axios' import { getMediaUrl, uploadFormData } from '@/utils/axios' import { formatRelativeDateInFrenchTimezone } from '@/utils/dateUtils' import { @@ -355,14 +355,14 @@ async function createPost() { creating.value = true try { - const response = await axios.post('/api/posts', { + const data = await postJson('/api/posts', { content: newPost.value.content, image_url: newPost.value.image_url, mentioned_user_ids: newPost.value.mentioned_user_ids }) // Add new post to the beginning of the list - posts.value.unshift(response.data) + posts.value.unshift(data) // Forcer le rafraîchissement de la date immédiatement dateRefreshKey.value++ diff --git a/frontend/src/views/Register.vue b/frontend/src/views/Register.vue index db29827..2cf0085 100644 --- a/frontend/src/views/Register.vue +++ b/frontend/src/views/Register.vue @@ -68,7 +68,10 @@ >
- +
- + +

C'est ce qui sera affiché aux autres membres

@@ -337,6 +344,9 @@
@@ -451,6 +461,9 @@ const isMobile = ref(false) const showPWAInstructions = ref(false) const isIOS = computed(() => /iPhone|iPad|iPod/i.test(navigator.userAgent)) +const isAndroid = computed(() => /Android/i.test(navigator.userAgent)) +const isWindows = computed(() => /Windows/i.test(navigator.userAgent)) +const isMac = computed(() => /Macintosh|Mac OS/i.test(navigator.userAgent) && !isIOS.value) function checkIfMobile() { isMobile.value = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) || @@ -476,8 +489,8 @@ async function handleInstallApp() { } catch (error) { console.error('Erreur lors de l\'installation:', error) } - } else if (isMobile.value) { - // Sur mobile sans beforeinstallprompt, afficher les instructions + } else { + // Si pas de beforeinstallprompt, afficher les instructions (mobile ou desktop) showPWAInstructions.value = true } }