1
0
mirror of https://github.com/immich-app/immich.git synced 2024-11-24 08:52:28 +02:00

fix(mobile): mobile logging out randomly (#11431)

* fix(mobile): refactor splash screen to not require online connection

* chore: bump flutter sdk path for vscode

* refactor: authentication provider always try network calls and only fail if 401 or no local user

* lint

* fix: revert change to lookup serverendpoint from store the isar store implementation is very broken

* fix: clear serverUrl and serverEndpoint on logout, and await logout call

* refactor: remove unneeded extra conditions in splash screen useEffect

* revert change to remove serverEndpoint on logging out

* pr feedback

---------

Co-authored-by: Zack Pollard <zackpollard@ymail.com>
This commit is contained in:
Alex 2024-07-30 13:15:48 -05:00 committed by GitHub
parent 21d3f248da
commit 17c3e8e8bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 73 additions and 103 deletions

View File

@ -1,5 +1,5 @@
{ {
"dart.flutterSdkPath": ".fvm/versions/3.22.1", "dart.flutterSdkPath": ".fvm/versions/3.22.3",
"search.exclude": { "search.exclude": {
"**/.fvm": true "**/.fvm": true
}, },

View File

@ -9,7 +9,6 @@ import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
@RoutePage() @RoutePage()
class SplashScreenPage extends HookConsumerWidget { class SplashScreenPage extends HookConsumerWidget {
@ -19,45 +18,22 @@ class SplashScreenPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final apiService = ref.watch(apiServiceProvider); final apiService = ref.watch(apiServiceProvider);
final serverUrl = Store.tryGet(StoreKey.serverUrl); final serverUrl = Store.tryGet(StoreKey.serverUrl);
final endpoint = Store.tryGet(StoreKey.serverEndpoint);
final accessToken = Store.tryGet(StoreKey.accessToken); final accessToken = Store.tryGet(StoreKey.accessToken);
final log = Logger("SplashScreenPage"); final log = Logger("SplashScreenPage");
void performLoggingIn() async { void performLoggingIn() async {
bool isSuccess = false; bool isAuthSuccess = false;
bool deviceIsOffline = false;
if (accessToken != null && serverUrl != null) { if (accessToken != null && serverUrl != null && endpoint != null) {
try { apiService.setEndpoint(endpoint);
// Resolve API server endpoint from user provided serverUrl
await apiService.resolveAndSetEndpoint(serverUrl);
} on ApiException catch (error, stackTrace) {
log.severe(
"Failed to resolve endpoint [ApiException]",
error,
stackTrace,
);
// okay, try to continue anyway if offline
if (error.code == 503) {
deviceIsOffline = true;
log.warning("Device seems to be offline upon launch");
} else {
log.severe("Failed to resolve endpoint", error);
}
} catch (error, stackTrace) {
log.severe(
"Failed to resolve endpoint [Catch All]",
error,
stackTrace,
);
}
try { try {
isSuccess = await ref isAuthSuccess = await ref
.read(authenticationProvider.notifier) .read(authenticationProvider.notifier)
.setSuccessLoginInfo( .setSuccessLoginInfo(
accessToken: accessToken, accessToken: accessToken,
serverUrl: serverUrl, serverUrl: serverUrl,
offlineLogin: deviceIsOffline,
); );
} catch (error, stackTrace) { } catch (error, stackTrace) {
log.severe( log.severe(
@ -66,39 +42,35 @@ class SplashScreenPage extends HookConsumerWidget {
stackTrace, stackTrace,
); );
} }
} else {
isAuthSuccess = false;
log.severe(
'Missing authentication, server, or endpoint info from the local store',
);
}
if (!isAuthSuccess) {
log.severe(
'Unable to login using offline or online methods - Logging out completely',
);
ref.read(authenticationProvider.notifier).logout();
context.replaceRoute(const LoginRoute());
return;
} }
// If the device is offline and there is a currentUser stored locallly
// Proceed into the app
if (deviceIsOffline && Store.tryGet(StoreKey.currentUser) != null) {
context.replaceRoute(const TabControllerRoute()); context.replaceRoute(const TabControllerRoute());
} else if (isSuccess) {
// If device was able to login through the internet successfully
final hasPermission = final hasPermission =
await ref.read(galleryPermissionNotifier.notifier).hasPermission; await ref.read(galleryPermissionNotifier.notifier).hasPermission;
if (hasPermission) { if (hasPermission) {
// Resume backup (if enable) then navigate // Resume backup (if enable) then navigate
ref.watch(backupProvider.notifier).resumeBackup(); ref.watch(backupProvider.notifier).resumeBackup();
} }
context.replaceRoute(const TabControllerRoute());
} else {
log.severe(
'Unable to login through offline or online methods - logging out completely',
);
ref.read(authenticationProvider.notifier).logout();
// User was unable to login through either offline or online methods
context.replaceRoute(const LoginRoute());
}
} }
useEffect( useEffect(
() { () {
if (serverUrl != null && accessToken != null) {
performLoggingIn(); performLoggingIn();
} else {
context.replaceRoute(const LoginRoute());
}
return null; return null;
}, },
[], [],

View File

@ -101,7 +101,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
try { try {
String? userEmail = Store.tryGet(StoreKey.currentUser)?.email; String? userEmail = Store.tryGet(StoreKey.currentUser)?.email;
_apiService.authenticationApi await _apiService.authenticationApi
.logout() .logout()
.then((_) => log.info("Logout was successful for $userEmail")) .then((_) => log.info("Logout was successful for $userEmail"))
.onError( .onError(
@ -156,7 +156,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
Future<bool> setSuccessLoginInfo({ Future<bool> setSuccessLoginInfo({
required String accessToken, required String accessToken,
required String serverUrl, required String serverUrl,
bool offlineLogin = false,
}) async { }) async {
_apiService.setAccessToken(accessToken); _apiService.setAccessToken(accessToken);
@ -165,31 +164,26 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid; Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid;
bool shouldChangePassword = false; bool shouldChangePassword = false;
User? user; User? user = Store.tryGet(StoreKey.currentUser);
bool retResult = false; UserAdminResponseDto? userResponse;
User? offlineUser = Store.tryGet(StoreKey.currentUser);
// If the user is offline and there is a user saved on the device,
// if not try an online login
if (offlineLogin && offlineUser != null) {
user = offlineUser;
retResult = false;
} else {
UserAdminResponseDto? userResponseDto;
UserPreferencesResponseDto? userPreferences; UserPreferencesResponseDto? userPreferences;
try { try {
userResponseDto = await _apiService.usersApi.getMyUser(); final responses = await Future.wait([
userPreferences = await _apiService.usersApi.getMyPreferences(); _apiService.usersApi.getMyUser(),
_apiService.usersApi.getMyPreferences(),
]);
userResponse = responses[0] as UserAdminResponseDto;
userPreferences = responses[1] as UserPreferencesResponseDto;
} on ApiException catch (error, stackTrace) { } on ApiException catch (error, stackTrace) {
if (error.code == 401) {
_log.severe("Unauthorized access, token likely expired. Logging out.");
return false;
}
_log.severe( _log.severe(
"Error getting user information from the server [API EXCEPTION]", "Error getting user information from the server [API EXCEPTION]",
error,
stackTrace, stackTrace,
); );
if (error.innerException is SocketException) {
state = state.copyWith(isAuthenticated: true);
}
} catch (error, stackTrace) { } catch (error, stackTrace) {
_log.severe( _log.severe(
"Error getting user information from the server [CATCH ALL]", "Error getting user information from the server [CATCH ALL]",
@ -198,24 +192,28 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
); );
} }
if (userResponseDto != null) { // If the user information is successfully retrieved, update the store
// Due to the flow of the code, this will always happen on first login
if (userResponse != null) {
Store.put(StoreKey.deviceId, deviceId); Store.put(StoreKey.deviceId, deviceId);
Store.put(StoreKey.deviceIdHash, fastHash(deviceId)); Store.put(StoreKey.deviceIdHash, fastHash(deviceId));
Store.put( Store.put(
StoreKey.currentUser, StoreKey.currentUser,
User.fromUserDto(userResponseDto, userPreferences), User.fromUserDto(userResponse, userPreferences),
); );
Store.put(StoreKey.serverUrl, serverUrl); Store.put(StoreKey.serverUrl, serverUrl);
Store.put(StoreKey.accessToken, accessToken); Store.put(StoreKey.accessToken, accessToken);
shouldChangePassword = userResponseDto.shouldChangePassword; shouldChangePassword = userResponse.shouldChangePassword;
user = User.fromUserDto(userResponseDto, userPreferences); user = User.fromUserDto(userResponse, userPreferences);
retResult = true;
} else { } else {
_log.severe("Unable to get user information from the server."); _log.severe("Unable to get user information from the server.");
return false;
} }
// If the user is null, the login was not successful
// and we don't have a local copy of the user from a prior successful login
if (user == null) {
return false;
} }
state = state.copyWith( state = state.copyWith(
@ -229,7 +227,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
deviceId: deviceId, deviceId: deviceId,
); );
return retResult; return true;
} }
} }