From 51ead3b85d98544719eb2acdeea322db256f6930 Mon Sep 17 00:00:00 2001 From: jld3103 Date: Sun, 9 Jul 2023 18:54:55 +0200 Subject: [PATCH] neon,app: Handle qrcode login from other apps --- .../android/app/src/main/AndroidManifest.xml | 7 ++ .../neon/neon/lib/src/pages/login_qrcode.dart | 70 +++++++++++++++---- packages/neon/neon/lib/src/router.dart | 34 +++++++++ packages/neon/neon/lib/src/router.g.dart | 23 ++++++ .../neon/neon/lib/src/utils/login_qrcode.dart | 47 +++++++++++++ 5 files changed, 166 insertions(+), 15 deletions(-) create mode 100644 packages/neon/neon/lib/src/utils/login_qrcode.dart diff --git a/packages/app/android/app/src/main/AndroidManifest.xml b/packages/app/android/app/src/main/AndroidManifest.xml index 86bb0637..197488d0 100644 --- a/packages/app/android/app/src/main/AndroidManifest.xml +++ b/packages/app/android/app/src/main/AndroidManifest.xml @@ -28,6 +28,13 @@ + + + + + + + diff --git a/packages/neon/neon/lib/src/pages/login_qrcode.dart b/packages/neon/neon/lib/src/pages/login_qrcode.dart index e1810175..08797c14 100644 --- a/packages/neon/neon/lib/src/pages/login_qrcode.dart +++ b/packages/neon/neon/lib/src/pages/login_qrcode.dart @@ -1,7 +1,10 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_zxing/flutter_zxing.dart'; import 'package:neon/src/router.dart'; import 'package:neon/src/utils/exceptions.dart'; +import 'package:neon/src/utils/login_qrcode.dart'; import 'package:neon/src/widgets/exception.dart'; class LoginQrcodePage extends StatefulWidget { @@ -14,7 +17,6 @@ class LoginQrcodePage extends StatefulWidget { } class _LoginQrcodePageState extends State { - final _urlRegex = RegExp(r'^nc://login/user:(.*)&password:(.*)&server:(.*)$'); String? _lastErrorURL; @override @@ -35,22 +37,11 @@ class _LoginQrcodePageState extends State { if (url == null) { throw InvalidQrcodeException(); } - final match = _urlRegex.allMatches(url).single; - if (match.groupCount != 3) { + final match = LoginQrcode.tryParse(url); + if (match == null) { throw InvalidQrcodeException(); } - final loginName = match.group(1)!; - final password = match.group(2)!; - final serverURL = match.group(3)!; - - final result = await LoginCheckServerStatusRoute(serverURL: serverURL).push(context); - if ((result ?? false) && mounted) { - LoginCheckAccountRoute( - serverURL: serverURL, - loginName: loginName, - password: password, - ).pushReplacement(context); - } + await processLoginQrcode(context, match); } catch (e, s) { if (_lastErrorURL != url) { debugPrint(e.toString()); @@ -64,3 +55,52 @@ class _LoginQrcodePageState extends State { ), ); } + +class LoginQrcodeIntermediatePage extends StatefulWidget { + const LoginQrcodeIntermediatePage({ + required this.serverURL, + required this.loginName, + required this.password, + super.key, + }); + + final String serverURL; + final String loginName; + final String password; + + @override + State createState() => _LoginQrcodeIntermediatePageState(); +} + +class _LoginQrcodeIntermediatePageState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((final _) { + unawaited( + processLoginQrcode( + context, + LoginQrcode( + server: widget.serverURL, + user: widget.loginName, + password: widget.password, + ), + ), + ); + }); + } + + @override + Widget build(final BuildContext context) => const SizedBox(); +} + +Future processLoginQrcode(final BuildContext context, final LoginQrcode qrcode) async { + final result = await LoginCheckServerStatusRoute(serverURL: qrcode.server).push(context); + if ((result ?? false) && context.mounted) { + LoginCheckAccountRoute( + serverURL: qrcode.server, + loginName: qrcode.user, + password: qrcode.password, + ).pushReplacement(context); + } +} diff --git a/packages/neon/neon/lib/src/router.dart b/packages/neon/neon/lib/src/router.dart index f09f8924..1a453365 100644 --- a/packages/neon/neon/lib/src/router.dart +++ b/packages/neon/neon/lib/src/router.dart @@ -13,6 +13,7 @@ import 'package:neon/src/pages/login_flow.dart'; import 'package:neon/src/pages/login_qrcode.dart'; import 'package:neon/src/pages/nextcloud_app_settings.dart'; import 'package:neon/src/pages/settings.dart'; +import 'package:neon/src/utils/login_qrcode.dart'; import 'package:neon/src/utils/stream_listenable.dart'; import 'package:provider/provider.dart'; @@ -30,6 +31,15 @@ class AppRouter extends GoRouter { redirect: (final context, final state) { final account = accountsBloc.activeAccount.valueOrNull; + final loginQrcode = LoginQrcode.tryParse(state.location); + if (loginQrcode != null) { + return LoginQrcodeIntermediateRoute( + serverURL: loginQrcode.server, + loginName: loginQrcode.user, + password: loginQrcode.password, + ).location; + } + // redirect to loginscreen when no account is logged in if (account == null && !state.location.startsWith(const LoginRoute().location)) { return const LoginRoute().location; @@ -110,6 +120,10 @@ class HomeRoute extends GoRouteData { path: 'qrcode', name: 'loginQrcode', ), + TypedGoRoute( + path: 'qrcode/intermediate/:serverURL/:loginName/:password', + name: 'loginQrcodeIntermediate', + ), TypedGoRoute( path: 'check/server/:serverURL', name: 'checkServerStatus', @@ -150,6 +164,26 @@ class LoginQrcodeRoute extends GoRouteData { Widget build(final BuildContext context, final GoRouterState state) => const LoginQrcodePage(); } +@immutable +class LoginQrcodeIntermediateRoute extends GoRouteData { + const LoginQrcodeIntermediateRoute({ + required this.serverURL, + required this.loginName, + required this.password, + }); + + final String serverURL; + final String loginName; + final String password; + + @override + Widget build(final BuildContext context, final GoRouterState state) => LoginQrcodeIntermediatePage( + serverURL: serverURL, + loginName: loginName, + password: password, + ); +} + @immutable class LoginCheckServerStatusRoute extends GoRouteData { const LoginCheckServerStatusRoute({ diff --git a/packages/neon/neon/lib/src/router.g.dart b/packages/neon/neon/lib/src/router.g.dart index b823ce90..c1730833 100644 --- a/packages/neon/neon/lib/src/router.g.dart +++ b/packages/neon/neon/lib/src/router.g.dart @@ -130,6 +130,11 @@ RouteBase get $loginRoute => GoRouteData.$route( name: 'loginQrcode', factory: $LoginQrcodeRouteExtension._fromState, ), + GoRouteData.$route( + path: 'qrcode/intermediate/:serverURL/:loginName/:password', + name: 'loginQrcodeIntermediate', + factory: $LoginQrcodeIntermediateRouteExtension._fromState, + ), GoRouteData.$route( path: 'check/server/:serverURL', name: 'checkServerStatus', @@ -192,6 +197,24 @@ extension $LoginQrcodeRouteExtension on LoginQrcodeRoute { void pushReplacement(BuildContext context) => context.pushReplacement(location); } +extension $LoginQrcodeIntermediateRouteExtension on LoginQrcodeIntermediateRoute { + static LoginQrcodeIntermediateRoute _fromState(GoRouterState state) => LoginQrcodeIntermediateRoute( + serverURL: state.pathParameters['serverURL']!, + loginName: state.pathParameters['loginName']!, + password: state.pathParameters['password']!, + ); + + String get location => GoRouteData.$location( + '/login/qrcode/intermediate/${Uri.encodeComponent(serverURL)}/${Uri.encodeComponent(loginName)}/${Uri.encodeComponent(password)}', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => context.pushReplacement(location); +} + extension $LoginCheckServerStatusRouteExtension on LoginCheckServerStatusRoute { static LoginCheckServerStatusRoute _fromState(GoRouterState state) => LoginCheckServerStatusRoute( serverURL: state.pathParameters['serverURL']!, diff --git a/packages/neon/neon/lib/src/utils/login_qrcode.dart b/packages/neon/neon/lib/src/utils/login_qrcode.dart new file mode 100644 index 00000000..4e2081e2 --- /dev/null +++ b/packages/neon/neon/lib/src/utils/login_qrcode.dart @@ -0,0 +1,47 @@ +import 'package:meta/meta.dart'; + +@internal +class LoginQrcode { + LoginQrcode({ + required this.server, + required this.user, + required this.password, + }); + + static final _loginQrcodeUrlRegex = RegExp(r'^nc://login/user:(.*)&password:(.*)&server:(.*)$'); + static final _loginQrcodePathRegex = RegExp(r'^/user:(.*)&password:(.*)&server:(.*)$'); + + static LoginQrcode parse(final String url) { + for (final regex in [_loginQrcodeUrlRegex, _loginQrcodePathRegex]) { + final matches = regex.allMatches(url); + if (matches.isEmpty) { + continue; + } + + final match = matches.single; + if (match.groupCount != 3) { + continue; + } + + return LoginQrcode( + server: match.group(3)!, + user: match.group(1)!, + password: match.group(2)!, + ); + } + + throw const FormatException(); + } + + static LoginQrcode? tryParse(final String url) { + try { + return parse(url); + } on FormatException { + return null; + } + } + + final String server; + final String user; + final String password; +}