diff --git a/frontend/src/components/TicketFloatingButton.vue b/frontend/src/components/TicketFloatingButton.vue index 3adc1d5..6eda096 100644 --- a/frontend/src/components/TicketFloatingButton.vue +++ b/frontend/src/components/TicketFloatingButton.vue @@ -200,11 +200,7 @@ async function submitTicket() { console.log(`DEBUG - FormData entry: ${key} = ${value}`) } - await axios.post('/api/tickets/', formData, { - headers: { - 'Content-Type': 'multipart/form-data', - } - }) + await axios.post('/api/tickets/', formData) toast.success('Ticket envoyé avec succès !') closeModal() diff --git a/frontend/src/layouts/DefaultLayout.vue b/frontend/src/layouts/DefaultLayout.vue index 4e5831c..f1f855e 100644 --- a/frontend/src/layouts/DefaultLayout.vue +++ b/frontend/src/layouts/DefaultLayout.vue @@ -442,10 +442,20 @@ onMounted(async () => { // Demander la permission pour les notifications push // Sur iOS, attendre un peu pour s'assurer que la PWA est bien détectée if (typeof notificationService.isIOS === 'function' && notificationService.isIOS()) { - // Attendre que la page soit complètement chargée + console.log('🍎 iOS détecté, vérification de la PWA...') + // Attendre que la page soit complètement chargée et que la PWA soit détectée setTimeout(async () => { - await notificationService.requestNotificationPermission() - }, 1000) + const isInstalled = notificationService.isPWAInstalled() + console.log('🍎 PWA installée:', isInstalled) + + if (isInstalled) { + console.log('🍎 Demande de permission pour les notifications...') + await notificationService.requestNotificationPermission() + } else { + console.warn('🍎 PWA non installée - Les notifications ne fonctionneront pas sur iOS') + console.warn('🍎 Instructions: Ajouter l\'app à l\'écran d\'accueil, puis l\'ouvrir depuis l\'écran d\'accueil') + } + }, 2000) // Attendre 2 secondes pour iOS } else { await notificationService.requestNotificationPermission() } diff --git a/frontend/src/services/notificationService.js b/frontend/src/services/notificationService.js index c26feae..c28f8f2 100644 --- a/frontend/src/services/notificationService.js +++ b/frontend/src/services/notificationService.js @@ -130,55 +130,101 @@ class NotificationService { // Gestion des notifications push PWA async requestNotificationPermission() { + console.log('🔔 requestNotificationPermission appelée') + console.log('🔔 isIOS:', this.isIOS()) + console.log('🔔 isPWAInstalled:', this.isPWAInstalled()) + console.log('🔔 Notification API disponible:', 'Notification' in window) + console.log('🔔 Permission actuelle:', Notification.permission) + if (!this.isNotificationSupported()) { - console.log('Notifications not supported on this platform') + console.warn('⚠️ Notifications not supported on this platform') + if (this.isIOS()) { + if (!this.isPWAInstalled()) { + console.warn('⚠️ iOS: PWA must be installed (added to home screen)') + } + const iosVersion = navigator.userAgent.match(/OS (\d+)_(\d+)/) + if (iosVersion) { + const major = parseInt(iosVersion[1], 10) + const minor = parseInt(iosVersion[2], 10) + if (major < 16 || (major === 16 && minor < 4)) { + console.warn(`⚠️ iOS: Version ${major}.${minor} - Push notifications require iOS 16.4+`) + } + } + } return false } if (Notification.permission === 'granted') { - console.log('Notification permission already granted') + console.log('✅ Notification permission already granted') return true } if (Notification.permission === 'denied') { - console.warn('Notification permission denied by user') + console.warn('❌ Notification permission denied by user') return false } // Sur iOS, s'assurer que la PWA est installée avant de demander if (this.isIOS() && !this.isPWAInstalled()) { - console.warn('iOS: Cannot request notification permission - PWA must be installed first') + console.warn('⚠️ iOS: Cannot request notification permission - PWA must be installed first') + console.warn('⚠️ Instructions: Add the app to home screen, then open it from home screen') return false } try { + console.log('🔔 Demande de permission...') const permission = await Notification.requestPermission() const granted = permission === 'granted' + console.log('🔔 Résultat de la demande:', permission) + if (granted) { - console.log('Notification permission granted') + console.log('✅ Notification permission granted') + + // Sur iOS, vérifier que le service worker est prêt + if (this.isIOS() && 'serviceWorker' in navigator) { + try { + const registration = await navigator.serviceWorker.ready + console.log('✅ Service worker ready on iOS') + + // Tester une notification pour vérifier que ça fonctionne + await registration.showNotification('Test LeDiscord', { + body: 'Les notifications sont activées !', + icon: '/icon-192x192.png', + badge: '/icon-96x96.png', + tag: 'test-notification' + }) + console.log('✅ Test notification sent successfully') + } catch (error) { + console.error('❌ Error testing notification on iOS:', error) + } + } } else { - console.warn('Notification permission denied:', permission) + console.warn('❌ Notification permission denied:', permission) } return granted } catch (error) { - console.error('Error requesting notification permission:', error) + console.error('❌ Error requesting notification permission:', error) return false } } async showPushNotification(title, options = {}) { + console.log('🔔 showPushNotification appelée:', { title, options }) + if (!this.isNotificationSupported()) { - console.warn('Cannot show notification - not supported on this platform') + console.warn('⚠️ Cannot show notification - not supported on this platform') return null } const hasPermission = await this.requestNotificationPermission() if (!hasPermission) { - console.warn('Cannot show notification - permission not granted') + console.warn('⚠️ Cannot show notification - permission not granted') return null } + + console.log('✅ Permission granted, affichage de la notification...') // Préparer les options de notification (iOS ne supporte pas vibrate) const notificationOptions = { @@ -203,29 +249,46 @@ class NotificationService { // Cela permet aux notifications de fonctionner même quand l'app est fermée if ('serviceWorker' in navigator) { try { + console.log('🔔 Tentative d\'affichage via service worker...') const registration = await navigator.serviceWorker.ready + console.log('🔔 Service worker ready, active:', !!registration.active) if (!registration.active) { - console.warn('Service worker not active, using fallback') + console.warn('⚠️ Service worker not active, using fallback') throw new Error('Service worker not active') } - // Envoyer un message au service worker pour afficher la notification - registration.active.postMessage({ - type: 'SHOW_NOTIFICATION', - title, - options: notificationOptions - }) + // Sur iOS, utiliser directement l'API du service worker + // (les messages peuvent ne pas fonctionner correctement) + if (this.isIOS()) { + console.log('🔔 iOS: Utilisation directe de showNotification') + await registration.showNotification(title, notificationOptions) + console.log('✅ Notification affichée via service worker (iOS)') + } else { + // Envoyer un message au service worker pour afficher la notification + registration.active.postMessage({ + type: 'SHOW_NOTIFICATION', + title, + options: notificationOptions + }) + + // Aussi utiliser l'API directe du service worker + await registration.showNotification(title, notificationOptions) + console.log('✅ Notification affichée via service worker') + } - // Aussi utiliser l'API directe du service worker - await registration.showNotification(title, notificationOptions) - - console.log('Notification shown via service worker') return null } catch (error) { - console.error('Error showing notification via service worker:', error) + console.error('❌ Error showing notification via service worker:', error) + console.error('❌ Error details:', { + message: error.message, + stack: error.stack, + name: error.name + }) // Continuer avec le fallback } + } else { + console.warn('⚠️ Service worker not available') } // Fallback: notification native du navigateur (seulement si le SW n'est pas disponible) @@ -270,6 +333,23 @@ class NotificationService { }) } } + + // Fonction de test pour vérifier que les notifications fonctionnent + async testNotification() { + console.log('🧪 Test de notification...') + const result = await this.showPushNotification('Test LeDiscord', { + body: 'Si vous voyez cette notification, les notifications push fonctionnent !', + link: '/' + }) + + if (result) { + console.log('✅ Test réussi - notification affichée') + return true + } else { + console.warn('⚠️ Test échoué - notification non affichée') + return false + } + } } export default new NotificationService() diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js index b823eb0..0ee9420 100644 --- a/frontend/src/stores/auth.js +++ b/frontend/src/stores/auth.js @@ -112,11 +112,7 @@ export const useAuthStore = defineStore('auth', () => { const formData = new FormData() formData.append('file', file) - const response = await axios.post('/api/users/me/avatar', formData, { - headers: { - 'Content-Type': 'multipart/form-data' - } - }) + const response = await axios.post('/api/users/me/avatar', formData) user.value = response.data toast.success('Avatar mis à jour') diff --git a/frontend/src/utils/axios.js b/frontend/src/utils/axios.js index 42e4b07..403323e 100644 --- a/frontend/src/utils/axios.js +++ b/frontend/src/utils/axios.js @@ -68,6 +68,16 @@ instance.interceptors.request.use( } } + // Augmenter le timeout pour les requêtes POST/PUT avec FormData (uploads) + if ((config.method === 'POST' || config.method === 'PUT') && config.data instanceof FormData) { + config.timeout = 120000 // 2 minutes pour les uploads + // Ne pas définir Content-Type pour FormData - laisser le navigateur l'ajouter avec la boundary + // C'est crucial sur mobile où définir explicitement le Content-Type peut causer des erreurs + if (config.headers && config.headers['Content-Type'] === 'multipart/form-data') { + delete config.headers['Content-Type'] + } + } + return config }, error => { @@ -112,12 +122,24 @@ 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') + // 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')) { - localStorage.removeItem('token') - router.push('/login') - toast.error('Session expirée, veuillez vous reconnecter') + // 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')) { + 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.') + } } } else if (error.response?.status === 403) { toast.error('Accès non autorisé') diff --git a/frontend/src/views/AlbumDetail.vue b/frontend/src/views/AlbumDetail.vue index f20fce0..f71c5ec 100644 --- a/frontend/src/views/AlbumDetail.vue +++ b/frontend/src/views/AlbumDetail.vue @@ -716,9 +716,7 @@ async function uploadMedia() { formData.append('files', media.file) }) - await axios.post(`/api/albums/${album.value.id}/media`, formData, { - headers: { 'Content-Type': 'multipart/form-data' } - }) + await axios.post(`/api/albums/${album.value.id}/media`, formData) // Refresh album data await fetchAlbum() diff --git a/frontend/src/views/Albums.vue b/frontend/src/views/Albums.vue index 48d35d1..aaea01f 100644 --- a/frontend/src/views/Albums.vue +++ b/frontend/src/views/Albums.vue @@ -654,7 +654,6 @@ async function createAlbum() { }) await axios.post(`/api/albums/${album.id}/media`, formData, { - headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (progressEvent) => { // Update progress for this batch const batchProgress = (progressEvent.loaded / progressEvent.total) * 100 diff --git a/frontend/src/views/Posts.vue b/frontend/src/views/Posts.vue index 1350542..c3db368 100644 --- a/frontend/src/views/Posts.vue +++ b/frontend/src/views/Posts.vue @@ -465,9 +465,7 @@ async function handleImageChange(event) { const formData = new FormData() formData.append('file', file) - const response = await axios.post('/api/posts/upload-image', formData, { - headers: { 'Content-Type': 'multipart/form-data' } - }) + const response = await axios.post('/api/posts/upload-image', formData) newPost.value.image_url = response.data.image_url } catch (error) { diff --git a/frontend/src/views/Vlogs.vue b/frontend/src/views/Vlogs.vue index 1692482..52640cc 100644 --- a/frontend/src/views/Vlogs.vue +++ b/frontend/src/views/Vlogs.vue @@ -435,9 +435,7 @@ async function createVlog() { formData.append('thumbnail', newVlog.value.thumbnailFile) } - const response = await axios.post('/api/vlogs/upload', formData, { - headers: { 'Content-Type': 'multipart/form-data' } - }) + const response = await axios.post('/api/vlogs/upload', formData) vlogs.value.unshift(response.data) showCreateModal.value = false diff --git a/frontend/vite.config.js b/frontend/vite.config.js index fa5d348..3144597 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -211,7 +211,11 @@ module.exports = defineConfig(({ command, mode }) => { } }, { - urlPattern: /^https?:\/\/.*\/api\/.*/i, + // Intercepter uniquement les requêtes GET pour l'API + urlPattern: ({ url, request }) => { + const urlString = url.href || url.toString() + return /^https?:\/\/.*\/api\/.*/i.test(urlString) && request.method === 'GET' + }, handler: 'NetworkFirst', options: { cacheName: 'api-cache',