diff --git a/common/db.go b/common/db.go index a5cf1c1..5e2fc4c 100644 --- a/common/db.go +++ b/common/db.go @@ -7,7 +7,7 @@ import ( "git.defalsify.org/vise.git/logging" ) -// DataType is a subprefix value used in association with vise/db.DATATYPE_USERDATA. +// DataType is a subprefix value used in association with vise/db.DATATYPE_USERDATA. // // All keys are used only within the context of a single account. Unless otherwise specified, the user context is the session id. // @@ -55,6 +55,8 @@ const ( DATA_ACTIVE_DECIMAL // EVM address of the currently active voucher DATA_ACTIVE_ADDRESS + //Holds count of the number of incorrect PIN attempts + DATA_INCORRECT_PIN_ATTEMPTS ) const ( diff --git a/common/pin.go b/common/pin.go index 6db9d15..13f21b3 100644 --- a/common/pin.go +++ b/common/pin.go @@ -6,9 +6,13 @@ import ( "golang.org/x/crypto/bcrypt" ) -// Define the regex pattern as a constant const ( + // Define the regex pattern as a constant pinPattern = `^\d{4}$` + + //Allowed incorrect PIN attempts + AllowedPINAttempts = uint8(3) + ) // checks whether the given input is a 4 digit number diff --git a/internal/handlers/handlerservice.go b/internal/handlers/handlerservice.go index 1da28c3..0d49b0c 100644 --- a/internal/handlers/handlerservice.go +++ b/internal/handlers/handlerservice.go @@ -128,6 +128,7 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn ls.DbRs.AddLocalFunc("view_statement", ussdHandlers.ViewTransactionStatement) ls.DbRs.AddLocalFunc("update_all_profile_items", ussdHandlers.UpdateAllProfileItems) ls.DbRs.AddLocalFunc("set_back", ussdHandlers.SetBack) + ls.DbRs.AddLocalFunc("show_blocked_account", ussdHandlers.ShowBlockedAccount) return ussdHandlers, nil } diff --git a/internal/handlers/ussd/menuhandler.go b/internal/handlers/ussd/menuhandler.go index ff0656d..607b812 100644 --- a/internal/handlers/ussd/menuhandler.go +++ b/internal/handlers/ussd/menuhandler.go @@ -734,11 +734,23 @@ func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (res if h.st.MatchFlag(flag_account_authorized, false) { res.FlagReset = append(res.FlagReset, flag_incorrect_pin) res.FlagSet = append(res.FlagSet, flag_allow_update, flag_account_authorized) + err := h.resetIncorrectPINAttempts(ctx, sessionId) + if err != nil { + return res, err + } } else { res.FlagSet = append(res.FlagSet, flag_allow_update) res.FlagReset = append(res.FlagReset, flag_account_authorized) + err := h.resetIncorrectPINAttempts(ctx, sessionId) + if err != nil { + return res, err + } } } else { + err := h.incrementIncorrectPINAttempts(ctx, sessionId) + if err != nil { + return res, err + } res.FlagSet = append(res.FlagSet, flag_incorrect_pin) res.FlagReset = append(res.FlagReset, flag_account_authorized) return res, nil @@ -752,8 +764,34 @@ func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (res // ResetIncorrectPin resets the incorrect pin flag after a new PIN attempt. func (h *Handlers) ResetIncorrectPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result + store := h.userdataStore + flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") + flag_account_blocked, _ := h.flagManager.GetFlag("flag_account_blocked") + + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } + res.FlagReset = append(res.FlagReset, flag_incorrect_pin) + + currentWrongPinAttempts, err := store.ReadEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS) + if err != nil { + if !db.IsNotFound(err) { + return res, err + } + } + pinAttemptsValue, _ := strconv.ParseUint(string(currentWrongPinAttempts), 0, 64) + remainingPINAttempts := common.AllowedPINAttempts - uint8(pinAttemptsValue) + if remainingPINAttempts == 0 { + res.FlagSet = append(res.FlagSet, flag_account_blocked) + return res, nil + } + if remainingPINAttempts < common.AllowedPINAttempts { + res.Content = strconv.Itoa(int(remainingPINAttempts)) + } + return res, nil } @@ -840,6 +878,16 @@ func (h *Handlers) QuitWithHelp(ctx context.Context, sym string, input []byte) ( return res, nil } +// ShowBlockedAccount displays a message after an account has been blocked and how to reach support. +func (h *Handlers) ShowBlockedAccount(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + code := codeFromCtx(ctx) + l := gotext.NewLocale(translationDir, code) + l.AddDomain("default") + res.Content = l.Get("Your account has been locked. For help on how to unblock your account, contact support at: 0757628885") + return res, nil +} + // VerifyYob verifies the length of the given input. func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result @@ -2075,3 +2123,53 @@ func (h *Handlers) UpdateAllProfileItems(ctx context.Context, sym string, input } return res, nil } + +// incrementIncorrectPINAttempts keeps track of the number of incorrect PIN attempts +func (h *Handlers) incrementIncorrectPINAttempts(ctx context.Context, sessionId string) error { + var pinAttemptsCount uint8 + store := h.userdataStore + + currentWrongPinAttempts, err := store.ReadEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS) + if err != nil { + if db.IsNotFound(err) { + //First time Wrong PIN attempt: initialize with a count of 1 + pinAttemptsCount = 1 + err = store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(strconv.Itoa(int(pinAttemptsCount)))) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write incorrect PIN attempts ", "key", common.DATA_INCORRECT_PIN_ATTEMPTS, "value", currentWrongPinAttempts, "error", err) + return err + } + return nil + } + } + pinAttemptsValue, _ := strconv.ParseUint(string(currentWrongPinAttempts), 0, 64) + pinAttemptsCount = uint8(pinAttemptsValue) + 1 + + err = store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(strconv.Itoa(int(pinAttemptsCount)))) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write incorrect PIN attempts ", "key", common.DATA_INCORRECT_PIN_ATTEMPTS, "value", pinAttemptsCount, "error", err) + return err + } + return nil +} + +// resetIncorrectPINAttempts resets the number of incorrect PIN attempts after a correct PIN entry +func (h *Handlers) resetIncorrectPINAttempts(ctx context.Context, sessionId string) error { + store := h.userdataStore + currentWrongPinAttempts, err := store.ReadEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS) + if err != nil { + if db.IsNotFound(err) { + return nil + } + return err + } + currentWrongPinAttemptsCount, _ := strconv.ParseUint(string(currentWrongPinAttempts), 0, 64) + if currentWrongPinAttemptsCount <= uint64(common.AllowedPINAttempts) { + err = store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(string("0"))) + if err != nil { + logg.ErrorCtxf(ctx, "failed to reset incorrect PIN attempts ", "key", common.DATA_INCORRECT_PIN_ATTEMPTS, "value", common.AllowedPINAttempts, "error", err) + return err + } + } + return nil +} diff --git a/internal/handlers/ussd/menuhandler_test.go b/internal/handlers/ussd/menuhandler_test.go index 914dffc..af1380d 100644 --- a/internal/handlers/ussd/menuhandler_test.go +++ b/internal/handlers/ussd/menuhandler_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "path" + "strconv" "strings" "testing" @@ -907,37 +908,79 @@ func TestResetAccountAuthorized(t *testing.T) { } func TestIncorrectPinReset(t *testing.T) { + sessionId := "session123" + ctx, store := InitializeTestStore(t) fm, err := NewFlagManager(flagsPath) + if err != nil { log.Fatal(err) } flag_incorrect_pin, _ := fm.parser.GetFlag("flag_incorrect_pin") + flag_account_blocked, _ := fm.parser.GetFlag("flag_account_blocked") + + ctx = context.WithValue(ctx, "SessionId", sessionId) // Define test cases tests := []struct { name string input []byte + attempts uint8 expectedResult resource.Result }{ { - name: "Test incorrect pin reset", + name: "Test when incorrect PIN attempts is 2", input: []byte(""), expectedResult: resource.Result{ FlagReset: []uint32{flag_incorrect_pin}, + Content: "1", //Expected remaining PIN attempts }, + attempts: 2, + }, + { + name: "Test incorrect pin reset when incorrect PIN attempts is 1", + input: []byte(""), + expectedResult: resource.Result{ + FlagReset: []uint32{flag_incorrect_pin}, + Content: "2", //Expected remaining PIN attempts + }, + attempts: 1, + }, + { + name: "Test incorrect pin reset when incorrect PIN attempts is 1", + input: []byte(""), + expectedResult: resource.Result{ + FlagReset: []uint32{flag_incorrect_pin}, + Content: "2", //Expected remaining PIN attempts + }, + attempts: 1, + }, + { + name: "Test incorrect pin reset when incorrect PIN attempts is 3(account expected to be blocked)", + input: []byte(""), + expectedResult: resource.Result{ + FlagReset: []uint32{flag_incorrect_pin}, + FlagSet: []uint32{flag_account_blocked}, + }, + attempts: 3, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + + if err := store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(strconv.Itoa(int(tt.attempts)))); err != nil { + t.Fatal(err) + } + // Create the Handlers instance with the mock flag manager h := &Handlers{ - flagManager: fm.parser, + flagManager: fm.parser, + userdataStore: store, } // Call the method - res, err := h.ResetIncorrectPin(context.Background(), "reset_incorrect_pin", tt.input) + res, err := h.ResetIncorrectPin(ctx, "reset_incorrect_pin", tt.input) if err != nil { t.Error(err) } @@ -2190,3 +2233,55 @@ func TestGetVoucherDetails(t *testing.T) { assert.NoError(t, err) assert.Equal(t, expectedResult, res) } + +func TestCountIncorrectPINAttempts(t *testing.T) { + ctx, store := InitializeTestStore(t) + sessionId := "session123" + ctx = context.WithValue(ctx, "SessionId", sessionId) + attempts := uint8(2) + + h := &Handlers{ + userdataStore: store, + } + err := store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(strconv.Itoa(int(attempts)))) + if err != nil { + t.Logf(err.Error()) + } + err = h.incrementIncorrectPINAttempts(ctx, sessionId) + if err != nil { + t.Logf(err.Error()) + } + + attemptsAfterCount, err := store.ReadEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS) + if err != nil { + t.Logf(err.Error()) + } + pinAttemptsValue, _ := strconv.ParseUint(string(attemptsAfterCount), 0, 64) + pinAttemptsCount := uint8(pinAttemptsValue) + expectedAttempts := attempts + 1 + assert.Equal(t, pinAttemptsCount, expectedAttempts) + +} + +func TestResetIncorrectPINAttempts(t *testing.T) { + ctx, store := InitializeTestStore(t) + sessionId := "session123" + ctx = context.WithValue(ctx, "SessionId", sessionId) + + err := store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(string("2"))) + if err != nil { + t.Logf(err.Error()) + } + + h := &Handlers{ + userdataStore: store, + } + h.resetIncorrectPINAttempts(ctx, sessionId) + incorrectAttempts, err := store.ReadEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS) + + if err != nil { + t.Logf(err.Error()) + } + assert.Equal(t, "0", string(incorrectAttempts)) + +} diff --git a/menutraversal_test/group_test.json b/menutraversal_test/group_test.json index f35beb9..0ffb49f 100644 --- a/menutraversal_test/group_test.json +++ b/menutraversal_test/group_test.json @@ -54,7 +54,7 @@ }, { "input": "1235", - "expectedContent": "Incorrect PIN\n1:Retry\n9:Quit" + "expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit" }, { "input": "1", @@ -95,7 +95,7 @@ }, { "input": "1235", - "expectedContent": "Incorrect PIN\n1:Retry\n9:Quit" + "expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit" }, { "input": "1", @@ -107,8 +107,7 @@ }, { "input": "0", - "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" - + "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" }, { "input": "0", @@ -141,7 +140,7 @@ }, { "input": "1235", - "expectedContent": "Incorrect PIN\n1:Retry\n9:Quit" + "expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit" }, { "input": "1", @@ -153,8 +152,7 @@ }, { "input": "0", - "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" - + "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" }, { "input": "0", @@ -195,7 +193,7 @@ }, { "input": "1", - "expectedContent": "Enter your year of birth\n0:Back" + "expectedContent": "Enter your year of birth\n0:Back" }, { "input": "1940", @@ -258,7 +256,6 @@ "input": "0", "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" } - ] }, { @@ -443,10 +440,4 @@ ] } ] -} - - - - - - +} \ No newline at end of file diff --git a/services/registration/blocked_account.vis b/services/registration/blocked_account.vis new file mode 100644 index 0000000..d8adab2 --- /dev/null +++ b/services/registration/blocked_account.vis @@ -0,0 +1,2 @@ +LOAD show_blocked_account 0 +HALT diff --git a/services/registration/incorrect_pin b/services/registration/incorrect_pin index 7fcf610..13a9562 100644 --- a/services/registration/incorrect_pin +++ b/services/registration/incorrect_pin @@ -1 +1 @@ -Incorrect PIN \ No newline at end of file +Incorrect PIN. You have: {{.reset_incorrect}} remaining attempt(s). \ No newline at end of file diff --git a/services/registration/incorrect_pin.vis b/services/registration/incorrect_pin.vis index 844f3d6..167364a 100644 --- a/services/registration/incorrect_pin.vis +++ b/services/registration/incorrect_pin.vis @@ -1,5 +1,7 @@ LOAD reset_incorrect 0 RELOAD reset_incorrect +MAP reset_incorrect +CATCH blocked_account flag_account_blocked 1 MOUT retry 1 MOUT quit 9 HALT diff --git a/services/registration/incorrect_pin_swa b/services/registration/incorrect_pin_swa index 34a0b28..ed22beb 100644 --- a/services/registration/incorrect_pin_swa +++ b/services/registration/incorrect_pin_swa @@ -1 +1 @@ -PIN ulioeka sio sahihi \ No newline at end of file +PIN ulioeka sio sahihi, una majaribio: {{.reset_incorrect}} yaliyobaki \ No newline at end of file diff --git a/services/registration/locale/swa/default.po b/services/registration/locale/swa/default.po index 27e80c4..6155063 100644 --- a/services/registration/locale/swa/default.po +++ b/services/registration/locale/swa/default.po @@ -10,6 +10,9 @@ msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!" msgid "For more help, please call: 0757628885" msgstr "Kwa usaidizi zaidi, piga: 0757628885" +msgid "Your account has been locked. For help on how to unblock your account, contact support at: 0757628885" +msgstr "Akaunti yako imefungwa. Kwa usaidizi wa jinsi ya kufungua akaunti yako, wasiliana na usaidizi kwa: 0757628885" + msgid "Balance: %s\n" msgstr "Salio: %s\n" diff --git a/services/registration/pp.csv b/services/registration/pp.csv index 26a8833..aa1eb05 100644 --- a/services/registration/pp.csv +++ b/services/registration/pp.csv @@ -28,3 +28,5 @@ flag,flag_gender_set,34,this is set when the gender of the profile is set flag,flag_location_set,35,this is set when the location of the profile is set flag,flag_offerings_set,36,this is set when the offerings of the profile is set flag,flag_back_set,37,this is set when it is a back navigation +flag,flag_account_blocked,38,this is set when an account has been blocked after the allowed incorrect PIN attempts have been exceeded + diff --git a/services/registration/root.vis b/services/registration/root.vis index 02ef9e9..102e6e5 100644 --- a/services/registration/root.vis +++ b/services/registration/root.vis @@ -1,3 +1,4 @@ +CATCH blocked_account flag_account_blocked 1 CATCH select_language flag_language_set 0 CATCH terms flag_account_created 0 LOAD check_account_status 0