Compare commits

...

Author SHA1 Message Date
  Tovi Jaeschke-Rogers 727928ba2f Add notifications when message is sent 2 years ago
  Tovi Jaeschke-Rogers 3bfdca0f50 WIP - Adding push notifications 2 years ago
  Tovi Jaeschke-Rogers 8b0ab86c83 Add repository classes to interact with DB 2 years ago
  Tovi Jaeschke-Rogers 78d51f70b8 Refactor to use model based create / update / delete 2 years ago
  Tovi Jaeschke-Rogers e7465238e3 Move message http calls to service class 2 years ago
  Tovi Jaeschke-Rogers 74922b2804 WIP - Start working on service classes and move models dir 2 years ago
  Tovi Jaeschke-Rogers 07c5e4af7a Update color schemes 2 years ago
  Tovi Jaeschke-Rogers b0b82d86e0 Add ability to set server URL on login 2 years ago
  Tovi Jaeschke-Rogers ce2d716b7b Add assets and change app icon 2 years ago
  Tovi Jaeschke-Rogers 6ff7b7d67f Remove redundant print statements 2 years ago
  Tovi Jaeschke-Rogers 64be7031a4 Rebrand back to "Envelope" 2 years ago
  Tovi Jaeschke-Rogers ad18b358ce Add permissions view 2 years ago
  Tovi Jaeschke-Rogers aac000cc9a Add .env.example 2 years ago
  Tovi Jaeschke-Rogers 911c9c4cce Merge pull request 'feature/dockerize-server' (#5) from feature/dockerize-server into develop 2 years ago
  Tovi Jaeschke-Rogers 71d6bf67f7 Add message_expiry to conversation settings 2 years ago
  Tovi Jaeschke-Rogers be084425bb Reload golang server on file change 2 years ago
  Tovi Jaeschke-Rogers 9a29a9d82c Add new tests 2 years ago
  Tovi Jaeschke-Rogers cc7caae931 Merge pull request 'feature/dockerize-server' (#4) from feature/dockerize-server into develop 2 years ago
  Tovi Jaeschke-Rogers d3553d5955 Add tests for friend list 2 years ago
  Tovi Jaeschke-Rogers 43fcd3b9e8 Add tests for CreateConversation and UpdateConversation 2 years ago
  Tovi Jaeschke-Rogers 2a368acc44 Update tests 2 years ago
  Tovi Jaeschke-Rogers 5a70720172 Seed rand 2 years ago
  Tovi Jaeschke-Rogers ee3de6a1a5 Add more tests for Api/Auth routes 2 years ago
  Tovi Jaeschke-Rogers b914a9f75c Move go server into docker container and start adding tests 2 years ago
  Tovi Jaeschke-Rogers b80e6ffa21 Merge pull request 'feature/add-attachment-support' (#3) from feature/add-attachment-support into develop 2 years ago
  Tovi Jaeschke-Rogers eb0bf8a06f Update attachements to send back filename not link 2 years ago
  Tovi Jaeschke-Rogers 19fbb9c25a Add profile images for user profiles 2 years ago
  Tovi Jaeschke-Rogers 70f6d6546f Add conversation image support 2 years ago
  Tovi Jaeschke-Rogers b1ad911e2d WIP - Adding images for conversation & user icons 2 years ago
  Tovi Jaeschke-Rogers f56ccfe942 WIP - Adding image support 2 years ago
  Tovi Jaeschke-Rogers c9581363dc WIP - Working on adding attachments to messages 2 years ago
  Tovi Jaeschke-Rogers 068701ae45 Merge pull request 'feature/profile-page' (#2) from feature/profile-page into develop 2 years ago
  Tovi Jaeschke-Rogers 54068e805d Update the profile page 2 years ago
  Tovi Jaeschke-Rogers e31b048389 Add the ability to change the server URL on signup & profile page 2 years ago
  Tovi Jaeschke-Rogers fce45d4cad Update the profile page to show the QR code on dropdown 2 years ago
  Tovi Jaeschke-Rogers 56c3f13ac2 Merge pull request 'feature/initial-project' (#1) from feature/initial-project into develop 2 years ago
  Tovi Jaeschke-Rogers ecac186187 Remove redunant w.WriteHeader lines after http.Error 2 years ago
  Tovi Jaeschke-Rogers bfa4f6b422 Add friends through scanning qr code 2 years ago
  Tovi Jaeschke-Rogers 2a5f243825 Fix state management on home page, and fix user conversations 2 years ago
  Tovi Jaeschke-Rogers c120552a6a Open conversation from friend list 2 years ago
  Tovi Jaeschke-Rogers b2e97646cd Fix accepting friend requests 2 years ago
  Tovi Jaeschke-Rogers 1f4d26165f Accept and reject friend requests 2 years ago
  Tovi Jaeschke-Rogers c89dcf10ec Fix messages doubling up due to mismatched ids 2 years ago
  Tovi Jaeschke-Rogers d7af0a9ac9 Working conversations and messages 2 years ago
  Tovi Jaeschke-Rogers b409ddaac8 Fix conversation creation & slight polish on converstation pages 2 years ago
  Tovi Jaeschke-Rogers 9502692958 Seperate views into subdirectories 2 years ago
  Tovi Jaeschke-Rogers b05b90e6d2 Remove friends table due to unnecessary added complexity 2 years ago
  Tovi Jaeschke-Rogers 4ca108dafa Make local conversation records 2 years ago
  Tovi Jaeschke-Rogers 283054bfd5 Update the conversation settings and profile page 2 years ago
  Tovi Jaeschke-Rogers 0dac8e86ae Code clean up and organisation of colors and profile storage 2 years ago
  Tovi Jaeschke-Rogers 66810ac55e Fix login check on initial load, add failed to send message 2 years ago
  Tovi Jaeschke-Rogers a6f54d5ef8 Finish sending messages 2 years ago
  Tovi Jaeschke-Rogers d8535fdfc7 Message conversations syncing to device 2 years ago
  Tovi Jaeschke-Rogers b5701cf777 Friends and conversations sync to device 2 years ago
  Tovi Jaeschke-Rogers 5eb1aed5c4 WIP - Working encryption with seeder 2 years ago
  Tovi Jaeschke-Rogers d67e4e89ba Update the message/thread structure, and fix the db seeder 2 years ago
  Tovi Jaeschke-Rogers 1a9f763112 Start adding routes for mobile interaction 2 years ago
  Tovi Jaeschke-Rogers 52576132cc Add initial conversation list/friend list 3 years ago
  Tovi Jaeschke-Rogers 00e3cc3620 Working on initial encryption key generation & authentication 3 years ago
  Tovi Jaeschke-Rogers f197d8eec5 Add initial Backend and flutter mobile app 3 years ago
240 changed files with 19792 additions and 1 deletions
Split View
  1. +5
    -0
      .gitignore
  2. +16
    -0
      Backend/.env.example
  3. +40
    -0
      Backend/Api/Auth/AddDeviceToken.go
  4. +59
    -0
      Backend/Api/Auth/AddProfileImage.go
  5. +78
    -0
      Backend/Api/Auth/AddProfileImage_test.go
  6. +72
    -0
      Backend/Api/Auth/ChangePassword.go
  7. +128
    -0
      Backend/Api/Auth/ChangePassword_test.go
  8. +52
    -0
      Backend/Api/Auth/ChangeUserMessageExpiry.go
  9. +89
    -0
      Backend/Api/Auth/ChangeUserMessageExpiry_test.go
  10. +10
    -0
      Backend/Api/Auth/Check.go
  11. +103
    -0
      Backend/Api/Auth/Login.go
  12. +94
    -0
      Backend/Api/Auth/Login_test.go
  13. +43
    -0
      Backend/Api/Auth/Logout.go
  14. +43
    -0
      Backend/Api/Auth/Logout_test.go
  15. +22
    -0
      Backend/Api/Auth/Passwords.go
  16. +53
    -0
      Backend/Api/Auth/Session.go
  17. +67
    -0
      Backend/Api/Auth/Signup.go
  18. +167
    -0
      Backend/Api/Auth/Signup_test.go
  19. BIN
      Backend/Api/Auth/profile_picture_test.png
  20. +69
    -0
      Backend/Api/Friends/AcceptFriendRequest.go
  21. +136
    -0
      Backend/Api/Friends/AcceptFriendRequest_test.go
  22. +86
    -0
      Backend/Api/Friends/CreateFriendRequest.go
  23. +203
    -0
      Backend/Api/Friends/CreateFriendRequest_test.go
  24. +47
    -0
      Backend/Api/Friends/Friends.go
  25. +123
    -0
      Backend/Api/Friends/Friends_test.go
  26. +41
    -0
      Backend/Api/Friends/RejectFriendRequest.go
  27. +63
    -0
      Backend/Api/Messages/AddConversationImage.go
  28. +70
    -0
      Backend/Api/Messages/ChangeConversationMessageExpiry.go
  29. +104
    -0
      Backend/Api/Messages/Conversations.go
  30. +255
    -0
      Backend/Api/Messages/Conversations_test.go
  31. +63
    -0
      Backend/Api/Messages/CreateConversation.go
  32. +127
    -0
      Backend/Api/Messages/CreateConversation_test.go
  33. +90
    -0
      Backend/Api/Messages/CreateMessage.go
  34. +131
    -0
      Backend/Api/Messages/CreateMessage_test.go
  35. +65
    -0
      Backend/Api/Messages/MessageThread.go
  36. +204
    -0
      Backend/Api/Messages/MessageThread_test.go
  37. +56
    -0
      Backend/Api/Messages/UpdateConversation.go
  38. +182
    -0
      Backend/Api/Messages/UpdateConversation_test.go
  39. +93
    -0
      Backend/Api/Routes.go
  40. +54
    -0
      Backend/Api/Users/SearchUsers.go
  41. +106
    -0
      Backend/Api/Users/SearchUsers_test.go
  42. +50
    -0
      Backend/Database/Attachments.go
  43. +31
    -0
      Backend/Database/Base.go
  44. +52
    -0
      Backend/Database/ConversationDetailUsers.go
  45. +75
    -0
      Backend/Database/ConversationDetails.go
  46. +48
    -0
      Backend/Database/DeviceTokens.go
  47. +110
    -0
      Backend/Database/FriendRequests.go
  48. +98
    -0
      Backend/Database/Init.go
  49. +49
    -0
      Backend/Database/MessageData.go
  50. +70
    -0
      Backend/Database/MessageExpiry.go
  51. +89
    -0
      Backend/Database/Messages.go
  52. +141
    -0
      Backend/Database/Seeder/FriendSeeder.go
  53. +321
    -0
      Backend/Database/Seeder/MessageSeeder.go
  54. +121
    -0
      Backend/Database/Seeder/Seed.go
  55. +79
    -0
      Backend/Database/Seeder/UserSeeder.go
  56. +193
    -0
      Backend/Database/Seeder/encryption.go
  57. BIN
      Backend/Database/Seeder/profile_image_enc.dat
  58. +59
    -0
      Backend/Database/Sessions.go
  59. +112
    -0
      Backend/Database/UserConversations.go
  60. +123
    -0
      Backend/Database/Users.go
  61. +14
    -0
      Backend/Dockerfile
  62. +16
    -0
      Backend/Dockerfile.prod
  63. +68
    -0
      Backend/Service/SendNotification.go
  64. +91
    -0
      Backend/Tests/Init.go
  65. +21
    -0
      Backend/Util/Bytes.go
  66. +39
    -0
      Backend/Util/Files.go
  67. +27
    -0
      Backend/Util/Strings.go
  68. +50
    -0
      Backend/Util/UserHelper.go
  69. +0
    -0
      Backend/assets/.gitkeep
  70. +0
    -0
      Backend/attachments/.gitkeep
  71. +8
    -0
      Backend/dev.sh
  72. +51
    -0
      Backend/go.mod
  73. +763
    -0
      Backend/go.sum
  74. +68
    -0
      Backend/main.go
  75. +13
    -1
      README.md
  76. +49
    -0
      docker-compose.yml
  77. +2
    -0
      mobile/.env.example
  78. +46
    -0
      mobile/.gitignore
  79. +10
    -0
      mobile/.metadata
  80. +16
    -0
      mobile/README.md
  81. +30
    -0
      mobile/analysis_options.yaml
  82. +13
    -0
      mobile/android/.gitignore
  83. +70
    -0
      mobile/android/app/build.gradle
  84. +46
    -0
      mobile/android/app/google-services.json
  85. +7
    -0
      mobile/android/app/src/debug/AndroidManifest.xml
  86. +35
    -0
      mobile/android/app/src/main/AndroidManifest.xml
  87. +25
    -0
      mobile/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java
  88. +6
    -0
      mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt
  89. +12
    -0
      mobile/android/app/src/main/res/drawable-v21/launch_background.xml
  90. +12
    -0
      mobile/android/app/src/main/res/drawable/launch_background.xml
  91. BIN
      mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  92. BIN
      mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  93. BIN
      mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  94. BIN
      mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  95. BIN
      mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  96. +18
    -0
      mobile/android/app/src/main/res/values-night/styles.xml
  97. +18
    -0
      mobile/android/app/src/main/res/values/styles.xml
  98. +7
    -0
      mobile/android/app/src/profile/AndroidManifest.xml
  99. +32
    -0
      mobile/android/build.gradle
  100. +3
    -0
      mobile/android/gradle.properties

+ 5
- 0
.gitignore View File

@ -0,0 +1,5 @@
/mobile/.env
/Backend/.env
/Backend/main
/Backend/attachments/*
/Backend/assets/*

+ 16
- 0
Backend/.env.example View File

@ -0,0 +1,16 @@
GO_ENV=local
DB_DATABASE=capsule
DB_HOST=postgres
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=password
DB_TESTING_DATABASE=capsule-testing
DB_TESTING_HOST=postgres-user
DB_TESTING_PORT=5432
DB_TESTING_USER=postgres
DB_TESTING_PASSWORD=password
FIREBASE_AUTH_KEY=

+ 40
- 0
Backend/Api/Auth/AddDeviceToken.go View File

@ -0,0 +1,40 @@
package Auth
import (
"encoding/json"
"net/http"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
)
type deviceToken struct {
Token string `json:"token"`
Type string `json:"type"`
}
func AddDeviceToken(w http.ResponseWriter, r *http.Request) {
var (
token deviceToken
userToken Database.DeviceToken
err error
)
err = json.NewDecoder(r.Body).Decode(&token)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
userToken = Database.DeviceToken{
Token: token.Token,
DeviceType: token.Type,
}
err = (&userToken).CreateUserDeviceToken()
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}

+ 59
- 0
Backend/Api/Auth/AddProfileImage.go View File

@ -0,0 +1,59 @@
package Auth
import (
"encoding/base64"
"encoding/json"
"net/http"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Util"
)
// AddProfileImage adds a profile image
func AddProfileImage(w http.ResponseWriter, r *http.Request) {
var (
user Database.User
attachment Database.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)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
fileName, err = Util.WriteFile(decodedFile)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
attachment.FilePath = fileName
user.Attachment = attachment
err = (&user).UpdateUser()
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}

+ 78
- 0
Backend/Api/Auth/AddProfileImage_test.go View File

@ -0,0 +1,78 @@
package Auth_test
import (
"bytes"
"encoding/base64"
"encoding/json"
"net/http"
"os"
"testing"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database/Seeder"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Tests"
)
func Test_AddProfileImage(t *testing.T) {
client, ts, err := Tests.InitTestEnv()
defer ts.Close()
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
}
dat, err := os.ReadFile("./profile_picture_test.png")
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
encDat, err := key.AesEncrypt(dat)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
a := Database.Attachment{
Mimetype: "image/png",
Extension: "png",
Data: base64.StdEncoding.EncodeToString(encDat),
}
jsonStr, _ := json.Marshal(a)
req, _ := http.NewRequest("POST", ts.URL+"/api/v1/auth/image", 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
}
u, err := Database.GetUserByUsername("test")
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
if u.AttachmentID.IsNil() {
t.Errorf("Attachment not assigned to user")
}
err = os.Remove("/app/attachments/" + u.Attachment.FilePath)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
}

+ 72
- 0
Backend/Api/Auth/ChangePassword.go View File

@ -0,0 +1,72 @@
package Auth
import (
"encoding/json"
"io/ioutil"
"net/http"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
)
type rawChangePassword struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
NewPasswordConfirm string `json:"new_password_confirm"`
PrivateKey string `json:"private_key"`
}
// ChangePassword handle change password action
func ChangePassword(w http.ResponseWriter, r *http.Request) {
var (
user Database.User
changePassword rawChangePassword
requestBody []byte
err error
)
user, err = CheckCookieCurrentUser(w, r)
if err != nil {
// Don't bother showing an error here, as the middleware handles auth
return
}
requestBody, err = ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
err = json.Unmarshal(requestBody, &changePassword)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
if !CheckPasswordHash(changePassword.OldPassword, user.Password) {
http.Error(w, "Invalid Current Password", http.StatusForbidden)
return
}
// This should never occur, due to frontend validation
if changePassword.NewPassword != changePassword.NewPasswordConfirm {
http.Error(w, "Invalid New Password", http.StatusUnprocessableEntity)
return
}
user.Password, err = HashPassword(changePassword.NewPassword)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
// Private key doesn't change at this point, is just re-encrypted with the new password
user.AsymmetricPrivateKey = changePassword.PrivateKey
err = (&user).UpdateUser()
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}

+ 128
- 0
Backend/Api/Auth/ChangePassword_test.go View File

@ -0,0 +1,128 @@
package Auth_test
import (
"bytes"
"encoding/json"
"net/http"
"testing"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Tests"
)
func Test_ChangePassword(t *testing.T) {
client, ts, err := Tests.InitTestEnv()
defer ts.Close()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
d := struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
NewPasswordConfirm string `json:"new_password_confirm"`
PrivateKey string `json:"private_key"`
}{
OldPassword: "password",
NewPassword: "password1",
NewPasswordConfirm: "password1",
PrivateKey: "",
}
jsonStr, _ := json.Marshal(d)
req, _ := http.NewRequest("POST", ts.URL+"/api/v1/auth/change_password", 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
}
u, err := Database.GetUserByUsername("test")
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
if !Auth.CheckPasswordHash("password1", u.Password) {
t.Errorf("Failed to verify the password has been changed")
}
}
func Test_ChangePasswordMismatchConfirmFails(t *testing.T) {
client, ts, err := Tests.InitTestEnv()
defer ts.Close()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
d := struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
NewPasswordConfirm string `json:"new_password_confirm"`
PrivateKey string `json:"private_key"`
}{
OldPassword: "password",
NewPassword: "password1",
NewPasswordConfirm: "password2",
PrivateKey: "",
}
jsonStr, _ := json.Marshal(d)
req, _ := http.NewRequest("POST", ts.URL+"/api/v1/auth/change_password", 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.StatusUnprocessableEntity {
t.Errorf("Expected %d, recieved %d", http.StatusUnprocessableEntity, resp.StatusCode)
}
}
func Test_ChangePasswordInvalidCurrentPasswordFails(t *testing.T) {
client, ts, err := Tests.InitTestEnv()
defer ts.Close()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
d := struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
NewPasswordConfirm string `json:"new_password_confirm"`
PrivateKey string `json:"private_key"`
}{
OldPassword: "password2",
NewPassword: "password1",
NewPasswordConfirm: "password1",
PrivateKey: "",
}
jsonStr, _ := json.Marshal(d)
req, _ := http.NewRequest("POST", ts.URL+"/api/v1/auth/change_password", 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.StatusForbidden {
t.Errorf("Expected %d, recieved %d", http.StatusForbidden, resp.StatusCode)
}
}

+ 52
- 0
Backend/Api/Auth/ChangeUserMessageExpiry.go View File

@ -0,0 +1,52 @@
package Auth
import (
"encoding/json"
"io/ioutil"
"net/http"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
)
type rawChangeMessageExpiry struct {
MessageExpiry string `json:"message_expiry"`
}
// ChangeUserMessageExpiry handles changing default message expiry for user
func ChangeUserMessageExpiry(w http.ResponseWriter, r *http.Request) {
var (
user Database.User
changeMessageExpiry rawChangeMessageExpiry
requestBody []byte
err error
)
// Ignore error here, as middleware should handle auth
user, _ = 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 = user.MessageExpiryDefault.Scan(changeMessageExpiry.MessageExpiry)
if err != nil {
http.Error(w, "Error", http.StatusUnprocessableEntity)
return
}
err = (&user).UpdateUser()
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}

+ 89
- 0
Backend/Api/Auth/ChangeUserMessageExpiry_test.go View File

@ -0,0 +1,89 @@
package Auth_test
import (
"bytes"
"encoding/json"
"net/http"
"testing"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Tests"
)
func Test_ChangeUserMessageExpiry(t *testing.T) {
client, ts, err := Tests.InitTestEnv()
defer ts.Close()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
d := struct {
MessageExpiry string `json:"message_expiry"`
}{
MessageExpiry: "fifteen_min",
}
jsonStr, _ := json.Marshal(d)
req, _ := http.NewRequest("POST", ts.URL+"/api/v1/auth/message_expiry", 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)
}
u, err := Database.GetUserByUsername("test")
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
if u.MessageExpiryDefault.String() != "fifteen_min" {
t.Errorf("Failed to verify the MessageExpiryDefault has been changed")
}
}
func Test_ChangeMessageExpiryInvalidData(t *testing.T) {
client, ts, err := Tests.InitTestEnv()
defer ts.Close()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
d := struct {
MessageExpiry string `json:"message_expiry"`
}{
MessageExpiry: "invalid_message_expiry",
}
jsonStr, _ := json.Marshal(d)
req, _ := http.NewRequest("POST", ts.URL+"/api/v1/auth/message_expiry", 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.StatusUnprocessableEntity {
t.Errorf("Expected %d, recieved %d", http.StatusUnprocessableEntity, resp.StatusCode)
}
u, err := Database.GetUserByUsername("test")
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
if u.MessageExpiryDefault.String() != "no_expiry" {
t.Errorf("Failed to verify the MessageExpiryDefault has not been changed")
}
}

+ 10
- 0
Backend/Api/Auth/Check.go View File

@ -0,0 +1,10 @@
package Auth
import (
"net/http"
)
// Check is used to check session viability
func Check(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}

+ 103
- 0
Backend/Api/Auth/Login.go View File

@ -0,0 +1,103 @@
package Auth
import (
"database/sql/driver"
"encoding/json"
"net/http"
"time"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
)
type credentials struct {
Username string `json:"username"`
Password string `json:"password"`
}
type loginResponse struct {
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"`
}
// Login logs the user into the system
func Login(w http.ResponseWriter, r *http.Request) {
var (
creds credentials
user Database.User
session Database.Session
expiresAt time.Time
messageExpiryRaw driver.Value
messageExpiry string
imageLink string
returnJSON []byte
err error
)
err = json.NewDecoder(r.Body).Decode(&creds)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
user, err = Database.GetUserByUsername(creds.Username)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if !CheckPasswordHash(creds.Password, user.Password) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// TODO: Revisit before production
expiresAt = time.Now().Add(12 * time.Hour)
session = Database.Session{
UserID: user.ID,
Expiry: expiresAt,
}
err = (&session).CreateSession()
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
http.SetCookie(w, &http.Cookie{
Name: "session_token",
Value: session.ID.String(),
Expires: expiresAt,
})
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)
}

+ 94
- 0
Backend/Api/Auth/Login_test.go View File

@ -0,0 +1,94 @@
package Auth_test
import (
"bytes"
"encoding/json"
"net/http"
"testing"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Tests"
)
func Test_Login(t *testing.T) {
_, ts, err := Tests.InitTestEnv()
defer ts.Close()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
d := struct {
Username string `json:"username"`
Password string `json:"password"`
}{
Username: "test",
Password: "password",
}
jsonStr, _ := json.Marshal(d)
req, _ := http.NewRequest("POST", ts.URL+"/api/v1/login", bytes.NewBuffer(jsonStr))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
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
}
u, err := Database.GetUserByUsername("test")
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
var session Database.Session
err = Database.DB.First(&session, "user_id = ?", u.ID.String()).Error
if err != nil {
t.Errorf("Expected user record, recieved %s", err.Error())
return
}
}
func Test_Login_PasswordFails(t *testing.T) {
_, ts, err := Tests.InitTestEnv()
defer ts.Close()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
d := struct {
Username string `json:"username"`
Password string `json:"password"`
}{
Username: "test",
Password: "password1",
}
jsonStr, _ := json.Marshal(d)
req, _ := http.NewRequest("POST", ts.URL+"/api/v1/login", bytes.NewBuffer(jsonStr))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
if resp.StatusCode != http.StatusUnauthorized {
t.Errorf("Expected %d, recieved %d", http.StatusUnauthorized, resp.StatusCode)
return
}
}

+ 43
- 0
Backend/Api/Auth/Logout.go View File

@ -0,0 +1,43 @@
package Auth
import (
"log"
"net/http"
"time"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
)
// Logout logs out from system
func Logout(w http.ResponseWriter, r *http.Request) {
var (
c *http.Cookie
sessionToken string
err error
)
c, err = r.Cookie("session_token")
if err != nil {
if err == http.ErrNoCookie {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusBadRequest)
return
}
sessionToken = c.Value
err = Database.DeleteSessionByID(sessionToken)
if err != nil {
log.Println("Could not delete session cookie")
}
http.SetCookie(w, &http.Cookie{
Name: "session_token",
Value: "",
Expires: time.Now(),
})
w.WriteHeader(http.StatusOK)
}

+ 43
- 0
Backend/Api/Auth/Logout_test.go View File

@ -0,0 +1,43 @@
package Auth_test
import (
"net/http"
"testing"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Tests"
)
func Test_Logout(t *testing.T) {
client, ts, err := Tests.InitTestEnv()
defer ts.Close()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
resp, err := client.Get(ts.URL + "/api/v1/logout")
if err != nil {
t.Errorf("Expected user record, recieved %s", err.Error())
return
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected %d, recieved %d", http.StatusOK, resp.StatusCode)
return
}
var session Database.Session
u, err := Database.GetUserByUsername("test")
if err != nil {
t.Errorf("Expected user record, recieved %s", err.Error())
return
}
err = Database.DB.First(&session, "user_id = ?", u.ID.String()).Error
if err == nil {
t.Errorf("Expected no session record, recieved %s", session.UserID)
return
}
}

+ 22
- 0
Backend/Api/Auth/Passwords.go View File

@ -0,0 +1,22 @@
package Auth
import (
"golang.org/x/crypto/bcrypt"
)
func HashPassword(password string) (string, error) {
var (
bytes []byte
err error
)
bytes, err = bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
func CheckPasswordHash(password, hash string) bool {
var (
err error
)
err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}

+ 53
- 0
Backend/Api/Auth/Session.go View File

@ -0,0 +1,53 @@
package Auth
import (
"errors"
"net/http"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
)
func CheckCookie(r *http.Request) (Database.Session, error) {
var (
c *http.Cookie
sessionToken string
userSession Database.Session
err error
)
c, err = r.Cookie("session_token")
if err != nil {
return userSession, err
}
sessionToken = c.Value
// We then get the session from our session map
userSession, err = Database.GetSessionByID(sessionToken)
if err != nil {
return userSession, errors.New("Cookie not found")
}
// If the session is present, but has expired, we can delete the session, and return
// an unauthorized status
if userSession.IsExpired() {
(&userSession).DeleteSession()
return userSession, errors.New("Cookie expired")
}
return userSession, nil
}
func CheckCookieCurrentUser(w http.ResponseWriter, r *http.Request) (Database.User, error) {
var (
session Database.Session
userData Database.User
err error
)
session, err = CheckCookie(r)
if err != nil {
return userData, err
}
return session.User, nil
}

+ 67
- 0
Backend/Api/Auth/Signup.go View File

@ -0,0 +1,67 @@
package Auth
import (
"encoding/json"
"net/http"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
)
type signup struct {
Username string `json:"username"`
Password string `json:"password"`
ConfirmPassword string `json:"confirm_password"`
PublicKey string `json:"asymmetric_public_key"`
PrivateKey string `json:"asymmetric_private_key"`
SymmetricKey string `json:"symmetric_key"`
}
// Signup to the platform
func Signup(w http.ResponseWriter, r *http.Request) {
var (
user Database.User
err error
)
err = json.NewDecoder(r.Body).Decode(&user)
if err != nil {
http.Error(w, "Invalid Data", http.StatusUnprocessableEntity)
return
}
if user.Username == "" ||
user.Password == "" ||
user.ConfirmPassword == "" ||
len(user.AsymmetricPrivateKey) == 0 ||
len(user.AsymmetricPublicKey) == 0 ||
len(user.SymmetricKey) == 0 {
http.Error(w, "Invalid Data", http.StatusUnprocessableEntity)
return
}
if user.Password != user.ConfirmPassword {
http.Error(w, "Invalid Data", http.StatusUnprocessableEntity)
return
}
err = Database.CheckUniqueUsername(user.Username)
if err != nil {
http.Error(w, "Invalid Data", http.StatusUnprocessableEntity)
return
}
user.Password, err = HashPassword(user.Password)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
err = (&user).CreateUser()
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}

+ 167
- 0
Backend/Api/Auth/Signup_test.go View File

@ -0,0 +1,167 @@
package Auth_test
import (
"bytes"
"encoding/base64"
"encoding/json"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"testing"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Api"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database/Seeder"
"github.com/gorilla/mux"
)
func Test_Signup(t *testing.T) {
log.SetOutput(ioutil.Discard)
Database.InitTest()
r := mux.NewRouter()
Api.InitAPIEndpoints(r)
ts := httptest.NewServer(r)
defer ts.Close()
userKey, _ := Seeder.GenerateAesKey()
pubKey := Seeder.GetPubKey()
d := struct {
Username string `json:"username"`
Password string `json:"password"`
ConfirmPassword string `json:"confirm_password"`
PubKey string `json:"asymmetric_public_key"`
PrivKey string `json:"asymmetric_private_key"`
SymKey string `json:"symmetric_key"`
}{
Username: "test",
Password: "password",
ConfirmPassword: "password",
PubKey: Seeder.PublicKey,
PrivKey: Seeder.EncryptedPrivateKey,
SymKey: base64.StdEncoding.EncodeToString(
Seeder.EncryptWithPublicKey(userKey.Key, pubKey),
),
}
jsonStr, _ := json.Marshal(d)
req, _ := http.NewRequest("POST", ts.URL+"/api/v1/signup", bytes.NewBuffer(jsonStr))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
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 user Database.User
err = Database.DB.First(&user, "username = ?", "test").Error
if err != nil {
t.Errorf("Expected user record, recieved %s", err.Error())
return
}
}
func Test_Signup_PasswordMismatchFails(t *testing.T) {
log.SetOutput(ioutil.Discard)
Database.InitTest()
r := mux.NewRouter()
Api.InitAPIEndpoints(r)
ts := httptest.NewServer(r)
defer ts.Close()
userKey, _ := Seeder.GenerateAesKey()
pubKey := Seeder.GetPubKey()
d := struct {
Username string `json:"username"`
Password string `json:"password"`
ConfirmPassword string `json:"confirm_password"`
PubKey string `json:"asymmetric_public_key"`
PrivKey string `json:"asymmetric_private_key"`
SymKey string `json:"symmetric_key"`
}{
Username: "test",
Password: "password",
ConfirmPassword: "password1",
PubKey: Seeder.PublicKey,
PrivKey: Seeder.EncryptedPrivateKey,
SymKey: base64.StdEncoding.EncodeToString(
Seeder.EncryptWithPublicKey(userKey.Key, pubKey),
),
}
jsonStr, _ := json.Marshal(d)
req, _ := http.NewRequest("POST", ts.URL+"/api/v1/signup", bytes.NewBuffer(jsonStr))
req.Header.Set("X-Custom-Header", "myvalue")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Errorf("Expected %d, recieved %d", http.StatusUnprocessableEntity, resp.StatusCode)
return
}
}
func Test_Signup_MissingDataFails(t *testing.T) {
log.SetOutput(ioutil.Discard)
Database.InitTest()
r := mux.NewRouter()
Api.InitAPIEndpoints(r)
ts := httptest.NewServer(r)
defer ts.Close()
d := struct {
Username string `json:"username"`
Password string `json:"password"`
ConfirmPassword string `json:"confirm_password"`
PubKey string `json:"asymmetric_public_key"`
PrivKey string `json:"asymmetric_private_key"`
SymKey string `json:"symmetric_key"`
}{
Username: "test",
Password: "password",
ConfirmPassword: "password",
PubKey: "",
PrivKey: "",
SymKey: "",
}
jsonStr, _ := json.Marshal(d)
req, _ := http.NewRequest("POST", ts.URL+"/api/v1/signup", bytes.NewBuffer(jsonStr))
req.Header.Set("X-Custom-Header", "myvalue")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
}
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Errorf("Expected %d, recieved %d", http.StatusUnprocessableEntity, resp.StatusCode)
}
}

BIN
Backend/Api/Auth/profile_picture_test.png View File

Before After
Width: 487  |  Height: 530  |  Size: 136 KiB

+ 69
- 0
Backend/Api/Friends/AcceptFriendRequest.go View File

@ -0,0 +1,69 @@
package Friends
import (
"encoding/json"
"io/ioutil"
"net/http"
"time"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"github.com/gorilla/mux"
)
// AcceptFriendRequest accepts friend requests
func AcceptFriendRequest(w http.ResponseWriter, r *http.Request) {
var (
oldFriendRequest Database.FriendRequest
newFriendRequest Database.FriendRequest
urlVars map[string]string
friendRequestID string
requestBody []byte
ok bool
err error
)
urlVars = mux.Vars(r)
friendRequestID, ok = urlVars["requestID"]
if !ok {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
oldFriendRequest, err = Database.GetFriendRequestByID(friendRequestID)
if err != nil {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
oldFriendRequest.AcceptedAt.Time = time.Now()
oldFriendRequest.AcceptedAt.Valid = true
requestBody, err = ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
err = json.Unmarshal(requestBody, &newFriendRequest)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
err = (&oldFriendRequest).UpdateFriendRequest()
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
newFriendRequest.AcceptedAt.Time = time.Now()
newFriendRequest.AcceptedAt.Valid = true
err = (&newFriendRequest).CreateFriendRequest()
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}

+ 136
- 0
Backend/Api/Friends/AcceptFriendRequest_test.go View File

@ -0,0 +1,136 @@
package Friends_test
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"testing"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database/Seeder"
"git.tovijaeschke.xyz/tovi/Envelope/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 := Database.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 = (&friendReq).CreateFriendRequest()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
friendReqResponse := Database.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 []Database.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
}
}
}

+ 86
- 0
Backend/Api/Friends/CreateFriendRequest.go View File

@ -0,0 +1,86 @@
package Friends
import (
"encoding/json"
"io/ioutil"
"net/http"
"time"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
)
// CreateFriendRequest creates a FriendRequest from post data
func CreateFriendRequest(w http.ResponseWriter, r *http.Request) {
var (
friendRequest Database.FriendRequest
requestBody []byte
returnJSON []byte
err error
)
requestBody, err = ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
err = json.Unmarshal(requestBody, &friendRequest)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
friendRequest.AcceptedAt.Scan(nil)
err = (&friendRequest).CreateFriendRequest()
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
returnJSON, err = json.MarshalIndent(friendRequest, "", " ")
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
// Return updated json
w.WriteHeader(http.StatusOK)
w.Write(returnJSON)
}
// CreateFriendRequestQrCode creates a FriendRequest from post data from qr code scan
func CreateFriendRequestQrCode(w http.ResponseWriter, r *http.Request) {
var (
friendRequests Database.FriendRequestList
requestBody []byte
i int
err error
)
requestBody, err = ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
err = json.Unmarshal(requestBody, &friendRequests)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
for i = range friendRequests {
friendRequests[i].AcceptedAt.Time = time.Now()
friendRequests[i].AcceptedAt.Valid = true
}
err = (&friendRequests).CreateFriendRequests()
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
// Return updated json
w.WriteHeader(http.StatusOK)
}

+ 203
- 0
Backend/Api/Friends/CreateFriendRequest_test.go View File

@ -0,0 +1,203 @@
package Friends_test
import (
"bytes"
"encoding/base64"
"encoding/json"
"net/http"
"testing"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database/Seeder"
"git.tovijaeschke.xyz/tovi/Envelope/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 := Database.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 Database.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 := Database.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 := Database.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([]Database.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 Database.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
}
}

+ 47
- 0
Backend/Api/Friends/Friends.go View File

@ -0,0 +1,47 @@
package Friends
import (
"encoding/json"
"net/http"
"net/url"
"strconv"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
)
// FriendRequestList gets friend request list
func FriendRequestList(w http.ResponseWriter, r *http.Request) {
var (
userSession Database.Session
friends Database.FriendRequestList
values url.Values
returnJSON []byte
page int
err error
)
values = r.URL.Query()
page, err = strconv.Atoi(values.Get("page"))
if err != nil {
page = 0
}
userSession, _ = Auth.CheckCookie(r)
friends, err = Database.GetFriendRequestsByUserID(userSession.UserID.String(), page)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
returnJSON, err = json.MarshalIndent(friends, "", " ")
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write(returnJSON)
}

+ 123
- 0
Backend/Api/Friends/Friends_test.go View File

@ -0,0 +1,123 @@
package Friends_test
import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"testing"
"time"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database/Seeder"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Tests"
)
func Test_FriendRequestList(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
}
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()
encPublicKey, err := key.AesEncrypt([]byte(Seeder.PublicKey))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
friendReq := Database.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),
),
}
if i > 20 {
friendReq.AcceptedAt.Time = time.Now()
friendReq.AcceptedAt.Valid = true
}
err = (&friendReq).CreateFriendRequest()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
}
req, _ := http.NewRequest("GET", ts.URL+"/api/v1/auth/friend_requests", nil)
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
}
requestBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("Expected %d, recieved %d", http.StatusOK, resp.StatusCode)
return
}
var users Database.FriendRequestList
json.Unmarshal(requestBody, &users)
if len(users) != 20 {
t.Errorf("Expected %d, recieved %d", 1, len(users))
return
}
for i := 0; i < 20; i++ {
eq := true
if i > 8 {
eq = false
}
if users[i].AcceptedAt.Valid != eq {
t.Errorf(
"Expected %v, recieved %v, on user %d",
eq, users[i].AcceptedAt.Valid,
i,
)
return
}
}
}

+ 41
- 0
Backend/Api/Friends/RejectFriendRequest.go View File

@ -0,0 +1,41 @@
package Friends
import (
"net/http"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"github.com/gorilla/mux"
)
// RejectFriendRequest rejects friend requests
func RejectFriendRequest(w http.ResponseWriter, r *http.Request) {
var (
friendRequest Database.FriendRequest
urlVars map[string]string
friendRequestID string
ok bool
err error
)
urlVars = mux.Vars(r)
friendRequestID, ok = urlVars["requestID"]
if !ok {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
friendRequest, err = Database.GetFriendRequestByID(friendRequestID)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
err = (&friendRequest).DeleteFriendRequest()
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}

+ 63
- 0
Backend/Api/Messages/AddConversationImage.go View File

@ -0,0 +1,63 @@
package Messages
import (
"encoding/base64"
"encoding/json"
"net/http"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"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 Database.Attachment
conversationDetail Database.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 = (&conversationDetail).UpdateConversationDetail()
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}

+ 70
- 0
Backend/Api/Messages/ChangeConversationMessageExpiry.go View File

@ -0,0 +1,70 @@
package Messages
import (
"encoding/json"
"io/ioutil"
"net/http"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"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 (
changeMessageExpiry rawChangeMessageExpiry
conversationDetail Database.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 = (&conversationDetail).UpdateConversationDetail()
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}

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

@ -0,0 +1,104 @@
package Messages
import (
"database/sql/driver"
"encoding/json"
"net/http"
"net/url"
"strconv"
"strings"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
)
// ConversationList returns an encrypted list of all Conversations
func ConversationList(w http.ResponseWriter, r *http.Request) {
var (
conversationDetails []Database.UserConversation
userSession Database.Session
returnJSON []byte
values url.Values
page int
err error
)
values = r.URL.Query()
page, err = strconv.Atoi(values.Get("page"))
if err != nil {
page = 0
}
userSession, _ = Auth.CheckCookie(r)
conversationDetails, err = Database.GetUserConversationsByUserId(
userSession.UserID.String(),
page,
)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
returnJSON, err = json.MarshalIndent(conversationDetails, "", " ")
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write(returnJSON)
}
// ConversationDetailsList returns an encrypted list of all ConversationDetails
func ConversationDetailsList(w http.ResponseWriter, r *http.Request) {
var (
conversationDetails []Database.ConversationDetail
detail Database.ConversationDetail
query url.Values
conversationIds []string
messageExpiryRaw driver.Value
returnJSON []byte
i int
ok bool
err error
)
query = r.URL.Query()
conversationIds, ok = query["conversation_detail_ids"]
if !ok {
http.Error(w, "Invalid Data", http.StatusBadGateway)
return
}
conversationIds = strings.Split(conversationIds[0], ",")
conversationDetails, err = Database.GetConversationDetailsByIds(
conversationIds,
)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
for i, detail = range conversationDetails {
messageExpiryRaw, _ = detail.MessageExpiryDefault.Value()
conversationDetails[i].MessageExpiry, _ = messageExpiryRaw.(string)
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
}
w.WriteHeader(http.StatusOK)
w.Write(returnJSON)
}

+ 255
- 0
Backend/Api/Messages/Conversations_test.go View File

@ -0,0 +1,255 @@
package Messages_test
import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"testing"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database/Seeder"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Tests"
)
func Test_ConversationsList(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")
key, err := Seeder.GenerateAesKey()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
nameCiphertext, err := key.AesEncrypt([]byte("Test conversation"))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
twoUserCiphertext, err := key.AesEncrypt([]byte("false"))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
messageThread := Database.ConversationDetail{
Name: base64.StdEncoding.EncodeToString(nameCiphertext),
TwoUser: base64.StdEncoding.EncodeToString(twoUserCiphertext),
}
err = (&messageThread).CreateConversationDetail()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
conversationDetailIDCiphertext, err := key.AesEncrypt([]byte(messageThread.ID.String()))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
adminCiphertext, err := key.AesEncrypt([]byte("true"))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
pubKey := Seeder.GetPubKey()
messageThreadUser := Database.UserConversation{
UserID: u.ID,
ConversationDetailID: base64.StdEncoding.EncodeToString(conversationDetailIDCiphertext),
Admin: base64.StdEncoding.EncodeToString(adminCiphertext),
SymmetricKey: base64.StdEncoding.EncodeToString(
Seeder.EncryptWithPublicKey(key.Key, pubKey),
),
}
err = (&messageThreadUser).CreateUserConversation()
req, _ := http.NewRequest("GET", ts.URL+"/api/v1/auth/conversations", nil)
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
}
requestBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("Expected %d, recieved %d", http.StatusOK, resp.StatusCode)
return
}
var conversations Database.UserConversationList
json.Unmarshal(requestBody, &conversations)
if len(conversations) != 1 {
t.Errorf("Expected %d, recieved %d", 1, len(conversations))
return
}
conv := conversations[0]
decodedId, err := base64.StdEncoding.DecodeString(conv.ConversationDetailID)
if err != nil {
t.Errorf("Expected %d, recieved %d", http.StatusOK, resp.StatusCode)
return
}
decrypedId, err := key.AesDecrypt(decodedId)
if err != nil {
t.Errorf("Expected %d, recieved %d", http.StatusOK, resp.StatusCode)
return
}
req, _ = http.NewRequest(
"GET",
ts.URL+"/api/v1/auth/conversation_details?conversation_detail_ids="+string(decrypedId),
nil,
)
resp, err = client.Do(req)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
var conversationDetails []Database.ConversationDetail
requestBody, err = ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("Expected %d, recieved %d", http.StatusOK, resp.StatusCode)
return
}
json.Unmarshal(requestBody, &conversationDetails)
if len(conversationDetails) != 1 {
t.Errorf("Expected %d, recieved %d", 1, len(conversations))
}
decodedName, err := base64.StdEncoding.DecodeString(conversationDetails[0].Name)
if err != nil {
t.Errorf("Expected %d, recieved %d", http.StatusOK, resp.StatusCode)
return
}
decrypedName, err := key.AesDecrypt(decodedName)
if err != nil {
t.Errorf("Expected %d, recieved %d", http.StatusOK, resp.StatusCode)
return
}
if string(decrypedName) != "Test conversation" {
t.Errorf("Expected %s, recieved %s", "Test converation", string(decrypedName))
}
}
func Test_ConversationsListPagination(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")
key, err := Seeder.GenerateAesKey()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
for i := 0; i < 40; i++ {
nameCiphertext, err := key.AesEncrypt([]byte(
fmt.Sprintf("Test conversation %d", i),
))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
twoUserCiphertext, err := key.AesEncrypt([]byte("false"))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
messageThread := Database.ConversationDetail{
Name: base64.StdEncoding.EncodeToString(nameCiphertext),
TwoUser: base64.StdEncoding.EncodeToString(twoUserCiphertext),
}
err = (&messageThread).CreateConversationDetail()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
conversationDetailIDCiphertext, err := key.AesEncrypt([]byte(messageThread.ID.String()))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
adminCiphertext, err := key.AesEncrypt([]byte("true"))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
pubKey := Seeder.GetPubKey()
messageThreadUser := Database.UserConversation{
UserID: u.ID,
ConversationDetailID: base64.StdEncoding.EncodeToString(conversationDetailIDCiphertext),
Admin: base64.StdEncoding.EncodeToString(adminCiphertext),
SymmetricKey: base64.StdEncoding.EncodeToString(
Seeder.EncryptWithPublicKey(key.Key, pubKey),
),
}
err = (&messageThreadUser).CreateUserConversation()
}
req, _ := http.NewRequest("GET", ts.URL+"/api/v1/auth/conversations?page=0", nil)
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
}
requestBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("Expected %d, recieved %d", http.StatusOK, resp.StatusCode)
return
}
var conversations []Database.UserConversation
json.Unmarshal(requestBody, &conversations)
if len(conversations) != 20 {
t.Errorf("Expected %d, recieved %d", 1, len(conversations))
}
}

+ 63
- 0
Backend/Api/Messages/CreateConversation.go View File

@ -0,0 +1,63 @@
package Messages
import (
"encoding/json"
"net/http"
"github.com/gofrs/uuid"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
)
// RawCreateConversationData for holding POST payload
type RawCreateConversationData struct {
ID string `json:"id"`
Name string `json:"name"`
TwoUser string `json:"two_user"`
AdminAddMembers string `json:"admin_add_members"`
AdminEditInfo string `json:"admin_edit_info"`
AdminSendMessages string `json:"admin_send_messages"`
Users []Database.ConversationDetailUser `json:"users"`
UserConversations Database.UserConversationList `json:"user_conversations"`
}
// CreateConversation creates ConversationDetail, ConversationDetailUsers and UserConversations
func CreateConversation(w http.ResponseWriter, r *http.Request) {
var (
rawConversationData RawCreateConversationData
messageThread Database.ConversationDetail
err error
)
err = json.NewDecoder(r.Body).Decode(&rawConversationData)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
messageThread = Database.ConversationDetail{
Base: Database.Base{
ID: uuid.FromStringOrNil(rawConversationData.ID),
},
Name: rawConversationData.Name,
TwoUser: rawConversationData.TwoUser,
AdminAddMembers: rawConversationData.AdminAddMembers,
AdminEditInfo: rawConversationData.AdminEditInfo,
AdminSendMessages: rawConversationData.AdminSendMessages,
Users: rawConversationData.Users,
}
err = (&messageThread).CreateConversationDetail()
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
err = (&rawConversationData.UserConversations).CreateUserConversations()
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}

+ 127
- 0
Backend/Api/Messages/CreateConversation_test.go View File

@ -0,0 +1,127 @@
package Messages_test
import (
"bytes"
"encoding/base64"
"encoding/json"
"net/http"
"testing"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database/Seeder"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Tests"
"github.com/gofrs/uuid"
)
func Test_CreateConversation(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")
key, err := Seeder.GenerateAesKey()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
nameCiphertext, err := key.AesEncrypt([]byte("Test conversation"))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
twoUserCiphertext, err := key.AesEncrypt([]byte("false"))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
id, err := uuid.NewV4()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
conversationDetailIDCiphertext, err := key.AesEncrypt([]byte(id.String()))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
adminCiphertext, err := key.AesEncrypt([]byte("true"))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
userIDCiphertext, err := key.AesEncrypt([]byte(u.ID.String()))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
usernameCiphertext, err := key.AesEncrypt([]byte(u.Username))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
pubKey := Seeder.GetPubKey()
d := struct {
ID string `json:"id"`
Name string `json:"name"`
TwoUser string `json:"two_user"`
Users []Database.ConversationDetailUser `json:"users"`
UserConversations []Database.UserConversation `json:"user_conversations"`
}{
ID: id.String(),
Name: base64.StdEncoding.EncodeToString(nameCiphertext),
TwoUser: base64.StdEncoding.EncodeToString(twoUserCiphertext),
Users: []Database.ConversationDetailUser{
{
ConversationDetailID: id,
UserID: base64.StdEncoding.EncodeToString(userIDCiphertext),
Username: base64.StdEncoding.EncodeToString(usernameCiphertext),
AssociationKey: "",
PublicKey: "",
Admin: base64.StdEncoding.EncodeToString(adminCiphertext),
},
},
UserConversations: []Database.UserConversation{
{
UserID: u.ID,
ConversationDetailID: base64.StdEncoding.EncodeToString(conversationDetailIDCiphertext),
Admin: base64.StdEncoding.EncodeToString(adminCiphertext),
SymmetricKey: base64.StdEncoding.EncodeToString(
Seeder.EncryptWithPublicKey(key.Key, pubKey),
),
},
},
}
jsonStr, _ := json.Marshal(d)
req, _ := http.NewRequest("POST", ts.URL+"/api/v1/auth/conversations", 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)
}
var c Database.ConversationDetail
err = Database.DB.First(&c, "id = ?", id.String()).Error
if err != nil {
t.Errorf("Expected conversation detail record, received %s", err.Error())
return
}
}

+ 90
- 0
Backend/Api/Messages/CreateMessage.go View File

@ -0,0 +1,90 @@
package Messages
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"time"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Service"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Util"
)
// TODO: Encrypt sender and conversationID metadata + add more
type payload struct {
Messages []rawMessageData `json:"messages"`
Sender string `json:"sender"`
ConversationID string `json:"conversation_id"`
Tokens []string `json:"tokens"`
}
type rawMessageData struct {
MessageData Database.MessageData `json:"message_data"`
Messages Database.MessageList `json:"message"`
}
// CreateMessage sends a message
func CreateMessage(w http.ResponseWriter, r *http.Request) {
var (
messagesData payload
messageData rawMessageData
message Database.Message
t time.Time
decodedFile []byte
fileName string
i int
err error
)
err = json.NewDecoder(r.Body).Decode(&messagesData)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
for _, messageData = range messagesData.Messages {
if messageData.MessageData.Data == "" {
decodedFile, err = base64.StdEncoding.DecodeString(messageData.MessageData.Attachment.Data)
fileName, err = Util.WriteFile(decodedFile)
messageData.MessageData.Attachment.FilePath = fileName
}
for i, message = range messageData.Messages {
t, err = time.Parse(time.RFC3339, message.ExpiryRaw)
if err == nil {
err = messageData.Messages[i].Expiry.Scan(t)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
}
}
err = (&messageData.MessageData).CreateMessageData()
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
err = (&messageData.Messages).CreateMessages()
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
}
_ = Service.SendNotification(
messagesData.Tokens,
fmt.Sprintf(
"%s sent a message",
messagesData.Sender,
),
map[string]string{
"conversation_id": messagesData.ConversationID,
},
)
w.WriteHeader(http.StatusNoContent)
}

+ 131
- 0
Backend/Api/Messages/CreateMessage_test.go View File

@ -0,0 +1,131 @@
package Messages_test
import (
"bytes"
"encoding/base64"
"encoding/json"
"net/http"
"testing"
"time"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database/Seeder"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Tests"
"github.com/gofrs/uuid"
)
// TODO: Write test for message expiry
func Test_CreateMessage(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")
key, err := Seeder.GenerateAesKey()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
dataCiphertext, err := key.AesEncrypt([]byte("Test message..."))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
senderIDCiphertext, err := key.AesEncrypt([]byte(u.ID.String()))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
id, err := uuid.NewV4()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
id2, err := uuid.NewV4()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
d := []struct {
MessageData struct {
ID uuid.UUID `json:"id"`
Data string `json:"data"`
SenderID string `json:"sender_id"`
SymmetricKey string `json:"symmetric_key"`
} `json:"message_data"`
Messages []struct {
ID uuid.UUID `json:"id"`
MessageDataID uuid.UUID `json:"message_data_id"`
SymmetricKey string `json:"symmetric_key"`
AssociationKey string `json:"association_key"`
Expiry time.Time `json:"expiry"`
} `json:"message"`
}{
{
MessageData: struct {
ID uuid.UUID `json:"id"`
Data string `json:"data"`
SenderID string `json:"sender_id"`
SymmetricKey string `json:"symmetric_key"`
}{
ID: id,
Data: base64.StdEncoding.EncodeToString(dataCiphertext),
SenderID: base64.StdEncoding.EncodeToString(senderIDCiphertext),
SymmetricKey: "",
},
Messages: []struct {
ID uuid.UUID `json:"id"`
MessageDataID uuid.UUID `json:"message_data_id"`
SymmetricKey string `json:"symmetric_key"`
AssociationKey string `json:"association_key"`
Expiry time.Time `json:"expiry"`
}{
{
ID: id2,
MessageDataID: id,
SymmetricKey: "",
AssociationKey: "",
Expiry: time.Now(),
},
},
},
}
jsonStr, _ := json.Marshal(d)
req, _ := http.NewRequest("POST", ts.URL+"/api/v1/auth/message", 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 m Database.Message
err = Database.DB.First(&m).Error
if err != nil {
t.Errorf("Expected conversation detail record, received %s", err.Error())
return
}
var md Database.MessageData
err = Database.DB.First(&md).Error
if err != nil {
t.Errorf("Expected conversation detail record, received %s", err.Error())
return
}
}

+ 65
- 0
Backend/Api/Messages/MessageThread.go View File

@ -0,0 +1,65 @@
package Messages
import (
"encoding/json"
"net/http"
"net/url"
"strconv"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"github.com/gorilla/mux"
)
// Messages gets messages by the associationKey
func Messages(w http.ResponseWriter, r *http.Request) {
var (
messages []Database.Message
message Database.Message
urlVars map[string]string
associationKey string
values url.Values
returnJSON []byte
page int
i int
ok bool
err error
)
urlVars = mux.Vars(r)
associationKey, ok = urlVars["associationKey"]
if !ok {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
values = r.URL.Query()
page, err = strconv.Atoi(values.Get("page"))
if err != nil {
page = 0
}
messages, err = Database.GetMessagesByAssociationKey(associationKey, page)
if !ok {
http.Error(w, "Not Found", http.StatusNotFound)
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)
return
}
w.WriteHeader(http.StatusOK)
w.Write(returnJSON)
}

+ 204
- 0
Backend/Api/Messages/MessageThread_test.go View File

@ -0,0 +1,204 @@
package Messages_test
import (
"encoding/base64"
"encoding/json"
"io/ioutil"
"net/http"
"testing"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database/Seeder"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Tests"
)
func Test_Messages(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")
userKey, err := Seeder.GenerateAesKey()
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
}
dataCiphertext, err := key.AesEncrypt([]byte("Test message"))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
senderIDCiphertext, err := key.AesEncrypt([]byte(u.ID.String()))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
keyCiphertext, err := userKey.AesEncrypt(
[]byte(base64.StdEncoding.EncodeToString(key.Key)),
)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
pubKey := Seeder.GetPubKey()
message := Database.Message{
MessageData: Database.MessageData{
Data: base64.StdEncoding.EncodeToString(dataCiphertext),
SenderID: base64.StdEncoding.EncodeToString(senderIDCiphertext),
SymmetricKey: base64.StdEncoding.EncodeToString(keyCiphertext),
},
SymmetricKey: base64.StdEncoding.EncodeToString(
Seeder.EncryptWithPublicKey(userKey.Key, pubKey),
),
AssociationKey: "AssociationKey",
}
err = (&message).CreateMessage()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
resp, err := client.Get(ts.URL + "/api/v1/auth/messages/AssociationKey")
if err != nil {
t.Errorf("Expected user record, recieved %s", err.Error())
return
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected %d, recieved %d", http.StatusOK, resp.StatusCode)
return
}
requestBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
var m Database.MessageList
err = json.Unmarshal(requestBody, &m)
if len(m) != 1 {
t.Errorf("Expected %d, recieved %d", 1, len(m))
}
msg := m[0]
decodedData, err := base64.StdEncoding.DecodeString(msg.MessageData.Data)
if err != nil {
t.Errorf("Expected %d, recieved %d", http.StatusOK, resp.StatusCode)
return
}
decrypedData, err := key.AesDecrypt(decodedData)
if err != nil {
t.Errorf("Expected %d, recieved %d", http.StatusOK, resp.StatusCode)
return
}
if string(decrypedData) != "Test message" {
t.Errorf("Expected %s, recieved %s", "Test converation", string(decrypedData))
}
}
func Test_MessagesPagination(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")
userKey, err := Seeder.GenerateAesKey()
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
}
dataCiphertext, err := key.AesEncrypt([]byte("Test message"))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
senderIDCiphertext, err := key.AesEncrypt([]byte(u.ID.String()))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
keyCiphertext, err := userKey.AesEncrypt(
[]byte(base64.StdEncoding.EncodeToString(key.Key)),
)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
pubKey := Seeder.GetPubKey()
for i := 0; i < 50; i++ {
message := Database.Message{
MessageData: Database.MessageData{
Data: base64.StdEncoding.EncodeToString(dataCiphertext),
SenderID: base64.StdEncoding.EncodeToString(senderIDCiphertext),
SymmetricKey: base64.StdEncoding.EncodeToString(keyCiphertext),
},
SymmetricKey: base64.StdEncoding.EncodeToString(
Seeder.EncryptWithPublicKey(userKey.Key, pubKey),
),
AssociationKey: "AssociationKey",
}
err = (&message).CreateMessage()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
}
resp, err := client.Get(ts.URL + "/api/v1/auth/messages/AssociationKey")
if err != nil {
t.Errorf("Expected user record, recieved %s", err.Error())
return
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected %d, recieved %d", http.StatusOK, resp.StatusCode)
return
}
requestBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
var m Database.MessageList
err = json.Unmarshal(requestBody, &m)
if len(m) != 20 {
t.Errorf("Expected %d, recieved %d", 20, len(m))
}
}

+ 56
- 0
Backend/Api/Messages/UpdateConversation.go View File

@ -0,0 +1,56 @@
package Messages
import (
"encoding/json"
"net/http"
"github.com/gofrs/uuid"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
)
type rawUpdateConversationData struct {
ID string `json:"id"`
Name string `json:"name"`
Users []Database.ConversationDetailUser `json:"users"`
UserConversations Database.UserConversationList `json:"user_conversations"`
}
// UpdateConversation updates the conversation data, such as title, users, etc
func UpdateConversation(w http.ResponseWriter, r *http.Request) {
var (
rawConversationData rawUpdateConversationData
messageThread Database.ConversationDetail
err error
)
err = json.NewDecoder(r.Body).Decode(&rawConversationData)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
messageThread = Database.ConversationDetail{
Base: Database.Base{
ID: uuid.FromStringOrNil(rawConversationData.ID),
},
Name: rawConversationData.Name,
Users: rawConversationData.Users,
}
err = (&messageThread).UpdateConversationDetail()
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
if len(rawConversationData.UserConversations) > 0 {
err = (&rawConversationData.UserConversations).UpdateOrCreateUserConversations()
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
}
w.WriteHeader(http.StatusNoContent)
}

+ 182
- 0
Backend/Api/Messages/UpdateConversation_test.go View File

@ -0,0 +1,182 @@
package Messages_test
import (
"bytes"
"encoding/base64"
"encoding/json"
"net/http"
"testing"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database/Seeder"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Tests"
)
func createConversation(key Seeder.AesKey) (Database.ConversationDetail, Database.UserConversation, Database.ConversationDetailUser, error) {
var (
cd Database.ConversationDetail
uc Database.UserConversation
cdu Database.ConversationDetailUser
)
u, err := Database.GetUserByUsername("test")
nameCiphertext, err := key.AesEncrypt([]byte("Test conversation"))
if err != nil {
return cd, uc, cdu, err
}
twoUserCiphertext, err := key.AesEncrypt([]byte("false"))
if err != nil {
return cd, uc, cdu, err
}
cd = Database.ConversationDetail{
Name: base64.StdEncoding.EncodeToString(nameCiphertext),
TwoUser: base64.StdEncoding.EncodeToString(twoUserCiphertext),
}
err = (&cd).CreateConversationDetail()
if err != nil {
return cd, uc, cdu, err
}
conversationDetailIDCiphertext, err := key.AesEncrypt([]byte(cd.ID.String()))
if err != nil {
return cd, uc, cdu, err
}
adminCiphertext, err := key.AesEncrypt([]byte("true"))
if err != nil {
return cd, uc, cdu, err
}
pubKey := Seeder.GetPubKey()
uc = Database.UserConversation{
UserID: u.ID,
ConversationDetailID: base64.StdEncoding.EncodeToString(conversationDetailIDCiphertext),
Admin: base64.StdEncoding.EncodeToString(adminCiphertext),
SymmetricKey: base64.StdEncoding.EncodeToString(
Seeder.EncryptWithPublicKey(key.Key, pubKey),
),
}
err = (&uc).CreateUserConversation()
if err != nil {
return cd, uc, cdu, err
}
userIDCiphertext, err := key.AesEncrypt([]byte(u.ID.String()))
if err != nil {
return cd, uc, cdu, err
}
usernameCiphertext, err := key.AesEncrypt([]byte(u.Username))
if err != nil {
return cd, uc, cdu, err
}
adminCiphertext, err = key.AesEncrypt([]byte("true"))
if err != nil {
return cd, uc, cdu, err
}
associationKeyCiphertext, err := key.AesEncrypt([]byte("association"))
if err != nil {
return cd, uc, cdu, err
}
publicKeyCiphertext, err := key.AesEncrypt([]byte(u.AsymmetricPublicKey))
if err != nil {
return cd, uc, cdu, err
}
cdu = Database.ConversationDetailUser{
ConversationDetailID: cd.ID,
UserID: base64.StdEncoding.EncodeToString(userIDCiphertext),
Username: base64.StdEncoding.EncodeToString(usernameCiphertext),
Admin: base64.StdEncoding.EncodeToString(adminCiphertext),
AssociationKey: base64.StdEncoding.EncodeToString(associationKeyCiphertext),
PublicKey: base64.StdEncoding.EncodeToString(publicKeyCiphertext),
}
err = (&cdu).CreateConversationDetailUser()
return cd, uc, cdu, err
}
func Test_UpdateConversation(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")
key, err := Seeder.GenerateAesKey()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
cd, uc, cdu, err := createConversation(key)
nameCiphertext, err := key.AesEncrypt([]byte("Not test conversation"))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
}
d := struct {
ID string `json:"id"`
Name string `json:"name"`
Users []Database.ConversationDetailUser
UserConversations []Database.UserConversation
}{
ID: cd.ID.String(),
Name: base64.StdEncoding.EncodeToString(nameCiphertext),
Users: []Database.ConversationDetailUser{
cdu,
},
UserConversations: []Database.UserConversation{
uc,
},
}
jsonStr, _ := json.Marshal(d)
req, _ := http.NewRequest("PUT", ts.URL+"/api/v1/auth/conversations", 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)
}
var ncd Database.ConversationDetail
err = Database.DB.First(&ncd, "id = ?", cd.ID.String()).Error
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
decodedName, err := base64.StdEncoding.DecodeString(ncd.Name)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
decrypedName, err := key.AesDecrypt(decodedName)
if err != nil {
t.Errorf("Expected %d, recieved %d", http.StatusOK, resp.StatusCode)
return
}
if string(decrypedName) != "Not test conversation" {
t.Errorf("Expected %s, recieved %s", "Not test converation", string(decrypedName))
}
}

+ 93
- 0
Backend/Api/Routes.go View File

@ -0,0 +1,93 @@
package Api
import (
"log"
"net/http"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Friends"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Messages"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Users"
"github.com/gorilla/mux"
)
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf(
"%s %s, Content Length: %d",
r.Method,
r.RequestURI,
r.ContentLength,
)
next.ServeHTTP(w, r)
})
}
func authenticationMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var err error
_, err = Auth.CheckCookie(r)
if err != nil {
http.Error(w, "Forbidden", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// InitAPIEndpoints initializes all API endpoints required by mobile app
func InitAPIEndpoints(router *mux.Router) {
var (
api *mux.Router
authAPI *mux.Router
fs http.Handler
)
log.Println("Initializing API routes...")
api = router.PathPrefix("/api/v1/").Subrouter()
api.Use(loggingMiddleware)
// Define routes for authentication
api.HandleFunc("/signup", Auth.Signup).Methods("POST")
api.HandleFunc("/login", Auth.Login).Methods("POST")
api.HandleFunc("/logout", Auth.Logout).Methods("GET")
authAPI = api.PathPrefix("/auth/").Subrouter()
authAPI.Use(authenticationMiddleware)
authAPI.HandleFunc("/check", Auth.Check).Methods("GET")
authAPI.HandleFunc("/device_token", Auth.AddDeviceToken).Methods("POST")
authAPI.HandleFunc("/change_password", Auth.ChangePassword).Methods("POST")
authAPI.HandleFunc("/message_expiry", Auth.ChangeUserMessageExpiry).Methods("POST")
authAPI.HandleFunc("/image", Auth.AddProfileImage).Methods("POST")
authAPI.HandleFunc("/users", Users.SearchUsers).Methods("GET")
authAPI.HandleFunc("/friend_requests", Friends.FriendRequestList).Methods("GET")
authAPI.HandleFunc("/friend_request", Friends.CreateFriendRequest).Methods("POST")
authAPI.HandleFunc("/friend_request/qr_code", Friends.CreateFriendRequestQrCode).Methods("POST")
authAPI.HandleFunc("/friend_request/{requestID}", Friends.AcceptFriendRequest).Methods("POST")
authAPI.HandleFunc("/friend_request/{requestID}", Friends.RejectFriendRequest).Methods("DELETE")
authAPI.HandleFunc("/conversations", Messages.ConversationList).Methods("GET")
authAPI.HandleFunc("/conversation_details", Messages.ConversationDetailsList).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("/conversations/{detailID}/message_expiry", Messages.ChangeConversationMessageExpiry).Methods("POST")
authAPI.HandleFunc("/conversations/{detailID}/users/{conversationUser}", Messages.ChangeConversationMessageExpiry).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))
}

+ 54
- 0
Backend/Api/Users/SearchUsers.go View File

@ -0,0 +1,54 @@
package Users
import (
"encoding/json"
"net/http"
"net/url"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
)
// SearchUsers searches a for a user by username
func SearchUsers(w http.ResponseWriter, r *http.Request) {
var (
user Database.User
query url.Values
rawUsername []string
username string
returnJSON []byte
ok bool
err error
)
query = r.URL.Query()
rawUsername, ok = query["username"]
if !ok {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
if len(rawUsername) != 1 {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
username = rawUsername[0]
user, err = Database.GetUserByUsername(username)
if err != nil {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
user.Password = ""
user.AsymmetricPrivateKey = ""
returnJSON, err = json.MarshalIndent(user, "", " ")
if err != nil {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
w.WriteHeader(http.StatusOK)
w.Write(returnJSON)
}

+ 106
- 0
Backend/Api/Users/SearchUsers_test.go View File

@ -0,0 +1,106 @@
package Users_test
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"testing"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Tests"
)
func Test_SearchUsers(t *testing.T) {
client, ts, err := Tests.InitTestEnv()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
u2, err := Tests.InitTestCreateUser("abcd")
req, _ := http.NewRequest(
"GET",
fmt.Sprintf("%s/api/v1/auth/users?username=%s", ts.URL, u2.Username),
nil,
)
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
}
requestBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("Expected %d, recieved %d", http.StatusOK, resp.StatusCode)
return
}
var user Database.User
json.Unmarshal(requestBody, &user)
if user.Username != "abcd" {
t.Errorf("Expected abcd, recieved %s", user.Username)
return
}
if user.Password != "" {
t.Errorf("Expected \"\", recieved %s", user.Password)
return
}
if user.AsymmetricPrivateKey != "" {
t.Errorf("Expected \"\", recieved %s", user.AsymmetricPrivateKey)
return
}
}
func Test_SearchUsersPartialMatchFails(t *testing.T) {
client, ts, err := Tests.InitTestEnv()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
_, err = Tests.InitTestCreateUser("abcd")
req, _ := http.NewRequest(
"GET",
fmt.Sprintf("%s/api/v1/auth/users?username=%s", ts.URL, "abc"),
nil,
)
resp, err := client.Do(req)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
if resp.StatusCode != http.StatusNotFound {
t.Errorf("Expected %d, recieved %d", http.StatusOK, resp.StatusCode)
return
}
requestBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("Expected %d, recieved %d", http.StatusOK, resp.StatusCode)
return
}
var user interface{}
json.Unmarshal(requestBody, &user)
if user != nil {
t.Errorf("Expected nil, recieved %+v", user)
return
}
}

+ 50
- 0
Backend/Database/Attachments.go View File

@ -0,0 +1,50 @@
package Database
import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// 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"`
}
// GetAttachmentByID gets the attachment record by the id
func GetAttachmentByID(id string) (MessageData, error) {
var (
messageData MessageData
err error
)
err = DB.Preload(clause.Associations).
First(&messageData, "id = ?", id).
Error
return messageData, err
}
// CreateAttachment creates the attachment record
func (attachment *Attachment) CreateAttachment() error {
var (
err error
)
err = DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(attachment).
Error
return err
}
// DeleteAttachment deletes the attachment record
func (attachment *Attachment) DeleteAttachment() error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(attachment).
Error
}

+ 31
- 0
Backend/Database/Base.go View File

@ -0,0 +1,31 @@
package Database
import (
"github.com/gofrs/uuid"
"gorm.io/gorm"
)
// Base contains common columns for all tables.
type Base struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
}
// BeforeCreate will set a UUID rather than numeric ID.
func (base *Base) BeforeCreate(tx *gorm.DB) error {
var (
id uuid.UUID
err error
)
if !base.ID.IsNil() {
return nil
}
id, err = uuid.NewV4()
if err != nil {
return err
}
base.ID = id
return nil
}

+ 52
- 0
Backend/Database/ConversationDetailUsers.go View File

@ -0,0 +1,52 @@
package Database
import (
"github.com/gofrs/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// ConversationDetailUser all users associated with a conversation
type ConversationDetailUser struct {
Base
ConversationDetailID uuid.UUID `gorm:"not null" json:"conversation_detail_id"`
ConversationDetail ConversationDetail `gorm:"not null" json:"conversation"`
UserID string `gorm:"not null" json:"user_id"` // Stored encrypted
Username string `gorm:"not null" json:"username"` // Stored encrypted
Admin string `gorm:"not null" json:"admin"` // Stored encrypted
AssociationKey string `gorm:"not null" json:"association_key"` // Stored encrypted
PublicKey string `gorm:"not null" json:"public_key"` // Stored encrypted
}
func GetConversationDetailUserById(id string) (ConversationDetailUser, error) {
var (
messageThread ConversationDetailUser
err error
)
err = DB.Preload(clause.Associations).
Where("id = ?", id).
First(&messageThread).
Error
return messageThread, err
}
func (detailUser *ConversationDetailUser) CreateConversationDetailUser() error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(detailUser).
Error
}
func (detailUser *ConversationDetailUser) UpdateConversationDetailUser() error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Where("id = ?", detailUser.ID).
Updates(detailUser).
Error
}
func (detailUser *ConversationDetailUser) DeleteConversationDetailUser() error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(detailUser).
Error
}

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

@ -0,0 +1,75 @@
package Database
import (
"github.com/gofrs/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// 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"`
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
AdminAddMembers string ` json:"admin_add_members"` // Stored encrypted
AdminEditInfo string ` json:"admin_edit_info"` // Stored encrypted
AdminSendMessages string ` json:"admin_send_messages"` // Stored encrypted
}
// GetConversationDetailByID gets by id
func GetConversationDetailByID(id string) (ConversationDetail, error) {
var (
conversationDetail ConversationDetail
err error
)
err = DB.Preload(clause.Associations).
Where("id = ?", id).
First(&conversationDetail).
Error
return conversationDetail, err
}
// GetConversationDetailsByIds gets by multiple ids
func GetConversationDetailsByIds(id []string) ([]ConversationDetail, error) {
var (
conversationDetail []ConversationDetail
err error
)
err = DB.Preload(clause.Associations).
Where("id IN ?", id).
Find(&conversationDetail).
Error
return conversationDetail, err
}
// CreateConversationDetail creates a ConversationDetail record
func (conversationDetail *ConversationDetail) CreateConversationDetail() error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(conversationDetail).
Error
}
// UpdateConversationDetail updates a ConversationDetail record
func (conversationDetail *ConversationDetail) UpdateConversationDetail() error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Where("id = ?", conversationDetail.ID).
Updates(conversationDetail).
Error
}
// DeleteConversationDetail deletes a ConversationDetail record
func (conversationDetail *ConversationDetail) DeleteConversationDetail() error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(conversationDetail).
Error
}

+ 48
- 0
Backend/Database/DeviceTokens.go View File

@ -0,0 +1,48 @@
package Database
import (
"github.com/gofrs/uuid"
"gorm.io/gorm"
)
type FriendRequestDeviceToken struct {
Base
DeviceTokenID uuid.UUID `gorm:"type:uuid;column:device_token_id;not null;" json:"device_token_id"`
DeviceToken DeviceToken `gorm:"not null;" json:"device_token"`
FriendRequestID uuid.UUID `gorm:"type:uuid;column:friend_request_id;not null;" json:"friend_request_id"`
FriendRequest FriendRequest `gorm:"not null;" json:"friend_device_tokens"`
}
type DeviceToken struct {
Base
Token string `gorm:"not null" json:"token"` // Stored encrypted
DeviceType string `gorm:"not null" json:"device_type"` // Stored encrypted
}
func GetUserDeviceTokenById(id string) (DeviceToken, error) {
var (
deviceToken DeviceToken
err error
)
err = DB.First(&deviceToken, "id = ?", id).
Error
return deviceToken, err
}
func (deviceToken *DeviceToken) CreateUserDeviceToken() error {
var err error
err = DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(deviceToken).
Error
return err
}
func (deviceToken *DeviceToken) DeleteUserDeviceToken() error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(deviceToken).
Error
}

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

@ -0,0 +1,110 @@
package Database
import (
"database/sql"
"time"
"github.com/gofrs/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// FriendRequest Set with Friend being the requestee, and UserID being the requester
type FriendRequest struct {
Base
UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"`
User User ` json:"user"`
FriendID string `gorm:"not null" json:"friend_id"` // Stored encrypted
FriendUsername string ` json:"friend_username"` // Stored encrypted
FriendImagePath string ` json:"friend_image_path"`
FriendPublicAsymmetricKey string ` json:"asymmetric_public_key"` // Stored encrypted
FriendRequestDeviceTokens []FriendRequestDeviceToken ` json:"tokens"`
SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted
AcceptedAt sql.NullTime ` json:"accepted_at"`
CreatedAt time.Time `gorm:"not null" json:"created_at"`
}
type FriendRequestList []FriendRequest
// GetFriendRequestByID gets friend request
func GetFriendRequestByID(id string) (FriendRequest, error) {
var (
friendRequest FriendRequest
err error
)
err = DB.Preload(clause.Associations).
First(&friendRequest, "id = ?", id).
Error
return friendRequest, err
}
// GetFriendRequestsByUserID gets friend request by user id
func GetFriendRequestsByUserID(userID string, page int) ([]FriendRequest, error) {
var (
friends []FriendRequest
offset int
err error
)
offset = page * PageSize
err = DB.Model(FriendRequest{}).
Preload("FriendRequestDeviceTokens.DeviceToken").
Where("user_id = ?", userID).
Offset(offset).
Limit(PageSize).
Order("created_at DESC").
Find(&friends).
Error
return friends, err
}
// CreateFriendRequest creates friend request
func (friendRequest *FriendRequest) CreateFriendRequest() error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(friendRequest).
Error
}
// CreateFriendRequests creates multiple friend requests
func (friendRequest *FriendRequestList) CreateFriendRequests() error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(friendRequest).
Error
}
// UpdateFriendRequest Updates friend request
func (friendRequest *FriendRequest) UpdateFriendRequest() error {
return DB.Where("id = ?", friendRequest.ID).
Updates(friendRequest).
Error
}
// DeleteFriendRequest deletes friend request
func (friendRequest *FriendRequest) DeleteFriendRequest() error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(friendRequest).
Error
}
func (friendRequest *FriendRequest) AttachDeviceToken(deviceToken DeviceToken, requester bool) error {
var (
requestToken FriendRequestDeviceToken
err error
)
requestToken = FriendRequestDeviceToken{
DeviceToken: deviceToken,
FriendRequestID: friendRequest.ID,
}
err = DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(&requestToken).
Error
return err
}

+ 98
- 0
Backend/Database/Init.go View File

@ -0,0 +1,98 @@
package Database
import (
"fmt"
"log"
"os"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
const (
PageSize = 20
)
var (
dbURL string
dbTestURL string
)
// DB db
var DB *gorm.DB
var models = []interface{}{
&Session{},
&Attachment{},
&User{},
&FriendRequest{},
&MessageData{},
&Message{},
&ConversationDetail{},
&ConversationDetailUser{},
&UserConversation{},
&DeviceToken{},
&FriendRequestDeviceToken{},
}
// Init initializes the database connection
func Init() {
var (
err error
)
log.Println("Initializing database...")
dbURL = fmt.Sprintf(
"postgres://%s:%s@%s:%s/%s",
os.Getenv("DB_USER"),
os.Getenv("DB_PASSWORD"),
os.Getenv("DB_HOST"),
os.Getenv("DB_PORT"),
os.Getenv("DB_DATABASE"),
)
DB, err = gorm.Open(postgres.Open(dbURL), &gorm.Config{})
if err != nil {
log.Fatalln(err)
}
log.Println("Running AutoMigrate...")
err = DB.AutoMigrate(models...)
if err != nil {
log.Fatalln(err)
}
}
// InitTest initializes the test datbase
func InitTest() {
var (
err error
)
dbTestURL = fmt.Sprintf(
"postgres://%s:%s@%s:%s/%s",
os.Getenv("DB_TESTING_USER"),
os.Getenv("DB_TESTING_PASSWORD"),
os.Getenv("DB_TESTING_HOST"),
os.Getenv("DB_TESTING_PORT"),
os.Getenv("DB_TESTING_DATABASE"),
)
DB, err = gorm.Open(postgres.Open(dbTestURL), &gorm.Config{})
if err != nil {
log.Fatalln(err)
}
err = DB.Migrator().DropTable(models...)
if err != nil {
panic(err)
}
err = DB.AutoMigrate(models...)
if err != nil {
panic(err)
}
}

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

@ -0,0 +1,49 @@
package Database
import (
"github.com/gofrs/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// MessageData holds the content of the message
// encrypted through the Message.SymmetricKey
type MessageData struct {
Base
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
}
func GetMessageDataById(id string) (MessageData, error) {
var (
messageData MessageData
err error
)
err = DB.Preload(clause.Associations).
First(&messageData, "id = ?", id).
Error
return messageData, err
}
func (messageData *MessageData) CreateMessageData() error {
var (
err error
)
err = DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(messageData).
Error
return err
}
func (messageData *MessageData) DeleteMessageData() error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(messageData).
Error
}

+ 70
- 0
Backend/Database/MessageExpiry.go View File

@ -0,0 +1,70 @@
package Database
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)
}

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

@ -0,0 +1,89 @@
package Database
import (
"database/sql"
"time"
"github.com/gofrs/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// Message holds data pertaining to each users' message
type Message struct {
Base
MessageDataID uuid.UUID ` json:"message_data_id"`
MessageData MessageData ` json:"message_data"`
SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted
AssociationKey string `gorm:"not null" json:"association_key"` // Stored encrypted
ExpiryRaw string ` json:"expiry"`
Expiry sql.NullTime ` json:"-"`
CreatedAt time.Time `gorm:"not null" json:"created_at"`
}
type MessageList []Message
// GetMessageByID gets a message
func GetMessageByID(id string) (Message, error) {
var (
message Message
err error
)
err = DB.Preload(clause.Associations).
First(&message, "id = ?", id).
Error
return message, err
}
// GetMessagesByAssociationKey for getting whole thread
func GetMessagesByAssociationKey(associationKey string, page int) ([]Message, error) {
var (
messages []Message
offset int
err error
)
offset = page * PageSize
err = DB.Preload("MessageData").
Preload("MessageData.Attachment").
Offset(offset).
Limit(PageSize).
Order("created_at DESC").
Find(&messages, "association_key = ?", associationKey).
Error
return messages, err
}
// CreateMessage creates a message record
func (message *Message) CreateMessage() error {
var err error
err = DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(message).
Error
return err
}
// CreateMessages creates multiple records
func (messages *MessageList) CreateMessages() error {
var err error
err = DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(messages).
Error
return err
}
// DeleteMessage deletes a message
func (message *Message) DeleteMessage() error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(message).
Error
}

+ 141
- 0
Backend/Database/Seeder/FriendSeeder.go View File

@ -0,0 +1,141 @@
package Seeder
import (
"encoding/base64"
"io"
"os"
"time"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
)
func seedFriend(userRequestTo, userRequestFrom Database.User, accepted bool) error {
var (
friendRequest Database.FriendRequest
symKey AesKey
encPublicKey []byte
err error
)
symKey, err = GenerateAesKey()
if err != nil {
return err
}
encPublicKey, err = symKey.AesEncrypt([]byte(PublicKey))
if err != nil {
return err
}
friendRequest = Database.FriendRequest{
UserID: userRequestTo.ID,
FriendID: base64.StdEncoding.EncodeToString(
EncryptWithPublicKey(
[]byte(userRequestFrom.ID.String()),
decodedPublicKey,
),
),
FriendUsername: base64.StdEncoding.EncodeToString(
EncryptWithPublicKey(
[]byte(userRequestFrom.Username),
decodedPublicKey,
),
),
FriendPublicAsymmetricKey: base64.StdEncoding.EncodeToString(
encPublicKey,
),
SymmetricKey: base64.StdEncoding.EncodeToString(
EncryptWithPublicKey(symKey.Key, decodedPublicKey),
),
}
if accepted {
friendRequest.AcceptedAt.Time = time.Now()
friendRequest.AcceptedAt.Valid = true
}
return (&friendRequest).CreateFriendRequest()
}
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 (
primaryUser Database.User
secondaryUser Database.User
accepted bool
i int
err error
)
// err = copyProfileImage()
// if err != nil {
// panic(err)
// }
primaryUser, err = Database.GetUserByUsername("testUser")
if err != nil {
panic(err)
}
secondaryUser, err = Database.GetUserByUsername("ATestUser2")
if err != nil {
panic(err)
}
err = seedFriend(primaryUser, secondaryUser, true)
if err != nil {
panic(err)
}
err = seedFriend(secondaryUser, primaryUser, true)
if err != nil {
panic(err)
}
accepted = false
for i = 0; i <= 5; i++ {
secondaryUser, err = Database.GetUserByUsername(userNames[i])
if err != nil {
panic(err)
}
if i > 3 {
accepted = true
}
err = seedFriend(primaryUser, secondaryUser, accepted)
if err != nil {
panic(err)
}
if accepted {
err = seedFriend(secondaryUser, primaryUser, accepted)
if err != nil {
panic(err)
}
}
}
}

+ 321
- 0
Backend/Database/Seeder/MessageSeeder.go View File

@ -0,0 +1,321 @@
package Seeder
import (
"encoding/base64"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"github.com/gofrs/uuid"
)
func seedMessage(
primaryUser, secondaryUser Database.User,
primaryUserAssociationKey, secondaryUserAssociationKey string,
i int,
) error {
var (
message Database.Message
messageData Database.MessageData
key, userKey AesKey
keyCiphertext []byte
plaintext string
dataCiphertext []byte
senderIDCiphertext []byte
err error
)
plaintext = "Test Message"
userKey, err = GenerateAesKey()
if err != nil {
panic(err)
}
key, err = GenerateAesKey()
if err != nil {
panic(err)
}
dataCiphertext, err = key.AesEncrypt([]byte(plaintext))
if err != nil {
panic(err)
}
senderIDCiphertext, err = key.AesEncrypt([]byte(primaryUser.ID.String()))
if err != nil {
panic(err)
}
if i%2 == 0 {
senderIDCiphertext, err = key.AesEncrypt([]byte(secondaryUser.ID.String()))
if err != nil {
panic(err)
}
}
keyCiphertext, err = userKey.AesEncrypt(
[]byte(base64.StdEncoding.EncodeToString(key.Key)),
)
if err != nil {
panic(err)
}
messageData = Database.MessageData{
Data: base64.StdEncoding.EncodeToString(dataCiphertext),
SenderID: base64.StdEncoding.EncodeToString(senderIDCiphertext),
SymmetricKey: base64.StdEncoding.EncodeToString(keyCiphertext),
}
message = Database.Message{
MessageData: messageData,
SymmetricKey: base64.StdEncoding.EncodeToString(
EncryptWithPublicKey(userKey.Key, decodedPublicKey),
),
AssociationKey: primaryUserAssociationKey,
}
err = (&message).CreateMessage()
if err != nil {
return err
}
message = Database.Message{
MessageData: messageData,
SymmetricKey: base64.StdEncoding.EncodeToString(
EncryptWithPublicKey(userKey.Key, decodedPublicKey),
),
AssociationKey: secondaryUserAssociationKey,
}
return (&message).CreateMessage()
}
func seedConversationDetail(key AesKey) (Database.ConversationDetail, error) {
var (
conversationDetail Database.ConversationDetail
name string
nameCiphertext []byte
falseCiphertext []byte
trueCiphertext []byte
err error
)
name = "Test Conversation"
nameCiphertext, err = key.AesEncrypt([]byte(name))
if err != nil {
panic(err)
}
falseCiphertext, err = key.AesEncrypt([]byte("false"))
if err != nil {
panic(err)
}
trueCiphertext, err = key.AesEncrypt([]byte("true"))
if err != nil {
panic(err)
}
conversationDetail = Database.ConversationDetail{
Name: base64.StdEncoding.EncodeToString(nameCiphertext),
TwoUser: base64.StdEncoding.EncodeToString(falseCiphertext),
AdminAddMembers: base64.StdEncoding.EncodeToString(trueCiphertext),
AdminEditInfo: base64.StdEncoding.EncodeToString(trueCiphertext),
AdminSendMessages: base64.StdEncoding.EncodeToString(falseCiphertext),
}
err = (&conversationDetail).CreateConversationDetail()
return conversationDetail, err
}
func seedUserConversation(
user Database.User,
threadID uuid.UUID,
key AesKey,
) (Database.UserConversation, error) {
var (
messageThreadUser Database.UserConversation
conversationDetailIDCiphertext []byte
adminCiphertext []byte
err error
)
conversationDetailIDCiphertext, err = key.AesEncrypt([]byte(threadID.String()))
if err != nil {
return messageThreadUser, err
}
adminCiphertext, err = key.AesEncrypt([]byte("true"))
if err != nil {
return messageThreadUser, err
}
messageThreadUser = Database.UserConversation{
UserID: user.ID,
ConversationDetailID: base64.StdEncoding.EncodeToString(conversationDetailIDCiphertext),
Admin: base64.StdEncoding.EncodeToString(adminCiphertext),
SymmetricKey: base64.StdEncoding.EncodeToString(
EncryptWithPublicKey(key.Key, decodedPublicKey),
),
}
err = (&messageThreadUser).CreateUserConversation()
return messageThreadUser, err
}
func seedConversationDetailUser(
user Database.User,
conversationDetail Database.ConversationDetail,
associationKey uuid.UUID,
admin bool,
key AesKey,
) (Database.ConversationDetailUser, error) {
var (
conversationDetailUser Database.ConversationDetailUser
userIDCiphertext []byte
usernameCiphertext []byte
adminCiphertext []byte
associationKeyCiphertext []byte
publicKeyCiphertext []byte
adminString = "false"
err error
)
if admin {
adminString = "true"
}
userIDCiphertext, err = key.AesEncrypt([]byte(user.ID.String()))
if err != nil {
return conversationDetailUser, err
}
usernameCiphertext, err = key.AesEncrypt([]byte(user.Username))
if err != nil {
return conversationDetailUser, err
}
adminCiphertext, err = key.AesEncrypt([]byte(adminString))
if err != nil {
return conversationDetailUser, err
}
associationKeyCiphertext, err = key.AesEncrypt([]byte(associationKey.String()))
if err != nil {
return conversationDetailUser, err
}
publicKeyCiphertext, err = key.AesEncrypt([]byte(user.AsymmetricPublicKey))
if err != nil {
return conversationDetailUser, err
}
conversationDetailUser = Database.ConversationDetailUser{
ConversationDetailID: conversationDetail.ID,
UserID: base64.StdEncoding.EncodeToString(userIDCiphertext),
Username: base64.StdEncoding.EncodeToString(usernameCiphertext),
Admin: base64.StdEncoding.EncodeToString(adminCiphertext),
AssociationKey: base64.StdEncoding.EncodeToString(associationKeyCiphertext),
PublicKey: base64.StdEncoding.EncodeToString(publicKeyCiphertext),
}
err = (&conversationDetailUser).CreateConversationDetailUser()
return conversationDetailUser, err
}
// SeedMessages seeds messages & conversations for testing
func SeedMessages() {
var (
conversationDetail Database.ConversationDetail
key AesKey
primaryUser Database.User
primaryUserAssociationKey uuid.UUID
secondaryUser Database.User
secondaryUserAssociationKey uuid.UUID
i int
err error
)
key, err = GenerateAesKey()
if err != nil {
panic(err)
}
conversationDetail, err = seedConversationDetail(key)
primaryUserAssociationKey, err = uuid.NewV4()
if err != nil {
panic(err)
}
secondaryUserAssociationKey, err = uuid.NewV4()
if err != nil {
panic(err)
}
primaryUser, err = Database.GetUserByUsername("testUser")
if err != nil {
panic(err)
}
_, err = seedUserConversation(
primaryUser,
conversationDetail.ID,
key,
)
if err != nil {
panic(err)
}
secondaryUser, err = Database.GetUserByUsername("ATestUser2")
if err != nil {
panic(err)
}
_, err = seedUserConversation(
secondaryUser,
conversationDetail.ID,
key,
)
if err != nil {
panic(err)
}
_, err = seedConversationDetailUser(
primaryUser,
conversationDetail,
primaryUserAssociationKey,
true,
key,
)
if err != nil {
panic(err)
}
_, err = seedConversationDetailUser(
secondaryUser,
conversationDetail,
secondaryUserAssociationKey,
false,
key,
)
if err != nil {
panic(err)
}
for i = 0; i <= 100; i++ {
err = seedMessage(
primaryUser,
secondaryUser,
primaryUserAssociationKey.String(),
secondaryUserAssociationKey.String(),
i,
)
if err != nil {
panic(err)
}
}
}

+ 121
- 0
Backend/Database/Seeder/Seed.go View File

@ -0,0 +1,121 @@
package Seeder
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"log"
)
const (
// EncryptedPrivateKey with "password"
EncryptedPrivateKey string = `sPhQsHpXYFqPb7qdmTY7APFwBb4m7meCITujDeKMQFnIjplOVm9ijjXU+YAmGvrX13ukBj8zo9MTVhjJUjJ917pyLhl4w8uyg1jCvplUYtJVXhGA9Wy3NqHMuq3SU3fKdlEM+oR4zYkbAYWp42XvulbcuVBEWiWkvHOrbdKPFpMmd54SL2c/vcWrmjgC7rTlJf2TYICZwRK+6Y0XZi5fSWeU0vg7+rHWKHc5MHHtAdAiL+HCa90c5gfh+hXkT5ojGHOkhT9kdLy3PTPN19EGpdXgZ3WFq1z9CZ6zX7uM091uR0IvgzfwaLx8HJCx7ViWQhioH9LJZgC73RMf/dwzejg2COy4QT/E59RPOczgd779rxiRmphMoR8xJYBFRlkTVmcUO4NcUE50Cc39hXezcekHuV1YQK4BXTrxGX1ceiCXYlKAWS9wHZpog9OldTCPBpw5XAWExh3kRzqdvsdHxHVE+TpAEIjDljAlc3r+FPHYH1zWWk41eQ/zz3Vkx5Zl4dMF9x+uUOspQXVb/4K42e9fMKychNUN5o/JzIwy7xOzgXa6iwf223On/mXKV6FK6Q8lojK7Wc8g7AwfqnN9//HjI14pVqGBJtn5ggL/g4qt0JFl3pV/6n/ZLMG6k8wpsaApLGvsTPqZHcv+C69Z33rZQ4TagXVxpmnWMpPCaR0+Dawn4iAce2UvUtIN2KbJNcTtRQo4z30+BbgmVKHgkR0EHMu4cYjJPYwJ5H8IYcQuFKb7+Cp33FD2Lv54I9uvtVHH9bWcid9K82y68PufJi/0icZ3EyEqZygez9mgJzxXO1b7xZMiosGs82QRv7IIOSzqBPRYv1Lxi3fWkgnOvw4dWFxJnKEI2+KD9K0z+XsgVlm26fdRklQAAf6xOJ1nJXBScbm12FBTWLMjLzHWz/iI9mQ+eGV9AREqrgQjUayXdnCsa0Q9bTTktxBkrJND4NUEDSGklhj9SY+VM0mhgAbkCvSE59vKtcNmCHx2Y+JnbZyKzJ71EaErX9vOpYCneKOjn8phVBJHQHM16QRLGyW4DUfn2CtAvb7Kks56kf/mn9YZDU68zSoLzm9rz7fjS2OUsxwmuv2IRCv/UTGgtfEfCs34qzagADfTNKTou7qkedhoygvuHiN4PzgGnjw1DQMks9PWr44z1gvIV4pEGiqgIuNHDjxKsfgQy0Cp2AV1+FNLWd1zd5t/K2pXR+knDoeHIZ2m6txQMl9I4GIyQ1bQFJWrYXPS8oMjvoH0YYVsHyShBsU2SKlG7nGbuUyoCR1EtRIzHMgP1Dq+Whqdbv67pRvhGVmydkCh0wbD+LJBcp2KJK+EQT9vv6GT5JW0oVHnE5TEXCnEJOW/rMhNMTMSccRmnVdguIE4HZsXx+cmV36jHgEt9bzcsvyWvFFoG4xL+t2UUnztX870vu//XaeVuOEAgehY/KLncrY7lhsQA4puCFIWpPteiCNhU1D8DTKc8V0ZtLT9a31SL1NLhZ+YHiD8Hs5SYdj6FW50E5yYUqPRPkg5mpbh88cRcPdsngCxU8iusNN3MSP07lO0h8zULDqtQsAq9p5o7IFTvWlAjekMy1sKTj3CuH7FuAkMHvwU0odMFeaS9T+8+4OGeprHwogWTzTbPnoOqOP/RC6vGfBvpju5s264hYguT24iXzhDFYk/8JQQe+USIbkQ7wXRw+/9cK8h5cs4LyaxMOx0pXHooxJ01bF8BYgYG4s0RB2gItzMk/L5/XhrOdWxEAdYR27s0dCN58gyvoU6phgQbTqvNTFYAObRcjfKfHu3PrFCYBBAKJ7Nm58C3rz832+ZTGVdQ3490TvO+sCLYKzpgtsqr8KyedG9LKa8wn/wlRD7kYn+J2SrMPY2Q0e4evyJaCAsolp/BQfy9JFtyRDPWTHn+jOHjW8ZN7vswGkRwYlSJSl0UC8mmJyS4lwnO/Vv4wBnDHQEzIycjn3JZAlV5ing0HKqUfW6G07453JXd8oZiMC/kIQjgWkdg34zxBYarVVrHFG5FIH9w7QWY8PCDU/kkcLniT0yD1/gkqAG2HpwaXEcSqX8Ofrbpd/IA7R7iCXYE5Q1mAvSvICpPg9Cf3CHjLyAEDz9cwKnZHkocXC8evdsTf2e7Wz8FFPAI3onFvym0MfZuRrIZitX1V8NOLedd3y74CwuErfzrr60DjyPRxGbJ4llMbm+ojeENe0HBedNm71jf+McSihKbSo5GDBxfVYVreYZ8A4iP0LsxtzQFxuzdeDL5KA9uNNw+LN9FN9vKhdALhQSnSfLPfMBsM/ey7dbxb4eRT0fpApX`
// PrivateKey for testing server side
PrivateKey string = `-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDJScQQJxWxKwqf
FmXH64QnRBVyW7cU25F+O9Zy96dqTjbV4ruWrzb4+txmK20ZPQvMxDLefhEzTXWb
HZV1P/XxgmEpaBVHwHnkhaPzzChOa/G18CDoCNrgyVzh5a31OotTCuGlS1bSkR53
ExPXQq8nPJKqN1tdwAslr4cT61zypKSmZsJa919IZeHL9p1/UMXknfThcR9z3rR4
ll6O7+dmrZzaDdJz39IC38K5yJsThau5hnddpssw76k4rZ9KFFlbWJSHFvWIAhst
lCV0kCwdAmPNVNavzQfvTxWeN1x9uJUstVlg60DRCmRhjC88K77sU+1jp4cp/Uv8
aSGRpytlAgMBAAECggEBALFbJr8YwRs/EnfEU2AI24OBkOgXecSOBq9UaAsavU+E
pPpmceU+c1CEMUhwwQs457m/shaqu9sZSCOpuHP8LGdk+tlyFTYImR5KxoBdBbK7
l9k4QLZSfxELO6TrLBDkSbic4N8098ZHCbHfhF7qKcyHqa8DYaTEPs4wz/M0Mcy0
xziCxMUFh/LhSLDH8PMMXZ+HV3+zmxdEqmaZvk3FQOGD1O39I9TA8PnFa11whVbN
nMSjxgmK+byPIM4LFXNHk+TZsJm1FaYaGVdLetAPET7p6XMrMWy+z/4dcb4GbYjY
0i5Xv1lVlIRgDB9xj0MOW5hzQzTPHC4JN4nIoBFSc20CgYEA5IgymckwqKJJWXRn
AIJ3guuEp4vBtjmdVCJnFmbPEeW+WY+CNuwn9DK78Zavfn1HruryE/hkYLVNPm8y
KSf16+tIadUXcao1UIVDNSVC6jtFmRLgWuPXbNKFQwUor1ai9IK+F3JV8pfr36HE
8rk/LEM0DIgsTg+j+IKT39a7IucCgYEA4XtKGhvnGUdcveMPcrvuQlSnucSpw5Ly
4KuRsTySdMihhxX1GSyg6F2T4YKFRqKZERsYgYk6A32u53If+VkXacvOsInwuoBa
FTb3fOQpw1xBSI7R3RgiriY4cCsDetexEBbg7/SrodpQu254A8+5PKxrSR1U+idx
boX745k1gdMCgYEAuZ7CctTOV/pQ137LdseBqO4BNlE2yvrrBf5XewOQZzoTLQ16
N4ADR765lxXMf1HkmnesnnnflglMr0yEEpepkLDvhT6WpzUXzsoe95jHTBdOhXGm
l0x+mp43rWMQU7Jr82wKWGL+2md5J5ButrOuUxZWvWMRkWn0xhHRaDsyjrsCgYAq
zNRMEG/VhI4+HROZm8KmJJuRz5rJ3OLtcqO9GNpUAKFomupjVO1WLi0b6UKTHdog
PRxxujKg5wKEPE2FbzvagS1CpWxkemifDkf8FPM4ehKKS1HavfIXTHn6ELAgaUDa
5Pzdj3vkxSP98AIn9w4aTkAvKLowobwOVrBxi2t0sQKBgHh2TrGSnlV3s1DijfNM
0JiwsHWz0hljybcZaZP45nsgGRiR15TcIiOLwkjaCws2tYtOSOT4sM7HV/s2mpPa
b0XvaLzh1iKG7HZ9tvPt/VhHlKKosNBK/j4fvgMZg7/bhRfHmaDQKoqlGbtyWjEQ
mj1b2/Gnbk3VYDR16BFfj7m2
-----END PRIVATE KEY-----`
// PublicKey for encryption
PublicKey string = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyUnEECcVsSsKnxZlx+uE
J0QVclu3FNuRfjvWcvenak421eK7lq82+PrcZittGT0LzMQy3n4RM011mx2VdT/1
8YJhKWgVR8B55IWj88woTmvxtfAg6Aja4Mlc4eWt9TqLUwrhpUtW0pEedxMT10Kv
JzySqjdbXcALJa+HE+tc8qSkpmbCWvdfSGXhy/adf1DF5J304XEfc960eJZeju/n
Zq2c2g3Sc9/SAt/CucibE4WruYZ3XabLMO+pOK2fShRZW1iUhxb1iAIbLZQldJAs
HQJjzVTWr80H708VnjdcfbiVLLVZYOtA0QpkYYwvPCu+7FPtY6eHKf1L/Gkhkacr
ZQIDAQAB
-----END PUBLIC KEY-----`
)
var (
decodedPublicKey *rsa.PublicKey
decodedPrivateKey *rsa.PrivateKey
)
// GetPubKey for seeding & tests
func GetPubKey() *rsa.PublicKey {
var (
block *pem.Block
decKey any
decPubKey *rsa.PublicKey
ok bool
err error
)
block, _ = pem.Decode([]byte(PublicKey))
decKey, err = x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
panic(err)
}
decPubKey, ok = decKey.(*rsa.PublicKey)
if !ok {
panic(errors.New("Invalid decodedPublicKey"))
}
return decPubKey
}
// GetPrivKey for seeding & tests
func GetPrivKey() *rsa.PrivateKey {
var (
block *pem.Block
decKey any
decPrivKey *rsa.PrivateKey
ok bool
err error
)
block, _ = pem.Decode([]byte(PrivateKey))
decKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
panic(err)
}
decPrivKey, ok = decKey.(*rsa.PrivateKey)
if !ok {
panic(errors.New("Invalid decodedPrivateKey"))
}
return decPrivKey
}
// Seed seeds semi random data for use in testing & development
func Seed() {
decodedPublicKey = GetPubKey()
decodedPrivateKey = GetPrivKey()
log.Println("Seeding users...")
SeedUsers()
log.Println("Seeding friend connections...")
SeedFriends()
log.Println("Seeding messages...")
SeedMessages()
}

+ 79
- 0
Backend/Database/Seeder/UserSeeder.go View File

@ -0,0 +1,79 @@
package Seeder
import (
"encoding/base64"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
)
var userNames = []string{
"assuredcoot",
"quotesteeve",
"blueberriessiemens",
"eliteexaggerate",
"twotrice",
"moderagged",
"duleelderly",
"stringdetailed",
"nodesanymore",
"sacredpolitical",
"pajamasenergy",
}
func createUser(username string) (Database.User, error) {
var (
userData Database.User
userKey AesKey
password string
err error
)
userKey, err = GenerateAesKey()
if err != nil {
panic(err)
}
password, err = Auth.HashPassword("password")
if err != nil {
return Database.User{}, err
}
userData = Database.User{
Username: username,
Password: password,
AsymmetricPrivateKey: EncryptedPrivateKey,
AsymmetricPublicKey: PublicKey,
SymmetricKey: base64.StdEncoding.EncodeToString(
EncryptWithPublicKey(userKey.Key, decodedPublicKey),
),
}
err = (&userData).CreateUser()
return userData, err
}
// SeedUsers used to create dummy users for testing & development
func SeedUsers() {
var (
i int
err error
)
// Seed users used for conversation seeding
_, err = createUser("testUser")
if err != nil {
panic(err)
}
_, err = createUser("ATestUser2")
if err != nil {
panic(err)
}
for i = 0; i <= 10; i++ {
_, err = createUser(userNames[i])
if err != nil {
panic(err)
}
}
}

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

@ -0,0 +1,193 @@
package Seeder
// THIS FILE IS ONLY USED FOR SEEDING DATA DURING DEVELOPMENT
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"fmt"
"hash"
"golang.org/x/crypto/pbkdf2"
)
type AesKey struct {
Key []byte
Iv []byte
}
func (key AesKey) encode() string {
return base64.StdEncoding.EncodeToString(key.Key)
}
// Appends padding.
func pkcs7Padding(data []byte, blocklen int) ([]byte, error) {
var (
padlen int = 1
pad []byte
)
if blocklen <= 0 {
return nil, fmt.Errorf("invalid blocklen %d", blocklen)
}
for ((len(data) + padlen) % blocklen) != 0 {
padlen = padlen + 1
}
pad = bytes.Repeat([]byte{byte(padlen)}, padlen)
return append(data, pad...), nil
}
// pkcs7strip remove pkcs7 padding
func pkcs7strip(data []byte, blockSize int) ([]byte, error) {
var (
length int
padLen int
ref []byte
)
length = len(data)
if length == 0 {
return nil, fmt.Errorf("pkcs7: Data is empty")
}
if (length % blockSize) != 0 {
return nil, fmt.Errorf("pkcs7: Data is not block-aligned")
}
padLen = int(data[length-1])
ref = bytes.Repeat([]byte{byte(padLen)}, padLen)
if padLen > blockSize || padLen == 0 || !bytes.HasSuffix(data, ref) {
return nil, fmt.Errorf("pkcs7: Invalid padding")
}
return data[:length-padLen], nil
}
func GenerateAesKey() (AesKey, error) {
var (
saltBytes []byte = []byte{}
password []byte
seed []byte
iv []byte
err error
)
password = make([]byte, 64)
_, err = rand.Read(password)
if err != nil {
return AesKey{}, err
}
seed = make([]byte, 64)
_, err = rand.Read(seed)
if err != nil {
return AesKey{}, err
}
iv = make([]byte, 16)
_, err = rand.Read(iv)
if err != nil {
return AesKey{}, err
}
return AesKey{
Key: pbkdf2.Key(
password,
saltBytes,
1000,
32,
func() hash.Hash { return hmac.New(sha256.New, seed) },
),
Iv: iv,
}, nil
}
func (key AesKey) AesEncrypt(plaintext []byte) ([]byte, error) {
var (
bPlaintext []byte
ciphertext []byte
block cipher.Block
err error
)
bPlaintext, err = pkcs7Padding(plaintext, 16)
block, err = aes.NewCipher(key.Key)
if err != nil {
return []byte{}, err
}
ciphertext = make([]byte, len(bPlaintext))
mode := cipher.NewCBCEncrypter(block, key.Iv)
mode.CryptBlocks(ciphertext, bPlaintext)
ciphertext = append(key.Iv, ciphertext...)
return ciphertext, nil
}
func (key AesKey) AesDecrypt(ciphertext []byte) ([]byte, error) {
var (
plaintext []byte
iv []byte
block cipher.Block
err error
)
iv = ciphertext[:aes.BlockSize]
plaintext = ciphertext[aes.BlockSize:]
block, err = aes.NewCipher(key.Key)
if err != nil {
return []byte{}, err
}
decMode := cipher.NewCBCDecrypter(block, iv)
decMode.CryptBlocks(plaintext, plaintext)
plaintext, err = pkcs7strip(plaintext, 16)
if err != nil {
return []byte{}, err
}
return plaintext, nil
}
// EncryptWithPublicKey encrypts data with public key
func EncryptWithPublicKey(msg []byte, pub *rsa.PublicKey) []byte {
var (
hash hash.Hash
)
hash = sha256.New()
ciphertext, err := rsa.EncryptOAEP(hash, rand.Reader, pub, msg, nil)
if err != nil {
panic(err)
}
return ciphertext
}
// DecryptWithPrivateKey decrypts data with private key
func decryptWithPrivateKey(ciphertext []byte, priv *rsa.PrivateKey) ([]byte, error) {
var (
hash hash.Hash
plaintext []byte
err error
)
hash = sha256.New()
plaintext, err = rsa.DecryptOAEP(hash, rand.Reader, priv, ciphertext, nil)
if err != nil {
return plaintext, err
}
return plaintext, nil
}

BIN
Backend/Database/Seeder/profile_image_enc.dat View File


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

@ -0,0 +1,59 @@
package Database
import (
"time"
"github.com/gofrs/uuid"
"gorm.io/gorm/clause"
)
func (s Session) IsExpired() bool {
return s.Expiry.Before(time.Now())
}
type Session struct {
Base
UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;"`
User User
Expiry time.Time
}
// GetSessionByID Gets session
func GetSessionByID(id string) (Session, error) {
var (
session Session
err error
)
err = DB.Preload(clause.Associations).
First(&session, "id = ?", id).
Error
return session, err
}
// CreateSession creates session
func (session *Session) CreateSession() error {
var (
err error
)
err = DB.Create(session).Error
return err
}
// DeleteSession deletes session
func (session *Session) DeleteSession() error {
return DB.Delete(session).Error
}
// DeleteSessionByID deletes session
func DeleteSessionByID(id string) error {
return DB.Delete(
&Session{},
"id = ?",
id,
).Error
}

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

@ -0,0 +1,112 @@
package Database
import (
"time"
"github.com/gofrs/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// UserConversation Used to link the current user to their conversations
type UserConversation struct {
Base
UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"`
User User ` json:"user"`
ConversationDetailID string `gorm:"not null" json:"conversation_detail_id"` // Stored encrypted
Admin string `gorm:"not null" json:"admin"` // Bool if user is admin of thread, stored encrypted
SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted
CreatedAt time.Time `gorm:"not null" json:"created_at"`
}
type UserConversationList []UserConversation
func GetUserConversationById(id string) (UserConversation, error) {
var (
message UserConversation
err error
)
err = DB.First(&message, "id = ?", id).
Error
return message, err
}
func GetUserConversationsByUserId(id string, page int) ([]UserConversation, error) {
var (
conversations []UserConversation
offset int
err error
)
offset = page * PageSize
err = DB.Offset(offset).
Limit(PageSize).
Order("created_at DESC").
Find(&conversations, "user_id = ?", id).
Error
return conversations, err
}
func (userConversation *UserConversation) CreateUserConversation() error {
var err error
err = DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(userConversation).
Error
return err
}
func (userConversations *UserConversationList) CreateUserConversations() error {
var err error
err = DB.Create(userConversations).
Error
return err
}
func (userConversation *UserConversation) UpdateUserConversation() error {
var err error
err = DB.Model(UserConversation{}).
Updates(userConversation).
Error
return err
}
func (userConversations *UserConversationList) UpdateUserConversations() error {
var err error
err = DB.Model(UserConversation{}).
Updates(userConversations).
Error
return err
}
func (userConversations *UserConversationList) UpdateOrCreateUserConversations() error {
var err error
err = DB.Model(UserConversation{}).
Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.AssignmentColumns([]string{"admin"}),
}).
Create(userConversations).
Error
return err
}
func (userConversation *UserConversation) DeleteUserConversation() error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(userConversation).
Error
}

+ 123
- 0
Backend/Database/Users.go View File

@ -0,0 +1,123 @@
package Database
import (
"errors"
"github.com/gofrs/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// 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")
}
if !tx.Statement.Changed("Email") {
tx.Statement.Omit("Email")
}
return nil
}
// User holds user data
type User struct {
Base
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', 'no_expiry')"` // Stored encrypted
}
func GetUserById(id string) (User, error) {
var (
user User
err error
)
err = DB.Preload(clause.Associations).
First(&user, "id = ?", id).
Error
return user, err
}
func GetUserByUsername(username string) (User, error) {
var (
user User
err error
)
err = DB.Preload(clause.Associations).
First(&user, "username = ?", username).
Error
return user, err
}
func CheckUniqueUsername(username string) error {
var (
exists bool
err error
)
err = DB.Model(User{}).
Select("count(*) > 0").
Where("username = ?", username).
Find(&exists).
Error
if err != nil {
return err
}
if exists {
return errors.New("Invalid username")
}
return nil
}
func (user *User) CreateUser() error {
var err error
err = DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(user).
Error
return err
}
func (user *User) UpdateUser() error {
var err error
err = DB.Model(&user).
Omit("id").
Where("id = ?", user.ID.String()).
Updates(user).
Error
if err != nil {
return err
}
err = DB.Model(User{}).
Where("id = ?", user.ID.String()).
First(user).
Error
return err
}
func (user *User) DeleteUser() error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(user).
Error
}

+ 14
- 0
Backend/Dockerfile View File

@ -0,0 +1,14 @@
FROM golang:1.19-alpine
RUN mkdir -p /go/src/git.tovijaeschke.xyz/Envelope/Backend
COPY ./ /go/src/git.tovijaeschke.xyz/Envelope/Backend
WORKDIR /go/src/git.tovijaeschke.xyz/Envelope/Backend
# For "go test" and development
RUN apk add gcc libc-dev inotify-tools
RUN go mod download
CMD [ "sh", "./dev.sh" ]

+ 16
- 0
Backend/Dockerfile.prod View File

@ -0,0 +1,16 @@
FROM golang:1.19-alpine
RUN mkdir -p /go/src/git.tovijaeschke.xyz/Envelope/Backend
COPY ./ /go/src/git.tovijaeschke.xyz/Envelope/Backend
WORKDIR /go/src/git.tovijaeschke.xyz/Envelope/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" ]

+ 68
- 0
Backend/Service/SendNotification.go View File

@ -0,0 +1,68 @@
package Service
import (
"context"
"os"
firebase "firebase.google.com/go"
"firebase.google.com/go/messaging"
"github.com/joho/godotenv"
"google.golang.org/api/option"
)
var (
ctx context.Context
fcmClient *messaging.Client
)
func init() {
var (
fireBaseAuthKey string
opts []option.ClientOption
app *firebase.App
err error
)
err = godotenv.Load()
if err != nil {
panic(err)
}
fireBaseAuthKey = os.Getenv("FIREBASE_AUTH_KEY")
opts = []option.ClientOption{
option.WithCredentialsFile(fireBaseAuthKey),
}
ctx = context.TODO()
app, err = firebase.NewApp(ctx, nil, opts...)
if err != nil {
panic(err)
}
fcmClient, err = app.Messaging(ctx)
if err != nil {
panic(err)
}
}
func SendNotification(tokens []string, title string, data map[string]string) error {
var (
message messaging.MulticastMessage
err error
)
message = messaging.MulticastMessage{
Notification: &messaging.Notification{
Title: title,
},
Data: data,
Tokens: tokens,
}
_, err = fcmClient.SendMulticast(context.TODO(), &message)
return err
}

+ 91
- 0
Backend/Tests/Init.go View File

@ -0,0 +1,91 @@
package Tests
import (
"encoding/base64"
"io/ioutil"
"log"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"net/url"
"time"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Api"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database/Seeder"
"github.com/gorilla/mux"
)
func InitTestCreateUser(username string) (Database.User, error) {
userKey, err := Seeder.GenerateAesKey()
if err != nil {
return Database.User{}, err
}
pubKey := Seeder.GetPubKey()
p, _ := Auth.HashPassword("password")
u := Database.User{
Username: username,
Password: p,
AsymmetricPublicKey: Seeder.PublicKey,
AsymmetricPrivateKey: Seeder.EncryptedPrivateKey,
SymmetricKey: base64.StdEncoding.EncodeToString(
Seeder.EncryptWithPublicKey(userKey.Key, pubKey),
),
}
err = (&u).CreateUser()
return u, err
}
// InitTestEnv initializes the test environment
// client is used for making authenticated requests
// ts is the testing server
// err, in case it fails ¯\_(ツ)_/¯
func InitTestEnv() (*http.Client, *httptest.Server, error) {
log.SetOutput(ioutil.Discard)
Database.InitTest()
r := mux.NewRouter()
Api.InitAPIEndpoints(r)
ts := httptest.NewServer(r)
u, err := InitTestCreateUser("test")
if err != nil {
return http.DefaultClient, ts, err
}
session := Database.Session{
UserID: u.ID,
Expiry: time.Now().Add(12 * time.Hour),
}
err = (&session).CreateSession()
if err != nil {
return http.DefaultClient, ts, err
}
jar, err := cookiejar.New(nil)
url, _ := url.Parse(ts.URL)
jar.SetCookies(
url,
[]*http.Cookie{
{
Name: "session_token",
Value: session.ID.String(),
MaxAge: 300,
},
},
)
client := &http.Client{
Jar: jar,
}
return client, ts, err
}

+ 21
- 0
Backend/Util/Bytes.go View File

@ -0,0 +1,21 @@
package Util
import (
"bytes"
"encoding/gob"
)
func ToBytes(key interface{}) ([]byte, error) {
var (
buf bytes.Buffer
enc *gob.Encoder
err error
)
enc = gob.NewEncoder(&buf)
err = enc.Encode(key)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}

+ 39
- 0
Backend/Util/Files.go View File

@ -0,0 +1,39 @@
package Util
import (
"fmt"
"os"
)
// WriteFile to disk
func WriteFile(contents []byte) (string, error) {
var (
fileName string
filePath string
f *os.File
err error
)
fileName = RandomString(32)
filePath = fmt.Sprintf(
"./attachments/%s",
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
}

+ 27
- 0
Backend/Util/Strings.go View File

@ -0,0 +1,27 @@
package Util
import (
"math/rand"
"time"
)
var (
letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
)
func init() {
rand.Seed(time.Now().UnixNano())
}
// RandomString generates a random string
func RandomString(n int) string {
var (
b []rune
i int
)
b = make([]rune, n)
for i = range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(b)
}

+ 50
- 0
Backend/Util/UserHelper.go View File

@ -0,0 +1,50 @@
package Util
import (
"errors"
"log"
"net/http"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"github.com/gorilla/mux"
)
func GetUserId(r *http.Request) (string, error) {
var (
urlVars map[string]string
id string
ok bool
)
urlVars = mux.Vars(r)
id, ok = urlVars["userID"]
if !ok {
return id, errors.New("Could not get id")
}
return id, nil
}
func GetUserById(w http.ResponseWriter, r *http.Request) (Database.User, error) {
var (
postData Database.User
id string
err error
)
id, err = GetUserId(r)
if err != nil {
log.Printf("Error encountered getting id\n")
http.Error(w, "Error", http.StatusInternalServerError)
return postData, err
}
postData, err = Database.GetUserById(id)
if err != nil {
log.Printf("Could not find user with id %s\n", id)
http.Error(w, "Error", http.StatusInternalServerError)
return postData, err
}
return postData, nil
}

+ 0
- 0
Backend/assets/.gitkeep View File


+ 0
- 0
Backend/attachments/.gitkeep View File


+ 8
- 0
Backend/dev.sh View File

@ -0,0 +1,8 @@
#!/bin/sh
while true; do
go build main.go
./main &
PID=$!
inotifywait --exclude 'attachments|main|/\..+' -r -e modify .
kill $PID
done

+ 51
- 0
Backend/go.mod View File

@ -0,0 +1,51 @@
module git.tovijaeschke.xyz/tovi/Envelope/Backend
go 1.18
require (
github.com/gofrs/uuid v4.2.0+incompatible
github.com/gorilla/mux v1.8.0
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
gorm.io/driver/postgres v1.3.4
gorm.io/gorm v1.23.4
)
require (
cloud.google.com/go v0.104.0 // indirect
cloud.google.com/go/compute v1.12.1 // indirect
cloud.google.com/go/compute/metadata v0.2.1 // indirect
cloud.google.com/go/firestore v1.8.0 // indirect
cloud.google.com/go/iam v0.3.0 // indirect
cloud.google.com/go/storage v1.27.0 // indirect
firebase.google.com/go v3.13.0+incompatible // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
github.com/googleapis/gax-go/v2 v2.6.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.11.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.2.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.10.0 // indirect
github.com/jackc/pgx/v4 v4.15.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.4 // indirect
github.com/joho/godotenv v1.4.0 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/net v0.1.0 // indirect
golang.org/x/oauth2 v0.1.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/api v0.102.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e // indirect
google.golang.org/grpc v1.50.1 // indirect
google.golang.org/protobuf v1.28.1 // indirect
)

+ 763
- 0
Backend/go.sum View File

@ -0,0 +1,763 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
cloud.google.com/go v0.104.0 h1:gSmWO7DY1vOm0MVU6DNXM11BWHHsTUmsC5cv1fuW5X8=
cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0=
cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48=
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.8.0 h1:HokMB9Io0hAyYzlGFeFVMgE3iaPXNvaIsDx5JzblGLI=
cloud.google.com/go/firestore v1.8.0/go.mod h1:r3KB8cAdRIe8znzoPWLw8S6gpDVd9treohhn8b09424=
cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc=
cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.27.0 h1:YOO045NZI9RKfCj1c5A/ZtuuENUc8OAW+gHdGnDgyMQ=
cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4=
firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Kangaroux/go-map-schema v0.6.1 h1:jXpOzi7kNFC6M8QSvJuI7xeDxObBrVHwA3D6vSrxuG4=
github.com/Kangaroux/go-map-schema v0.6.1/go.mod h1:56jN+6h/N8Pmn5D+JL9gREOvZTlVEAvXtXyLd/NRjh4=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs=
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/googleapis/gax-go/v2 v2.6.0 h1:SXk3ABtQYDT/OH8jAyvEOQ58mgawq5C4o/4/89qN2ZU=
github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.11.0 h1:HiHArx4yFbwl91X3qqIHtUFoiIfLNJXCQRsnzkiwwaQ=
github.com/jackc/pgconn v1.11.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns=
github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38=
github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w=
github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.1.0 h1:isLCZuhj4v+tYv7eskaN4v/TM+A1begWWgyVJDdl1+Y=
golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
google.golang.org/api v0.102.0 h1:JxJl2qQ85fRMPNvlZY/enexbxpCjLwGhZUtgfGeQ51I=
google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e h1:S9GbmC1iCgvbLyAokVCwiO6tVIrU9Y7c5oMx1V/ki/Y=
google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.3.4 h1:evZ7plF+Bp+Lr1mO5NdPvd6M/N98XtwHixGB+y7fdEQ=
gorm.io/driver/postgres v1.3.4/go.mod h1:y0vEuInFKJtijuSGu9e5bs5hzzSzPK+LancpKpvbRBw=
gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.4 h1:1BKWM67O6CflSLcwGQR7ccfmC4ebOxQrTfOQGRE9wjg=
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

+ 68
- 0
Backend/main.go View File

@ -0,0 +1,68 @@
package main
import (
"flag"
"log"
"net/http"
"os"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Api"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database/Seeder"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Service"
"github.com/gorilla/mux"
"github.com/joho/godotenv"
)
var (
seed bool
sendNotification bool
)
func init() {
var err error
err = godotenv.Load()
if err != nil {
panic(err)
}
Database.Init()
flag.BoolVar(&seed, "seed", false, "Seed database for development")
flag.BoolVar(&sendNotification, "test-notify", false, "Send test notification")
flag.Parse()
}
func main() {
var (
router *mux.Router
err error
)
if seed && os.Getenv("GO_ENV") != "production" {
Seeder.Seed()
return
}
if sendNotification && os.Getenv("GO_ENV") != "production" {
Service.SendNotification(
[]string{"eoGWtGAOR3OON6uQTVwjwM:APA91bH68gggBHj1jm68xMY10LQBWVO1r5x0JH4An7dpq4nJJ1GjQw8EJVoKpkz8BSXvxFxU2p5azeO4HE0yUqmfJlCGVBwrBvi4ZmgiIMkg6LajsZuPu96gPblKIjpVnxL99AvQYFib"},
"Test Message",
make(map[string]string),
)
return
}
router = mux.NewRouter()
Api.InitAPIEndpoints(router)
log.Println("Listening on port :8080")
err = http.ListenAndServe(":8080", router)
if err != nil {
panic(err)
}
}

+ 13
- 1
README.md View File

@ -1,3 +1,15 @@
# Envelope
Encrypted messaging app
Encrypted messaging app
## TODO
- Add friends profile picture
- Add conversation pagination
- Add message pagination
- Finish off conversation settings page
- Finish message expiry
- Fix error when creating existing conversation between friends
- Sort conversation based on latest message
- Fix admin bool on conversation object frontend
- Fix image picker being patchy on iOS

+ 49
- 0
docker-compose.yml View File

@ -0,0 +1,49 @@
version: "3"
services:
server:
build:
context: ./Backend
ports:
- "8080:8080"
volumes:
- "./Backend:/go/src/git.tovijaeschke.xyz/Envelope/Backend"
links:
- postgres
- postgres-testing
depends_on:
postgres:
condition: service_healthy
depends_on:
postgres-testing:
condition: service_healthy
postgres:
image: postgres:14.5
ports:
- "54321:5432"
environment:
POSTGRES_DB: envelope
POSTGRES_PASSWORD: password
volumes:
- /var/lib/postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
postgres-testing:
image: postgres:14.5
ports:
- "54322:5432"
environment:
POSTGRES_DB: envelope-testing
POSTGRES_PASSWORD: password
tmpfs:
- /var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5

+ 2
- 0
mobile/.env.example View File

@ -0,0 +1,2 @@
SERVER_URL=http://localhost:8080/
ENVIRONMENT=development

+ 46
- 0
mobile/.gitignore View File

@ -0,0 +1,46 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

+ 10
- 0
mobile/.metadata View File

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: c860cba910319332564e1e9d470a17074c1f2dfd
channel: stable
project_type: app

+ 16
- 0
mobile/README.md View File

@ -0,0 +1,16 @@
# mobile
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
For help getting started with Flutter, view our
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

+ 30
- 0
mobile/analysis_options.yaml View File

@ -0,0 +1,30 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
prefer_single_quotes: true
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

+ 13
- 0
mobile/android/.gitignore View File

@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

+ 70
- 0
mobile/android/app/build.gradle View File

@ -0,0 +1,70 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 33
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
applicationId "com.envelope.envelope"
minSdkVersion 20
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation platform('com.google.firebase:firebase-bom:31.0.2')
}

+ 46
- 0
mobile/android/app/google-services.json View File

@ -0,0 +1,46 @@
{
"project_info": {
"project_number": "623758852972",
"project_id": "envelope-32eff",
"storage_bucket": "envelope-32eff.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:623758852972:android:af080c15b69efa00676949",
"android_client_info": {
"package_name": "com.envelope.envelope"
}
},
"oauth_client": [
{
"client_id": "623758852972-6ngjav8bc663ld3f04i6naoudlo88roa.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCwGtPho7BUI9AF8In216bDXU87cVpgdu0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "623758852972-6ngjav8bc663ld3f04i6naoudlo88roa.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "623758852972-7jdesrmtt4ccc7qvhpfpstdesf8rto2b.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.envelope.envelope"
}
}
]
}
}
}
],
"configuration_version": "1"
}

+ 7
- 0
mobile/android/app/src/debug/AndroidManifest.xml View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.envelope.envelope">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

+ 35
- 0
mobile/android/app/src/main/AndroidManifest.xml View File

@ -0,0 +1,35 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.envelope.envelope">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:label="Envelope"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>

+ 25
- 0
mobile/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java View File

@ -0,0 +1,25 @@
// Generated file.
//
// If you wish to remove Flutter's multidex support, delete this entire file.
//
// Modifications to this file should be done in a copy under a different name
// as this file may be regenerated.
package io.flutter.app;
import android.app.Application;
import android.content.Context;
import androidx.annotation.CallSuper;
import androidx.multidex.MultiDex;
/**
* Extension of {@link android.app.Application}, adding multidex support.
*/
public class FlutterMultiDexApplication extends Application {
@Override
@CallSuper
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}

+ 6
- 0
mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt View File

@ -0,0 +1,6 @@
package com.envelope.envelope
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

+ 12
- 0
mobile/android/app/src/main/res/drawable-v21/launch_background.xml View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

+ 12
- 0
mobile/android/app/src/main/res/drawable/launch_background.xml View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

BIN
mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png View File

Before After
Width: 72  |  Height: 72  |  Size: 2.7 KiB

BIN
mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png View File

Before After
Width: 48  |  Height: 48  |  Size: 2.3 KiB

BIN
mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png View File

Before After
Width: 96  |  Height: 96  |  Size: 3.8 KiB

BIN
mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png View File

Before After
Width: 144  |  Height: 144  |  Size: 5.1 KiB

BIN
mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png View File

Before After
Width: 192  |  Height: 192  |  Size: 6.5 KiB

+ 18
- 0
mobile/android/app/src/main/res/values-night/styles.xml View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

+ 18
- 0
mobile/android/app/src/main/res/values/styles.xml View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

+ 7
- 0
mobile/android/app/src/profile/AndroidManifest.xml View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.envelope.envelope">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

+ 32
- 0
mobile/android/build.gradle View File

@ -0,0 +1,32 @@
buildscript {
ext.kotlin_version = '1.6.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.gms:google-services:4.3.13'
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}

+ 3
- 0
mobile/android/gradle.properties View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save