Compare commits

..

25 Commits

Author SHA1 Message Date
Carlosokumu
b698f08136 chore: add space after punctuation 2025-01-08 15:27:10 +03:00
Carlosokumu
efdb52bccd chore: add space after punctuation 2025-01-08 14:54:58 +03:00
Carlosokumu
2ff9fed3c5 chore: rename countIncorrectPINAttempts to incrementIncorrectPINAttempts 2025-01-08 14:54:57 +03:00
Carlosokumu
477b4cf8f6 chore : rename remainingPINAttempts to currentWrongPinAttemptsCount 2025-01-08 14:54:57 +03:00
Carlosokumu
ed6651697a chore : add variable description to AllowedPINAttempts 2025-01-08 14:54:56 +03:00
carlos
c359d99075 Merge branch 'master' into account-pin-block-v2 2025-01-08 10:00:46 +01:00
Carlosokumu
8d477356f3 update tests 2025-01-08 11:47:55 +03:00
Carlosokumu
7f3294a8a2 update tests 2025-01-08 11:47:41 +03:00
f49e54a562 Merge pull request 'Space after comma' (#259) from lash/helpcomma into master
Reviewed-on: #259
2025-01-08 07:57:18 +01:00
lash
5081b6d4ce Space after comma 2025-01-08 06:48:35 +00:00
Carlosokumu
4d72ae0313 add handler showing a message for a blocked account 2025-01-08 09:30:51 +03:00
Carlosokumu
4fe64a7747 show message for a blocked account 2025-01-08 09:29:00 +03:00
Carlosokumu
3004698d5b add a message for a blocked account 2025-01-08 09:28:31 +03:00
Carlosokumu
50c7ff1046 register handler to show blocked account message 2025-01-08 09:27:45 +03:00
Carlosokumu
07b061a68b remove blocked account templates 2025-01-08 09:26:53 +03:00
Carlosokumu
64fba91670 catch blocked account 2025-01-07 14:38:44 +03:00
Carlosokumu
c15958a1ad reset incorrect pin attempts on correct entry 2025-01-07 14:32:44 +03:00
Carlosokumu
ee442daefa add blocked account node 2025-01-07 14:03:53 +03:00
656052dc74 Merge pull request 'trim any leading whitespace in the input' (#258) from send-input-fix into master
Some checks failed
release / docker (push) Has been cancelled
Reviewed-on: #258
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2025-01-07 10:33:20 +01:00
Carlosokumu
11d30583a4 map content of reset_incorrect and catch blocked account 2025-01-07 10:50:30 +03:00
Carlosokumu
f83f539046 add node to show remaining pin attempts 2025-01-07 10:48:59 +03:00
Carlosokumu
562bd4fa24 check for incorrect pin 2025-01-06 22:54:31 +03:00
Carlosokumu
90df0eefc3 add value for allowed number of PIN attempts 2025-01-06 22:53:59 +03:00
Carlosokumu
b37f2a0a11 add flag for when an account has been blocked 2025-01-06 21:06:54 +03:00
Carlosokumu
68e4c9af03 add key for incorrect pin attempts 2025-01-06 21:00:34 +03:00
13 changed files with 227 additions and 26 deletions

View File

@@ -7,7 +7,7 @@ import (
"git.defalsify.org/vise.git/logging" "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. // 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 DATA_ACTIVE_DECIMAL
// EVM address of the currently active voucher // EVM address of the currently active voucher
DATA_ACTIVE_ADDRESS DATA_ACTIVE_ADDRESS
//Holds count of the number of incorrect PIN attempts
DATA_INCORRECT_PIN_ATTEMPTS
) )
const ( const (

View File

@@ -6,9 +6,13 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
// Define the regex pattern as a constant
const ( const (
// Define the regex pattern as a constant
pinPattern = `^\d{4}$` pinPattern = `^\d{4}$`
//Allowed incorrect PIN attempts
AllowedPINAttempts = uint8(3)
) )
// checks whether the given input is a 4 digit number // checks whether the given input is a 4 digit number

View File

@@ -128,6 +128,7 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn
ls.DbRs.AddLocalFunc("view_statement", ussdHandlers.ViewTransactionStatement) ls.DbRs.AddLocalFunc("view_statement", ussdHandlers.ViewTransactionStatement)
ls.DbRs.AddLocalFunc("update_all_profile_items", ussdHandlers.UpdateAllProfileItems) ls.DbRs.AddLocalFunc("update_all_profile_items", ussdHandlers.UpdateAllProfileItems)
ls.DbRs.AddLocalFunc("set_back", ussdHandlers.SetBack) ls.DbRs.AddLocalFunc("set_back", ussdHandlers.SetBack)
ls.DbRs.AddLocalFunc("show_blocked_account", ussdHandlers.ShowBlockedAccount)
return ussdHandlers, nil return ussdHandlers, nil
} }

View File

@@ -734,11 +734,23 @@ func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (res
if h.st.MatchFlag(flag_account_authorized, false) { if h.st.MatchFlag(flag_account_authorized, false) {
res.FlagReset = append(res.FlagReset, flag_incorrect_pin) res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
res.FlagSet = append(res.FlagSet, flag_allow_update, flag_account_authorized) res.FlagSet = append(res.FlagSet, flag_allow_update, flag_account_authorized)
err := h.resetIncorrectPINAttempts(ctx, sessionId)
if err != nil {
return res, err
}
} else { } else {
res.FlagSet = append(res.FlagSet, flag_allow_update) res.FlagSet = append(res.FlagSet, flag_allow_update)
res.FlagReset = append(res.FlagReset, flag_account_authorized) res.FlagReset = append(res.FlagReset, flag_account_authorized)
err := h.resetIncorrectPINAttempts(ctx, sessionId)
if err != nil {
return res, err
}
} }
} else { } else {
err := h.incrementIncorrectPINAttempts(ctx, sessionId)
if err != nil {
return res, err
}
res.FlagSet = append(res.FlagSet, flag_incorrect_pin) res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
res.FlagReset = append(res.FlagReset, flag_account_authorized) res.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil 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. // 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) { func (h *Handlers) ResetIncorrectPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
store := h.userdataStore
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") 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) 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 return res, nil
} }
@@ -835,11 +873,21 @@ func (h *Handlers) QuitWithHelp(ctx context.Context, sym string, input []byte) (
l := gotext.NewLocale(translationDir, code) l := gotext.NewLocale(translationDir, code)
l.AddDomain("default") l.AddDomain("default")
res.Content = l.Get("For more help,please call: 0757628885") res.Content = l.Get("For more help, please call: 0757628885")
res.FlagReset = append(res.FlagReset, flag_account_authorized) res.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil 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. // VerifyYob verifies the length of the given input.
func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
@@ -2075,3 +2123,53 @@ func (h *Handlers) UpdateAllProfileItems(ctx context.Context, sym string, input
} }
return res, nil 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
}

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"log" "log"
"path" "path"
"strconv"
"strings" "strings"
"testing" "testing"
@@ -907,37 +908,79 @@ func TestResetAccountAuthorized(t *testing.T) {
} }
func TestIncorrectPinReset(t *testing.T) { func TestIncorrectPinReset(t *testing.T) {
sessionId := "session123"
ctx, store := InitializeTestStore(t)
fm, err := NewFlagManager(flagsPath) fm, err := NewFlagManager(flagsPath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
flag_incorrect_pin, _ := fm.parser.GetFlag("flag_incorrect_pin") 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 // Define test cases
tests := []struct { tests := []struct {
name string name string
input []byte input []byte
attempts uint8
expectedResult resource.Result expectedResult resource.Result
}{ }{
{ {
name: "Test incorrect pin reset", name: "Test when incorrect PIN attempts is 2",
input: []byte(""), input: []byte(""),
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagReset: []uint32{flag_incorrect_pin}, 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 // Create the Handlers instance with the mock flag manager
h := &Handlers{ h := &Handlers{
flagManager: fm.parser, flagManager: fm.parser,
userdataStore: store,
} }
// Call the method // 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 { if err != nil {
t.Error(err) t.Error(err)
} }
@@ -2190,3 +2233,55 @@ func TestGetVoucherDetails(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expectedResult, res) 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))
}

View File

@@ -54,7 +54,7 @@
}, },
{ {
"input": "1235", "input": "1235",
"expectedContent": "Incorrect PIN\n1:Retry\n9:Quit" "expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit"
}, },
{ {
"input": "1", "input": "1",
@@ -95,7 +95,7 @@
}, },
{ {
"input": "1235", "input": "1235",
"expectedContent": "Incorrect PIN\n1:Retry\n9:Quit" "expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit"
}, },
{ {
"input": "1", "input": "1",
@@ -107,8 +107,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
}, },
{ {
"input": "0", "input": "0",
@@ -141,7 +140,7 @@
}, },
{ {
"input": "1235", "input": "1235",
"expectedContent": "Incorrect PIN\n1:Retry\n9:Quit" "expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit"
}, },
{ {
"input": "1", "input": "1",
@@ -153,8 +152,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
}, },
{ {
"input": "0", "input": "0",
@@ -195,7 +193,7 @@
}, },
{ {
"input": "1", "input": "1",
"expectedContent": "Enter your year of birth\n0:Back" "expectedContent": "Enter your year of birth\n0:Back"
}, },
{ {
"input": "1940", "input": "1940",
@@ -258,7 +256,6 @@
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
} }
] ]
}, },
{ {
@@ -443,10 +440,4 @@
] ]
} }
] ]
} }

View File

@@ -0,0 +1,2 @@
LOAD show_blocked_account 0
HALT

View File

@@ -1 +1 @@
Incorrect PIN Incorrect PIN. You have: {{.reset_incorrect}} remaining attempt(s).

View File

@@ -1,5 +1,7 @@
LOAD reset_incorrect 0 LOAD reset_incorrect 0
RELOAD reset_incorrect RELOAD reset_incorrect
MAP reset_incorrect
CATCH blocked_account flag_account_blocked 1
MOUT retry 1 MOUT retry 1
MOUT quit 9 MOUT quit 9
HALT HALT

View File

@@ -1 +1 @@
PIN ulioeka sio sahihi PIN ulioeka sio sahihi, una majaribio: {{.reset_incorrect}} yaliyobaki

View File

@@ -7,8 +7,11 @@ msgstr "Ombi lako limetumwa. %s atapokea %s %s kutoka kwa %s."
msgid "Thank you for using Sarafu. Goodbye!" msgid "Thank you for using Sarafu. Goodbye!"
msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!" msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!"
msgid "For more help,please call: 0757628885" msgid "For more help, please call: 0757628885"
msgstr "Kwa usaidizi zaidi,piga: 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" msgid "Balance: %s\n"
msgstr "Salio: %s\n" msgstr "Salio: %s\n"

View File

@@ -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_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_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_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
1 flag flag_language_set 8 checks whether the user has set their prefered language
28 flag flag_location_set 35 this is set when the location of the profile is set
29 flag flag_offerings_set 36 this is set when the offerings of the profile is set
30 flag flag_back_set 37 this is set when it is a back navigation
31 flag flag_account_blocked 38 this is set when an account has been blocked after the allowed incorrect PIN attempts have been exceeded
32

View File

@@ -1,3 +1,4 @@
CATCH blocked_account flag_account_blocked 1
CATCH select_language flag_language_set 0 CATCH select_language flag_language_set 0
CATCH terms flag_account_created 0 CATCH terms flag_account_created 0
LOAD check_account_status 0 LOAD check_account_status 0