From ac299ec7ba82537766f19ba6dddd53840123d752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Sun, 2 Jun 2024 17:09:57 +0200 Subject: [PATCH] Better client/login/authState handling --- flutter/lib/api/api_client.dart | 71 ++++---- flutter/lib/api/api_exception.dart | 13 ++ .../components/hidable_fab/hidable_fab.dart | 24 +++ flutter/lib/main.dart | 59 +++++-- flutter/lib/models/key_token_auth.dart | 11 -- flutter/lib/nav_layout.dart | 31 ++-- flutter/lib/pages/account/account.dart | 44 +++-- flutter/lib/pages/account/login.dart | 14 +- .../lib/pages/channel_list/channel_list.dart | 10 +- .../pages/channel_list/channel_list_item.dart | 8 +- .../lib/pages/message_list/message_list.dart | 10 +- .../lib/pages/message_view/message_view.dart | 6 +- flutter/lib/pages/send/root.dart | 159 ------------------ flutter/lib/pages/send/send.dart | 142 ++++++++++++++++ flutter/lib/state/app_auth.dart | 157 +++++++++++++++++ flutter/lib/state/token_source.dart | 21 +++ flutter/lib/state/user_account.dart | 117 ------------- 17 files changed, 496 insertions(+), 401 deletions(-) create mode 100644 flutter/lib/api/api_exception.dart create mode 100644 flutter/lib/components/hidable_fab/hidable_fab.dart delete mode 100644 flutter/lib/models/key_token_auth.dart delete mode 100644 flutter/lib/pages/send/root.dart create mode 100644 flutter/lib/pages/send/send.dart create mode 100644 flutter/lib/state/app_auth.dart create mode 100644 flutter/lib/state/token_source.dart delete mode 100644 flutter/lib/state/user_account.dart diff --git a/flutter/lib/api/api_client.dart b/flutter/lib/api/api_client.dart index e9fb7e4..1484113 100644 --- a/flutter/lib/api/api_client.dart +++ b/flutter/lib/api/api_client.dart @@ -1,18 +1,19 @@ import 'dart:convert'; -import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; +import 'package:simplecloudnotifier/api/api_exception.dart'; import 'package:simplecloudnotifier/models/api_error.dart'; import 'package:simplecloudnotifier/models/client.dart'; -import 'package:simplecloudnotifier/models/key_token_auth.dart'; import 'package:simplecloudnotifier/models/keytoken.dart'; import 'package:simplecloudnotifier/models/subscription.dart'; import 'package:simplecloudnotifier/models/user.dart'; +import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/globals.dart'; import 'package:simplecloudnotifier/state/request_log.dart'; import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/message.dart'; +import 'package:simplecloudnotifier/state/token_source.dart'; import 'package:simplecloudnotifier/utils/toaster.dart'; enum ChannelSelector { @@ -84,7 +85,7 @@ class APIClient { RequestLog.addRequestAPIError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, apierr); Toaster.error("Error", 'Request "${name}" failed'); - throw Exception(apierr.message); + throw APIException(responseStatusCode, apierr.error, apierr.errhighlight, apierr.message); } catch (exc, trace) { ApplicationLog.warn('Failed to decode api response as error-object', additional: exc.toString() + "\nBody:\n" + responseBody, trace: trace); } @@ -117,21 +118,21 @@ class APIClient { // ========================================================================================================================================================== - static Future getUser(KeyTokenAuth auth, String uid) async { + static Future getUser(TokenSource auth, String uid) async { return await _request( name: 'getUser', method: 'GET', relURL: 'users/$uid', fn: User.fromJson, - authToken: auth.tokenAdmin, + authToken: auth.getToken(), ); } - static Future addClient(KeyTokenAuth? auth, String fcmToken, String agentModel, String agentVersion, String? name, String clientType) async { + static Future addClient(TokenSource auth, String fcmToken, String agentModel, String agentVersion, String? name, String clientType) async { return await _request( name: 'addClient', method: 'POST', - relURL: 'users/${auth!.userId}/clients', + relURL: 'users/${auth.getUserID()}/clients', jsonBody: { 'fcm_token': fcmToken, 'agent_model': agentModel, @@ -140,15 +141,15 @@ class APIClient { 'name': name, }, fn: Client.fromJson, - authToken: auth.tokenAdmin, + authToken: auth.getToken(), ); } - static Future updateClient(KeyTokenAuth? auth, String clientID, String fcmToken, String agentModel, String? name, String agentVersion) async { + static Future updateClient(TokenSource auth, String clientID, String fcmToken, String agentModel, String? name, String agentVersion) async { return await _request( name: 'updateClient', method: 'PUT', - relURL: 'users/${auth!.userId}/clients/$clientID', + relURL: 'users/${auth.getUserID()}/clients/$clientID', jsonBody: { 'fcm_token': fcmToken, 'agent_model': agentModel, @@ -156,22 +157,32 @@ class APIClient { 'name': name, }, fn: Client.fromJson, - authToken: auth.tokenAdmin, + authToken: auth.getToken(), ); } - static Future> getChannelList(KeyTokenAuth auth, ChannelSelector sel) async { + static Future getClient(TokenSource auth, String cid) async { + return await _request( + name: 'getClient', + method: 'GET', + relURL: 'users/${auth.getUserID()}/clients/$cid', + fn: Client.fromJson, + authToken: auth.getToken(), + ); + } + + static Future> getChannelList(TokenSource auth, ChannelSelector sel) async { return await _request( name: 'getChannelList', method: 'GET', - relURL: 'users/${auth.userId}/channels', + relURL: 'users/${auth.getUserID()}/channels', query: {'selector': sel.apiKey}, fn: (json) => ChannelWithSubscription.fromJsonArray(json['channels'] as List), - authToken: auth.tokenAdmin, + authToken: auth.getToken(), ); } - static Future<(String, List)> getMessageList(KeyTokenAuth auth, String pageToken, {int? pageSize, List? channelIDs}) async { + static Future<(String, List)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, List? channelIDs}) async { return await _request( name: 'getMessageList', method: 'GET', @@ -182,48 +193,48 @@ class APIClient { if (channelIDs != null) 'channel_id': channelIDs.join(","), }, fn: (json) => Message.fromPaginatedJsonArray(json, 'messages', 'next_page_token'), - authToken: auth.tokenAdmin, + authToken: auth.getToken(), ); } - static Future getMessage(KeyTokenAuth auth, String msgid) async { + static Future getMessage(TokenSource auth, String msgid) async { return await _request( name: 'getMessage', method: 'GET', relURL: 'messages/$msgid', query: {}, fn: Message.fromJson, - authToken: auth.tokenAdmin, + authToken: auth.getToken(), ); } - static Future> getSubscriptionList(KeyTokenAuth auth) async { + static Future> getSubscriptionList(TokenSource auth) async { return await _request( name: 'getSubscriptionList', method: 'GET', - relURL: 'users/${auth.userId}/subscriptions', + relURL: 'users/${auth.getUserID()}/subscriptions', fn: (json) => Subscription.fromJsonArray(json['subscriptions'] as List), - authToken: auth.tokenAdmin, + authToken: auth.getToken(), ); } - static Future> getClientList(KeyTokenAuth auth) async { + static Future> getClientList(TokenSource auth) async { return await _request( name: 'getClientList', method: 'GET', - relURL: 'users/${auth.userId}/clients', + relURL: 'users/${auth.getUserID()}/clients', fn: (json) => Client.fromJsonArray(json['clients'] as List), - authToken: auth.tokenAdmin, + authToken: auth.getToken(), ); } - static Future> getKeyTokenList(KeyTokenAuth auth) async { + static Future> getKeyTokenList(TokenSource auth) async { return await _request( name: 'getKeyTokenList', method: 'GET', - relURL: 'users/${auth.userId}/keys', + relURL: 'users/${auth.getUserID()}/keys', fn: (json) => KeyToken.fromJsonArray(json['keys'] as List), - authToken: auth.tokenAdmin, + authToken: auth.getToken(), ); } @@ -245,13 +256,13 @@ class APIClient { ); } - static Future getKeyToken(KeyTokenAuth auth, String kid) async { + static Future getKeyToken(TokenSource auth, String kid) async { return await _request( name: 'getKeyToken', method: 'GET', - relURL: 'users/${auth.userId}/keys/$kid', + relURL: 'users/${auth.getUserID()}/keys/$kid', fn: KeyToken.fromJson, - authToken: auth.tokenAdmin, + authToken: auth.getToken(), ); } diff --git a/flutter/lib/api/api_exception.dart b/flutter/lib/api/api_exception.dart new file mode 100644 index 0000000..a0dbf41 --- /dev/null +++ b/flutter/lib/api/api_exception.dart @@ -0,0 +1,13 @@ +class APIException implements Exception { + final int httpStatus; + final String error; + final String errHighlight; + final String message; + + APIException(this.httpStatus, this.error, this.errHighlight, this.message); + + @override + String toString() { + return '[$error] $message'; + } +} diff --git a/flutter/lib/components/hidable_fab/hidable_fab.dart b/flutter/lib/components/hidable_fab/hidable_fab.dart new file mode 100644 index 0000000..3d0f577 --- /dev/null +++ b/flutter/lib/components/hidable_fab/hidable_fab.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class HidableFAB extends StatelessWidget { + final VoidCallback? onPressed; + final IconData icon; + + const HidableFAB({ + super.key, + this.onPressed, + required this.icon, + }); + + Widget build(BuildContext context) { + return Visibility( + visible: MediaQuery.viewInsetsOf(context).bottom == 0.0, // hide when keyboard is shown + child: FloatingActionButton( + onPressed: onPressed, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(17))), + elevation: 2.0, + child: Icon(icon), + ), + ); + } +} diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 32bbca4..d39bba6 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -3,12 +3,13 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/models/client.dart'; import 'package:simplecloudnotifier/nav_layout.dart'; import 'package:simplecloudnotifier/state/app_theme.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/globals.dart'; import 'package:simplecloudnotifier/state/request_log.dart'; -import 'package:simplecloudnotifier/state/user_account.dart'; +import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:toastification/toastification.dart'; import 'firebase_options.dart'; @@ -41,7 +42,20 @@ void main() async { ApplicationLog.error('Failed to open Hive-Box: scn-logs: ' + exc.toString(), trace: trace); } - UserAccount(); // ensure UserAccount is loaded + final appAuth = AppAuth(); // ensure UserAccount is loaded + + if (appAuth.isAuth()) { + try { + await appAuth.loadUser(); + } catch (exc, trace) { + ApplicationLog.error('Failed to load user (on startup): ' + exc.toString(), trace: trace); + } + try { + await appAuth.loadClient(); + } catch (exc, trace) { + ApplicationLog.error('Failed to load user (on startup): ' + exc.toString(), trace: trace); + } + } await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); @@ -71,7 +85,7 @@ void main() async { runApp( MultiProvider( providers: [ - ChangeNotifierProvider(create: (context) => UserAccount(), lazy: false), + ChangeNotifierProvider(create: (context) => AppAuth(), lazy: false), ChangeNotifierProvider(create: (context) => AppTheme(), lazy: false), ], child: const SCNApp(), @@ -80,27 +94,38 @@ void main() async { } void setFirebaseToken(String fcmToken) async { - final acc = UserAccount(); + final acc = AppAuth(); final oldToken = Globals().getPrefFCMToken(); - if (oldToken != null && oldToken == fcmToken && acc.client != null && acc.client!.fcmToken == fcmToken) { + await Globals().setPrefFCMToken(fcmToken); + + ApplicationLog.info('New firebase token received', additional: 'Token: $fcmToken (old: $oldToken)'); + + if (!acc.isAuth()) return; + + Client? client; + try { + client = await acc.loadClient(force: true); + } catch (exc, trace) { + ApplicationLog.error('Failed to get client: ' + exc.toString(), trace: trace); + return; + } + + if (oldToken != null && oldToken == fcmToken && client != null && client!.fcmToken == fcmToken) { ApplicationLog.info('Firebase token unchanged - do nothing', additional: 'Token: $fcmToken'); return; } - ApplicationLog.info('New firebase token received', additional: 'Token: $fcmToken (old: $oldToken)'); - - await Globals().setPrefFCMToken(fcmToken); - - if (acc.auth != null) { - if (acc.client == null) { - final client = await APIClient.addClient(acc.auth, fcmToken, Globals().deviceModel, Globals().version, Globals().hostname, Globals().clientType); - acc.setClient(client); - } else { - final client = await APIClient.updateClient(acc.auth, acc.client!.clientID, fcmToken, Globals().deviceModel, Globals().hostname, Globals().version); - acc.setClient(client); - } + if (client == null) { + // should not really happen - perhaps someone externally deleted the client? + final newClient = await APIClient.addClient(acc, fcmToken, Globals().deviceModel, Globals().version, Globals().hostname, Globals().clientType); + acc.setClientAndClientID(newClient); + await acc.save(); + } else { + final newClient = await APIClient.updateClient(acc, client.clientID, fcmToken, Globals().deviceModel, Globals().hostname, Globals().version); + acc.setClientAndClientID(newClient); + await acc.save(); } } diff --git a/flutter/lib/models/key_token_auth.dart b/flutter/lib/models/key_token_auth.dart deleted file mode 100644 index 4fb503b..0000000 --- a/flutter/lib/models/key_token_auth.dart +++ /dev/null @@ -1,11 +0,0 @@ -class KeyTokenAuth { - final String userId; - final String tokenAdmin; - final String tokenSend; - - KeyTokenAuth({ - required this.userId, - required this.tokenAdmin, - required this.tokenSend, - }); -} diff --git a/flutter/lib/nav_layout.dart b/flutter/lib/nav_layout.dart index 2e629c3..7399389 100644 --- a/flutter/lib/nav_layout.dart +++ b/flutter/lib/nav_layout.dart @@ -2,14 +2,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_lazy_indexed_stack/flutter_lazy_indexed_stack.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; +import 'package:simplecloudnotifier/components/hidable_fab/hidable_fab.dart'; import 'package:simplecloudnotifier/components/layout/app_bar.dart'; import 'package:simplecloudnotifier/pages/channel_list/channel_list.dart'; -import 'package:simplecloudnotifier/pages/send/root.dart'; +import 'package:simplecloudnotifier/pages/send/send.dart'; import 'package:simplecloudnotifier/components/bottom_fab/fab_bottom_app_bar.dart'; import 'package:simplecloudnotifier/pages/account/account.dart'; import 'package:simplecloudnotifier/pages/message_list/message_list.dart'; import 'package:simplecloudnotifier/pages/settings/root.dart'; -import 'package:simplecloudnotifier/state/user_account.dart'; +import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/utils/toaster.dart'; class SCNNavLayout extends StatefulWidget { @@ -24,15 +25,15 @@ class _SCNNavLayoutState extends State { @override initState() { - final userAcc = Provider.of(context, listen: false); - if (userAcc.auth == null) _selectedIndex = 2; + final userAcc = Provider.of(context, listen: false); + if (!userAcc.isAuth()) _selectedIndex = 2; super.initState(); } void _onItemTapped(int index) { - final userAcc = Provider.of(context, listen: false); - if (userAcc.auth == null) { + final userAcc = Provider.of(context, listen: false); + if (!userAcc.isAuth()) { Toaster.info("Not logged in", "Please login or create a new account first"); return; } @@ -43,8 +44,8 @@ class _SCNNavLayoutState extends State { } void _onFABTapped() { - final userAcc = Provider.of(context, listen: false); - if (userAcc.auth == null) { + final userAcc = Provider.of(context, listen: false); + if (!userAcc.isAuth()) { Toaster.info("Not logged in", "Please login or create a new account first"); return; } @@ -75,16 +76,10 @@ class _SCNNavLayoutState extends State { ), bottomNavigationBar: _buildNavBar(context), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, - floatingActionButton: _buildFAB(context), - ); - } - - Widget _buildFAB(BuildContext context) { - return FloatingActionButton( - onPressed: _onFABTapped, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(17))), - elevation: 2.0, - child: const Icon(FontAwesomeIcons.solidPaperPlaneTop), + floatingActionButton: HidableFAB( + onPressed: _onFABTapped, + icon: FontAwesomeIcons.solidPaperPlaneTop, + ), ); } diff --git a/flutter/lib/pages/account/account.dart b/flutter/lib/pages/account/account.dart index bf42c2c..46f298c 100644 --- a/flutter/lib/pages/account/account.dart +++ b/flutter/lib/pages/account/account.dart @@ -1,15 +1,13 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; -import 'package:simplecloudnotifier/models/key_token_auth.dart'; import 'package:simplecloudnotifier/models/user.dart'; import 'package:simplecloudnotifier/pages/account/login.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/globals.dart'; -import 'package:simplecloudnotifier/state/user_account.dart'; +import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/utils/toaster.dart'; class AccountRootPage extends StatefulWidget { @@ -26,7 +24,7 @@ class _AccountRootPageState extends State { late Future? futureChannelAllCount; late Future? futureChannelSubscribedCount; - late UserAccount userAcc; + late AppAuth userAcc; bool loading = false; @@ -34,7 +32,7 @@ class _AccountRootPageState extends State { void initState() { super.initState(); - userAcc = Provider.of(context, listen: false); + userAcc = Provider.of(context, listen: false); userAcc.addListener(_onAuthStateChanged); _onAuthStateChanged(); } @@ -52,34 +50,34 @@ class _AccountRootPageState extends State { futureChannelAllCount = null; futureChannelSubscribedCount = null; - if (userAcc.auth != null) { + if (userAcc.isAuth()) { futureChannelAllCount = () async { - if (userAcc.auth == null) throw new Exception('not logged in'); - final channels = await APIClient.getChannelList(userAcc.auth!, ChannelSelector.all); + if (!userAcc.isAuth()) throw new Exception('not logged in'); + final channels = await APIClient.getChannelList(userAcc, ChannelSelector.all); return channels.length; }(); futureChannelSubscribedCount = () async { - if (userAcc.auth == null) throw new Exception('not logged in'); - final channels = await APIClient.getChannelList(userAcc.auth!, ChannelSelector.subscribed); + if (!userAcc.isAuth()) throw new Exception('not logged in'); + final channels = await APIClient.getChannelList(userAcc, ChannelSelector.subscribed); return channels.length; }(); futureSubscriptionCount = () async { - if (userAcc.auth == null) throw new Exception('not logged in'); - final subs = await APIClient.getSubscriptionList(userAcc.auth!); + if (!userAcc.isAuth()) throw new Exception('not logged in'); + final subs = await APIClient.getSubscriptionList(userAcc); return subs.length; }(); futureClientCount = () async { - if (userAcc.auth == null) throw new Exception('not logged in'); - final clients = await APIClient.getClientList(userAcc.auth!); + if (!userAcc.isAuth()) throw new Exception('not logged in'); + final clients = await APIClient.getClientList(userAcc); return clients.length; }(); futureKeyCount = () async { - if (userAcc.auth == null) throw new Exception('not logged in'); - final keys = await APIClient.getKeyTokenList(userAcc.auth!); + if (!userAcc.isAuth()) throw new Exception('not logged in'); + final keys = await APIClient.getKeyTokenList(userAcc); return keys.length; }(); } @@ -87,13 +85,13 @@ class _AccountRootPageState extends State { @override Widget build(BuildContext context) { - return Consumer( + return Consumer( builder: (context, acc, child) { - if (acc.auth == null) { + if (!userAcc.isAuth()) { return _buildNoAuth(context); } else { return FutureBuilder( - future: acc.loadUser(false), + future: acc.loadUser(force: false), builder: ((context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasError) { @@ -165,7 +163,7 @@ class _AccountRootPageState extends State { ); } - Widget _buildShowAccount(BuildContext context, UserAccount acc, User user) { + Widget _buildShowAccount(BuildContext context, AppAuth acc, User user) { //TODO better layout return Column( children: [ @@ -446,7 +444,7 @@ class _AccountRootPageState extends State { void _createNewAccount() async { setState(() => loading = true); - final acc = Provider.of(context, listen: false); + final acc = Provider.of(context, listen: false); try { final notificationSettings = await FirebaseMessaging.instance.requestPermission(provisional: true); @@ -467,7 +465,7 @@ class _AccountRootPageState extends State { final user = await APIClient.createUserWithClient(null, fcmToken, Globals().platform, Globals().version, Globals().hostname, Globals().clientType); - acc.set(user.user, user.clients[0], KeyTokenAuth(userId: user.user.userID, tokenAdmin: user.adminKey, tokenSend: user.sendKey)); + acc.set(user.user, user.clients[0], user.adminKey, user.sendKey); await acc.save(); Toaster.success("Success", 'Successfully Created a new account'); @@ -480,7 +478,7 @@ class _AccountRootPageState extends State { } void _logout() async { - final acc = Provider.of(context, listen: false); + final acc = Provider.of(context, listen: false); //TODO clear messages/channels/etc in open views acc.clear(); diff --git a/flutter/lib/pages/account/login.dart b/flutter/lib/pages/account/login.dart index 90dba6e..3230708 100644 --- a/flutter/lib/pages/account/login.dart +++ b/flutter/lib/pages/account/login.dart @@ -4,10 +4,10 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart'; -import 'package:simplecloudnotifier/models/key_token_auth.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/globals.dart'; -import 'package:simplecloudnotifier/state/user_account.dart'; +import 'package:simplecloudnotifier/state/app_auth.dart'; +import 'package:simplecloudnotifier/state/token_source.dart'; import 'package:simplecloudnotifier/utils/toaster.dart'; class AccountLoginPage extends StatefulWidget { @@ -115,7 +115,7 @@ class _AccountLoginPageState extends State { } void _login() async { - final acc = Provider.of(context, listen: false); + final acc = Provider.of(context, listen: false); try { setState(() => loading = true); @@ -145,13 +145,11 @@ class _AccountLoginPageState extends State { return; } - final kta = KeyTokenAuth(userId: uid, tokenAdmin: atokv, tokenSend: stokv); + final user = await APIClient.getUser(DirectTokenSource(uid, atokv), uid); - final user = await APIClient.getUser(kta, uid); + final client = await APIClient.addClient(DirectTokenSource(uid, atokv), fcmToken, Globals().deviceModel, Globals().version, Globals().hostname, Globals().clientType); - final client = await APIClient.addClient(kta, fcmToken, Globals().deviceModel, Globals().version, Globals().hostname, Globals().clientType); - - acc.set(user, client, kta); + acc.set(user, client, atokv, stokv); await acc.save(); Toaster.success("Login", "Successfully logged in"); diff --git a/flutter/lib/pages/channel_list/channel_list.dart b/flutter/lib/pages/channel_list/channel_list.dart index e54170a..3c70919 100644 --- a/flutter/lib/pages/channel_list/channel_list.dart +++ b/flutter/lib/pages/channel_list/channel_list.dart @@ -4,7 +4,7 @@ import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; -import 'package:simplecloudnotifier/state/user_account.dart'; +import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/pages/channel_list/channel_list_item.dart'; class ChannelRootPage extends StatefulWidget { @@ -17,8 +17,6 @@ class ChannelRootPage extends StatefulWidget { class _ChannelRootPageState extends State { final PagingController _pagingController = PagingController(firstPageKey: 0); - late UserAccount userAcc; - @override void initState() { _pagingController.addPageRequestListener((pageKey) { @@ -34,15 +32,15 @@ class _ChannelRootPageState extends State { } Future _fetchPage(int pageKey) async { - final acc = Provider.of(context, listen: false); + final acc = Provider.of(context, listen: false); - if (acc.auth == null) { + if (!acc.isAuth()) { _pagingController.error = 'Not logged in'; return; } try { - final items = (await APIClient.getChannelList(acc.auth!, ChannelSelector.all)).map((p) => p.channel).toList(); + final items = (await APIClient.getChannelList(acc, ChannelSelector.all)).map((p) => p.channel).toList(); items.sort((a, b) => -1 * (a.timestampLastSent ?? '').compareTo(b.timestampLastSent ?? '')); diff --git a/flutter/lib/pages/channel_list/channel_list_item.dart b/flutter/lib/pages/channel_list/channel_list_item.dart index d5db1b0..a400015 100644 --- a/flutter/lib/pages/channel_list/channel_list_item.dart +++ b/flutter/lib/pages/channel_list/channel_list_item.dart @@ -4,7 +4,7 @@ import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/message.dart'; -import 'package:simplecloudnotifier/state/user_account.dart'; +import 'package:simplecloudnotifier/state/app_auth.dart'; class ChannelListItem extends StatefulWidget { static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm'); @@ -29,11 +29,11 @@ class _ChannelListItemState extends State { void initState() { super.initState(); - final acc = Provider.of(context, listen: false); + final acc = Provider.of(context, listen: false); - if (acc.auth != null) { + if (acc.isAuth()) { () async { - final (_, channelMessages) = await APIClient.getMessageList(acc.auth!, '@start', pageSize: 1, channelIDs: [widget.channel.channelID]); + final (_, channelMessages) = await APIClient.getMessageList(acc, '@start', pageSize: 1, channelIDs: [widget.channel.channelID]); setState(() { lastMessage = channelMessages.firstOrNull; }); diff --git a/flutter/lib/pages/message_list/message_list.dart b/flutter/lib/pages/message_list/message_list.dart index c7c8d4b..570a668 100644 --- a/flutter/lib/pages/message_list/message_list.dart +++ b/flutter/lib/pages/message_list/message_list.dart @@ -6,7 +6,7 @@ import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/message.dart'; import 'package:simplecloudnotifier/pages/message_view/message_view.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; -import 'package:simplecloudnotifier/state/user_account.dart'; +import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart'; class MessageListPage extends StatefulWidget { @@ -38,20 +38,20 @@ class _MessageListPageState extends State { } Future _fetchPage(String thisPageToken) async { - final acc = Provider.of(context, listen: false); + final acc = Provider.of(context, listen: false); - if (acc.auth == null) { + if (!acc.isAuth()) { _pagingController.error = 'Not logged in'; return; } try { if (_channels == null) { - final channels = await APIClient.getChannelList(acc.auth!, ChannelSelector.allAny); + final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny); _channels = {for (var v in channels) v.channel.channelID: v.channel}; } - final (npt, newItems) = await APIClient.getMessageList(acc.auth!, thisPageToken, pageSize: _pageSize); + final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: _pageSize); if (npt == '@end') { _pagingController.appendLastPage(newItems); diff --git a/flutter/lib/pages/message_view/message_view.dart b/flutter/lib/pages/message_view/message_view.dart index 1450bd0..c323b00 100644 --- a/flutter/lib/pages/message_view/message_view.dart +++ b/flutter/lib/pages/message_view/message_view.dart @@ -3,7 +3,7 @@ import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/models/message.dart'; -import 'package:simplecloudnotifier/state/user_account.dart'; +import 'package:simplecloudnotifier/state/app_auth.dart'; class MessageViewPage extends StatefulWidget { const MessageViewPage({super.key, required this.message}); @@ -24,9 +24,9 @@ class _MessageViewPageState extends State { } Future fetchMessage() async { - final acc = Provider.of(context, listen: false); + final acc = Provider.of(context, listen: false); - return await APIClient.getMessage(acc.auth!, widget.message.messageID); + return await APIClient.getMessage(acc, widget.message.messageID); } @override diff --git a/flutter/lib/pages/send/root.dart b/flutter/lib/pages/send/root.dart deleted file mode 100644 index fed2331..0000000 --- a/flutter/lib/pages/send/root.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:qr_flutter/qr_flutter.dart'; -import 'package:simplecloudnotifier/state/application_log.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:simplecloudnotifier/state/user_account.dart'; - -class SendRootPage extends StatefulWidget { - const SendRootPage({super.key}); - - @override - State createState() => _SendRootPageState(); -} - -class _SendRootPageState extends State { - late TextEditingController _msgTitle; - late TextEditingController _msgContent; - - @override - void initState() { - super.initState(); - _msgTitle = TextEditingController(); - _msgContent = TextEditingController(); - } - - @override - void dispose() { - _msgTitle.dispose(); - _msgContent.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, acc, child) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildQRCode(context, acc), - const SizedBox(height: 16), - FractionallySizedBox( - widthFactor: 1.0, - child: TextField( - controller: _msgTitle, - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: 'Title', - ), - ), - ), - const SizedBox(height: 16), - FractionallySizedBox( - widthFactor: 1.0, - child: TextField( - controller: _msgContent, - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: 'Text', - ), - ), - ), - const SizedBox(height: 16), - FilledButton( - style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), - onPressed: _send, - child: const Text('Send'), - ), - ], - ), - ); - }, - ); - } - - void _send() { - //... - } - - Widget _buildQRCode(BuildContext context, UserAccount acc) { - if (acc.auth == null) { - return const Placeholder(); - } - - if (acc.user == null) { - return FutureBuilder( - future: acc.loadUser(false), - builder: ((context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); //TODO better error display - } - var url = 'https://simplecloudnotifier.de?preset_user_id=${acc.user!.userID}&preset_user_key=${acc.auth!.tokenSend}'; - return GestureDetector( - onTap: () { - _openWeb(url); - }, - child: QrImageView( - data: url, - version: QrVersions.auto, - size: 400.0, - eyeStyle: QrEyeStyle( - eyeShape: QrEyeShape.square, - color: Theme.of(context).textTheme.bodyLarge?.color, - ), - dataModuleStyle: QrDataModuleStyle( - dataModuleShape: QrDataModuleShape.square, - color: Theme.of(context).textTheme.bodyLarge?.color, - ), - ), - ); - } - return const SizedBox( - width: 400.0, - height: 400.0, - child: Center(child: CircularProgressIndicator()), - ); - }), - ); - } - - var url = 'https://simplecloudnotifier.de?preset_user_id=${acc.user!.userID}&preset_user_key=${acc.auth!.tokenSend}'; - - return GestureDetector( - onTap: () { - _openWeb(url); - }, - child: QrImageView( - data: url, - version: QrVersions.auto, - size: 400.0, - eyeStyle: QrEyeStyle( - eyeShape: QrEyeShape.square, - color: Theme.of(context).textTheme.bodyLarge?.color, - ), - dataModuleStyle: QrDataModuleStyle( - dataModuleShape: QrDataModuleShape.square, - color: Theme.of(context).textTheme.bodyLarge?.color, - ), - ), - ); - } - - void _openWeb(String url) async { - try { - final Uri uri = Uri.parse(url); - - if (await canLaunchUrl(uri)) { - await launchUrl(uri); - } else { - // TODO ("Cannot open URL"); - } - } catch (exc, trace) { - ApplicationLog.error('Failed to open URL: ' + exc.toString(), additional: 'URL: ${url}', trace: trace); - } - } -} diff --git a/flutter/lib/pages/send/send.dart b/flutter/lib/pages/send/send.dart new file mode 100644 index 0000000..573c327 --- /dev/null +++ b/flutter/lib/pages/send/send.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:simplecloudnotifier/state/app_auth.dart'; + +class SendRootPage extends StatefulWidget { + const SendRootPage({super.key}); + + @override + State createState() => _SendRootPageState(); +} + +class _SendRootPageState extends State { + late TextEditingController _msgTitle; + late TextEditingController _msgContent; + + @override + void initState() { + super.initState(); + _msgTitle = TextEditingController(); + _msgContent = TextEditingController(); + } + + @override + void dispose() { + _msgTitle.dispose(); + _msgContent.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, acc, child) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildQRCode(context, acc), + const SizedBox(height: 16), + FractionallySizedBox( + widthFactor: 1.0, + child: TextField( + controller: _msgTitle, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Title', + ), + ), + ), + const SizedBox(height: 16), + FractionallySizedBox( + widthFactor: 1.0, + child: TextField( + controller: _msgContent, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Text', + ), + minLines: 2, + maxLines: null, + keyboardType: TextInputType.multiline, + ), + ), + const SizedBox(height: 16), + FilledButton( + style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), + onPressed: _send, + child: const Text('Send'), + ), + const SizedBox(height: 32), + ], + ), + ), + ); + }, + ); + } + + void _send() { + //... + } + + Widget _buildQRCode(BuildContext context, AppAuth acc) { + if (!acc.isAuth()) { + return const Placeholder(); + } + + return FutureBuilder( + future: acc.loadUser(force: false), + builder: ((context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); //TODO better error display + } + var url = (acc.tokenSend == null) ? 'https://simplecloudnotifier.de?preset_user_id=${acc.userID}' : 'https://simplecloudnotifier.de?preset_user_id=${acc.userID}&preset_user_key=${acc.tokenSend}'; + return GestureDetector( + onTap: () { + _openWeb(url); + }, + child: QrImageView( + data: url, + version: QrVersions.auto, + size: 300.0, + eyeStyle: QrEyeStyle( + eyeShape: QrEyeShape.square, + color: Theme.of(context).textTheme.bodyLarge?.color, + ), + dataModuleStyle: QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: Theme.of(context).textTheme.bodyLarge?.color, + ), + ), + ); + } + return const SizedBox( + width: 300.0, + height: 300.0, + child: Center(child: CircularProgressIndicator()), + ); + }), + ); + } + + void _openWeb(String url) async { + try { + final Uri uri = Uri.parse(url); + + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + // TODO ("Cannot open URL"); + } + } catch (exc, trace) { + ApplicationLog.error('Failed to open URL: ' + exc.toString(), additional: 'URL: ${url}', trace: trace); + } + } +} diff --git a/flutter/lib/state/app_auth.dart b/flutter/lib/state/app_auth.dart new file mode 100644 index 0000000..5000c91 --- /dev/null +++ b/flutter/lib/state/app_auth.dart @@ -0,0 +1,157 @@ +import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/api/api_exception.dart'; +import 'package:simplecloudnotifier/models/client.dart'; +import 'package:simplecloudnotifier/models/user.dart'; +import 'package:simplecloudnotifier/state/globals.dart'; +import 'package:simplecloudnotifier/state/token_source.dart'; + +class AppAuth extends ChangeNotifier implements TokenSource { + String? _clientID; + String? _userID; + String? _tokenAdmin; + String? _tokenSend; + + User? _user; + Client? _client; + + String? get userID => _userID; + String? get tokenAdmin => _tokenAdmin; + String? get tokenSend => _tokenSend; + + static AppAuth? _singleton = AppAuth._internal(); + + factory AppAuth() { + return _singleton ?? (_singleton = AppAuth._internal()); + } + + AppAuth._internal() { + load(); + } + + bool isAuth() { + return _userID != null && _tokenAdmin != null; + } + + void set(User user, Client client, String tokenAdmin, String tokenSend) { + _client = client; + _user = user; + _userID = user.userID; + _clientID = client.clientID; + _tokenAdmin = tokenAdmin; + _tokenSend = tokenSend; + notifyListeners(); + } + + void setClientAndClientID(Client client) { + _client = client; + _clientID = client.clientID; + notifyListeners(); + } + + void clear() { + _clientID = null; + _userID = null; + _tokenAdmin = null; + _tokenSend = null; + + _client = null; + _user = null; + + notifyListeners(); + } + + void load() { + final uid = Globals().sharedPrefs.getString('auth.userid'); + final cid = Globals().sharedPrefs.getString('auth.clientid'); + final toka = Globals().sharedPrefs.getString('auth.tokenadmin'); + final toks = Globals().sharedPrefs.getString('auth.tokensend'); + + if (uid == null || toka == null || toks == null || cid == null) { + clear(); + return; + } + + _clientID = cid; + _userID = uid; + _tokenAdmin = toka; + _tokenSend = toks; + + _client = null; + _user = null; + + notifyListeners(); + } + + Future save() async { + final prefs = await SharedPreferences.getInstance(); + if (_clientID == null || _userID == null || _tokenAdmin == null || _tokenSend == null) { + await prefs.remove('auth.userid'); + await prefs.remove('auth.tokenadmin'); + await prefs.remove('auth.tokensend'); + } else { + await prefs.setString('auth.userid', _userID!); + await prefs.setString('auth.clientid', _clientID!); + await prefs.setString('auth.tokenadmin', _tokenAdmin!); + await prefs.setString('auth.tokensend', _tokenSend!); + } + } + + Future loadUser({bool force = false}) async { + if (!force && _user != null && _user!.userID == _userID) { + return _user!; + } + + if (_userID == null || _tokenAdmin == null) { + throw Exception('Not authenticated'); + } + + final user = await APIClient.getUser(this, _userID!); + + _user = user; + notifyListeners(); + + await save(); + + return user; + } + + Future loadClient({bool force = false}) async { + if (!force && _client != null && _client!.clientID == _clientID) { + return _client!; + } + + if (_clientID == null || _tokenAdmin == null) { + throw Exception('Not authenticated'); + } + + try { + final client = await APIClient.getClient(this, _clientID!); + + _client = client; + notifyListeners(); + + await save(); + + return client; + } on APIException catch (_) { + _client = null; + notifyListeners(); + return null; + } catch (exc) { + _client = null; + rethrow; + } + } + + @override + String getToken() { + return _tokenAdmin!; + } + + @override + String getUserID() { + return _userID!; + } +} diff --git a/flutter/lib/state/token_source.dart b/flutter/lib/state/token_source.dart new file mode 100644 index 0000000..3d4fa8f --- /dev/null +++ b/flutter/lib/state/token_source.dart @@ -0,0 +1,21 @@ +abstract class TokenSource { + String getToken(); + String getUserID(); +} + +class DirectTokenSource implements TokenSource { + final String _userID; + final String _token; + + DirectTokenSource(this._userID, this._token); + + @override + String getUserID() { + return _userID; + } + + @override + String getToken() { + return _token; + } +} diff --git a/flutter/lib/state/user_account.dart b/flutter/lib/state/user_account.dart deleted file mode 100644 index 98805a5..0000000 --- a/flutter/lib/state/user_account.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:simplecloudnotifier/api/api_client.dart'; -import 'package:simplecloudnotifier/models/client.dart'; -import 'package:simplecloudnotifier/models/key_token_auth.dart'; -import 'package:simplecloudnotifier/models/user.dart'; -import 'package:simplecloudnotifier/state/globals.dart'; - -class UserAccount extends ChangeNotifier { - User? _user; - User? get user => _user; - - Client? _client; - Client? get client => _client; - - KeyTokenAuth? _auth; - KeyTokenAuth? get auth => _auth; - - static UserAccount? _singleton = UserAccount._internal(); - - factory UserAccount() { - return _singleton ?? (_singleton = UserAccount._internal()); - } - - UserAccount._internal() { - load(); - } - - void setToken(KeyTokenAuth auth) { - _auth = auth; - _user = null; - notifyListeners(); - } - - void clearToken() { - _auth = null; - _user = null; - notifyListeners(); - } - - void setUser(User user) { - _user = user; - notifyListeners(); - } - - void clearUser() { - _user = null; - notifyListeners(); - } - - void setClient(Client client) { - _client = client; - notifyListeners(); - } - - void clearClient() { - _client = null; - notifyListeners(); - } - - void set(User user, Client client, KeyTokenAuth auth) { - _client = client; - _user = user; - _auth = auth; - notifyListeners(); - } - - void clear() { - _client = null; - _user = null; - _auth = null; - notifyListeners(); - } - - void load() { - final uid = Globals().sharedPrefs.getString('auth.userid'); - final toka = Globals().sharedPrefs.getString('auth.tokenadmin'); - final toks = Globals().sharedPrefs.getString('auth.tokensend'); - - if (uid != null && toka != null && toks != null) { - setToken(KeyTokenAuth(userId: uid, tokenAdmin: toka, tokenSend: toks)); - } else { - clearToken(); - } - } - - Future save() async { - final prefs = await SharedPreferences.getInstance(); - if (_auth == null) { - await prefs.remove('auth.userid'); - await prefs.remove('auth.tokenadmin'); - await prefs.remove('auth.tokensend'); - } else { - await prefs.setString('auth.userid', _auth!.userId); - await prefs.setString('auth.tokenadmin', _auth!.tokenAdmin); - await prefs.setString('auth.tokensend', _auth!.tokenSend); - } - } - - Future loadUser(bool force) async { - if (!force && _user != null) { - return _user!; - } - - if (_auth == null) { - throw Exception('Not authenticated'); - } - - final user = await APIClient.getUser(_auth!, _auth!.userId); - - setUser(user); - - await save(); - - return user; - } -}