mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-06 01:15:44 -06:00
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:
35
lib/core/config/hive/hive_extensions.dart
Normal file
35
lib/core/config/hive/hive_extensions.dart
Normal 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);
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -240,7 +240,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
const DocumentPermissionsWidget(),
|
||||
DocumentPermissionsWidget(
|
||||
document: state.document,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
40
pubspec.lock
40
pubspec.lock
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user