move auth feature from container to profile
This commit is contained in:
@@ -17,10 +17,13 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:weblibre/core/providers/app_state.dart';
|
import 'package:weblibre/core/providers/app_state.dart';
|
||||||
import 'package:weblibre/core/routing/routes.dart';
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/providers/profile_auth.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/onboarding.dart';
|
import 'package:weblibre/features/user/domain/repositories/onboarding.dart';
|
||||||
|
|
||||||
part 'router.g.dart';
|
part 'router.g.dart';
|
||||||
@@ -30,6 +33,7 @@ Future<GoRouter> router(Ref ref) async {
|
|||||||
ref.watch(appStateKeyProvider); //Rebuild router on key changes
|
ref.watch(appStateKeyProvider); //Rebuild router on key changes
|
||||||
|
|
||||||
final onboardingRepository = ref.read(onboardingRepositoryProvider.notifier);
|
final onboardingRepository = ref.read(onboardingRepositoryProvider.notifier);
|
||||||
|
unawaited(ref.read(profileAuthStateProvider.notifier).bootstrapFromProfile());
|
||||||
|
|
||||||
String? initialLocation;
|
String? initialLocation;
|
||||||
|
|
||||||
@@ -46,10 +50,32 @@ Future<GoRouter> router(Ref ref) async {
|
|||||||
initialLocation = route.location;
|
initialLocation = route.location;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final profileAuthRefreshListenable = ref.watch(profileAuthProvider);
|
||||||
|
|
||||||
return GoRouter(
|
return GoRouter(
|
||||||
debugLogDiagnostics: true,
|
debugLogDiagnostics: true,
|
||||||
routes: $appRoutes,
|
routes: $appRoutes,
|
||||||
initialLocation: initialLocation ?? const BrowserRoute().location,
|
initialLocation: initialLocation ?? const LockRoute().location,
|
||||||
|
refreshListenable: profileAuthRefreshListenable,
|
||||||
|
redirect: (context, state) {
|
||||||
|
final authenticated = ref.read(profileAuthStateProvider);
|
||||||
|
final currentTopRouteName = state.topRoute?.name;
|
||||||
|
final isOnLockRoute = currentTopRouteName == LockRoute.name;
|
||||||
|
final isOnOnboarding = currentTopRouteName == OnboardingRoute.name;
|
||||||
|
|
||||||
|
// Don't redirect during onboarding
|
||||||
|
if (isOnOnboarding) return null;
|
||||||
|
|
||||||
|
if (!authenticated && !isOnLockRoute) {
|
||||||
|
return const LockRoute().location;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authenticated && isOnLockRoute) {
|
||||||
|
return const BrowserRoute().location;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class RouterProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$routerHash() => r'cbaa7e982114942303574f9573f4a75b79955583';
|
String _$routerHash() => r'4402ca2d7061945c395f3963d6bde29be8999f96';
|
||||||
|
|
||||||
@ProviderFor(CurrentTopRoute)
|
@ProviderFor(CurrentTopRoute)
|
||||||
final currentTopRouteProvider = CurrentTopRouteProvider._();
|
final currentTopRouteProvider = CurrentTopRouteProvider._();
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ import 'package:weblibre/features/user/domain/presentation/screens/profile_backu
|
|||||||
import 'package:weblibre/features/user/domain/presentation/screens/profile_edit.dart';
|
import 'package:weblibre/features/user/domain/presentation/screens/profile_edit.dart';
|
||||||
import 'package:weblibre/features/user/domain/presentation/screens/profile_list.dart';
|
import 'package:weblibre/features/user/domain/presentation/screens/profile_list.dart';
|
||||||
import 'package:weblibre/features/user/domain/presentation/screens/profile_restore.dart';
|
import 'package:weblibre/features/user/domain/presentation/screens/profile_restore.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/presentation/widgets/auth_gate.dart';
|
||||||
import 'package:weblibre/features/web_feed/presentation/add_feed_dialog.dart';
|
import 'package:weblibre/features/web_feed/presentation/add_feed_dialog.dart';
|
||||||
import 'package:weblibre/features/web_feed/presentation/screens/feed_article.dart';
|
import 'package:weblibre/features/web_feed/presentation/screens/feed_article.dart';
|
||||||
import 'package:weblibre/features/web_feed/presentation/screens/feed_article_list.dart';
|
import 'package:weblibre/features/web_feed/presentation/screens/feed_article_list.dart';
|
||||||
@@ -103,10 +104,13 @@ class AboutRoute extends GoRouteData with $AboutRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@TypedGoRoute<OnboardingRoute>(
|
@TypedGoRoute<OnboardingRoute>(
|
||||||
name: 'OnboardingRoute',
|
name: OnboardingRoute.name,
|
||||||
path: '/onboarding/:currentRevision/:targetRevision',
|
path: '${OnboardingRoute.pathPrefix}/:currentRevision/:targetRevision',
|
||||||
)
|
)
|
||||||
class OnboardingRoute extends GoRouteData with $OnboardingRoute {
|
class OnboardingRoute extends GoRouteData with $OnboardingRoute {
|
||||||
|
static const name = 'OnboardingRoute';
|
||||||
|
static const pathPrefix = '/onboarding';
|
||||||
|
|
||||||
final int currentRevision;
|
final int currentRevision;
|
||||||
final int targetRevision;
|
final int targetRevision;
|
||||||
|
|
||||||
@@ -123,3 +127,16 @@ class OnboardingRoute extends GoRouteData with $OnboardingRoute {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TypedGoRoute<LockRoute>(name: LockRoute.name, path: LockRoute.path)
|
||||||
|
class LockRoute extends GoRouteData with $LockRoute {
|
||||||
|
static const name = 'LockRoute';
|
||||||
|
static const path = '/lock';
|
||||||
|
|
||||||
|
const LockRoute();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
|
return const LockScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ part of 'routes.dart';
|
|||||||
List<RouteBase> get $appRoutes => [
|
List<RouteBase> get $appRoutes => [
|
||||||
$aboutRoute,
|
$aboutRoute,
|
||||||
$onboardingRoute,
|
$onboardingRoute,
|
||||||
|
$lockRoute,
|
||||||
$bangMenuRoute,
|
$bangMenuRoute,
|
||||||
$bookmarksRoute,
|
$bookmarksRoute,
|
||||||
$browserRoute,
|
$browserRoute,
|
||||||
@@ -78,6 +79,32 @@ mixin $OnboardingRoute on GoRouteData {
|
|||||||
void replace(BuildContext context) => context.replace(location);
|
void replace(BuildContext context) => context.replace(location);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RouteBase get $lockRoute => GoRouteData.$route(
|
||||||
|
path: '/lock',
|
||||||
|
name: 'LockRoute',
|
||||||
|
factory: $LockRoute._fromState,
|
||||||
|
);
|
||||||
|
|
||||||
|
mixin $LockRoute on GoRouteData {
|
||||||
|
static LockRoute _fromState(GoRouterState state) => const LockRoute();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get location => GoRouteData.$location('/lock');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void go(BuildContext context) => context.go(location);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void pushReplacement(BuildContext context) =>
|
||||||
|
context.pushReplacement(location);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void replace(BuildContext context) => context.replace(location);
|
||||||
|
}
|
||||||
|
|
||||||
RouteBase get $bangMenuRoute => GoRouteData.$route(
|
RouteBase get $bangMenuRoute => GoRouteData.$route(
|
||||||
path: '/bangs',
|
path: '/bangs',
|
||||||
name: 'BangRoute',
|
name: 'BangRoute',
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import 'package:fast_equatable/fast_equatable.dart';
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:uuid/uuid_value.dart';
|
import 'package:uuid/uuid_value.dart';
|
||||||
import 'package:weblibre/core/uuid.dart';
|
import 'package:weblibre/core/uuid.dart';
|
||||||
|
import 'package:weblibre/features/user/data/models/auth_settings.dart';
|
||||||
|
|
||||||
part 'profile.g.dart';
|
part 'profile.g.dart';
|
||||||
|
|
||||||
@@ -31,19 +32,31 @@ class Profile with FastEquatable {
|
|||||||
@CopyWithField(immutable: true)
|
@CopyWithField(immutable: true)
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
|
final AuthSettings authSettings;
|
||||||
|
|
||||||
late final uuidValue = UuidValue.fromString(id);
|
late final uuidValue = UuidValue.fromString(id);
|
||||||
|
|
||||||
static String getNewProfileId() => uuid.v7();
|
static String getNewProfileId() => uuid.v7();
|
||||||
|
|
||||||
Profile({required this.id, required this.name});
|
Profile({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
AuthSettings? authSettings,
|
||||||
|
}) : authSettings = authSettings ?? AuthSettings.withDefaults();
|
||||||
|
|
||||||
factory Profile.create({required String name}) {
|
factory Profile.create({
|
||||||
return Profile(id: getNewProfileId(), name: name);
|
required String name,
|
||||||
|
AuthSettings? authSettings,
|
||||||
|
}) {
|
||||||
|
return Profile(
|
||||||
|
id: getNewProfileId(),
|
||||||
|
name: name,
|
||||||
|
authSettings: authSettings,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get hashParameters => [id, name];
|
List<Object?> get hashParameters => [id, name, authSettings];
|
||||||
|
|
||||||
factory Profile.fromJson(Map<String, dynamic> json) =>
|
factory Profile.fromJson(Map<String, dynamic> json) =>
|
||||||
_$ProfileFromJson(json);
|
_$ProfileFromJson(json);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ part of 'profile.dart';
|
|||||||
abstract class _$ProfileCWProxy {
|
abstract class _$ProfileCWProxy {
|
||||||
Profile name(String name);
|
Profile name(String name);
|
||||||
|
|
||||||
|
Profile authSettings(AuthSettings? authSettings);
|
||||||
|
|
||||||
/// Creates a new instance with the provided field values.
|
/// Creates a new instance with the provided field values.
|
||||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `Profile(...).copyWith.fieldName(value)`.
|
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `Profile(...).copyWith.fieldName(value)`.
|
||||||
///
|
///
|
||||||
@@ -16,7 +18,7 @@ abstract class _$ProfileCWProxy {
|
|||||||
/// ```dart
|
/// ```dart
|
||||||
/// Profile(...).copyWith(id: 12, name: "My name")
|
/// Profile(...).copyWith(id: 12, name: "My name")
|
||||||
/// ```
|
/// ```
|
||||||
Profile call({String name});
|
Profile call({String name, AuthSettings? authSettings});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Callable proxy for `copyWith` functionality.
|
/// Callable proxy for `copyWith` functionality.
|
||||||
@@ -29,6 +31,10 @@ class _$ProfileCWProxyImpl implements _$ProfileCWProxy {
|
|||||||
@override
|
@override
|
||||||
Profile name(String name) => call(name: name);
|
Profile name(String name) => call(name: name);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Profile authSettings(AuthSettings? authSettings) =>
|
||||||
|
call(authSettings: authSettings);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
/// Creates a new instance with the provided field values.
|
/// Creates a new instance with the provided field values.
|
||||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `Profile(...).copyWith.fieldName(value)`.
|
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `Profile(...).copyWith.fieldName(value)`.
|
||||||
@@ -37,13 +43,20 @@ class _$ProfileCWProxyImpl implements _$ProfileCWProxy {
|
|||||||
/// ```dart
|
/// ```dart
|
||||||
/// Profile(...).copyWith(id: 12, name: "My name")
|
/// Profile(...).copyWith(id: 12, name: "My name")
|
||||||
/// ```
|
/// ```
|
||||||
Profile call({Object? name = const $CopyWithPlaceholder()}) {
|
Profile call({
|
||||||
|
Object? name = const $CopyWithPlaceholder(),
|
||||||
|
Object? authSettings = const $CopyWithPlaceholder(),
|
||||||
|
}) {
|
||||||
return Profile(
|
return Profile(
|
||||||
id: _value.id,
|
id: _value.id,
|
||||||
name: name == const $CopyWithPlaceholder() || name == null
|
name: name == const $CopyWithPlaceholder() || name == null
|
||||||
? _value.name
|
? _value.name
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
: name as String,
|
: name as String,
|
||||||
|
authSettings: authSettings == const $CopyWithPlaceholder()
|
||||||
|
? _value.authSettings
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: authSettings as AuthSettings?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,10 +72,16 @@ extension $ProfileCopyWith on Profile {
|
|||||||
// JsonSerializableGenerator
|
// JsonSerializableGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
Profile _$ProfileFromJson(Map<String, dynamic> json) =>
|
Profile _$ProfileFromJson(Map<String, dynamic> json) => Profile(
|
||||||
Profile(id: json['id'] as String, name: json['name'] as String);
|
id: json['id'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
authSettings: json['authSettings'] == null
|
||||||
|
? null
|
||||||
|
: AuthSettings.fromJson(json['authSettings'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$ProfileToJson(Profile instance) => <String, dynamic>{
|
Map<String, dynamic> _$ProfileToJson(Profile instance) => <String, dynamic>{
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
|
'authSettings': instance.authSettings.toJson(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -273,16 +273,6 @@ class TabRepository extends _$TabRepository {
|
|||||||
if (!ref.mounted) return false;
|
if (!ref.mounted) return false;
|
||||||
|
|
||||||
if (containerData != null) {
|
if (containerData != null) {
|
||||||
if (containerData.metadata.authSettings.authenticationRequired) {
|
|
||||||
// ignore: only_use_keep_alive_inside_keep_alive
|
|
||||||
if (containerData.id != ref.read(selectedContainerProvider)) {
|
|
||||||
logger.w(
|
|
||||||
'Tried to open authenticated tab $tabId but container not selected',
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (containerData.metadata.useProxy) {
|
if (containerData.metadata.useProxy) {
|
||||||
final proxyPluginHealthy = await GeckoContainerProxyService()
|
final proxyPluginHealthy = await GeckoContainerProxyService()
|
||||||
.healthcheck();
|
.healthcheck();
|
||||||
@@ -380,20 +370,15 @@ class TabRepository extends _$TabRepository {
|
|||||||
|
|
||||||
if (!ref.mounted) return;
|
if (!ref.mounted) return;
|
||||||
|
|
||||||
//We only take containers without authentication!
|
|
||||||
final availableContainers = await ref
|
final availableContainers = await ref
|
||||||
.read(containerRepositoryProvider.notifier)
|
.read(containerRepositoryProvider.notifier)
|
||||||
.getAllContainersWithCount();
|
.getAllContainersWithCount();
|
||||||
|
|
||||||
final nextAvailableContainerUnauthenticated = availableContainers
|
final nextAvailableContainer = availableContainers.firstOrNull;
|
||||||
.firstWhereOrNull(
|
|
||||||
(container) =>
|
|
||||||
container.metadata.authSettings.authenticationRequired == false,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!ref.mounted) return;
|
if (!ref.mounted) return;
|
||||||
|
|
||||||
final nextContainerTabs = await nextAvailableContainerUnauthenticated
|
final nextContainerTabs = await nextAvailableContainer
|
||||||
.mapNotNull(
|
.mapNotNull(
|
||||||
(container) => ref
|
(container) => ref
|
||||||
.read(containerRepositoryProvider.notifier)
|
.read(containerRepositoryProvider.notifier)
|
||||||
@@ -404,15 +389,6 @@ class TabRepository extends _$TabRepository {
|
|||||||
if (nextContainerTabs.isNotEmpty) {
|
if (nextContainerTabs.isNotEmpty) {
|
||||||
return _tabsService.selectTab(tabId: nextContainerTabs!.first);
|
return _tabsService.selectTab(tabId: nextContainerTabs!.first);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ref.mounted &&
|
|
||||||
availableContainers.any(
|
|
||||||
(container) => container.metadata.authSettings.authenticationRequired,
|
|
||||||
)) {
|
|
||||||
//Last resort push new tab to avoid any authenticated tab is selected
|
|
||||||
// ignore: avoid_redundant_argument_values
|
|
||||||
await addTab(selectTab: true, private: false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> closeTab(String tabId) async {
|
Future<void> closeTab(String tabId) async {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$tabRepositoryHash() => r'db35cef59703f15f1b099b6adb42f7623bec612f';
|
String _$tabRepositoryHash() => r'f0ff983df4115bfed7ff3dbc8994592bc46a1daa';
|
||||||
|
|
||||||
abstract class _$TabRepository extends $Notifier<void> {
|
abstract class _$TabRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
+2
-11
@@ -25,7 +25,6 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
|
||||||
import 'package:weblibre/presentation/icons/weblibre_icons.dart';
|
import 'package:weblibre/presentation/icons/weblibre_icons.dart';
|
||||||
import 'package:weblibre/utils/form_validators.dart';
|
import 'package:weblibre/utils/form_validators.dart';
|
||||||
@@ -72,16 +71,8 @@ class OpenSharedContent extends HookConsumerWidget {
|
|||||||
child: ContainerChips(
|
child: ContainerChips(
|
||||||
displayMenu: false,
|
displayMenu: false,
|
||||||
selectedContainer: selectedContainer.value,
|
selectedContainer: selectedContainer.value,
|
||||||
onSelected: (container) async {
|
onSelected: (container) {
|
||||||
if (container != null) {
|
selectedContainer.value = container;
|
||||||
if (await ref
|
|
||||||
.read(selectedContainerProvider.notifier)
|
|
||||||
.authenticateContainer(container)) {
|
|
||||||
selectedContainer.value = container;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedContainer.value = container;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onDeleted: (container) {
|
onDeleted: (container) {
|
||||||
selectedContainer.value = null;
|
selectedContainer.value = null;
|
||||||
|
|||||||
+5
@@ -53,6 +53,7 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/co
|
|||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/share_intent/domain/entities/shared_content.dart';
|
import 'package:weblibre/features/share_intent/domain/entities/shared_content.dart';
|
||||||
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/providers/profile_auth.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/cache.dart';
|
import 'package:weblibre/features/user/domain/repositories/cache.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
import 'package:weblibre/features/user/domain/services/local_authentication.dart';
|
import 'package:weblibre/features/user/domain/services/local_authentication.dart';
|
||||||
@@ -553,6 +554,10 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
|||||||
_timerPaused = false;
|
_timerPaused = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unawaited(
|
||||||
|
ref.read(profileAuthStateProvider.notifier).revalidateAfterResume(),
|
||||||
|
);
|
||||||
|
|
||||||
if (_suggestionCountTime != null &&
|
if (_suggestionCountTime != null &&
|
||||||
DateTime.now().difference(_suggestionCountTime!) >
|
DateTime.now().difference(_suggestionCountTime!) >
|
||||||
widget.suggestionTimeout) {
|
widget.suggestionTimeout) {
|
||||||
|
|||||||
@@ -338,19 +338,8 @@ class SearchScreen extends HookConsumerWidget {
|
|||||||
height: 48,
|
height: 48,
|
||||||
child: ContainerChips(
|
child: ContainerChips(
|
||||||
selectedContainer: selectedContainer.value,
|
selectedContainer: selectedContainer.value,
|
||||||
onSelected: (container) async {
|
onSelected: (container) {
|
||||||
if (container != null) {
|
selectedContainer.value = container;
|
||||||
if (await ref
|
|
||||||
.read(
|
|
||||||
selectedContainerProvider
|
|
||||||
.notifier,
|
|
||||||
)
|
|
||||||
.authenticateContainer(container)) {
|
|
||||||
selectedContainer.value = container;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedContainer.value = container;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onDeleted: (container) {
|
onDeleted: (container) {
|
||||||
selectedContainer.value = null;
|
selectedContainer.value = null;
|
||||||
|
|||||||
+2
-11
@@ -101,7 +101,6 @@ class TabSearch extends HookConsumerWidget {
|
|||||||
.then((containers) {
|
.then((containers) {
|
||||||
return containers.firstWhereOrNull(
|
return containers.firstWhereOrNull(
|
||||||
(container) =>
|
(container) =>
|
||||||
!container.metadata.authSettings.authenticationRequired &&
|
|
||||||
containerIdsWithResults.value.containsKey(container.id),
|
containerIdsWithResults.value.containsKey(container.id),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -145,16 +144,8 @@ class TabSearch extends HookConsumerWidget {
|
|||||||
showUnassignedChip: containerIdsWithResults.value.containsKey(
|
showUnassignedChip: containerIdsWithResults.value.containsKey(
|
||||||
null,
|
null,
|
||||||
),
|
),
|
||||||
onSelected: (container) async {
|
onSelected: (container) {
|
||||||
if (container != null) {
|
selectedContainer.value = container;
|
||||||
if (await ref
|
|
||||||
.read(selectedContainerProvider.notifier)
|
|
||||||
.authenticateContainer(container)) {
|
|
||||||
selectedContainer.value = container;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedContainer.value = container;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onDeleted: (container) {
|
onDeleted: (container) {
|
||||||
selectedContainer.value = null;
|
selectedContainer.value = null;
|
||||||
|
|||||||
@@ -26,49 +26,12 @@ import 'package:weblibre/data/database/converters/icon_data.dart';
|
|||||||
|
|
||||||
part 'container_data.g.dart';
|
part 'container_data.g.dart';
|
||||||
|
|
||||||
@CopyWith()
|
|
||||||
@JsonSerializable()
|
|
||||||
class ContainerAuthSettings with FastEquatable {
|
|
||||||
final bool authenticationRequired;
|
|
||||||
final bool lockOnAppBackground;
|
|
||||||
final Duration? lockTimeout;
|
|
||||||
|
|
||||||
ContainerAuthSettings({
|
|
||||||
required this.authenticationRequired,
|
|
||||||
required this.lockOnAppBackground,
|
|
||||||
required this.lockTimeout,
|
|
||||||
});
|
|
||||||
|
|
||||||
ContainerAuthSettings.withDefaults({
|
|
||||||
bool? authenticationRequired,
|
|
||||||
bool? lockOnAppBackground,
|
|
||||||
Duration? lockTimeout,
|
|
||||||
}) : this(
|
|
||||||
authenticationRequired: authenticationRequired ?? false,
|
|
||||||
lockOnAppBackground: lockOnAppBackground ?? false,
|
|
||||||
lockTimeout: lockTimeout,
|
|
||||||
);
|
|
||||||
|
|
||||||
factory ContainerAuthSettings.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$ContainerAuthSettingsFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$ContainerAuthSettingsToJson(this);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get hashParameters => [
|
|
||||||
authenticationRequired,
|
|
||||||
lockOnAppBackground,
|
|
||||||
lockTimeout,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
@CopyWith()
|
@CopyWith()
|
||||||
@JsonSerializable(constructor: 'withDefaults')
|
@JsonSerializable(constructor: 'withDefaults')
|
||||||
class ContainerMetadata with FastEquatable {
|
class ContainerMetadata with FastEquatable {
|
||||||
@IconDataJsonConverter()
|
@IconDataJsonConverter()
|
||||||
final IconData? iconData;
|
final IconData? iconData;
|
||||||
final String? contextualIdentity;
|
final String? contextualIdentity;
|
||||||
final ContainerAuthSettings authSettings;
|
|
||||||
|
|
||||||
@JsonKey(defaultValue: false)
|
@JsonKey(defaultValue: false)
|
||||||
final bool useProxy;
|
final bool useProxy;
|
||||||
@@ -81,7 +44,6 @@ class ContainerMetadata with FastEquatable {
|
|||||||
ContainerMetadata({
|
ContainerMetadata({
|
||||||
required this.iconData,
|
required this.iconData,
|
||||||
required this.contextualIdentity,
|
required this.contextualIdentity,
|
||||||
required this.authSettings,
|
|
||||||
required this.useProxy,
|
required this.useProxy,
|
||||||
required this.clearDataOnExit,
|
required this.clearDataOnExit,
|
||||||
required this.assignedSites,
|
required this.assignedSites,
|
||||||
@@ -90,14 +52,12 @@ class ContainerMetadata with FastEquatable {
|
|||||||
ContainerMetadata.withDefaults({
|
ContainerMetadata.withDefaults({
|
||||||
IconData? iconData,
|
IconData? iconData,
|
||||||
String? contextualIdentity,
|
String? contextualIdentity,
|
||||||
ContainerAuthSettings? authSettings,
|
|
||||||
bool? useProxy,
|
bool? useProxy,
|
||||||
bool? clearDataOnExit,
|
bool? clearDataOnExit,
|
||||||
List<Uri>? assignedSites,
|
List<Uri>? assignedSites,
|
||||||
}) : this(
|
}) : this(
|
||||||
iconData: iconData,
|
iconData: iconData,
|
||||||
contextualIdentity: contextualIdentity,
|
contextualIdentity: contextualIdentity,
|
||||||
authSettings: authSettings ?? ContainerAuthSettings.withDefaults(),
|
|
||||||
useProxy: useProxy ?? false,
|
useProxy: useProxy ?? false,
|
||||||
clearDataOnExit: clearDataOnExit ?? false,
|
clearDataOnExit: clearDataOnExit ?? false,
|
||||||
assignedSites: assignedSites,
|
assignedSites: assignedSites,
|
||||||
@@ -112,7 +72,6 @@ class ContainerMetadata with FastEquatable {
|
|||||||
List<Object?> get hashParameters => [
|
List<Object?> get hashParameters => [
|
||||||
iconData,
|
iconData,
|
||||||
contextualIdentity,
|
contextualIdentity,
|
||||||
authSettings,
|
|
||||||
useProxy,
|
useProxy,
|
||||||
clearDataOnExit,
|
clearDataOnExit,
|
||||||
assignedSites,
|
assignedSites,
|
||||||
|
|||||||
@@ -6,96 +6,11 @@ part of 'container_data.dart';
|
|||||||
// CopyWithGenerator
|
// CopyWithGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
abstract class _$ContainerAuthSettingsCWProxy {
|
|
||||||
ContainerAuthSettings authenticationRequired(bool authenticationRequired);
|
|
||||||
|
|
||||||
ContainerAuthSettings lockOnAppBackground(bool lockOnAppBackground);
|
|
||||||
|
|
||||||
ContainerAuthSettings lockTimeout(Duration? lockTimeout);
|
|
||||||
|
|
||||||
/// Creates a new instance with the provided field values.
|
|
||||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `ContainerAuthSettings(...).copyWith.fieldName(value)`.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// ContainerAuthSettings(...).copyWith(id: 12, name: "My name")
|
|
||||||
/// ```
|
|
||||||
ContainerAuthSettings call({
|
|
||||||
bool authenticationRequired,
|
|
||||||
bool lockOnAppBackground,
|
|
||||||
Duration? lockTimeout,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Callable proxy for `copyWith` functionality.
|
|
||||||
/// Use as `instanceOfContainerAuthSettings.copyWith(...)` or call `instanceOfContainerAuthSettings.copyWith.fieldName(value)` for a single field.
|
|
||||||
class _$ContainerAuthSettingsCWProxyImpl
|
|
||||||
implements _$ContainerAuthSettingsCWProxy {
|
|
||||||
const _$ContainerAuthSettingsCWProxyImpl(this._value);
|
|
||||||
|
|
||||||
final ContainerAuthSettings _value;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ContainerAuthSettings authenticationRequired(bool authenticationRequired) =>
|
|
||||||
call(authenticationRequired: authenticationRequired);
|
|
||||||
|
|
||||||
@override
|
|
||||||
ContainerAuthSettings lockOnAppBackground(bool lockOnAppBackground) =>
|
|
||||||
call(lockOnAppBackground: lockOnAppBackground);
|
|
||||||
|
|
||||||
@override
|
|
||||||
ContainerAuthSettings lockTimeout(Duration? lockTimeout) =>
|
|
||||||
call(lockTimeout: lockTimeout);
|
|
||||||
|
|
||||||
@override
|
|
||||||
/// Creates a new instance with the provided field values.
|
|
||||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `ContainerAuthSettings(...).copyWith.fieldName(value)`.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// ContainerAuthSettings(...).copyWith(id: 12, name: "My name")
|
|
||||||
/// ```
|
|
||||||
ContainerAuthSettings call({
|
|
||||||
Object? authenticationRequired = const $CopyWithPlaceholder(),
|
|
||||||
Object? lockOnAppBackground = const $CopyWithPlaceholder(),
|
|
||||||
Object? lockTimeout = const $CopyWithPlaceholder(),
|
|
||||||
}) {
|
|
||||||
return ContainerAuthSettings(
|
|
||||||
authenticationRequired:
|
|
||||||
authenticationRequired == const $CopyWithPlaceholder() ||
|
|
||||||
authenticationRequired == null
|
|
||||||
? _value.authenticationRequired
|
|
||||||
// ignore: cast_nullable_to_non_nullable
|
|
||||||
: authenticationRequired as bool,
|
|
||||||
lockOnAppBackground:
|
|
||||||
lockOnAppBackground == const $CopyWithPlaceholder() ||
|
|
||||||
lockOnAppBackground == null
|
|
||||||
? _value.lockOnAppBackground
|
|
||||||
// ignore: cast_nullable_to_non_nullable
|
|
||||||
: lockOnAppBackground as bool,
|
|
||||||
lockTimeout: lockTimeout == const $CopyWithPlaceholder()
|
|
||||||
? _value.lockTimeout
|
|
||||||
// ignore: cast_nullable_to_non_nullable
|
|
||||||
: lockTimeout as Duration?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension $ContainerAuthSettingsCopyWith on ContainerAuthSettings {
|
|
||||||
/// Returns a callable class used to build a new instance with modified fields.
|
|
||||||
/// Example: `instanceOfContainerAuthSettings.copyWith(...)` or `instanceOfContainerAuthSettings.copyWith.fieldName(...)`.
|
|
||||||
// ignore: library_private_types_in_public_api
|
|
||||||
_$ContainerAuthSettingsCWProxy get copyWith =>
|
|
||||||
_$ContainerAuthSettingsCWProxyImpl(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class _$ContainerMetadataCWProxy {
|
abstract class _$ContainerMetadataCWProxy {
|
||||||
ContainerMetadata iconData(IconData? iconData);
|
ContainerMetadata iconData(IconData? iconData);
|
||||||
|
|
||||||
ContainerMetadata contextualIdentity(String? contextualIdentity);
|
ContainerMetadata contextualIdentity(String? contextualIdentity);
|
||||||
|
|
||||||
ContainerMetadata authSettings(ContainerAuthSettings authSettings);
|
|
||||||
|
|
||||||
ContainerMetadata useProxy(bool useProxy);
|
ContainerMetadata useProxy(bool useProxy);
|
||||||
|
|
||||||
ContainerMetadata clearDataOnExit(bool clearDataOnExit);
|
ContainerMetadata clearDataOnExit(bool clearDataOnExit);
|
||||||
@@ -112,7 +27,6 @@ abstract class _$ContainerMetadataCWProxy {
|
|||||||
ContainerMetadata call({
|
ContainerMetadata call({
|
||||||
IconData? iconData,
|
IconData? iconData,
|
||||||
String? contextualIdentity,
|
String? contextualIdentity,
|
||||||
ContainerAuthSettings authSettings,
|
|
||||||
bool useProxy,
|
bool useProxy,
|
||||||
bool clearDataOnExit,
|
bool clearDataOnExit,
|
||||||
List<Uri>? assignedSites,
|
List<Uri>? assignedSites,
|
||||||
@@ -133,10 +47,6 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
|||||||
ContainerMetadata contextualIdentity(String? contextualIdentity) =>
|
ContainerMetadata contextualIdentity(String? contextualIdentity) =>
|
||||||
call(contextualIdentity: contextualIdentity);
|
call(contextualIdentity: contextualIdentity);
|
||||||
|
|
||||||
@override
|
|
||||||
ContainerMetadata authSettings(ContainerAuthSettings authSettings) =>
|
|
||||||
call(authSettings: authSettings);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ContainerMetadata useProxy(bool useProxy) => call(useProxy: useProxy);
|
ContainerMetadata useProxy(bool useProxy) => call(useProxy: useProxy);
|
||||||
|
|
||||||
@@ -159,7 +69,6 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
|||||||
ContainerMetadata call({
|
ContainerMetadata call({
|
||||||
Object? iconData = const $CopyWithPlaceholder(),
|
Object? iconData = const $CopyWithPlaceholder(),
|
||||||
Object? contextualIdentity = const $CopyWithPlaceholder(),
|
Object? contextualIdentity = const $CopyWithPlaceholder(),
|
||||||
Object? authSettings = const $CopyWithPlaceholder(),
|
|
||||||
Object? useProxy = const $CopyWithPlaceholder(),
|
Object? useProxy = const $CopyWithPlaceholder(),
|
||||||
Object? clearDataOnExit = const $CopyWithPlaceholder(),
|
Object? clearDataOnExit = const $CopyWithPlaceholder(),
|
||||||
Object? assignedSites = const $CopyWithPlaceholder(),
|
Object? assignedSites = const $CopyWithPlaceholder(),
|
||||||
@@ -173,11 +82,6 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
|||||||
? _value.contextualIdentity
|
? _value.contextualIdentity
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
: contextualIdentity as String?,
|
: contextualIdentity as String?,
|
||||||
authSettings:
|
|
||||||
authSettings == const $CopyWithPlaceholder() || authSettings == null
|
|
||||||
? _value.authSettings
|
|
||||||
// ignore: cast_nullable_to_non_nullable
|
|
||||||
: authSettings as ContainerAuthSettings,
|
|
||||||
useProxy: useProxy == const $CopyWithPlaceholder() || useProxy == null
|
useProxy: useProxy == const $CopyWithPlaceholder() || useProxy == null
|
||||||
? _value.useProxy
|
? _value.useProxy
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
@@ -294,24 +198,6 @@ extension $ContainerDataCopyWith on ContainerData {
|
|||||||
// JsonSerializableGenerator
|
// JsonSerializableGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
ContainerAuthSettings _$ContainerAuthSettingsFromJson(
|
|
||||||
Map<String, dynamic> json,
|
|
||||||
) => ContainerAuthSettings(
|
|
||||||
authenticationRequired: json['authenticationRequired'] as bool,
|
|
||||||
lockOnAppBackground: json['lockOnAppBackground'] as bool,
|
|
||||||
lockTimeout: json['lockTimeout'] == null
|
|
||||||
? null
|
|
||||||
: Duration(microseconds: (json['lockTimeout'] as num).toInt()),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$ContainerAuthSettingsToJson(
|
|
||||||
ContainerAuthSettings instance,
|
|
||||||
) => <String, dynamic>{
|
|
||||||
'authenticationRequired': instance.authenticationRequired,
|
|
||||||
'lockOnAppBackground': instance.lockOnAppBackground,
|
|
||||||
'lockTimeout': instance.lockTimeout?.inMicroseconds,
|
|
||||||
};
|
|
||||||
|
|
||||||
ContainerMetadata _$ContainerMetadataFromJson(Map<String, dynamic> json) =>
|
ContainerMetadata _$ContainerMetadataFromJson(Map<String, dynamic> json) =>
|
||||||
ContainerMetadata.withDefaults(
|
ContainerMetadata.withDefaults(
|
||||||
iconData: _$JsonConverterFromJson<Map<String, dynamic>, IconData>(
|
iconData: _$JsonConverterFromJson<Map<String, dynamic>, IconData>(
|
||||||
@@ -319,11 +205,6 @@ ContainerMetadata _$ContainerMetadataFromJson(Map<String, dynamic> json) =>
|
|||||||
const IconDataJsonConverter().fromJson,
|
const IconDataJsonConverter().fromJson,
|
||||||
),
|
),
|
||||||
contextualIdentity: json['contextualIdentity'] as String?,
|
contextualIdentity: json['contextualIdentity'] as String?,
|
||||||
authSettings: json['authSettings'] == null
|
|
||||||
? null
|
|
||||||
: ContainerAuthSettings.fromJson(
|
|
||||||
json['authSettings'] as Map<String, dynamic>,
|
|
||||||
),
|
|
||||||
useProxy: json['useProxy'] as bool? ?? false,
|
useProxy: json['useProxy'] as bool? ?? false,
|
||||||
clearDataOnExit: json['clearDataOnExit'] as bool? ?? false,
|
clearDataOnExit: json['clearDataOnExit'] as bool? ?? false,
|
||||||
assignedSites: (json['assignedSites'] as List<dynamic>?)
|
assignedSites: (json['assignedSites'] as List<dynamic>?)
|
||||||
@@ -339,7 +220,6 @@ Map<String, dynamic> _$ContainerMetadataToJson(
|
|||||||
const IconDataJsonConverter().toJson,
|
const IconDataJsonConverter().toJson,
|
||||||
),
|
),
|
||||||
'contextualIdentity': instance.contextualIdentity,
|
'contextualIdentity': instance.contextualIdentity,
|
||||||
'authSettings': instance.authSettings.toJson(),
|
|
||||||
'useProxy': instance.useProxy,
|
'useProxy': instance.useProxy,
|
||||||
'clearDataOnExit': instance.clearDataOnExit,
|
'clearDataOnExit': instance.clearDataOnExit,
|
||||||
'assignedSites': instance.assignedSites?.map((e) => e.toString()).toList(),
|
'assignedSites': instance.assignedSites?.map((e) => e.toString()).toList(),
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
|
|||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/user/domain/services/local_authentication.dart';
|
|
||||||
|
|
||||||
part 'selected_container.g.dart';
|
part 'selected_container.g.dart';
|
||||||
|
|
||||||
@@ -45,48 +44,23 @@ class SelectedContainer extends _$SelectedContainer {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> authenticateContainer(ContainerData container) async {
|
|
||||||
var passAuth = false;
|
|
||||||
if (container.metadata.authSettings.authenticationRequired) {
|
|
||||||
final authResult = await ref
|
|
||||||
.read(localAuthenticationServiceProvider.notifier)
|
|
||||||
.authenticate(
|
|
||||||
authKey: 'container_access::${container.id}',
|
|
||||||
localizedReason: 'Require authentication for container',
|
|
||||||
settings: container.metadata.authSettings,
|
|
||||||
useAuthCache: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (authResult) {
|
|
||||||
passAuth = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
passAuth = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return passAuth;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<SetContainerResult> setContainerId(String id) async {
|
Future<SetContainerResult> setContainerId(String id) async {
|
||||||
final container = await ref
|
final container = await ref
|
||||||
.read(containerRepositoryProvider.notifier)
|
.read(containerRepositoryProvider.notifier)
|
||||||
.getContainerData(id);
|
.getContainerData(id);
|
||||||
|
|
||||||
if (ref.mounted && container != null) {
|
if (ref.mounted && container != null) {
|
||||||
final passAuth = await authenticateContainer(container);
|
if (container.metadata.useProxy) {
|
||||||
if (passAuth) {
|
final proxyPluginHealthy = await GeckoContainerProxyService()
|
||||||
if (container.metadata.useProxy) {
|
.healthcheck();
|
||||||
final proxyPluginHealthy = await GeckoContainerProxyService()
|
|
||||||
.healthcheck();
|
|
||||||
|
|
||||||
if (proxyPluginHealthy) {
|
if (proxyPluginHealthy) {
|
||||||
state = id;
|
|
||||||
return SetContainerResult.successHasProxy;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
state = id;
|
state = id;
|
||||||
return SetContainerResult.success;
|
return SetContainerResult.successHasProxy;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
state = id;
|
||||||
|
return SetContainerResult.success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class SelectedContainerProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$selectedContainerHash() => r'e1d20f0c2e7764e82937ba486de52c92184c0e4a';
|
String _$selectedContainerHash() => r'0ffa17823ec95c94b9cf9b2d005de19d6ff992f8';
|
||||||
|
|
||||||
abstract class _$SelectedContainer extends $Notifier<String?> {
|
abstract class _$SelectedContainer extends $Notifier<String?> {
|
||||||
String? build();
|
String? build();
|
||||||
|
|||||||
@@ -32,19 +32,10 @@ import 'package:weblibre/features/geckoview/features/tabs/presentation/dialogs/d
|
|||||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_sites.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_sites.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/color_picker_dialog.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/color_picker_dialog.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart';
|
||||||
import 'package:weblibre/features/user/domain/services/local_authentication.dart';
|
|
||||||
import 'package:weblibre/presentation/icons/tor_icons.dart';
|
import 'package:weblibre/presentation/icons/tor_icons.dart';
|
||||||
|
|
||||||
enum _DialogMode { create, edit }
|
enum _DialogMode { create, edit }
|
||||||
|
|
||||||
const _timeoutOptions = <DropdownMenuItem<Duration?>>[
|
|
||||||
DropdownMenuItem(child: Text('Immediately')),
|
|
||||||
DropdownMenuItem(value: Duration(minutes: 1), child: Text('1 minute')),
|
|
||||||
DropdownMenuItem(value: Duration(minutes: 5), child: Text('5 minutes')),
|
|
||||||
DropdownMenuItem(value: Duration(minutes: 15), child: Text('15 minutes')),
|
|
||||||
DropdownMenuItem(value: Duration(hours: 1), child: Text('1 hour')),
|
|
||||||
];
|
|
||||||
|
|
||||||
class ContainerEditScreen extends HookConsumerWidget {
|
class ContainerEditScreen extends HookConsumerWidget {
|
||||||
final _DialogMode _mode;
|
final _DialogMode _mode;
|
||||||
|
|
||||||
@@ -77,7 +68,6 @@ class ContainerEditScreen extends HookConsumerWidget {
|
|||||||
final contextualIdentity = useState(
|
final contextualIdentity = useState(
|
||||||
initialContainer.metadata.contextualIdentity,
|
initialContainer.metadata.contextualIdentity,
|
||||||
);
|
);
|
||||||
final authSettings = useState(initialContainer.metadata.authSettings);
|
|
||||||
final useProxy = useState(initialContainer.metadata.useProxy);
|
final useProxy = useState(initialContainer.metadata.useProxy);
|
||||||
final clearDataOnExit = useState(initialContainer.metadata.clearDataOnExit);
|
final clearDataOnExit = useState(initialContainer.metadata.clearDataOnExit);
|
||||||
final assignedSites = useState(initialContainer.metadata.assignedSites);
|
final assignedSites = useState(initialContainer.metadata.assignedSites);
|
||||||
@@ -106,7 +96,6 @@ class ContainerEditScreen extends HookConsumerWidget {
|
|||||||
color: selectedColor.value,
|
color: selectedColor.value,
|
||||||
metadata: initialContainer.metadata.copyWith(
|
metadata: initialContainer.metadata.copyWith(
|
||||||
contextualIdentity: contextualIdentity.value,
|
contextualIdentity: contextualIdentity.value,
|
||||||
authSettings: authSettings.value,
|
|
||||||
useProxy: useProxy.value && contextualIdentity.value != null,
|
useProxy: useProxy.value && contextualIdentity.value != null,
|
||||||
clearDataOnExit:
|
clearDataOnExit:
|
||||||
clearDataOnExit.value && contextualIdentity.value != null,
|
clearDataOnExit.value && contextualIdentity.value != null,
|
||||||
@@ -114,24 +103,6 @@ class ContainerEditScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
//Check for permissions, when auth is set or getting set
|
|
||||||
if (initialContainer
|
|
||||||
.metadata
|
|
||||||
.authSettings
|
|
||||||
.authenticationRequired ||
|
|
||||||
container.metadata.authSettings.authenticationRequired) {
|
|
||||||
final authResult = await ref
|
|
||||||
.read(localAuthenticationServiceProvider.notifier)
|
|
||||||
.authenticate(
|
|
||||||
authKey: 'container_access::${container.id}',
|
|
||||||
localizedReason: 'Require authentication for container',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!authResult) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (_mode) {
|
switch (_mode) {
|
||||||
case _DialogMode.create:
|
case _DialogMode.create:
|
||||||
await ref
|
await ref
|
||||||
@@ -283,48 +254,6 @@ class ContainerEditScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
SwitchListTile.adaptive(
|
|
||||||
value: authSettings.value.authenticationRequired,
|
|
||||||
title: const Text('Require Authentication'),
|
|
||||||
secondary: const Icon(MdiIcons.fingerprint),
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
onChanged: (value) {
|
|
||||||
authSettings.value = authSettings.value.copyWith
|
|
||||||
.authenticationRequired(value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (authSettings.value.authenticationRequired)
|
|
||||||
CheckboxListTile.adaptive(
|
|
||||||
value: authSettings.value.lockOnAppBackground,
|
|
||||||
title: const Text('Auto-lock on background'),
|
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
|
||||||
onChanged: (value) {
|
|
||||||
authSettings.value = authSettings.value.copyWith
|
|
||||||
.lockOnAppBackground(value!);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (authSettings.value.authenticationRequired)
|
|
||||||
CheckboxListTile.adaptive(
|
|
||||||
value: authSettings.value.lockTimeout != null,
|
|
||||||
title: const Text('Timeout'),
|
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
|
||||||
onChanged: (value) {
|
|
||||||
final newValue = value!
|
|
||||||
? _timeoutOptions[1].value
|
|
||||||
: null;
|
|
||||||
|
|
||||||
authSettings.value = authSettings.value.copyWith
|
|
||||||
.lockTimeout(newValue);
|
|
||||||
},
|
|
||||||
secondary: DropdownButton(
|
|
||||||
value: authSettings.value.lockTimeout,
|
|
||||||
items: _timeoutOptions,
|
|
||||||
onChanged: (value) {
|
|
||||||
authSettings.value = authSettings.value.copyWith
|
|
||||||
.lockTimeout(value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.web),
|
leading: const Icon(Icons.web),
|
||||||
title: const Text('Assigned Sites'),
|
title: const Text('Assigned Sites'),
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class SettingSection extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 16.0),
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2024-2026 Fabian Freund.
|
||||||
|
*
|
||||||
|
* This file is part of WebLibre
|
||||||
|
* (see https://weblibre.eu).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||||
|
import 'package:fast_equatable/fast_equatable.dart';
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'auth_settings.g.dart';
|
||||||
|
|
||||||
|
enum AutoLockMode { background, timeout }
|
||||||
|
|
||||||
|
@CopyWith()
|
||||||
|
@JsonSerializable()
|
||||||
|
class AuthSettings with FastEquatable {
|
||||||
|
final bool authenticationRequired;
|
||||||
|
final AutoLockMode autoLockMode;
|
||||||
|
final Duration timeout;
|
||||||
|
|
||||||
|
AuthSettings({
|
||||||
|
required this.authenticationRequired,
|
||||||
|
required this.autoLockMode,
|
||||||
|
required this.timeout,
|
||||||
|
});
|
||||||
|
|
||||||
|
AuthSettings.withDefaults({
|
||||||
|
bool? authenticationRequired,
|
||||||
|
AutoLockMode? autoLockMode,
|
||||||
|
Duration? timeout,
|
||||||
|
}) : this(
|
||||||
|
authenticationRequired: authenticationRequired ?? false,
|
||||||
|
autoLockMode: autoLockMode ?? AutoLockMode.background,
|
||||||
|
timeout: timeout ?? const Duration(minutes: 5),
|
||||||
|
);
|
||||||
|
|
||||||
|
AuthSettings withBackgroundLock() {
|
||||||
|
return copyWith(autoLockMode: AutoLockMode.background);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthSettings withTimeoutLock(Duration value) {
|
||||||
|
return copyWith(autoLockMode: AutoLockMode.timeout, timeout: value);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory AuthSettings.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$AuthSettingsFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$AuthSettingsToJson(this);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get hashParameters => [
|
||||||
|
authenticationRequired,
|
||||||
|
autoLockMode,
|
||||||
|
timeout,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'auth_settings.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// CopyWithGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
abstract class _$AuthSettingsCWProxy {
|
||||||
|
AuthSettings authenticationRequired(bool authenticationRequired);
|
||||||
|
|
||||||
|
AuthSettings autoLockMode(AutoLockMode autoLockMode);
|
||||||
|
|
||||||
|
AuthSettings timeout(Duration timeout);
|
||||||
|
|
||||||
|
/// Creates a new instance with the provided field values.
|
||||||
|
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `AuthSettings(...).copyWith.fieldName(value)`.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// AuthSettings(...).copyWith(id: 12, name: "My name")
|
||||||
|
/// ```
|
||||||
|
AuthSettings call({
|
||||||
|
bool authenticationRequired,
|
||||||
|
AutoLockMode autoLockMode,
|
||||||
|
Duration timeout,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Callable proxy for `copyWith` functionality.
|
||||||
|
/// Use as `instanceOfAuthSettings.copyWith(...)` or call `instanceOfAuthSettings.copyWith.fieldName(value)` for a single field.
|
||||||
|
class _$AuthSettingsCWProxyImpl implements _$AuthSettingsCWProxy {
|
||||||
|
const _$AuthSettingsCWProxyImpl(this._value);
|
||||||
|
|
||||||
|
final AuthSettings _value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
AuthSettings authenticationRequired(bool authenticationRequired) =>
|
||||||
|
call(authenticationRequired: authenticationRequired);
|
||||||
|
|
||||||
|
@override
|
||||||
|
AuthSettings autoLockMode(AutoLockMode autoLockMode) =>
|
||||||
|
call(autoLockMode: autoLockMode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
AuthSettings timeout(Duration timeout) => call(timeout: timeout);
|
||||||
|
|
||||||
|
@override
|
||||||
|
/// Creates a new instance with the provided field values.
|
||||||
|
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `AuthSettings(...).copyWith.fieldName(value)`.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// AuthSettings(...).copyWith(id: 12, name: "My name")
|
||||||
|
/// ```
|
||||||
|
AuthSettings call({
|
||||||
|
Object? authenticationRequired = const $CopyWithPlaceholder(),
|
||||||
|
Object? autoLockMode = const $CopyWithPlaceholder(),
|
||||||
|
Object? timeout = const $CopyWithPlaceholder(),
|
||||||
|
}) {
|
||||||
|
return AuthSettings(
|
||||||
|
authenticationRequired:
|
||||||
|
authenticationRequired == const $CopyWithPlaceholder() ||
|
||||||
|
authenticationRequired == null
|
||||||
|
? _value.authenticationRequired
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: authenticationRequired as bool,
|
||||||
|
autoLockMode:
|
||||||
|
autoLockMode == const $CopyWithPlaceholder() || autoLockMode == null
|
||||||
|
? _value.autoLockMode
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: autoLockMode as AutoLockMode,
|
||||||
|
timeout: timeout == const $CopyWithPlaceholder() || timeout == null
|
||||||
|
? _value.timeout
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: timeout as Duration,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension $AuthSettingsCopyWith on AuthSettings {
|
||||||
|
/// Returns a callable class used to build a new instance with modified fields.
|
||||||
|
/// Example: `instanceOfAuthSettings.copyWith(...)` or `instanceOfAuthSettings.copyWith.fieldName(...)`.
|
||||||
|
// ignore: library_private_types_in_public_api
|
||||||
|
_$AuthSettingsCWProxy get copyWith => _$AuthSettingsCWProxyImpl(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
AuthSettings _$AuthSettingsFromJson(Map<String, dynamic> json) => AuthSettings(
|
||||||
|
authenticationRequired: json['authenticationRequired'] as bool,
|
||||||
|
autoLockMode: $enumDecode(_$AutoLockModeEnumMap, json['autoLockMode']),
|
||||||
|
timeout: Duration(microseconds: (json['timeout'] as num).toInt()),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$AuthSettingsToJson(AuthSettings instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'authenticationRequired': instance.authenticationRequired,
|
||||||
|
'autoLockMode': _$AutoLockModeEnumMap[instance.autoLockMode]!,
|
||||||
|
'timeout': instance.timeout.inMicroseconds,
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$AutoLockModeEnumMap = {
|
||||||
|
AutoLockMode.background: 'background',
|
||||||
|
AutoLockMode.timeout: 'timeout',
|
||||||
|
};
|
||||||
@@ -27,20 +27,82 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:weblibre/core/filesystem.dart';
|
import 'package:weblibre/core/filesystem.dart';
|
||||||
import 'package:weblibre/core/routing/routes.dart';
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
import 'package:weblibre/domain/entities/profile.dart';
|
import 'package:weblibre/domain/entities/profile.dart';
|
||||||
|
import 'package:weblibre/features/settings/presentation/widgets/sections.dart';
|
||||||
|
import 'package:weblibre/features/user/data/models/auth_settings.dart';
|
||||||
import 'package:weblibre/features/user/domain/presentation/dialogs/delete_profile_dialog.dart';
|
import 'package:weblibre/features/user/domain/presentation/dialogs/delete_profile_dialog.dart';
|
||||||
import 'package:weblibre/features/user/domain/presentation/utils/profile_switch_handler.dart';
|
import 'package:weblibre/features/user/domain/presentation/utils/profile_switch_handler.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/providers/profile_auth.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/profile.dart';
|
import 'package:weblibre/features/user/domain/repositories/profile.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/services/local_authentication.dart';
|
||||||
import 'package:weblibre/utils/form_validators.dart';
|
import 'package:weblibre/utils/form_validators.dart';
|
||||||
|
|
||||||
|
const _timeoutOptions = <DropdownMenuItem<Duration?>>[
|
||||||
|
DropdownMenuItem(value: Duration(minutes: 1), child: Text('1 minute')),
|
||||||
|
DropdownMenuItem(value: Duration(minutes: 5), child: Text('5 minutes')),
|
||||||
|
DropdownMenuItem(value: Duration(minutes: 15), child: Text('15 minutes')),
|
||||||
|
DropdownMenuItem(value: Duration(hours: 1), child: Text('1 hour')),
|
||||||
|
];
|
||||||
|
|
||||||
class ProfileEditScreen extends HookConsumerWidget {
|
class ProfileEditScreen extends HookConsumerWidget {
|
||||||
final Profile? profile;
|
final Profile? profile;
|
||||||
|
|
||||||
const ProfileEditScreen({required this.profile});
|
const ProfileEditScreen({required this.profile});
|
||||||
|
|
||||||
|
Future<void> _handleSave(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
GlobalKey<FormState> formKey,
|
||||||
|
String name,
|
||||||
|
AuthSettings authSettings,
|
||||||
|
) async {
|
||||||
|
if (!(formKey.currentState?.validate() ?? false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require biometric confirmation when enabling/changing auth
|
||||||
|
if (profile != null &&
|
||||||
|
(profile!.authSettings.authenticationRequired ||
|
||||||
|
authSettings.authenticationRequired)) {
|
||||||
|
final authResult = await ref
|
||||||
|
.read(localAuthenticationServiceProvider.notifier)
|
||||||
|
.authenticate(
|
||||||
|
authKey: profileAccessAuthKey(profile!.id),
|
||||||
|
localizedReason: 'Require authentication for profile',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!authResult) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile != null) {
|
||||||
|
await ref
|
||||||
|
.read(profileRepositoryProvider.notifier)
|
||||||
|
.updateProfileMetadata(
|
||||||
|
profile!.copyWith(name: name, authSettings: authSettings),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await ref
|
||||||
|
.read(profileRepositoryProvider.notifier)
|
||||||
|
.createProfile(name: name, authSettings: authSettings);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||||
final nameTextController = useTextEditingController(text: profile?.name);
|
final nameTextController = useTextEditingController(text: profile?.name);
|
||||||
|
final authSettings = useState(
|
||||||
|
profile?.authSettings ?? AuthSettings.withDefaults(),
|
||||||
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -50,27 +112,13 @@ class ProfileEditScreen extends HookConsumerWidget {
|
|||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (formKey.currentState?.validate() ?? false) {
|
await _handleSave(
|
||||||
if (profile != null) {
|
context,
|
||||||
await ref
|
ref,
|
||||||
.read(profileRepositoryProvider.notifier)
|
formKey,
|
||||||
.updateProfileMetadata(
|
nameTextController.text,
|
||||||
profile!.copyWith.name(nameTextController.text),
|
authSettings.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
context.pop();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await ref
|
|
||||||
.read(profileRepositoryProvider.notifier)
|
|
||||||
.createProfile(name: nameTextController.text);
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
context.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.check),
|
icon: const Icon(Icons.check),
|
||||||
),
|
),
|
||||||
@@ -78,77 +126,191 @@ class ProfileEditScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
body: Form(
|
body: Form(
|
||||||
key: formKey,
|
key: formKey,
|
||||||
child: Padding(
|
child: ListView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
child: ListView(
|
children: [
|
||||||
children: [
|
TextFormField(
|
||||||
TextFormField(
|
controller: nameTextController,
|
||||||
controller: nameTextController,
|
decoration: const InputDecoration(
|
||||||
decoration: const InputDecoration(
|
label: Text('Name'),
|
||||||
label: Text('Name'),
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
|
||||||
),
|
|
||||||
validator: validateProfileName,
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
validator: validateProfileName,
|
||||||
if (profile != null) ...[
|
),
|
||||||
SizedBox(
|
const SizedBox(height: 24),
|
||||||
width: double.infinity,
|
_AuthSection(
|
||||||
child: OutlinedButton.icon(
|
authSettings: authSettings.value,
|
||||||
label: const Text('Backup'),
|
onAuthSettingsChanged: (newSettings) {
|
||||||
icon: const Icon(MdiIcons.safe),
|
authSettings.value = newSettings;
|
||||||
onPressed: () async {
|
},
|
||||||
await BackupProfileRoute(
|
),
|
||||||
profile: jsonEncode(profile!.toJson()),
|
const SizedBox(height: 24),
|
||||||
).push(context);
|
if (profile != null) ...[_ProfileActionsSection(profile: profile!)],
|
||||||
},
|
],
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
if (filesystem.selectedProfile != profile!.uuidValue)
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: OutlinedButton.icon(
|
|
||||||
label: const Text('Switch to this Profile'),
|
|
||||||
icon: const Icon(MdiIcons.accountSwitch),
|
|
||||||
onPressed: () async {
|
|
||||||
await handleSwitchProfile(context, ref, profile!);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: OutlinedButton.icon(
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
side: BorderSide(
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
foregroundColor: Theme.of(context).colorScheme.error,
|
|
||||||
iconColor: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
label: const Text('Delete'),
|
|
||||||
icon: const Icon(Icons.delete),
|
|
||||||
onPressed: () async {
|
|
||||||
final result = await showDeleteProfileDialog(context);
|
|
||||||
|
|
||||||
if (result == true) {
|
|
||||||
await ref
|
|
||||||
.read(profileRepositoryProvider.notifier)
|
|
||||||
.deleteProfile(profile!.uuidValue.uuid);
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
context.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AuthSection extends StatelessWidget {
|
||||||
|
final AuthSettings authSettings;
|
||||||
|
final ValueChanged<AuthSettings> onAuthSettingsChanged;
|
||||||
|
|
||||||
|
const _AuthSection({
|
||||||
|
required this.authSettings,
|
||||||
|
required this.onAuthSettingsChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SettingSection(name: 'Authentication'),
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
value: authSettings.authenticationRequired,
|
||||||
|
title: const Text('Require Authentication'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Lock this profile when switching away from the app',
|
||||||
|
),
|
||||||
|
secondary: const Icon(MdiIcons.fingerprint),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
onChanged: (value) {
|
||||||
|
onAuthSettingsChanged(
|
||||||
|
authSettings.copyWith.authenticationRequired(value),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (authSettings.authenticationRequired) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const ListTile(
|
||||||
|
title: Text('Auto-lock Behavior'),
|
||||||
|
subtitle: Text('Choose when to lock the profile'),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
leading: Icon(MdiIcons.lockClock),
|
||||||
|
),
|
||||||
|
RadioGroup<AutoLockMode>(
|
||||||
|
groupValue: authSettings.autoLockMode,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
onAuthSettingsChanged(
|
||||||
|
authSettings.copyWith.autoLockMode(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Column(
|
||||||
|
children: [
|
||||||
|
RadioListTile.adaptive(
|
||||||
|
value: AutoLockMode.background,
|
||||||
|
title: Text('Lock on Background'),
|
||||||
|
subtitle: Text(
|
||||||
|
'Lock immediately when app goes to background',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
RadioListTile.adaptive(
|
||||||
|
value: AutoLockMode.timeout,
|
||||||
|
title: Text('Lock After Timeout'),
|
||||||
|
subtitle: Text('Lock after a period of inactivity'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (authSettings.autoLockMode == AutoLockMode.timeout)
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Timeout Duration'),
|
||||||
|
subtitle: const Text('How long to wait before locking'),
|
||||||
|
leading: const Icon(MdiIcons.timerOutline),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
trailing: DropdownButton<Duration?>(
|
||||||
|
value: authSettings.timeout,
|
||||||
|
items: _timeoutOptions,
|
||||||
|
underline: const SizedBox.shrink(),
|
||||||
|
onChanged: (Duration? value) {
|
||||||
|
if (value != null) {
|
||||||
|
onAuthSettingsChanged(authSettings.copyWith.timeout(value));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProfileActionsSection extends ConsumerWidget {
|
||||||
|
final Profile profile;
|
||||||
|
|
||||||
|
const _ProfileActionsSection({required this.profile});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SettingSection(name: 'Profile Actions'),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
label: const Text('Backup'),
|
||||||
|
icon: const Icon(MdiIcons.safe),
|
||||||
|
onPressed: () async {
|
||||||
|
await BackupProfileRoute(
|
||||||
|
profile: jsonEncode(profile.toJson()),
|
||||||
|
).push(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
if (filesystem.selectedProfile != profile.uuidValue)
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
label: const Text('Switch to this Profile'),
|
||||||
|
icon: const Icon(MdiIcons.accountSwitch),
|
||||||
|
onPressed: () async {
|
||||||
|
await handleSwitchProfile(context, ref, profile);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (filesystem.selectedProfile != profile.uuidValue)
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
side: BorderSide(color: Theme.of(context).colorScheme.error),
|
||||||
|
foregroundColor: Theme.of(context).colorScheme.error,
|
||||||
|
iconColor: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
label: const Text('Delete'),
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
onPressed: () async {
|
||||||
|
final result = await showDeleteProfileDialog(context);
|
||||||
|
|
||||||
|
if (result == true) {
|
||||||
|
await ref
|
||||||
|
.read(profileRepositoryProvider.notifier)
|
||||||
|
.deleteProfile(profile.uuidValue.uuid);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2024-2026 Fabian Freund.
|
||||||
|
*
|
||||||
|
* This file is part of WebLibre
|
||||||
|
* (see https://weblibre.eu).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/providers/profile_auth.dart';
|
||||||
|
import 'package:weblibre/presentation/hooks/on_initialization.dart';
|
||||||
|
|
||||||
|
class LockScreen extends HookConsumerWidget {
|
||||||
|
const LockScreen();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isAuthenticating = useState(false);
|
||||||
|
final didAutoAuthenticate = useRef(false);
|
||||||
|
|
||||||
|
Future<void> authenticate() async {
|
||||||
|
if (isAuthenticating.value) return;
|
||||||
|
|
||||||
|
isAuthenticating.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ref.read(profileAuthStateProvider.notifier).authenticate();
|
||||||
|
} finally {
|
||||||
|
if (context.mounted) {
|
||||||
|
isAuthenticating.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useOnInitialization(() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!didAutoAuthenticate.value) {
|
||||||
|
didAutoAuthenticate.value = true;
|
||||||
|
unawaited(authenticate());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(MdiIcons.lock, size: 64),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text('Profile is locked'),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FilledButton.icon(
|
||||||
|
style: FilledButton.styleFrom(minimumSize: const Size(160, 40)),
|
||||||
|
icon: const Icon(MdiIcons.fingerprint),
|
||||||
|
label: Text(isAuthenticating.value ? 'Unlocking...' : 'Unlock'),
|
||||||
|
onPressed: isAuthenticating.value ? null : authenticate,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2024-2026 Fabian Freund.
|
||||||
|
*
|
||||||
|
* This file is part of WebLibre
|
||||||
|
* (see https://weblibre.eu).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/providers.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/services/local_authentication.dart';
|
||||||
|
|
||||||
|
part 'profile_auth.g.dart';
|
||||||
|
|
||||||
|
String profileAccessAuthKey(String profileId) => 'profile_access::$profileId';
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
class ProfileAuthState extends _$ProfileAuthState {
|
||||||
|
bool _bootstrapped = false;
|
||||||
|
|
||||||
|
Future<void> bootstrapFromProfile() async {
|
||||||
|
if (_bootstrapped) return;
|
||||||
|
|
||||||
|
final profile = await ref.read(selectedProfileProvider.future);
|
||||||
|
if (!ref.mounted) return;
|
||||||
|
|
||||||
|
_bootstrapped = true;
|
||||||
|
|
||||||
|
if (!profile.authSettings.authenticationRequired) {
|
||||||
|
_unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> authenticate() async {
|
||||||
|
final profile = await ref.read(selectedProfileProvider.future);
|
||||||
|
if (!ref.mounted) return false;
|
||||||
|
|
||||||
|
if (!profile.authSettings.authenticationRequired) {
|
||||||
|
_unlock();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await ref
|
||||||
|
.read(localAuthenticationServiceProvider.notifier)
|
||||||
|
.authenticate(
|
||||||
|
authKey: profileAccessAuthKey(profile.id),
|
||||||
|
localizedReason: 'Unlock profile',
|
||||||
|
settings: profile.authSettings,
|
||||||
|
useAuthCache: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ref.mounted) return false;
|
||||||
|
|
||||||
|
state = result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> revalidateAfterResume() async {
|
||||||
|
if (!state) return;
|
||||||
|
|
||||||
|
final profile = await ref.read(selectedProfileProvider.future);
|
||||||
|
if (!ref.mounted || !profile.authSettings.authenticationRequired) return;
|
||||||
|
|
||||||
|
final cached = ref
|
||||||
|
.read(localAuthenticationServiceProvider.notifier)
|
||||||
|
.isCached(profileAccessAuthKey(profile.id));
|
||||||
|
|
||||||
|
if (!cached && ref.mounted) {
|
||||||
|
_lock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _lock() {
|
||||||
|
state = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _unlock() {
|
||||||
|
state = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool build() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
Raw<ProfileAuthNotifier> profileAuthNotifier(Ref ref) {
|
||||||
|
final notifier = ProfileAuthNotifier();
|
||||||
|
|
||||||
|
ref.listen<bool>(profileAuthStateProvider, (_, _) {
|
||||||
|
notifier.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
ref.onDispose(notifier.dispose);
|
||||||
|
|
||||||
|
return notifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProfileAuthNotifier extends ChangeNotifier {
|
||||||
|
void notify() => notifyListeners();
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'profile_auth.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(ProfileAuthState)
|
||||||
|
final profileAuthStateProvider = ProfileAuthStateProvider._();
|
||||||
|
|
||||||
|
final class ProfileAuthStateProvider
|
||||||
|
extends $NotifierProvider<ProfileAuthState, bool> {
|
||||||
|
ProfileAuthStateProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'profileAuthStateProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$profileAuthStateHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
ProfileAuthState create() => ProfileAuthState();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(bool value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<bool>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$profileAuthStateHash() => r'9eb65fdb76baa0b088fc12a8063ea4ee63d54ac4';
|
||||||
|
|
||||||
|
abstract class _$ProfileAuthState extends $Notifier<bool> {
|
||||||
|
bool build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final ref = this.ref as $Ref<bool, bool>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<bool, bool>,
|
||||||
|
bool,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleCreate(ref, build);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ProviderFor(profileAuthNotifier)
|
||||||
|
final profileAuthProvider = ProfileAuthNotifierProvider._();
|
||||||
|
|
||||||
|
final class ProfileAuthNotifierProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
Raw<ProfileAuthNotifier>,
|
||||||
|
Raw<ProfileAuthNotifier>,
|
||||||
|
Raw<ProfileAuthNotifier>
|
||||||
|
>
|
||||||
|
with $Provider<Raw<ProfileAuthNotifier>> {
|
||||||
|
ProfileAuthNotifierProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'profileAuthProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$profileAuthNotifierHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$ProviderElement<Raw<ProfileAuthNotifier>> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $ProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Raw<ProfileAuthNotifier> create(Ref ref) {
|
||||||
|
return profileAuthNotifier(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(Raw<ProfileAuthNotifier> value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<Raw<ProfileAuthNotifier>>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$profileAuthNotifierHash() =>
|
||||||
|
r'795f47b1494e4a9cdd74f5ff22d431b2bc59ffbd';
|
||||||
@@ -20,8 +20,8 @@
|
|||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import 'package:weblibre/core/filesystem.dart';
|
import 'package:weblibre/core/filesystem.dart';
|
||||||
|
|
||||||
import 'package:weblibre/domain/entities/profile.dart';
|
import 'package:weblibre/domain/entities/profile.dart';
|
||||||
|
import 'package:weblibre/features/user/data/models/auth_settings.dart';
|
||||||
|
|
||||||
part 'profile.g.dart';
|
part 'profile.g.dart';
|
||||||
|
|
||||||
@@ -40,8 +40,11 @@ class ProfileRepository extends _$ProfileRepository {
|
|||||||
await filesystem.setStartupProfile(UuidValue.withValidation(id));
|
await filesystem.setStartupProfile(UuidValue.withValidation(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Profile> createProfile({required String name}) async {
|
Future<Profile> createProfile({
|
||||||
final profile = Profile.create(name: name);
|
required String name,
|
||||||
|
AuthSettings? authSettings,
|
||||||
|
}) async {
|
||||||
|
final profile = Profile.create(name: name, authSettings: authSettings);
|
||||||
if (!await filesystem.createNewProfile(profile)) {
|
if (!await filesystem.createNewProfile(profile)) {
|
||||||
throw Exception('Could not create profile');
|
throw Exception('Could not create profile');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ final class ProfileRepositoryProvider
|
|||||||
ProfileRepository create() => ProfileRepository();
|
ProfileRepository create() => ProfileRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$profileRepositoryHash() => r'e925dba74b0f15244fea8fff8391b5b67509be09';
|
String _$profileRepositoryHash() => r'c17702af1e59727ab3fec26e9ca659048e92c8bb';
|
||||||
|
|
||||||
abstract class _$ProfileRepository extends $AsyncNotifier<List<Profile>> {
|
abstract class _$ProfileRepository extends $AsyncNotifier<List<Profile>> {
|
||||||
FutureOr<List<Profile>> build();
|
FutureOr<List<Profile>> build();
|
||||||
|
|||||||
@@ -22,46 +22,50 @@ import 'dart:async';
|
|||||||
import 'package:local_auth/local_auth.dart';
|
import 'package:local_auth/local_auth.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:weblibre/core/logger.dart';
|
import 'package:weblibre/core/logger.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
import 'package:weblibre/features/user/data/models/auth_settings.dart';
|
||||||
|
|
||||||
part 'local_authentication.g.dart';
|
part 'local_authentication.g.dart';
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class LocalAuthenticationService extends _$LocalAuthenticationService {
|
class LocalAuthenticationService extends _$LocalAuthenticationService {
|
||||||
final _auth = LocalAuthentication();
|
final _auth = LocalAuthentication();
|
||||||
final _cache = <String, (DateTime, ContainerAuthSettings)>{};
|
final _cache = <String, (DateTime, AuthSettings)>{};
|
||||||
|
|
||||||
bool _cacheAuth(String authKey) {
|
|
||||||
final auth = _cache[authKey];
|
|
||||||
if (auth != null && auth.$2.lockTimeout != null) {
|
|
||||||
return DateTime.now().difference(auth.$1) < auth.$2.lockTimeout!;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void evictCacheOnBackground() {
|
void evictCacheOnBackground() {
|
||||||
_cache.removeWhere((key, value) => value.$2.lockOnAppBackground);
|
_cache.removeWhere(
|
||||||
|
(key, value) => value.$2.autoLockMode == AutoLockMode.background,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isCached(String authKey) {
|
||||||
|
final auth = _cache[authKey];
|
||||||
|
|
||||||
|
if (auth == null) return false;
|
||||||
|
if (auth.$2.autoLockMode == AutoLockMode.timeout) {
|
||||||
|
return DateTime.now().difference(auth.$1) < auth.$2.timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background mode cache stays valid until app background eviction.
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> authenticate({
|
Future<bool> authenticate({
|
||||||
required String authKey,
|
required String authKey,
|
||||||
required String localizedReason,
|
required String localizedReason,
|
||||||
ContainerAuthSettings? settings,
|
AuthSettings? settings,
|
||||||
bool useAuthCache = false,
|
bool useAuthCache = false,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
var result = useAuthCache && _cacheAuth(authKey);
|
final useCache = useAuthCache && isCached(authKey);
|
||||||
|
final success =
|
||||||
|
useCache ||
|
||||||
|
await _auth.authenticate(localizedReason: localizedReason);
|
||||||
|
|
||||||
if (!result) {
|
if (success && settings != null) {
|
||||||
result = await _auth.authenticate(localizedReason: localizedReason);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result && settings != null) {
|
|
||||||
_cache[authKey] = (DateTime.now(), settings);
|
_cache[authKey] = (DateTime.now(), settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return success;
|
||||||
} on LocalAuthException catch (e, s) {
|
} on LocalAuthException catch (e, s) {
|
||||||
logger.e('Could not authenticate', error: e, stackTrace: s);
|
logger.e('Could not authenticate', error: e, stackTrace: s);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ final class LocalAuthenticationServiceProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$localAuthenticationServiceHash() =>
|
String _$localAuthenticationServiceHash() =>
|
||||||
r'1aab9214af5487dc770658c7be6f66083f5d6931';
|
r'0f4b2b47e94b2426a2219eca4eb2258bf683ab7c';
|
||||||
|
|
||||||
abstract class _$LocalAuthenticationService extends $AsyncNotifier<bool> {
|
abstract class _$LocalAuthenticationService extends $AsyncNotifier<bool> {
|
||||||
FutureOr<bool> build();
|
FutureOr<bool> build();
|
||||||
|
|||||||
Reference in New Issue
Block a user