96 lines
No EOL
3.1 KiB
Dart
96 lines
No EOL
3.1 KiB
Dart
// lib/remote/auth_client.dart
|
||
import 'dart:convert';
|
||
import 'package:http/http.dart' as http;
|
||
|
||
/// Gestisce autenticazione remota e caching del Bearer token.
|
||
/// - [baseUrl]: URL base del server (con o senza '/')
|
||
/// - [email]/[password]: credenziali
|
||
/// - [loginPath]: path dell'endpoint di login (default 'auth/login')
|
||
/// - [timeout]: timeout per le richieste (default 20s)
|
||
class RemoteAuth {
|
||
final Uri base;
|
||
final String email;
|
||
final String password;
|
||
final String loginPath;
|
||
final Duration timeout;
|
||
|
||
String? _token;
|
||
|
||
RemoteAuth({
|
||
required String baseUrl,
|
||
required this.email,
|
||
required this.password,
|
||
this.loginPath = 'auth/login',
|
||
this.timeout = const Duration(seconds: 20),
|
||
}) : base = Uri.parse(baseUrl.endsWith('/') ? baseUrl : '$baseUrl/');
|
||
|
||
Uri get _loginUri => base.resolve(loginPath);
|
||
|
||
/// Esegue il login e memorizza il token.
|
||
/// Lancia eccezione con messaggio chiaro in caso di errore HTTP, rete o JSON.
|
||
Future<String> login() async {
|
||
final uri = _loginUri;
|
||
final headers = {'Content-Type': 'application/json'};
|
||
final bodyStr = json.encode({'email': email, 'password': password});
|
||
|
||
http.Response res;
|
||
try {
|
||
res = await http
|
||
.post(uri, headers: headers, body: bodyStr)
|
||
.timeout(timeout);
|
||
} catch (e) {
|
||
throw Exception('Login fallito: errore di rete verso $uri: $e');
|
||
}
|
||
|
||
// Follow esplicito per redirect POST moderni (307/308) mantenendo metodo e body
|
||
if ({307, 308}.contains(res.statusCode) && res.headers['location'] != null) {
|
||
final redirectUri = uri.resolve(res.headers['location']!);
|
||
try {
|
||
res = await http
|
||
.post(redirectUri, headers: headers, body: bodyStr)
|
||
.timeout(timeout);
|
||
} catch (e) {
|
||
throw Exception('Login fallito: errore di rete verso $redirectUri: $e');
|
||
}
|
||
}
|
||
|
||
if (res.statusCode != 200) {
|
||
final snippet = utf8.decode(res.bodyBytes.take(200).toList());
|
||
throw Exception(
|
||
'Login fallito: HTTP ${res.statusCode} ${res.reasonPhrase} – $snippet',
|
||
);
|
||
}
|
||
|
||
// Parsing JSON robusto
|
||
Map<String, dynamic> map;
|
||
try {
|
||
map = json.decode(utf8.decode(res.bodyBytes)) as Map<String, dynamic>;
|
||
} catch (_) {
|
||
throw Exception('Login fallito: risposta non è un JSON valido');
|
||
}
|
||
|
||
// Supporto sia 'token' sia 'access_token'
|
||
final token = (map['token'] ?? map['access_token']) as String?;
|
||
if (token == null || token.isEmpty) {
|
||
throw Exception('Login fallito: token assente nella risposta');
|
||
}
|
||
|
||
_token = token;
|
||
return token;
|
||
}
|
||
|
||
/// Ritorna gli header con Bearer; se non hai token, esegue login.
|
||
Future<Map<String, String>> authHeaders() async {
|
||
_token ??= await login();
|
||
return {'Authorization': 'Bearer $_token'};
|
||
}
|
||
|
||
/// Forza il rinnovo del token (es. dopo 401) e ritorna i nuovi header.
|
||
Future<Map<String, String>> refreshAndHeaders() async {
|
||
_token = null;
|
||
return await authHeaders();
|
||
}
|
||
|
||
/// Accesso in sola lettura al token corrente (può essere null).
|
||
String? get token => _token;
|
||
} |