diff --git a/lib/mains/main_netknights.dart b/lib/mains/main_netknights.dart index c6d7c69ac..95b60495b 100644 --- a/lib/mains/main_netknights.dart +++ b/lib/mains/main_netknights.dart @@ -35,6 +35,7 @@ import '../utils/logger.dart'; import '../utils/riverpod/riverpod_providers/generated_providers/settings_notifier.dart'; import '../utils/riverpod/riverpod_providers/generated_providers/app_constraints_notifier.dart'; import '../views/add_token_manually_view/add_token_manually_view.dart'; +import '../views/container_view/container_view.dart'; import '../views/feedback_view/feedback_view.dart'; import '../views/import_tokens_view/import_tokens_view.dart'; import '../views/license_view/license_view.dart'; @@ -117,6 +118,7 @@ class PrivacyIDEAAuthenticator extends ConsumerWidget { SettingsView.routeName: (context) => const SettingsView(), SplashScreen.routeName: (context) => SplashScreen(customization: _customization, appConstraints: constraints), QRScannerView.routeName: (context) => const QRScannerView(), + ContainerView.routeName: (context) => const ContainerView(), }, ); }); diff --git a/lib/model/tokens/container_credentials.dart b/lib/model/tokens/container_credentials.dart index 66e6c692d..aecef6ead 100644 --- a/lib/model/tokens/container_credentials.dart +++ b/lib/model/tokens/container_credentials.dart @@ -41,7 +41,7 @@ part 'container_credentials.g.dart'; // &hash_algorithm=SHA256 // &passphrase=Enter%20your%20passphrase -@freezed +@Freezed(toStringOverride: false) class ContainerCredential with _$ContainerCredential { static const eccUtils = EccUtils(); const ContainerCredential._(); @@ -139,12 +139,19 @@ class ContainerCredential with _$ContainerCredential { factory ContainerCredential.fromJson(Map json) => _$ContainerCredentialFromJson(json); - // // Sign with private client key - // String signMessage(String message) { - // final signer = Signer(hashAlgorithm.name); - // signer.init(true, PrivateKey(ecPrivateClientKey!.d, ecPrivateClientKey!.parameters)); - // return signer.signMessage(message); - // }; + @override + String toString() => 'ContainerCredential(' + 'issuer: $issuer, ' + 'nonce: $nonce, ' + 'timestamp: $timestamp, ' + 'finalizationUrl: $finalizationUrl, ' + 'serial: $serial, ' + 'ecKeyAlgorithm: $ecKeyAlgorithm, ' + 'hashAlgorithm: $hashAlgorithm, ' + 'finalizationState: $finalizationState, ' + 'passphrase: $passphrase, ' + 'publicServerKey: $publicServerKey, ' + 'publicClientKey: $publicClientKey)'; } enum ContainerFinalizationState { @@ -299,10 +306,10 @@ extension EcKeyAlgorithmX on EcKeyAlgorithm { EcKeyAlgorithm.secp521r1 => 'secp521r1', }; } +//be99ff65b1c38ae8a7d6caf8799a0cce3749fe0e|2024-08-27 14:30:58.371312Z|http://192.168.0.230:5000/container/register/finalize|SMPH0000D49C +//be99ff65b1c38ae8a7d6caf8799a0cce3749fe0e|2024-08-27T14:30:58.371312+00:00|http://192.168.0.230:5000/container/register/finalize|SMPH0000D49C class EccUtils { - final String algorithmName = 'EC'; - const EccUtils(); String serializeECPublicKey(ECPublicKey publicKey) => CryptoUtils.encodeEcPublicKeyToPem(publicKey); @@ -311,7 +318,7 @@ class EccUtils { ECPrivateKey deserializeECPrivateKey(String ecPrivateKey) => CryptoUtils.ecPrivateKeyFromPem(ecPrivateKey); String trySignWithPrivateKey(ECPrivateKey privateKey, String message) { - final ecSignature = CryptoUtils.ecSign(privateKey, Uint8List.fromList(message.codeUnits)); + final ecSignature = CryptoUtils.ecSign(privateKey, Uint8List.fromList(message.codeUnits), algorithmName: 'SHA-256/ECDSA'); String signatureBase64 = CryptoUtils.ecSignatureToBase64(ecSignature); return signatureBase64; } diff --git a/lib/model/tokens/container_credentials.freezed.dart b/lib/model/tokens/container_credentials.freezed.dart index 7cb4fe051..467b42dc9 100644 --- a/lib/model/tokens/container_credentials.freezed.dart +++ b/lib/model/tokens/container_credentials.freezed.dart @@ -430,11 +430,6 @@ class _$ContainerCredentialUnfinalizedImpl @JsonKey(name: 'runtimeType') final String $type; - @override - String toString() { - return 'ContainerCredential.unfinalized(issuer: $issuer, nonce: $nonce, timestamp: $timestamp, finalizationUrl: $finalizationUrl, serial: $serial, ecKeyAlgorithm: $ecKeyAlgorithm, hashAlgorithm: $hashAlgorithm, finalizationState: $finalizationState, passphrase: $passphrase, publicServerKey: $publicServerKey, publicClientKey: $publicClientKey, privateClientKey: $privateClientKey)'; - } - @override bool operator ==(Object other) { return identical(this, other) || @@ -885,11 +880,6 @@ class _$ContainerCredentialFinalizedImpl extends ContainerCredentialFinalized { @JsonKey(name: 'runtimeType') final String $type; - @override - String toString() { - return 'ContainerCredential.finalized(issuer: $issuer, nonce: $nonce, timestamp: $timestamp, finalizationUrl: $finalizationUrl, serial: $serial, ecKeyAlgorithm: $ecKeyAlgorithm, hashAlgorithm: $hashAlgorithm, finalizationState: $finalizationState, passphrase: $passphrase, publicServerKey: $publicServerKey, publicClientKey: $publicClientKey, privateClientKey: $privateClientKey)'; - } - @override bool operator ==(Object other) { return identical(this, other) || diff --git a/lib/repo/secure_token_repository.dart b/lib/repo/secure_token_repository.dart index 1ff0aae19..8f8be505e 100644 --- a/lib/repo/secure_token_repository.dart +++ b/lib/repo/secure_token_repository.dart @@ -135,7 +135,7 @@ class SecureTokenRepository implements TokenRepository { } } if (failedTokens.isNotEmpty) { - Logger.warning( + Logger.error( 'Could not save all tokens (${tokens.length - failedTokens.length}/${tokens.length}) to secure storage', name: 'secure_token_repository.dart#saveOrReplaceTokens', stackTrace: StackTrace.current, @@ -150,9 +150,9 @@ class SecureTokenRepository implements TokenRepository { Future saveOrReplaceToken(Token token) => _protect(() => _saveOrReplaceToken(token)); Future _saveOrReplaceToken(Token token) async { try { - await _storage.write(key: _TOKEN_PREFIX + token.id, value: jsonEncode(token)); - } catch (e, s) { - Logger.error('Could not save token to secure storage', name: 'secure_token_repository.dart#saveOrReplaceToken', error: e, stackTrace: s); + await _storage.write(key: _TOKEN_PREFIX + token.id, value: jsonEncode(token.toJson())); + } catch (e) { + Logger.warning('Could not save token to secure storage', error: e, name: 'secure_token_repository.dart#saveOrReplaceToken', verbose: true); return false; } return true; diff --git a/lib/utils/privacyidea_io_client.dart b/lib/utils/privacyidea_io_client.dart index 6a64699b2..24f3bb374 100644 --- a/lib/utils/privacyidea_io_client.dart +++ b/lib/utils/privacyidea_io_client.dart @@ -19,6 +19,7 @@ */ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; @@ -116,16 +117,16 @@ class PrivacyideaIOClient { } on HandshakeException catch (e, _) { Logger.warning('Handshake failed. sslVerify: $sslVerify', name: 'utils.dart#doPost'); showMessage(message: 'Handshake failed, please check the server certificate and try again.'); - response = Response('${e.runtimeType}', 525); + response = Response('Handshake failed', 525); } on TimeoutException catch (e, _) { Logger.info('Post request timed out', name: 'utils.dart#doPost'); - response = Response('${e.runtimeType}', 408); + response = Response('Request timed out', 408); } on SocketException catch (e, _) { Logger.info('Post request failed', name: 'utils.dart#doPost'); - response = Response('${e.runtimeType}', 404); + response = Response('Failed to send request', 404); } catch (e, _) { Logger.warning('Something unexpected happened', name: 'utils.dart#doPost'); - response = Response('${e.runtimeType}', 404); + response = Response('Failed to send request', 404); } if (response.statusCode != 200) { @@ -196,3 +197,21 @@ class PrivacyideaIOClient { return response; } } + +extension PiServerMessage on Response { + String? get piServerMessage { + try { + return jsonDecode(body)['result']['error']['message'].toString(); + } catch (e, _) { + return null; + } + } + + String? get piStatusCode { + try { + return jsonDecode(body)['result']['error']['code'].toString(); + } catch (e, _) { + return null; + } + } +} diff --git a/lib/utils/push_provider.dart b/lib/utils/push_provider.dart index b9c53467e..6942fd15d 100644 --- a/lib/utils/push_provider.dart +++ b/lib/utils/push_provider.dart @@ -353,7 +353,7 @@ class PushProvider { response = instance != null ? await instance!._ioClient.doGet(url: token.url!, parameters: parameters, sslVerify: token.sslVerify) : await const PrivacyideaIOClient().doGet(url: token.url!, parameters: parameters, sslVerify: token.sslVerify); - } catch (e) { + } catch (_) { if (isManually) { globalRef?.read(statusMessageProvider.notifier).state = ( AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorWhenPullingChallenges(token.serial), diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/credential_notifier.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/credential_notifier.dart index 5461f8ecb..b1a33f5db 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/credential_notifier.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/credential_notifier.dart @@ -17,6 +17,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import 'dart:async'; import 'dart:convert'; import 'package:basic_utils/basic_utils.dart'; @@ -42,6 +43,7 @@ part 'credential_notifier.g.dart'; final containerCredentialsProvider = containerCredentialsNotifierProviderOf( repo: SecureContainerCredentialsRepository(), ioClient: const PrivacyideaIOClient(), + eccUtils: const EccUtils(), ); @Riverpod(keepAlive: true) @@ -52,9 +54,10 @@ class ContainerCredentialsNotifier extends _$ContainerCredentialsNotifier with R ContainerCredentialsNotifier({ ContainerCredentialsRepository? repoOverride, PrivacyideaIOClient? ioClientOverride, - NotifierProviderRef? refOverride, + EccUtils? eccUtilsOverride, }) : _repoOverride = repoOverride, - _ioClientOverride = ioClientOverride; + _ioClientOverride = ioClientOverride, + _eccUtilsOverride = eccUtilsOverride; @override ContainerCredentialsRepository get repo => _repo; @@ -66,15 +69,21 @@ class ContainerCredentialsNotifier extends _$ContainerCredentialsNotifier with R late PrivacyideaIOClient _ioClient; final PrivacyideaIOClient? _ioClientOverride; + @override + EccUtils get eccUtils => _eccUtils; + late EccUtils _eccUtils; + final EccUtils? _eccUtilsOverride; + @override Future build({ required ContainerCredentialsRepository repo, required PrivacyideaIOClient ioClient, + required EccUtils eccUtils, }) async { await _stateMutex.acquire(); _repo = _repoOverride ?? repo; _ioClient = _ioClientOverride ?? ioClient; - // _ref = _refOverride ?? ref; + _eccUtils = _eccUtilsOverride ?? eccUtils; Logger.warning('Building credentialsProvider', name: 'CredentialsNotifier'); final initState = await _repo.loadCredentialsState(); for (var credential in initState.credentials.whereType()) { @@ -84,6 +93,63 @@ class ContainerCredentialsNotifier extends _$ContainerCredentialsNotifier with R return initState; } +////////////////////////////////////////////////////////////////// +////////////////////////// REPO METHODS ////////////////////////// +////////////////////////////////////////////////////////////////// + + Future _saveCredentialToRepo(ContainerCredential credential) async { + return await _repoMutex.protect(() async => await _repo.saveCredential(credential)); + } + + Future _saveCredentialsStateToRepo(CredentialsState credentialsState) async { + return await _repoMutex.protect(() async => await _repo.saveCredentialsState(credentialsState)); + } + + Future _deleteCredentialFromRepo(ContainerCredential credential) async { + return await _repoMutex.protect(() async => await _repo.deleteCredential(credential.serial)); + } + + Future _deleteCredentialsStateToRepo() async { + return await _repoMutex.protect(() async => await _repo.deleteAllCredentials()); + } + +/*////////////////////////////////////////////////////////////////// +////////////////////////// PUBLIC METHODS ////////////////////////// +///////////////////////////////////////////////////////////////// */ + +// ADD CREDENTIALS + + Future addCredential(ContainerCredential credential) async { + await _stateMutex.acquire(); + final newState = await _saveCredentialToRepo(credential); + await update((_) => newState); + _stateMutex.release(); + return newState; + } + + Future addCredentials(List credentials) async { + await _stateMutex.acquire(); + final newCredentials = credentials.toList(); + final oldCredentials = (await future).credentials; + final combinedCredentials = []; + for (var oldCredential in oldCredentials) { + final newCredential = newCredentials.firstWhereOrNull((newCredential) => newCredential.serial == oldCredential.serial); + if (newCredential == null) { + combinedCredentials.add(oldCredential); + } else { + combinedCredentials.add(newCredential); + newCredentials.remove(newCredential); + } + } + combinedCredentials.addAll(newCredentials); + final newState = await _saveCredentialsStateToRepo(CredentialsState(credentials: combinedCredentials)); + await update((_) => newState); + _stateMutex.release(); + return newState; + } + + // UPDATE CREDENTIALS + @override Future update( FutureOr Function(CredentialsState state) cb, { @@ -104,20 +170,22 @@ class ContainerCredentialsNotifier extends _$ContainerCredentialsNotifier with R } final updated = updater(currentCredential); final newState = await _saveCredentialToRepo(updated); - state = AsyncValue.data(newState); + await update((_) => newState); _stateMutex.release(); return updated; } - Future addCredential(ContainerCredential credential) async { + // DELETE CREDENTIALS + + Future deleteCredential(ContainerCredential credential) async { await _stateMutex.acquire(); - final newState = await _saveCredentialToRepo(credential); - state = AsyncValue.data(newState); + final newState = await _deleteCredentialFromRepo(credential); + await update((_) => newState); _stateMutex.release(); return newState; } - Future addCredentials(List credentials) async { + Future deleteCredentials(List credentials) async { await _stateMutex.acquire(); final newCredentials = credentials.toList(); final oldCredentials = (await future).credentials; @@ -127,24 +195,16 @@ class ContainerCredentialsNotifier extends _$ContainerCredentialsNotifier with R if (newCredential == null) { combinedCredentials.add(oldCredential); } else { - combinedCredentials.add(newCredential); newCredentials.remove(newCredential); } } - combinedCredentials.addAll(newCredentials); final newState = await _saveCredentialsStateToRepo(CredentialsState(credentials: combinedCredentials)); - state = AsyncValue.data(newState); + await update((_) => newState); _stateMutex.release(); return newState; } - Future _saveCredentialToRepo(ContainerCredential credential) async { - return await _repoMutex.protect(() async => await _repo.saveCredential(credential)); - } - - Future _saveCredentialsStateToRepo(CredentialsState credentialsState) async { - return await _repoMutex.protect(() async => await _repo.saveCredentialsState(credentialsState)); - } + // HANDLE PROCESSOR RESULTS @override Future handleProcessorResult(ProcessorResult result, Map args) { @@ -163,32 +223,59 @@ class ContainerCredentialsNotifier extends _$ContainerCredentialsNotifier with R final stateCredentials = currentState.credentials; final stateCredentialsSerials = stateCredentials.map((e) => e.serial); final newCredentials = containerCredentials.where((element) => !stateCredentialsSerials.contains(element.serial)).toList(); + Logger.info('Handling processor results: adding Credential', name: 'CredentialsNotifier#handleProcessorResults'); await addCredentials(newCredentials); - for (var credential in containerCredentials) { - if (stateCredentialsSerials is! ContainerCredentialUnfinalized) continue; + Logger.info('Handling processor results: adding done (${newCredentials.length})', name: 'CredentialsNotifier#handleProcessorResults'); + for (var credential in newCredentials) { + Logger.info('Handling processor results: finalize check ()', name: 'CredentialsNotifier#handleProcessorResults'); + if (credential is! ContainerCredentialUnfinalized) continue; + Logger.info('Handling processor results: finalize', name: 'CredentialsNotifier#handleProcessorResults'); await finalize(credential); } return null; } final Mutex _finalizationMutex = Mutex(); - Future finalize(ContainerCredential containerCredential) async { - ContainerCredential? credential = containerCredential; + Future finalize(ContainerCredential credential) async { await _finalizationMutex.acquire(); if (credential is! ContainerCredentialUnfinalized) { _finalizationMutex.release(); - throw ArgumentError('Credential must not be finalized'); + throw ArgumentError('Container must not be finalized'); + } + Logger.info('Finalizing container ${credential.serial}', name: 'CredentialsNotifier#finalize'); + try { + credential = await _generateKeyPair(credential); + final Response response; + (credential, response) = await _sendPublicKey(credential); + if (response.statusCode != 200) { + if (response.piServerMessage != null) { + ref.read(statusMessageProvider.notifier).state = ( + 'Failed to finalize container: ${response.piServerMessage}', + response.piStatusCode != null ? 'PI Server code ${response.piStatusCode}' : 'Status code ${response.body}' + ); + } else { + ref.read(statusMessageProvider.notifier).state = ('Failed to finalize container: ${response.body}', 'Status code ${response.statusCode}'); + } + _finalizationMutex.release(); + return; + } + final ECPublicKey publicServerKey; + (credential, publicServerKey) = await _parseResponse(credential, response); + await updateCredential(credential, (c) => c.finalize(publicServerKey: publicServerKey)!); + } on StateError { + Logger.info('Container was removed while finalizing', name: 'CredentialsNotifier#finalize'); + } catch (e) { + Logger.error('Failed to finalize container ${credential.serial}', name: 'CredentialsNotifier#finalize', error: e); + _finalizationMutex.release(); + return; } - Logger.info('Finalizing container credential ${credential.serial}', name: 'CredentialsNotifier#finalize'); - credential = await _generateKeyPair(credential); - final Response response; - (credential, response) = await _sendPublicKey(credential); - final ECPublicKey publicServerKey; - (credential, publicServerKey) = await _parseResponse(credential, response); - await updateCredential(credential, (c) => c.finalize(publicServerKey: publicServerKey)!); _finalizationMutex.release(); } +//////////////////////////////////////////////////////////////////////////// +////////////////////////// PRIVATE HELPER METHODS ////////////////////////// +//////////////////////////////////////////////////////////////////////////// + /// Finalization substep 1: Generate key pair Future _generateKeyPair(ContainerCredential containerCredential) async { // generatingKeyPair, @@ -208,8 +295,8 @@ class ContainerCredentialsNotifier extends _$ContainerCredentialsNotifier with R // sendingPublicKey, // sendingPublicKeyFailed, // sendingPublicKeyCompleted, - ContainerCredential? credential = containerCredential; - final ecPrivateClientKey = credential.ecPrivateClientKey!; + ContainerCredential? container = containerCredential; + final ecPrivateClientKey = container.ecPrivateClientKey!; //POST /container/register/finalize // Request: { // 'container_serial': , @@ -217,32 +304,39 @@ class ContainerCredentialsNotifier extends _$ContainerCredentialsNotifier with R // 'signature': )>, // } - final passphrase = credential.passphrase != null ? EnterPassphraseDialog.show(await globalContext) : null; - final message = '${credential.nonce}' - '|${credential.timestamp}' - '|${credential.finalizationUrl}' - '|${credential.serial}' + final passphrase = container.passphrase != null ? EnterPassphraseDialog.show(await globalContext) : null; + final message = '${container.nonce}' + '|${container.timestamp.toIso8601String().replaceFirst('Z', '+00:00')}' + '|${container.finalizationUrl}' + '|${container.serial}' '${passphrase != null ? '|$passphrase' : ''}'; + print(message); + final signature = eccUtils.trySignWithPrivateKey(ecPrivateClientKey, message); - final signature = const EccUtils().trySignWithPrivateKey(ecPrivateClientKey, message); final body = { - 'container_serial': credential.serial, - 'public_client_key': credential.publicClientKey, + 'container_serial': container.serial, + 'public_client_key': container.publicClientKey, 'signature': signature, }; final Response response; - credential = await updateCredential(credential, (c) => c.copyWith(finalizationState: ContainerFinalizationState.sendingPublicKey)); - if (credential == null) throw StateError('Credential was removed'); + container = await updateCredential(container, (c) => c.copyWith(finalizationState: ContainerFinalizationState.sendingPublicKey)); + if (container == null) throw StateError('Credential was removed'); try { - response = await _ioClient.doPost(url: credential.finalizationUrl, body: body); + response = await _ioClient.doPost(url: container.finalizationUrl, body: body, sslVerify: false); //TODO: sslVerify } catch (e) { ref.read(statusMessageProvider.notifier).state = ('Failed to finalize container', e.toString()); - await updateCredential(credential, (c) => c.copyWith(finalizationState: ContainerFinalizationState.sendingPublicKeyFailed)); + await updateCredential(container, (c) => c.copyWith(finalizationState: ContainerFinalizationState.sendingPublicKeyFailed)); rethrow; } - credential = await updateCredential(credential, (c) => c.copyWith(finalizationState: ContainerFinalizationState.sendingPublicKeyCompleted)); - if (credential == null) throw StateError('Credential was removed'); - return (credential, response); + if (response.statusCode != 200) { + container = await updateCredential(container, (c) => c.copyWith(finalizationState: ContainerFinalizationState.sendingPublicKeyFailed)); + if (container == null) throw StateError('Credential was removed'); + return (container, response); + } + + container = await updateCredential(container, (c) => c.copyWith(finalizationState: ContainerFinalizationState.sendingPublicKeyCompleted)); + if (container == null) throw StateError('Credential was removed'); + return (container, response); } /// Finalization substep 3: Parse response @@ -259,8 +353,12 @@ class ContainerCredentialsNotifier extends _$ContainerCredentialsNotifier with R if (credential == null) throw StateError('Credential was removed'); try { responseJson = jsonDecode(responseBody); - validateMap(responseJson, {PUBLIC_SERVER_KEY: const TypeMatcher()}); - publicServerKey = const EccUtils().deserializeECPublicKey(responseJson[PUBLIC_SERVER_KEY]); + validateMap(responseJson, {'result': const TypeMatcher>()}); + final result = responseJson['result']; + validateMap(result, {'value': const TypeMatcher>()}); + final value = result['value']; + validateMap(value, {'public_server_key': const TypeMatcher()}); + publicServerKey = const EccUtils().deserializeECPublicKey(value['public_server_key']); } catch (e) { ref.read(statusMessageProvider.notifier).state = ('Failed to decode response body', e.toString()); await updateCredential(credential, (c) => c.copyWith(finalizationState: ContainerFinalizationState.parsingResponseFailed)); diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/credential_notifier.g.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/credential_notifier.g.dart index cdd06aefc..400d6ac5c 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/credential_notifier.g.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/credential_notifier.g.dart @@ -7,7 +7,7 @@ part of 'credential_notifier.dart'; // ************************************************************************** String _$containerCredentialsNotifierHash() => - r'31ed5a5d583df5880f443784cbf454d97d0e6519'; + r'e89ea14f6a73f042c4cf8f0d80a10b2be7605082'; /// Copied from Dart SDK class _SystemHash { @@ -34,10 +34,12 @@ abstract class _$ContainerCredentialsNotifier extends BuildlessAsyncNotifier { late final ContainerCredentialsRepository repo; late final PrivacyideaIOClient ioClient; + late final EccUtils eccUtils; FutureOr build({ required ContainerCredentialsRepository repo, required PrivacyideaIOClient ioClient, + required EccUtils eccUtils, }); } @@ -56,10 +58,12 @@ class ContainerCredentialsNotifierFamily ContainerCredentialsNotifierProvider call({ required ContainerCredentialsRepository repo, required PrivacyideaIOClient ioClient, + required EccUtils eccUtils, }) { return ContainerCredentialsNotifierProvider( repo: repo, ioClient: ioClient, + eccUtils: eccUtils, ); } @@ -70,6 +74,7 @@ class ContainerCredentialsNotifierFamily return call( repo: provider.repo, ioClient: provider.ioClient, + eccUtils: provider.eccUtils, ); } @@ -95,10 +100,12 @@ class ContainerCredentialsNotifierProvider extends AsyncNotifierProviderImpl< ContainerCredentialsNotifierProvider({ required ContainerCredentialsRepository repo, required PrivacyideaIOClient ioClient, + required EccUtils eccUtils, }) : this._internal( () => ContainerCredentialsNotifier() ..repo = repo - ..ioClient = ioClient, + ..ioClient = ioClient + ..eccUtils = eccUtils, from: containerCredentialsNotifierProviderOf, name: r'containerCredentialsNotifierProviderOf', debugGetCreateSourceHash: @@ -110,6 +117,7 @@ class ContainerCredentialsNotifierProvider extends AsyncNotifierProviderImpl< ContainerCredentialsNotifierFamily._allTransitiveDependencies, repo: repo, ioClient: ioClient, + eccUtils: eccUtils, ); ContainerCredentialsNotifierProvider._internal( @@ -121,10 +129,12 @@ class ContainerCredentialsNotifierProvider extends AsyncNotifierProviderImpl< required super.from, required this.repo, required this.ioClient, + required this.eccUtils, }) : super.internal(); final ContainerCredentialsRepository repo; final PrivacyideaIOClient ioClient; + final EccUtils eccUtils; @override FutureOr runNotifierBuild( @@ -133,6 +143,7 @@ class ContainerCredentialsNotifierProvider extends AsyncNotifierProviderImpl< return notifier.build( repo: repo, ioClient: ioClient, + eccUtils: eccUtils, ); } @@ -143,7 +154,8 @@ class ContainerCredentialsNotifierProvider extends AsyncNotifierProviderImpl< override: ContainerCredentialsNotifierProvider._internal( () => create() ..repo = repo - ..ioClient = ioClient, + ..ioClient = ioClient + ..eccUtils = eccUtils, from: from, name: null, dependencies: null, @@ -151,6 +163,7 @@ class ContainerCredentialsNotifierProvider extends AsyncNotifierProviderImpl< debugGetCreateSourceHash: null, repo: repo, ioClient: ioClient, + eccUtils: eccUtils, ), ); } @@ -165,7 +178,8 @@ class ContainerCredentialsNotifierProvider extends AsyncNotifierProviderImpl< bool operator ==(Object other) { return other is ContainerCredentialsNotifierProvider && other.repo == repo && - other.ioClient == ioClient; + other.ioClient == ioClient && + other.eccUtils == eccUtils; } @override @@ -173,6 +187,7 @@ class ContainerCredentialsNotifierProvider extends AsyncNotifierProviderImpl< var hash = _SystemHash.combine(0, runtimeType.hashCode); hash = _SystemHash.combine(hash, repo.hashCode); hash = _SystemHash.combine(hash, ioClient.hashCode); + hash = _SystemHash.combine(hash, eccUtils.hashCode); return _SystemHash.finish(hash); } @@ -185,6 +200,9 @@ mixin ContainerCredentialsNotifierRef /// The parameter `ioClient` of this provider. PrivacyideaIOClient get ioClient; + + /// The parameter `eccUtils` of this provider. + EccUtils get eccUtils; } class _ContainerCredentialsNotifierProviderElement @@ -198,6 +216,9 @@ class _ContainerCredentialsNotifierProviderElement @override PrivacyideaIOClient get ioClient => (origin as ContainerCredentialsNotifierProvider).ioClient; + @override + EccUtils get eccUtils => + (origin as ContainerCredentialsNotifierProvider).eccUtils; } // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/push_request_provider.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/push_request_provider.dart index ad4921795..096a3599a 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/push_request_provider.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/push_request_provider.dart @@ -390,7 +390,7 @@ class PushRequestNotifier extends _$PushRequestNotifier { try { Logger.info('Sending push request response.', name: 'token_widgets.dart#_handleReaction'); response = await _ioClient.doPost(sslVerify: pushRequest.sslVerify, url: pushRequest.uri, body: body); - } catch (e) { + } catch (_) { Logger.warning('Sending push request response failed. Retrying.', name: 'token_widgets.dart#handleReaction'); try { response = await _ioClient.doPost(sslVerify: pushRequest.sslVerify, url: pushRequest.uri, body: body); diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/push_request_provider.g.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/push_request_provider.g.dart index 5473a3a39..d35b5124a 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/push_request_provider.g.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/push_request_provider.g.dart @@ -7,7 +7,7 @@ part of 'push_request_provider.dart'; // ************************************************************************** String _$pushRequestNotifierHash() => - r'34e1b940ac7ae3abb4d5887bae44d76f147b6c38'; + r'83bb88bfb1fbc8623da1d368a5bb8808734b1e88'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/views/container_view/container_view.dart b/lib/views/container_view/container_view.dart new file mode 100644 index 000000000..d4741603d --- /dev/null +++ b/lib/views/container_view/container_view.dart @@ -0,0 +1,154 @@ +/* + * privacyIDEA Authenticator + * + * Author: Frank Merkel + * + * Copyright (c) 2024 NetKnights GmbH + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/credential_notifier.dart'; +import 'package:privacyidea_authenticator/views/main_view/main_view_widgets/main_view_navigation_buttons/qr_scanner_button.dart'; +import 'package:privacyidea_authenticator/views/main_view/main_view_widgets/token_widgets/token_widget_tile.dart'; + +import '../../l10n/app_localizations.dart'; +import '../../model/tokens/container_credentials.dart'; +import '../../utils/customization/theme_extentions/action_theme.dart'; +import '../main_view/main_view_widgets/token_widgets/slideable_action.dart'; +import '../../widgets/pi_slideable.dart'; +import '../view_interface.dart'; + +const String groupTag = 'container-actions'; + +class ContainerView extends ConsumerView { + static const String routeName = '/container'; + + @override + get routeSettings => const RouteSettings(name: routeName); + + const ContainerView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final credentials = ref.watch(containerCredentialsProvider).whenOrNull(data: (data) => data.credentials) ?? []; + return Scaffold( + appBar: AppBar(title: const Text('Container')), + floatingActionButton: const QrScannerButton(), + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + for (var containerCredential in credentials) ContainerWidget(containerCredential: containerCredential), + ], + ), + ), + ); + } +} + +class ContainerWidget extends ConsumerWidget { + final ContainerCredential containerCredential; + + final List stack; + + const ContainerWidget({ + required this.containerCredential, + this.stack = const [], + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) => ClipRRect( + child: PiSlideable( + groupTag: groupTag, + identifier: containerCredential.serial, + actions: [ + DeleteContainerAction(container: containerCredential, key: Key('${containerCredential.serial}-DeleteContainerAction')), + EditContainerAction(container: containerCredential, key: Key('${containerCredential.serial}-EditContainerAction')), + ], + stack: stack, + tile: TokenWidgetTile( + title: Text(containerCredential.serial), + subtitles: [ + 'issuer: ${containerCredential.issuer}', + 'finalizationState: ${containerCredential.finalizationState.name}', + ], + trailing: IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + // Open Slidable + }, + ), + ), + ), + ); +} + +class DeleteContainerAction extends PiSlideableAction { + final ContainerCredential container; + + const DeleteContainerAction({ + required this.container, + super.key, + }); + + @override + CustomSlidableAction build(BuildContext context, WidgetRef ref) => CustomSlidableAction( + onPressed: (BuildContext context) => ref.read(containerCredentialsProvider.notifier).deleteCredential(container), + backgroundColor: Theme.of(context).extension()!.deleteColor, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Icons.delete_forever), + Text( + AppLocalizations.of(context)!.delete, + overflow: TextOverflow.fade, + softWrap: false, + ), + ], + ), + ); +} + +class EditContainerAction extends PiSlideableAction { + final ContainerCredential container; + + const EditContainerAction({ + required this.container, + super.key, + }); + + @override + CustomSlidableAction build(BuildContext context, WidgetRef ref) => CustomSlidableAction( + onPressed: (BuildContext context) {}, + backgroundColor: Theme.of(context).extension()!.editColor, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Icons.edit), + Text( + AppLocalizations.of(context)!.edit, + overflow: TextOverflow.fade, + softWrap: false, + ), + ], + ), + ); +} diff --git a/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart b/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart index f194659dd..8ad4a640a 100644 --- a/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart +++ b/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart @@ -21,8 +21,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/credential_notifier.dart'; - import '../../../model/mixins/sortable_mixin.dart'; import '../../../model/riverpod_states/settings_state.dart'; import '../../../model/token_folder.dart'; @@ -106,10 +104,6 @@ class _MainViewTokensListState extends ConsumerState { if ((showSortables.isEmpty)) return const NoTokenScreen(); - final credentials = ref.read(containerCredentialsProvider).whenOrNull( - data: (data) => data.credentials, - ); - return Stack( children: [ Column( @@ -163,12 +157,6 @@ class _MainViewTokensListState extends ConsumerState { bottomPaddingIfLast: 80, ) : const SizedBox(height: 80), - ...(credentials!.map((credential) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text('${credential.serial} | ${credential.issuer} | ${credential.finalizationState.name}'), - ); - }).toList()) ], ), ), diff --git a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/actions/edit_day_password_token_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/actions/edit_day_password_token_action.dart index 3ef5c57a2..753ee0fa6 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/actions/edit_day_password_token_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/actions/edit_day_password_token_action.dart @@ -30,9 +30,9 @@ import '../../../../../../utils/lock_auth.dart'; import '../../../../../../utils/riverpod/riverpod_providers/generated_providers/introduction_provider.dart'; import '../../../../../../widgets/focused_item_as_overlay.dart'; import '../../default_token_actions/default_edit_action_dialog.dart'; -import '../../token_action.dart'; +import '../../slideable_action.dart'; -class EditDayPassowrdTokenAction extends TokenAction { +class EditDayPassowrdTokenAction extends PiSlideableAction { final DayPasswordToken token; const EditDayPassowrdTokenAction({ diff --git a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart index bc6943fed..d3a56f0e8 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart @@ -28,9 +28,9 @@ import '../../../../../utils/lock_auth.dart'; import '../../../../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; import '../../../../../widgets/dialog_widgets/default_dialog.dart'; import '../../loading_indicator.dart'; -import '../token_action.dart'; +import '../slideable_action.dart'; -class DefaultDeleteAction extends TokenAction { +class DefaultDeleteAction extends PiSlideableAction { final Token token; const DefaultDeleteAction({super.key, required this.token}); diff --git a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_edit_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_edit_action.dart index 85ba8ba2f..73e5b0f68 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_edit_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_edit_action.dart @@ -31,10 +31,10 @@ import '../../../../../utils/logger.dart'; import '../../../../../utils/riverpod/riverpod_providers/generated_providers/introduction_provider.dart'; import '../../../../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; import '../../../../../widgets/focused_item_as_overlay.dart'; -import '../token_action.dart'; +import '../slideable_action.dart'; import 'default_edit_action_dialog.dart'; -class DefaultEditAction extends TokenAction { +class DefaultEditAction extends PiSlideableAction { final Token token; const DefaultEditAction({required this.token, super.key}); diff --git a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_lock_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_lock_action.dart index 0adfe03a5..b228d36e0 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_lock_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_lock_action.dart @@ -31,9 +31,9 @@ import '../../../../../utils/logger.dart'; import '../../../../../utils/riverpod/riverpod_providers/generated_providers/introduction_provider.dart'; import '../../../../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; import '../../../../../widgets/focused_item_as_overlay.dart'; -import '../token_action.dart'; +import '../slideable_action.dart'; -class DefaultLockAction extends TokenAction { +class DefaultLockAction extends PiSlideableAction { final Token token; const DefaultLockAction({required this.token, super.key}); diff --git a/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/actions/edit_hotp_token_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/actions/edit_hotp_token_action.dart index 6c0457e19..3a1ed32d6 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/actions/edit_hotp_token_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/actions/edit_hotp_token_action.dart @@ -30,9 +30,9 @@ import '../../../../../../utils/lock_auth.dart'; import '../../../../../../utils/riverpod/riverpod_providers/generated_providers/introduction_provider.dart'; import '../../../../../../widgets/focused_item_as_overlay.dart'; import '../../default_token_actions/default_edit_action_dialog.dart'; -import '../../token_action.dart'; +import '../../slideable_action.dart'; -class EditHOTPTokenAction extends TokenAction { +class EditHOTPTokenAction extends PiSlideableAction { final HOTPToken token; const EditHOTPTokenAction({ diff --git a/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget.dart b/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget.dart index acc629314..48633c811 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget.dart @@ -35,12 +35,10 @@ class HOTPTokenWidget extends TokenWidget { super.key, }); @override - TokenWidgetBase build(BuildContext context) { - return TokenWidgetBase( - token: token, - tile: HOTPTokenWidgetTile(token, key: ValueKey(token.id)), - dragIcon: Icons.replay, - editAction: EditHOTPTokenAction(token: token), - ); - } + TokenWidgetBase build(BuildContext context) => TokenWidgetBase( + token: token, + tile: HOTPTokenWidgetTile(token, key: ValueKey(token.id)), + dragIcon: Icons.replay, + editAction: EditHOTPTokenAction(token: token), + ); } diff --git a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/actions/edit_push_token_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/actions/edit_push_token_action.dart index e2d2caa17..3d3adf8fa 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/actions/edit_push_token_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/actions/edit_push_token_action.dart @@ -32,9 +32,9 @@ import '../../../../../../utils/riverpod/riverpod_providers/generated_providers/ import '../../../../../../widgets/enable_text_edit_after_many_taps.dart'; import '../../../../../../widgets/focused_item_as_overlay.dart'; import '../../default_token_actions/default_edit_action_dialog.dart'; -import '../../token_action.dart'; +import '../../slideable_action.dart'; -class EditPushTokenAction extends TokenAction { +class EditPushTokenAction extends PiSlideableAction { final PushToken token; const EditPushTokenAction({ diff --git a/lib/views/main_view/main_view_widgets/token_widgets/token_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/slideable_action.dart similarity index 90% rename from lib/views/main_view/main_view_widgets/token_widgets/token_action.dart rename to lib/views/main_view/main_view_widgets/token_widgets/slideable_action.dart index b6c43b71a..04ef0dbf3 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/token_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/slideable_action.dart @@ -21,8 +21,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; -abstract class TokenAction extends ConsumerWidget { - const TokenAction({super.key}); +abstract class PiSlideableAction extends ConsumerWidget { + const PiSlideableAction({super.key}); @override CustomSlidableAction build(BuildContext context, WidgetRef ref); } diff --git a/lib/views/main_view/main_view_widgets/token_widgets/token_widget.dart b/lib/views/main_view/main_view_widgets/token_widgets/token_widget.dart index 12120eb37..e2a0ae88b 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/token_widget.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/token_widget.dart @@ -22,6 +22,7 @@ import 'package:flutter/material.dart'; import 'token_widget_base.dart'; abstract class TokenWidget extends StatelessWidget { + static const String groupTag = 'token-actions'; const TokenWidget({super.key}); @override diff --git a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart index 0c102968c..2448603f0 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart @@ -31,15 +31,16 @@ import '../../../../utils/utils.dart'; import 'default_token_actions/default_delete_action.dart'; import 'default_token_actions/default_edit_action.dart'; import 'default_token_actions/default_lock_action.dart'; -import 'token_action.dart'; -import 'token_widget_slideable.dart'; +import 'slideable_action.dart'; +import '../../../../widgets/pi_slideable.dart'; +import 'token_widget.dart'; class TokenWidgetBase extends ConsumerWidget { final Widget tile; final Token token; - final TokenAction? deleteAction; - final TokenAction? editAction; - final TokenAction? lockAction; + final PiSlideableAction? deleteAction; + final PiSlideableAction? editAction; + final PiSlideableAction? lockAction; final List stack; final IconData dragIcon; @@ -57,7 +58,7 @@ class TokenWidgetBase extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final SortableMixin? draggingSortable = ref.watch(draggingSortableProvider); - final List actions = [ + final List actions = [ deleteAction ?? DefaultDeleteAction(token: token, key: Key('${token.id}deleteAction')), editAction ?? DefaultEditAction(token: token, key: Key('${token.id}editAction')), ]; @@ -103,8 +104,9 @@ class TokenWidgetBase extends ConsumerWidget { ), data: token, child: ClipRRect( - child: TokenWidgetSlideable( - token: token, + child: PiSlideable( + groupTag: TokenWidget.groupTag, + identifier: token.id, actions: actions, stack: stack, tile: tile, @@ -114,8 +116,9 @@ class TokenWidgetBase extends ConsumerWidget { : draggingSortable == token ? const SizedBox() : ClipRRect( - child: TokenWidgetSlideable( - token: token, + child: PiSlideable( + groupTag: TokenWidget.groupTag, + identifier: token.id, actions: actions, stack: stack, tile: tile, diff --git a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/actions/edit_totp_token_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/actions/edit_totp_token_action.dart index 10a271af2..285c86e0b 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/actions/edit_totp_token_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/actions/edit_totp_token_action.dart @@ -30,9 +30,9 @@ import '../../../../../../utils/lock_auth.dart'; import '../../../../../../utils/riverpod/riverpod_providers/generated_providers/introduction_provider.dart'; import '../../../../../../widgets/focused_item_as_overlay.dart'; import '../../default_token_actions/default_edit_action_dialog.dart'; -import '../../token_action.dart'; +import '../../slideable_action.dart'; -class EditTOTPTokenAction extends TokenAction { +class EditTOTPTokenAction extends PiSlideableAction { final TOTPToken token; const EditTOTPTokenAction({ diff --git a/lib/views/settings_view/settings_groups/settings_group_container.dart b/lib/views/settings_view/settings_groups/settings_group_container.dart new file mode 100644 index 000000000..e761114eb --- /dev/null +++ b/lib/views/settings_view/settings_groups/settings_group_container.dart @@ -0,0 +1,48 @@ +/* + * privacyIDEA Authenticator + * + * Author: Frank Merkel + * + * Copyright (c) 2024 NetKnights GmbH + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:privacyidea_authenticator/views/container_view/container_view.dart'; + +import '../settings_view_widgets/settings_groups.dart'; + +class SettingsGroupContainer extends StatelessWidget { + const SettingsGroupContainer({super.key}); + + @override + Widget build(BuildContext context) => SettingsGroup( + title: 'Container', + children: [ + TextButton( + child: ListTile( + title: Text( + 'Container', + style: Theme.of(context).textTheme.bodyMedium, + overflow: TextOverflow.fade, + softWrap: false, + ), + style: ListTileStyle.list, + trailing: Icon(Icons.arrow_forward_ios), + ), + onPressed: () => Navigator.of(context).pushNamed(ContainerView.routeName), + ), + ], + ); +} diff --git a/lib/views/settings_view/settings_view.dart b/lib/views/settings_view/settings_view.dart index bff4e741d..7d85af3b9 100644 --- a/lib/views/settings_view/settings_view.dart +++ b/lib/views/settings_view/settings_view.dart @@ -25,6 +25,7 @@ import '../../model/tokens/push_token.dart'; import '../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; import '../../widgets/push_request_listener.dart'; import '../view_interface.dart'; +import 'settings_groups/settings_group_container.dart'; import 'settings_groups/settings_group_error_log.dart'; import 'settings_groups/settings_group_general.dart'; import 'settings_groups/settings_group_import_export_tokens.dart'; @@ -73,6 +74,8 @@ class SettingsView extends ConsumerView { ), const Divider(), const SettingsGroupErrorLog(), + const Divider(), + const SettingsGroupContainer(), ], ), ), diff --git a/lib/views/view_interface.dart b/lib/views/view_interface.dart index d0295832f..96d181a15 100644 --- a/lib/views/view_interface.dart +++ b/lib/views/view_interface.dart @@ -19,6 +19,7 @@ */ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +export 'package:flutter_riverpod/flutter_riverpod.dart' show WidgetRef; abstract class ViewWidget extends Widget { RouteSettings get routeSettings; diff --git a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_slideable.dart b/lib/widgets/pi_slideable.dart similarity index 78% rename from lib/views/main_view/main_view_widgets/token_widgets/token_widget_slideable.dart rename to lib/widgets/pi_slideable.dart index fd9e5afba..79e64a4d4 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_slideable.dart +++ b/lib/widgets/pi_slideable.dart @@ -20,17 +20,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; -import '../../../../model/tokens/token.dart'; -import 'token_action.dart'; +import '../views/main_view/main_view_widgets/token_widgets/slideable_action.dart'; -class TokenWidgetSlideable extends StatelessWidget { - final Token token; - final List actions; +class PiSlideable extends StatelessWidget { + final String groupTag; + final String identifier; + final List actions; final List stack; final Widget tile; - const TokenWidgetSlideable({ - required this.token, + const PiSlideable({ + required this.groupTag, + required this.identifier, required this.actions, required this.stack, required this.tile, @@ -47,8 +48,8 @@ class TokenWidgetSlideable extends StatelessWidget { ); return actions.isNotEmpty ? Slidable( - key: ValueKey(token.id), - groupTag: 'myTag', // This is used to only let one be open at a time. + key: ValueKey('$groupTag-$identifier'), + groupTag: groupTag, endActionPane: ActionPane( motion: const DrawerMotion(), extentRatio: 1,