import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:dio/dio.dart'; import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hive_flutter/adapters.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.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'; import 'package:paperless_mobile/core/database/tables/local_user_settings.dart'; import 'package:paperless_mobile/core/database/tables/user_credentials.dart'; import 'package:paperless_mobile/core/factory/paperless_api_factory.dart'; import 'package:paperless_mobile/core/model/info_message_exception.dart'; import 'package:paperless_mobile/core/security/session_manager.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/features/login/model/login_form_credentials.dart'; import 'package:paperless_mobile/features/login/services/authentication_service.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; part 'authentication_state.dart'; class AuthenticationCubit extends Cubit { final LocalAuthenticationService _localAuthService; final PaperlessApiFactory _apiFactory; final SessionManager _sessionManager; final ConnectivityStatusService _connectivityService; AuthenticationCubit( this._localAuthService, this._apiFactory, this._sessionManager, this._connectivityService, ) : super(const UnauthenticatedState()); Future login({ required LoginFormCredentials credentials, required String serverUrl, ClientCertificate? clientCertificate, }) async { assert(credentials.username != null && credentials.password != null); final localUserId = "${credentials.username}@$serverUrl"; _debugPrintMessage( "login", "Trying to login $localUserId...", ); await _addUser( localUserId, serverUrl, credentials, clientCertificate, _sessionManager, ); // Mark logged in user as currently active user. final globalSettings = Hive.box(HiveBoxes.globalSettings).getValue()!; globalSettings.loggedInUserId = localUserId; await globalSettings.save(); emit( AuthenticatedState( localUserId: localUserId, ), ); _debugPrintMessage( "login", "User successfully logged in.", ); } /// Switches to another account if it exists. Future switchAccount(String localUserId) async { emit(const SwitchingAccountsState()); final globalSettings = Hive.box(HiveBoxes.globalSettings).getValue()!; if (globalSettings.loggedInUserId == localUserId) { emit(AuthenticatedState(localUserId: localUserId)); return; } final userAccountBox = Hive.box(HiveBoxes.localUserAccount); if (!userAccountBox.containsKey(localUserId)) { debugPrint("User $localUserId not yet registered."); return; } final account = userAccountBox.get(localUserId)!; if (account.settings.isBiometricAuthenticationEnabled) { final authenticated = await _localAuthService .authenticateLocalUser("Authenticate to switch your account."); if (!authenticated) { debugPrint("User not authenticated."); return; } } await withEncryptedBox( HiveBoxes.localUserCredentials, (credentialsBox) async { if (!credentialsBox.containsKey(localUserId)) { await credentialsBox.close(); debugPrint("Invalid authentication for $localUserId"); return; } final credentials = credentialsBox.get(localUserId); await _resetExternalState(); _sessionManager.updateSettings( authToken: credentials!.token, clientCertificate: credentials.clientCertificate, baseUrl: account.serverUrl, ); globalSettings.loggedInUserId = localUserId; await globalSettings.save(); final apiVersion = await _getApiVersion(_sessionManager.client); await _updateRemoteUser( _sessionManager, Hive.box(HiveBoxes.localUserAccount) .get(localUserId)!, apiVersion, ); emit(AuthenticatedState( localUserId: localUserId, )); }); } Future addAccount({ required LoginFormCredentials credentials, required String serverUrl, ClientCertificate? clientCertificate, required bool enableBiometricAuthentication, }) async { assert(credentials.password != null && credentials.username != null); final localUserId = "${credentials.username}@$serverUrl"; final sessionManager = SessionManager(); await _addUser( localUserId, serverUrl, credentials, clientCertificate, sessionManager, ); return localUserId; } Future removeAccount(String userId) async { final userAccountBox = Hive.box(HiveBoxes.localUserAccount); final userAppStateBox = Hive.box(HiveBoxes.localUserAppState); await userAccountBox.delete(userId); await userAppStateBox.delete(userId); await withEncryptedBox( HiveBoxes.localUserCredentials, (box) { box.delete(userId); }); } /// /// Performs a conditional hydration based on the local authentication success. /// Future restoreSessionState() async { _debugPrintMessage( "restoreSessionState", "Trying to restore previous session...", ); final globalSettings = Hive.box(HiveBoxes.globalSettings).getValue()!; final localUserId = globalSettings.loggedInUserId; if (localUserId == null) { _debugPrintMessage( "restoreSessionState", "There is nothing to restore.", ); // If there is nothing to restore, we can quit here. emit(const UnauthenticatedState()); return; } final localUserAccountBox = Hive.box(HiveBoxes.localUserAccount); final localUserAccount = localUserAccountBox.get(localUserId)!; _debugPrintMessage( "restoreSessionState", "Checking if biometric authentication is required...", ); if (localUserAccount.settings.isBiometricAuthenticationEnabled) { _debugPrintMessage( "restoreSessionState", "Biometric authentication required, waiting for user to authenticate...", ); final authenticationMesage = (await S.delegate.load(Locale(globalSettings.preferredLocaleSubtag))) .verifyYourIdentity; final localAuthSuccess = await _localAuthService.authenticateLocalUser(authenticationMesage); if (!localAuthSuccess) { emit(const RequiresLocalAuthenticationState()); _debugPrintMessage( "restoreSessionState", "User could not be authenticated.", ); return; } _debugPrintMessage( "restoreSessionState", "User successfully autheticated.", ); } else { _debugPrintMessage( "restoreSessionState", "Biometric authentication not configured, skipping.", ); } _debugPrintMessage( "restoreSessionState", "Trying to retrieve authentication credentials...", ); final authentication = await withEncryptedBox( HiveBoxes.localUserCredentials, (box) { return box.get(globalSettings.loggedInUserId!); }); if (authentication == null) { _debugPrintMessage( "restoreSessionState", "Could not retrieve existing authentication credentials.", ); throw Exception( "User should be authenticated but no authentication information was found.", ); } _debugPrintMessage( "restoreSessionState", "Authentication credentials successfully retrieved.", ); _debugPrintMessage( "restoreSessionState", "Updating current session state...", ); _sessionManager.updateSettings( clientCertificate: authentication.clientCertificate, authToken: authentication.token, baseUrl: localUserAccount.serverUrl, ); _debugPrintMessage( "restoreSessionState", "Current session state successfully updated.", ); final hasInternetConnection = await _connectivityService.isConnectedToInternet(); if (hasInternetConnection) { _debugPrintMessage( "restoreSessionMState", "Updating server user...", ); final apiVersion = await _getApiVersion(_sessionManager.client); await _updateRemoteUser( _sessionManager, localUserAccount, apiVersion, ); _debugPrintMessage( "restoreSessionMState", "Successfully updated server user.", ); } else { _debugPrintMessage( "restoreSessionMState", "Skipping update of server user (no internet connection).", ); } emit(AuthenticatedState(localUserId: localUserId)); _debugPrintMessage( "restoreSessionState", "Session was successfully restored.", ); } Future logout() async { _debugPrintMessage( "logout", "Trying to log out current user...", ); await _resetExternalState(); final globalSettings = Hive.box(HiveBoxes.globalSettings).getValue()!; globalSettings.loggedInUserId = null; await globalSettings.save(); emit(const UnauthenticatedState()); _debugPrintMessage( "logout", "User successfully logged out.", ); } Future _resetExternalState() async { _debugPrintMessage( "_resetExternalState", "Resetting session manager and clearing storage...", ); _sessionManager.resetSettings(); await HydratedBloc.storage.clear(); _debugPrintMessage( "_resetExternalState", "Session manager successfully reset and storage cleared.", ); } Future _addUser( String localUserId, String serverUrl, LoginFormCredentials credentials, ClientCertificate? clientCert, SessionManager sessionManager, ) async { assert(credentials.username != null && credentials.password != null); _debugPrintMessage("_addUser", "Adding new user $localUserId..."); sessionManager.updateSettings( baseUrl: serverUrl, clientCertificate: clientCert, ); final authApi = _apiFactory.createAuthenticationApi(sessionManager.client); _debugPrintMessage( "_addUser", "Trying to login user ${credentials.username} on $serverUrl...", ); final token = await authApi.login( username: credentials.username!, password: credentials.password!, ); _debugPrintMessage( "_addUser", "Successfully acquired token.", ); sessionManager.updateSettings( baseUrl: serverUrl, clientCertificate: clientCert, authToken: token, ); final userAccountBox = Hive.box(HiveBoxes.localUserAccount); final userStateBox = Hive.box(HiveBoxes.localUserAppState); if (userAccountBox.containsKey(localUserId)) { _debugPrintMessage( "_addUser", "An error occurred! The user $localUserId already exists.", ); throw InfoMessageException(code: ErrorCode.userAlreadyExists); } final apiVersion = await _getApiVersion(sessionManager.client); _debugPrintMessage( "_addUser", "Trying to fetch user object for $localUserId...", ); late UserModel serverUser; try { serverUser = await _apiFactory .createUserApi( sessionManager.client, apiVersion: apiVersion, ) .findCurrentUser(); } on DioException catch (error, stackTrace) { _debugPrintMessage( "_addUser", "An error occurred: ${error.message}", stackTrace: stackTrace, ); rethrow; } _debugPrintMessage( "_addUser", "User object successfully fetched.", ); _debugPrintMessage( "_addUser", "Persisting local user account...", ); // Create user account await userAccountBox.put( localUserId, LocalUserAccount( id: localUserId, settings: LocalUserSettings(), serverUrl: serverUrl, paperlessUser: serverUser, apiVersion: apiVersion, ), ); _debugPrintMessage( "_addUser", "Local user account successfully persisted.", ); _debugPrintMessage( "_addUser", "Persisting user state...", ); // Create user state await userStateBox.put( localUserId, LocalUserAppState(userId: localUserId), ); _debugPrintMessage( "_addUser", "User state successfully persisted.", ); // Save credentials in encrypted box await withEncryptedBox(HiveBoxes.localUserCredentials, (box) async { _debugPrintMessage( "_addUser", "Saving user credentials inside encrypted storage...", ); await box.put( localUserId, UserCredentials( token: token, clientCertificate: clientCert, ), ); _debugPrintMessage( "_addUser", "User credentials successfully saved.", ); }); final hostsBox = Hive.box(HiveBoxes.hosts); if (!hostsBox.values.contains(serverUrl)) { await hostsBox.add(serverUrl); } return serverUser.id; } Future _getApiVersion(Dio dio) async { _debugPrintMessage( "_getApiVersion", "Trying to fetch API version...", ); final response = await dio.get("/api/"); final apiVersion = int.parse(response.headers.value('x-api-version') ?? "3"); _debugPrintMessage( "_getApiVersion", "API version ($apiVersion) successfully retrieved.", ); return apiVersion; } /// Fetches possibly updated (permissions, name, updated server version and thus new user model, ...) remote user data. Future _updateRemoteUser( SessionManager sessionManager, LocalUserAccount localUserAccount, int apiVersion, ) async { _debugPrintMessage( "_updateRemoteUser", "Updating paperless user object...", ); final updatedPaperlessUser = await _apiFactory .createUserApi( sessionManager.client, apiVersion: apiVersion, ) .findCurrentUser(); localUserAccount.paperlessUser = updatedPaperlessUser; await localUserAccount.save(); _debugPrintMessage( "_updateRemoteUser", "Paperless user object successfully updated.", ); } void _debugPrintMessage( String methodName, String message, { Object? error, StackTrace? stackTrace, }) { debugPrint("AuthenticationCubit#$methodName: $message"); if (error != null) { debugPrint(error.toString()); } if (stackTrace != null) { debugPrintStack(stackTrace: stackTrace); } } }