diff --git a/.gitignore b/.gitignore
index f7fec7f..cdb1a17 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
/mobile/.env
+/Backend/attachments/*
diff --git a/Backend/Api/Auth/AddProfileImage.go b/Backend/Api/Auth/AddProfileImage.go
new file mode 100644
index 0000000..31c7f64
--- /dev/null
+++ b/Backend/Api/Auth/AddProfileImage.go
@@ -0,0 +1,50 @@
+package Auth
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "net/http"
+
+ "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
+ "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
+ "git.tovijaeschke.xyz/tovi/Envelope/Backend/Util"
+)
+
+// AddProfileImage adds a profile image
+func AddProfileImage(w http.ResponseWriter, r *http.Request) {
+ var (
+ user Models.User
+ attachment Models.Attachment
+ decodedFile []byte
+ fileName string
+ err error
+ )
+
+ // Ignore error here, as middleware should handle auth
+ user, _ = CheckCookieCurrentUser(w, r)
+
+ err = json.NewDecoder(r.Body).Decode(&attachment)
+ if err != nil {
+ http.Error(w, "Error", http.StatusInternalServerError)
+ return
+ }
+
+ if attachment.Data == "" {
+ http.Error(w, "Error", http.StatusInternalServerError)
+ return
+ }
+
+ decodedFile, err = base64.StdEncoding.DecodeString(attachment.Data)
+ fileName, err = Util.WriteFile(decodedFile)
+ attachment.FilePath = fileName
+
+ user.Attachment = attachment
+
+ err = Database.UpdateUser(user.ID.String(), &user)
+ if err != nil {
+ http.Error(w, "Error", http.StatusInternalServerError)
+ return
+ }
+
+ w.WriteHeader(http.StatusNoContent)
+}
diff --git a/Backend/Api/Auth/ChangeMessageExpiry.go b/Backend/Api/Auth/ChangeMessageExpiry.go
index 8f8721f..acad218 100644
--- a/Backend/Api/Auth/ChangeMessageExpiry.go
+++ b/Backend/Api/Auth/ChangeMessageExpiry.go
@@ -10,7 +10,7 @@ import (
)
type rawChangeMessageExpiry struct {
- MessageExpiry string `json:"message_exipry"`
+ MessageExpiry string `json:"message_expiry"`
}
// ChangeMessageExpiry handles changing default message expiry for user
@@ -37,7 +37,7 @@ func ChangeMessageExpiry(w http.ResponseWriter, r *http.Request) {
return
}
- user.AsymmetricPrivateKey = changeMessageExpiry.MessageExpiry
+ user.MessageExpiryDefault.Scan(changeMessageExpiry.MessageExpiry)
err = Database.UpdateUser(
user.ID.String(),
diff --git a/Backend/Api/Auth/Login.go b/Backend/Api/Auth/Login.go
index 61225af..d217493 100644
--- a/Backend/Api/Auth/Login.go
+++ b/Backend/Api/Auth/Login.go
@@ -16,73 +16,43 @@ type credentials struct {
}
type loginResponse struct {
- Status string `json:"status"`
- Message string `json:"message"`
- AsymmetricPublicKey string `json:"asymmetric_public_key"`
- AsymmetricPrivateKey string `json:"asymmetric_private_key"`
UserID string `json:"user_id"`
Username string `json:"username"`
+ AsymmetricPublicKey string `json:"asymmetric_public_key"`
+ AsymmetricPrivateKey string `json:"asymmetric_private_key"`
+ SymmetricKey string `json:"symmetric_key"`
MessageExpiryDefault string `json:"message_expiry_default"`
+ ImageLink string `json:"image_link"`
}
-func makeLoginResponse(w http.ResponseWriter, code int, message, pubKey, privKey string, user Models.User) {
+// Login logs the user into the system
+func Login(w http.ResponseWriter, r *http.Request) {
var (
- status = "error"
+ creds credentials
+ user Models.User
+ session Models.Session
+ expiresAt time.Time
messageExpiryRaw driver.Value
messageExpiry string
+ imageLink string
returnJSON []byte
err error
)
- if code >= 200 && code <= 300 {
- status = "success"
- }
-
- messageExpiryRaw, _ = user.MessageExpiryDefault.Value()
- messageExpiry, _ = messageExpiryRaw.(string)
-
- returnJSON, err = json.MarshalIndent(loginResponse{
- Status: status,
- Message: message,
- AsymmetricPublicKey: pubKey,
- AsymmetricPrivateKey: privKey,
- UserID: user.ID.String(),
- Username: user.Username,
- MessageExpiryDefault: messageExpiry,
- }, "", " ")
- if err != nil {
- http.Error(w, "Error", http.StatusInternalServerError)
- return
- }
-
- // Return updated json
- w.WriteHeader(code)
- w.Write(returnJSON)
-}
-
-// Login logs the user into the system
-func Login(w http.ResponseWriter, r *http.Request) {
- var (
- creds credentials
- userData Models.User
- session Models.Session
- expiresAt time.Time
- err error
- )
err = json.NewDecoder(r.Body).Decode(&creds)
if err != nil {
- makeLoginResponse(w, http.StatusInternalServerError, "An error occurred", "", "", userData)
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
- userData, err = Database.GetUserByUsername(creds.Username)
+ user, err = Database.GetUserByUsername(creds.Username)
if err != nil {
- makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "", userData)
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
- if !CheckPasswordHash(creds.Password, userData.Password) {
- makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "", userData)
+ if !CheckPasswordHash(creds.Password, user.Password) {
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
@@ -90,13 +60,13 @@ func Login(w http.ResponseWriter, r *http.Request) {
expiresAt = time.Now().Add(12 * time.Hour)
session = Models.Session{
- UserID: userData.ID,
+ UserID: user.ID,
Expiry: expiresAt,
}
err = Database.CreateSession(&session)
if err != nil {
- makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "", userData)
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
@@ -106,12 +76,29 @@ func Login(w http.ResponseWriter, r *http.Request) {
Expires: expiresAt,
})
- makeLoginResponse(
- w,
- http.StatusOK,
- "Successfully logged in",
- userData.AsymmetricPublicKey,
- userData.AsymmetricPrivateKey,
- userData,
- )
+ if user.AttachmentID != nil {
+ imageLink = user.Attachment.FilePath
+ }
+
+ messageExpiryRaw, _ = user.MessageExpiryDefault.Value()
+ messageExpiry, _ = messageExpiryRaw.(string)
+
+ returnJSON, err = json.MarshalIndent(loginResponse{
+ UserID: user.ID.String(),
+ Username: user.Username,
+ AsymmetricPublicKey: user.AsymmetricPublicKey,
+ AsymmetricPrivateKey: user.AsymmetricPrivateKey,
+ SymmetricKey: user.SymmetricKey,
+ MessageExpiryDefault: messageExpiry,
+ ImageLink: imageLink,
+ }, "", " ")
+
+ if err != nil {
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ // Return updated json
+ w.WriteHeader(http.StatusOK)
+ w.Write(returnJSON)
}
diff --git a/Backend/Api/Friends/AcceptFriendRequest.go b/Backend/Api/Friends/AcceptFriendRequest.go
index adfa0e5..aa9e233 100644
--- a/Backend/Api/Friends/AcceptFriendRequest.go
+++ b/Backend/Api/Friends/AcceptFriendRequest.go
@@ -32,7 +32,7 @@ func AcceptFriendRequest(w http.ResponseWriter, r *http.Request) {
oldFriendRequest, err = Database.GetFriendRequestByID(friendRequestID)
if err != nil {
- http.Error(w, "Error", http.StatusInternalServerError)
+ http.Error(w, "Not Found", http.StatusNotFound)
return
}
diff --git a/Backend/Api/Messages/AddConversationImage.go b/Backend/Api/Messages/AddConversationImage.go
new file mode 100644
index 0000000..1da2866
--- /dev/null
+++ b/Backend/Api/Messages/AddConversationImage.go
@@ -0,0 +1,64 @@
+package Messages
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "net/http"
+
+ "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
+ "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
+ "git.tovijaeschke.xyz/tovi/Envelope/Backend/Util"
+ "github.com/gorilla/mux"
+)
+
+// AddConversationImage adds an image for a conversation icon
+func AddConversationImage(w http.ResponseWriter, r *http.Request) {
+ var (
+ attachment Models.Attachment
+ conversationDetail Models.ConversationDetail
+ urlVars map[string]string
+ detailID string
+ decodedFile []byte
+ fileName string
+ ok bool
+ err error
+ )
+
+ urlVars = mux.Vars(r)
+ detailID, ok = urlVars["detailID"]
+ if !ok {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ return
+ }
+
+ conversationDetail, err = Database.GetConversationDetailByID(detailID)
+ if err != nil {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ return
+ }
+
+ err = json.NewDecoder(r.Body).Decode(&attachment)
+ if err != nil {
+ http.Error(w, "Error", http.StatusInternalServerError)
+ return
+ }
+
+ if attachment.Data == "" {
+ http.Error(w, "Error", http.StatusInternalServerError)
+ return
+ }
+
+ decodedFile, err = base64.StdEncoding.DecodeString(attachment.Data)
+ fileName, err = Util.WriteFile(decodedFile)
+ attachment.FilePath = fileName
+
+ conversationDetail.Attachment = attachment
+
+ err = Database.UpdateConversationDetail(&conversationDetail)
+ if err != nil {
+ http.Error(w, "Error", http.StatusInternalServerError)
+ return
+ }
+
+ w.WriteHeader(http.StatusNoContent)
+}
diff --git a/Backend/Api/Messages/Conversations.go b/Backend/Api/Messages/Conversations.go
index 27d1470..a1681da 100644
--- a/Backend/Api/Messages/Conversations.go
+++ b/Backend/Api/Messages/Conversations.go
@@ -14,10 +14,10 @@ import (
// EncryptedConversationList returns an encrypted list of all Conversations
func EncryptedConversationList(w http.ResponseWriter, r *http.Request) {
var (
- userConversations []Models.UserConversation
- userSession Models.Session
- returnJSON []byte
- err error
+ conversationDetails []Models.UserConversation
+ userSession Models.Session
+ returnJSON []byte
+ err error
)
userSession, err = Auth.CheckCookie(r)
@@ -26,7 +26,7 @@ func EncryptedConversationList(w http.ResponseWriter, r *http.Request) {
return
}
- userConversations, err = Database.GetUserConversationsByUserId(
+ conversationDetails, err = Database.GetUserConversationsByUserId(
userSession.UserID.String(),
)
if err != nil {
@@ -34,7 +34,7 @@ func EncryptedConversationList(w http.ResponseWriter, r *http.Request) {
return
}
- returnJSON, err = json.MarshalIndent(userConversations, "", " ")
+ returnJSON, err = json.MarshalIndent(conversationDetails, "", " ")
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
@@ -47,12 +47,14 @@ func EncryptedConversationList(w http.ResponseWriter, r *http.Request) {
// EncryptedConversationDetailsList returns an encrypted list of all ConversationDetails
func EncryptedConversationDetailsList(w http.ResponseWriter, r *http.Request) {
var (
- userConversations []Models.ConversationDetail
- query url.Values
- conversationIds []string
- returnJSON []byte
- ok bool
- err error
+ conversationDetails []Models.ConversationDetail
+ detail Models.ConversationDetail
+ query url.Values
+ conversationIds []string
+ returnJSON []byte
+ i int
+ ok bool
+ err error
)
query = r.URL.Query()
@@ -62,10 +64,9 @@ func EncryptedConversationDetailsList(w http.ResponseWriter, r *http.Request) {
return
}
- // TODO: Fix error handling here
conversationIds = strings.Split(conversationIds[0], ",")
- userConversations, err = Database.GetConversationDetailsByIds(
+ conversationDetails, err = Database.GetConversationDetailsByIds(
conversationIds,
)
if err != nil {
@@ -73,7 +74,15 @@ func EncryptedConversationDetailsList(w http.ResponseWriter, r *http.Request) {
return
}
- returnJSON, err = json.MarshalIndent(userConversations, "", " ")
+ for i, detail = range conversationDetails {
+ if detail.AttachmentID == nil {
+ continue
+ }
+
+ conversationDetails[i].Attachment.ImageLink = detail.Attachment.FilePath
+ }
+
+ returnJSON, err = json.MarshalIndent(conversationDetails, "", " ")
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
diff --git a/Backend/Api/Messages/CreateMessage.go b/Backend/Api/Messages/CreateMessage.go
index c233fc8..052f128 100644
--- a/Backend/Api/Messages/CreateMessage.go
+++ b/Backend/Api/Messages/CreateMessage.go
@@ -1,40 +1,54 @@
package Messages
import (
+ "encoding/base64"
"encoding/json"
"net/http"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
+ "git.tovijaeschke.xyz/tovi/Envelope/Backend/Util"
)
-type RawMessageData struct {
+type rawMessageData struct {
MessageData Models.MessageData `json:"message_data"`
Messages []Models.Message `json:"message"`
}
+// CreateMessage sends a message
func CreateMessage(w http.ResponseWriter, r *http.Request) {
var (
- rawMessageData RawMessageData
- err error
+ messagesData []rawMessageData
+ messageData rawMessageData
+ decodedFile []byte
+ fileName string
+ err error
)
- err = json.NewDecoder(r.Body).Decode(&rawMessageData)
+ err = json.NewDecoder(r.Body).Decode(&messagesData)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
- err = Database.CreateMessageData(&rawMessageData.MessageData)
- if err != nil {
- http.Error(w, "Error", http.StatusInternalServerError)
- return
- }
-
- err = Database.CreateMessages(&rawMessageData.Messages)
- if err != nil {
- http.Error(w, "Error", http.StatusInternalServerError)
- return
+ for _, messageData = range messagesData {
+ if messageData.MessageData.Data == "" {
+ decodedFile, err = base64.StdEncoding.DecodeString(messageData.MessageData.Attachment.Data)
+ fileName, err = Util.WriteFile(decodedFile)
+ messageData.MessageData.Attachment.FilePath = fileName
+ }
+
+ err = Database.CreateMessageData(&messageData.MessageData)
+ if err != nil {
+ http.Error(w, "Error", http.StatusInternalServerError)
+ return
+ }
+
+ err = Database.CreateMessages(&messageData.Messages)
+ if err != nil {
+ http.Error(w, "Error", http.StatusInternalServerError)
+ return
+ }
}
w.WriteHeader(http.StatusOK)
diff --git a/Backend/Api/Messages/MessageThread.go b/Backend/Api/Messages/MessageThread.go
index 14fac7c..686f1c1 100644
--- a/Backend/Api/Messages/MessageThread.go
+++ b/Backend/Api/Messages/MessageThread.go
@@ -14,9 +14,11 @@ import (
func Messages(w http.ResponseWriter, r *http.Request) {
var (
messages []Models.Message
+ message Models.Message
urlVars map[string]string
associationKey string
returnJSON []byte
+ i int
ok bool
err error
)
@@ -34,6 +36,14 @@ func Messages(w http.ResponseWriter, r *http.Request) {
return
}
+ for i, message = range messages {
+ if message.MessageData.AttachmentID == nil {
+ continue
+ }
+
+ messages[i].MessageData.Attachment.ImageLink = message.MessageData.Attachment.FilePath
+ }
+
returnJSON, err = json.MarshalIndent(messages, "", " ")
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
diff --git a/Backend/Api/Messages/UpdateConversation.go b/Backend/Api/Messages/UpdateConversation.go
index 93b5215..4900ba8 100644
--- a/Backend/Api/Messages/UpdateConversation.go
+++ b/Backend/Api/Messages/UpdateConversation.go
@@ -10,16 +10,17 @@ import (
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
)
-type RawUpdateConversationData struct {
+type rawUpdateConversationData struct {
ID string `json:"id"`
Name string `json:"name"`
Users []Models.ConversationDetailUser `json:"users"`
UserConversations []Models.UserConversation `json:"user_conversations"`
}
+// UpdateConversation updates the conversation data, such as title, users, etc
func UpdateConversation(w http.ResponseWriter, r *http.Request) {
var (
- rawConversationData RawCreateConversationData
+ rawConversationData rawUpdateConversationData
messageThread Models.ConversationDetail
err error
)
@@ -52,5 +53,5 @@ func UpdateConversation(w http.ResponseWriter, r *http.Request) {
}
}
- w.WriteHeader(http.StatusOK)
+ w.WriteHeader(http.StatusNoContent)
}
diff --git a/Backend/Api/Routes.go b/Backend/Api/Routes.go
index 999a2f2..8b0c280 100644
--- a/Backend/Api/Routes.go
+++ b/Backend/Api/Routes.go
@@ -44,6 +44,7 @@ func InitAPIEndpoints(router *mux.Router) {
var (
api *mux.Router
authAPI *mux.Router
+ fs http.Handler
)
log.Println("Initializing API routes...")
@@ -63,6 +64,7 @@ func InitAPIEndpoints(router *mux.Router) {
authAPI.HandleFunc("/change_password", Auth.ChangePassword).Methods("POST")
authAPI.HandleFunc("/message_expiry", Auth.ChangeMessageExpiry).Methods("POST")
+ authAPI.HandleFunc("/image", Auth.AddProfileImage).Methods("POST")
authAPI.HandleFunc("/users", Users.SearchUsers).Methods("GET")
@@ -76,7 +78,12 @@ func InitAPIEndpoints(router *mux.Router) {
authAPI.HandleFunc("/conversation_details", Messages.EncryptedConversationDetailsList).Methods("GET")
authAPI.HandleFunc("/conversations", Messages.CreateConversation).Methods("POST")
authAPI.HandleFunc("/conversations", Messages.UpdateConversation).Methods("PUT")
+ authAPI.HandleFunc("/conversations/{detailID}/image", Messages.AddConversationImage).Methods("POST")
authAPI.HandleFunc("/message", Messages.CreateMessage).Methods("POST")
authAPI.HandleFunc("/messages/{associationKey}", Messages.Messages).Methods("GET")
+
+ // TODO: Add authentication to this route
+ fs = http.FileServer(http.Dir("./attachments/"))
+ router.PathPrefix("/files/").Handler(http.StripPrefix("/files/", fs))
}
diff --git a/Backend/Database/Attachments.go b/Backend/Database/Attachments.go
new file mode 100644
index 0000000..3097a04
--- /dev/null
+++ b/Backend/Database/Attachments.go
@@ -0,0 +1,42 @@
+package Database
+
+import (
+ "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
+
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+)
+
+// GetAttachmentByID gets the attachment record by the id
+func GetAttachmentByID(id string) (Models.MessageData, error) {
+ var (
+ messageData Models.MessageData
+ err error
+ )
+
+ err = DB.Preload(clause.Associations).
+ First(&messageData, "id = ?", id).
+ Error
+
+ return messageData, err
+}
+
+// CreateAttachment creates the attachment record
+func CreateAttachment(messageData *Models.MessageData) error {
+ var (
+ err error
+ )
+
+ err = DB.Session(&gorm.Session{FullSaveAssociations: true}).
+ Create(messageData).
+ Error
+
+ return err
+}
+
+// DeleteAttachment deletes the attachment record
+func DeleteAttachment(messageData *Models.MessageData) error {
+ return DB.Session(&gorm.Session{FullSaveAssociations: true}).
+ Delete(messageData).
+ Error
+}
diff --git a/Backend/Database/ConversationDetails.go b/Backend/Database/ConversationDetails.go
index 9893022..af04edb 100644
--- a/Backend/Database/ConversationDetails.go
+++ b/Backend/Database/ConversationDetails.go
@@ -7,7 +7,8 @@ import (
"gorm.io/gorm/clause"
)
-func GetConversationDetailById(id string) (Models.ConversationDetail, error) {
+// GetConversationDetailByID gets by id
+func GetConversationDetailByID(id string) (Models.ConversationDetail, error) {
var (
messageThread Models.ConversationDetail
err error
@@ -21,6 +22,7 @@ func GetConversationDetailById(id string) (Models.ConversationDetail, error) {
return messageThread, err
}
+// GetConversationDetailsByIds gets by multiple ids
func GetConversationDetailsByIds(id []string) ([]Models.ConversationDetail, error) {
var (
messageThread []Models.ConversationDetail
@@ -35,12 +37,14 @@ func GetConversationDetailsByIds(id []string) ([]Models.ConversationDetail, erro
return messageThread, err
}
+// CreateConversationDetail creates a ConversationDetail record
func CreateConversationDetail(messageThread *Models.ConversationDetail) error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(messageThread).
Error
}
+// UpdateConversationDetail updates a ConversationDetail record
func UpdateConversationDetail(messageThread *Models.ConversationDetail) error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Where("id = ?", messageThread.ID).
@@ -48,6 +52,7 @@ func UpdateConversationDetail(messageThread *Models.ConversationDetail) error {
Error
}
+// DeleteConversationDetail deletes a ConversationDetail record
func DeleteConversationDetail(messageThread *Models.ConversationDetail) error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(messageThread).
diff --git a/Backend/Database/Init.go b/Backend/Database/Init.go
index 4481002..f4b6fb9 100644
--- a/Backend/Database/Init.go
+++ b/Backend/Database/Init.go
@@ -20,6 +20,7 @@ var DB *gorm.DB
func getModels() []interface{} {
return []interface{}{
&Models.Session{},
+ &Models.Attachment{},
&Models.User{},
&Models.FriendRequest{},
&Models.MessageData{},
diff --git a/Backend/Database/Messages.go b/Backend/Database/Messages.go
index 67cf8d3..f415c0e 100644
--- a/Backend/Database/Messages.go
+++ b/Backend/Database/Messages.go
@@ -7,7 +7,8 @@ import (
"gorm.io/gorm/clause"
)
-func GetMessageById(id string) (Models.Message, error) {
+// GetMessageByID gets a message
+func GetMessageByID(id string) (Models.Message, error) {
var (
message Models.Message
err error
@@ -20,6 +21,8 @@ func GetMessageById(id string) (Models.Message, error) {
return message, err
}
+// GetMessagesByAssociationKey for getting whole thread
+// TODO: Add pagination
func GetMessagesByAssociationKey(associationKey string) ([]Models.Message, error) {
var (
messages []Models.Message
@@ -27,12 +30,14 @@ func GetMessagesByAssociationKey(associationKey string) ([]Models.Message, error
)
err = DB.Preload("MessageData").
+ Preload("MessageData.Attachment").
Find(&messages, "association_key = ?", associationKey).
Error
return messages, err
}
+// CreateMessage creates a message record
func CreateMessage(message *Models.Message) error {
var err error
@@ -43,6 +48,7 @@ func CreateMessage(message *Models.Message) error {
return err
}
+// CreateMessages creates multiple records
func CreateMessages(messages *[]Models.Message) error {
var err error
@@ -53,6 +59,7 @@ func CreateMessages(messages *[]Models.Message) error {
return err
}
+// DeleteMessage deletes a message
func DeleteMessage(message *Models.Message) error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(message).
diff --git a/Backend/Database/Seeder/FriendSeeder.go b/Backend/Database/Seeder/FriendSeeder.go
index f3b5203..e317d13 100644
--- a/Backend/Database/Seeder/FriendSeeder.go
+++ b/Backend/Database/Seeder/FriendSeeder.go
@@ -2,6 +2,8 @@ package Seeder
import (
"encoding/base64"
+ "io"
+ "os"
"time"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
@@ -56,6 +58,28 @@ func seedFriend(userRequestTo, userRequestFrom Models.User, accepted bool) error
return Database.CreateFriendRequest(&friendRequest)
}
+func copyProfileImage() error {
+ var (
+ srcFile *os.File
+ dstFile *os.File
+ err error
+ )
+
+ srcFile, err = os.Open("./Database/Seeder/profile_image_enc.dat")
+ if err != nil {
+ return err
+ }
+
+ dstFile, err = os.Create("./attachments/profile_image")
+ if err != nil {
+ return err
+ }
+
+ defer dstFile.Close()
+ _, err = io.Copy(dstFile, srcFile)
+ return err
+}
+
// SeedFriends creates dummy friends for testing/development
func SeedFriends() {
var (
@@ -66,6 +90,11 @@ func SeedFriends() {
err error
)
+ err = copyProfileImage()
+ if err != nil {
+ panic(err)
+ }
+
primaryUser, err = Database.GetUserByUsername("testUser")
if err != nil {
panic(err)
diff --git a/Backend/Database/Seeder/UserSeeder.go b/Backend/Database/Seeder/UserSeeder.go
index ce13b2a..c65a94e 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"
@@ -23,10 +25,16 @@ var userNames = []string{
func createUser(username string) (Models.User, error) {
var (
userData Models.User
+ userKey aesKey
password string
err error
)
+ userKey, err = generateAesKey()
+ if err != nil {
+ panic(err)
+ }
+
password, err = Auth.HashPassword("password")
if err != nil {
return Models.User{}, err
@@ -37,12 +45,16 @@ func createUser(username string) (Models.User, error) {
Password: password,
AsymmetricPrivateKey: encryptedPrivateKey,
AsymmetricPublicKey: publicKey,
+ SymmetricKey: base64.StdEncoding.EncodeToString(
+ encryptWithPublicKey(userKey.Key, decodedPublicKey),
+ ),
}
err = Database.CreateUser(&userData)
return userData, err
}
+// SeedUsers used to create dummy users for testing & development
func SeedUsers() {
var (
i int
diff --git a/Backend/Database/Seeder/profile_image_enc.dat b/Backend/Database/Seeder/profile_image_enc.dat
new file mode 100644
index 0000000..f82798a
Binary files /dev/null and b/Backend/Database/Seeder/profile_image_enc.dat differ
diff --git a/Backend/Models/Attachments.go b/Backend/Models/Attachments.go
new file mode 100644
index 0000000..739369e
--- /dev/null
+++ b/Backend/Models/Attachments.go
@@ -0,0 +1,11 @@
+package Models
+
+// Attachment holds the attachment data
+type Attachment struct {
+ Base
+ FilePath string `gorm:"not null" json:"-"`
+ Mimetype string `gorm:"not null" json:"mimetype"`
+ Extension string `gorm:"not null" json:"extension"`
+ Data string `gorm:"-" json:"data"`
+ ImageLink string `gorm:"-" json:"image_link"`
+}
diff --git a/Backend/Models/Conversations.go b/Backend/Models/Conversations.go
index fa88987..1c9e53a 100644
--- a/Backend/Models/Conversations.go
+++ b/Backend/Models/Conversations.go
@@ -7,9 +7,11 @@ import (
// ConversationDetail stores the name for the conversation
type ConversationDetail struct {
Base
- Name string `gorm:"not null" json:"name"` // Stored encrypted
- Users []ConversationDetailUser ` json:"users"`
- TwoUser string `gorm:"not null" json:"two_user"`
+ Name string `gorm:"not null" json:"name"` // Stored encrypted
+ Users []ConversationDetailUser ` json:"users"`
+ TwoUser string `gorm:"not null" json:"two_user"`
+ AttachmentID *uuid.UUID ` json:"attachment_id"`
+ Attachment Attachment ` json:"attachment"`
}
// ConversationDetailUser all users associated with a customer
diff --git a/Backend/Models/Friends.go b/Backend/Models/Friends.go
index 967af7d..9dc892d 100644
--- a/Backend/Models/Friends.go
+++ b/Backend/Models/Friends.go
@@ -11,8 +11,9 @@ 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
- FriendUsername string ` json:"friend_username"` // Stored encrypted
+ FriendID string `gorm:"not null" json:"friend_id"` // Stored encrypted
+ FriendUsername string ` json:"friend_username"` // Stored encrypted
+ FriendImagePath string ` json:"friend_image_path"`
FriendPublicAsymmetricKey string ` json:"asymmetric_public_key"` // Stored encrypted
SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted
AcceptedAt sql.NullTime ` json:"accepted_at"`
diff --git a/Backend/Models/Messages.go b/Backend/Models/Messages.go
index 9e995b5..bf05e3b 100644
--- a/Backend/Models/Messages.go
+++ b/Backend/Models/Messages.go
@@ -11,9 +11,11 @@ import (
// encrypted through the Message.SymmetricKey
type MessageData struct {
Base
- Data string `gorm:"not null" json:"data"` // Stored encrypted
- SenderID string `gorm:"not null" json:"sender_id"` // Stored encrypted
- SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted
+ Data string ` json:"data"` // Stored encrypted
+ AttachmentID *uuid.UUID ` json:"attachment_id"`
+ Attachment Attachment ` json:"attachment"`
+ SenderID string `gorm:"not null" json:"sender_id"` // Stored encrypted
+ SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted
}
// Message holds data pertaining to each users' message
diff --git a/Backend/Models/Users.go b/Backend/Models/Users.go
index 685b774..811c3ab 100644
--- a/Backend/Models/Users.go
+++ b/Backend/Models/Users.go
@@ -3,6 +3,7 @@ package Models
import (
"database/sql/driver"
+ "github.com/gofrs/uuid"
"gorm.io/gorm"
)
@@ -58,6 +59,17 @@ 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"`
- MessageExpiryDefault MessageExpiry `gorm:"default:no_expiry" json:"-" sql:"type:ENUM('fifteen_min', 'thirty_min', 'one_hour', 'three_hour', 'six_hour', 'twelve_hour', 'one_day', 'three_day')"` // Stored encrypted
-
+ SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted
+ AttachmentID *uuid.UUID ` json:"attachment_id"`
+ Attachment Attachment ` json:"attachment"`
+ MessageExpiryDefault MessageExpiry `gorm:"default:no_expiry" json:"-" sql:"type:ENUM(
+ 'fifteen_min',
+ 'thirty_min',
+ 'one_hour',
+ 'three_hour',
+ 'six_hour',
+ 'twelve_hour',
+ 'one_day',
+ 'three_day'
+ )"` // Stored encrypted
}
diff --git a/Backend/Util/Files.go b/Backend/Util/Files.go
new file mode 100644
index 0000000..4ee8b81
--- /dev/null
+++ b/Backend/Util/Files.go
@@ -0,0 +1,46 @@
+package Util
+
+import (
+ "fmt"
+ "os"
+)
+
+// WriteFile to disk
+func WriteFile(contents []byte) (string, error) {
+ var (
+ fileName string
+ filePath string
+ cwd string
+ f *os.File
+ err error
+ )
+
+ cwd, err = os.Getwd()
+ if err != nil {
+ return fileName, err
+ }
+
+ fileName = RandomString(32)
+
+ filePath = fmt.Sprintf(
+ "%s/attachments/%s",
+ cwd,
+ fileName,
+ )
+
+ f, err = os.Create(filePath)
+
+ if err != nil {
+ return fileName, err
+ }
+
+ defer f.Close()
+
+ _, err = f.Write(contents)
+
+ if err != nil {
+ return fileName, err
+ }
+
+ return fileName, nil
+}
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
index d3ba628..f2762fc 100644
--- a/mobile/ios/Runner/Info.plist
+++ b/mobile/ios/Runner/Info.plist
@@ -47,5 +47,11 @@
NSCameraUsageDescription
This app needs camera access to scan QR codes
+ NSPhotoLibraryUsageDescription
+ Upload images for screen background
+ NSCameraUsageDescription
+ Upload image from camera for screen background
+ NSMicrophoneUsageDescription
+ Post videos to profile
diff --git a/mobile/lib/components/custom_circle_avatar.dart b/mobile/lib/components/custom_circle_avatar.dart
index bf7d1b8..1680fec 100644
--- a/mobile/lib/components/custom_circle_avatar.dart
+++ b/mobile/lib/components/custom_circle_avatar.dart
@@ -1,3 +1,5 @@
+import 'dart:io';
+
import 'package:flutter/material.dart';
enum AvatarTypes {
@@ -6,74 +8,107 @@ enum AvatarTypes {
image,
}
-class CustomCircleAvatar extends StatefulWidget {
+class CustomCircleAvatar extends StatelessWidget {
final String? initials;
final Icon? icon;
- final String? imagePath;
+ final File? image;
+ final Function ()? editImageCallback;
final double radius;
const CustomCircleAvatar({
- Key? key,
- this.initials,
- this.icon,
- this.imagePath,
- this.radius = 20,
+ Key? key,
+ this.initials,
+ this.icon,
+ this.image,
+ this.editImageCallback,
+ this.radius = 20,
}) : super(key: key);
- @override
- _CustomCircleAvatarState createState() => _CustomCircleAvatarState();
-}
-
-class _CustomCircleAvatarState extends State{
- AvatarTypes type = AvatarTypes.image;
-
- @override
- void initState() {
- super.initState();
+ Widget avatar() {
+ AvatarTypes? type;
- if (widget.imagePath != null) {
- type = AvatarTypes.image;
- return;
- }
+ if (icon != null) {
+ type = AvatarTypes.icon;
+ }
- if (widget.icon != null) {
- type = AvatarTypes.icon;
- return;
- }
+ if (initials != null) {
+ type = AvatarTypes.initials;
+ }
- if (widget.initials != null) {
- type = AvatarTypes.initials;
- return;
- }
+ if (image != null) {
+ type = AvatarTypes.image;
+ }
+ if (type == null) {
throw ArgumentError('Invalid arguments passed to CustomCircleAvatar');
- }
+ }
- Widget avatar() {
if (type == AvatarTypes.initials) {
return CircleAvatar(
- backgroundColor: Colors.grey[300],
- child: Text(widget.initials!),
- radius: widget.radius,
+ backgroundColor: Colors.grey[300],
+ child: Text(initials!),
+ radius: radius,
);
}
if (type == AvatarTypes.icon) {
return CircleAvatar(
- backgroundColor: Colors.grey[300],
- child: widget.icon,
- radius: widget.radius,
+ backgroundColor: Colors.grey[300],
+ child: icon,
+ radius: radius,
);
}
- return CircleAvatar(
- backgroundImage: AssetImage(widget.imagePath!),
- radius: widget.radius,
+ return Container(
+ width: radius * 2,
+ height: radius * 2,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ image: DecorationImage(
+ image: Image.file(image!).image,
+ fit: BoxFit.fill
+ ),
+ ),
+ );
+ }
+
+ Widget editIcon(BuildContext context) {
+ if (editImageCallback == null) {
+ return const SizedBox.shrink();
+ }
+
+ return SizedBox(
+ height: (radius * 2),
+ width: (radius * 2),
+ child: Align(
+ alignment: Alignment.bottomRight,
+ child: GestureDetector(
+ onTap: editImageCallback,
+ child: Container(
+ height: (radius / 2) + (radius / 7),
+ width: (radius / 2) + (radius / 7),
+ decoration: BoxDecoration(
+ color: Theme.of(context).scaffoldBackgroundColor,
+ borderRadius: BorderRadius.circular(30),
+ ),
+ child: Icon(
+ Icons.add,
+ color: Theme.of(context).primaryColor,
+ size: radius / 2
+ ),
+ ),
+ ),
+ ),
);
}
@override
Widget build(BuildContext context) {
- return avatar();
+ return Stack(
+ children: [
+ avatar(),
+ editIcon(context),
+ ]
+ );
}
}
diff --git a/mobile/lib/components/file_picker.dart b/mobile/lib/components/file_picker.dart
new file mode 100644
index 0000000..7160e0d
--- /dev/null
+++ b/mobile/lib/components/file_picker.dart
@@ -0,0 +1,106 @@
+import 'package:flutter/material.dart';
+import 'package:image_picker/image_picker.dart';
+
+class FilePicker extends StatelessWidget {
+ FilePicker({
+ Key? key,
+ this.cameraHandle,
+ this.galleryHandleSingle,
+ this.galleryHandleMultiple,
+ this.fileHandle,
+ }) : super(key: key);
+
+ final Function(XFile image)? cameraHandle;
+ final Function(XFile image)? galleryHandleSingle;
+ final Function(List images)? galleryHandleMultiple;
+ // TODO: Implement. Perhaps after first release?
+ final Function()? fileHandle;
+
+ final ImagePicker _picker = ImagePicker();
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.only(top: 10, bottom: 10, left: 5, right: 5),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ _filePickerSelection(
+ hasHandle: cameraHandle != null,
+ icon: Icons.camera_alt,
+ onTap: () async {
+ final XFile? image = await _picker.pickImage(source: ImageSource.camera);
+ if (image == null) {
+ return;
+ }
+ cameraHandle!(image);
+ },
+ context: context,
+ ),
+ _filePickerSelection(
+ hasHandle: galleryHandleSingle != null,
+ icon: Icons.image,
+ onTap: () async {
+ final XFile? image = await _picker.pickImage(source: ImageSource.gallery);
+ if (image == null) {
+ return;
+ }
+ galleryHandleSingle!(image);
+ },
+ context: context,
+ ),
+ _filePickerSelection(
+ hasHandle: galleryHandleMultiple != null,
+ icon: Icons.image,
+ onTap: () async {
+ final List? images = await _picker.pickMultiImage();
+ if (images == null) {
+ return;
+ }
+ galleryHandleMultiple!(images);
+ },
+ context: context,
+ ),
+ _filePickerSelection(
+ hasHandle: fileHandle != null,
+ icon: Icons.file_present_sharp,
+ onTap: () {
+ },
+ context: context,
+ ),
+ ],
+ )
+ );
+ }
+
+ Widget _filePickerSelection({
+ required bool hasHandle,
+ required IconData icon,
+ required Function() onTap,
+ required BuildContext context
+ }) {
+ if (!hasHandle) {
+ return const SizedBox.shrink();
+ }
+
+ return Padding(
+ padding: const EdgeInsets.only(left: 5, right: 5),
+ child: GestureDetector(
+ onTap: onTap,
+ child: Container(
+ height: 75,
+ width: 75,
+ decoration: BoxDecoration(
+ color: Theme.of(context).primaryColor,
+ borderRadius: BorderRadius.circular(25),
+ ),
+ child: Icon(
+ icon,
+ size: 40,
+ ),
+ ),
+ ),
+ );
+ }
+}
+
diff --git a/mobile/lib/components/user_search_result.dart b/mobile/lib/components/user_search_result.dart
index c8c7b95..2885e7e 100644
--- a/mobile/lib/components/user_search_result.dart
+++ b/mobile/lib/components/user_search_result.dart
@@ -41,7 +41,6 @@ class _UserSearchResultState extends State{
CustomCircleAvatar(
initials: widget.user.username[0].toUpperCase(),
icon: const Icon(Icons.person, size: 80),
- imagePath: null,
radius: 50,
),
const SizedBox(height: 10),
diff --git a/mobile/lib/components/view_image.dart b/mobile/lib/components/view_image.dart
new file mode 100644
index 0000000..2fe3fcf
--- /dev/null
+++ b/mobile/lib/components/view_image.dart
@@ -0,0 +1,35 @@
+import 'package:Envelope/components/custom_title_bar.dart';
+import 'package:Envelope/models/image_message.dart';
+import 'package:flutter/material.dart';
+
+class ViewImage extends StatelessWidget {
+ const ViewImage({
+ Key? key,
+ required this.message,
+ }) : super(key: key);
+
+ final ImageMessage message;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.black,
+ appBar: const CustomTitleBar(
+ title: Text(''),
+ showBack: true,
+ backgroundColor: Colors.black,
+ ),
+ body: Center(
+ child: InteractiveViewer(
+ panEnabled: false,
+ minScale: 1,
+ maxScale: 4,
+ child: Image.file(
+ message.file,
+ ),
+ )
+ ),
+ );
+ }
+}
+
diff --git a/mobile/lib/exceptions/update_data_exception.dart b/mobile/lib/exceptions/update_data_exception.dart
new file mode 100644
index 0000000..8d1d6bb
--- /dev/null
+++ b/mobile/lib/exceptions/update_data_exception.dart
@@ -0,0 +1,13 @@
+
+class UpdateDataException implements Exception {
+ final String _message;
+
+ UpdateDataException([
+ this._message = 'An error occured while updating data.',
+ ]);
+
+ @override
+ String toString() {
+ return _message;
+ }
+}
diff --git a/mobile/lib/models/conversations.dart b/mobile/lib/models/conversations.dart
index e7d760d..d8222d3 100644
--- a/mobile/lib/models/conversations.dart
+++ b/mobile/lib/models/conversations.dart
@@ -1,11 +1,15 @@
import 'dart:convert';
+import 'dart:io';
import 'dart:typed_data';
import 'package:Envelope/models/messages.dart';
+import 'package:Envelope/models/text_messages.dart';
+import 'package:mime/mime.dart';
import 'package:pointycastle/export.dart';
import 'package:sqflite/sqflite.dart';
import 'package:uuid/uuid.dart';
+import '../utils/storage/write_file.dart';
import '/models/conversation_users.dart';
import '/models/friends.dart';
import '/models/my_profile.dart';
@@ -34,7 +38,7 @@ Future createConversation(String title, List friends, bool
status: ConversationStatus.pending,
isRead: true,
);
-
+
await db.insert(
'conversations',
conversation.toMap(),
@@ -142,6 +146,11 @@ Future getConversationById(String id) async {
throw ArgumentError('Invalid user id');
}
+ File? file;
+ if (maps[0]['file'] != null && maps[0]['file'] != '') {
+ file = File(maps[0]['file']);
+ }
+
return Conversation(
id: maps[0]['id'],
userId: maps[0]['user_id'],
@@ -151,6 +160,7 @@ Future getConversationById(String id) async {
twoUser: maps[0]['two_user'] == 1,
status: ConversationStatus.values[maps[0]['status']],
isRead: maps[0]['is_read'] == 1,
+ icon: file,
);
}
@@ -164,6 +174,12 @@ Future> getConversations() async {
);
return List.generate(maps.length, (i) {
+
+ File? file;
+ if (maps[i]['file'] != null && maps[i]['file'] != '') {
+ file = File(maps[i]['file']);
+ }
+
return Conversation(
id: maps[i]['id'],
userId: maps[i]['user_id'],
@@ -173,6 +189,7 @@ Future> getConversations() async {
twoUser: maps[i]['two_user'] == 1,
status: ConversationStatus.values[maps[i]['status']],
isRead: maps[i]['is_read'] == 1,
+ icon: file,
);
});
}
@@ -184,7 +201,7 @@ Future getTwoUserConversation(String userId) async {
final List