mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +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:
parent
e01e4e6530
commit
b3e51cc849
19 changed files with 384 additions and 105 deletions
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,
|
||||
});
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Binary file not shown.
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue