diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml index abc9cf8..db8477b 100644 --- a/flutter/android/app/src/main/AndroidManifest.xml +++ b/flutter/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + + + ); RequestLog.addRequestSuccess(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders); + print('[REQUEST|FIN] [${method}] ${name}'); return result; } else { RequestLog.addRequestSuccess(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders); + print('[REQUEST|FIN] [${method}] ${name}'); return null as T; } } catch (exc, trace) { @@ -120,7 +124,7 @@ class APIClient { method: 'GET', relURL: '/users/$uid', fn: null, - auth: KeyTokenAuth(userId: uid, token: tok), + auth: KeyTokenAuth(userId: uid, tokenAdmin: tok, tokenSend: ''), ); return true; } catch (e) { @@ -138,7 +142,7 @@ class APIClient { ); } - static Future addClient(KeyTokenAuth? auth, String fcmToken, String agentModel, String agentVersion, String clientType) async { + static Future addClient(KeyTokenAuth? auth, String fcmToken, String agentModel, String agentVersion, String? descriptionName, String clientType) async { return await _request( name: 'addClient', method: 'POST', @@ -148,13 +152,14 @@ class APIClient { 'agent_model': agentModel, 'agent_version': agentVersion, 'client_type': clientType, + 'description_name': descriptionName, }, fn: Client.fromJson, auth: auth, ); } - static Future updateClient(KeyTokenAuth? auth, String clientID, String fcmToken, String agentModel, String agentVersion) async { + static Future updateClient(KeyTokenAuth? auth, String clientID, String fcmToken, String agentModel, String? descriptionName, String agentVersion) async { return await _request( name: 'updateClient', method: 'PUT', @@ -163,6 +168,7 @@ class APIClient { 'fcm_token': fcmToken, 'agent_model': agentModel, 'agent_version': agentVersion, + 'description_name': descriptionName, }, fn: Client.fromJson, auth: auth, @@ -235,4 +241,22 @@ class APIClient { auth: auth, ); } + + static Future createUserWithClient(String? username, String clientFcmToken, String clientAgentModel, String clientAgentVersion, String? clientDescriptionName, String clientType) async { + return await _request( + name: 'createUserWithClient', + method: 'POST', + relURL: 'users', + jsonBody: { + 'username': username, + 'fcm_token': clientFcmToken, + 'agent_model': clientAgentModel, + 'agent_version': clientAgentVersion, + 'description_name': clientDescriptionName, + 'client_type': clientType, + 'no_client': false, + }, + fn: UserWithClientsAndKeys.fromJson, + ); + } } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index dab5914..32bbca4 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -14,6 +14,8 @@ import 'package:toastification/toastification.dart'; import 'firebase_options.dart'; void main() async { + print('[INIT] Application starting...'); + WidgetsFlutterBinding.ensureInitialized(); await Hive.initFlutter(); @@ -43,7 +45,7 @@ void main() async { await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - final notificationSettings = await FirebaseMessaging.instance.requestPermission(provisional: true); + await FirebaseMessaging.instance.requestPermission(provisional: true); FirebaseMessaging.instance.onTokenRefresh.listen((fcmToken) { try { @@ -55,6 +57,15 @@ void main() async { ApplicationLog.error('Failed to listen to token refresh events: ' + (err?.toString() ?? '')); }); + try { + final fcmToken = await FirebaseMessaging.instance.getToken(); + if (fcmToken != null) { + setFirebaseToken(fcmToken); + } + } catch (exc, trace) { + ApplicationLog.error('Failed to get+set firebase token: ' + exc.toString(), trace: trace); + } + ApplicationLog.debug('Application started'); runApp( @@ -69,14 +80,25 @@ void main() async { } void setFirebaseToken(String fcmToken) async { - ApplicationLog.info('New firebase token: $fcmToken'); final acc = UserAccount(); + + final oldToken = Globals().getPrefFCMToken(); + + if (oldToken != null && oldToken == fcmToken && acc.client != null && acc.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().platform, Globals().version, Globals().clientType); + 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().platform, Globals().version); + final client = await APIClient.updateClient(acc.auth, acc.client!.clientID, fcmToken, Globals().deviceModel, Globals().hostname, Globals().version); acc.setClient(client); } } diff --git a/flutter/lib/models/channel.dart b/flutter/lib/models/channel.dart index 337e19d..51b40c1 100644 --- a/flutter/lib/models/channel.dart +++ b/flutter/lib/models/channel.dart @@ -38,33 +38,18 @@ class Channel { } } -class ChannelWithSubscription extends Channel { +class ChannelWithSubscription { + final Channel channel; final Subscription subscription; ChannelWithSubscription({ - required super.channelID, - required super.ownerUserID, - required super.internalName, - required super.displayName, - required super.descriptionName, - required super.subscribeKey, - required super.timestampCreated, - required super.timestampLastSent, - required super.messagesSent, + required this.channel, required this.subscription, }); factory ChannelWithSubscription.fromJson(Map json) { return ChannelWithSubscription( - channelID: json['channel_id'] as String, - ownerUserID: json['owner_user_id'] as String, - internalName: json['internal_name'] as String, - displayName: json['display_name'] as String, - descriptionName: json['description_name'] as String?, - subscribeKey: json['subscribe_key'] as String?, - timestampCreated: json['timestamp_created'] as String, - timestampLastSent: json['timestamp_lastsent'] as String?, - messagesSent: json['messages_sent'] as int, + channel: Channel.fromJson(json), subscription: Subscription.fromJson(json['subscription'] as Map), ); } diff --git a/flutter/lib/models/client.dart b/flutter/lib/models/client.dart index 895d90c..c448ee2 100644 --- a/flutter/lib/models/client.dart +++ b/flutter/lib/models/client.dart @@ -28,7 +28,7 @@ class Client { timestampCreated: json['timestamp_created'] as String, agentModel: json['agent_model'] as String, agentVersion: json['agent_version'] as String, - descriptionName: json['description_name'] as String?, + descriptionName: json.containsKey('description_name') ? (json['description_name'] as String?) : null, //TODO change once API is updated / branch is merged ); } diff --git a/flutter/lib/models/key_token_auth.dart b/flutter/lib/models/key_token_auth.dart index a0be225..4fb503b 100644 --- a/flutter/lib/models/key_token_auth.dart +++ b/flutter/lib/models/key_token_auth.dart @@ -1,6 +1,11 @@ class KeyTokenAuth { final String userId; - final String token; + final String tokenAdmin; + final String tokenSend; - KeyTokenAuth({required this.userId, required this.token}); + KeyTokenAuth({ + required this.userId, + required this.tokenAdmin, + required this.tokenSend, + }); } diff --git a/flutter/lib/models/user.dart b/flutter/lib/models/user.dart index c8f4eb8..4a9b140 100644 --- a/flutter/lib/models/user.dart +++ b/flutter/lib/models/user.dart @@ -1,3 +1,5 @@ +import 'package:simplecloudnotifier/models/client.dart'; + class User { final String userID; final String? username; @@ -62,3 +64,29 @@ class User { ); } } + +class UserWithClientsAndKeys { + final User user; + final List clients; + final String sendKey; + final String readKey; + final String adminKey; + + UserWithClientsAndKeys({ + required this.user, + required this.clients, + required this.sendKey, + required this.readKey, + required this.adminKey, + }); + + factory UserWithClientsAndKeys.fromJson(Map json) { + return UserWithClientsAndKeys( + user: User.fromJson(json), + clients: Client.fromJsonArray(json['clients'] as List), + sendKey: json['send_key'] as String, + readKey: json['read_key'] as String, + adminKey: json['admin_key'] as String, + ); + } +} diff --git a/flutter/lib/pages/account/account.dart b/flutter/lib/pages/account/account.dart index 903933e..130c47a 100644 --- a/flutter/lib/pages/account/account.dart +++ b/flutter/lib/pages/account/account.dart @@ -1,10 +1,16 @@ +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/utils/toaster.dart'; class AccountRootPage extends StatefulWidget { const AccountRootPage({super.key}); @@ -22,6 +28,8 @@ class _AccountRootPageState extends State { late UserAccount userAcc; + bool loading = false; + @override void initState() { super.initState(); @@ -102,25 +110,54 @@ class _AccountRootPageState extends State { } Widget buildNoAuth(BuildContext context) { - return Center( + return Padding( + padding: const EdgeInsets.fromLTRB(24, 32, 24, 16), child: Column( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - ElevatedButton( - style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), - onPressed: () { - //TODO - }, - child: const Text('Use existing account'), - ), + if (!loading) + Center( + child: Container( + width: 200, + height: 200, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + borderRadius: BorderRadius.circular(100), + ), + child: Center(child: FaIcon(FontAwesomeIcons.userSecret, size: 96, color: Theme.of(context).colorScheme.onSecondary)), + ), + ), + if (loading) + Center( + child: Container( + width: 200, + height: 200, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + borderRadius: BorderRadius.circular(100), + ), + child: Center(child: CircularProgressIndicator(color: Theme.of(context).colorScheme.onSecondary)), + ), + ), const SizedBox(height: 32), - ElevatedButton( - style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), + FilledButton( + style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 24), padding: const EdgeInsets.fromLTRB(8, 12, 8, 12)), onPressed: () { - //TODO + if (loading) return; + createNewAccount(); }, child: const Text('Create new account'), ), + const SizedBox(height: 16), + FilledButton.tonal( + style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 24), padding: const EdgeInsets.fromLTRB(8, 12, 8, 12)), + onPressed: () { + if (loading) return; + Navigator.push(context, MaterialPageRoute(builder: (context) => AccountLoginPage())); + }, + child: const Text('Use existing account'), + ), ], ), ); @@ -391,4 +428,40 @@ class _AccountRootPageState extends State { ), ); } + + void createNewAccount() async { + setState(() => loading = true); + + final acc = Provider.of(context, listen: false); + + try { + final notificationSettings = await FirebaseMessaging.instance.requestPermission(provisional: true); + + if (notificationSettings.authorizationStatus == AuthorizationStatus.denied) { + Toaster.error("Missing Permission", 'Please allow notifications to create an account'); + return; + } + + final fcmToken = await FirebaseMessaging.instance.getToken(); + + if (fcmToken == null) { + Toaster.warn("Missing Token", 'No FCM Token found, please allow notifications, ensure you have a network connection and restart the app'); + return; + } + + await Globals().setPrefFCMToken(fcmToken); + + final user = await APIClient.createUserWithClient(null, fcmToken, Globals().platform, Globals().version, Globals().hostname, Globals().clientType); + + acc.setUser(user.user); + acc.setToken(KeyTokenAuth(userId: user.user.userID, tokenAdmin: user.adminKey, tokenSend: user.sendKey)); + acc.setClient(user.clients[0]); + await acc.save(); + } catch (exc, trace) { + ApplicationLog.error('Failed to create user account: ' + exc.toString(), trace: trace); + Toaster.error("Error", 'Failed to create user account'); + } finally { + setState(() => loading = false); + } + } } diff --git a/flutter/lib/pages/account/login.dart b/flutter/lib/pages/account/login.dart index 3ed8361..7ce7795 100644 --- a/flutter/lib/pages/account/login.dart +++ b/flutter/lib/pages/account/login.dart @@ -54,8 +54,8 @@ class _AccountLoginPageState extends State { ), ), const SizedBox(height: 16), - ElevatedButton( - style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), + FilledButton( + style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), onPressed: _login, child: const Text('Login'), ), @@ -66,7 +66,7 @@ class _AccountLoginPageState extends State { void _login() async { final msgr = ScaffoldMessenger.of(context); - final prov = Provider.of(context, listen: false); + final acc = Provider.of(context, listen: false); try { final uid = _ctrlUserID.text; @@ -79,8 +79,8 @@ class _AccountLoginPageState extends State { content: Text('Data ok'), //TODO use toast? ), ); - prov.setToken(KeyTokenAuth(userId: uid, token: tok)); - await prov.save(); + acc.setToken(KeyTokenAuth(userId: uid, tokenAdmin: tok, tokenSend: '')); //TOTO send token + await acc.save(); widget.onLogin?.call(); } else { msgr.showSnackBar( diff --git a/flutter/lib/pages/channel_list/channel_list.dart b/flutter/lib/pages/channel_list/channel_list.dart index 677b524..e54170a 100644 --- a/flutter/lib/pages/channel_list/channel_list.dart +++ b/flutter/lib/pages/channel_list/channel_list.dart @@ -42,7 +42,7 @@ class _ChannelRootPageState extends State { } try { - final items = await APIClient.getChannelList(acc.auth!, ChannelSelector.all); + final items = (await APIClient.getChannelList(acc.auth!, ChannelSelector.all)).map((p) => p.channel).toList(); items.sort((a, b) => -1 * (a.timestampLastSent ?? '').compareTo(b.timestampLastSent ?? '')); diff --git a/flutter/lib/pages/debug/debug_actions.dart b/flutter/lib/pages/debug/debug_actions.dart index b4947d5..5573308 100644 --- a/flutter/lib/pages/debug/debug_actions.dart +++ b/flutter/lib/pages/debug/debug_actions.dart @@ -17,28 +17,28 @@ class _DebugActionsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - ElevatedButton( - style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), + FilledButton( + style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), onPressed: () => Toaster.success("Hello World", "This was a triumph!"), child: const Text('Show Success Notification'), ), - ElevatedButton( - style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), + FilledButton( + style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), onPressed: () => Toaster.info("Hello World", "This was a triumph!"), child: const Text('Show Info Notification'), ), - ElevatedButton( - style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), + FilledButton( + style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), onPressed: () => Toaster.warn("Hello World", "This was a triumph!"), child: const Text('Show Warn Notification'), ), - ElevatedButton( - style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), + FilledButton( + style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), onPressed: () => Toaster.error("Hello World", "This was a triumph!"), child: const Text('Show Info Notification'), ), - ElevatedButton( - style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), + FilledButton( + style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), onPressed: () => Toaster.simple("Hello World"), child: const Text('Show Simple Notification'), ), diff --git a/flutter/lib/pages/message_list/message_list.dart b/flutter/lib/pages/message_list/message_list.dart index 75d219a..c7c8d4b 100644 --- a/flutter/lib/pages/message_list/message_list.dart +++ b/flutter/lib/pages/message_list/message_list.dart @@ -48,7 +48,7 @@ class _MessageListPageState extends State { try { if (_channels == null) { final channels = await APIClient.getChannelList(acc.auth!, ChannelSelector.allAny); - _channels = {for (var v in channels) v.channelID: v}; + _channels = {for (var v in channels) v.channel.channelID: v.channel}; } final (npt, newItems) = await APIClient.getMessageList(acc.auth!, thisPageToken, pageSize: _pageSize); diff --git a/flutter/lib/pages/send/root.dart b/flutter/lib/pages/send/root.dart index 14f8894..fed2331 100644 --- a/flutter/lib/pages/send/root.dart +++ b/flutter/lib/pages/send/root.dart @@ -63,8 +63,8 @@ class _SendRootPageState extends State { ), ), const SizedBox(height: 16), - ElevatedButton( - style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), + FilledButton( + style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), onPressed: _send, child: const Text('Send'), ), @@ -92,7 +92,7 @@ class _SendRootPageState extends State { if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); //TODO better error display } - var url = 'https://simplecloudnotifier.com?preset_user_id=${acc.user!.userID}&preset_user_key=TODO'; // TODO get send-only key + var url = 'https://simplecloudnotifier.de?preset_user_id=${acc.user!.userID}&preset_user_key=${acc.auth!.tokenSend}'; return GestureDetector( onTap: () { _openWeb(url); @@ -121,7 +121,7 @@ class _SendRootPageState extends State { ); } - var url = 'https://simplecloudnotifier.com?preset_user_id=${acc.user!.userID}&preset_user_key=TODO'; // TODO get send-only key + var url = 'https://simplecloudnotifier.de?preset_user_id=${acc.user!.userID}&preset_user_key=${acc.auth!.tokenSend}'; return GestureDetector( onTap: () { diff --git a/flutter/lib/state/globals.dart b/flutter/lib/state/globals.dart index 063ef81..572b16c 100644 --- a/flutter/lib/state/globals.dart +++ b/flutter/lib/state/globals.dart @@ -55,4 +55,12 @@ class Globals { this.sharedPrefs = await SharedPreferences.getInstance(); } + + String? getPrefFCMToken() { + return sharedPrefs.getString("fcm.token"); + } + + Future setPrefFCMToken(String value) { + return sharedPrefs.setString("fcm.token", value); + } } diff --git a/flutter/lib/state/user_account.dart b/flutter/lib/state/user_account.dart index 8cf8c26..3255e4b 100644 --- a/flutter/lib/state/user_account.dart +++ b/flutter/lib/state/user_account.dart @@ -60,10 +60,11 @@ class UserAccount extends ChangeNotifier { void load() { final uid = Globals().sharedPrefs.getString('auth.userid'); - final tok = Globals().sharedPrefs.getString('auth.token'); + final toka = Globals().sharedPrefs.getString('auth.tokenadmin'); + final toks = Globals().sharedPrefs.getString('auth.tokensend'); - if (uid != null && tok != null) { - setToken(KeyTokenAuth(userId: uid, token: tok)); + if (uid != null && toka != null && toks != null) { + setToken(KeyTokenAuth(userId: uid, tokenAdmin: toka, tokenSend: toks)); } else { clearToken(); } @@ -73,10 +74,12 @@ class UserAccount extends ChangeNotifier { final prefs = await SharedPreferences.getInstance(); if (_auth == null) { await prefs.remove('auth.userid'); - await prefs.remove('auth.token'); + await prefs.remove('auth.tokenadmin'); + await prefs.remove('auth.tokensend'); } else { await prefs.setString('auth.userid', _auth!.userId); - await prefs.setString('auth.token', _auth!.token); + await prefs.setString('auth.tokenadmin', _auth!.tokenAdmin); + await prefs.setString('auth.tokensend', _auth!.tokenSend); } }