diff --git a/Backend/Api/Auth/Check.go b/Backend/Api/Auth/Check.go new file mode 100644 index 0000000..e503183 --- /dev/null +++ b/Backend/Api/Auth/Check.go @@ -0,0 +1,9 @@ +package Auth + +import ( + "net/http" +) + +func Check(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} diff --git a/Backend/Api/Routes.go b/Backend/Api/Routes.go index 7d528ed..5aee439 100644 --- a/Backend/Api/Routes.go +++ b/Backend/Api/Routes.go @@ -59,6 +59,8 @@ func InitApiEndpoints(router *mux.Router) { authApi = api.PathPrefix("/auth/").Subrouter() authApi.Use(authenticationMiddleware) + authApi.HandleFunc("/check", Auth.Check).Methods("GET") + // Define routes for friends and friend requests authApi.HandleFunc("/friend_requests", Friends.EncryptedFriendRequestList).Methods("GET") authApi.HandleFunc("/friend_request", Friends.CreateFriendRequest).Methods("POST") diff --git a/mobile/lib/models/messages.dart b/mobile/lib/models/messages.dart index 0cb0bef..ae0edac 100644 --- a/mobile/lib/models/messages.dart +++ b/mobile/lib/models/messages.dart @@ -1,6 +1,5 @@ import 'dart:convert'; import 'dart:typed_data'; -import 'package:uuid/uuid.dart'; import 'package:Envelope/models/conversation_users.dart'; import 'package:Envelope/models/conversations.dart'; import 'package:pointycastle/export.dart'; @@ -23,6 +22,7 @@ class Message { String senderUsername; String associationKey; String createdAt; + bool failedToSend; Message({ required this.id, required this.symmetricKey, @@ -32,6 +32,7 @@ class Message { required this.senderUsername, required this.associationKey, required this.createdAt, + required this.failedToSend, }); @@ -65,6 +66,7 @@ class Message { senderUsername: 'Unknown', associationKey: json['association_key'], createdAt: json['created_at'], + failedToSend: false, ); } @@ -151,6 +153,7 @@ class Message { 'sender_username': senderUsername, 'association_key': associationKey, 'created_at': createdAt, + 'failed_to_send': failedToSend ? 1 : 0, }; } @@ -179,6 +182,7 @@ Future> getMessagesForThread(Conversation conversation) async { senderUsername: maps[i]['sender_username'], associationKey: maps[i]['association_key'], createdAt: maps[i]['created_at'], + failedToSend: maps[i]['failed_to_send'] == 1, ); }); diff --git a/mobile/lib/utils/storage/conversations.dart b/mobile/lib/utils/storage/conversations.dart index 749bd67..fa4564e 100644 --- a/mobile/lib/utils/storage/conversations.dart +++ b/mobile/lib/utils/storage/conversations.dart @@ -13,63 +13,64 @@ import '/utils/encryption/aes_helper.dart'; Future updateConversations() async { RSAPrivateKey privKey = await getPrivateKey(); - var resp = await http.get( - Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'), - headers: { - 'cookie': await getSessionCookie(), - } - ); - - if (resp.statusCode != 200) { + try { + var resp = await http.get( + Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'), + headers: { + 'cookie': await getSessionCookie(), + } + ); + + if (resp.statusCode != 200) { throw Exception(resp.body); - } + } - List conversations = []; - List conversationsDetailIds = []; + List conversations = []; + List conversationsDetailIds = []; - List conversationsJson = jsonDecode(resp.body); + List conversationsJson = jsonDecode(resp.body); - for (var i = 0; i < conversationsJson.length; i++) { + for (var i = 0; i < conversationsJson.length; i++) { Conversation conversation = Conversation.fromJson( - conversationsJson[i] as Map, - privKey, + conversationsJson[i] as Map, + privKey, ); conversations.add(conversation); conversationsDetailIds.add(conversation.conversationDetailId); - } + } - Map params = {}; - params['conversation_detail_ids'] = conversationsDetailIds.join(','); - var uri = Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversation_details'); - uri = uri.replace(queryParameters: params); + Map params = {}; + params['conversation_detail_ids'] = conversationsDetailIds.join(','); + var uri = Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversation_details'); + uri = uri.replace(queryParameters: params); - resp = await http.get( - uri, - headers: { - 'cookie': await getSessionCookie(), - } - ); + resp = await http.get( + uri, + headers: { + 'cookie': await getSessionCookie(), + } + ); - if (resp.statusCode != 200) { + if (resp.statusCode != 200) { throw Exception(resp.body); - } + } - final db = await getDatabaseConnection(); + final db = await getDatabaseConnection(); - List conversationsDetailsJson = jsonDecode(resp.body); - for (var i = 0; i < conversationsDetailsJson.length; i++) { + List conversationsDetailsJson = jsonDecode(resp.body); + for (var i = 0; i < conversationsDetailsJson.length; i++) { var conversationDetailJson = conversationsDetailsJson[i] as Map; var conversation = findConversationByDetailId(conversations, conversationDetailJson['id']); conversation.name = AesHelper.aesDecrypt( - base64.decode(conversation.symmetricKey), - base64.decode(conversationDetailJson['name']), + base64.decode(conversation.symmetricKey), + base64.decode(conversationDetailJson['name']), ); await db.insert( - 'conversations', - conversation.toMap(), - conflictAlgorithm: ConflictAlgorithm.replace, + 'conversations', + conversation.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, ); List usersData = json.decode( @@ -80,16 +81,19 @@ Future updateConversations() async { ); for (var i = 0; i < usersData.length; i++) { - ConversationUser conversationUser = ConversationUser.fromJson( - usersData[i] as Map, - conversation.id, - ); - - await db.insert( - 'conversation_users', - conversationUser.toMap(), - conflictAlgorithm: ConflictAlgorithm.replace, - ); + ConversationUser conversationUser = ConversationUser.fromJson( + usersData[i] as Map, + conversation.id, + ); + + await db.insert( + 'conversation_users', + conversationUser.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); } + } + } catch (SocketException) { + return; } } diff --git a/mobile/lib/utils/storage/database.dart b/mobile/lib/utils/storage/database.dart index 41419e5..86e2a31 100644 --- a/mobile/lib/utils/storage/database.dart +++ b/mobile/lib/utils/storage/database.dart @@ -65,7 +65,8 @@ Future getDatabaseConnection() async { sender_id TEXT, sender_username TEXT, association_key TEXT, - created_at TEXT + created_at TEXT, + failed_to_send INTEGER ); '''); diff --git a/mobile/lib/utils/storage/friends.dart b/mobile/lib/utils/storage/friends.dart index a4fc827..f3347cb 100644 --- a/mobile/lib/utils/storage/friends.dart +++ b/mobile/lib/utils/storage/friends.dart @@ -10,17 +10,18 @@ import '/utils/storage/session_cookie.dart'; import '/utils/encryption/aes_helper.dart'; Future updateFriends() async { - RSAPrivateKey privKey = await getPrivateKey(); + RSAPrivateKey privKey = await getPrivateKey(); + try { var resp = await http.get( - Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/friend_requests'), - headers: { - 'cookie': await getSessionCookie(), - } + Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/friend_requests'), + headers: { + 'cookie': await getSessionCookie(), + } ); if (resp.statusCode != 200) { - throw Exception(resp.body); + throw Exception(resp.body); } List friends = []; @@ -29,14 +30,14 @@ Future updateFriends() async { List friendsRequestJson = jsonDecode(resp.body); for (var i = 0; i < friendsRequestJson.length; i++) { - friends.add( - Friend.fromJson( - friendsRequestJson[i] as Map, - privKey, - ) - ); - - friendIds.add(friends[i].friendId); + friends.add( + Friend.fromJson( + friendsRequestJson[i] as Map, + privKey, + ) + ); + + friendIds.add(friends[i].friendId); } Map params = {}; @@ -45,35 +46,39 @@ Future updateFriends() async { uri = uri.replace(queryParameters: params); resp = await http.get( - uri, - headers: { - 'cookie': await getSessionCookie(), - } + uri, + headers: { + 'cookie': await getSessionCookie(), + } ); if (resp.statusCode != 200) { - throw Exception(resp.body); + throw Exception(resp.body); } final db = await getDatabaseConnection(); List friendsJson = jsonDecode(resp.body); for (var i = 0; i < friendsJson.length; i++) { - var friendJson = friendsJson[i] as Map; - var friend = findFriendByFriendId(friends, friendJson['id']); + var friendJson = friendsJson[i] as Map; + var friend = findFriendByFriendId(friends, friendJson['id']); - friend.username = AesHelper.aesDecrypt( - base64.decode(friend.friendSymmetricKey), - base64.decode(friendJson['username']), - ); + friend.username = AesHelper.aesDecrypt( + base64.decode(friend.friendSymmetricKey), + base64.decode(friendJson['username']), + ); - friend.asymmetricPublicKey = friendJson['asymmetric_public_key']; + friend.asymmetricPublicKey = friendJson['asymmetric_public_key']; - await db.insert( - 'friends', - friend.toMap(), - conflictAlgorithm: ConflictAlgorithm.replace, - ); + await db.insert( + 'friends', + friend.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); } + + } catch (SocketException) { + return; + } } diff --git a/mobile/lib/utils/storage/messages.dart b/mobile/lib/utils/storage/messages.dart index 33c13e6..ea9457f 100644 --- a/mobile/lib/utils/storage/messages.dart +++ b/mobile/lib/utils/storage/messages.dart @@ -52,12 +52,16 @@ Future updateMessageThread(Conversation conversation, {RSAPrivateKey? priv } Future updateMessageThreads({List? conversations}) async { - RSAPrivateKey privKey = await getPrivateKey(); + try { + RSAPrivateKey privKey = await getPrivateKey(); - conversations ??= await getConversations(); + conversations ??= await getConversations(); - for (var i = 0; i < conversations.length; i++) { - await updateMessageThread(conversations[i], privKey: privKey); + for (var i = 0; i < conversations.length; i++) { + await updateMessageThread(conversations[i], privKey: privKey); + } + } catch(SocketException) { + return; } } @@ -74,7 +78,6 @@ Future sendMessage(Conversation conversation, String data) async { ConversationUser currentUser = await getConversationUserByUsername(conversation, username); - Message message = Message( id: messageDataId, symmetricKey: '', @@ -82,31 +85,44 @@ Future sendMessage(Conversation conversation, String data) async { senderId: userId, senderUsername: username, data: data, - createdAt: DateTime.now().toIso8601String(), associationKey: currentUser.associationKey, + createdAt: DateTime.now().toIso8601String(), + failedToSend: false, ); final db = await getDatabaseConnection(); - print(await db.query('messages')); - await db.insert( 'messages', message.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); - String messageJson = await message.toJson(conversation, messageDataId); - - final resp = await http.post( - Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/message'), - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - 'cookie': await getSessionCookie(), - }, - body: messageJson, - ); - - // TODO: If statusCode not successfull, mark as needing resend - print(resp.statusCode); + String sessionCookie = await getSessionCookie(); + + message.toJson(conversation, messageDataId) + .then((messageJson) { + return http.post( + Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/message'), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + 'cookie': sessionCookie, + }, + body: messageJson, + ); + }) + .then((resp) { + if (resp.statusCode != 200) { + throw Exception('Unable to send message'); + } + }) + .catchError((exception) { + message.failedToSend = true; + db.update( + 'messages', + message.toMap(), + where: 'id = ?', + whereArgs: [message.id], + ); + }); } diff --git a/mobile/lib/views/authentication/login.dart b/mobile/lib/views/authentication/login.dart index 9802423..7c60879 100644 --- a/mobile/lib/views/authentication/login.dart +++ b/mobile/lib/views/authentication/login.dart @@ -67,7 +67,7 @@ Future login(context, String username, String password) async { setPrivateKey(rsaPriv); final preferences = await SharedPreferences.getInstance(); - preferences.setBool('islogin', true); + preferences.setString('logged_in_at', (DateTime.now()).toIso8601String()); preferences.setString('userId', response.userId); preferences.setString('username', response.username); preferences.setString('asymmetricPublicKey', response.asymmetricPublicKey); diff --git a/mobile/lib/views/main/conversation_detail.dart b/mobile/lib/views/main/conversation_detail.dart index c3f0bb4..f00e84e 100644 --- a/mobile/lib/views/main/conversation_detail.dart +++ b/mobile/lib/views/main/conversation_detail.dart @@ -54,6 +54,32 @@ class _ConversationDetailState extends State { setState(() {}); } + Widget usernameOrFailedToSend(int index) { + if (messages[index].senderUsername != username) { + return Text(messages[index].senderUsername); + } + + if (messages[index].failedToSend) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + Icon( + Icons.warning_rounded, + color: Colors.red, + size: 20, + ), + Text( + 'Failed to send', + style: TextStyle(color: Colors.red, fontSize: 12), + textAlign: TextAlign.right, + ), + ], + ); + } + + return const SizedBox.shrink(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -102,7 +128,7 @@ class _ConversationDetailState extends State { reverse: true, itemBuilder: (context, index) { return Container( - padding: const EdgeInsets.only(left: 14,right: 14,top: 0,bottom: 0), + padding: const EdgeInsets.only(left: 14,right: 14,top: 0,bottom: 10), child: Align( alignment: ( messages[index].senderUsername == username ? @@ -126,9 +152,7 @@ class _ConversationDetailState extends State { padding: const EdgeInsets.all(12), child: Text(messages[index].data, style: const TextStyle(fontSize: 15)), ), - messages[index].senderUsername != username ? - Text(messages[index].senderUsername) : - const SizedBox.shrink(), + usernameOrFailedToSend(index), Text( convertToAgo(messages[index].createdAt), textAlign: TextAlign.left, diff --git a/mobile/lib/views/main/home.dart b/mobile/lib/views/main/home.dart index 06c6295..f7a0d6d 100644 --- a/mobile/lib/views/main/home.dart +++ b/mobile/lib/views/main/home.dart @@ -1,11 +1,14 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:http/http.dart' as http; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import '/views/main/conversation_list.dart'; import '/views/main/friend_list.dart'; import '/views/main/profile.dart'; import '/utils/storage/friends.dart'; import '/utils/storage/conversations.dart'; import '/utils/storage/messages.dart'; +import '/utils/storage/session_cookie.dart'; import '/models/conversations.dart'; import '/models/friends.dart'; @@ -53,12 +56,46 @@ class _HomeState extends State { }); } - // TODO: Do server GET check here - Future checkLogin() async { + Future checkLogin() async { SharedPreferences preferences = await SharedPreferences.getInstance(); - if (preferences.getBool('islogin') != true) { + + var loggedInTime = preferences.getString('logged_in_at'); + if (loggedInTime == null) { + preferences.remove('logged_in_at'); + preferences.remove('username'); + preferences.remove('userId'); + preferences.remove('asymmetricPublicKey'); Navigator.pushNamedAndRemoveUntil(context, '/landing', ModalRoute.withName('/landing')); + return; + } + + DateTime loggedInAt = DateTime.parse(loggedInTime); + bool isAfter = loggedInAt.isAfter((DateTime.now()).add(const Duration(hours: 12))); + int statusCode = 200; + + try { + var resp = await http.get( + Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/check'), + headers: { + 'cookie': await getSessionCookie(), + } + ); + statusCode = resp.statusCode; + } catch(SocketException) { + if (!isAfter) { + return; + } } + + if (!isAfter && statusCode == 200) { + return; + } + + preferences.remove('logged_in_at'); + preferences.remove('username'); + preferences.remove('userId'); + preferences.remove('asymmetricPublicKey'); + Navigator.pushNamedAndRemoveUntil(context, '/landing', ModalRoute.withName('/landing')); } void _onItemTapped(int index) { diff --git a/mobile/lib/views/main/profile.dart b/mobile/lib/views/main/profile.dart index 64d65ff..ccc84d5 100644 --- a/mobile/lib/views/main/profile.dart +++ b/mobile/lib/views/main/profile.dart @@ -37,7 +37,9 @@ class _ProfileState extends State { onTap: () async { deleteDb(); final preferences = await SharedPreferences.getInstance(); - preferences.setBool('islogin', false); + preferences.remove('logged_in_at'); + preferences.remove('username'); + preferences.remove('userId'); preferences.remove(rsaPrivateKeyName); Navigator.pushNamedAndRemoveUntil(context, '/landing', ModalRoute.withName('/landing')); },