diff --git a/lib/core/api/authed_http.dart b/lib/core/api/authed_http.dart new file mode 100644 index 0000000..c150a92 --- /dev/null +++ b/lib/core/api/authed_http.dart @@ -0,0 +1,57 @@ +import 'dart:html' as html; +import 'package:http/http.dart' as http; + +/// Client HTTP authentifié pour les routes `/api/*` protégées. +/// +/// POURQUOI CE FICHIER EXISTE +/// Les routes `/api/epg`, `/api/recordings` et `/api/season-passes` sont +/// montées derrière `authMiddleware` côté serveur. Or plusieurs écrans les +/// appelaient avec `package:http` nu, qui n'envoie ni en-tête `Authorization` +/// ni cookie : sur le web, `BrowserClient` a `withCredentials = false`, donc +/// le cookie `session` reste à quai. Toutes ces requêtes repartaient en 401 — +/// guide TV vide, liste d'enregistrements vide, season passes vides. +/// +/// Ce wrapper injecte le même jeton que `ApiClient` et `XtreamService` +/// (`localStorage['auth_token']`), de sorte qu'il n'existe qu'une seule +/// source de vérité pour la session. +class AuthedHttp { + AuthedHttp._(); + + /// Jeton de session posé par `ApiClient.setToken` à la connexion. + static String? get _token { + final t = html.window.localStorage['auth_token']; + return (t != null && t.isNotEmpty) ? t : null; + } + + static Map _headers([Map? extra]) { + final token = _token; + return { + if (extra != null) ...extra, + if (token != null) 'Authorization': 'Bearer $token', + }; + } + + static Future get(Uri url, {Map? headers}) => + http.get(url, headers: _headers(headers)); + + static Future post( + Uri url, { + Map? headers, + Object? body, + }) => + http.post(url, headers: _headers(headers), body: body); + + static Future put( + Uri url, { + Map? headers, + Object? body, + }) => + http.put(url, headers: _headers(headers), body: body); + + static Future delete( + Uri url, { + Map? headers, + Object? body, + }) => + http.delete(url, headers: _headers(headers), body: body); +} diff --git a/lib/features/iptv/widgets/recording_modal.dart b/lib/features/iptv/widgets/recording_modal.dart index b15096a..0914cb7 100644 --- a/lib/features/iptv/widgets/recording_modal.dart +++ b/lib/features/iptv/widgets/recording_modal.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:http/http.dart' as http; +import '../../../core/api/authed_http.dart'; import '../../../core/models/iptv_models.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/widgets/glass_container.dart'; @@ -45,7 +45,7 @@ class _RecordingModalState extends State { try { // Utilisation d'une URL relative en Web (ou d'une configuration pour autres plateformes) - final response = await http.post( + final response = await AuthedHttp.post( Uri.parse('/api/recordings'), headers: {'Content-Type': 'application/json'}, body: json.encode({ diff --git a/lib/features/iptv/widgets/recordings_tab.dart b/lib/features/iptv/widgets/recordings_tab.dart index 285b7da..196f2c2 100644 --- a/lib/features/iptv/widgets/recordings_tab.dart +++ b/lib/features/iptv/widgets/recordings_tab.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:http/http.dart' as http; +import '../../../core/api/authed_http.dart'; import '../../../core/models/iptv_models.dart'; import '../../../core/models/playlist_config.dart'; import '../../../core/theme/app_colors.dart'; @@ -111,7 +111,7 @@ class _EpgGuideViewState extends ConsumerState<_EpgGuideView> }); try { - final response = await http.get(Uri.parse('/api/epg/${ch.streamId}')); + final response = await AuthedHttp.get(Uri.parse('/api/epg/${ch.streamId}')); if (mounted) { if (response.statusCode == 200) { final data = json.decode(response.body) as Map; @@ -589,7 +589,7 @@ class _ProgrammeCard extends StatelessWidget { DateTime end, ) async { try { - final response = await http.post( + final response = await AuthedHttp.post( Uri.parse('/api/recordings'), headers: {'Content-Type': 'application/json'}, body: json.encode({ @@ -745,7 +745,7 @@ class _RecordingsListViewState extends State<_RecordingsListView> { _error = null; }); try { - final response = await http.get(Uri.parse('/api/recordings')); + final response = await AuthedHttp.get(Uri.parse('/api/recordings')); if (response.statusCode == 200) { setState(() { final decoded = json.decode(response.body); @@ -767,7 +767,7 @@ class _RecordingsListViewState extends State<_RecordingsListView> { } Future _stopRecording(String id, String title) async { - await http.post(Uri.parse('/api/recordings/stop/$id')); + await AuthedHttp.post(Uri.parse('/api/recordings/stop/$id')); _fetchRecordings(); if (mounted) { ScaffoldMessenger.of(context) @@ -776,7 +776,7 @@ class _RecordingsListViewState extends State<_RecordingsListView> { } Future _deleteRecording(String id) async { - await http.delete(Uri.parse('/api/recordings/$id')); + await AuthedHttp.delete(Uri.parse('/api/recordings/$id')); _fetchRecordings(); } @@ -817,7 +817,7 @@ class _RecordingsListViewState extends State<_RecordingsListView> { Future _showLogs(String id, String title) async { try { - final response = await http.get(Uri.parse('/api/recordings/logs/$id')); + final response = await AuthedHttp.get(Uri.parse('/api/recordings/logs/$id')); if (!mounted) return; final content = response.statusCode == 200 ? (json.decode(response.body)['logs'] as String? ?? 'Aucun log') @@ -1095,7 +1095,7 @@ class _SeasonPassesViewState extends State<_SeasonPassesView> { Future _loadPasses() async { setState(() => _isLoading = true); try { - final r = await http.get(Uri.parse('/api/season-passes')); + final r = await AuthedHttp.get(Uri.parse('/api/season-passes')); if (r.statusCode == 200) { setState(() { _passes = json.decode(r.body); @@ -1110,7 +1110,7 @@ class _SeasonPassesViewState extends State<_SeasonPassesView> { } Future _deletePass(String id, String title) async { - await http.delete(Uri.parse('/api/season-passes/$id')); + await AuthedHttp.delete(Uri.parse('/api/season-passes/$id')); _loadPasses(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -1177,7 +1177,7 @@ class _SeasonPassesViewState extends State<_SeasonPassesView> { ? '/api/live/$c.ts' : urlCtrl.text.trim(); Navigator.pop(ctx); - final r = await http.post( + final r = await AuthedHttp.post( Uri.parse('/api/season-passes'), headers: {'Content-Type': 'application/json'}, body: json.encode( diff --git a/lib/features/iptv/widgets/simple_recording_widget.dart b/lib/features/iptv/widgets/simple_recording_widget.dart index de6f664..0045241 100644 --- a/lib/features/iptv/widgets/simple_recording_widget.dart +++ b/lib/features/iptv/widgets/simple_recording_widget.dart @@ -1,6 +1,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; +import '../../../core/api/authed_http.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../../core/models/iptv_models.dart'; import '../../../core/theme/app_colors.dart'; @@ -31,7 +31,7 @@ class _SimpleRecordingWidgetState extends State { setState(() => _status = 'Starting...'); try { - final response = await http.post( + final response = await AuthedHttp.post( Uri.parse('/api/record/now'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ @@ -66,7 +66,7 @@ class _SimpleRecordingWidgetState extends State { final endTime = _startTime.add(Duration(minutes: _durationMinutes)); try { - final response = await http.post( + final response = await AuthedHttp.post( Uri.parse('/api/record/schedule'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ @@ -96,7 +96,7 @@ class _SimpleRecordingWidgetState extends State { /// 🔴 Stop recording Future _stopRecording() async { try { - await http.post( + await AuthedHttp.post( Uri.parse('/api/record/stop/${widget.channel.streamId}'), headers: {'Content-Type': 'application/json'}, );