mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +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 443 additions and 149 deletions
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue