mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(mobile) Add OAuth Login On Mobile (#990)
* Added return type for oauth/callback * Remove console.log * Redirect app * Wording * Added loading state change * Added OAuth login on mobile * Return correct status for correct redirection * Auto discovery OAuth Login
This commit is contained in:
		
							
								
								
									
										
											BIN
										
									
								
								docs/docs/usage/img/authentik-redirect.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/docs/usage/img/authentik-redirect.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 51 KiB  | 
@@ -28,9 +28,17 @@ Before enabling OAuth in Immich, a new client application needs to be configured
 | 
			
		||||
 | 
			
		||||
2. Configure Redirect URIs/Origins
 | 
			
		||||
 | 
			
		||||
   1. The **Sign-in redirect URIs** should include:
 | 
			
		||||
  The **Sign-in redirect URIs** should include:
 | 
			
		||||
 | 
			
		||||
      - All URLs that will be used to access the login page of the Immich web client (eg. `http://localhost:2283/auth/login`, `http://192.168.0.200:2283/auth/login`, `https://immich.example.com/auth/login`)
 | 
			
		||||
  * All URLs that will be used to access the login page of the Immich web client (eg. `http://localhost:2283/auth/login`, `http://192.168.0.200:2283/auth/login`, `https://immich.example.com/auth/login`)
 | 
			
		||||
  * Mobile app redirect URL `app.immich:/`
 | 
			
		||||
  
 | 
			
		||||
:::caution
 | 
			
		||||
You **MUST** include `app.immich:/` as the redirect URI for iOS and Android mobile app to work properly. 
 | 
			
		||||
 | 
			
		||||
**Authentik example**
 | 
			
		||||
<img src={require('./img/authentik-redirect.png').default} title="Authentik Redirection URL" width="80%" />
 | 
			
		||||
:::
 | 
			
		||||
 | 
			
		||||
## Enable OAuth
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
android {
 | 
			
		||||
    compileSdkVersion flutter.compileSdkVersion
 | 
			
		||||
    compileSdkVersion 33
 | 
			
		||||
 | 
			
		||||
    compileOptions {
 | 
			
		||||
        sourceCompatibility JavaVersion.VERSION_1_8
 | 
			
		||||
 
 | 
			
		||||
@@ -12,15 +12,26 @@
 | 
			
		||||
      </intent-filter>
 | 
			
		||||
 | 
			
		||||
    </activity>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <activity
 | 
			
		||||
      android:name="com.linusu.flutter_web_auth.CallbackActivity"
 | 
			
		||||
      android:exported="true">
 | 
			
		||||
      <intent-filter android:label="flutter_web_auth">
 | 
			
		||||
        <action android:name="android.intent.action.VIEW" />
 | 
			
		||||
        <category android:name="android.intent.category.DEFAULT" />
 | 
			
		||||
        <category android:name="android.intent.category.BROWSABLE" />
 | 
			
		||||
        <data android:scheme="app.immich" />
 | 
			
		||||
      </intent-filter>
 | 
			
		||||
    </activity>
 | 
			
		||||
    <!-- Don't delete the meta-data below.
 | 
			
		||||
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
 | 
			
		||||
    <meta-data android:name="flutterEmbedding" android:value="2" />
 | 
			
		||||
    <!-- Disables default WorkManager initialization to use our custom initialization -->
 | 
			
		||||
    <provider
 | 
			
		||||
        android:name="androidx.startup.InitializationProvider"
 | 
			
		||||
        android:authorities="${applicationId}.androidx-startup"
 | 
			
		||||
        tools:node="remove">
 | 
			
		||||
    </provider>
 | 
			
		||||
      android:name="androidx.startup.InitializationProvider"
 | 
			
		||||
      android:authorities="${applicationId}.androidx-startup"
 | 
			
		||||
      tools:node="remove"></provider>
 | 
			
		||||
  </application>
 | 
			
		||||
  <uses-permission android:name="android.permission.INTERNET" />
 | 
			
		||||
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
 | 
			
		||||
 
 | 
			
		||||
@@ -109,7 +109,9 @@
 | 
			
		||||
  "login_form_err_invalid_email": "Invalid Email",
 | 
			
		||||
  "login_form_err_leading_whitespace": "Leading whitespace",
 | 
			
		||||
  "login_form_err_trailing_whitespace": "Trailing whitespace",
 | 
			
		||||
  "login_form_failed_login": "Error logging you in, check server url, email and password",
 | 
			
		||||
  "login_form_failed_login": "Error logging you in, check server URL, email and password",
 | 
			
		||||
  "login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL",
 | 
			
		||||
  "login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server",
 | 
			
		||||
  "login_form_label_email": "Email",
 | 
			
		||||
  "login_form_label_password": "Password",
 | 
			
		||||
  "login_form_password_hint": "password",
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,8 @@ PODS:
 | 
			
		||||
  - flutter_udid (0.0.1):
 | 
			
		||||
    - Flutter
 | 
			
		||||
    - SAMKeychain
 | 
			
		||||
  - flutter_web_auth (0.5.0):
 | 
			
		||||
    - Flutter
 | 
			
		||||
  - fluttertoast (0.0.2):
 | 
			
		||||
    - Flutter
 | 
			
		||||
    - Toast
 | 
			
		||||
@@ -37,6 +39,7 @@ PODS:
 | 
			
		||||
DEPENDENCIES:
 | 
			
		||||
  - Flutter (from `Flutter`)
 | 
			
		||||
  - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
 | 
			
		||||
  - flutter_web_auth (from `.symlinks/plugins/flutter_web_auth/ios`)
 | 
			
		||||
  - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
 | 
			
		||||
  - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
 | 
			
		||||
  - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
 | 
			
		||||
@@ -60,6 +63,8 @@ EXTERNAL SOURCES:
 | 
			
		||||
    :path: Flutter
 | 
			
		||||
  flutter_udid:
 | 
			
		||||
    :path: ".symlinks/plugins/flutter_udid/ios"
 | 
			
		||||
  flutter_web_auth:
 | 
			
		||||
    :path: ".symlinks/plugins/flutter_web_auth/ios"
 | 
			
		||||
  fluttertoast:
 | 
			
		||||
    :path: ".symlinks/plugins/fluttertoast/ios"
 | 
			
		||||
  image_picker_ios:
 | 
			
		||||
@@ -86,6 +91,7 @@ EXTERNAL SOURCES:
 | 
			
		||||
SPEC CHECKSUMS:
 | 
			
		||||
  Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
 | 
			
		||||
  flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
 | 
			
		||||
  flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d
 | 
			
		||||
  fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037
 | 
			
		||||
  FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
 | 
			
		||||
  image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb
 | 
			
		||||
 
 | 
			
		||||
@@ -5,21 +5,25 @@ part 'hive_saved_login_info.model.g.dart';
 | 
			
		||||
@HiveType(typeId: 0)
 | 
			
		||||
class HiveSavedLoginInfo {
 | 
			
		||||
  @HiveField(0)
 | 
			
		||||
  String email;
 | 
			
		||||
  String email; // DEPRECATED
 | 
			
		||||
 | 
			
		||||
  @HiveField(1)
 | 
			
		||||
  String password;
 | 
			
		||||
  String password; // DEPRECATED
 | 
			
		||||
 | 
			
		||||
  @HiveField(2)
 | 
			
		||||
  String serverUrl;
 | 
			
		||||
 | 
			
		||||
  @HiveField(3)
 | 
			
		||||
  @HiveField(3, defaultValue: false)
 | 
			
		||||
  bool isSaveLogin;
 | 
			
		||||
 | 
			
		||||
  @HiveField(4, defaultValue: "")
 | 
			
		||||
  String accessToken;
 | 
			
		||||
 | 
			
		||||
  HiveSavedLoginInfo({
 | 
			
		||||
    required this.email,
 | 
			
		||||
    required this.password,
 | 
			
		||||
    required this.serverUrl,
 | 
			
		||||
    required this.isSaveLogin,
 | 
			
		||||
    required this.accessToken,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,14 +20,15 @@ class HiveSavedLoginInfoAdapter extends TypeAdapter<HiveSavedLoginInfo> {
 | 
			
		||||
      email: fields[0] as String,
 | 
			
		||||
      password: fields[1] as String,
 | 
			
		||||
      serverUrl: fields[2] as String,
 | 
			
		||||
      isSaveLogin: fields[3] as bool,
 | 
			
		||||
      isSaveLogin: fields[3] == null ? false : fields[3] as bool,
 | 
			
		||||
      accessToken: fields[4] == null ? '' : fields[4] as String,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void write(BinaryWriter writer, HiveSavedLoginInfo obj) {
 | 
			
		||||
    writer
 | 
			
		||||
      ..writeByte(4)
 | 
			
		||||
      ..writeByte(5)
 | 
			
		||||
      ..writeByte(0)
 | 
			
		||||
      ..write(obj.email)
 | 
			
		||||
      ..writeByte(1)
 | 
			
		||||
@@ -35,7 +36,9 @@ class HiveSavedLoginInfoAdapter extends TypeAdapter<HiveSavedLoginInfo> {
 | 
			
		||||
      ..writeByte(2)
 | 
			
		||||
      ..write(obj.serverUrl)
 | 
			
		||||
      ..writeByte(3)
 | 
			
		||||
      ..write(obj.isSaveLogin);
 | 
			
		||||
      ..write(obj.isSaveLogin)
 | 
			
		||||
      ..writeByte(4)
 | 
			
		||||
      ..write(obj.accessToken);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
@@ -74,15 +74,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Store device id to local storage
 | 
			
		||||
    var deviceInfo = await _deviceInfoService.getDeviceInfo();
 | 
			
		||||
    Hive.box(userInfoBox).put(deviceIdKey, deviceInfo["deviceId"]);
 | 
			
		||||
 | 
			
		||||
    state = state.copyWith(
 | 
			
		||||
      deviceId: deviceInfo["deviceId"],
 | 
			
		||||
      deviceType: deviceInfo["deviceType"],
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Make sign-in request
 | 
			
		||||
    try {
 | 
			
		||||
      var loginResponse = await _apiService.authenticationApi.login(
 | 
			
		||||
@@ -97,65 +88,15 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      Hive.box(userInfoBox).put(accessTokenKey, loginResponse.accessToken);
 | 
			
		||||
 | 
			
		||||
      state = state.copyWith(
 | 
			
		||||
        isAuthenticated: true,
 | 
			
		||||
        userId: loginResponse.userId,
 | 
			
		||||
        userEmail: loginResponse.userEmail,
 | 
			
		||||
        firstName: loginResponse.firstName,
 | 
			
		||||
        lastName: loginResponse.lastName,
 | 
			
		||||
        profileImagePath: loginResponse.profileImagePath,
 | 
			
		||||
        isAdmin: loginResponse.isAdmin,
 | 
			
		||||
        shouldChangePassword: loginResponse.shouldChangePassword,
 | 
			
		||||
      return setSuccessLoginInfo(
 | 
			
		||||
        accessToken: loginResponse.accessToken,
 | 
			
		||||
        isSavedLoginInfo: isSavedLoginInfo,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // Login Success - Set Access Token to API Client
 | 
			
		||||
      _apiService.setAccessToken(loginResponse.accessToken);
 | 
			
		||||
 | 
			
		||||
      if (isSavedLoginInfo) {
 | 
			
		||||
        // Save login info to local storage
 | 
			
		||||
        Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).put(
 | 
			
		||||
          savedLoginInfoKey,
 | 
			
		||||
          HiveSavedLoginInfo(
 | 
			
		||||
            email: email,
 | 
			
		||||
            password: password,
 | 
			
		||||
            isSaveLogin: true,
 | 
			
		||||
            serverUrl: Hive.box(userInfoBox).get(serverEndpointKey),
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox)
 | 
			
		||||
            .delete(savedLoginInfoKey);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      HapticFeedback.vibrate();
 | 
			
		||||
      debugPrint("Error logging in $e");
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Register device info
 | 
			
		||||
    try {
 | 
			
		||||
      DeviceInfoResponseDto? deviceInfo =
 | 
			
		||||
          await _apiService.deviceInfoApi.createDeviceInfo(
 | 
			
		||||
        CreateDeviceInfoDto(
 | 
			
		||||
          deviceId: state.deviceId,
 | 
			
		||||
          deviceType: state.deviceType,
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (deviceInfo == null) {
 | 
			
		||||
        debugPrint('Device Info Response is null');
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      state = state.copyWith(deviceInfo: deviceInfo);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("ERROR Register Device Info: $e");
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<bool> logout() async {
 | 
			
		||||
@@ -215,6 +156,74 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<bool> setSuccessLoginInfo({
 | 
			
		||||
    required String accessToken,
 | 
			
		||||
    required bool isSavedLoginInfo,
 | 
			
		||||
  }) async {
 | 
			
		||||
    Hive.box(userInfoBox).put(accessTokenKey, accessToken);
 | 
			
		||||
 | 
			
		||||
    _apiService.setAccessToken(accessToken);
 | 
			
		||||
    var userResponseDto = await _apiService.userApi.getMyUserInfo();
 | 
			
		||||
 | 
			
		||||
    if (userResponseDto != null) {
 | 
			
		||||
      var deviceInfo = await _deviceInfoService.getDeviceInfo();
 | 
			
		||||
      Hive.box(userInfoBox).put(deviceIdKey, deviceInfo["deviceId"]);
 | 
			
		||||
 | 
			
		||||
      state = state.copyWith(
 | 
			
		||||
        isAuthenticated: true,
 | 
			
		||||
        userId: userResponseDto.id,
 | 
			
		||||
        userEmail: userResponseDto.email,
 | 
			
		||||
        firstName: userResponseDto.firstName,
 | 
			
		||||
        lastName: userResponseDto.lastName,
 | 
			
		||||
        profileImagePath: userResponseDto.profileImagePath,
 | 
			
		||||
        isAdmin: userResponseDto.isAdmin,
 | 
			
		||||
        shouldChangePassword: userResponseDto.shouldChangePassword,
 | 
			
		||||
        deviceId: deviceInfo["deviceId"],
 | 
			
		||||
        deviceType: deviceInfo["deviceType"],
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (isSavedLoginInfo) {
 | 
			
		||||
        // Save login info to local storage
 | 
			
		||||
        Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).put(
 | 
			
		||||
          savedLoginInfoKey,
 | 
			
		||||
          HiveSavedLoginInfo(
 | 
			
		||||
            email: "",
 | 
			
		||||
            password: "",
 | 
			
		||||
            isSaveLogin: true,
 | 
			
		||||
            serverUrl: Hive.box(userInfoBox).get(serverEndpointKey),
 | 
			
		||||
            accessToken: accessToken,
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox)
 | 
			
		||||
            .delete(savedLoginInfoKey);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Register device info
 | 
			
		||||
    try {
 | 
			
		||||
      DeviceInfoResponseDto? deviceInfo =
 | 
			
		||||
          await _apiService.deviceInfoApi.createDeviceInfo(
 | 
			
		||||
        CreateDeviceInfoDto(
 | 
			
		||||
          deviceId: state.deviceId,
 | 
			
		||||
          deviceType: state.deviceType,
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (deviceInfo == null) {
 | 
			
		||||
        debugPrint('Device Info Response is null');
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      state = state.copyWith(deviceInfo: deviceInfo);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("ERROR Register Device Info: $e");
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final authenticationProvider =
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								mobile/lib/modules/login/providers/oauth.provider.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								mobile/lib/modules/login/providers/oauth.provider.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/login/services/oauth.service.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
 | 
			
		||||
 | 
			
		||||
final OAuthServiceProvider =
 | 
			
		||||
    Provider((ref) => OAuthService(ref.watch(apiServiceProvider)));
 | 
			
		||||
							
								
								
									
										39
									
								
								mobile/lib/modules/login/services/oauth.service.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								mobile/lib/modules/login/services/oauth.service.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
import 'package:immich_mobile/shared/services/api.service.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
import 'package:flutter_web_auth/flutter_web_auth.dart';
 | 
			
		||||
 | 
			
		||||
// Redirect URL = app.immich://
 | 
			
		||||
 | 
			
		||||
class OAuthService {
 | 
			
		||||
  final ApiService _apiService;
 | 
			
		||||
  final callbackUrlScheme = 'app.immich';
 | 
			
		||||
 | 
			
		||||
  OAuthService(this._apiService);
 | 
			
		||||
 | 
			
		||||
  Future<OAuthConfigResponseDto?> getOAuthServerConfig(
 | 
			
		||||
    String serverEndpoint,
 | 
			
		||||
  ) async {
 | 
			
		||||
    _apiService.setEndpoint(serverEndpoint);
 | 
			
		||||
 | 
			
		||||
    return await _apiService.oAuthApi.generateConfig(
 | 
			
		||||
      OAuthConfigDto(redirectUri: '$callbackUrlScheme:/'),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<LoginResponseDto?> oAuthLogin(String oauthUrl) async {
 | 
			
		||||
    try {
 | 
			
		||||
      var result = await FlutterWebAuth.authenticate(
 | 
			
		||||
        url: oauthUrl,
 | 
			
		||||
        callbackUrlScheme: callbackUrlScheme,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      return await _apiService.oAuthApi.callback(
 | 
			
		||||
        OAuthCallbackDto(
 | 
			
		||||
          url: result,
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,11 +6,14 @@ import 'package:hive/hive.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/constants/hive_box.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/login/providers/oauth.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/routing/router.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
class LoginForm extends HookConsumerWidget {
 | 
			
		||||
  const LoginForm({Key? key}) : super(key: key);
 | 
			
		||||
@@ -23,10 +26,47 @@ class LoginForm extends HookConsumerWidget {
 | 
			
		||||
        useTextEditingController.fromValue(TextEditingValue.empty);
 | 
			
		||||
    final serverEndpointController =
 | 
			
		||||
        useTextEditingController(text: 'login_form_endpoint_hint'.tr());
 | 
			
		||||
    final apiService = ref.watch(apiServiceProvider);
 | 
			
		||||
    final serverEndpointFocusNode = useFocusNode();
 | 
			
		||||
    final isSaveLoginInfo = useState<bool>(false);
 | 
			
		||||
    final isLoading = useState<bool>(false);
 | 
			
		||||
    final isOauthEnable = useState<bool>(false);
 | 
			
		||||
    final oAuthButtonLabel = useState<String>('OAuth');
 | 
			
		||||
 | 
			
		||||
    getServeLoginConfig() async {
 | 
			
		||||
      if (!serverEndpointFocusNode.hasFocus) {
 | 
			
		||||
        var urlText = serverEndpointController.text.trim();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
          var endpointUrl = Uri.tryParse(urlText);
 | 
			
		||||
 | 
			
		||||
          if (endpointUrl != null) {
 | 
			
		||||
            isLoading.value = true;
 | 
			
		||||
            apiService.setEndpoint(endpointUrl.toString());
 | 
			
		||||
            var loginConfig = await apiService.oAuthApi.generateConfig(
 | 
			
		||||
              OAuthConfigDto(redirectUri: endpointUrl.toString()),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (loginConfig != null) {
 | 
			
		||||
              isOauthEnable.value = loginConfig.enabled;
 | 
			
		||||
              oAuthButtonLabel.value = loginConfig.buttonText ?? 'OAuth';
 | 
			
		||||
            } else {
 | 
			
		||||
              isOauthEnable.value = false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            isLoading.value = false;
 | 
			
		||||
          }
 | 
			
		||||
        } catch (_) {
 | 
			
		||||
          isLoading.value = false;
 | 
			
		||||
          isOauthEnable.value = false;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    useEffect(
 | 
			
		||||
      () {
 | 
			
		||||
        serverEndpointFocusNode.addListener(getServeLoginConfig);
 | 
			
		||||
 | 
			
		||||
        var loginInfo = Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox)
 | 
			
		||||
            .get(savedLoginInfoKey);
 | 
			
		||||
 | 
			
		||||
@@ -37,6 +77,7 @@ class LoginForm extends HookConsumerWidget {
 | 
			
		||||
          isSaveLoginInfo.value = loginInfo.isSaveLogin;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        getServeLoginConfig();
 | 
			
		||||
        return null;
 | 
			
		||||
      },
 | 
			
		||||
      [],
 | 
			
		||||
@@ -67,7 +108,10 @@ class LoginForm extends HookConsumerWidget {
 | 
			
		||||
              ),
 | 
			
		||||
              EmailInput(controller: usernameController),
 | 
			
		||||
              PasswordInput(controller: passwordController),
 | 
			
		||||
              ServerEndpointInput(controller: serverEndpointController),
 | 
			
		||||
              ServerEndpointInput(
 | 
			
		||||
                controller: serverEndpointController,
 | 
			
		||||
                focusNode: serverEndpointFocusNode,
 | 
			
		||||
              ),
 | 
			
		||||
              CheckboxListTile(
 | 
			
		||||
                activeColor: Theme.of(context).primaryColor,
 | 
			
		||||
                contentPadding: const EdgeInsets.symmetric(horizontal: 8),
 | 
			
		||||
@@ -92,12 +136,52 @@ class LoginForm extends HookConsumerWidget {
 | 
			
		||||
                  }
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
              LoginButton(
 | 
			
		||||
                emailController: usernameController,
 | 
			
		||||
                passwordController: passwordController,
 | 
			
		||||
                serverEndpointController: serverEndpointController,
 | 
			
		||||
                isSavedLoginInfo: isSaveLoginInfo.value,
 | 
			
		||||
              ),
 | 
			
		||||
              if (isLoading.value)
 | 
			
		||||
                const SizedBox(
 | 
			
		||||
                  width: 24,
 | 
			
		||||
                  height: 24,
 | 
			
		||||
                  child: CircularProgressIndicator(
 | 
			
		||||
                    strokeWidth: 2,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              if (!isLoading.value)
 | 
			
		||||
                Column(
 | 
			
		||||
                  crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
                  mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    LoginButton(
 | 
			
		||||
                      emailController: usernameController,
 | 
			
		||||
                      passwordController: passwordController,
 | 
			
		||||
                      serverEndpointController: serverEndpointController,
 | 
			
		||||
                      isSavedLoginInfo: isSaveLoginInfo.value,
 | 
			
		||||
                    ),
 | 
			
		||||
                    if (isOauthEnable.value) ...[
 | 
			
		||||
                      Padding(
 | 
			
		||||
                        padding: const EdgeInsets.symmetric(
 | 
			
		||||
                          horizontal: 16.0,
 | 
			
		||||
                        ),
 | 
			
		||||
                        child: Divider(
 | 
			
		||||
                          color: Brightness.dark == Theme.of(context).brightness
 | 
			
		||||
                              ? Colors.white
 | 
			
		||||
                              : Colors.black,
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                      OAuthLoginButton(
 | 
			
		||||
                        serverEndpointController: serverEndpointController,
 | 
			
		||||
                        isSavedLoginInfo: isSaveLoginInfo.value,
 | 
			
		||||
                        buttonLabel: oAuthButtonLabel.value,
 | 
			
		||||
                        isLoading: isLoading,
 | 
			
		||||
                        onLoginSuccess: () {
 | 
			
		||||
                          isLoading.value = false;
 | 
			
		||||
                          ref.watch(backupProvider.notifier).resumeBackup();
 | 
			
		||||
                          AutoRouter.of(context).replace(
 | 
			
		||||
                            const TabControllerRoute(),
 | 
			
		||||
                          );
 | 
			
		||||
                        },
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ],
 | 
			
		||||
                )
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
@@ -108,9 +192,12 @@ class LoginForm extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
class ServerEndpointInput extends StatelessWidget {
 | 
			
		||||
  final TextEditingController controller;
 | 
			
		||||
 | 
			
		||||
  const ServerEndpointInput({Key? key, required this.controller})
 | 
			
		||||
      : super(key: key);
 | 
			
		||||
  final FocusNode focusNode;
 | 
			
		||||
  const ServerEndpointInput({
 | 
			
		||||
    Key? key,
 | 
			
		||||
    required this.controller,
 | 
			
		||||
    required this.focusNode,
 | 
			
		||||
  }) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  String? _validateInput(String? url) {
 | 
			
		||||
    if (url?.startsWith(RegExp(r'https?://')) == true) {
 | 
			
		||||
@@ -131,6 +218,7 @@ class ServerEndpointInput extends StatelessWidget {
 | 
			
		||||
      ),
 | 
			
		||||
      validator: _validateInput,
 | 
			
		||||
      autovalidateMode: AutovalidateMode.always,
 | 
			
		||||
      focusNode: focusNode,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -200,13 +288,9 @@ class LoginButton extends ConsumerWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    return ElevatedButton(
 | 
			
		||||
    return ElevatedButton.icon(
 | 
			
		||||
      style: ElevatedButton.styleFrom(
 | 
			
		||||
        visualDensity: VisualDensity.standard,
 | 
			
		||||
        backgroundColor: Theme.of(context).primaryColor,
 | 
			
		||||
        foregroundColor: Colors.grey[50],
 | 
			
		||||
        elevation: 2,
 | 
			
		||||
        padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
 | 
			
		||||
        padding: const EdgeInsets.symmetric(vertical: 12),
 | 
			
		||||
      ),
 | 
			
		||||
      onPressed: () async {
 | 
			
		||||
        // This will remove current cache asset state of previous user login.
 | 
			
		||||
@@ -238,10 +322,101 @@ class LoginButton extends ConsumerWidget {
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      child: const Text(
 | 
			
		||||
      icon: const Icon(Icons.login_rounded),
 | 
			
		||||
      label: const Text(
 | 
			
		||||
        "login_form_button_text",
 | 
			
		||||
        style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
 | 
			
		||||
      ).tr(),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class OAuthLoginButton extends ConsumerWidget {
 | 
			
		||||
  final TextEditingController serverEndpointController;
 | 
			
		||||
  final bool isSavedLoginInfo;
 | 
			
		||||
  final ValueNotifier<bool> isLoading;
 | 
			
		||||
  final VoidCallback onLoginSuccess;
 | 
			
		||||
  final String buttonLabel;
 | 
			
		||||
 | 
			
		||||
  const OAuthLoginButton({
 | 
			
		||||
    Key? key,
 | 
			
		||||
    required this.serverEndpointController,
 | 
			
		||||
    required this.isSavedLoginInfo,
 | 
			
		||||
    required this.isLoading,
 | 
			
		||||
    required this.onLoginSuccess,
 | 
			
		||||
    required this.buttonLabel,
 | 
			
		||||
  }) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    var oAuthService = ref.watch(OAuthServiceProvider);
 | 
			
		||||
 | 
			
		||||
    void performOAuthLogin() async {
 | 
			
		||||
      ref.watch(assetProvider.notifier).clearAllAsset();
 | 
			
		||||
      OAuthConfigResponseDto? oAuthServerConfig;
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        oAuthServerConfig = await oAuthService
 | 
			
		||||
            .getOAuthServerConfig(serverEndpointController.text);
 | 
			
		||||
 | 
			
		||||
        isLoading.value = true;
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        ImmichToast.show(
 | 
			
		||||
          context: context,
 | 
			
		||||
          msg: "login_form_failed_get_oauth_server_config".tr(),
 | 
			
		||||
          toastType: ToastType.error,
 | 
			
		||||
        );
 | 
			
		||||
        isLoading.value = false;
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (oAuthServerConfig != null && oAuthServerConfig.enabled) {
 | 
			
		||||
        var loginResponseDto =
 | 
			
		||||
            await oAuthService.oAuthLogin(oAuthServerConfig.url!);
 | 
			
		||||
 | 
			
		||||
        if (loginResponseDto != null) {
 | 
			
		||||
          var isSuccess = await ref
 | 
			
		||||
              .watch(authenticationProvider.notifier)
 | 
			
		||||
              .setSuccessLoginInfo(
 | 
			
		||||
                accessToken: loginResponseDto.accessToken,
 | 
			
		||||
                isSavedLoginInfo: isSavedLoginInfo,
 | 
			
		||||
              );
 | 
			
		||||
 | 
			
		||||
          if (isSuccess) {
 | 
			
		||||
            isLoading.value = false;
 | 
			
		||||
            onLoginSuccess();
 | 
			
		||||
          } else {
 | 
			
		||||
            ImmichToast.show(
 | 
			
		||||
              context: context,
 | 
			
		||||
              msg: "login_form_failed_login".tr(),
 | 
			
		||||
              toastType: ToastType.error,
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        isLoading.value = false;
 | 
			
		||||
      } else {
 | 
			
		||||
        ImmichToast.show(
 | 
			
		||||
          context: context,
 | 
			
		||||
          msg: "login_form_failed_get_oauth_server_disable".tr(),
 | 
			
		||||
          toastType: ToastType.info,
 | 
			
		||||
        );
 | 
			
		||||
        isLoading.value = false;
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return ElevatedButton.icon(
 | 
			
		||||
      style: ElevatedButton.styleFrom(
 | 
			
		||||
        backgroundColor: Theme.of(context).primaryColor.withAlpha(230),
 | 
			
		||||
        padding: const EdgeInsets.symmetric(vertical: 12),
 | 
			
		||||
      ),
 | 
			
		||||
      onPressed: performOAuthLogin,
 | 
			
		||||
      icon: const Icon(Icons.pin_rounded),
 | 
			
		||||
      label: Text(
 | 
			
		||||
        buttonLabel,
 | 
			
		||||
        style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ class ApiService {
 | 
			
		||||
 | 
			
		||||
  late UserApi userApi;
 | 
			
		||||
  late AuthenticationApi authenticationApi;
 | 
			
		||||
  late OAuthApi oAuthApi;
 | 
			
		||||
  late AlbumApi albumApi;
 | 
			
		||||
  late AssetApi assetApi;
 | 
			
		||||
  late ServerInfoApi serverInfoApi;
 | 
			
		||||
@@ -14,6 +15,7 @@ class ApiService {
 | 
			
		||||
    _apiClient = ApiClient(basePath: endpoint);
 | 
			
		||||
    userApi = UserApi(_apiClient);
 | 
			
		||||
    authenticationApi = AuthenticationApi(_apiClient);
 | 
			
		||||
    oAuthApi = OAuthApi(_apiClient);
 | 
			
		||||
    albumApi = AlbumApi(_apiClient);
 | 
			
		||||
    assetApi = AssetApi(_apiClient);
 | 
			
		||||
    serverInfoApi = ServerInfoApi(_apiClient);
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ class ImmichToast {
 | 
			
		||||
    required String msg,
 | 
			
		||||
    ToastType toastType = ToastType.info,
 | 
			
		||||
    ToastGravity gravity = ToastGravity.TOP,
 | 
			
		||||
    int durationInSecond = 3,
 | 
			
		||||
  }) {
 | 
			
		||||
    final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
 | 
			
		||||
    final fToast = FToast();
 | 
			
		||||
@@ -77,7 +78,7 @@ class ImmichToast {
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      gravity: gravity,
 | 
			
		||||
      toastDuration: const Duration(seconds: 2),
 | 
			
		||||
      toastDuration: Duration(seconds: durationInSecond),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,30 +8,34 @@ import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/routing/router.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
 | 
			
		||||
 | 
			
		||||
class SplashScreenPage extends HookConsumerWidget {
 | 
			
		||||
  const SplashScreenPage({Key? key}) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final apiService = ref.watch(apiServiceProvider);
 | 
			
		||||
    HiveSavedLoginInfo? loginInfo =
 | 
			
		||||
        Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
 | 
			
		||||
 | 
			
		||||
    void performLoggingIn() async {
 | 
			
		||||
      var isAuthenticated =
 | 
			
		||||
          await ref.read(authenticationProvider.notifier).login(
 | 
			
		||||
                loginInfo!.email,
 | 
			
		||||
                loginInfo.password,
 | 
			
		||||
                loginInfo.serverUrl,
 | 
			
		||||
                true,
 | 
			
		||||
              );
 | 
			
		||||
      if (loginInfo != null) {
 | 
			
		||||
        // Make sure API service is initialized
 | 
			
		||||
        apiService.setEndpoint(loginInfo.serverUrl);
 | 
			
		||||
 | 
			
		||||
      if (isAuthenticated) {
 | 
			
		||||
        // Resume backup (if enable) then navigate
 | 
			
		||||
        ref.watch(backupProvider.notifier).resumeBackup();
 | 
			
		||||
        AutoRouter.of(context).replace(const TabControllerRoute());
 | 
			
		||||
      } else {
 | 
			
		||||
        AutoRouter.of(context).replace(const LoginRoute());
 | 
			
		||||
        var isSuccess =
 | 
			
		||||
            await ref.read(authenticationProvider.notifier).setSuccessLoginInfo(
 | 
			
		||||
                  accessToken: loginInfo.accessToken,
 | 
			
		||||
                  isSavedLoginInfo: true,
 | 
			
		||||
                );
 | 
			
		||||
        if (isSuccess) {
 | 
			
		||||
          // Resume backup (if enable) then navigate
 | 
			
		||||
          ref.watch(backupProvider.notifier).resumeBackup();
 | 
			
		||||
          AutoRouter.of(context).replace(const TabControllerRoute());
 | 
			
		||||
        } else {
 | 
			
		||||
          AutoRouter.of(context).replace(const LoginRoute());
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -43,43 +43,46 @@ class UserResponseDto {
 | 
			
		||||
  DateTime? deletedAt;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool operator ==(Object other) => identical(this, other) || other is UserResponseDto &&
 | 
			
		||||
     other.id == id &&
 | 
			
		||||
     other.email == email &&
 | 
			
		||||
     other.firstName == firstName &&
 | 
			
		||||
     other.lastName == lastName &&
 | 
			
		||||
     other.createdAt == createdAt &&
 | 
			
		||||
     other.profileImagePath == profileImagePath &&
 | 
			
		||||
     other.shouldChangePassword == shouldChangePassword &&
 | 
			
		||||
     other.isAdmin == isAdmin &&
 | 
			
		||||
     other.deletedAt == deletedAt;
 | 
			
		||||
  bool operator ==(Object other) =>
 | 
			
		||||
      identical(this, other) ||
 | 
			
		||||
      other is UserResponseDto &&
 | 
			
		||||
          other.id == id &&
 | 
			
		||||
          other.email == email &&
 | 
			
		||||
          other.firstName == firstName &&
 | 
			
		||||
          other.lastName == lastName &&
 | 
			
		||||
          other.createdAt == createdAt &&
 | 
			
		||||
          other.profileImagePath == profileImagePath &&
 | 
			
		||||
          other.shouldChangePassword == shouldChangePassword &&
 | 
			
		||||
          other.isAdmin == isAdmin &&
 | 
			
		||||
          other.deletedAt == deletedAt;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode =>
 | 
			
		||||
    // ignore: unnecessary_parenthesis
 | 
			
		||||
    (id.hashCode) +
 | 
			
		||||
    (email.hashCode) +
 | 
			
		||||
    (firstName.hashCode) +
 | 
			
		||||
    (lastName.hashCode) +
 | 
			
		||||
    (createdAt.hashCode) +
 | 
			
		||||
    (profileImagePath.hashCode) +
 | 
			
		||||
    (shouldChangePassword.hashCode) +
 | 
			
		||||
    (isAdmin.hashCode) +
 | 
			
		||||
    (deletedAt == null ? 0 : deletedAt!.hashCode);
 | 
			
		||||
      // ignore: unnecessary_parenthesis
 | 
			
		||||
      (id.hashCode) +
 | 
			
		||||
      (email.hashCode) +
 | 
			
		||||
      (firstName.hashCode) +
 | 
			
		||||
      (lastName.hashCode) +
 | 
			
		||||
      (createdAt.hashCode) +
 | 
			
		||||
      (profileImagePath.hashCode) +
 | 
			
		||||
      (shouldChangePassword.hashCode) +
 | 
			
		||||
      (isAdmin.hashCode) +
 | 
			
		||||
      (deletedAt == null ? 0 : deletedAt!.hashCode);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() => 'UserResponseDto[id=$id, email=$email, firstName=$firstName, lastName=$lastName, createdAt=$createdAt, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, isAdmin=$isAdmin, deletedAt=$deletedAt]';
 | 
			
		||||
  String toString() =>
 | 
			
		||||
      'UserResponseDto[id=$id, email=$email, firstName=$firstName, lastName=$lastName, createdAt=$createdAt, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, isAdmin=$isAdmin, deletedAt=$deletedAt]';
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toJson() {
 | 
			
		||||
    final _json = <String, dynamic>{};
 | 
			
		||||
      _json[r'id'] = id;
 | 
			
		||||
      _json[r'email'] = email;
 | 
			
		||||
      _json[r'firstName'] = firstName;
 | 
			
		||||
      _json[r'lastName'] = lastName;
 | 
			
		||||
      _json[r'createdAt'] = createdAt;
 | 
			
		||||
      _json[r'profileImagePath'] = profileImagePath;
 | 
			
		||||
      _json[r'shouldChangePassword'] = shouldChangePassword;
 | 
			
		||||
      _json[r'isAdmin'] = isAdmin;
 | 
			
		||||
    _json[r'id'] = id;
 | 
			
		||||
    _json[r'email'] = email;
 | 
			
		||||
    _json[r'firstName'] = firstName;
 | 
			
		||||
    _json[r'lastName'] = lastName;
 | 
			
		||||
    _json[r'createdAt'] = createdAt;
 | 
			
		||||
    _json[r'profileImagePath'] = profileImagePath;
 | 
			
		||||
    _json[r'shouldChangePassword'] = shouldChangePassword;
 | 
			
		||||
    _json[r'isAdmin'] = isAdmin;
 | 
			
		||||
    if (deletedAt != null) {
 | 
			
		||||
      _json[r'deletedAt'] = deletedAt!.toUtc().toIso8601String();
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -98,13 +101,13 @@ class UserResponseDto {
 | 
			
		||||
      // Ensure that the map contains the required keys.
 | 
			
		||||
      // Note 1: the values aren't checked for validity beyond being non-null.
 | 
			
		||||
      // Note 2: this code is stripped in release mode!
 | 
			
		||||
      assert(() {
 | 
			
		||||
        requiredKeys.forEach((key) {
 | 
			
		||||
          assert(json.containsKey(key), 'Required key "UserResponseDto[$key]" is missing from JSON.');
 | 
			
		||||
          assert(json[key] != null, 'Required key "UserResponseDto[$key]" has a null value in JSON.');
 | 
			
		||||
        });
 | 
			
		||||
        return true;
 | 
			
		||||
      }());
 | 
			
		||||
      // assert(() {
 | 
			
		||||
      //   requiredKeys.forEach((key) {
 | 
			
		||||
      //     assert(json.containsKey(key), 'Required key "UserResponseDto[$key]" is missing from JSON.');
 | 
			
		||||
      //     assert(json[key] != null, 'Required key "UserResponseDto[$key]" has a null value in JSON.');
 | 
			
		||||
      //   });
 | 
			
		||||
      //   return true;
 | 
			
		||||
      // }());
 | 
			
		||||
 | 
			
		||||
      return UserResponseDto(
 | 
			
		||||
        id: mapValueOfType<String>(json, r'id')!,
 | 
			
		||||
@@ -113,7 +116,8 @@ class UserResponseDto {
 | 
			
		||||
        lastName: mapValueOfType<String>(json, r'lastName')!,
 | 
			
		||||
        createdAt: mapValueOfType<String>(json, r'createdAt')!,
 | 
			
		||||
        profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
 | 
			
		||||
        shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
 | 
			
		||||
        shouldChangePassword:
 | 
			
		||||
            mapValueOfType<bool>(json, r'shouldChangePassword')!,
 | 
			
		||||
        isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
 | 
			
		||||
        deletedAt: mapDateTime(json, r'deletedAt', ''),
 | 
			
		||||
      );
 | 
			
		||||
@@ -121,7 +125,10 @@ class UserResponseDto {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static List<UserResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
 | 
			
		||||
  static List<UserResponseDto>? listFromJson(
 | 
			
		||||
    dynamic json, {
 | 
			
		||||
    bool growable = false,
 | 
			
		||||
  }) {
 | 
			
		||||
    final result = <UserResponseDto>[];
 | 
			
		||||
    if (json is List && json.isNotEmpty) {
 | 
			
		||||
      for (final row in json) {
 | 
			
		||||
@@ -149,12 +156,18 @@ class UserResponseDto {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // maps a json object with a list of UserResponseDto-objects as value to a dart map
 | 
			
		||||
  static Map<String, List<UserResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
			
		||||
  static Map<String, List<UserResponseDto>> mapListFromJson(
 | 
			
		||||
    dynamic json, {
 | 
			
		||||
    bool growable = false,
 | 
			
		||||
  }) {
 | 
			
		||||
    final map = <String, List<UserResponseDto>>{};
 | 
			
		||||
    if (json is Map && json.isNotEmpty) {
 | 
			
		||||
      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
			
		||||
      for (final entry in json.entries) {
 | 
			
		||||
        final value = UserResponseDto.listFromJson(entry.value, growable: growable,);
 | 
			
		||||
        final value = UserResponseDto.listFromJson(
 | 
			
		||||
          entry.value,
 | 
			
		||||
          growable: growable,
 | 
			
		||||
        );
 | 
			
		||||
        if (value != null) {
 | 
			
		||||
          map[entry.key] = value;
 | 
			
		||||
        }
 | 
			
		||||
@@ -176,4 +189,3 @@ class UserResponseDto {
 | 
			
		||||
    'deletedAt',
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -366,6 +366,13 @@ packages:
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.0.0"
 | 
			
		||||
  flutter_web_auth:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_web_auth
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.5.0"
 | 
			
		||||
  flutter_web_plugins:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description: flutter
 | 
			
		||||
 
 | 
			
		||||
@@ -40,6 +40,7 @@ dependencies:
 | 
			
		||||
  latlong2: ^0.8.1
 | 
			
		||||
  collection: ^1.16.0
 | 
			
		||||
  http_parser: ^4.0.1
 | 
			
		||||
  flutter_web_auth: ^0.5.0
 | 
			
		||||
 | 
			
		||||
  openapi:
 | 
			
		||||
    path: openapi
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import { ApiTags } from '@nestjs/swagger';
 | 
			
		||||
import { Response } from 'express';
 | 
			
		||||
import { AuthType } from '../../constants/jwt.constant';
 | 
			
		||||
import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
 | 
			
		||||
import { LoginResponseDto } from '../auth/response-dto/login-response.dto';
 | 
			
		||||
import { OAuthCallbackDto } from './dto/oauth-auth-code.dto';
 | 
			
		||||
import { OAuthConfigDto } from './dto/oauth-config.dto';
 | 
			
		||||
import { OAuthService } from './oauth.service';
 | 
			
		||||
@@ -19,7 +20,10 @@ export class OAuthController {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Post('/callback')
 | 
			
		||||
  public async callback(@Res({ passthrough: true }) response: Response, @Body(ValidationPipe) dto: OAuthCallbackDto) {
 | 
			
		||||
  public async callback(
 | 
			
		||||
    @Res({ passthrough: true }) response: Response,
 | 
			
		||||
    @Body(ValidationPipe) dto: OAuthCallbackDto,
 | 
			
		||||
  ): Promise<LoginResponseDto> {
 | 
			
		||||
    const loginResponse = await this.oauthService.callback(dto);
 | 
			
		||||
    response.setHeader('Set-Cookie', this.immichJwtService.getCookies(loginResponse, AuthType.OAUTH));
 | 
			
		||||
    return loginResponse;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user