Added create connection

This commit is contained in:
Juan Gilsanz Polo 2022-09-26 18:49:41 +02:00
parent a97ae20631
commit 3acfc7c5a5
10 changed files with 411 additions and 21 deletions

View file

@ -14,5 +14,17 @@
"general": "General",
"connection": "Connection",
"authentication": "Authentication",
"other": "Other"
"other": "Other",
"invalidPort": "Invalid port",
"invalidPath": "Invalid path",
"invalidIpDomain": "Invalid IP or domain",
"ipDomainNotEmpty": "IP or domain cannot be empty",
"nameNotEmpty": "Name cannot be empty",
"invalidUsernamePassword": "Invalid username or password",
"tooManyAttempts": "Too many attempts. Try again later.",
"cantReachServer": "Can't reach server. Check connection data.",
"sslError": "SSL error",
"unknownError": "Unknown error",
"connectionNotCreated": "Connection couldn't be created",
"connecting": "Connecting..."
}

View file

@ -14,5 +14,17 @@
"general": "General",
"connection": "Conexión",
"authentication": "Autenticación",
"other": "Otros"
"other": "Otros",
"invalidPort": "Puerto no válido",
"invalidPath": "Ruta no válida",
"invalidIpDomain": "IP o dominio no válido",
"ipDomainNotEmpty": "IP o dominio no puede estar vacío",
"nameNotEmpty": "Name cannot be empty",
"invalidUsernamePassword": "Usuario o contraseña no válidos.",
"tooManyAttempts": "Demasiados intentos. Prueba de nuevo más tarde.",
"cantReachServer": "No se puede alcanzar el servidor. Comprueba los datos de conexión.",
"sslError": "Error de SSL",
"unknownError": "Error desconocido",
"connectionNotCreated": "No se pudo crear la conexión",
"connecting": "Conectando..."
}

View file

@ -1,14 +1,16 @@
class Server {
final String name;
final String connectionMethod;
final String domain;
final String? path;
final int? port;
final String user;
final String password;
final bool defaultServer;
final String id;
String name;
String connectionMethod;
String domain;
String? path;
int? port;
String user;
String password;
bool defaultServer;
const Server({
Server({
required this.id,
required this.name,
required this.connectionMethod,
required this.domain,

View file

@ -27,10 +27,87 @@ class ServersProvider with ChangeNotifier {
notifyListeners();
}
Future<bool> createServer(Server server) async {
final saved = await saveServerIntoDb(server);
if (saved == true) {
if (server.defaultServer == true) {
final defaultServer = await setDefaultServer(server);
if (defaultServer == true) {
_serversList.add(server);
notifyListeners();
return true;
}
else {
return false;
}
}
else {
_serversList.add(server);
notifyListeners();
return true;
}
}
else {
return false;
}
}
Future<bool> setDefaultServer(Server server) async {
final updated = await setDefaultServerDb(server.id);
if (updated == true) {
List<Server> newServers = _serversList.map((s) {
if (s.id == server.id) {
s.defaultServer = true;
return s;
}
else {
s.defaultServer = false;
return s;
}
}).toList();
_serversList = newServers;
notifyListeners();
return true;
}
else {
return false;
}
}
Future<bool> saveServerIntoDb(Server server) async {
try {
return await _dbInstance!.transaction((txn) async {
await txn.rawInsert(
'INSERT INTO servers (id, name, connectionMethod, domain, path, port, user, password, defaultServer) VALUES ("${server.id}", "${server.name}", "${server.connectionMethod}", "${server.domain}", "${server.path}", ${server.port}, "${server.user}", "${server.password}", 0)',
);
return true;
});
} catch (e) {
return false;
}
}
Future<bool> setDefaultServerDb(String id) async {
try {
return await _dbInstance!.transaction((txn) async {
await txn.rawUpdate(
'UPDATE servers SET defaultServer = 0 WHERE defaultServer = 1',
);
await txn.rawUpdate(
'UPDATE servers SET defaultServer = 1 WHERE id = "$id"',
);
return true;
});
} catch (e) {
return false;
}
}
void saveFromDb(List<Map<String, dynamic>>? data) async {
if (data != null) {
for (var server in data) {
final Server serverObj = Server(
id: server['id'],
name: server['name'],
connectionMethod: server['connectionMethod'],
domain: server['domain'],
@ -38,10 +115,10 @@ class ServersProvider with ChangeNotifier {
port: server['port'],
user: server['user'],
password: server['password'],
defaultServer: convertFromIntToBool(server['isDefaultServer'])!,
defaultServer: convertFromIntToBool(server['defaultServer'])!,
);
_serversList.add(serverObj);
if (convertFromIntToBool(server['isDefaultServer']) == true) {
if (convertFromIntToBool(server['defaultServer']) == true) {
_selectedServer = serverObj;
}
}

View file

@ -1,10 +1,15 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
class Connect extends StatelessWidget {
const Connect({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
return Container();
}
}

View file

@ -8,7 +8,7 @@ Future<Map<String, dynamic>> loadDb() async {
'adguard_home_manager.db',
version: 1,
onCreate: (Database db, int version) async {
await db.execute("CREATE TABLE servers (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, connectionMethod TEXT, domain TEXT, path TEXT, port INTEGER, user TEXT, password TEXT, defaultServer INTEGER)");
await db.execute("CREATE TABLE servers (id TEXT PRIMARY KEY, name TEXT, connectionMethod TEXT, domain TEXT, path TEXT, port INTEGER, user TEXT, password TEXT, defaultServer INTEGER)");
},
onUpgrade: (Database db, int oldVersion, int newVersion) async {

View file

@ -0,0 +1,47 @@
// ignore_for_file: depend_on_referenced_packages
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:adguard_home_manager/models/server.dart';
Future login(Server server) async {
try {
final result = await http.post(
Uri.parse("${server.connectionMethod}://${server.domain}${server.path ?? ""}${server.port != null ? ':${server.port}' : ""}/control/login"),
body: jsonEncode({
"name": server.user,
"password": server.password
})
);
if (result.statusCode == 200) {
return {'result': 'success'};
}
else if (result.statusCode == 400) {
return {
'result': 'error',
'message': 'invalid_username_password'
};
}
else if (result.statusCode == 429) {
return {
'result': 'error',
'message': 'many_attempts'
};
}
else {
return {'result': 'error'};
}
} on SocketException {
return {'result': 'no_connection'};
} on TimeoutException {
return {'result': 'no_connection'};
} on HandshakeException {
return {'result': 'ssl_error'};
} catch (e) {
return {'result': 'error'};
}
}

View file

@ -1,8 +1,15 @@
// ignore_for_file: use_build_context_synchronously
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/widgets/custom_radio_toggle.dart';
import 'package:adguard_home_manager/services/http_requests.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
import 'package:adguard_home_manager/models/server.dart';
import 'package:adguard_home_manager/config/system_overlay_style.dart';
class AddServerModal extends StatefulWidget {
@ -13,7 +20,10 @@ class AddServerModal extends StatefulWidget {
}
class _AddServerModalState extends State<AddServerModal> {
final uuid = const Uuid();
final TextEditingController nameController = TextEditingController();
String? nameError;
String connectionType = "http";
@ -32,6 +42,10 @@ class _AddServerModalState extends State<AddServerModal> {
bool defaultServer = false;
bool allDataValid = false;
bool isConnecting = false;
Widget sectionLabel(String label) {
return Padding(
padding: const EdgeInsets.symmetric(
@ -53,12 +67,14 @@ class _AddServerModalState extends State<AddServerModal> {
required TextEditingController controller,
String? error,
required IconData icon,
TextInputType? keyboardType
TextInputType? keyboardType,
Function(String)? onChanged,
}) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextFormField(
controller: controller,
onChanged: onChanged,
decoration: InputDecoration(
prefixIcon: Icon(icon),
errorText: error,
@ -74,8 +90,168 @@ class _AddServerModalState extends State<AddServerModal> {
);
}
void checkDataValid() {
if (
nameController.text != '' &&
ipDomainController.text != '' &&
ipDomainError == null &&
pathError == null &&
portError == null &&
userController.text != '' &&
passwordController.text != ''
) {
setState(() {
allDataValid = true;
});
}
else {
setState(() {
allDataValid = false;
});
}
}
void validatePort(String? value) {
if (value != null && value != '') {
if (int.tryParse(value) != null && int.parse(value) <= 65535) {
setState(() {
portError = null;
});
}
else {
setState(() {
portError = AppLocalizations.of(context)!.invalidPort;
});
}
}
else {
setState(() {
portError = null;
});
}
checkDataValid();
}
void validateSubroute(String? value) {
if (value != null && value != '') {
RegExp subrouteRegexp = RegExp(r'^\/\b([A-Za-z0-9_\-~/]*)[^\/|\.|\:]$');
if (subrouteRegexp.hasMatch(value) == true) {
setState(() {
pathError = null;
});
}
else {
setState(() {
pathError = AppLocalizations.of(context)!.invalidPath;
});
}
}
else {
setState(() {
pathError = null;
});
}
checkDataValid();
}
void validateAddress(String? value) {
if (value != null && value != '') {
RegExp ipAddress = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$');
RegExp domain = RegExp(r'^((?!-))(xn--)?[a-z0-9][a-z0-9-_]{0,61}[a-z0-9]{0,1}\.(xn--)?([a-z0-9\-]{1,61}|[a-z0-9-]{1,30}\.[a-z]{2,})$');
if (ipAddress.hasMatch(value) == true || domain.hasMatch(value) == true) {
setState(() {
ipDomainError = null;
});
}
else {
setState(() {
ipDomainError = AppLocalizations.of(context)!.invalidIpDomain;
});
}
}
else {
setState(() {
ipDomainError = AppLocalizations.of(context)!.ipDomainNotEmpty;
});
}
checkDataValid();
}
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context, listen: false);
final mediaQuery = MediaQuery.of(context);
void connect() async {
final Server serverObj = Server(
id: uuid.v4(),
name: nameController.text,
connectionMethod: connectionType,
domain: ipDomainController.text,
port: int.parse(portController.text),
user: userController.text,
password: passwordController.text,
defaultServer: defaultServer
);
final result = await login(serverObj);
if (result['result'] == 'success') {
final serverCreated = await serversProvider.createServer(serverObj);
if (serverCreated == true) {
Navigator.pop(context);
}
else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.connectionNotCreated),
backgroundColor: Colors.red,
)
);
}
}
else if (result['result'] == 'error' && result['message'] == 'invalid_username_password') {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.invalidUsernamePassword),
backgroundColor: Colors.red,
)
);
}
else if (result['result'] == 'error' && result['message'] == 'many_attempts') {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.tooManyAttempts),
backgroundColor: Colors.red,
)
);
}
else if (result['result'] == 'error' && result['message'] == 'no_connection') {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.cantReachServer),
backgroundColor: Colors.red,
)
);
}
else if (result['result'] == 'error' && result['message'] == 'ssl_error') {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.sslError),
backgroundColor: Colors.red,
)
);
}
else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.unknownError),
backgroundColor: Colors.red,
)
);
}
}
return Stack(
children: [
Scaffold(
@ -89,7 +265,9 @@ class _AddServerModalState extends State<AddServerModal> {
padding: const EdgeInsets.only(right: 10),
child: IconButton(
tooltip: AppLocalizations.of(context)!.connect,
onPressed: () => {},
onPressed: allDataValid == true
? () => connect()
: null,
icon: const Icon(Icons.login_rounded)
),
),
@ -102,7 +280,11 @@ class _AddServerModalState extends State<AddServerModal> {
textField(
label: AppLocalizations.of(context)!.name,
controller: nameController,
icon: Icons.badge_rounded
icon: Icons.badge_rounded,
error: nameError,
onChanged: (value) => value == ''
? setState(() => nameError = AppLocalizations.of(context)!.nameNotEmpty)
: setState(() => nameError = null)
),
sectionLabel(AppLocalizations.of(context)!.connection),
Row(
@ -128,14 +310,16 @@ class _AddServerModalState extends State<AddServerModal> {
controller: ipDomainController,
icon: Icons.link_rounded,
error: ipDomainError,
keyboardType: TextInputType.url
keyboardType: TextInputType.url,
onChanged: validateAddress
),
const SizedBox(height: 20),
textField(
label: AppLocalizations.of(context)!.path,
controller: pathController,
icon: Icons.route_rounded,
error: pathError
error: pathError,
onChanged: validateSubroute
),
const SizedBox(height: 20),
textField(
@ -143,20 +327,23 @@ class _AddServerModalState extends State<AddServerModal> {
controller: portController,
icon: Icons.numbers_rounded,
error: portError,
keyboardType: TextInputType.number
keyboardType: TextInputType.number,
onChanged: validatePort
),
sectionLabel(AppLocalizations.of(context)!.authentication),
textField(
label: AppLocalizations.of(context)!.username,
controller: userController,
icon: Icons.person_rounded,
onChanged: (_) => checkDataValid()
),
const SizedBox(height: 20),
textField(
label: AppLocalizations.of(context)!.password,
controller: passwordController,
icon: Icons.lock_rounded,
keyboardType: TextInputType.visiblePassword
keyboardType: TextInputType.visiblePassword,
onChanged: (_) => checkDataValid()
),
sectionLabel(AppLocalizations.of(context)!.other),
Material(
@ -187,6 +374,39 @@ class _AddServerModalState extends State<AddServerModal> {
const SizedBox(height: 20),
],
),
),
AnimatedOpacity(
opacity: isConnecting == true ? 1 : 0,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
child: IgnorePointer(
ignoring: isConnecting == true ? false : true,
child: Scaffold(
backgroundColor: Colors.transparent,
body: Container(
width: mediaQuery.size.width,
height: mediaQuery.size.height,
color: const Color.fromRGBO(0, 0, 0, 0.7),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(
color: Colors.white,
),
const SizedBox(height: 30),
Text(
AppLocalizations.of(context)!.connecting,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
fontSize: 26
),
)
],
),
),
),
),
)
],
);

View file

@ -43,6 +43,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.16.0"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
cupertino_icons:
dependency: "direct main"
description:
@ -355,6 +362,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
uuid:
dependency: "direct main"
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.6"
vector_math:
dependency: transitive
description:

View file

@ -42,6 +42,7 @@ dependencies:
dynamic_color: ^1.5.4
animations: ^2.0.5
device_info_plus: ^4.1.2
uuid: ^3.0.6
dev_dependencies:
flutter_test: