151 lines
5.2 KiB
Dart
151 lines
5.2 KiB
Dart
// 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 on‑demand). 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 {};
|
||
}
|
||
}
|