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
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
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 l’URL n’est 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 l’icô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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user