HTTP-клиент к /api/v1 с Sanctum-токеном (хранится в secure storage), модели зеркалят Laravel API Resources. Экраны: логин, список устройств с живым статусом, детальная страница устройства (история телеметрии, turn-on/turn-off/set-level с проверкой capability), списки зон и правил автоматизации (пока только чтение — формы создания/редактирования впереди).
88 lines
2.6 KiB
Dart
88 lines
2.6 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import 'api_config.dart';
|
|
|
|
/// Thrown for any non-2xx response. `errors` is populated for Laravel's
|
|
/// standard 422 validation shape (`{"message": ..., "errors": {field: [...]}}`).
|
|
class ApiException implements Exception {
|
|
ApiException(this.statusCode, this.message, {this.errors});
|
|
|
|
final int statusCode;
|
|
final String message;
|
|
final Map<String, List<String>>? errors;
|
|
|
|
bool get isUnauthorized => statusCode == 401;
|
|
|
|
@override
|
|
String toString() => message;
|
|
}
|
|
|
|
/// Thin wrapper over `package:http` for the `/api/v1` backend: builds the
|
|
/// URL, attaches the bearer token (set by AuthService after login), decodes
|
|
/// JSON, and turns non-2xx responses into [ApiException].
|
|
class ApiClient {
|
|
String? _token;
|
|
|
|
void setToken(String? token) => _token = token;
|
|
|
|
Uri _uri(String path) => Uri.parse('${ApiConfig.apiV1Url}$path');
|
|
|
|
Map<String, String> get _headers => {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
if (_token != null) 'Authorization': 'Bearer $_token',
|
|
};
|
|
|
|
Future<dynamic> get(String path) async {
|
|
return _handle(await http.get(_uri(path), headers: _headers));
|
|
}
|
|
|
|
Future<dynamic> post(String path, [Map<String, dynamic>? body]) async {
|
|
return _handle(await http.post(
|
|
_uri(path),
|
|
headers: _headers,
|
|
body: body == null ? null : jsonEncode(body),
|
|
));
|
|
}
|
|
|
|
Future<dynamic> put(String path, [Map<String, dynamic>? body]) async {
|
|
return _handle(await http.put(
|
|
_uri(path),
|
|
headers: _headers,
|
|
body: body == null ? null : jsonEncode(body),
|
|
));
|
|
}
|
|
|
|
Future<dynamic> delete(String path) async {
|
|
return _handle(await http.delete(_uri(path), headers: _headers));
|
|
}
|
|
|
|
dynamic _handle(http.Response response) {
|
|
final ok = response.statusCode >= 200 && response.statusCode < 300;
|
|
|
|
if (response.body.isEmpty) {
|
|
if (ok) return null;
|
|
throw ApiException(response.statusCode, 'Ошибка запроса (${response.statusCode})');
|
|
}
|
|
|
|
final decoded = jsonDecode(utf8.decode(response.bodyBytes));
|
|
|
|
if (ok) return decoded;
|
|
|
|
final message = decoded is Map && decoded['message'] is String
|
|
? decoded['message'] as String
|
|
: 'Ошибка запроса (${response.statusCode})';
|
|
|
|
Map<String, List<String>>? errors;
|
|
if (decoded is Map && decoded['errors'] is Map) {
|
|
errors = (decoded['errors'] as Map).map(
|
|
(key, value) => MapEntry(key as String, List<String>.from(value as List)),
|
|
);
|
|
}
|
|
|
|
throw ApiException(response.statusCode, message, errors: errors);
|
|
}
|
|
}
|