diff --git a/lib/friendica_client.dart b/lib/friendica_client.dart index 0b35a57..f9775e8 100644 --- a/lib/friendica_client.dart +++ b/lib/friendica_client.dart @@ -1,11 +1,11 @@ import 'dart:convert'; import 'dart:io'; +import 'package:result_monad/result_monad.dart'; + import 'models/credentials.dart'; import 'models/exec_error.dart'; import 'models/timeline_entry.dart'; -import 'package:result_monad/result_monad.dart'; - import 'serializers/mastodon/timeline_entry_mastodon_extensions.dart'; class FriendicaClient { @@ -14,6 +14,8 @@ class FriendicaClient { String get serverName => _credentials.serverName; + Credentials get credentials => _credentials; + FriendicaClient({required Credentials credentials}) : _credentials = credentials { final authenticationString = diff --git a/lib/main.dart b/lib/main.dart index d923e8f..67e85ad 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,33 @@ import 'package:flutter/material.dart'; -import 'package:flutter_portal/globals.dart'; -import 'package:flutter_portal/routes.dart'; -import 'package:flutter_portal/screens/sign_in.dart'; -import 'package:flutter_portal/services/auth_service.dart'; import 'package:provider/provider.dart'; +import 'package:result_monad/result_monad.dart'; -void main() { - getIt.registerLazySingleton(() => AuthService()); +import 'globals.dart'; +import 'routes.dart'; +import 'screens/sign_in.dart'; +import 'services/auth_service.dart'; +import 'services/secrets_service.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + final authService = AuthService(); + final secretsService = SecretsService(); + getIt.registerSingleton(secretsService); + getIt.registerSingleton(authService); + + await secretsService.initialize().andThenSuccessAsync((credentials) async { + if (credentials.isEmpty) { + return; + } + + final wasLoggedIn = await authService.getStoredLoginState(); + if (wasLoggedIn) { + final result = await authService.signIn(credentials); + print('Startup login result: $result'); + } else { + print('Was not logged in'); + } + }); runApp(const App()); } @@ -17,22 +38,6 @@ class App extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { - // return MaterialApp( - // title: 'Flutter Demo', - // theme: ThemeData( - // // This is the theme of your application. - // // - // // Try running your application with "flutter run". You'll see the - // // application has a blue toolbar. Then, without quitting the app, try - // // changing the primarySwatch below to Colors.green and then invoke - // // "hot reload" (press "r" in the console where you ran "flutter run", - // // or simply save your changes to "hot reload" in a Flutter IDE). - // // Notice that the counter didn't reset back to zero; the application - // // is not restarted. - // primarySwatch: Colors.blue, - // ), - // home: const Home(), - // ); return MultiProvider( providers: [ ChangeNotifierProvider( diff --git a/lib/models/credentials.dart b/lib/models/credentials.dart index 6d847ce..264724b 100644 --- a/lib/models/credentials.dart +++ b/lib/models/credentials.dart @@ -11,6 +11,17 @@ class Credentials { required this.password, required this.serverName}); + factory Credentials.empty() => Credentials( + username: '', + password: '', + serverName: '', + ); + + bool get isEmpty => + username.isEmpty && password.isEmpty && serverName.isEmpty; + + String get handle => '$username@$serverName'; + Credentials copy({String? username, String? password, String? serverName}) { return Credentials( username: username ?? this.username, @@ -31,7 +42,7 @@ class Credentials { password: password, serverName: elements[1], ); - print(result); + return Result.ok(result); } diff --git a/lib/routes.dart b/lib/routes.dart index 0d6c93e..68139a1 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -1,7 +1,10 @@ import 'package:go_router/go_router.dart'; +import 'globals.dart'; import 'screens/home.dart'; import 'screens/sign_in.dart'; +import 'screens/splash.dart'; +import 'services/auth_service.dart'; class ScreenPaths { static String splash = '/splash'; @@ -12,9 +15,33 @@ class ScreenPaths { } bool needAuthChangeInitialized = true; +final _authService = getIt(); +final allowedLoggedOut = [ + ScreenPaths.splash, + ScreenPaths.signin, + ScreenPaths.signup +]; + final appRouter = GoRouter( - initialLocation: ScreenPaths.signin, + initialLocation: ScreenPaths.home, debugLogDiagnostics: true, + refreshListenable: _authService, + redirect: (context, state) async { + print('redirect handler'); + final loggedIn = _authService.loggedIn; + print('$loggedIn ${state.location}'); + if (!loggedIn && !allowedLoggedOut.contains(state.location)) { + print('Redirecting to sign in'); + return ScreenPaths.signin; + } + + if (loggedIn && allowedLoggedOut.contains(state.location)) { + print('Redirecting to home'); + return ScreenPaths.home; + } + + return null; + }, routes: [ GoRoute( path: ScreenPaths.signin, @@ -26,4 +53,9 @@ final appRouter = GoRouter( name: ScreenPaths.home, builder: (context, state) => HomeScreen(), ), + GoRoute( + path: ScreenPaths.splash, + name: ScreenPaths.splash, + builder: (context, state) => SplashScreen(), + ), ]); diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 0bd0911..73feb67 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart'; +import 'package:provider/provider.dart'; import 'package:result_monad/result_monad.dart'; import '../controls/padding.dart'; @@ -14,7 +15,7 @@ class HomeScreen extends StatelessWidget { @override Widget build(BuildContext context) { - final clientResult = getIt().currentClient; + final clientResult = context.read().currentClient; final body = clientResult.fold(onSuccess: (client) { return Column( children: [ @@ -50,6 +51,14 @@ class HomeScreen extends StatelessWidget { return Scaffold( appBar: AppBar( title: Text('Home'), + actions: [ + IconButton( + onPressed: () { + getIt().signOut(); + }, + icon: Icon(Icons.logout), + ) + ], ), body: body, ); diff --git a/lib/screens/sign_in.dart b/lib/screens/sign_in.dart index 08399cc..f961fd3 100644 --- a/lib/screens/sign_in.dart +++ b/lib/screens/sign_in.dart @@ -1,21 +1,36 @@ import 'package:email_validator/email_validator.dart'; import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:result_monad/result_monad.dart'; import '../controls/padding.dart'; -import '../friendica_client.dart'; import '../globals.dart'; import '../models/credentials.dart'; -import '../routes.dart'; import '../services/auth_service.dart'; +import '../services/secrets_service.dart'; import '../utils/snackbar_builder.dart'; -class SignInScreen extends StatelessWidget { +class SignInScreen extends StatefulWidget { + @override + State createState() => _SignInScreenState(); +} + +class _SignInScreenState extends State { final formKey = GlobalKey(); final usernameController = TextEditingController(); final passwordController = TextEditingController(); + @override + void initState() { + super.initState(); + getIt().credentials.andThenSuccess((credentials) { + if (credentials.isEmpty) { + return; + } + + usernameController.text = credentials.handle; + passwordController.text = credentials.password; + }); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -81,20 +96,16 @@ class SignInScreen extends StatelessWidget { void _signIn(BuildContext context) async { if (formKey.currentState?.validate() ?? false) { print('Attempting login...'); - await Credentials.buildFromHandle( + final result = await Credentials.buildFromHandle( usernameController.text, passwordController.text, - ) - .andThenSuccess((creds) => FriendicaClient(credentials: creds)) - .andThenAsync((client) async => - (await client.getMyProfile()).mapValue((_) => client)) - .match(onSuccess: (client) { - print('Logged in'); - getIt().updateClient(client); - context.pushNamed(ScreenPaths.home); - }, onError: (error) { - buildSnackbar(context, 'Error logging in: $error'); + ).andThenSuccess((creds) async { + return await getIt().signIn(creds); }); + + if (result.isFailure) { + buildSnackbar(context, 'Error signing in: ${result.error}'); + } } } } diff --git a/lib/screens/splash.dart b/lib/screens/splash.dart new file mode 100644 index 0000000..cce2c11 --- /dev/null +++ b/lib/screens/splash.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class SplashScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text('Friendica Portal'), + ], + )), + ); + } +} diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 5c52641..561dcf0 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -1,11 +1,16 @@ import 'package:flutter/foundation.dart'; import 'package:result_monad/result_monad.dart'; +import 'package:shared_preferences/shared_preferences.dart'; -import '../models/exec_error.dart'; import '../friendica_client.dart'; +import '../globals.dart'; +import '../models/credentials.dart'; +import '../models/exec_error.dart'; +import 'secrets_service.dart'; class AuthService extends ChangeNotifier { FriendicaClient? _friendicaClient; + bool _loggedIn = false; Result get currentClient { if (_friendicaClient == null) { @@ -18,14 +23,44 @@ class AuthService extends ChangeNotifier { return Result.ok(_friendicaClient!); } - Result updateClient(FriendicaClient newClient) { - _friendicaClient = newClient; - notifyListeners(); - return Result.ok(newClient); + bool get loggedIn => _loggedIn && _friendicaClient != null; + + Future getStoredLoginState() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool('logged-in') ?? false; } - void clearCredentials() { + FutureResult signIn( + Credentials credentials) async { + final client = FriendicaClient(credentials: credentials); + final result = await client.getMyProfile(); + if (result.isFailure) { + return result.errorCast(); + } + + getIt().storeCredentials(client.credentials); + await _setLoginState(true); + _friendicaClient = client; + notifyListeners(); + return Result.ok(client); + } + + Future signOut() async { + print('Sign out'); + await _setLoginState(false); _friendicaClient = null; notifyListeners(); } + + Future clearCredentials() async { + _friendicaClient = null; + await _setLoginState(false); + notifyListeners(); + } + + Future _setLoginState(bool state) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('logged-in', state); + _loggedIn = state; + } } diff --git a/lib/services/secrets_service.dart b/lib/services/secrets_service.dart new file mode 100644 index 0000000..182909a --- /dev/null +++ b/lib/services/secrets_service.dart @@ -0,0 +1,83 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:result_monad/result_monad.dart'; + +import '../models/credentials.dart'; +import '../models/exec_error.dart'; + +class SecretsService { + static const _usernameKey = 'username'; + static const _passwordKey = 'password'; + static const _serverNameKey = 'server-name'; + + Credentials? _cachedCredentials; + + Result get credentials => _cachedCredentials != null + ? Result.ok(_cachedCredentials!) + : Result.error( + ExecError( + type: ErrorType.localError, + message: 'Credentials not initialized', + ), + ); + + final _secureStorage = const FlutterSecureStorage( + iOptions: IOSOptions( + accessibility: KeychainAccessibility.first_unlock, + ), + ); + + FutureResult initialize() async { + return await getCredentials(); + } + + FutureResult clearCredentials() async { + try { + await _secureStorage.delete(key: _usernameKey); + await _secureStorage.read(key: _passwordKey); + await _secureStorage.read(key: _serverNameKey); + return Result.ok(Credentials.empty()); + } on PlatformException catch (e) { + return Result.error(ExecError( + type: ErrorType.localError, + message: e.message ?? '', + )); + } + } + + Result storeCredentials(Credentials credentials) { + try { + _secureStorage.write(key: _usernameKey, value: credentials.username); + _secureStorage.write(key: _passwordKey, value: credentials.password); + _secureStorage.write(key: _serverNameKey, value: credentials.serverName); + return Result.ok(credentials); + } on PlatformException catch (e) { + return Result.error(ExecError( + type: ErrorType.localError, + message: e.message ?? '', + )); + } + } + + FutureResult getCredentials() async { + try { + final username = await _secureStorage.read(key: _usernameKey); + final password = await _secureStorage.read(key: _passwordKey); + final serverName = await _secureStorage.read(key: _serverNameKey); + if (username == null || password == null || serverName == null) { + return Result.ok(Credentials.empty()); + } + _cachedCredentials = Credentials( + username: username, + password: password, + serverName: serverName, + ); + return Result.ok(_cachedCredentials!); + } on PlatformException catch (e) { + return Result.error(ExecError( + type: ErrorType.localError, + message: e.message ?? '', + )); + } + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 321a26b..3a812a5 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) desktop_window_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWindowPlugin"); desktop_window_plugin_register_with_registrar(desktop_window_registrar); + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index df1c4a0..c0731f5 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_window + flutter_secure_storage_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 1e05600..6d58128 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,12 +6,14 @@ import FlutterMacOS import Foundation import desktop_window +import flutter_secure_storage_macos import path_provider_macos import shared_preferences_macos import sqflite func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DesktopWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWindowPlugin")) + FlutterSecureStorageMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageMacosPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) diff --git a/pubspec.lock b/pubspec.lock index 4f55d7f..51151d7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -146,6 +146,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index d25e38f..e706fd6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,22 +10,23 @@ environment: dependencies: flutter: sdk: flutter - cupertino_icons: ^1.0.2 - provider: ^6.0.4 - email_validator: ^2.1.17 - get_it: ^7.2.0 - go_router: ^5.1.3 - uuid: ^3.0.6 - logging: ^1.1.0 - get_it_mixin: ^3.1.4 cached_network_image: ^3.2.2 - result_monad: ^2.0.2 + cupertino_icons: ^1.0.2 + desktop_window: ^0.4.0 + email_validator: ^2.1.17 + flutter_secure_storage: ^6.0.0 flutter_widget_from_html_core: ^0.9.0 + get_it: ^7.2.0 + get_it_mixin: ^3.1.4 + go_router: ^5.1.3 + logging: ^1.1.0 markdown: ^6.0.1 metadata_fetch: ^0.4.1 - shared_preferences: ^2.0.15 network_to_file_image: ^4.0.1 - desktop_window: ^0.4.0 + provider: ^6.0.4 + result_monad: ^2.0.2 + shared_preferences: ^2.0.15 + uuid: ^3.0.6 time_machine: ^0.9.17 dev_dependencies: @@ -36,6 +37,17 @@ dev_dependencies: flutter: uses-material-design: true +parts: + uet-lms: + source: . + plugin: flutter + flutter-target: lib/main.dart + build-packages: + - libsecret-1-dev + - libjsoncpp-dev + stage-packages: + - libsecret-1-dev + - libjsoncpp1-dev # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 1923f29..e823d16 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,8 +7,11 @@ #include "generated_plugin_registrant.h" #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { DesktopWindowPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DesktopWindowPlugin")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 5601eb6..e2adeed 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_window + flutter_secure_storage_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST