From 1d378224512a64c2b7898671796cf9e974784c6e Mon Sep 17 00:00:00 2001 From: "Scott E. Graves" Date: Fri, 7 Mar 2025 11:36:43 -0600 Subject: [PATCH] Create management portal in Flutter #39 --- web/repertory/lib/helpers.dart | 151 +++++++++++++++--- .../lib/screens/add_mount_screen.dart | 6 + web/repertory/lib/widgets/mount_settings.dart | 121 ++++++++++++-- 3 files changed, 243 insertions(+), 35 deletions(-) diff --git a/web/repertory/lib/helpers.dart b/web/repertory/lib/helpers.dart index 38ebc6a1..e2a8b196 100644 --- a/web/repertory/lib/helpers.dart +++ b/web/repertory/lib/helpers.dart @@ -1,31 +1,28 @@ +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; -String formatMountName(String type, String name) { - if (type == 'remote') { - return name.replaceAll('_', ':'); - } +typedef Validator = bool Function(String); - return name; -} - -String getBaseUri() { - if (kDebugMode || !kIsWeb) { - return 'http://127.0.0.1:30000'; - } - - return Uri.base.origin; -} - -String initialCaps(String txt) { - if (txt.isEmpty) { - return txt; - } - - if (txt.length == 1) { - return txt[0].toUpperCase(); - } - - return txt[0].toUpperCase() + txt.substring(1).toLowerCase(); +bool containsRestrictedChar(String value) { + const invalidChars = [ + '!', + '"', + '\$', + '&', + '\'', + '(', + ')', + '*', + ';', + '<', + '>', + '?', + '`', + '{', + '|', + '}', + ]; + return invalidChars.firstWhereOrNull((char) => value.contains(char)) != null; } Map createDefaultSettings(String mountType) { @@ -52,7 +49,7 @@ Map createDefaultSettings(String mountType) { return { 'HostConfig': { 'ApiPassword': '', - 'ApiPort': '9980', + 'ApiPort': 9980, 'HostNameOrIp': 'localhost', }, 'SiaConfig': {'Bucket': 'default'}, @@ -61,3 +58,105 @@ Map createDefaultSettings(String mountType) { return {}; } + +String formatMountName(String type, String name) { + if (type == 'remote') { + return name.replaceAll('_', ':'); + } + + return name; +} + +String getBaseUri() { + if (kDebugMode || !kIsWeb) { + return 'http://127.0.0.1:30000'; + } + + return Uri.base.origin; +} + +List getSettingValidators(String settingPath) { + switch (settingPath) { + case 'EncryptConfig.EncryptionToken': + return [(value) => value.isNotEmpty]; + case 'EncryptConfig.Path': + return [ + (value) => value.trim().isNotEmpty, + (value) => !containsRestrictedChar(value), + ]; + case 'HostConfig.ApiPassword': + return [(value) => value.isNotEmpty]; + case 'HostConfig.ApiPort': + return [ + (value) { + int? intValue = int.tryParse(value); + if (intValue == null) { + return false; + } + + return (intValue > 0 && intValue < 65536); + }, + (value) => Uri.tryParse('http://localhost:$value/') != null, + ]; + case 'HostConfig.HostNameOrIp': + return [ + (value) => value.trim().isNotEmpty, + (value) => Uri.tryParse('http://$value:9000/') != null, + ]; + case 'HostConfig.Protocol': + return [(value) => value == "http" || value == "https"]; + case 'S3Config.AccessKey': + return [(value) => value.isNotEmpty]; + case 'S3Config.Bucket': + return [(value) => value.trim().isNotEmpty]; + case 'S3Config.SecretKey': + return [(value) => value.isNotEmpty]; + case 'S3Config.URL': + return [(value) => Uri.tryParse(value) != null]; + case 'SiaConfig.Bucket': + return [(value) => value.trim().isNotEmpty]; + } + + return []; +} + +String initialCaps(String txt) { + if (txt.isEmpty) { + return txt; + } + + if (txt.length == 1) { + return txt[0].toUpperCase(); + } + + return txt[0].toUpperCase() + txt.substring(1).toLowerCase(); +} + +bool validateSettings( + Map settings, + List failed, { + String? rootKey, +}) { + settings.forEach((key, value) { + if (value is Map) { + validateSettings( + value as Map, + failed, + rootKey: rootKey == null ? key : '$rootKey.$key', + ); + } else { + final validators = getSettingValidators(key); + for (var validator in validators) { + if (!validator(value.toString())) { + if (rootKey == null) { + failed.add(key); + } else { + failed.add('$rootKey.$key'); + } + } + } + } + }); + + return failed.isEmpty; +} diff --git a/web/repertory/lib/screens/add_mount_screen.dart b/web/repertory/lib/screens/add_mount_screen.dart index 1121d03f..4e987dae 100644 --- a/web/repertory/lib/screens/add_mount_screen.dart +++ b/web/repertory/lib/screens/add_mount_screen.dart @@ -121,11 +121,17 @@ class _AddMountScreenState extends State { if (_mount != null) ElevatedButton.icon( onPressed: () { + List failed = []; + if (!validateSettings(_settings[_mountType]!, failed)) { + return; + } + Provider.of(context, listen: false).add( _mountType, _mountNameController.text, _settings[_mountType]!, ); + Navigator.pop(context); }, label: const Text('Add'), diff --git a/web/repertory/lib/widgets/mount_settings.dart b/web/repertory/lib/widgets/mount_settings.dart index c5a77aca..7d7e22de 100644 --- a/web/repertory/lib/widgets/mount_settings.dart +++ b/web/repertory/lib/widgets/mount_settings.dart @@ -2,6 +2,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:repertory/constants.dart'; +import 'package:repertory/helpers.dart' show Validator, getSettingValidators; import 'package:repertory/models/mount.dart'; import 'package:settings_ui/settings_ui.dart'; @@ -48,7 +49,14 @@ class _MountSettingsWidgetState extends State { } } - void _addIntSetting(list, root, key, value, isAdvanced) { + void _addIntSetting( + list, + root, + key, + value, + isAdvanced, { + List validators = const [], + }) { if (!isAdvanced || widget.showAdvanced) { list.add( SettingsTile.navigation( @@ -69,6 +77,15 @@ class _MountSettingsWidgetState extends State { TextButton( child: Text('OK'), onPressed: () { + final result = validators.firstWhereOrNull( + (func) => !func(updatedValue), + ); + if (result != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('$key must be valid')), + ); + return; + } setState(() { root[key] = int.parse(updatedValue); widget.onChanged?.call(widget.settings); @@ -176,7 +193,15 @@ class _MountSettingsWidgetState extends State { } } - void _addStringSetting(list, root, key, value, icon, isAdvanced) { + void _addStringSetting( + list, + root, + key, + value, + icon, + isAdvanced, { + List validators = const [], + }) { if (!isAdvanced || widget.showAdvanced) { list.add( SettingsTile.navigation( @@ -197,6 +222,16 @@ class _MountSettingsWidgetState extends State { TextButton( child: Text('OK'), onPressed: () { + final result = validators.firstWhereOrNull( + (func) => !func(updatedValue), + ); + if (result != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('$key must be valid')), + ); + return; + } + setState(() { root[key] = updatedValue; widget.onChanged?.call(widget.settings); @@ -234,7 +269,14 @@ class _MountSettingsWidgetState extends State { if (key == 'ApiAuth') { _addPasswordSetting(commonSettings, widget.settings, key, value, true); } else if (key == 'ApiPort') { - _addIntSetting(commonSettings, widget.settings, key, value, true); + _addIntSetting( + commonSettings, + widget.settings, + key, + value, + true, + validators: getSettingValidators(key), + ); } else if (key == 'ApiUser') { _addStringSetting( commonSettings, @@ -243,6 +285,7 @@ class _MountSettingsWidgetState extends State { value, Icons.person, true, + validators: getSettingValidators(key), ); } else if (key == 'DatabaseType') { _addListSetting( @@ -255,7 +298,14 @@ class _MountSettingsWidgetState extends State { true, ); } else if (key == 'DownloadTimeoutSeconds') { - _addIntSetting(commonSettings, widget.settings, key, value, true); + _addIntSetting( + commonSettings, + widget.settings, + key, + value, + true, + validators: getSettingValidators(key), + ); } else if (key == 'EnableDownloadTimeout') { _addBooleanSetting(commonSettings, widget.settings, key, value, true); } else if (key == 'EnableDriveEvents') { @@ -271,15 +321,43 @@ class _MountSettingsWidgetState extends State { false, ); } else if (key == 'EvictionDelayMinutes') { - _addIntSetting(commonSettings, widget.settings, key, value, true); + _addIntSetting( + commonSettings, + widget.settings, + key, + value, + true, + validators: getSettingValidators(key), + ); } else if (key == 'EvictionUseAccessedTime') { _addBooleanSetting(commonSettings, widget.settings, key, value, true); } else if (key == 'MaxCacheSizeBytes') { - _addIntSetting(commonSettings, widget.settings, key, value, false); + _addIntSetting( + commonSettings, + widget.settings, + key, + value, + false, + validators: getSettingValidators(key), + ); } else if (key == 'MaxUploadCount') { - _addIntSetting(commonSettings, widget.settings, key, value, true); + _addIntSetting( + commonSettings, + widget.settings, + key, + value, + true, + validators: getSettingValidators(key), + ); } else if (key == 'OnlineCheckRetrySeconds') { - _addIntSetting(commonSettings, widget.settings, key, value, true); + _addIntSetting( + commonSettings, + widget.settings, + key, + value, + true, + validators: getSettingValidators(key), + ); } else if (key == 'PreferredDownloadType') { _addListSetting( commonSettings, @@ -291,7 +369,14 @@ class _MountSettingsWidgetState extends State { false, ); } else if (key == 'RetryReadCount') { - _addIntSetting(commonSettings, widget.settings, key, value, true); + _addIntSetting( + commonSettings, + widget.settings, + key, + value, + true, + validators: getSettingValidators(key), + ); } else if (key == 'RingBufferFileSize') { _addIntListSetting( commonSettings, @@ -321,6 +406,7 @@ class _MountSettingsWidgetState extends State { subValue, Icons.folder, false, + validators: getSettingValidators('$key.$subKey'), ); } }); @@ -334,6 +420,7 @@ class _MountSettingsWidgetState extends State { subValue, Icons.support_agent, true, + validators: getSettingValidators('$key.$subKey'), ); } else if (subKey == 'ApiPassword') { _addPasswordSetting( @@ -350,6 +437,7 @@ class _MountSettingsWidgetState extends State { subKey, subValue, false, + validators: getSettingValidators('$key.$subKey'), ); } else if (subKey == 'ApiUser') { _addStringSetting( @@ -359,6 +447,7 @@ class _MountSettingsWidgetState extends State { subValue, Icons.person, true, + validators: getSettingValidators('$key.$subKey'), ); } else if (subKey == 'HostNameOrIp') { _addStringSetting( @@ -377,6 +466,7 @@ class _MountSettingsWidgetState extends State { subValue, Icons.route, true, + validators: getSettingValidators('$key.$subKey'), ); } else if (subKey == 'Protocol') { _addListSetting( @@ -395,6 +485,7 @@ class _MountSettingsWidgetState extends State { subKey, subValue, true, + validators: getSettingValidators('$key.$subKey'), ); } }); @@ -407,6 +498,7 @@ class _MountSettingsWidgetState extends State { subKey, subValue, false, + validators: getSettingValidators('$key.$subKey'), ); } else if (subKey == 'EncryptionToken') { _addPasswordSetting( @@ -424,6 +516,7 @@ class _MountSettingsWidgetState extends State { subValue, Icons.computer, false, + validators: getSettingValidators('$key.$subKey'), ); } else if (subKey == 'MaxConnections') { _addIntSetting( @@ -432,6 +525,7 @@ class _MountSettingsWidgetState extends State { subKey, subValue, true, + validators: getSettingValidators('$key.$subKey'), ); } else if (subKey == 'ReceiveTimeoutMs') { _addIntSetting( @@ -440,6 +534,7 @@ class _MountSettingsWidgetState extends State { subKey, subValue, true, + validators: getSettingValidators('$key.$subKey'), ); } else if (subKey == 'SendTimeoutMs') { _addIntSetting( @@ -448,6 +543,7 @@ class _MountSettingsWidgetState extends State { subKey, subValue, true, + validators: getSettingValidators('$key.$subKey'), ); } }); @@ -470,6 +566,7 @@ class _MountSettingsWidgetState extends State { subKey, subValue, false, + validators: getSettingValidators('$key.$subKey'), ); } else if (subKey == 'ClientPoolSize') { _addIntSetting( @@ -478,6 +575,7 @@ class _MountSettingsWidgetState extends State { subKey, subValue, true, + validators: getSettingValidators('$key.$subKey'), ); } else if (subKey == 'EncryptionToken') { _addPasswordSetting( @@ -507,6 +605,7 @@ class _MountSettingsWidgetState extends State { subValue, Icons.folder, false, + validators: getSettingValidators('$key.$subKey'), ); } else if (subKey == 'EncryptionToken') { _addPasswordSetting( @@ -524,6 +623,7 @@ class _MountSettingsWidgetState extends State { subValue, Icons.map, false, + validators: getSettingValidators('$key.$subKey'), ); } else if (subKey == 'SecretKey') { _addPasswordSetting( @@ -540,6 +640,7 @@ class _MountSettingsWidgetState extends State { subKey, subValue, true, + validators: getSettingValidators('$key.$subKey'), ); } else if (subKey == 'URL') { _addStringSetting( @@ -549,6 +650,7 @@ class _MountSettingsWidgetState extends State { subValue, Icons.http, false, + validators: getSettingValidators('$key.$subKey'), ); } else if (subKey == 'UsePathStyle') { _addBooleanSetting( @@ -578,6 +680,7 @@ class _MountSettingsWidgetState extends State { subValue, Icons.folder, false, + validators: getSettingValidators('$key.$subKey'), ); } });