From d240e493b143659005f4acdf23fb07fde92620b0 Mon Sep 17 00:00:00 2001 From: inexcode Date: Thu, 3 Mar 2022 20:38:30 +0300 Subject: [PATCH] Add user synchronization and SSH keys screen --- assets/translations/en.json | 4 +- assets/translations/ru.json | 9 +- lib/config/hive_config.dart | 7 +- lib/logic/api_maps/hetzner.dart | 2 +- lib/logic/api_maps/server.dart | 200 +++++++++--- .../cubit/dns_records/dns_records_cubit.dart | 2 +- .../cubit/forms/user/user_form_cubit.dart | 3 +- lib/logic/cubit/jobs/jobs_cubit.dart | 14 +- lib/logic/cubit/users/users_cubit.dart | 290 +++++++++++++++++- lib/logic/cubit/users/users_state.dart | 18 +- lib/logic/models/dns_records.dart | 2 +- lib/logic/models/dns_records.g.dart | 3 +- lib/logic/models/job.dart | 25 +- lib/logic/models/user.dart | 29 +- lib/logic/models/user.g.dart | 15 +- lib/ui/pages/more/more.dart | 232 +------------- lib/ui/pages/ssh_keys/ssh_keys.dart | 75 +++++ lib/ui/pages/users/user.dart | 6 +- lib/ui/pages/users/user_details.dart | 33 +- lib/ui/pages/users/users.dart | 38 +-- pubspec.lock | 81 +++-- pubspec.yaml | 62 ++-- 22 files changed, 756 insertions(+), 394 deletions(-) create mode 100644 lib/ui/pages/ssh_keys/ssh_keys.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index 94fd392e..4a88a948 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -301,7 +301,9 @@ "upgradeSuccess": "Server upgrade started", "upgradeFailed": "Failed to upgrade server", "upgradeServer": "Upgrade server", - "rebootServer": "Reboot server" + "rebootServer": "Reboot server", + "create_ssh_key": "Create SSH key for {}", + "delete_ssh_key": "Delete SSH key for {}" }, "validations": { "required": "Required", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 9325e855..081b328a 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -62,6 +62,11 @@ "6": "Действие приведет к удалению сервера. После этого он будет недоступен." } }, + "ssh": { + "title": "SSH ключи", + "create": "Добавить SSH ключ", + "delete": "Удалить SSH ключ" + }, "onboarding": { "_comment": "страницы онбординга", "page1_title": "Цифровая независимость доступна каждому", @@ -302,7 +307,9 @@ "upgradeSuccess": "Запущено обновление сервера", "upgradeFailed": "Обновить сервер не вышло", "upgradeServer": "Обновить сервер", - "rebootServer": "Перезагрузить сервер" + "rebootServer": "Перезагрузить сервер", + "create_ssh_key": "Создать SSH ключ для {}", + "delete_ssh_key": "Удалить SSH ключ для {}" }, "validations": { "required": "Обязательное поле.", diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index 769c7fa1..e7ed84e3 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -23,14 +23,14 @@ class HiveConfig { await Hive.openBox(BNames.users); await Hive.openBox(BNames.servicesState); - var cipher = HiveAesCipher(await getEncriptedKey(BNames.key)); + var cipher = HiveAesCipher(await getEncryptedKey(BNames.key)); await Hive.openBox(BNames.appConfig, encryptionCipher: cipher); - var sshCipher = HiveAesCipher(await getEncriptedKey(BNames.sshEnckey)); + var sshCipher = HiveAesCipher(await getEncryptedKey(BNames.sshEnckey)); await Hive.openBox(BNames.sshConfig, encryptionCipher: sshCipher); } - static Future getEncriptedKey(String encKey) async { + static Future getEncryptedKey(String encKey) async { final secureStorage = FlutterSecureStorage(); var hasEncryptionKey = await secureStorage.containsKey(key: encKey); if (!hasEncryptionKey) { @@ -48,6 +48,7 @@ class BNames { static String isDarkModeOn = 'isDarkModeOn'; static String isOnbordingShowing = 'isOnbordingShowing'; static String users = 'users'; + static String rootKeys = 'rootKeys'; static String appSettings = 'appSettings'; static String servicesState = 'servicesState'; diff --git a/lib/logic/api_maps/hetzner.dart b/lib/logic/api_maps/hetzner.dart index 13de3ff6..1f906f85 100644 --- a/lib/logic/api_maps/hetzner.dart +++ b/lib/logic/api_maps/hetzner.dart @@ -135,7 +135,7 @@ class HetznerApi extends ApiMap { /// check the branch name, it could be "development" or "master". /// final userdataString = - "#cloud-config\nruncmd:\n- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/master/nixos-infect | PROVIDER=hetzner NIX_CHANNEL=nixos-21.05 DOMAIN='$domainName' LUSER='${escapeQuotes(rootUser.login)}' PASSWORD='${escapeQuotes(rootUser.password)}' CF_TOKEN=$cloudFlareKey DB_PASSWORD=${escapeQuotes(dbPassword)} API_TOKEN=$apiToken HOSTNAME=${escapeQuotes(hostname)} bash 2>&1 | tee /tmp/infect.log"; + "#cloud-config\nruncmd:\n- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/master/nixos-infect | PROVIDER=hetzner NIX_CHANNEL=nixos-21.05 DOMAIN='$domainName' LUSER='${escapeQuotes(rootUser.login)}' PASSWORD='${escapeQuotes(rootUser.password ?? 'PASS')}' CF_TOKEN=$cloudFlareKey DB_PASSWORD=${escapeQuotes(dbPassword)} API_TOKEN=$apiToken HOSTNAME=${escapeQuotes(hostname)} bash 2>&1 | tee /tmp/infect.log"; print(userdataString); final data = { diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 2174baf7..036f460f 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -13,6 +13,20 @@ import 'package:selfprivacy/logic/models/user.dart'; import 'api_map.dart'; +class ApiResponse { + final int statusCode; + final String? errorMessage; + final D data; + + get isSuccess => statusCode >= 200 && statusCode < 300; + + ApiResponse({ + required this.statusCode, + this.errorMessage, + required this.data, + }); +} + class ServerApi extends ApiMap { bool hasLogger; bool isWithToken; @@ -50,31 +64,151 @@ class ServerApi extends ApiMap { return res; } - Future createUser(User user) async { - bool res; + Future> createUser(User user) async { Response response; var client = await getClient(); // POST request with JSON body containing username and password - try { - response = await client.post( - '/users', - data: { - 'username': user.login, - 'password': user.password, - }, - options: Options( - contentType: 'application/json', + + response = await client.post( + '/users', + data: { + 'username': user.login, + 'password': user.password, + }, + options: Options( + contentType: 'application/json', + ), + ); + + close(client); + + if (response.statusCode == HttpStatus.created) { + return ApiResponse( + statusCode: response.statusCode ?? HttpStatus.internalServerError, + data: User( + login: user.login, + password: user.password, + isFoundOnServer: true, ), ); - res = response.statusCode == HttpStatus.created; + } else { + return ApiResponse( + statusCode: response.statusCode ?? HttpStatus.internalServerError, + data: User( + login: user.login, + password: user.password, + isFoundOnServer: false, + note: response.data['message'] ?? null, + ), + errorMessage: response.data?.containsKey('error') ?? false + ? response.data['error'] + : null, + ); + } + } + + Future>> getUsersList() async { + List res; + Response response; + + var client = await getClient(); + response = await client.get('/users'); + try { + res = (json.decode(response.data) as List) + .map((e) => e as String) + .toList(); } catch (e) { - print(e); - res = false; + res = []; } close(client); - return res; + return ApiResponse>( + statusCode: response.statusCode ?? HttpStatus.internalServerError, + data: res, + ); + } + + Future> addUserSshKey(User user, String sshKey) async { + Response response; + + var client = await getClient(); + response = await client.post( + '/services/ssh/keys/${user.login}', + data: { + 'public_key': sshKey, + }, + ); + + close(client); + return ApiResponse( + statusCode: response.statusCode ?? HttpStatus.internalServerError, + data: null, + errorMessage: response.data?.containsKey('error') ?? false + ? response.data['error'] + : null, + ); + } + + Future> addRootSshKey(String ssh) async { + Response response; + + var client = await getClient(); + response = await client.put( + '/services/ssh/key/send', + data: {"public_key": ssh}, + ); + close(client); + + return ApiResponse( + statusCode: response.statusCode ?? HttpStatus.internalServerError, + data: null, + errorMessage: response.data?.containsKey('error') ?? false + ? response.data['error'] + : null, + ); + } + + Future>> getUserSshKeys(User user) async { + List res; + Response response; + + var client = await getClient(); + response = await client.get('/services/ssh/keys/${user.login}'); + try { + res = (response.data as List).map((e) => e as String).toList(); + } catch (e) { + print(e); + res = []; + } + + close(client); + return ApiResponse>( + statusCode: response.statusCode ?? HttpStatus.internalServerError, + data: res, + errorMessage: response.data is List + ? null + : response.data?.containsKey('error') ?? false + ? response.data['error'] + : null, + ); + } + + Future> deleteUserSshKey(User user, String sshKey) async { + Response response; + + var client = await getClient(); + response = await client.delete('/services/ssh/keys/${user.login}', + data: {"public_key": sshKey}); + close(client); + + return ApiResponse( + statusCode: response.statusCode ?? HttpStatus.internalServerError, + data: null, + errorMessage: response.data?.containsKey('error') ?? false + ? response.data['error'] + : null, + ); } Future deleteUser(User user) async { @@ -89,7 +223,8 @@ class ServerApi extends ApiMap { contentType: 'application/json', ), ); - res = response.statusCode == HttpStatus.ok; + res = response.statusCode == HttpStatus.ok || + response.statusCode == HttpStatus.notFound; } catch (e) { print(e); res = false; @@ -125,16 +260,7 @@ class ServerApi extends ApiMap { Future switchService(ServiceTypes type, bool needToTurnOn) async { var client = await getClient(); client.post('/services/${type.url}/${needToTurnOn ? 'enable' : 'disable'}'); - client.close(); - } - - Future sendSsh(String ssh) async { - var client = await getClient(); - client.put( - '/services/ssh/key/send', - data: {"public_key": ssh}, - ); - client.close(); + close(client); } Future> servicesPowerCheck() async { @@ -161,13 +287,13 @@ class ServerApi extends ApiMap { 'bucket': bucket.bucketName, }, ); - client.close(); + close(client); } Future startBackup() async { var client = await getClient(); client.put('/services/restic/backup/create'); - client.close(); + close(client); } Future> getBackups() async { @@ -210,13 +336,13 @@ class ServerApi extends ApiMap { Future forceBackupListReload() async { var client = await getClient(); client.get('/services/restic/backup/reload'); - client.close(); + close(client); } Future restoreBackup(String backupId) async { var client = await getClient(); client.put('/services/restic/backup/restore', data: {'backupId': backupId}); - client.close(); + close(client); } Future pullConfigurationUpdate() async { @@ -229,21 +355,21 @@ class ServerApi extends ApiMap { Future reboot() async { var client = await getClient(); Response response = await client.get('/system/reboot'); - client.close(); + close(client); return response.statusCode == HttpStatus.ok; } Future upgrade() async { var client = await getClient(); Response response = await client.get('/system/configuration/upgrade'); - client.close(); + close(client); return response.statusCode == HttpStatus.ok; } Future getAutoUpgradeSettings() async { var client = await getClient(); Response response = await client.get('/system/configuration/autoUpgrade'); - client.close(); + close(client); return AutoUpgradeSettings.fromJson(response.data); } @@ -253,13 +379,13 @@ class ServerApi extends ApiMap { '/system/configuration/autoUpgrade', data: settings.toJson(), ); - client.close(); + close(client); } Future getServerTimezone() async { var client = await getClient(); Response response = await client.get('/system/configuration/timezone'); - client.close(); + close(client); return TimeZoneSettings.fromString(response.data); } @@ -270,13 +396,13 @@ class ServerApi extends ApiMap { '/system/configuration/timezone', data: settings.toJson(), ); - client.close(); + close(client); } Future getDkim() async { var client = await getClient(); Response response = await client.get('/services/mailserver/dkim'); - client.close(); + close(client); // if got 404 raise exception if (response.statusCode == HttpStatus.notFound) { diff --git a/lib/logic/cubit/dns_records/dns_records_cubit.dart b/lib/logic/cubit/dns_records/dns_records_cubit.dart index f35a9125..227ac227 100644 --- a/lib/logic/cubit/dns_records/dns_records_cubit.dart +++ b/lib/logic/cubit/dns_records/dns_records_cubit.dart @@ -79,7 +79,7 @@ class DnsRecordsCubit extends AppConfigDependendCubit { @override void onChange(Change change) { - print(change); + // print(change); super.onChange(change); } diff --git a/lib/logic/cubit/forms/user/user_form_cubit.dart b/lib/logic/cubit/forms/user/user_form_cubit.dart index 4bb78eeb..24f67437 100644 --- a/lib/logic/cubit/forms/user/user_form_cubit.dart +++ b/lib/logic/cubit/forms/user/user_form_cubit.dart @@ -34,7 +34,8 @@ class UserFormCubit extends FormCubit { ); password = FieldCubit( - initalValue: isEdit ? user!.password : StringGenerators.userPassword(), + initalValue: + isEdit ? (user?.password ?? '') : StringGenerators.userPassword(), validations: [ RequiredStringValidation('validations.required'.tr()), ValidationModel((s) => passwordRegExp.hasMatch(s), diff --git a/lib/logic/cubit/jobs/jobs_cubit.dart b/lib/logic/cubit/jobs/jobs_cubit.dart index b093ba08..6d91d4c4 100644 --- a/lib/logic/cubit/jobs/jobs_cubit.dart +++ b/lib/logic/cubit/jobs/jobs_cubit.dart @@ -1,3 +1,5 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; @@ -5,10 +7,9 @@ import 'package:selfprivacy/logic/cubit/services/services_cubit.dart'; import 'package:selfprivacy/logic/cubit/users/users_cubit.dart'; import 'package:selfprivacy/logic/get_it/ssh.dart'; import 'package:selfprivacy/logic/models/job.dart'; -import 'package:equatable/equatable.dart'; import 'package:selfprivacy/logic/models/user.dart'; + export 'package:provider/provider.dart'; -import 'package:easy_localization/easy_localization.dart'; part 'jobs_state.dart'; @@ -103,12 +104,10 @@ class JobsCubit extends Cubit { var hasServiceJobs = false; for (var job in jobs) { if (job is CreateUserJob) { - newUsers.add(job.user); - await api.createUser(job.user); + await usersCubit.createUser(job.user); } if (job is DeleteUserJob) { - final deleted = await api.deleteUser(job.user); - if (deleted) usersCubit.remove(job.user); + await usersCubit.deleteUser(job.user); } if (job is ServiceToggleJob) { hasServiceJobs = true; @@ -116,11 +115,10 @@ class JobsCubit extends Cubit { } if (job is CreateSSHKeyJob) { await getIt().generateKeys(); - api.sendSsh(getIt().savedPubKey!); + api.addRootSshKey(getIt().savedPubKey!); } } - usersCubit.addUsers(newUsers); await api.pullConfigurationUpdate(); await api.apply(); if (hasServiceJobs) { diff --git a/lib/logic/cubit/users/users_cubit.dart b/lib/logic/cubit/users/users_cubit.dart index a38db016..cc228b37 100644 --- a/lib/logic/cubit/users/users_cubit.dart +++ b/lib/logic/cubit/users/users_cubit.dart @@ -3,35 +3,295 @@ import 'package:equatable/equatable.dart'; import 'package:hive/hive.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/models/user.dart'; + +import '../../api_maps/server.dart'; + export 'package:provider/provider.dart'; part 'users_state.dart'; class UsersCubit extends Cubit { - UsersCubit() : super(UsersState([])); + UsersCubit() + : super(UsersState( + [], User(login: 'root'), User(login: 'loading...'))); Box box = Hive.box(BNames.users); + Box configBox = Hive.box(BNames.appConfig); - void load() async { + final api = ServerApi(); + + Future load() async { var loadedUsers = box.values.toList(); - + final primaryUser = + configBox.get(BNames.rootUser, defaultValue: User(login: 'loading...')); + List rootKeys = [ + ...configBox.get(BNames.rootKeys, defaultValue: []) + ]; if (loadedUsers.isNotEmpty) { - emit(UsersState(loadedUsers)); + emit(UsersState( + loadedUsers, User(login: 'root', sshKeys: rootKeys), primaryUser)); + } + + final usersFromServer = await api.getUsersList(); + if (usersFromServer.isSuccess) { + final updatedList = + mergeLocalAndServerUsers(loadedUsers, usersFromServer.data); + emit(UsersState( + updatedList, User(login: 'root', sshKeys: rootKeys), primaryUser)); + } + + final usersWithSshKeys = await loadSshKeys(state.users); + // Update the users it the box + box.clear(); + box.addAll(usersWithSshKeys); + + final rootUserWithSshKeys = (await loadSshKeys([state.rootUser])).first; + configBox.put(BNames.rootKeys, rootUserWithSshKeys.sshKeys); + final primaryUserWithSshKeys = + (await loadSshKeys([state.primaryUser])).first; + configBox.put(BNames.rootUser, primaryUserWithSshKeys); + emit(UsersState( + usersWithSshKeys, rootUserWithSshKeys, primaryUserWithSshKeys)); + } + + List mergeLocalAndServerUsers( + List localUsers, List serverUsers) { + // If local user not exists on server, add it with isFoundOnServer = false + // If server user not exists on local, add it + + List mergedUsers = []; + List serverUsersCopy = List.from(serverUsers); + + for (var localUser in localUsers) { + if (serverUsersCopy.contains(localUser.login)) { + mergedUsers.add(User( + login: localUser.login, + isFoundOnServer: true, + password: localUser.password, + sshKeys: localUser.sshKeys, + )); + serverUsersCopy.remove(localUser.login); + } else { + mergedUsers.add(User( + login: localUser.login, + isFoundOnServer: false, + password: localUser.password, + note: localUser.note, + )); + } + } + + for (var serverUser in serverUsersCopy) { + mergedUsers.add(User( + login: serverUser, + isFoundOnServer: true, + )); + } + + return mergedUsers; + } + + Future> loadSshKeys(List users) async { + List updatedUsers = []; + + for (var user in users) { + if (user.isFoundOnServer || + user.login == 'root' || + user.login == state.primaryUser.login) { + final sshKeys = await api.getUserSshKeys(user); + print('sshKeys for $user: ${sshKeys.data}'); + if (sshKeys.isSuccess) { + updatedUsers.add(User( + login: user.login, + isFoundOnServer: true, + password: user.password, + sshKeys: sshKeys.data, + note: user.note, + )); + } else { + updatedUsers.add(User( + login: user.login, + isFoundOnServer: true, + password: user.password, + note: user.note, + )); + } + } else { + updatedUsers.add(User( + login: user.login, + isFoundOnServer: false, + password: user.password, + note: user.note, + )); + } + } + return updatedUsers; + } + + Future refresh() async { + List updatedUsers = state.users; + final usersFromServer = await api.getUsersList(); + if (usersFromServer.isSuccess) { + updatedUsers = + mergeLocalAndServerUsers(state.users, usersFromServer.data); + } + final usersWithSshKeys = await loadSshKeys(updatedUsers); + box.clear(); + box.addAll(usersWithSshKeys); + final rootUserWithSshKeys = (await loadSshKeys([state.rootUser])).first; + configBox.put(BNames.rootKeys, rootUserWithSshKeys.sshKeys); + final primaryUserWithSshKeys = + (await loadSshKeys([state.primaryUser])).first; + configBox.put(BNames.rootUser, primaryUserWithSshKeys); + emit(UsersState( + usersWithSshKeys, rootUserWithSshKeys, primaryUserWithSshKeys)); + return; + } + + Future createUser(User user) async { + // If user exists on server, do nothing + if (state.users.any((u) => u.login == user.login && u.isFoundOnServer)) { + return; + } + // If user is root or primary user, do nothing + if (user.login == 'root' || user.login == state.primaryUser.login) { + return; + } + final result = await api.createUser(user); + await box.add(result.data); + emit(state.copyWith(users: box.values.toList())); + } + + Future deleteUser(User user) async { + // If user is primary or root, don't delete + if (user.login == state.primaryUser.login || user.login == 'root') { + return; + } + final result = await api.deleteUser(user); + if (result) { + await box.deleteAt(box.values.toList().indexOf(user)); + emit(state.copyWith(users: box.values.toList())); } } - void addUsers(List users) async { - var newUserList = [...state.users, ...users]; - - await box.addAll(users); - emit(UsersState(newUserList)); + Future addSshKey(User user, String publicKey) async { + // If adding root key, use api.addRootSshKey + // Otherwise, use api.addUserSshKey + if (user.login == 'root') { + final result = await api.addRootSshKey(publicKey); + if (result.isSuccess) { + // Add ssh key to the array of root keys + final rootKeys = + configBox.get(BNames.rootKeys, defaultValue: []) as List; + rootKeys.add(publicKey); + configBox.put(BNames.rootKeys, rootKeys); + emit(state.copyWith( + rootUser: User( + login: state.rootUser.login, + isFoundOnServer: true, + password: state.rootUser.password, + sshKeys: rootKeys, + note: state.rootUser.note, + ), + )); + } + } else { + final result = await api.addUserSshKey(user, publicKey); + if (result.isSuccess) { + // If it is primary user, update primary user + if (user.login == state.primaryUser.login) { + List primaryUserKeys = state.primaryUser.sshKeys; + primaryUserKeys.add(publicKey); + final updatedUser = User( + login: state.primaryUser.login, + isFoundOnServer: true, + password: state.primaryUser.password, + sshKeys: primaryUserKeys, + note: state.primaryUser.note, + ); + configBox.put(BNames.rootUser, updatedUser); + emit(state.copyWith( + primaryUser: updatedUser, + )); + } else { + // If it is not primary user, update user + List userKeys = user.sshKeys; + userKeys.add(publicKey); + final updatedUser = User( + login: user.login, + isFoundOnServer: true, + password: user.password, + sshKeys: userKeys, + note: user.note, + ); + await box.putAt(box.values.toList().indexOf(user), updatedUser); + emit(state.copyWith( + users: box.values.toList(), + )); + } + } + } } - void remove(User user) async { - var users = [...state.users]; - var index = users.indexOf(user); - users.remove(user); - await box.deleteAt(index); + Future deleteSshKey(User user, String publicKey) async { + // All keys are deleted via api.deleteUserSshKey - emit(UsersState(users)); + final result = await api.deleteUserSshKey(user, publicKey); + if (result.isSuccess) { + // If it is root user, delete key from root keys + // If it is primary user, update primary user + // If it is not primary user, update user + + if (user.login == 'root') { + final rootKeys = + configBox.get(BNames.rootKeys, defaultValue: []) as List; + rootKeys.remove(publicKey); + configBox.put(BNames.rootKeys, rootKeys); + emit(state.copyWith( + rootUser: User( + login: state.rootUser.login, + isFoundOnServer: true, + password: state.rootUser.password, + sshKeys: rootKeys, + note: state.rootUser.note, + ), + )); + return; + } + if (user.login == state.primaryUser.login) { + List primaryUserKeys = state.primaryUser.sshKeys; + primaryUserKeys.remove(publicKey); + final updatedUser = User( + login: state.primaryUser.login, + isFoundOnServer: true, + password: state.primaryUser.password, + sshKeys: primaryUserKeys, + note: state.primaryUser.note, + ); + configBox.put(BNames.rootUser, updatedUser); + emit(state.copyWith( + primaryUser: updatedUser, + )); + return; + } + List userKeys = user.sshKeys; + userKeys.remove(publicKey); + final updatedUser = User( + login: user.login, + isFoundOnServer: true, + password: user.password, + sshKeys: userKeys, + note: user.note, + ); + await box.putAt(box.values.toList().indexOf(user), updatedUser); + emit(state.copyWith( + users: box.values.toList(), + )); + } + } + + @override + void onChange(Change change) { + print(change); + super.onChange(change); } } diff --git a/lib/logic/cubit/users/users_state.dart b/lib/logic/cubit/users/users_state.dart index 88c56146..429b5cfa 100644 --- a/lib/logic/cubit/users/users_state.dart +++ b/lib/logic/cubit/users/users_state.dart @@ -1,12 +1,26 @@ part of 'users_cubit.dart'; class UsersState extends Equatable { - const UsersState(this.users); + const UsersState(this.users, this.rootUser, this.primaryUser); final List users; + final User rootUser; + final User primaryUser; @override - List get props => users; + List get props => [users, rootUser, primaryUser]; + + UsersState copyWith({ + List? users, + User? rootUser, + User? primaryUser, + }) { + return UsersState( + users ?? this.users, + rootUser ?? this.rootUser, + primaryUser ?? this.primaryUser, + ); + } bool get isEmpty => users.isEmpty; } diff --git a/lib/logic/models/dns_records.dart b/lib/logic/models/dns_records.dart index 349f2c41..25aad046 100644 --- a/lib/logic/models/dns_records.dart +++ b/lib/logic/models/dns_records.dart @@ -20,5 +20,5 @@ class DnsRecord { final int priority; final bool proxied; - toJson() => _$DnsRecordsToJson(this); + toJson() => _$DnsRecordToJson(this); } diff --git a/lib/logic/models/dns_records.g.dart b/lib/logic/models/dns_records.g.dart index df5d1cd9..c8c12c34 100644 --- a/lib/logic/models/dns_records.g.dart +++ b/lib/logic/models/dns_records.g.dart @@ -6,8 +6,7 @@ part of 'dns_records.dart'; // JsonSerializableGenerator // ************************************************************************** -Map _$DnsRecordsToJson(DnsRecord instance) => - { +Map _$DnsRecordToJson(DnsRecord instance) => { 'type': instance.type, 'name': instance.name, 'content': instance.content, diff --git a/lib/logic/models/job.dart b/lib/logic/models/job.dart index 70d238a9..698cd1fb 100644 --- a/lib/logic/models/job.dart +++ b/lib/logic/models/job.dart @@ -1,8 +1,8 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/utils/password_generator.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'user.dart'; @@ -68,8 +68,27 @@ class ServiceToggleJob extends ToggleJob { } class CreateSSHKeyJob extends Job { - CreateSSHKeyJob() : super(title: '${"more.create_ssh_key".tr()}'); + CreateSSHKeyJob({ + required this.user, + required this.publicKey, + }) : super(title: '${"jobs.create_ssh_key".tr(args: [user.login])}'); + + final User user; + final String publicKey; @override - List get props => [id, title]; + List get props => [id, title, user, publicKey]; +} + +class DeleteSSHKeyJob extends Job { + DeleteSSHKeyJob({ + required this.user, + required this.publicKey, + }) : super(title: '${"jobs.delete_ssh_key".tr(args: [user.login])}'); + + final User user; + final String publicKey; + + @override + List get props => [id, title, user, publicKey]; } diff --git a/lib/logic/models/user.dart b/lib/logic/models/user.dart index 0d1c48b3..8161f61b 100644 --- a/lib/logic/models/user.dart +++ b/lib/logic/models/user.dart @@ -1,10 +1,8 @@ import 'dart:ui'; -import 'package:crypt/crypt.dart'; import 'package:equatable/equatable.dart'; -import 'package:selfprivacy/utils/color_utils.dart'; import 'package:hive/hive.dart'; -import 'package:selfprivacy/utils/password_generator.dart'; +import 'package:selfprivacy/utils/color_utils.dart'; part 'user.g.dart'; @@ -12,26 +10,33 @@ part 'user.g.dart'; class User extends Equatable { User({ required this.login, - required this.password, + this.password, + this.sshKeys = const [], + this.isFoundOnServer = true, + this.note, }); @HiveField(0) final String login; @HiveField(1) - final String password; + final String? password; + + @HiveField(2, defaultValue: const []) + final List sshKeys; + + @HiveField(3, defaultValue: true) + final bool isFoundOnServer; + + @HiveField(4) + final String? note; @override - List get props => [login, password]; + List get props => [login, password, sshKeys, isFoundOnServer, note]; Color get color => stringToColor(login); - Crypt get hashPassword => Crypt.sha512( - password, - salt: StringGenerators.passwordSalt(), - ); - String toString() { - return login; + return '$login, ${isFoundOnServer ? 'found' : 'not found'}, ${sshKeys.length} ssh keys, note: $note'; } } diff --git a/lib/logic/models/user.g.dart b/lib/logic/models/user.g.dart index 796ea5dd..a1889dc1 100644 --- a/lib/logic/models/user.g.dart +++ b/lib/logic/models/user.g.dart @@ -18,18 +18,27 @@ class UserAdapter extends TypeAdapter { }; return User( login: fields[0] as String, - password: fields[1] as String, + password: fields[1] as String?, + sshKeys: fields[2] == null ? [] : (fields[2] as List).cast(), + isFoundOnServer: fields[3] == null ? true : fields[3] as bool, + note: fields[4] as String?, ); } @override void write(BinaryWriter writer, User obj) { writer - ..writeByte(2) + ..writeByte(5) ..writeByte(0) ..write(obj.login) ..writeByte(1) - ..write(obj.password); + ..write(obj.password) + ..writeByte(2) + ..write(obj.sshKeys) + ..writeByte(3) + ..write(obj.isFoundOnServer) + ..writeByte(4) + ..write(obj.note); } @override diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index 3c06ef90..0717d2bc 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -1,30 +1,21 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:ionicons/ionicons.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; -import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/config/text_themes.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; -import 'package:selfprivacy/logic/get_it/ssh.dart'; -import 'package:selfprivacy/logic/models/job.dart'; -import 'package:selfprivacy/logic/models/state_types.dart'; -import 'package:selfprivacy/ui/components/action_button/action_button.dart'; -import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; -import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart'; import 'package:selfprivacy/ui/pages/initializing/initializing.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; import 'package:selfprivacy/ui/pages/rootRoute.dart'; +import 'package:selfprivacy/ui/pages/ssh_keys/ssh_keys.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:share_plus/share_plus.dart'; +import '../../../logic/cubit/users/users_cubit.dart'; import 'about/about.dart'; import 'app_settings/app_setting.dart'; import 'console/console.dart'; @@ -83,73 +74,12 @@ class MorePage extends StatelessWidget { iconData: BrandIcons.terminal, goTo: Console(), ), - _MoreMenuTapItem( - title: 'more.create_ssh_key'.tr(), - iconData: Ionicons.key_outline, - onTap: isReady - ? () { - if (getIt().isSSHKeyGenerated) { - showDialog( - context: context, - builder: (BuildContext context) { - return _SSHExitsDetails( - onShareTap: () { - Share.share( - getIt().savedPrivateKey!); - }, - onDeleteTap: () { - showDialog( - context: context, - builder: (_) { - return BrandAlert( - title: 'modals.3'.tr(), - contentText: - 'more.delete_ssh_text'.tr(), - actions: [ - ActionButton( - text: 'more.yes_delete'.tr(), - isRed: true, - onPressed: () { - getIt().clear(); - Navigator.of(context).pop(); - }), - ActionButton( - text: 'basis.cancel'.tr(), - ), - ], - ); - }, - ); - }, - onCopyTap: () { - Clipboard.setData(ClipboardData( - text: getIt() - .savedPrivateKey!)); - getIt() - .showSnackBar('more.copied_ssh'.tr()); - }, - ); - }, - ); - } else { - showDialog( - context: context, - builder: (BuildContext context) { - return _MoreDetails( - title: 'more.create_ssh_key'.tr(), - icon: Ionicons.key_outline, - onTap: () { - jobsCubit.createShhJobIfNotExist( - CreateSSHKeyJob()); - }, - text: 'more.generate_key_text'.tr(), - ); - }, - ); - } - } - : null, - ), + _NavItem( + title: 'more.create_ssh_key'.tr(), + iconData: Ionicons.key_outline, + goTo: SshKeysPage( + user: context.read().state.rootUser, + )), ], ), ) @@ -159,150 +89,6 @@ class MorePage extends StatelessWidget { } } -class _SSHExitsDetails extends StatelessWidget { - const _SSHExitsDetails({ - Key? key, - required this.onDeleteTap, - required this.onShareTap, - required this.onCopyTap, - }) : super(key: key); - final Function onDeleteTap; - final Function onShareTap; - final Function onCopyTap; - - @override - Widget build(BuildContext context) { - var textStyle = body1Style.copyWith( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : BrandColors.black); - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - child: SingleChildScrollView( - child: Container( - width: 350, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: paddingH15V30, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SizedBox(height: 10), - Text( - 'more.ssh_key_exist_text'.tr(), - style: textStyle, - ), - SizedBox(height: 10), - Container( - child: BrandButton.text( - onPressed: () { - Navigator.of(context).pop(); - onShareTap(); - }, - title: 'more.share'.tr(), - ), - ), - Container( - alignment: Alignment.centerLeft, - child: BrandButton.text( - onPressed: () { - Navigator.of(context).pop(); - onDeleteTap(); - }, - title: 'basis.delete'.tr(), - ), - ), - Container( - child: BrandButton.text( - onPressed: () { - Navigator.of(context).pop(); - onCopyTap(); - }, - title: 'more.copy_buffer'.tr(), - ), - ), - ], - ), - ) - ], - ), - ), - ), - ); - } -} - -class _MoreDetails extends StatelessWidget { - const _MoreDetails({ - Key? key, - required this.icon, - required this.title, - required this.onTap, - required this.text, - }) : super(key: key); - final String title; - final IconData icon; - final Function onTap; - final String text; - @override - Widget build(BuildContext context) { - var textStyle = body1Style.copyWith( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : BrandColors.black); - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - child: SingleChildScrollView( - child: Container( - width: 350, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: paddingH15V30, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - IconStatusMask( - status: StateType.stable, - child: Icon(icon, size: 40, color: Colors.white), - ), - SizedBox(height: 10), - BrandText.h2(title), - SizedBox(height: 10), - Text( - text, - style: textStyle, - ), - SizedBox(height: 40), - Center( - child: Container( - child: BrandButton.rised( - onPressed: () { - Navigator.of(context).pop(); - onTap(); - }, - text: 'more.generate_key'.tr(), - ), - ), - ), - ], - ), - ) - ], - ), - ), - ), - ); - } -} - class _NavItem extends StatelessWidget { const _NavItem({ Key? key, diff --git a/lib/ui/pages/ssh_keys/ssh_keys.dart b/lib/ui/pages/ssh_keys/ssh_keys.dart new file mode 100644 index 00000000..3e3ecd7f --- /dev/null +++ b/lib/ui/pages/ssh_keys/ssh_keys.dart @@ -0,0 +1,75 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; + +import '../../../logic/models/user.dart'; + +// Get user object as a parameter +class SshKeysPage extends StatefulWidget { + final User user; + + SshKeysPage({Key? key, required this.user}) : super(key: key); + + @override + _SshKeysPageState createState() => _SshKeysPageState(); +} + +class _SshKeysPageState extends State { + @override + Widget build(BuildContext context) { + return BrandHeroScreen( + heroTitle: 'ssh.title'.tr(), + heroSubtitle: widget.user.login, + heroIcon: BrandIcons.key, + children: [ + if (widget.user.login == 'root') + Column( + children: [ + // Show alert card if user is root + BrandCards.outlined( + child: ListTile( + leading: Icon( + Icons.warning_rounded, + color: Theme.of(context).colorScheme.error, + ), + title: Text('ssh.root.title'.tr()), + subtitle: Text('ssh.root.subtitle'.tr()), + ), + ) + ], + ), + BrandCards.outlined( + child: Column( + children: [ + ListTile( + title: Text( + 'ssh.create'.tr(), + style: Theme.of(context).textTheme.headline6, + ), + leading: Icon(Icons.add_circle_outline_rounded), + ), + Divider(height: 0), + // show a list of ListTiles with ssh keys + // Clicking on one should delete it + Column( + children: widget.user.sshKeys.map((key) { + return ListTile( + title: + Text('${key.split(' ')[2]} (${key.split(' ')[0]})'), + // do not overflow text + subtitle: Text(key.split(' ')[1], + maxLines: 1, overflow: TextOverflow.ellipsis), + onTap: () { + // TODO: delete ssh key + }); + }).toList(), + ) + ], + ), + ), + ], + ); + } +} diff --git a/lib/ui/pages/users/user.dart b/lib/ui/pages/users/user.dart index 1d1fb9a8..a748a374 100644 --- a/lib/ui/pages/users/user.dart +++ b/lib/ui/pages/users/user.dart @@ -34,7 +34,11 @@ class _User extends StatelessWidget { Flexible( child: isRootUser ? BrandText.h4Underlined(user.login) - : BrandText.h4(user.login), + // cross out text if user not found on server + : BrandText.h4(user.login, + style: user.isFoundOnServer + ? null + : TextStyle(decoration: TextDecoration.lineThrough)), ), ], ), diff --git a/lib/ui/pages/users/user_details.dart b/lib/ui/pages/users/user_details.dart index 30e0849a..0f614859 100644 --- a/lib/ui/pages/users/user_details.dart +++ b/lib/ui/pages/users/user_details.dart @@ -141,13 +141,19 @@ class _UserDetails extends StatelessWidget { alignment: Alignment.centerLeft, child: BrandText.h4('${user.login}@$domainName'), ), - SizedBox(height: 14), - BrandText.small('basis.password'.tr()), - Container( - height: 40, - alignment: Alignment.centerLeft, - child: BrandText.h4(user.password), - ), + if (user.password != null) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 14), + BrandText.small('basis.password'.tr()), + Container( + height: 40, + alignment: Alignment.centerLeft, + child: BrandText.h4(user.password), + ), + ], + ), SizedBox(height: 24), BrandDivider(), SizedBox(height: 20), @@ -160,6 +166,19 @@ class _UserDetails extends StatelessWidget { }, ), SizedBox(height: 20), + BrandDivider(), + SizedBox(height: 20), + ListTile( + onTap: () { + Navigator.of(context) + .push(materialRoute(SshKeysPage(user: user))); + }, + title: Text('ssh.title'.tr()), + subtitle: user.sshKeys.length > 0 + ? Text('ssh.subtitle_with_keys' + .tr(args: [user.sshKeys.length.toString()])) + : Text('ssh.subtitle_without_keys'.tr()), + trailing: Icon(BrandIcons.key)), ], ), ) diff --git a/lib/ui/pages/users/users.dart b/lib/ui/pages/users/users.dart index 306b7479..ed1d10bd 100644 --- a/lib/ui/pages/users/users.dart +++ b/lib/ui/pages/users/users.dart @@ -1,10 +1,9 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:hive/hive.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; -import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/config/text_themes.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/user/user_form_cubit.dart'; @@ -19,16 +18,18 @@ import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/ui/helpers/modals.dart'; +import 'package:selfprivacy/ui/pages/ssh_keys/ssh_keys.dart'; import 'package:selfprivacy/utils/ui_helpers.dart'; import 'package:share_plus/share_plus.dart'; +import '../../../utils/route_transitions/basic.dart'; + +part 'empty.dart'; part 'fab.dart'; part 'new_user.dart'; -part 'user_details.dart'; part 'user.dart'; -part 'empty.dart'; +part 'user_details.dart'; class UsersPage extends StatelessWidget { const UsersPage({Key? key}) : super(key: key); @@ -37,12 +38,8 @@ class UsersPage extends StatelessWidget { Widget build(BuildContext context) { final usersCubitState = context.watch().state; var isReady = context.watch().state is AppConfigFinished; - final users = [...usersCubitState.users]; - //Todo: listen box events - User? user = Hive.box(BNames.appConfig).get(BNames.rootUser); - if (user != null) { - users.insert(0, user); - } + final primaryUser = usersCubitState.primaryUser; + final users = [primaryUser, ...usersCubitState.users]; final isEmpty = users.isEmpty; Widget child; @@ -56,14 +53,19 @@ class UsersPage extends StatelessWidget { text: 'users.add_new_user'.tr(), ), ) - : ListView.builder( - itemCount: users.length, - itemBuilder: (BuildContext context, int index) { - return _User( - user: users[index], - isRootUser: index == 0, - ); + : RefreshIndicator( + onRefresh: () async { + context.read().refresh(); }, + child: ListView.builder( + itemCount: users.length, + itemBuilder: (BuildContext context, int index) { + return _User( + user: users[index], + isRootUser: index == 0, + ); + }, + ), ); } diff --git a/pubspec.lock b/pubspec.lock index a08f1ac6..24f40613 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,7 +21,7 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.1.11" + version: "3.2.0" args: dependency: transitive description: @@ -56,7 +56,7 @@ packages: name: basic_utils url: "https://pub.dartlang.org" source: hosted - version: "3.9.4" + version: "4.2.0" bloc: dependency: transitive description: @@ -301,7 +301,7 @@ packages: name: fl_chart url: "https://pub.dartlang.org" source: hosted - version: "0.40.6" + version: "0.45.0" flutter: dependency: "direct main" description: flutter @@ -320,7 +320,7 @@ packages: name: flutter_launcher_icons url: "https://pub.dartlang.org" source: hosted - version: "0.9.0" + version: "0.9.2" flutter_localizations: dependency: transitive description: flutter @@ -346,7 +346,42 @@ packages: name: flutter_secure_storage url: "https://pub.dartlang.org" source: hosted - version: "4.2.1" + version: "5.0.2" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" flutter_test: dependency: "direct dev" description: flutter @@ -419,7 +454,7 @@ packages: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.2.0" http_parser: dependency: transitive description: @@ -482,7 +517,7 @@ packages: name: local_auth url: "https://pub.dartlang.org" source: hosted - version: "1.1.10" + version: "1.1.11" logging: dependency: transitive description: @@ -587,7 +622,7 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.0.9" path_provider_android: dependency: transitive description: @@ -657,7 +692,7 @@ packages: name: pointycastle url: "https://pub.dartlang.org" source: hosted - version: "3.5.0" + version: "3.5.1" pool: dependency: transitive description: @@ -720,7 +755,7 @@ packages: name: share_plus url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "3.0.5" share_plus_linux: dependency: transitive description: @@ -769,28 +804,28 @@ packages: name: shared_preferences_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.10" + version: "2.0.11" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios url: "https://pub.dartlang.org" source: hosted - version: "2.0.9" + version: "2.1.0" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.1.0" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" shared_preferences_platform_interface: dependency: transitive description: @@ -811,7 +846,7 @@ packages: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.1.0" shelf: dependency: transitive description: @@ -977,35 +1012,35 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.18" + version: "6.0.20" url_launcher_android: dependency: transitive description: name: url_launcher_android url: "https://pub.dartlang.org" source: hosted - version: "6.0.14" + version: "6.0.15" url_launcher_ios: dependency: transitive description: name: url_launcher_ios url: "https://pub.dartlang.org" source: hosted - version: "6.0.14" + version: "6.0.15" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "3.0.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "3.0.0" url_launcher_platform_interface: dependency: transitive description: @@ -1026,7 +1061,7 @@ packages: name: url_launcher_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "3.0.0" vector_math: dependency: transitive description: @@ -1047,7 +1082,7 @@ packages: name: wakelock url: "https://pub.dartlang.org" source: hosted - version: "0.5.6" + version: "0.6.1+1" wakelock_macos: dependency: transitive description: @@ -1103,7 +1138,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.3.11" + version: "2.4.1" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9eb7cfee..f38fc009 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,45 +8,45 @@ environment: flutter: ">=2.10.0" dependencies: - auto_size_text: 3.0.0-nullsafety.0 - basic_utils: 3.4.0 - crypt: 4.0.1 - cubit_form: 2.0.1 - cupertino_icons: 1.0.2 - dio: 4.0.1 - easy_localization: 3.0.0 - either_option: 2.0.1-dev.1 - equatable: 2.0.3 - fl_chart: 0.40.0 + auto_size_text: ^3.0.0 + basic_utils: ^4.2.0 + crypt: ^4.2.1 + cubit_form: ^2.0.1 + cupertino_icons: ^1.0.4 + dio: ^4.0.4 + easy_localization: ^3.0.0 + either_option: ^2.0.1-dev.1 + equatable: ^2.0.3 + fl_chart: ^0.45.0 flutter: sdk: flutter - flutter_bloc: 8.0.1 - flutter_markdown: 0.6.9 - flutter_secure_storage: 4.2.1 - get_it: 7.2.0 - hive: 2.0.5 - hive_flutter: 1.1.0 - ionicons: 0.1.2 - json_annotation: 4.3.0 - local_auth: 1.1.7 - modal_bottom_sheet: 2.0.0 - nanoid: 1.0.0 - package_info: 2.0.0 - pointycastle: 3.3.2 - pretty_dio_logger: 1.2.0-beta-1 - provider: 6.0.0 - rsa_encrypt: 2.0.0 - share_plus: 2.1.4 - ssh_key: 0.7.0 + flutter_bloc: ^8.0.1 + flutter_markdown: ^0.6.9 + flutter_secure_storage: ^5.0.2 + get_it: ^7.2.0 + hive: ^2.0.5 + hive_flutter: ^1.1.0 + ionicons: ^0.1.2 + json_annotation: ^4.4.0 + local_auth: ^1.1.11 + modal_bottom_sheet: ^2.0.0 + nanoid: ^1.0.0 + package_info: ^2.0.2 + pointycastle: ^3.5.1 + pretty_dio_logger: ^1.2.0-beta-1 + provider: ^6.0.2 + rsa_encrypt: ^2.0.0 + share_plus: ^3.0.5 + ssh_key: ^0.7.1 timezone: ^0.8.0 - url_launcher: 6.0.2 - wakelock: 0.5.0+2 + url_launcher: ^6.0.20 + wakelock: ^0.6.1+1 dev_dependencies: flutter_test: sdk: flutter build_runner: ^2.1.1 - flutter_launcher_icons: ^0.9.0 + flutter_launcher_icons: ^0.9.2 hive_generator: ^1.0.0 json_serializable: ^6.1.4