diff --git a/.gitignore b/.gitignore index e46bbea..f7fec7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -/mobile/nsconfig.json +/mobile/.env diff --git a/Backend/Api/Auth/Login.go b/Backend/Api/Auth/Login.go index d3e5116..39c374f 100644 --- a/Backend/Api/Auth/Login.go +++ b/Backend/Api/Auth/Login.go @@ -7,8 +7,6 @@ import ( "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" - - "github.com/gofrs/uuid" ) type Credentials struct { @@ -52,11 +50,11 @@ func makeLoginResponse(w http.ResponseWriter, code int, message, pubKey, privKey func Login(w http.ResponseWriter, r *http.Request) { var ( - creds Credentials - userData Models.User - sessionToken uuid.UUID - expiresAt time.Time - err error + creds Credentials + userData Models.User + session Models.Session + expiresAt time.Time + err error ) err = json.NewDecoder(r.Body).Decode(&creds) @@ -76,23 +74,22 @@ func Login(w http.ResponseWriter, r *http.Request) { return } - sessionToken, err = uuid.NewV4() - if err != nil { - makeLoginResponse(w, http.StatusInternalServerError, "An error occurred", "", "") - return - } + expiresAt = time.Now().Add(12 * time.Hour) - expiresAt = time.Now().Add(1 * time.Hour) + session = Models.Session{ + UserID: userData.ID, + Expiry: expiresAt, + } - Sessions[sessionToken.String()] = Session{ - UserID: userData.ID.String(), - Username: userData.Username, - Expiry: expiresAt, + err = Database.CreateSession(&session) + if err != nil { + makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "") + return } http.SetCookie(w, &http.Cookie{ Name: "session_token", - Value: sessionToken.String(), + Value: session.ID.String(), Expires: expiresAt, }) diff --git a/Backend/Api/Auth/Logout.go b/Backend/Api/Auth/Logout.go index 822b21d..486b575 100644 --- a/Backend/Api/Auth/Logout.go +++ b/Backend/Api/Auth/Logout.go @@ -1,8 +1,11 @@ package Auth import ( + "log" "net/http" "time" + + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" ) func Logout(w http.ResponseWriter, r *http.Request) { @@ -24,7 +27,10 @@ func Logout(w http.ResponseWriter, r *http.Request) { sessionToken = c.Value - delete(Sessions, sessionToken) + err = Database.DeleteSessionById(sessionToken) + if err != nil { + log.Println("Could not delete session cookie") + } http.SetCookie(w, &http.Cookie{ Name: "session_token", diff --git a/Backend/Api/Auth/Session.go b/Backend/Api/Auth/Session.go index f97bbaf..ffcfae2 100644 --- a/Backend/Api/Auth/Session.go +++ b/Backend/Api/Auth/Session.go @@ -3,32 +3,16 @@ package Auth import ( "errors" "net/http" - "time" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" ) -var ( - Sessions = map[string]Session{} -) - -type Session struct { - UserID string - Username string - Expiry time.Time -} - -func (s Session) IsExpired() bool { - return s.Expiry.Before(time.Now()) -} - -func CheckCookie(r *http.Request) (Session, error) { +func CheckCookie(r *http.Request) (Models.Session, error) { var ( c *http.Cookie sessionToken string - userSession Session - exists bool + userSession Models.Session err error ) @@ -39,15 +23,15 @@ func CheckCookie(r *http.Request) (Session, error) { sessionToken = c.Value // We then get the session from our session map - userSession, exists = Sessions[sessionToken] - if !exists { + userSession, err = Database.GetSessionById(sessionToken) + if err != nil { return userSession, errors.New("Cookie not found") } // If the session is present, but has expired, we can delete the session, and return // an unauthorized status if userSession.IsExpired() { - delete(Sessions, sessionToken) + Database.DeleteSession(&userSession) return userSession, errors.New("Cookie expired") } @@ -56,24 +40,15 @@ func CheckCookie(r *http.Request) (Session, error) { func CheckCookieCurrentUser(w http.ResponseWriter, r *http.Request) (Models.User, error) { var ( - userSession Session - userData Models.User - err error + session Models.Session + userData Models.User + err error ) - userSession, err = CheckCookie(r) + session, err = CheckCookie(r) if err != nil { return userData, err } - userData, err = Database.GetUserById(userSession.UserID) - if err != nil { - return userData, err - } - - if userData.ID.String() != userSession.UserID { - return userData, errors.New("Is not current user") - } - - return userData, nil + return session.User, nil } diff --git a/Backend/Api/Friends/EncryptedFriendsList.go b/Backend/Api/Friends/EncryptedFriendsList.go index 2db0531..d9f8d61 100644 --- a/Backend/Api/Friends/EncryptedFriendsList.go +++ b/Backend/Api/Friends/EncryptedFriendsList.go @@ -3,16 +3,18 @@ package Friends import ( "encoding/json" "net/http" + "net/url" + "strings" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" ) -func EncryptedFriendList(w http.ResponseWriter, r *http.Request) { +func EncryptedFriendRequestList(w http.ResponseWriter, r *http.Request) { var ( - userSession Auth.Session - friends []Models.Friend + userSession Models.Session + friends []Models.FriendRequest returnJson []byte err error ) @@ -23,7 +25,43 @@ func EncryptedFriendList(w http.ResponseWriter, r *http.Request) { return } - friends, err = Database.GetFriendsByUserId(userSession.UserID) + friends, err = Database.GetFriendRequestsByUserId(userSession.UserID.String()) + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + returnJson, err = json.MarshalIndent(friends, "", " ") + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(returnJson) +} + +func EncryptedFriendList(w http.ResponseWriter, r *http.Request) { + var ( + friends []Models.Friend + query url.Values + friendIds []string + returnJson []byte + ok bool + err error + ) + + query = r.URL.Query() + friendIds, ok = query["friend_ids"] + if !ok { + http.Error(w, "Invalid Data", http.StatusBadGateway) + return + } + + // TODO: Fix error handling here + friendIds = strings.Split(friendIds[0], ",") + + friends, err = Database.GetFriendsByIds(friendIds) if err != nil { http.Error(w, "Error", http.StatusInternalServerError) return diff --git a/Backend/Api/Friends/FriendRequest.go b/Backend/Api/Friends/FriendRequest.go index 4943f17..126605d 100644 --- a/Backend/Api/Friends/FriendRequest.go +++ b/Backend/Api/Friends/FriendRequest.go @@ -16,7 +16,7 @@ func FriendRequest(w http.ResponseWriter, r *http.Request) { requestBody []byte requestJson map[string]interface{} friendID string - friendRequest Models.Friend + friendRequest Models.FriendRequest ok bool err error ) @@ -45,7 +45,7 @@ func FriendRequest(w http.ResponseWriter, r *http.Request) { return } - friendRequest = Models.Friend{ + friendRequest = Models.FriendRequest{ UserID: user.ID, FriendID: friendID, } diff --git a/Backend/Api/Friends/Friends.go b/Backend/Api/Friends/Friends.go index a991fb6..050b4d8 100644 --- a/Backend/Api/Friends/Friends.go +++ b/Backend/Api/Friends/Friends.go @@ -35,10 +35,10 @@ func Friend(w http.ResponseWriter, r *http.Request) { func CreateFriendRequest(w http.ResponseWriter, r *http.Request) { var ( - friendData Models.Friend - requestBody []byte - returnJson []byte - err error + friendRequest Models.FriendRequest + requestBody []byte + returnJson []byte + err error ) requestBody, err = ioutil.ReadAll(r.Body) @@ -46,17 +46,17 @@ func CreateFriendRequest(w http.ResponseWriter, r *http.Request) { panic(err) } - err = json.Unmarshal(requestBody, &friendData) + err = json.Unmarshal(requestBody, &friendRequest) if err != nil { panic(err) } - err = Database.CreateFriendRequest(&friendData) + err = Database.CreateFriendRequest(&friendRequest) if err != nil { panic(err) } - returnJson, err = json.MarshalIndent(friendData, "", " ") + returnJson, err = json.MarshalIndent(friendRequest, "", " ") if err != nil { http.Error(w, "Error", http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError) diff --git a/Backend/Api/Messages/Conversations.go b/Backend/Api/Messages/Conversations.go new file mode 100644 index 0000000..c0beeee --- /dev/null +++ b/Backend/Api/Messages/Conversations.go @@ -0,0 +1,83 @@ +package Messages + +import ( + "encoding/json" + "net/http" + "net/url" + "strings" + + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" +) + +func EncryptedConversationList(w http.ResponseWriter, r *http.Request) { + var ( + userConversations []Models.UserConversation + userSession Models.Session + returnJson []byte + err error + ) + + userSession, err = Auth.CheckCookie(r) + if err != nil { + http.Error(w, "Forbidden", http.StatusUnauthorized) + return + } + + userConversations, err = Database.GetUserConversationsByUserId( + userSession.UserID.String(), + ) + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + returnJson, err = json.MarshalIndent(userConversations, "", " ") + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(returnJson) +} + +func EncryptedConversationDetailsList(w http.ResponseWriter, r *http.Request) { + var ( + userConversations []Models.ConversationDetail + query url.Values + conversationIds []string + returnJson []byte + ok bool + err error + ) + + query = r.URL.Query() + conversationIds, ok = query["conversation_detail_ids"] + if !ok { + http.Error(w, "Invalid Data", http.StatusBadGateway) + return + } + + // TODO: Fix error handling here + conversationIds = strings.Split(conversationIds[0], ",") + + userConversations, err = Database.GetConversationDetailsByIds( + conversationIds, + ) + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + returnJson, err = json.MarshalIndent(userConversations, "", " ") + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(returnJson) + +} diff --git a/Backend/Api/Messages/MessageThread.go b/Backend/Api/Messages/MessageThread.go index 8a86209..3fef1d3 100644 --- a/Backend/Api/Messages/MessageThread.go +++ b/Backend/Api/Messages/MessageThread.go @@ -4,29 +4,21 @@ import ( "encoding/json" "net/http" - "git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" "github.com/gorilla/mux" ) -func MessageThread(w http.ResponseWriter, r *http.Request) { +func ConversationDetail(w http.ResponseWriter, r *http.Request) { var ( - userData Models.User - messageThread Models.MessageThread - urlVars map[string]string - threadKey string - returnJson []byte - ok bool - err error + conversationDetail Models.ConversationDetail + urlVars map[string]string + threadKey string + returnJson []byte + ok bool + err error ) - userData, err = Auth.CheckCookieCurrentUser(w, r) - if !ok { - http.Error(w, "Forbidden", http.StatusUnauthorized) - return - } - urlVars = mux.Vars(r) threadKey, ok = urlVars["threadKey"] if !ok { @@ -34,13 +26,13 @@ func MessageThread(w http.ResponseWriter, r *http.Request) { return } - messageThread, err = Database.GetMessageThreadById(threadKey, userData) + conversationDetail, err = Database.GetConversationDetailById(threadKey) if !ok { http.Error(w, "Not Found", http.StatusNotFound) return } - returnJson, err = json.MarshalIndent(messageThread, "", " ") + returnJson, err = json.MarshalIndent(conversationDetail, "", " ") if err != nil { http.Error(w, "Error", http.StatusInternalServerError) return diff --git a/Backend/Api/Routes.go b/Backend/Api/Routes.go index a362154..d4c2cdb 100644 --- a/Backend/Api/Routes.go +++ b/Backend/Api/Routes.go @@ -60,11 +60,17 @@ func InitApiEndpoints(router *mux.Router) { authApi.Use(authenticationMiddleware) // Define routes for friends and friend requests - authApi.HandleFunc("/friend", Friends.CreateFriendRequest).Methods("POST") + authApi.HandleFunc("/friend_requests", Friends.EncryptedFriendRequestList).Methods("GET") + authApi.HandleFunc("/friend_request", Friends.CreateFriendRequest).Methods("POST") + authApi.HandleFunc("/friends", Friends.EncryptedFriendList).Methods("GET") - authApi.HandleFunc("/friend/{userID}", Friends.Friend).Methods("GET") - authApi.HandleFunc("/friend/{userID}/request", Friends.FriendRequest).Methods("POST") + + authApi.HandleFunc("/conversations", Messages.EncryptedConversationList).Methods("GET") + authApi.HandleFunc("/conversation_details", Messages.EncryptedConversationDetailsList).Methods("GET") + + // authApi.HandleFunc("/user/{userID}", Friends.Friend).Methods("GET") + // authApi.HandleFunc("/user/{userID}/request", Friends.FriendRequest).Methods("POST") // Define routes for messages - authApi.HandleFunc("/messages/{threadKey}", Messages.MessageThread).Methods("GET") + authApi.HandleFunc("/messages/{threadKey}", Messages.ConversationDetail).Methods("GET") } diff --git a/Backend/Database/ConversationDetails.go b/Backend/Database/ConversationDetails.go new file mode 100644 index 0000000..144e28c --- /dev/null +++ b/Backend/Database/ConversationDetails.go @@ -0,0 +1,55 @@ +package Database + +import ( + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +func GetConversationDetailById(id string) (Models.ConversationDetail, error) { + var ( + messageThread Models.ConversationDetail + err error + ) + + err = DB.Preload(clause.Associations). + Where("id = ?", id). + First(&messageThread). + Error + + return messageThread, err +} + +func GetConversationDetailsByIds(id []string) ([]Models.ConversationDetail, error) { + var ( + messageThread []Models.ConversationDetail + err error + ) + + err = DB.Preload(clause.Associations). + Where("id = ?", id). + First(&messageThread). + Error + + return messageThread, err +} + +func CreateConversationDetail(messageThread *Models.ConversationDetail) error { + return DB.Session(&gorm.Session{FullSaveAssociations: true}). + Create(messageThread). + Error +} + +func UpdateConversationDetail(messageThread *Models.ConversationDetail) error { + return DB.Session(&gorm.Session{FullSaveAssociations: true}). + Where("id = ?", messageThread.ID). + Updates(messageThread). + Error +} + +func DeleteConversationDetail(messageThread *Models.ConversationDetail) error { + return DB.Session(&gorm.Session{FullSaveAssociations: true}). + Delete(messageThread). + Error +} diff --git a/Backend/Database/FriendRequests.go b/Backend/Database/FriendRequests.go new file mode 100644 index 0000000..dec6860 --- /dev/null +++ b/Backend/Database/FriendRequests.go @@ -0,0 +1,47 @@ +package Database + +import ( + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +func GetFriendRequestById(id string) (Models.FriendRequest, error) { + var ( + friendRequest Models.FriendRequest + err error + ) + + err = DB.Preload(clause.Associations). + First(&friendRequest, "id = ?", id). + Error + + return friendRequest, err +} + +func GetFriendRequestsByUserId(userID string) ([]Models.FriendRequest, error) { + var ( + friends []Models.FriendRequest + err error + ) + + err = DB.Model(Models.FriendRequest{}). + Where("user_id = ?", userID). + Find(&friends). + Error + + return friends, err +} + +func CreateFriendRequest(FriendRequest *Models.FriendRequest) error { + return DB.Session(&gorm.Session{FullSaveAssociations: true}). + Create(FriendRequest). + Error +} + +func DeleteFriendRequest(FriendRequest *Models.FriendRequest) error { + return DB.Session(&gorm.Session{FullSaveAssociations: true}). + Delete(FriendRequest). + Error +} diff --git a/Backend/Database/Friends.go b/Backend/Database/Friends.go index d82ccab..102e657 100644 --- a/Backend/Database/Friends.go +++ b/Backend/Database/Friends.go @@ -9,39 +9,66 @@ import ( func GetFriendById(id string) (Models.Friend, error) { var ( - friend Models.Friend - err error + userData Models.Friend + err error ) err = DB.Preload(clause.Associations). - First(&friend, "id = ?", id). + First(&userData, "id = ?", id). Error - return friend, err + return userData, err } -func GetFriendsByUserId(userID string) ([]Models.Friend, error) { +func GetFriendsByIds(ids []string) ([]Models.Friend, error) { var ( - friends []Models.Friend - err error + userData []Models.Friend + err error ) - err = DB.Model(Models.Friend{}). - Where("user_id = ?", userID). - Find(&friends). + err = DB.Preload(clause.Associations). + Find(&userData, ids). Error - return friends, err + return userData, err } -func CreateFriendRequest(friend *Models.Friend) error { - return DB.Session(&gorm.Session{FullSaveAssociations: true}). - Create(friend). +func CreateFriend(userData *Models.Friend) error { + var ( + err error + ) + + err = DB.Session(&gorm.Session{FullSaveAssociations: true}). + Create(userData). Error + + return err +} + +func UpdateFriend(id string, userData *Models.Friend) error { + var ( + err error + ) + err = DB.Model(&userData). + Omit("id"). + Where("id = ?", id). + Updates(userData). + Error + + if err != nil { + return err + } + + err = DB.Model(Models.Friend{}). + Where("id = ?", id). + First(userData). + Error + + return err } -func DeleteFriend(friend *Models.Friend) error { +func DeleteFriend(userData *Models.Friend) error { return DB.Session(&gorm.Session{FullSaveAssociations: true}). - Delete(friend). + Delete(userData). Error } diff --git a/Backend/Database/Init.go b/Backend/Database/Init.go index 12f90ae..f8537d4 100644 --- a/Backend/Database/Init.go +++ b/Backend/Database/Init.go @@ -18,12 +18,14 @@ var ( func GetModels() []interface{} { return []interface{}{ + &Models.Session{}, &Models.User{}, &Models.Friend{}, + &Models.FriendRequest{}, &Models.MessageData{}, &Models.Message{}, - &Models.MessageThread{}, - &Models.MessageThreadUser{}, + &Models.ConversationDetail{}, + &Models.UserConversation{}, } } diff --git a/Backend/Database/MessageThreadUsers.go b/Backend/Database/MessageThreadUsers.go deleted file mode 100644 index 842bc15..0000000 --- a/Backend/Database/MessageThreadUsers.go +++ /dev/null @@ -1,39 +0,0 @@ -package Database - -import ( - "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" - - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -func GetMessageThreadUserById(id string) (Models.MessageThreadUser, error) { - var ( - message Models.MessageThreadUser - err error - ) - - err = DB.Preload(clause.Associations). - First(&message, "id = ?", id). - Error - - return message, err -} - -func CreateMessageThreadUser(messageThreadUser *Models.MessageThreadUser) error { - var ( - err error - ) - - err = DB.Session(&gorm.Session{FullSaveAssociations: true}). - Create(messageThreadUser). - Error - - return err -} - -func DeleteMessageThreadUser(messageThreadUser *Models.MessageThreadUser) error { - return DB.Session(&gorm.Session{FullSaveAssociations: true}). - Delete(messageThreadUser). - Error -} diff --git a/Backend/Database/MessageThreads.go b/Backend/Database/MessageThreads.go deleted file mode 100644 index be3ffc1..0000000 --- a/Backend/Database/MessageThreads.go +++ /dev/null @@ -1,42 +0,0 @@ -package Database - -import ( - "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" - - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -func GetMessageThreadById(id string, user Models.User) (Models.MessageThread, error) { - var ( - messageThread Models.MessageThread - err error - ) - - err = DB.Preload(clause.Associations). - Where("id = ?", id). - Where("user_id = ?", user.ID). - First(&messageThread). - Error - - return messageThread, err -} - -func CreateMessageThread(messageThread *Models.MessageThread) error { - return DB.Session(&gorm.Session{FullSaveAssociations: true}). - Create(messageThread). - Error -} - -func UpdateMessageThread(messageThread *Models.MessageThread) error { - return DB.Session(&gorm.Session{FullSaveAssociations: true}). - Where("id = ?", messageThread.ID). - Updates(messageThread). - Error -} - -func DeleteMessageThread(messageThread *Models.MessageThread) error { - return DB.Session(&gorm.Session{FullSaveAssociations: true}). - Delete(messageThread). - Error -} diff --git a/Backend/Database/Seeder/FriendSeeder.go b/Backend/Database/Seeder/FriendSeeder.go index bcacf9d..033b7a6 100644 --- a/Backend/Database/Seeder/FriendSeeder.go +++ b/Backend/Database/Seeder/FriendSeeder.go @@ -8,30 +8,43 @@ import ( "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" ) -func seedFriend(user, friendUser Models.User) error { +func seedFriend(userRequestTo, userRequestFrom Models.User) error { var ( - friend Models.Friend - err error + friendRequest Models.FriendRequest + decodedID []byte + id []byte + decodedSymKey []byte + symKey []byte + err error ) - friend = Models.Friend{ - UserID: user.ID, - FriendID: base64.StdEncoding.EncodeToString(encryptWithPublicKey([]byte(friendUser.ID.String()), decodedPublicKey)), - AcceptedAt: time.Now(), + decodedID, err = base64.StdEncoding.DecodeString(userRequestFrom.FriendID) + if err != nil { + return err + } + id, err = decryptWithPrivateKey(decodedID, decodedPrivateKey) + if err != nil { + return err } - err = Database.CreateFriendRequest(&friend) + decodedSymKey, err = base64.StdEncoding.DecodeString(userRequestFrom.FriendSymmetricKey) if err != nil { return err } + symKey, err = decryptWithPrivateKey(decodedSymKey, decodedPrivateKey) - friend = Models.Friend{ - UserID: friendUser.ID, - FriendID: base64.StdEncoding.EncodeToString(encryptWithPublicKey([]byte(user.ID.String()), decodedPublicKey)), + friendRequest = Models.FriendRequest{ + UserID: userRequestTo.ID, AcceptedAt: time.Now(), + FriendID: base64.StdEncoding.EncodeToString( + encryptWithPublicKey(id, decodedPublicKey), + ), + SymmetricKey: base64.StdEncoding.EncodeToString( + encryptWithPublicKey(symKey, decodedPublicKey), + ), } - return Database.CreateFriendRequest(&friend) + return Database.CreateFriendRequest(&friendRequest) } func SeedFriends() { @@ -53,6 +66,14 @@ func SeedFriends() { } err = seedFriend(primaryUser, secondaryUser) + if err != nil { + panic(err) + } + + err = seedFriend(secondaryUser, primaryUser) + if err != nil { + panic(err) + } for i = 0; i <= 3; i++ { secondaryUser, err = Database.GetUserByUsername(userNames[i]) @@ -64,5 +85,10 @@ func SeedFriends() { if err != nil { panic(err) } + + err = seedFriend(secondaryUser, primaryUser) + if err != nil { + panic(err) + } } } diff --git a/Backend/Database/Seeder/MessageSeeder.go b/Backend/Database/Seeder/MessageSeeder.go index 29de572..06cd78e 100644 --- a/Backend/Database/Seeder/MessageSeeder.go +++ b/Backend/Database/Seeder/MessageSeeder.go @@ -1,15 +1,8 @@ package Seeder import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "crypto/rsa" - "crypto/sha256" - "encoding/pem" + "encoding/base64" "fmt" - "hash" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" @@ -17,108 +10,49 @@ import ( "github.com/gofrs/uuid" ) -// EncryptWithPublicKey encrypts data with public key -func encryptWithPublicKey(msg []byte, pub *rsa.PublicKey) []byte { +func seedMessage( + primaryUser Models.User, + primaryUserThreadKey, secondaryUserThreadKey string, + thread Models.ConversationDetail, + i int, +) error { var ( - hash hash.Hash + message Models.Message + messageData Models.MessageData + key aesKey + plaintext string + dataCiphertext []byte + senderIdCiphertext []byte + err error ) - hash = sha256.New() - ciphertext, err := rsa.EncryptOAEP(hash, rand.Reader, pub, msg, nil) + key, err = generateAesKey() if err != nil { panic(err) } - return ciphertext -} -func PKCS5Padding(ciphertext []byte, blockSize int, after int) []byte { - var ( - padding int - padtext []byte - ) - padding = (blockSize - len(ciphertext)%blockSize) - padtext = bytes.Repeat([]byte{byte(padding)}, padding) - return append(ciphertext, padtext...) -} - -func generateAesKey() (*pem.Block, cipher.BlockMode) { - var ( - pemBlock *pem.Block - block cipher.Block - - bKey []byte - bIV []byte - - err error - ) + plaintext = "Test Message" - bKey = make([]byte, 32) - _, err = rand.Read(bKey) - if err != nil { - panic(err) - } - bIV = make([]byte, 16) - _, err = rand.Read(bIV) + dataCiphertext, err = key.aesEncrypt([]byte(plaintext)) if err != nil { panic(err) } - pemBlock = &pem.Block{ - Type: "AES KEY", - Bytes: bKey, - } - - block, err = aes.NewCipher(bKey) + senderIdCiphertext, err = key.aesEncrypt(primaryUser.ID.Bytes()) if err != nil { panic(err) } - return pemBlock, cipher.NewCBCEncrypter(block, bIV) -} - -func seedMessage( - primaryUser Models.User, - primaryUserThreadKey, secondaryUserThreadKey string, - thread Models.MessageThread, - i int, -) error { - var ( - message Models.Message - messageData Models.MessageData - - messagePemBlock *pem.Block - messageMode cipher.BlockMode - - plaintext string - dataCiphertext []byte - senderIdCiphertext []byte - - bPlaintext []byte - bSenderIdPlaintext []byte - - err error - ) - - plaintext = "Test Message" - bPlaintext = PKCS5Padding([]byte(plaintext), aes.BlockSize, len(plaintext)) - bSenderIdPlaintext = PKCS5Padding(primaryUser.ID.Bytes(), aes.BlockSize, len(primaryUser.ID.Bytes())) - - dataCiphertext = make([]byte, len(bPlaintext)) - senderIdCiphertext = make([]byte, len(bSenderIdPlaintext)) - - messagePemBlock, messageMode = generateAesKey() - - messageMode.CryptBlocks(dataCiphertext, bPlaintext) - messageMode.CryptBlocks(senderIdCiphertext, bSenderIdPlaintext) - messageData = Models.MessageData{ - Data: dataCiphertext, - SenderID: senderIdCiphertext, + Data: base64.StdEncoding.EncodeToString(dataCiphertext), + SenderID: base64.StdEncoding.EncodeToString(senderIdCiphertext), } message = Models.Message{ - MessageData: messageData, - SymmetricKey: encryptWithPublicKey(pem.EncodeToMemory(messagePemBlock), decodedPublicKey), + MessageData: messageData, + SymmetricKey: base64.StdEncoding.EncodeToString( + encryptWithPublicKey(key.Key, decodedPublicKey), + ), MessageThreadKey: primaryUserThreadKey, } @@ -130,8 +64,10 @@ func seedMessage( // The symmetric key would be encrypted with secondary users public key in production // But due to using the same pub/priv key pair for all users, we will just duplicate it message = Models.Message{ - MessageDataID: message.MessageDataID, - SymmetricKey: encryptWithPublicKey(pem.EncodeToMemory(messagePemBlock), decodedPublicKey), + MessageDataID: message.MessageDataID, + SymmetricKey: base64.StdEncoding.EncodeToString( + encryptWithPublicKey(key.Key, decodedPublicKey), + ), MessageThreadKey: secondaryUserThreadKey, } @@ -143,107 +79,96 @@ func seedMessage( return err } -func seedMessageThread(threadPemBlock *pem.Block, threadMode cipher.BlockMode) (Models.MessageThread, error) { +func seedConversationDetail(key aesKey) (Models.ConversationDetail, error) { var ( - messageThread Models.MessageThread - + messageThread Models.ConversationDetail name string - bNamePlaintext []byte nameCiphertext []byte - - err error + err error ) name = "Test Conversation" - bNamePlaintext = PKCS5Padding([]byte(name), aes.BlockSize, len(name)) - nameCiphertext = make([]byte, len(bNamePlaintext)) - - threadMode.CryptBlocks(nameCiphertext, bNamePlaintext) + nameCiphertext, err = key.aesEncrypt([]byte(name)) + if err != nil { + panic(err) + } - messageThread = Models.MessageThread{ - Name: nameCiphertext, + messageThread = Models.ConversationDetail{ + Name: base64.StdEncoding.EncodeToString(nameCiphertext), } - err = Database.CreateMessageThread(&messageThread) + err = Database.CreateConversationDetail(&messageThread) return messageThread, err } -func seedUpdateMessageThreadUsers( +func seedUpdateUserConversation( userJson string, - threadPemBlock *pem.Block, - threadMode cipher.BlockMode, - messageThread Models.MessageThread, -) (Models.MessageThread, error) { + key aesKey, + messageThread Models.ConversationDetail, +) (Models.ConversationDetail, error) { var ( - bUsersPlaintext []byte usersCiphertext []byte err error ) - bUsersPlaintext = PKCS5Padding([]byte(userJson), aes.BlockSize, len(userJson)) - usersCiphertext = make([]byte, len(bUsersPlaintext)) - - threadMode.CryptBlocks(usersCiphertext, bUsersPlaintext) + usersCiphertext, err = key.aesEncrypt([]byte(userJson)) + if err != nil { + return messageThread, err + } - messageThread.Users = usersCiphertext - err = Database.UpdateMessageThread(&messageThread) + messageThread.Users = base64.StdEncoding.EncodeToString(usersCiphertext) + err = Database.UpdateConversationDetail(&messageThread) return messageThread, err } -func seedMessageThreadUser( +func seedUserConversation( user Models.User, threadID uuid.UUID, messageThreadKey string, - threadPemBlock *pem.Block, - threadMode cipher.BlockMode, -) (Models.MessageThreadUser, error) { + key aesKey, +) (Models.UserConversation, error) { var ( - messageThreadUser Models.MessageThreadUser - - bThreadIdPlaintext []byte + messageThreadUser Models.UserConversation threadIdCiphertext []byte - - bKeyPlaintext []byte - keyCiphertext []byte - - bAdminPlaintext []byte - adminCiphertext []byte - - err error + keyCiphertext []byte + adminCiphertext []byte + err error ) - bThreadIdPlaintext = PKCS5Padding(threadID.Bytes(), aes.BlockSize, len(threadID.String())) - threadIdCiphertext = make([]byte, len(bThreadIdPlaintext)) - - bKeyPlaintext = PKCS5Padding([]byte(messageThreadKey), aes.BlockSize, len(messageThreadKey)) - keyCiphertext = make([]byte, len(bKeyPlaintext)) + threadIdCiphertext, err = key.aesEncrypt([]byte(threadID.String())) + if err != nil { + return messageThreadUser, err + } - bAdminPlaintext = PKCS5Padding([]byte("true"), aes.BlockSize, len("true")) - adminCiphertext = make([]byte, len(bAdminPlaintext)) + keyCiphertext, err = key.aesEncrypt([]byte(messageThreadKey)) + if err != nil { + return messageThreadUser, err + } - threadMode.CryptBlocks(threadIdCiphertext, bThreadIdPlaintext) - threadMode.CryptBlocks(keyCiphertext, bKeyPlaintext) - threadMode.CryptBlocks(adminCiphertext, bAdminPlaintext) + adminCiphertext, err = key.aesEncrypt([]byte("true")) + if err != nil { + return messageThreadUser, err + } - messageThreadUser = Models.MessageThreadUser{ - UserID: user.ID, - MessageThreadID: threadIdCiphertext, - MessageThreadKey: keyCiphertext, - Admin: adminCiphertext, - SymmetricKey: encryptWithPublicKey(pem.EncodeToMemory(threadPemBlock), decodedPublicKey), + messageThreadUser = Models.UserConversation{ + UserID: user.ID, + ConversationDetailID: base64.StdEncoding.EncodeToString(threadIdCiphertext), + MessageThreadKey: base64.StdEncoding.EncodeToString(keyCiphertext), + Admin: base64.StdEncoding.EncodeToString(adminCiphertext), + SymmetricKey: base64.StdEncoding.EncodeToString( + encryptWithPublicKey(key.Key, decodedPublicKey), + ), } - err = Database.CreateMessageThreadUser(&messageThreadUser) + err = Database.CreateUserConversation(&messageThreadUser) return messageThreadUser, err } func SeedMessages() { var ( - messageThread Models.MessageThread - threadPemBlock *pem.Block - threadMode cipher.BlockMode - + messageThread Models.ConversationDetail + key aesKey primaryUser Models.User primaryUserThreadKey string secondaryUser Models.User @@ -251,13 +176,13 @@ func SeedMessages() { userJson string - thread Models.MessageThread + thread Models.ConversationDetail i int err error ) - threadPemBlock, threadMode = generateAesKey() - messageThread, err = seedMessageThread(threadPemBlock, threadMode) + key, err = generateAesKey() + messageThread, err = seedConversationDetail(key) primaryUserThreadKey = Util.RandomString(32) secondaryUserThreadKey = Util.RandomString(32) @@ -266,12 +191,11 @@ func SeedMessages() { panic(err) } - _, err = seedMessageThreadUser( + _, err = seedUserConversation( primaryUser, messageThread.ID, primaryUserThreadKey, - threadPemBlock, - threadMode, + key, ) secondaryUser, err = Database.GetUserByUsername("testUser2") @@ -279,12 +203,11 @@ func SeedMessages() { panic(err) } - _, err = seedMessageThreadUser( + _, err = seedUserConversation( secondaryUser, messageThread.ID, secondaryUserThreadKey, - threadPemBlock, - threadMode, + key, ) userJson = fmt.Sprintf( @@ -308,10 +231,9 @@ func SeedMessages() { secondaryUser.Username, ) - messageThread, err = seedUpdateMessageThreadUsers( + messageThread, err = seedUpdateUserConversation( userJson, - threadPemBlock, - threadMode, + key, messageThread, ) diff --git a/Backend/Database/Seeder/Seed.go b/Backend/Database/Seeder/Seed.go index 28825f4..7e9a373 100644 --- a/Backend/Database/Seeder/Seed.go +++ b/Backend/Database/Seeder/Seed.go @@ -1,9 +1,7 @@ package Seeder import ( - "crypto/rand" "crypto/rsa" - "crypto/sha256" "crypto/x509" "encoding/pem" "errors" @@ -56,20 +54,10 @@ ZQIDAQAB ) var ( - decodedPublicKey *rsa.PublicKey - // decodedPrivateKey *rsa.PrivateKey + decodedPublicKey *rsa.PublicKey + decodedPrivateKey *rsa.PrivateKey ) -// DecryptWithPrivateKey decrypts data with private key -func decryptWithPrivateKey(ciphertext []byte, priv *rsa.PrivateKey) []byte { - hash := sha256.New() - plaintext, err := rsa.DecryptOAEP(hash, rand.Reader, priv, ciphertext, nil) - if err != nil { - panic(err) - } - return plaintext -} - func Seed() { var ( block *pem.Block @@ -88,6 +76,16 @@ func Seed() { panic(errors.New("Invalid decodedPublicKey")) } + block, _ = pem.Decode([]byte(privateKey)) + decKey, err = x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + panic(err) + } + decodedPrivateKey, ok = decKey.(*rsa.PrivateKey) + if !ok { + panic(errors.New("Invalid decodedPrivateKey")) + } + log.Println("Seeding users...") SeedUsers() diff --git a/Backend/Database/Seeder/UserSeeder.go b/Backend/Database/Seeder/UserSeeder.go index fd7cc7e..b9e12e6 100644 --- a/Backend/Database/Seeder/UserSeeder.go +++ b/Backend/Database/Seeder/UserSeeder.go @@ -1,6 +1,8 @@ package Seeder import ( + "encoding/base64" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" @@ -22,9 +24,12 @@ var userNames = []string{ func createUser(username string) (Models.User, error) { var ( - userData Models.User - password string - err error + userData Models.User + key aesKey + publicUserData Models.Friend + password string + usernameCiphertext []byte + err error ) password, err = Auth.HashPassword("password") @@ -32,11 +37,40 @@ func createUser(username string) (Models.User, error) { return Models.User{}, err } + key, err = generateAesKey() + if err != nil { + return Models.User{}, err + } + + usernameCiphertext, err = key.aesEncrypt([]byte(username)) + if err != nil { + return Models.User{}, err + } + + publicUserData = Models.Friend{ + Username: base64.StdEncoding.EncodeToString(usernameCiphertext), + AsymmetricPublicKey: base64.StdEncoding.EncodeToString([]byte(publicKey)), + } + + err = Database.CreateFriend(&publicUserData) + if err != nil { + return userData, err + } + userData = Models.User{ Username: username, Password: password, AsymmetricPrivateKey: encryptedPrivateKey, AsymmetricPublicKey: publicKey, + FriendID: base64.StdEncoding.EncodeToString( + encryptWithPublicKey( + []byte(publicUserData.ID.String()), + decodedPublicKey, + ), + ), + FriendSymmetricKey: base64.StdEncoding.EncodeToString( + encryptWithPublicKey(key.Key, decodedPublicKey), + ), } err = Database.CreateUser(&userData) diff --git a/Backend/Database/Seeder/encryption.go b/Backend/Database/Seeder/encryption.go new file mode 100644 index 0000000..a116134 --- /dev/null +++ b/Backend/Database/Seeder/encryption.go @@ -0,0 +1,188 @@ +package Seeder + +// THIS FILE IS ONLY USED FOR SEEDING DATA DURING DEVELOPMENT + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" + "fmt" + "hash" + + "golang.org/x/crypto/pbkdf2" +) + +type aesKey struct { + Key []byte + Iv []byte +} + +func (key aesKey) encode() string { + return base64.StdEncoding.EncodeToString(key.Key) +} + +// Appends padding. +func pkcs7Padding(data []byte, blocklen int) ([]byte, error) { + var ( + padlen int = 1 + pad []byte + ) + if blocklen <= 0 { + return nil, fmt.Errorf("invalid blocklen %d", blocklen) + } + + for ((len(data) + padlen) % blocklen) != 0 { + padlen = padlen + 1 + } + + pad = bytes.Repeat([]byte{byte(padlen)}, padlen) + return append(data, pad...), nil +} + +// pkcs7strip remove pkcs7 padding +func pkcs7strip(data []byte, blockSize int) ([]byte, error) { + var ( + length int + padLen int + ref []byte + ) + + length = len(data) + if length == 0 { + return nil, fmt.Errorf("pkcs7: Data is empty") + } + + if (length % blockSize) != 0 { + return nil, fmt.Errorf("pkcs7: Data is not block-aligned") + } + + padLen = int(data[length-1]) + ref = bytes.Repeat([]byte{byte(padLen)}, padLen) + + if padLen > blockSize || padLen == 0 || !bytes.HasSuffix(data, ref) { + return nil, fmt.Errorf("pkcs7: Invalid padding") + } + + return data[:length-padLen], nil +} + +func generateAesKey() (aesKey, error) { + var ( + saltBytes []byte = []byte{} + password []byte + seed []byte + iv []byte + err error + ) + + password = make([]byte, 64) + _, err = rand.Read(password) + if err != nil { + return aesKey{}, err + } + + seed = make([]byte, 64) + _, err = rand.Read(seed) + if err != nil { + return aesKey{}, err + } + + iv = make([]byte, 16) + _, err = rand.Read(iv) + if err != nil { + return aesKey{}, err + } + + return aesKey{ + Key: pbkdf2.Key( + password, + saltBytes, + 1000, + 32, + func() hash.Hash { return hmac.New(sha256.New, seed) }, + ), + Iv: iv, + }, nil +} + +func (key aesKey) aesEncrypt(plaintext []byte) ([]byte, error) { + var ( + bPlaintext []byte + ciphertext []byte + block cipher.Block + err error + ) + + bPlaintext, err = pkcs7Padding(plaintext, 16) + + block, err = aes.NewCipher(key.Key) + if err != nil { + return []byte{}, err + } + + ciphertext = make([]byte, len(bPlaintext)) + mode := cipher.NewCBCEncrypter(block, key.Iv) + mode.CryptBlocks(ciphertext, bPlaintext) + + ciphertext = append(key.Iv, ciphertext...) + + return ciphertext, nil +} + +func (key aesKey) aesDecrypt(ciphertext []byte) ([]byte, error) { + var ( + plaintext []byte + iv []byte + block cipher.Block + err error + ) + + iv = ciphertext[:aes.BlockSize] + plaintext = ciphertext[aes.BlockSize:] + + block, err = aes.NewCipher(key.Key) + if err != nil { + return []byte{}, err + } + + decMode := cipher.NewCBCDecrypter(block, iv) + decMode.CryptBlocks(plaintext, plaintext) + + return plaintext, nil +} + +// EncryptWithPublicKey encrypts data with public key +func encryptWithPublicKey(msg []byte, pub *rsa.PublicKey) []byte { + var ( + hash hash.Hash + ) + + hash = sha256.New() + ciphertext, err := rsa.EncryptOAEP(hash, rand.Reader, pub, msg, nil) + if err != nil { + panic(err) + } + return ciphertext +} + +// DecryptWithPrivateKey decrypts data with private key +func decryptWithPrivateKey(ciphertext []byte, priv *rsa.PrivateKey) ([]byte, error) { + var ( + hash hash.Hash + plaintext []byte + err error + ) + + hash = sha256.New() + + plaintext, err = rsa.DecryptOAEP(hash, rand.Reader, priv, ciphertext, nil) + if err != nil { + return plaintext, err + } + return plaintext, nil +} diff --git a/Backend/Database/Sessions.go b/Backend/Database/Sessions.go new file mode 100644 index 0000000..1f125df --- /dev/null +++ b/Backend/Database/Sessions.go @@ -0,0 +1,38 @@ +package Database + +import ( + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" + + "gorm.io/gorm/clause" +) + +func GetSessionById(id string) (Models.Session, error) { + var ( + session Models.Session + err error + ) + + err = DB.Preload(clause.Associations). + First(&session, "id = ?", id). + Error + + return session, err +} + +func CreateSession(session *Models.Session) error { + var ( + err error + ) + + err = DB.Create(session).Error + + return err +} + +func DeleteSession(session *Models.Session) error { + return DB.Delete(session).Error +} + +func DeleteSessionById(id string) error { + return DB.Delete(&Models.Session{}, id).Error +} diff --git a/Backend/Database/UserConversations.go b/Backend/Database/UserConversations.go new file mode 100644 index 0000000..8e5490b --- /dev/null +++ b/Backend/Database/UserConversations.go @@ -0,0 +1,49 @@ +package Database + +import ( + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" + + "gorm.io/gorm" +) + +func GetUserConversationById(id string) (Models.UserConversation, error) { + var ( + message Models.UserConversation + err error + ) + + err = DB.First(&message, "id = ?", id). + Error + + return message, err +} + +func GetUserConversationsByUserId(id string) ([]Models.UserConversation, error) { + var ( + conversations []Models.UserConversation + err error + ) + + err = DB.Find(&conversations, "user_id = ?", id). + Error + + return conversations, err +} + +func CreateUserConversation(messageThreadUser *Models.UserConversation) error { + var ( + err error + ) + + err = DB.Session(&gorm.Session{FullSaveAssociations: true}). + Create(messageThreadUser). + Error + + return err +} + +func DeleteUserConversation(messageThreadUser *Models.UserConversation) error { + return DB.Session(&gorm.Session{FullSaveAssociations: true}). + Delete(messageThreadUser). + Error +} diff --git a/Backend/Models/Friends.go b/Backend/Models/Friends.go index b435223..5022d9b 100644 --- a/Backend/Models/Friends.go +++ b/Backend/Models/Friends.go @@ -6,11 +6,19 @@ import ( "github.com/gofrs/uuid" ) -// Set with User being the requestee, and FriendID being the requester +// TODO: Add profile picture type Friend struct { Base - UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"` - User User `json:"user"` - FriendID string `gorm:"not null" json:"friend_id"` // Stored encrypted - AcceptedAt time.Time `json:"accepted_at"` + Username string `gorm:"not null" json:"username"` // Stored encrypted + AsymmetricPublicKey string `gorm:"not null" json:"asymmetric_public_key"` // Stored encrypted +} + +// Set with Friend being the requestee, and RequestFromID being the requester +type FriendRequest struct { + Base + UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"` + User User `json:"user"` + FriendID string `gorm:"not null" json:"friend_id"` // Stored encrypted + SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted + AcceptedAt time.Time `json:"accepted_at"` } diff --git a/Backend/Models/Messages.go b/Backend/Models/Messages.go index 6e1893e..6c3d5a3 100644 --- a/Backend/Models/Messages.go +++ b/Backend/Models/Messages.go @@ -2,32 +2,34 @@ package Models import "github.com/gofrs/uuid" +// TODO: Add support for images type MessageData struct { Base - Data []byte `gorm:"not null" json:"data"` // Stored encrypted - SenderID []byte `gorm:"not null" json:"sender_id"` + Data string `gorm:"not null" json:"data"` // Stored encrypted + SenderID string `gorm:"not null" json:"sender_id"` } type Message struct { Base MessageDataID uuid.UUID `json:"-"` MessageData MessageData `json:"message_data"` - SymmetricKey []byte `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"` } -type MessageThread struct { +// TODO: Rename to ConversationDetails +type ConversationDetail struct { Base - Name []byte `gorm:"not null" json:"name"` - Users []byte `json:"users"` + Name string `gorm:"not null" json:"name"` + Users string `json:"users"` // Stored as encrypted JSON } -type MessageThreadUser struct { +type UserConversation struct { Base - UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"` - User User `json:"user"` - MessageThreadID []byte `gorm:"not null" json:"message_thread_link_id"` - MessageThreadKey []byte `gorm:"not null" json:"message_thread_key"` - Admin []byte `gorm:"not null" json:"admin"` // Bool if user is admin of thread, stored encrypted - SymmetricKey []byte `gorm:"not null" json:"symmetric_key"` // Stored encrypted + UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"` + User User `json:"user"` + ConversationDetailID string `gorm:"not null" json:"conversation_detail_id"` // Stored encrypted + MessageThreadKey string `gorm:"not null" json:"message_thread_key"` // Stored encrypted + Admin string `gorm:"not null" json:"admin"` // Bool if user is admin of thread, stored encrypted + SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted } diff --git a/Backend/Models/Sessions.go b/Backend/Models/Sessions.go new file mode 100644 index 0000000..1f2e215 --- /dev/null +++ b/Backend/Models/Sessions.go @@ -0,0 +1,18 @@ +package Models + +import ( + "time" + + "github.com/gofrs/uuid" +) + +func (s Session) IsExpired() bool { + return s.Expiry.Before(time.Now()) +} + +type Session struct { + Base + UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;"` + User User + Expiry time.Time +} diff --git a/Backend/Models/Users.go b/Backend/Models/Users.go index 1c688c3..0525b52 100644 --- a/Backend/Models/Users.go +++ b/Backend/Models/Users.go @@ -20,4 +20,6 @@ type User struct { ConfirmPassword string `gorm:"-" json:"confirm_password"` AsymmetricPrivateKey string `gorm:"not null" json:"asymmetric_private_key"` // Stored encrypted AsymmetricPublicKey string `gorm:"not null" json:"asymmetric_public_key"` + FriendID string `gorm:"not null" json:"public_user_id"` // Stored encrypted + FriendSymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted } diff --git a/mobile/lib/components/custom_circle_avatar.dart b/mobile/lib/components/custom_circle_avatar.dart new file mode 100644 index 0000000..8492623 --- /dev/null +++ b/mobile/lib/components/custom_circle_avatar.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +class CustomCircleAvatar extends StatefulWidget { + final String initials; + final String? imagePath; + + const CustomCircleAvatar({ + Key? key, + required this.initials, + this.imagePath, + }) : super(key: key); + + @override + _CustomCircleAvatarState createState() => _CustomCircleAvatarState(); +} + +class _CustomCircleAvatarState extends State{ + + bool _checkLoading = true; + + @override + void initState() { + super.initState(); + if (widget.imagePath != null) { + _checkLoading = false; + } + } + + @override + Widget build(BuildContext context) { + return _checkLoading == true ? + CircleAvatar( + backgroundColor: Colors.grey[300], + child: Text(widget.initials) + ) : CircleAvatar( + backgroundImage: AssetImage(widget.imagePath!) + ); + } +} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 8bad991..0bd22e7 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import '/views/main/home.dart'; import '/views/authentication/unauthenticated_landing.dart'; import '/views/authentication/login.dart'; import '/views/authentication/signup.dart'; -void main() { +void main() async { + await dotenv.load(fileName: ".env"); runApp(const MyApp()); } diff --git a/mobile/lib/models/conversations.dart b/mobile/lib/models/conversations.dart index 89a5373..8c4b996 100644 --- a/mobile/lib/models/conversations.dart +++ b/mobile/lib/models/conversations.dart @@ -1,30 +1,116 @@ -const messageTypeSender = 'sender'; -const messageTypeReceiver = 'receiver'; +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'; -class Message { + +Conversation findConversationByDetailId(List conversations, String id) { + for (var conversation in conversations) { + if (conversation.conversationDetailId == id) { + return conversation; + } + } + // Or return `null`. + throw ArgumentError.value(id, "id", "No element with that id"); +} + +class Conversation { String id; - String conversationId; + String userId; + String conversationDetailId; + String messageThreadKey; String symmetricKey; - String data; - String messageType; - String? decryptedData; - Message({ + bool admin; + String name; + String? users; + + Conversation({ required this.id, - required this.conversationId, + required this.userId, + required this.conversationDetailId, + required this.messageThreadKey, required this.symmetricKey, - required this.data, - required this.messageType, - this.decryptedData, + required this.admin, + required this.name, + this.users, }); + + + factory Conversation.fromJson(Map json, RSAPrivateKey privKey) { + var symmetricKeyDecrypted = CryptoUtils.rsaDecrypt( + base64.decode(json['symmetric_key']), + privKey, + ); + + var detailId = AesHelper.aesDecrypt( + symmetricKeyDecrypted, + base64.decode(json['conversation_detail_id']), + ); + + var threadKey = AesHelper.aesDecrypt( + symmetricKeyDecrypted, + base64.decode(json['message_thread_key']), + ); + + var admin = AesHelper.aesDecrypt( + symmetricKeyDecrypted, + base64.decode(json['admin']), + ); + + return Conversation( + id: json['id'], + userId: json['user_id'], + conversationDetailId: detailId, + messageThreadKey: threadKey, + symmetricKey: base64.encode(symmetricKeyDecrypted), + admin: admin == 'true', + name: 'Unknown', + ); + } + + @override + String toString() { + return ''' + + +id: $id +userId: $userId +name: $name +admin: $admin'''; + } + + Map toMap() { + return { + 'id': id, + 'user_id': userId, + 'conversation_detail_id': conversationDetailId, + 'message_thread_key': messageThreadKey, + 'symmetric_key': symmetricKey, + 'admin': admin ? 1 : 0, + 'name': name, + 'users': users, + }; + } } -class Conversation { - String id; - String friendId; - String recentMessageId; - Conversation({ - required this.id, - required this.friendId, - required this.recentMessageId, + +// A method that retrieves all the dogs from the dogs table. +Future> getConversations() async { + final db = await getDatabaseConnection(); + + final List> maps = await db.query('conversations'); + + return List.generate(maps.length, (i) { + return Conversation( + id: maps[i]['id'], + userId: maps[i]['user_id'], + conversationDetailId: maps[i]['conversation_detail_id'], + messageThreadKey: maps[i]['message_thread_key'], + symmetricKey: maps[i]['symmetric_key'], + admin: maps[i]['admin'] == 1, + name: maps[i]['name'], + users: maps[i]['users'], + ); }); } diff --git a/mobile/lib/models/friends.dart b/mobile/lib/models/friends.dart index 8afdcb4..0fcb23c 100644 --- a/mobile/lib/models/friends.dart +++ b/mobile/lib/models/friends.dart @@ -1,32 +1,57 @@ import 'dart:convert'; import "package:pointycastle/export.dart"; 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; + } + } + // 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 }); factory Friend.fromJson(Map json, RSAPrivateKey privKey) { + // TODO: Remove encrypted entries var friendIdDecrypted = CryptoUtils.rsaDecrypt( base64.decode(json['friend_id']), 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'], ); } @@ -34,21 +59,46 @@ class Friend{ @override String toString() { return ''' + + id: $id -userId: $userId, -friendId: $friendId, -friendIdDecrypted: $friendIdDecrypted, -accepted_at: $acceptedAt, -'''; +userId: $userId +username: $username +friendIdDecrypted: $friendIdDecrypted +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, }; } } + + +// A method that retrieves all the dogs from the dogs table. +Future> getFriends() async { + final db = await getDatabaseConnection(); + + 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'], + ); + }); +} diff --git a/mobile/lib/models/messages.dart b/mobile/lib/models/messages.dart new file mode 100644 index 0000000..26cb0f4 --- /dev/null +++ b/mobile/lib/models/messages.dart @@ -0,0 +1,20 @@ + +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, + }); +} diff --git a/mobile/lib/utils/encryption/aes_helper.dart b/mobile/lib/utils/encryption/aes_helper.dart index 04f2ece..adad897 100644 --- a/mobile/lib/utils/encryption/aes_helper.dart +++ b/mobile/lib/utils/encryption/aes_helper.dart @@ -60,9 +60,16 @@ class AesHelper { return Uint8List(len)..setRange(0, len, src); } - static String aesEncrypt(String password, Uint8List plaintext, + static String aesEncrypt(dynamic password, Uint8List plaintext, {String mode = cbcMode}) { - Uint8List derivedKey = deriveKey(password); + + Uint8List derivedKey; + if (password is String) { + derivedKey = deriveKey(password); + } else { + derivedKey = password; + } + KeyParameter keyParam = KeyParameter(derivedKey); BlockCipher aes = AESEngine(); @@ -93,9 +100,16 @@ class AesHelper { return base64.encode(cipherIvBytes); } - static String aesDecrypt(String password, Uint8List ciphertext, + static String aesDecrypt(dynamic password, Uint8List ciphertext, {String mode = cbcMode}) { - Uint8List derivedKey = deriveKey(password); + + Uint8List derivedKey; + if (password is String) { + derivedKey = deriveKey(password); + } else { + derivedKey = password; + } + KeyParameter keyParam = KeyParameter(derivedKey); BlockCipher aes = AESEngine(); diff --git a/mobile/lib/utils/storage/conversations.dart b/mobile/lib/utils/storage/conversations.dart new file mode 100644 index 0000000..b2c5266 --- /dev/null +++ b/mobile/lib/utils/storage/conversations.dart @@ -0,0 +1,79 @@ +import 'dart:convert'; +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 '/models/conversations.dart'; +import '/utils/storage/database.dart'; +import '/utils/storage/session_cookie.dart'; +import '/utils/storage/encryption_keys.dart'; +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) { + throw Exception(resp.body); + } + + List conversations = []; + List conversationsDetailIds = []; + + List conversationsJson = jsonDecode(resp.body); + + for (var i = 0; i < conversationsJson.length; i++) { + Conversation conversation = Conversation.fromJson( + 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); + + resp = await http.get( + uri, + headers: { + 'cookie': await getSessionCookie(), + } + ); + + if (resp.statusCode != 200) { + throw Exception(resp.body); + } + + final db = await getDatabaseConnection(); + + 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']), + ); + + conversation.users = AesHelper.aesDecrypt( + base64.decode(conversation.symmetricKey), + base64.decode(conversationDetailJson['users']), + ); + + await db.insert( + 'conversations', + conversation.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } +} diff --git a/mobile/lib/utils/storage/database.dart b/mobile/lib/utils/storage/database.dart index 0b306c4..3de37f6 100644 --- a/mobile/lib/utils/storage/database.dart +++ b/mobile/lib/utils/storage/database.dart @@ -6,29 +6,44 @@ import 'package:sqflite/sqflite.dart'; Future getDatabaseConnection() async { WidgetsFlutterBinding.ensureInitialized(); + final path = join(await getDatabasesPath(), 'envelope.db'); + final database = openDatabase( - // Set the path to the database. Note: Using the `join` function from the - // `path` package is best practice to ensure the path is correctly - // constructed for each platform. - join(await getDatabasesPath(), 'envelope.db'), - // When the database is first created, create a table to store dogs. - onCreate: (db, version) { - // Run the CREATE TABLE statement on the database. - return db.execute( - ''' - CREATE TABLE IF NOT EXISTS friends( - id BLOB PRIMARY KEY, - user_id BLOB, - friend_id BLOB, - friend_id_decrypted BLOB, - accepted_at TEXT - ); - ''', - ); - }, - // Set the version. This executes the onCreate function and provides a - // path to perform database upgrades and downgrades. - version: 1, + path, + // TODO: remove friend_id_decrypted and symmetric_key_decrypted + onCreate: (db, version) async { + await db.execute( + ''' + CREATE TABLE IF NOT EXISTS friends( + id TEXT PRIMARY KEY, + user_id TEXT, + username TEXT, + friend_id TEXT, + friend_id_decrypted TEXT, + symmetric_key TEXT, + symmetric_key_decrypted TEXT, + accepted_at TEXT + ); + '''); + + // TODO: Change users to use its own table, as it is a json blob + await db.execute( + ''' + CREATE TABLE IF NOT EXISTS conversations( + id TEXT PRIMARY KEY, + user_id TEXT, + conversation_detail_id TEXT, + message_thread_key TEXT, + symmetric_key TEXT, + admin INTEGER, + name TEXT, + users TEXT + ); + '''); + }, + // Set the version. This executes the onCreate function and provides a + // path to perform database upgrades and downgrades. + version: 1, ); return database; diff --git a/mobile/lib/utils/storage/friends.dart b/mobile/lib/utils/storage/friends.dart index 3e564ae..b0374a2 100644 --- a/mobile/lib/utils/storage/friends.dart +++ b/mobile/lib/utils/storage/friends.dart @@ -1,16 +1,19 @@ -// import 'package:sqflite/sqflite.dart'; -import 'package:http/http.dart' as http; import 'dart:convert'; -import "package:pointycastle/export.dart"; +import 'package:http/http.dart' as http; +import 'package:pointycastle/export.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import '/models/friends.dart'; +import '/utils/storage/database.dart'; import '/utils/storage/encryption_keys.dart'; import '/utils/storage/session_cookie.dart'; +import '/utils/encryption/aes_helper.dart'; -void getFriends() async { +Future updateFriends() async { RSAPrivateKey privKey = await getPrivateKey(); - final resp = await http.get( - Uri.parse('http://192.168.1.5:8080/api/v1/auth/friends'), + var resp = await http.get( + Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/friend_requests'), headers: { 'cookie': await getSessionCookie(), } @@ -19,20 +22,56 @@ void getFriends() async { if (resp.statusCode != 200) { throw Exception(resp.body); } - + List friends = []; + List friendIds = []; - List friendsJson = jsonDecode(resp.body); + List friendsRequestJson = jsonDecode(resp.body); - for (var i = 0; i < friendsJson.length; i++) { + for (var i = 0; i < friendsRequestJson.length; i++) { friends.add( Friend.fromJson( - friendsJson[i] as Map, + friendsRequestJson[i] as Map, privKey, ) ); + + friendIds.add(friends[i].friendIdDecrypted); + } + + Map params = {}; + params['friend_ids'] = friendIds.join(','); + var uri = Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/friends'); + uri = uri.replace(queryParameters: params); + + resp = await http.get( + uri, + headers: { + 'cookie': await getSessionCookie(), + } + ); + + if (resp.statusCode != 200) { + throw Exception(resp.body); } - print(friends); + 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']); + + friend.username = AesHelper.aesDecrypt( + base64.decode(friend.friendSymmetricKeyDecrypted), + base64.decode(friendJson['username']), + ); + + await db.insert( + 'friends', + friend.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } } diff --git a/mobile/lib/views/authentication/login.dart b/mobile/lib/views/authentication/login.dart index 806729b..29075d1 100644 --- a/mobile/lib/views/authentication/login.dart +++ b/mobile/lib/views/authentication/login.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import '/utils/encryption/crypto_utils.dart'; import '/utils/encryption/aes_helper.dart'; import '/utils/storage/encryption_keys.dart'; @@ -32,7 +33,7 @@ class LoginResponse { Future login(context, String username, String password) async { final resp = await http.post( - Uri.parse('http://192.168.1.5:8080/api/v1/login'), + Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/login'), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, @@ -117,7 +118,7 @@ class _LoginWidgetState extends State { minimumSize: const Size.fromHeight(50), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), textStyle: const TextStyle( - fontSize: 20, + fontSize: 20, fontWeight: FontWeight.bold, color: Colors.red, ), diff --git a/mobile/lib/views/main/conversation_detail.dart b/mobile/lib/views/main/conversation_detail.dart index 8732f80..4ed7182 100644 --- a/mobile/lib/views/main/conversation_detail.dart +++ b/mobile/lib/views/main/conversation_detail.dart @@ -1,50 +1,21 @@ import 'package:flutter/material.dart'; import '/models/conversations.dart'; +import '/models/messages.dart'; class ConversationDetail extends StatefulWidget{ - const ConversationDetail({Key? key}) : super(key: key); + final Conversation conversation; + const ConversationDetail({ + Key? key, + required this.conversation, + }) : super(key: key); - @override - _ConversationDetailState createState() => _ConversationDetailState(); + @override + _ConversationDetailState createState() => _ConversationDetailState(); } class _ConversationDetailState extends State { - Conversation conversation = Conversation( - id: '777', - friendId: 'abc', - recentMessageId: '111', - ); - - List messages = [ - Message( - id: '444', - conversationId: '777', - symmetricKey: '', - data: 'This is a message', - messageType: messageTypeSender, - ), - Message( - id: '444', - conversationId: '777', - symmetricKey: '', - data: 'This is a message', - messageType: messageTypeReceiver, - ), - Message( - id: '444', - conversationId: '777', - symmetricKey: '', - data: 'This is a message', - messageType: messageTypeSender - ), - Message( - id: '444', - conversationId: '777', - symmetricKey: '', - data: 'This is a message', - messageType: messageTypeReceiver, - ), - ]; + List messages = [ + ]; @override Widget build(BuildContext context) { @@ -69,8 +40,13 @@ class _ConversationDetailState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Text("Kriss Benwat",style: TextStyle( fontSize: 16 ,fontWeight: FontWeight.w600),), + children: [ + Text( + widget.conversation.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600), + ), ], ), ), @@ -91,11 +67,13 @@ class _ConversationDetailState extends State { 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: (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: (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)), diff --git a/mobile/lib/views/main/conversation_list.dart b/mobile/lib/views/main/conversation_list.dart index b47e678..5a5ca08 100644 --- a/mobile/lib/views/main/conversation_list.dart +++ b/mobile/lib/views/main/conversation_list.dart @@ -10,41 +10,35 @@ class ConversationList extends StatefulWidget { } class _ConversationListState extends State { - List messages = [ - Message( - id: '123', - conversationId: 'xyz', - data: '', - symmetricKey: '', - messageType: 'reciever', - ), - ]; + List conversations = []; - List friends = [ - Conversation( - id: 'xyz', - friendId: 'abc', - recentMessageId: '123', - ), - ]; + @override + void initState() { + super.initState(); + fetchConversations(); + } + + void fetchConversations() async { + conversations = await getConversations(); + setState(() {}); + } Widget list() { - if (friends.isEmpty) { + if (conversations.isEmpty) { return const Center( child: Text('No Conversations'), ); } return ListView.builder( - itemCount: friends.length, + itemCount: conversations.length, shrinkWrap: true, padding: const EdgeInsets.only(top: 16), physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, i) { return ConversationListItem( - id: friends[i].id, - username: 'Test', + conversation: conversations[i], ); }, ); diff --git a/mobile/lib/views/main/conversation_list_item.dart b/mobile/lib/views/main/conversation_list_item.dart index e8b6c23..78b1d55 100644 --- a/mobile/lib/views/main/conversation_list_item.dart +++ b/mobile/lib/views/main/conversation_list_item.dart @@ -1,60 +1,66 @@ +import 'package:Envelope/components/custom_circle_avatar.dart'; +import 'package:Envelope/models/conversations.dart'; import 'package:flutter/material.dart'; import '/views/main/conversation_detail.dart'; class ConversationListItem extends StatefulWidget{ - final String id; - final String username; - const ConversationListItem({ - Key? key, - required this.id, - required this.username, - }) : super(key: key); + final Conversation conversation; + const ConversationListItem({ + Key? key, + required this.conversation, + }) : super(key: key); - @override - _ConversationListItemState createState() => _ConversationListItemState(); + @override + _ConversationListItemState createState() => _ConversationListItemState(); } class _ConversationListItemState extends State { - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (context){ - return ConversationDetail(); - })); - }, - child: Container( - padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10), - child: Row( - children: [ - Expanded( - child: Row( - children: [ - // CircleAvatar( - // backgroundImage: NetworkImage(widget.imageUrl), - // maxRadius: 30, - // ), - //const SizedBox(width: 16), - Expanded( - child: Container( - color: Colors.transparent, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(widget.username, style: const TextStyle(fontSize: 16)), - const SizedBox(height: 6), - //Text(widget.messageText,style: TextStyle(fontSize: 13,color: Colors.grey.shade600, fontWeight: widget.isMessageRead?FontWeight.bold:FontWeight.normal),), - const Divider(), - ], - ), - ), - ), - ], - ), - ), - ], - ), - ), + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context){ + return ConversationDetail( + conversation: widget.conversation, + ); + })); + }, + child: Container( + padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10), + child: Row( + children: [ + Expanded( + child: Row( + children: [ + CustomCircleAvatar( + initials: widget.conversation.name[0].toUpperCase(), + imagePath: null, + ), + const SizedBox(width: 16), + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: Container( + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.conversation.name, + style: const TextStyle(fontSize: 16) + ), + //Text(widget.messageText,style: TextStyle(fontSize: 13,color: Colors.grey.shade600, fontWeight: widget.isMessageRead?FontWeight.bold:FontWeight.normal),), + ], + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), ); - } + } } diff --git a/mobile/lib/views/main/friend_list.dart b/mobile/lib/views/main/friend_list.dart index 9299cb0..2437672 100644 --- a/mobile/lib/views/main/friend_list.dart +++ b/mobile/lib/views/main/friend_list.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import '/models/friends.dart'; import '/views/main/friend_list_item.dart'; -import '/utils/storage/friends.dart'; class FriendList extends StatefulWidget { const FriendList({Key? key}) : super(key: key); @@ -15,10 +14,14 @@ class _FriendListState extends State { @override void initState() { - getFriends(); super.initState(); + fetchFriends(); } + void fetchFriends() async { + friends = await getFriends(); + setState(() {}); + } Widget list() { @@ -36,7 +39,7 @@ class _FriendListState extends State { itemBuilder: (context, i) { return FriendListItem( id: friends[i].id, - username: 'test', + username: friends[i].username!, ); }, ); diff --git a/mobile/lib/views/main/friend_list_item.dart b/mobile/lib/views/main/friend_list_item.dart index 42c5215..6fbc26b 100644 --- a/mobile/lib/views/main/friend_list_item.dart +++ b/mobile/lib/views/main/friend_list_item.dart @@ -1,56 +1,67 @@ +import 'package:Envelope/components/custom_circle_avatar.dart'; import 'package:flutter/material.dart'; class FriendListItem extends StatefulWidget{ - final String id; - final String username; - const FriendListItem({ - Key? key, - required this.id, - required this.username, - }) : super(key: key); + final String id; + final String username; + final String? imagePath; + const FriendListItem({ + Key? key, + required this.id, + required this.username, + this.imagePath, + }) : super(key: key); - @override - _FriendListItemState createState() => _FriendListItemState(); + @override + _FriendListItemState createState() => _FriendListItemState(); } class _FriendListItemState extends State { - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: (){ - }, - child: Container( - padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10), - child: Row( - children: [ - Expanded( - child: Row( - children: [ - // CircleAvatar( - // backgroundImage: NetworkImage(widget.imageUrl), - // maxRadius: 30, - // ), - //const SizedBox(width: 16), - Expanded( - child: Container( - color: Colors.transparent, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(widget.username, style: const TextStyle(fontSize: 16)), - const SizedBox(height: 6), - //Text(widget.messageText,style: TextStyle(fontSize: 13,color: Colors.grey.shade600, fontWeight: widget.isMessageRead?FontWeight.bold:FontWeight.normal),), - const Divider(), - ], - ), - ), - ), - ], - ), - ), - ], - ), - ), + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: (){ + }, + child: Container( + padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10), + child: Row( + children: [ + Expanded( + child: Row( + children: [ + CustomCircleAvatar( + initials: widget.username[0].toUpperCase(), + imagePath: widget.imagePath, + ), + const SizedBox(width: 16), + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: Container( + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.username, style: const TextStyle(fontSize: 16)), + // Text( + // widget.messageText, + // style: TextStyle(fontSize: 13, + // color: Colors.grey.shade600, + // fontWeight: widget.isMessageRead?FontWeight.bold:FontWeight.normal + // ), + // ), + ], + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), ); - } + } } diff --git a/mobile/lib/views/main/home.dart b/mobile/lib/views/main/home.dart index 85b3ecc..fcf9900 100644 --- a/mobile/lib/views/main/home.dart +++ b/mobile/lib/views/main/home.dart @@ -3,71 +3,80 @@ import 'package:shared_preferences/shared_preferences.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'; class Home extends StatefulWidget { - const Home({Key? key}) : super(key: key); + const Home({Key? key}) : super(key: key); - @override - State createState() => _HomeState(); + @override + State createState() => _HomeState(); } class _HomeState extends State { - @override - void initState() { - checkLogin(); - super.initState(); - } + @override + void initState() { + super.initState(); + updateData(); + } + + void updateData() async { + await checkLogin(); + await updateFriends(); + await updateConversations(); + } - Future checkLogin() async { - SharedPreferences preferences = await SharedPreferences.getInstance(); - if (preferences.getBool('islogin') != true) { - Navigator.pushNamedAndRemoveUntil(context, '/landing', ModalRoute.withName('/landing')); - } + // TODO: Do server GET check here + Future checkLogin() async { + SharedPreferences preferences = await SharedPreferences.getInstance(); + if (preferences.getBool('islogin') != true) { + Navigator.pushNamedAndRemoveUntil(context, '/landing', ModalRoute.withName('/landing')); } + } - int _selectedIndex = 0; - static const List _widgetOptions = [ - ConversationList(), - FriendList(), - Profile(), - ]; + int _selectedIndex = 0; + static const List _widgetOptions = [ + ConversationList(), + FriendList(), + Profile(), + ]; - void _onItemTapped(int index) { - setState(() { - _selectedIndex = index; - }); - } + void _onItemTapped(int index) { + setState(() { + _selectedIndex = index; + }); + } - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async => false, - child: Scaffold( - body: _widgetOptions.elementAt(_selectedIndex), - bottomNavigationBar: BottomNavigationBar( - currentIndex: _selectedIndex, - onTap: _onItemTapped, - selectedItemColor: Colors.red, - unselectedItemColor: Colors.grey.shade600, - selectedLabelStyle: const TextStyle(fontWeight: FontWeight.w600), - unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.w600), - type: BottomNavigationBarType.fixed, - items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.message), - label: "Chats", - ), - BottomNavigationBarItem( - icon: Icon(Icons.group_work), - label: "Friends", - ), - BottomNavigationBarItem( - icon: Icon(Icons.account_box), - label: "Profile", - ), - ], - ), - ), - ); - } + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async => false, + child: Scaffold( + body: _widgetOptions.elementAt(_selectedIndex), + bottomNavigationBar: BottomNavigationBar( + currentIndex: _selectedIndex, + onTap: _onItemTapped, + selectedItemColor: Colors.red, + unselectedItemColor: Colors.grey.shade600, + selectedLabelStyle: const TextStyle(fontWeight: FontWeight.w600), + unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.w600), + type: BottomNavigationBarType.fixed, + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.message), + label: "Chats", + ), + BottomNavigationBarItem( + icon: Icon(Icons.group_work), + label: "Friends", + ), + BottomNavigationBarItem( + icon: Icon(Icons.account_box), + label: "Profile", + ), + ], + ), + ), + ); + } } diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index e09490d..c7d9286 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -90,6 +90,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.2" flutter_lints: dependency: "direct dev" description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 86d417a..9dffd90 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: shared_preferences: ^2.0.15 sqflite: ^2.0.2 path: 1.8.1 + flutter_dotenv: ^5.0.2 dev_dependencies: flutter_test: @@ -27,6 +28,9 @@ dev_dependencies: flutter_lints: ^1.0.0 flutter: - uses-material-design: true + assets: + - .env + +