feat(accounts)_: cherry-pick Persist acceptance of Terms of Use & Privacy policy (#5766) (#5977)

* feat(accounts)_: Persist acceptance of Terms of Use & Privacy policy (#5766)

The original GH issue https://github.com/status-im/status-mobile/issues/21113
came from a request from the Legal team. We must show to Status v1 users the new
terms (Terms of Use & Privacy Policy) right after they upgrade to Status v2
from the stores.

The solution we use is to create a flag in the accounts table, named
hasAcceptedTerms. The flag will be set to true on the first account ever
created in v2 and we provide a native call in mobile/status.go#AcceptTerms,
which allows the client to persist the user's choice in case they are upgrading
(from v1 -> v2, or from a v2 older than this PR).

This solution is not the best because we should store the setting in a separate
table, not in the accounts table.

Related Mobile PR https://github.com/status-im/status-mobile/pull/21124

* fix(test)_: Compare addresses using uppercased strings

---------

Co-authored-by: Icaro Motta <icaro.ldm@gmail.com>
This commit is contained in:
Jonathan Rainville 2024-10-23 09:53:25 -04:00 committed by GitHub
parent 8dc70e91ea
commit 132ea05fc8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 255 additions and 10 deletions

View File

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

View File

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

View File

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

View File

@ -443,6 +443,15 @@ func createAccountAndLogin(requestJSON string) string {
return makeJSONResponse(nil)
}
func AcceptTerms() string {
return callWithResponse(acceptTerms)
}
func acceptTerms() string {
err := statusBackend.AcceptTerms()
return makeJSONResponse(err)
}
func LoginAccount(requestJSON string) string {
return callWithResponse(loginAccount, requestJSON)
}

View File

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

View File

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

View File

@ -0,0 +1 @@
ALTER TABLE accounts ADD COLUMN hasAcceptedTerms BOOLEAN NOT NULL DEFAULT FALSE;