diff --git a/lib/business_logic/auth/oauth.dart b/lib/business_logic/auth/oauth.dart index 8af7ada..28b40c0 100644 --- a/lib/business_logic/auth/oauth.dart +++ b/lib/business_logic/auth/oauth.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:loris/business_logic/account/account.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -38,6 +39,10 @@ String _authCode = ""; String _url = ""; App? _app; +App? getApp() { + return _app; +} + Response readAuthcode(Request request) { Map params = request.url.queryParameters; if (params.containsKey("code") && params["code"] != null) { @@ -72,20 +77,27 @@ Future handleFullOauth(String url) async { return response.statusCode; } - _authCode = ""; - var handler = const Pipeline().addHandler(readAuthcode); - var server = await shelf_io.serve(handler, 'localhost', 1312); - await pollCode(); - server.close(); - String token = - await getToken(_authCode, _app!.clientId, _app!.clientSecret, _url); - await saveIdentity( - token, - url, - _app!, - _authCode, - ); - return 200; + if (!kIsWeb) { + // if this is not a web app we can simply start a server + // and listen for a code there + _authCode = ""; + var handler = const Pipeline().addHandler(readAuthcode); + var server = await shelf_io.serve(handler, 'localhost', 1312); + await pollCode(); + server.close(); + String token = + await getToken(_authCode, _app!.clientId, _app!.clientSecret, _url); + await saveIdentity( + token, + url, + _app!.clientId, + _app!.clientSecret, + _authCode, + ); + return 200; + } else { + return 6969696969696969; + } } catch (e) { return 400; } @@ -94,7 +106,8 @@ Future handleFullOauth(String url) async { Future saveIdentity( String token, String baseurl, - App app, + String clientId, + String clientSecret, String authCode, ) async { Map headers = {"Authorization": "Bearer $token"}; @@ -111,7 +124,8 @@ Future saveIdentity( final account = AccountModel.fromJson(jsonDecode(response.body)); await global.settings!.addNewIdentity("${account.acct}@$baseurl"); await global.settings!.saveActiveIdentity("${account.acct}@$baseurl"); - await global.settings!.identities["${account.acct}@$baseurl"]!.saveApp(app); + await global.settings!.identities["${account.acct}@$baseurl"]! + .saveApp(clientId, clientSecret); await global.settings!.identities["${account.acct}@$baseurl"]! .saveAuthCode(authCode); await global.settings!.identities["${account.acct}@$baseurl"]! @@ -141,7 +155,7 @@ Future doOauthFlow() async { String url = _url; try { http.Response response = await registerApp(url); - openBrowserForAuthCode(url, App.fromJson(jsonDecode(response.body))); + openBrowserForAuthCode(url, _app!); return response; } catch (e) { return http.Response(jsonEncode({}), 404); @@ -151,19 +165,36 @@ Future doOauthFlow() async { Future registerApp(String baseurl) async { //String url = baseurl Uri."api/v1/apps"; Uri url = Uri.https(baseurl, "/api/v1/apps"); + final Map params = { + 'client_name': global.name, + 'redirect_uris': kIsWeb + ? "${Uri.base.origin}/login/redirect.html" + : "http://localhost:1312", + 'scopes': "read write follow push", + 'website': global.website, + }; final response = await http.post( url, headers: global.defaultHeaders, - body: jsonEncode({ - 'client_name': global.name, - 'redirect_uris': "http://localhost:1312", - 'scopes': "read write follow push", - 'website': global.website - }), + body: jsonEncode(params), ); + print(response.body); + _app = App.fromJson(jsonDecode(response.body)); return response; } +Uri getAuthUrl(String baseurl, App app) { + return Uri( + scheme: "https", + path: "$baseurl/oauth/authorize", + // ignore: prefer_interpolation_to_compose_strings + query: "client_id=" + + app.clientId + + "&scope=read+write+follow+push" + + "&redirect_uri=${kIsWeb ? "${Uri.base.origin}/login/redirect.html" : "http://localhost:1312"}" + + "&response_type=code"); +} + void openBrowserForAuthCode(String baseurl, App app) { Uri url = Uri( scheme: "https", @@ -172,9 +203,14 @@ void openBrowserForAuthCode(String baseurl, App app) { query: "client_id=" + app.clientId + "&scope=read+write+follow+push" + - "&redirect_uri=http://localhost:1312" + + "&redirect_uri=${kIsWeb ? "${Uri.base.origin}/login/redirect.html" : "http://localhost:1312"}" + "&response_type=code"); - launchUrl(url); + launchUrl( + mode: LaunchMode.inAppWebView, + url, + webOnlyWindowName: "loris", + webViewConfiguration: const WebViewConfiguration(), + ); } Future getToken( @@ -184,21 +220,27 @@ Future getToken( String baseurl, ) async { Uri url = Uri.https(baseurl, "/oauth/token"); + final args = jsonEncode({ + 'grant_type': "authorization_code", + 'client_id': appId, + 'client_secret': clientSecret, + 'redirect_uri': kIsWeb + ? "${Uri.base.origin}/login/redirect.html" + : "http://localhost:1312", + 'scope': "read write follow push", + 'code': authCode, + }); final response = await http.post( url, headers: global.defaultHeaders, - body: jsonEncode({ - 'grant_type': "authorization_code", - 'client_id': appId, - 'client_secret': clientSecret, - 'redirect_uri': "http://localhost:1312", - 'scope': "read write follow push", - 'code': authCode, - }), + body: args, ); + print(args); + print(response.body); if (response.statusCode == 200) { final dec = jsonDecode(response.body); return dec["access_token"]!; } + return ""; } diff --git a/lib/business_logic/settings.dart b/lib/business_logic/settings.dart index 2b98c74..2b44c0e 100644 --- a/lib/business_logic/settings.dart +++ b/lib/business_logic/settings.dart @@ -1,7 +1,6 @@ import 'package:flutter/painting.dart'; import 'package:loris/themes/themes.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import '../business_logic/auth/oauth.dart' as oauth; import './websocket.dart' as websocket; import 'package:loris/themes/themes.dart' as themes; @@ -72,11 +71,11 @@ class AccountSettings { return await prefs.setString("$identity.$authCodeKey", code); } - Future saveApp(oauth.App app) async { - clientId = app.clientId; - clientSecret = app.clientSecret; - await prefs.setString("$identity.$clientSecretKey", app.clientSecret); - await prefs.setString("$identity.$clientIdKey", app.clientId); + Future saveApp(String clientSecret, String clientId) async { + this.clientId = clientId; + this.clientSecret = clientSecret; + await prefs.setString("$identity.$clientSecretKey", clientSecret); + await prefs.setString("$identity.$clientIdKey", clientId); } Future saveToken(String token) async { @@ -86,6 +85,10 @@ class AccountSettings { } class Settings { + static const currentClientSecretKey = "currentsecret"; + late String currentClientSecret; + static const currentClientIdkey = "currentclientid"; + late String currentClientId; late double postWidth = 0.8; static const postWidthKey = "post-width"; late double maxPostWidth = 1000; @@ -108,6 +111,11 @@ class Settings { Settings settings = Settings._create(); settings.prefs = await SharedPreferences.getInstance(); + + settings.currentClientSecret = + settings.prefs.getString(currentClientSecretKey) ?? ""; + settings.currentClientId = + settings.prefs.getString(currentClientIdkey) ?? ""; settings.locale = Locale(settings.prefs.getString(localeKey) ?? "en"); settings.batchSize = settings.prefs.getInt(batchSizeKey) ?? 20; @@ -152,6 +160,16 @@ class Settings { return prefs.setDouble(maxPostWidthKey, width); } + Future saveCurrentClientId(String id) async { + currentClientId = id; + return prefs.setString(currentClientIdkey, id); + } + + Future saveCurrentClientSecret(String secret) async { + currentClientId = secret; + return prefs.setString(currentClientSecretKey, secret); + } + Future savePostWidth(double width) { postWidth = width; return prefs.setDouble(postWidthKey, width); diff --git a/lib/i18n/en_US.json b/lib/i18n/en_US.json index dc10ee8..35efdef 100644 --- a/lib/i18n/en_US.json +++ b/lib/i18n/en_US.json @@ -52,6 +52,7 @@ "make-post": "make post", "content-warning": "content warning", "theme-title": "theme", - "send-post": "computer, send post" + "send-post": "computer, send post", + "jacking-in": "jacking in..." } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index b4816db..4d6f4b8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,11 +8,13 @@ import 'business_logic/settings.dart' as settings; import 'package:flutter_localizations/flutter_localizations.dart'; import 'themes/themes.dart' as themes; import 'global.dart' as global; +import 'package:url_strategy/url_strategy.dart'; ThemeData theme = themes.getTheme(themes.available[1]); Locale activeLocale = const Locale("en_US"); void main() async { + setPathUrlStrategy(); Intl.defaultLocale = "en_US"; global.settings = await settings.Settings.create(); activeLocale = global.settings!.locale; @@ -58,10 +60,29 @@ class _LorisState extends State { LocalJsonLocalization.delegate, ], initialRoute: global.settings!.identities.isEmpty ? "/login" : "/", - routes: { - '/': (context) => const MainScaffold(), - '/login': (context) => const Login(), - }, + onGenerateRoute: (s) => RouterGenerator.generateRoute(s), ); } } + +class RouterGenerator { + static Route generateRoute(RouteSettings s) { + var routingData = s.name; + switch (routingData) { + case "/login": + return MaterialPageRoute( + builder: (context) { + return const Login(); + }, + settings: s, + ); + default: + return MaterialPageRoute( + builder: (context) { + return const MainScaffold(); + }, + settings: s, + ); + } + } +} diff --git a/lib/pages/login.dart b/lib/pages/login.dart index fae7228..d357757 100644 --- a/lib/pages/login.dart +++ b/lib/pages/login.dart @@ -1,5 +1,7 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:localization/localization.dart'; +import 'package:loris/pages/login/weblogin.dart'; import '../business_logic/auth/oauth.dart' as oauth; class Login extends StatefulWidget { @@ -67,11 +69,20 @@ class _LoginFormState extends State { ); } else { formKey.currentState?.save(); - Navigator.push( + if (kIsWeb) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => Weblogin(url: instanceUrl), + )); + } else { + Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => AuthPage(url: instanceUrl), - )); + ), + ); + } } }, icon: const Icon(Icons.login), diff --git a/lib/pages/login/weblogin.dart b/lib/pages/login/weblogin.dart new file mode 100644 index 0000000..15e5336 --- /dev/null +++ b/lib/pages/login/weblogin.dart @@ -0,0 +1,100 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:localization/localization.dart'; +import 'package:universal_html/html.dart' as html; +import 'package:loris/business_logic/auth/oauth.dart' as oauth; + +class Weblogin extends StatefulWidget { + const Weblogin({Key? key, required this.url}) : super(key: key); + final String url; + + @override + State createState() => _WebloginState(); +} + +class _WebloginState extends State { + bool doneLoading = false; + void doAuth() async { + final appresponse = await oauth.registerApp(widget.url); + if (appresponse.statusCode != 200) { + informAboutFailure(appresponse.statusCode); + return; + } + final app = oauth.App.fromJson(jsonDecode(appresponse.body)); + + var popupWin = html.window.open( + oauth.getAuthUrl(widget.url, app).toString(), + "loris", + ); + + html.window.onMessage.listen((event) async { + final uri = Uri.parse(event.data); + if (uri.queryParameters.containsKey("code")) { + popupWin.close(); + final token = await oauth.getToken( + uri.queryParameters["code"]!, + app.clientId, + app.clientSecret, + widget.url, + ); + await oauth.saveIdentity(token, widget.url, app.clientId, + app.clientSecret, uri.queryParameters["code"]!); + setState(() { + doneLoading = true; + }); + } + }); + } + + void informAboutFailure(int i) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text("error: $i"))); + } + + @override + void initState() { + super.initState(); + doAuth(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + SelectableText(widget.url), + doneLoading + ? TextButton.icon( + onPressed: () { + Navigator.of(context).pushReplacementNamed("/"); + }, + icon: const Icon( + Icons.navigate_next, + ), + label: Text( + "jack-in".i18n(), + ), + ) + : const LoadingIndicator(), + ], + ), + ); + } +} + +class LoadingIndicator extends StatelessWidget { + const LoadingIndicator({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Wrap( + alignment: WrapAlignment.center, + spacing: 24, + children: [ + const CircularProgressIndicator(), + SelectableText("jacking-in".i18n()), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index ee32982..5ff95e2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -266,6 +266,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.4" + routemaster: + dependency: "direct main" + description: + name: routemaster + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" shared_preferences: dependency: "direct main" description: @@ -309,7 +316,7 @@ packages: source: hosted version: "2.0.0" shared_preferences_web: - dependency: transitive + dependency: "direct main" description: name: shared_preferences_web url: "https://pub.dartlang.org" @@ -383,6 +390,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + universal_html: + dependency: "direct main" + description: + name: universal_html + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + universal_io: + dependency: transitive + description: + name: universal_io + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" url_launcher: dependency: "direct main" description: @@ -439,6 +460,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + url_strategy: + dependency: "direct main" + description: + name: url_strategy + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0ba9cdb..4e9f6f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,10 @@ dependencies: flutter_markdown: ^0.6.10+5 markdown: ^5.0.0 html2md: ^1.2.6 + routemaster: ^1.0.1 + url_strategy: ^0.2.0 + shared_preferences_web: ^2.0.4 + universal_html: ^2.0.8 dev_dependencies: flutter_test: diff --git a/web/login/redirect.html b/web/login/redirect.html new file mode 100644 index 0000000..4bdb091 --- /dev/null +++ b/web/login/redirect.html @@ -0,0 +1,18 @@ + + + + + + Connexion Succeeded + + + + + + + + + \ No newline at end of file