diff --git a/repertory/repertory/include/ui/handlers.hpp b/repertory/repertory/include/ui/handlers.hpp index 27e4e7f3..d049fea2 100644 --- a/repertory/repertory/include/ui/handlers.hpp +++ b/repertory/repertory/include/ui/handlers.hpp @@ -23,11 +23,26 @@ #define REPERTORY_INCLUDE_UI_HANDLERS_HPP_ #include "events/consumers/console_consumer.hpp" +#include "utils/common.hpp" namespace repertory::ui { class mgmt_app_config; class handlers final { +private: + static constexpr const auto nonce_length{128U}; + static constexpr const auto nonce_timeout{15U}; + + struct nonce_data final { + std::chrono::system_clock::time_point creation{ + std::chrono::system_clock::now(), + }; + + std::string nonce{ + utils::generate_random_string(nonce_length), + }; + }; + public: handlers(mgmt_app_config *config, httplib::Server *server); @@ -49,34 +64,50 @@ private: console_consumer console; mutable std::mutex mtx_; mutable std::unordered_map mtx_lookup_; + std::mutex nonce_mtx_; + std::unordered_map nonce_lookup_; + std::condition_variable nonce_notify_; + std::unique_ptr nonce_thread_; + stop_type stop_requested{false}; private: [[nodiscard]] auto data_directory_exists(provider_type prov, std::string_view name) const -> bool; - void handle_get_mount(auto &&req, auto &&res) const; + void handle_get_mount(const httplib::Request &req, + httplib::Response &res) const; - void handle_get_mount_list(auto &&res) const; + void handle_get_mount_list(httplib::Response &res) const; - void handle_get_mount_location(auto &&req, auto &&res) const; + void handle_get_mount_location(const httplib::Request &req, + httplib::Response &res) const; - void handle_get_mount_status(auto &&req, auto &&res) const; + void handle_get_mount_status(const httplib::Request &req, + httplib::Response &res) const; - void handle_get_settings(auto &&res) const; + void handle_get_nonce(httplib::Response &res); - void handle_post_add_mount(auto &&req, auto &&res) const; + void handle_get_settings(httplib::Response &res) const; - void handle_post_mount(auto &&req, auto &&res) const; + void handle_post_add_mount(const httplib::Request &req, + httplib::Response &res) const; - void handle_put_set_value_by_name(auto &&req, auto &&res) const; + void handle_post_mount(const httplib::Request &req, + httplib::Response &res) const; - void handle_put_settings(auto &&req, auto &&res) const; + void handle_put_set_value_by_name(const httplib::Request &req, + httplib::Response &res) const; + + void handle_put_settings(const httplib::Request &req, + httplib::Response &res) const; auto launch_process(provider_type prov, std::string_view name, std::vector args, bool background = false) const -> std::vector; + void removed_expired_nonces(); + void set_key_value(provider_type prov, std::string_view name, std::string_view key, std::string_view value) const; }; diff --git a/repertory/repertory/src/ui/handlers.cpp b/repertory/repertory/src/ui/handlers.cpp index 54445cd1..fb325fe2 100644 --- a/repertory/repertory/src/ui/handlers.cpp +++ b/repertory/repertory/src/ui/handlers.cpp @@ -23,7 +23,6 @@ #include "app_config.hpp" #include "events/event_system.hpp" -#include "rpc/common.hpp" #include "types/repertory.hpp" #include "ui/mgmt_app_config.hpp" #include "utils/collection.hpp" @@ -60,7 +59,8 @@ namespace { reinterpret_cast( &decoded.at(crypto_aead_xchacha20poly1305_IETF_NPUBBYTES)), decoded.size() - crypto_aead_xchacha20poly1305_IETF_NPUBBYTES, - reinterpret_cast(REPERTORY.data()), 9U, + reinterpret_cast(REPERTORY.data()), + REPERTORY.length(), reinterpret_cast(decoded.data()), reinterpret_cast(key.data())); if (res != 0) { @@ -109,15 +109,27 @@ handlers::handlers(mgmt_app_config *config, httplib::Server *server) REPERTORY_USES_FUNCTION_NAME(); server_->set_pre_routing_handler( - [this](auto &&req, auto &&res) -> httplib::Server::HandlerResponse { - if (rpc::check_authorization(*config_, req)) { + [this](const httplib::Request &req, + auto &&res) -> httplib::Server::HandlerResponse { + if (req.path == "/api/v1/nonce" || req.path == "/ui" || + req.path.starts_with("/ui/")) { return httplib::Server::HandlerResponse::Unhandled; } + auto auth = + decrypt(req.get_param_value("auth"), config_->get_api_password()); + if (utils::string::begins_with( + auth, fmt::format("{}_", config_->get_api_user()))) { + auto nonce = auth.substr(config_->get_api_user().length() + 1U); + + mutex_lock lock(nonce_mtx_); + if (nonce_lookup_.contains(nonce)) { + nonce_lookup_.erase(nonce); + return httplib::Server::HandlerResponse::Unhandled; + } + } + res.status = http_error_codes::unauthorized; - res.set_header( - "WWW-Authenticate", - R"(Basic realm="Repertory Management Portal", charset="UTF-8")"); return httplib::Server::HandlerResponse::Handled; }); @@ -155,15 +167,16 @@ handlers::handlers(mgmt_app_config *config, httplib::Server *server) handle_get_mount_list(res); }); - server->Get("/api/v1/mount_status", - [this](const httplib::Request &req, auto &&res) { - handle_get_mount_status(req, res); - }); + server->Get("/api/v1/mount_status", [this](auto &&req, auto &&res) { + handle_get_mount_status(req, res); + }); - server->Get("/api/v1/settings", - [this](const httplib::Request & /* req */, auto &&res) { - handle_get_settings(res); - }); + server->Get("/api/v1/nonce", + [this](auto && /* req */, auto &&res) { handle_get_nonce(res); }); + + server->Get("/api/v1/settings", [this](auto && /* req */, auto &&res) { + handle_get_settings(res); + }); server->Post("/api/v1/add_mount", [this](auto &&req, auto &&res) { handle_post_add_mount(req, res); @@ -225,6 +238,9 @@ handlers::handlers(mgmt_app_config *config, httplib::Server *server) event_system::instance().start(); + nonce_thread_ = + std::make_unique([this]() { removed_expired_nonces(); }); + server_->listen("127.0.0.1", config_->get_api_port()); if (this_server != nullptr) { this_server = nullptr; @@ -232,7 +248,20 @@ handlers::handlers(mgmt_app_config *config, httplib::Server *server) } } -handlers::~handlers() { event_system::instance().stop(); } +handlers::~handlers() { + if (nonce_thread_) { + stop_requested = true; + + unique_mutex_lock lock(nonce_mtx_); + nonce_notify_.notify_all(); + lock.unlock(); + + nonce_thread_->join(); + nonce_thread_.reset(); + } + + event_system::instance().stop(); +} auto handlers::data_directory_exists(provider_type prov, std::string_view name) const -> bool { @@ -253,7 +282,8 @@ auto handlers::data_directory_exists(provider_type prov, return ret; } -void handlers::handle_get_mount(auto &&req, auto &&res) const { +void handlers::handle_get_mount(const httplib::Request &req, + httplib::Response &res) const { REPERTORY_USES_FUNCTION_NAME(); auto prov = provider_type_from_string(req.get_param_value("type")); @@ -282,7 +312,7 @@ void handlers::handle_get_mount(auto &&req, auto &&res) const { res.status = http_error_codes::ok; } -void handlers::handle_get_mount_list(auto &&res) const { +void handlers::handle_get_mount_list(httplib::Response &res) const { auto data_dir = utils::file::directory{app_config::get_root_data_directory()}; nlohmann::json result; @@ -311,7 +341,8 @@ void handlers::handle_get_mount_list(auto &&res) const { res.status = http_error_codes::ok; } -void handlers::handle_get_mount_location(auto &&req, auto &&res) const { +void handlers::handle_get_mount_location(const httplib::Request &req, + httplib::Response &res) const { auto name = req.get_param_value("name"); auto prov = provider_type_from_string(req.get_param_value("type")); @@ -329,7 +360,8 @@ void handlers::handle_get_mount_location(auto &&req, auto &&res) const { res.status = http_error_codes::ok; } -void handlers::handle_get_mount_status(auto &&req, auto &&res) const { +void handlers::handle_get_mount_status(const httplib::Request &req, + httplib::Response &res) const { REPERTORY_USES_FUNCTION_NAME(); auto name = req.get_param_value("name"); @@ -379,7 +411,18 @@ void handlers::handle_get_mount_status(auto &&req, auto &&res) const { res.status = http_error_codes::ok; } -void handlers::handle_get_settings(auto &&res) const { +void handlers::handle_get_nonce(httplib::Response &res) { + mutex_lock lock(nonce_mtx_); + + nonce_data nonce{}; + nonce_lookup_[nonce.nonce] = nonce; + + nlohmann::json data({{"nonce", nonce.nonce}}); + res.set_content(data.dump(), "application/json"); + res.status = http_error_codes::ok; +} + +void handlers::handle_get_settings(httplib::Response &res) const { auto settings = config_->to_json(); settings[JSON_API_PASSWORD] = ""; settings.erase(JSON_MOUNT_LOCATIONS); @@ -387,7 +430,8 @@ void handlers::handle_get_settings(auto &&res) const { res.status = http_error_codes::ok; } -void handlers::handle_post_add_mount(auto &&req, auto &&res) const { +void handlers::handle_post_add_mount(const httplib::Request &req, + httplib::Response &res) const { auto name = req.get_param_value("name"); auto prov = provider_type_from_string(req.get_param_value("type")); if (data_directory_exists(prov, name)) { @@ -431,7 +475,8 @@ void handlers::handle_post_add_mount(auto &&req, auto &&res) const { res.status = http_error_codes::ok; } -void handlers::handle_post_mount(auto &&req, auto &&res) const { +void handlers::handle_post_mount(const httplib::Request &req, + httplib::Response &res) const { auto name = req.get_param_value("name"); auto prov = provider_type_from_string(req.get_param_value("type")); @@ -462,7 +507,8 @@ void handlers::handle_post_mount(auto &&req, auto &&res) const { res.status = http_error_codes::ok; } -void handlers::handle_put_set_value_by_name(auto &&req, auto &&res) const { +void handlers::handle_put_set_value_by_name(const httplib::Request &req, + httplib::Response &res) const { auto name = req.get_param_value("name"); auto prov = provider_type_from_string(req.get_param_value("type")); @@ -483,7 +529,8 @@ void handlers::handle_put_set_value_by_name(auto &&req, auto &&res) const { res.status = http_error_codes::ok; } -void handlers::handle_put_settings(auto &&req, auto &&res) const { +void handlers::handle_put_settings(const httplib::Request &req, + httplib::Response &res) const { nlohmann::json data = nlohmann::json::parse(req.get_param_value("data")); if (data.contains(JSON_API_PASSWORD)) { @@ -620,6 +667,38 @@ auto handlers::launch_process(provider_type prov, std::string_view name, false); } +void handlers::removed_expired_nonces() { + unique_mutex_lock lock(nonce_mtx_); + lock.unlock(); + + while (not stop_requested) { + lock.lock(); + auto nonces = nonce_lookup_; + lock.unlock(); + + for (const auto &[key, value] : nonces) { + if (std::chrono::duration_cast( + std::chrono::system_clock::now() - value.creation) + .count() >= nonce_timeout) { + lock.lock(); + nonce_lookup_.erase(key); + lock.unlock(); + } + } + + if (stop_requested) { + break; + } + + lock.lock(); + if (stop_requested) { + break; + } + nonce_notify_.wait_for(lock, std::chrono::seconds(1U)); + lock.unlock(); + } +} + void handlers::set_key_value(provider_type prov, std::string_view name, std::string_view key, std::string_view value) const { diff --git a/web/repertory/lib/helpers.dart b/web/repertory/lib/helpers.dart index 39655aa8..1271dfb1 100644 --- a/web/repertory/lib/helpers.dart +++ b/web/repertory/lib/helpers.dart @@ -246,8 +246,8 @@ bool validateSettings( Future> convertAllToString( Map settings, + SecureKey key, ) async { - String? password; Future> convert(Map settings) async { for (var entry in settings.entries) { if (entry.value is Map) { @@ -262,14 +262,7 @@ Future> convertAllToString( continue; } - if (password == null) { - password = await promptPassword(); - if (password == null) { - throw NullPasswordException(); - } - } - - settings[entry.key] = encryptValue(entry.value, password!); + settings[entry.key] = encryptValue(entry.value, key); continue; } @@ -286,7 +279,7 @@ Future> convertAllToString( return convert(settings); } -String encryptValue(String value, String password) { +String encryptValue(String value, SecureKey key) { if (value.isEmpty) { return value; } @@ -296,17 +289,12 @@ String encryptValue(String value, String password) { return value; } - final keyHash = sodium.crypto.genericHash( - outLen: sodium.crypto.aeadXChaCha20Poly1305IETF.keyBytes, - message: Uint8List.fromList(password.toCharArray()), - ); - final crypto = sodium.crypto.aeadXChaCha20Poly1305IETF; final nonce = sodium.secureRandom(crypto.nonceBytes).extractBytes(); final data = crypto.encrypt( additionalData: Uint8List.fromList('repertory'.toCharArray()), - key: SecureKey.fromList(sodium, keyHash), + key: key, message: Uint8List.fromList(value.toCharArray()), nonce: nonce, ); diff --git a/web/repertory/lib/main.dart b/web/repertory/lib/main.dart index 473b95a7..afa61a73 100644 --- a/web/repertory/lib/main.dart +++ b/web/repertory/lib/main.dart @@ -2,9 +2,11 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:repertory/constants.dart' as constants; import 'package:repertory/helpers.dart'; +import 'package:repertory/models/auth.dart'; import 'package:repertory/models/mount.dart'; import 'package:repertory/models/mount_list.dart'; import 'package:repertory/screens/add_mount_screen.dart'; +import 'package:repertory/screens/auth_screen.dart'; import 'package:repertory/screens/edit_mount_screen.dart'; import 'package:repertory/screens/edit_settings_screen.dart'; import 'package:repertory/screens/home_screen.dart'; @@ -17,8 +19,15 @@ void main() async { debugPrint('$e'); } + final auth = Auth(); runApp( - ChangeNotifierProvider(create: (_) => MountList(), child: const MyApp()), + MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => auth), + ChangeNotifierProvider(create: (_) => MountList(auth)), + ], + child: const MyApp(), + ), ); } @@ -55,12 +64,18 @@ class _MyAppState extends State { title: constants.appTitle, initialRoute: '/', routes: { - '/': (context) => const HomeScreen(title: constants.appTitle), - '/add': - (context) => const AddMountScreen(title: constants.addMountTitle), - '/settings': + '/': (context) => - const EditSettingsScreen(title: constants.appSettingsTitle), + const AuthCheck(child: HomeScreen(title: constants.appTitle)), + '/add': + (context) => const AuthCheck( + child: AddMountScreen(title: constants.addMountTitle), + ), + '/auth': (context) => const AuthScreen(title: constants.appTitle), + '/settings': + (context) => const AuthCheck( + child: EditSettingsScreen(title: constants.appSettingsTitle), + ), }, onGenerateRoute: (settings) { if (settings.name != '/edit') { @@ -70,10 +85,12 @@ class _MyAppState extends State { final mount = settings.arguments as Mount; return MaterialPageRoute( builder: (context) { - return EditMountScreen( - mount: mount, - title: - '${mount.provider} [${formatMountName(mount.type, mount.name)}] Settings', + return AuthCheck( + child: EditMountScreen( + mount: mount, + title: + '${mount.provider} [${formatMountName(mount.type, mount.name)}] Settings', + ), ); }, ); @@ -81,3 +98,22 @@ class _MyAppState extends State { ); } } + +class AuthCheck extends StatelessWidget { + final Widget child; + const AuthCheck({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, auth, __) { + if (!auth.authenticated) { + Navigator.of(context).pushReplacementNamed('/auth'); + return SizedBox.shrink(); + } + + return child; + }, + ); + } +} diff --git a/web/repertory/lib/models/auth.dart b/web/repertory/lib/models/auth.dart new file mode 100644 index 00000000..a92b2a34 --- /dev/null +++ b/web/repertory/lib/models/auth.dart @@ -0,0 +1,55 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:repertory/constants.dart' as constants; +import 'package:repertory/helpers.dart'; +import 'package:sodium_libs/sodium_libs.dart'; + +class Auth with ChangeNotifier { + bool _authenticated = false; + SecureKey? _key; + String _user = ""; + + bool get authenticated => _authenticated; + SecureKey get key => _key!; + + Future authenticate(String user, String password) async { + final sodium = constants.sodium; + if (sodium == null) { + return; + } + + final keyHash = sodium.crypto.genericHash( + outLen: sodium.crypto.aeadXChaCha20Poly1305IETF.keyBytes, + message: Uint8List.fromList(password.toCharArray()), + ); + + _authenticated = true; + _key = SecureKey.fromList(sodium, keyHash); + _user = user; + + notifyListeners(); + } + + Future createAuth() async { + try { + final response = await http.get( + Uri.parse(Uri.encodeFull('${getBaseUri()}/api/v1/nonce')), + ); + + if (response.statusCode != 200) { + return ""; + } + + final nonce = jsonDecode(response.body)["nonce"]; + debugPrint('nonce: $nonce'); + return encryptValue('${_user}_$nonce', key); + } catch (e) { + debugPrint('$e'); + } + + return ""; + } +} diff --git a/web/repertory/lib/models/mount.dart b/web/repertory/lib/models/mount.dart index 7b664786..5e9a1e8a 100644 --- a/web/repertory/lib/models/mount.dart +++ b/web/repertory/lib/models/mount.dart @@ -3,16 +3,18 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:repertory/helpers.dart'; +import 'package:repertory/models/auth.dart'; import 'package:repertory/models/mount_list.dart'; import 'package:repertory/types/mount_config.dart'; class Mount with ChangeNotifier { + final Auth _auth; final MountConfig mountConfig; final MountList? _mountList; bool _isMounting = false; bool _isRefreshing = false; - Mount(this.mountConfig, this._mountList, {isAdd = false}) { + Mount(this._auth, this.mountConfig, this._mountList, {isAdd = false}) { if (isAdd) { return; } @@ -29,9 +31,12 @@ class Mount with ChangeNotifier { Future _fetch() async { try { + final auth = await _auth.createAuth(); final response = await http.get( Uri.parse( - Uri.encodeFull('${getBaseUri()}/api/v1/mount?name=$name&type=$type'), + Uri.encodeFull( + '${getBaseUri()}/api/v1/mount?auth=$auth&name=$name&type=$type', + ), ), ); @@ -57,10 +62,11 @@ class Mount with ChangeNotifier { Future _fetchStatus() async { try { + final auth = await _auth.createAuth(); final response = await http.get( Uri.parse( Uri.encodeFull( - '${getBaseUri()}/api/v1/mount_status?name=$name&type=$type', + '${getBaseUri()}/api/v1/mount_status?auth=$auth&name=$name&type=$type', ), ), ); @@ -87,10 +93,11 @@ class Mount with ChangeNotifier { Future getMountLocation() async { try { + final auth = await _auth.createAuth(); final response = await http.get( Uri.parse( Uri.encodeFull( - '${getBaseUri()}/api/v1/mount_location?name=$name&type=$type', + '${getBaseUri()}/api/v1/mount_location?auth=$auth&name=$name&type=$type', ), ), ); @@ -120,10 +127,11 @@ class Mount with ChangeNotifier { await Future.delayed(Duration(seconds: 1)); } + final auth = await _auth.createAuth(); final response = await http.post( Uri.parse( Uri.encodeFull( - '${getBaseUri()}/api/v1/mount?unmount=$unmount&name=$name&type=$type&location=$location', + '${getBaseUri()}/api/v1/mount?auth=$auth&unmount=$unmount&name=$name&type=$type&location=$location', ), ), ); @@ -167,10 +175,11 @@ class Mount with ChangeNotifier { Future setValue(String key, String value) async { try { + final auth = await _auth.createAuth(); final response = await http.put( Uri.parse( Uri.encodeFull( - '${getBaseUri()}/api/v1/set_value_by_name?name=$name&type=$type&key=$key&value=$value', + '${getBaseUri()}/api/v1/set_value_by_name?auth=$auth&name=$name&type=$type&key=$key&value=$value', ), ), ); diff --git a/web/repertory/lib/models/mount_list.dart b/web/repertory/lib/models/mount_list.dart index d3627b74..b7e2921e 100644 --- a/web/repertory/lib/models/mount_list.dart +++ b/web/repertory/lib/models/mount_list.dart @@ -6,16 +6,21 @@ import 'package:flutter/material.dart' show ModalRoute; import 'package:http/http.dart' as http; import 'package:repertory/constants.dart' as constants; import 'package:repertory/helpers.dart'; +import 'package:repertory/models/auth.dart'; import 'package:repertory/models/mount.dart'; import 'package:repertory/types/mount_config.dart'; class MountList with ChangeNotifier { - MountList() { + final Auth _auth; + + MountList(this._auth) { _fetch(); } List _mountList = []; + Auth get auth => _auth; + UnmodifiableListView get items => UnmodifiableListView(_mountList); @@ -46,8 +51,9 @@ class MountList with ChangeNotifier { Future _fetch() async { try { + final auth = await _auth.createAuth(); final response = await http.get( - Uri.parse('${getBaseUri()}/api/v1/mount_list'), + Uri.parse('${getBaseUri()}/api/v1/mount_list?auth=$auth'), ); if (response.statusCode == 404) { @@ -64,7 +70,10 @@ class MountList with ChangeNotifier { jsonDecode(response.body).forEach((type, value) { nextList.addAll( value - .map((name) => Mount(MountConfig(type: type, name: name), this)) + .map( + (name) => + Mount(_auth, MountConfig(type: type, name: name), this), + ) .toList(), ); }); @@ -107,11 +116,15 @@ class MountList with ChangeNotifier { } try { - final map = await convertAllToString(jsonDecode(jsonEncode(mountConfig))); + final auth = await _auth.createAuth(); + final map = await convertAllToString( + jsonDecode(jsonEncode(mountConfig)), + _auth.key, + ); final response = await http.post( Uri.parse( Uri.encodeFull( - '${getBaseUri()}/api/v1/add_mount?name=$name&type=$type&config=${jsonEncode(map)}', + '${getBaseUri()}/api/v1/add_mount?auth=$auth&name=$name&type=$type&config=${jsonEncode(map)}', ), ), ); diff --git a/web/repertory/lib/screens/add_mount_screen.dart b/web/repertory/lib/screens/add_mount_screen.dart index 2fff8574..ac2672fe 100644 --- a/web/repertory/lib/screens/add_mount_screen.dart +++ b/web/repertory/lib/screens/add_mount_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:repertory/constants.dart' as constants; import 'package:repertory/helpers.dart'; +import 'package:repertory/models/auth.dart'; import 'package:repertory/models/mount.dart'; import 'package:repertory/models/mount_list.dart'; import 'package:repertory/types/mount_config.dart'; @@ -49,147 +50,158 @@ class _AddMountScreenState extends State { ), body: Padding( padding: const EdgeInsets.all(constants.padding), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Card( - margin: EdgeInsets.all(0.0), - child: Padding( - padding: const EdgeInsets.all(constants.padding), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Provider Type'), - const SizedBox(width: constants.padding), - DropdownButton( - autofocus: true, - value: _mountType, - onChanged: (mountType) => _handleChange(mountType ?? ''), - items: - constants.providerTypeList - .map>((item) { - return DropdownMenuItem( - value: item, - child: Text(item), - ); - }) - .toList(), - ), - ], - ), - ), - ), - if (_mountType.isNotEmpty && _mountType != 'Remote') - const SizedBox(height: constants.padding), - if (_mountType.isNotEmpty && _mountType != 'Remote') - Card( - margin: EdgeInsets.all(0.0), - child: Padding( - padding: const EdgeInsets.all(constants.padding), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Configuration Name'), - const SizedBox(width: constants.padding), - TextField( - autofocus: true, - controller: _mountNameController, - keyboardType: TextInputType.text, - inputFormatters: [ - FilteringTextInputFormatter.deny(RegExp(r'\s')), - ], - onChanged: (_) => _handleChange(_mountType), - ), - ], - ), - ), - ), - if (_mount != null) const SizedBox(height: constants.padding), - if (_mount != null) - Expanded( - child: Card( + child: Consumer( + builder: (context, auth, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Card( margin: EdgeInsets.all(0.0), child: Padding( padding: const EdgeInsets.all(constants.padding), - child: MountSettingsWidget( - isAdd: true, - mount: _mount!, - settings: _settings[_mountType]!, - showAdvanced: _showAdvanced, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Provider Type'), + const SizedBox(width: constants.padding), + DropdownButton( + autofocus: true, + value: _mountType, + onChanged: + (mountType) => + _handleChange(auth, mountType ?? ''), + items: + constants.providerTypeList + .map>((item) { + return DropdownMenuItem( + value: item, + child: Text(item), + ); + }) + .toList(), + ), + ], ), ), ), - ), - if (_mount != null) const SizedBox(height: constants.padding), - if (_mount != null) - Builder( - builder: (context) { - return ElevatedButton.icon( - onPressed: () async { - final mountList = Provider.of(context); + if (_mountType.isNotEmpty && _mountType != 'Remote') + const SizedBox(height: constants.padding), + if (_mountType.isNotEmpty && _mountType != 'Remote') + Card( + margin: EdgeInsets.all(0.0), + child: Padding( + padding: const EdgeInsets.all(constants.padding), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Configuration Name'), + const SizedBox(width: constants.padding), + TextField( + autofocus: true, + controller: _mountNameController, + keyboardType: TextInputType.text, + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp(r'\s')), + ], + onChanged: (_) => _handleChange(auth, _mountType), + ), + ], + ), + ), + ), + if (_mount != null) const SizedBox(height: constants.padding), + if (_mount != null) + Expanded( + child: Card( + margin: EdgeInsets.all(0.0), + child: Padding( + padding: const EdgeInsets.all(constants.padding), + child: MountSettingsWidget( + isAdd: true, + mount: _mount!, + settings: _settings[_mountType]!, + showAdvanced: _showAdvanced, + ), + ), + ), + ), + if (_mount != null) const SizedBox(height: constants.padding), + if (_mount != null) + Builder( + builder: (context) { + return ElevatedButton.icon( + onPressed: () async { + final mountList = Provider.of(context); - List failed = []; - if (!validateSettings(_settings[_mountType]!, failed)) { - for (var key in failed) { - displayErrorMessage( - context, - "Setting '$key' is not valid", + List failed = []; + if (!validateSettings( + _settings[_mountType]!, + failed, + )) { + for (var key in failed) { + displayErrorMessage( + context, + "Setting '$key' is not valid", + ); + } + return; + } + + if (mountList.hasConfigName( + _mountNameController.text, + )) { + return displayErrorMessage( + context, + "Configuration name '${_mountNameController.text}' already exists", + ); + } + + if (_mountType == "Sia" || _mountType == "S3") { + final bucket = + _settings[_mountType]!["${_mountType}Config"]["Bucket"] + as String; + if (mountList.hasBucketName(_mountType, bucket)) { + return displayErrorMessage( + context, + "Bucket '$bucket' already exists", + ); + } + } + + final success = await mountList.add( + _mountType, + _mountType == 'Remote' + ? '${_settings[_mountType]!['RemoteConfig']['HostNameOrIp']}_${_settings[_mountType]!['RemoteConfig']['ApiPort']}' + : _mountNameController.text, + _settings[_mountType]!, ); - } - return; - } - if (mountList.hasConfigName(_mountNameController.text)) { - return displayErrorMessage( - context, - "Configuration name '${_mountNameController.text}' already exists", - ); - } + if (!success || !context.mounted) { + return; + } - if (_mountType == "Sia" || _mountType == "S3") { - final bucket = - _settings[_mountType]!["${_mountType}Config"]["Bucket"] - as String; - if (mountList.hasBucketName(_mountType, bucket)) { - return displayErrorMessage( - context, - "Bucket '$bucket' already exists", - ); - } - } - - final success = await mountList.add( - _mountType, - _mountType == 'Remote' - ? '${_settings[_mountType]!['RemoteConfig']['HostNameOrIp']}_${_settings[_mountType]!['RemoteConfig']['ApiPort']}' - : _mountNameController.text, - _settings[_mountType]!, + Navigator.pop(context); + }, + label: const Text('Add'), + icon: const Icon(Icons.add), ); - - if (!success || !context.mounted) { - return; - } - - Navigator.pop(context); }, - label: const Text('Add'), - icon: const Icon(Icons.add), - ); - }, - ), - ], + ), + ], + ); + }, ), ), ); } - void _handleChange(String mountType) { + void _handleChange(Auth auth, String mountType) { setState(() { final changed = _mountType != mountType; @@ -204,6 +216,7 @@ class _AddMountScreenState extends State { (_mountNameController.text.isEmpty) ? null : Mount( + auth, MountConfig( name: _mountNameController.text, settings: _settings[mountType], diff --git a/web/repertory/lib/screens/auth_screen.dart b/web/repertory/lib/screens/auth_screen.dart new file mode 100644 index 00000000..7cc483a2 --- /dev/null +++ b/web/repertory/lib/screens/auth_screen.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:repertory/constants.dart' as constants; +import 'package:repertory/models/auth.dart'; + +class AuthScreen extends StatefulWidget { + final String title; + const AuthScreen({super.key, required this.title}); + + @override + State createState() => _AuthScreenState(); +} + +class _AuthScreenState extends State { + bool _enabled = true; + final _passwordController = TextEditingController(); + final _userController = TextEditingController(); + + @override + Widget build(context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(widget.title), + ), + body: Consumer( + builder: (context, auth, _) { + if (auth.authenticated) { + Navigator.of(context).pushReplacementNamed('/'); + return SizedBox.shrink(); + } + + return Center( + child: Card( + child: Padding( + padding: const EdgeInsets.all(constants.padding), + child: SizedBox( + width: 400, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Logon to Repertory Portal', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: constants.padding), + TextField( + decoration: InputDecoration(labelText: 'Username'), + controller: _userController, + ), + const SizedBox(height: constants.padding), + TextField( + obscureText: true, + decoration: InputDecoration(labelText: 'Password'), + controller: _passwordController, + ), + const SizedBox(height: constants.padding), + ElevatedButton( + onPressed: + _enabled + ? () async { + setState(() => _enabled = false); + await auth.authenticate( + _userController.text, + _passwordController.text, + ); + setState(() => _enabled = true); + } + : null, + child: const Text('Login'), + ), + ], + ), + ), + ), + ), + ); + }, + ), + ); + } + + @override + void setState(VoidCallback fn) { + if (!mounted) { + return; + } + + super.setState(fn); + } +} diff --git a/web/repertory/lib/screens/edit_settings_screen.dart b/web/repertory/lib/screens/edit_settings_screen.dart index 658294fc..13f3f632 100644 --- a/web/repertory/lib/screens/edit_settings_screen.dart +++ b/web/repertory/lib/screens/edit_settings_screen.dart @@ -2,7 +2,9 @@ import 'dart:convert' show jsonDecode, jsonEncode; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; +import 'package:provider/provider.dart'; import 'package:repertory/helpers.dart'; +import 'package:repertory/models/auth.dart'; import 'package:repertory/widgets/ui_settings.dart'; class EditSettingsScreen extends StatefulWidget { @@ -41,8 +43,9 @@ class _EditSettingsScreenState extends State { Future> _grabSettings() async { try { + final auth = await Provider.of(context, listen: false).createAuth(); final response = await http.get( - Uri.parse('${getBaseUri()}/api/v1/settings'), + Uri.parse('${getBaseUri()}/api/v1/settings?auth=$auth'), ); if (response.statusCode != 200) { diff --git a/web/repertory/lib/widgets/mount_settings.dart b/web/repertory/lib/widgets/mount_settings.dart index 87252891..ad923450 100644 --- a/web/repertory/lib/widgets/mount_settings.dart +++ b/web/repertory/lib/widgets/mount_settings.dart @@ -7,6 +7,7 @@ import 'package:repertory/helpers.dart' getChanged, getSettingDescription, getSettingValidators; +import 'package:repertory/models/auth.dart'; import 'package:repertory/models/mount.dart'; import 'package:repertory/models/mount_list.dart'; import 'package:repertory/settings.dart'; @@ -622,7 +623,8 @@ class _MountSettingsWidgetState extends State { widget.settings, ); if (settings.isNotEmpty) { - convertAllToString(settings).then((map) { + final authProvider = Provider.of(context, listen: false); + convertAllToString(settings, authProvider.key).then((map) { map.forEach((key, value) { if (value is Map) { value.forEach((subKey, subValue) { diff --git a/web/repertory/lib/widgets/ui_settings.dart b/web/repertory/lib/widgets/ui_settings.dart index 5ffcadc6..47704c29 100644 --- a/web/repertory/lib/widgets/ui_settings.dart +++ b/web/repertory/lib/widgets/ui_settings.dart @@ -2,6 +2,7 @@ import 'dart:convert' show jsonEncode; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; +import 'package:provider/provider.dart'; import 'package:repertory/helpers.dart' show convertAllToString, @@ -11,6 +12,7 @@ import 'package:repertory/helpers.dart' getSettingDescription, getSettingValidators, trimNotEmptyValidator; +import 'package:repertory/models/auth.dart'; import 'package:repertory/settings.dart'; import 'package:settings_ui/settings_ui.dart'; @@ -103,13 +105,15 @@ class _UISettingsWidgetState extends State { void dispose() { final settings = getChanged(widget.origSettings, widget.settings); if (settings.isNotEmpty) { - convertAllToString(settings) + final authProvider = Provider.of(context, listen: false); + convertAllToString(settings, authProvider.key) .then((map) async { try { + final auth = await authProvider.createAuth(); final response = await http.put( Uri.parse( Uri.encodeFull( - '${getBaseUri()}/api/v1/settings?data=${jsonEncode(map)}', + '${getBaseUri()}/api/v1/settings?auth=$auth&data=${jsonEncode(map)}', ), ), );