diff --git a/api/backend_test.go b/api/backend_test.go index 3a40cee0e40..b65845b5f9f 100644 --- a/api/backend_test.go +++ b/api/backend_test.go @@ -855,6 +855,7 @@ func TestLoginAccount(t *testing.T) { acc, err := b.CreateAccountAndLogin(createAccountRequest) require.NoError(t, err) require.Equal(t, nameserver, b.config.WakuV2Config.Nameserver) + require.True(t, acc.HasAcceptedTerms) waitForLogin(c) require.NoError(t, b.Logout()) @@ -1684,6 +1685,30 @@ func TestRestoreAccountAndLoginWithoutDisplayName(t *testing.T) { require.NotEmpty(t, account.Name) } +func TestAcceptTerms(t *testing.T) { + tmpdir := t.TempDir() + b := NewGethStatusBackend() + conf, err := params.NewNodeConfig(tmpdir, 1777) + require.NoError(t, err) + require.NoError(t, b.AccountManager().InitKeystore(conf.KeyStoreDir)) + b.UpdateRootDataDir(conf.DataDir) + require.NoError(t, b.OpenAccounts()) + nameserver := "8.8.8.8" + createAccountRequest := &requests.CreateAccount{ + DisplayName: "some-display-name", + CustomizationColor: "#ffffff", + Password: "some-password", + RootDataDir: tmpdir, + LogFilePath: tmpdir + "/log", + WakuV2Nameserver: &nameserver, + WakuV2Fleet: "status.staging", + } + _, err = b.CreateAccountAndLogin(createAccountRequest) + require.NoError(t, err) + err = b.AcceptTerms() + require.NoError(t, err) +} + func TestCreateAccountPathsValidation(t *testing.T) { tmpdir := t.TempDir() diff --git a/api/geth_backend.go b/api/geth_backend.go index 1be579c3640..c76d2b5eb5a 100644 --- a/api/geth_backend.go +++ b/api/geth_backend.go @@ -238,6 +238,24 @@ func (b *GethStatusBackend) GetAccounts() ([]multiaccounts.Account, error) { return b.multiaccountsDB.GetAccounts() } +func (b *GethStatusBackend) AcceptTerms() error { + b.mu.Lock() + defer b.mu.Unlock() + if b.multiaccountsDB == nil { + return errors.New("accounts db wasn't initialized") + } + + accounts, err := b.multiaccountsDB.GetAccounts() + if err != nil { + return err + } + if len(accounts) == 0 { + return errors.New("accounts is empty") + } + + return b.multiaccountsDB.UpdateHasAcceptedTerms(accounts[0].KeyUID, true) +} + func (b *GethStatusBackend) getAccountByKeyUID(keyUID string) (*multiaccounts.Account, error) { b.mu.Lock() defer b.mu.Unlock() @@ -1580,6 +1598,14 @@ func (b *GethStatusBackend) buildAccount(request *requests.CreateAccount, input acc.KDFIterations = dbsetup.ReducedKDFIterationsNumber } + count, err := b.multiaccountsDB.GetAccountsCount() + if err != nil { + return nil, err + } + if count == 0 { + acc.HasAcceptedTerms = true + } + if request.ImagePath != "" { imageCropRectangle := request.ImageCropRectangle if imageCropRectangle == nil { diff --git a/api/old_mobile_user_upgrading_from_v1_to_v2_test.go b/api/old_mobile_user_upgrading_from_v1_to_v2_test.go index 58eb52acf99..8f651df7d3e 100644 --- a/api/old_mobile_user_upgrading_from_v1_to_v2_test.go +++ b/api/old_mobile_user_upgrading_from_v1_to_v2_test.go @@ -141,6 +141,11 @@ func (s *OldMobileUserUpgradingFromV1ToV2Test) TestLoginAndMigrationsStillWorkWi s.Require().True(len(keyKps[0].Accounts) == 1) info, err = generator.LoadAccount(keyKps[0].Accounts[0].Address.Hex(), oldMobileUserPasswd) s.Require().NoError(err) + + // The user should manually accept terms, so we make sure we don't set it + // automatically by mistake. + s.Require().False(info.ToMultiAccount().HasAcceptedTerms) + s.Require().Equal(keyKps[0].KeyUID, info.KeyUID) s.Require().Equal(keyKps[0].Accounts[0].KeyUID, info.KeyUID) info, err = generator.ImportPrivateKey("c3ad0b50652318f845565c13761e5369ce75dcbc2a94616e15b829d4b07410fe") diff --git a/mobile/status.go b/mobile/status.go index a121d85b4df..1f9dc478e41 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -443,6 +443,11 @@ func createAccountAndLogin(requestJSON string) string { return makeJSONResponse(nil) } +func AcceptTerms() string { + err := statusBackend.AcceptTerms() + return makeJSONResponse(err) +} + func LoginAccount(requestJSON string) string { return callWithResponse(loginAccount, requestJSON) } diff --git a/multiaccounts/database.go b/multiaccounts/database.go index f3b427409f1..799acda1f80 100644 --- a/multiaccounts/database.go +++ b/multiaccounts/database.go @@ -29,6 +29,9 @@ type Account struct { Images []images.IdentityImage `json:"images"` KDFIterations int `json:"kdfIterations,omitempty"` CustomizationColorClock uint64 `json:"-"` + + // HasAcceptedTerms will be set to true when the first account is created. + HasAcceptedTerms bool `json:"hasAcceptedTerms"` } func (a *Account) RefersToKeycard() bool { @@ -145,7 +148,7 @@ func (db *Database) GetAccountKDFIterationsNumber(keyUID string) (kdfIterationsN } func (db *Database) GetAccounts() (rst []Account, err error) { - rows, err := db.db.Query("SELECT a.name, a.loginTimestamp, a.identicon, a.colorHash, a.colorId, a.customizationColor, a.customizationColorClock, a.keycardPairing, a.keyUid, a.kdfIterations, ii.name, ii.image_payload, ii.width, ii.height, ii.file_size, ii.resize_target, ii.clock FROM accounts AS a LEFT JOIN identity_images AS ii ON ii.key_uid = a.keyUid ORDER BY loginTimestamp DESC") + rows, err := db.db.Query("SELECT a.name, a.loginTimestamp, a.identicon, a.colorHash, a.colorId, a.customizationColor, a.customizationColorClock, a.keycardPairing, a.keyUid, a.kdfIterations, a.hasAcceptedTerms, ii.name, ii.image_payload, ii.width, ii.height, ii.file_size, ii.resize_target, ii.clock FROM accounts AS a LEFT JOIN identity_images AS ii ON ii.key_uid = a.keyUid ORDER BY loginTimestamp DESC") if err != nil { return nil, err } @@ -179,6 +182,7 @@ func (db *Database) GetAccounts() (rst []Account, err error) { &acc.KeycardPairing, &acc.KeyUID, &acc.KDFIterations, + &acc.HasAcceptedTerms, &iiName, &ii.Payload, &iiWidth, @@ -236,8 +240,14 @@ func (db *Database) GetAccounts() (rst []Account, err error) { return rst, nil } +func (db *Database) GetAccountsCount() (int, error) { + var count int + err := db.db.QueryRow("SELECT COUNT(1) FROM accounts").Scan(&count) + return count, err +} + func (db *Database) GetAccount(keyUID string) (*Account, error) { - rows, err := db.db.Query("SELECT a.name, a.loginTimestamp, a.identicon, a.colorHash, a.colorId, a.customizationColor, a.customizationColorClock, a.keycardPairing, a.keyUid, a.kdfIterations, ii.key_uid, ii.name, ii.image_payload, ii.width, ii.height, ii.file_size, ii.resize_target, ii.clock FROM accounts AS a LEFT JOIN identity_images AS ii ON ii.key_uid = a.keyUid WHERE a.keyUid = ? ORDER BY loginTimestamp DESC", keyUID) + rows, err := db.db.Query("SELECT a.name, a.loginTimestamp, a.identicon, a.colorHash, a.colorId, a.customizationColor, a.customizationColorClock, a.keycardPairing, a.keyUid, a.kdfIterations, a.hasAcceptedTerms, ii.key_uid, ii.name, ii.image_payload, ii.width, ii.height, ii.file_size, ii.resize_target, ii.clock FROM accounts AS a LEFT JOIN identity_images AS ii ON ii.key_uid = a.keyUid WHERE a.keyUid = ? ORDER BY loginTimestamp DESC", keyUID) if err != nil { return nil, err } @@ -273,6 +283,7 @@ func (db *Database) GetAccount(keyUID string) (*Account, error) { &acc.KeycardPairing, &acc.KeyUID, &acc.KDFIterations, + &acc.HasAcceptedTerms, &iiKeyUID, &iiName, &ii.Payload, @@ -323,7 +334,7 @@ func (db *Database) SaveAccount(account Account) error { account.KDFIterations = dbsetup.ReducedKDFIterationsNumber } - _, err = db.db.Exec("INSERT OR REPLACE INTO accounts (name, identicon, colorHash, colorId, customizationColor, customizationColorClock, keycardPairing, keyUid, kdfIterations, loginTimestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", account.Name, account.Identicon, colorHash, account.ColorID, account.CustomizationColor, account.CustomizationColorClock, account.KeycardPairing, account.KeyUID, account.KDFIterations, account.Timestamp) + _, err = db.db.Exec("INSERT OR REPLACE INTO accounts (name, identicon, colorHash, colorId, customizationColor, customizationColorClock, keycardPairing, keyUid, kdfIterations, loginTimestamp, hasAcceptedTerms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", account.Name, account.Identicon, colorHash, account.ColorID, account.CustomizationColor, account.CustomizationColorClock, account.KeycardPairing, account.KeyUID, account.KDFIterations, account.Timestamp, account.HasAcceptedTerms) if err != nil { return err } @@ -340,6 +351,11 @@ func (db *Database) UpdateDisplayName(keyUID string, displayName string) error { return err } +func (db *Database) UpdateHasAcceptedTerms(keyUID string, hasAcceptedTerms bool) error { + _, err := db.db.Exec("UPDATE accounts SET hasAcceptedTerms = ? WHERE keyUid = ?", hasAcceptedTerms, keyUID) + return err +} + func (db *Database) UpdateAccount(account Account) error { colorHash, err := json.Marshal(account.ColorHash) if err != nil { @@ -350,7 +366,7 @@ func (db *Database) UpdateAccount(account Account) error { account.KDFIterations = dbsetup.ReducedKDFIterationsNumber } - _, err = db.db.Exec("UPDATE accounts SET name = ?, identicon = ?, colorHash = ?, colorId = ?, customizationColor = ?, customizationColorClock = ?, keycardPairing = ?, kdfIterations = ? WHERE keyUid = ?", account.Name, account.Identicon, colorHash, account.ColorID, account.CustomizationColor, account.CustomizationColorClock, account.KeycardPairing, account.KDFIterations, account.KeyUID) + _, err = db.db.Exec("UPDATE accounts SET name = ?, identicon = ?, colorHash = ?, colorId = ?, customizationColor = ?, customizationColorClock = ?, keycardPairing = ?, kdfIterations = ?, hasAcceptedTerms = ? WHERE keyUid = ?", account.Name, account.Identicon, colorHash, account.ColorID, account.CustomizationColor, account.CustomizationColorClock, account.KeycardPairing, account.KDFIterations, account.HasAcceptedTerms, account.KeyUID) return err } diff --git a/multiaccounts/database_test.go b/multiaccounts/database_test.go index a4cdce33dba..a6647ea6c0a 100644 --- a/multiaccounts/database_test.go +++ b/multiaccounts/database_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "io/ioutil" "os" + "strings" "testing" "github.com/status-im/status-go/common/dbsetup" @@ -39,10 +40,17 @@ func TestAccounts(t *testing.T) { func TestAccountsUpdate(t *testing.T) { db, stop := setupTestDB(t) defer stop() - expected := Account{KeyUID: "string", CustomizationColor: common.CustomizationColorBlue, ColorHash: ColorHash{{4, 3}, {4, 0}, {4, 3}, {4, 0}}, ColorID: 10, KDFIterations: dbsetup.ReducedKDFIterationsNumber} + expected := Account{ + KeyUID: "string", + CustomizationColor: common.CustomizationColorBlue, + ColorHash: ColorHash{{4, 3}, {4, 0}, {4, 3}, {4, 0}}, + ColorID: 10, + KDFIterations: dbsetup.ReducedKDFIterationsNumber, + } require.NoError(t, db.SaveAccount(expected)) expected.Name = "chars" expected.CustomizationColor = common.CustomizationColorMagenta + expected.HasAcceptedTerms = true require.NoError(t, db.UpdateAccount(expected)) rst, err := db.GetAccounts() require.NoError(t, err) @@ -50,6 +58,53 @@ func TestAccountsUpdate(t *testing.T) { require.Equal(t, expected, rst[0]) } +func TestUpdateHasAcceptedTerms(t *testing.T) { + db, stop := setupTestDB(t) + defer stop() + keyUID := "string" + expected := Account{ + KeyUID: keyUID, + KDFIterations: dbsetup.ReducedKDFIterationsNumber, + } + require.NoError(t, db.SaveAccount(expected)) + accounts, err := db.GetAccounts() + require.NoError(t, err) + require.Equal(t, []Account{expected}, accounts) + + // Update from false -> true + require.NoError(t, db.UpdateHasAcceptedTerms(keyUID, true)) + account, err := db.GetAccount(keyUID) + require.NoError(t, err) + expected.HasAcceptedTerms = true + require.Equal(t, &expected, account) + + // Update from true -> false + require.NoError(t, db.UpdateHasAcceptedTerms(keyUID, false)) + account, err = db.GetAccount(keyUID) + require.NoError(t, err) + expected.HasAcceptedTerms = false + require.Equal(t, &expected, account) +} + +func TestDatabase_GetAccountsCount(t *testing.T) { + db, stop := setupTestDB(t) + defer stop() + + count, err := db.GetAccountsCount() + require.NoError(t, err) + require.Equal(t, 0, count) + + account := Account{ + KeyUID: keyUID, + KDFIterations: dbsetup.ReducedKDFIterationsNumber, + } + require.NoError(t, db.SaveAccount(account)) + + count, err = db.GetAccountsCount() + require.NoError(t, err) + require.Equal(t, 1, count) +} + func TestLoginUpdate(t *testing.T) { db, stop := setupTestDB(t) defer stop() @@ -148,20 +203,26 @@ func TestDatabase_DeleteIdentityImage(t *testing.T) { require.Empty(t, oii) } +func removeAllWhitespace(s string) string { + tmp := strings.ReplaceAll(s, " ", "") + tmp = strings.ReplaceAll(tmp, "\n", "") + tmp = strings.ReplaceAll(tmp, "\t", "") + return tmp +} + func TestDatabase_GetAccountsWithIdentityImages(t *testing.T) { db, stop := setupTestDB(t) defer stop() testAccs := []Account{ - {Name: "string", KeyUID: keyUID, Identicon: "data"}, + {Name: "string", KeyUID: keyUID, Identicon: "data", HasAcceptedTerms: true}, {Name: "string", KeyUID: keyUID2}, {Name: "string", KeyUID: keyUID2 + "2"}, {Name: "string", KeyUID: keyUID2 + "3"}, } - expected := `[{"name":"string","timestamp":100,"identicon":"data","colorHash":null,"colorId":0,"keycard-pairing":"","key-uid":"0xdeadbeef","images":[{"keyUid":"0xdeadbeef","type":"large","uri":"data:image/png;base64,iVBORw0KGgoAAAANSUg=","width":240,"height":300,"fileSize":1024,"resizeTarget":240,"clock":0},{"keyUid":"0xdeadbeef","type":"thumbnail","uri":"data:image/jpeg;base64,/9j/2wCEAFA3PEY8MlA=","width":80,"height":80,"fileSize":256,"resizeTarget":80,"clock":0}],"kdfIterations":3200},{"name":"string","timestamp":10,"identicon":"","colorHash":null,"colorId":0,"keycard-pairing":"","key-uid":"0x1337beef","images":null,"kdfIterations":3200},{"name":"string","timestamp":0,"identicon":"","colorHash":null,"colorId":0,"keycard-pairing":"","key-uid":"0x1337beef2","images":null,"kdfIterations":3200},{"name":"string","timestamp":0,"identicon":"","colorHash":null,"colorId":0,"keycard-pairing":"","key-uid":"0x1337beef3","images":[{"keyUid":"0x1337beef3","type":"large","uri":"data:image/png;base64,iVBORw0KGgoAAAANSUg=","width":240,"height":300,"fileSize":1024,"resizeTarget":240,"clock":0},{"keyUid":"0x1337beef3","type":"thumbnail","uri":"data:image/jpeg;base64,/9j/2wCEAFA3PEY8MlA=","width":80,"height":80,"fileSize":256,"resizeTarget":80,"clock":0}],"kdfIterations":3200}]` for _, a := range testAccs { - require.NoError(t, db.SaveAccount(a)) + require.NoError(t, db.SaveAccount(a), a.KeyUID) } seedTestDBWithIdentityImages(t, db, keyUID) @@ -178,14 +239,116 @@ func TestDatabase_GetAccountsWithIdentityImages(t *testing.T) { accJSON, err := json.Marshal(accs) require.NoError(t, err) - require.Exactly(t, expected, string(accJSON)) + expected := ` +[ + { + "name": "string", + "timestamp": 100, + "identicon": "data", + "colorHash": null, + "colorId": 0, + "keycard-pairing": "", + "key-uid": "0xdeadbeef", + "images": [ + { + "keyUid": "0xdeadbeef", + "type": "large", + "uri": "data:image/png;base64,iVBORw0KGgoAAAANSUg=", + "width": 240, + "height": 300, + "fileSize": 1024, + "resizeTarget": 240, + "clock": 0 + }, + { + "keyUid": "0xdeadbeef", + "type": "thumbnail", + "uri": "data:image/jpeg;base64,/9j/2wCEAFA3PEY8MlA=", + "width": 80, + "height": 80, + "fileSize": 256, + "resizeTarget": 80, + "clock": 0 + } + ], + "kdfIterations": 3200, + "hasAcceptedTerms": true + }, + { + "name": "string", + "timestamp": 10, + "identicon": "", + "colorHash": null, + "colorId": 0, + "keycard-pairing": "", + "key-uid": "0x1337beef", + "images": null, + "kdfIterations": 3200, + "hasAcceptedTerms": false + }, + { + "name": "string", + "timestamp": 0, + "identicon": "", + "colorHash": null, + "colorId": 0, + "keycard-pairing": "", + "key-uid": "0x1337beef2", + "images": null, + "kdfIterations": 3200, + "hasAcceptedTerms": false + }, + { + "name": "string", + "timestamp": 0, + "identicon": "", + "colorHash": null, + "colorId": 0, + "keycard-pairing": "", + "key-uid": "0x1337beef3", + "images": [ + { + "keyUid": "0x1337beef3", + "type": "large", + "uri": "data:image/png;base64,iVBORw0KGgoAAAANSUg=", + "width": 240, + "height": 300, + "fileSize": 1024, + "resizeTarget": 240, + "clock": 0 + }, + { + "keyUid": "0x1337beef3", + "type": "thumbnail", + "uri": "data:image/jpeg;base64,/9j/2wCEAFA3PEY8MlA=", + "width": 80, + "height": 80, + "fileSize": 256, + "resizeTarget": 80, + "clock": 0 + } + ], + "kdfIterations": 3200, + "hasAcceptedTerms": false + } +] +` + + require.Exactly(t, removeAllWhitespace(expected), string(accJSON)) } func TestDatabase_GetAccount(t *testing.T) { db, stop := setupTestDB(t) defer stop() - expected := Account{Name: "string", KeyUID: keyUID, ColorHash: ColorHash{{4, 3}, {4, 0}, {4, 3}, {4, 0}}, ColorID: 10, KDFIterations: dbsetup.ReducedKDFIterationsNumber} + expected := Account{ + Name: "string", + KeyUID: keyUID, + ColorHash: ColorHash{{4, 3}, {4, 0}, {4, 3}, {4, 0}}, + ColorID: 10, + KDFIterations: dbsetup.ReducedKDFIterationsNumber, + HasAcceptedTerms: true, + } require.NoError(t, db.SaveAccount(expected)) account, err := db.GetAccount(expected.KeyUID) diff --git a/multiaccounts/migrations/sql/1724407149_add_has_accepted_terms_to_accounts.up.sql b/multiaccounts/migrations/sql/1724407149_add_has_accepted_terms_to_accounts.up.sql new file mode 100644 index 00000000000..d346f880297 --- /dev/null +++ b/multiaccounts/migrations/sql/1724407149_add_has_accepted_terms_to_accounts.up.sql @@ -0,0 +1 @@ +ALTER TABLE accounts ADD COLUMN hasAcceptedTerms BOOLEAN NOT NULL DEFAULT FALSE;