petitspas/frontend/lib/widgets/common/auth_network_image.dart
Julien Martin 3bfdadd301 fix(front): images validation dossier, traces debug, baseUrl via Env
- ApiConfig.baseUrl dérivé de Env.apiBaseUrl (alignement API / origine médias).
- AuthNetworkImage : uploads publics sans Bearer (CORS web) ; log erreurs en debug.
- debugPrint kDebugMode : GET dossier, EnfantDossier.fromJson, absoluteMediaUrl, carte enfant.
- ParentDossier id depuis user_id ; fallbacks présentation / photo.

Made-with: Cursor
2026-04-11 17:08:34 +02:00

131 lines
3.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:p_tits_pas/services/api/tokenService.dart';
/// [Image.network] avec en-tête `Authorization` uniquement si lURL nest pas un fichier statique public.
///
/// Les chemins `/uploads/...` sont servis sans auth (voir back + Traefik) : ne pas envoyer de Bearer,
/// sinon en **cross-origin** (ex. admin Flutter web en local, API en prod) le navigateur peut bloquer
/// la requête (CORS) et afficher licône « image cassée ».
class AuthNetworkImage extends StatefulWidget {
const AuthNetworkImage({
super.key,
required this.url,
this.width,
this.height,
this.fit = BoxFit.cover,
this.loadingBuilder,
this.errorBuilder,
});
final String url;
final double? width;
final double? height;
final BoxFit fit;
final ImageLoadingBuilder? loadingBuilder;
final ImageErrorWidgetBuilder? errorBuilder;
static bool isPublicUploadUrl(String url) {
final u = url.toLowerCase();
return u.contains('/uploads/');
}
@override
State<AuthNetworkImage> createState() => _AuthNetworkImageState();
}
class _AuthNetworkImageState extends State<AuthNetworkImage> {
late final Future<Map<String, String>?> _headersFuture;
@override
void initState() {
super.initState();
final isPublic = AuthNetworkImage.isPublicUploadUrl(widget.url);
_headersFuture =
isPublic ? Future<Map<String, String>?>.value(null) : _loadHeaders();
if (kDebugMode) {
debugPrint(
'[PetitsPas/image] préparation chargement url=${widget.url} | '
'uploadPublic=$isPublic | avecBearer=${!isPublic}',
);
}
}
static Future<Map<String, String>?> _loadHeaders() async {
final t = await TokenService.getToken();
if (t == null || t.isEmpty) return null;
return {'Authorization': 'Bearer $t'};
}
ImageErrorWidgetBuilder _wrapErrorBuilder() {
return (BuildContext context, Object error, StackTrace? stackTrace) {
if (kDebugMode) {
debugPrint(
'[PetitsPas/image] ❌ échec chargement url=${widget.url} | erreur=$error',
);
}
final inner = widget.errorBuilder;
if (inner != null) {
return inner(context, error, stackTrace);
}
return ColoredBox(
color: Colors.grey.shade200,
child: Center(
child: Icon(Icons.broken_image_outlined, color: Colors.grey.shade500),
),
);
};
}
@override
Widget build(BuildContext context) {
final err = _wrapErrorBuilder();
if (AuthNetworkImage.isPublicUploadUrl(widget.url)) {
return Image.network(
widget.url,
width: widget.width,
height: widget.height,
fit: widget.fit,
loadingBuilder: widget.loadingBuilder,
errorBuilder: err,
);
}
return FutureBuilder<Map<String, String>?>(
future: _headersFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return SizedBox(
width: widget.width,
height: widget.height,
child: ColoredBox(
color: Colors.grey.shade200,
child: const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
),
);
}
final headers = snapshot.data;
if (kDebugMode && headers != null) {
debugPrint('[PetitsPas/image] requête avec en-tête Authorization (Bearer présent)');
}
return Image.network(
widget.url,
width: widget.width,
height: widget.height,
fit: widget.fit,
headers: headers,
loadingBuilder: widget.loadingBuilder,
errorBuilder: err,
);
},
);
}
}