1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-17 01:06:46 +01: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:
Alex 2022-11-20 11:43:10 -06:00 committed by GitHub
parent e01e4e6530
commit b3e51cc849
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 384 additions and 105 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View file

@ -28,9 +28,17 @@ Before enabling OAuth in Immich, a new client application needs to be configured
2. Configure Redirect URIs/Origins 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 ## Enable OAuth

View file

@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
android { android {
compileSdkVersion flutter.compileSdkVersion compileSdkVersion 33
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8

View file

@ -12,15 +12,26 @@
</intent-filter> </intent-filter>
</activity> </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. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data android:name="flutterEmbedding" android:value="2" /> <meta-data android:name="flutterEmbedding" android:value="2" />
<!-- Disables default WorkManager initialization to use our custom initialization --> <!-- Disables default WorkManager initialization to use our custom initialization -->
<provider <provider
android:name="androidx.startup.InitializationProvider" android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup" android:authorities="${applicationId}.androidx-startup"
tools:node="remove"> tools:node="remove"></provider>
</provider>
</application> </application>
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

View file

@ -109,7 +109,9 @@
"login_form_err_invalid_email": "Invalid Email", "login_form_err_invalid_email": "Invalid Email",
"login_form_err_leading_whitespace": "Leading whitespace", "login_form_err_leading_whitespace": "Leading whitespace",
"login_form_err_trailing_whitespace": "Trailing 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_email": "Email",
"login_form_label_password": "Password", "login_form_label_password": "Password",
"login_form_password_hint": "password", "login_form_password_hint": "password",

View file

@ -3,6 +3,8 @@ PODS:
- flutter_udid (0.0.1): - flutter_udid (0.0.1):
- Flutter - Flutter
- SAMKeychain - SAMKeychain
- flutter_web_auth (0.5.0):
- Flutter
- fluttertoast (0.0.2): - fluttertoast (0.0.2):
- Flutter - Flutter
- Toast - Toast
@ -37,6 +39,7 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- flutter_web_auth (from `.symlinks/plugins/flutter_web_auth/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
@ -60,6 +63,8 @@ EXTERNAL SOURCES:
:path: Flutter :path: Flutter
flutter_udid: flutter_udid:
:path: ".symlinks/plugins/flutter_udid/ios" :path: ".symlinks/plugins/flutter_udid/ios"
flutter_web_auth:
:path: ".symlinks/plugins/flutter_web_auth/ios"
fluttertoast: fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios" :path: ".symlinks/plugins/fluttertoast/ios"
image_picker_ios: image_picker_ios:
@ -86,6 +91,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d
fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037 fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb

View file

@ -5,21 +5,25 @@ part 'hive_saved_login_info.model.g.dart';
@HiveType(typeId: 0) @HiveType(typeId: 0)
class HiveSavedLoginInfo { class HiveSavedLoginInfo {
@HiveField(0) @HiveField(0)
String email; String email; // DEPRECATED
@HiveField(1) @HiveField(1)
String password; String password; // DEPRECATED
@HiveField(2) @HiveField(2)
String serverUrl; String serverUrl;
@HiveField(3) @HiveField(3, defaultValue: false)
bool isSaveLogin; bool isSaveLogin;
@HiveField(4, defaultValue: "")
String accessToken;
HiveSavedLoginInfo({ HiveSavedLoginInfo({
required this.email, required this.email,
required this.password, required this.password,
required this.serverUrl, required this.serverUrl,
required this.isSaveLogin, required this.isSaveLogin,
required this.accessToken,
}); });
} }

View file

@ -74,15 +74,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
return false; 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 // Make sign-in request
try { try {
var loginResponse = await _apiService.authenticationApi.login( var loginResponse = await _apiService.authenticationApi.login(
@ -97,65 +88,15 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
return false; return false;
} }
Hive.box(userInfoBox).put(accessTokenKey, loginResponse.accessToken); return setSuccessLoginInfo(
accessToken: loginResponse.accessToken,
state = state.copyWith( isSavedLoginInfo: isSavedLoginInfo,
isAuthenticated: true,
userId: loginResponse.userId,
userEmail: loginResponse.userEmail,
firstName: loginResponse.firstName,
lastName: loginResponse.lastName,
profileImagePath: loginResponse.profileImagePath,
isAdmin: loginResponse.isAdmin,
shouldChangePassword: loginResponse.shouldChangePassword,
); );
// 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) { } catch (e) {
HapticFeedback.vibrate(); HapticFeedback.vibrate();
debugPrint("Error logging in $e"); debugPrint("Error logging in $e");
return false; 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 { Future<bool> logout() async {
@ -215,6 +156,74 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
return false; 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 = final authenticationProvider =

View 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)));

View 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;
}
}
}

View file

@ -6,11 +6,14 @@ import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.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/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/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/shared/providers/asset.provider.dart';
import 'package:immich_mobile/modules/login/providers/authentication.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/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:openapi/api.dart';
class LoginForm extends HookConsumerWidget { class LoginForm extends HookConsumerWidget {
const LoginForm({Key? key}) : super(key: key); const LoginForm({Key? key}) : super(key: key);
@ -23,10 +26,47 @@ class LoginForm extends HookConsumerWidget {
useTextEditingController.fromValue(TextEditingValue.empty); useTextEditingController.fromValue(TextEditingValue.empty);
final serverEndpointController = final serverEndpointController =
useTextEditingController(text: 'login_form_endpoint_hint'.tr()); useTextEditingController(text: 'login_form_endpoint_hint'.tr());
final apiService = ref.watch(apiServiceProvider);
final serverEndpointFocusNode = useFocusNode();
final isSaveLoginInfo = useState<bool>(false); 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( useEffect(
() { () {
serverEndpointFocusNode.addListener(getServeLoginConfig);
var loginInfo = Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox) var loginInfo = Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox)
.get(savedLoginInfoKey); .get(savedLoginInfoKey);
@ -37,6 +77,7 @@ class LoginForm extends HookConsumerWidget {
isSaveLoginInfo.value = loginInfo.isSaveLogin; isSaveLoginInfo.value = loginInfo.isSaveLogin;
} }
getServeLoginConfig();
return null; return null;
}, },
[], [],
@ -67,7 +108,10 @@ class LoginForm extends HookConsumerWidget {
), ),
EmailInput(controller: usernameController), EmailInput(controller: usernameController),
PasswordInput(controller: passwordController), PasswordInput(controller: passwordController),
ServerEndpointInput(controller: serverEndpointController), ServerEndpointInput(
controller: serverEndpointController,
focusNode: serverEndpointFocusNode,
),
CheckboxListTile( CheckboxListTile(
activeColor: Theme.of(context).primaryColor, activeColor: Theme.of(context).primaryColor,
contentPadding: const EdgeInsets.symmetric(horizontal: 8), contentPadding: const EdgeInsets.symmetric(horizontal: 8),
@ -92,12 +136,52 @@ class LoginForm extends HookConsumerWidget {
} }
}, },
), ),
LoginButton( if (isLoading.value)
emailController: usernameController, const SizedBox(
passwordController: passwordController, width: 24,
serverEndpointController: serverEndpointController, height: 24,
isSavedLoginInfo: isSaveLoginInfo.value, 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 { class ServerEndpointInput extends StatelessWidget {
final TextEditingController controller; final TextEditingController controller;
final FocusNode focusNode;
const ServerEndpointInput({Key? key, required this.controller}) const ServerEndpointInput({
: super(key: key); Key? key,
required this.controller,
required this.focusNode,
}) : super(key: key);
String? _validateInput(String? url) { String? _validateInput(String? url) {
if (url?.startsWith(RegExp(r'https?://')) == true) { if (url?.startsWith(RegExp(r'https?://')) == true) {
@ -131,6 +218,7 @@ class ServerEndpointInput extends StatelessWidget {
), ),
validator: _validateInput, validator: _validateInput,
autovalidateMode: AutovalidateMode.always, autovalidateMode: AutovalidateMode.always,
focusNode: focusNode,
); );
} }
} }
@ -200,13 +288,9 @@ class LoginButton extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton( return ElevatedButton.icon(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
visualDensity: VisualDensity.standard, padding: const EdgeInsets.symmetric(vertical: 12),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.grey[50],
elevation: 2,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
), ),
onPressed: () async { onPressed: () async {
// This will remove current cache asset state of previous user login. // 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", "login_form_button_text",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
).tr(), ).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),
),
);
}
}

View file

@ -5,6 +5,7 @@ class ApiService {
late UserApi userApi; late UserApi userApi;
late AuthenticationApi authenticationApi; late AuthenticationApi authenticationApi;
late OAuthApi oAuthApi;
late AlbumApi albumApi; late AlbumApi albumApi;
late AssetApi assetApi; late AssetApi assetApi;
late ServerInfoApi serverInfoApi; late ServerInfoApi serverInfoApi;
@ -14,6 +15,7 @@ class ApiService {
_apiClient = ApiClient(basePath: endpoint); _apiClient = ApiClient(basePath: endpoint);
userApi = UserApi(_apiClient); userApi = UserApi(_apiClient);
authenticationApi = AuthenticationApi(_apiClient); authenticationApi = AuthenticationApi(_apiClient);
oAuthApi = OAuthApi(_apiClient);
albumApi = AlbumApi(_apiClient); albumApi = AlbumApi(_apiClient);
assetApi = AssetApi(_apiClient); assetApi = AssetApi(_apiClient);
serverInfoApi = ServerInfoApi(_apiClient); serverInfoApi = ServerInfoApi(_apiClient);

View file

@ -9,6 +9,7 @@ class ImmichToast {
required String msg, required String msg,
ToastType toastType = ToastType.info, ToastType toastType = ToastType.info,
ToastGravity gravity = ToastGravity.TOP, ToastGravity gravity = ToastGravity.TOP,
int durationInSecond = 3,
}) { }) {
final isDarkTheme = Theme.of(context).brightness == Brightness.dark; final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
final fToast = FToast(); final fToast = FToast();
@ -77,7 +78,7 @@ class ImmichToast {
), ),
), ),
gravity: gravity, gravity: gravity,
toastDuration: const Duration(seconds: 2), toastDuration: Duration(seconds: durationInSecond),
); );
} }
} }

View file

@ -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/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
class SplashScreenPage extends HookConsumerWidget { class SplashScreenPage extends HookConsumerWidget {
const SplashScreenPage({Key? key}) : super(key: key); const SplashScreenPage({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final apiService = ref.watch(apiServiceProvider);
HiveSavedLoginInfo? loginInfo = HiveSavedLoginInfo? loginInfo =
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey); Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
void performLoggingIn() async { void performLoggingIn() async {
var isAuthenticated = if (loginInfo != null) {
await ref.read(authenticationProvider.notifier).login( // Make sure API service is initialized
loginInfo!.email, apiService.setEndpoint(loginInfo.serverUrl);
loginInfo.password,
loginInfo.serverUrl,
true,
);
if (isAuthenticated) { var isSuccess =
// Resume backup (if enable) then navigate await ref.read(authenticationProvider.notifier).setSuccessLoginInfo(
ref.watch(backupProvider.notifier).resumeBackup(); accessToken: loginInfo.accessToken,
AutoRouter.of(context).replace(const TabControllerRoute()); isSavedLoginInfo: true,
} else { );
AutoRouter.of(context).replace(const LoginRoute()); 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());
}
} }
} }

View file

@ -366,6 +366,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0" 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: flutter_web_plugins:
dependency: transitive dependency: transitive
description: flutter description: flutter

View file

@ -40,6 +40,7 @@ dependencies:
latlong2: ^0.8.1 latlong2: ^0.8.1
collection: ^1.16.0 collection: ^1.16.0
http_parser: ^4.0.1 http_parser: ^4.0.1
flutter_web_auth: ^0.5.0
openapi: openapi:
path: openapi path: openapi

View file

@ -3,6 +3,7 @@ import { ApiTags } from '@nestjs/swagger';
import { Response } from 'express'; import { Response } from 'express';
import { AuthType } from '../../constants/jwt.constant'; import { AuthType } from '../../constants/jwt.constant';
import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; 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 { OAuthCallbackDto } from './dto/oauth-auth-code.dto';
import { OAuthConfigDto } from './dto/oauth-config.dto'; import { OAuthConfigDto } from './dto/oauth-config.dto';
import { OAuthService } from './oauth.service'; import { OAuthService } from './oauth.service';
@ -19,7 +20,10 @@ export class OAuthController {
} }
@Post('/callback') @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); const loginResponse = await this.oauthService.callback(dto);
response.setHeader('Set-Cookie', this.immichJwtService.getCookies(loginResponse, AuthType.OAUTH)); response.setHeader('Set-Cookie', this.immichJwtService.getCookies(loginResponse, AuthType.OAUTH));
return loginResponse; return loginResponse;