partial logon support
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good

This commit is contained in:
Scott E. Graves 2025-03-22 01:25:30 -05:00
parent 40e57f3262
commit 5b09333f0d
12 changed files with 526 additions and 199 deletions

View File

@ -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<std::string, std::recursive_mutex> mtx_lookup_;
std::mutex nonce_mtx_;
std::unordered_map<std::string, nonce_data> nonce_lookup_;
std::condition_variable nonce_notify_;
std::unique_ptr<std::thread> 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<std::string> args,
bool background = false) const
-> std::vector<std::string>;
void removed_expired_nonces();
void set_key_value(provider_type prov, std::string_view name,
std::string_view key, std::string_view value) const;
};

View File

@ -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<const unsigned char *>(
&decoded.at(crypto_aead_xchacha20poly1305_IETF_NPUBBYTES)),
decoded.size() - crypto_aead_xchacha20poly1305_IETF_NPUBBYTES,
reinterpret_cast<const unsigned char *>(REPERTORY.data()), 9U,
reinterpret_cast<const unsigned char *>(REPERTORY.data()),
REPERTORY.length(),
reinterpret_cast<const unsigned char *>(decoded.data()),
reinterpret_cast<const unsigned char *>(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<std::thread>([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::seconds>(
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 {

View File

@ -246,8 +246,8 @@ bool validateSettings(
Future<Map<String, dynamic>> convertAllToString(
Map<String, dynamic> settings,
SecureKey key,
) async {
String? password;
Future<Map<String, dynamic>> convert(Map<String, dynamic> settings) async {
for (var entry in settings.entries) {
if (entry.value is Map<String, dynamic>) {
@ -262,14 +262,7 @@ Future<Map<String, dynamic>> 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<Map<String, dynamic>> 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,
);

View File

@ -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<MyApp> {
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<MyApp> {
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<MyApp> {
);
}
}
class AuthCheck extends StatelessWidget {
final Widget child;
const AuthCheck({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Consumer<Auth>(
builder: (context, auth, __) {
if (!auth.authenticated) {
Navigator.of(context).pushReplacementNamed('/auth');
return SizedBox.shrink();
}
return child;
},
);
}
}

View File

@ -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<void> 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<String> 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 "";
}
}

View File

@ -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<void> _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<void> _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<String?> 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<void> 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',
),
),
);

View File

@ -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<Mount> _mountList = [];
Auth get auth => _auth;
UnmodifiableListView<Mount> get items =>
UnmodifiableListView<Mount>(_mountList);
@ -46,8 +51,9 @@ class MountList with ChangeNotifier {
Future<void> _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)}',
),
),
);

View File

@ -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<AddMountScreen> {
),
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<String>(
autofocus: true,
value: _mountType,
onChanged: (mountType) => _handleChange(mountType ?? ''),
items:
constants.providerTypeList
.map<DropdownMenuItem<String>>((item) {
return DropdownMenuItem<String>(
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<Auth>(
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<String>(
autofocus: true,
value: _mountType,
onChanged:
(mountType) =>
_handleChange(auth, mountType ?? ''),
items:
constants.providerTypeList
.map<DropdownMenuItem<String>>((item) {
return DropdownMenuItem<String>(
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<MountList>(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<MountList>(context);
List<String> failed = [];
if (!validateSettings(_settings[_mountType]!, failed)) {
for (var key in failed) {
displayErrorMessage(
context,
"Setting '$key' is not valid",
List<String> 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<AddMountScreen> {
(_mountNameController.text.isEmpty)
? null
: Mount(
auth,
MountConfig(
name: _mountNameController.text,
settings: _settings[mountType],

View File

@ -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<AuthScreen> createState() => _AuthScreenState();
}
class _AuthScreenState extends State<AuthScreen> {
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<Auth>(
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);
}
}

View File

@ -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<EditSettingsScreen> {
Future<Map<String, dynamic>> _grabSettings() async {
try {
final auth = await Provider.of<Auth>(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) {

View File

@ -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<MountSettingsWidget> {
widget.settings,
);
if (settings.isNotEmpty) {
convertAllToString(settings).then((map) {
final authProvider = Provider.of<Auth>(context, listen: false);
convertAllToString(settings, authProvider.key).then((map) {
map.forEach((key, value) {
if (value is Map<String, dynamic>) {
value.forEach((subKey, subValue) {

View File

@ -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<UISettingsWidget> {
void dispose() {
final settings = getChanged(widget.origSettings, widget.settings);
if (settings.isNotEmpty) {
convertAllToString(settings)
final authProvider = Provider.of<Auth>(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)}',
),
),
);