feat: Add permission checks, fix search, fix document upload, fix linked documents always being loaded all at once instead of paged

This commit is contained in:
Anton Stubenbord
2023-05-27 18:14:35 +02:00
parent 4f13146dbc
commit b30ede6661
19 changed files with 280 additions and 182 deletions

View File

@@ -0,0 +1,35 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive_flutter/adapters.dart';
///
/// Opens an encrypted box, calls [callback] with the now opened box, awaits
/// [callback] to return and returns the calculated value. Closes the box after.
///
Future<R?> withEncryptedBox<T, R>(String name, FutureOr<R?> Function(Box<T> box) callback) async {
final key = await _getEncryptedBoxKey();
final box = await Hive.openBox<T>(
name,
encryptionCipher: HiveAesCipher(key),
);
final result = await callback(box);
await box.close();
return result;
}
Future<Uint8List> _getEncryptedBoxKey() async {
const secureStorage = FlutterSecureStorage();
if (!await secureStorage.containsKey(key: 'key')) {
final key = Hive.generateSecureKey();
await secureStorage.write(
key: 'key',
value: base64UrlEncode(key),
);
}
final key = (await secureStorage.read(key: 'key'))!;
return base64Decode(key);
}

View File

@@ -15,7 +15,6 @@ import 'package:paperless_mobile/core/repository/user_repository.dart';
import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart'; import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart';
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart'; import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart';
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart'; import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart';
import 'package:paperless_mobile/features/document_scan/view/scanner_page.dart';
import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart';
import 'package:paperless_mobile/features/document_search/view/document_search_page.dart'; import 'package:paperless_mobile/features/document_search/view/document_search_page.dart';
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
@@ -38,6 +37,7 @@ import 'package:provider/provider.dart';
Future<void> pushDocumentSearchPage(BuildContext context) { Future<void> pushDocumentSearchPage(BuildContext context) {
final currentUser = final currentUser =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser; Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser;
final userRepo = context.read<UserRepository>();
return Navigator.of(context).push( return Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => MultiProvider( builder: (_) => MultiProvider(
@@ -46,6 +46,7 @@ Future<void> pushDocumentSearchPage(BuildContext context) {
Provider.value(value: context.read<PaperlessDocumentsApi>()), Provider.value(value: context.read<PaperlessDocumentsApi>()),
Provider.value(value: context.read<DocumentChangedNotifier>()), Provider.value(value: context.read<DocumentChangedNotifier>()),
Provider.value(value: context.read<CacheManager>()), Provider.value(value: context.read<CacheManager>()),
Provider.value(value: userRepo),
], ],
builder: (context, _) { builder: (context, _) {
return BlocProvider( return BlocProvider(

View File

@@ -2,6 +2,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/constants.dart'; import 'package:paperless_mobile/constants.dart';
import 'package:paperless_mobile/core/widgets/paperless_logo.dart'; import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
@@ -92,8 +93,11 @@ class AppDrawer extends StatelessWidget {
), ),
onTap: () => Navigator.of(context).push( onTap: () => Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => Provider.value( builder: (_) => MultiProvider(
value: context.read<ApiVersion>(), providers: [
Provider.value(value: context.read<PaperlessServerStatsApi>()),
Provider.value(value: context.read<ApiVersion>()),
],
child: const SettingsPage(), child: const SettingsPage(),
), ),
), ),

View File

@@ -240,7 +240,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
SliverOverlapInjector( SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
), ),
const DocumentPermissionsWidget(), DocumentPermissionsWidget(
document: state.document,
),
], ],
), ),
], ],

View File

@@ -1,8 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
class DocumentPermissionsWidget extends StatelessWidget { class DocumentPermissionsWidget extends StatefulWidget {
const DocumentPermissionsWidget({super.key}); final DocumentModel document;
const DocumentPermissionsWidget({super.key, required this.document});
@override
State<DocumentPermissionsWidget> createState() => _DocumentPermissionsWidgetState();
}
class _DocumentPermissionsWidgetState extends State<DocumentPermissionsWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const SliverToBoxAdapter( return const SliverToBoxAdapter(

View File

@@ -18,8 +18,6 @@ import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart'; import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/document_scan/view/widgets/scanned_image_item.dart'; import 'package:paperless_mobile/features/document_scan/view/widgets/scanned_image_item.dart';
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart'; import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';

View File

@@ -27,88 +27,89 @@ class DocumentSearchBar extends StatefulWidget {
class _DocumentSearchBarState extends State<DocumentSearchBar> { class _DocumentSearchBarState extends State<DocumentSearchBar> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return OpenContainer( return Container(
transitionDuration: const Duration(milliseconds: 200), margin: EdgeInsets.only(top: 8),
transitionType: ContainerTransitionType.fadeThrough, child: OpenContainer(
closedElevation: 1, transitionDuration: const Duration(milliseconds: 200),
middleColor: Theme.of(context).colorScheme.surfaceVariant, transitionType: ContainerTransitionType.fadeThrough,
openColor: Theme.of(context).colorScheme.background, closedElevation: 1,
closedColor: Theme.of(context).colorScheme.surfaceVariant, middleColor: Theme.of(context).colorScheme.surfaceVariant,
closedShape: RoundedRectangleBorder( openColor: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(56), closedColor: Theme.of(context).colorScheme.surfaceVariant,
), closedShape: RoundedRectangleBorder(
closedBuilder: (_, action) { borderRadius: BorderRadius.circular(56),
return InkWell( ),
child: ConstrainedBox( closedBuilder: (_, action) {
constraints: const BoxConstraints( return InkWell(
maxWidth: 720, onTap: action,
minWidth: 360, child: ConstrainedBox(
maxHeight: 56, constraints: const BoxConstraints(
minHeight: 48, maxWidth: 720,
), minWidth: 360,
child: Row( maxHeight: 56,
mainAxisAlignment: MainAxisAlignment.spaceBetween, minHeight: 48,
children: [ ),
Flexible( child: Row(
child: Padding( mainAxisAlignment: MainAxisAlignment.spaceBetween,
padding: EdgeInsets.symmetric(horizontal: 8), children: [
child: Row( Flexible(
children: [ child: Padding(
IconButton( padding: const EdgeInsets.symmetric(horizontal: 8),
icon: Icon(Icons.menu), child: Row(
onPressed: Scaffold.of(context).openDrawer, crossAxisAlignment: CrossAxisAlignment.center,
), children: [
Expanded( IconButton(
child: Hero( icon: const Icon(Icons.menu),
tag: "search_hero_tag", onPressed: Scaffold.of(context).openDrawer,
child: TextField( ),
enabled: false, Flexible(
decoration: InputDecoration( child: Text(
border: InputBorder.none, S.of(context)!.searchDocuments,
hintText: S.of(context)!.searchDocuments, style: Theme.of(context).textTheme.bodyLarge?.copyWith(
hintStyle: TextStyle( fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).hintColor,
), ),
),
), ),
), ),
), ],
], ),
), ),
), ),
), _buildUserAvatar(context),
_buildUserAvatar(context), ],
], ),
), ),
), );
); },
}, openBuilder: (_, action) {
openBuilder: (_, action) { return MultiProvider(
return MultiProvider( providers: [
providers: [ Provider.value(value: context.read<LabelRepository>()),
Provider.value(value: context.read<LabelRepository>()), Provider.value(value: context.read<PaperlessDocumentsApi>()),
Provider.value(value: context.read<PaperlessDocumentsApi>()), Provider.value(value: context.read<CacheManager>()),
Provider.value(value: context.read<CacheManager>()), Provider.value(value: context.read<ApiVersion>()),
Provider.value(value: context.read<ApiVersion>()), if (context.read<ApiVersion>().hasMultiUserSupport)
Provider.value(value: context.read<UserRepository>()), Provider.value(value: context.read<UserRepository>()),
], ],
child: Provider( child: Provider(
create: (_) => DocumentSearchCubit( create: (_) => DocumentSearchCubit(
context.read(), context.read(),
context.read(), context.read(),
context.read(), context.read(),
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState) Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
.get(LocalUserAccount.current.id)!, .get(LocalUserAccount.current.id)!,
),
builder: (_, __) => const DocumentSearchPage(),
), ),
builder: (_, __) => const DocumentSearchPage(), );
), },
); ),
},
); );
} }
IconButton _buildUserAvatar(BuildContext context) { IconButton _buildUserAvatar(BuildContext context) {
return IconButton( return IconButton(
padding: const EdgeInsets.all(6),
icon: GlobalSettingsBuilder( icon: GlobalSettingsBuilder(
builder: (context, settings) { builder: (context, settings) {
return ValueListenableBuilder( return ValueListenableBuilder(

View File

@@ -21,6 +21,7 @@ class SliverSearchBar extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final currentUser = final currentUser =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser; Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser;
return SliverPersistentHeader( return SliverPersistentHeader(
floating: floating, floating: floating,
pinned: pinned, pinned: pinned,

View File

@@ -114,8 +114,9 @@ class DocumentsCubit extends HydratedCubit<DocumentsState> with DocumentPagingBl
void setViewType(ViewType viewType) { void setViewType(ViewType viewType) {
emit(state.copyWith(viewType: viewType)); emit(state.copyWith(viewType: viewType));
_userState.documentsPageViewType = viewType; _userState
_userState.save(); ..documentsPageViewType = viewType
..save();
} }
@override @override

View File

@@ -22,6 +22,7 @@ import 'package:paperless_mobile/features/saved_view/view/saved_view_list.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:sliver_tools/sliver_tools.dart';
class DocumentFilterIntent { class DocumentFilterIntent {
final DocumentFilter? filter; final DocumentFilter? filter;
@@ -107,7 +108,7 @@ class _DocumentsPageState extends State<DocumentsPage> with SingleTickerProvider
}, },
builder: (context, connectivityState) { builder: (context, connectivityState) {
return SafeArea( return SafeArea(
top: context.watch<DocumentsCubit>().state.selection.isEmpty, top: true,
child: Scaffold( child: Scaffold(
drawer: const AppDrawer(), drawer: const AppDrawer(),
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>( floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
@@ -160,13 +161,18 @@ class _DocumentsPageState extends State<DocumentsPage> with SingleTickerProvider
handle: searchBarHandle, handle: searchBarHandle,
sliver: BlocBuilder<DocumentsCubit, DocumentsState>( sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) { builder: (context, state) {
if (state.selection.isNotEmpty) { return AnimatedSwitcher(
// Show selection app bar when selection mode is active layoutBuilder: SliverAnimatedSwitcher.defaultLayoutBuilder,
return DocumentSelectionSliverAppBar( transitionBuilder: SliverAnimatedSwitcher.defaultTransitionBuilder,
state: state, child: state.selection.isEmpty
); ? const SliverSearchBar(floating: true)
} : DocumentSelectionSliverAppBar(
return const SliverSearchBar(floating: true); state: state,
),
duration: const Duration(
milliseconds: 250,
),
);
}, },
), ),
), ),

View File

@@ -35,7 +35,7 @@ class ViewTypeSelectionWidget extends StatelessWidget {
), // Ensures text is not split into two lines ), // Ensures text is not split into two lines
position: PopupMenuPosition.under, position: PopupMenuPosition.under,
initialValue: viewType, initialValue: viewType,
icon: Icon(icon), icon: Icon(icon, color: Theme.of(context).colorScheme.primary),
itemBuilder: (context) => [ itemBuilder: (context) => [
_buildViewTypeOption( _buildViewTypeOption(
context, context,

View File

@@ -10,13 +10,12 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/global/constants.dart'; import 'package:paperless_mobile/core/global/constants.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/core/service/file_description.dart'; import 'package:paperless_mobile/core/service/file_description.dart';
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
import 'package:paperless_mobile/features/document_scan/view/scanner_page.dart'; import 'package:paperless_mobile/features/document_scan/view/scanner_page.dart';
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart'; import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart';
import 'package:paperless_mobile/features/home/view/route_description.dart'; import 'package:paperless_mobile/features/home/view/route_description.dart';
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
@@ -139,25 +138,23 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
} }
return; return;
} }
if (!LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.add, PermissionTarget.document)) {
Fluttertoast.showToast(
msg: "You do not have the permissions to upload documents.",
);
return;
}
final fileDescription = FileDescription.fromPath(mediaFile.path); final fileDescription = FileDescription.fromPath(mediaFile.path);
if (await File(mediaFile.path).exists()) { if (await File(mediaFile.path).exists()) {
final bytes = File(mediaFile.path).readAsBytesSync(); final bytes = File(mediaFile.path).readAsBytesSync();
final result = await Navigator.push<DocumentUploadResult>( final result = await pushDocumentUploadPreparationPage(
context, context,
MaterialPageRoute( bytes: bytes,
builder: (context) => BlocProvider.value( filename: fileDescription.filename,
value: DocumentUploadCubit( title: fileDescription.filename,
context.read(), fileExtension: fileDescription.extension,
context.read(),
),
child: DocumentUploadPreparationPage(
fileBytes: bytes,
filename: fileDescription.filename,
title: fileDescription.filename,
fileExtension: fileDescription.extension,
),
),
),
); );
if (result?.success ?? false) { if (result?.success ?? false) {
await Fluttertoast.showToast( await Fluttertoast.showToast(

View File

@@ -5,12 +5,14 @@ class RouteDescription {
final Icon icon; final Icon icon;
final Icon selectedIcon; final Icon selectedIcon;
final Widget Function(Widget icon)? badgeBuilder; final Widget Function(Widget icon)? badgeBuilder;
final bool enabled;
RouteDescription({ RouteDescription({
required this.label, required this.label,
required this.icon, required this.icon,
required this.selectedIcon, required this.selectedIcon,
this.badgeBuilder, this.badgeBuilder,
this.enabled = true,
}); });
NavigationDestination toNavigationDestination() { NavigationDestination toNavigationDestination() {

View File

@@ -146,7 +146,6 @@ class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateM
labels: context.watch<LabelCubit>().state.correspondents, labels: context.watch<LabelCubit>().state.correspondents,
filterBuilder: (label) => DocumentFilter( filterBuilder: (label) => DocumentFilter(
correspondent: IdQueryParameter.fromId(label.id!), correspondent: IdQueryParameter.fromId(label.id!),
pageSize: label.documentCount ?? 0,
), ),
canEdit: LocalUserAccount.current.paperlessUser.hasPermission( canEdit: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.change, PermissionTarget.correspondent), PermissionAction.change, PermissionTarget.correspondent),
@@ -171,7 +170,6 @@ class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateM
labels: context.watch<LabelCubit>().state.documentTypes, labels: context.watch<LabelCubit>().state.documentTypes,
filterBuilder: (label) => DocumentFilter( filterBuilder: (label) => DocumentFilter(
documentType: IdQueryParameter.fromId(label.id!), documentType: IdQueryParameter.fromId(label.id!),
pageSize: label.documentCount ?? 0,
), ),
canEdit: LocalUserAccount.current.paperlessUser.hasPermission( canEdit: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.change, PermissionTarget.documentType), PermissionAction.change, PermissionTarget.documentType),
@@ -196,7 +194,6 @@ class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateM
labels: context.watch<LabelCubit>().state.tags, labels: context.watch<LabelCubit>().state.tags,
filterBuilder: (label) => DocumentFilter( filterBuilder: (label) => DocumentFilter(
tags: TagsQuery.ids(include: [label.id!]), tags: TagsQuery.ids(include: [label.id!]),
pageSize: label.documentCount ?? 0,
), ),
canEdit: LocalUserAccount.current.paperlessUser canEdit: LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.change, PermissionTarget.tag), .hasPermission(PermissionAction.change, PermissionTarget.tag),
@@ -231,7 +228,6 @@ class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateM
onEdit: _openEditStoragePathPage, onEdit: _openEditStoragePathPage,
filterBuilder: (label) => DocumentFilter( filterBuilder: (label) => DocumentFilter(
storagePath: IdQueryParameter.fromId(label.id!), storagePath: IdQueryParameter.fromId(label.id!),
pageSize: label.documentCount ?? 0,
), ),
canEdit: LocalUserAccount.current.paperlessUser.hasPermission( canEdit: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.change, PermissionTarget.storagePath), PermissionAction.change, PermissionTarget.storagePath),

View File

@@ -1,13 +1,10 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive_flutter/adapters.dart'; import 'package:hive_flutter/adapters.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart'; import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart'; import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
@@ -88,39 +85,38 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
return; return;
} }
} }
await withEncryptedBox<UserCredentials, void>(HiveBoxes.localUserCredentials,
(credentialsBox) async {
if (!credentialsBox.containsKey(localUserId)) {
await credentialsBox.close();
debugPrint("Invalid authentication for $localUserId");
return;
}
final credentials = credentialsBox.get(localUserId);
await _resetExternalState();
final credentialsBox = await _getUserCredentialsBox(); _sessionManager.updateSettings(
if (!credentialsBox.containsKey(localUserId)) { authToken: credentials!.token,
await credentialsBox.close(); clientCertificate: credentials.clientCertificate,
debugPrint("Invalid authentication for $localUserId"); baseUrl: account.serverUrl,
return; );
}
final credentials = credentialsBox.get(localUserId);
await credentialsBox.close();
await _resetExternalState(); globalSettings.currentLoggedInUser = localUserId;
await globalSettings.save();
_sessionManager.updateSettings( final apiVersion = await _getApiVersion(_sessionManager.client);
authToken: credentials!.token,
clientCertificate: credentials.clientCertificate,
baseUrl: account.serverUrl,
);
globalSettings.currentLoggedInUser = localUserId; await _updateRemoteUser(
await globalSettings.save(); _sessionManager,
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).get(localUserId)!,
apiVersion,
);
final apiVersion = await _getApiVersion(_sessionManager.client); emit(AuthenticationState.authenticated(
localUserId: localUserId,
await _updateRemoteUser( apiVersion: apiVersion,
_sessionManager, ));
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).get(localUserId)!, });
apiVersion,
);
emit(AuthenticationState.authenticated(
localUserId: localUserId,
apiVersion: apiVersion,
));
} }
Future<String> addAccount({ Future<String> addAccount({
@@ -146,14 +142,14 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
Future<void> removeAccount(String userId) async { Future<void> removeAccount(String userId) async {
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!; final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
final userAccountBox = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount); final userAccountBox = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
final userCredentialsBox = await _getUserCredentialsBox();
final userAppStateBox = Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState); final userAppStateBox = Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
final currentUser = globalSettings.currentLoggedInUser; final currentUser = globalSettings.currentLoggedInUser;
await userAccountBox.delete(userId); await userAccountBox.delete(userId);
await userAppStateBox.delete(userId); await userAppStateBox.delete(userId);
await userCredentialsBox.delete(userId); await withEncryptedBox(HiveBoxes.localUserCredentials, (box) {
await userCredentialsBox.close(); box.delete(userId);
});
if (currentUser == userId) { if (currentUser == userId) {
return logout(); return logout();
@@ -182,9 +178,10 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
} }
} }
final userCredentialsBox = await _getUserCredentialsBox(); final authentication = await withEncryptedBox<UserCredentials, UserCredentials>(
final authentication = userCredentialsBox.get(globalSettings.currentLoggedInUser!); HiveBoxes.localUserCredentials, (box) {
await userCredentialsBox.close(); return box.get(globalSettings.currentLoggedInUser!);
});
if (authentication == null) { if (authentication == null) {
throw Exception( throw Exception(
@@ -218,28 +215,6 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
emit(const AuthenticationState.unauthenticated()); emit(const AuthenticationState.unauthenticated());
} }
Future<Uint8List> _getEncryptedBoxKey() async {
const secureStorage = FlutterSecureStorage();
if (!await secureStorage.containsKey(key: 'key')) {
final key = Hive.generateSecureKey();
await secureStorage.write(
key: 'key',
value: base64UrlEncode(key),
);
}
final key = (await secureStorage.read(key: 'key'))!;
return base64Decode(key);
}
Future<Box<UserCredentials>> _getUserCredentialsBox() async {
final keyBytes = await _getEncryptedBoxKey();
return Hive.openBox<UserCredentials>(
HiveBoxes.localUserCredentials,
encryptionCipher: HiveAesCipher(keyBytes),
);
}
Future<void> _resetExternalState() async { Future<void> _resetExternalState() async {
_sessionManager.resetSettings(); _sessionManager.resetSettings();
await HydratedBloc.storage.clear(); await HydratedBloc.storage.clear();
@@ -305,15 +280,15 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
); );
// Save credentials in encrypted box // Save credentials in encrypted box
final userCredentialsBox = await _getUserCredentialsBox(); await withEncryptedBox(HiveBoxes.localUserCredentials, (box) async {
await userCredentialsBox.put( await box.put(
localUserId, localUserId,
UserCredentials( UserCredentials(
token: token, token: token,
clientCertificate: clientCert, clientCertificate: clientCert,
), ),
); );
userCredentialsBox.close(); });
return serverUser.id; return serverUser.id;
} }

View File

@@ -4,6 +4,7 @@ import 'package:paperless_mobile/features/settings/view/pages/application_settin
import 'package:paperless_mobile/features/settings/view/pages/security_settings_page.dart'; import 'package:paperless_mobile/features/settings/view/pages/security_settings_page.dart';
import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart'; import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:provider/provider.dart';
class SettingsPage extends StatelessWidget { class SettingsPage extends StatelessWidget {
const SettingsPage({super.key}); const SettingsPage({super.key});
@@ -25,10 +26,21 @@ class SettingsPage extends StatelessWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
subtitle: FutureBuilder<PaperlessServerInformationModel>( subtitle: FutureBuilder<PaperlessServerInformationModel>(
future: context.read<PaperlessServerStatsApi>().getServerInformation(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) {
return Text(
"Something went wrong while retrieving server data.", //TODO: INTL
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(color: Theme.of(context).colorScheme.error),
textAlign: TextAlign.center,
);
}
if (!snapshot.hasData) { if (!snapshot.hasData) {
return Text( return Text(
"Loading server information...", "Loading server information...", //TODO: INTL
style: Theme.of(context).textTheme.labelSmall, style: Theme.of(context).textTheme.labelSmall,
textAlign: TextAlign.center, textAlign: TextAlign.center,
); );
@@ -39,7 +51,9 @@ class SettingsPage extends StatelessWidget {
' ' + ' ' +
serverData.version.toString() + serverData.version.toString() +
' (API v${serverData.apiVersion})', ' (API v${serverData.apiVersion})',
style: Theme.of(context).textTheme.labelSmall, style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Theme.of(context).colorScheme.secondary,
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
); );
}, },

View File

@@ -11,4 +11,20 @@ extension UserPermissionExtension on UserModel {
v2: (_) => true, v2: (_) => true,
); );
} }
bool hasPermissions(List<PermissionAction> actions, List<PermissionTarget> targets) {
return map(
v3: (user) {
final permissions = [
for (var action in actions)
for (var target in targets) [action, target].join("_")
];
return permissions.every((requestedPermission) =>
user.userPermissions.contains(requestedPermission) ||
user.inheritedPermissions
.any((element) => element.split(".").last == requestedPermission));
},
v2: (_) => true,
);
}
} }

View File

@@ -1417,6 +1417,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.99" version: "0.0.99"
sliver_tools:
dependency: "direct main"
description:
name: sliver_tools
sha256: ccdc502098a8bfa07b3ec582c282620031481300035584e1bb3aca296a505e8c
url: "https://pub.dev"
source: hosted
version: "0.2.10"
source_gen: source_gen:
dependency: transitive dependency: transitive
description: description:
@@ -1705,6 +1713,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
webview_flutter:
dependency: "direct main"
description:
name: webview_flutter
sha256: "5604dac1178680a34fbe4a08c7b69ec42cca6601dc300009ec9ff69bef284cc2"
url: "https://pub.dev"
source: hosted
version: "4.2.1"
webview_flutter_android:
dependency: transitive
description:
name: webview_flutter_android
sha256: "57a22c86065375c1598b57224f92d6008141be0c877c64100de8bfb6f71083d8"
url: "https://pub.dev"
source: hosted
version: "3.7.1"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
sha256: "656e2aeaef318900fffd21468b6ddc7958c7092a642f0e7220bac328b70d4a81"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
webview_flutter_wkwebview:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: "6bbc6ade302b842999b27cbaa7171241c273deea8a9c73f92ceb3d811c767de2"
url: "https://pub.dev"
source: hosted
version: "3.4.4"
win32: win32:
dependency: transitive dependency: transitive
description: description:

View File

@@ -92,6 +92,8 @@ dependencies:
animations: ^2.0.7 animations: ^2.0.7
hive_flutter: ^1.1.0 hive_flutter: ^1.1.0
flutter_secure_storage: ^8.0.0 flutter_secure_storage: ^8.0.0
sliver_tools: ^0.2.10
webview_flutter: ^4.2.1
dependency_overrides: dependency_overrides:
intl: ^0.18.0 intl: ^0.18.0