aves_mio22/lib/remote/remote_http.dart
2026-04-18 20:05:02 +02:00

151 lines
5.2 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.

// lib/remote/remote_http.dart
import 'dart:convert';
import 'dart:developer' as dev;
import 'package:flutter/services.dart';
import 'remote_settings.dart';
import 'auth_client.dart';
/// Helper HTTP per risorse remote (URL + headers di autenticazione).
class RemoteHttp {
static RemoteAuth? _auth;
static String? _base;
// Cache dell'ultimo set di header (mai null: {} se non autenticato)
static Map<String, String> _cachedHeaders = const {};
static Future<void>? _initFuture; // evita init concorrenti
/// ✅ Inietta l'istanza di RemoteAuth usata dal controller,
/// così API e immagini condividono lo stesso token/cache.
static void attach({required String baseUrl, required RemoteAuth auth}) {
_base = baseUrl.trim().isEmpty ? null : baseUrl.trim();
_auth = auth;
_cachedHeaders = const {};
_initFuture = Future.value(); // init "risolta"
dev.log('[RemoteHttp] attach: base=$_base (shared auth)', name: 'RemoteHttp');
}
/// Inizializza auth e base URL dai settings sicuri (robusto: non lancia).
static Future<void> init() {
_initFuture ??= _doInit();
return _initFuture!;
}
static Future<void> _doInit() async {
try {
final s = await RemoteSettings.load();
final base = s.baseUrl.trim();
_base = base.isEmpty ? null : base;
_auth = RemoteAuth(baseUrl: s.baseUrl, email: s.email, password: s.password);
dev.log('[RemoteHttp] init: base=$_base, email=${s.email.isNotEmpty ? '***' : '(none)'}',
name: 'RemoteHttp');
} catch (e, st) {
dev.log('[RemoteHttp] init ERROR: $e', name: 'RemoteHttp', error: e, stackTrace: st);
_auth = null;
_base = null;
}
}
/// ✅ Se il token JWT è quasi scaduto, forziamo refresh prima di usarlo per le immagini.
static bool _isJwtExpiringSoon(String token, {Duration skew = const Duration(seconds: 60)}) {
try {
final parts = token.split('.');
if (parts.length < 2) return false;
final payload = parts[1];
String normalized = payload.replaceAll('-', '+').replaceAll('_', '/');
while (normalized.length % 4 != 0) {
normalized += '=';
}
final decoded = utf8.decode(base64.decode(normalized));
final map = jsonDecode(decoded);
if (map is! Map) return false;
final exp = map['exp'];
if (exp is! num) return false;
final expMs = (exp.toInt()) * 1000;
final nowMs = DateTime.now().millisecondsSinceEpoch;
return (expMs - nowMs) <= skew.inMilliseconds;
} catch (_) {
return false;
}
}
/// Headers correnti (login ondemand). Non lancia; ritorna {} se non disponibili.
static Future<Map<String, String>> headers() async {
if (_auth == null) {
await init();
if (_auth == null) {
_cachedHeaders = const {};
dev.log('[RemoteHttp] headers: init failed → NO TOKEN', name: 'RemoteHttp');
return _cachedHeaders;
}
}
try {
// ✅ refresh proattivo se token vicino a scadenza
final tok = _auth!.token;
if (tok != null && tok.isNotEmpty && _isJwtExpiringSoon(tok)) {
dev.log('[RemoteHttp] token expiring soon -> refresh', name: 'RemoteHttp');
final h = await _auth!.refreshAndHeaders();
_cachedHeaders = Map<String, String>.from(h);
return _cachedHeaders;
}
final h = await _auth!.authHeaders(); // login on-demand
_cachedHeaders = Map<String, String>.from(h);
if ((_cachedHeaders['Authorization']?.isEmpty ?? true)) {
dev.log('[RemoteHttp] headers: NO Authorization', name: 'RemoteHttp');
} else {
dev.log('[RemoteHttp] headers: Authorization: Bearer ***', name: 'RemoteHttp');
}
return _cachedHeaders;
} on PlatformException catch (e, st) {
dev.log('[RemoteHttp] headers PlatformException: $e → returning {}',
name: 'RemoteHttp', error: e, stackTrace: st);
_cachedHeaders = const {};
return _cachedHeaders;
} catch (e, st) {
dev.log('[RemoteHttp] headers ERROR: $e → returning {}',
name: 'RemoteHttp', error: e, stackTrace: st);
_cachedHeaders = const {};
return _cachedHeaders;
}
}
/// Ultimi header noti (sincrono). Mai null; {} se non autenticato.
static Map<String, String> peekHeaders() => _cachedHeaders;
/// Converte path relativo in assoluto. Se è già http/https lo ritorna comè.
static String absUrl(String? relativePath) {
if (relativePath == null || relativePath.isEmpty) return '';
final lp = relativePath.trim().toLowerCase();
if (lp.startsWith('http://') || lp.startsWith('https://')) {
return relativePath;
}
if (_base == null || _base!.isEmpty) return '';
final b = _base!.endsWith('/') ? _base! : '${_base!}/';
final rel = relativePath.startsWith('/') ? relativePath.substring(1) : relativePath;
return '$b$rel';
}
/// Warm-up: scaldare gli header.
static Future<void> warmUp() async {
await init();
try {
await headers();
} catch (_) {
// già loggato
}
}
/// Rilegge i settings a runtime (es. utente cambia base/email/password).
static Future<void> refreshFromSettings() async {
_initFuture = null;
await init();
_cachedHeaders = const {};
}
}