From 27a3d02d43075ffd265a6c87e49d3c8603729d5a Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Sat, 9 Nov 2024 21:21:12 -0500 Subject: [PATCH 01/50] v0.20.0 api updates --- lib/core/auth/bloc/auth_bloc.dart | 8 ++++---- lib/core/singletons/lemmy_client.dart | 2 +- lib/user/pages/user_settings_page.dart | 2 +- pubspec.lock | 8 +++----- pubspec.yaml | 7 ++++--- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index 07f336792..825621ebd 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -52,7 +52,7 @@ class AuthBloc extends Bloc { LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: account.jwt)); - bool downvotesEnabled = getSiteResponse.siteView.localSite.enableDownvotes; + bool downvotesEnabled = getSiteResponse.siteView.localSite.enableDownvotes ?? false; return emit(state.copyWith( status: AuthStatus.success, @@ -109,7 +109,7 @@ class AuthBloc extends Bloc { try { getSiteResponse = await lemmy.run(GetSite(auth: activeAccount.jwt)).timeout(const Duration(seconds: 15)); - downvotesEnabled = getSiteResponse.siteView.localSite.enableDownvotes; + downvotesEnabled = getSiteResponse.siteView.localSite.enableDownvotes ?? false; } catch (e) { return emit(state.copyWith(status: AuthStatus.failureCheckingInstance, errorMessage: getExceptionErrorMessage(e))); } @@ -168,7 +168,7 @@ class AuthBloc extends Bloc { SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences; prefs.setString('active_profile_id', account.id); - bool downvotesEnabled = getSiteResponse.siteView.localSite.enableDownvotes; + bool downvotesEnabled = getSiteResponse.siteView.localSite.enableDownvotes ?? false; return emit(state.copyWith(status: AuthStatus.success, account: account, isLoggedIn: true, downvotesEnabled: downvotesEnabled, getSiteResponse: getSiteResponse)); } on LemmyApiException catch (e) { @@ -211,7 +211,7 @@ class AuthBloc extends Bloc { Account? account = (activeProfileId != null) ? await Account.fetchAccount(activeProfileId) : null; GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: account?.jwt)); - bool downvotesEnabled = getSiteResponse.siteView.localSite.enableDownvotes; + bool downvotesEnabled = getSiteResponse.siteView.localSite.enableDownvotes ?? false; return emit(state.copyWith(status: AuthStatus.success, account: account, isLoggedIn: activeProfileId?.isNotEmpty == true, downvotesEnabled: downvotesEnabled, getSiteResponse: getSiteResponse)); }); diff --git a/lib/core/singletons/lemmy_client.dart b/lib/core/singletons/lemmy_client.dart index 971895704..d61df6497 100644 --- a/lib/core/singletons/lemmy_client.dart +++ b/lib/core/singletons/lemmy_client.dart @@ -11,7 +11,7 @@ class LemmyClient { LemmyClient._initialize(); void changeBaseUrl(String baseUrl) { - lemmyApiV3 = LemmyApiV3(baseUrl); + lemmyApiV3 = LemmyApiV3(baseUrl, tls: false, debug: true); _populateSiteInfo(); // Do NOT await this. Let it populate in the background. } diff --git a/lib/user/pages/user_settings_page.dart b/lib/user/pages/user_settings_page.dart index 31e5dfabd..6e722f36d 100644 --- a/lib/user/pages/user_settings_page.dart +++ b/lib/user/pages/user_settings_page.dart @@ -324,7 +324,7 @@ class _UserSettingsPageState extends State { ), ListOption( description: l10n.defaultFeedSortType, - value: ListPickerItem(label: localUser.defaultSortType.value, icon: Icons.local_fire_department_rounded, payload: localUser.defaultSortType), + value: ListPickerItem(label: localUser.defaultSortType!.value, icon: Icons.local_fire_department_rounded, payload: localUser.defaultSortType), options: [ ...SortPicker.getDefaultSortTypeItems(minimumVersion: Version(0, 19, 0, preRelease: ["rc", "1"])), ...topSortTypeItems diff --git a/pubspec.lock b/pubspec.lock index 1f47a28c0..c3ff760ab 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1133,11 +1133,9 @@ packages: lemmy_api_client: dependency: "direct main" description: - path: "." - ref: "874005c9013de53f526562f882fe8231eb0ce5ae" - resolved-ref: "874005c9013de53f526562f882fe8231eb0ce5ae" - url: "https://github.com/thunder-app/lemmy_api_client.git" - source: git + path: "../lemmy_api_client" + relative: true + source: path version: "0.21.0" link_preview_generator: dependency: "direct main" diff --git a/pubspec.yaml b/pubspec.yaml index 85de210b1..f7d9186eb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,9 +13,10 @@ dependencies: push: path: packages/push/push lemmy_api_client: - git: - url: "https://github.com/thunder-app/lemmy_api_client.git" - ref: 874005c9013de53f526562f882fe8231eb0ce5ae + path: ../lemmy_api_client + #git: + # url: "https://github.com/thunder-app/lemmy_api_client.git" + # ref: 16d14a1c13ac9522e85188ad9cf23d8912ec8fee link_preview_generator: git: url: "https://github.com/thunder-app/link_preview_generator.git" From 2f60b6a269c8d861b9967e75e501dfeb281f8c67 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Sun, 17 Nov 2024 12:51:33 -0500 Subject: [PATCH 02/50] add oauth2_client package --- linux/flutter/generated_plugin_registrant.cc | 12 ++++++++---- linux/flutter/generated_plugins.cmake | 3 ++- macos/Flutter/GeneratedPluginRegistrant.swift | 6 ++++++ windows/flutter/generated_plugin_registrant.cc | 6 ++++++ windows/flutter/generated_plugins.cmake | 2 ++ 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 3c33ff3a2..bde1a5d3c 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,9 +8,10 @@ #include #include -#include +#include #include #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) dynamic_color_registrar = @@ -19,13 +20,16 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); - g_autoptr(FlPluginRegistrar) gtk_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); - gtk_plugin_register_with_registrar(gtk_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); g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_to_front_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowToFrontPlugin"); + window_to_front_plugin_register_with_registrar(window_to_front_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 19433b77f..7708a2461 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,9 +5,10 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_color file_selector_linux - gtk + flutter_secure_storage_linux sqlite3_flutter_libs url_launcher_linux + window_to_front ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index aea1e666a..2d4ff1d37 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -12,6 +12,8 @@ import dynamic_color import file_selector_macos import flutter_inappwebview_macos import flutter_local_notifications +import flutter_secure_storage_macos +import flutter_web_auth_2 import gal import path_provider_foundation import share_plus @@ -21,6 +23,7 @@ import sqlite3_flutter_libs import url_launcher_macos import video_player_avfoundation import webview_flutter_wkwebview +import window_to_front func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) @@ -30,6 +33,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) @@ -39,4 +44,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) FLTWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "FLTWebViewFlutterPlugin")) + WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin")) } diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index f00a0a2a8..bdde1a7ba 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -11,11 +11,13 @@ #include #include #include +#include #include #include #include #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { AppLinksPluginCApiRegisterWithRegistrar( @@ -28,6 +30,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); GalPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("GalPluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( @@ -38,4 +42,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowToFrontPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowToFrontPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index a768ae795..08bb3e39f 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -8,11 +8,13 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_color file_selector_windows flutter_inappwebview_windows + flutter_secure_storage_windows gal permission_handler_windows share_plus sqlite3_flutter_libs url_launcher_windows + window_to_front ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From 9905ea0887d5bf86eead3a19cdc87e89a6ba0121 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Sun, 17 Nov 2024 13:32:42 -0500 Subject: [PATCH 03/50] add flutter_web_auth_2 Co-authored-by: William --- lib/account/pages/login_page.dart | 36 +++++++++++++++++++ linux/flutter/generated_plugin_registrant.cc | 12 ++++--- linux/flutter/generated_plugins.cmake | 3 +- macos/Flutter/GeneratedPluginRegistrant.swift | 4 +-- macos/Podfile.lock | 18 ++++++++++ .../flutter/generated_plugin_registrant.cc | 6 ++-- windows/flutter/generated_plugins.cmake | 2 +- 7 files changed, 70 insertions(+), 11 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index 0958f35b3..5b0be437e 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -20,6 +20,15 @@ import 'package:thunder/utils/links.dart'; import 'package:thunder/utils/text_input_formatter.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:oauth2_client/oauth2_client.dart'; + +class PrivacyPortalOAuth2Client extends OAuth2Client { + PrivacyPortalOAuth2Client({required super.redirectUri, required super.customUriScheme}) + : super( + authorizeUrl: 'https://app.privacyportal.org/oauth/authorize', //Your service's authorization url + tokenUrl: 'https://api.privacyportal.org/oauth/token'); +} + class LoginPage extends StatefulWidget { final VoidCallback popRegister; final bool anonymous; @@ -435,6 +444,22 @@ class _LoginPageState extends State with SingleTickerProviderStateMix style: theme.textTheme.titleMedium?.copyWith(color: !isLoading && fieldsFilledIn ? theme.colorScheme.onPrimary : theme.colorScheme.primary)), ), const SizedBox(height: 12.0), + ElevatedButton( + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(60), + backgroundColor: theme.colorScheme.primary, + textStyle: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.onPrimary, + ), + ), + onPressed: (!isLoading && _passwordTextEditingController.text.isNotEmpty && _passwordTextEditingController.text.isNotEmpty && _instanceTextEditingController.text.isNotEmpty) + ? _handleOAuthLogin + : (_instanceTextEditingController.text.isNotEmpty && widget.anonymous) + ? () => _addAnonymousInstance(context) + : null, + child: Text("Privacy Portal", style: theme.textTheme.titleMedium?.copyWith(color: !isLoading && fieldsFilledIn ? theme.colorScheme.onPrimary : theme.colorScheme.primary)), + ), + const SizedBox(height: 12.0), TextButton( style: ElevatedButton.styleFrom(minimumSize: const Size.fromHeight(60)), onPressed: !isLoading ? () => widget.popRegister() : null, @@ -464,6 +489,17 @@ class _LoginPageState extends State with SingleTickerProviderStateMix ); } + void _handleOAuthLogin({bool showContentWarning = true}) { + TextInput.finishAutofillContext(); + + // Perform login authentication + OAuth2Client privacyPortalClient = PrivacyPortalOAuth2Client(redirectUri: 'thunder:/oauth_redirect', customUriScheme: 'thunder'); + +//Then, instantiate the helper passing the previously instantiated client + OAuth2Helper oauth2Helper = + OAuth2Helper(client, grantType: OAuth2Helper.authorizationCode, clientId: 'your_client_id', clientSecret: 'your_client_secret', scopes: ['https://www.googleapis.com/auth/drive.readonly']); + } + void _addAnonymousInstance(BuildContext context) async { final AppLocalizations l10n = AppLocalizations.of(context)!; diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index bde1a5d3c..3bc20b2ae 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,23 +6,27 @@ #include "generated_plugin_registrant.h" +#include #include #include -#include +#include #include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); + desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); g_autoptr(FlPluginRegistrar) dynamic_color_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_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); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 7708a2461..b1ecbc1c2 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,9 +3,10 @@ # list(APPEND FLUTTER_PLUGIN_LIST + desktop_webview_window dynamic_color file_selector_linux - flutter_secure_storage_linux + gtk sqlite3_flutter_libs url_launcher_linux window_to_front diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 2d4ff1d37..d8c1e098c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,12 +7,12 @@ import Foundation import app_links import connectivity_plus +import desktop_webview_window import device_info_plus import dynamic_color import file_selector_macos import flutter_inappwebview_macos import flutter_local_notifications -import flutter_secure_storage_macos import flutter_web_auth_2 import gal import path_provider_foundation @@ -28,12 +28,12 @@ import window_to_front func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) + DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) - FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 2de4b80fe..3b039ebb9 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -15,6 +15,10 @@ PODS: - OrderedSet (~> 6.0.3) - flutter_local_notifications (0.0.1): - FlutterMacOS + - flutter_secure_storage_macos (6.1.1): + - FlutterMacOS + - flutter_web_auth_2 (3.0.0): + - FlutterMacOS - FlutterMacOS (1.0.0) - gal (1.0.0): - Flutter @@ -58,6 +62,8 @@ PODS: - webview_flutter_wkwebview (0.0.1): - Flutter - FlutterMacOS + - window_to_front (0.0.1): + - FlutterMacOS DEPENDENCIES: - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) @@ -67,6 +73,8 @@ DEPENDENCIES: - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) + - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) + - flutter_web_auth_2 (from `Flutter/ephemeral/.symlinks/plugins/flutter_web_auth_2/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) @@ -77,6 +85,7 @@ DEPENDENCIES: - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`) - webview_flutter_wkwebview (from `Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin`) + - window_to_front (from `Flutter/ephemeral/.symlinks/plugins/window_to_front/macos`) SPEC REPOS: trunk: @@ -98,6 +107,10 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos flutter_local_notifications: :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos + flutter_secure_storage_macos: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos + flutter_web_auth_2: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_web_auth_2/macos FlutterMacOS: :path: Flutter/ephemeral gal: @@ -118,6 +131,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin webview_flutter_wkwebview: :path: Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin + window_to_front: + :path: Flutter/ephemeral/.symlinks/plugins/window_to_front/macos SPEC CHECKSUMS: app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a @@ -127,6 +142,8 @@ SPEC CHECKSUMS: file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b flutter_local_notifications: 7062189aabf7f50938a7b8b6614ffa97656eb0bf + flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9 + flutter_web_auth_2: 2e1dc2d2139973e4723c5286ce247dd590390d70 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 @@ -139,6 +156,7 @@ SPEC CHECKSUMS: url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 + window_to_front: 4cdc24ddd8461ad1a55fa06286d6a79d8b29e8d8 PODFILE CHECKSUM: c2e95c8c0fe03c5c57e438583cae4cc732296009 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index bdde1a7ba..96e5b4d3a 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,10 @@ #include #include +#include #include #include #include -#include #include #include #include @@ -24,14 +24,14 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("AppLinksPluginCApi")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + DesktopWebviewWindowPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); - FlutterSecureStorageWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); GalPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("GalPluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 08bb3e39f..03df4b235 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,10 +5,10 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links connectivity_plus + desktop_webview_window dynamic_color file_selector_windows flutter_inappwebview_windows - flutter_secure_storage_windows gal permission_handler_windows share_plus From a9b03cb74bd8218a7e9b4cfad4147c57b3475b15 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Sun, 17 Nov 2024 15:10:55 -0500 Subject: [PATCH 04/50] Add Privacy Portal Login Co-authored-by: William --- lib/account/pages/login_page.dart | 22 +++---- lib/core/auth/bloc/auth_bloc.dart | 93 ++++++++++++++++++++++++++++++ lib/core/auth/bloc/auth_event.dart | 8 +++ macos/Podfile.lock | 10 ++-- 4 files changed, 114 insertions(+), 19 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index 5b0be437e..dd19a0b21 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -20,14 +20,9 @@ import 'package:thunder/utils/links.dart'; import 'package:thunder/utils/text_input_formatter.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:oauth2_client/oauth2_client.dart'; - -class PrivacyPortalOAuth2Client extends OAuth2Client { - PrivacyPortalOAuth2Client({required super.redirectUri, required super.customUriScheme}) - : super( - authorizeUrl: 'https://app.privacyportal.org/oauth/authorize', //Your service's authorization url - tokenUrl: 'https://api.privacyportal.org/oauth/token'); -} +import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; +import 'dart:convert' show jsonDecode; +import 'package:http/http.dart' as http; class LoginPage extends StatefulWidget { final VoidCallback popRegister; @@ -491,13 +486,12 @@ class _LoginPageState extends State with SingleTickerProviderStateMix void _handleOAuthLogin({bool showContentWarning = true}) { TextInput.finishAutofillContext(); - // Perform login authentication - OAuth2Client privacyPortalClient = PrivacyPortalOAuth2Client(redirectUri: 'thunder:/oauth_redirect', customUriScheme: 'thunder'); - -//Then, instantiate the helper passing the previously instantiated client - OAuth2Helper oauth2Helper = - OAuth2Helper(client, grantType: OAuth2Helper.authorizationCode, clientId: 'your_client_id', clientSecret: 'your_client_secret', scopes: ['https://www.googleapis.com/auth/drive.readonly']); + context.read().add( + OAuthLoginAttempt( + instance: _instanceTextEditingController.text.trim(), + ), + ); } void _addAnonymousInstance(BuildContext context) async { diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index 825621ebd..e87db98d2 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -1,5 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:lemmy_api_client/v3.dart'; import 'package:collection/collection.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; @@ -184,6 +185,98 @@ class AuthBloc extends Bloc { } }); + /// This event should be triggered when the user logs in with a username/password + on((event, emit) async { + LemmyClient lemmyClient = LemmyClient.instance; + String originalBaseUrl = lemmyClient.lemmyApiV3.host; + String clientId = '9d16fb35-090f-4426-a456-368d9412861f'; + String callbackUrlScheme = 'thunder'; + + try { + emit(state.copyWith(status: AuthStatus.loading, account: null, isLoggedIn: false)); + + String instance = event.instance; + if (instance.startsWith('https://')) instance = instance.replaceAll('https://', ''); + if (instance.startsWith('http://')) instance = instance.replaceAll('http://', ''); + + lemmyClient.changeBaseUrl(instance); + LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; + + // https://app.privacyportal.org/oauth/authorize + final url = Uri.https('app.privacyportal.org', 'oauth/authorize', { + 'response_type': 'code', + 'client_id': clientId, + 'redirect_uri': "thunder", + 'scope': 'email', + 'state': 'hellohello', + }); + + // Present the dialog to the user. + final result = await FlutterWebAuth2.authenticate(url: url.toString(), callbackUrlScheme: callbackUrlScheme); + + // TODO: Do we need to check that state matches here? + // Example: if (uri != null && uri.toString().startsWith("myapp")) {} + + // Extract the code. + String code = Uri.parse(result).queryParameters['code'] ?? "failed"; + // Fail to authenticate if code is null. + + // TODO: Put this somewhere. + // // Get the access token from the response + // final accessToken = jsonDecode(response.body)['access_token'] as String; + + LoginResponse loginResponse = await lemmy.run(AuthenticateWithOAuth( + code: code, + oauth_provider_id: "privacy_portal", + redirect_uri: 'thunder:/', + )); + + if (loginResponse.jwt == null) { + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false)); + } + + GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: loginResponse.jwt)); + + //if (event.showContentWarning && getSiteResponse.siteView.site.contentWarning?.isNotEmpty == true) { + // return emit(state.copyWith(status: AuthStatus.contentWarning, contentWarning: getSiteResponse.siteView.site.contentWarning)); + //} + + // Create a new account in the database + Account? account = Account( + id: '', + username: getSiteResponse.myUser?.localUserView.person.name, + jwt: loginResponse.jwt, + instance: instance, + userId: getSiteResponse.myUser?.localUserView.person.id, + index: -1, + ); + + account = await Account.insertAccount(account); + + if (account == null) { + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false)); + } + + // Set this account as the active account + SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences; + prefs.setString('active_profile_id', account.id); + + bool downvotesEnabled = getSiteResponse.siteView.localSite.enableDownvotes ?? false; + + return emit(state.copyWith(status: AuthStatus.success, account: account, isLoggedIn: true, downvotesEnabled: downvotesEnabled, getSiteResponse: getSiteResponse)); + } on LemmyApiException catch (e) { + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString())); + } catch (e) { + try { + // Restore the original baseUrl + lemmyClient.changeBaseUrl(originalBaseUrl); + } catch (e, s) { + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: s.toString())); + } + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString())); + } + }); + on((event, emit) async { return emit(state.copyWith(status: AuthStatus.failure, errorMessage: AppLocalizations.of(GlobalContext.context)!.loginAttemptCanceled)); }); diff --git a/lib/core/auth/bloc/auth_event.dart b/lib/core/auth/bloc/auth_event.dart index d2a65b353..0f8ba5004 100644 --- a/lib/core/auth/bloc/auth_event.dart +++ b/lib/core/auth/bloc/auth_event.dart @@ -29,6 +29,14 @@ class LoginAttempt extends AuthEvent { const LoginAttempt({required this.username, required this.password, required this.instance, this.totp = "", this.showContentWarning = true}); } +/// The [LoginAttempt] event should be triggered whenever the user attempts to log in for the first time. +/// This event is responsible for login authentication and handling related errors. +class OAuthLoginAttempt extends AuthEvent { + final String instance; + + const OAuthLoginAttempt({required this.instance}); +} + /// Cancels a login attempt by emitting the `failure` state. class CancelLoginAttempt extends AuthEvent { const CancelLoginAttempt(); diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 3b039ebb9..c33df8e36 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -4,6 +4,8 @@ PODS: - connectivity_plus (0.0.1): - Flutter - FlutterMacOS + - desktop_webview_window (0.0.1): + - FlutterMacOS - device_info_plus (0.0.1): - FlutterMacOS - dynamic_color (0.0.2): @@ -15,8 +17,6 @@ PODS: - OrderedSet (~> 6.0.3) - flutter_local_notifications (0.0.1): - FlutterMacOS - - flutter_secure_storage_macos (6.1.1): - - FlutterMacOS - flutter_web_auth_2 (3.0.0): - FlutterMacOS - FlutterMacOS (1.0.0) @@ -68,12 +68,12 @@ PODS: DEPENDENCIES: - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`) + - desktop_webview_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - dynamic_color (from `Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - flutter_web_auth_2 (from `Flutter/ephemeral/.symlinks/plugins/flutter_web_auth_2/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) @@ -97,6 +97,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos connectivity_plus: :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin + desktop_webview_window: + :path: Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos dynamic_color: @@ -107,8 +109,6 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos flutter_local_notifications: :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos - flutter_secure_storage_macos: - :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos flutter_web_auth_2: :path: Flutter/ephemeral/.symlinks/plugins/flutter_web_auth_2/macos FlutterMacOS: From b0db713fff98c717e09e52ee5e62fb325954b462 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Fri, 22 Nov 2024 20:20:22 -0500 Subject: [PATCH 05/50] the privacy portal part is working but, the redirect_uri in unreachable --- lib/core/auth/bloc/auth_bloc.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index e87db98d2..63c4a7e7d 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -189,7 +189,7 @@ class AuthBloc extends Bloc { on((event, emit) async { LemmyClient lemmyClient = LemmyClient.instance; String originalBaseUrl = lemmyClient.lemmyApiV3.host; - String clientId = '9d16fb35-090f-4426-a456-368d9412861f'; + String clientId = '014d0e00-e6c2-4a69-a175-9e67934359a5'; // TODO: Is this client_id different than the lemmy client_id? ( I think it is ) String callbackUrlScheme = 'thunder'; try { @@ -206,8 +206,8 @@ class AuthBloc extends Bloc { final url = Uri.https('app.privacyportal.org', 'oauth/authorize', { 'response_type': 'code', 'client_id': clientId, - 'redirect_uri': "thunder", - 'scope': 'email', + 'redirect_uri': "http://localhost:40000/callback", + 'scope': 'openid email', 'state': 'hellohello', }); From b24bd745d1745ca21b32225aaf084bd389807924 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Sat, 30 Nov 2024 16:52:59 -0500 Subject: [PATCH 06/50] got the calback working --- lib/core/auth/bloc/auth_bloc.dart | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index 63c4a7e7d..fdac1a40a 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -1,5 +1,8 @@ +import 'dart:io'; + import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:lemmy_api_client/v3.dart'; import 'package:collection/collection.dart'; @@ -206,29 +209,38 @@ class AuthBloc extends Bloc { final url = Uri.https('app.privacyportal.org', 'oauth/authorize', { 'response_type': 'code', 'client_id': clientId, - 'redirect_uri': "http://localhost:40000/callback", + 'redirect_uri': "http://localhost:40000", 'scope': 'openid email', 'state': 'hellohello', }); + HttpServer server = await HttpServer.bind("localhost", 40000); + // Present the dialog to the user. - final result = await FlutterWebAuth2.authenticate(url: url.toString(), callbackUrlScheme: callbackUrlScheme); + final result = FlutterWebAuth2.authenticate(url: url.toString(), callbackUrlScheme: callbackUrlScheme); + + final httpResult = await server.first; + //await req.response.close(); + await server.close(); // TODO: Do we need to check that state matches here? // Example: if (uri != null && uri.toString().startsWith("myapp")) {} + debugPrint(httpResult.uri.toString()); // Extract the code. - String code = Uri.parse(result).queryParameters['code'] ?? "failed"; + String code = Uri.parse(httpResult.uri.toString()).queryParameters['code'] ?? "failed"; // Fail to authenticate if code is null. // TODO: Put this somewhere. // // Get the access token from the response // final accessToken = jsonDecode(response.body)['access_token'] as String; + debugPrint("CODE"); + debugPrint(code); LoginResponse loginResponse = await lemmy.run(AuthenticateWithOAuth( code: code, oauth_provider_id: "privacy_portal", - redirect_uri: 'thunder:/', + redirect_uri: 'http://localhost:40000', )); if (loginResponse.jwt == null) { From 83d3eaba04afc31ebe9a044b0a0878f4b64cc17b Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Sat, 30 Nov 2024 22:33:27 -0500 Subject: [PATCH 07/50] stuck on the last step of getting the jwt from lemmy --- lib/core/auth/bloc/auth_bloc.dart | 42 +++++++++++++++++++++++-------- pubspec.yaml | 2 +- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index fdac1a40a..aee974360 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io'; import 'package:bloc/bloc.dart'; @@ -16,6 +17,7 @@ import 'package:thunder/account/models/account.dart'; import 'package:thunder/core/singletons/preferences.dart'; import 'package:thunder/core/singletons/lemmy_client.dart'; import 'package:thunder/utils/global_context.dart'; +import 'package:http/http.dart' as http; part 'auth_event.dart'; part 'auth_state.dart'; @@ -192,7 +194,7 @@ class AuthBloc extends Bloc { on((event, emit) async { LemmyClient lemmyClient = LemmyClient.instance; String originalBaseUrl = lemmyClient.lemmyApiV3.host; - String clientId = '014d0e00-e6c2-4a69-a175-9e67934359a5'; // TODO: Is this client_id different than the lemmy client_id? ( I think it is ) + String clientId = '9d16fb35-090f-4426-a456-368d9412861f'; String callbackUrlScheme = 'thunder'; try { @@ -237,17 +239,35 @@ class AuthBloc extends Bloc { debugPrint("CODE"); debugPrint(code); - LoginResponse loginResponse = await lemmy.run(AuthenticateWithOAuth( - code: code, - oauth_provider_id: "privacy_portal", - redirect_uri: 'http://localhost:40000', - )); + //GetSiteResponse getSiteResponse2 = await lemmy.run(const GetSite()); + //debugPrint("SITE"); - if (loginResponse.jwt == null) { - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false)); - } + //LoginResponse loginResponse = await lemmy.run(AuthenticateWithOAuth( + // code: code, + // oauth_provider_id: 1, + // redirect_uri: 'http://localhost:40000', + //)); - GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: loginResponse.jwt)); + // Use this code to get an access token + final response = await http.post(Uri.parse('http://localhost/api/v3/oauth/authenticate'), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode({ + 'code': code, + 'oauth_provider_id': 1, + 'redirect_uri': 'http://localhost:40000', + }), + encoding: Encoding.getByName('utf-8')); + + // Get the access token from the response + String respString = response.toString(); + final accessToken = jsonDecode(response.toString())['access_token'] as String; + + debugPrint("JWT"); + debugPrint(accessToken); + + GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: accessToken)); //if (event.showContentWarning && getSiteResponse.siteView.site.contentWarning?.isNotEmpty == true) { // return emit(state.copyWith(status: AuthStatus.contentWarning, contentWarning: getSiteResponse.siteView.site.contentWarning)); @@ -257,7 +277,7 @@ class AuthBloc extends Bloc { Account? account = Account( id: '', username: getSiteResponse.myUser?.localUserView.person.name, - jwt: loginResponse.jwt, + jwt: accessToken, instance: instance, userId: getSiteResponse.myUser?.localUserView.person.id, index: -1, diff --git a/pubspec.yaml b/pubspec.yaml index f7d9186eb..362f09c17 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,7 +64,7 @@ dependencies: gal: "^2.2.0" html: "^0.15.4" html_unescape: "^2.0.0" - http: "^1.2.1" + http: ^1.2.2 image_picker: "^1.0.0" intl: "^0.19.0" jovial_svg: "^1.1.19" From 926ec5fecabcb49fe3db1899b011eda89c8ca1d9 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Mon, 2 Dec 2024 21:08:03 -0500 Subject: [PATCH 08/50] its working --- lib/core/auth/bloc/auth_bloc.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index aee974360..d3ddae119 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -211,7 +211,7 @@ class AuthBloc extends Bloc { final url = Uri.https('app.privacyportal.org', 'oauth/authorize', { 'response_type': 'code', 'client_id': clientId, - 'redirect_uri': "http://localhost:40000", + 'redirect_uri': "http://localhost:40000/oauth/callback", 'scope': 'openid email', 'state': 'hellohello', }); @@ -256,13 +256,14 @@ class AuthBloc extends Bloc { body: json.encode({ 'code': code, 'oauth_provider_id': 1, - 'redirect_uri': 'http://localhost:40000', + 'redirect_uri': 'http://localhost:40000/oauth/callback', }), encoding: Encoding.getByName('utf-8')); + debugPrint("RESPONSE"); // Get the access token from the response - String respString = response.toString(); - final accessToken = jsonDecode(response.toString())['access_token'] as String; + //String respString = response.toString(); + final accessToken = jsonDecode(response.body)['jwt'] as String; debugPrint("JWT"); debugPrint(accessToken); From 4db929d7efdd00165f340bad90e0584998b4d54b Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Mon, 2 Dec 2024 21:30:11 -0500 Subject: [PATCH 09/50] touch ups --- lib/core/auth/bloc/auth_bloc.dart | 54 ++++++++++++++----------------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index d3ddae119..ff39a78c9 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -190,12 +190,13 @@ class AuthBloc extends Bloc { } }); - /// This event should be triggered when the user logs in with a username/password + /// This event should be triggered when the user logs in with oauth. on((event, emit) async { LemmyClient lemmyClient = LemmyClient.instance; String originalBaseUrl = lemmyClient.lemmyApiV3.host; + + // lemmy client_id, can be found be found on the lemmy OAuth Configuration page. String clientId = '9d16fb35-090f-4426-a456-368d9412861f'; - String callbackUrlScheme = 'thunder'; try { emit(state.copyWith(status: AuthStatus.loading, account: null, isLoggedIn: false)); @@ -207,62 +208,55 @@ class AuthBloc extends Bloc { lemmyClient.changeBaseUrl(instance); LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; - // https://app.privacyportal.org/oauth/authorize + String redirectUri = "http://localhost:40000/oauth/callback"; // This must end in /oauth/callback. + + // TODO: Lookup available auth providers in the lemmy site response @ /api/v3/site. + // For now it is hard coded for PrivacyPortal. + // TODO: Make `state` a random string. final url = Uri.https('app.privacyportal.org', 'oauth/authorize', { 'response_type': 'code', 'client_id': clientId, - 'redirect_uri': "http://localhost:40000/oauth/callback", + 'redirect_uri': redirectUri, 'scope': 'openid email', 'state': 'hellohello', }); + // Start http server to receive callback. + // TODO: Figure out how to do this in a better cross-platform way. Maybe, https://pub.dev/packages/app_links HttpServer server = await HttpServer.bind("localhost", 40000); // Present the dialog to the user. - final result = FlutterWebAuth2.authenticate(url: url.toString(), callbackUrlScheme: callbackUrlScheme); + final result = FlutterWebAuth2.authenticate(url: url.toString(), callbackUrlScheme: "thunder"); + + // Wait for response. + final providerResponse = await server.first; - final httpResult = await server.first; - //await req.response.close(); await server.close(); - // TODO: Do we need to check that state matches here? + // TODO: Check that `state` matches the `state` that was sent to the provider. // Example: if (uri != null && uri.toString().startsWith("myapp")) {} - debugPrint(httpResult.uri.toString()); - // Extract the code. - String code = Uri.parse(httpResult.uri.toString()).queryParameters['code'] ?? "failed"; - // Fail to authenticate if code is null. - - // TODO: Put this somewhere. - // // Get the access token from the response - // final accessToken = jsonDecode(response.body)['access_token'] as String; + // Extract the code from the response. + String code = Uri.parse(providerResponse.uri.toString()).queryParameters['code'] ?? "failed"; debugPrint("CODE"); debugPrint(code); - //GetSiteResponse getSiteResponse2 = await lemmy.run(const GetSite()); - //debugPrint("SITE"); - - //LoginResponse loginResponse = await lemmy.run(AuthenticateWithOAuth( - // code: code, - // oauth_provider_id: 1, - // redirect_uri: 'http://localhost:40000', - //)); + // TODO: Fail to authenticate if code is null. - // Use this code to get an access token + // TODO: This should use lemmy_api_client. + // Authenthicate to lemmy and get a jwt. + // Durring this step lemmy with connect to the Provider to get the user info. final response = await http.post(Uri.parse('http://localhost/api/v3/oauth/authenticate'), headers: { 'Content-Type': 'application/json', }, body: json.encode({ 'code': code, - 'oauth_provider_id': 1, - 'redirect_uri': 'http://localhost:40000/oauth/callback', + 'oauth_provider_id': 1, // This id can be found in the site reponse. + 'redirect_uri': redirectUri, }), encoding: Encoding.getByName('utf-8')); - debugPrint("RESPONSE"); - // Get the access token from the response - //String respString = response.toString(); final accessToken = jsonDecode(response.body)['jwt'] as String; debugPrint("JWT"); From 4b08c9bb983af5b6b07864b40e70275c354b4e8c Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Fri, 6 Dec 2024 20:23:26 -0500 Subject: [PATCH 10/50] make state a random string and check the response from provider --- lib/core/auth/bloc/auth_bloc.dart | 19 ++++++++++------- pubspec.lock | 34 ++++++++++++++++++++++++++++++- pubspec.yaml | 2 ++ 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index ff39a78c9..313a20d7f 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -18,6 +18,7 @@ import 'package:thunder/core/singletons/preferences.dart'; import 'package:thunder/core/singletons/lemmy_client.dart'; import 'package:thunder/utils/global_context.dart'; import 'package:http/http.dart' as http; +import 'package:uuid/uuid.dart'; part 'auth_event.dart'; part 'auth_state.dart'; @@ -208,17 +209,18 @@ class AuthBloc extends Bloc { lemmyClient.changeBaseUrl(instance); LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; - String redirectUri = "http://localhost:40000/oauth/callback"; // This must end in /oauth/callback. - // TODO: Lookup available auth providers in the lemmy site response @ /api/v3/site. // For now it is hard coded for PrivacyPortal. - // TODO: Make `state` a random string. + + // Build oauth provider url. + String redirectUri = "http://localhost:40000/oauth/callback"; // This must end in /oauth/callback. + String oauthClientState = const Uuid().v4(); final url = Uri.https('app.privacyportal.org', 'oauth/authorize', { 'response_type': 'code', 'client_id': clientId, 'redirect_uri': redirectUri, 'scope': 'openid email', - 'state': 'hellohello', + 'state': oauthClientState, }); // Start http server to receive callback. @@ -228,13 +230,16 @@ class AuthBloc extends Bloc { // Present the dialog to the user. final result = FlutterWebAuth2.authenticate(url: url.toString(), callbackUrlScheme: "thunder"); - // Wait for response. + // Wait for response from Provider. final providerResponse = await server.first; await server.close(); - // TODO: Check that `state` matches the `state` that was sent to the provider. - // Example: if (uri != null && uri.toString().startsWith("myapp")) {} + // oauthProviderState must match oauthClientState to ensure the response came from the Provider. + String oauthProviderState = Uri.parse(providerResponse.uri.toString()).queryParameters['state'] ?? "failed"; + if (oauthClientState != oauthProviderState) { + throw Exception("OAuth state check failed: oauthProviderState must match oauthClientState to ensure the response came from the Provider."); + } // Extract the code from the response. String code = Uri.parse(providerResponse.uri.toString()).queryParameters['code'] ?? "failed"; diff --git a/pubspec.lock b/pubspec.lock index c3ff760ab..579c26c18 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -382,6 +382,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" + desktop_webview_window: + dependency: transitive + description: + name: desktop_webview_window + sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0" + url: "https://pub.dev" + source: hosted + version: "0.2.3" device_info_plus: dependency: "direct main" description: @@ -853,6 +861,22 @@ packages: url: "https://pub.dev" source: hosted version: "5.2.0" + flutter_web_auth_2: + dependency: "direct main" + description: + name: flutter_web_auth_2 + sha256: "3c14babeaa066c371f3a743f204dd0d348b7d42ffa6fae7a9847a521aff33696" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + flutter_web_auth_2_platform_interface: + dependency: transitive + description: + name: flutter_web_auth_2_platform_interface + sha256: c63a472c8070998e4e422f6b34a17070e60782ac442107c70000dd1bed645f4d + url: "https://pub.dev" + source: hosted + version: "4.1.0" flutter_web_plugins: dependency: transitive description: flutter @@ -1918,7 +1942,7 @@ packages: source: hosted version: "3.1.3" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff @@ -2069,6 +2093,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.5" + window_to_front: + dependency: transitive + description: + name: window_to_front + sha256: "7aef379752b7190c10479e12b5fd7c0b9d92adc96817d9e96c59937929512aee" + url: "https://pub.dev" + source: hosted + version: "0.0.3" xayn_readability: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 362f09c17..3fce59630 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,6 +94,8 @@ dependencies: youtube_player_flutter: "^9.1.0" youtube_player_iframe: "^5.2.0" freezed_annotation: ^2.4.4 + flutter_web_auth_2: ^4.0.1 + uuid: ^4.5.1 dev_dependencies: flutter_test: From 899149ab48eb0a8f807d7db2a7e79752ef3d1186 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Fri, 6 Dec 2024 20:51:44 -0500 Subject: [PATCH 11/50] raise if no code received from provider --- lib/core/auth/bloc/auth_bloc.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index 313a20d7f..a71821fb3 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -237,8 +237,8 @@ class AuthBloc extends Bloc { // oauthProviderState must match oauthClientState to ensure the response came from the Provider. String oauthProviderState = Uri.parse(providerResponse.uri.toString()).queryParameters['state'] ?? "failed"; - if (oauthClientState != oauthProviderState) { - throw Exception("OAuth state check failed: oauthProviderState must match oauthClientState to ensure the response came from the Provider."); + if (oauthProviderState == "failed" || oauthClientState != oauthProviderState) { + throw Exception("OAuth state-check failed: oauthProviderState $oauthClientState must match oauthClientState $oauthClientState to ensure the response came from the Provider."); } // Extract the code from the response. @@ -246,7 +246,9 @@ class AuthBloc extends Bloc { debugPrint("CODE"); debugPrint(code); - // TODO: Fail to authenticate if code is null. + if (code == "failed") { + throw Exception("OAuth login failed: no code received from provider."); + } // TODO: This should use lemmy_api_client. // Authenthicate to lemmy and get a jwt. From c70ce16d04cf789442ec5f318340278177ce9790 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Wed, 11 Dec 2024 18:30:25 -0500 Subject: [PATCH 12/50] Add showContentWarning to OAuthLoginAttempt --- lib/core/auth/bloc/auth_event.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/core/auth/bloc/auth_event.dart b/lib/core/auth/bloc/auth_event.dart index 0f8ba5004..5e56fbadf 100644 --- a/lib/core/auth/bloc/auth_event.dart +++ b/lib/core/auth/bloc/auth_event.dart @@ -33,8 +33,9 @@ class LoginAttempt extends AuthEvent { /// This event is responsible for login authentication and handling related errors. class OAuthLoginAttempt extends AuthEvent { final String instance; + final bool showContentWarning; - const OAuthLoginAttempt({required this.instance}); + const OAuthLoginAttempt({required this.instance, this.showContentWarning = true}); } /// Cancels a login attempt by emitting the `failure` state. From 11e14c4dcd76c9057a7b64c82705c1ef958c6881 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Wed, 11 Dec 2024 21:05:30 -0500 Subject: [PATCH 13/50] Use the provider from the site_response --- lib/core/auth/bloc/auth_bloc.dart | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index a71821fb3..c4e9ae70a 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:math'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; @@ -197,7 +198,7 @@ class AuthBloc extends Bloc { String originalBaseUrl = lemmyClient.lemmyApiV3.host; // lemmy client_id, can be found be found on the lemmy OAuth Configuration page. - String clientId = '9d16fb35-090f-4426-a456-368d9412861f'; + //String clientId = '9d16fb35-090f-4426-a456-368d9412861f'; try { emit(state.copyWith(status: AuthStatus.loading, account: null, isLoggedIn: false)); @@ -209,17 +210,20 @@ class AuthBloc extends Bloc { lemmyClient.changeBaseUrl(instance); LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; - // TODO: Lookup available auth providers in the lemmy site response @ /api/v3/site. - // For now it is hard coded for PrivacyPortal. + // TODO: Select from a list of Providers, for now it is hard coded to provider0. + GetSiteResponse siteResponse = await lemmy.run(const GetSite()); + ProviderView provider = siteResponse.oauthProviders!.elementAt(0); + debugPrint(provider.toString()); + var authorizationEndpoint = Uri.parse(provider.authorizationEndpoint); // Build oauth provider url. String redirectUri = "http://localhost:40000/oauth/callback"; // This must end in /oauth/callback. String oauthClientState = const Uuid().v4(); - final url = Uri.https('app.privacyportal.org', 'oauth/authorize', { + final url = Uri.https(authorizationEndpoint.host, authorizationEndpoint.path, { 'response_type': 'code', - 'client_id': clientId, + 'client_id': provider.clientId, 'redirect_uri': redirectUri, - 'scope': 'openid email', + 'scope': provider.scopes, 'state': oauthClientState, }); @@ -271,6 +275,7 @@ class AuthBloc extends Bloc { GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: accessToken)); + // TODO: Login fails when this is uncommented. //if (event.showContentWarning && getSiteResponse.siteView.site.contentWarning?.isNotEmpty == true) { // return emit(state.copyWith(status: AuthStatus.contentWarning, contentWarning: getSiteResponse.siteView.site.contentWarning)); //} From 0a5f43e7d8c3a782c9559af11d0fdb1f7d258c98 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Fri, 13 Dec 2024 20:06:18 -0500 Subject: [PATCH 14/50] Update the login page to display a list of providers --- lib/account/pages/login_page.dart | 30 +++++++++++++++++------------- lib/utils/instance.dart | 6 +++++- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index dd19a0b21..7987d97ee 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -50,6 +50,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix bool instanceValidated = true; bool instanceAwaitingValidation = true; String? instanceError; + List oauthProviders = []; bool isLoading = false; @@ -80,6 +81,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix _instanceTextEditingController.addListener(() async { if (currentInstance != _instanceTextEditingController.text) { setState(() => instanceIcon = null); + setState(() => oauthProviders = []); currentInstance = _instanceTextEditingController.text; } @@ -97,6 +99,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix await getInstanceInfo(_instanceTextEditingController.text).then((value) { // Make sure the icon we looked up still matches the text if (currentInstance == _instanceTextEditingController.text) { + setState(() => oauthProviders = value.oauthProviders ?? []); setState(() => instanceIcon = value.icon); } }); @@ -439,21 +442,22 @@ class _LoginPageState extends State with SingleTickerProviderStateMix style: theme.textTheme.titleMedium?.copyWith(color: !isLoading && fieldsFilledIn ? theme.colorScheme.onPrimary : theme.colorScheme.primary)), ), const SizedBox(height: 12.0), - ElevatedButton( - style: ElevatedButton.styleFrom( - minimumSize: const Size.fromHeight(60), - backgroundColor: theme.colorScheme.primary, - textStyle: theme.textTheme.titleMedium?.copyWith( - color: theme.colorScheme.onPrimary, + for (final provider in oauthProviders) + ElevatedButton( + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(60), + backgroundColor: theme.colorScheme.primary, + textStyle: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.onPrimary, + ), ), + onPressed: (!isLoading && _passwordTextEditingController.text.isNotEmpty && _passwordTextEditingController.text.isNotEmpty && _instanceTextEditingController.text.isNotEmpty) + ? _handleOAuthLogin + : (_instanceTextEditingController.text.isNotEmpty && widget.anonymous) + ? () => _addAnonymousInstance(context) + : null, + child: Text(provider.displayName, style: theme.textTheme.titleMedium?.copyWith(color: !isLoading && fieldsFilledIn ? theme.colorScheme.onPrimary : theme.colorScheme.primary)), ), - onPressed: (!isLoading && _passwordTextEditingController.text.isNotEmpty && _passwordTextEditingController.text.isNotEmpty && _instanceTextEditingController.text.isNotEmpty) - ? _handleOAuthLogin - : (_instanceTextEditingController.text.isNotEmpty && widget.anonymous) - ? () => _addAnonymousInstance(context) - : null, - child: Text("Privacy Portal", style: theme.textTheme.titleMedium?.copyWith(color: !isLoading && fieldsFilledIn ? theme.colorScheme.onPrimary : theme.colorScheme.primary)), - ), const SizedBox(height: 12.0), TextButton( style: ElevatedButton.styleFrom(minimumSize: const Size.fromHeight(60)), diff --git a/lib/utils/instance.dart b/lib/utils/instance.dart index 33cf60eed..02925b96b 100644 --- a/lib/utils/instance.dart +++ b/lib/utils/instance.dart @@ -179,6 +179,7 @@ class GetInstanceInfoResponse { final String? domain; final int? users; final int? id; + final List? oauthProviders; const GetInstanceInfoResponse({ required this.success, @@ -188,6 +189,7 @@ class GetInstanceInfoResponse { this.domain, this.users, this.id, + this.oauthProviders, }); bool isMetadataPopulated() => icon != null || version != null || name != null || users != null; @@ -199,7 +201,8 @@ Future getInstanceInfo(String? url, {int? id, Duration? } try { - final site = await LemmyApiV3(url!).run(const GetSite()).timeout(timeout ?? const Duration(seconds: 5)); + // TODO: need to remove tls and debug args, this is just for testing. + final site = await LemmyApiV3(url!, tls: false, debug: true).run(const GetSite()).timeout(timeout ?? const Duration(seconds: 5)); return GetInstanceInfoResponse( success: true, icon: site.siteView.site.icon, @@ -208,6 +211,7 @@ Future getInstanceInfo(String? url, {int? id, Duration? domain: fetchInstanceNameFromUrl(site.siteView.site.actorId), users: site.siteView.counts.users, id: id, + oauthProviders: site.oauthProviders, ); } catch (e) { // Bad instances will throw an exception, so no icon From 07dd4b66a86c19525c335f7d319cf433e16576ed Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Fri, 13 Dec 2024 20:44:30 -0500 Subject: [PATCH 15/50] Dynamically add oauth providers to login screen --- lib/account/pages/login_page.dart | 10 +++++++--- lib/core/auth/bloc/auth_bloc.dart | 5 +---- lib/core/auth/bloc/auth_event.dart | 5 +++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index 7987d97ee..8466f8c54 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -451,8 +451,10 @@ class _LoginPageState extends State with SingleTickerProviderStateMix color: theme.colorScheme.onPrimary, ), ), - onPressed: (!isLoading && _passwordTextEditingController.text.isNotEmpty && _passwordTextEditingController.text.isNotEmpty && _instanceTextEditingController.text.isNotEmpty) - ? _handleOAuthLogin + onPressed: (!isLoading && _instanceTextEditingController.text.isNotEmpty) + ? () { + _handleOAuthLogin(provider: provider); + } : (_instanceTextEditingController.text.isNotEmpty && widget.anonymous) ? () => _addAnonymousInstance(context) : null, @@ -488,12 +490,14 @@ class _LoginPageState extends State with SingleTickerProviderStateMix ); } - void _handleOAuthLogin({bool showContentWarning = true}) { + // TODO: Set showContentWarning default value to true. Need to relogin after. + void _handleOAuthLogin({required ProviderView provider, bool showContentWarning = false}) { TextInput.finishAutofillContext(); // Perform login authentication context.read().add( OAuthLoginAttempt( instance: _instanceTextEditingController.text.trim(), + provider: provider, ), ); } diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index c4e9ae70a..77d682c86 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -205,14 +205,11 @@ class AuthBloc extends Bloc { String instance = event.instance; if (instance.startsWith('https://')) instance = instance.replaceAll('https://', ''); - if (instance.startsWith('http://')) instance = instance.replaceAll('http://', ''); lemmyClient.changeBaseUrl(instance); LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; - // TODO: Select from a list of Providers, for now it is hard coded to provider0. - GetSiteResponse siteResponse = await lemmy.run(const GetSite()); - ProviderView provider = siteResponse.oauthProviders!.elementAt(0); + ProviderView provider = event.provider; debugPrint(provider.toString()); var authorizationEndpoint = Uri.parse(provider.authorizationEndpoint); diff --git a/lib/core/auth/bloc/auth_event.dart b/lib/core/auth/bloc/auth_event.dart index 5e56fbadf..5acd83e70 100644 --- a/lib/core/auth/bloc/auth_event.dart +++ b/lib/core/auth/bloc/auth_event.dart @@ -29,13 +29,14 @@ class LoginAttempt extends AuthEvent { const LoginAttempt({required this.username, required this.password, required this.instance, this.totp = "", this.showContentWarning = true}); } -/// The [LoginAttempt] event should be triggered whenever the user attempts to log in for the first time. +/// The [OAuthLoginAttempt] event should be triggered whenever the user attempts to log in with OAuth. /// This event is responsible for login authentication and handling related errors. class OAuthLoginAttempt extends AuthEvent { final String instance; + final ProviderView provider; final bool showContentWarning; - const OAuthLoginAttempt({required this.instance, this.showContentWarning = true}); + const OAuthLoginAttempt({required this.instance, required this.provider, this.showContentWarning = true}); } /// Cancels a login attempt by emitting the `failure` state. From 8a656263341b7e3ed51e83089cce8d3891e1ec1e Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Fri, 13 Dec 2024 20:52:28 -0500 Subject: [PATCH 16/50] Touch ups --- lib/account/pages/login_page.dart | 3 ++- lib/core/auth/bloc/auth_bloc.dart | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index 8466f8c54..16fa5b5b6 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -458,7 +458,8 @@ class _LoginPageState extends State with SingleTickerProviderStateMix : (_instanceTextEditingController.text.isNotEmpty && widget.anonymous) ? () => _addAnonymousInstance(context) : null, - child: Text(provider.displayName, style: theme.textTheme.titleMedium?.copyWith(color: !isLoading && fieldsFilledIn ? theme.colorScheme.onPrimary : theme.colorScheme.primary)), + child: Text(provider.displayName, + style: theme.textTheme.titleMedium?.copyWith(color: !isLoading && _instanceTextEditingController.text.isNotEmpty ? theme.colorScheme.onPrimary : theme.colorScheme.primary)), ), const SizedBox(height: 12.0), TextButton( diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index 77d682c86..c5efc78b8 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -197,9 +197,6 @@ class AuthBloc extends Bloc { LemmyClient lemmyClient = LemmyClient.instance; String originalBaseUrl = lemmyClient.lemmyApiV3.host; - // lemmy client_id, can be found be found on the lemmy OAuth Configuration page. - //String clientId = '9d16fb35-090f-4426-a456-368d9412861f'; - try { emit(state.copyWith(status: AuthStatus.loading, account: null, isLoggedIn: false)); From 3b89294bcac0e1e20bd4c22615e7fbf00419d678 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Fri, 20 Dec 2024 20:24:32 -0500 Subject: [PATCH 17/50] working on macos --- assets/localhost.crt | 30 ++++++++++++++++++ assets/localhost.key | 52 +++++++++++++++++++++++++++++++ lib/core/auth/bloc/auth_bloc.dart | 22 +++++++++++-- macos/Podfile.lock | 2 +- pubspec.yaml | 2 ++ 5 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 assets/localhost.crt create mode 100644 assets/localhost.key diff --git a/assets/localhost.crt b/assets/localhost.crt new file mode 100644 index 000000000..d16481156 --- /dev/null +++ b/assets/localhost.crt @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFMjCCAxqgAwIBAgIULb6rzw3r+aSudB/DozCTMVXsyfowDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MTIyMDIzNTc1OFoXDTM0MTIx +ODIzNTc1OFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAmo9tmKwewzLFrrigRpUJei4hS6hp4SWHx96YR0OQY508 +4Vkg+0Pp/EevCc4f9LP8YsYrejM+WtqHNnowuvcDK4ww0qPlGbqsvqkJYW6wqSEZ +ExpDeqUAmga1XEkPukQNWOYTyodklogLaU9iW6Mhc2ruoPKeSrNseU3KXZF0LVmc +WKerZIvl94CM6C8zIEOUoG7CEehhhRRWURm0vnfbeoA/QF3n0vXT3IHXXA3nuRPu +QfOZoixK7qMMqcB9ht+SEGs0EEF/kPluPXst6HFKPbjJHHCKLiwssjVFiz7eZzKJ +U2ADGYouUKBOAzFe37/VH8MaczbBorQ8XQICrfJnUcYQIZc2CrvVNTsHsVrK8qp4 +OaXozMgGKOmaW0UZeHOF2AltZpY7fY7jtpcecxieX18nNN9LHuGGQe/E3pkNWNue +aIAEXD0ugdXr+4pzRNgc4p8ZRpKXG/TpO8G/rI3rGgUPdtdiYPC0Lie9EFebaLwj +euMbgVJs54RkgPJVXoIAYkQqkLpTj5/7HafLk393Q9QjL3kIYfkV2nEHm1CVWkOj +lxH2nxqE1/K62ayD+S1SzA0TxaU3eA3ON6LvPmvNCUMU3h/uWnA0EorTACNR2ILS ++eIgarB9UuEa8+I/Sz9KCNd6jK+fW7pKoMGcl32Su91IEHP5tBIt+jQVigZfeNcC +AwEAAaN8MHowHQYDVR0OBBYEFG4bXAzNFBtFsH6AahtMOF3RtcL2MB8GA1UdIwQY +MBaAFG4bXAzNFBtFsH6AahtMOF3RtcL2MA8GA1UdEwEB/wQFMAMBAf8wJwYDVR0R +BCAwHoIJbG9jYWxob3N0ggsqLmxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0BAQsF +AAOCAgEAOOTBo2AJCNdSiwtDYIq7QIF55RcFM8s/x1/k+Lgv/zrXH6Zoy5xlYDKN +xfSjWhSvccGm4MaEdryvsaUy0MRhFUlATigpbYYOyZOadnUWBoEfIzigEgHBCZ/5 +MYcOgzh2nk2eZPrkZKZIr6rjcEuQxIoqM9p4xlHPqN9bssDJktA7hfwE76EDs4NR +SyLqfqfxhxdSrLclox6T3gCIUYhBiun2Q1l3E92p9mSwThhYDhDp83lTIIjJjR7u +pnpTYyNKSWIp4TVDLJzmc2abaV0anFtWbfcUB+DMxWgdrcD4CM+7COqiLCfNoq+Q +5o3e3fGGkowr/f+TtHEwbfrg7JVCw9GV4vi/JLHcSWzFU5yZsPARoUkEF0oDCL+P +CX4aNcY5ZDKsQnye3iHcD83vq5NrQI3lZZ2Vk55d46PBa/m0q+VJ/39BU5Tpdtnv +OKGPSzgp21GpA6lyxLwC2guG3kMx1zWH9ZZLeyIniXOy9jln6b+lLU4z0/ruwc2N +erEpgtu6TSY7JwpdhvdlwJccgYGU02bddDVfpVE4f7yEvujV7D/Nk1X5DKs0fX6X +rF8TGlb93EjCKug3cBS2n9hWAlYEsfotxmHw+BGOBvalwpIrjB1vyuslzF+eQdHq +IC+ho1OIuD6XCDrenesRgzavv1RzlyJBBUcJW119Femsd3jbnwY= +-----END CERTIFICATE----- diff --git a/assets/localhost.key b/assets/localhost.key new file mode 100644 index 000000000..2b51b4d4c --- /dev/null +++ b/assets/localhost.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCaj22YrB7DMsWu +uKBGlQl6LiFLqGnhJYfH3phHQ5BjnTzhWSD7Q+n8R68Jzh/0s/xixit6Mz5a2oc2 +ejC69wMrjDDSo+UZuqy+qQlhbrCpIRkTGkN6pQCaBrVcSQ+6RA1Y5hPKh2SWiAtp +T2JboyFzau6g8p5Ks2x5TcpdkXQtWZxYp6tki+X3gIzoLzMgQ5SgbsIR6GGFFFZR +GbS+d9t6gD9AXefS9dPcgddcDee5E+5B85miLEruowypwH2G35IQazQQQX+Q+W49 +ey3ocUo9uMkccIouLCyyNUWLPt5nMolTYAMZii5QoE4DMV7fv9UfwxpzNsGitDxd +AgKt8mdRxhAhlzYKu9U1OwexWsryqng5pejMyAYo6ZpbRRl4c4XYCW1mljt9juO2 +lx5zGJ5fXyc030se4YZB78TemQ1Y255ogARcPS6B1ev7inNE2BzinxlGkpcb9Ok7 +wb+sjesaBQ9212Jg8LQuJ70QV5tovCN64xuBUmznhGSA8lVeggBiRCqQulOPn/sd +p8uTf3dD1CMveQhh+RXacQebUJVaQ6OXEfafGoTX8rrZrIP5LVLMDRPFpTd4Dc43 +ou8+a80JQxTeH+5acDQSitMAI1HYgtL54iBqsH1S4Rrz4j9LP0oI13qMr59bukqg +wZyXfZK73UgQc/m0Ei36NBWKBl941wIDAQABAoICABka1O1of7KrC4r+uCHe0WRE +G+pjF5eXdf+T+14P7mMPxjTAOmg1tsrgheDs1ynzFjqg/6zgp+8v8ah6nnGv75bi +NYfxUSQluytY34ow5YcDNkRxDqbcKEXccxbjUyepKBXZgTtzVHZS8K+RUmOaErPh +mZMQ9X8it6rYZNdf6eP32zpXObKiOp9CBSEtkXtbHsgUVXd9LGHmVMLljwMlCsRS +EnQNDLuqbFgzytxL8eiRATE7NSgvU5iBaWwlNP50UBBUqWc+jE7rBOn9mQ5ZYHq4 +CgqRiRBI4pWrq3kbpBpVDhM51CcQ18cG0sUX/tYPHboEAcbXDQq1hdyBmBfS+M8B +2xwD/pAfSrVjjNtrjYyfbizYzwlFwWAPILZQSj8yIVIHqDHse1uzdwD/sZVhLhVH +hj3Ug6DsVLIIBt7Wq9i0tz09QVy6DfT3LtksWeCp2CLHTpb7iQSMBYoRg2dTV/AB +ZHAzQDk76YX44T2azmVacBofyGqywPHE9Wz4NXqekKGMRRWx9XCEalesivE76kmP +++yK0C3DE/YZUlbGz/STHT52JMIpdtjWcUGGDFD0xD5X87p5fCba2TUA/MyqXvcK +EUaJMmgn30zYvOP2Xj4y4vQi1YVAl7Xbn1fIjHOVpBPkuDFfnwo3AuQeP/GoIpj0 +lKJ9HLh4/8GCH92imuBhAoIBAQDNPng2G62xmmNvSC+eeiXWoEzYciyXGEDI8rOG +4EvZ3ldirJSNbCC05OqHSgvYnFJgixb4EzZN3LGSuLrvcyQqVsz2nxEt+oWs8ten +Od0l0N1dMX3lh7Wi+Rb/JgUS4b41bqNT7tT9tCBFAU9Imuv9XEO/vaMt+32E2XCG +BSqKN4sMvT6ySgVqpykAjbaftpmTUBHraJCEDYhDmd4nkolfwuEBpW34ND2ZwCPy +X/WEU/+fKqPQ2JeJI5JAfm3YPvq4B0L/ELbgK2XRJQLAqLNVNbUdxsyzJDjFbGbd +Q9fygouUHO+wUxqxAE/SqetIQbWxiSElme7IrCFocsO9vE0nAoIBAQDAyEkiIRH8 +q5edkYCF90k7ki061yG/TpZr6MTXlNY1KnHWfBlSrmlQJ4ZT02phzz+geFj/QZb0 +u5FbLIlN0MN7JabT6JmwaPz5bt/x3mdj+YScCBk8lWwEyYsDvLioVphbrZT4S2XB +oj4R41nW/ft8lbWW+wPxa4JvJjR7Kilu9vCK1le0tGtnvNrHjGllF0cLkZ4mE5+l +VEl9MCvmyXM+BGjuBKlAsDbBG81JpN4wpmqYCuLvy5f8pRH8BXgSkNYsASiADXCG +NgN39eEGjHciL/13nlKYAdFgXEtFw6E7IrGZK35EbaluXXS29fIRekQFLmxwOtdz +ZRJ91C05vSTRAoIBAFP4k/wnNNgt/zKfSQRAm0yFRwtjIwUqYg8U5QhwqffYNM5l +J134+CSqZ520WMZlpnpjTaFvUs9mVKxfsfOXmxtLag4YpFG4ZoqMzFhZnzYCjx66 +yfRnopOr75GyP28rNsProR0M4M1vragt0f81iwmcfwdqkeGVPBRnVdcvM+lasiQj +JQySpkatX2QflrEfZxPTNZGntUChvLdTs4VjOZsZQy+GPEjJLs7BwrM+OVfLehDn +xCAFDXKJQCPs1gocMj2qkumCMB/lAYIg71BddQmOsKwfEs7UKfnz0N4EDMzmRi7x +68qrJYd3RjE9XcqxP6IEJbCZmw01B3IRSi5NZQ8CggEATe/q2RhjjDHW7sXHHuHV +QncbQAF/TDc6Ss/k3H74hq/tK9gp6KpIOzZvcO40wOwnffmJiVB79d7qqeB8dfAj +R2L2ag9MKuyW8URo1wCh7eIPQYFoqnyCGgFc6Rrf0HaJy+6GHkdlEP5Fd7fhNzCg +/kIMEsjSVESxi7v3VZ+69nhw0MBM3updzaelDy1t4oehmkS5mg0u6okD2M+jv/7L +T1Q7E5bg0h0rVbCmstIrXaG50FP+YRF/FY2qkqenXmIdo9aoB/Tm++tURagq3Bnn +g/PA1h40p+18Nye46rBnO2AQSqsxtfpbmBnCOMF/pp82Zp3ZCxpOxgEjk6k2y3Pz +MQKCAQAHj1FpBvQmHlzwYqvj3lRHbPohZljHfQJG8dE0PzQLA74hiPilDtds9wOe +epyshPdT1r9nXJ9u8QccZZaf05atOI89YluilhtgLqsZGLCiCSsjOsKVTf+vgx08 +lHVppe1aig7tl22nzIdw8a4IJMtIID2LVKl2QZTCnLL7dtOyyNOw0gV4lwHOrDS+ +2z9IUjHd02yxsM4dfyQOqZxNhLDaxFPx8gyB+De6AWmSl7OKeHv/Y/teQYWj1vWe +sQpDyvxDEUEELEsq/iyvhazRohdKQb7FkcksvQCd9yixdWcvZj4jiRUjxwGbVHdI +NufTrvdPtl0kAdTS4rNV85WVJG3N +-----END PRIVATE KEY----- diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index c5efc78b8..01626d8e5 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -5,6 +5,7 @@ import 'dart:math'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:lemmy_api_client/v3.dart'; import 'package:collection/collection.dart'; @@ -12,6 +13,7 @@ import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:stream_transform/stream_transform.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:path/path.dart' as path; import 'package:thunder/utils/error_messages.dart'; import 'package:thunder/account/models/account.dart'; @@ -211,7 +213,7 @@ class AuthBloc extends Bloc { var authorizationEndpoint = Uri.parse(provider.authorizationEndpoint); // Build oauth provider url. - String redirectUri = "http://localhost:40000/oauth/callback"; // This must end in /oauth/callback. + String redirectUri = "https://localhost:40000/oauth/callback"; // This must end in /oauth/callback. String oauthClientState = const Uuid().v4(); final url = Uri.https(authorizationEndpoint.host, authorizationEndpoint.path, { 'response_type': 'code', @@ -223,7 +225,21 @@ class AuthBloc extends Bloc { // Start http server to receive callback. // TODO: Figure out how to do this in a better cross-platform way. Maybe, https://pub.dev/packages/app_links - HttpServer server = await HttpServer.bind("localhost", 40000); + //Directory.current = path.dirname(Platform.script.toFilePath()); + + var chain = utf8.encode(await rootBundle.loadString('assets/localhost.crt')); + var key = utf8.encode(await rootBundle.loadString('assets/localhost.key')); + var serverContext = SecurityContext(); + serverContext.useCertificateChainBytes(chain); + serverContext.usePrivateKeyBytes(key); + + var server = await HttpServer.bindSecure("localhost", 40000, serverContext); + // await server.forEach((HttpRequest request) { + // request.response.write('Hello, world!'); + // request.response.close(); + // }); + + //HttpServer server = await HttpServer.bind("localhost", 40000); // Present the dialog to the user. final result = FlutterWebAuth2.authenticate(url: url.toString(), callbackUrlScheme: "thunder"); @@ -251,7 +267,7 @@ class AuthBloc extends Bloc { // TODO: This should use lemmy_api_client. // Authenthicate to lemmy and get a jwt. // Durring this step lemmy with connect to the Provider to get the user info. - final response = await http.post(Uri.parse('http://localhost/api/v3/oauth/authenticate'), + final response = await http.post(Uri.parse('https://hopandzip.com/api/v3/oauth/authenticate'), headers: { 'Content-Type': 'application/json', }, diff --git a/macos/Podfile.lock b/macos/Podfile.lock index c33df8e36..f4587142a 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -137,12 +137,12 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563 + desktop_webview_window: d4365e71bcd4e1aa0c14cf0377aa24db0c16a7e2 device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215 dynamic_color: 2eaa27267de1ca20d879fbd6e01259773fb1670f file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b flutter_local_notifications: 7062189aabf7f50938a7b8b6614ffa97656eb0bf - flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9 flutter_web_auth_2: 2e1dc2d2139973e4723c5286ce247dd590390d70 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 diff --git a/pubspec.yaml b/pubspec.yaml index 3fce59630..91764edf3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -111,6 +111,8 @@ flutter: uses-material-design: true generate: true assets: + - "assets/localhost.crt" + - "assets/localhost.key" - "assets/logo.png" - "assets/logo_monochrome.png" - "assets/logo_android.png" From f342e2c29101030751d623053fbdf3c694b2fc8d Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Sat, 21 Dec 2024 21:56:55 -0500 Subject: [PATCH 18/50] working better now --- lib/core/auth/bloc/auth_bloc.dart | 22 ++++++++-------------- lib/core/singletons/lemmy_client.dart | 2 +- lib/thunder/pages/thunder_page.dart | 1 + lib/utils/instance.dart | 2 +- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index 01626d8e5..51fafb07f 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -198,6 +198,7 @@ class AuthBloc extends Bloc { on((event, emit) async { LemmyClient lemmyClient = LemmyClient.instance; String originalBaseUrl = lemmyClient.lemmyApiV3.host; + HttpServer? server; try { emit(state.copyWith(status: AuthStatus.loading, account: null, isLoggedIn: false)); @@ -223,23 +224,15 @@ class AuthBloc extends Bloc { 'state': oauthClientState, }); - // Start http server to receive callback. - // TODO: Figure out how to do this in a better cross-platform way. Maybe, https://pub.dev/packages/app_links - //Directory.current = path.dirname(Platform.script.toFilePath()); - + // Start https server to receive callback. This is just for development. + // TODO: Figure out how to do this in a better way for mobile. var chain = utf8.encode(await rootBundle.loadString('assets/localhost.crt')); var key = utf8.encode(await rootBundle.loadString('assets/localhost.key')); var serverContext = SecurityContext(); serverContext.useCertificateChainBytes(chain); serverContext.usePrivateKeyBytes(key); - - var server = await HttpServer.bindSecure("localhost", 40000, serverContext); - // await server.forEach((HttpRequest request) { - // request.response.write('Hello, world!'); - // request.response.close(); - // }); - - //HttpServer server = await HttpServer.bind("localhost", 40000); + server = await HttpServer.bindSecure("localhost", 40000, serverContext); + //server = await HttpServer.bind("localhost", 40000); // Present the dialog to the user. final result = FlutterWebAuth2.authenticate(url: url.toString(), callbackUrlScheme: "thunder"); @@ -267,7 +260,7 @@ class AuthBloc extends Bloc { // TODO: This should use lemmy_api_client. // Authenthicate to lemmy and get a jwt. // Durring this step lemmy with connect to the Provider to get the user info. - final response = await http.post(Uri.parse('https://hopandzip.com/api/v3/oauth/authenticate'), + final response = await http.post(Uri.parse('https://$instance/api/v3/oauth/authenticate'), headers: { 'Content-Type': 'application/json', }, @@ -285,7 +278,7 @@ class AuthBloc extends Bloc { GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: accessToken)); - // TODO: Login fails when this is uncommented. + // TODO: Login fails when this is uncommented. Have to get this working. //if (event.showContentWarning && getSiteResponse.siteView.site.contentWarning?.isNotEmpty == true) { // return emit(state.copyWith(status: AuthStatus.contentWarning, contentWarning: getSiteResponse.siteView.site.contentWarning)); //} @@ -317,6 +310,7 @@ class AuthBloc extends Bloc { return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString())); } catch (e) { try { + await server!.close(); // Restore the original baseUrl lemmyClient.changeBaseUrl(originalBaseUrl); } catch (e, s) { diff --git a/lib/core/singletons/lemmy_client.dart b/lib/core/singletons/lemmy_client.dart index d61df6497..4264e9a21 100644 --- a/lib/core/singletons/lemmy_client.dart +++ b/lib/core/singletons/lemmy_client.dart @@ -11,7 +11,7 @@ class LemmyClient { LemmyClient._initialize(); void changeBaseUrl(String baseUrl) { - lemmyApiV3 = LemmyApiV3(baseUrl, tls: false, debug: true); + lemmyApiV3 = LemmyApiV3(baseUrl, tls: true, debug: true); _populateSiteInfo(); // Do NOT await this. Let it populate in the background. } diff --git a/lib/thunder/pages/thunder_page.dart b/lib/thunder/pages/thunder_page.dart index 3f01410c7..3940a91c4 100644 --- a/lib/thunder/pages/thunder_page.dart +++ b/lib/thunder/pages/thunder_page.dart @@ -95,6 +95,7 @@ class _ThunderState extends State { @override void initState() { super.initState(); + context.read().add(const LogOutOfAllAccounts()); selectedPageIndex = widget.pageController.initialPage; diff --git a/lib/utils/instance.dart b/lib/utils/instance.dart index 02925b96b..bcfef18b2 100644 --- a/lib/utils/instance.dart +++ b/lib/utils/instance.dart @@ -202,7 +202,7 @@ Future getInstanceInfo(String? url, {int? id, Duration? try { // TODO: need to remove tls and debug args, this is just for testing. - final site = await LemmyApiV3(url!, tls: false, debug: true).run(const GetSite()).timeout(timeout ?? const Duration(seconds: 5)); + final site = await LemmyApiV3(url!, tls: true, debug: true).run(const GetSite()).timeout(timeout ?? const Duration(seconds: 5)); return GetInstanceInfoResponse( success: true, icon: site.siteView.site.icon, From d6cc39717d87d7487f885f3a6266a7b508ff8d91 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Sun, 22 Dec 2024 16:00:14 -0500 Subject: [PATCH 19/50] touch ups --- lib/core/auth/bloc/auth_bloc.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index 51fafb07f..134f23419 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -250,8 +250,7 @@ class AuthBloc extends Bloc { // Extract the code from the response. String code = Uri.parse(providerResponse.uri.toString()).queryParameters['code'] ?? "failed"; - debugPrint("CODE"); - debugPrint(code); + debugPrint("CODE $code"); if (code == "failed") { throw Exception("OAuth login failed: no code received from provider."); @@ -259,7 +258,7 @@ class AuthBloc extends Bloc { // TODO: This should use lemmy_api_client. // Authenthicate to lemmy and get a jwt. - // Durring this step lemmy with connect to the Provider to get the user info. + // Durring this step lemmy connects to the Provider to get the user info. final response = await http.post(Uri.parse('https://$instance/api/v3/oauth/authenticate'), headers: { 'Content-Type': 'application/json', @@ -271,10 +270,11 @@ class AuthBloc extends Bloc { }), encoding: Encoding.getByName('utf-8')); + // TODO: Need to add a step to set the account username. + final accessToken = jsonDecode(response.body)['jwt'] as String; - debugPrint("JWT"); - debugPrint(accessToken); + debugPrint("JWT $accessToken"); GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: accessToken)); From 401acbdb73215d06f5ffff0be05731ca5ee100cf Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Sun, 29 Dec 2024 23:50:51 -0500 Subject: [PATCH 20/50] oauth is working with localhost callback on desktop and mobile, working on getting thunderapp.dev callback working --- ios/Podfile.lock | 8 +- lib/core/auth/bloc/auth_bloc.dart | 44 ++++++---- .../deep_links_cubit/deep_links_cubit.dart | 11 ++- lib/thunder/enums/deep_link_enums.dart | 1 + lib/thunder/pages/thunder_page.dart | 82 +++++++++++++++++++ 5 files changed, 128 insertions(+), 18 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 24fbb48e7..2e8be8492 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -30,6 +30,8 @@ PODS: - Flutter - flutter_sharing_intent (0.0.1): - Flutter + - flutter_web_auth_2 (3.0.0): + - Flutter - gal (1.0.0): - Flutter - FlutterMacOS @@ -95,6 +97,7 @@ DEPENDENCIES: - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_sharing_intent (from `.symlinks/plugins/flutter_sharing_intent/ios`) + - flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`) - gal (from `.symlinks/plugins/gal/darwin`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) @@ -141,6 +144,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_sharing_intent: :path: ".symlinks/plugins/flutter_sharing_intent/ios" + flutter_web_auth_2: + :path: ".symlinks/plugins/flutter_web_auth_2/ios" gal: :path: ".symlinks/plugins/gal/darwin" image_picker_ios: @@ -183,6 +188,7 @@ SPEC CHECKSUMS: flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98 gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 + flutter_web_auth_2: 06d500582775790a0d4c323222fcb6d7990f9603 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 @@ -200,4 +206,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 8d23d5c4d896af3a5f2a08e0206462ca9882e556 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index 134f23419..fb866e4dc 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -199,6 +199,7 @@ class AuthBloc extends Bloc { LemmyClient lemmyClient = LemmyClient.instance; String originalBaseUrl = lemmyClient.lemmyApiV3.host; HttpServer? server; + bool dev = false; try { emit(state.copyWith(status: AuthStatus.loading, account: null, isLoggedIn: false)); @@ -214,7 +215,7 @@ class AuthBloc extends Bloc { var authorizationEndpoint = Uri.parse(provider.authorizationEndpoint); // Build oauth provider url. - String redirectUri = "https://localhost:40000/oauth/callback"; // This must end in /oauth/callback. + String redirectUri = dev ? "https://localhost:40000/oauth/callback" : "https://thunderapp.dev/oauth/callback"; // This must end in /oauth/callback. String oauthClientState = const Uuid().v4(); final url = Uri.https(authorizationEndpoint.host, authorizationEndpoint.path, { 'response_type': 'code', @@ -224,32 +225,45 @@ class AuthBloc extends Bloc { 'state': oauthClientState, }); - // Start https server to receive callback. This is just for development. - // TODO: Figure out how to do this in a better way for mobile. - var chain = utf8.encode(await rootBundle.loadString('assets/localhost.crt')); - var key = utf8.encode(await rootBundle.loadString('assets/localhost.key')); - var serverContext = SecurityContext(); - serverContext.useCertificateChainBytes(chain); - serverContext.usePrivateKeyBytes(key); - server = await HttpServer.bindSecure("localhost", 40000, serverContext); - //server = await HttpServer.bind("localhost", 40000); + debugPrint("URL $url"); + + // Start a localhost https server to receive callback. This is just for development. + // ignore: dead_code + if (dev) { + var chain = utf8.encode(await rootBundle.loadString('assets/localhost.crt')); + var key = utf8.encode(await rootBundle.loadString('assets/localhost.key')); + var serverContext = SecurityContext(); + serverContext.useCertificateChainBytes(chain); + serverContext.usePrivateKeyBytes(key); + server = await HttpServer.bindSecure("localhost", 40000, serverContext); + } // Present the dialog to the user. - final result = FlutterWebAuth2.authenticate(url: url.toString(), callbackUrlScheme: "thunder"); + // TODO: Probably remove FlutterWebAuth2 its just being used to open the provider page. + debugPrint("REDIRECT"); - // Wait for response from Provider. - final providerResponse = await server.first; + final result = FlutterWebAuth2.authenticate(url: url.toString(), callbackUrlScheme: "https"); + debugPrint("REDIRECT DONE"); + if (!dev) { + return; + } + // Wait for response from Provider. + // ignore: dead_code + var providerResponse = await server!.first; await server.close(); + String providerResponseString = providerResponse.uri.toString(); + + debugPrint("RESULT $providerResponseString"); // oauthProviderState must match oauthClientState to ensure the response came from the Provider. - String oauthProviderState = Uri.parse(providerResponse.uri.toString()).queryParameters['state'] ?? "failed"; + String oauthProviderState = Uri.parse(providerResponseString).queryParameters['state'] ?? "failed"; if (oauthProviderState == "failed" || oauthClientState != oauthProviderState) { throw Exception("OAuth state-check failed: oauthProviderState $oauthClientState must match oauthClientState $oauthClientState to ensure the response came from the Provider."); } // Extract the code from the response. - String code = Uri.parse(providerResponse.uri.toString()).queryParameters['code'] ?? "failed"; + String code = Uri.parse(providerResponseString).queryParameters['code'] ?? "failed"; debugPrint("CODE $code"); if (code == "failed") { diff --git a/lib/thunder/cubits/deep_links_cubit/deep_links_cubit.dart b/lib/thunder/cubits/deep_links_cubit/deep_links_cubit.dart index 0d67621ef..f975de961 100644 --- a/lib/thunder/cubits/deep_links_cubit/deep_links_cubit.dart +++ b/lib/thunder/cubits/deep_links_cubit/deep_links_cubit.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:app_links/app_links.dart'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:thunder/thunder/enums/deep_link_enums.dart'; @@ -25,6 +26,7 @@ class DeepLinksCubit extends Cubit { try { // First, check to see if this is an internal Thunder link List internalLinks = ['thunder://setting-']; + debugPrint("APP LINK $link"); if (internalLinks.where((internalLink) => link.startsWith(internalLink)).isNotEmpty) { return emit(state.copyWith( @@ -33,8 +35,13 @@ class DeepLinksCubit extends Cubit { linkType: LinkType.thunder, )); } - - if (link.contains("/u/")) { + if (link.contains("/oauth/callback")) { + emit(state.copyWith( + deepLinkStatus: DeepLinkStatus.success, + link: link, + linkType: LinkType.oauth, + )); + } else if (link.contains("/u/")) { emit(state.copyWith( deepLinkStatus: DeepLinkStatus.success, link: link, diff --git a/lib/thunder/enums/deep_link_enums.dart b/lib/thunder/enums/deep_link_enums.dart index 8e8c65e41..9ae666102 100644 --- a/lib/thunder/enums/deep_link_enums.dart +++ b/lib/thunder/enums/deep_link_enums.dart @@ -7,4 +7,5 @@ enum LinkType { community, modlog, thunder, + oauth, } diff --git a/lib/thunder/pages/thunder_page.dart b/lib/thunder/pages/thunder_page.dart index 3940a91c4..c1b668cfd 100644 --- a/lib/thunder/pages/thunder_page.dart +++ b/lib/thunder/pages/thunder_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; // Flutter @@ -59,6 +60,7 @@ import 'package:thunder/instance/utils/navigate_instance.dart'; import 'package:thunder/post/utils/navigate_post.dart'; import 'package:thunder/notification/utils/navigate_notification.dart'; import 'package:thunder/utils/settings_utils.dart'; +import 'package:http/http.dart' as http; String? currentIntent; @@ -242,6 +244,8 @@ class _ThunderState extends State { if (context.mounted) await _navigateToInstance(link); case LinkType.thunder: if (context.mounted) await _navigateToInternal(link); + case LinkType.oauth: + if (context.mounted) await _oauthCallback(link); case LinkType.unknown: if (context.mounted) { _showLinkProcessingError(context, AppLocalizations.of(context)!.uriNotSupported, link); @@ -249,6 +253,84 @@ class _ThunderState extends State { } } + Future _oauthCallback(String link) async { + // TODO: Need to know state, and instance. + + try { + String redirectUri = "https://thunderapp.dev/oauth/callback"; + debugPrint("_oauthCallback $link"); + // oauthProviderState must match oauthClientState to ensure the response came from the Provider. + String oauthProviderState = Uri.parse(link).queryParameters['state'] ?? "failed"; + if (oauthProviderState == "failed" || oauthClientState != oauthProviderState) { + throw Exception("OAuth state-check failed: oauthProviderState $oauthClientState must match oauthClientState $oauthClientState to ensure the response came from the Provider."); + } + + // Extract the code from the response. + String code = Uri.parse(link).queryParameters['code'] ?? "failed"; + + debugPrint("CODE $code"); + + if (code == "failed") { + throw Exception("OAuth login failed: no code received from provider."); + } + + // TODO: This should use lemmy_api_client. + // Authenthicate to lemmy and get a jwt. + // Durring this step lemmy connects to the Provider to get the user info. + final response = await http.post(Uri.parse('https://$instance/api/v3/oauth/authenticate'), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode({ + 'code': code, + 'oauth_provider_id': 1, // This id can be found in the site reponse. + 'redirect_uri': redirectUri, + }), + encoding: Encoding.getByName('utf-8')); + + // TODO: Need to add a step to set the account username. + + final accessToken = jsonDecode(response.body)['jwt'] as String; + + debugPrint("JWT $accessToken"); + + GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: accessToken)); + + // TODO: Login fails when this is uncommented. Have to get this working. + //if (event.showContentWarning && getSiteResponse.siteView.site.contentWarning?.isNotEmpty == true) { + // return emit(state.copyWith(status: AuthStatus.contentWarning, contentWarning: getSiteResponse.siteView.site.contentWarning)); + //} + + // Create a new account in the database + Account? account = Account( + id: '', + username: getSiteResponse.myUser?.localUserView.person.name, + jwt: accessToken, + instance: instance, + userId: getSiteResponse.myUser?.localUserView.person.id, + index: -1, + ); + + account = await Account.insertAccount(account); + + if (account == null) { + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false)); + } + + // Set this account as the active account + SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences; + prefs.setString('active_profile_id', account.id); + + bool downvotesEnabled = getSiteResponse.siteView.localSite.enableDownvotes ?? false; + + return emit(state.copyWith(status: AuthStatus.success, account: account, isLoggedIn: true, downvotesEnabled: downvotesEnabled, getSiteResponse: getSiteResponse)); + } catch (e) { + if (context.mounted) { + _showLinkProcessingError(context, AppLocalizations.of(context)!.exceptionProcessingUri, link); + } + } + } + Future _navigateToInstance(String link) async { try { await navigateToInstancePage( From 530cef62414820cb29ec85062dc5e71d6179a89e Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Mon, 30 Dec 2024 21:12:51 -0500 Subject: [PATCH 21/50] remove code from _oauthCallback --- lib/thunder/pages/thunder_page.dart | 68 ----------------------------- 1 file changed, 68 deletions(-) diff --git a/lib/thunder/pages/thunder_page.dart b/lib/thunder/pages/thunder_page.dart index c1b668cfd..19ce2314d 100644 --- a/lib/thunder/pages/thunder_page.dart +++ b/lib/thunder/pages/thunder_page.dart @@ -254,76 +254,8 @@ class _ThunderState extends State { } Future _oauthCallback(String link) async { - // TODO: Need to know state, and instance. - try { - String redirectUri = "https://thunderapp.dev/oauth/callback"; debugPrint("_oauthCallback $link"); - // oauthProviderState must match oauthClientState to ensure the response came from the Provider. - String oauthProviderState = Uri.parse(link).queryParameters['state'] ?? "failed"; - if (oauthProviderState == "failed" || oauthClientState != oauthProviderState) { - throw Exception("OAuth state-check failed: oauthProviderState $oauthClientState must match oauthClientState $oauthClientState to ensure the response came from the Provider."); - } - - // Extract the code from the response. - String code = Uri.parse(link).queryParameters['code'] ?? "failed"; - - debugPrint("CODE $code"); - - if (code == "failed") { - throw Exception("OAuth login failed: no code received from provider."); - } - - // TODO: This should use lemmy_api_client. - // Authenthicate to lemmy and get a jwt. - // Durring this step lemmy connects to the Provider to get the user info. - final response = await http.post(Uri.parse('https://$instance/api/v3/oauth/authenticate'), - headers: { - 'Content-Type': 'application/json', - }, - body: json.encode({ - 'code': code, - 'oauth_provider_id': 1, // This id can be found in the site reponse. - 'redirect_uri': redirectUri, - }), - encoding: Encoding.getByName('utf-8')); - - // TODO: Need to add a step to set the account username. - - final accessToken = jsonDecode(response.body)['jwt'] as String; - - debugPrint("JWT $accessToken"); - - GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: accessToken)); - - // TODO: Login fails when this is uncommented. Have to get this working. - //if (event.showContentWarning && getSiteResponse.siteView.site.contentWarning?.isNotEmpty == true) { - // return emit(state.copyWith(status: AuthStatus.contentWarning, contentWarning: getSiteResponse.siteView.site.contentWarning)); - //} - - // Create a new account in the database - Account? account = Account( - id: '', - username: getSiteResponse.myUser?.localUserView.person.name, - jwt: accessToken, - instance: instance, - userId: getSiteResponse.myUser?.localUserView.person.id, - index: -1, - ); - - account = await Account.insertAccount(account); - - if (account == null) { - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false)); - } - - // Set this account as the active account - SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences; - prefs.setString('active_profile_id', account.id); - - bool downvotesEnabled = getSiteResponse.siteView.localSite.enableDownvotes ?? false; - - return emit(state.copyWith(status: AuthStatus.success, account: account, isLoggedIn: true, downvotesEnabled: downvotesEnabled, getSiteResponse: getSiteResponse)); } catch (e) { if (context.mounted) { _showLinkProcessingError(context, AppLocalizations.of(context)!.exceptionProcessingUri, link); From a81682eb6af58a45ab9fe1b2c450468f4eda7af6 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Mon, 30 Dec 2024 23:32:38 -0500 Subject: [PATCH 22/50] split OAuthLoginAttempt into 2 parts --- lib/account/pages/login_page.dart | 2 +- lib/core/auth/bloc/auth_bloc.dart | 117 ++++++++++++++++++++++++++-- lib/core/auth/bloc/auth_event.dart | 12 ++- lib/core/auth/bloc/auth_state.dart | 8 ++ lib/thunder/pages/thunder_page.dart | 1 + 5 files changed, 129 insertions(+), 11 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index 16fa5b5b6..9e4459e12 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -496,7 +496,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix TextInput.finishAutofillContext(); // Perform login authentication context.read().add( - OAuthLoginAttempt( + OAuthLoginAttemptPart1( instance: _instanceTextEditingController.text.trim(), provider: provider, ), diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index fb866e4dc..b76287a86 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; @@ -14,6 +15,7 @@ import 'package:stream_transform/stream_transform.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:path/path.dart' as path; +import 'package:thunder/thunder/cubits/deep_links_cubit/deep_links_cubit.dart'; import 'package:thunder/utils/error_messages.dart'; import 'package:thunder/account/models/account.dart'; @@ -195,7 +197,7 @@ class AuthBloc extends Bloc { }); /// This event should be triggered when the user logs in with oauth. - on((event, emit) async { + on((event, emit) async { LemmyClient lemmyClient = LemmyClient.instance; String originalBaseUrl = lemmyClient.lemmyApiV3.host; HttpServer? server; @@ -216,13 +218,13 @@ class AuthBloc extends Bloc { // Build oauth provider url. String redirectUri = dev ? "https://localhost:40000/oauth/callback" : "https://thunderapp.dev/oauth/callback"; // This must end in /oauth/callback. - String oauthClientState = const Uuid().v4(); + String oauthState = const Uuid().v4(); final url = Uri.https(authorizationEndpoint.host, authorizationEndpoint.path, { 'response_type': 'code', 'client_id': provider.clientId, 'redirect_uri': redirectUri, 'scope': provider.scopes, - 'state': oauthClientState, + 'state': oauthState, }); debugPrint("URL $url"); @@ -244,10 +246,9 @@ class AuthBloc extends Bloc { final result = FlutterWebAuth2.authenticate(url: url.toString(), callbackUrlScheme: "https"); debugPrint("REDIRECT DONE"); - if (!dev) { - return; - } + return emit(state.copyWith(oauthState: oauthState, oauthInstance: instance)); + /* // Wait for response from Provider. // ignore: dead_code var providerResponse = await server!.first; @@ -258,8 +259,8 @@ class AuthBloc extends Bloc { // oauthProviderState must match oauthClientState to ensure the response came from the Provider. String oauthProviderState = Uri.parse(providerResponseString).queryParameters['state'] ?? "failed"; - if (oauthProviderState == "failed" || oauthClientState != oauthProviderState) { - throw Exception("OAuth state-check failed: oauthProviderState $oauthClientState must match oauthClientState $oauthClientState to ensure the response came from the Provider."); + if (oauthProviderState == "failed" || oauthState != oauthProviderState) { + throw Exception("OAuth state-check failed: oauthProviderState $oauthState must match oauthClientState $oauthState to ensure the response came from the Provider."); } // Extract the code from the response. @@ -320,6 +321,7 @@ class AuthBloc extends Bloc { bool downvotesEnabled = getSiteResponse.siteView.localSite.enableDownvotes ?? false; return emit(state.copyWith(status: AuthStatus.success, account: account, isLoggedIn: true, downvotesEnabled: downvotesEnabled, getSiteResponse: getSiteResponse)); + */ } on LemmyApiException catch (e) { return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString())); } catch (e) { @@ -334,6 +336,105 @@ class AuthBloc extends Bloc { } }); + on((event, emit) async { + LemmyClient lemmyClient = LemmyClient.instance; + String originalBaseUrl = lemmyClient.lemmyApiV3.host; + + try { + debugPrint("PART2 ${event.link}"); + + LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; + String redirectUri = "https://thunderapp.dev/oauth/callback"; + String providerResponse = event.link; + + if (state.oauthState == null) { + throw Exception("OAuth login failed: oauthState is null."); + } + + if (state.oauthInstance == null) { + throw Exception("OAuth login failed: oauthInstance is null."); + } + + debugPrint("RESULT $providerResponse"); + + // oauthProviderState must match oauthClientState to ensure the response came from the Provider. + String oauthProviderState = Uri.parse(providerResponse).queryParameters['state'] ?? "failed"; + if (oauthProviderState == "failed" || state.oauthState != oauthProviderState) { + throw Exception("OAuth state-check failed: oauthProviderState ${state.oauthState} must match oauthClientState ${state.oauthState} to ensure the response came from the Provider."); + } + + // Extract the code from the response. + String code = Uri.parse(providerResponse).queryParameters['code'] ?? "failed"; + debugPrint("CODE $code"); + + if (code == "failed") { + throw Exception("OAuth login failed: no code received from provider."); + } + + // TODO: This should use lemmy_api_client. + // Authenthicate to lemmy and get a jwt. + // Durring this step lemmy connects to the Provider to get the user info. + final response = await http.post(Uri.parse('https://${state.oauthInstance}/api/v3/oauth/authenticate'), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode({ + 'code': code, + 'oauth_provider_id': 1, // This id can be found in the site reponse. + 'redirect_uri': redirectUri, + }), + encoding: Encoding.getByName('utf-8')); + + // TODO: Need to add a step to set the account username. + + final accessToken = jsonDecode(response.body)['jwt'] as String; + + debugPrint("JWT $accessToken"); + + GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: accessToken)); + + // TODO: Login fails when this is uncommented. Have to get this working. + //if (event.showContentWarning && getSiteResponse.siteView.site.contentWarning?.isNotEmpty == true) { + // return emit(state.copyWith(status: AuthStatus.contentWarning, contentWarning: getSiteResponse.siteView.site.contentWarning)); + //} + + // Create a new account in the database + Account? account = Account( + id: '', + username: getSiteResponse.myUser?.localUserView.person.name, + jwt: accessToken, + instance: state.oauthInstance ?? "", + userId: getSiteResponse.myUser?.localUserView.person.id, + index: -1, + ); + + account = await Account.insertAccount(account); + + if (account == null) { + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false)); + } + + // Set this account as the active account + SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences; + prefs.setString('active_profile_id', account.id); + + bool downvotesEnabled = getSiteResponse.siteView.localSite.enableDownvotes ?? false; + + return emit(state.copyWith( + status: AuthStatus.success, account: account, isLoggedIn: true, downvotesEnabled: downvotesEnabled, getSiteResponse: getSiteResponse, oauthState: null, oauthInstance: null)); + } on LemmyApiException catch (e) { + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null)); + } catch (e) { + try { + // Restore the original baseUrl + lemmyClient.changeBaseUrl(originalBaseUrl); + } catch (e, s) { + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: s.toString(), oauthState: null, oauthInstance: null)); + } + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null)); + } + }); + on((event, emit) async { return emit(state.copyWith(status: AuthStatus.failure, errorMessage: AppLocalizations.of(GlobalContext.context)!.loginAttemptCanceled)); }); diff --git a/lib/core/auth/bloc/auth_event.dart b/lib/core/auth/bloc/auth_event.dart index 5acd83e70..7dd369ed3 100644 --- a/lib/core/auth/bloc/auth_event.dart +++ b/lib/core/auth/bloc/auth_event.dart @@ -31,12 +31,20 @@ class LoginAttempt extends AuthEvent { /// The [OAuthLoginAttempt] event should be triggered whenever the user attempts to log in with OAuth. /// This event is responsible for login authentication and handling related errors. -class OAuthLoginAttempt extends AuthEvent { +class OAuthLoginAttemptPart1 extends AuthEvent { final String instance; final ProviderView provider; final bool showContentWarning; - const OAuthLoginAttempt({required this.instance, required this.provider, this.showContentWarning = true}); + const OAuthLoginAttemptPart1({required this.instance, required this.provider, this.showContentWarning = true}); +} + +/// The [OAuthLoginAttempt] event should be triggered whenever the user attempts to log in with OAuth. +/// This event is responsible for login authentication and handling related errors. +class OAuthLoginAttemptPart2 extends AuthEvent { + final String link; + + const OAuthLoginAttemptPart2({required this.link}); } /// Cancels a login attempt by emitting the `failure` state. diff --git a/lib/core/auth/bloc/auth_state.dart b/lib/core/auth/bloc/auth_state.dart index 172d08cd7..7cf8b45cf 100644 --- a/lib/core/auth/bloc/auth_state.dart +++ b/lib/core/auth/bloc/auth_state.dart @@ -12,6 +12,8 @@ class AuthState extends Equatable { this.getSiteResponse, this.reload = true, this.contentWarning, + this.oauthInstance, + this.oauthState, }); final AuthStatus status; @@ -22,6 +24,8 @@ class AuthState extends Equatable { final GetSiteResponse? getSiteResponse; final bool reload; final String? contentWarning; + final String? oauthInstance; + final String? oauthState; AuthState copyWith({ AuthStatus? status, @@ -32,6 +36,8 @@ class AuthState extends Equatable { GetSiteResponse? getSiteResponse, bool? reload, String? contentWarning, + String? oauthInstance, + String? oauthState, }) { return AuthState( status: status ?? this.status, @@ -42,6 +48,8 @@ class AuthState extends Equatable { getSiteResponse: getSiteResponse ?? this.getSiteResponse, reload: reload ?? this.reload, contentWarning: contentWarning, + oauthInstance: oauthInstance ?? this.oauthInstance, + oauthState: oauthState ?? this.oauthInstance, ); } diff --git a/lib/thunder/pages/thunder_page.dart b/lib/thunder/pages/thunder_page.dart index 19ce2314d..c4932c4a4 100644 --- a/lib/thunder/pages/thunder_page.dart +++ b/lib/thunder/pages/thunder_page.dart @@ -256,6 +256,7 @@ class _ThunderState extends State { Future _oauthCallback(String link) async { try { debugPrint("_oauthCallback $link"); + context.read().add(OAuthLoginAttemptPart2(link: link)); } catch (e) { if (context.mounted) { _showLinkProcessingError(context, AppLocalizations.of(context)!.exceptionProcessingUri, link); From a25d11b125157c7145ca94e8a65ad87d5c3883ca Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Mon, 30 Dec 2024 23:48:07 -0500 Subject: [PATCH 23/50] oauth login is working on android --- lib/core/auth/bloc/auth_bloc.dart | 8 +++++++- lib/core/auth/bloc/auth_state.dart | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index b76287a86..291fca970 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -247,7 +247,7 @@ class AuthBloc extends Bloc { final result = FlutterWebAuth2.authenticate(url: url.toString(), callbackUrlScheme: "https"); debugPrint("REDIRECT DONE"); - return emit(state.copyWith(oauthState: oauthState, oauthInstance: instance)); + emit(state.copyWith(oauthState: oauthState, oauthInstance: instance)); /* // Wait for response from Provider. // ignore: dead_code @@ -336,6 +336,12 @@ class AuthBloc extends Bloc { } }); + @override + void onChange(Change change) { + super.onChange(change); + debugPrint("CHANGE $change"); + } + on((event, emit) async { LemmyClient lemmyClient = LemmyClient.instance; String originalBaseUrl = lemmyClient.lemmyApiV3.host; diff --git a/lib/core/auth/bloc/auth_state.dart b/lib/core/auth/bloc/auth_state.dart index 7cf8b45cf..97a38ab68 100644 --- a/lib/core/auth/bloc/auth_state.dart +++ b/lib/core/auth/bloc/auth_state.dart @@ -54,5 +54,5 @@ class AuthState extends Equatable { } @override - List get props => [status, isLoggedIn, errorMessage, account, downvotesEnabled, getSiteResponse, reload]; + List get props => [status, isLoggedIn, errorMessage, account, downvotesEnabled, getSiteResponse, reload, contentWarning, oauthInstance, oauthState]; } From b5a5e074a6fc861cf6a9231c0946b955f57a9513 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Thu, 2 Jan 2025 23:32:25 -0500 Subject: [PATCH 24/50] Clean up --- lib/core/auth/bloc/auth_bloc.dart | 95 ++++++++++++++++++------------ lib/core/auth/bloc/auth_event.dart | 10 ++++ 2 files changed, 68 insertions(+), 37 deletions(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index 291fca970..89cec9250 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'dart:math'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; @@ -14,8 +13,6 @@ import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:stream_transform/stream_transform.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:path/path.dart' as path; -import 'package:thunder/thunder/cubits/deep_links_cubit/deep_links_cubit.dart'; import 'package:thunder/utils/error_messages.dart'; import 'package:thunder/account/models/account.dart'; @@ -196,12 +193,11 @@ class AuthBloc extends Bloc { } }); - /// This event should be triggered when the user logs in with oauth. - on((event, emit) async { + /// This should only be used for development. + on((event, emit) async { LemmyClient lemmyClient = LemmyClient.instance; String originalBaseUrl = lemmyClient.lemmyApiV3.host; HttpServer? server; - bool dev = false; try { emit(state.copyWith(status: AuthStatus.loading, account: null, isLoggedIn: false)); @@ -217,7 +213,7 @@ class AuthBloc extends Bloc { var authorizationEndpoint = Uri.parse(provider.authorizationEndpoint); // Build oauth provider url. - String redirectUri = dev ? "https://localhost:40000/oauth/callback" : "https://thunderapp.dev/oauth/callback"; // This must end in /oauth/callback. + String redirectUri = "https://localhost:40000/oauth/callback"; // This must end in /oauth/callback. String oauthState = const Uuid().v4(); final url = Uri.https(authorizationEndpoint.host, authorizationEndpoint.path, { 'response_type': 'code', @@ -227,36 +223,22 @@ class AuthBloc extends Bloc { 'state': oauthState, }); - debugPrint("URL $url"); - // Start a localhost https server to receive callback. This is just for development. - // ignore: dead_code - if (dev) { - var chain = utf8.encode(await rootBundle.loadString('assets/localhost.crt')); - var key = utf8.encode(await rootBundle.loadString('assets/localhost.key')); - var serverContext = SecurityContext(); - serverContext.useCertificateChainBytes(chain); - serverContext.usePrivateKeyBytes(key); - server = await HttpServer.bindSecure("localhost", 40000, serverContext); - } + var chain = utf8.encode(await rootBundle.loadString('assets/localhost.crt')); + var key = utf8.encode(await rootBundle.loadString('assets/localhost.key')); + var serverContext = SecurityContext(); + serverContext.useCertificateChainBytes(chain); + serverContext.usePrivateKeyBytes(key); + server = await HttpServer.bindSecure("localhost", 40000, serverContext); // Present the dialog to the user. - // TODO: Probably remove FlutterWebAuth2 its just being used to open the provider page. - debugPrint("REDIRECT"); - final result = FlutterWebAuth2.authenticate(url: url.toString(), callbackUrlScheme: "https"); - debugPrint("REDIRECT DONE"); - emit(state.copyWith(oauthState: oauthState, oauthInstance: instance)); - /* // Wait for response from Provider. - // ignore: dead_code - var providerResponse = await server!.first; + var providerResponse = await server.first; await server.close(); String providerResponseString = providerResponse.uri.toString(); - debugPrint("RESULT $providerResponseString"); - // oauthProviderState must match oauthClientState to ensure the response came from the Provider. String oauthProviderState = Uri.parse(providerResponseString).queryParameters['state'] ?? "failed"; if (oauthProviderState == "failed" || oauthState != oauthProviderState) { @@ -265,7 +247,6 @@ class AuthBloc extends Bloc { // Extract the code from the response. String code = Uri.parse(providerResponseString).queryParameters['code'] ?? "failed"; - debugPrint("CODE $code"); if (code == "failed") { throw Exception("OAuth login failed: no code received from provider."); @@ -289,8 +270,6 @@ class AuthBloc extends Bloc { final accessToken = jsonDecode(response.body)['jwt'] as String; - debugPrint("JWT $accessToken"); - GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: accessToken)); // TODO: Login fails when this is uncommented. Have to get this working. @@ -321,7 +300,6 @@ class AuthBloc extends Bloc { bool downvotesEnabled = getSiteResponse.siteView.localSite.enableDownvotes ?? false; return emit(state.copyWith(status: AuthStatus.success, account: account, isLoggedIn: true, downvotesEnabled: downvotesEnabled, getSiteResponse: getSiteResponse)); - */ } on LemmyApiException catch (e) { return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString())); } catch (e) { @@ -336,11 +314,54 @@ class AuthBloc extends Bloc { } }); - @override - void onChange(Change change) { - super.onChange(change); - debugPrint("CHANGE $change"); - } + /// This event should be triggered when the user logs in with oauth. + on((event, emit) async { + LemmyClient lemmyClient = LemmyClient.instance; + String originalBaseUrl = lemmyClient.lemmyApiV3.host; + + try { + emit(state.copyWith(status: AuthStatus.loading, account: null, isLoggedIn: false)); + + String instance = event.instance; + if (instance.startsWith('https://')) instance = instance.replaceAll('https://', ''); + + lemmyClient.changeBaseUrl(instance); + LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; + + ProviderView provider = event.provider; + debugPrint(provider.toString()); + var authorizationEndpoint = Uri.parse(provider.authorizationEndpoint); + + // Build oauth provider url. + String redirectUri = "https://thunderapp.dev/oauth/callback"; // This must end in /oauth/callback. + String oauthState = const Uuid().v4(); + final url = Uri.https(authorizationEndpoint.host, authorizationEndpoint.path, { + 'response_type': 'code', + 'client_id': provider.clientId, + 'redirect_uri': redirectUri, + 'scope': provider.scopes, + 'state': oauthState, + }); + + debugPrint("URL $url"); + + // Present the dialog to the user. + // TODO: Probably remove FlutterWebAuth2 its just being used to open the provider page. + final result = FlutterWebAuth2.authenticate(url: url.toString(), callbackUrlScheme: "https"); + + return emit(state.copyWith(oauthState: oauthState, oauthInstance: instance)); + } on LemmyApiException catch (e) { + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString())); + } catch (e) { + try { + // Restore the original baseUrl + lemmyClient.changeBaseUrl(originalBaseUrl); + } catch (e, s) { + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: s.toString())); + } + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString())); + } + }); on((event, emit) async { LemmyClient lemmyClient = LemmyClient.instance; diff --git a/lib/core/auth/bloc/auth_event.dart b/lib/core/auth/bloc/auth_event.dart index 7dd369ed3..4cfe3c81b 100644 --- a/lib/core/auth/bloc/auth_event.dart +++ b/lib/core/auth/bloc/auth_event.dart @@ -39,6 +39,16 @@ class OAuthLoginAttemptPart1 extends AuthEvent { const OAuthLoginAttemptPart1({required this.instance, required this.provider, this.showContentWarning = true}); } +/// The [OAuthLoginAttemptDesktop] event should be triggered whenever the user attempts to log in with OAuth. +/// This event is responsible for login authentication and handling related errors. +class OAuthLoginAttemptDesktop extends AuthEvent { + final String instance; + final ProviderView provider; + final bool showContentWarning; + + const OAuthLoginAttemptDesktop({required this.instance, required this.provider, this.showContentWarning = true}); +} + /// The [OAuthLoginAttempt] event should be triggered whenever the user attempts to log in with OAuth. /// This event is responsible for login authentication and handling related errors. class OAuthLoginAttemptPart2 extends AuthEvent { From e93ca0c0ef9dcbbcbaf2b85a95a33a79959e0942 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Fri, 3 Jan 2025 00:04:56 -0500 Subject: [PATCH 25/50] about to remove WebAuthPackage --- ios/Podfile.lock | 8 ++++---- lib/core/auth/bloc/auth_bloc.dart | 16 ++++++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2e8be8492..f4f3ebb9a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -69,7 +69,7 @@ PODS: - sqlite3_flutter_libs (0.0.1): - Flutter - FlutterMacOS - - sqlite3 (~> 3.47.2) + - sqlite3 (~> 3.47.1) - sqlite3/dbstatvtab - sqlite3/fts5 - sqlite3/perf-threadsafe @@ -176,7 +176,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0 background_fetch: 39f11371c0dce04b001c4bfd5e782bcccb0a85e2 - connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695 + connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563 device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_custom_tabs_ios: a651b18786388923b62de8c0537607de87c2eccf @@ -187,8 +187,8 @@ SPEC CHECKSUMS: flutter_local_notifications: df98d66e515e1ca797af436137b4459b160ad8c9 flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98 - gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 flutter_web_auth_2: 06d500582775790a0d4c323222fcb6d7990f9603 + gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 @@ -199,7 +199,7 @@ SPEC CHECKSUMS: shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d sqlite3: 7559e33dae4c78538df563795af3a86fc887ee71 - sqlite3_flutter_libs: 58ae36c0dd086395d066b4fe4de9cdca83e717b3 + sqlite3_flutter_libs: 1b4e98da20ebd4e9b1240269b78cdcf492dbe9f3 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index 89cec9250..a6a8be55b 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -20,6 +20,7 @@ import 'package:thunder/core/singletons/preferences.dart'; import 'package:thunder/core/singletons/lemmy_client.dart'; import 'package:thunder/utils/global_context.dart'; import 'package:http/http.dart' as http; +import 'package:url_launcher/url_launcher.dart'; import 'package:uuid/uuid.dart'; part 'auth_event.dart'; @@ -231,8 +232,10 @@ class AuthBloc extends Bloc { serverContext.usePrivateKeyBytes(key); server = await HttpServer.bindSecure("localhost", 40000, serverContext); - // Present the dialog to the user. - final result = FlutterWebAuth2.authenticate(url: url.toString(), callbackUrlScheme: "https"); + // Present the login dialog to the user. + if (!await launchUrl(url)) { + throw Exception('Could not launch $url'); + } // Wait for response from Provider. var providerResponse = await server.first; @@ -254,7 +257,7 @@ class AuthBloc extends Bloc { // TODO: This should use lemmy_api_client. // Authenthicate to lemmy and get a jwt. - // Durring this step lemmy connects to the Provider to get the user info. + // During this step lemmy connects to the Provider to get the user info. final response = await http.post(Uri.parse('https://$instance/api/v3/oauth/authenticate'), headers: { 'Content-Type': 'application/json', @@ -345,9 +348,10 @@ class AuthBloc extends Bloc { debugPrint("URL $url"); - // Present the dialog to the user. - // TODO: Probably remove FlutterWebAuth2 its just being used to open the provider page. - final result = FlutterWebAuth2.authenticate(url: url.toString(), callbackUrlScheme: "https"); + // Present the login dialog to the user. + if (!await launchUrl(url)) { + throw Exception('Could not launch $url'); + } return emit(state.copyWith(oauthState: oauthState, oauthInstance: instance)); } on LemmyApiException catch (e) { From 320c093761b5b606d3dbe494418ff5856a944155 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Fri, 3 Jan 2025 00:11:06 -0500 Subject: [PATCH 26/50] removed flutter_web_auth_2 --- lib/account/pages/login_page.dart | 4 ---- lib/core/auth/bloc/auth_bloc.dart | 1 - linux/flutter/generated_plugin_registrant.cc | 8 -------- linux/flutter/generated_plugins.cmake | 2 -- macos/Flutter/GeneratedPluginRegistrant.swift | 6 ------ pubspec.lock | 16 ---------------- windows/flutter/generated_plugin_registrant.cc | 6 ------ windows/flutter/generated_plugins.cmake | 2 -- 8 files changed, 45 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index 9e4459e12..5e2a18c72 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -20,10 +20,6 @@ import 'package:thunder/utils/links.dart'; import 'package:thunder/utils/text_input_formatter.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; -import 'dart:convert' show jsonDecode; -import 'package:http/http.dart' as http; - class LoginPage extends StatefulWidget { final VoidCallback popRegister; final bool anonymous; diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index a6a8be55b..2e5758c82 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -6,7 +6,6 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:lemmy_api_client/v3.dart'; import 'package:collection/collection.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 3bc20b2ae..3c33ff3a2 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,18 +6,13 @@ #include "generated_plugin_registrant.h" -#include #include #include #include #include #include -#include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); - desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); g_autoptr(FlPluginRegistrar) dynamic_color_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); @@ -33,7 +28,4 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); - g_autoptr(FlPluginRegistrar) window_to_front_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "WindowToFrontPlugin"); - window_to_front_plugin_register_with_registrar(window_to_front_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index b1ecbc1c2..19433b77f 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,13 +3,11 @@ # list(APPEND FLUTTER_PLUGIN_LIST - desktop_webview_window dynamic_color file_selector_linux gtk sqlite3_flutter_libs url_launcher_linux - window_to_front ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d8c1e098c..aea1e666a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,13 +7,11 @@ import Foundation import app_links import connectivity_plus -import desktop_webview_window import device_info_plus import dynamic_color import file_selector_macos import flutter_inappwebview_macos import flutter_local_notifications -import flutter_web_auth_2 import gal import path_provider_foundation import share_plus @@ -23,18 +21,15 @@ import sqlite3_flutter_libs import url_launcher_macos import video_player_avfoundation import webview_flutter_wkwebview -import window_to_front func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) - DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) - FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) @@ -44,5 +39,4 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) FLTWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "FLTWebViewFlutterPlugin")) - WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 579c26c18..9a994ad86 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -382,14 +382,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" - desktop_webview_window: - dependency: transitive - description: - name: desktop_webview_window - sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0" - url: "https://pub.dev" - source: hosted - version: "0.2.3" device_info_plus: dependency: "direct main" description: @@ -2093,14 +2085,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.5" - window_to_front: - dependency: transitive - description: - name: window_to_front - sha256: "7aef379752b7190c10479e12b5fd7c0b9d92adc96817d9e96c59937929512aee" - url: "https://pub.dev" - source: hosted - version: "0.0.3" xayn_readability: dependency: "direct main" description: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 96e5b4d3a..f00a0a2a8 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,7 +8,6 @@ #include #include -#include #include #include #include @@ -17,15 +16,12 @@ #include #include #include -#include void RegisterPlugins(flutter::PluginRegistry* registry) { AppLinksPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("AppLinksPluginCApi")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); - DesktopWebviewWindowPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); FileSelectorWindowsRegisterWithRegistrar( @@ -42,6 +38,4 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); - WindowToFrontPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("WindowToFrontPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 03df4b235..a768ae795 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,7 +5,6 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links connectivity_plus - desktop_webview_window dynamic_color file_selector_windows flutter_inappwebview_windows @@ -14,7 +13,6 @@ list(APPEND FLUTTER_PLUGIN_LIST share_plus sqlite3_flutter_libs url_launcher_windows - window_to_front ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From b8395a1ab461ff289d7672fa0fc498cd68fe870c Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Fri, 3 Jan 2025 00:16:43 -0500 Subject: [PATCH 27/50] ios build update --- ios/Podfile.lock | 6 ------ .../xcshareddata/xcschemes/Open In Thunder.xcscheme | 1 - 2 files changed, 7 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f4f3ebb9a..00153fc42 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -30,8 +30,6 @@ PODS: - Flutter - flutter_sharing_intent (0.0.1): - Flutter - - flutter_web_auth_2 (3.0.0): - - Flutter - gal (1.0.0): - Flutter - FlutterMacOS @@ -97,7 +95,6 @@ DEPENDENCIES: - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_sharing_intent (from `.symlinks/plugins/flutter_sharing_intent/ios`) - - flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`) - gal (from `.symlinks/plugins/gal/darwin`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) @@ -144,8 +141,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_sharing_intent: :path: ".symlinks/plugins/flutter_sharing_intent/ios" - flutter_web_auth_2: - :path: ".symlinks/plugins/flutter_web_auth_2/ios" gal: :path: ".symlinks/plugins/gal/darwin" image_picker_ios: @@ -187,7 +182,6 @@ SPEC CHECKSUMS: flutter_local_notifications: df98d66e515e1ca797af436137b4459b160ad8c9 flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98 - flutter_web_auth_2: 06d500582775790a0d4c323222fcb6d7990f9603 gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Open In Thunder.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Open In Thunder.xcscheme index 6a6b36b47..4819c8a51 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Open In Thunder.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Open In Thunder.xcscheme @@ -73,7 +73,6 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES" - askForAppToLaunch = "Yes" launchAutomaticallySubstyle = "2"> From fcb868adb1c98e1743dd4c03dab3419c45d46d89 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Fri, 3 Jan 2025 18:31:22 -0500 Subject: [PATCH 28/50] use AuthenticateWithOauth from lemmy_api_client --- lib/core/auth/bloc/auth_bloc.dart | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index 2e5758c82..dfa998c64 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -368,6 +368,7 @@ class AuthBloc extends Bloc { on((event, emit) async { LemmyClient lemmyClient = LemmyClient.instance; + LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; String originalBaseUrl = lemmyClient.lemmyApiV3.host; try { @@ -401,23 +402,14 @@ class AuthBloc extends Bloc { throw Exception("OAuth login failed: no code received from provider."); } - // TODO: This should use lemmy_api_client. + // TODO: dynamic provider_id. // Authenthicate to lemmy and get a jwt. // Durring this step lemmy connects to the Provider to get the user info. - final response = await http.post(Uri.parse('https://${state.oauthInstance}/api/v3/oauth/authenticate'), - headers: { - 'Content-Type': 'application/json', - }, - body: json.encode({ - 'code': code, - 'oauth_provider_id': 1, // This id can be found in the site reponse. - 'redirect_uri': redirectUri, - }), - encoding: Encoding.getByName('utf-8')); + LoginResponse loginResponse = await lemmy.run(AuthenticateWithOAuth(code: code, oauth_provider_id: 1, redirect_uri: redirectUri)); // TODO: Need to add a step to set the account username. - final accessToken = jsonDecode(response.body)['jwt'] as String; + final accessToken = loginResponse.jwt as String; debugPrint("JWT $accessToken"); From bd8698667d0598328b2792d33f04778688cad7a8 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Fri, 3 Jan 2025 18:43:36 -0500 Subject: [PATCH 29/50] dynamic oauth provider id --- lib/core/auth/bloc/auth_bloc.dart | 9 +++++++-- lib/core/auth/bloc/auth_state.dart | 27 +++++++++++++++------------ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index dfa998c64..1ca1d9b1f 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -333,6 +333,7 @@ class AuthBloc extends Bloc { ProviderView provider = event.provider; debugPrint(provider.toString()); var authorizationEndpoint = Uri.parse(provider.authorizationEndpoint); + var providerId = provider.id; // Build oauth provider url. String redirectUri = "https://thunderapp.dev/oauth/callback"; // This must end in /oauth/callback. @@ -352,7 +353,7 @@ class AuthBloc extends Bloc { throw Exception('Could not launch $url'); } - return emit(state.copyWith(oauthState: oauthState, oauthInstance: instance)); + return emit(state.copyWith(oauthState: oauthState, oauthInstance: instance, oauthProviderId: provider.id)); } on LemmyApiException catch (e) { return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString())); } catch (e) { @@ -386,6 +387,10 @@ class AuthBloc extends Bloc { throw Exception("OAuth login failed: oauthInstance is null."); } + if (state.oauthProviderId == null) { + throw Exception("OAuth login failed: oauthProviderId is null."); + } + debugPrint("RESULT $providerResponse"); // oauthProviderState must match oauthClientState to ensure the response came from the Provider. @@ -405,7 +410,7 @@ class AuthBloc extends Bloc { // TODO: dynamic provider_id. // Authenthicate to lemmy and get a jwt. // Durring this step lemmy connects to the Provider to get the user info. - LoginResponse loginResponse = await lemmy.run(AuthenticateWithOAuth(code: code, oauth_provider_id: 1, redirect_uri: redirectUri)); + LoginResponse loginResponse = await lemmy.run(AuthenticateWithOAuth(code: code, oauth_provider_id: state.oauthProviderId ?? -1, redirect_uri: redirectUri)); // TODO: Need to add a step to set the account username. diff --git a/lib/core/auth/bloc/auth_state.dart b/lib/core/auth/bloc/auth_state.dart index 97a38ab68..d0d618b07 100644 --- a/lib/core/auth/bloc/auth_state.dart +++ b/lib/core/auth/bloc/auth_state.dart @@ -14,6 +14,7 @@ class AuthState extends Equatable { this.contentWarning, this.oauthInstance, this.oauthState, + this.oauthProviderId, }); final AuthStatus status; @@ -26,6 +27,7 @@ class AuthState extends Equatable { final String? contentWarning; final String? oauthInstance; final String? oauthState; + final int? oauthProviderId; AuthState copyWith({ AuthStatus? status, @@ -38,21 +40,22 @@ class AuthState extends Equatable { String? contentWarning, String? oauthInstance, String? oauthState, + int? oauthProviderId, }) { return AuthState( - status: status ?? this.status, - isLoggedIn: isLoggedIn ?? false, - errorMessage: errorMessage, - account: account, - downvotesEnabled: downvotesEnabled ?? this.downvotesEnabled, - getSiteResponse: getSiteResponse ?? this.getSiteResponse, - reload: reload ?? this.reload, - contentWarning: contentWarning, - oauthInstance: oauthInstance ?? this.oauthInstance, - oauthState: oauthState ?? this.oauthInstance, - ); + status: status ?? this.status, + isLoggedIn: isLoggedIn ?? false, + errorMessage: errorMessage, + account: account, + downvotesEnabled: downvotesEnabled ?? this.downvotesEnabled, + getSiteResponse: getSiteResponse ?? this.getSiteResponse, + reload: reload ?? this.reload, + contentWarning: contentWarning, + oauthInstance: oauthInstance ?? this.oauthInstance, + oauthState: oauthState ?? this.oauthInstance, + oauthProviderId: oauthProviderId ?? this.oauthProviderId); } @override - List get props => [status, isLoggedIn, errorMessage, account, downvotesEnabled, getSiteResponse, reload, contentWarning, oauthInstance, oauthState]; + List get props => [status, isLoggedIn, errorMessage, account, downvotesEnabled, getSiteResponse, reload, contentWarning, oauthInstance, oauthState, oauthProviderId]; } From 414ae72eb7085750eb437aae181a52562ae772d6 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Fri, 3 Jan 2025 18:48:56 -0500 Subject: [PATCH 30/50] remove https localhost code --- lib/core/auth/bloc/auth_bloc.dart | 144 +++--------------------------- 1 file changed, 14 insertions(+), 130 deletions(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index 1ca1d9b1f..1763e2742 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -193,129 +193,6 @@ class AuthBloc extends Bloc { } }); - /// This should only be used for development. - on((event, emit) async { - LemmyClient lemmyClient = LemmyClient.instance; - String originalBaseUrl = lemmyClient.lemmyApiV3.host; - HttpServer? server; - - try { - emit(state.copyWith(status: AuthStatus.loading, account: null, isLoggedIn: false)); - - String instance = event.instance; - if (instance.startsWith('https://')) instance = instance.replaceAll('https://', ''); - - lemmyClient.changeBaseUrl(instance); - LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; - - ProviderView provider = event.provider; - debugPrint(provider.toString()); - var authorizationEndpoint = Uri.parse(provider.authorizationEndpoint); - - // Build oauth provider url. - String redirectUri = "https://localhost:40000/oauth/callback"; // This must end in /oauth/callback. - String oauthState = const Uuid().v4(); - final url = Uri.https(authorizationEndpoint.host, authorizationEndpoint.path, { - 'response_type': 'code', - 'client_id': provider.clientId, - 'redirect_uri': redirectUri, - 'scope': provider.scopes, - 'state': oauthState, - }); - - // Start a localhost https server to receive callback. This is just for development. - var chain = utf8.encode(await rootBundle.loadString('assets/localhost.crt')); - var key = utf8.encode(await rootBundle.loadString('assets/localhost.key')); - var serverContext = SecurityContext(); - serverContext.useCertificateChainBytes(chain); - serverContext.usePrivateKeyBytes(key); - server = await HttpServer.bindSecure("localhost", 40000, serverContext); - - // Present the login dialog to the user. - if (!await launchUrl(url)) { - throw Exception('Could not launch $url'); - } - - // Wait for response from Provider. - var providerResponse = await server.first; - await server.close(); - String providerResponseString = providerResponse.uri.toString(); - - // oauthProviderState must match oauthClientState to ensure the response came from the Provider. - String oauthProviderState = Uri.parse(providerResponseString).queryParameters['state'] ?? "failed"; - if (oauthProviderState == "failed" || oauthState != oauthProviderState) { - throw Exception("OAuth state-check failed: oauthProviderState $oauthState must match oauthClientState $oauthState to ensure the response came from the Provider."); - } - - // Extract the code from the response. - String code = Uri.parse(providerResponseString).queryParameters['code'] ?? "failed"; - - if (code == "failed") { - throw Exception("OAuth login failed: no code received from provider."); - } - - // TODO: This should use lemmy_api_client. - // Authenthicate to lemmy and get a jwt. - // During this step lemmy connects to the Provider to get the user info. - final response = await http.post(Uri.parse('https://$instance/api/v3/oauth/authenticate'), - headers: { - 'Content-Type': 'application/json', - }, - body: json.encode({ - 'code': code, - 'oauth_provider_id': 1, // This id can be found in the site reponse. - 'redirect_uri': redirectUri, - }), - encoding: Encoding.getByName('utf-8')); - - // TODO: Need to add a step to set the account username. - - final accessToken = jsonDecode(response.body)['jwt'] as String; - - GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: accessToken)); - - // TODO: Login fails when this is uncommented. Have to get this working. - //if (event.showContentWarning && getSiteResponse.siteView.site.contentWarning?.isNotEmpty == true) { - // return emit(state.copyWith(status: AuthStatus.contentWarning, contentWarning: getSiteResponse.siteView.site.contentWarning)); - //} - - // Create a new account in the database - Account? account = Account( - id: '', - username: getSiteResponse.myUser?.localUserView.person.name, - jwt: accessToken, - instance: instance, - userId: getSiteResponse.myUser?.localUserView.person.id, - index: -1, - ); - - account = await Account.insertAccount(account); - - if (account == null) { - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false)); - } - - // Set this account as the active account - SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences; - prefs.setString('active_profile_id', account.id); - - bool downvotesEnabled = getSiteResponse.siteView.localSite.enableDownvotes ?? false; - - return emit(state.copyWith(status: AuthStatus.success, account: account, isLoggedIn: true, downvotesEnabled: downvotesEnabled, getSiteResponse: getSiteResponse)); - } on LemmyApiException catch (e) { - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString())); - } catch (e) { - try { - await server!.close(); - // Restore the original baseUrl - lemmyClient.changeBaseUrl(originalBaseUrl); - } catch (e, s) { - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: s.toString())); - } - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString())); - } - }); - /// This event should be triggered when the user logs in with oauth. on((event, emit) async { LemmyClient lemmyClient = LemmyClient.instance; @@ -355,15 +232,15 @@ class AuthBloc extends Bloc { return emit(state.copyWith(oauthState: oauthState, oauthInstance: instance, oauthProviderId: provider.id)); } on LemmyApiException catch (e) { - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString())); + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProviderId: null)); } catch (e) { try { // Restore the original baseUrl lemmyClient.changeBaseUrl(originalBaseUrl); } catch (e, s) { - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: s.toString())); + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: s.toString(), oauthState: null, oauthInstance: null, oauthProviderId: null)); } - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString())); + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProviderId: null)); } }); @@ -448,17 +325,24 @@ class AuthBloc extends Bloc { bool downvotesEnabled = getSiteResponse.siteView.localSite.enableDownvotes ?? false; return emit(state.copyWith( - status: AuthStatus.success, account: account, isLoggedIn: true, downvotesEnabled: downvotesEnabled, getSiteResponse: getSiteResponse, oauthState: null, oauthInstance: null)); + status: AuthStatus.success, + account: account, + isLoggedIn: true, + downvotesEnabled: downvotesEnabled, + getSiteResponse: getSiteResponse, + oauthState: null, + oauthInstance: null, + oauthProviderId: null)); } on LemmyApiException catch (e) { - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null)); + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProviderId: null)); } catch (e) { try { // Restore the original baseUrl lemmyClient.changeBaseUrl(originalBaseUrl); } catch (e, s) { - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: s.toString(), oauthState: null, oauthInstance: null)); + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: s.toString(), oauthState: null, oauthInstance: null, oauthProviderId: null)); } - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null)); + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProviderId: null)); } }); From 1919ec0f4f880180a826aa38e15b02bc94e4a927 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Fri, 3 Jan 2025 18:55:30 -0500 Subject: [PATCH 31/50] clean up --- lib/core/auth/bloc/auth_bloc.dart | 42 ++++++------------------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index 1763e2742..dd9ea4cf0 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -197,22 +197,17 @@ class AuthBloc extends Bloc { on((event, emit) async { LemmyClient lemmyClient = LemmyClient.instance; String originalBaseUrl = lemmyClient.lemmyApiV3.host; + String instance = event.instance; + ProviderView provider = event.provider; try { emit(state.copyWith(status: AuthStatus.loading, account: null, isLoggedIn: false)); - String instance = event.instance; if (instance.startsWith('https://')) instance = instance.replaceAll('https://', ''); - lemmyClient.changeBaseUrl(instance); - LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; - - ProviderView provider = event.provider; - debugPrint(provider.toString()); - var authorizationEndpoint = Uri.parse(provider.authorizationEndpoint); - var providerId = provider.id; // Build oauth provider url. + var authorizationEndpoint = Uri.parse(provider.authorizationEndpoint); String redirectUri = "https://thunderapp.dev/oauth/callback"; // This must end in /oauth/callback. String oauthState = const Uuid().v4(); final url = Uri.https(authorizationEndpoint.host, authorizationEndpoint.path, { @@ -223,8 +218,6 @@ class AuthBloc extends Bloc { 'state': oauthState, }); - debugPrint("URL $url"); - // Present the login dialog to the user. if (!await launchUrl(url)) { throw Exception('Could not launch $url'); @@ -248,28 +241,14 @@ class AuthBloc extends Bloc { LemmyClient lemmyClient = LemmyClient.instance; LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; String originalBaseUrl = lemmyClient.lemmyApiV3.host; + String redirectUri = "https://thunderapp.dev/oauth/callback"; + String providerResponse = event.link; try { - debugPrint("PART2 ${event.link}"); - - LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; - String redirectUri = "https://thunderapp.dev/oauth/callback"; - String providerResponse = event.link; - - if (state.oauthState == null) { - throw Exception("OAuth login failed: oauthState is null."); - } - - if (state.oauthInstance == null) { - throw Exception("OAuth login failed: oauthInstance is null."); - } - - if (state.oauthProviderId == null) { - throw Exception("OAuth login failed: oauthProviderId is null."); + if (state.oauthState == null || state.oauthInstance == null || state.oauthProviderId == null) { + throw Exception("OAuth login failed: oauthState, oauthInstance, or oauthProviderId is null."); } - debugPrint("RESULT $providerResponse"); - // oauthProviderState must match oauthClientState to ensure the response came from the Provider. String oauthProviderState = Uri.parse(providerResponse).queryParameters['state'] ?? "failed"; if (oauthProviderState == "failed" || state.oauthState != oauthProviderState) { @@ -278,14 +257,11 @@ class AuthBloc extends Bloc { // Extract the code from the response. String code = Uri.parse(providerResponse).queryParameters['code'] ?? "failed"; - debugPrint("CODE $code"); - if (code == "failed") { throw Exception("OAuth login failed: no code received from provider."); } - // TODO: dynamic provider_id. - // Authenthicate to lemmy and get a jwt. + // Authenthicate to lemmy instance and get a jwt. // Durring this step lemmy connects to the Provider to get the user info. LoginResponse loginResponse = await lemmy.run(AuthenticateWithOAuth(code: code, oauth_provider_id: state.oauthProviderId ?? -1, redirect_uri: redirectUri)); @@ -293,8 +269,6 @@ class AuthBloc extends Bloc { final accessToken = loginResponse.jwt as String; - debugPrint("JWT $accessToken"); - GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: accessToken)); // TODO: Login fails when this is uncommented. Have to get this working. From 4df64e57e69151bf787080974898abbdb16e1742 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Fri, 3 Jan 2025 18:59:45 -0500 Subject: [PATCH 32/50] remove fake certs --- assets/localhost.crt | 30 ------------------------- assets/localhost.key | 52 -------------------------------------------- pubspec.yaml | 2 -- 3 files changed, 84 deletions(-) delete mode 100644 assets/localhost.crt delete mode 100644 assets/localhost.key diff --git a/assets/localhost.crt b/assets/localhost.crt deleted file mode 100644 index d16481156..000000000 --- a/assets/localhost.crt +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFMjCCAxqgAwIBAgIULb6rzw3r+aSudB/DozCTMVXsyfowDQYJKoZIhvcNAQEL -BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MTIyMDIzNTc1OFoXDTM0MTIx -ODIzNTc1OFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF -AAOCAg8AMIICCgKCAgEAmo9tmKwewzLFrrigRpUJei4hS6hp4SWHx96YR0OQY508 -4Vkg+0Pp/EevCc4f9LP8YsYrejM+WtqHNnowuvcDK4ww0qPlGbqsvqkJYW6wqSEZ -ExpDeqUAmga1XEkPukQNWOYTyodklogLaU9iW6Mhc2ruoPKeSrNseU3KXZF0LVmc -WKerZIvl94CM6C8zIEOUoG7CEehhhRRWURm0vnfbeoA/QF3n0vXT3IHXXA3nuRPu -QfOZoixK7qMMqcB9ht+SEGs0EEF/kPluPXst6HFKPbjJHHCKLiwssjVFiz7eZzKJ -U2ADGYouUKBOAzFe37/VH8MaczbBorQ8XQICrfJnUcYQIZc2CrvVNTsHsVrK8qp4 -OaXozMgGKOmaW0UZeHOF2AltZpY7fY7jtpcecxieX18nNN9LHuGGQe/E3pkNWNue -aIAEXD0ugdXr+4pzRNgc4p8ZRpKXG/TpO8G/rI3rGgUPdtdiYPC0Lie9EFebaLwj -euMbgVJs54RkgPJVXoIAYkQqkLpTj5/7HafLk393Q9QjL3kIYfkV2nEHm1CVWkOj -lxH2nxqE1/K62ayD+S1SzA0TxaU3eA3ON6LvPmvNCUMU3h/uWnA0EorTACNR2ILS -+eIgarB9UuEa8+I/Sz9KCNd6jK+fW7pKoMGcl32Su91IEHP5tBIt+jQVigZfeNcC -AwEAAaN8MHowHQYDVR0OBBYEFG4bXAzNFBtFsH6AahtMOF3RtcL2MB8GA1UdIwQY -MBaAFG4bXAzNFBtFsH6AahtMOF3RtcL2MA8GA1UdEwEB/wQFMAMBAf8wJwYDVR0R -BCAwHoIJbG9jYWxob3N0ggsqLmxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0BAQsF -AAOCAgEAOOTBo2AJCNdSiwtDYIq7QIF55RcFM8s/x1/k+Lgv/zrXH6Zoy5xlYDKN -xfSjWhSvccGm4MaEdryvsaUy0MRhFUlATigpbYYOyZOadnUWBoEfIzigEgHBCZ/5 -MYcOgzh2nk2eZPrkZKZIr6rjcEuQxIoqM9p4xlHPqN9bssDJktA7hfwE76EDs4NR -SyLqfqfxhxdSrLclox6T3gCIUYhBiun2Q1l3E92p9mSwThhYDhDp83lTIIjJjR7u -pnpTYyNKSWIp4TVDLJzmc2abaV0anFtWbfcUB+DMxWgdrcD4CM+7COqiLCfNoq+Q -5o3e3fGGkowr/f+TtHEwbfrg7JVCw9GV4vi/JLHcSWzFU5yZsPARoUkEF0oDCL+P -CX4aNcY5ZDKsQnye3iHcD83vq5NrQI3lZZ2Vk55d46PBa/m0q+VJ/39BU5Tpdtnv -OKGPSzgp21GpA6lyxLwC2guG3kMx1zWH9ZZLeyIniXOy9jln6b+lLU4z0/ruwc2N -erEpgtu6TSY7JwpdhvdlwJccgYGU02bddDVfpVE4f7yEvujV7D/Nk1X5DKs0fX6X -rF8TGlb93EjCKug3cBS2n9hWAlYEsfotxmHw+BGOBvalwpIrjB1vyuslzF+eQdHq -IC+ho1OIuD6XCDrenesRgzavv1RzlyJBBUcJW119Femsd3jbnwY= ------END CERTIFICATE----- diff --git a/assets/localhost.key b/assets/localhost.key deleted file mode 100644 index 2b51b4d4c..000000000 --- a/assets/localhost.key +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCaj22YrB7DMsWu -uKBGlQl6LiFLqGnhJYfH3phHQ5BjnTzhWSD7Q+n8R68Jzh/0s/xixit6Mz5a2oc2 -ejC69wMrjDDSo+UZuqy+qQlhbrCpIRkTGkN6pQCaBrVcSQ+6RA1Y5hPKh2SWiAtp -T2JboyFzau6g8p5Ks2x5TcpdkXQtWZxYp6tki+X3gIzoLzMgQ5SgbsIR6GGFFFZR -GbS+d9t6gD9AXefS9dPcgddcDee5E+5B85miLEruowypwH2G35IQazQQQX+Q+W49 -ey3ocUo9uMkccIouLCyyNUWLPt5nMolTYAMZii5QoE4DMV7fv9UfwxpzNsGitDxd -AgKt8mdRxhAhlzYKu9U1OwexWsryqng5pejMyAYo6ZpbRRl4c4XYCW1mljt9juO2 -lx5zGJ5fXyc030se4YZB78TemQ1Y255ogARcPS6B1ev7inNE2BzinxlGkpcb9Ok7 -wb+sjesaBQ9212Jg8LQuJ70QV5tovCN64xuBUmznhGSA8lVeggBiRCqQulOPn/sd -p8uTf3dD1CMveQhh+RXacQebUJVaQ6OXEfafGoTX8rrZrIP5LVLMDRPFpTd4Dc43 -ou8+a80JQxTeH+5acDQSitMAI1HYgtL54iBqsH1S4Rrz4j9LP0oI13qMr59bukqg -wZyXfZK73UgQc/m0Ei36NBWKBl941wIDAQABAoICABka1O1of7KrC4r+uCHe0WRE -G+pjF5eXdf+T+14P7mMPxjTAOmg1tsrgheDs1ynzFjqg/6zgp+8v8ah6nnGv75bi -NYfxUSQluytY34ow5YcDNkRxDqbcKEXccxbjUyepKBXZgTtzVHZS8K+RUmOaErPh -mZMQ9X8it6rYZNdf6eP32zpXObKiOp9CBSEtkXtbHsgUVXd9LGHmVMLljwMlCsRS -EnQNDLuqbFgzytxL8eiRATE7NSgvU5iBaWwlNP50UBBUqWc+jE7rBOn9mQ5ZYHq4 -CgqRiRBI4pWrq3kbpBpVDhM51CcQ18cG0sUX/tYPHboEAcbXDQq1hdyBmBfS+M8B -2xwD/pAfSrVjjNtrjYyfbizYzwlFwWAPILZQSj8yIVIHqDHse1uzdwD/sZVhLhVH -hj3Ug6DsVLIIBt7Wq9i0tz09QVy6DfT3LtksWeCp2CLHTpb7iQSMBYoRg2dTV/AB -ZHAzQDk76YX44T2azmVacBofyGqywPHE9Wz4NXqekKGMRRWx9XCEalesivE76kmP -++yK0C3DE/YZUlbGz/STHT52JMIpdtjWcUGGDFD0xD5X87p5fCba2TUA/MyqXvcK -EUaJMmgn30zYvOP2Xj4y4vQi1YVAl7Xbn1fIjHOVpBPkuDFfnwo3AuQeP/GoIpj0 -lKJ9HLh4/8GCH92imuBhAoIBAQDNPng2G62xmmNvSC+eeiXWoEzYciyXGEDI8rOG -4EvZ3ldirJSNbCC05OqHSgvYnFJgixb4EzZN3LGSuLrvcyQqVsz2nxEt+oWs8ten -Od0l0N1dMX3lh7Wi+Rb/JgUS4b41bqNT7tT9tCBFAU9Imuv9XEO/vaMt+32E2XCG -BSqKN4sMvT6ySgVqpykAjbaftpmTUBHraJCEDYhDmd4nkolfwuEBpW34ND2ZwCPy -X/WEU/+fKqPQ2JeJI5JAfm3YPvq4B0L/ELbgK2XRJQLAqLNVNbUdxsyzJDjFbGbd -Q9fygouUHO+wUxqxAE/SqetIQbWxiSElme7IrCFocsO9vE0nAoIBAQDAyEkiIRH8 -q5edkYCF90k7ki061yG/TpZr6MTXlNY1KnHWfBlSrmlQJ4ZT02phzz+geFj/QZb0 -u5FbLIlN0MN7JabT6JmwaPz5bt/x3mdj+YScCBk8lWwEyYsDvLioVphbrZT4S2XB -oj4R41nW/ft8lbWW+wPxa4JvJjR7Kilu9vCK1le0tGtnvNrHjGllF0cLkZ4mE5+l -VEl9MCvmyXM+BGjuBKlAsDbBG81JpN4wpmqYCuLvy5f8pRH8BXgSkNYsASiADXCG -NgN39eEGjHciL/13nlKYAdFgXEtFw6E7IrGZK35EbaluXXS29fIRekQFLmxwOtdz -ZRJ91C05vSTRAoIBAFP4k/wnNNgt/zKfSQRAm0yFRwtjIwUqYg8U5QhwqffYNM5l -J134+CSqZ520WMZlpnpjTaFvUs9mVKxfsfOXmxtLag4YpFG4ZoqMzFhZnzYCjx66 -yfRnopOr75GyP28rNsProR0M4M1vragt0f81iwmcfwdqkeGVPBRnVdcvM+lasiQj -JQySpkatX2QflrEfZxPTNZGntUChvLdTs4VjOZsZQy+GPEjJLs7BwrM+OVfLehDn -xCAFDXKJQCPs1gocMj2qkumCMB/lAYIg71BddQmOsKwfEs7UKfnz0N4EDMzmRi7x -68qrJYd3RjE9XcqxP6IEJbCZmw01B3IRSi5NZQ8CggEATe/q2RhjjDHW7sXHHuHV -QncbQAF/TDc6Ss/k3H74hq/tK9gp6KpIOzZvcO40wOwnffmJiVB79d7qqeB8dfAj -R2L2ag9MKuyW8URo1wCh7eIPQYFoqnyCGgFc6Rrf0HaJy+6GHkdlEP5Fd7fhNzCg -/kIMEsjSVESxi7v3VZ+69nhw0MBM3updzaelDy1t4oehmkS5mg0u6okD2M+jv/7L -T1Q7E5bg0h0rVbCmstIrXaG50FP+YRF/FY2qkqenXmIdo9aoB/Tm++tURagq3Bnn -g/PA1h40p+18Nye46rBnO2AQSqsxtfpbmBnCOMF/pp82Zp3ZCxpOxgEjk6k2y3Pz -MQKCAQAHj1FpBvQmHlzwYqvj3lRHbPohZljHfQJG8dE0PzQLA74hiPilDtds9wOe -epyshPdT1r9nXJ9u8QccZZaf05atOI89YluilhtgLqsZGLCiCSsjOsKVTf+vgx08 -lHVppe1aig7tl22nzIdw8a4IJMtIID2LVKl2QZTCnLL7dtOyyNOw0gV4lwHOrDS+ -2z9IUjHd02yxsM4dfyQOqZxNhLDaxFPx8gyB+De6AWmSl7OKeHv/Y/teQYWj1vWe -sQpDyvxDEUEELEsq/iyvhazRohdKQb7FkcksvQCd9yixdWcvZj4jiRUjxwGbVHdI -NufTrvdPtl0kAdTS4rNV85WVJG3N ------END PRIVATE KEY----- diff --git a/pubspec.yaml b/pubspec.yaml index 91764edf3..3fce59630 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -111,8 +111,6 @@ flutter: uses-material-design: true generate: true assets: - - "assets/localhost.crt" - - "assets/localhost.key" - "assets/logo.png" - "assets/logo_monochrome.png" - "assets/logo_android.png" From 2d347f6ff39bc6fa6809e41e6cf8359ab3747d0e Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Fri, 3 Jan 2025 19:12:00 -0500 Subject: [PATCH 33/50] cleanup --- lib/account/pages/login_page.dart | 6 +++--- lib/utils/instance.dart | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index 5e2a18c72..b0d2f067b 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -487,14 +487,14 @@ class _LoginPageState extends State with SingleTickerProviderStateMix ); } - // TODO: Set showContentWarning default value to true. Need to relogin after. - void _handleOAuthLogin({required ProviderView provider, bool showContentWarning = false}) { + void _handleOAuthLogin({required ProviderView provider, bool showContentWarning = true}) { TextInput.finishAutofillContext(); - // Perform login authentication + // Perform oauth login authentication. context.read().add( OAuthLoginAttemptPart1( instance: _instanceTextEditingController.text.trim(), provider: provider, + showContentWarning: showContentWarning, ), ); } diff --git a/lib/utils/instance.dart b/lib/utils/instance.dart index bcfef18b2..430bf1719 100644 --- a/lib/utils/instance.dart +++ b/lib/utils/instance.dart @@ -201,8 +201,7 @@ Future getInstanceInfo(String? url, {int? id, Duration? } try { - // TODO: need to remove tls and debug args, this is just for testing. - final site = await LemmyApiV3(url!, tls: true, debug: true).run(const GetSite()).timeout(timeout ?? const Duration(seconds: 5)); + final site = await LemmyApiV3(url!).run(const GetSite()).timeout(timeout ?? const Duration(seconds: 5)); return GetInstanceInfoResponse( success: true, icon: site.siteView.site.icon, From 88b695b5925198f6b6bdd03ec3fc3e74843357f3 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Fri, 3 Jan 2025 19:37:36 -0500 Subject: [PATCH 34/50] more clean up --- lib/core/auth/bloc/auth_bloc.dart | 16 +++++++++------- lib/core/auth/bloc/auth_event.dart | 14 ++------------ lib/core/singletons/lemmy_client.dart | 2 +- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index dd9ea4cf0..f684fb552 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -265,22 +265,24 @@ class AuthBloc extends Bloc { // Durring this step lemmy connects to the Provider to get the user info. LoginResponse loginResponse = await lemmy.run(AuthenticateWithOAuth(code: code, oauth_provider_id: state.oauthProviderId ?? -1, redirect_uri: redirectUri)); - // TODO: Need to add a step to set the account username. + // TODO: Need to add a step to set the account username on the first login. - final accessToken = loginResponse.jwt as String; + if (loginResponse.jwt == null) { + throw Exception("OAuth login failed: no jwt received from lemmy instance."); + } - GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: accessToken)); + GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: loginResponse.jwt)); // TODO: Login fails when this is uncommented. Have to get this working. - //if (event.showContentWarning && getSiteResponse.siteView.site.contentWarning?.isNotEmpty == true) { - // return emit(state.copyWith(status: AuthStatus.contentWarning, contentWarning: getSiteResponse.siteView.site.contentWarning)); - //} + // if (event.showContentWarning && getSiteResponse.siteView.site.contentWarning?.isNotEmpty == true) { + // return emit(state.copyWith(status: AuthStatus.contentWarning, contentWarning: getSiteResponse.siteView.site.contentWarning)); + // } // Create a new account in the database Account? account = Account( id: '', username: getSiteResponse.myUser?.localUserView.person.name, - jwt: accessToken, + jwt: loginResponse.jwt, instance: state.oauthInstance ?? "", userId: getSiteResponse.myUser?.localUserView.person.id, index: -1, diff --git a/lib/core/auth/bloc/auth_event.dart b/lib/core/auth/bloc/auth_event.dart index 4cfe3c81b..5d404b05d 100644 --- a/lib/core/auth/bloc/auth_event.dart +++ b/lib/core/auth/bloc/auth_event.dart @@ -29,7 +29,7 @@ class LoginAttempt extends AuthEvent { const LoginAttempt({required this.username, required this.password, required this.instance, this.totp = "", this.showContentWarning = true}); } -/// The [OAuthLoginAttempt] event should be triggered whenever the user attempts to log in with OAuth. +/// The [OAuthLoginAttemptPart1] event should be triggered whenever the user attempts to log in with OAuth. /// This event is responsible for login authentication and handling related errors. class OAuthLoginAttemptPart1 extends AuthEvent { final String instance; @@ -39,17 +39,7 @@ class OAuthLoginAttemptPart1 extends AuthEvent { const OAuthLoginAttemptPart1({required this.instance, required this.provider, this.showContentWarning = true}); } -/// The [OAuthLoginAttemptDesktop] event should be triggered whenever the user attempts to log in with OAuth. -/// This event is responsible for login authentication and handling related errors. -class OAuthLoginAttemptDesktop extends AuthEvent { - final String instance; - final ProviderView provider; - final bool showContentWarning; - - const OAuthLoginAttemptDesktop({required this.instance, required this.provider, this.showContentWarning = true}); -} - -/// The [OAuthLoginAttempt] event should be triggered whenever the user attempts to log in with OAuth. +/// The [OAuthLoginAttemptPart2] event should be triggered whenever the user attempts to log in with OAuth. /// This event is responsible for login authentication and handling related errors. class OAuthLoginAttemptPart2 extends AuthEvent { final String link; diff --git a/lib/core/singletons/lemmy_client.dart b/lib/core/singletons/lemmy_client.dart index 4264e9a21..971895704 100644 --- a/lib/core/singletons/lemmy_client.dart +++ b/lib/core/singletons/lemmy_client.dart @@ -11,7 +11,7 @@ class LemmyClient { LemmyClient._initialize(); void changeBaseUrl(String baseUrl) { - lemmyApiV3 = LemmyApiV3(baseUrl, tls: true, debug: true); + lemmyApiV3 = LemmyApiV3(baseUrl); _populateSiteInfo(); // Do NOT await this. Let it populate in the background. } From 9431a6cd7da15468f066d87ae0973f3230063cd0 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Fri, 3 Jan 2025 21:33:22 -0500 Subject: [PATCH 35/50] trying to get contentWarning to work --- lib/account/pages/login_page.dart | 21 +++++++++++++++--- lib/core/auth/bloc/auth_bloc.dart | 34 +++++++++++++----------------- lib/core/auth/bloc/auth_event.dart | 8 ++++++- lib/core/auth/bloc/auth_state.dart | 14 +++++++----- 4 files changed, 49 insertions(+), 28 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index b0d2f067b..3d4017a08 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -173,7 +173,11 @@ class _LoginPageState extends State with SingleTickerProviderStateMix if (context.mounted) { if (acceptedContentWarning) { // Do another login attempt, this time without the content warning - _handleLogin(showContentWarning: false); + if (state.oauthLink == null) { + _handleLogin(showContentWarning: false); + } else { + _handleOAuthLoginPart2(link: state.oauthLink!, showContentWarning: false); + } } else { // Cancel the login context.read().add(const CancelLoginAttempt()); @@ -449,7 +453,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix ), onPressed: (!isLoading && _instanceTextEditingController.text.isNotEmpty) ? () { - _handleOAuthLogin(provider: provider); + _handleOAuthLoginPart1(provider: provider); } : (_instanceTextEditingController.text.isNotEmpty && widget.anonymous) ? () => _addAnonymousInstance(context) @@ -487,7 +491,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix ); } - void _handleOAuthLogin({required ProviderView provider, bool showContentWarning = true}) { + void _handleOAuthLoginPart1({required ProviderView provider, bool showContentWarning = true}) { TextInput.finishAutofillContext(); // Perform oauth login authentication. context.read().add( @@ -499,6 +503,17 @@ class _LoginPageState extends State with SingleTickerProviderStateMix ); } + void _handleOAuthLoginPart2({required String link, bool showContentWarning = true}) { + TextInput.finishAutofillContext(); + // Perform oauth login authentication. + context.read().add( + OAuthLoginAttemptPart2( + link: link, + showContentWarning: showContentWarning, + ), + ); + } + void _addAnonymousInstance(BuildContext context) async { final AppLocalizations l10n = AppLocalizations.of(context)!; diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index f684fb552..28e425574 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -1,11 +1,7 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; import 'package:lemmy_api_client/v3.dart'; import 'package:collection/collection.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; @@ -18,7 +14,6 @@ import 'package:thunder/account/models/account.dart'; import 'package:thunder/core/singletons/preferences.dart'; import 'package:thunder/core/singletons/lemmy_client.dart'; import 'package:thunder/utils/global_context.dart'; -import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; import 'package:uuid/uuid.dart'; @@ -223,17 +218,17 @@ class AuthBloc extends Bloc { throw Exception('Could not launch $url'); } - return emit(state.copyWith(oauthState: oauthState, oauthInstance: instance, oauthProviderId: provider.id)); + return emit(state.copyWith(oauthState: oauthState, oauthInstance: instance, oauthProvider: provider)); } on LemmyApiException catch (e) { - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProviderId: null)); + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProvider: null)); } catch (e) { try { // Restore the original baseUrl lemmyClient.changeBaseUrl(originalBaseUrl); } catch (e, s) { - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: s.toString(), oauthState: null, oauthInstance: null, oauthProviderId: null)); + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: s.toString(), oauthState: null, oauthInstance: null, oauthProvider: null)); } - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProviderId: null)); + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProvider: null)); } }); @@ -245,14 +240,14 @@ class AuthBloc extends Bloc { String providerResponse = event.link; try { - if (state.oauthState == null || state.oauthInstance == null || state.oauthProviderId == null) { + if (state.oauthState == null || state.oauthInstance == null || state.oauthProvider == null) { throw Exception("OAuth login failed: oauthState, oauthInstance, or oauthProviderId is null."); } // oauthProviderState must match oauthClientState to ensure the response came from the Provider. String oauthProviderState = Uri.parse(providerResponse).queryParameters['state'] ?? "failed"; if (oauthProviderState == "failed" || state.oauthState != oauthProviderState) { - throw Exception("OAuth state-check failed: oauthProviderState ${state.oauthState} must match oauthClientState ${state.oauthState} to ensure the response came from the Provider."); + throw Exception("OAuth state-check failed: oauthProviderState $oauthProviderState must match oauthClientState ${state.oauthState} to ensure the response came from the Provider."); } // Extract the code from the response. @@ -263,7 +258,8 @@ class AuthBloc extends Bloc { // Authenthicate to lemmy instance and get a jwt. // Durring this step lemmy connects to the Provider to get the user info. - LoginResponse loginResponse = await lemmy.run(AuthenticateWithOAuth(code: code, oauth_provider_id: state.oauthProviderId ?? -1, redirect_uri: redirectUri)); + if (event.showContentWarning) {} + LoginResponse loginResponse = await lemmy.run(AuthenticateWithOAuth(code: code, oauth_provider_id: state.oauthProvider!.id, redirect_uri: redirectUri)); // TODO: Need to add a step to set the account username on the first login. @@ -274,9 +270,9 @@ class AuthBloc extends Bloc { GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: loginResponse.jwt)); // TODO: Login fails when this is uncommented. Have to get this working. - // if (event.showContentWarning && getSiteResponse.siteView.site.contentWarning?.isNotEmpty == true) { - // return emit(state.copyWith(status: AuthStatus.contentWarning, contentWarning: getSiteResponse.siteView.site.contentWarning)); - // } + if (event.showContentWarning && getSiteResponse.siteView.site.contentWarning?.isNotEmpty == true) { + return emit(state.copyWith(status: AuthStatus.contentWarning, contentWarning: getSiteResponse.siteView.site.contentWarning, oauthState: state.oauthState, oauthLink: providerResponse)); + } // Create a new account in the database Account? account = Account( @@ -308,17 +304,17 @@ class AuthBloc extends Bloc { getSiteResponse: getSiteResponse, oauthState: null, oauthInstance: null, - oauthProviderId: null)); + oauthProvider: null)); } on LemmyApiException catch (e) { - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProviderId: null)); + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProvider: null)); } catch (e) { try { // Restore the original baseUrl lemmyClient.changeBaseUrl(originalBaseUrl); } catch (e, s) { - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: s.toString(), oauthState: null, oauthInstance: null, oauthProviderId: null)); + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: s.toString(), oauthState: null, oauthInstance: null, oauthProvider: null)); } - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProviderId: null)); + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProvider: null)); } }); diff --git a/lib/core/auth/bloc/auth_event.dart b/lib/core/auth/bloc/auth_event.dart index 5d404b05d..edbb06014 100644 --- a/lib/core/auth/bloc/auth_event.dart +++ b/lib/core/auth/bloc/auth_event.dart @@ -43,8 +43,9 @@ class OAuthLoginAttemptPart1 extends AuthEvent { /// This event is responsible for login authentication and handling related errors. class OAuthLoginAttemptPart2 extends AuthEvent { final String link; + final bool showContentWarning; - const OAuthLoginAttemptPart2({required this.link}); + const OAuthLoginAttemptPart2({required this.link, this.showContentWarning = true}); } /// Cancels a login attempt by emitting the `failure` state. @@ -52,6 +53,11 @@ class CancelLoginAttempt extends AuthEvent { const CancelLoginAttempt(); } +/// Cancels a login attempt by emitting the `failure` state. +class ShowContentWarning extends AuthEvent { + const ShowContentWarning(); +} + /// TODO: Consolidate logic to have adding accounts (for both authenticated and anonymous accounts) placed here class AddAccount extends AuthEvent {} diff --git a/lib/core/auth/bloc/auth_state.dart b/lib/core/auth/bloc/auth_state.dart index d0d618b07..3712bb8f0 100644 --- a/lib/core/auth/bloc/auth_state.dart +++ b/lib/core/auth/bloc/auth_state.dart @@ -14,7 +14,8 @@ class AuthState extends Equatable { this.contentWarning, this.oauthInstance, this.oauthState, - this.oauthProviderId, + this.oauthLink, + this.oauthProvider, }); final AuthStatus status; @@ -27,7 +28,8 @@ class AuthState extends Equatable { final String? contentWarning; final String? oauthInstance; final String? oauthState; - final int? oauthProviderId; + final String? oauthLink; + final ProviderView? oauthProvider; AuthState copyWith({ AuthStatus? status, @@ -40,7 +42,8 @@ class AuthState extends Equatable { String? contentWarning, String? oauthInstance, String? oauthState, - int? oauthProviderId, + String? oauthLink, + ProviderView? oauthProvider, }) { return AuthState( status: status ?? this.status, @@ -53,9 +56,10 @@ class AuthState extends Equatable { contentWarning: contentWarning, oauthInstance: oauthInstance ?? this.oauthInstance, oauthState: oauthState ?? this.oauthInstance, - oauthProviderId: oauthProviderId ?? this.oauthProviderId); + oauthLink: oauthLink ?? this.oauthLink, + oauthProvider: oauthProvider ?? this.oauthProvider); } @override - List get props => [status, isLoggedIn, errorMessage, account, downvotesEnabled, getSiteResponse, reload, contentWarning, oauthInstance, oauthState, oauthProviderId]; + List get props => [status, isLoggedIn, errorMessage, account, downvotesEnabled, getSiteResponse, reload, contentWarning, oauthInstance, oauthState, oauthLink, oauthProvider]; } From 64e4825c26935190f46ac47bfb323bb9b2854a60 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Sat, 4 Jan 2025 18:02:07 -0500 Subject: [PATCH 36/50] Added AddAccount to AuthBloc, got contentWarning working for oauth. --- lib/account/pages/login_page.dart | 41 +++++++++++++--------- lib/core/auth/bloc/auth_bloc.dart | 54 ++++++++++++++++++----------- lib/core/auth/bloc/auth_event.dart | 9 +++-- lib/core/auth/bloc/auth_state.dart | 33 ++++++++++-------- lib/thunder/pages/thunder_page.dart | 2 ++ 5 files changed, 83 insertions(+), 56 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index 3d4017a08..b40ba27f9 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -173,11 +173,31 @@ class _LoginPageState extends State with SingleTickerProviderStateMix if (context.mounted) { if (acceptedContentWarning) { // Do another login attempt, this time without the content warning - if (state.oauthLink == null) { - _handleLogin(showContentWarning: false); - } else { - _handleOAuthLoginPart2(link: state.oauthLink!, showContentWarning: false); - } + _handleLogin(showContentWarning: false); + } else { + // Cancel the login + context.read().add(const CancelLoginAttempt()); + } + } + } else if (state.status == AuthStatus.oauthContentWarning) { + bool acceptedContentWarning = false; + + await showThunderDialog( + context: context, + title: l10n.contentWarning, + contentText: state.contentWarning, + onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), + secondaryButtonText: l10n.decline, + onPrimaryButtonPressed: (dialogContext, _) async { + Navigator.of(dialogContext).pop(); + acceptedContentWarning = true; + }, + primaryButtonText: l10n.accept, + ); + + if (context.mounted) { + if (acceptedContentWarning) { + context.read().add(const AddAccount()); } else { // Cancel the login context.read().add(const CancelLoginAttempt()); @@ -503,17 +523,6 @@ class _LoginPageState extends State with SingleTickerProviderStateMix ); } - void _handleOAuthLoginPart2({required String link, bool showContentWarning = true}) { - TextInput.finishAutofillContext(); - // Perform oauth login authentication. - context.read().add( - OAuthLoginAttemptPart2( - link: link, - showContentWarning: showContentWarning, - ), - ); - } - void _addAnonymousInstance(BuildContext context) async { final AppLocalizations l10n = AppLocalizations.of(context)!; diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index 28e425574..9976b7988 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -258,7 +258,6 @@ class AuthBloc extends Bloc { // Authenthicate to lemmy instance and get a jwt. // Durring this step lemmy connects to the Provider to get the user info. - if (event.showContentWarning) {} LoginResponse loginResponse = await lemmy.run(AuthenticateWithOAuth(code: code, oauth_provider_id: state.oauthProvider!.id, redirect_uri: redirectUri)); // TODO: Need to add a step to set the account username on the first login. @@ -269,11 +268,6 @@ class AuthBloc extends Bloc { GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: loginResponse.jwt)); - // TODO: Login fails when this is uncommented. Have to get this working. - if (event.showContentWarning && getSiteResponse.siteView.site.contentWarning?.isNotEmpty == true) { - return emit(state.copyWith(status: AuthStatus.contentWarning, contentWarning: getSiteResponse.siteView.site.contentWarning, oauthState: state.oauthState, oauthLink: providerResponse)); - } - // Create a new account in the database Account? account = Account( id: '', @@ -284,7 +278,35 @@ class AuthBloc extends Bloc { index: -1, ); - account = await Account.insertAccount(account); + // Save account to AuthBlock state and show the content warning. + if (getSiteResponse.siteView.site.contentWarning?.isNotEmpty == true) { + return emit(state.copyWith( + status: AuthStatus.oauthContentWarning, contentWarning: getSiteResponse.siteView.site.contentWarning, oauthState: state.oauthState, oauthLink: providerResponse, tempAccount: account)); + } + } on LemmyApiException catch (e) { + return emit( + state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProvider: null, tempAccount: null)); + } catch (e) { + try { + // Restore the original baseUrl + lemmyClient.changeBaseUrl(originalBaseUrl); + } catch (e, s) { + return emit( + state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: s.toString(), oauthState: null, oauthInstance: null, oauthProvider: null, tempAccount: null)); + } + return emit( + state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProvider: null, tempAccount: null)); + } + }); + + on((event, emit) async { + try { + if (state.tempAccount == null) { + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false)); + } + + Account? account = await Account.insertAccount(state.tempAccount!); + emit(state.copyWith(tempAccount: null)); if (account == null) { return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false)); @@ -294,32 +316,22 @@ class AuthBloc extends Bloc { SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences; prefs.setString('active_profile_id', account.id); - bool downvotesEnabled = getSiteResponse.siteView.localSite.enableDownvotes ?? false; - return emit(state.copyWith( status: AuthStatus.success, account: account, isLoggedIn: true, - downvotesEnabled: downvotesEnabled, - getSiteResponse: getSiteResponse, + //downvotesEnabled: downvotesEnabled, + //getSiteResponse: getSiteResponse, oauthState: null, oauthInstance: null, oauthProvider: null)); - } on LemmyApiException catch (e) { - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProvider: null)); } catch (e) { - try { - // Restore the original baseUrl - lemmyClient.changeBaseUrl(originalBaseUrl); - } catch (e, s) { - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: s.toString(), oauthState: null, oauthInstance: null, oauthProvider: null)); - } - return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProvider: null)); + return emit(state.copyWith(status: AuthStatus.failure, tempAccount: null, account: null, isLoggedIn: false)); } }); on((event, emit) async { - return emit(state.copyWith(status: AuthStatus.failure, errorMessage: AppLocalizations.of(GlobalContext.context)!.loginAttemptCanceled)); + return emit(state.copyWith(status: AuthStatus.failure, errorMessage: AppLocalizations.of(GlobalContext.context)!.loginAttemptCanceled, tempAccount: null)); }); /// When we log out of all accounts, clear the instance information diff --git a/lib/core/auth/bloc/auth_event.dart b/lib/core/auth/bloc/auth_event.dart index edbb06014..d72db555d 100644 --- a/lib/core/auth/bloc/auth_event.dart +++ b/lib/core/auth/bloc/auth_event.dart @@ -43,9 +43,11 @@ class OAuthLoginAttemptPart1 extends AuthEvent { /// This event is responsible for login authentication and handling related errors. class OAuthLoginAttemptPart2 extends AuthEvent { final String link; - final bool showContentWarning; + const OAuthLoginAttemptPart2({required this.link}); +} - const OAuthLoginAttemptPart2({required this.link, this.showContentWarning = true}); +class AddAccount extends AuthEvent { + const AddAccount(); } /// Cancels a login attempt by emitting the `failure` state. @@ -58,9 +60,6 @@ class ShowContentWarning extends AuthEvent { const ShowContentWarning(); } -/// TODO: Consolidate logic to have adding accounts (for both authenticated and anonymous accounts) placed here -class AddAccount extends AuthEvent {} - /// The [RemoveAccount] event should be triggered whenever the user removes a given account. /// Currently, this event only handles removing authenticated accounts. /// diff --git a/lib/core/auth/bloc/auth_state.dart b/lib/core/auth/bloc/auth_state.dart index 3712bb8f0..e12bd411e 100644 --- a/lib/core/auth/bloc/auth_state.dart +++ b/lib/core/auth/bloc/auth_state.dart @@ -1,12 +1,13 @@ part of 'auth_bloc.dart'; -enum AuthStatus { initial, loading, success, failure, failureCheckingInstance, contentWarning } +enum AuthStatus { initial, loading, success, failure, failureCheckingInstance, contentWarning, oauthContentWarning, oauthCreateUsername } class AuthState extends Equatable { const AuthState({ this.status = AuthStatus.initial, this.isLoggedIn = false, this.errorMessage, + this.tempAccount, this.account, this.downvotesEnabled = true, this.getSiteResponse, @@ -22,6 +23,7 @@ class AuthState extends Equatable { final bool isLoggedIn; final String? errorMessage; final Account? account; + final Account? tempAccount; final bool downvotesEnabled; final GetSiteResponse? getSiteResponse; final bool reload; @@ -35,6 +37,7 @@ class AuthState extends Equatable { AuthStatus? status, bool? isLoggedIn, String? errorMessage, + Account? tempAccount, Account? account, bool? downvotesEnabled, GetSiteResponse? getSiteResponse, @@ -46,20 +49,22 @@ class AuthState extends Equatable { ProviderView? oauthProvider, }) { return AuthState( - status: status ?? this.status, - isLoggedIn: isLoggedIn ?? false, - errorMessage: errorMessage, - account: account, - downvotesEnabled: downvotesEnabled ?? this.downvotesEnabled, - getSiteResponse: getSiteResponse ?? this.getSiteResponse, - reload: reload ?? this.reload, - contentWarning: contentWarning, - oauthInstance: oauthInstance ?? this.oauthInstance, - oauthState: oauthState ?? this.oauthInstance, - oauthLink: oauthLink ?? this.oauthLink, - oauthProvider: oauthProvider ?? this.oauthProvider); + status: status ?? this.status, + isLoggedIn: isLoggedIn ?? false, + errorMessage: errorMessage, + tempAccount: tempAccount ?? this.tempAccount, + account: account, + downvotesEnabled: downvotesEnabled ?? this.downvotesEnabled, + getSiteResponse: getSiteResponse ?? this.getSiteResponse, + reload: reload ?? this.reload, + contentWarning: contentWarning, + oauthInstance: oauthInstance ?? this.oauthInstance, + oauthState: oauthState ?? this.oauthInstance, + oauthLink: oauthLink ?? this.oauthLink, + oauthProvider: oauthProvider ?? this.oauthProvider, + ); } @override - List get props => [status, isLoggedIn, errorMessage, account, downvotesEnabled, getSiteResponse, reload, contentWarning, oauthInstance, oauthState, oauthLink, oauthProvider]; + List get props => [status, isLoggedIn, errorMessage, tempAccount, account, downvotesEnabled, getSiteResponse, reload, contentWarning, oauthInstance, oauthState, oauthLink, oauthProvider]; } diff --git a/lib/thunder/pages/thunder_page.dart b/lib/thunder/pages/thunder_page.dart index c4932c4a4..2100e374e 100644 --- a/lib/thunder/pages/thunder_page.dart +++ b/lib/thunder/pages/thunder_page.dart @@ -547,6 +547,8 @@ class _ThunderState extends State { ), ); case AuthStatus.contentWarning: + case AuthStatus.oauthContentWarning: + case AuthStatus.oauthCreateUsername: case AuthStatus.success: Version? version = thunderBlocState.version; bool showInAppUpdateNotification = thunderBlocState.showInAppUpdateNotification; From 8a9d3abbdef6d68557e5599c6e148dc53da19ab0 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Sat, 4 Jan 2025 21:13:05 -0500 Subject: [PATCH 37/50] add a place holder for picking your username with oauth --- lib/account/pages/login_page.dart | 26 +++++++++++++- lib/core/auth/bloc/auth_bloc.dart | 56 ++++++++++++++++++----------- lib/core/auth/bloc/auth_event.dart | 16 +++++---- lib/core/auth/bloc/auth_state.dart | 21 ++++++++++- lib/thunder/pages/thunder_page.dart | 2 +- 5 files changed, 91 insertions(+), 30 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index b40ba27f9..5e7ad5380 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -203,6 +203,30 @@ class _LoginPageState extends State with SingleTickerProviderStateMix context.read().add(const CancelLoginAttempt()); } } + } else if (state.status == AuthStatus.oauthCreateUsername) { + bool completedUsername = false; + + await showThunderDialog( + context: context, + title: "Pick your username", + contentText: "Pick your username", + onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), + secondaryButtonText: l10n.cancel, + onPrimaryButtonPressed: (dialogContext, _) async { + Navigator.of(dialogContext).pop(); + completedUsername = true; + }, + primaryButtonText: l10n.accept, + ); + + if (context.mounted) { + if (completedUsername) { + context.read().add(const OAuthCreateAccount()); + } else { + // Cancel the login + context.read().add(const CancelLoginAttempt()); + } + } } }, ), @@ -515,7 +539,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix TextInput.finishAutofillContext(); // Perform oauth login authentication. context.read().add( - OAuthLoginAttemptPart1( + OAuthLoginAttempt( instance: _instanceTextEditingController.text.trim(), provider: provider, showContentWarning: showContentWarning, diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index 9976b7988..153b2ab8b 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -188,8 +188,8 @@ class AuthBloc extends Bloc { } }); - /// This event should be triggered when the user logs in with oauth. - on((event, emit) async { + /// Log in with OAuth Provider to get a code. + on((event, emit) async { LemmyClient lemmyClient = LemmyClient.instance; String originalBaseUrl = lemmyClient.lemmyApiV3.host; String instance = event.instance; @@ -232,7 +232,7 @@ class AuthBloc extends Bloc { } }); - on((event, emit) async { + on((event, emit) async { LemmyClient lemmyClient = LemmyClient.instance; LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; String originalBaseUrl = lemmyClient.lemmyApiV3.host; @@ -260,19 +260,44 @@ class AuthBloc extends Bloc { // Durring this step lemmy connects to the Provider to get the user info. LoginResponse loginResponse = await lemmy.run(AuthenticateWithOAuth(code: code, oauth_provider_id: state.oauthProvider!.id, redirect_uri: redirectUri)); - // TODO: Need to add a step to set the account username on the first login. - if (loginResponse.jwt == null) { throw Exception("OAuth login failed: no jwt received from lemmy instance."); } - GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: loginResponse.jwt)); + return emit(state.copyWith(status: AuthStatus.oauthCreateUsername, oauthJwt: loginResponse.jwt)); + } on LemmyApiException catch (e) { + return emit( + state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProvider: null, tempAccount: null)); + } catch (e) { + try { + // Restore the original baseUrl + lemmyClient.changeBaseUrl(originalBaseUrl); + } catch (e, s) { + return emit( + state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: s.toString(), oauthState: null, oauthInstance: null, oauthProvider: null, tempAccount: null)); + } + return emit( + state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProvider: null, tempAccount: null)); + } + }); + + /// Adds the tempAccount and sets it as the active account. + on((event, emit) async { + LemmyClient lemmyClient = LemmyClient.instance; + LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; + try { + if (state.oauthJwt == null) { + return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false)); + } + + // TODO: Need to add a step to set the account username on the first login. + GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: state.oauthJwt)); // Create a new account in the database Account? account = Account( id: '', username: getSiteResponse.myUser?.localUserView.person.name, - jwt: loginResponse.jwt, + jwt: state.oauthJwt, instance: state.oauthInstance ?? "", userId: getSiteResponse.myUser?.localUserView.person.id, index: -1, @@ -280,25 +305,14 @@ class AuthBloc extends Bloc { // Save account to AuthBlock state and show the content warning. if (getSiteResponse.siteView.site.contentWarning?.isNotEmpty == true) { - return emit(state.copyWith( - status: AuthStatus.oauthContentWarning, contentWarning: getSiteResponse.siteView.site.contentWarning, oauthState: state.oauthState, oauthLink: providerResponse, tempAccount: account)); + return emit(state.copyWith(status: AuthStatus.oauthContentWarning, contentWarning: getSiteResponse.siteView.site.contentWarning, oauthState: state.oauthState, tempAccount: account)); } - } on LemmyApiException catch (e) { - return emit( - state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProvider: null, tempAccount: null)); } catch (e) { - try { - // Restore the original baseUrl - lemmyClient.changeBaseUrl(originalBaseUrl); - } catch (e, s) { - return emit( - state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: s.toString(), oauthState: null, oauthInstance: null, oauthProvider: null, tempAccount: null)); - } - return emit( - state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProvider: null, tempAccount: null)); + return emit(state.copyWith(status: AuthStatus.failure, tempAccount: null, account: null, isLoggedIn: false)); } }); + /// Adds the tempAccount and sets it as the active account. on((event, emit) async { try { if (state.tempAccount == null) { diff --git a/lib/core/auth/bloc/auth_event.dart b/lib/core/auth/bloc/auth_event.dart index d72db555d..b81677f55 100644 --- a/lib/core/auth/bloc/auth_event.dart +++ b/lib/core/auth/bloc/auth_event.dart @@ -29,21 +29,25 @@ class LoginAttempt extends AuthEvent { const LoginAttempt({required this.username, required this.password, required this.instance, this.totp = "", this.showContentWarning = true}); } -/// The [OAuthLoginAttemptPart1] event should be triggered whenever the user attempts to log in with OAuth. +/// The [OAuthLoginAttempt] event should be triggered whenever the user attempts to log in with OAuth. /// This event is responsible for login authentication and handling related errors. -class OAuthLoginAttemptPart1 extends AuthEvent { +class OAuthLoginAttempt extends AuthEvent { final String instance; final ProviderView provider; final bool showContentWarning; - const OAuthLoginAttemptPart1({required this.instance, required this.provider, this.showContentWarning = true}); + const OAuthLoginAttempt({required this.instance, required this.provider, this.showContentWarning = true}); } -/// The [OAuthLoginAttemptPart2] event should be triggered whenever the user attempts to log in with OAuth. +/// The [OAuthGetJwt] event should be triggered whenever the user attempts to log in with OAuth. /// This event is responsible for login authentication and handling related errors. -class OAuthLoginAttemptPart2 extends AuthEvent { +class OAuthGetJwt extends AuthEvent { final String link; - const OAuthLoginAttemptPart2({required this.link}); + const OAuthGetJwt({required this.link}); +} + +class OAuthCreateAccount extends AuthEvent { + const OAuthCreateAccount(); } class AddAccount extends AuthEvent { diff --git a/lib/core/auth/bloc/auth_state.dart b/lib/core/auth/bloc/auth_state.dart index e12bd411e..83f4b12e6 100644 --- a/lib/core/auth/bloc/auth_state.dart +++ b/lib/core/auth/bloc/auth_state.dart @@ -16,6 +16,7 @@ class AuthState extends Equatable { this.oauthInstance, this.oauthState, this.oauthLink, + this.oauthJwt, this.oauthProvider, }); @@ -31,6 +32,7 @@ class AuthState extends Equatable { final String? oauthInstance; final String? oauthState; final String? oauthLink; + final String? oauthJwt; final ProviderView? oauthProvider; AuthState copyWith({ @@ -46,6 +48,7 @@ class AuthState extends Equatable { String? oauthInstance, String? oauthState, String? oauthLink, + String? oauthJwt, ProviderView? oauthProvider, }) { return AuthState( @@ -61,10 +64,26 @@ class AuthState extends Equatable { oauthInstance: oauthInstance ?? this.oauthInstance, oauthState: oauthState ?? this.oauthInstance, oauthLink: oauthLink ?? this.oauthLink, + oauthJwt: oauthJwt ?? oauthJwt, oauthProvider: oauthProvider ?? this.oauthProvider, ); } @override - List get props => [status, isLoggedIn, errorMessage, tempAccount, account, downvotesEnabled, getSiteResponse, reload, contentWarning, oauthInstance, oauthState, oauthLink, oauthProvider]; + List get props => [ + status, + isLoggedIn, + errorMessage, + tempAccount, + account, + downvotesEnabled, + getSiteResponse, + reload, + contentWarning, + oauthInstance, + oauthState, + oauthLink, + oauthJwt, + oauthProvider, + ]; } diff --git a/lib/thunder/pages/thunder_page.dart b/lib/thunder/pages/thunder_page.dart index 2100e374e..13abb3057 100644 --- a/lib/thunder/pages/thunder_page.dart +++ b/lib/thunder/pages/thunder_page.dart @@ -256,7 +256,7 @@ class _ThunderState extends State { Future _oauthCallback(String link) async { try { debugPrint("_oauthCallback $link"); - context.read().add(OAuthLoginAttemptPart2(link: link)); + context.read().add(OAuthGetJwt(link: link)); } catch (e) { if (context.mounted) { _showLinkProcessingError(context, AppLocalizations.of(context)!.exceptionProcessingUri, link); From 49004f39410d3b49e1e1af2fefea8ca19cd17e94 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Sat, 4 Jan 2025 22:05:38 -0500 Subject: [PATCH 38/50] Working on SignUp --- lib/account/pages/login_page.dart | 10 +++++----- lib/core/auth/bloc/auth_bloc.dart | 8 +++++--- lib/core/auth/bloc/auth_state.dart | 2 +- lib/thunder/pages/thunder_page.dart | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index 5e7ad5380..b04aa0c51 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -203,24 +203,24 @@ class _LoginPageState extends State with SingleTickerProviderStateMix context.read().add(const CancelLoginAttempt()); } } - } else if (state.status == AuthStatus.oauthCreateUsername) { - bool completedUsername = false; + } else if (state.status == AuthStatus.oauthSignUp) { + bool completedSignUp = false; await showThunderDialog( context: context, - title: "Pick your username", + title: "Sign Up", contentText: "Pick your username", onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), secondaryButtonText: l10n.cancel, onPrimaryButtonPressed: (dialogContext, _) async { Navigator.of(dialogContext).pop(); - completedUsername = true; + completedSignUp = true; }, primaryButtonText: l10n.accept, ); if (context.mounted) { - if (completedUsername) { + if (completedSignUp) { context.read().add(const OAuthCreateAccount()); } else { // Cancel the login diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index 153b2ab8b..b0c985aec 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -220,6 +220,9 @@ class AuthBloc extends Bloc { return emit(state.copyWith(oauthState: oauthState, oauthInstance: instance, oauthProvider: provider)); } on LemmyApiException catch (e) { + // TODO: I think this is the right place to Sign Up and Create a username. + // I think the first login will fail which will let you know you need to create a username. + // I think there will be an exception somewhere else if your application is waiting for approval. return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProvider: null)); } catch (e) { try { @@ -264,7 +267,7 @@ class AuthBloc extends Bloc { throw Exception("OAuth login failed: no jwt received from lemmy instance."); } - return emit(state.copyWith(status: AuthStatus.oauthCreateUsername, oauthJwt: loginResponse.jwt)); + return emit(state.copyWith(status: AuthStatus.oauthSignUp, oauthJwt: loginResponse.jwt)); } on LemmyApiException catch (e) { return emit( state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProvider: null, tempAccount: null)); @@ -281,7 +284,7 @@ class AuthBloc extends Bloc { } }); - /// Adds the tempAccount and sets it as the active account. + /// Create the tempAccount on((event, emit) async { LemmyClient lemmyClient = LemmyClient.instance; LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; @@ -290,7 +293,6 @@ class AuthBloc extends Bloc { return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false)); } - // TODO: Need to add a step to set the account username on the first login. GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: state.oauthJwt)); // Create a new account in the database diff --git a/lib/core/auth/bloc/auth_state.dart b/lib/core/auth/bloc/auth_state.dart index 83f4b12e6..87729348d 100644 --- a/lib/core/auth/bloc/auth_state.dart +++ b/lib/core/auth/bloc/auth_state.dart @@ -1,6 +1,6 @@ part of 'auth_bloc.dart'; -enum AuthStatus { initial, loading, success, failure, failureCheckingInstance, contentWarning, oauthContentWarning, oauthCreateUsername } +enum AuthStatus { initial, loading, success, failure, failureCheckingInstance, contentWarning, oauthContentWarning, oauthSignUp } class AuthState extends Equatable { const AuthState({ diff --git a/lib/thunder/pages/thunder_page.dart b/lib/thunder/pages/thunder_page.dart index 13abb3057..4454c17d3 100644 --- a/lib/thunder/pages/thunder_page.dart +++ b/lib/thunder/pages/thunder_page.dart @@ -548,7 +548,7 @@ class _ThunderState extends State { ); case AuthStatus.contentWarning: case AuthStatus.oauthContentWarning: - case AuthStatus.oauthCreateUsername: + case AuthStatus.oauthSignUp: case AuthStatus.success: Version? version = thunderBlocState.version; bool showInAppUpdateNotification = thunderBlocState.showInAppUpdateNotification; From b353b8b33b6681cf4c6e38f2d7eeee8b2d2ed8f5 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Sun, 5 Jan 2025 15:31:12 -0500 Subject: [PATCH 39/50] Create username is working --- lib/account/pages/login_page.dart | 4 +- lib/core/auth/bloc/auth_bloc.dart | 96 +++++++++++++++++++---------- lib/core/auth/bloc/auth_event.dart | 16 ++--- lib/core/auth/bloc/auth_state.dart | 25 +++++--- lib/thunder/pages/thunder_page.dart | 2 +- 5 files changed, 93 insertions(+), 50 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index b04aa0c51..a1a149802 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -205,6 +205,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix } } else if (state.status == AuthStatus.oauthSignUp) { bool completedSignUp = false; + String? username; await showThunderDialog( context: context, @@ -215,13 +216,14 @@ class _LoginPageState extends State with SingleTickerProviderStateMix onPrimaryButtonPressed: (dialogContext, _) async { Navigator.of(dialogContext).pop(); completedSignUp = true; + username = "abcabcabc"; }, primaryButtonText: l10n.accept, ); if (context.mounted) { if (completedSignUp) { - context.read().add(const OAuthCreateAccount()); + context.read().add(OAuthLoginAttempt(username: username)); } else { // Cancel the login context.read().add(const CancelLoginAttempt()); diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index b0c985aec..df159dfe1 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -192,8 +192,8 @@ class AuthBloc extends Bloc { on((event, emit) async { LemmyClient lemmyClient = LemmyClient.instance; String originalBaseUrl = lemmyClient.lemmyApiV3.host; - String instance = event.instance; - ProviderView provider = event.provider; + String instance = event.instance ?? state.oauthInstance!; + ProviderView provider = event.provider ?? state.oauthProvider!; try { emit(state.copyWith(status: AuthStatus.loading, account: null, isLoggedIn: false)); @@ -218,11 +218,8 @@ class AuthBloc extends Bloc { throw Exception('Could not launch $url'); } - return emit(state.copyWith(oauthState: oauthState, oauthInstance: instance, oauthProvider: provider)); + return emit(state.copyWith(oauthState: oauthState, oauthInstance: instance, oauthProvider: provider, oauthUsername: event.username)); } on LemmyApiException catch (e) { - // TODO: I think this is the right place to Sign Up and Create a username. - // I think the first login will fail which will let you know you need to create a username. - // I think there will be an exception somewhere else if your application is waiting for approval. return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProvider: null)); } catch (e) { try { @@ -235,12 +232,19 @@ class AuthBloc extends Bloc { } }); - on((event, emit) async { + // This is triggered by app_link callback. + on((event, emit) async { LemmyClient lemmyClient = LemmyClient.instance; LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; String originalBaseUrl = lemmyClient.lemmyApiV3.host; String redirectUri = "https://thunderapp.dev/oauth/callback"; - String providerResponse = event.link; + String providerResponse = event.link ?? state.oauthLink!; + String instance = state.oauthInstance!; + String? username = event.username ?? state.oauthUsername; + emit(state.copyWith(oauthLink: providerResponse)); + + if (instance.startsWith('https://')) instance = instance.replaceAll('https://', ''); + lemmyClient.changeBaseUrl(instance); try { if (state.oauthState == null || state.oauthInstance == null || state.oauthProvider == null) { @@ -259,36 +263,24 @@ class AuthBloc extends Bloc { throw Exception("OAuth login failed: no code received from provider."); } + // TODO: I think there will be an exception somewhere else if your application is waiting for approval. + // Authenthicate to lemmy instance and get a jwt. // Durring this step lemmy connects to the Provider to get the user info. - LoginResponse loginResponse = await lemmy.run(AuthenticateWithOAuth(code: code, oauth_provider_id: state.oauthProvider!.id, redirect_uri: redirectUri)); + // TODO: Some reason I can't call this 2 times in a row. + LoginResponse loginResponse = await lemmy.run(AuthenticateWithOAuth( + username: username, + code: code, + oauth_provider_id: state.oauthProvider!.id, + redirect_uri: redirectUri, + )); if (loginResponse.jwt == null) { throw Exception("OAuth login failed: no jwt received from lemmy instance."); } - return emit(state.copyWith(status: AuthStatus.oauthSignUp, oauthJwt: loginResponse.jwt)); - } on LemmyApiException catch (e) { - return emit( - state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProvider: null, tempAccount: null)); - } catch (e) { - try { - // Restore the original baseUrl - lemmyClient.changeBaseUrl(originalBaseUrl); - } catch (e, s) { - return emit( - state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: s.toString(), oauthState: null, oauthInstance: null, oauthProvider: null, tempAccount: null)); - } - return emit( - state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString(), oauthState: null, oauthInstance: null, oauthProvider: null, tempAccount: null)); - } - }); + emit(state.copyWith(oauthJwt: loginResponse.jwt, oauthLink: null)); - /// Create the tempAccount - on((event, emit) async { - LemmyClient lemmyClient = LemmyClient.instance; - LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; - try { if (state.oauthJwt == null) { return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false)); } @@ -309,8 +301,50 @@ class AuthBloc extends Bloc { if (getSiteResponse.siteView.site.contentWarning?.isNotEmpty == true) { return emit(state.copyWith(status: AuthStatus.oauthContentWarning, contentWarning: getSiteResponse.siteView.site.contentWarning, oauthState: state.oauthState, tempAccount: account)); } + } on LemmyApiException catch (e) { + if (e.message == 'registration_username_required') { + return emit(state.copyWith(status: AuthStatus.oauthSignUp, oauthState: state.oauthState)); + } else { + return emit(state.copyWith( + status: AuthStatus.failure, + account: null, + isLoggedIn: false, + errorMessage: e.toString(), + oauthLink: null, + oauthState: null, + oauthInstance: null, + oauthProvider: null, + tempAccount: null, + )); + } } catch (e) { - return emit(state.copyWith(status: AuthStatus.failure, tempAccount: null, account: null, isLoggedIn: false)); + try { + // Restore the original baseUrl + lemmyClient.changeBaseUrl(originalBaseUrl); + } catch (e, s) { + return emit(state.copyWith( + status: AuthStatus.failure, + account: null, + isLoggedIn: false, + errorMessage: s.toString(), + oauthLink: null, + oauthState: null, + oauthInstance: null, + oauthProvider: null, + tempAccount: null, + )); + } + return emit(state.copyWith( + status: AuthStatus.failure, + account: null, + isLoggedIn: false, + errorMessage: e.toString(), + oauthLink: null, + oauthState: null, + oauthInstance: null, + oauthProvider: null, + tempAccount: null, + )); } }); diff --git a/lib/core/auth/bloc/auth_event.dart b/lib/core/auth/bloc/auth_event.dart index b81677f55..785e785ec 100644 --- a/lib/core/auth/bloc/auth_event.dart +++ b/lib/core/auth/bloc/auth_event.dart @@ -32,18 +32,20 @@ class LoginAttempt extends AuthEvent { /// The [OAuthLoginAttempt] event should be triggered whenever the user attempts to log in with OAuth. /// This event is responsible for login authentication and handling related errors. class OAuthLoginAttempt extends AuthEvent { - final String instance; - final ProviderView provider; + final String? instance; + final ProviderView? provider; + final String? username; final bool showContentWarning; - const OAuthLoginAttempt({required this.instance, required this.provider, this.showContentWarning = true}); + const OAuthLoginAttempt({this.instance, this.provider, this.username, this.showContentWarning = true}); } -/// The [OAuthGetJwt] event should be triggered whenever the user attempts to log in with OAuth. +/// The [OAuthLoginAttemptPart2] event should be triggered whenever the user attempts to log in with OAuth. /// This event is responsible for login authentication and handling related errors. -class OAuthGetJwt extends AuthEvent { - final String link; - const OAuthGetJwt({required this.link}); +class OAuthLoginAttemptPart2 extends AuthEvent { + final String? link; + final String? username; + const OAuthLoginAttemptPart2({required this.link, this.username}); } class OAuthCreateAccount extends AuthEvent { diff --git a/lib/core/auth/bloc/auth_state.dart b/lib/core/auth/bloc/auth_state.dart index 87729348d..7c4694392 100644 --- a/lib/core/auth/bloc/auth_state.dart +++ b/lib/core/auth/bloc/auth_state.dart @@ -14,9 +14,10 @@ class AuthState extends Equatable { this.reload = true, this.contentWarning, this.oauthInstance, - this.oauthState, - this.oauthLink, this.oauthJwt, + this.oauthLink, + this.oauthState, + this.oauthUsername, this.oauthProvider, }); @@ -30,9 +31,10 @@ class AuthState extends Equatable { final bool reload; final String? contentWarning; final String? oauthInstance; - final String? oauthState; - final String? oauthLink; final String? oauthJwt; + final String? oauthLink; + final String? oauthState; + final String? oauthUsername; final ProviderView? oauthProvider; AuthState copyWith({ @@ -46,9 +48,10 @@ class AuthState extends Equatable { bool? reload, String? contentWarning, String? oauthInstance, - String? oauthState, - String? oauthLink, String? oauthJwt, + String? oauthLink, + String? oauthState, + String? oauthUsername, ProviderView? oauthProvider, }) { return AuthState( @@ -62,9 +65,10 @@ class AuthState extends Equatable { reload: reload ?? this.reload, contentWarning: contentWarning, oauthInstance: oauthInstance ?? this.oauthInstance, - oauthState: oauthState ?? this.oauthInstance, + oauthJwt: oauthJwt ?? this.oauthJwt, oauthLink: oauthLink ?? this.oauthLink, - oauthJwt: oauthJwt ?? oauthJwt, + oauthState: oauthState ?? this.oauthState, + oauthUsername: oauthUsername ?? this.oauthUsername, oauthProvider: oauthProvider ?? this.oauthProvider, ); } @@ -81,9 +85,10 @@ class AuthState extends Equatable { reload, contentWarning, oauthInstance, - oauthState, - oauthLink, oauthJwt, + oauthLink, + oauthState, + oauthUsername, oauthProvider, ]; } diff --git a/lib/thunder/pages/thunder_page.dart b/lib/thunder/pages/thunder_page.dart index 4454c17d3..8cf302cd4 100644 --- a/lib/thunder/pages/thunder_page.dart +++ b/lib/thunder/pages/thunder_page.dart @@ -256,7 +256,7 @@ class _ThunderState extends State { Future _oauthCallback(String link) async { try { debugPrint("_oauthCallback $link"); - context.read().add(OAuthGetJwt(link: link)); + context.read().add(OAuthLoginAttemptPart2(link: link)); } catch (e) { if (context.mounted) { _showLinkProcessingError(context, AppLocalizations.of(context)!.exceptionProcessingUri, link); From c956c5d3a70e2b545ff49d080f1ec5099f9e42eb Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Sun, 5 Jan 2025 23:57:58 -0500 Subject: [PATCH 40/50] omg all of the parts are working --- lib/account/pages/login_page.dart | 25 +++++------ lib/core/auth/bloc/auth_bloc.dart | 3 +- lib/shared/input_dialogs.dart | 75 +++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 15 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index a1a149802..4c443d641 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -13,6 +13,7 @@ import 'package:thunder/core/auth/bloc/auth_bloc.dart'; import 'package:thunder/core/singletons/lemmy_client.dart'; import 'package:thunder/instances.dart'; import 'package:thunder/shared/dialogs.dart'; +import 'package:thunder/shared/input_dialogs.dart'; import 'package:thunder/shared/snackbar.dart'; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; import 'package:thunder/utils/instance.dart'; @@ -207,19 +208,17 @@ class _LoginPageState extends State with SingleTickerProviderStateMix bool completedSignUp = false; String? username; - await showThunderDialog( - context: context, - title: "Sign Up", - contentText: "Pick your username", - onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), - secondaryButtonText: l10n.cancel, - onPrimaryButtonPressed: (dialogContext, _) async { - Navigator.of(dialogContext).pop(); - completedSignUp = true; - username = "abcabcabc"; - }, - primaryButtonText: l10n.accept, - ); + await showBlockingInputDialog( + context: context, + title: "Sign Up", + inputLabel: l10n.username, + getSuggestions: (_) => [], + suggestionBuilder: (payload) => Container(), + onSubmitted: ({payload, value}) { + completedSignUp = true; + username = value; + return Future.value(null); + }); if (context.mounted) { if (completedSignUp) { diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index df159dfe1..367fc7dfe 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -263,11 +263,10 @@ class AuthBloc extends Bloc { throw Exception("OAuth login failed: no code received from provider."); } - // TODO: I think there will be an exception somewhere else if your application is waiting for approval. + // TODO: I think there will be an exception somewhere if your application is waiting for approval. // Authenthicate to lemmy instance and get a jwt. // Durring this step lemmy connects to the Provider to get the user info. - // TODO: Some reason I can't call this 2 times in a row. LoginResponse loginResponse = await lemmy.run(AuthenticateWithOAuth( username: username, code: code, diff --git a/lib/shared/input_dialogs.dart b/lib/shared/input_dialogs.dart index d97ec5c7a..9c73f9e56 100644 --- a/lib/shared/input_dialogs.dart +++ b/lib/shared/input_dialogs.dart @@ -527,3 +527,78 @@ void showInputDialog({ }), ); } + +/// Shows a dialog which takes input and offers suggestions +Future showBlockingInputDialog({ + required BuildContext context, + required String title, + required String inputLabel, + required Future Function({T? payload, String? value}) onSubmitted, + required FutureOr?> Function(String query) getSuggestions, + required Widget Function(T payload) suggestionBuilder, +}) async { + final textController = TextEditingController(); + // Capture our content widget's setState function so we can call it outside the widget + StateSetter? contentWidgetSetState; + String? contentWidgetError; + + await showThunderDialog( + context: context, + title: title, + onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), + secondaryButtonText: AppLocalizations.of(context)!.cancel, + primaryButtonInitialEnabled: false, + onPrimaryButtonPressed: (dialogContext, setPrimaryButtonEnabled) async { + setPrimaryButtonEnabled(false); + final String? submitError = await onSubmitted(value: textController.text); + contentWidgetSetState?.call(() => contentWidgetError = submitError); + Navigator.of(dialogContext).pop(); + }, + primaryButtonText: AppLocalizations.of(context)!.ok, + // Use a stateful widget for the content so we can update the error message + contentWidgetBuilder: (setPrimaryButtonEnabled) => StatefulBuilder(builder: (context, setState) { + contentWidgetSetState = setState; + return SizedBox( + width: min(MediaQuery.of(context).size.width, 700), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TypeAheadField( + controller: textController, + builder: (context, controller, focusNode) => TextField( + controller: controller, + focusNode: focusNode, + onChanged: (value) { + setPrimaryButtonEnabled(value.trim().isNotEmpty); + setState(() => contentWidgetError = null); + }, + autofocus: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: inputLabel, + errorText: contentWidgetError, + ), + onSubmitted: (text) async { + setPrimaryButtonEnabled(false); + final String? submitError = await onSubmitted(value: text); + setState(() => contentWidgetError = submitError); + }, + ), + suggestionsCallback: getSuggestions, + itemBuilder: (context, payload) => suggestionBuilder(payload), + onSelected: (payload) async { + setPrimaryButtonEnabled(false); + final String? submitError = await onSubmitted(payload: payload); + setState(() => contentWidgetError = submitError); + }, + hideOnEmpty: true, + hideOnLoading: true, + hideOnError: true, + ), + ], + ), + ); + }), + ); + return null; +} From 9f6e6beffb30ebb7e0935918f8c9c8bc5551bcec Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Mon, 6 Jan 2025 20:21:29 -0500 Subject: [PATCH 41/50] touch ups --- lib/account/pages/login_page.dart | 5 +-- lib/core/auth/bloc/auth_bloc.dart | 62 ++++++++++++++++++++++-------- lib/core/auth/bloc/auth_event.dart | 6 +-- pubspec.lock | 12 +++--- 4 files changed, 57 insertions(+), 28 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index 4c443d641..5dc0f2cf0 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -498,7 +498,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix ), onPressed: (!isLoading && _instanceTextEditingController.text.isNotEmpty) ? () { - _handleOAuthLoginPart1(provider: provider); + _handleOAuthLogin(provider: provider); } : (_instanceTextEditingController.text.isNotEmpty && widget.anonymous) ? () => _addAnonymousInstance(context) @@ -536,14 +536,13 @@ class _LoginPageState extends State with SingleTickerProviderStateMix ); } - void _handleOAuthLoginPart1({required ProviderView provider, bool showContentWarning = true}) { + void _handleOAuthLogin({required ProviderView provider}) { TextInput.finishAutofillContext(); // Perform oauth login authentication. context.read().add( OAuthLoginAttempt( instance: _instanceTextEditingController.text.trim(), provider: provider, - showContentWarning: showContentWarning, ), ); } diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index 367fc7dfe..fad25a64b 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -232,7 +232,7 @@ class AuthBloc extends Bloc { } }); - // This is triggered by app_link callback. + /// Using the code from the previous step, login to lemmy instance an get the jwt. This is triggered by app_link callback. on((event, emit) async { LemmyClient lemmyClient = LemmyClient.instance; LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; @@ -263,8 +263,6 @@ class AuthBloc extends Bloc { throw Exception("OAuth login failed: no code received from provider."); } - // TODO: I think there will be an exception somewhere if your application is waiting for approval. - // Authenthicate to lemmy instance and get a jwt. // Durring this step lemmy connects to the Provider to get the user info. LoginResponse loginResponse = await lemmy.run(AuthenticateWithOAuth( @@ -298,7 +296,14 @@ class AuthBloc extends Bloc { // Save account to AuthBlock state and show the content warning. if (getSiteResponse.siteView.site.contentWarning?.isNotEmpty == true) { - return emit(state.copyWith(status: AuthStatus.oauthContentWarning, contentWarning: getSiteResponse.siteView.site.contentWarning, oauthState: state.oauthState, tempAccount: account)); + return emit(state.copyWith( + status: AuthStatus.oauthContentWarning, + contentWarning: getSiteResponse.siteView.site.contentWarning, + downvotesEnabled: getSiteResponse.siteView.localSite.enableDownvotes ?? false, + getSiteResponse: getSiteResponse, + oauthState: state.oauthState, + tempAccount: account, + )); } } on LemmyApiException catch (e) { if (e.message == 'registration_username_required') { @@ -350,11 +355,12 @@ class AuthBloc extends Bloc { /// Adds the tempAccount and sets it as the active account. on((event, emit) async { try { - if (state.tempAccount == null) { + Account? account = state.tempAccount ?? event.account; + if (account == null) { return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false)); } - Account? account = await Account.insertAccount(state.tempAccount!); + account = await Account.insertAccount(account); emit(state.copyWith(tempAccount: null)); if (account == null) { @@ -366,21 +372,45 @@ class AuthBloc extends Bloc { prefs.setString('active_profile_id', account.id); return emit(state.copyWith( - status: AuthStatus.success, - account: account, - isLoggedIn: true, - //downvotesEnabled: downvotesEnabled, - //getSiteResponse: getSiteResponse, - oauthState: null, - oauthInstance: null, - oauthProvider: null)); + status: AuthStatus.success, + account: account, + isLoggedIn: true, + tempAccount: null, + oauthState: null, + oauthInstance: null, + oauthProvider: null, + oauthUsername: null, + oauthJwt: null, + oauthLink: null, + )); } catch (e) { - return emit(state.copyWith(status: AuthStatus.failure, tempAccount: null, account: null, isLoggedIn: false)); + return emit(state.copyWith( + status: AuthStatus.failure, + tempAccount: null, + account: null, + isLoggedIn: false, + oauthState: null, + oauthInstance: null, + oauthProvider: null, + oauthUsername: null, + oauthJwt: null, + oauthLink: null, + )); } }); on((event, emit) async { - return emit(state.copyWith(status: AuthStatus.failure, errorMessage: AppLocalizations.of(GlobalContext.context)!.loginAttemptCanceled, tempAccount: null)); + return emit(state.copyWith( + status: AuthStatus.failure, + errorMessage: AppLocalizations.of(GlobalContext.context)!.loginAttemptCanceled, + tempAccount: null, + oauthState: null, + oauthProvider: null, + oauthLink: null, + oauthJwt: null, + oauthInstance: null, + oauthUsername: null, + )); }); /// When we log out of all accounts, clear the instance information diff --git a/lib/core/auth/bloc/auth_event.dart b/lib/core/auth/bloc/auth_event.dart index 785e785ec..a3916e489 100644 --- a/lib/core/auth/bloc/auth_event.dart +++ b/lib/core/auth/bloc/auth_event.dart @@ -35,9 +35,8 @@ class OAuthLoginAttempt extends AuthEvent { final String? instance; final ProviderView? provider; final String? username; - final bool showContentWarning; - const OAuthLoginAttempt({this.instance, this.provider, this.username, this.showContentWarning = true}); + const OAuthLoginAttempt({this.instance, this.provider, this.username}); } /// The [OAuthLoginAttemptPart2] event should be triggered whenever the user attempts to log in with OAuth. @@ -53,7 +52,8 @@ class OAuthCreateAccount extends AuthEvent { } class AddAccount extends AuthEvent { - const AddAccount(); + final Account? account; + const AddAccount({this.account}); } /// Cancels a login attempt by emitting the `failure` state. diff --git a/pubspec.lock b/pubspec.lock index 9a994ad86..b11da65e1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -546,18 +546,18 @@ packages: dependency: "direct main" description: name: flex_color_scheme - sha256: "09bea5d776f694c5a67f2229f2aa500cc7cce369322dc6500ab01cf9ad1b4e1a" + sha256: "90f4fe67b9561ae8a4af117df65a8ce9988624025667c54e6d304e65cff77d52" url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.0.2" flex_seed_scheme: dependency: transitive description: name: flex_seed_scheme - sha256: d3ba3c5c92d2d79d45e94b4c6c71d01fac3c15017da1545880c53864da5dfeb0 + sha256: "7639d2c86268eff84a909026eb169f008064af0fb3696a651b24b0fa24a40334" url: "https://pub.dev" source: hosted - version: "3.5.0" + version: "3.4.1" flutter: dependency: "direct main" description: flutter @@ -990,10 +990,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.1.2" + version: "4.0.2" image: dependency: transitive description: From 4b71d024de009e8c5a8ba2a10b408f454973a4d2 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Tue, 7 Jan 2025 12:30:34 -0500 Subject: [PATCH 42/50] update lemmy_api_client in pubspec --- lib/account/pages/login_page.dart | 1 + pubspec.lock | 8 +++++--- pubspec.yaml | 9 +++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index 5dc0f2cf0..25e33c6bd 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -174,6 +174,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix if (context.mounted) { if (acceptedContentWarning) { // Do another login attempt, this time without the content warning + // TODO: This can be updated to use AddAccount instead of starting the login process over. _handleLogin(showContentWarning: false); } else { // Cancel the login diff --git a/pubspec.lock b/pubspec.lock index b11da65e1..2fda8da08 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1149,9 +1149,11 @@ packages: lemmy_api_client: dependency: "direct main" description: - path: "../lemmy_api_client" - relative: true - source: path + path: "." + ref: "2886218a71c75d14cb21d2079bfb3666603d3221" + resolved-ref: "2886218a71c75d14cb21d2079bfb3666603d3221" + url: "https://github.com/gwbischof/lemmy_api_client.git" + source: git version: "0.21.0" link_preview_generator: dependency: "direct main" diff --git a/pubspec.yaml b/pubspec.yaml index 3fce59630..cf0cae719 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,10 +13,11 @@ dependencies: push: path: packages/push/push lemmy_api_client: - path: ../lemmy_api_client - #git: - # url: "https://github.com/thunder-app/lemmy_api_client.git" - # ref: 16d14a1c13ac9522e85188ad9cf23d8912ec8fee + git: + url: "https://github.com/gwbischof/lemmy_api_client.git" + ref: 2886218a71c75d14cb21d2079bfb3666603d3221 + #url: "https://github.com/thunder-app/lemmy_api_client.git" + #ref: 16d14a1c13ac9522e85188ad9cf23d8912ec8fee link_preview_generator: git: url: "https://github.com/thunder-app/link_preview_generator.git" From ab98b1223e18b7ced0ec25f20a725a7be4a68ac5 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Thu, 9 Jan 2025 01:01:27 -0500 Subject: [PATCH 43/50] touch up --- pubspec.lock | 20 ++------------------ pubspec.yaml | 1 - 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 2fda8da08..cfb7a83c3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -853,22 +853,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.2.0" - flutter_web_auth_2: - dependency: "direct main" - description: - name: flutter_web_auth_2 - sha256: "3c14babeaa066c371f3a743f204dd0d348b7d42ffa6fae7a9847a521aff33696" - url: "https://pub.dev" - source: hosted - version: "4.1.0" - flutter_web_auth_2_platform_interface: - dependency: transitive - description: - name: flutter_web_auth_2_platform_interface - sha256: c63a472c8070998e4e422f6b34a17070e60782ac442107c70000dd1bed645f4d - url: "https://pub.dev" - source: hosted - version: "4.1.0" flutter_web_plugins: dependency: transitive description: flutter @@ -990,10 +974,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" image: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index cf0cae719..2ef8a9bac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -95,7 +95,6 @@ dependencies: youtube_player_flutter: "^9.1.0" youtube_player_iframe: "^5.2.0" freezed_annotation: ^2.4.4 - flutter_web_auth_2: ^4.0.1 uuid: ^4.5.1 dev_dependencies: From 5950fd96a3252125bc62cd4a060cd082ac437e27 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Thu, 9 Jan 2025 01:35:08 -0500 Subject: [PATCH 44/50] touch ups --- lib/core/auth/bloc/auth_bloc.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index fad25a64b..9a0a93a19 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -21,6 +21,7 @@ part 'auth_event.dart'; part 'auth_state.dart'; const throttleDuration = Duration(milliseconds: 100); +const String redirectUri = "https://thunderapp.dev/oauth/callback"; // This must end in /oauth/callback. EventTransformer throttleDroppable(Duration duration) { return (events, mapper) { @@ -52,7 +53,7 @@ class AuthBloc extends Bloc { prefs.setString('active_profile_id', event.accountId); // Check to see the instance settings (for checking if downvotes are enabled) - LemmyClient.instance.changeBaseUrl(account.instance.replaceAll('https://', '')); + LemmyClient.instance.changeBaseUrl(account.instance.replaceFirst('https://', '')); LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: account.jwt)); @@ -103,7 +104,7 @@ class AuthBloc extends Bloc { if (activeAccount.username != null && activeAccount.jwt != null) { // Set lemmy client to use the instance - LemmyClient.instance.changeBaseUrl(activeAccount.instance.replaceAll('https://', '')); + LemmyClient.instance.changeBaseUrl(activeAccount.instance.replaceFirst('https://', '')); // Check to see the instance settings (for checking if downvotes are enabled) LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; @@ -131,7 +132,7 @@ class AuthBloc extends Bloc { emit(state.copyWith(status: AuthStatus.loading, account: null, isLoggedIn: false)); String instance = event.instance; - if (instance.startsWith('https://')) instance = instance.replaceAll('https://', ''); + if (instance.startsWith('https://')) instance = instance.replaceFirst('https://', ''); lemmyClient.changeBaseUrl(instance); LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; @@ -198,12 +199,12 @@ class AuthBloc extends Bloc { try { emit(state.copyWith(status: AuthStatus.loading, account: null, isLoggedIn: false)); - if (instance.startsWith('https://')) instance = instance.replaceAll('https://', ''); + if (instance.startsWith('https://')) instance = instance.replaceFirst('https://', ''); lemmyClient.changeBaseUrl(instance); // Build oauth provider url. var authorizationEndpoint = Uri.parse(provider.authorizationEndpoint); - String redirectUri = "https://thunderapp.dev/oauth/callback"; // This must end in /oauth/callback. + String oauthState = const Uuid().v4(); final url = Uri.https(authorizationEndpoint.host, authorizationEndpoint.path, { 'response_type': 'code', @@ -237,13 +238,12 @@ class AuthBloc extends Bloc { LemmyClient lemmyClient = LemmyClient.instance; LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; String originalBaseUrl = lemmyClient.lemmyApiV3.host; - String redirectUri = "https://thunderapp.dev/oauth/callback"; String providerResponse = event.link ?? state.oauthLink!; String instance = state.oauthInstance!; String? username = event.username ?? state.oauthUsername; emit(state.copyWith(oauthLink: providerResponse)); - if (instance.startsWith('https://')) instance = instance.replaceAll('https://', ''); + if (instance.startsWith('https://')) instance = instance.replaceFirst('https://', ''); lemmyClient.changeBaseUrl(instance); try { @@ -427,7 +427,7 @@ class AuthBloc extends Bloc { emit(state.copyWith(status: AuthStatus.loading, isLoggedIn: state.isLoggedIn, account: state.account)); // When the instance changes, update the fullSiteView - LemmyClient.instance.changeBaseUrl(event.instance.replaceAll('https://', '')); + LemmyClient.instance.changeBaseUrl(event.instance.replaceFirst('https://', '')); LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; // Check to see if there is an active, non-anonymous account From 70a194074d3d20cd3a89acd512d4f0c16903f10c Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Sat, 11 Jan 2025 17:15:27 -0500 Subject: [PATCH 45/50] reduce some lines of code --- lib/account/pages/login_page.dart | 36 +++++++------------------------ 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index 25e33c6bd..7128b6b97 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -155,7 +155,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix } else if (state.status == AuthStatus.success && context.read().state.isLoggedIn) { Navigator.of(context).pop(); showSnackbar(AppLocalizations.of(context)!.loginSucceeded); - } else if (state.status == AuthStatus.contentWarning) { + } else if (state.status == AuthStatus.contentWarning || state.status == AuthStatus.oauthContentWarning) { bool acceptedContentWarning = false; await showThunderDialog( @@ -173,33 +173,13 @@ class _LoginPageState extends State with SingleTickerProviderStateMix if (context.mounted) { if (acceptedContentWarning) { - // Do another login attempt, this time without the content warning - // TODO: This can be updated to use AddAccount instead of starting the login process over. - _handleLogin(showContentWarning: false); - } else { - // Cancel the login - context.read().add(const CancelLoginAttempt()); - } - } - } else if (state.status == AuthStatus.oauthContentWarning) { - bool acceptedContentWarning = false; - - await showThunderDialog( - context: context, - title: l10n.contentWarning, - contentText: state.contentWarning, - onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), - secondaryButtonText: l10n.decline, - onPrimaryButtonPressed: (dialogContext, _) async { - Navigator.of(dialogContext).pop(); - acceptedContentWarning = true; - }, - primaryButtonText: l10n.accept, - ); - - if (context.mounted) { - if (acceptedContentWarning) { - context.read().add(const AddAccount()); + if (state.status == AuthStatus.oauthContentWarning) { + context.read().add(const AddAccount()); + } else { + // Do another login attempt, this time without the content warning + // TODO: This can be updated to use AddAccount instead of starting the login process over. + _handleLogin(showContentWarning: false); + } } else { // Cancel the login context.read().add(const CancelLoginAttempt()); From 50815651ee4421f6d39cfd5e14f756d35f16c958 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Sat, 11 Jan 2025 17:28:04 -0500 Subject: [PATCH 46/50] sign up localization --- lib/account/pages/login_page.dart | 2 +- lib/l10n/app_en.arb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index 7128b6b97..e065113c1 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -191,7 +191,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix await showBlockingInputDialog( context: context, - title: "Sign Up", + title: l10n.signUp, inputLabel: l10n.username, getSuggestions: (_) => [], suggestionBuilder: (payload) => Container(), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1cd7224ea..52b1d0224 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -2291,6 +2291,10 @@ "@sidebarBottomNavDoubleTapDescription": {}, "sidebarBottomNavSwipeDescription": "Swipe bottom nav to open sidebar", "@sidebarBottomNavSwipeDescription": {}, + "signUp": "Sign Up", + "@signUp": { + "description": "Title for thunder sign up dialog." + }, "small": "Small", "@small": { "description": "Description for small font scale" From f3080128fdca5cb3a4f6730c4754d9a1178d3515 Mon Sep 17 00:00:00 2001 From: Garrett Bischof Date: Tue, 14 Jan 2025 15:13:00 -0500 Subject: [PATCH 47/50] remvoe extra LogOut --- lib/thunder/pages/thunder_page.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/thunder/pages/thunder_page.dart b/lib/thunder/pages/thunder_page.dart index 8cf302cd4..af66c5a94 100644 --- a/lib/thunder/pages/thunder_page.dart +++ b/lib/thunder/pages/thunder_page.dart @@ -97,7 +97,6 @@ class _ThunderState extends State { @override void initState() { super.initState(); - context.read().add(const LogOutOfAllAccounts()); selectedPageIndex = widget.pageController.initialPage; From 849fadfca52f4dbaafa0a1dd3a008acff28b34bd Mon Sep 17 00:00:00 2001 From: Micah Morrison Date: Tue, 14 Jan 2025 18:33:03 -0500 Subject: [PATCH 48/50] Add SSO login heading and spacer between providers --- lib/account/pages/login_page.dart | 12 ++++++++++-- lib/l10n/app_en.arb | 4 ++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index e065113c1..ccdb89d5d 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -467,8 +467,15 @@ class _LoginPageState extends State with SingleTickerProviderStateMix child: Text(widget.anonymous ? AppLocalizations.of(context)!.add : AppLocalizations.of(context)!.login, style: theme.textTheme.titleMedium?.copyWith(color: !isLoading && fieldsFilledIn ? theme.colorScheme.onPrimary : theme.colorScheme.primary)), ), - const SizedBox(height: 12.0), - for (final provider in oauthProviders) + if (oauthProviders.isNotEmpty) ...[ + const SizedBox(height: 20.0), + Text( + l10n.orLogInWithSso, + style: theme.textTheme.titleMedium, + ), + ], + for (final provider in oauthProviders) ...[ + const SizedBox(height: 12.0), ElevatedButton( style: ElevatedButton.styleFrom( minimumSize: const Size.fromHeight(60), @@ -487,6 +494,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix child: Text(provider.displayName, style: theme.textTheme.titleMedium?.copyWith(color: !isLoading && _instanceTextEditingController.text.isNotEmpty ? theme.colorScheme.onPrimary : theme.colorScheme.primary)), ), + ], const SizedBox(height: 12.0), TextButton( style: ElevatedButton.styleFrom(minimumSize: const Size.fromHeight(60)), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 52b1d0224..f2178d9ef 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1593,6 +1593,10 @@ "@openSettings": { "description": "Prompt for the user to open system settings" }, + "orLogInWithSso": "Or log in with SSO", + "@orLogInWithSso": { + "description": "Heading displayed on login page for SSO login options" + }, "orange": "Orange", "@orange": { "description": "The color orange" From f282dafdc0258485f7a0392936f864cae9ec3b45 Mon Sep 17 00:00:00 2001 From: Micah Morrison Date: Fri, 17 Jan 2025 16:04:03 -0500 Subject: [PATCH 49/50] Get the lemmy api client after changing the base URL for OAuth --- lib/core/auth/bloc/auth_bloc.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index 9a0a93a19..8e165388f 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -236,7 +236,6 @@ class AuthBloc extends Bloc { /// Using the code from the previous step, login to lemmy instance an get the jwt. This is triggered by app_link callback. on((event, emit) async { LemmyClient lemmyClient = LemmyClient.instance; - LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; String originalBaseUrl = lemmyClient.lemmyApiV3.host; String providerResponse = event.link ?? state.oauthLink!; String instance = state.oauthInstance!; @@ -246,6 +245,8 @@ class AuthBloc extends Bloc { if (instance.startsWith('https://')) instance = instance.replaceFirst('https://', ''); lemmyClient.changeBaseUrl(instance); + LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; + try { if (state.oauthState == null || state.oauthInstance == null || state.oauthProvider == null) { throw Exception("OAuth login failed: oauthState, oauthInstance, or oauthProviderId is null."); From d8720e41512654ec23ff1934dc03be361cd8c735 Mon Sep 17 00:00:00 2001 From: Micah Morrison Date: Tue, 21 Jan 2025 12:17:13 -0500 Subject: [PATCH 50/50] Fix profile modal staying open after logging into new account --- lib/account/pages/login_page.dart | 5 +++-- lib/account/widgets/profile_modal_body.dart | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index ccdb89d5d..d87eb24f8 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -23,9 +23,10 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class LoginPage extends StatefulWidget { final VoidCallback popRegister; + final VoidCallback popModal; final bool anonymous; - const LoginPage({super.key, required this.popRegister, this.anonymous = false}); + const LoginPage({super.key, required this.popRegister, required this.popModal, this.anonymous = false}); @override State createState() => _LoginPageState(); @@ -153,7 +154,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix showSnackbar(AppLocalizations.of(context)!.loginFailed(state.errorMessage ?? AppLocalizations.of(context)!.missingErrorMessage)); } else if (state.status == AuthStatus.success && context.read().state.isLoggedIn) { - Navigator.of(context).pop(); + widget.popModal(); showSnackbar(AppLocalizations.of(context)!.loginSucceeded); } else if (state.status == AuthStatus.contentWarning || state.status == AuthStatus.oauthContentWarning) { bool acceptedContentWarning = false; diff --git a/lib/account/widgets/profile_modal_body.dart b/lib/account/widgets/profile_modal_body.dart index 6099d66c4..60feba715 100644 --- a/lib/account/widgets/profile_modal_body.dart +++ b/lib/account/widgets/profile_modal_body.dart @@ -52,6 +52,10 @@ class _ProfileModalBodyState extends State { ProfileModalBody.shellNavigatorKey.currentState!.pop(); } + void popModal() { + Navigator.of(context).pop(); + } + @override Widget build(BuildContext context) { return Navigator( @@ -86,7 +90,7 @@ class _ProfileModalBodyState extends State { break; case '/login': - page = LoginPage(popRegister: popRegister, anonymous: (settings.arguments as Map)['anonymous']!); + page = LoginPage(popRegister: popRegister, popModal: popModal, anonymous: (settings.arguments as Map)['anonymous']!); break; } return SwipeablePageRoute(