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/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_scan/view/scanner_page.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_upload/cubit/document_upload_cubit.dart';
@@ -38,6 +37,7 @@ import 'package:provider/provider.dart';
Future<void> pushDocumentSearchPage(BuildContext context) {
final currentUser =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser;
final userRepo = context.read<UserRepository>();
return Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => MultiProvider(
@@ -46,6 +46,7 @@ Future<void> pushDocumentSearchPage(BuildContext context) {
Provider.value(value: context.read<PaperlessDocumentsApi>()),
Provider.value(value: context.read<DocumentChangedNotifier>()),
Provider.value(value: context.read<CacheManager>()),
Provider.value(value: userRepo),
],
builder: (context, _) {
return BlocProvider(

View File

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

View File

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

View File

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

View File

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

View File

@@ -114,8 +114,9 @@ class DocumentsCubit extends HydratedCubit<DocumentsState> with DocumentPagingBl
void setViewType(ViewType viewType) {
emit(state.copyWith(viewType: viewType));
_userState.documentsPageViewType = viewType;
_userState.save();
_userState
..documentsPageViewType = viewType
..save();
}
@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/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:sliver_tools/sliver_tools.dart';
class DocumentFilterIntent {
final DocumentFilter? filter;
@@ -107,7 +108,7 @@ class _DocumentsPageState extends State<DocumentsPage> with SingleTickerProvider
},
builder: (context, connectivityState) {
return SafeArea(
top: context.watch<DocumentsCubit>().state.selection.isEmpty,
top: true,
child: Scaffold(
drawer: const AppDrawer(),
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
@@ -160,13 +161,18 @@ class _DocumentsPageState extends State<DocumentsPage> with SingleTickerProvider
handle: searchBarHandle,
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
if (state.selection.isNotEmpty) {
// Show selection app bar when selection mode is active
return DocumentSelectionSliverAppBar(
state: state,
);
}
return const SliverSearchBar(floating: true);
return AnimatedSwitcher(
layoutBuilder: SliverAnimatedSwitcher.defaultLayoutBuilder,
transitionBuilder: SliverAnimatedSwitcher.defaultTransitionBuilder,
child: state.selection.isEmpty
? const SliverSearchBar(floating: true)
: DocumentSelectionSliverAppBar(
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
position: PopupMenuPosition.under,
initialValue: viewType,
icon: Icon(icon),
icon: Icon(icon, color: Theme.of(context).colorScheme.primary),
itemBuilder: (context) => [
_buildViewTypeOption(
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/database/tables/local_user_account.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/saved_view_repository.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/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/home/view/route_description.dart';
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
@@ -139,25 +138,23 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
}
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);
if (await File(mediaFile.path).exists()) {
final bytes = File(mediaFile.path).readAsBytesSync();
final result = await Navigator.push<DocumentUploadResult>(
final result = await pushDocumentUploadPreparationPage(
context,
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: DocumentUploadCubit(
context.read(),
context.read(),
),
child: DocumentUploadPreparationPage(
fileBytes: bytes,
filename: fileDescription.filename,
title: fileDescription.filename,
fileExtension: fileDescription.extension,
),
),
),
bytes: bytes,
filename: fileDescription.filename,
title: fileDescription.filename,
fileExtension: fileDescription.extension,
);
if (result?.success ?? false) {
await Fluttertoast.showToast(

View File

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

View File

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

View File

@@ -1,13 +1,10 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive_flutter/adapters.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_extensions.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_app_state.dart';
@@ -88,39 +85,38 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
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();
if (!credentialsBox.containsKey(localUserId)) {
await credentialsBox.close();
debugPrint("Invalid authentication for $localUserId");
return;
}
final credentials = credentialsBox.get(localUserId);
await credentialsBox.close();
_sessionManager.updateSettings(
authToken: credentials!.token,
clientCertificate: credentials.clientCertificate,
baseUrl: account.serverUrl,
);
await _resetExternalState();
globalSettings.currentLoggedInUser = localUserId;
await globalSettings.save();
_sessionManager.updateSettings(
authToken: credentials!.token,
clientCertificate: credentials.clientCertificate,
baseUrl: account.serverUrl,
);
final apiVersion = await _getApiVersion(_sessionManager.client);
globalSettings.currentLoggedInUser = localUserId;
await globalSettings.save();
await _updateRemoteUser(
_sessionManager,
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).get(localUserId)!,
apiVersion,
);
final apiVersion = await _getApiVersion(_sessionManager.client);
await _updateRemoteUser(
_sessionManager,
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).get(localUserId)!,
apiVersion,
);
emit(AuthenticationState.authenticated(
localUserId: localUserId,
apiVersion: apiVersion,
));
emit(AuthenticationState.authenticated(
localUserId: localUserId,
apiVersion: apiVersion,
));
});
}
Future<String> addAccount({
@@ -146,14 +142,14 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
Future<void> removeAccount(String userId) async {
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
final userAccountBox = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
final userCredentialsBox = await _getUserCredentialsBox();
final userAppStateBox = Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
final currentUser = globalSettings.currentLoggedInUser;
await userAccountBox.delete(userId);
await userAppStateBox.delete(userId);
await userCredentialsBox.delete(userId);
await userCredentialsBox.close();
await withEncryptedBox(HiveBoxes.localUserCredentials, (box) {
box.delete(userId);
});
if (currentUser == userId) {
return logout();
@@ -182,9 +178,10 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
}
}
final userCredentialsBox = await _getUserCredentialsBox();
final authentication = userCredentialsBox.get(globalSettings.currentLoggedInUser!);
await userCredentialsBox.close();
final authentication = await withEncryptedBox<UserCredentials, UserCredentials>(
HiveBoxes.localUserCredentials, (box) {
return box.get(globalSettings.currentLoggedInUser!);
});
if (authentication == null) {
throw Exception(
@@ -218,28 +215,6 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
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 {
_sessionManager.resetSettings();
await HydratedBloc.storage.clear();
@@ -305,15 +280,15 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
);
// Save credentials in encrypted box
final userCredentialsBox = await _getUserCredentialsBox();
await userCredentialsBox.put(
localUserId,
UserCredentials(
token: token,
clientCertificate: clientCert,
),
);
userCredentialsBox.close();
await withEncryptedBox(HiveBoxes.localUserCredentials, (box) async {
await box.put(
localUserId,
UserCredentials(
token: token,
clientCertificate: clientCert,
),
);
});
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/widgets/user_settings_builder.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:provider/provider.dart';
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
@@ -25,10 +26,21 @@ class SettingsPage extends StatelessWidget {
textAlign: TextAlign.center,
),
subtitle: FutureBuilder<PaperlessServerInformationModel>(
future: context.read<PaperlessServerStatsApi>().getServerInformation(),
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) {
return Text(
"Loading server information...",
"Loading server information...", //TODO: INTL
style: Theme.of(context).textTheme.labelSmall,
textAlign: TextAlign.center,
);
@@ -39,7 +51,9 @@ class SettingsPage extends StatelessWidget {
' ' +
serverData.version.toString() +
' (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,
);
},

View File

@@ -11,4 +11,20 @@ extension UserPermissionExtension on UserModel {
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
source: sdk
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:
dependency: transitive
description:
@@ -1705,6 +1713,38 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:

View File

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