Added create and remove static lease and restore leases

This commit is contained in:
Juan Gilsanz Polo 2022-10-15 02:25:15 +02:00
parent b48c2a8d3b
commit 8f8ef05a3f
9 changed files with 724 additions and 20 deletions

View file

@ -355,5 +355,25 @@
"settingsNotSaved": "Settings couldn't be saved",
"restoringConfig": "Restoring configuration...",
"configRestored": "Configuration restored successfully",
"configNotRestored": "The configuration couldn't be restored"
"configNotRestored": "The configuration couldn't be restored",
"dhcpStatic": "DHCP static leases",
"noDhcpStaticLeases": "No DHCP static leases found",
"deleting": "Deleting...",
"staticLeaseDeleted": "DHCP static lease deleted successfully",
"staticLeaseNotDeleted": "The DHCP static lease could not be deleted",
"deleteStaticLease": "Delete static lease",
"deleteStaticLeaseDescription": "The DHCP static lease will be deleted. This action cannot be reverted.",
"addStaticLease": "Add static lease",
"macAddress": "MAC address",
"macAddressNotValid": "MAC address not valid",
"hostName": "Host name",
"hostNameError": "Host name cannot be empty",
"creating": "Creating...",
"staticLeaseCreated": "DHCP static lease created successfully",
"staticLeaseNotCreated": "The DHCP static lease couldn't be created",
"staticLeaseExists": "The DHCP static lease already exists",
"restoreLeases": "Restore leases",
"restoringLeases": "Restoring leases...",
"leasesRestored": "Leases restored successfully",
"leasesNotRestored": "The leases couldn't be restored"
}

View file

@ -355,5 +355,25 @@
"settingsNotSaved": "No se ha podido guardar la configuración",
"restoringConfig": "Restaurando configuración...",
"configRestored": "Configuración restaurada correctamente",
"configNotRestored": "La configuración no ha podido ser restaurada"
"configNotRestored": "La configuración no ha podido ser restaurada",
"dhcpStatic": "Asignaciones DHCP estáticas",
"noDhcpStaticLeases": "No se han encontrado asignaciones DHCP estáticas",
"deleting": "Eliminando...",
"staticLeaseDeleted": "Asignación DHCP estática eliminada correctamente",
"staticLeaseNotDeleted": "La asignación DHCP estática no pudo ser eliminada",
"deleteStaticLease": "Eliminar asignación estática",
"deleteStaticLeaseDescription": "La asignación DHCP estática será eliminada. Esta acción no puede ser revertida.",
"addStaticLease": "Añadir asignación estática",
"macAddress": "Dirección MAC",
"macAddressNotValid": "Dirección MAC no válida",
"hostName": "Nombre del host",
"hostNameError": "Nombre del host no puede estar vacío",
"creating": "Creando...",
"staticLeaseCreated": "Asignación DHCP estática creada correctamente",
"staticLeaseNotCreated": "No se ha podido crear la asignación DHCP estática",
"staticLeaseExists": "La asignación DHCP estática ya existe",
"restoreLeases": "Restaurar asignaciones",
"restoringLeases": "Restaurando asignaciones...",
"leasesRestored": "Asignaciones restauradas correctamente",
"leasesNotRestored": "Las asignaciones no pudieron ser restauradas"
}

View file

@ -68,8 +68,8 @@ class DhcpStatus {
String interfaceName;
IpVersion v4;
IpVersion v6;
List<dynamic> leases;
List<dynamic> staticLeases;
List<Lease> leases;
List<Lease> staticLeases;
bool enabled;
DhcpStatus({
@ -85,8 +85,8 @@ class DhcpStatus {
interfaceName: json["interface_name"],
v4: IpVersion.fromJson(json["v4"]),
v6: IpVersion.fromJson(json["v6"]),
leases: List<dynamic>.from(json["leases"].map((x) => x)),
staticLeases: List<dynamic>.from(json["static_leases"].map((x) => x)),
leases: List<Lease>.from(json["leases"].map((x) => Lease.fromJson(x))),
staticLeases: List<Lease>.from(json["static_leases"].map((x) => Lease.fromJson(x))),
enabled: json["enabled"],
);
@ -94,8 +94,8 @@ class DhcpStatus {
"interface_name": interfaceName,
"v4": v4.toJson(),
"v6": v6.toJson(),
"leases": List<dynamic>.from(leases.map((x) => x)),
"static_leases": List<dynamic>.from(staticLeases.map((x) => x)),
"leases": List<Lease>.from(leases.map((x) => x)),
"static_leases": List<Lease>.from(staticLeases.map((x) => x)),
"enabled": enabled,
};
}
@ -131,3 +131,27 @@ class IpVersion {
"lease_duration": leaseDuration,
};
}
class Lease {
final String mac;
final String hostname;
final String ip;
Lease({
required this.mac,
required this.hostname,
required this.ip,
});
factory Lease.fromJson(Map<String, dynamic> json) => Lease(
mac: json["mac"],
hostname: json["hostname"],
ip: json["ip"],
);
Map<String, dynamic> toJson() => {
"mac": mac,
"hostname": hostname,
"ip": ip,
};
}

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';
import 'package:adguard_home_manager/models/filtering.dart';
import 'package:adguard_home_manager/models/dhcp.dart';
import 'package:adguard_home_manager/models/filtering_status.dart';
import 'package:adguard_home_manager/models/clients_allowed_blocked.dart';
import 'package:adguard_home_manager/models/clients.dart';
@ -31,6 +32,11 @@ class ServersProvider with ChangeNotifier {
data: null
);
final DhcpModel _dhcp = DhcpModel(
loadStatus: 0, // 0 = loading, 1 = loaded, 2 = error
data: null
);
FilteringStatus? _filteringStatus;
List<Server> get serversList {
@ -61,6 +67,10 @@ class ServersProvider with ChangeNotifier {
return _filtering;
}
DhcpModel get dhcp {
return _dhcp;
}
void setDbInstance(Database db) {
_dbInstance = db;
}
@ -135,6 +145,18 @@ class ServersProvider with ChangeNotifier {
notifyListeners();
}
void setDhcpData(DhcpData data) {
_dhcp.data = data;
notifyListeners();
}
void setDhcpLoadStatus(int status, bool notify) {
_dhcp.loadStatus = status;
if (notify == true) {
notifyListeners();
}
}
Future<bool> createServer(Server server) async {
final saved = await saveServerIntoDb(server);
if (saved == true) {

View file

@ -0,0 +1,218 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/models/dhcp.dart';
class AddStaticLeaseModal extends StatefulWidget {
final void Function(Lease) onConfirm;
const AddStaticLeaseModal({
Key? key,
required this.onConfirm,
}) : super(key: key);
@override
State<AddStaticLeaseModal> createState() => _AddStaticLeaseModalState();
}
class _AddStaticLeaseModalState extends State<AddStaticLeaseModal> {
final TextEditingController macController = TextEditingController();
String? macError;
final TextEditingController ipController = TextEditingController();
String? ipError;
final TextEditingController hostNameController = TextEditingController();
String? hostNameError;
bool validData = false;
void validateMac(String value) {
final RegExp macRegex = RegExp(r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$');
if (macRegex.hasMatch(value)) {
setState(() => macError = null);
}
else {
setState(() => macError = AppLocalizations.of(context)!.macAddressNotValid);
}
validateData();
}
void validateIp(String value) {
RegExp ipAddress = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$');
if (ipAddress.hasMatch(value) == true) {
setState(() => ipError = null);
}
else {
setState(() => ipError = AppLocalizations.of(context)!.ipNotValid);
}
validateData();
}
void validateData() {
if (
macController.text != '' &&
macError == null &&
ipController.text != '' &&
ipError == null &&
hostNameController.text != '' &&
hostNameError == null
) {
setState(() => validData = true);
}
else {
setState(() => validData = false);
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: MediaQuery.of(context).viewInsets,
child: Container(
height: MediaQuery.of(context).viewInsets.bottom > 0
? MediaQuery.of(context).size.height-MediaQuery.of(context).viewInsets.bottom-25
: 550,
decoration: BoxDecoration(
color: Theme.of(context).dialogBackgroundColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
)
),
child: Column(
children: [
SizedBox(
height: MediaQuery.of(context).viewInsets.bottom > 0
? MediaQuery.of(context).size.height-425
: MediaQuery.of(context).size.height-400,
child: ListView(
shrinkWrap: true,
primary: false,
children: [
const Padding(
padding: EdgeInsets.only(top: 28),
child: Icon(
Icons.add,
size: 26,
),
),
const SizedBox(height: 20),
Text(
AppLocalizations.of(context)!.addStaticLease,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 24
),
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: TextFormField(
controller: macController,
onChanged: validateMac,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.smartphone_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
errorText: macError,
labelText: AppLocalizations.of(context)!.macAddress,
),
),
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: TextFormField(
controller: ipController,
onChanged: validateIp,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.link_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
errorText: ipError,
labelText: AppLocalizations.of(context)!.ipAddress,
),
),
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: TextFormField(
controller: hostNameController,
onChanged: (value) {
if (value != '') {
setState(() => hostNameError = null);
}
else {
setState(() => hostNameError = AppLocalizations.of(context)!.hostNameError);
}
validateData();
},
decoration: InputDecoration(
prefixIcon: const Icon(Icons.badge_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
errorText: hostNameError,
labelText: AppLocalizations.of(context)!.hostName,
),
),
),
],
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(AppLocalizations.of(context)!.cancel),
),
const SizedBox(width: 20),
TextButton(
onPressed: validData == true
? () {
Navigator.pop(context);
widget.onConfirm(
Lease(
mac: macController.text,
hostname: hostNameController.text,
ip: ipController.text
)
);
}
: null,
child: Text(
AppLocalizations.of(context)!.confirm,
style: TextStyle(
color: validData == true
? Theme.of(context).primaryColor
: Colors.grey
),
),
),
],
),
)
],
),
)
],
),
),
);
}
}

View file

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class DeleteStaticLeaseModal extends StatelessWidget {
final void Function() onConfirm;
const DeleteStaticLeaseModal({
Key? key,
required this.onConfirm
}) : super(key: key);
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Column(
children: [
const Icon(
Icons.delete_rounded,
size: 26,
),
const SizedBox(height: 20),
Text(
AppLocalizations.of(context)!.deleteStaticLease,
textAlign: TextAlign.center,
)
],
),
content: Text(AppLocalizations.of(context)!.deleteStaticLeaseDescription),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(AppLocalizations.of(context)!.cancel)
),
TextButton(
onPressed: () {
onConfirm();
Navigator.pop(context);
},
child: Text(AppLocalizations.of(context)!.confirm)
),
],
);
}
}

View file

@ -6,6 +6,7 @@ import 'package:bottom_sheet/bottom_sheet.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/settings/section_label.dart';
import 'package:adguard_home_manager/screens/settings/dhcp/dhcp_static.dart';
import 'package:adguard_home_manager/screens/settings/dhcp/select_interface_modal.dart';
import 'package:adguard_home_manager/functions/snackbar.dart';
@ -45,8 +46,6 @@ class DhcpWidget extends StatefulWidget {
}
class _DhcpWidgetState extends State<DhcpWidget> {
DhcpModel dhcp = DhcpModel(loadStatus: 0);
NetworkInterface? selectedInterface;
bool enabled = false;
@ -72,14 +71,15 @@ class _DhcpWidgetState extends State<DhcpWidget> {
bool dataValid = false;
void loadDhcpStatus() async {
widget.serversProvider.setDhcpLoadStatus(0, false);
final result = await getDhcpData(server: widget.serversProvider.selectedServer!);
if (mounted) {
if (result['result'] == 'success') {
widget.serversProvider.setDhcpLoadStatus(1, true);
widget.serversProvider.setDhcpData(result['data']);
setState(() {
dhcp.loadStatus = 1;
dhcp.data = result['data'];
if (result['data'].dhcpStatus.interfaceName != '') {
selectedInterface = result['data'].networkInterfaces.firstWhere((interface) => interface.name == result['data'].dhcpStatus.interfaceName);
@ -94,7 +94,7 @@ class _DhcpWidgetState extends State<DhcpWidget> {
});
}
else {
setState(() => dhcp.loadStatus = 2);
widget.serversProvider.setDhcpLoadStatus(2, true);
}
}
checkDataValid();
@ -287,6 +287,41 @@ class _DhcpWidgetState extends State<DhcpWidget> {
});
}
void restoreLeases() async {
Future.delayed(const Duration(seconds: 0), () async {
ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.restoringLeases);
final result = await restoreAllLeases(server: serversProvider.selectedServer!);
processModal.close();
if (result['result'] == 'success') {
DhcpData data = serversProvider.dhcp.data!;
data.dhcpStatus.staticLeases = [];
data.dhcpStatus.leases = [];
serversProvider.setDhcpData(data);
showSnacbkar(
context: context,
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.leasesRestored,
color: Colors.green
);
}
else {
appConfigProvider.addLog(result['log']);
showSnacbkar(
context: context,
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.leasesNotRestored,
color: Colors.red
);
}
});
}
void selectInterface() {
Future.delayed(const Duration(seconds: 0), () {
showFlexibleBottomSheet(
@ -298,7 +333,7 @@ class _DhcpWidgetState extends State<DhcpWidget> {
anchors: [0.95],
context: context,
builder: (ctx, controller, offset) => SelectInterfaceModal(
interfaces: dhcp.data!.networkInterfaces,
interfaces: serversProvider.dhcp.data!.networkInterfaces,
scrollController: controller,
onSelect: (interface) => setState(() {
clearAll();
@ -311,7 +346,7 @@ class _DhcpWidgetState extends State<DhcpWidget> {
}
Widget generateBody() {
switch (dhcp.loadStatus) {
switch (serversProvider.dhcp.loadStatus) {
case 0:
return SizedBox(
width: double.maxFinite,
@ -564,7 +599,36 @@ class _DhcpWidgetState extends State<DhcpWidget> {
keyboardType: TextInputType.number,
),
),
]
],
const SizedBox(height: 20),
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) => DhcpStatic(
items: serversProvider.dhcp.data!.dhcpStatus.staticLeases
)
));
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppLocalizations.of(context)!.dhcpStatic,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16
),
),
const Icon(Icons.arrow_forward_rounded)
],
),
),
),
),
],
);
}
@ -589,7 +653,7 @@ class _DhcpWidgetState extends State<DhcpWidget> {
ElevatedButton(
onPressed: selectInterface,
child: Text(AppLocalizations.of(context)!.selectInterface)
)
),
],
);
}
@ -647,6 +711,16 @@ class _DhcpWidgetState extends State<DhcpWidget> {
],
)
),
PopupMenuItem(
onTap: restoreLeases,
child: Row(
children: [
const Icon(Icons.settings_backup_restore_rounded),
const SizedBox(width: 10),
Text(AppLocalizations.of(context)!.restoreLeases)
],
)
),
PopupMenuItem(
onTap: restoreConfig,
child: Row(
@ -656,7 +730,7 @@ class _DhcpWidgetState extends State<DhcpWidget> {
Text(AppLocalizations.of(context)!.restoreConfiguration)
],
)
)
),
]
),
const SizedBox(width: 10)

View file

@ -0,0 +1,168 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:animations/animations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/settings/dhcp/delete_static_lease_modal.dart';
import 'package:adguard_home_manager/screens/settings/dhcp/add_static_lease_modal.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/functions/snackbar.dart';
import 'package:adguard_home_manager/services/http_requests.dart';
import 'package:adguard_home_manager/classes/process_modal.dart';
import 'package:adguard_home_manager/models/dhcp.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
class DhcpStatic extends StatelessWidget {
final List<Lease> items;
const DhcpStatic({
Key? key,
required this.items,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
void deleteLease(Lease lease) async {
ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.deleting);
final result = await deleteStaticLease(server: serversProvider.selectedServer!, data: {
"mac": lease.mac,
"ip": lease.ip,
"hostname": lease.hostname
});
processModal.close();
if (result['result'] == 'success') {
DhcpData data = serversProvider.dhcp.data!;
data.dhcpStatus.staticLeases = data.dhcpStatus.staticLeases.where((l) => l.mac != lease.mac).toList();
serversProvider.setDhcpData(data);
showSnacbkar(
context: context,
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.staticLeaseDeleted,
color: Colors.green
);
}
else {
appConfigProvider.addLog(result['log']);
showSnacbkar(
context: context,
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.staticLeaseNotDeleted,
color: Colors.red
);
}
}
void createLease(Lease lease) async {
ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.creating);
final result = await createStaticLease(server: serversProvider.selectedServer!, data: {
"mac": lease.mac,
"ip": lease.ip,
"hostname": lease.hostname,
});
processModal.close();
if (result['result'] == 'success') {
DhcpData data = serversProvider.dhcp.data!;
data.dhcpStatus.staticLeases.add(lease);
serversProvider.setDhcpData(data);
showSnacbkar(
context: context,
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.staticLeaseCreated,
color: Colors.green
);
}
else if (result['result'] == 'error' && result['message'] == 'already_exists' ) {
appConfigProvider.addLog(result['log']);
showSnacbkar(
context: context,
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.staticLeaseExists,
color: Colors.red
);
}
else {
appConfigProvider.addLog(result['log']);
showSnacbkar(
context: context,
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.staticLeaseNotCreated,
color: Colors.red
);
}
}
void openAddStaticLease() {
showModalBottomSheet(
context: context,
builder: (context) => AddStaticLeaseModal(
onConfirm: createLease
),
backgroundColor: Colors.transparent,
isScrollControlled: true
);
}
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.dhcpStatic),
),
body: items.isNotEmpty
? ListView.builder(
padding: const EdgeInsets.only(top: 0),
itemCount: items.length,
itemBuilder: (context, index) => ListTile(
isThreeLine: true,
title: Text(items[index].ip),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(items[index].mac),
Text(items[index].hostname),
],
),
trailing: IconButton(
onPressed: () {
showModal(
context: context,
builder: (context) => DeleteStaticLeaseModal(
onConfirm: () => deleteLease(items[index])
)
);
},
icon: const Icon(Icons.delete)
),
),
)
: Center(
child: Text(
AppLocalizations.of(context)!.noDhcpStaticLeases,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.grey,
fontSize: 22
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: openAddStaticLease,
child: const Icon(Icons.add),
),
);
}
}

View file

@ -1228,3 +1228,117 @@ Future resetDhcpConfig({
return result;
}
}
Future deleteStaticLease({
required Server server,
required Map<String, dynamic> data
}) async {
final result = await apiRequest(
urlPath: '/dhcp/remove_static_lease',
method: 'post',
server: server,
body: data,
type: 'remove_static_lease'
);
if (result['hasResponse'] == true) {
if (result['statusCode'] == 200) {
return {'result': 'success'};
}
else {
return {
'result': 'error',
'log': AppLog(
type: 'remove_static_lease',
dateTime: DateTime.now(),
message: 'error_code_not_expected',
statusCode: result['statusCode'].toString(),
resBody: result['body'],
)
};
}
}
else {
return result;
}
}
Future createStaticLease({
required Server server,
required Map<String, dynamic> data
}) async {
final result = await apiRequest(
urlPath: '/dhcp/add_static_lease',
method: 'post',
server: server,
body: data,
type: 'add_static_lease'
);
if (result['hasResponse'] == true) {
if (result['statusCode'] == 200) {
return {'result': 'success'};
}
else if (result['statusCode'] == 400 && result['body'].contains('static lease already exists')) {
return {
'result': 'error',
'message': 'already_exists',
'log': AppLog(
type: 'add_static_lease',
dateTime: DateTime.now(),
message: 'already_exists',
statusCode: result['statusCode'].toString(),
resBody: result['body'],
)
};
}
else {
return {
'result': 'error',
'log': AppLog(
type: 'add_static_lease',
dateTime: DateTime.now(),
message: 'error_code_not_expected',
statusCode: result['statusCode'].toString(),
resBody: result['body'],
)
};
}
}
else {
return result;
}
}
Future restoreAllLeases({
required Server server,
}) async {
final result = await apiRequest(
urlPath: '/dhcp/reset_leases',
method: 'post',
server: server,
body: {},
type: 'restore_all_leases'
);
if (result['hasResponse'] == true) {
if (result['statusCode'] == 200) {
return {'result': 'success'};
}
else {
return {
'result': 'error',
'log': AppLog(
type: 'restore_all_leases',
dateTime: DateTime.now(),
message: 'error_code_not_expected',
statusCode: result['statusCode'].toString(),
resBody: result['body'],
)
};
}
}
else {
return result;
}
}