From d8535fdfc7248e0000348ab70011ac1fb262ed99 Mon Sep 17 00:00:00 2001 From: Tovi Jaeschke-Rogers Date: Sun, 26 Jun 2022 19:21:42 +0930 Subject: [PATCH] Message conversations syncing to device --- Backend/Api/Auth/Login.go | 14 +- Backend/Api/Messages/MessageThread.go | 19 +- Backend/Api/Routes.go | 2 +- Backend/Database/Messages.go | 13 + Backend/Database/Seeder/MessageSeeder.go | 12 +- Backend/Models/Messages.go | 7 +- mobile/lib/models/conversations.dart | 1 - mobile/lib/models/friends.dart | 170 +++++---- mobile/lib/models/messages.dart | 116 +++++- mobile/lib/utils/storage/database.dart | 21 +- mobile/lib/utils/storage/friends.dart | 4 +- mobile/lib/utils/storage/messages.dart | 76 ++++ mobile/lib/views/authentication/login.dart | 349 +++++++++--------- .../unauthenticated_landing.dart | 142 +++---- .../lib/views/main/conversation_detail.dart | 259 ++++++++----- mobile/lib/views/main/conversation_list.dart | 1 + .../views/main/conversation_list_item.dart | 1 + mobile/lib/views/main/home.dart | 2 + mobile/lib/views/main/profile.dart | 2 + 19 files changed, 751 insertions(+), 460 deletions(-) create mode 100644 mobile/lib/utils/storage/messages.dart diff --git a/Backend/Api/Auth/Login.go b/Backend/Api/Auth/Login.go index 39c374f..b91d133 100644 --- a/Backend/Api/Auth/Login.go +++ b/Backend/Api/Auth/Login.go @@ -19,9 +19,10 @@ type loginResponse struct { Message string `json:"message"` AsymmetricPublicKey string `json:"asymmetric_public_key"` AsymmetricPrivateKey string `json:"asymmetric_private_key"` + UserID string `json:"user_id"` } -func makeLoginResponse(w http.ResponseWriter, code int, message, pubKey, privKey string) { +func makeLoginResponse(w http.ResponseWriter, code int, message, pubKey, privKey string, userId string) { var ( status string = "error" returnJson []byte @@ -36,6 +37,7 @@ func makeLoginResponse(w http.ResponseWriter, code int, message, pubKey, privKey Message: message, AsymmetricPublicKey: pubKey, AsymmetricPrivateKey: privKey, + UserID: userId, }, "", " ") if err != nil { http.Error(w, "Error", http.StatusInternalServerError) @@ -59,21 +61,22 @@ func Login(w http.ResponseWriter, r *http.Request) { err = json.NewDecoder(r.Body).Decode(&creds) if err != nil { - makeLoginResponse(w, http.StatusInternalServerError, "An error occurred", "", "") + makeLoginResponse(w, http.StatusInternalServerError, "An error occurred", "", "", "") return } userData, err = Database.GetUserByUsername(creds.Username) if err != nil { - makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "") + makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "", "") return } if !CheckPasswordHash(creds.Password, userData.Password) { - makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "") + makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "", "") return } + // TODO: Revisit before production expiresAt = time.Now().Add(12 * time.Hour) session = Models.Session{ @@ -83,7 +86,7 @@ func Login(w http.ResponseWriter, r *http.Request) { err = Database.CreateSession(&session) if err != nil { - makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "") + makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "", "") return } @@ -99,5 +102,6 @@ func Login(w http.ResponseWriter, r *http.Request) { "Successfully logged in", userData.AsymmetricPublicKey, userData.AsymmetricPrivateKey, + userData.ID.String(), ) } diff --git a/Backend/Api/Messages/MessageThread.go b/Backend/Api/Messages/MessageThread.go index 3fef1d3..7eb3b27 100644 --- a/Backend/Api/Messages/MessageThread.go +++ b/Backend/Api/Messages/MessageThread.go @@ -6,17 +6,18 @@ import ( "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" + "github.com/gorilla/mux" ) -func ConversationDetail(w http.ResponseWriter, r *http.Request) { +func Messages(w http.ResponseWriter, r *http.Request) { var ( - conversationDetail Models.ConversationDetail - urlVars map[string]string - threadKey string - returnJson []byte - ok bool - err error + messages []Models.Message + urlVars map[string]string + threadKey string + returnJson []byte + ok bool + err error ) urlVars = mux.Vars(r) @@ -26,13 +27,13 @@ func ConversationDetail(w http.ResponseWriter, r *http.Request) { return } - conversationDetail, err = Database.GetConversationDetailById(threadKey) + messages, err = Database.GetMessagesByThreadKey(threadKey) if !ok { http.Error(w, "Not Found", http.StatusNotFound) return } - returnJson, err = json.MarshalIndent(conversationDetail, "", " ") + returnJson, err = json.MarshalIndent(messages, "", " ") if err != nil { http.Error(w, "Error", http.StatusInternalServerError) return diff --git a/Backend/Api/Routes.go b/Backend/Api/Routes.go index d4c2cdb..651578e 100644 --- a/Backend/Api/Routes.go +++ b/Backend/Api/Routes.go @@ -72,5 +72,5 @@ func InitApiEndpoints(router *mux.Router) { // authApi.HandleFunc("/user/{userID}/request", Friends.FriendRequest).Methods("POST") // Define routes for messages - authApi.HandleFunc("/messages/{threadKey}", Messages.ConversationDetail).Methods("GET") + authApi.HandleFunc("/messages/{threadKey}", Messages.Messages).Methods("GET") } diff --git a/Backend/Database/Messages.go b/Backend/Database/Messages.go index 866cc40..4c1c352 100644 --- a/Backend/Database/Messages.go +++ b/Backend/Database/Messages.go @@ -20,6 +20,19 @@ func GetMessageById(id string) (Models.Message, error) { return message, err } +func GetMessagesByThreadKey(threadKey string) ([]Models.Message, error) { + var ( + messages []Models.Message + err error + ) + + err = DB.Preload(clause.Associations). + Find(&messages, "message_thread_key = ?", threadKey). + Error + + return messages, err +} + func CreateMessage(message *Models.Message) error { var ( err error diff --git a/Backend/Database/Seeder/MessageSeeder.go b/Backend/Database/Seeder/MessageSeeder.go index 06cd78e..aba744a 100644 --- a/Backend/Database/Seeder/MessageSeeder.go +++ b/Backend/Database/Seeder/MessageSeeder.go @@ -11,7 +11,7 @@ import ( ) func seedMessage( - primaryUser Models.User, + primaryUser, secondaryUser Models.User, primaryUserThreadKey, secondaryUserThreadKey string, thread Models.ConversationDetail, i int, @@ -38,11 +38,18 @@ func seedMessage( panic(err) } - senderIdCiphertext, err = key.aesEncrypt(primaryUser.ID.Bytes()) + senderIdCiphertext, err = key.aesEncrypt([]byte(primaryUser.ID.String())) if err != nil { panic(err) } + if i%2 == 0 { + senderIdCiphertext, err = key.aesEncrypt([]byte(secondaryUser.ID.String())) + if err != nil { + panic(err) + } + } + messageData = Models.MessageData{ Data: base64.StdEncoding.EncodeToString(dataCiphertext), SenderID: base64.StdEncoding.EncodeToString(senderIdCiphertext), @@ -240,6 +247,7 @@ func SeedMessages() { for i = 0; i <= 20; i++ { err = seedMessage( primaryUser, + secondaryUser, primaryUserThreadKey, secondaryUserThreadKey, thread, diff --git a/Backend/Models/Messages.go b/Backend/Models/Messages.go index 6c3d5a3..d9f309c 100644 --- a/Backend/Models/Messages.go +++ b/Backend/Models/Messages.go @@ -1,6 +1,10 @@ package Models -import "github.com/gofrs/uuid" +import ( + "time" + + "github.com/gofrs/uuid" +) // TODO: Add support for images type MessageData struct { @@ -15,6 +19,7 @@ type Message struct { MessageData MessageData `json:"message_data"` SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted MessageThreadKey string `gorm:"not null" json:"message_thread_key"` + CreatedAt time.Time `gorm:"not null" json:"created_at"` } // TODO: Rename to ConversationDetails diff --git a/mobile/lib/models/conversations.dart b/mobile/lib/models/conversations.dart index 8c4b996..2963b6b 100644 --- a/mobile/lib/models/conversations.dart +++ b/mobile/lib/models/conversations.dart @@ -4,7 +4,6 @@ import '/utils/encryption/crypto_utils.dart'; import '/utils/encryption/aes_helper.dart'; import '/utils/storage/database.dart'; - Conversation findConversationByDetailId(List conversations, String id) { for (var conversation in conversations) { if (conversation.conversationDetailId == id) { diff --git a/mobile/lib/models/friends.dart b/mobile/lib/models/friends.dart index 0fcb23c..3bd1b1e 100644 --- a/mobile/lib/models/friends.dart +++ b/mobile/lib/models/friends.dart @@ -4,101 +4,117 @@ import '/utils/encryption/crypto_utils.dart'; import '/utils/storage/database.dart'; Friend findFriendByFriendId(List friends, String id) { - for (var friend in friends) { - if (friend.friendIdDecrypted == id) { - return friend; - } + for (var friend in friends) { + if (friend.friendId == id) { + return friend; } + } // Or return `null`. throw ArgumentError.value(id, "id", "No element with that id"); } class Friend{ - String id; - String userId; - String? username; - String friendId; - String friendIdDecrypted; - String friendSymmetricKey; - String friendSymmetricKeyDecrypted; - String acceptedAt; - Friend({ - required this.id, - required this.userId, - required this.friendId, - required this.friendIdDecrypted, - required this.friendSymmetricKey, - required this.friendSymmetricKeyDecrypted, - required this.acceptedAt, - this.username - }); + String id; + String userId; + String? username; + String friendId; + String friendSymmetricKey; + String acceptedAt; + Friend({ + required this.id, + required this.userId, + required this.friendId, + required this.friendSymmetricKey, + required this.acceptedAt, + this.username + }); - factory Friend.fromJson(Map json, RSAPrivateKey privKey) { - // TODO: Remove encrypted entries - var friendIdDecrypted = CryptoUtils.rsaDecrypt( - base64.decode(json['friend_id']), - privKey, - ); + factory Friend.fromJson(Map json, RSAPrivateKey privKey) { + var friendIdDecrypted = CryptoUtils.rsaDecrypt( + base64.decode(json['friend_id']), + privKey, + ); - var friendSymmetricKeyDecrypted = CryptoUtils.rsaDecrypt( - base64.decode(json['symmetric_key']), - privKey, - ); + var friendSymmetricKeyDecrypted = CryptoUtils.rsaDecrypt( + base64.decode(json['symmetric_key']), + privKey, + ); - return Friend( - id: json['id'], - userId: json['user_id'], - friendId: json['friend_id'], - friendIdDecrypted: String.fromCharCodes(friendIdDecrypted), - friendSymmetricKey: json['symmetric_key'], - friendSymmetricKeyDecrypted: base64.encode(friendSymmetricKeyDecrypted), - acceptedAt: json['accepted_at'], - ); - } + return Friend( + id: json['id'], + userId: json['user_id'], + friendId: String.fromCharCodes(friendIdDecrypted), + friendSymmetricKey: base64.encode(friendSymmetricKeyDecrypted), + acceptedAt: json['accepted_at'], + ); + } - @override - String toString() { - return ''' + @override + String toString() { + return ''' -id: $id -userId: $userId -username: $username -friendIdDecrypted: $friendIdDecrypted -accepted_at: $acceptedAt'''; - } + id: $id + userId: $userId + username: $username + friendId: $friendId + accepted_at: $acceptedAt'''; + } - Map toMap() { - return { - 'id': id, - 'user_id': userId, - 'username': username, - 'friend_id': friendId, - 'friend_id_decrypted': friendIdDecrypted, - 'symmetric_key': friendSymmetricKey, - 'symmetric_key_decrypted': friendSymmetricKeyDecrypted, - 'accepted_at': acceptedAt, - }; - } + Map toMap() { + return { + 'id': id, + 'user_id': userId, + 'username': username, + 'friend_id': friendId, + 'symmetric_key': friendSymmetricKey, + 'accepted_at': acceptedAt, + }; + } } // A method that retrieves all the dogs from the dogs table. Future> getFriends() async { - final db = await getDatabaseConnection(); + final db = await getDatabaseConnection(); - final List> maps = await db.query('friends'); + final List> maps = await db.query('friends'); - return List.generate(maps.length, (i) { - return Friend( - id: maps[i]['id'], - userId: maps[i]['user_id'], - friendId: maps[i]['friend_id'], - friendIdDecrypted: maps[i]['friend_id_decrypted'], - friendSymmetricKey: maps[i]['symmetric_key'], - friendSymmetricKeyDecrypted: maps[i]['symmetric_key_decrypted'], - acceptedAt: maps[i]['accepted_at'], - username: maps[i]['username'], - ); - }); + return List.generate(maps.length, (i) { + return Friend( + id: maps[i]['id'], + userId: maps[i]['user_id'], + friendId: maps[i]['friend_id'], + friendSymmetricKey: maps[i]['symmetric_key'], + acceptedAt: maps[i]['accepted_at'], + username: maps[i]['username'], + ); + }); } + +// Future getFriendByUserId(String userId) async { +// final db = await getDatabaseConnection(); +// +// List whereArguments = [userId]; +// +// final List> maps = await db.query( +// 'friends', +// where: 'friend_id = ?', +// whereArgs: whereArguments, +// ); +// +// print(userId); +// +// if (maps.length != 1) { +// throw ArgumentError('Invalid user id'); +// } +// +// return Friend( +// id: maps[0]['id'], +// userId: maps[0]['user_id'], +// friendId: maps[0]['friend_id'], +// friendSymmetricKey: maps[0]['symmetric_key'], +// acceptedAt: maps[0]['accepted_at'], +// username: maps[0]['username'], +// ); +// } diff --git a/mobile/lib/models/messages.dart b/mobile/lib/models/messages.dart index 26cb0f4..4fc3728 100644 --- a/mobile/lib/models/messages.dart +++ b/mobile/lib/models/messages.dart @@ -1,20 +1,108 @@ +import 'dart:convert'; +import 'package:pointycastle/export.dart'; +import '/utils/encryption/crypto_utils.dart'; +import '/utils/encryption/aes_helper.dart'; +import '/utils/storage/database.dart'; +import '/models/friends.dart'; const messageTypeSender = 'sender'; const messageTypeReceiver = 'receiver'; class Message { - String id; - String symmetricKey; - String messageThreadKey; - String data; - String senderId; - String senderUsername; - Message({ - required this.id, - required this.symmetricKey, - required this.messageThreadKey, - required this.data, - required this.senderId, - required this.senderUsername, - }); + String id; + String symmetricKey; + String messageThreadKey; + String data; + String senderId; + String senderUsername; + String createdAt; + Message({ + required this.id, + required this.symmetricKey, + required this.messageThreadKey, + required this.data, + required this.senderId, + required this.senderUsername, + required this.createdAt, + }); + + + factory Message.fromJson(Map json, RSAPrivateKey privKey) { + var symmetricKey = CryptoUtils.rsaDecrypt( + base64.decode(json['symmetric_key']), + privKey, + ); + + var data = AesHelper.aesDecrypt( + symmetricKey, + base64.decode(json['message_data']['data']), + ); + + var senderId = AesHelper.aesDecrypt( + symmetricKey, + base64.decode(json['message_data']['sender_id']), + ); + + return Message( + id: json['id'], + messageThreadKey: json['message_thread_key'], + symmetricKey: base64.encode(symmetricKey), + data: data, + senderId: senderId, + senderUsername: 'Unknown', // TODO + createdAt: json['created_at'], + ); + } + + @override + String toString() { + return ''' + + + id: $id + data: $data + senderId: $senderId + senderUsername: $senderUsername + createdAt: $createdAt +'''; + } + + Map toMap() { + return { + 'id': id, + 'message_thread_key': messageThreadKey, + 'symmetric_key': symmetricKey, + 'data': data, + 'sender_id': senderId, + 'sender_username': senderUsername, + 'created_at': createdAt, + }; + } + +} + +Future> getMessagesForThread(String messageThreadKey) async { + final db = await getDatabaseConnection(); + + List whereArguments = [messageThreadKey]; + + final List> maps = await db.query( + 'messages', + where: 'message_thread_key = ?', + whereArgs: whereArguments, + orderBy: 'created_at DESC', + ); + + return List.generate(maps.length, (i) { + return Message( + id: maps[i]['id'], + messageThreadKey: maps[i]['message_thread_key'], + symmetricKey: maps[i]['symmetric_key'], + data: maps[i]['data'], + senderId: maps[i]['sender_id'], + senderUsername: maps[i]['sender_username'], + createdAt: maps[i]['created_at'], + ); + }); + } diff --git a/mobile/lib/utils/storage/database.dart b/mobile/lib/utils/storage/database.dart index 3de37f6..3be45e7 100644 --- a/mobile/lib/utils/storage/database.dart +++ b/mobile/lib/utils/storage/database.dart @@ -3,6 +3,11 @@ import 'package:flutter/widgets.dart'; import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart'; +Future deleteDb() async { + final path = join(await getDatabasesPath(), 'envelope.db'); + deleteDatabase(path); +} + Future getDatabaseConnection() async { WidgetsFlutterBinding.ensureInitialized(); @@ -10,7 +15,6 @@ Future getDatabaseConnection() async { final database = openDatabase( path, - // TODO: remove friend_id_decrypted and symmetric_key_decrypted onCreate: (db, version) async { await db.execute( ''' @@ -19,9 +23,7 @@ Future getDatabaseConnection() async { user_id TEXT, username TEXT, friend_id TEXT, - friend_id_decrypted TEXT, symmetric_key TEXT, - symmetric_key_decrypted TEXT, accepted_at TEXT ); '''); @@ -40,6 +42,19 @@ Future getDatabaseConnection() async { users TEXT ); '''); + + await db.execute( + ''' + CREATE TABLE IF NOT EXISTS messages( + id TEXT PRIMARY KEY, + message_thread_key TEXT, + symmetric_key TEXT, + data TEXT, + sender_id TEXT, + sender_username TEXT, + created_at TEXT + ); + '''); }, // Set the version. This executes the onCreate function and provides a // path to perform database upgrades and downgrades. diff --git a/mobile/lib/utils/storage/friends.dart b/mobile/lib/utils/storage/friends.dart index b0374a2..529bbcf 100644 --- a/mobile/lib/utils/storage/friends.dart +++ b/mobile/lib/utils/storage/friends.dart @@ -36,7 +36,7 @@ Future updateFriends() async { ) ); - friendIds.add(friends[i].friendIdDecrypted); + friendIds.add(friends[i].friendId); } Map params = {}; @@ -63,7 +63,7 @@ Future updateFriends() async { var friend = findFriendByFriendId(friends, friendJson['id']); friend.username = AesHelper.aesDecrypt( - base64.decode(friend.friendSymmetricKeyDecrypted), + base64.decode(friend.friendSymmetricKey), base64.decode(friendJson['username']), ); diff --git a/mobile/lib/utils/storage/messages.dart b/mobile/lib/utils/storage/messages.dart new file mode 100644 index 0000000..c2fb659 --- /dev/null +++ b/mobile/lib/utils/storage/messages.dart @@ -0,0 +1,76 @@ +import 'dart:convert'; +import 'package:Envelope/models/messages.dart'; +import 'package:http/http.dart' as http; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:pointycastle/export.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:Envelope/models/conversations.dart'; +import '/utils/storage/session_cookie.dart'; +import '/utils/storage/encryption_keys.dart'; +import '/utils/storage/database.dart'; +import '/models/conversations.dart'; + +// TODO: Move this to table +Map> _mapUsers(String users) { + List usersJson = jsonDecode(users); + + Map> mapped = {}; + + for (var i = 0; i < usersJson.length; i++) { + mapped[usersJson[i]['id']] = { + 'username': usersJson[i]['username'], + 'admin': usersJson[i]['admin'], + }; + } + + return mapped; +} + +Future updateMessageThread(Conversation conversation, {RSAPrivateKey? privKey}) async { + privKey ??= await getPrivateKey(); + + var resp = await http.get( + Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/messages/${conversation.messageThreadKey}'), + headers: { + 'cookie': await getSessionCookie(), + } + ); + + if (resp.statusCode != 200) { + throw Exception(resp.body); + } + + var mapped = _mapUsers(conversation.users!); + + List messageThreadJson = jsonDecode(resp.body); + + final db = await getDatabaseConnection(); + + for (var i = 0; i < messageThreadJson.length; i++) { + Message message = Message.fromJson( + messageThreadJson[i] as Map, + privKey, + ); + + // TODO: Fix this + message.senderUsername = mapped[message.senderId]!['username']!; + + await db.insert( + 'messages', + message.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + + } +} + +Future updateMessageThreads({List? conversations}) async { + RSAPrivateKey privKey = await getPrivateKey(); + + conversations ??= await getConversations(); + + for (var i = 0; i < conversations.length; i++) { + await updateMessageThread(conversations[i], privKey: privKey); + } +} + diff --git a/mobile/lib/views/authentication/login.dart b/mobile/lib/views/authentication/login.dart index 29075d1..b702f11 100644 --- a/mobile/lib/views/authentication/login.dart +++ b/mobile/lib/views/authentication/login.dart @@ -9,198 +9,201 @@ import '/utils/storage/encryption_keys.dart'; import '/utils/storage/session_cookie.dart'; class LoginResponse { - final String status; - final String message; - final String asymmetricPublicKey; - final String asymmetricPrivateKey; - - const LoginResponse({ - required this.status, - required this.message, - required this.asymmetricPublicKey, - required this.asymmetricPrivateKey, - }); - - factory LoginResponse.fromJson(Map json) { - return LoginResponse( - status: json['status'], - message: json['message'], - asymmetricPublicKey: json['asymmetric_public_key'], - asymmetricPrivateKey: json['asymmetric_private_key'], - ); - } + final String status; + final String message; + final String asymmetricPublicKey; + final String asymmetricPrivateKey; + final String userId; + + const LoginResponse({ + required this.status, + required this.message, + required this.asymmetricPublicKey, + required this.asymmetricPrivateKey, + required this.userId, + }); + + factory LoginResponse.fromJson(Map json) { + return LoginResponse( + status: json['status'], + message: json['message'], + asymmetricPublicKey: json['asymmetric_public_key'], + asymmetricPrivateKey: json['asymmetric_private_key'], + userId: json['user_id'], + ); + } } Future login(context, String username, String password) async { - final resp = await http.post( - Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/login'), - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - }, - body: jsonEncode({ - 'username': username, - 'password': password, - }), - ); + final resp = await http.post( + Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/login'), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: jsonEncode({ + 'username': username, + 'password': password, + }), + ); - if (resp.statusCode != 200) { - throw Exception(resp.body); - } + if (resp.statusCode != 200) { + throw Exception(resp.body); + } - String? rawCookie = resp.headers['set-cookie']; - if (rawCookie != null) { - int index = rawCookie.indexOf(';'); - setSessionCookie((index == -1) ? rawCookie : rawCookie.substring(0, index)); - } + String? rawCookie = resp.headers['set-cookie']; + if (rawCookie != null) { + int index = rawCookie.indexOf(';'); + setSessionCookie((index == -1) ? rawCookie : rawCookie.substring(0, index)); + } - LoginResponse response = LoginResponse.fromJson(jsonDecode(resp.body)); + LoginResponse response = LoginResponse.fromJson(jsonDecode(resp.body)); - var rsaPrivPem = AesHelper.aesDecrypt(password, base64.decode(response.asymmetricPrivateKey)); + var rsaPrivPem = AesHelper.aesDecrypt(password, base64.decode(response.asymmetricPrivateKey)); - debugPrint(rsaPrivPem); + debugPrint(rsaPrivPem); - var rsaPriv = CryptoUtils.rsaPrivateKeyFromPem(rsaPrivPem); - setPrivateKey(rsaPriv); + var rsaPriv = CryptoUtils.rsaPrivateKeyFromPem(rsaPrivPem); + setPrivateKey(rsaPriv); - final preferences = await SharedPreferences.getInstance(); - preferences.setBool('islogin', true); + final preferences = await SharedPreferences.getInstance(); + preferences.setBool('islogin', true); + preferences.setString('userId', response.userId); - return response; + return response; } class Login extends StatelessWidget { - const Login({Key? key}) : super(key: key); - - static const String _title = 'Envelope'; - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.cyan, - appBar: AppBar( - title: null, - automaticallyImplyLeading: true, - //`true` if you want Flutter to automatically add Back Button when needed, - //or `false` if you want to force your own back button every where - leading: IconButton(icon: const Icon(Icons.arrow_back), - onPressed:() => { - Navigator.pop(context) - } - ) - ), - body: const SafeArea( - child: LoginWidget(), - ), - ); - } + const Login({Key? key}) : super(key: key); + + static const String _title = 'Envelope'; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.cyan, + appBar: AppBar( + title: null, + automaticallyImplyLeading: true, + //`true` if you want Flutter to automatically add Back Button when needed, + //or `false` if you want to force your own back button every where + leading: IconButton(icon: const Icon(Icons.arrow_back), + onPressed:() => { + Navigator.pop(context) + } + ) + ), + body: const SafeArea( + child: LoginWidget(), + ), + ); + } } class LoginWidget extends StatefulWidget { - const LoginWidget({Key? key}) : super(key: key); + const LoginWidget({Key? key}) : super(key: key); - @override - State createState() => _LoginWidgetState(); + @override + State createState() => _LoginWidgetState(); } class _LoginWidgetState extends State { - final _formKey = GlobalKey(); - - TextEditingController usernameController = TextEditingController(); - TextEditingController passwordController = TextEditingController(); - - @override - Widget build(BuildContext context) { - const TextStyle _inputTextStyle = TextStyle(fontSize: 18, color: Colors.black); - - final ButtonStyle _buttonStyle = ElevatedButton.styleFrom( - primary: Colors.white, - onPrimary: Colors.cyan, - minimumSize: const Size.fromHeight(50), - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), - textStyle: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.red, - ), - ); - - return Center( - child: Form( - key: _formKey, - child: Center( - child: Padding( - padding: const EdgeInsets.all(15), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text('Login', style: TextStyle(fontSize: 35, color: Colors.white),), - const SizedBox(height: 30), - TextFormField( - controller: usernameController, - decoration: const InputDecoration( - hintText: 'Username', - ), - style: _inputTextStyle, - // The validator receives the text that the user has entered. - validator: (value) { - if (value == null || value.isEmpty) { - return 'Create a username'; - } - return null; - }, - ), - const SizedBox(height: 5), - TextFormField( - controller: passwordController, - obscureText: true, - enableSuggestions: false, - autocorrect: false, - decoration: const InputDecoration( - hintText: 'Password', - ), - style: _inputTextStyle, - // The validator receives the text that the user has entered. - validator: (value) { - if (value == null || value.isEmpty) { - return 'Enter a password'; - } - return null; - }, - ), - const SizedBox(height: 5), - ElevatedButton( - style: _buttonStyle, - onPressed: () { - if (_formKey.currentState!.validate()) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Processing Data')), - ); - - login( - context, - usernameController.text, - passwordController.text, - ).then((value) { - Navigator. - pushNamedAndRemoveUntil( - context, - '/home', - ModalRoute.withName('/home'), - ); - }).catchError((error) { - print(error); // TODO: Show error on interface - }); - } - }, - child: const Text('Submit'), - ), - ], - ) - ) - ) - - ) - ); - } + final _formKey = GlobalKey(); + + TextEditingController usernameController = TextEditingController(); + TextEditingController passwordController = TextEditingController(); + + @override + Widget build(BuildContext context) { + const TextStyle _inputTextStyle = TextStyle(fontSize: 18, color: Colors.black); + + final ButtonStyle _buttonStyle = ElevatedButton.styleFrom( + primary: Colors.white, + onPrimary: Colors.cyan, + minimumSize: const Size.fromHeight(50), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + textStyle: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ); + + return Center( + child: Form( + key: _formKey, + child: Center( + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text('Login', style: TextStyle(fontSize: 35, color: Colors.white),), + const SizedBox(height: 30), + TextFormField( + controller: usernameController, + decoration: const InputDecoration( + hintText: 'Username', + ), + style: _inputTextStyle, + // The validator receives the text that the user has entered. + validator: (value) { + if (value == null || value.isEmpty) { + return 'Create a username'; + } + return null; + }, + ), + const SizedBox(height: 5), + TextFormField( + controller: passwordController, + obscureText: true, + enableSuggestions: false, + autocorrect: false, + decoration: const InputDecoration( + hintText: 'Password', + ), + style: _inputTextStyle, + // The validator receives the text that the user has entered. + validator: (value) { + if (value == null || value.isEmpty) { + return 'Enter a password'; + } + return null; + }, + ), + const SizedBox(height: 5), + ElevatedButton( + style: _buttonStyle, + onPressed: () { + if (_formKey.currentState!.validate()) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Processing Data')), + ); + + login( + context, + usernameController.text, + passwordController.text, + ).then((value) { + Navigator. + pushNamedAndRemoveUntil( + context, + '/home', + ModalRoute.withName('/home'), + ); + }).catchError((error) { + print(error); // TODO: Show error on interface + }); + } + }, + child: const Text('Submit'), + ), + ], + ) + ) + ) + ) + ); + } } diff --git a/mobile/lib/views/authentication/unauthenticated_landing.dart b/mobile/lib/views/authentication/unauthenticated_landing.dart index e17a06f..1bb6c31 100644 --- a/mobile/lib/views/authentication/unauthenticated_landing.dart +++ b/mobile/lib/views/authentication/unauthenticated_landing.dart @@ -4,82 +4,82 @@ import './login.dart'; import './signup.dart'; class UnauthenticatedLandingWidget extends StatefulWidget { - const UnauthenticatedLandingWidget({Key? key}) : super(key: key); + const UnauthenticatedLandingWidget({Key? key}) : super(key: key); - @override - State createState() => _UnauthenticatedLandingWidgetState(); + @override + State createState() => _UnauthenticatedLandingWidgetState(); } class _UnauthenticatedLandingWidgetState extends State { - @override - Widget build(BuildContext context) { - final ButtonStyle buttonStyle = ElevatedButton.styleFrom( - primary: Colors.white, - onPrimary: Colors.cyan, - minimumSize: const Size.fromHeight(50), - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), - textStyle: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.red, - ), - ); + @override + Widget build(BuildContext context) { + final ButtonStyle buttonStyle = ElevatedButton.styleFrom( + primary: Colors.white, + onPrimary: Colors.cyan, + minimumSize: const Size.fromHeight(50), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + textStyle: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ); - return WillPopScope( - onWillPop: () async => false, - child: Scaffold( - backgroundColor: Colors.cyan, - body: SafeArea( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: const [ - FaIcon(FontAwesomeIcons.envelope, color: Colors.white, size: 40), - SizedBox(width: 15), - Text('Envelope', style: TextStyle(fontSize: 40, color: Colors.white),) - ] - ), - ), - const SizedBox(height: 10), - Padding( - padding: const EdgeInsets.all(15), - child: Column ( - children: [ - ElevatedButton( - child: const Text('Login'), - onPressed: () => { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => const Login()), - ), - }, + return WillPopScope( + onWillPop: () async => false, + child: Scaffold( + backgroundColor: Colors.cyan, + body: SafeArea( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + FaIcon(FontAwesomeIcons.envelope, color: Colors.white, size: 40), + SizedBox(width: 15), + Text('Envelope', style: TextStyle(fontSize: 40, color: Colors.white),) + ] + ), + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.all(15), + child: Column ( + children: [ + ElevatedButton( + child: const Text('Login'), + onPressed: () => { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const Login()), + ), + }, - style: buttonStyle, - ), - const SizedBox(height: 20), - ElevatedButton( - child: const Text('Sign Up'), - onPressed: () => { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => const Signup()), - ), - }, - style: buttonStyle, - ), - ] - ), - ), - ], - ), - ), - ), - ), - ); - } + style: buttonStyle, + ), + const SizedBox(height: 20), + ElevatedButton( + child: const Text('Sign Up'), + onPressed: () => { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const Signup()), + ), + }, + style: buttonStyle, + ), + ] + ), + ), + ], + ), + ), + ), + ), + ); + } } diff --git a/mobile/lib/views/main/conversation_detail.dart b/mobile/lib/views/main/conversation_detail.dart index 4ed7182..aaa158e 100644 --- a/mobile/lib/views/main/conversation_detail.dart +++ b/mobile/lib/views/main/conversation_detail.dart @@ -1,7 +1,27 @@ import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '/models/conversations.dart'; import '/models/messages.dart'; +String convertToAgo(String input){ + DateTime time = DateTime.parse(input); + Duration diff = DateTime.now().difference(time); + + if(diff.inDays >= 1){ + return '${diff.inDays} day${diff.inDays == 1 ? "" : "s"} ago'; + } + if(diff.inHours >= 1){ + return '${diff.inHours} hour${diff.inHours == 1 ? "" : "s"} ago'; + } + if(diff.inMinutes >= 1){ + return '${diff.inMinutes} minute${diff.inMinutes == 1 ? "" : "s"} ago'; + } + if (diff.inSeconds >= 1){ + return '${diff.inSeconds} second${diff.inSeconds == 1 ? "" : "s"} ago'; + } + return 'just now'; +} + class ConversationDetail extends StatefulWidget{ final Conversation conversation; const ConversationDetail({ @@ -11,130 +31,167 @@ class ConversationDetail extends StatefulWidget{ @override _ConversationDetailState createState() => _ConversationDetailState(); + } class _ConversationDetailState extends State { - List messages = [ - ]; + List messages = []; + String userId = ''; + + @override + void initState() { + super.initState(); + fetchMessages(); + } + + void fetchMessages() async { + final preferences = await SharedPreferences.getInstance(); + userId = preferences.getString('userId')!; + messages = await getMessagesForThread(widget.conversation.messageThreadKey); + setState(() {}); + } @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - elevation: 0, - automaticallyImplyLeading: false, - backgroundColor: Colors.white, - flexibleSpace: SafeArea( - child: Container( - padding: const EdgeInsets.only(right: 16), - child: Row( - children: [ - IconButton( - onPressed: (){ - Navigator.pop(context); - }, - icon: const Icon(Icons.arrow_back,color: Colors.black,), - ), - const SizedBox(width: 2,), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - widget.conversation.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600), - ), - ], - ), - ), - const Icon(Icons.settings,color: Colors.black54,), - ], - ), - ), + return Scaffold( + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + backgroundColor: Colors.white, + flexibleSpace: SafeArea( + child: Container( + padding: const EdgeInsets.only(right: 16), + child: Row( + children: [ + IconButton( + onPressed: (){ + Navigator.pop(context); + }, + icon: const Icon(Icons.arrow_back,color: Colors.black,), + ), + const SizedBox(width: 2,), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.conversation.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600), + ), + ], + ), + ), + const Icon(Icons.settings,color: Colors.black54,), + ], ), - ), - body: Stack( - children: [ - ListView.builder( - itemCount: messages.length, - shrinkWrap: true, - padding: const EdgeInsets.only(top: 10,bottom: 10), - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - return Container( - padding: const EdgeInsets.only(left: 14,right: 14,top: 10,bottom: 10), - child: Align( - // alignment: (messages[index].messageType == messageTypeReceiver ? Alignment.topLeft:Alignment.topRight), - alignment: Alignment.topLeft, // TODO: compare senderId to current user id - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - // color: (messages[index].messageType == messageTypeReceiver ? Colors.grey.shade200:Colors.blue[200]), - color: (true ? Colors.grey.shade200:Colors.blue[200]), - ), - padding: const EdgeInsets.all(16), - child: Text(messages[index].data, style: const TextStyle(fontSize: 15)), + ), + ), + ), + body: Stack( + children: [ + ListView.builder( + itemCount: messages.length, + shrinkWrap: true, + padding: const EdgeInsets.only(top: 10,bottom: 10), + reverse: true, + itemBuilder: (context, index) { + return Container( + padding: const EdgeInsets.only(left: 14,right: 14,top: 0,bottom: 0), + child: Align( + alignment: ( + messages[index].senderId == userId ? + Alignment.topLeft: + Alignment.topRight + ), + child: Column( + crossAxisAlignment: messages[index].senderId == userId ? + CrossAxisAlignment.start : + CrossAxisAlignment.end, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: ( + messages[index].senderId == userId ? + Colors.grey.shade200 : + Colors.blue[200] ), + ), + padding: const EdgeInsets.all(12), + child: Text(messages[index].data, style: const TextStyle(fontSize: 15)), + ), + messages[index].senderId != userId ? + Text(messages[index].senderUsername) : + const SizedBox.shrink(), + Text( + convertToAgo(messages[index].createdAt), + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 12, + color: Colors.grey[500], + ), ), - ); - }, - ), - Align( - alignment: Alignment.bottomLeft, - child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 200.0, + ] + ) ), - child: Container( - padding: const EdgeInsets.only(left: 10,bottom: 10,top: 10), - // height: 60, - width: double.infinity, - color: Colors.white, - child: Row( - children: [ + ); + }, + ), + Align( + alignment: Alignment.bottomLeft, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 200.0, + ), + child: Container( + padding: const EdgeInsets.only(left: 10,bottom: 10,top: 10), + // height: 60, + width: double.infinity, + color: Colors.white, + child: Row( + children: [ GestureDetector( - onTap: (){ - }, - child: Container( - height: 30, - width: 30, - decoration: BoxDecoration( - color: Colors.lightBlue, - borderRadius: BorderRadius.circular(30), - ), - child: const Icon(Icons.add, color: Colors.white, size: 20, ), + onTap: (){ + }, + child: Container( + height: 30, + width: 30, + decoration: BoxDecoration( + color: Colors.lightBlue, + borderRadius: BorderRadius.circular(30), ), + child: const Icon(Icons.add, color: Colors.white, size: 20, ), + ), ), const SizedBox(width: 15,), const Expanded( - child: TextField( - decoration: InputDecoration( - hintText: "Write message...", - hintStyle: TextStyle(color: Colors.black54), - border: InputBorder.none, - ), - maxLines: null, + child: TextField( + decoration: InputDecoration( + hintText: "Write message...", + hintStyle: TextStyle(color: Colors.black54), + border: InputBorder.none, ), + maxLines: null, + ), ), const SizedBox(width: 15,), FloatingActionButton( - onPressed: () { - }, - child: const Icon(Icons.send,color: Colors.white,size: 18,), - backgroundColor: Colors.blue, - elevation: 0, + onPressed: () { + }, + child: const Icon(Icons.send,color: Colors.white,size: 18,), + backgroundColor: Colors.blue, + elevation: 0, ), ], - + ), ), ), - ), - ), - ], + ), + ], ), - ); + ); } } diff --git a/mobile/lib/views/main/conversation_list.dart b/mobile/lib/views/main/conversation_list.dart index 5a5ca08..7ff74b6 100644 --- a/mobile/lib/views/main/conversation_list.dart +++ b/mobile/lib/views/main/conversation_list.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '/models/conversations.dart'; import '/views/main/conversation_list_item.dart'; +import '/utils/storage/messages.dart'; class ConversationList extends StatefulWidget { const ConversationList({Key? key}) : super(key: key); diff --git a/mobile/lib/views/main/conversation_list_item.dart b/mobile/lib/views/main/conversation_list_item.dart index 78b1d55..ecfc157 100644 --- a/mobile/lib/views/main/conversation_list_item.dart +++ b/mobile/lib/views/main/conversation_list_item.dart @@ -15,6 +15,7 @@ class ConversationListItem extends StatefulWidget{ } class _ConversationListItemState extends State { + @override Widget build(BuildContext context) { return GestureDetector( diff --git a/mobile/lib/views/main/home.dart b/mobile/lib/views/main/home.dart index fcf9900..7d4e6e9 100644 --- a/mobile/lib/views/main/home.dart +++ b/mobile/lib/views/main/home.dart @@ -5,6 +5,7 @@ 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'; class Home extends StatefulWidget { const Home({Key? key}) : super(key: key); @@ -24,6 +25,7 @@ class _HomeState extends State { await checkLogin(); await updateFriends(); await updateConversations(); + await updateMessageThreads(); } // TODO: Do server GET check here diff --git a/mobile/lib/views/main/profile.dart b/mobile/lib/views/main/profile.dart index 488d1b6..64d65ff 100644 --- a/mobile/lib/views/main/profile.dart +++ b/mobile/lib/views/main/profile.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '/utils/storage/encryption_keys.dart'; +import '/utils/storage/database.dart'; class Profile extends StatefulWidget { const Profile({Key? key}) : super(key: key); @@ -34,6 +35,7 @@ class _ProfileState extends State { ), child: GestureDetector( onTap: () async { + deleteDb(); final preferences = await SharedPreferences.getInstance(); preferences.setBool('islogin', false); preferences.remove(rsaPrivateKeyName);