Browse Source

Friends and conversations sync to device

pull/1/head
Tovi Jaeschke-Rogers 2 years ago
parent
commit
b5701cf777
45 changed files with 1474 additions and 685 deletions
  1. +1
    -1
      .gitignore
  2. +15
    -18
      Backend/Api/Auth/Login.go
  3. +7
    -1
      Backend/Api/Auth/Logout.go
  4. +10
    -35
      Backend/Api/Auth/Session.go
  5. +42
    -4
      Backend/Api/Friends/EncryptedFriendsList.go
  6. +2
    -2
      Backend/Api/Friends/FriendRequest.go
  7. +7
    -7
      Backend/Api/Friends/Friends.go
  8. +83
    -0
      Backend/Api/Messages/Conversations.go
  9. +9
    -17
      Backend/Api/Messages/MessageThread.go
  10. +10
    -4
      Backend/Api/Routes.go
  11. +55
    -0
      Backend/Database/ConversationDetails.go
  12. +47
    -0
      Backend/Database/FriendRequests.go
  13. +43
    -16
      Backend/Database/Friends.go
  14. +4
    -2
      Backend/Database/Init.go
  15. +0
    -39
      Backend/Database/MessageThreadUsers.go
  16. +0
    -42
      Backend/Database/MessageThreads.go
  17. +38
    -12
      Backend/Database/Seeder/FriendSeeder.go
  18. +87
    -165
      Backend/Database/Seeder/MessageSeeder.go
  19. +12
    -14
      Backend/Database/Seeder/Seed.go
  20. +37
    -3
      Backend/Database/Seeder/UserSeeder.go
  21. +188
    -0
      Backend/Database/Seeder/encryption.go
  22. +38
    -0
      Backend/Database/Sessions.go
  23. +49
    -0
      Backend/Database/UserConversations.go
  24. +13
    -5
      Backend/Models/Friends.go
  25. +15
    -13
      Backend/Models/Messages.go
  26. +18
    -0
      Backend/Models/Sessions.go
  27. +2
    -0
      Backend/Models/Users.go
  28. +39
    -0
      mobile/lib/components/custom_circle_avatar.dart
  29. +3
    -1
      mobile/lib/main.dart
  30. +106
    -20
      mobile/lib/models/conversations.dart
  31. +55
    -5
      mobile/lib/models/friends.dart
  32. +20
    -0
      mobile/lib/models/messages.dart
  33. +18
    -4
      mobile/lib/utils/encryption/aes_helper.dart
  34. +79
    -0
      mobile/lib/utils/storage/conversations.dart
  35. +37
    -22
      mobile/lib/utils/storage/database.dart
  36. +50
    -11
      mobile/lib/utils/storage/friends.dart
  37. +3
    -2
      mobile/lib/views/authentication/login.dart
  38. +21
    -43
      mobile/lib/views/main/conversation_detail.dart
  39. +14
    -20
      mobile/lib/views/main/conversation_list.dart
  40. +56
    -50
      mobile/lib/views/main/conversation_list_item.dart
  41. +6
    -3
      mobile/lib/views/main/friend_list.dart
  42. +58
    -47
      mobile/lib/views/main/friend_list_item.dart
  43. +65
    -56
      mobile/lib/views/main/home.dart
  44. +7
    -0
      mobile/pubspec.lock
  45. +5
    -1
      mobile/pubspec.yaml

+ 1
- 1
.gitignore View File

@ -1 +1 @@
/mobile/nsconfig.json
/mobile/.env

+ 15
- 18
Backend/Api/Auth/Login.go View File

@ -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,
})


+ 7
- 1
Backend/Api/Auth/Logout.go View File

@ -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",


+ 10
- 35
Backend/Api/Auth/Session.go View File

@ -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
}

+ 42
- 4
Backend/Api/Friends/EncryptedFriendsList.go View File

@ -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


+ 2
- 2
Backend/Api/Friends/FriendRequest.go View File

@ -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,
}


+ 7
- 7
Backend/Api/Friends/Friends.go View File

@ -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)


+ 83
- 0
Backend/Api/Messages/Conversations.go View File

@ -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)
}

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

@ -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


+ 10
- 4
Backend/Api/Routes.go View File

@ -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")
}

+ 55
- 0
Backend/Database/ConversationDetails.go View File

@ -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
}

+ 47
- 0
Backend/Database/FriendRequests.go View File

@ -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
}

+ 43
- 16
Backend/Database/Friends.go View File

@ -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
}

+ 4
- 2
Backend/Database/Init.go View File

@ -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{},
}
}


+ 0
- 39
Backend/Database/MessageThreadUsers.go View File

@ -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
}

+ 0
- 42
Backend/Database/MessageThreads.go View File

@ -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
}

+ 38
- 12
Backend/Database/Seeder/FriendSeeder.go View File

@ -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)
}
}
}

+ 87
- 165
Backend/Database/Seeder/MessageSeeder.go View File

@ -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,
)


+ 12
- 14
Backend/Database/Seeder/Seed.go View File

@ -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()


+ 37
- 3
Backend/Database/Seeder/UserSeeder.go View File

@ -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)


+ 188
- 0
Backend/Database/Seeder/encryption.go View File

@ -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
}

+ 38
- 0
Backend/Database/Sessions.go View File

@ -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
}

+ 49
- 0
Backend/Database/UserConversations.go View File

@ -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
}

+ 13
- 5
Backend/Models/Friends.go View File

@ -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"`
}

+ 15
- 13
Backend/Models/Messages.go View File

@ -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
}

+ 18
- 0
Backend/Models/Sessions.go View File

@ -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
}

+ 2
- 0
Backend/Models/Users.go View File

@ -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
}

+ 39
- 0
mobile/lib/components/custom_circle_avatar.dart View File

@ -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<CustomCircleAvatar>{
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!)
);
}
}

+ 3
- 1
mobile/lib/main.dart View File

@ -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());
}


+ 106
- 20
mobile/lib/models/conversations.dart View File

@ -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<Conversation> 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<String, dynamic> 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<String, dynamic> 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<List<Conversation>> getConversations() async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> 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'],
);
});
}

+ 55
- 5
mobile/lib/models/friends.dart View File

@ -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<Friend> 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<String, dynamic> 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<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,
};
}
}
// A method that retrieves all the dogs from the dogs table.
Future<List<Friend>> getFriends() async {
final db = await getDatabaseConnection();
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'],
);
});
}

+ 20
- 0
mobile/lib/models/messages.dart View File

@ -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,
});
}

+ 18
- 4
mobile/lib/utils/encryption/aes_helper.dart View File

@ -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();


+ 79
- 0
mobile/lib/utils/storage/conversations.dart View File

@ -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<void> 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<Conversation> conversations = [];
List<String> conversationsDetailIds = [];
List<dynamic> conversationsJson = jsonDecode(resp.body);
for (var i = 0; i < conversationsJson.length; i++) {
Conversation conversation = Conversation.fromJson(
conversationsJson[i] as Map<String, dynamic>,
privKey,
);
conversations.add(conversation);
conversationsDetailIds.add(conversation.conversationDetailId);
}
Map<String, String> 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<dynamic> conversationsDetailsJson = jsonDecode(resp.body);
for (var i = 0; i < conversationsDetailsJson.length; i++) {
var conversationDetailJson = conversationsDetailsJson[i] as Map<String, dynamic>;
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,
);
}
}

+ 37
- 22
mobile/lib/utils/storage/database.dart View File

@ -6,29 +6,44 @@ import 'package:sqflite/sqflite.dart';
Future<Database> 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;


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

@ -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<void> 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<Friend> friends = [];
List<String> friendIds = [];
List<dynamic> friendsJson = jsonDecode(resp.body);
List<dynamic> 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<String, dynamic>,
friendsRequestJson[i] as Map<String, dynamic>,
privKey,
)
);
friendIds.add(friends[i].friendIdDecrypted);
}
Map<String, String> 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<dynamic> friendsJson = jsonDecode(resp.body);
for (var i = 0; i < friendsJson.length; i++) {
var friendJson = friendsJson[i] as Map<String, dynamic>;
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,
);
}
}

+ 3
- 2
mobile/lib/views/authentication/login.dart View File

@ -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<LoginResponse> 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: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
@ -117,7 +118,7 @@ class _LoginWidgetState extends State<LoginWidget> {
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,
),


+ 21
- 43
mobile/lib/views/main/conversation_detail.dart View File

@ -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<ConversationDetail> {
Conversation conversation = Conversation(
id: '777',
friendId: 'abc',
recentMessageId: '111',
);
List<Message> 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<Message> messages = [
];
@override
Widget build(BuildContext context) {
@ -69,8 +40,13 @@ class _ConversationDetailState extends State<ConversationDetail> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
Text("Kriss Benwat",style: TextStyle( fontSize: 16 ,fontWeight: FontWeight.w600),),
children: <Widget>[
Text(
widget.conversation.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600),
),
],
),
),
@ -91,11 +67,13 @@ class _ConversationDetailState extends State<ConversationDetail> {
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)),


+ 14
- 20
mobile/lib/views/main/conversation_list.dart View File

@ -10,41 +10,35 @@ class ConversationList extends StatefulWidget {
}
class _ConversationListState extends State<ConversationList> {
List<Message> messages = [
Message(
id: '123',
conversationId: 'xyz',
data: '',
symmetricKey: '',
messageType: 'reciever',
),
];
List<Conversation> conversations = [];
List<Conversation> 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],
);
},
);


+ 56
- 50
mobile/lib/views/main/conversation_list_item.dart View File

@ -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<ConversationListItem> {
@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: <Widget>[
Expanded(
child: Row(
children: <Widget>[
// CircleAvatar(
// backgroundImage: NetworkImage(widget.imageUrl),
// maxRadius: 30,
// ),
//const SizedBox(width: 16),
Expanded(
child: Container(
color: Colors.transparent,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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: <Widget>[
Expanded(
child: Row(
children: <Widget>[
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: <Widget>[
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),),
],
),
),
),
),
],
),
),
],
),
),
);
}
}
}

+ 6
- 3
mobile/lib/views/main/friend_list.dart View File

@ -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<FriendList> {
@override
void initState() {
getFriends();
super.initState();
fetchFriends();
}
void fetchFriends() async {
friends = await getFriends();
setState(() {});
}
Widget list() {
@ -36,7 +39,7 @@ class _FriendListState extends State<FriendList> {
itemBuilder: (context, i) {
return FriendListItem(
id: friends[i].id,
username: 'test',
username: friends[i].username!,
);
},
);


+ 58
- 47
mobile/lib/views/main/friend_list_item.dart View File

@ -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<FriendListItem> {
@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: <Widget>[
Expanded(
child: Row(
children: <Widget>[
// CircleAvatar(
// backgroundImage: NetworkImage(widget.imageUrl),
// maxRadius: 30,
// ),
//const SizedBox(width: 16),
Expanded(
child: Container(
color: Colors.transparent,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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: <Widget>[
Expanded(
child: Row(
children: <Widget>[
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: <Widget>[
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
// ),
// ),
],
),
),
),
),
],
),
),
],
),
),
);
}
}
}

+ 65
- 56
mobile/lib/views/main/home.dart View File

@ -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<Home> createState() => _HomeState();
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
@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<Widget> _widgetOptions = <Widget>[
ConversationList(),
FriendList(),
Profile(),
];
int _selectedIndex = 0;
static const List<Widget> _widgetOptions = <Widget>[
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",
),
],
),
),
);
}
}

+ 7
- 0
mobile/pubspec.lock View File

@ -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:


+ 5
- 1
mobile/pubspec.yaml View File

@ -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

Loading…
Cancel
Save