Каркас приложения: авторизация, зоны/устройства/правила, управление устройствами
HTTP-клиент к /api/v1 с Sanctum-токеном (хранится в secure storage), модели зеркалят Laravel API Resources. Экраны: логин, список устройств с живым статусом, детальная страница устройства (история телеметрии, turn-on/turn-off/set-level с проверкой capability), списки зон и правил автоматизации (пока только чтение — формы создания/редактирования впереди).
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
import '../models/user.dart';
|
||||
import 'api_client.dart';
|
||||
|
||||
/// Session state + token persistence. The token is a Sanctum personal
|
||||
/// access token, named per-device on the backend (see `/login`'s
|
||||
/// `device_name`) so a user could later revoke one device without others.
|
||||
class AuthService extends ChangeNotifier {
|
||||
AuthService(this._apiClient);
|
||||
|
||||
final ApiClient _apiClient;
|
||||
final _storage = const FlutterSecureStorage();
|
||||
static const _tokenKey = 'auth_token';
|
||||
|
||||
User? _user;
|
||||
bool _isLoading = true; // true until restoreSession() resolves once at startup
|
||||
|
||||
User? get user => _user;
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isAuthenticated => _user != null;
|
||||
|
||||
/// Call once at app startup: restores a saved token and validates it
|
||||
/// against `/me` — a saved token can be stale if it was revoked
|
||||
/// server-side (e.g. logged out from another device's session list).
|
||||
Future<void> restoreSession() async {
|
||||
final token = await _storage.read(key: _tokenKey);
|
||||
if (token == null) {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
_apiClient.setToken(token);
|
||||
try {
|
||||
final json = await _apiClient.get('/me');
|
||||
_user = User.fromJson(json['data'] as Map<String, dynamic>);
|
||||
} on ApiException {
|
||||
await _storage.delete(key: _tokenKey);
|
||||
_apiClient.setToken(null);
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> login(String email, String password) async {
|
||||
final json = await _apiClient.post('/login', {
|
||||
'email': email,
|
||||
'password': password,
|
||||
'device_name': _deviceName(),
|
||||
});
|
||||
|
||||
final token = json['token'] as String;
|
||||
await _storage.write(key: _tokenKey, value: token);
|
||||
_apiClient.setToken(token);
|
||||
_user = User.fromJson(json['user'] as Map<String, dynamic>);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
try {
|
||||
await _apiClient.post('/logout');
|
||||
} on ApiException {
|
||||
// Token may already be invalid server-side (expired/revoked) —
|
||||
// still clear local state so the user isn't stuck logged in.
|
||||
}
|
||||
await _storage.delete(key: _tokenKey);
|
||||
_apiClient.setToken(null);
|
||||
_user = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String _deviceName() {
|
||||
if (kIsWeb) return 'web';
|
||||
if (Platform.isIOS) return 'iOS device';
|
||||
if (Platform.isAndroid) return 'Android device';
|
||||
return Platform.operatingSystem;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user