feat(ui): support authenticated image loading with cache manager
Add Riverpod-aware image header and cache manager support for network images used in avatar and markdown widgets. Convert AvatarImage to a ConsumerWidget and read a self-signed cache manager and HTTP headers from Riverpod so CachedNetworkImage can send auth/custom headers and use the provided cache manager. In Markdown image builder, obtain headers and cache manager from a ProviderContainer (via ProviderScope.containerOf) to enable the same authenticated loading in non-consumer contexts. Introduce image_header_utils.dart to centralize building Authorization and custom headers from auth/api providers, with helpers for Ref, WidgetRef, and ProviderContainer. Add dependency adjustments in pubspec.lock for flutter_cache_manager and http marked as direct main. These changes ensure protected images (self-signed or auth-required) load correctly across the app and reuse the configured cache manager.
This commit is contained in:
47
lib/core/network/image_header_utils.dart
Normal file
47
lib/core/network/image_header_utils.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:conduit/core/providers/app_providers.dart';
|
||||
import 'package:conduit/features/auth/providers/unified_auth_providers.dart';
|
||||
|
||||
/// Builds HTTP headers for protected image requests.
|
||||
///
|
||||
/// Includes Authorization (Bearer token or API key) and any server-configured
|
||||
/// custom headers. Returns `null` if no headers are needed.
|
||||
Map<String, String>? buildImageHeadersFromRef(Ref ref) {
|
||||
final api = ref.read(apiServiceProvider);
|
||||
final token = ref.read(authTokenProvider3);
|
||||
return _build(api, token);
|
||||
}
|
||||
|
||||
Map<String, String>? buildImageHeadersFromWidgetRef(WidgetRef ref) {
|
||||
final api = ref.read(apiServiceProvider);
|
||||
final token = ref.read(authTokenProvider3);
|
||||
return _build(api, token);
|
||||
}
|
||||
|
||||
/// Same as [buildImageHeadersFromRef] but using a [ProviderContainer], useful
|
||||
/// when you don't have a `Ref` (e.g., in non-Consumer widgets/utilities).
|
||||
Map<String, String>? buildImageHeadersFromContainer(
|
||||
ProviderContainer container,
|
||||
) {
|
||||
final api = container.read(apiServiceProvider);
|
||||
final token = container.read(authTokenProvider3);
|
||||
return _build(api, token);
|
||||
}
|
||||
|
||||
Map<String, String>? _build(dynamic api, String? token) {
|
||||
final headers = <String, String>{};
|
||||
|
||||
if (token != null && token.isNotEmpty) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
} else if (api?.serverConfig.apiKey != null &&
|
||||
api!.serverConfig.apiKey!.isNotEmpty) {
|
||||
headers['Authorization'] = 'Bearer ${api.serverConfig.apiKey}';
|
||||
}
|
||||
|
||||
if (api != null && api.serverConfig.customHeaders.isNotEmpty) {
|
||||
headers.addAll(api.serverConfig.customHeaders);
|
||||
}
|
||||
|
||||
return headers.isEmpty ? null : headers;
|
||||
}
|
||||
66
lib/core/network/self_signed_image_cache_manager.dart
Normal file
66
lib/core/network/self_signed_image_cache_manager.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/io_client.dart';
|
||||
|
||||
import '../models/server_config.dart';
|
||||
import '../providers/app_providers.dart';
|
||||
|
||||
/// Returns a CacheManager that accepts a self-signed certificate for the
|
||||
/// currently active server's host/port. Returns null when not needed.
|
||||
///
|
||||
/// Notes
|
||||
/// - Scoped to the configured host and (optionally) port only.
|
||||
/// - Not available on web (browsers enforce TLS validation).
|
||||
final selfSignedImageCacheManagerProvider = Provider<CacheManager?>((ref) {
|
||||
final active = ref.watch(activeServerProvider);
|
||||
|
||||
return active.maybeWhen(
|
||||
data: (server) {
|
||||
if (server == null) return null;
|
||||
return _buildForServer(server);
|
||||
},
|
||||
orElse: () => null,
|
||||
);
|
||||
});
|
||||
|
||||
CacheManager? _buildForServer(ServerConfig server) {
|
||||
if (kIsWeb) return null;
|
||||
if (!server.allowSelfSignedCertificates) return null;
|
||||
|
||||
final uri = _parseUri(server.url);
|
||||
if (uri == null) return null;
|
||||
|
||||
// Configure a HttpClient that accepts only this host (+ optional port).
|
||||
final client = HttpClient();
|
||||
final host = uri.host.toLowerCase();
|
||||
final port = uri.hasPort ? uri.port : null;
|
||||
|
||||
client.badCertificateCallback =
|
||||
(X509Certificate cert, String requestHost, int requestPort) {
|
||||
if (requestHost.toLowerCase() != host) return false;
|
||||
if (port == null) return true; // Any port on this host
|
||||
return requestPort == port; // Exact host+port only
|
||||
};
|
||||
|
||||
final ioClient = IOClient(client);
|
||||
final fileService = HttpFileService(httpClient: ioClient);
|
||||
|
||||
// Use a stable key per host/port to share cache across widgets.
|
||||
final key = 'conduit-selfsigned-$host:${port ?? 0}';
|
||||
return CacheManager(Config(key, fileService: fileService));
|
||||
}
|
||||
|
||||
Uri? _parseUri(String url) {
|
||||
final trimmed = url.trim();
|
||||
if (trimmed.isEmpty) return null;
|
||||
Uri? parsed = Uri.tryParse(trimmed);
|
||||
if (parsed == null) return null;
|
||||
if (!parsed.hasScheme) {
|
||||
parsed =
|
||||
Uri.tryParse('https://$trimmed') ?? Uri.tryParse('http://$trimmed');
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
Reference in New Issue
Block a user