import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_udid/flutter_udid.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/models/authentication/authentication_state.model.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/db.dart'; import 'package:immich_mobile/utils/hash.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; class AuthenticationNotifier extends StateNotifier { AuthenticationNotifier( this._apiService, this._db, this._ref, ) : super( AuthenticationState( deviceId: "", userId: "", userEmail: "", name: '', profileImagePath: '', isAdmin: false, shouldChangePassword: false, isAuthenticated: false, ), ); final ApiService _apiService; final Isar _db; final StateNotifierProviderRef _ref; final _log = Logger("AuthenticationNotifier"); Future login( String email, String password, String serverUrl, ) async { try { // Resolve API server endpoint from user provided serverUrl await _apiService.resolveAndSetEndpoint(serverUrl); await _apiService.serverInfoApi.pingServer(); } catch (e) { debugPrint('Invalid Server Endpoint Url $e'); return false; } // Make sign-in request DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); if (Platform.isIOS) { var iosInfo = await deviceInfoPlugin.iosInfo; _apiService.authenticationApi.apiClient .addDefaultHeader('deviceModel', iosInfo.utsname.machine); _apiService.authenticationApi.apiClient .addDefaultHeader('deviceType', 'iOS'); } else { var androidInfo = await deviceInfoPlugin.androidInfo; _apiService.authenticationApi.apiClient .addDefaultHeader('deviceModel', androidInfo.model); _apiService.authenticationApi.apiClient .addDefaultHeader('deviceType', 'Android'); } try { var loginResponse = await _apiService.authenticationApi.login( LoginCredentialDto( email: email, password: password, ), ); if (loginResponse == null) { debugPrint('Login Response is null'); return false; } return setSuccessLoginInfo( accessToken: loginResponse.accessToken, serverUrl: serverUrl, ); } catch (e) { debugPrint("Error logging in $e"); return false; } } Future logout() async { var log = Logger('AuthenticationNotifier'); try { String? userEmail = Store.tryGet(StoreKey.currentUser)?.email; _apiService.authenticationApi .logout() .then((_) => log.info("Logout was successful for $userEmail")) .onError( (error, stackTrace) => log.severe("Logout failed for $userEmail", error, stackTrace), ); await Future.wait([ clearAssetsAndAlbums(_db), Store.delete(StoreKey.currentUser), Store.delete(StoreKey.accessToken), ]); _ref.invalidate(albumProvider); _ref.invalidate(sharedAlbumProvider); state = state.copyWith( deviceId: "", userId: "", userEmail: "", name: '', profileImagePath: '', isAdmin: false, shouldChangePassword: false, isAuthenticated: false, ); } catch (e, stack) { log.severe('Logout failed', e, stack); } } updateUserProfileImagePath(String path) { state = state.copyWith(profileImagePath: path); } Future changePassword(String newPassword) async { try { await _apiService.usersApi.updateMyUser( UserUpdateMeDto( password: newPassword, ), ); state = state.copyWith(shouldChangePassword: false); return true; } catch (e) { debugPrint("Error changing password $e"); return false; } } Future setSuccessLoginInfo({ required String accessToken, required String serverUrl, bool offlineLogin = false, }) async { _apiService.setAccessToken(accessToken); // Get the deviceid from the store if it exists, otherwise generate a new one String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid; bool shouldChangePassword = false; User? user; bool retResult = false; 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; try { userResponseDto = await _apiService.usersApi.getMyUser(); userPreferences = await _apiService.usersApi.getMyPreferences(); } on ApiException catch (error, stackTrace) { _log.severe( "Error getting user information from the server [API EXCEPTION]", error, stackTrace, ); if (error.innerException is SocketException) { state = state.copyWith(isAuthenticated: true); } } catch (error, stackTrace) { _log.severe( "Error getting user information from the server [CATCH ALL]", error, stackTrace, ); } if (userResponseDto != null) { Store.put(StoreKey.deviceId, deviceId); Store.put(StoreKey.deviceIdHash, fastHash(deviceId)); Store.put( StoreKey.currentUser, User.fromUserDto(userResponseDto, userPreferences), ); Store.put(StoreKey.serverUrl, serverUrl); Store.put(StoreKey.accessToken, accessToken); shouldChangePassword = userResponseDto.shouldChangePassword; user = User.fromUserDto(userResponseDto, userPreferences); retResult = true; } else { _log.severe("Unable to get user information from the server."); return false; } } state = state.copyWith( isAuthenticated: true, userId: user.id, userEmail: user.email, name: user.name, profileImagePath: user.profileImagePath, isAdmin: user.isAdmin, shouldChangePassword: shouldChangePassword, deviceId: deviceId, ); return retResult; } } final authenticationProvider = StateNotifierProvider((ref) { return AuthenticationNotifier( ref.watch(apiServiceProvider), ref.watch(dbProvider), ref, ); });