Files
immich/mobile/lib/providers/auth.provider.dart
shenlong 9fa8de7baa feat: add cloud id during native sync (#20418)
* use adjustment time in iOS for hash reset

# Conflicts:
#	mobile/lib/infrastructure/repositories/local_album.repository.dart
#	mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart

* migration

* feat: sync cloudId and eTag on sync

* fixes fixes

* more fixes

* re-sync updated eTags

* add server version check & auto sync cloud ids on compatible servers

* fix test

* remove button from sync status page

* chore: modify for testing

* more changes

* chore: add commas in toString

* use cached provider in splash screen

* read upload service provider to prevent reset

* log errors from fetching cloud id mapping

* WIP: migrate cloud id - debug log

* ignore locked asset update

* bulk update metadata

* change log text

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-01-14 12:34:11 -06:00

211 lines
6.6 KiB
Dart

import 'package:flutter_udid/flutter_udid.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/models/auth/auth_state.model.dart';
import 'package:immich_mobile/models/auth/login_response.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/auth.service.dart';
import 'package:immich_mobile/services/secure_storage.service.dart';
import 'package:immich_mobile/services/upload.service.dart';
import 'package:immich_mobile/services/widget.service.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier(
ref.watch(authServiceProvider),
ref.watch(apiServiceProvider),
ref.watch(userServiceProvider),
ref.watch(secureStorageServiceProvider),
ref.watch(widgetServiceProvider),
ref,
);
});
class AuthNotifier extends StateNotifier<AuthState> {
final AuthService _authService;
final ApiService _apiService;
final UserService _userService;
final SecureStorageService _secureStorageService;
final WidgetService _widgetService;
final Ref _ref;
final _log = Logger("AuthenticationNotifier");
static const Duration _timeoutDuration = Duration(seconds: 7);
AuthNotifier(
this._authService,
this._apiService,
this._userService,
this._secureStorageService,
this._widgetService,
this._ref,
) : super(
const AuthState(
deviceId: "",
userId: "",
userEmail: "",
name: '',
profileImagePath: '',
isAdmin: false,
isAuthenticated: false,
),
);
Future<String> validateServerUrl(String url) {
return _authService.validateServerUrl(url);
}
/// Validating the url is the alternative connecting server url without
/// saving the information to the local database
Future<bool> validateAuxilaryServerUrl(String url) async {
try {
final validEndpoint = await _apiService.resolveEndpoint(url);
return await _authService.validateAuxilaryServerUrl(validEndpoint);
} catch (_) {
return false;
}
}
Future<LoginResponse> login(String email, String password) async {
final response = await _authService.login(email, password);
await saveAuthInfo(accessToken: response.accessToken);
return response;
}
Future<void> logout() async {
try {
await _secureStorageService.delete(kSecuredPinCode);
await _widgetService.clearCredentials();
await _authService.logout();
await _ref.read(uploadServiceProvider).cancelBackup();
} finally {
await _cleanUp();
}
}
Future<void> _cleanUp() async {
state = const AuthState(
deviceId: "",
userId: "",
userEmail: "",
name: '',
profileImagePath: '',
isAdmin: false,
isAuthenticated: false,
);
}
void updateUserProfileImagePath(String path) {
state = state.copyWith(profileImagePath: path);
}
Future<bool> changePassword(String newPassword) async {
try {
await _authService.changePassword(newPassword);
return true;
} catch (_) {
return false;
}
}
Future<bool> saveAuthInfo({required String accessToken}) async {
await _apiService.setAccessToken(accessToken);
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
final customHeaders = Store.tryGet(StoreKey.customHeaders);
await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders);
// Get the deviceid from the store if it exists, otherwise generate a new one
String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid;
UserDto? user = _userService.tryGetMyUser();
try {
final serverUser = await _userService.refreshMyUser().timeout(_timeoutDuration);
if (serverUser == null) {
_log.severe("Unable to get user information from the server.");
} else {
// If the user information is successfully retrieved, update the store
// Due to the flow of the code, this will always happen on first login
user = serverUser;
await Store.put(StoreKey.deviceId, deviceId);
await Store.put(StoreKey.deviceIdHash, fastHash(deviceId));
await Store.put(StoreKey.accessToken, accessToken);
}
} on ApiException catch (error, stackTrace) {
if (error.code == 401) {
_log.severe("Unauthorized access, token likely expired. Logging out.");
return false;
}
_log.severe("Error getting user information from the server [API EXCEPTION]", stackTrace);
} catch (error, stackTrace) {
_log.severe("Error getting user information from the server [CATCH ALL]", error, stackTrace);
dPrint(() => "Error getting user information from the server [CATCH ALL] $error $stackTrace");
}
// 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(
deviceId: deviceId,
userId: user.id,
userEmail: user.email,
isAuthenticated: true,
name: user.name,
isAdmin: user.isAdmin,
);
return true;
}
Future<void> saveWifiName(String wifiName) async {
await Store.put(StoreKey.preferredWifiName, wifiName);
}
Future<void> saveLocalEndpoint(String url) async {
await Store.put(StoreKey.localEndpoint, url);
}
String? getSavedWifiName() {
return Store.tryGet(StoreKey.preferredWifiName);
}
String? getSavedLocalEndpoint() {
return Store.tryGet(StoreKey.localEndpoint);
}
/// Returns the current server endpoint (with /api) URL from the store
String? getServerEndpoint() {
return Store.tryGet(StoreKey.serverEndpoint);
}
Future<String?> setOpenApiServiceEndpoint() {
return _authService.setOpenApiServiceEndpoint();
}
Future<bool> unlockPinCode(String pinCode) {
return _authService.unlockPinCode(pinCode);
}
Future<void> lockPinCode() {
return _authService.lockPinCode();
}
Future<void> setupPinCode(String pinCode) {
return _authService.setupPinCode(pinCode);
}
}