Browse Source

Message conversations syncing to device

pull/1/head
Tovi Jaeschke-Rogers 2 years ago
parent
commit
d8535fdfc7
19 changed files with 751 additions and 460 deletions
  1. +9
    -5
      Backend/Api/Auth/Login.go
  2. +10
    -9
      Backend/Api/Messages/MessageThread.go
  3. +1
    -1
      Backend/Api/Routes.go
  4. +13
    -0
      Backend/Database/Messages.go
  5. +10
    -2
      Backend/Database/Seeder/MessageSeeder.go
  6. +6
    -1
      Backend/Models/Messages.go
  7. +0
    -1
      mobile/lib/models/conversations.dart
  8. +93
    -77
      mobile/lib/models/friends.dart
  9. +102
    -14
      mobile/lib/models/messages.dart
  10. +18
    -3
      mobile/lib/utils/storage/database.dart
  11. +2
    -2
      mobile/lib/utils/storage/friends.dart
  12. +76
    -0
      mobile/lib/utils/storage/messages.dart
  13. +176
    -173
      mobile/lib/views/authentication/login.dart
  14. +71
    -71
      mobile/lib/views/authentication/unauthenticated_landing.dart
  15. +158
    -101
      mobile/lib/views/main/conversation_detail.dart
  16. +1
    -0
      mobile/lib/views/main/conversation_list.dart
  17. +1
    -0
      mobile/lib/views/main/conversation_list_item.dart
  18. +2
    -0
      mobile/lib/views/main/home.dart
  19. +2
    -0
      mobile/lib/views/main/profile.dart

+ 9
- 5
Backend/Api/Auth/Login.go View File

@ -19,9 +19,10 @@ type loginResponse struct {
Message string `json:"message"` Message string `json:"message"`
AsymmetricPublicKey string `json:"asymmetric_public_key"` AsymmetricPublicKey string `json:"asymmetric_public_key"`
AsymmetricPrivateKey string `json:"asymmetric_private_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 ( var (
status string = "error" status string = "error"
returnJson []byte returnJson []byte
@ -36,6 +37,7 @@ func makeLoginResponse(w http.ResponseWriter, code int, message, pubKey, privKey
Message: message, Message: message,
AsymmetricPublicKey: pubKey, AsymmetricPublicKey: pubKey,
AsymmetricPrivateKey: privKey, AsymmetricPrivateKey: privKey,
UserID: userId,
}, "", " ") }, "", " ")
if err != nil { if err != nil {
http.Error(w, "Error", http.StatusInternalServerError) 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) err = json.NewDecoder(r.Body).Decode(&creds)
if err != nil { if err != nil {
makeLoginResponse(w, http.StatusInternalServerError, "An error occurred", "", "")
makeLoginResponse(w, http.StatusInternalServerError, "An error occurred", "", "", "")
return return
} }
userData, err = Database.GetUserByUsername(creds.Username) userData, err = Database.GetUserByUsername(creds.Username)
if err != nil { if err != nil {
makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "")
makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "", "")
return return
} }
if !CheckPasswordHash(creds.Password, userData.Password) { if !CheckPasswordHash(creds.Password, userData.Password) {
makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "")
makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "", "")
return return
} }
// TODO: Revisit before production
expiresAt = time.Now().Add(12 * time.Hour) expiresAt = time.Now().Add(12 * time.Hour)
session = Models.Session{ session = Models.Session{
@ -83,7 +86,7 @@ func Login(w http.ResponseWriter, r *http.Request) {
err = Database.CreateSession(&session) err = Database.CreateSession(&session)
if err != nil { if err != nil {
makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "")
makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "", "")
return return
} }
@ -99,5 +102,6 @@ func Login(w http.ResponseWriter, r *http.Request) {
"Successfully logged in", "Successfully logged in",
userData.AsymmetricPublicKey, userData.AsymmetricPublicKey,
userData.AsymmetricPrivateKey, userData.AsymmetricPrivateKey,
userData.ID.String(),
) )
} }

+ 10
- 9
Backend/Api/Messages/MessageThread.go View File

@ -6,17 +6,18 @@ import (
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
func ConversationDetail(w http.ResponseWriter, r *http.Request) {
func Messages(w http.ResponseWriter, r *http.Request) {
var ( 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) urlVars = mux.Vars(r)
@ -26,13 +27,13 @@ func ConversationDetail(w http.ResponseWriter, r *http.Request) {
return return
} }
conversationDetail, err = Database.GetConversationDetailById(threadKey)
messages, err = Database.GetMessagesByThreadKey(threadKey)
if !ok { if !ok {
http.Error(w, "Not Found", http.StatusNotFound) http.Error(w, "Not Found", http.StatusNotFound)
return return
} }
returnJson, err = json.MarshalIndent(conversationDetail, "", " ")
returnJson, err = json.MarshalIndent(messages, "", " ")
if err != nil { if err != nil {
http.Error(w, "Error", http.StatusInternalServerError) http.Error(w, "Error", http.StatusInternalServerError)
return return


+ 1
- 1
Backend/Api/Routes.go View File

@ -72,5 +72,5 @@ func InitApiEndpoints(router *mux.Router) {
// authApi.HandleFunc("/user/{userID}/request", Friends.FriendRequest).Methods("POST") // authApi.HandleFunc("/user/{userID}/request", Friends.FriendRequest).Methods("POST")
// Define routes for messages // Define routes for messages
authApi.HandleFunc("/messages/{threadKey}", Messages.ConversationDetail).Methods("GET")
authApi.HandleFunc("/messages/{threadKey}", Messages.Messages).Methods("GET")
} }

+ 13
- 0
Backend/Database/Messages.go View File

@ -20,6 +20,19 @@ func GetMessageById(id string) (Models.Message, error) {
return message, err 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 { func CreateMessage(message *Models.Message) error {
var ( var (
err error err error


+ 10
- 2
Backend/Database/Seeder/MessageSeeder.go View File

@ -11,7 +11,7 @@ import (
) )
func seedMessage( func seedMessage(
primaryUser Models.User,
primaryUser, secondaryUser Models.User,
primaryUserThreadKey, secondaryUserThreadKey string, primaryUserThreadKey, secondaryUserThreadKey string,
thread Models.ConversationDetail, thread Models.ConversationDetail,
i int, i int,
@ -38,11 +38,18 @@ func seedMessage(
panic(err) panic(err)
} }
senderIdCiphertext, err = key.aesEncrypt(primaryUser.ID.Bytes())
senderIdCiphertext, err = key.aesEncrypt([]byte(primaryUser.ID.String()))
if err != nil { if err != nil {
panic(err) panic(err)
} }
if i%2 == 0 {
senderIdCiphertext, err = key.aesEncrypt([]byte(secondaryUser.ID.String()))
if err != nil {
panic(err)
}
}
messageData = Models.MessageData{ messageData = Models.MessageData{
Data: base64.StdEncoding.EncodeToString(dataCiphertext), Data: base64.StdEncoding.EncodeToString(dataCiphertext),
SenderID: base64.StdEncoding.EncodeToString(senderIdCiphertext), SenderID: base64.StdEncoding.EncodeToString(senderIdCiphertext),
@ -240,6 +247,7 @@ func SeedMessages() {
for i = 0; i <= 20; i++ { for i = 0; i <= 20; i++ {
err = seedMessage( err = seedMessage(
primaryUser, primaryUser,
secondaryUser,
primaryUserThreadKey, primaryUserThreadKey,
secondaryUserThreadKey, secondaryUserThreadKey,
thread, thread,


+ 6
- 1
Backend/Models/Messages.go View File

@ -1,6 +1,10 @@
package Models package Models
import "github.com/gofrs/uuid"
import (
"time"
"github.com/gofrs/uuid"
)
// TODO: Add support for images // TODO: Add support for images
type MessageData struct { type MessageData struct {
@ -15,6 +19,7 @@ type Message struct {
MessageData MessageData `json:"message_data"` MessageData MessageData `json:"message_data"`
SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted
MessageThreadKey string `gorm:"not null" json:"message_thread_key"` MessageThreadKey string `gorm:"not null" json:"message_thread_key"`
CreatedAt time.Time `gorm:"not null" json:"created_at"`
} }
// TODO: Rename to ConversationDetails // TODO: Rename to ConversationDetails


+ 0
- 1
mobile/lib/models/conversations.dart View File

@ -4,7 +4,6 @@ import '/utils/encryption/crypto_utils.dart';
import '/utils/encryption/aes_helper.dart'; import '/utils/encryption/aes_helper.dart';
import '/utils/storage/database.dart'; import '/utils/storage/database.dart';
Conversation findConversationByDetailId(List<Conversation> conversations, String id) { Conversation findConversationByDetailId(List<Conversation> conversations, String id) {
for (var conversation in conversations) { for (var conversation in conversations) {
if (conversation.conversationDetailId == id) { if (conversation.conversationDetailId == id) {


+ 93
- 77
mobile/lib/models/friends.dart View File

@ -4,101 +4,117 @@ import '/utils/encryption/crypto_utils.dart';
import '/utils/storage/database.dart'; import '/utils/storage/database.dart';
Friend findFriendByFriendId(List<Friend> friends, String id) { Friend findFriendByFriendId(List<Friend> 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`. // Or return `null`.
throw ArgumentError.value(id, "id", "No element with that id"); throw ArgumentError.value(id, "id", "No element with that id");
} }
class Friend{ 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<String, dynamic> json, RSAPrivateKey privKey) {
// TODO: Remove encrypted entries
var friendIdDecrypted = CryptoUtils.rsaDecrypt(
base64.decode(json['friend_id']),
privKey,
);
factory Friend.fromJson(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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. // A method that retrieves all the dogs from the dogs table.
Future<List<Friend>> getFriends() async { Future<List<Friend>> getFriends() async {
final db = await getDatabaseConnection();
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> maps = await db.query('friends');
final List<Map<String, dynamic>> 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<Friend> getFriendByUserId(String userId) async {
// final db = await getDatabaseConnection();
//
// List<dynamic> whereArguments = [userId];
//
// final List<Map<String, dynamic>> 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'],
// );
// }

+ 102
- 14
mobile/lib/models/messages.dart View File

@ -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 messageTypeSender = 'sender';
const messageTypeReceiver = 'receiver'; const messageTypeReceiver = 'receiver';
class Message { 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<String, dynamic> 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<String, dynamic> toMap() {
return {
'id': id,
'message_thread_key': messageThreadKey,
'symmetric_key': symmetricKey,
'data': data,
'sender_id': senderId,
'sender_username': senderUsername,
'created_at': createdAt,
};
}
}
Future<List<Message>> getMessagesForThread(String messageThreadKey) async {
final db = await getDatabaseConnection();
List<dynamic> whereArguments = [messageThreadKey];
final List<Map<String, dynamic>> 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'],
);
});
} }

+ 18
- 3
mobile/lib/utils/storage/database.dart View File

@ -3,6 +3,11 @@ import 'package:flutter/widgets.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
Future<void> deleteDb() async {
final path = join(await getDatabasesPath(), 'envelope.db');
deleteDatabase(path);
}
Future<Database> getDatabaseConnection() async { Future<Database> getDatabaseConnection() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -10,7 +15,6 @@ Future<Database> getDatabaseConnection() async {
final database = openDatabase( final database = openDatabase(
path, path,
// TODO: remove friend_id_decrypted and symmetric_key_decrypted
onCreate: (db, version) async { onCreate: (db, version) async {
await db.execute( await db.execute(
''' '''
@ -19,9 +23,7 @@ Future<Database> getDatabaseConnection() async {
user_id TEXT, user_id TEXT,
username TEXT, username TEXT,
friend_id TEXT, friend_id TEXT,
friend_id_decrypted TEXT,
symmetric_key TEXT, symmetric_key TEXT,
symmetric_key_decrypted TEXT,
accepted_at TEXT accepted_at TEXT
); );
'''); ''');
@ -40,6 +42,19 @@ Future<Database> getDatabaseConnection() async {
users TEXT 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 // Set the version. This executes the onCreate function and provides a
// path to perform database upgrades and downgrades. // path to perform database upgrades and downgrades.


+ 2
- 2
mobile/lib/utils/storage/friends.dart View File

@ -36,7 +36,7 @@ Future<void> updateFriends() async {
) )
); );
friendIds.add(friends[i].friendIdDecrypted);
friendIds.add(friends[i].friendId);
} }
Map<String, String> params = {}; Map<String, String> params = {};
@ -63,7 +63,7 @@ Future<void> updateFriends() async {
var friend = findFriendByFriendId(friends, friendJson['id']); var friend = findFriendByFriendId(friends, friendJson['id']);
friend.username = AesHelper.aesDecrypt( friend.username = AesHelper.aesDecrypt(
base64.decode(friend.friendSymmetricKeyDecrypted),
base64.decode(friend.friendSymmetricKey),
base64.decode(friendJson['username']), base64.decode(friendJson['username']),
); );


+ 76
- 0
mobile/lib/utils/storage/messages.dart View File

@ -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<String, Map<String, String>> _mapUsers(String users) {
List<dynamic> usersJson = jsonDecode(users);
Map<String, Map<String, String>> mapped = {};
for (var i = 0; i < usersJson.length; i++) {
mapped[usersJson[i]['id']] = {
'username': usersJson[i]['username'],
'admin': usersJson[i]['admin'],
};
}
return mapped;
}
Future<void> 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<dynamic> messageThreadJson = jsonDecode(resp.body);
final db = await getDatabaseConnection();
for (var i = 0; i < messageThreadJson.length; i++) {
Message message = Message.fromJson(
messageThreadJson[i] as Map<String, dynamic>,
privKey,
);
// TODO: Fix this
message.senderUsername = mapped[message.senderId]!['username']!;
await db.insert(
'messages',
message.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
}
Future<void> updateMessageThreads({List<Conversation>? conversations}) async {
RSAPrivateKey privKey = await getPrivateKey();
conversations ??= await getConversations();
for (var i = 0; i < conversations.length; i++) {
await updateMessageThread(conversations[i], privKey: privKey);
}
}

+ 176
- 173
mobile/lib/views/authentication/login.dart View File

@ -9,198 +9,201 @@ import '/utils/storage/encryption_keys.dart';
import '/utils/storage/session_cookie.dart'; import '/utils/storage/session_cookie.dart';
class LoginResponse { 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<String, dynamic> 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<String, dynamic> json) {
return LoginResponse(
status: json['status'],
message: json['message'],
asymmetricPublicKey: json['asymmetric_public_key'],
asymmetricPrivateKey: json['asymmetric_private_key'],
userId: json['user_id'],
);
}
} }
Future<LoginResponse> login(context, String username, String password) async { Future<LoginResponse> login(context, String username, String password) async {
final resp = await http.post(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/login'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'username': username,
'password': password,
}),
);
final resp = await http.post(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/login'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'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 { 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 { class LoginWidget extends StatefulWidget {
const LoginWidget({Key? key}) : super(key: key);
const LoginWidget({Key? key}) : super(key: key);
@override
State<LoginWidget> createState() => _LoginWidgetState();
@override
State<LoginWidget> createState() => _LoginWidgetState();
} }
class _LoginWidgetState extends State<LoginWidget> { class _LoginWidgetState extends State<LoginWidget> {
final _formKey = GlobalKey<FormState>();
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<FormState>();
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'),
),
],
)
)
)
)
);
}
} }

+ 71
- 71
mobile/lib/views/authentication/unauthenticated_landing.dart View File

@ -4,82 +4,82 @@ import './login.dart';
import './signup.dart'; import './signup.dart';
class UnauthenticatedLandingWidget extends StatefulWidget { class UnauthenticatedLandingWidget extends StatefulWidget {
const UnauthenticatedLandingWidget({Key? key}) : super(key: key);
const UnauthenticatedLandingWidget({Key? key}) : super(key: key);
@override
State<UnauthenticatedLandingWidget> createState() => _UnauthenticatedLandingWidgetState();
@override
State<UnauthenticatedLandingWidget> createState() => _UnauthenticatedLandingWidgetState();
} }
class _UnauthenticatedLandingWidgetState extends State<UnauthenticatedLandingWidget> { class _UnauthenticatedLandingWidgetState extends State<UnauthenticatedLandingWidget> {
@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: <Widget>[
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: <Widget>[
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,
),
]
),
),
],
),
),
),
),
);
}
} }

+ 158
- 101
mobile/lib/views/main/conversation_detail.dart View File

@ -1,7 +1,27 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '/models/conversations.dart'; import '/models/conversations.dart';
import '/models/messages.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{ class ConversationDetail extends StatefulWidget{
final Conversation conversation; final Conversation conversation;
const ConversationDetail({ const ConversationDetail({
@ -11,130 +31,167 @@ class ConversationDetail extends StatefulWidget{
@override @override
_ConversationDetailState createState() => _ConversationDetailState(); _ConversationDetailState createState() => _ConversationDetailState();
} }
class _ConversationDetailState extends State<ConversationDetail> { class _ConversationDetailState extends State<ConversationDetail> {
List<Message> messages = [
];
List<Message> 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 @override
Widget build(BuildContext context) { 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: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
Text(
widget.conversation.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600),
),
],
),
),
const Icon(Icons.settings,color: Colors.black54,),
],
), ),
),
body: Stack(
children: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
);
},
),
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: <Widget>[
GestureDetector( 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 SizedBox(width: 15,),
const Expanded( 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,), const SizedBox(width: 15,),
FloatingActionButton( 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,
), ),
], ],
),
), ),
), ),
),
),
],
),
],
), ),
);
);
} }
} }

+ 1
- 0
mobile/lib/views/main/conversation_list.dart View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '/models/conversations.dart'; import '/models/conversations.dart';
import '/views/main/conversation_list_item.dart'; import '/views/main/conversation_list_item.dart';
import '/utils/storage/messages.dart';
class ConversationList extends StatefulWidget { class ConversationList extends StatefulWidget {
const ConversationList({Key? key}) : super(key: key); const ConversationList({Key? key}) : super(key: key);


+ 1
- 0
mobile/lib/views/main/conversation_list_item.dart View File

@ -15,6 +15,7 @@ class ConversationListItem extends StatefulWidget{
} }
class _ConversationListItemState extends State<ConversationListItem> { class _ConversationListItemState extends State<ConversationListItem> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(


+ 2
- 0
mobile/lib/views/main/home.dart View File

@ -5,6 +5,7 @@ import '/views/main/friend_list.dart';
import '/views/main/profile.dart'; import '/views/main/profile.dart';
import '/utils/storage/friends.dart'; import '/utils/storage/friends.dart';
import '/utils/storage/conversations.dart'; import '/utils/storage/conversations.dart';
import '/utils/storage/messages.dart';
class Home extends StatefulWidget { class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key); const Home({Key? key}) : super(key: key);
@ -24,6 +25,7 @@ class _HomeState extends State<Home> {
await checkLogin(); await checkLogin();
await updateFriends(); await updateFriends();
await updateConversations(); await updateConversations();
await updateMessageThreads();
} }
// TODO: Do server GET check here // TODO: Do server GET check here


+ 2
- 0
mobile/lib/views/main/profile.dart View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '/utils/storage/encryption_keys.dart'; import '/utils/storage/encryption_keys.dart';
import '/utils/storage/database.dart';
class Profile extends StatefulWidget { class Profile extends StatefulWidget {
const Profile({Key? key}) : super(key: key); const Profile({Key? key}) : super(key: key);
@ -34,6 +35,7 @@ class _ProfileState extends State<Profile> {
), ),
child: GestureDetector( child: GestureDetector(
onTap: () async { onTap: () async {
deleteDb();
final preferences = await SharedPreferences.getInstance(); final preferences = await SharedPreferences.getInstance();
preferences.setBool('islogin', false); preferences.setBool('islogin', false);
preferences.remove(rsaPrivateKeyName); preferences.remove(rsaPrivateKeyName);


Loading…
Cancel
Save