Каркас приложения: авторизация, зоны/устройства/правила, управление устройствами
HTTP-клиент к /api/v1 с Sanctum-токеном (хранится в secure storage), модели зеркалят Laravel API Resources. Экраны: логин, список устройств с живым статусом, детальная страница устройства (история телеметрии, turn-on/turn-off/set-level с проверкой capability), списки зон и правил автоматизации (пока только чтение — формы создания/редактирования впереди).
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user