From 0a87ac572bd9f35f7b505857d1d7e6b1ded4fdc9 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 7 Jul 2025 19:44:17 +0200 Subject: [PATCH] Add QrCode authentication --- docker_prod/docker-compose.yml | 3 +- moneymgr_mobile/devtools_options.yaml | 3 + moneymgr_mobile/lib/providers/auth_state.dart | 2 +- .../lib/routes/login/base_auth_page.dart | 14 ++-- .../lib/routes/login/login_screen.dart | 2 +- .../lib/routes/login/qr_auth_screen.dart | 80 +++++++++++++++++++ .../lib/services/router/router.dart | 2 + .../lib/services/router/routes_list.dart | 3 + .../lib/services/storage/secure_storage.dart | 1 + moneymgr_mobile/lib/utils/extensions.dart | 17 ++-- .../Flutter/GeneratedPluginRegistrant.swift | 2 + moneymgr_mobile/pubspec.lock | 10 ++- moneymgr_mobile/pubspec.yaml | 3 + 13 files changed, 125 insertions(+), 17 deletions(-) create mode 100644 moneymgr_mobile/devtools_options.yaml create mode 100644 moneymgr_mobile/lib/routes/login/qr_auth_screen.dart diff --git a/docker_prod/docker-compose.yml b/docker_prod/docker-compose.yml index 0f18383..311a95e 100644 --- a/docker_prod/docker-compose.yml +++ b/docker_prod/docker-compose.yml @@ -76,4 +76,5 @@ services: - S3_ACCESS_KEY=$MINIO_ROOT_USER - S3_SECRET_KEY=$MINIO_ROOT_PASSWORD - REDIS_HOSTNAME=redis - - REDIS_PASSWORD=${REDIS_PASS:-secretredis} \ No newline at end of file + - REDIS_PASSWORD=${REDIS_PASS:-secretredis} + - UNSECURE_AUTO_LOGIN_EMAIL=$UNSECURE_AUTO_LOGIN_EMAIL diff --git a/moneymgr_mobile/devtools_options.yaml b/moneymgr_mobile/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/moneymgr_mobile/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/moneymgr_mobile/lib/providers/auth_state.dart b/moneymgr_mobile/lib/providers/auth_state.dart index c1c24d8..8927326 100644 --- a/moneymgr_mobile/lib/providers/auth_state.dart +++ b/moneymgr_mobile/lib/providers/auth_state.dart @@ -58,7 +58,7 @@ enum AuthState { unknown(redirectPath: homePage, allowedPaths: [homePage]), unauthenticated( redirectPath: authPage, - allowedPaths: [authPage, manualAuthPage, settingsPage], + allowedPaths: [authPage, qrAuthPath, manualAuthPage, settingsPage], ), authenticated( redirectPath: homePage, diff --git a/moneymgr_mobile/lib/routes/login/base_auth_page.dart b/moneymgr_mobile/lib/routes/login/base_auth_page.dart index 1b39c32..04505d1 100644 --- a/moneymgr_mobile/lib/routes/login/base_auth_page.dart +++ b/moneymgr_mobile/lib/routes/login/base_auth_page.dart @@ -35,12 +35,14 @@ class BaseAuthPage extends StatelessWidget { ], ), body: SingleChildScrollView( - child: SeparatedColumn( - padding: EdgeInsets.all(context.gutter), - separatorBuilder: () => const Gutter(), - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: children, + child: IntrinsicHeight( + child: SeparatedColumn( + padding: EdgeInsets.all(context.gutter), + separatorBuilder: () => const Gutter(), + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: children, + ), ), ), ); diff --git a/moneymgr_mobile/lib/routes/login/login_screen.dart b/moneymgr_mobile/lib/routes/login/login_screen.dart index c4d0804..e76a385 100644 --- a/moneymgr_mobile/lib/routes/login/login_screen.dart +++ b/moneymgr_mobile/lib/routes/login/login_screen.dart @@ -24,7 +24,7 @@ class LoginScreen extends HookConsumerWidget { label: "Enter manually authentication information", ), _LoginChoice( - route: manualAuthPage, + route: qrAuthPath, icon: Icons.qr_code_2, label: "Scan authentication Qr Code", ), diff --git a/moneymgr_mobile/lib/routes/login/qr_auth_screen.dart b/moneymgr_mobile/lib/routes/login/qr_auth_screen.dart new file mode 100644 index 0000000..1c3b201 --- /dev/null +++ b/moneymgr_mobile/lib/routes/login/qr_auth_screen.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:logging/logging.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:moneymgr_mobile/providers/auth_state.dart'; +import 'package:moneymgr_mobile/services/api/api_token.dart'; +import 'package:moneymgr_mobile/services/router/routes_list.dart'; +import 'package:moneymgr_mobile/utils/extensions.dart'; + +class QrAuthScreen extends HookConsumerWidget { + const QrAuthScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final loading = useState(false); + + handleCapture(BarcodeCapture barcodes) async { + if (loading.value) return; + + if (barcodes.barcodes.length != 1) return; + final b = barcodes.barcodes[0]; + + if (b.format != BarcodeFormat.qrCode) { + context.showTextSnackBar( + "Only QrCode are supported!", + duration: Duration(seconds: 1), + ); + return; + } + + final value = b.rawValue ?? ""; + + Logger.root.finest("Decoded QrCode: $value"); + + if (!value.startsWith("moneymgr://")) { + context.showTextSnackBar( + "Not a MoneyMgr Qr Code!", + duration: Duration(seconds: 1), + ); + return; + } + + // Decode token + final uri = Uri.parse( + value.replaceFirst("moneymgr://", "http://test.com/?"), + ); + final token = ApiToken( + apiUrl: uri.queryParameters["api"] ?? "", + tokenId: int.tryParse(uri.queryParameters["id"] ?? "") ?? 0, + tokenValue: uri.queryParameters["secret"] ?? "", + ); + + // Attempt to authenticate using token + try { + loading.value = true; + + await ref.read(currentAuthStateProvider.notifier).setAuthToken(token); + + if (context.mounted) { + if (context.canPop()) context.pop(); + context.replace(profilePage); + } + } catch (e, s) { + Logger.root.severe("Failed to authenticate user! $e $s"); + if (context.mounted) { + context.showTextSnackBar("Failed to authenticate user! $e"); + } + } finally { + loading.value = false; + } + } + + return Scaffold( + appBar: AppBar(title: Text('QR Authentication')), + body: MobileScanner(onDetect: handleCapture), + ); + } +} diff --git a/moneymgr_mobile/lib/services/router/router.dart b/moneymgr_mobile/lib/services/router/router.dart index 752b420..150dde9 100644 --- a/moneymgr_mobile/lib/services/router/router.dart +++ b/moneymgr_mobile/lib/services/router/router.dart @@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:moneymgr_mobile/providers/auth_state.dart'; import 'package:moneymgr_mobile/routes/login/login_screen.dart'; import 'package:moneymgr_mobile/routes/login/manual_auth_screen.dart'; +import 'package:moneymgr_mobile/routes/login/qr_auth_screen.dart'; import 'package:moneymgr_mobile/routes/settings/settings_screen.dart'; import 'package:moneymgr_mobile/services/router/routes_list.dart'; import 'package:moneymgr_mobile/widgets/scaffold_with_navigation.dart'; @@ -61,6 +62,7 @@ GoRouter router(Ref ref) { routes: [ GoRoute(path: homePage, builder: (_, __) => const Scaffold()), GoRoute(path: authPage, builder: (_, __) => const LoginScreen()), + GoRoute(path: qrAuthPath, builder: (_, __) => const QrAuthScreen()), GoRoute( path: manualAuthPage, builder: (_, __) => const ManualAuthScreen(), diff --git a/moneymgr_mobile/lib/services/router/routes_list.dart b/moneymgr_mobile/lib/services/router/routes_list.dart index c18f6a3..c2dd99d 100644 --- a/moneymgr_mobile/lib/services/router/routes_list.dart +++ b/moneymgr_mobile/lib/services/router/routes_list.dart @@ -4,6 +4,9 @@ const homePage = "/"; /// Authentication path const authPage = "/login"; +/// Qr Code authentication +const qrAuthPath = "/login/qr"; + /// Manual authentication const manualAuthPage = "/login/manual"; diff --git a/moneymgr_mobile/lib/services/storage/secure_storage.dart b/moneymgr_mobile/lib/services/storage/secure_storage.dart index 3430a7e..101c635 100644 --- a/moneymgr_mobile/lib/services/storage/secure_storage.dart +++ b/moneymgr_mobile/lib/services/storage/secure_storage.dart @@ -51,6 +51,7 @@ class SecureStorage { if (tokenStr != null) { return ApiToken.fromJson(jsonDecode(tokenStr)); } + return null; } /// Set auth token diff --git a/moneymgr_mobile/lib/utils/extensions.dart b/moneymgr_mobile/lib/utils/extensions.dart index ce7aed5..da9623c 100644 --- a/moneymgr_mobile/lib/utils/extensions.dart +++ b/moneymgr_mobile/lib/utils/extensions.dart @@ -15,18 +15,21 @@ extension BuildContextX on BuildContext { /// Shows a floating snack bar with text as its content. ScaffoldFeatureController showTextSnackBar( - String text, - ) => - ScaffoldMessenger.of(this).showSnackBar(SnackBar( - behavior: SnackBarBehavior.floating, - content: Text(text), - )); + String text, { + Duration? duration, + }) => ScaffoldMessenger.of(this).showSnackBar( + SnackBar( + behavior: SnackBarBehavior.floating, + content: Text(text), + duration: duration ?? Duration(milliseconds: 4000), + ), + ); void showAppLicensePage() => showLicensePage( context: this, useRootNavigator: true, applicationName: 'MoneyMgr', - applicationLegalese: '(c) Pierre HUBERT 2025 - ${DateTime.now().year}' + applicationLegalese: '(c) Pierre HUBERT 2025 - ${DateTime.now().year}', ); } diff --git a/moneymgr_mobile/macos/Flutter/GeneratedPluginRegistrant.swift b/moneymgr_mobile/macos/Flutter/GeneratedPluginRegistrant.swift index 37af1fe..1e200a8 100644 --- a/moneymgr_mobile/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/moneymgr_mobile/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,11 +6,13 @@ import FlutterMacOS import Foundation import flutter_secure_storage_macos +import mobile_scanner import path_provider_foundation import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/moneymgr_mobile/pubspec.lock b/moneymgr_mobile/pubspec.lock index 37a61e1..5cb086d 100644 --- a/moneymgr_mobile/pubspec.lock +++ b/moneymgr_mobile/pubspec.lock @@ -632,6 +632,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: "54005bdea7052d792d35b4fef0f84ec5ddc3a844b250ecd48dc192fb9b4ebc95" + url: "https://pub.dev" + source: hosted + version: "7.0.1" package_config: dependency: transitive description: @@ -1071,4 +1079,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.8.1 <4.0.0" - flutter: ">=3.27.0" + flutter: ">=3.29.0" diff --git a/moneymgr_mobile/pubspec.yaml b/moneymgr_mobile/pubspec.yaml index 11ce2df..c5f2614 100644 --- a/moneymgr_mobile/pubspec.yaml +++ b/moneymgr_mobile/pubspec.yaml @@ -73,6 +73,9 @@ dependencies: # API requests dio: ^5.8.0+1 + # Qr Code library + mobile_scanner: ^7.0.1 + dev_dependencies: flutter_test: sdk: flutter