diff --git a/android/fastlane/metadata/android/de-DE/changelogs/4053.txt b/android/fastlane/metadata/android/de-DE/changelogs/4053.txt new file mode 100644 index 00000000..a89053b1 --- /dev/null +++ b/android/fastlane/metadata/android/de-DE/changelogs/4053.txt @@ -0,0 +1,2 @@ +- Beheben eines Problems mit mTLS +- Kleinere Fehlerbehebungen \ No newline at end of file diff --git a/android/fastlane/metadata/android/en-US/changelogs/4053.txt b/android/fastlane/metadata/android/en-US/changelogs/4053.txt new file mode 100644 index 00000000..623c6816 --- /dev/null +++ b/android/fastlane/metadata/android/en-US/changelogs/4053.txt @@ -0,0 +1,2 @@ +- Fix issue with mTLS +- Some minor bug fixes \ No newline at end of file diff --git a/lib/core/factory/paperless_api_factory.dart b/lib/core/factory/paperless_api_factory.dart index e239fffd..01f54529 100644 --- a/lib/core/factory/paperless_api_factory.dart +++ b/lib/core/factory/paperless_api_factory.dart @@ -2,13 +2,29 @@ import 'package:dio/dio.dart'; import 'package:paperless_api/paperless_api.dart'; abstract class PaperlessApiFactory { - PaperlessDocumentsApi createDocumentsApi(Dio dio, {required int apiVersion}); - PaperlessSavedViewsApi createSavedViewsApi(Dio dio, - {required int apiVersion}); - PaperlessLabelsApi createLabelsApi(Dio dio, {required int apiVersion}); - PaperlessServerStatsApi createServerStatsApi(Dio dio, - {required int apiVersion}); - PaperlessTasksApi createTasksApi(Dio dio, {required int apiVersion}); + PaperlessDocumentsApi createDocumentsApi( + Dio dio, { + required int apiVersion, + }); + PaperlessSavedViewsApi createSavedViewsApi( + Dio dio, { + required int apiVersion, + }); + PaperlessLabelsApi createLabelsApi( + Dio dio, { + required int apiVersion, + }); + PaperlessServerStatsApi createServerStatsApi( + Dio dio, { + required int apiVersion, + }); + PaperlessTasksApi createTasksApi( + Dio dio, { + required int apiVersion, + }); PaperlessAuthenticationApi createAuthenticationApi(Dio dio); - PaperlessUserApi createUserApi(Dio dio, {required int apiVersion}); + PaperlessUserApi createUserApi( + Dio dio, { + required int apiVersion, + }); } diff --git a/lib/features/changelogs/view/changelog_dialog.dart b/lib/features/changelogs/view/changelog_dialog.dart index 202d271a..e2cfec0f 100644 --- a/lib/features/changelogs/view/changelog_dialog.dart +++ b/lib/features/changelogs/view/changelog_dialog.dart @@ -63,6 +63,7 @@ class ChangelogDialog extends StatelessWidget { } const _versionNumbers = { + "4053": "3.2.1", "4043": "3.2.0", "4033": "3.1.8", "4023": "3.1.7", diff --git a/lib/features/login/model/client_certificate.dart b/lib/features/login/model/client_certificate.dart index 3dd39b80..e171809d 100644 --- a/lib/features/login/model/client_certificate.dart +++ b/lib/features/login/model/client_certificate.dart @@ -8,13 +8,27 @@ part 'client_certificate.g.dart'; @HiveType(typeId: HiveTypeIds.clientCertificate) class ClientCertificate { @HiveField(0) - Uint8List bytes; + final Uint8List bytes; + @HiveField(2, defaultValue: "cert.pfx") + final String filename; @HiveField(1) - String? passphrase; - + final String? passphrase; ClientCertificate({ required this.bytes, + required this.filename, this.passphrase, }); + + ClientCertificate copyWith({ + Uint8List? bytes, + String? filename, + String? passphrase, + }) { + return ClientCertificate( + bytes: bytes ?? this.bytes, + filename: filename ?? this.filename, + passphrase: passphrase ?? this.passphrase, + ); + } } diff --git a/lib/features/login/model/client_certificate_form_model.dart b/lib/features/login/model/client_certificate_form_model.dart deleted file mode 100644 index b168d151..00000000 --- a/lib/features/login/model/client_certificate_form_model.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:typed_data'; - -class ClientCertificateFormModel { - static const bytesKey = 'bytes'; - static const passphraseKey = 'passphrase'; - - final Uint8List bytes; - final String? passphrase; - - ClientCertificateFormModel({ - required this.bytes, - this.passphrase, - }); - - ClientCertificateFormModel copyWith({ - Uint8List? bytes, - String? passphrase, - String? filePath, - }) { - return ClientCertificateFormModel( - bytes: bytes ?? this.bytes, - passphrase: passphrase ?? this.passphrase, - ); - } -} diff --git a/lib/features/login/view/add_account_page.dart b/lib/features/login/view/add_account_page.dart index 7bb82c66..04296cdf 100644 --- a/lib/features/login/view/add_account_page.dart +++ b/lib/features/login/view/add_account_page.dart @@ -12,7 +12,6 @@ import 'package:paperless_mobile/core/model/info_message_exception.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart'; -import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart'; import 'package:paperless_mobile/features/login/model/login_form_credentials.dart'; import 'package:paperless_mobile/features/login/model/reachability_status.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart'; @@ -230,75 +229,14 @@ class _AddAccountPageState extends State { ), ), ); - return Scaffold( - appBar: AppBar( - title: Text(widget.titleText), - ), - bottomNavigationBar: BottomAppBar( - child: Row( - mainAxisAlignment: widget.bottomLeftButton != null - ? MainAxisAlignment.spaceBetween - : MainAxisAlignment.end, - children: [ - if (widget.bottomLeftButton != null) widget.bottomLeftButton!, - FilledButton( - child: Text(S.of(context)!.loginPageSignInTitle), - onPressed: _reachabilityStatus == ReachabilityStatus.reachable && - !_isFormSubmitted - ? _onSubmit - : null, - ), - ], - ), - ), - resizeToAvoidBottomInset: true, - body: AutofillGroup( - child: FormBuilder( - key: _formKey, - child: ListView( - children: [ - ServerAddressFormField( - initialValue: widget.initialServerUrl, - onChanged: (address) { - _updateReachability(address); - }, - ).padded(), - ClientCertificateFormField( - initialBytes: widget.initialClientCertificate?.bytes, - initialPassphrase: widget.initialClientCertificate?.passphrase, - onChanged: (_) => _updateReachability(), - ).padded(), - _buildStatusIndicator(), - if (_reachabilityStatus == ReachabilityStatus.reachable) ...[ - UserCredentialsFormField( - formKey: _formKey, - initialUsername: widget.initialUsername, - initialPassword: widget.initialPassword, - onFieldsSubmitted: _onSubmit, - ), - Text( - S.of(context)!.loginRequiredPermissionsHint, - style: Theme.of(context).textTheme.bodySmall?.apply( - color: Theme.of(context) - .colorScheme - .onBackground - .withOpacity(0.6), - ), - ).padded(16), - ], - ], - ), - ), - ), - ); } Future _updateReachability([String? address]) async { setState(() { _isCheckingConnection = true; }); - final certForm = - _formKey.currentState?.getRawValue( + final selectedCertificate = + _formKey.currentState?.getRawValue( ClientCertificateFormField.fkClientCertificate, ); final status = await context @@ -307,12 +245,7 @@ class _AddAccountPageState extends State { address ?? _formKey.currentState! .getRawValue(ServerAddressFormField.fkServerAddress), - certForm != null - ? ClientCertificate( - bytes: certForm.bytes, - passphrase: certForm.passphrase, - ) - : null, + selectedCertificate, ); setState(() { _isCheckingConnection = false; @@ -383,16 +316,10 @@ class _AddAccountPageState extends State { }); if (_formKey.currentState?.saveAndValidate() ?? false) { final form = _formKey.currentState!.value; - ClientCertificate? clientCert; final clientCertFormModel = form[ClientCertificateFormField.fkClientCertificate] - as ClientCertificateFormModel?; - if (clientCertFormModel != null) { - clientCert = ClientCertificate( - bytes: clientCertFormModel.bytes, - passphrase: clientCertFormModel.passphrase, - ); - } + as ClientCertificate?; + final credentials = form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials; try { @@ -401,7 +328,7 @@ class _AddAccountPageState extends State { credentials.username!, credentials.password!, form[ServerAddressFormField.fkServerAddress], - clientCert, + clientCertFormModel, ); } on PaperlessApiException catch (error) { showErrorMessage(context, error); diff --git a/lib/features/login/view/widgets/form_fields/client_certificate_form_field.dart b/lib/features/login/view/widgets/form_fields/client_certificate_form_field.dart index 10874899..c56ddfe2 100644 --- a/lib/features/login/view/widgets/form_fields/client_certificate_form_field.dart +++ b/lib/features/login/view/widgets/form_fields/client_certificate_form_field.dart @@ -5,7 +5,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart'; +import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:path/path.dart' as p; @@ -15,14 +15,16 @@ class ClientCertificateFormField extends StatefulWidget { static const fkClientCertificate = 'clientCertificate'; final String? initialPassphrase; + final String? initialFilename; final Uint8List? initialBytes; - final ValueChanged? onChanged; + final ValueChanged? onChanged; const ClientCertificateFormField({ super.key, this.onChanged, this.initialPassphrase, this.initialBytes, + this.initialFilename, }); @override @@ -32,23 +34,23 @@ class ClientCertificateFormField extends StatefulWidget { class _ClientCertificateFormFieldState extends State with AutomaticKeepAliveClientMixin { - File? _selectedFile; @override Widget build(BuildContext context) { super.build(context); - return FormBuilderField( + return FormBuilderField( key: const ValueKey('login-client-cert'), name: ClientCertificateFormField.fkClientCertificate, onChanged: widget.onChanged, initialValue: widget.initialBytes != null - ? ClientCertificateFormModel( + ? ClientCertificate( bytes: widget.initialBytes!, + filename: widget.initialFilename!, passphrase: widget.initialPassphrase, ) : null, builder: (field) { final theme = - Theme.of(context).copyWith(dividerColor: Colors.transparent); //new + Theme.of(context).copyWith(dividerColor: Colors.transparent); return Theme( data: theme, child: ExpansionTile( @@ -74,11 +76,10 @@ class _ClientCertificateFormFieldState extends State _buildSelectedFileText(field).paddedOnly(left: 8), ], ), - if (_selectedFile != null) + if (field.value?.filename != null) IconButton( icon: const Icon(Icons.close), onPressed: () => setState(() { - _selectedFile = null; field.didChange(null); }), ) @@ -103,7 +104,7 @@ class _ClientCertificateFormFieldState extends State // : null, // ), // ), - if (_selectedFile != null) ...[ + if (field.value?.filename != null) ...[ ObscuredInputTextFormField( key: const ValueKey('login-client-cert-passphrase'), initialValue: field.value?.passphrase, @@ -124,7 +125,7 @@ class _ClientCertificateFormFieldState extends State } Future _onSelectFile( - FormFieldState field, + FormFieldState field, ) async { final result = await FilePicker.platform.pickFiles( allowMultiple: false, @@ -137,21 +138,18 @@ class _ClientCertificateFormFieldState extends State showSnackBar(context, S.of(context)!.invalidCertificateFormat); return; } - File file = File(result.files.single.path!); - setState(() { - _selectedFile = file; - }); + File file = File(path); final bytes = await file.readAsBytes(); - final changedValue = field.value?.copyWith(bytes: bytes) ?? - ClientCertificateFormModel(bytes: bytes); + final changedValue = ClientCertificate( + bytes: bytes, + filename: p.basename(path), + ); field.didChange(changedValue); } - Widget _buildSelectedFileText( - FormFieldState field) { + Widget _buildSelectedFileText(FormFieldState field) { if (field.value == null) { - assert(_selectedFile == null); return Text( S.of(context)!.selectFile, style: Theme.of(context).textTheme.labelMedium?.apply( @@ -159,9 +157,8 @@ class _ClientCertificateFormFieldState extends State ), ); } else { - assert(_selectedFile != null); return Text( - _selectedFile!.path.split("/").last, + p.basename(field.value!.filename), style: const TextStyle( overflow: TextOverflow.ellipsis, ), diff --git a/packages/paperless_api/lib/src/models/custom_field_model.dart b/packages/paperless_api/lib/src/models/custom_field_model.dart index c49b48b9..f1e86ce6 100644 --- a/packages/paperless_api/lib/src/models/custom_field_model.dart +++ b/packages/paperless_api/lib/src/models/custom_field_model.dart @@ -7,7 +7,7 @@ part 'custom_field_model.g.dart'; @JsonSerializable() class CustomFieldModel with EquatableMixin { final int? id; - final String name; + final String? name; final CustomFieldDataType dataType; CustomFieldModel({ @@ -24,3 +24,18 @@ class CustomFieldModel with EquatableMixin { Map toJson() => _$CustomFieldModelToJson(this); } + +/// An instance of the [CustomFieldModel]. +@JsonSerializable() +class CustomFieldInstance { + final int? id; + final dynamic value; + + const CustomFieldInstance({ + this.id, + this.value, + }); + + factory CustomFieldInstance.fromJson(Map json) => + _$CustomFieldInstanceFromJson(json); +} diff --git a/packages/paperless_api/lib/src/models/document_model.dart b/packages/paperless_api/lib/src/models/document_model.dart index 38df5206..08756371 100644 --- a/packages/paperless_api/lib/src/models/document_model.dart +++ b/packages/paperless_api/lib/src/models/document_model.dart @@ -52,7 +52,7 @@ class DocumentModel extends Equatable { /// Only present if full_perms=true final Permissions? permissions; - final Iterable customFields; + final Iterable customFields; const DocumentModel({ required this.id, @@ -98,7 +98,7 @@ class DocumentModel extends Equatable { bool? userCanChange, Iterable? notes, Permissions? permissions, - Iterable? customFields, + Iterable? customFields, }) { return DocumentModel( id: id, diff --git a/pubspec.yaml b/pubspec.yaml index 7e99e5fa..0e9f95a5 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: 3.2.0+404 +version: 3.2.1+405 environment: sdk: ">=3.1.0 <4.0.0"