diff --git a/Backend/Api/Auth/ChangeMessageExpiry.go b/Backend/Api/Auth/ChangeUserMessageExpiry.go similarity index 88% rename from Backend/Api/Auth/ChangeMessageExpiry.go rename to Backend/Api/Auth/ChangeUserMessageExpiry.go index aa2fd5e..4805519 100644 --- a/Backend/Api/Auth/ChangeMessageExpiry.go +++ b/Backend/Api/Auth/ChangeUserMessageExpiry.go @@ -13,8 +13,8 @@ type rawChangeMessageExpiry struct { MessageExpiry string `json:"message_expiry"` } -// ChangeMessageExpiry handles changing default message expiry for user -func ChangeMessageExpiry(w http.ResponseWriter, r *http.Request) { +// ChangeUserMessageExpiry handles changing default message expiry for user +func ChangeUserMessageExpiry(w http.ResponseWriter, r *http.Request) { var ( user Models.User changeMessageExpiry rawChangeMessageExpiry diff --git a/Backend/Api/Auth/ChangeMessageExpiry_test.go b/Backend/Api/Auth/ChangeUserMessageExpiry_test.go similarity index 97% rename from Backend/Api/Auth/ChangeMessageExpiry_test.go rename to Backend/Api/Auth/ChangeUserMessageExpiry_test.go index 2c48c75..ca13511 100644 --- a/Backend/Api/Auth/ChangeMessageExpiry_test.go +++ b/Backend/Api/Auth/ChangeUserMessageExpiry_test.go @@ -10,7 +10,7 @@ import ( "git.tovijaeschke.xyz/tovi/Capsule/Backend/Tests" ) -func Test_ChangeMessageExpiry(t *testing.T) { +func Test_ChangeUserMessageExpiry(t *testing.T) { client, ts, err := Tests.InitTestEnv() defer ts.Close() if err != nil { diff --git a/Backend/Api/Friends/AcceptFriendRequest_test.go b/Backend/Api/Friends/AcceptFriendRequest_test.go new file mode 100644 index 0000000..ff5c642 --- /dev/null +++ b/Backend/Api/Friends/AcceptFriendRequest_test.go @@ -0,0 +1,137 @@ +package Friends_test + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "testing" + + "git.tovijaeschke.xyz/tovi/Capsule/Backend/Database" + "git.tovijaeschke.xyz/tovi/Capsule/Backend/Database/Seeder" + "git.tovijaeschke.xyz/tovi/Capsule/Backend/Models" + "git.tovijaeschke.xyz/tovi/Capsule/Backend/Tests" +) + +func Test_AcceptFriendRequest(t *testing.T) { + client, ts, err := Tests.InitTestEnv() + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + u, err := Database.GetUserByUsername("test") + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + u2, err := Tests.InitTestCreateUser("test2") + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + key, err := Seeder.GenerateAesKey() + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + decodedPublicKey := Seeder.GetPubKey() + + encPublicKey, err := key.AesEncrypt([]byte(Seeder.PublicKey)) + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + friendReq := Models.FriendRequest{ + UserID: u.ID, + FriendID: base64.StdEncoding.EncodeToString( + Seeder.EncryptWithPublicKey( + []byte(u2.ID.String()), + decodedPublicKey, + ), + ), + FriendUsername: base64.StdEncoding.EncodeToString( + Seeder.EncryptWithPublicKey( + []byte(u2.Username), + decodedPublicKey, + ), + ), + FriendPublicAsymmetricKey: base64.StdEncoding.EncodeToString( + encPublicKey, + ), + SymmetricKey: base64.StdEncoding.EncodeToString( + Seeder.EncryptWithPublicKey(key.Key, decodedPublicKey), + ), + } + + err = Database.CreateFriendRequest(&friendReq) + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + friendReqResponse := Models.FriendRequest{ + UserID: u2.ID, + FriendID: base64.StdEncoding.EncodeToString( + Seeder.EncryptWithPublicKey( + []byte(u.ID.String()), + decodedPublicKey, + ), + ), + FriendUsername: base64.StdEncoding.EncodeToString( + Seeder.EncryptWithPublicKey( + []byte(u.Username), + decodedPublicKey, + ), + ), + FriendPublicAsymmetricKey: base64.StdEncoding.EncodeToString( + encPublicKey, + ), + SymmetricKey: base64.StdEncoding.EncodeToString( + Seeder.EncryptWithPublicKey(key.Key, decodedPublicKey), + ), + } + + jsonStr, _ := json.Marshal(friendReqResponse) + req, _ := http.NewRequest( + "POST", + fmt.Sprintf( + "%s/api/v1/auth/friend_request/%s", + ts.URL, + friendReq.ID, + ), + bytes.NewBuffer(jsonStr), + ) + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + if resp.StatusCode != http.StatusNoContent { + t.Errorf("Expected %d, recieved %d", http.StatusNoContent, resp.StatusCode) + return + } + + var reqs []Models.FriendRequest + + err = Database.DB.Find(&reqs).Error + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + for _, r := range reqs { + if r.AcceptedAt.Valid != true { + t.Errorf("Expected true, recieved false") + return + } + } +} diff --git a/Backend/Api/Friends/CreateFriendRequest_test.go b/Backend/Api/Friends/CreateFriendRequest_test.go new file mode 100644 index 0000000..49b152e --- /dev/null +++ b/Backend/Api/Friends/CreateFriendRequest_test.go @@ -0,0 +1,204 @@ +package Friends_test + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "net/http" + "testing" + + "git.tovijaeschke.xyz/tovi/Capsule/Backend/Database" + "git.tovijaeschke.xyz/tovi/Capsule/Backend/Database/Seeder" + "git.tovijaeschke.xyz/tovi/Capsule/Backend/Models" + "git.tovijaeschke.xyz/tovi/Capsule/Backend/Tests" +) + +func Test_CreateFriendRequest(t *testing.T) { + client, ts, err := Tests.InitTestEnv() + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + u, err := Database.GetUserByUsername("test") + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + u2, err := Tests.InitTestCreateUser("test2") + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + key, err := Seeder.GenerateAesKey() + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + decodedPublicKey := Seeder.GetPubKey() + + encPublicKey, err := key.AesEncrypt([]byte(Seeder.PublicKey)) + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + friendReq := Models.FriendRequest{ + UserID: u.ID, + FriendID: base64.StdEncoding.EncodeToString( + Seeder.EncryptWithPublicKey( + []byte(u2.ID.String()), + decodedPublicKey, + ), + ), + FriendUsername: base64.StdEncoding.EncodeToString( + Seeder.EncryptWithPublicKey( + []byte(u2.Username), + decodedPublicKey, + ), + ), + FriendPublicAsymmetricKey: base64.StdEncoding.EncodeToString( + encPublicKey, + ), + SymmetricKey: base64.StdEncoding.EncodeToString( + Seeder.EncryptWithPublicKey(key.Key, decodedPublicKey), + ), + } + + jsonStr, _ := json.Marshal(friendReq) + req, _ := http.NewRequest("POST", ts.URL+"/api/v1/auth/friend_request", bytes.NewBuffer(jsonStr)) + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + if resp.StatusCode != http.StatusOK { + t.Errorf("Expected %d, recieved %d", http.StatusOK, resp.StatusCode) + return + } + + var r Models.FriendRequest + + err = Database.DB.First(&r).Error + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + if r.AcceptedAt.Valid == true { + t.Errorf("Expected false, recieved true") + return + } +} + +func Test_CreateFriendRequestQrCode(t *testing.T) { + client, ts, err := Tests.InitTestEnv() + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + u, err := Database.GetUserByUsername("test") + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + u2, err := Tests.InitTestCreateUser("test2") + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + key, err := Seeder.GenerateAesKey() + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + decodedPublicKey := Seeder.GetPubKey() + + encPublicKey, err := key.AesEncrypt([]byte(Seeder.PublicKey)) + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + friendReq := Models.FriendRequest{ + UserID: u.ID, + FriendID: base64.StdEncoding.EncodeToString( + Seeder.EncryptWithPublicKey( + []byte(u2.ID.String()), + decodedPublicKey, + ), + ), + FriendUsername: base64.StdEncoding.EncodeToString( + Seeder.EncryptWithPublicKey( + []byte(u2.Username), + decodedPublicKey, + ), + ), + FriendPublicAsymmetricKey: base64.StdEncoding.EncodeToString( + encPublicKey, + ), + SymmetricKey: base64.StdEncoding.EncodeToString( + Seeder.EncryptWithPublicKey(key.Key, decodedPublicKey), + ), + } + + friendReq2 := Models.FriendRequest{ + UserID: u2.ID, + FriendID: base64.StdEncoding.EncodeToString( + Seeder.EncryptWithPublicKey( + []byte(u.ID.String()), + decodedPublicKey, + ), + ), + FriendUsername: base64.StdEncoding.EncodeToString( + Seeder.EncryptWithPublicKey( + []byte(u.Username), + decodedPublicKey, + ), + ), + FriendPublicAsymmetricKey: base64.StdEncoding.EncodeToString( + encPublicKey, + ), + SymmetricKey: base64.StdEncoding.EncodeToString( + Seeder.EncryptWithPublicKey(key.Key, decodedPublicKey), + ), + } + + jsonStr, _ := json.Marshal([]Models.FriendRequest{friendReq, friendReq2}) + req, _ := http.NewRequest("POST", ts.URL+"/api/v1/auth/friend_request/qr_code", bytes.NewBuffer(jsonStr)) + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + if resp.StatusCode != http.StatusOK { + t.Errorf("Expected %d, recieved %d", http.StatusOK, resp.StatusCode) + return + } + + var r Models.FriendRequest + + err = Database.DB.First(&r).Error + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + if r.AcceptedAt.Valid == false { + t.Errorf("Expected true, recieved false") + return + } +} diff --git a/Backend/Api/Friends/FriendRequest.go b/Backend/Api/Friends/FriendRequest.go deleted file mode 100644 index c704800..0000000 --- a/Backend/Api/Friends/FriendRequest.go +++ /dev/null @@ -1,60 +0,0 @@ -package Friends - -import ( - "encoding/json" - "io/ioutil" - "net/http" - - "git.tovijaeschke.xyz/tovi/Capsule/Backend/Database" - "git.tovijaeschke.xyz/tovi/Capsule/Backend/Models" - "git.tovijaeschke.xyz/tovi/Capsule/Backend/Util" -) - -func FriendRequest(w http.ResponseWriter, r *http.Request) { - var ( - user Models.User - requestBody []byte - requestJson map[string]interface{} - friendID string - friendRequest Models.FriendRequest - ok bool - err error - ) - - user, err = Util.GetUserById(w, r) - if err != nil { - http.Error(w, "Not Found", http.StatusNotFound) - return - } - - requestBody, err = ioutil.ReadAll(r.Body) - if err != nil { - http.Error(w, "Not Found", http.StatusNotFound) - return - } - - json.Unmarshal(requestBody, &requestJson) - if requestJson["id"] == nil { - http.Error(w, "Invalid Data", http.StatusBadRequest) - return - } - - friendID, ok = requestJson["id"].(string) - if !ok { - http.Error(w, "Error", http.StatusInternalServerError) - return - } - - friendRequest = Models.FriendRequest{ - UserID: user.ID, - FriendID: friendID, - } - - err = Database.CreateFriendRequest(&friendRequest) - if requestJson["id"] == nil { - http.Error(w, "Error", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusNoContent) -} diff --git a/Backend/Api/Friends/Friends_test.go b/Backend/Api/Friends/Friends_test.go index d18de12..ee177b8 100644 --- a/Backend/Api/Friends/Friends_test.go +++ b/Backend/Api/Friends/Friends_test.go @@ -28,17 +28,17 @@ func Test_FriendRequestList(t *testing.T) { return } + key, err := Seeder.GenerateAesKey() + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + for i := 0; i < 30; i++ { u2, err := Tests.InitTestCreateUser(fmt.Sprintf("test%d", i)) decodedPublicKey := Seeder.GetPubKey() - key, err := Seeder.GenerateAesKey() - if err != nil { - t.Errorf("Expected nil, recieved %s", err.Error()) - return - } - encPublicKey, err := key.AesEncrypt([]byte(Seeder.PublicKey)) if err != nil { t.Errorf("Expected nil, recieved %s", err.Error()) diff --git a/Backend/Api/Messages/ChangeConversationMessageExpiry.go b/Backend/Api/Messages/ChangeConversationMessageExpiry.go new file mode 100644 index 0000000..b589590 --- /dev/null +++ b/Backend/Api/Messages/ChangeConversationMessageExpiry.go @@ -0,0 +1,74 @@ +package Messages + +import ( + "encoding/json" + "io/ioutil" + "net/http" + + "git.tovijaeschke.xyz/tovi/Capsule/Backend/Database" + "git.tovijaeschke.xyz/tovi/Capsule/Backend/Models" + "github.com/gorilla/mux" +) + +type rawChangeMessageExpiry struct { + MessageExpiry string `json:"message_expiry"` +} + +// ChangeUserMessageExpiry handles changing default message expiry for user +func ChangeConversationMessageExpiry(w http.ResponseWriter, r *http.Request) { + var ( + // user Models.User + changeMessageExpiry rawChangeMessageExpiry + conversationDetail Models.ConversationDetail + requestBody []byte + urlVars map[string]string + detailID 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 + } + + // Ignore error here, as middleware should handle auth + // TODO: Check if user in conversation + // user, _ = Auth.CheckCookieCurrentUser(w, r) + + requestBody, err = ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + err = json.Unmarshal(requestBody, &changeMessageExpiry) + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + err = conversationDetail.MessageExpiryDefault.Scan(changeMessageExpiry.MessageExpiry) + if err != nil { + http.Error(w, "Error", http.StatusUnprocessableEntity) + return + } + + 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 1639111..03da54e 100644 --- a/Backend/Api/Messages/Conversations.go +++ b/Backend/Api/Messages/Conversations.go @@ -1,6 +1,7 @@ package Messages import ( + "database/sql/driver" "encoding/json" "net/http" "net/url" @@ -58,6 +59,7 @@ func ConversationDetailsList(w http.ResponseWriter, r *http.Request) { detail Models.ConversationDetail query url.Values conversationIds []string + messageExpiryRaw driver.Value returnJSON []byte i int ok bool @@ -82,6 +84,9 @@ func ConversationDetailsList(w http.ResponseWriter, r *http.Request) { } for i, detail = range conversationDetails { + messageExpiryRaw, _ = detail.MessageExpiryDefault.Value() + conversationDetails[i].MessageExpiry, _ = messageExpiryRaw.(string) + if detail.AttachmentID == nil { continue } diff --git a/Backend/Api/Messages/CreateMessage.go b/Backend/Api/Messages/CreateMessage.go index 04cb15e..faf6dad 100644 --- a/Backend/Api/Messages/CreateMessage.go +++ b/Backend/Api/Messages/CreateMessage.go @@ -44,14 +44,12 @@ func CreateMessage(w http.ResponseWriter, r *http.Request) { for i, message = range messageData.Messages { t, err = time.Parse("2006-01-02T15:04:05Z", message.ExpiryRaw) - if err != nil { - http.Error(w, "Error", http.StatusInternalServerError) - return - } - err = messageData.Messages[i].Expiry.Scan(t) - if err != nil { - http.Error(w, "Error", http.StatusInternalServerError) - return + if err == nil { + err = messageData.Messages[i].Expiry.Scan(t) + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } } } diff --git a/Backend/Api/Routes.go b/Backend/Api/Routes.go index 73d98f4..5fd7ab6 100644 --- a/Backend/Api/Routes.go +++ b/Backend/Api/Routes.go @@ -63,7 +63,7 @@ func InitAPIEndpoints(router *mux.Router) { authAPI.HandleFunc("/check", Auth.Check).Methods("GET") authAPI.HandleFunc("/change_password", Auth.ChangePassword).Methods("POST") - authAPI.HandleFunc("/message_expiry", Auth.ChangeMessageExpiry).Methods("POST") + authAPI.HandleFunc("/message_expiry", Auth.ChangeUserMessageExpiry).Methods("POST") authAPI.HandleFunc("/image", Auth.AddProfileImage).Methods("POST") authAPI.HandleFunc("/users", Users.SearchUsers).Methods("GET") @@ -79,6 +79,7 @@ func InitAPIEndpoints(router *mux.Router) { 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("/conversations/{detailID}/message_expiry", Messages.ChangeConversationMessageExpiry).Methods("POST") authAPI.HandleFunc("/message", Messages.CreateMessage).Methods("POST") authAPI.HandleFunc("/messages/{associationKey}", Messages.Messages).Methods("GET") diff --git a/Backend/Database/ConversationDetails.go b/Backend/Database/ConversationDetails.go index 811fa62..7de9fe5 100644 --- a/Backend/Database/ConversationDetails.go +++ b/Backend/Database/ConversationDetails.go @@ -10,51 +10,51 @@ import ( // GetConversationDetailByID gets by id func GetConversationDetailByID(id string) (Models.ConversationDetail, error) { var ( - messageThread Models.ConversationDetail - err error + conversationDetail Models.ConversationDetail + err error ) err = DB.Preload(clause.Associations). Where("id = ?", id). - First(&messageThread). + First(&conversationDetail). Error - return messageThread, err + return conversationDetail, err } // GetConversationDetailsByIds gets by multiple ids func GetConversationDetailsByIds(id []string) ([]Models.ConversationDetail, error) { var ( - messageThread []Models.ConversationDetail - err error + conversationDetail []Models.ConversationDetail + err error ) err = DB.Preload(clause.Associations). Where("id IN ?", id). - Find(&messageThread). + Find(&conversationDetail). Error - return messageThread, err + return conversationDetail, err } // CreateConversationDetail creates a ConversationDetail record -func CreateConversationDetail(messageThread *Models.ConversationDetail) error { +func CreateConversationDetail(conversationDetail *Models.ConversationDetail) error { return DB.Session(&gorm.Session{FullSaveAssociations: true}). - Create(messageThread). + Create(conversationDetail). Error } // UpdateConversationDetail updates a ConversationDetail record -func UpdateConversationDetail(messageThread *Models.ConversationDetail) error { +func UpdateConversationDetail(conversationDetail *Models.ConversationDetail) error { return DB.Session(&gorm.Session{FullSaveAssociations: true}). - Where("id = ?", messageThread.ID). - Updates(messageThread). + Where("id = ?", conversationDetail.ID). + Updates(conversationDetail). Error } // DeleteConversationDetail deletes a ConversationDetail record -func DeleteConversationDetail(messageThread *Models.ConversationDetail) error { +func DeleteConversationDetail(conversationDetail *Models.ConversationDetail) error { return DB.Session(&gorm.Session{FullSaveAssociations: true}). - Delete(messageThread). + Delete(conversationDetail). Error } diff --git a/Backend/Database/Seeder/FriendSeeder.go b/Backend/Database/Seeder/FriendSeeder.go index 43cdd0f..cc5edee 100644 --- a/Backend/Database/Seeder/FriendSeeder.go +++ b/Backend/Database/Seeder/FriendSeeder.go @@ -90,10 +90,10 @@ func SeedFriends() { err error ) - err = copyProfileImage() - if err != nil { - panic(err) - } + // err = copyProfileImage() + // if err != nil { + // panic(err) + // } primaryUser, err = Database.GetUserByUsername("testUser") if err != nil { diff --git a/Backend/Dockerfile b/Backend/Dockerfile index 9fea42e..71c43c5 100644 --- a/Backend/Dockerfile +++ b/Backend/Dockerfile @@ -6,11 +6,9 @@ COPY ./ /go/src/git.tovijaeschke.xyz/Capsule/Backend WORKDIR /go/src/git.tovijaeschke.xyz/Capsule/Backend -# For "go test" -RUN apk add gcc libc-dev +# For "go test" and development +RUN apk add gcc libc-dev inotify-tools RUN go mod download -RUN go build -o /go/bin/capsule-server main.go - -CMD [ "/go/bin/capsule-server" ] +CMD [ "sh", "./dev.sh" ] diff --git a/Backend/Dockerfile.prod b/Backend/Dockerfile.prod new file mode 100644 index 0000000..9fea42e --- /dev/null +++ b/Backend/Dockerfile.prod @@ -0,0 +1,16 @@ +FROM golang:1.19-alpine + +RUN mkdir -p /go/src/git.tovijaeschke.xyz/Capsule/Backend + +COPY ./ /go/src/git.tovijaeschke.xyz/Capsule/Backend + +WORKDIR /go/src/git.tovijaeschke.xyz/Capsule/Backend + +# For "go test" +RUN apk add gcc libc-dev + +RUN go mod download + +RUN go build -o /go/bin/capsule-server main.go + +CMD [ "/go/bin/capsule-server" ] diff --git a/Backend/Models/Conversations.go b/Backend/Models/Conversations.go index 6df37ec..bc6ff3a 100644 --- a/Backend/Models/Conversations.go +++ b/Backend/Models/Conversations.go @@ -9,11 +9,13 @@ 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"` - AttachmentID *uuid.UUID ` json:"attachment_id"` - Attachment Attachment ` json:"attachment"` + 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"` + 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', 'no_expiry')"` // Stored encrypted + MessageExpiry string `gorm:"-" json:"message_expiry"` // Stored encrypted } // ConversationDetailUser all users associated with a customer diff --git a/Backend/Models/MessageExpiry.go b/Backend/Models/MessageExpiry.go new file mode 100644 index 0000000..4d25ccc --- /dev/null +++ b/Backend/Models/MessageExpiry.go @@ -0,0 +1,70 @@ +package Models + +import ( + "database/sql/driver" + "errors" +) + +// MessageExpiry holds values for how long messages should expire by default +type MessageExpiry []uint8 + +const ( + // MessageExpiryFifteenMin expires after 15 minutes + MessageExpiryFifteenMin = "fifteen_min" + // MessageExpiryThirtyMin expires after 30 minutes + MessageExpiryThirtyMin = "thirty_min" + // MessageExpiryOneHour expires after one hour + MessageExpiryOneHour = "one_hour" + // MessageExpiryThreeHour expires after three hours + MessageExpiryThreeHour = "three_hour" + // MessageExpirySixHour expires after six hours + MessageExpirySixHour = "six_hour" + // MessageExpiryTwelveHour expires after twelve hours + MessageExpiryTwelveHour = "twelve_hour" + // MessageExpiryOneDay expires after one day + MessageExpiryOneDay = "one_day" + // MessageExpiryThreeDay expires after three days + MessageExpiryThreeDay = "three_day" + // MessageExpiryNoExpiry never expires + MessageExpiryNoExpiry = "no_expiry" +) + +// MessageExpiryValues list of all expiry values for validation +var MessageExpiryValues = []string{ + MessageExpiryFifteenMin, + MessageExpiryThirtyMin, + MessageExpiryOneHour, + MessageExpiryThreeHour, + MessageExpirySixHour, + MessageExpiryTwelveHour, + MessageExpiryOneDay, + MessageExpiryThreeDay, + MessageExpiryNoExpiry, +} + +// Scan new value into MessageExpiry +func (e *MessageExpiry) Scan(value interface{}) error { + var ( + strValue = value.(string) + m string + ) + + for _, m = range MessageExpiryValues { + if strValue != m { + continue + } + *e = MessageExpiry(strValue) + return nil + } + + return errors.New("Invalid MessageExpiry value") +} + +// Value gets value out of MessageExpiry column +func (e MessageExpiry) Value() (driver.Value, error) { + return string(e), nil +} + +func (e MessageExpiry) String() string { + return string(e) +} diff --git a/Backend/Models/Users.go b/Backend/Models/Users.go index 736289e..fa587d4 100644 --- a/Backend/Models/Users.go +++ b/Backend/Models/Users.go @@ -1,84 +1,20 @@ package Models import ( - "database/sql/driver" - "errors" - "github.com/gofrs/uuid" "gorm.io/gorm" ) -// BeforeUpdate prevents updating the email if it has not changed +// BeforeUpdate prevents updating the username or email if it has not changed // This stops a unique constraint error func (u *User) BeforeUpdate(tx *gorm.DB) (err error) { if !tx.Statement.Changed("Username") { tx.Statement.Omit("Username") } - return nil -} - -// MessageExpiry holds values for how long messages should expire by default -type MessageExpiry []uint8 - -const ( - // MessageExpiryFifteenMin expires after 15 minutes - MessageExpiryFifteenMin = "fifteen_min" - // MessageExpiryThirtyMin expires after 30 minutes - MessageExpiryThirtyMin = "thirty_min" - // MessageExpiryOneHour expires after one hour - MessageExpiryOneHour = "one_hour" - // MessageExpiryThreeHour expires after three hours - MessageExpiryThreeHour = "three_hour" - // MessageExpirySixHour expires after six hours - MessageExpirySixHour = "six_hour" - // MessageExpiryTwelveHour expires after twelve hours - MessageExpiryTwelveHour = "twelve_hour" - // MessageExpiryOneDay expires after one day - MessageExpiryOneDay = "one_day" - // MessageExpiryThreeDay expires after three days - MessageExpiryThreeDay = "three_day" - // MessageExpiryNoExpiry never expires - MessageExpiryNoExpiry = "no_expiry" -) - -// MessageExpiryValues list of all expiry values for validation -var MessageExpiryValues = []string{ - MessageExpiryFifteenMin, - MessageExpiryThirtyMin, - MessageExpiryOneHour, - MessageExpiryThreeHour, - MessageExpirySixHour, - MessageExpiryTwelveHour, - MessageExpiryOneDay, - MessageExpiryThreeDay, - MessageExpiryNoExpiry, -} - -// Scan new value into MessageExpiry -func (e *MessageExpiry) Scan(value interface{}) error { - var ( - strValue = value.(string) - m string - ) - - for _, m = range MessageExpiryValues { - if strValue != m { - continue - } - *e = MessageExpiry(strValue) - return nil + if !tx.Statement.Changed("Email") { + tx.Statement.Omit("Email") } - - return errors.New("Invalid MessageExpiry value") -} - -// Value gets value out of MessageExpiry column -func (e MessageExpiry) Value() (driver.Value, error) { - return string(e), nil -} - -func (e MessageExpiry) String() string { - return string(e) + return nil } // User holds user data @@ -87,19 +23,11 @@ type User struct { Username string `gorm:"not null;unique" json:"username"` Password string `gorm:"not null" json:"password"` ConfirmPassword string `gorm:"-" json:"confirm_password"` + Email string ` json:"email"` AsymmetricPrivateKey string `gorm:"not null" json:"asymmetric_private_key"` // Stored encrypted AsymmetricPublicKey string `gorm:"not null" json:"asymmetric_public_key"` 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 + 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', 'no_expiry')"` // Stored encrypted } diff --git a/Backend/dev.sh b/Backend/dev.sh new file mode 100644 index 0000000..8b41296 --- /dev/null +++ b/Backend/dev.sh @@ -0,0 +1,8 @@ +#!/bin/sh +while true; do + go build main.go + ./main & + PID=$! + inotifywait -r -e modify . + kill $PID +done diff --git a/Backend/main b/Backend/main new file mode 100755 index 0000000..8bb2fa5 Binary files /dev/null and b/Backend/main differ diff --git a/README.md b/README.md index d43ef78..2fff85b 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,14 @@ Encrypted messaging app ## TODO -[x] Fix adding users to conversations -[x] Fix users recieving messages -[x] Fix the admin checks on conversation settings page -[x] Fix sending messages in a conversation that includes users that are not the current users friend -[x] Add admin checks to conversation settings page -[ ] Add admin checks on backend -[ ] Add errors to login / signup page -[ ] Add errors when updating conversations -[ ] Refactor the update conversations function -[ ] Finish the friends list page -[ ] Allow adding friends -[ ] Finish the disappearing messages functionality +- Fix friends list search +- Add friends profile picture +- Add conversation pagination +- Add message pagination +- Add message pagination +- Finish off conversation settings page +- Finish message expiry +- Add back button to QR scanner +- Add more padding at message send text box +- Fix error when creating existing conversation between friends +- Sort conversation based on latest message diff --git a/docker-compose.yml b/docker-compose.yml index 6c89730..05a879a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: ports: - "8080:8080" volumes: - - "./Backend:/app" + - "./Backend:/go/src/git.tovijaeschke.xyz/Capsule/Backend" links: - postgres - postgres-testing diff --git a/mobile/ios/Flutter/AppFrameworkInfo.plist b/mobile/ios/Flutter/AppFrameworkInfo.plist index 8d4492f..9625e10 100644 --- a/mobile/ios/Flutter/AppFrameworkInfo.plist +++ b/mobile/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/mobile/ios/Flutter/Debug.xcconfig b/mobile/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/mobile/ios/Flutter/Debug.xcconfig +++ b/mobile/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/mobile/ios/Flutter/Release.xcconfig b/mobile/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/mobile/ios/Flutter/Release.xcconfig +++ b/mobile/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/mobile/ios/Podfile b/mobile/ios/Podfile new file mode 100644 index 0000000..88359b2 --- /dev/null +++ b/mobile/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock new file mode 100644 index 0000000..85e330e --- /dev/null +++ b/mobile/ios/Podfile.lock @@ -0,0 +1,59 @@ +PODS: + - Flutter (1.0.0) + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) + - image_picker_ios (0.0.1): + - Flutter + - MTBBarcodeScanner (5.0.11) + - path_provider_ios (0.0.1): + - Flutter + - qr_code_scanner (0.2.0): + - Flutter + - MTBBarcodeScanner + - shared_preferences_ios (0.0.1): + - Flutter + - sqflite (0.0.2): + - Flutter + - FMDB (>= 2.7.5) + +DEPENDENCIES: + - Flutter (from `Flutter`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`) + - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) + - sqflite (from `.symlinks/plugins/sqflite/ios`) + +SPEC REPOS: + trunk: + - FMDB + - MTBBarcodeScanner + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + path_provider_ios: + :path: ".symlinks/plugins/path_provider_ios/ios" + qr_code_scanner: + :path: ".symlinks/plugins/qr_code_scanner/ios" + shared_preferences_ios: + :path: ".symlinks/plugins/shared_preferences_ios/ios" + sqflite: + :path: ".symlinks/plugins/sqflite/ios" + +SPEC CHECKSUMS: + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb + MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb + path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 + qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e + shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad + sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 + +PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 + +COCOAPODS: 1.11.3 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index e400c34..5696b06 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7C176E8E73493FDFE1D7C309 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 00CA36B62CB085270A92E728 /* Pods_Runner.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -29,12 +30,15 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 00CA36B62CB085270A92E728 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7D2302AC69A24C6395A4B678 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 7F7E44AA84E3C88CDE6063C2 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -42,6 +46,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E1FB47F20CF9808436BCAA60 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,6 +54,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 7C176E8E73493FDFE1D7C309 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -72,6 +78,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + BF068434B7059F0CC04D9821 /* Pods */, + AA66445C27884182A5A74088 /* Frameworks */, ); sourceTree = ""; }; @@ -98,6 +106,25 @@ path = Runner; sourceTree = ""; }; + AA66445C27884182A5A74088 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 00CA36B62CB085270A92E728 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + BF068434B7059F0CC04D9821 /* Pods */ = { + isa = PBXGroup; + children = ( + 7F7E44AA84E3C88CDE6063C2 /* Pods-Runner.debug.xcconfig */, + 7D2302AC69A24C6395A4B678 /* Pods-Runner.release.xcconfig */, + E1FB47F20CF9808436BCAA60 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -105,12 +132,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 90DE9D5E1F568436E776C753 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 1015E9199730C97F537825AC /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -169,6 +198,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 1015E9199730C97F537825AC /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -183,6 +229,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 90DE9D5E1F568436E776C753 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -272,7 +340,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -349,7 +417,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -398,7 +466,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata b/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index f2762fc..c9e78a4 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -53,5 +53,7 @@ Upload image from camera for screen background NSMicrophoneUsageDescription Post videos to profile + CADisableMinimumFrameDurationOnPhone + diff --git a/mobile/lib/models/conversations.dart b/mobile/lib/models/conversations.dart index 621dfae..c07683f 100644 --- a/mobile/lib/models/conversations.dart +++ b/mobile/lib/models/conversations.dart @@ -37,6 +37,7 @@ Future createConversation(String title, List friends, bool twoUser: twoUser, status: ConversationStatus.pending, isRead: true, + messageExpiryDefault: 'no_expiry' ); await db.insert( @@ -161,6 +162,7 @@ Future getConversationById(String id) async { status: ConversationStatus.values[maps[0]['status']], isRead: maps[0]['is_read'] == 1, icon: file, + messageExpiryDefault: maps[0]['message_expiry'], ); } @@ -190,6 +192,7 @@ Future> getConversations() async { status: ConversationStatus.values[maps[i]['status']], isRead: maps[i]['is_read'] == 1, icon: file, + messageExpiryDefault: maps[i]['message_expiry'] ?? 'no_expiry', ); }); } @@ -223,6 +226,7 @@ Future getTwoUserConversation(String userId) async { twoUser: maps[0]['two_user'] == 1, status: ConversationStatus.values[maps[0]['status']], isRead: maps[0]['is_read'] == 1, + messageExpiryDefault: maps[0]['message_expiry'], ); } @@ -236,6 +240,7 @@ class Conversation { bool twoUser; ConversationStatus status; bool isRead; + String messageExpiryDefault = 'no_expiry'; File? icon; Conversation({ @@ -247,6 +252,7 @@ class Conversation { required this.twoUser, required this.status, required this.isRead, + required this.messageExpiryDefault, this.icon, }); @@ -276,6 +282,7 @@ class Conversation { twoUser: false, status: ConversationStatus.complete, isRead: true, + messageExpiryDefault: 'no_expiry', ); } @@ -321,6 +328,7 @@ class Conversation { 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)), 'users': await getEncryptedConversationUsers(this, symKey), 'two_user': AesHelper.aesEncrypt(symKey, Uint8List.fromList((twoUser ? 'true' : 'false').codeUnits)), + 'message_expiry': messageExpiryDefault, 'user_conversations': userConversations, }; @@ -358,6 +366,7 @@ class Conversation { 'status': status.index, 'is_read': isRead ? 1 : 0, 'file': icon != null ? icon!.path : null, + 'message_expiry': messageExpiryDefault, }; } diff --git a/mobile/lib/models/my_profile.dart b/mobile/lib/models/my_profile.dart index 37bc0c4..cbf97ed 100644 --- a/mobile/lib/models/my_profile.dart +++ b/mobile/lib/models/my_profile.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:Capsule/utils/storage/get_file.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:pointycastle/impl.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -11,7 +10,7 @@ import '/utils/encryption/aes_helper.dart'; import '/utils/encryption/crypto_utils.dart'; // TODO: Replace this with the prod url when server is deployed -String defaultServerUrl = dotenv.env['SERVER_URL'] ?? 'http://192.168.1.5:8080'; +String defaultServerUrl = dotenv.env['SERVER_URL'] ?? 'http://localhost:8080/'; class MyProfile { String id; diff --git a/mobile/lib/models/text_messages.dart b/mobile/lib/models/text_messages.dart index e9ba715..0b85c59 100644 --- a/mobile/lib/models/text_messages.dart +++ b/mobile/lib/models/text_messages.dart @@ -99,7 +99,7 @@ class TextMessage extends Message { ); Map messageData = { - 'id': id, + 'id': messageDataId, 'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(text.codeUnits)), 'sender_id': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(senderId.codeUnits)), 'symmetric_key': AesHelper.aesEncrypt( diff --git a/mobile/lib/utils/storage/conversations.dart b/mobile/lib/utils/storage/conversations.dart index 9d1367f..198d26b 100644 --- a/mobile/lib/utils/storage/conversations.dart +++ b/mobile/lib/utils/storage/conversations.dart @@ -116,6 +116,8 @@ Future updateConversations() async { var conversationDetailJson = conversationsDetailsJson[i] as Map; var conversation = findConversationByDetailId(conversations, conversationDetailJson['id']); + conversation.messageExpiryDefault = conversationDetailJson['message_expiry']; + conversation.twoUser = AesHelper.aesDecrypt( base64.decode(conversation.symmetricKey), base64.decode(conversationDetailJson['two_user']), @@ -194,7 +196,7 @@ Future uploadConversation(Conversation conversation, BuildContext context) body: jsonEncode(conversationJson), ); - if (resp.statusCode != 200) { + if (resp.statusCode != 204) { showMessage('Failed to create conversation', context); } } diff --git a/mobile/lib/utils/storage/database.dart b/mobile/lib/utils/storage/database.dart index 0d85531..7670074 100644 --- a/mobile/lib/utils/storage/database.dart +++ b/mobile/lib/utils/storage/database.dart @@ -40,7 +40,8 @@ Future getDatabaseConnection() async { two_user INTEGER, status INTEGER, is_read INTEGER, - file TEXT + file TEXT, + message_expiry TEXT ); '''); diff --git a/mobile/lib/utils/storage/messages.dart b/mobile/lib/utils/storage/messages.dart index 8c07669..d2f8486 100644 --- a/mobile/lib/utils/storage/messages.dart +++ b/mobile/lib/utils/storage/messages.dart @@ -100,7 +100,7 @@ Future sendMessage(Conversation conversation, { body: jsonEncode(messagesToSend), ) .then((resp) { - if (resp.statusCode != 200) { + if (resp.statusCode != 204) { throw Exception('Unable to send message'); } }) diff --git a/mobile/lib/views/authentication/login.dart b/mobile/lib/views/authentication/login.dart index d4c56a0..44788b3 100644 --- a/mobile/lib/views/authentication/login.dart +++ b/mobile/lib/views/authentication/login.dart @@ -87,8 +87,8 @@ class _LoginWidgetState extends State { ); final ButtonStyle buttonStyle = ElevatedButton.styleFrom( - primary: Theme.of(context).colorScheme.surface, - onPrimary: Theme.of(context).colorScheme.onSurface, + backgroundColor: Theme.of(context).colorScheme.surface, + foregroundColor: Theme.of(context).colorScheme.onSurface, minimumSize: const Size.fromHeight(50), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), textStyle: TextStyle( diff --git a/mobile/lib/views/main/conversation/detail.dart b/mobile/lib/views/main/conversation/detail.dart index ddc3e58..61658ff 100644 --- a/mobile/lib/views/main/conversation/detail.dart +++ b/mobile/lib/views/main/conversation/detail.dart @@ -271,8 +271,14 @@ class _ConversationDetailState extends State { ], ), - showFilePicker ? + AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + transitionBuilder: (Widget child, Animation animation) { + return SizeTransition(sizeFactor: animation, child: child); + }, + child: showFilePicker ? FilePicker( + key: const Key('filePicker'), cameraHandle: (XFile image) {}, galleryHandleMultiple: (List images) async { for (var img in images) { @@ -284,7 +290,8 @@ class _ConversationDetailState extends State { }, fileHandle: () {}, ) : - const SizedBox.shrink(), + const SizedBox(height: 15), + ), ], ), ), diff --git a/mobile/lib/views/main/conversation/settings.dart b/mobile/lib/views/main/conversation/settings.dart index e68d88c..9aacc23 100644 --- a/mobile/lib/views/main/conversation/settings.dart +++ b/mobile/lib/views/main/conversation/settings.dart @@ -1,14 +1,18 @@ +import 'dart:convert'; import 'dart:io'; -import 'package:Capsule/components/custom_title_bar.dart'; -import 'package:Capsule/components/flash_message.dart'; -import 'package:Capsule/exceptions/update_data_exception.dart'; -import 'package:Capsule/models/friends.dart'; -import 'package:Capsule/utils/encryption/crypto_utils.dart'; -import 'package:Capsule/utils/storage/write_file.dart'; -import 'package:Capsule/views/main/conversation/create_add_users.dart'; +import 'package:Capsule/utils/storage/session_cookie.dart'; import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import '/components/custom_title_bar.dart'; +import '/components/flash_message.dart'; +import '/components/select_message_ttl.dart'; +import '/exceptions/update_data_exception.dart'; +import '/models/friends.dart'; +import '/utils/encryption/crypto_utils.dart'; +import '/utils/storage/write_file.dart'; +import '/views/main/conversation/create_add_users.dart'; import '/models/conversation_users.dart'; import '/models/conversations.dart'; import '/models/my_profile.dart'; @@ -122,13 +126,7 @@ class _ConversationSettingsState extends State { widget.conversation.name = conversationName; widget.conversation.icon = writtenFile; - final db = await getDatabaseConnection(); - db.update( - 'conversations', - widget.conversation.toMap(), - where: 'id = ?', - whereArgs: [widget.conversation.id], - ); + await saveConversation(); await updateConversation(widget.conversation, updatedImage: updatedImage) .catchError((error) { @@ -256,7 +254,38 @@ class _ConversationSettingsState extends State { ) ), onPressed: () { - print('Disappearing Messages'); + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => SelectMessageTTL( + widgetTitle: 'Message Expiry', + currentSelected: widget.conversation.messageExpiryDefault, + backCallback: (String messageExpiry) async { + widget.conversation.messageExpiryDefault = messageExpiry; + + http.post( + await MyProfile.getServerUrl( + 'api/v1/auth/conversations/${widget.conversation.id}/message_expiry' + ), + headers: { + 'cookie': await getSessionCookie(), + }, + body: jsonEncode({ + 'message_expiry': messageExpiry, + }), + ).then((http.Response response) { + if (response.statusCode == 204) { + return; + } + + showMessage( + 'Could not change the default message expiry, please try again later.', + context, + ); + }); + + saveConversation(); + } + )) + ); } ), TextButton.icon( @@ -331,5 +360,15 @@ return Theme.of(context).colorScheme.onBackground; getUsers(); setState(() {}); } + + saveConversation() async { + final db = await getDatabaseConnection(); + db.update( + 'conversations', + widget.conversation.toMap(), + where: 'id = ?', + whereArgs: [widget.conversation.id], + ); + } } diff --git a/mobile/lib/views/main/profile/change_password.dart b/mobile/lib/views/main/profile/change_password.dart index 2f77448..358fc88 100644 --- a/mobile/lib/views/main/profile/change_password.dart +++ b/mobile/lib/views/main/profile/change_password.dart @@ -169,7 +169,7 @@ class ChangePassword extends StatelessWidget { return; } - if (resp.statusCode != 200) { + if (resp.statusCode != 204) { showMessage( 'An unexpected error occured, please try again later.', context, diff --git a/mobile/lib/views/main/profile/profile.dart b/mobile/lib/views/main/profile/profile.dart index feefc91..63bd867 100644 --- a/mobile/lib/views/main/profile/profile.dart +++ b/mobile/lib/views/main/profile/profile.dart @@ -64,7 +64,6 @@ class _ProfileState extends State { backdropEnabled: true, backdropOpacity: 0.2, minHeight: 0, - maxHeight: 450, panel: Center( child: _profileQrCode(), ), @@ -73,7 +72,13 @@ class _ProfileState extends State { child: Column( children: [ usernameHeading(), - fileSelector(), + AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + transitionBuilder: (Widget child, Animation animation) { + return SizeTransition(sizeFactor: animation, child: child); + }, + child: fileSelector(), + ), SizedBox(height: showFileSelector ? 10 : 30), settings(), const SizedBox(height: 30), @@ -126,7 +131,8 @@ class _ProfileState extends State { return const SizedBox.shrink(); } - return Padding( + return Padding( + key: const Key('fileSelector'), padding: const EdgeInsets.only(top: 10), child: FilePicker( cameraHandle: _setProfileImage, @@ -168,7 +174,7 @@ class _ProfileState extends State { } showMessage( - 'Could not change your default message expiry, please try again later.', + 'Could not add profile picture, please try again later.', context, ); }); @@ -255,7 +261,7 @@ class _ProfileState extends State { 'message_expiry': messageExpiry, }), ).then((http.Response response) { - if (response.statusCode == 200) { + if (response.statusCode == 204) { return; } diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index d22d858..93f2c89 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -14,7 +14,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.9.0" boolean_selector: dependency: transitive description: @@ -28,21 +28,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" + version: "1.2.1" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" collection: dependency: transitive description: @@ -84,7 +77,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: transitive description: @@ -218,21 +211,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: "direct main" description: @@ -246,7 +239,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" path_provider: dependency: "direct main" description: @@ -419,7 +412,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.9.0" sqflite: dependency: "direct main" description: @@ -454,7 +447,7 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" synchronized: dependency: transitive description: @@ -468,14 +461,14 @@ packages: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.12" typed_data: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 93dd5bb..7874976 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: http: ^0.13.4 shared_preferences: ^2.0.15 sqflite: ^2.0.2 - path: 1.8.1 + path: any flutter_dotenv: ^5.0.2 intl: ^0.17.0 uuid: ^3.0.6