feat: secure home credentials

This commit is contained in:
Artem Kokos
2026-04-22 23:25:48 +07:00
parent 6a961209cc
commit 7c0a2675c6
22 changed files with 1782 additions and 397 deletions

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/home_config.dart';
import '../providers/providers.dart';
import '../services/api_client.dart';
/// Экран создания или редактирования "дома" (сервера Ignis).
class HomeEditScreen extends ConsumerStatefulWidget {
@@ -34,7 +35,6 @@ class _HomeEditScreenState extends ConsumerState<HomeEditScreen> {
if (_isEdit) {
_nameCtrl.text = widget.home!.name;
_urlCtrl.text = widget.home!.url;
_keyCtrl.text = widget.home!.apiKey;
if (widget.home!.latitude != null) {
_latCtrl.text = widget.home!.latitude.toString();
}
@@ -42,6 +42,7 @@ class _HomeEditScreenState extends ConsumerState<HomeEditScreen> {
_lonCtrl.text = widget.home!.longitude.toString();
}
_geofenceEnabled = widget.home!.geofenceEnabled;
_loadApiKey();
}
// Следим за полями координат чтобы обновлять доступность Switch
@@ -49,6 +50,15 @@ class _HomeEditScreenState extends ConsumerState<HomeEditScreen> {
_lonCtrl.addListener(_onCoordsChanged);
}
Future<void> _loadApiKey() async {
final apiKey = await ref
.read(settingsServiceProvider)
.getHomeApiKey(widget.home!.id);
if (mounted && apiKey != null) {
_keyCtrl.text = apiKey;
}
}
void _onCoordsChanged() {
// Если координаты очистили -- выключаем геофенс
if (!_hasCoordinates && _geofenceEnabled) {
@@ -73,9 +83,7 @@ class _HomeEditScreenState extends ConsumerState<HomeEditScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_isEdit ? 'РЕДАКТИРОВАТЬ ДОМ' : 'НОВЫЙ ДОМ'),
),
appBar: AppBar(title: Text(_isEdit ? 'РЕДАКТИРОВАТЬ ДОМ' : 'НОВЫЙ ДОМ')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
@@ -136,7 +144,9 @@ class _HomeEditScreenState extends ConsumerState<HomeEditScreen> {
hintText: '51.128',
),
keyboardType: const TextInputType.numberWithOptions(
decimal: true, signed: true),
decimal: true,
signed: true,
),
),
),
const SizedBox(width: 12),
@@ -149,7 +159,9 @@ class _HomeEditScreenState extends ConsumerState<HomeEditScreen> {
hintText: '71.430',
),
keyboardType: const TextInputType.numberWithOptions(
decimal: true, signed: true),
decimal: true,
signed: true,
),
),
),
],
@@ -224,20 +236,41 @@ class _HomeEditScreenState extends ConsumerState<HomeEditScreen> {
Future<void> _save() async {
final name = _nameCtrl.text.trim();
final url = _urlCtrl.text.trim();
final rawUrl = _urlCtrl.text.trim();
final key = _keyCtrl.text.trim();
final latText = _latCtrl.text.trim();
final lonText = _lonCtrl.text.trim();
if (name.isEmpty || url.isEmpty || key.isEmpty) {
if (name.isEmpty || rawUrl.isEmpty || key.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Заполните все обязательные поля')),
);
return;
}
late final String url;
try {
url = IgnisApi.normalizeBaseUrl(rawUrl);
final parsed = Uri.parse(url);
if ((parsed.scheme != 'http' && parsed.scheme != 'https') ||
parsed.host.isEmpty) {
throw const FormatException();
}
} catch (_) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Некорректный адрес сервера')),
);
return;
}
double? lat;
double? lon;
if (latText.isEmpty != lonText.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Введите обе координаты')));
return;
}
if (latText.isNotEmpty && lonText.isNotEmpty) {
lat = double.tryParse(latText);
lon = double.tryParse(lonText);
@@ -247,6 +280,12 @@ class _HomeEditScreenState extends ConsumerState<HomeEditScreen> {
);
return;
}
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Координаты вне допустимого диапазона')),
);
return;
}
}
setState(() => _saving = true);
@@ -257,7 +296,6 @@ class _HomeEditScreenState extends ConsumerState<HomeEditScreen> {
? widget.home!.copyWith(
name: name,
url: url,
apiKey: key,
latitude: lat,
longitude: lon,
geofenceEnabled: clearCoords ? false : _geofenceEnabled,
@@ -267,22 +305,38 @@ class _HomeEditScreenState extends ConsumerState<HomeEditScreen> {
id: DateTime.now().millisecondsSinceEpoch.toString(),
name: name,
url: url,
apiKey: key,
latitude: lat,
longitude: lon,
geofenceEnabled: _geofenceEnabled,
);
if (_isEdit) {
await ref.read(homesProvider.notifier).update(home);
} else {
await ref.read(homesProvider.notifier).add(home);
try {
await ref.read(apiProvider).validateCredentials(url, key);
if (_isEdit) {
await ref.read(homesProvider.notifier).update(home, apiKey: key);
} else {
await ref.read(homesProvider.notifier).add(home, apiKey: key);
}
final currentHome = ref.read(currentHomeProvider);
if (currentHome?.id == home.id) {
await ref.read(currentHomeProvider.notifier).switchTo(home);
}
// Синхронизировать фоновый таск с новыми настройками
final allHomes = ref.read(homesProvider);
await syncGeofenceTask(allHomes);
if (mounted) Navigator.of(context).pop();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Не удалось проверить дом: $e')));
}
} finally {
if (mounted) setState(() => _saving = false);
}
// Синхронизировать фоновый таск с новыми настройками
final allHomes = ref.read(homesProvider);
await syncGeofenceTask(allHomes);
if (mounted) Navigator.of(context).pop();
}
}