mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
fix(api): send session token on EPG, recordings and season-pass calls
The security hardening in 60d3f42 moved /api/epg, /api/recordings and
/api/season-passes behind authMiddleware, but the Flutter client still
called them through bare package:http. Those requests carry no
Authorization header, and on web BrowserClient sets withCredentials to
false so the session cookie is not sent either — every call came back
401. Symptoms: empty TV guide, empty recordings list, empty season
passes.
Adds AuthedHttp, a thin wrapper that injects the same token ApiClient
and XtreamService already use (localStorage['auth_token']), and routes
the 13 affected calls through it.
subtitle_service is left on plain http: it fetches third-party subtitle
URLs, not our API, and must not leak the session token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1feeaae78a
commit
ad3e970c8d
4 files changed
+73
-16
No files matched your search
@@ -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<String, String> _headers([Map<String, String>? extra]) {
|
||||||
|
final token = _token;
|
||||||
|
return {
|
||||||
|
if (extra != null) ...extra,
|
||||||
|
if (token != null) 'Authorization': 'Bearer $token',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<http.Response> get(Uri url, {Map<String, String>? headers}) =>
|
||||||
|
http.get(url, headers: _headers(headers));
|
||||||
|
|
||||||
|
static Future<http.Response> post(
|
||||||
|
Uri url, {
|
||||||
|
Map<String, String>? headers,
|
||||||
|
Object? body,
|
||||||
|
}) =>
|
||||||
|
http.post(url, headers: _headers(headers), body: body);
|
||||||
|
|
||||||
|
static Future<http.Response> put(
|
||||||
|
Uri url, {
|
||||||
|
Map<String, String>? headers,
|
||||||
|
Object? body,
|
||||||
|
}) =>
|
||||||
|
http.put(url, headers: _headers(headers), body: body);
|
||||||
|
|
||||||
|
static Future<http.Response> delete(
|
||||||
|
Uri url, {
|
||||||
|
Map<String, String>? headers,
|
||||||
|
Object? body,
|
||||||
|
}) =>
|
||||||
|
http.delete(url, headers: _headers(headers), body: body);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.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/iptv_models.dart';
|
||||||
import '../../../core/theme/app_colors.dart';
|
import '../../../core/theme/app_colors.dart';
|
||||||
import '../../../core/widgets/glass_container.dart';
|
import '../../../core/widgets/glass_container.dart';
|
||||||
@@ -45,7 +45,7 @@ class _RecordingModalState extends State<RecordingModal> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Utilisation d'une URL relative en Web (ou d'une configuration pour autres plateformes)
|
// 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'),
|
Uri.parse('/api/recordings'),
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: json.encode({
|
body: json.encode({
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'dart:convert';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:google_fonts/google_fonts.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/iptv_models.dart';
|
||||||
import '../../../core/models/playlist_config.dart';
|
import '../../../core/models/playlist_config.dart';
|
||||||
import '../../../core/theme/app_colors.dart';
|
import '../../../core/theme/app_colors.dart';
|
||||||
@@ -111,7 +111,7 @@ class _EpgGuideViewState extends ConsumerState<_EpgGuideView>
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
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 (mounted) {
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final data = json.decode(response.body) as Map<String, dynamic>;
|
final data = json.decode(response.body) as Map<String, dynamic>;
|
||||||
@@ -589,7 +589,7 @@ class _ProgrammeCard extends StatelessWidget {
|
|||||||
DateTime end,
|
DateTime end,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
final response = await http.post(
|
final response = await AuthedHttp.post(
|
||||||
Uri.parse('/api/recordings'),
|
Uri.parse('/api/recordings'),
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: json.encode({
|
body: json.encode({
|
||||||
@@ -745,7 +745,7 @@ class _RecordingsListViewState extends State<_RecordingsListView> {
|
|||||||
_error = null;
|
_error = null;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
final response = await http.get(Uri.parse('/api/recordings'));
|
final response = await AuthedHttp.get(Uri.parse('/api/recordings'));
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
setState(() {
|
setState(() {
|
||||||
final decoded = json.decode(response.body);
|
final decoded = json.decode(response.body);
|
||||||
@@ -767,7 +767,7 @@ class _RecordingsListViewState extends State<_RecordingsListView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _stopRecording(String id, String title) async {
|
Future<void> _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();
|
_fetchRecordings();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
@@ -776,7 +776,7 @@ class _RecordingsListViewState extends State<_RecordingsListView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _deleteRecording(String id) async {
|
Future<void> _deleteRecording(String id) async {
|
||||||
await http.delete(Uri.parse('/api/recordings/$id'));
|
await AuthedHttp.delete(Uri.parse('/api/recordings/$id'));
|
||||||
_fetchRecordings();
|
_fetchRecordings();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -817,7 +817,7 @@ class _RecordingsListViewState extends State<_RecordingsListView> {
|
|||||||
|
|
||||||
Future<void> _showLogs(String id, String title) async {
|
Future<void> _showLogs(String id, String title) async {
|
||||||
try {
|
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;
|
if (!mounted) return;
|
||||||
final content = response.statusCode == 200
|
final content = response.statusCode == 200
|
||||||
? (json.decode(response.body)['logs'] as String? ?? 'Aucun log')
|
? (json.decode(response.body)['logs'] as String? ?? 'Aucun log')
|
||||||
@@ -1095,7 +1095,7 @@ class _SeasonPassesViewState extends State<_SeasonPassesView> {
|
|||||||
Future<void> _loadPasses() async {
|
Future<void> _loadPasses() async {
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
try {
|
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) {
|
if (r.statusCode == 200) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_passes = json.decode(r.body);
|
_passes = json.decode(r.body);
|
||||||
@@ -1110,7 +1110,7 @@ class _SeasonPassesViewState extends State<_SeasonPassesView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _deletePass(String id, String title) async {
|
Future<void> _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();
|
_loadPasses();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -1177,7 +1177,7 @@ class _SeasonPassesViewState extends State<_SeasonPassesView> {
|
|||||||
? '/api/live/$c.ts'
|
? '/api/live/$c.ts'
|
||||||
: urlCtrl.text.trim();
|
: urlCtrl.text.trim();
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
final r = await http.post(
|
final r = await AuthedHttp.post(
|
||||||
Uri.parse('/api/season-passes'),
|
Uri.parse('/api/season-passes'),
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: json.encode(
|
body: json.encode(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
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 'package:google_fonts/google_fonts.dart';
|
||||||
import '../../../core/models/iptv_models.dart';
|
import '../../../core/models/iptv_models.dart';
|
||||||
import '../../../core/theme/app_colors.dart';
|
import '../../../core/theme/app_colors.dart';
|
||||||
@@ -31,7 +31,7 @@ class _SimpleRecordingWidgetState extends State<SimpleRecordingWidget> {
|
|||||||
setState(() => _status = 'Starting...');
|
setState(() => _status = 'Starting...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await http.post(
|
final response = await AuthedHttp.post(
|
||||||
Uri.parse('/api/record/now'),
|
Uri.parse('/api/record/now'),
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: jsonEncode({
|
body: jsonEncode({
|
||||||
@@ -66,7 +66,7 @@ class _SimpleRecordingWidgetState extends State<SimpleRecordingWidget> {
|
|||||||
final endTime = _startTime.add(Duration(minutes: _durationMinutes));
|
final endTime = _startTime.add(Duration(minutes: _durationMinutes));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await http.post(
|
final response = await AuthedHttp.post(
|
||||||
Uri.parse('/api/record/schedule'),
|
Uri.parse('/api/record/schedule'),
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: jsonEncode({
|
body: jsonEncode({
|
||||||
@@ -96,7 +96,7 @@ class _SimpleRecordingWidgetState extends State<SimpleRecordingWidget> {
|
|||||||
/// 🔴 Stop recording
|
/// 🔴 Stop recording
|
||||||
Future<void> _stopRecording() async {
|
Future<void> _stopRecording() async {
|
||||||
try {
|
try {
|
||||||
await http.post(
|
await AuthedHttp.post(
|
||||||
Uri.parse('/api/record/stop/${widget.channel.streamId}'),
|
Uri.parse('/api/record/stop/${widget.channel.streamId}'),
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user