diff --git a/android/app/build.gradle b/android/app/build.gradle index ef85465..e1915bd 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -14,10 +20,10 @@ if (credentialsPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') +/*def flutterRoot = localProperties.getProperty('flutter.sdk') if (flutterRoot == null) { throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} +}*/ def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { @@ -29,9 +35,9 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' +/*apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"*/ android { compileSdkVersion 34 @@ -97,5 +103,5 @@ flutter { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10" } \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index a1ce039..bc157bd 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.9.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.2.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() diff --git a/android/settings.gradle b/android/settings.gradle index 44e62bc..1c2bf11 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,11 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.2.0" apply false + id "org.jetbrains.kotlin.android" version "1.9.10" apply false +} + +include ":app" diff --git a/assets/images/no_account.png b/assets/images/no_accounts.png similarity index 100% rename from assets/images/no_account.png rename to assets/images/no_accounts.png diff --git a/assets/images/no_results.png b/assets/images/no_results.png new file mode 100644 index 0000000..bc567ca Binary files /dev/null and b/assets/images/no_results.png differ diff --git a/lib/bloc/account_details/account_details_bloc.dart b/lib/bloc/account_details/account_details_bloc.dart index 00151b7..37e0d07 100644 --- a/lib/bloc/account_details/account_details_bloc.dart +++ b/lib/bloc/account_details/account_details_bloc.dart @@ -1,39 +1,42 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:otp_manager/bloc/account_details/account_details_event.dart'; import 'package:otp_manager/bloc/account_details/account_details_state.dart'; -import 'package:otp_manager/models/account.dart'; -import 'package:otp_manager/repository/local_repository.dart'; +import 'package:otp_manager/repository/interface/account_repository.dart'; +import 'package:otp_manager/repository/interface/shared_account_repository.dart'; +import 'package:otp_manager/repository/interface/user_repository.dart'; import 'package:otp_manager/routing/constants.dart'; +import '../../domain/account_service.dart'; import '../../routing/navigation_service.dart'; class AccountDetailsBloc extends Bloc { - final LocalRepositoryImpl localRepositoryImpl; - final Account account; + final UserRepository userRepository; + final AccountRepository accountRepository; + final AccountService accountService; + final SharedAccountRepository sharedAccountRepository; + final dynamic account; // Account | SharedAccount final NavigationService _navigationService = NavigationService(); AccountDetailsBloc({ - required this.localRepositoryImpl, + required this.userRepository, + required this.accountRepository, + required this.accountService, + required this.sharedAccountRepository, required this.account, }) : super( - AccountDetailsState.initial(account, localRepositoryImpl.getUser()!), + AccountDetailsState.initial(account, userRepository.get()!), ) { on(_onDeleteAccount); } void _onDeleteAccount( DeleteAccount event, Emitter emit) { - if (localRepositoryImpl.setAccountAsDeleted(state.account.id)) { - emit(state.copyWith( - accountDeleted: - "${state.account.type == "totp" ? "TOTP" : "HOTP"} has been removed")); - _navigationService.resetToScreen(homeRoute); - } else { - emit(state.copyWith( - accountDeleted: "There was an error while deleting the account")); - _navigationService.goBack(); - } + accountService.setAsDeleted(state.account); + + emit(state.copyWith( + message: "${state.account.type.toUpperCase()} has been removed")); + _navigationService.resetToScreen(homeRoute); } } diff --git a/lib/bloc/account_details/account_details_state.dart b/lib/bloc/account_details/account_details_state.dart index 09974a9..76d1143 100644 --- a/lib/bloc/account_details/account_details_state.dart +++ b/lib/bloc/account_details/account_details_state.dart @@ -1,31 +1,33 @@ import 'package:equatable/equatable.dart'; import 'package:otp_manager/models/user.dart'; -import '../../models/account.dart'; - class AccountDetailsState extends Equatable { - final Account account; - final String accountDeleted; + final dynamic account; // Account | ShareAccount + final String message; final String password; + final String serverUrl; const AccountDetailsState({ required this.account, - required this.accountDeleted, + required this.message, required this.password, + required this.serverUrl, }); AccountDetailsState.initial(this.account, User user) - : accountDeleted = "", - password = user.password ?? ""; + : message = "", + password = user.password!, + serverUrl = user.url; - AccountDetailsState copyWith({String? accountDeleted}) { + AccountDetailsState copyWith({String? message}) { return AccountDetailsState( account: account, - accountDeleted: accountDeleted ?? this.accountDeleted, + message: message ?? this.message, password: password, + serverUrl: serverUrl, ); } @override - List get props => [accountDeleted]; + List get props => [message]; } diff --git a/lib/bloc/auth/auth_bloc.dart b/lib/bloc/auth/auth_bloc.dart index bf653ce..912bddb 100644 --- a/lib/bloc/auth/auth_bloc.dart +++ b/lib/bloc/auth/auth_bloc.dart @@ -4,7 +4,7 @@ import 'package:crypto/crypto.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:otp_manager/bloc/auth/auth_event.dart'; import 'package:otp_manager/bloc/auth/auth_state.dart'; -import 'package:otp_manager/repository/local_repository.dart'; +import 'package:otp_manager/repository/interface/user_repository.dart'; import 'package:otp_manager/routing/constants.dart'; import '../../domain/nextcloud_service.dart'; @@ -12,25 +12,23 @@ import '../../models/user.dart'; import '../../routing/navigation_service.dart'; class AuthBloc extends Bloc { - final LocalRepositoryImpl localRepositoryImpl; + final UserRepository userRepository; final NextcloudService nextcloudService; - late User user = localRepositoryImpl.getUser()!; + late User user = userRepository.get()!; final NavigationService _navigationService = NavigationService(); AuthBloc({ - required this.localRepositoryImpl, + required this.userRepository, required this.nextcloudService, - }) : super( - AuthState.initial(localRepositoryImpl.getUser()!), - ) { + }) : super(const AuthState.initial()) { on(_onAuthenticated); on(_onPasswordSubmit); on(_onPasswordChanged); on(_onResetAttempts); on(_onShowFingerAuth); - if (state.password != "") { + if (user.password != null) { add(ShowFingerAuth()); } } @@ -51,7 +49,7 @@ class AuthBloc extends Bloc { DateTime.now().add(const Duration(minutes: 5)); } - localRepositoryImpl.updateUser(user); + userRepository.update(user); } void _onResetAttempts(ResetAttempts event, Emitter emit) { @@ -75,9 +73,6 @@ class AuthBloc extends Bloc { if (state.attempts == 0) { emit(state.copyWith(attempts: 3)); } - - emit(state.copyWith(isError: false)); - emit(state.copyWith(isError: true)); } void _onAuthenticated(Authenticated event, Emitter emit) { @@ -100,7 +95,7 @@ class AuthBloc extends Bloc { user.password = password; user.iv = iv; - localRepositoryImpl.updateUser(user); + userRepository.update(user); } else { _error(emit, result["error"]!); return; diff --git a/lib/bloc/auth/auth_state.dart b/lib/bloc/auth/auth_state.dart index b5bc413..cc16e1a 100644 --- a/lib/bloc/auth/auth_state.dart +++ b/lib/bloc/auth/auth_state.dart @@ -1,45 +1,38 @@ import 'package:equatable/equatable.dart'; -import 'package:otp_manager/models/user.dart'; class AuthState extends Equatable { final int attempts; final String password; final String message; - final bool isError; final bool canShowFingerAuth; const AuthState({ required this.attempts, required this.password, required this.message, - required this.isError, required this.canShowFingerAuth, }); - AuthState.initial(User user) + const AuthState.initial() : attempts = 3, - password = user.password ?? "", + password = "", message = "", - isError = false, canShowFingerAuth = false; AuthState copyWith({ int? attempts, String? password, String? message, - bool? isError, bool? canShowFingerAuth, }) { return AuthState( attempts: attempts ?? this.attempts, password: password ?? this.password, message: message ?? this.message, - isError: isError ?? this.isError, canShowFingerAuth: canShowFingerAuth ?? this.canShowFingerAuth, ); } @override - List get props => - [attempts, password, message, isError, canShowFingerAuth]; + List get props => [attempts, password, message, canShowFingerAuth]; } diff --git a/lib/bloc/home/home_bloc.dart b/lib/bloc/home/home_bloc.dart index 33572d4..d20b072 100644 --- a/lib/bloc/home/home_bloc.dart +++ b/lib/bloc/home/home_bloc.dart @@ -3,34 +3,44 @@ import 'package:otp_manager/bloc/home/home_event.dart'; import 'package:otp_manager/bloc/home/home_state.dart'; import 'package:otp_manager/domain/nextcloud_service.dart'; import 'package:otp_manager/models/account.dart'; -import 'package:otp_manager/repository/local_repository.dart'; +import 'package:otp_manager/repository/interface/account_repository.dart'; +import 'package:otp_manager/repository/interface/shared_account_repository.dart'; +import 'package:otp_manager/repository/interface/user_repository.dart'; import 'package:otp_manager/routing/constants.dart'; +import '../../domain/account_service.dart'; +import '../../models/shared_account.dart'; import '../../routing/navigation_service.dart'; class HomeBloc extends Bloc { - final LocalRepositoryImpl localRepositoryImpl; + final UserRepository userRepository; + final AccountRepository accountRepository; + final AccountService accountService; + final SharedAccountRepository sharedAccountRepository; final NextcloudService nextcloudService; final NavigationService _navigationService = NavigationService(); HomeBloc({ - required this.localRepositoryImpl, + required this.userRepository, + required this.accountRepository, + required this.accountService, + required this.sharedAccountRepository, required this.nextcloudService, }) : super( - HomeState.initial(localRepositoryImpl.getUser()!), + HomeState.initial(userRepository.get()!), ) { on(_onNextcloudSync); on(_onGetAccounts); on(_onLogout); on(_onReorder); on(_onDeleteAccount); - on(_onIncrementCounter); on(_onSortByName); on(_onSortByIssuer); on(_onSortById); on(_onSearchBarValueChanged); on(_onIsAppUpdatedChanged); + on(_onShowMessage); add(GetAccounts()); } @@ -46,30 +56,33 @@ class HomeBloc extends Bloc { if (!state.isAppUpdated) { emit(state.copyWith( syncStatus: -1, - syncError: + message: "Update the app to the latest version to be able to synchronize")); - emit(state.copyWith(syncError: "")); + emit(state.copyWith(message: "")); } else if (!state.isGuest) { emit(state.copyWith(syncStatus: 1)); final Map result = await nextcloudService.sync(); if (result["error"] != null) { - emit(state.copyWith(syncStatus: -1, syncError: result["error"])); - emit(state.copyWith(syncError: "")); - } else if (result["toAdd"].length > 0 || result["toEdit"].length > 0) { + emit(state.copyWith(syncStatus: -1, message: result["error"])); + emit(state.copyWith(message: "")); + } else { if (nextcloudService.syncAccountsToAddToEdit( - result["toAdd"], result["toEdit"])) { + result["accounts"], result["sharedAccounts"])) { + if (accountService.repairPositionError()) { + await nextcloudService.sync(); + } emit(state.copyWith(syncStatus: 0)); } else { emit(state.copyWith( syncStatus: -1, - syncError: "Password has changed. Insert the new one", + message: "Password has changed. Insert the new one", )); - emit(state.copyWith(syncError: "")); + emit(state.copyWith(message: "")); _navigationService.replaceScreen(authRoute); } - } else { + emit(state.copyWith(syncStatus: 0)); } } else { @@ -79,22 +92,37 @@ class HomeBloc extends Bloc { add(GetAccounts()); } + List mergeResults( + List accounts, List sharedAccounts) { + List result = [...accounts, ...sharedAccounts]; + + result.sort((a, b) => a.position.compareTo(b.position)); + + return result; + } + void _onGetAccounts(GetAccounts event, Emitter emit) { if (state.searchBarValue == "") { emit(state.copyWith( - accounts: localRepositoryImpl.getVisibleAccounts(), + accounts: mergeResults( + accountRepository.getVisible(), + sharedAccountRepository.getVisible(), + ), )); } else { emit(state.copyWith( - accounts: localRepositoryImpl - .getVisibleFilteredAccounts(state.searchBarValue), + accounts: mergeResults( + accountRepository.getVisibleFiltered(state.searchBarValue), + sharedAccountRepository.getVisibleFiltered(state.searchBarValue), + ), )); } } void _onLogout(Logout event, Emitter emit) { - localRepositoryImpl.removeAllUsers(); - localRepositoryImpl.removeAllAccounts(); + userRepository.removeAll(); + accountRepository.removeAll(); + sharedAccountRepository.removeAll(); _navigationService.resetToScreen(loginRoute); } @@ -105,70 +133,36 @@ class HomeBloc extends Bloc { sortedByIssuerDesc: "null", )); - final user = localRepositoryImpl.getUser()!; + final user = userRepository.get()!; user.sortedByNameDesc = state.sortedByNameDesc; user.sortedByIssuerDesc = state.sortedByIssuerDesc; user.sortedByIdDesc = state.sortedByIdDesc; - localRepositoryImpl.updateUser(user); - - List accountsBetween; - - int difference; - int newIndex = event.newIndex; - - if (event.newIndex > event.oldIndex) { - newIndex -= 1; - accountsBetween = localRepositoryImpl.getAccountBetweenPositions( - event.oldIndex, newIndex); - difference = -1; - } else { - accountsBetween = localRepositoryImpl.getAccountBetweenPositions( - event.newIndex - 1, event.oldIndex - 1); - difference = 1; - } - - var account = localRepositoryImpl.getAccountByPosition(event.oldIndex); - - for (var account in accountsBetween) { - account.position = account.position! + difference; - account.toUpdate = true; - localRepositoryImpl.updateAccount(account); - } + userRepository.update(user); - account?.position = newIndex; - account?.toUpdate = true; - localRepositoryImpl.updateAccount(account!); + accountService.reorder(event.oldIndex, event.newIndex); add(NextcloudSync()); } void _onDeleteAccount(DeleteAccount event, Emitter emit) { - if (localRepositoryImpl.setAccountAsDeleted(event.id)) { - Account? accountDeleted = localRepositoryImpl.getAccount(event.id); + if (event.account != null) { + accountService.setAsDeleted(event.account); add(NextcloudSync()); emit(state.copyWith( - accountDeleted: - "${accountDeleted?.type.toUpperCase()} has been removed")); + message: "${event.account.type.toUpperCase()} has been removed")); } else { emit(state.copyWith( - accountDeleted: "There was an error while deleting the account")); + message: "There was an error while deleting the account")); } - emit(state.copyWith(accountDeleted: "")); + emit(state.copyWith(message: "")); _navigationService.goBack(); } - void _onIncrementCounter(IncrementCounter event, Emitter emit) { - event.account.counter = event.account.counter! + 1; - event.account.toUpdate = true; - localRepositoryImpl.updateAccount(event.account); - add(NextcloudSync()); - } - void _onSortById(SortById event, Emitter emit) { - List accounts = localRepositoryImpl.getVisibleAccounts(); + List accounts = accountRepository.getVisible(); if (state.sortedByIdDesc == null || state.sortedByIdDesc == true) { accounts.sort((b, a) => a.id.compareTo(b.id)); @@ -187,7 +181,7 @@ class HomeBloc extends Bloc { } void _onSortByName(SortByName event, Emitter emit) { - List accounts = localRepositoryImpl.getVisibleAccounts(); + List accounts = accountRepository.getVisible(); if (state.sortedByNameDesc == null || state.sortedByNameDesc == true) { accounts.sort((a, b) => a.name.compareTo(b.name)); @@ -206,7 +200,7 @@ class HomeBloc extends Bloc { } void _onSortByIssuer(SortByIssuer event, Emitter emit) { - List accounts = localRepositoryImpl.getVisibleAccounts(); + List accounts = accountRepository.getVisible(); if (state.sortedByIssuerDesc == null || state.sortedByIssuerDesc == true) { accounts.sort((a, b) => (a.issuer ?? "").compareTo(b.issuer ?? "")); @@ -226,16 +220,15 @@ class HomeBloc extends Bloc { } void _updateSorting(List accounts) { - final user = localRepositoryImpl.getUser()!; + final user = userRepository.get()!; user.sortedByNameDesc = state.sortedByNameDesc; user.sortedByIssuerDesc = state.sortedByIssuerDesc; user.sortedByIdDesc = state.sortedByIdDesc; - localRepositoryImpl.updateUser(user); + userRepository.update(user); for (int i = 0; i < accounts.length; i++) { accounts[i].position = i; - accounts[i].toUpdate = true; - localRepositoryImpl.updateAccount(accounts[i]); + accountRepository.update(accounts[i]); } add(NextcloudSync()); @@ -245,4 +238,9 @@ class HomeBloc extends Bloc { SearchBarValueChanged event, Emitter emit) { emit(state.copyWith(searchBarValue: event.value)); } + + void _onShowMessage(ShowMessage event, Emitter emit) { + emit(state.copyWith(message: event.message)); + emit(state.copyWith(message: "")); + } } diff --git a/lib/bloc/home/home_event.dart b/lib/bloc/home/home_event.dart index 178ee13..565d359 100644 --- a/lib/bloc/home/home_event.dart +++ b/lib/bloc/home/home_event.dart @@ -1,5 +1,4 @@ import 'package:equatable/equatable.dart'; -import 'package:otp_manager/models/account.dart'; class HomeEvent extends Equatable { const HomeEvent(); @@ -40,18 +39,9 @@ class Reorder extends HomeEvent { } class DeleteAccount extends HomeEvent { - const DeleteAccount({required this.id}); + const DeleteAccount({required this.account}); - final int id; - - @override - List get props => [id]; -} - -class IncrementCounter extends HomeEvent { - const IncrementCounter({required this.account}); - - final Account account; + final dynamic account; // Account | SharedAccount @override List get props => [account]; @@ -65,3 +55,12 @@ class SearchBarValueChanged extends HomeEvent { @override List get props => [value]; } + +class ShowMessage extends HomeEvent { + const ShowMessage({required this.message}); + + final String message; + + @override + List get props => [message]; +} diff --git a/lib/bloc/home/home_state.dart b/lib/bloc/home/home_state.dart index 4f80387..08b45e7 100644 --- a/lib/bloc/home/home_state.dart +++ b/lib/bloc/home/home_state.dart @@ -1,15 +1,13 @@ import 'package:equatable/equatable.dart'; -import 'package:otp_manager/models/account.dart'; import 'package:otp_manager/models/user.dart'; class HomeState extends Equatable { - final List accounts; + final List accounts; // Account | SharedAccount final int refreshTime; final int syncStatus; // 1 = SYNCING, 0 = OK, -1 = ERROR - final String syncError; final String password; final bool isGuest; - final String accountDeleted; + final String message; final bool? sortedByNameDesc; final bool? sortedByIssuerDesc; final bool? sortedByIdDesc; @@ -20,10 +18,9 @@ class HomeState extends Equatable { required this.accounts, required this.refreshTime, required this.syncStatus, - required this.syncError, required this.password, required this.isGuest, - required this.accountDeleted, + required this.message, required this.sortedByNameDesc, required this.sortedByIssuerDesc, required this.sortedByIdDesc, @@ -35,10 +32,9 @@ class HomeState extends Equatable { : accounts = [], refreshTime = 30, syncStatus = 1, - syncError = "", password = user.password ?? "", isGuest = user.isGuest, - accountDeleted = "", + message = "", sortedByNameDesc = user.sortedByNameDesc, sortedByIssuerDesc = user.sortedByIssuerDesc, sortedByIdDesc = user.sortedByIdDesc, @@ -46,11 +42,10 @@ class HomeState extends Equatable { isAppUpdated = false; HomeState copyWith({ - List? accounts, + List? accounts, int? refreshTime, int? syncStatus, - String? syncError, - String? accountDeleted, + String? message, dynamic sortedByNameDesc, dynamic sortedByIssuerDesc, dynamic sortedByIdDesc, @@ -61,10 +56,9 @@ class HomeState extends Equatable { accounts: accounts ?? this.accounts, refreshTime: refreshTime ?? this.refreshTime, syncStatus: syncStatus ?? this.syncStatus, - syncError: syncError ?? this.syncError, password: password, isGuest: isGuest, - accountDeleted: accountDeleted ?? this.accountDeleted, + message: message ?? this.message, sortedByNameDesc: sortedByNameDesc == "null" ? null : sortedByNameDesc ?? this.sortedByNameDesc, @@ -84,8 +78,7 @@ class HomeState extends Equatable { accounts, refreshTime, syncStatus, - syncError, - accountDeleted, + message, sortedByNameDesc, sortedByIssuerDesc, sortedByIdDesc, diff --git a/lib/bloc/icon_picker/icon_picker_bloc.dart b/lib/bloc/icon_picker/icon_picker_bloc.dart index 3b7014a..831bbdd 100644 --- a/lib/bloc/icon_picker/icon_picker_bloc.dart +++ b/lib/bloc/icon_picker/icon_picker_bloc.dart @@ -2,15 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:otp_manager/bloc/icon_picker/icon_picker_event.dart'; import 'package:otp_manager/bloc/icon_picker/icon_picker_state.dart'; +import 'package:otp_manager/utils/icon_picker_helper.dart'; import 'package:otp_manager/utils/simple_icons.dart'; -import '../../repository/local_repository.dart'; - class IconPickerBloc extends Bloc { - final LocalRepositoryImpl localRepositoryImpl; final String issuer; - IconPickerBloc({required this.localRepositoryImpl, required this.issuer}) + IconPickerBloc({required this.issuer}) : super(IconPickerState.initial(issuer)) { on(_onSearchBarValueChanged); on(_onInitIcons); @@ -20,14 +18,7 @@ class IconPickerBloc extends Bloc { void _onInitIcons(InitIcons event, Emitter emit) { if (issuer != "") { - Map iconsBestMatch = {}; - - simpleIcons.forEach((key, value) { - if (iconsBestMatch.length != 3 && key.contains(issuer)) { - iconsBestMatch[key] = value; - } - }); - + Map iconsBestMatch = IconPickerHelper.findBestMatch(issuer); emit(state.copyWith(iconsBestMatch: iconsBestMatch)); } } diff --git a/lib/bloc/login/login_bloc.dart b/lib/bloc/login/login_bloc.dart index aa51973..902d79b 100644 --- a/lib/bloc/login/login_bloc.dart +++ b/lib/bloc/login/login_bloc.dart @@ -1,31 +1,33 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:otp_manager/bloc/login/login_event.dart'; import 'package:otp_manager/bloc/login/login_state.dart'; +import 'package:otp_manager/repository/interface/user_repository.dart'; import '../../main.dart' show logger; import '../../models/user.dart'; -import '../../repository/local_repository.dart'; import '../../routing/constants.dart'; import '../../routing/navigation_service.dart'; class LoginBloc extends Bloc { - final LocalRepositoryImpl localRepositoryImpl; + final UserRepository userRepository; final NavigationService _navigationService = NavigationService(); - LoginBloc({required this.localRepositoryImpl}) + LoginBloc({required this.userRepository}) : super(const LoginState.initial()) { on(_onUrlSubmit); on(_onUrlChanged); } void _onUrlSubmit(UrlSubmit event, Emitter emit) { - var url = Uri.parse(state.url.trim()); + String url = state.url.trim(); + + url = url.endsWith("/") ? url.substring(0, url.length - 1) : url; if (url.toString() == "http://localhost") { - localRepositoryImpl.updateUser( + userRepository.update( User( - url: url.toString(), + url: url, appPassword: "test", isGuest: true, ), @@ -33,14 +35,16 @@ class LoginBloc extends Bloc { _navigationService.resetToScreen(homeRoute); } else { try { - _navigationService.navigateTo(webViewerRoute, - arguments: url.toString()); + _navigationService.navigateTo( + webViewerRoute, + arguments: url, + ); } catch (e) { logger.e(e); emit( state.copyWith( - url: url.toString(), + url: url, error: "The URL entered is not valid!", ), ); diff --git a/lib/bloc/manual/manual_bloc.dart b/lib/bloc/manual/manual_bloc.dart index ef98bdf..84606b4 100644 --- a/lib/bloc/manual/manual_bloc.dart +++ b/lib/bloc/manual/manual_bloc.dart @@ -1,20 +1,28 @@ import 'package:diacritic/diacritic.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:otp_manager/repository/interface/account_repository.dart'; +import 'package:otp_manager/repository/interface/shared_account_repository.dart'; import 'package:otp_manager/utils/base32.dart'; +import 'package:otp_manager/utils/icon_picker_helper.dart'; +import '../../domain/account_service.dart'; import '../../models/account.dart'; -import '../../repository/local_repository.dart'; -import '../../utils/simple_icons.dart'; import '../../utils/uri_decoder.dart'; import 'manual_event.dart'; import 'manual_state.dart'; class ManualBloc extends Bloc { - final Account? account; - final LocalRepositoryImpl localRepositoryImpl; - - ManualBloc({this.account, required this.localRepositoryImpl}) - : super(ManualState.initial(account)) { + final dynamic account; // Account | SharedAccount + final AccountRepository accountRepository; + final SharedAccountRepository sharedAccountRepository; + final AccountService accountService; + + ManualBloc({ + this.account, + required this.accountRepository, + required this.sharedAccountRepository, + required this.accountService, + }) : super(ManualState.initial(account)) { on(_onAddOrEditAccount); on(_onIconKeyChanged); on(_onNameChanged); @@ -26,59 +34,69 @@ class ManualBloc extends Bloc { on(_onDigitsValueChanged); } - void _storeAccount(Emitter emit, Account account, String msg) { - localRepositoryImpl.updateAccount(account); + void _updateAccount(Emitter emit, dynamic account, String msg) { + if (account is Account) { + accountRepository.update(account); + } else { + sharedAccountRepository.update(account); + } emit(state.copyWith(message: msg)); } - void _onAddOrEditAccount(AddOrEditAccount event, Emitter emit) { - String name = Uri.decodeFull(removeDiacritics(state.name.trim())); - String issuer = Uri.decodeFull(removeDiacritics(state.issuer.trim())); - String secretKey = state.secretKey.trim().toUpperCase(); + bool _isFormValid( + String name, String issuer, String secretKey, Emitter emit) { + bool isValid = true; if (name.isEmpty) { emit(state.copyWith(nameError: "The account name is required")); + isValid = false; } else if (name.length > 256) { emit(state.copyWith( nameError: "The account name cannot be longer than 256 characters")); + isValid = false; } if (issuer.length > 256) { emit(state.copyWith( issuer: "The account issuer cannot be longer than 256 characters")); + isValid = false; } + if (account != null) return isValid; + if (secretKey.isEmpty) { emit(state.copyWith(secretKeyError: "The secret key is required")); + isValid = false; } else if (secretKey.length < 16) { emit(state.copyWith( secretKeyError: "The secret key cannot be shorter than 16 characters")); + isValid = false; } else if (secretKey.length > 512) { emit(state.copyWith( secretKeyError: "The secret key cannot be longer than 512 characters")); + isValid = false; } else if (!Base32.isValid(secretKey)) { emit(state.copyWith( secretKeyError: "The secret key is not base 32 encoded")); + isValid = false; } - if (state.nameError == null && - state.issuerError == null && - state.secretKeyError == null) { - Account newAccount; - int? lastPosition = localRepositoryImpl.getAccountLastPosition(); - int position; + return isValid; + } - if (lastPosition != null) { - position = lastPosition + 1; - } else { - position = 0; - } + void _onAddOrEditAccount(AddOrEditAccount event, Emitter emit) { + String name = Uri.decodeFull(removeDiacritics(state.name.trim())); + String issuer = Uri.decodeFull(removeDiacritics(state.issuer.trim())); + String secretKey = state.secretKey.trim().toUpperCase(); + + if (_isFormValid(name, issuer, secretKey, emit)) { + int position = accountService.getLastPosition() + 1; if (account == null) { - newAccount = Account( - icon: state.iconKey, + Account newAccount = Account( + iconKey: state.iconKey, secret: secretKey, name: name, issuer: issuer, @@ -89,14 +107,15 @@ class ManualBloc extends Bloc { position: position, ); - Account? sameAccount = - localRepositoryImpl.getAccountBySecret(secretKey); + Account? sameAccount = accountRepository.getBySecret(secretKey); if (sameAccount == null) { - _storeAccount(emit, newAccount, "New account has been added"); + accountRepository.add(newAccount); + emit(state.copyWith(message: "New account has been added")); } else if (sameAccount.deleted) { newAccount.id = sameAccount.id; - _storeAccount(emit, newAccount, "New account has been added"); + accountRepository.add(newAccount); + emit(state.copyWith(message: "New account has been added")); } else { emit( state.copyWith(secretKeyError: "This secret key already exists")); @@ -105,16 +124,16 @@ class ManualBloc extends Bloc { account?.iconKey = state.iconKey; account?.name = name; account?.issuer = issuer; - account?.dbAlgorithm = - UriDecoder.getAlgorithmFromString(state.algorithmValue); - account?.digits = state.digitsValue; - account?.type = state.codeTypeValue; - account?.period = - state.codeTypeValue == "totp" ? state.intervalValue : null; - account?.toUpdate = true; - newAccount = account!; - - _storeAccount(emit, newAccount, "Account has been edited"); + if (account is Account) { + account?.dbAlgorithm = + UriDecoder.getAlgorithmFromString(state.algorithmValue); + account?.digits = state.digitsValue; + account?.type = state.codeTypeValue; + account?.period = + state.codeTypeValue == "totp" ? state.intervalValue : null; + } + + _updateAccount(emit, account!, "Account has been edited"); } } } @@ -134,8 +153,7 @@ class ManualBloc extends Bloc { state.copyWith( iconKey: event.issuer.isEmpty ? "default" - : simpleIcons.keys.firstWhere((v) => v.contains(event.issuer), - orElse: () => "default"), + : IconPickerHelper.findFirst(event.issuer), ), ); } diff --git a/lib/bloc/manual/manual_state.dart b/lib/bloc/manual/manual_state.dart index cb5ce87..510cd02 100644 --- a/lib/bloc/manual/manual_state.dart +++ b/lib/bloc/manual/manual_state.dart @@ -1,8 +1,7 @@ import 'package:equatable/equatable.dart'; +import 'package:otp_manager/models/shared_account.dart'; import 'package:otp_manager/utils/uri_decoder.dart'; -import '../../models/account.dart'; - class ManualState extends Equatable { final String iconKey; final String name; @@ -17,6 +16,7 @@ class ManualState extends Equatable { final int digitsValue; final bool isEdit; final String message; + final bool isSharedAccount; const ManualState({ required this.iconKey, @@ -32,9 +32,10 @@ class ManualState extends Equatable { required this.digitsValue, required this.isEdit, required this.message, + required this.isSharedAccount, }); - ManualState.initial(Account? account) + ManualState.initial(dynamic account) : iconKey = account?.iconKey ?? 'default', name = account?.name ?? "", issuer = account?.issuer ?? "", @@ -47,7 +48,8 @@ class ManualState extends Equatable { algorithmValue = UriDecoder.getAlgorithmFromAlgo(account?.algorithm), digitsValue = account?.digits ?? 6, isEdit = account != null, - message = ""; + message = "", + isSharedAccount = account is SharedAccount; ManualState copyWith({ String? iconKey, @@ -80,6 +82,7 @@ class ManualState extends Equatable { digitsValue: digitsValue ?? this.digitsValue, isEdit: isEdit, message: message ?? this.message, + isSharedAccount: isSharedAccount, ); } diff --git a/lib/bloc/otp_account/otp_account_bloc.dart b/lib/bloc/otp_account/otp_account_bloc.dart index 1c4d3fe..d46460f 100644 --- a/lib/bloc/otp_account/otp_account_bloc.dart +++ b/lib/bloc/otp_account/otp_account_bloc.dart @@ -1,26 +1,37 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:otp/otp.dart'; +import 'package:otp_manager/domain/nextcloud_service.dart'; import 'package:otp_manager/models/account.dart'; +import 'package:otp_manager/models/shared_account.dart'; +import 'package:otp_manager/repository/interface/account_repository.dart'; +import 'package:otp_manager/repository/interface/shared_account_repository.dart'; -import '../../repository/local_repository.dart'; import '../home/home_bloc.dart'; -import '../home/home_event.dart' hide IncrementCounter; +import '../home/home_event.dart'; import 'otp_account_event.dart'; import 'otp_account_state.dart'; class OtpAccountBloc extends Bloc { final HomeBloc homeBloc; - final LocalRepositoryImpl localRepositoryImpl; + final AccountRepository accountRepository; + final NextcloudService nextcloudService; + final SharedAccountRepository sharedAccountRepository; OtpAccountBloc({ required this.homeBloc, - required this.localRepositoryImpl, + required this.accountRepository, + required this.nextcloudService, + required this.sharedAccountRepository, }) : super(const OtpAccountState.initial()) { on(_onIncrementCounter); on(_onGenerateOtpCode); } - String _getOtp(Account account) { + String _getOtp(dynamic account) { + if (account is SharedAccount && !account.unlocked) { + return "Click here to unlock your shared account"; + } + if (account.type == "totp") { return OTP.generateTOTPCodeString( account.secret, @@ -30,27 +41,46 @@ class OtpAccountBloc extends Bloc { length: account.digits as int, isGoogle: true, ); - } else if (account.counter! > 0) { - return OTP.generateHOTPCodeString( - account.secret, - account.counter!, - algorithm: account.algorithm, - length: account.digits as int, - isGoogle: true, - ); + } else if (account.type == "hotp") { + if (account.counter! >= 0) { + return OTP.generateHOTPCodeString( + account.secret, + account.counter!, + algorithm: account.algorithm, + length: account.digits as int, + isGoogle: true, + ); + } + return "Click here to generate HOTP code"; } + return "null"; } void _onIncrementCounter( IncrementCounter event, Emitter emit) async { - event.account.counter = event.account.counter! + 1; - event.account.toUpdate = true; - localRepositoryImpl.updateAccount(event.account); + emit(state.copyWith(disableIncrement: true)); + int? updatedCounter = await nextcloudService.updateCounter(event.account); - emit(state.copyWith(otpCode: _getOtp(event.account))); + if (updatedCounter == null) { + homeBloc.add(const ShowMessage( + message: "There was an error while incrementing counter")); + homeBloc.add(const ShowMessage(message: "")); + } else { + event.account.counter = updatedCounter; + + if (event.account is Account) { + accountRepository.add(event.account); // update without sync + } else { + sharedAccountRepository.add(event.account); // update without sync + } + + emit(state.copyWith(otpCode: _getOtp(event.account))); + } + + await Future.delayed(const Duration(seconds: 1)); - homeBloc.add(NextcloudSync()); + emit(state.copyWith(disableIncrement: false)); } void _onGenerateOtpCode( diff --git a/lib/bloc/otp_account/otp_account_event.dart b/lib/bloc/otp_account/otp_account_event.dart index c13762a..26e5935 100644 --- a/lib/bloc/otp_account/otp_account_event.dart +++ b/lib/bloc/otp_account/otp_account_event.dart @@ -1,7 +1,5 @@ import 'package:equatable/equatable.dart'; -import '../../models/account.dart'; - class OtpAccountEvent extends Equatable { const OtpAccountEvent(); @@ -12,7 +10,7 @@ class OtpAccountEvent extends Equatable { class GenerateOtpCode extends OtpAccountEvent { const GenerateOtpCode({required this.account}); - final Account account; + final dynamic account; // Account | SharedAccount @override List get props => [account]; @@ -21,7 +19,7 @@ class GenerateOtpCode extends OtpAccountEvent { class IncrementCounter extends OtpAccountEvent { const IncrementCounter({required this.account}); - final Account account; + final dynamic account; // Account | SharedAccount @override List get props => [account]; diff --git a/lib/bloc/otp_account/otp_account_state.dart b/lib/bloc/otp_account/otp_account_state.dart index 21d947c..f0f8c30 100644 --- a/lib/bloc/otp_account/otp_account_state.dart +++ b/lib/bloc/otp_account/otp_account_state.dart @@ -2,16 +2,23 @@ import 'package:equatable/equatable.dart'; class OtpAccountState extends Equatable { final String? otpCode; + final bool disableIncrement; - const OtpAccountState({required this.otpCode}); + const OtpAccountState({ + required this.otpCode, + required this.disableIncrement, + }); - const OtpAccountState.initial() : this(otpCode: null); + const OtpAccountState.initial() + : this(otpCode: null, disableIncrement: false); - OtpAccountState copyWith({String? otpCode}) { + OtpAccountState copyWith({String? otpCode, bool? disableIncrement}) { return OtpAccountState( - otpCode: otpCode == "null" ? null : otpCode ?? this.otpCode); + otpCode: otpCode == "null" ? null : otpCode ?? this.otpCode, + disableIncrement: disableIncrement ?? this.disableIncrement, + ); } @override - List get props => [otpCode]; + List get props => [otpCode, disableIncrement]; } diff --git a/lib/bloc/otp_manager/otp_manager_bloc.dart b/lib/bloc/otp_manager/otp_manager_bloc.dart index f8ec6bd..50d0c19 100644 --- a/lib/bloc/otp_manager/otp_manager_bloc.dart +++ b/lib/bloc/otp_manager/otp_manager_bloc.dart @@ -1,15 +1,15 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:otp_manager/bloc/otp_manager/otp_manager_event.dart'; import 'package:otp_manager/bloc/otp_manager/otp_manager_state.dart'; -import 'package:otp_manager/repository/local_repository.dart'; +import 'package:otp_manager/repository/interface/user_repository.dart'; class OtpManagerBloc extends Bloc { - final LocalRepositoryImpl localRepositoryImpl; + final UserRepository userRepository; - OtpManagerBloc({required this.localRepositoryImpl}) + OtpManagerBloc({required this.userRepository}) : super(OtpManagerState.initial( - localRepositoryImpl.getUser(), - localRepositoryImpl.isLogged(), + userRepository.get(), + userRepository.isLogged(), )) { on(_onCopyWithTapToggled); on(_onDarkThemeToggled); @@ -17,17 +17,17 @@ class OtpManagerBloc extends Bloc { void _onCopyWithTapToggled( CopyWithTapToggled event, Emitter emit) { - final user = localRepositoryImpl.getUser(); + final user = userRepository.get(); user?.copyWithTap = !user.copyWithTap; - localRepositoryImpl.updateUser(user!); + userRepository.update(user!); emit(state.copyWith(copyWithTap: user.copyWithTap)); } void _onDarkThemeToggled( DarkThemeToggled event, Emitter emit) { - final user = localRepositoryImpl.getUser(); + final user = userRepository.get(); user?.darkTheme = !user.darkTheme; - localRepositoryImpl.updateUser(user!); + userRepository.update(user!); emit(state.copyWith(darkTheme: user.darkTheme)); } } diff --git a/lib/bloc/qr_code_scanner/qr_code_scanner_bloc.dart b/lib/bloc/qr_code_scanner/qr_code_scanner_bloc.dart index aceb007..3fc0114 100644 --- a/lib/bloc/qr_code_scanner/qr_code_scanner_bloc.dart +++ b/lib/bloc/qr_code_scanner/qr_code_scanner_bloc.dart @@ -1,16 +1,20 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:otp_manager/bloc/qr_code_scanner/qr_code_scanner_event.dart'; import 'package:otp_manager/bloc/qr_code_scanner/qr_code_scanner_state.dart'; -import 'package:otp_manager/repository/local_repository.dart'; +import 'package:otp_manager/repository/interface/account_repository.dart'; +import '../../domain/account_service.dart'; import '../../models/account.dart'; import '../../utils/uri_decoder.dart'; class QrCodeScannerBloc extends Bloc { - final LocalRepositoryImpl localRepositoryImpl; + final AccountRepository accountRepository; + final AccountService accountService; - QrCodeScannerBloc({required this.localRepositoryImpl}) - : super( + QrCodeScannerBloc({ + required this.accountRepository, + required this.accountService, + }) : super( const QrCodeScannerState.initial(), ) { on(_onErrorChanged); @@ -31,9 +35,10 @@ class QrCodeScannerBloc extends Bloc { var atLeastOneAdded = false; for (var account in newAccounts) { - if (!localRepositoryImpl.accountAlreadyExists(account.secret)) { + if (!accountRepository.alreadyExists(account.secret)) { atLeastOneAdded = true; - localRepositoryImpl.addAccount(account); + account.position = accountService.getLastPosition() + 1; + accountRepository.add(account); } } diff --git a/lib/bloc/settings/settings_bloc.dart b/lib/bloc/settings/settings_bloc.dart index 3498ca1..18d2734 100644 --- a/lib/bloc/settings/settings_bloc.dart +++ b/lib/bloc/settings/settings_bloc.dart @@ -2,17 +2,17 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:otp_manager/bloc/settings/settings_event.dart'; import 'package:otp_manager/bloc/settings/settings_state.dart'; -import 'package:otp_manager/models/user.dart'; -import 'package:otp_manager/repository/local_repository.dart'; import 'package:otp_manager/logger/save_log.dart'; +import 'package:otp_manager/models/user.dart'; +import 'package:otp_manager/repository/interface/user_repository.dart'; import 'package:otp_manager/utils/launch_url.dart'; import 'package:package_info_plus/package_info_plus.dart'; class SettingsBloc extends Bloc { - final LocalRepositoryImpl localRepositoryImpl; + final UserRepository userRepository; - SettingsBloc({required this.localRepositoryImpl}) - : super(SettingsState.initial(localRepositoryImpl.getUser()!)) { + SettingsBloc({required this.userRepository}) + : super(SettingsState.initial(userRepository.get()!)) { on(_onSaveLog); on(_onInitPackageInfo); on(_onOpenLink); @@ -40,13 +40,13 @@ class SettingsBloc extends Bloc { void _onAskTimeChanged(AskTimeChanged event, Emitter emit) { emit(state.copyWith(selectedAskTimeIndex: event.index)); - User user = localRepositoryImpl.getUser()!; + User user = userRepository.get()!; user.dbPasswordAskTime = event.index; _updatePasswordExpirationDate(user); - localRepositoryImpl.updateUser(user); + userRepository.update(user); } void _updatePasswordExpirationDate(User user) { diff --git a/lib/bloc/unlock_shared_account/unlock_shared_account_bloc.dart b/lib/bloc/unlock_shared_account/unlock_shared_account_bloc.dart new file mode 100644 index 0000000..05bc8eb --- /dev/null +++ b/lib/bloc/unlock_shared_account/unlock_shared_account_bloc.dart @@ -0,0 +1,65 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:otp_manager/bloc/home/home_event.dart'; +import 'package:otp_manager/bloc/unlock_shared_account/unlock_shared_account_event.dart'; +import 'package:otp_manager/bloc/unlock_shared_account/unlock_shared_account_state.dart'; +import 'package:otp_manager/repository/interface/shared_account_repository.dart'; + +import '../../domain/nextcloud_service.dart'; +import '../../routing/navigation_service.dart'; +import '../home/home_bloc.dart'; + +class UnlockSharedAccountBloc + extends Bloc { + final NextcloudService nextcloudService; + final SharedAccountRepository sharedAccountRepository; + final int accountId; + final HomeBloc homeBloc; + + final NavigationService _navigationService = NavigationService(); + + UnlockSharedAccountBloc({ + required this.sharedAccountRepository, + required this.nextcloudService, + required this.accountId, + required this.homeBloc, + }) : super( + const UnlockSharedAccountState.initial(), + ) { + on(_onPasswordSubmit); + on(_onPasswordChanged); + on(_onResetAttempts); + } + + void _onResetAttempts( + ResetAttempts event, Emitter emit) { + emit(state.copyWith(attempts: 3)); + } + + void _onPasswordChanged( + PasswordChanged event, Emitter emit) { + emit(state.copyWith(password: event.password, errorMsg: "")); + } + + void _error(Emitter emit, String msg) { + emit(state.copyWith(errorMsg: msg, attempts: state.attempts - 1)); + + if (state.attempts == 0) { + emit(state.copyWith(attempts: 3)); + } + } + + void _onPasswordSubmit( + PasswordSubmit event, Emitter emit) async { + String? result = + await nextcloudService.unlockSharedAccount(accountId, state.password); + + if (result == null) { + homeBloc.add( + const ShowMessage(message: "Shared account unlocked with success")); + homeBloc.add(NextcloudSync()); + _navigationService.goBack(); + } else { + _error(emit, result); + } + } +} diff --git a/lib/bloc/unlock_shared_account/unlock_shared_account_event.dart b/lib/bloc/unlock_shared_account/unlock_shared_account_event.dart new file mode 100644 index 0000000..bf0bf68 --- /dev/null +++ b/lib/bloc/unlock_shared_account/unlock_shared_account_event.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; + +class UnlockSharedAccountEvent extends Equatable { + const UnlockSharedAccountEvent(); + + @override + List get props => []; +} + +class PasswordChanged extends UnlockSharedAccountEvent { + const PasswordChanged({required this.password}); + + final String password; + + @override + List get props => [password]; +} + +class PasswordSubmit extends UnlockSharedAccountEvent {} + +class ResetAttempts extends UnlockSharedAccountEvent {} diff --git a/lib/bloc/unlock_shared_account/unlock_shared_account_state.dart b/lib/bloc/unlock_shared_account/unlock_shared_account_state.dart new file mode 100644 index 0000000..39a2f09 --- /dev/null +++ b/lib/bloc/unlock_shared_account/unlock_shared_account_state.dart @@ -0,0 +1,38 @@ +import 'package:equatable/equatable.dart'; + +class UnlockSharedAccountState extends Equatable { + final int attempts; + final String password; + final String message; + final String errorMsg; + + const UnlockSharedAccountState({ + required this.attempts, + required this.password, + required this.message, + required this.errorMsg, + }); + + const UnlockSharedAccountState.initial() + : attempts = 3, + password = "", + message = "", + errorMsg = ""; + UnlockSharedAccountState copyWith({ + int? attempts, + String? password, + String? message, + String? errorMsg, + bool? canShowFingerAuth, + }) { + return UnlockSharedAccountState( + attempts: attempts ?? this.attempts, + password: password ?? this.password, + message: message ?? this.message, + errorMsg: errorMsg ?? this.errorMsg, + ); + } + + @override + List get props => [attempts, password, message, errorMsg]; +} diff --git a/lib/bloc/web_viewer/web_viewer_bloc.dart b/lib/bloc/web_viewer/web_viewer_bloc.dart index e907009..d892a39 100644 --- a/lib/bloc/web_viewer/web_viewer_bloc.dart +++ b/lib/bloc/web_viewer/web_viewer_bloc.dart @@ -1,37 +1,38 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:nextcloud/core.dart'; import 'package:nextcloud/nextcloud.dart'; import 'package:otp_manager/bloc/web_viewer/web_viewer_event.dart'; import 'package:otp_manager/bloc/web_viewer/web_viewer_state.dart'; import 'package:otp_manager/models/user.dart'; +import 'package:otp_manager/repository/interface/user_repository.dart'; import 'package:webview_flutter/webview_flutter.dart'; import '../../main.dart' show logger; -import '../../repository/local_repository.dart'; import '../../routing/constants.dart'; import '../../routing/navigation_service.dart'; class WebViewerBloc extends Bloc { - final LocalRepositoryImpl localRepositoryImpl; + final UserRepository userRepository; final NavigationService _navigationService = NavigationService(); final String nextcloudUrl; - WebViewerBloc({required this.nextcloudUrl, required this.localRepositoryImpl}) + WebViewerBloc({required this.nextcloudUrl, required this.userRepository}) : super(WebViewerState.initial()) { on(_onInitNextcloudLogin); on(_onUpdateLoadingScreen); } - Future _nextcloudLoginSetup() async { + Future _nextcloudLoginFlowV2() async { final client = NextcloudClient( - nextcloudUrl, + Uri.parse(nextcloudUrl), userAgentOverride: 'OTP Manager App', ); - final NextcloudCoreLoginFlowInit init = await client.core.initLoginFlow(); + final init = await client.core.clientFlowLoginV2.init(); state.webViewController ..setNavigationDelegate( @@ -45,17 +46,17 @@ class WebViewerBloc extends Bloc { ); if (url.endsWith("grant") || url.endsWith("apptoken")) { - client.core - .getLoginFlowResult(token: init.poll.token) + client.core.clientFlowLoginV2 + .poll(token: init.body.poll.token) .then((result) { - localRepositoryImpl.updateUser( + userRepository.update( User( url: nextcloudUrl, - appPassword: result.appPassword, + appPassword: result.body.appPassword, isGuest: false, ), ); - _navigationService.resetToScreen(homeRoute); + _navigationService.resetToScreen(authRoute); }); } }, @@ -77,9 +78,7 @@ class WebViewerBloc extends Bloc { }, ), ) - ..loadRequest(Uri.parse(init.login)); - - return init; + ..loadRequest(Uri.parse(init.body.login)); } void _onUpdateLoadingScreen( @@ -91,7 +90,7 @@ class WebViewerBloc extends Bloc { InitNextcloudLogin event, Emitter emit) async { state.webViewController.setJavaScriptMode(JavaScriptMode.unrestricted); - await _nextcloudLoginSetup() + await _nextcloudLoginFlowV2() .timeout(const Duration(seconds: 10)) .catchError((error, stackTrace) { logger.e(error); diff --git a/lib/bloc/web_viewer/web_viewer_state.dart b/lib/bloc/web_viewer/web_viewer_state.dart index 8371db4..8593f3c 100644 --- a/lib/bloc/web_viewer/web_viewer_state.dart +++ b/lib/bloc/web_viewer/web_viewer_state.dart @@ -23,12 +23,13 @@ class WebViewerState extends Equatable { error = "", webViewController = WebViewController(); - WebViewerState copyWith( - {double? percentage, - bool? isLogin, - bool? isLoading, - String? error, - WebViewController? webViewController}) { + WebViewerState copyWith({ + double? percentage, + bool? isLogin, + bool? isLoading, + String? error, + WebViewController? webViewController, + }) { return WebViewerState( percentage: percentage ?? this.percentage, isLogin: isLogin ?? this.isLogin, diff --git a/lib/domain/account_service.dart b/lib/domain/account_service.dart new file mode 100644 index 0000000..7ddcf29 --- /dev/null +++ b/lib/domain/account_service.dart @@ -0,0 +1,147 @@ +import 'dart:math'; + +import 'package:otp_manager/models/shared_account.dart'; + +import '../main.dart' show logger; +import '../models/account.dart'; +import '../repository/interface/account_repository.dart'; +import '../repository/interface/shared_account_repository.dart'; + +/// Performs operations by managing both accounts (the user's own) and +/// shared accounts (those shared with the user) +class AccountService { + final AccountRepository accountRepository; + final SharedAccountRepository sharedAccountRepository; + + AccountService({ + required this.accountRepository, + required this.sharedAccountRepository, + }); + + int getLastPosition() { + int accountLastPosition = accountRepository.getLastPosition(); + int sharedAccountLastPosition = sharedAccountRepository.getLastPosition(); + + return max(accountLastPosition, sharedAccountLastPosition); + } + + void setAsDeleted(dynamic account) { + account.deleted = true; + + accountRepository.scalePositionAfter(account.position!); + sharedAccountRepository.scalePositionAfter(account.position!); + + account.position = null; + + account is SharedAccount + ? sharedAccountRepository.update(account) + : accountRepository.update(account); + } + + bool repairPositionError() { + logger.d("AccountRepositoryImpl._checkPositions start"); + + List allAccounts = [ + ...accountRepository.getVisible(), + ...sharedAccountRepository.getVisible(), + ]; + + allAccounts.sort((a, b) => a.position.compareTo(b.position)); + + void adjustAccountsPosition(int start, int difference) { + logger.d("AccountRepositoryImpl._adjustAccountsPosition start"); + + for (int i = start; i < allAccounts.length; i++) { + allAccounts[i].position = allAccounts[i].position! + difference; + allAccounts[i].toUpdate = true; + + if (allAccounts[i] is Account) { + accountRepository.update(allAccounts[i]); + } else { + sharedAccountRepository.update(allAccounts[i]); + } + } + } + + if (allAccounts.isNotEmpty) { + bool repairedError = false; + + // check if first account has position = 0 + if (allAccounts[0].position != 0) { + repairedError = true; + adjustAccountsPosition(0, -allAccounts[0].position); + } + + for (int i = 0; i < allAccounts.length - 1; i++) { + if (allAccounts[i].position == allAccounts[i + 1].position) { + // there are two accounts with same position + // increment by 1 the position of all accounts after + + repairedError = true; + adjustAccountsPosition(i + 1, 1); + } else if (allAccounts[i].position + 1 != allAccounts[i + 1].position) { + // accounts do not have a step of 1 + + repairedError = true; + allAccounts[i + 1].position = allAccounts[i].position + 1; + allAccounts[i + 1].toUpdate = true; + + if (allAccounts[i + 1] is Account) { + accountRepository.update(allAccounts[i + 1]); + } else { + sharedAccountRepository.update(allAccounts[i + 1]); + } + } + } + + return repairedError; + } + + return false; + } + + void reorder(int oldIndex, int newIndex) { + List accountsBetween; + List sharedAccountsBetween; + + int difference; + int newPosition = newIndex; + + if (newIndex > oldIndex) { + newPosition -= 1; + accountsBetween = + accountRepository.getBetweenPositions(oldIndex, newPosition); + sharedAccountsBetween = + sharedAccountRepository.getBetweenPositions(oldIndex, newPosition); + difference = -1; + } else { + accountsBetween = + accountRepository.getBetweenPositions(newIndex - 1, oldIndex - 1); + sharedAccountsBetween = sharedAccountRepository.getBetweenPositions( + newIndex - 1, oldIndex - 1); + difference = 1; + } + + dynamic accountToMove = (accountRepository.getByPosition(oldIndex) ?? + sharedAccountRepository.getByPosition(oldIndex))!; + + for (Account accountBetween in accountsBetween) { + accountBetween.position = accountBetween.position! + difference; + accountRepository.update(accountBetween); + } + + for (SharedAccount sharedAccountBetween in sharedAccountsBetween) { + sharedAccountBetween.position = + sharedAccountBetween.position! + difference; + sharedAccountRepository.update(sharedAccountBetween); + } + + accountToMove.position = newPosition; + + if (accountToMove is Account) { + accountRepository.update(accountToMove); + } else { + sharedAccountRepository.update(accountToMove); + } + } +} diff --git a/lib/domain/nextcloud_service.dart b/lib/domain/nextcloud_service.dart index 01d371b..e847eec 100644 --- a/lib/domain/nextcloud_service.dart +++ b/lib/domain/nextcloud_service.dart @@ -1,49 +1,52 @@ import 'dart:convert'; -import 'package:http/http.dart' as http; +import 'package:otp_manager/domain/account_service.dart'; import 'package:otp_manager/main.dart'; -import 'package:otp_manager/repository/local_repository.dart'; +import 'package:otp_manager/models/account.dart'; +import 'package:otp_manager/repository/interface/account_repository.dart'; +import 'package:otp_manager/repository/interface/shared_account_repository.dart'; +import 'package:otp_manager/repository/interface/user_repository.dart'; import 'package:otp_manager/routing/constants.dart'; import 'package:otp_manager/routing/navigation_service.dart'; import 'package:otp_manager/utils/encryption.dart'; +import 'package:otp_manager/utils/nextcloud_ocs_api.dart'; +import 'package:package_info_plus/package_info_plus.dart'; -import '../repository/nextcloud_repository.dart'; +import '../repository/interface/nextcloud_repository.dart'; import '../utils/base32.dart'; class NextcloudService { - final NextcloudRepositoryImpl _nextcloudRepositoryImpl; - final LocalRepositoryImpl _localRepositoryImpl; - - NextcloudService(this._nextcloudRepositoryImpl, this._localRepositoryImpl); + final NextcloudRepository nextcloudRepository; + final UserRepository userRepository; + final AccountRepository accountRepository; + final AccountService accountService; + final SharedAccountRepository sharedAccountRepository; + final Encryption encryption; + + NextcloudService({ + required this.nextcloudRepository, + required this.userRepository, + required this.accountRepository, + required this.accountService, + required this.sharedAccountRepository, + required this.encryption, + }); Future> checkPassword(String password) async { logger.d("NextcloudService.checkPassword start"); Map result = {"error": null, "iv": null}; - try { - http.Response response = await _nextcloudRepositoryImpl.sendHttpRequest( - _localRepositoryImpl.getUser()!, - "password/check", - jsonEncode({"password": password}), - ); - - if (response.statusCode == 400) { - var body = jsonDecode(response.body); - logger.e("statusCode: ${response.statusCode}\nbody: ${response.body}"); - result["error"] = body["error"]; - } else if (response.statusCode == 404) { - result["error"] = - "You need to set a password before. Please update the OTP Manager extension on your Nextcloud server to version 0.3.0 or higher."; - } else { - var body = jsonDecode(response.body); - result["iv"] = body["iv"]; - } - } catch (e) { - logger.e(e); - result["error"] = - "An error encountered while checking password. Try to reload after a while!"; - } + await nextcloudRepository.sendHttpRequest( + resource: PasswordAPI.check, + data: {"password": password}, + onComplete: (response) => result["iv"] = jsonDecode(response.body)["iv"], + onFailed: (response) => result["error"] = jsonDecode( + response.body)["error"] ?? + "You need to set a password before. Please update the OTP Manager extension on your Nextcloud server to version 0.3.0 or higher.", + onError: () => result["error"] = + "An error encountered while checking password. Try to reload after a while!", + ); return result; } @@ -53,65 +56,73 @@ class NextcloudService { Map syncResult = { "error": null, - "toAdd": [], - "toEdit": [] + "accounts": {"toAdd": [], "toEdit": []}, + "sharedAccounts": {"toAdd": [], "toEdit": []}, }; - final accounts = _localRepositoryImpl.getAllAccounts(); - final user = _localRepositoryImpl.getUser()!; + final accounts = accountRepository.getAll(); + final sharedAccounts = sharedAccountRepository.getAll(); + final user = userRepository.get()!; if (user.password == null || user.iv == null) { NavigationService().replaceScreen(authRoute); } for (var e in accounts) { - e.encryptedSecret ??= - Encryption.encrypt(e.secret, user.password!, user.iv!); + e.encryptedSecret ??= encryption.encrypt(data: e.secret); + accountRepository.add(e); // update without sync } - var data = jsonEncode({"accounts": jsonDecode(accounts.toString())}); + final appInfo = await PackageInfo.fromPlatform(); - try { - final user = _localRepositoryImpl.getUser(); - http.Response response = await _nextcloudRepositoryImpl.sendHttpRequest( - user, - "accounts/sync", - data, - ); + var data = { + "accounts": jsonDecode(accounts.toString()), + "sharedAccounts": jsonDecode(sharedAccounts.toString()), + "appVersion": appInfo.version + }; - if (response.statusCode == 200) { - _localRepositoryImpl.updateNeverSync(); + await nextcloudRepository.sendHttpRequest( + resource: SyncAPI.sync, + data: data, + onComplete: (response) { var body = jsonDecode(response.body); + if (body.isNotEmpty) { - _localRepositoryImpl.deleteOldAccounts(body["toDelete"]); - syncResult["toAdd"] = body["toAdd"]; - syncResult["toEdit"] = body["toEdit"]; - } + accountRepository.updateNeverSync(); + sharedAccountRepository.updateNeverSync(); + accountRepository.deleteOld(body["accounts"]["toDelete"]); + sharedAccountRepository.deleteOld(body["sharedAccounts"]["toDelete"]); - if (_localRepositoryImpl.repairPositionError()) sync(); - } else { - logger.e("statusCode: ${response.statusCode}\nbody: ${response.body}"); - syncResult["error"] = - "The nextcloud server returns an error. Try to reload after a while!"; - } - } catch (e) { - logger.e(e); - syncResult["error"] = - "An error encountered while synchronising. Try to reload after a while!"; - } + syncResult["accounts"] = body["accounts"]; + syncResult["sharedAccounts"] = body["sharedAccounts"]; + } + }, + onFailed: (response) { + if (response.statusCode == 404) { + syncResult["error"] = + "Please update the OTP Manager Nextcloud extension to version >= 0.5.0"; + } else { + syncResult["error"] = jsonDecode(response.body)["error"] ?? + "The nextcloud server returns an error. Try to reload after a while!"; + } + }, + onError: () => syncResult["error"] = + "An error encountered while synchronising. Try to reload after a while!", + ); return syncResult; } bool _decryptSecretAccounts(List accounts) { - final user = _localRepositoryImpl.getUser()!; + final user = userRepository.get()!; for (var account in accounts) { account["encryptedSecret"] = account["secret"]; + if (account["unlocked"] == 0) continue; + try { - var decrypted = - Encryption.decrypt(account["secret"], user.password!, user.iv!); + String decrypted = encryption.decrypt(dataBase64: account["secret"])!; if (!Base32.isValid(decrypted)) throw FormatException; @@ -119,22 +130,64 @@ class NextcloudService { } catch (_) { user.password = null; user.iv = null; - _localRepositoryImpl.updateUser(user); + userRepository.update(user); return false; } } return true; } - - bool syncAccountsToAddToEdit(List accountsToAdd, List accountsToEdit) { - if (_decryptSecretAccounts(accountsToAdd) && - _decryptSecretAccounts(accountsToEdit)) { - _localRepositoryImpl.addNewAccounts(accountsToAdd); - _localRepositoryImpl.updateEditedAccounts(accountsToEdit); + + bool syncAccountsToAddToEdit( + Map accounts, Map sharedAccounts) { + if (_decryptSecretAccounts(accounts["toAdd"]) && + _decryptSecretAccounts(accounts["toEdit"]) && + _decryptSecretAccounts(sharedAccounts["toAdd"]) && + _decryptSecretAccounts(sharedAccounts["toEdit"])) { + accountRepository.addNew(accounts["toAdd"]); + accountRepository.updateEdited(accounts["toEdit"]); + sharedAccountRepository.addNew(sharedAccounts["toAdd"]); + sharedAccountRepository.updateEdited(sharedAccounts["toEdit"]); + return true; } else { return false; } } + + Future unlockSharedAccount( + int accountId, String sharedPassword) async { + final user = userRepository.get()!; + + String? result; + + await nextcloudRepository.sendHttpRequest( + resource: "share/unlock", + data: { + "accountId": accountId, + "currentPassword": user.password!, + "tempPassword": sharedPassword, + }, + onFailed: (response) => result = jsonDecode(response.body)["error"] ?? + "An error encountered while checking password. Try to reload after a while!", + onError: () => result = + "An error encountered while checking password. Try to reload after a while!", + ); + + return result; + } + + Future updateCounter(dynamic account) async { + int? result; + + await nextcloudRepository.sendHttpRequest( + resource: account is Account + ? AccountAPI.updateCounter + : SharedAccountAPI.updateCounter, + data: {"secret": account.encryptedSecret}, + onComplete: (response) => result = int.tryParse(response.body), + ); + + return result; + } } diff --git a/lib/main.dart b/lib/main.dart index dbd3d75..8922a4e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,10 +4,18 @@ import 'package:flutter/material.dart' hide Router; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:logger/logger.dart' hide FileOutput; import 'package:otp_manager/bloc/otp_manager/otp_manager_bloc.dart'; +import 'package:otp_manager/domain/account_service.dart'; import 'package:otp_manager/domain/nextcloud_service.dart'; import 'package:otp_manager/logger/filter.dart'; -import 'package:otp_manager/repository/local_repository.dart'; -import 'package:otp_manager/repository/nextcloud_repository.dart'; +import 'package:otp_manager/repository/interface/account_repository.dart'; +import 'package:otp_manager/repository/impl/account_repository_impl.dart'; +import 'package:otp_manager/repository/impl/nextcloud_repository_impl.dart'; +import 'package:otp_manager/repository/impl/shared_account_repository_impl.dart'; +import 'package:otp_manager/repository/impl/user_repository_impl.dart'; +import 'package:otp_manager/repository/interface/nextcloud_repository.dart'; +import 'package:otp_manager/repository/interface/shared_account_repository.dart'; +import 'package:otp_manager/repository/interface/user_repository.dart'; +import 'package:otp_manager/utils/encryption.dart'; import 'logger/file_output.dart'; import "object_box/objectbox.dart"; @@ -46,22 +54,45 @@ Future main() async { runApp( MultiRepositoryProvider( providers: [ - RepositoryProvider( - create: (_) => LocalRepositoryImpl(), + RepositoryProvider( + create: (_) => UserRepositoryImpl(), ), - RepositoryProvider( - create: (_) => NextcloudRepositoryImpl(), + RepositoryProvider( + create: (_) => AccountRepositoryImpl(), + ), + RepositoryProvider( + create: (_) => SharedAccountRepositoryImpl(), + ), + RepositoryProvider( + create: (context) => NextcloudRepositoryImpl( + userRepository: context.read(), + ), + ), + RepositoryProvider( + create: (context) => Encryption( + userRepository: context.read(), + ), + ), + RepositoryProvider( + create: (context) => AccountService( + accountRepository: context.read(), + sharedAccountRepository: context.read(), + ), ), RepositoryProvider( create: (context) => NextcloudService( - context.read(), - context.read(), + userRepository: context.read(), + accountService: context.read(), + accountRepository: context.read(), + nextcloudRepository: context.read(), + sharedAccountRepository: context.read(), + encryption: context.read(), ), ), ], child: BlocProvider( create: (context) => OtpManagerBloc( - localRepositoryImpl: context.read(), + userRepository: context.read(), ), child: const OtpManager(), ), diff --git a/lib/models/account.dart b/lib/models/account.dart index 62db998..c5657e5 100644 --- a/lib/models/account.dart +++ b/lib/models/account.dart @@ -1,9 +1,7 @@ import 'package:objectbox/objectbox.dart'; import 'package:otp/otp.dart'; -import '../utils/simple_icons.dart'; - -enum AlgorithmTypes { sha1, sha256, sha512 } +import '../utils/algorithms.dart'; @Entity() class Account { @@ -20,7 +18,7 @@ class Account { String type; int? period; - // HOTP code + // HOTP counter int? counter; // Synchronization @@ -64,10 +62,10 @@ class Account { this.isNew = true, int? counter, int? dbAlgorithm, - String? icon, + this.iconKey = "default", }) { if (type == "hotp") { - this.counter = counter ?? 0; + this.counter = counter ?? -1; } this.dbAlgorithm = dbAlgorithm; @@ -75,21 +73,12 @@ class Account { // set default value digits = digits ?? 6; period = period ?? 30; - - if(icon != null && icon != "default") { - iconKey = icon; - } else if(issuer != null && issuer != "") { - String toFind = issuer!.replaceAll(" ", "").toLowerCase(); - iconKey = simpleIcons.keys.firstWhere((v) => v.contains(toFind), orElse: () => "default"); - } else { - iconKey = "default"; - } } void _ensureStableEnumValues() { - assert(AlgorithmTypes.sha1.index == 0); - assert(AlgorithmTypes.sha256.index == 1); - assert(AlgorithmTypes.sha512.index == 2); + assert(Algorithms.sha1.index == 0); + assert(Algorithms.sha256.index == 1); + assert(Algorithms.sha512.index == 2); } String toUri() { @@ -106,7 +95,7 @@ class Account { @override toString() => '{' - '"id": "$id", ' + '"id": $id, ' '"secret": "$encryptedSecret", ' '"name": "$name", ' '"issuer": "$issuer", ' diff --git a/lib/models/shared_account.dart b/lib/models/shared_account.dart new file mode 100644 index 0000000..c820814 --- /dev/null +++ b/lib/models/shared_account.dart @@ -0,0 +1,121 @@ +import 'package:objectbox/objectbox.dart'; +import 'package:otp/otp.dart'; + +import '../utils/algorithms.dart'; + +@Entity() +class SharedAccount { + int id = 0; + + @Unique() + String? secret; + @Unique() + String encryptedSecret; + + // Customizable fields + String name; + String? issuer; + int? position; + String iconKey = 'default'; + + // Required fields to generate code + @Property(uid: 541832795838973838) + int period; + @Property(uid: 8305642788148493574) + int digits; + String type; + + @Transient() + late Algorithm algorithm; + + int? get dbAlgorithm { + _ensureStableEnumValues(); + return algorithm.index; + } + + set dbAlgorithm(int? value) { + _ensureStableEnumValues(); + if (value == 1) { + algorithm = Algorithm.SHA256; + } else if (value == 2) { + algorithm = Algorithm.SHA512; + } else { + algorithm = Algorithm.SHA1; + } + } + + int? counter; + + String password; + String iv; + bool unlocked; + + DateTime? expiredAt; + + String sharerUserId; + + // Synchronization + bool deleted = false; + bool toUpdate = false; + int nextcloudAccountId; // the account_id stored on nextcloud server + + SharedAccount({ + this.secret, + required this.encryptedSecret, + required this.name, + this.issuer, + this.position, + required this.period, + required this.digits, + required this.type, + required this.unlocked, + required this.password, + required this.iv, + this.toUpdate = false, + required this.nextcloudAccountId, + required this.sharerUserId, + this.expiredAt, + int? dbAlgorithm, + int? counter, + this.iconKey = "default", + }) { + if (type == "hotp") { + this.counter = counter ?? -1; + } + + this.dbAlgorithm = dbAlgorithm; + } + + String toUri() { + return Uri.encodeFull("otpauth://" + "$type/" + "$name?" + "secret=$secret&" + "issuer=$issuer&" + "period=$period&" + "digits=$digits&" + "algorithm=${algorithm.name.toUpperCase()}" + "${type == "hotp" ? '&counter=$counter' : ''}"); + } + + void _ensureStableEnumValues() { + assert(Algorithms.sha1.index == 0); + assert(Algorithms.sha256.index == 1); + assert(Algorithms.sha512.index == 2); + } + + @override + toString() => '{' + '"id": $id, ' + '"secret": "$encryptedSecret", ' + '"name": "$name", ' + '"issuer": "$issuer", ' + '"position": $position, ' + '"unlocked": $unlocked, ' + '"icon": "$iconKey", ' + '"deleted": $deleted, ' + '"toUpdate": $toUpdate, ' + '"accountId": $nextcloudAccountId, ' + '"expiredAt": "$expiredAt"' + '}'; +} diff --git a/lib/models/user.dart b/lib/models/user.dart index 59a775d..ca4b1ea 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -1,6 +1,12 @@ import 'package:objectbox/objectbox.dart'; -enum PasswordAskTime { everyOpening, oneMinutes, threeMinutes, fiveMinutes, never } +enum PasswordAskTime { + everyOpening, + oneMinutes, + threeMinutes, + fiveMinutes, + never +} @Entity() class User { @@ -67,8 +73,8 @@ class User { @override toString() => '{' 'id: $id, ' - 'url: $url, ' - 'appPassword: $appPassword, ' + 'url: "$url", ' + 'appPassword: "$appPassword", ' 'copyWithTap: $copyWithTap, ' 'darkTheme: $darkTheme, ' 'passwordAskTime: $passwordAskTime, ' diff --git a/lib/object_box/objectbox-model.json b/lib/object_box/objectbox-model.json index 32c4499..3deddba 100644 --- a/lib/object_box/objectbox-model.json +++ b/lib/object_box/objectbox-model.json @@ -4,181 +4,320 @@ "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", "entities": [ { - "id": "1:4285661817482440357", - "lastPropertyId": "16:8717556734128686126", - "name": "Account", + "id": "2:7664507538808873390", + "lastPropertyId": "14:6803514055320618522", + "name": "User", "properties": [ { - "id": "1:4196324365274800843", + "id": "1:44689720708305736", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:5499781806572440173", + "id": "2:5526531381599208511", + "name": "url", + "type": 9 + }, + { + "id": "3:3402658675008878628", + "name": "appPassword", + "type": 9 + }, + { + "id": "4:8749150439072134116", + "name": "copyWithTap", + "type": 1 + }, + { + "id": "5:5941937808846895761", + "name": "darkTheme", + "type": 1 + }, + { + "id": "6:6627109253779031772", + "name": "password", + "type": 9 + }, + { + "id": "7:1799353112031269157", + "name": "isGuest", + "type": 1 + }, + { + "id": "8:3931286562875030069", + "name": "iv", + "type": 9 + }, + { + "id": "9:2981116505199087943", + "name": "dbPasswordAskTime", + "type": 6 + }, + { + "id": "10:1615550960879192014", + "name": "passwordExpirationDate", + "type": 10 + }, + { + "id": "12:4342457815063604129", + "name": "sortedByNameDesc", + "type": 1 + }, + { + "id": "13:7062927719479673821", + "name": "sortedByIssuerDesc", + "type": 1 + }, + { + "id": "14:6803514055320618522", + "name": "sortedByIdDesc", + "type": 1 + } + ], + "relations": [] + }, + { + "id": "3:860749960594552220", + "lastPropertyId": "26:6022724058279062421", + "name": "SharedAccount", + "properties": [ + { + "id": "1:465967563402052303", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:6725507773058692621", "name": "secret", "type": 9, "flags": 2080, - "indexId": "1:6872732689934629113" + "indexId": "4:36583233985468225" + }, + { + "id": "3:9197254111241877069", + "name": "encryptedSecret", + "type": 9, + "flags": 2080, + "indexId": "5:1310402725267861224" }, { - "id": "3:5331048462233554699", + "id": "4:1110271080951620260", "name": "name", "type": 9 }, { - "id": "4:3414124140591454233", + "id": "5:254997892986597869", "name": "issuer", "type": 9 }, { - "id": "5:5765109435556013442", - "name": "digits", + "id": "6:8550273249514471470", + "name": "position", "type": 6 }, { - "id": "6:8055134117450821090", - "name": "type", + "id": "7:5373299474332088004", + "name": "iconKey", "type": 9 }, { - "id": "7:625049145654439937", - "name": "period", - "type": 6 + "id": "8:4510449948011559159", + "name": "unlocked", + "type": 1 }, { - "id": "8:977657200259796930", - "name": "counter", - "type": 6 + "id": "9:5308568307637569631", + "name": "password", + "type": 9 }, { - "id": "9:5666433557640929472", + "id": "10:3809077292105225208", + "name": "iv", + "type": 9 + }, + { + "id": "11:6145624297622387664", + "name": "expiredAt", + "type": 10 + }, + { + "id": "14:1877362120768794223", "name": "deleted", "type": 1 }, { - "id": "10:6153010926974240376", + "id": "15:20869043581372742", "name": "toUpdate", "type": 1 }, { - "id": "11:1138692681414253855", - "name": "isNew", - "type": 1 + "id": "18:462781350638672157", + "name": "nextcloudAccountId", + "type": 6 }, { - "id": "12:2560099814064116621", - "name": "position", + "id": "21:8529014441444047819", + "name": "type", + "type": 9 + }, + { + "id": "22:7780104596846351901", + "name": "counter", "type": 6 }, { - "id": "13:4227822487933505977", + "id": "23:2186306724992388881", "name": "dbAlgorithm", "type": 6 }, { - "id": "15:4148299309095428910", - "name": "encryptedSecret", - "type": 9, - "flags": 2080, - "indexId": "3:8995856266510989675" + "id": "24:541832795838973838", + "name": "period", + "type": 6 }, { - "id": "16:8717556734128686126", - "name": "iconKey", + "id": "25:8305642788148493574", + "name": "digits", + "type": 6 + }, + { + "id": "26:6022724058279062421", + "name": "sharerUserId", "type": 9 } ], "relations": [] }, { - "id": "2:7664507538808873390", - "lastPropertyId": "14:6803514055320618522", - "name": "User", + "id": "4:7771284864676363757", + "lastPropertyId": "17:8473003272334681411", + "name": "Account", "properties": [ { - "id": "1:44689720708305736", + "id": "1:5690512713677150815", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:5526531381599208511", - "name": "url", - "type": 9 - }, - { - "id": "3:3402658675008878628", - "name": "appPassword", - "type": 9 + "id": "2:764365313117772762", + "name": "secret", + "type": 9, + "flags": 2080, + "indexId": "7:1765617462195928164" }, { - "id": "4:8749150439072134116", - "name": "copyWithTap", - "type": 1 + "id": "3:4866305243911162766", + "name": "encryptedSecret", + "type": 9, + "flags": 2080, + "indexId": "8:5049009102107265940" }, { - "id": "5:5941937808846895761", - "name": "darkTheme", - "type": 1 + "id": "4:5627044136747708999", + "name": "name", + "type": 9 }, { - "id": "6:6627109253779031772", - "name": "password", + "id": "5:1176160872258484875", + "name": "issuer", "type": 9 }, { - "id": "7:1799353112031269157", - "name": "isGuest", - "type": 1 + "id": "6:3840452760786499784", + "name": "digits", + "type": 6 }, { - "id": "8:3931286562875030069", - "name": "iv", + "id": "7:1201923751681621487", + "name": "type", "type": 9 }, { - "id": "9:2981116505199087943", - "name": "dbPasswordAskTime", + "id": "8:6164019483591908921", + "name": "period", "type": 6 }, { - "id": "10:1615550960879192014", - "name": "passwordExpirationDate", - "type": 10 + "id": "9:4266999627018316383", + "name": "counter", + "type": 6 }, { - "id": "12:4342457815063604129", - "name": "sortedByNameDesc", + "id": "10:6379989081224713273", + "name": "deleted", "type": 1 }, { - "id": "13:7062927719479673821", - "name": "sortedByIssuerDesc", + "id": "11:1495470283274525251", + "name": "toUpdate", "type": 1 }, { - "id": "14:6803514055320618522", - "name": "sortedByIdDesc", + "id": "12:81203181874669708", + "name": "isNew", "type": 1 + }, + { + "id": "13:1972502715044199489", + "name": "position", + "type": 6 + }, + { + "id": "14:2929728897220498988", + "name": "iconKey", + "type": 9 + }, + { + "id": "15:8773757693584991685", + "name": "dbAlgorithm", + "type": 6 } ], "relations": [] } ], - "lastEntityId": "2:7664507538808873390", - "lastIndexId": "3:8995856266510989675", + "lastEntityId": "4:7771284864676363757", + "lastIndexId": "8:5049009102107265940", "lastRelationId": "0:0", "lastSequenceId": "0:0", "modelVersion": 5, "modelVersionParserMinimum": 5, - "retiredEntityUids": [], + "retiredEntityUids": [ + 4285661817482440357 + ], "retiredIndexUids": [ - 2541609273737449069 + 2541609273737449069, + 5279383409808009499 ], "retiredPropertyUids": [ 6227200453650360822, - 6298052200764982397 + 6298052200764982397, + 4364559736598528656, + 8677054977235051389, + 3183151217015853559, + 7236789457296781163, + 4196324365274800843, + 5499781806572440173, + 5331048462233554699, + 3414124140591454233, + 5765109435556013442, + 8055134117450821090, + 625049145654439937, + 977657200259796930, + 5666433557640929472, + 6153010926974240376, + 1138692681414253855, + 2560099814064116621, + 4227822487933505977, + 4148299309095428910, + 8717556734128686126, + 2299181908849101080, + 8473003272334681411, + 7179818338099359069, + 4944082648458440469 ], "retiredRelationUids": [], "version": 1 diff --git a/lib/otp_manager.dart b/lib/otp_manager.dart index f5a94b1..92a0afe 100644 --- a/lib/otp_manager.dart +++ b/lib/otp_manager.dart @@ -21,19 +21,11 @@ class OtpManager extends HookWidget { colorScheme: lightColorScheme, primaryColor: Colors.blue, primarySwatch: Colors.blue, - /*textTheme: TextTheme( - titleMedium: GoogleFonts.roboto(/*color: Colors.white*/), - titleLarge: GoogleFonts.roboto(), - ),*/ ), darkTheme: ThemeData( colorScheme: darkColorScheme, primaryColor: Colors.blue, primarySwatch: Colors.blue, - /*textTheme: TextTheme( - titleMedium: GoogleFonts.roboto(/*color: Colors.white*/), - titleLarge: GoogleFonts.roboto(), - ),*/ ), themeMode: state.darkTheme ? ThemeMode.dark : ThemeMode.light, onGenerateRoute: Router.generateRoute, diff --git a/lib/repository/impl/account_repository_impl.dart b/lib/repository/impl/account_repository_impl.dart new file mode 100644 index 0000000..7b8715c --- /dev/null +++ b/lib/repository/impl/account_repository_impl.dart @@ -0,0 +1,173 @@ +import '../../main.dart' show objectBox, logger; +import '../../models/account.dart'; +import '../../object_box/objectbox.g.dart'; +import '../../utils/icon_picker_helper.dart'; +import '../interface/account_repository.dart'; + +class AccountRepositoryImpl extends AccountRepository { + @override + void addNew(List nextcloudAccounts) { + logger.d("AccountRepositoryImpl._addNew start"); + + for (var account in nextcloudAccounts) { + String iconKey = "default"; + + if (account["icon"] != null && account["icon"] != "default") { + iconKey = account["icon"]; + } else if (account["issuer"] != null && account["issuer"] != "") { + iconKey = IconPickerHelper.findFirst(account["issuer"]); + } + + super.box.put( + Account( + name: account["name"], + issuer: account["issuer"], + secret: account["secret"], + encryptedSecret: account["encryptedSecret"], + type: account["type"], + dbAlgorithm: account["algorithm"], + digits: account["digits"], + period: account["period"], + counter: account["counter"], + iconKey: iconKey, + position: account["position"], + toUpdate: iconKey != "default", + isNew: false, + ), + ); + } + } + + @override + void updateEdited(List nextcloudAccounts) { + logger.d("AccountRepositoryImpl._updateEdited start"); + + for (var account in nextcloudAccounts) { + Account? accountToUpdate = super + .box + .query(Account_.secret.equals(account["secret"])) + .build() + .findFirst(); + accountToUpdate?.name = account["name"]; + accountToUpdate?.issuer = account["issuer"]; + accountToUpdate?.digits = account["digits"]; + accountToUpdate?.type = account["type"]; + accountToUpdate?.dbAlgorithm = account["algorithm"]; + accountToUpdate?.period = account["period"]; + accountToUpdate?.counter = account["counter"]; + accountToUpdate?.iconKey = account["icon"] ?? accountToUpdate.iconKey; + accountToUpdate?.position = account["position"]; + super.box.put(accountToUpdate!); + } + } + + @override + bool alreadyExists(String secret) { + return super + .box + .query(Account_.secret.equals(secret)) + .build() + .find() + .isNotEmpty; + } + + @override + void deleteOld(List nextcloudAccountIds) { + logger.d("AccountRepositoryImpl._deleteOld start"); + + super + .box + .query(Account_.deleted.equals(true)) + .build() + .find() + .forEach((Account account) => super.box.remove(account.id)); + + for (var id in nextcloudAccountIds) { + super.box.remove(id); + } + } + + @override + void updateNeverSync() { + logger.d("AccountRepositoryImpl._updateNeverSync start"); + + super + .box + .query(Account_.toUpdate.equals(true) | Account_.isNew.equals(true)) + .build() + .find() + .forEach((Account account) { + account.isNew = false; + account.toUpdate = false; + super.box.put(account); + }); + } + + @override + List getVisible() { + return (super.box.query(Account_.deleted.equals(false)) + ..order(Account_.position)) + .build() + .find(); + } + + @override + List getVisibleFiltered(String filter) { + return (super.box.query(Account_.deleted.equals(false) & + (Account_.name.contains(filter, caseSensitive: false) | + Account_.issuer.contains(filter, caseSensitive: false))) + ..order(Account_.position)) + .build() + .find(); + } + + @override + Account? getBySecret(String secret) { + return super.box.query(Account_.secret.equals(secret)).build().findFirst(); + } + + @override + void scalePositionAfter(int position) { + super + .box + .query(Account_.deleted.equals(false) & + Account_.position.greaterThan(position)) + .build() + .find() + .forEach((account) { + account.position = account.position! - 1; + account.toUpdate = true; + super.box.put(account); + }); + } + + @override + List getBetweenPositions(int min, int max) { + return super + .box + .query(Account_.deleted.equals(false) & + Account_.position.greaterThan(min) & + Account_.position.lessOrEqual(max)) + .build() + .find(); + } + + @override + Account? getByPosition(int position) { + return super + .box + .query(Account_.position.equals(position)) + .build() + .findFirst(); + } + + @override + int getLastPosition() { + Account? lastAccount = (super.box.query(Account_.deleted.equals(false)) + ..order(Account_.position, flags: Order.descending)) + .build() + .findFirst(); + + return lastAccount == null ? -1 : lastAccount.position!; + } +} diff --git a/lib/repository/impl/nextcloud_repository_impl.dart b/lib/repository/impl/nextcloud_repository_impl.dart new file mode 100644 index 0000000..78605ed --- /dev/null +++ b/lib/repository/impl/nextcloud_repository_impl.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:otp_manager/repository/interface/user_repository.dart'; + +import '../../main.dart' show logger; +import '../../models/user.dart'; +import '../interface/nextcloud_repository.dart'; + +class NextcloudRepositoryImpl extends NextcloudRepository { + final UserRepository userRepository; + late final User? user = userRepository.get(); + + NextcloudRepositoryImpl({required this.userRepository}); + + @override + Future sendHttpRequest({ + required String resource, + required Map data, + void Function(http.Response)? onComplete, + void Function(http.Response)? onFailed, + void Function()? onError, + }) async { + logger.d("NextcloudRepositoryImpl._sendHttpRequest start"); + + if (user == null) { + onError?.call(); + return; + } + + await http + .post( + Uri.parse("${user?.url}/ocs/v2.php/apps/otpmanager/$resource"), + headers: { + 'Authorization': 'Bearer ${user?.appPassword}', + 'Content-Type': 'application/json', + }, + body: jsonEncode(data), + ) + .timeout(const Duration(seconds: 5)) + .then((response) { + if (response.statusCode == 200) { + onComplete?.call(response); + } else { + logger.e("statusCode: ${response.statusCode}\nbody: ${response.body}"); + onFailed?.call(response); + } + }).catchError((e, stackTrace) { + logger.e(e); + onError?.call(); + }); + } +} diff --git a/lib/repository/impl/shared_account_repository_impl.dart b/lib/repository/impl/shared_account_repository_impl.dart new file mode 100644 index 0000000..b74d0dd --- /dev/null +++ b/lib/repository/impl/shared_account_repository_impl.dart @@ -0,0 +1,172 @@ +import 'package:otp_manager/models/shared_account.dart'; +import 'package:otp_manager/object_box/objectbox.g.dart'; +import 'package:otp_manager/utils/icon_picker_helper.dart'; + +import '../../main.dart' show objectBox, logger; +import '../interface/shared_account_repository.dart'; + +class SharedAccountRepositoryImpl extends SharedAccountRepository { + @override + void addNew(List nextcloudAccounts) { + logger.d("SharedAccountRepositoryImpl._addNew start"); + + for (var sharedAccount in nextcloudAccounts) { + String iconKey = "default"; + + if (sharedAccount["icon"] != null && sharedAccount["icon"] != "default") { + iconKey = sharedAccount["icon"]; + } else if (sharedAccount["issuer"] != null && + sharedAccount["issuer"] != "") { + iconKey = IconPickerHelper.findFirst(sharedAccount["issuer"]); + } + + super.box.put( + SharedAccount( + name: sharedAccount["name"], + issuer: sharedAccount["issuer"], + secret: sharedAccount["secret"], + encryptedSecret: sharedAccount["encryptedSecret"], + iconKey: iconKey, + position: sharedAccount["position"], + toUpdate: iconKey != "default", + password: sharedAccount["password"], + iv: sharedAccount["iv"], + nextcloudAccountId: sharedAccount["account_id"], + expiredAt: sharedAccount["expired_at"] != null + ? DateTime.parse(sharedAccount["expired_at"]) + : null, + period: sharedAccount["period"], + digits: sharedAccount["digits"], + type: sharedAccount["type"], + counter: sharedAccount["counter"], + dbAlgorithm: sharedAccount["algorithm"], + unlocked: sharedAccount["unlocked"] == 1, + sharerUserId: sharedAccount["user_id"], + ), + ); + } + } + + @override + void updateEdited(List nextcloudAccounts) { + logger.d("SharedAccountRepositoryImpl._updateEdited start"); + + for (var sharedAccount in nextcloudAccounts) { + SharedAccount? sharedAccountToUpdate = super + .box + .query(SharedAccount_.nextcloudAccountId + .equals(sharedAccount["account_id"])) + .build() + .findFirst(); + + sharedAccountToUpdate?.name = sharedAccount["name"]; + sharedAccountToUpdate?.issuer = sharedAccount["issuer"]; + sharedAccountToUpdate?.secret = sharedAccount["secret"]; + sharedAccountToUpdate?.encryptedSecret = sharedAccount["encryptedSecret"]; + sharedAccountToUpdate?.unlocked = sharedAccount["unlocked"] == 1; + sharedAccountToUpdate?.counter = sharedAccount["counter"]; + sharedAccountToUpdate?.expiredAt = sharedAccount["expired_at"] != null + ? DateTime.parse(sharedAccount["expired_at"]) + : null; + sharedAccountToUpdate?.iconKey = + sharedAccount["icon"] ?? sharedAccountToUpdate.iconKey; + sharedAccountToUpdate?.position = sharedAccount["position"]; + + super.box.put(sharedAccountToUpdate!); + } + } + + @override + List getVisible() { + return (super.box.query(SharedAccount_.deleted.equals(false)) + ..order(SharedAccount_.position)) + .build() + .find(); + } + + @override + List getVisibleFiltered(String filter) { + return (super.box.query(SharedAccount_.deleted.equals(false) & + (SharedAccount_.name.contains(filter, caseSensitive: false) | + SharedAccount_.issuer.contains(filter, caseSensitive: false))) + ..order(SharedAccount_.position)) + .build() + .find(); + } + + @override + void deleteOld(List nextcloudAccountIds) { + logger.d("SharedAccountRepositoryImpl._deleteOld start"); + + super + .box + .query(SharedAccount_.deleted.equals(true)) + .build() + .find() + .forEach((SharedAccount account) => super.box.remove(account.id)); + + for (var id in nextcloudAccountIds) { + super.box.remove(id); + } + } + + @override + void updateNeverSync() { + logger.d("SharedAccountRepositoryImpl._updateNeverSync start"); + + super + .box + .query(SharedAccount_.toUpdate.equals(true)) + .build() + .find() + .forEach((SharedAccount account) { + account.toUpdate = false; + super.box.put(account); + }); + } + + @override + void scalePositionAfter(int position) { + super + .box + .query(SharedAccount_.deleted.equals(false) & + SharedAccount_.position.greaterThan(position)) + .build() + .find() + .forEach((sharedAccount) { + sharedAccount.position = sharedAccount.position! - 1; + update(sharedAccount); + }); + } + + @override + int getLastPosition() { + SharedAccount? lastAccount = + (super.box.query(SharedAccount_.deleted.equals(false)) + ..order(SharedAccount_.position, flags: Order.descending)) + .build() + .findFirst(); + + return lastAccount == null ? -1 : lastAccount.position!; + } + + @override + List getBetweenPositions(int min, int max) { + return super + .box + .query(SharedAccount_.deleted.equals(false) & + SharedAccount_.position.greaterThan(min) & + SharedAccount_.position.lessOrEqual(max)) + .build() + .find(); + } + + @override + SharedAccount? getByPosition(int position) { + return super + .box + .query(SharedAccount_.position.equals(position)) + .build() + .findFirst(); + } +} diff --git a/lib/repository/impl/user_repository_impl.dart b/lib/repository/impl/user_repository_impl.dart new file mode 100644 index 0000000..3954104 --- /dev/null +++ b/lib/repository/impl/user_repository_impl.dart @@ -0,0 +1,34 @@ +import '../../main.dart' show objectBox; +import '../../models/user.dart'; +import '../interface/user_repository.dart'; + +class UserRepositoryImpl implements UserRepository { + final _userBox = objectBox.store.box(); + + @override + void add(User user) { + _userBox.put(user); + } + + @override + User? get() { + final users = _userBox.getAll(); + + return users.isNotEmpty ? users[0] : null; + } + + @override + bool isLogged() { + return _userBox.getAll().isNotEmpty; + } + + @override + void removeAll() { + _userBox.removeAll(); + } + + @override + void update(User user) { + _userBox.put(user); + } +} diff --git a/lib/repository/interface/account_repository.dart b/lib/repository/interface/account_repository.dart new file mode 100644 index 0000000..9b7e950 --- /dev/null +++ b/lib/repository/interface/account_repository.dart @@ -0,0 +1,8 @@ +import 'package:otp_manager/repository/interface/base_account_repository.dart'; + +import '../../models/account.dart'; + +abstract class AccountRepository extends BaseAccountRepository { + bool alreadyExists(String secret); + Account? getBySecret(String secret); +} diff --git a/lib/repository/interface/base_account_repository.dart b/lib/repository/interface/base_account_repository.dart new file mode 100644 index 0000000..06e017a --- /dev/null +++ b/lib/repository/interface/base_account_repository.dart @@ -0,0 +1,41 @@ +import '../../main.dart'; + +abstract class BaseAccountRepository { + final box = objectBox.store.box(); + + void add(AccountType account) { + box.put(account); + } + + void update(AccountType account) { + (account as dynamic).toUpdate = true; + box.put(account); + } + + void remove(int id) { + box.remove(id); + } + + void removeAll() { + box.removeAll(); + } + + AccountType? get(int id) { + return box.get(id); + } + + List getAll() { + return box.getAll(); + } + + List getVisible(); + List getVisibleFiltered(String filter); + void updateNeverSync(); + void deleteOld(List nextcloudAccountIds); + void addNew(List nextcloudAccounts); + void updateEdited(List nextcloudAccounts); + void scalePositionAfter(int position); + int getLastPosition(); + List getBetweenPositions(int min, int max); + AccountType? getByPosition(int position); +} diff --git a/lib/repository/interface/nextcloud_repository.dart b/lib/repository/interface/nextcloud_repository.dart new file mode 100644 index 0000000..9426365 --- /dev/null +++ b/lib/repository/interface/nextcloud_repository.dart @@ -0,0 +1,11 @@ +import 'package:http/http.dart'; + +abstract class NextcloudRepository { + Future sendHttpRequest({ + required String resource, + required Map data, + void Function(Response)? onComplete, + void Function(Response)? onFailed, + void Function()? onError, + }); +} diff --git a/lib/repository/interface/shared_account_repository.dart b/lib/repository/interface/shared_account_repository.dart new file mode 100644 index 0000000..ced7d89 --- /dev/null +++ b/lib/repository/interface/shared_account_repository.dart @@ -0,0 +1,6 @@ +import 'package:otp_manager/repository/interface/base_account_repository.dart'; + +import '../../models/shared_account.dart'; + +abstract class SharedAccountRepository + extends BaseAccountRepository {} diff --git a/lib/repository/interface/user_repository.dart b/lib/repository/interface/user_repository.dart new file mode 100644 index 0000000..0a5701b --- /dev/null +++ b/lib/repository/interface/user_repository.dart @@ -0,0 +1,9 @@ +import '../../models/user.dart'; + +abstract class UserRepository { + User? get(); + void add(User user); + void update(User user); + bool isLogged(); + void removeAll(); +} diff --git a/lib/repository/local_repository.dart b/lib/repository/local_repository.dart deleted file mode 100644 index fa28831..0000000 --- a/lib/repository/local_repository.dart +++ /dev/null @@ -1,309 +0,0 @@ -import 'package:otp_manager/main.dart' show logger, objectBox; - -import '../models/account.dart'; -import '../models/user.dart'; -import '../object_box/objectbox.g.dart'; - -abstract class LocalRepository { - User? getUser(); - void addUser(User user); - void updateUser(User user); - void addAccount(Account account); - bool accountAlreadyExists(String secret); - bool isLogged(); - List getAllAccounts(); - void updateEditedAccounts(List nextcloudAccounts); - void deleteOldAccounts(List nextcloudAccountIds); - void addNewAccounts(List nextcloudAccounts); - void updateNeverSync(); - bool repairPositionError(); - List getVisibleAccounts(); - List getVisibleFilteredAccounts(String filter); - void removeAllUsers(); - void removeAllAccounts(); - void removeAccount(int id); - void updateAccount(Account account); - Account? getAccount(int id); - Account? getAccountBySecret(String secret); - void scaleAccountsPositionAfter(int position); - List getAccountBetweenPositions(int min, int max); - Account? getAccountByPosition(int position); - bool setAccountAsDeleted(int id); - int? getAccountLastPosition(); -} - -class LocalRepositoryImpl extends LocalRepository { - final _userBox = objectBox.store.box(); - final _accountBox = objectBox.store.box(); - - @override - User? getUser() { - final users = _userBox.getAll(); - - return users.isNotEmpty ? users[0] : null; - } - - @override - void addUser(User user) { - _userBox.put(user); - } - - @override - void updateUser(User user) { - _userBox.put(user); - } - - @override - void addAccount(Account account) { - _accountBox.put(account); - } - - @override - bool accountAlreadyExists(String secret) { - return _accountBox - .query(Account_.secret.equals(secret)) - .build() - .find() - .isNotEmpty; - } - - @override - bool isLogged() { - return _userBox.getAll().isNotEmpty; - } - - @override - List getAllAccounts() { - return _accountBox.getAll(); - } - - @override - void updateEditedAccounts(List nextcloudAccounts) { - logger.d("Nextcloud._updateEditedAccounts start"); - - for (var account in nextcloudAccounts) { - Account? accountToUpdate = _accountBox - .query(Account_.secret.equals(account["secret"])) - .build() - .findFirst(); - accountToUpdate?.name = account["name"]; - accountToUpdate?.issuer = account["issuer"]; - accountToUpdate?.digits = account["digits"]; - accountToUpdate?.type = account["type"]; - accountToUpdate?.dbAlgorithm = account["algorithm"]; - accountToUpdate?.period = account["period"]; - accountToUpdate?.counter = account["counter"]; - accountToUpdate?.iconKey = account["icon"] ?? accountToUpdate.iconKey; - accountToUpdate?.position = account["position"]; - _accountBox.put(accountToUpdate!); - } - } - - @override - void deleteOldAccounts(List nextcloudAccountIds) { - logger.d("Nextcloud._deleteOldAccounts start"); - - _accountBox - .query(Account_.deleted.equals(true)) - .build() - .find() - .forEach((Account account) => _accountBox.remove(account.id)); - - for (var id in nextcloudAccountIds) { - _accountBox.remove(int.parse(id)); - } - } - - @override - bool repairPositionError() { - logger.d("Nextcloud._checkPositions start"); - - final accounts = _accountBox - .query(Account_.deleted.equals(false)) - .order(Account_.position) - .build() - .find(); - - void adjustAccountsPosition(int start, int difference) { - logger.d("Nextcloud._adjustAccountsPosition start"); - - for (int i = start; i < accounts.length; i++) { - accounts[i].position = accounts[i].position! + difference; - accounts[i].toUpdate = true; - _accountBox.put(accounts[i]); - } - } - - bool repairedError = false; - - // check if first account has position = 0 - if (accounts.isNotEmpty) { - if (accounts[0].position != 0) { - repairedError = true; - adjustAccountsPosition(0, -1); - } - - // if there two accounts with same position - // increment by 1 the position of all accounts after - for (int i = 0; i < accounts.length - 1; i++) { - if (accounts[i].position == accounts[i + 1].position) { - repairedError = true; - adjustAccountsPosition(i + 1, 1); - } - } - - return repairedError; - } - - return false; - } - - @override - void updateNeverSync() { - logger.d("Nextcloud._updateNeverSync start"); - - _accountBox - .query(Account_.toUpdate.equals(true) | Account_.isNew.equals(true)) - .build() - .find() - .forEach((Account account) { - account.isNew = false; - account.toUpdate = false; - _accountBox.put(account); - }); - } - - @override - void addNewAccounts(List nextcloudAccounts) { - logger.d("Nextcloud._addNewAccounts start"); - - for (var account in nextcloudAccounts) { - _accountBox.put( - Account( - name: account["name"], - issuer: account["issuer"], - secret: account["secret"], - encryptedSecret: account["encryptedSecret"], - type: account["type"], - dbAlgorithm: account["algorithm"], - digits: account["digits"], - period: account["period"], - counter: account["counter"], - icon: account["icon"], - position: account["position"], - toUpdate: false, - isNew: false, - ), - ); - } - } - - @override - List getVisibleAccounts() { - return (_accountBox.query(Account_.deleted.equals(false)) - ..order(Account_.position)) - .build() - .find(); - } - - @override - List getVisibleFilteredAccounts(String filter) { - return (_accountBox.query(Account_.deleted.equals(false) & - (Account_.name.contains(filter, caseSensitive: false) | - Account_.issuer.contains(filter, caseSensitive: false))) - ..order(Account_.position)) - .build() - .find(); - } - - @override - void removeAllAccounts() { - _accountBox.removeAll(); - } - - @override - void removeAllUsers() { - _userBox.removeAll(); - } - - @override - void removeAccount(int id) { - _accountBox.remove(id); - } - - @override - void updateAccount(Account account) { - _accountBox.put(account); - } - - @override - Account? getAccount(int id) { - return _accountBox.get(id); - } - - @override - Account? getAccountBySecret(String secret) { - return _accountBox - .query(Account_.secret.equals(secret)) - .build() - .findFirst(); - } - - @override - void scaleAccountsPositionAfter(int position) { - _accountBox - .query(Account_.deleted.equals(false) & - Account_.position.greaterThan(position)) - .build() - .find() - .forEach((account) { - account.position = account.position! - 1; - account.toUpdate = true; - _accountBox.put(account); - }); - } - - @override - List getAccountBetweenPositions(int min, int max) { - return _accountBox - .query(Account_.deleted.equals(false) & - Account_.position.greaterThan(min) & - Account_.position.lessOrEqual(max)) - .build() - .find(); - } - - @override - Account? getAccountByPosition(int position) { - return _accountBox - .query(Account_.position.equals(position)) - .build() - .findFirst(); - } - - @override - bool setAccountAsDeleted(int id) { - Account? accountToRemove = getAccount(id); - - if (accountToRemove != null) { - accountToRemove.deleted = true; - - scaleAccountsPositionAfter(accountToRemove.position!); - - accountToRemove.position = null; - updateAccount(accountToRemove); - return true; - } - - return false; - } - - @override - int? getAccountLastPosition() { - return (_accountBox.query(Account_.deleted.equals(false)) - ..order(Account_.position, flags: Order.descending)) - .build() - .findFirst() - ?.position; - } -} diff --git a/lib/repository/nextcloud_repository.dart b/lib/repository/nextcloud_repository.dart deleted file mode 100644 index 702e1a0..0000000 --- a/lib/repository/nextcloud_repository.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:http/http.dart' as http; - -import '../main.dart' show logger; - -abstract class NextcloudRepository { - Future sendHttpRequest(user, resource, data); -} - -class NextcloudRepositoryImpl extends NextcloudRepository { - @override - Future sendHttpRequest(user, resource, data) { - logger.d("Nextcloud._sendHttpRequest start"); - - return http - .post( - Uri.parse("${user.url}/index.php/apps/otpmanager/$resource"), - headers: { - 'Authorization': 'Bearer ${user.appPassword}', - 'Content-Type': 'application/json' - }, - body: data, - ) - .timeout(const Duration(seconds: 5)) - .catchError((e, stackTrace) { - logger.e(e); - return http.Response( - "The nextcloud server is unreachable now. Try to reload after a while!", - 600, - ); - }); - } -} diff --git a/lib/routing/router.dart b/lib/routing/router.dart index 7d7a573..11460e5 100644 --- a/lib/routing/router.dart +++ b/lib/routing/router.dart @@ -7,12 +7,14 @@ import 'package:otp_manager/bloc/login/login_bloc.dart'; import 'package:otp_manager/bloc/manual/manual_bloc.dart'; import 'package:otp_manager/bloc/qr_code_scanner/qr_code_scanner_bloc.dart'; import 'package:otp_manager/bloc/settings/settings_bloc.dart'; +import 'package:otp_manager/domain/account_service.dart'; import 'package:otp_manager/domain/nextcloud_service.dart'; -import 'package:otp_manager/repository/local_repository.dart'; +import 'package:otp_manager/repository/interface/account_repository.dart'; +import 'package:otp_manager/repository/interface/shared_account_repository.dart'; +import 'package:otp_manager/repository/interface/user_repository.dart'; import '../bloc/home/home_bloc.dart'; import '../bloc/web_viewer/web_viewer_bloc.dart'; -import '../models/account.dart'; import '../screens/account_details.dart'; import '../screens/auth.dart'; import '../screens/home/home.dart'; @@ -31,7 +33,10 @@ class Router { return CupertinoPageRoute( builder: (_) => BlocProvider( create: (context) => HomeBloc( - localRepositoryImpl: context.read(), + userRepository: context.read(), + accountRepository: context.read(), + accountService: context.read(), + sharedAccountRepository: context.read(), nextcloudService: context.read(), ), child: const Home(), @@ -43,7 +48,7 @@ class Router { return CupertinoPageRoute( builder: (_) => BlocProvider( create: (context) => SettingsBloc( - localRepositoryImpl: context.read(), + userRepository: context.read(), ), child: Settings(), ), @@ -52,7 +57,8 @@ class Router { return CupertinoPageRoute( builder: (_) => BlocProvider( create: (context) => QrCodeScannerBloc( - localRepositoryImpl: context.read(), + accountRepository: context.read(), + accountService: context.read(), ), child: QrCodeScanner(), ), @@ -61,8 +67,11 @@ class Router { return CupertinoPageRoute( builder: (_) => BlocProvider( create: (context) => AccountDetailsBloc( - localRepositoryImpl: context.read(), - account: settings.arguments as Account, + userRepository: context.read(), + accountRepository: context.read(), + accountService: context.read(), + sharedAccountRepository: context.read(), + account: settings.arguments as dynamic, ), child: const AccountDetails(), ), @@ -71,7 +80,7 @@ class Router { return CupertinoPageRoute( builder: (_) => BlocProvider( create: (context) => LoginBloc( - localRepositoryImpl: context.read(), + userRepository: context.read(), ), child: const Login(), ), @@ -80,20 +89,22 @@ class Router { return CupertinoPageRoute( builder: (_) => BlocProvider( create: (context) => WebViewerBloc( + userRepository: context.read(), nextcloudUrl: settings.arguments as String, - localRepositoryImpl: context.read(), ), child: const WebViewer(), ), ); case manualRoute: Map arguments = settings.arguments as Map; - Account? account = arguments["account"]; + var account = arguments["account"]; return CupertinoPageRoute( builder: (_) => BlocProvider( create: (context) => ManualBloc( - localRepositoryImpl: context.read(), + accountRepository: context.read(), + sharedAccountRepository: context.read(), + accountService: context.read(), account: account, ), child: const Manual(), @@ -103,7 +114,7 @@ class Router { return CupertinoPageRoute( builder: (_) => BlocProvider( create: (context) => AuthBloc( - localRepositoryImpl: context.read(), + userRepository: context.read(), nextcloudService: context.read(), ), child: Auth(), diff --git a/lib/screens/account_details.dart b/lib/screens/account_details.dart index b5a2899..f325e2e 100644 --- a/lib/screens/account_details.dart +++ b/lib/screens/account_details.dart @@ -1,7 +1,11 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; import 'package:otp_manager/bloc/account_details/account_details_bloc.dart'; import 'package:otp_manager/bloc/account_details/account_details_state.dart'; +import 'package:otp_manager/models/shared_account.dart'; import 'package:otp_manager/utils/show_snackbar.dart'; import '../bloc/account_details/account_details_event.dart'; @@ -12,7 +16,7 @@ import '../utils/delete_modal.dart'; class AccountDetails extends StatelessWidget { const AccountDetails({Key? key}) : super(key: key); - ListTile newItem(String title, String trailingText) { + ListTile accountDetail(String title, String trailingText) { return ListTile( title: Text(title), trailing: SizedBox( @@ -38,18 +42,12 @@ class AccountDetails extends StatelessWidget { IconButton( icon: const Icon(Icons.edit), onPressed: () { - if (context.read().state.password == "") { - showSnackBar( - context: context, - msg: "To edit an account you have to set a pin before"); - } else { - NavigationService().navigateTo( - manualRoute, - arguments: { - "account": context.read().state.account - }, - ); - } + NavigationService().navigateTo( + manualRoute, + arguments: { + "account": context.read().state.account + }, + ); }, ), IconButton( @@ -66,33 +64,87 @@ class AccountDetails extends StatelessWidget { ), body: BlocConsumer( listener: (context, state) { - if (state.accountDeleted != "") { - showSnackBar(context: context, msg: state.accountDeleted); + if (state.message != "") { + showSnackBar(context: context, msg: state.message); } }, builder: (context, state) { - return Center( - child: ListView( - children: ListTile.divideTiles( - context: context, - tiles: [ - newItem("Name", state.account.name), - newItem("Issuer", state.account.issuer ?? ""), - if (state.account.type == "totp") - newItem("Period", "${state.account.period}s"), - newItem("Digits", state.account.digits.toString()), - newItem("Algorithm", - state.account.algorithm.toString().split(".")[1]), - newItem("Type", state.account.type.toUpperCase()), - if (state.account.type == "hotp") - newItem( + return Column( + children: [ + ListView( + shrinkWrap: true, + children: ListTile.divideTiles( + context: context, + tiles: [ + accountDetail("Name", state.account.name), + accountDetail("Issuer", state.account.issuer ?? ""), + if (state.account.type == "totp") + accountDetail("Period", "${state.account.period}s"), + accountDetail("Digits", state.account.digits.toString()), + accountDetail("Algorithm", + state.account.algorithm.toString().split(".")[1]), + accountDetail("Type", state.account.type.toUpperCase()), + if (state.account.type == "hotp") + accountDetail( "Counter", state.account.counter != null ? state.account.counter.toString() - : "") - ], - ).toList(), - ), + : "", + ), + if (state.account is SharedAccount) ...[ + accountDetail( + "Expired At", + state.account.expiredAt == null + ? "Never expires" + : DateFormat('yyyy-MM-dd') + .format(state.account.expiredAt), + ), + ], + ], + ).toList(), + ), + if (state.account is SharedAccount) ...[ + Padding( + padding: const EdgeInsets.only(top: 20.0, bottom: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Stack( + alignment: Alignment.center, + children: [ + const Icon(Icons.person), + ClipRRect( + borderRadius: BorderRadius.circular(100.0), + child: Image.network( + "${state.serverUrl}/avatar/${state.account.sharerUserId}/64", + fit: BoxFit.fill, + height: 50.0, + width: 50.0, + errorBuilder: (_, __, ___) => + const Icon(Icons.person), + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Transform.rotate( + angle: 90 * pi / 180, + child: const Icon(Icons.link), + ), + ) + ], + ), + ), + Text( + "Shared by ${state.account.sharerUserId}", + style: const TextStyle( + fontStyle: FontStyle.italic, + //color: Colors.grey, + ), + ), + ] + ], ); }, ), diff --git a/lib/screens/auth.dart b/lib/screens/auth.dart index 917c963..862bced 100644 --- a/lib/screens/auth.dart +++ b/lib/screens/auth.dart @@ -74,20 +74,18 @@ class Auth extends HookWidget { return Stack( alignment: Alignment.center, children: [ - const Padding( - padding: EdgeInsets.fromLTRB(0, 0, 0, 250), - child: Icon( - Icons.lock, - size: 100, - color: Colors.blue, - ), - ), Column( mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon( + Icons.lock, + size: 100, + color: Theme.of(context).primaryColor, + ), Padding( - padding: const EdgeInsets.fromLTRB(10, 20, 10, 0), + padding: const EdgeInsets.fromLTRB(10, 50, 10, 50), child: AuthInput( + label: "Password", onChanged: (value) => context .read() .add(PasswordChanged(password: value)), @@ -97,6 +95,16 @@ class Auth extends HookWidget { errorMsg: state.message, ), ), + if (state.canShowFingerAuth) + IconButton( + onPressed: () => + context.read().add(ShowFingerAuth()), + icon: Icon( + Icons.fingerprint, + size: 60, + color: Theme.of(context).primaryColor, + ), + ), ], ), ], diff --git a/lib/screens/home/body.dart b/lib/screens/home/body.dart index cafbfb1..400cb65 100644 --- a/lib/screens/home/body.dart +++ b/lib/screens/home/body.dart @@ -16,11 +16,8 @@ class HomeBody extends StatelessWidget { Widget build(BuildContext context) { return BlocConsumer( listener: (context, state) { - if (state.syncError != "") { - showSnackBar(context: context, msg: state.syncError); - } - if (state.accountDeleted != "") { - showSnackBar(context: context, msg: state.accountDeleted); + if (state.message != "") { + showSnackBar(context: context, msg: state.message); } }, builder: (context, state) { @@ -56,7 +53,7 @@ class HomeBody extends StatelessWidget { ), ), ], - if (state.accounts.isEmpty) + if (state.accounts.isEmpty && state.searchBarValue == "") Positioned( right: 0, bottom: 0, @@ -71,7 +68,17 @@ class HomeBody extends StatelessWidget { onRefresh: () async => context.read().add(NextcloudSync()), child: state.accounts.isEmpty - ? const EmptyData() + ? (state.searchBarValue == "" + ? const EmptyData( + imageName: "no_accounts", + title: "Add your first account", + description: + "You currently have no account. Synchronise by dragging down or create a new one below.") + : EmptyData( + imageName: "no_results", + title: "No accounts for: ${state.searchBarValue}", + description: "", + )) : const OtpAccountsList(), ), ) diff --git a/lib/screens/home/empty_data.dart b/lib/screens/home/empty_data.dart index ccc1391..560fa61 100644 --- a/lib/screens/home/empty_data.dart +++ b/lib/screens/home/empty_data.dart @@ -1,11 +1,20 @@ import 'package:flutter/material.dart'; class EmptyData extends StatelessWidget { - const EmptyData({Key? key}) : super(key: key); + const EmptyData({ + Key? key, + required this.imageName, + required this.title, + required this.description, + }) : super(key: key); + + final String imageName; + final String title; + final String description; @override Widget build(BuildContext context) { - return const CustomScrollView( + return CustomScrollView( slivers: [ SliverFillRemaining( child: Column( @@ -16,14 +25,14 @@ class EmptyData extends StatelessWidget { Image( height: 300, image: AssetImage( - "./assets/images/no_account.png", + "./assets/images/$imageName.png", ), ), Padding( - padding: EdgeInsets.only(top: 10), + padding: const EdgeInsets.only(top: 10), child: Text( - "Add your first account", - style: TextStyle( + title, + style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w600, ), @@ -34,11 +43,10 @@ class EmptyData extends StatelessWidget { child: SizedBox( width: 300, child: Padding( - padding: EdgeInsets.only(top: 8), + padding: const EdgeInsets.only(top: 8), child: Text( - "You currently have no account. " - "Synchronise by dragging down or create a new one below.", - style: TextStyle( + description, + style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: Colors.grey, diff --git a/lib/screens/home/home.dart b/lib/screens/home/home.dart index a8dcb22..345dfef 100644 --- a/lib/screens/home/home.dart +++ b/lib/screens/home/home.dart @@ -15,22 +15,28 @@ class Home extends HookWidget { Widget build(BuildContext context) { return UpgradeAlert( upgrader: Upgrader( - debugLogging: false, - debugDisplayOnce: false, - showIgnore: false, - showLater: true, - durationUntilAlertAgain: const Duration(hours: 2), - willDisplayUpgrade: ({String? appStoreVersion, required bool display, String? installedVersion, String? minAppVersion}) { - if (!display) { - context.read().add(const IsAppUpdatedChanged(value: true)); + debugLogging: false, + debugDisplayOnce: false, + showIgnore: false, + showLater: true, + durationUntilAlertAgain: const Duration(hours: 2), + willDisplayUpgrade: ({ + String? appStoreVersion, + required bool display, + String? installedVersion, + String? minAppVersion, + }) { + if (!display) { + context + .read() + .add(const IsAppUpdatedChanged(value: true)); + context.read().add(NextcloudSync()); + } + }, + onLater: () { context.read().add(NextcloudSync()); - } - }, - onLater: () { - context.read().add(NextcloudSync()); - return true; - } - ), + return true; + }), child: const Scaffold( appBar: HomeAppBar(), body: HomeBody(), diff --git a/lib/screens/otp_account.dart b/lib/screens/home/otp_account.dart similarity index 56% rename from lib/screens/otp_account.dart rename to lib/screens/home/otp_account.dart index 47d46f2..e858ec0 100644 --- a/lib/screens/otp_account.dart +++ b/lib/screens/home/otp_account.dart @@ -7,29 +7,54 @@ import 'package:flutter_slidable/flutter_slidable.dart' import 'package:otp_manager/bloc/otp_account/otp_account_bloc.dart'; import 'package:otp_manager/bloc/otp_account/otp_account_event.dart'; import 'package:otp_manager/bloc/otp_manager/otp_manager_state.dart'; +import 'package:otp_manager/bloc/unlock_shared_account/unlock_shared_account_bloc.dart'; +import 'package:otp_manager/domain/nextcloud_service.dart'; import 'package:otp_manager/models/account.dart'; +import 'package:otp_manager/models/shared_account.dart'; +import 'package:otp_manager/repository/interface/shared_account_repository.dart'; +import 'package:otp_manager/screens/unlock_shared_account.dart'; import 'package:otp_manager/utils/custom_circular_countdown_timer.dart'; import 'package:otp_manager/utils/custom_slidable_action.dart'; import 'package:otp_manager/utils/simple_icons.dart'; -import '../bloc/home/home_bloc.dart'; -import '../bloc/home/home_event.dart' hide IncrementCounter; -import '../bloc/otp_account/otp_account_state.dart'; -import '../bloc/otp_manager/otp_manager_bloc.dart'; -import '../routing/constants.dart'; -import '../routing/navigation_service.dart'; -import '../utils/delete_modal.dart'; -import '../utils/qr_code_modal.dart'; -import '../utils/show_snackbar.dart'; -import '../utils/tooltip.dart'; +import '../../bloc/home/home_bloc.dart'; +import '../../bloc/home/home_event.dart'; +import '../../bloc/otp_account/otp_account_state.dart'; +import '../../bloc/otp_manager/otp_manager_bloc.dart'; +import '../../routing/constants.dart'; +import '../../routing/navigation_service.dart'; +import '../../utils/delete_modal.dart'; +import '../../utils/qr_code_modal.dart'; +import '../../utils/show_snackbar.dart'; +import '../../utils/tooltip.dart'; class OtpAccount extends HookWidget { OtpAccount({Key? key, required this.account}) : super(key: key); - final Account account; + final dynamic account; // Account | SharedAccount final NavigationService _navigationService = NavigationService(); + void _showUnlockSharedAccountModal(BuildContext context) { + showModalBottomSheet( + context: context, + showDragHandle: true, + isScrollControlled: true, + useSafeArea: true, + builder: (BuildContext _) { + return BlocProvider( + create: (_) => UnlockSharedAccountBloc( + nextcloudService: context.read(), + sharedAccountRepository: context.read(), + accountId: account.nextcloudAccountId, + homeBloc: context.read(), + ), + child: const UnlockSharedAccount(), + ); + }, + ); + } + @override Widget build(BuildContext context) { useEffect(() { @@ -37,20 +62,31 @@ class OtpAccount extends HookWidget { return null; }, []); + useEffect(() { + // account counter may have changed after sync -> regenerate code + if (account.type.toLowerCase() == "hotp") { + context.read().add(GenerateOtpCode(account: account)); + } + return null; + }); + return BlocBuilder( - builder: (context, state) { + builder: (otpAccountContext, otpAccountState) { return BlocBuilder( - builder: (context, otpManagerState) { + builder: (otpManagerContext, otpManagerState) { return InkWell( onTap: () { - if (state.otpCode == null) { - context + if (account is SharedAccount && !account.unlocked) { + _showUnlockSharedAccountModal(otpManagerContext); + } else if (account.type == "hotp" && account.counter < 0) { + otpManagerContext .read() .add(IncrementCounter(account: account)); } else if (otpManagerState.copyWithTap) { - Clipboard.setData(ClipboardData(text: state.otpCode!)); + Clipboard.setData( + ClipboardData(text: otpAccountState.otpCode!)); showSnackBar( - context: context, + context: otpManagerContext, msg: "${account.type.toUpperCase()} code copied"); } else { _navigationService.navigateTo( @@ -62,7 +98,8 @@ class OtpAccount extends HookWidget { child: Slidable( closeOnScroll: true, endActionPane: ActionPane( - extentRatio: 0.75, + extentRatio: + account is Account || account.unlocked ? 0.75 : 0.5, motion: const ScrollMotion(), children: [ CustomSlidableAction( @@ -76,14 +113,17 @@ class OtpAccount extends HookWidget { arguments: {"account": account}, ), ), - CustomSlidableAction( - label: "QR", - icon: Icons.qr_code, - padding: const EdgeInsets.fromLTRB(0, 10, 7, 10), - backgroundColor: Colors.grey, - border: BorderRadius.circular(10.0), - onPressed: () => showQrCodeModal(context, account), - ), + if (account is Account || + (account is SharedAccount && account.unlocked)) + CustomSlidableAction( + label: "QR", + icon: Icons.qr_code, + padding: const EdgeInsets.fromLTRB(0, 10, 7, 10), + backgroundColor: Colors.grey, + border: BorderRadius.circular(10.0), + onPressed: () => + showQrCodeModal(otpManagerContext, account), + ), CustomSlidableAction( label: "Delete", icon: Icons.delete, @@ -91,11 +131,11 @@ class OtpAccount extends HookWidget { backgroundColor: Colors.red, border: BorderRadius.circular(10.0), onPressed: () => showDeleteModal( - context, + otpManagerContext, account, - () => context + () => otpManagerContext .read() - .add(DeleteAccount(id: account.id))), + .add(DeleteAccount(account: account))), ), ], ), @@ -114,7 +154,7 @@ class OtpAccount extends HookWidget { simpleIcons['default'], ), title: Text( - account.name, + "(${account.position}) ${account.name}", overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 14.5), ), @@ -136,17 +176,27 @@ class OtpAccount extends HookWidget { Padding( padding: const EdgeInsets.only(right: 15.0), child: Text( - state.otpCode ?? "- " * account.digits!, - style: const TextStyle( - fontSize: 28, - color: Colors.blue, + otpAccountState.otpCode ?? + "- " * account.digits!, + style: TextStyle( + fontSize: otpAccountState.otpCode == null + ? 28 + : otpAccountState.otpCode! + .startsWith("C") + ? 14 + : 28, + color: Theme.of(context).primaryColor, ), ), ), - if (account.type == "totp") + if ((account is Account && + account.type == "totp") || + (account is SharedAccount && + account.unlocked && + account.type == "totp")) CustomCircularCountDownTimer( period: account.period!, - callback: () => context + callback: () => otpManagerContext .read() .add(GenerateOtpCode(account: account)), ), @@ -158,7 +208,8 @@ class OtpAccount extends HookWidget { trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - if (account.toUpdate == true || account.isNew) + if (account.toUpdate == true || + (account is Account && account.isNew)) Padding( padding: const EdgeInsets.fromLTRB(0, 0, 8, 0), child: tooltip( @@ -170,12 +221,20 @@ class OtpAccount extends HookWidget { ), ), ), + if (account is SharedAccount && !account.unlocked) + IconButton( + icon: const Icon(Icons.lock_open), + onPressed: () => _showUnlockSharedAccountModal( + otpManagerContext), + ), if (account.type == "hotp") IconButton( icon: const Icon(Icons.refresh), - onPressed: () => context - .read() - .add(IncrementCounter(account: account)), + onPressed: otpAccountState.disableIncrement + ? null + : () => otpManagerContext + .read() + .add(IncrementCounter(account: account)), ), if (otpManagerState.copyWithTap) IconButton( @@ -185,14 +244,14 @@ class OtpAccount extends HookWidget { arguments: account, ), ) - else if (state.otpCode != null) + else if (otpAccountState.otpCode != null) IconButton( icon: const Icon(Icons.copy), onPressed: () { - Clipboard.setData( - ClipboardData(text: state.otpCode!)); + Clipboard.setData(ClipboardData( + text: otpAccountState.otpCode!)); showSnackBar( - context: context, + context: otpManagerContext, msg: "${account.type.toUpperCase()} code copied"); }, diff --git a/lib/screens/home/otp_accounts_list.dart b/lib/screens/home/otp_accounts_list.dart index 5bf5c2d..04f4a97 100644 --- a/lib/screens/home/otp_accounts_list.dart +++ b/lib/screens/home/otp_accounts_list.dart @@ -1,28 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:otp_manager/domain/nextcloud_service.dart'; +import 'package:otp_manager/repository/interface/account_repository.dart'; +import 'package:otp_manager/repository/interface/shared_account_repository.dart'; import '../../bloc/home/home_bloc.dart'; import '../../bloc/home/home_event.dart'; import '../../bloc/home/home_state.dart'; import '../../bloc/otp_account/otp_account_bloc.dart'; -import '../../repository/local_repository.dart'; -import '../../utils/show_snackbar.dart'; -import '../otp_account.dart'; +import 'otp_account.dart'; class OtpAccountsList extends StatelessWidget { const OtpAccountsList({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return BlocConsumer( - listener: (context, state) { - if (state.syncError != "") { - showSnackBar(context: context, msg: state.syncError); - } - if (state.accountDeleted != "") { - showSnackBar(context: context, msg: state.accountDeleted); - } - }, + return BlocBuilder( builder: (context, state) { return ReorderableListView.builder( shrinkWrap: true, @@ -34,11 +27,15 @@ class OtpAccountsList extends StatelessWidget { physics: const AlwaysScrollableScrollPhysics(), itemBuilder: (context, index) { var account = state.accounts[index]; + return BlocProvider( - key: ValueKey(account.id), + key: ValueKey(account.secret), create: (context) => OtpAccountBloc( homeBloc: context.read(), - localRepositoryImpl: context.read(), + accountRepository: context.read(), + nextcloudService: context.read(), + sharedAccountRepository: + context.read(), ), child: OtpAccount( account: account, diff --git a/lib/screens/icon_picker.dart b/lib/screens/icon_picker.dart index 4a96bd4..3b9fb46 100644 --- a/lib/screens/icon_picker.dart +++ b/lib/screens/icon_picker.dart @@ -43,7 +43,8 @@ class IconPicker extends HookWidget { }, ), ), - if (state.searchBarValue == "" && state.iconsBestMatch.isNotEmpty) ...[ + if (state.searchBarValue == "" && + state.iconsBestMatch.isNotEmpty) ...[ const Text( 'Best match based on issuer', style: TextStyle(fontSize: 18), diff --git a/lib/screens/manual.dart b/lib/screens/manual.dart index 59e076f..de6dec3 100644 --- a/lib/screens/manual.dart +++ b/lib/screens/manual.dart @@ -12,7 +12,6 @@ import 'package:otp_manager/utils/simple_icons.dart'; import '../bloc/icon_picker/icon_picker_bloc.dart'; import '../bloc/manual/manual_state.dart'; -import '../repository/local_repository.dart'; import 'icon_picker.dart'; class Manual extends HookWidget { @@ -66,8 +65,6 @@ class Manual extends HookWidget { builder: (BuildContext context) { return BlocProvider( create: (context) => IconPickerBloc( - localRepositoryImpl: context - .read(), issuer: state.issuer, ), child: const IconPicker(), @@ -161,168 +158,173 @@ class Manual extends HookWidget { }, ), ), - Padding( - padding: const EdgeInsets.all(15.0), - child: TextFormField( - initialValue: state.secretKey, - readOnly: state.isEdit, - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: "Secret key", - errorText: state.secretKeyError, - suffixIcon: state.secretKeyError == null - ? const Icon(Icons.vpn_key) - : const Icon(Icons.error, color: Colors.red), + if (!state.isSharedAccount) ...[ + Padding( + padding: const EdgeInsets.all(15.0), + child: TextFormField( + initialValue: state.secretKey, + readOnly: state.isEdit, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: "Secret key", + errorText: state.secretKeyError, + suffixIcon: state.secretKeyError == null + ? const Icon(Icons.vpn_key) + : const Icon(Icons.error, color: Colors.red), + ), + onChanged: (value) { + context + .read() + .add(SecretKeyChanged(secretKey: value)); + }, + onTap: () { + if (state.isEdit) { + Clipboard.setData( + ClipboardData(text: state.secretKey)); + showSnackBar( + context: context, msg: "Secrey key copied"); + } + }, ), - onChanged: (value) { - context - .read() - .add(SecretKeyChanged(secretKey: value)); - }, - onTap: () { - if (state.isEdit) { - Clipboard.setData(ClipboardData(text: state.secretKey)); - showSnackBar( - context: context, msg: "Secrey key copied"); - } - }, ), - ), - Row( - children: [ - Expanded( - flex: 80, - child: Padding( - padding: const EdgeInsets.all(15.0), - child: DropdownButtonFormField2( - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: "Type of code", - ), - dropdownDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - ), - value: state.codeTypeValue, - items: const [ - DropdownMenuItem( - value: "totp", - child: Text("Based on time (TOTP)"), + Row( + children: [ + Expanded( + flex: 80, + child: Padding( + padding: const EdgeInsets.all(15.0), + child: DropdownButtonFormField2( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: "Type of code", ), - DropdownMenuItem( - value: "hotp", - child: Text("Based on counter (HOTP)"), + dropdownDecoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), ), - ], - onChanged: (String? value) { - value == "hotp" - ? animationController.forward() - : animationController.reverse(); - context.read().add( - CodeTypeValueChanged(codeTypeValue: value!)); - }, + value: state.codeTypeValue, + items: const [ + DropdownMenuItem( + value: "totp", + child: Text("Based on time (TOTP)"), + ), + DropdownMenuItem( + value: "hotp", + child: Text("Based on counter (HOTP)"), + ), + ], + onChanged: (String? value) { + value == "hotp" + ? animationController.forward() + : animationController.reverse(); + context.read().add( + CodeTypeValueChanged(codeTypeValue: value!)); + }, + ), ), ), - ), - if (animation != 0) + if (animation != 0) + Expanded( + flex: animation, + child: Padding( + padding: + const EdgeInsets.fromLTRB(0, 15.0, 15.0, 15.0), + child: DropdownButtonFormField2( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: "Interval", + ), + dropdownDecoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + ), + value: state.intervalValue, + items: const [ + DropdownMenuItem(value: 30, child: Text("30s")), + DropdownMenuItem(value: 45, child: Text("45s")), + DropdownMenuItem(value: 60, child: Text("60s")), + ], + onChanged: (int? value) { + if (value == null) return; + context.read().add( + IntervalValueChanged(intervalValue: value)); + }, + ), + ), + ), + ], + ), + Row( + children: [ Expanded( - flex: animation, + flex: 5, child: Padding( padding: - const EdgeInsets.fromLTRB(0, 15.0, 15.0, 15.0), + const EdgeInsets.fromLTRB(15.0, 15.0, 0, 15.0), child: DropdownButtonFormField2( decoration: const InputDecoration( border: OutlineInputBorder(), - labelText: "Interval", + labelText: "Algorithm", ), dropdownDecoration: BoxDecoration( borderRadius: BorderRadius.circular(4.0), ), - value: state.intervalValue, + value: state.algorithmValue, items: const [ - DropdownMenuItem(value: 30, child: Text("30s")), - DropdownMenuItem(value: 45, child: Text("45s")), - DropdownMenuItem(value: 60, child: Text("60s")), + DropdownMenuItem( + value: "SHA1", + child: Text("SHA1"), + ), + DropdownMenuItem( + value: "SHA256", + child: Text("SHA256"), + ), + DropdownMenuItem( + value: "SHA512", + child: Text("SHA512"), + ), ], - onChanged: (int? value) { - if (value == null) return; + onChanged: (String? value) { context.read().add( - IntervalValueChanged(intervalValue: value)); + AlgorithmValueChanged( + algorithmValue: value!)); }, ), ), ), - ], - ), - Row( - children: [ - Expanded( - flex: 5, - child: Padding( - padding: const EdgeInsets.fromLTRB(15.0, 15.0, 0, 15.0), - child: DropdownButtonFormField2( - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: "Algorithm", - ), - dropdownDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - ), - value: state.algorithmValue, - items: const [ - DropdownMenuItem( - value: "SHA1", - child: Text("SHA1"), - ), - DropdownMenuItem( - value: "SHA256", - child: Text("SHA256"), + Expanded( + flex: 5, + child: Padding( + padding: const EdgeInsets.all(15.0), + child: DropdownButtonFormField2( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: "Digits", ), - DropdownMenuItem( - value: "SHA512", - child: Text("SHA512"), + dropdownDecoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), ), - ], - onChanged: (String? value) { - context.read().add( - AlgorithmValueChanged(algorithmValue: value!)); - }, - ), - ), - ), - Expanded( - flex: 5, - child: Padding( - padding: const EdgeInsets.all(15.0), - child: DropdownButtonFormField2( - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: "Digits", - ), - dropdownDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), + value: state.digitsValue, + items: const [ + DropdownMenuItem( + value: 4, + child: Text("4"), + ), + DropdownMenuItem( + value: 6, + child: Text("6"), + ), + ], + onChanged: (int? value) { + if (value == null) return; + context + .read() + .add(DigitsValueChanged(digitsValue: value)); + }, ), - value: state.digitsValue, - items: const [ - DropdownMenuItem( - value: 4, - child: Text("4"), - ), - DropdownMenuItem( - value: 6, - child: Text("6"), - ), - ], - onChanged: (int? value) { - if (value == null) return; - context - .read() - .add(DigitsValueChanged(digitsValue: value)); - }, ), ), - ), - ], - ), + ], + ), + ] ], ), ); diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 770d7a9..655c20a 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -42,7 +42,10 @@ class Settings extends HookWidget { title: const Text("Bug Report"), content: RichText( text: TextSpan( - style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.normal), + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.normal), text: "If you have found a bug and want to report " "it to the developer, contact him via email on ", children: [ diff --git a/lib/screens/unlock_shared_account.dart b/lib/screens/unlock_shared_account.dart new file mode 100644 index 0000000..3119948 --- /dev/null +++ b/lib/screens/unlock_shared_account.dart @@ -0,0 +1,74 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:otp_manager/bloc/unlock_shared_account/unlock_shared_account_bloc.dart'; +import 'package:otp_manager/bloc/unlock_shared_account/unlock_shared_account_state.dart'; + +import '../bloc/unlock_shared_account/unlock_shared_account_event.dart'; +import '../utils/auth_input.dart'; +import '../utils/show_snackbar.dart'; + +class UnlockSharedAccount extends HookWidget { + const UnlockSharedAccount({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final enabled = useState(true); + + return BlocConsumer( + listener: (context, state) { + if (state.attempts == 0) { + showSnackBar( + context: context, + msg: "Too many attempts. Wait 5 seconds to try again.", + ); + enabled.value = false; + + Timer(const Duration(seconds: 5), () { + enabled.value = true; + }); + } + + if (state.errorMsg != "") { + showSnackBar(context: context, msg: state.errorMsg); + } + }, builder: (context, state) { + return Stack( + alignment: Alignment.center, + children: [ + const Padding( + padding: EdgeInsets.fromLTRB(0, 0, 0, 250), + child: Icon( + Icons.lock_open, + size: 100, + color: Colors.blue, + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(10, 20, 10, 0), + child: AuthInput( + label: "Shared Password", + helper: + "Insert the password that was used to share this account", + onChanged: (value) => context + .read() + .add(PasswordChanged(password: value)), + onSubmit: () => context + .read() + .add(PasswordSubmit()), + enabled: enabled.value, + errorMsg: state.errorMsg, + ), + ), + ], + ), + ], + ); + }); + } +} diff --git a/lib/screens/web_viewer.dart b/lib/screens/web_viewer.dart index f5d7b94..b861c40 100644 --- a/lib/screens/web_viewer.dart +++ b/lib/screens/web_viewer.dart @@ -63,4 +63,4 @@ class WebViewer extends HookWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/utils/algorithms.dart b/lib/utils/algorithms.dart new file mode 100644 index 0000000..1a458db --- /dev/null +++ b/lib/utils/algorithms.dart @@ -0,0 +1,2 @@ + +enum Algorithms { sha1, sha256, sha512 } \ No newline at end of file diff --git a/lib/utils/auth_input.dart b/lib/utils/auth_input.dart index e9ea9e9..fbf768e 100644 --- a/lib/utils/auth_input.dart +++ b/lib/utils/auth_input.dart @@ -12,12 +12,17 @@ class AuthInput extends HookWidget { required this.onSubmit, required this.enabled, required this.errorMsg, + required this.label, + this.helper, + }) : super(key: key); final Function(String) onChanged; final Function() onSubmit; final bool enabled; final String errorMsg; + final String label; + final String? helper; @override Widget build(BuildContext context) { @@ -27,7 +32,7 @@ class AuthInput extends HookWidget { void toggleObscured() { obscured.value = !obscured.value; if (passwordFocusNode.hasPrimaryFocus) { - return; // If focus is on text field, dont unfocus + return; // If focus is on text field, don't unfocus } passwordFocusNode.canRequestFocus = false; // Prevents focus if tap on eye } @@ -42,7 +47,8 @@ class AuthInput extends HookWidget { focusNode: passwordFocusNode, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.never, - labelText: "Password", + labelText: label, + helperText: helper, filled: true, fillColor: state.darkTheme ? Colors.grey.shade800 : null, isDense: true, diff --git a/lib/utils/delete_modal.dart b/lib/utils/delete_modal.dart index 988c744..c3bb93c 100644 --- a/lib/utils/delete_modal.dart +++ b/lib/utils/delete_modal.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; -import '../models/account.dart'; - void showDeleteModal( BuildContext context, - Account account, + dynamic account, Function onPressed, ) => showDialog( diff --git a/lib/utils/encryption.dart b/lib/utils/encryption.dart index 9bfa28b..ecd2a4e 100644 --- a/lib/utils/encryption.dart +++ b/lib/utils/encryption.dart @@ -1,7 +1,24 @@ import 'package:encrypt/encrypt.dart'; +import 'package:otp_manager/repository/interface/user_repository.dart'; class Encryption { - static String decrypt(String dataBase64, String keyBase16, String ivBase16) { + final UserRepository userRepository; + + Encryption({required this.userRepository}); + + String? decrypt({ + required String dataBase64, + String? keyBase16, + String? ivBase16, + }) { + if (keyBase16 == null || ivBase16 == null) { + final user = userRepository.get(); + if (user == null || user.password == null || user.iv == null) return null; + + keyBase16 = user.password!; + ivBase16 = user.iv!; + } + final encrypter = _getEncrypter(keyBase16); return encrypter.decrypt( @@ -10,13 +27,25 @@ class Encryption { ); } - static String encrypt(String data, String keyBase16, String ivBase16) { + String? encrypt({ + required String data, + String? keyBase16, + String? ivBase16, + }) { + if (keyBase16 == null || ivBase16 == null) { + final user = userRepository.get(); + if (user == null || user.password == null || user.iv == null) return null; + + keyBase16 = user.password!; + ivBase16 = user.iv!; + } + final encrypter = _getEncrypter(keyBase16); return encrypter.encrypt(data, iv: IV.fromBase16(ivBase16)).base64; } - static Encrypter _getEncrypter(String keyBase16) { + Encrypter _getEncrypter(String keyBase16) { return Encrypter(AES(Key.fromBase16(keyBase16), mode: AESMode.cbc)); } } diff --git a/lib/utils/icon_picker_helper.dart b/lib/utils/icon_picker_helper.dart new file mode 100644 index 0000000..24c08dc --- /dev/null +++ b/lib/utils/icon_picker_helper.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:otp_manager/utils/simple_icons.dart'; + +class IconPickerHelper { + static String findFirst(String toFind) { + toFind = toFind.replaceAll(" ", "").toLowerCase(); + + return simpleIcons.keys + .firstWhere((v) => v.contains(toFind), orElse: () => "default"); + } + + static Map findBestMatch(String toFind) { + toFind = toFind.replaceAll(" ", "").toLowerCase(); + + Map iconsBestMatch = {}; + + simpleIcons.forEach((key, value) { + if (iconsBestMatch.length != 3 && key.contains(toFind)) { + iconsBestMatch[key] = value; + } + }); + + return iconsBestMatch; + } +} diff --git a/lib/utils/nextcloud_ocs_api.dart b/lib/utils/nextcloud_ocs_api.dart new file mode 100644 index 0000000..100b5c3 --- /dev/null +++ b/lib/utils/nextcloud_ocs_api.dart @@ -0,0 +1,18 @@ +/// This file contains all the OCS APIs that the app uses to interface with the Nextcloud extension. + +class SharedAccountAPI { + static const unlock = "share/unlock"; + static const updateCounter = "share/update-counter"; +} + +class AccountAPI { + static const updateCounter = "accounts/update-counter"; +} + +class SyncAPI { + static const sync = "accounts/sync"; +} + +class PasswordAPI { + static const check = "password/check"; +} diff --git a/lib/utils/qr_code_modal.dart b/lib/utils/qr_code_modal.dart index 7137350..0d6acba 100644 --- a/lib/utils/qr_code_modal.dart +++ b/lib/utils/qr_code_modal.dart @@ -1,11 +1,9 @@ import 'package:flutter/material.dart'; import 'package:pretty_qr_code/pretty_qr_code.dart'; -import '../models/account.dart'; - void showQrCodeModal( BuildContext context, - Account account, + dynamic account, ) => showDialog( context: context, diff --git a/lib/utils/uri_decoder.dart b/lib/utils/uri_decoder.dart index 0d24744..a4f8c97 100644 --- a/lib/utils/uri_decoder.dart +++ b/lib/utils/uri_decoder.dart @@ -4,11 +4,10 @@ import 'dart:typed_data'; import 'package:base32/base32.dart'; import 'package:diacritic/diacritic.dart'; import 'package:otp/otp.dart'; +import 'package:otp_manager/utils/algorithms.dart'; import '../../generated_protoc/google_auth.pb.dart'; -import '../main.dart'; import '../models/account.dart'; -import '../object_box/objectbox.g.dart'; class UriDecoder { Map _getNameAndIssuer(Uri queryUriParams) { @@ -36,12 +35,12 @@ class UriDecoder { } static int getAlgorithmFromString(String algorithm) { - int algo = AlgorithmTypes.sha1.index; + int algo = Algorithms.sha1.index; if (algorithm.contains("SHA256")) { - algo = AlgorithmTypes.sha256.index; + algo = Algorithms.sha256.index; } else if (algorithm.contains("SHA512")) { - algo = AlgorithmTypes.sha512.index; + algo = Algorithms.sha512.index; } return algo; @@ -65,28 +64,15 @@ class UriDecoder { List accounts = []; - int? lastPosition = - (objectBox.store.box().query(Account_.deleted.equals(false)) - ..order(Account_.position, flags: Order.descending)) - .build() - .findFirst() - ?.position; - int position; - - if (lastPosition != null) { - position = lastPosition + 1; - } else { - position = 0; - } - payload.otpParameters.asMap().forEach((index, params) { var tmp = params.toProto3Json() as Map; tmp["name"] = Uri.decodeFull(removeDiacritics(tmp["name"].toString())); + String secret = base32 + .encode(Uint8List.fromList(payload.otpParameters[index].secret)) + .toUpperCase(); var newAccount = Account( - secret: base32 - .encode(Uint8List.fromList(payload.otpParameters[index].secret)) - .toUpperCase(), + secret: secret, name: tmp["name"].contains(':') ? tmp["name"].split(':')[1] : tmp["name"], issuer: Uri.decodeFull(removeDiacritics(tmp["issuer"] ?? "")), @@ -94,11 +80,9 @@ class UriDecoder { digits: 6, type: tmp["type"], period: 30, - position: position, ); accounts.add(newAccount); - position++; }); return accounts; @@ -114,19 +98,6 @@ class UriDecoder { } else { var tmp = uriDecoded.queryParameters; var nameAndIssuer = _getNameAndIssuer(uriDecoded); - int? lastPosition = - (objectBox.store.box().query(Account_.deleted.equals(false)) - ..order(Account_.position, flags: Order.descending)) - .build() - .findFirst() - ?.position; - int position; - - if (lastPosition != null) { - position = lastPosition + 1; - } else { - position = 0; - } var newAccount = Account( secret: tmp["secret"].toString().toUpperCase(), @@ -136,7 +107,6 @@ class UriDecoder { digits: int.tryParse(tmp["digits"].toString()), type: uriDecoded.host, period: int.tryParse(tmp["period"].toString()), - position: position, ); accounts.add(newAccount); diff --git a/pubspec.yaml b/pubspec.yaml index d1cfa82..df92d47 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.4.5+21 +version: 1.5.0+22 environment: sdk: ">=2.16.2 <3.0.0" @@ -42,14 +42,7 @@ dependencies: flutter_speed_dial: ^6.0.0 # MIT objectbox: ^1.5.0 # Apache-2.0 (Trademark use) objectbox_flutter_libs: any # Apache-2.0 (Trademark use) - - # nextcloud: ^3.0.2 # BSD-3 - nextcloud: - git: - url: https://github.com/provokateurin/nextcloud-neon - path: packages/nextcloud - ref: 4e4d6d7 - + nextcloud: ^5.0.2 webview_flutter: ^4.0.6 # BSD-3-Clause dropdown_button2: 1.6.2 # MIT mobile_scanner: ^2.0.0 # BSD-3-Clause @@ -77,6 +70,7 @@ dependencies: arrow_path: ^3.1.0 pretty_qr_code: ^3.1.0 diacritic: ^0.1.5 + intl: ^0.19.0 dev_dependencies: flutter_test: