diff --git a/.env.example b/.env.example index f60c1aa..6d0368f 100644 --- a/.env.example +++ b/.env.example @@ -6,13 +6,9 @@ HOST=127.0.0.1 AT_ENDPOINT=/ussd/africastalking #PostgreSQL -DB_HOST=localhost -DB_USER=postgres -DB_PASSWORD=strongpass -DB_NAME=urdt_ussd -DB_PORT=5432 -DB_SSLMODE=disable -DB_TIMEZONE=Africa/Nairobi +DB_CONN=postgres://postgres:strongpass@localhost:5432/urdt_ussd +#DB_TIMEZONE=Africa/Nairobi +#DB_SCHEMA=vise #External API Calls CUSTODIAL_URL_BASE=http://localhost:5003 diff --git a/cmd/africastalking/main.go b/cmd/africastalking/main.go index dbb4ab1..6be7a11 100644 --- a/cmd/africastalking/main.go +++ b/cmd/africastalking/main.go @@ -21,7 +21,6 @@ import ( "git.grassecon.net/urdt/ussd/initializers" "git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/http/at" - httpserver "git.grassecon.net/urdt/ussd/internal/http/at" "git.grassecon.net/urdt/ussd/internal/storage" "git.grassecon.net/urdt/ussd/remote" "git.grassecon.net/urdt/ussd/internal/args" @@ -38,10 +37,11 @@ var ( func init() { initializers.LoadEnvVariables(baseDir) } + func main() { config.LoadConfig() - var dbDir string + var connStr string var resourceDir string var size uint var database string @@ -49,12 +49,12 @@ func main() { var engineDebug bool var host string var port uint + var err error var gettextDir string var langs args.LangVar - flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") + flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") - flag.StringVar(&database, "db", "gdbm", "database to be used") - flag.StringVar(&dbSchema, "schema", "public", "database schema to be used") + flag.StringVar(&connStr, "c", "", "connection string") flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.UintVar(&size, "s", 160, "max size of output") flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") @@ -63,7 +63,16 @@ func main() { flag.Var(&langs, "language", "add symbol resolution for language") flag.Parse() - logg.Infof("start command", "build", build, "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) + if connStr != "" { + connStr = config.DbConn + } + connData, err := storage.ToConnData(connStr) + if err != nil { + fmt.Fprintf(os.Stderr, "connstr err: %v", err) + os.Exit(1) + } + + logg.Infof("start command", "build", build, "conn", connData, "resourcedir", resourceDir, "outputsize", size) ctx := context.Background() ctx = context.WithValue(ctx, "Database", database) @@ -88,14 +97,13 @@ func main() { cfg.EngineDebug = true } - menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) - rs, err := menuStorageService.GetResource(ctx) + menuStorageService := storage.NewMenuStorageService(connData, resourceDir) if err != nil { fmt.Fprintf(os.Stderr, err.Error()) os.Exit(1) } - err = menuStorageService.EnsureDbDir() + rs, err := menuStorageService.GetResource(ctx) if err != nil { fmt.Fprintf(os.Stderr, err.Error()) os.Exit(1) @@ -141,7 +149,7 @@ func main() { rp := &at.ATRequestParser{} bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl) - sh := httpserver.NewATSessionHandler(bsh) + sh := at.NewATSessionHandler(bsh) mux := http.NewServeMux() mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh) diff --git a/cmd/async/main.go b/cmd/async/main.go index d874cbf..59af2e5 100644 --- a/cmd/async/main.go +++ b/cmd/async/main.go @@ -50,8 +50,8 @@ func (p *asyncRequestParser) GetInput(r any) ([]byte, error) { func main() { config.LoadConfig() + var connStr string var sessionId string - var dbDir string var resourceDir string var size uint var database string @@ -59,13 +59,13 @@ func main() { var engineDebug bool var host string var port uint + var err error var gettextDir string var langs args.LangVar + flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") - flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") - flag.StringVar(&database, "db", "gdbm", "database to be used") - flag.StringVar(&dbSchema, "schema", "public", "database schema to be used") + flag.StringVar(&connStr, "c", "", "connection string") flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.UintVar(&size, "s", 160, "max size of output") flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") @@ -74,7 +74,16 @@ func main() { flag.Var(&langs, "language", "add symbol resolution for language") flag.Parse() - logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId) + if connStr != "" { + connStr = config.DbConn + } + connData, err := storage.ToConnData(connStr) + if err != nil { + fmt.Fprintf(os.Stderr, "connstr err: %v", err) + os.Exit(1) + } + + logg.Infof("start command", "conn", connData, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId) ctx := context.Background() ctx = context.WithValue(ctx, "Database", database) @@ -100,18 +109,18 @@ func main() { cfg.EngineDebug = true } - menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) + menuStorageService := storage.NewMenuStorageService(connData, resourceDir) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + rs, err := menuStorageService.GetResource(ctx) if err != nil { fmt.Fprintf(os.Stderr, err.Error()) os.Exit(1) } - err = menuStorageService.EnsureDbDir() - if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) - os.Exit(1) - } userdataStore, err := menuStorageService.GetUserdataDb(ctx) if err != nil { diff --git a/cmd/http/main.go b/cmd/http/main.go index 4c0679a..af0aefc 100644 --- a/cmd/http/main.go +++ b/cmd/http/main.go @@ -40,7 +40,7 @@ func init() { func main() { config.LoadConfig() - var dbDir string + var connStr string var resourceDir string var size uint var database string @@ -48,12 +48,12 @@ func main() { var engineDebug bool var host string var port uint + var err error var gettextDir string var langs args.LangVar - flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") + flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") - flag.StringVar(&database, "db", "gdbm", "database to be used") - flag.StringVar(&dbSchema, "schema", "public", "database schema to be used") + flag.StringVar(&connStr, "c", "", "connection string") flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.UintVar(&size, "s", 160, "max size of output") flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") @@ -62,7 +62,16 @@ func main() { flag.Var(&langs, "language", "add symbol resolution for language") flag.Parse() - logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) + if connStr != "" { + connStr = config.DbConn + } + connData, err := storage.ToConnData(connStr) + if err != nil { + fmt.Fprintf(os.Stderr, "connstr err: %v", err) + os.Exit(1) + } + + logg.Infof("start command", "conn", connData, "resourcedir", resourceDir, "outputsize", size) ctx := context.Background() ctx = context.WithValue(ctx, "Database", database) @@ -88,19 +97,14 @@ func main() { cfg.EngineDebug = true } - menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) + menuStorageService := storage.NewMenuStorageService(connData, resourceDir) + rs, err := menuStorageService.GetResource(ctx) if err != nil { fmt.Fprintf(os.Stderr, err.Error()) os.Exit(1) } - err = menuStorageService.EnsureDbDir() - if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) - os.Exit(1) - } - userdataStore, err := menuStorageService.GetUserdataDb(ctx) if err != nil { fmt.Fprintf(os.Stderr, err.Error()) diff --git a/cmd/main.go b/cmd/main.go index 8e3df59..5c89c05 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -35,25 +35,36 @@ func init() { func main() { config.LoadConfig() - var dbDir string + var connStr string var size uint var sessionId string var database string var dbSchema string var engineDebug bool + var resourceDir string + var err error var gettextDir string var langs args.LangVar + + flag.StringVar(&resourceDir, "resourcedir", scriptDir, "resource dir") flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") - flag.StringVar(&database, "db", "gdbm", "database to be used") - flag.StringVar(&dbSchema, "schema", "public", "database schema to be used") - flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") + flag.StringVar(&connStr, "c", "", "connection string") flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.UintVar(&size, "s", 160, "max size of output") flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory") flag.Var(&langs, "language", "add symbol resolution for language") flag.Parse() - logg.Infof("start command", "dbdir", dbDir, "outputsize", size) + if connStr != "" { + connStr = config.DbConn + } + connData, err := storage.ToConnData(connStr) + if err != nil { + fmt.Fprintf(os.Stderr, "connstr err: %v", err) + os.Exit(1) + } + + logg.Infof("start command", "conn", connData, "outputsize", size) if len(langs.Langs()) == 0 { langs.Set(config.DefaultLanguage) @@ -81,18 +92,12 @@ func main() { MenuSeparator: menuSeparator, } - resourceDir := scriptDir - menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) + menuStorageService := storage.NewMenuStorageService(connData, resourceDir) + if gettextDir != "" { menuStorageService = menuStorageService.WithGettext(gettextDir, langs.Langs()) } - err = menuStorageService.EnsureDbDir() - if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) - os.Exit(1) - } - rs, err := menuStorageService.GetResource(ctx) if err != nil { fmt.Fprintf(os.Stderr, err.Error()) diff --git a/cmd/ssh/main.go b/cmd/ssh/main.go index ae046a5..51023e5 100644 --- a/cmd/ssh/main.go +++ b/cmd/ssh/main.go @@ -14,7 +14,10 @@ import ( "git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/logging" + "git.grassecon.net/urdt/ussd/config" + "git.grassecon.net/urdt/ussd/initializers" "git.grassecon.net/urdt/ussd/internal/ssh" + "git.grassecon.net/urdt/ussd/internal/storage" ) var ( @@ -26,25 +29,49 @@ var ( build = "dev" ) +func init() { + initializers.LoadEnvVariables() +} + func main() { - var dbDir string + config.LoadConfig() + + var connStr string + var authConnStr string var resourceDir string var size uint var engineDebug bool var stateDebug bool var host string var port uint - flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") + flag.StringVar(&connStr, "c", "", "connection string") + flag.StringVar(&authConnStr, "authdb", "", "auth connection string") flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") - flag.BoolVar(&engineDebug, "engine-debug", false, "use engine debug output") - flag.BoolVar(&stateDebug, "state-debug", false, "use engine debug output") + flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.UintVar(&size, "s", 160, "max size of output") - flag.StringVar(&host, "h", "127.0.0.1", "http host") - flag.UintVar(&port, "p", 7122, "http port") + flag.StringVar(&host, "h", "127.0.0.1", "socket host") + flag.UintVar(&port, "p", 7122, "socket port") flag.Parse() + if connStr == "" { + connStr = config.DbConn + } + if authConnStr == "" { + authConnStr = connStr + } + connData, err := storage.ToConnData(connStr) + if err != nil { + fmt.Fprintf(os.Stderr, "connstr err: %v", err) + os.Exit(1) + } + authConnData, err := storage.ToConnData(authConnStr) + if err != nil { + fmt.Fprintf(os.Stderr, "auth connstr err: %v", err) + os.Exit(1) + } + sshKeyFile := flag.Arg(0) - _, err := os.Stat(sshKeyFile) + _, err = os.Stat(sshKeyFile) if err != nil { fmt.Fprintf(os.Stderr, "cannot open ssh server private key file: %v\n", err) os.Exit(1) @@ -57,7 +84,7 @@ func main() { logg.WarnCtxf(ctx, "!!!!! Do not expose to internet and only use with tunnel!") logg.WarnCtxf(ctx, "!!!!! (See ssh -L <...>)") - logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "keyfile", sshKeyFile, "host", host, "port", port) + logg.Infof("start command", "conn", connData, "authconn", authConnData, "resourcedir", resourceDir, "outputsize", size, "keyfile", sshKeyFile, "host", host, "port", port) pfp := path.Join(scriptDir, "pp.csv") @@ -73,7 +100,7 @@ func main() { cfg.EngineDebug = true } - authKeyStore, err := ssh.NewSshKeyStore(ctx, dbDir) + authKeyStore, err := ssh.NewSshKeyStore(ctx, authConnData.String()) if err != nil { fmt.Fprintf(os.Stderr, "keystore file open error: %v", err) os.Exit(1) @@ -92,10 +119,10 @@ func main() { signal.Notify(cterm, os.Interrupt, syscall.SIGTERM) runner := &ssh.SshRunner{ - Cfg: cfg, - Debug: engineDebug, - FlagFile: pfp, - DbDir: dbDir, + Cfg: cfg, + Debug: engineDebug, + FlagFile: pfp, + Conn: connData, ResourceDir: resourceDir, SrvKeyFile: sshKeyFile, Host: host, 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/common/storage.go b/common/storage.go index d37bce3..2960578 100644 --- a/common/storage.go +++ b/common/storage.go @@ -23,17 +23,17 @@ type StorageServices interface { GetPersister(ctx context.Context) (*persist.Persister, error) GetUserdataDb(ctx context.Context) (db.Db, error) GetResource(ctx context.Context) (resource.Resource, error) - EnsureDbDir() error } type StorageService struct { svc *storage.MenuStorageService } -func NewStorageService(dbDir string) *StorageService { - return &StorageService{ - svc: storage.NewMenuStorageService(dbDir, ""), +func NewStorageService(conn storage.ConnData) (*StorageService, error) { + svc := &StorageService{ + svc: storage.NewMenuStorageService(conn, ""), } + return svc, nil } func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) { @@ -47,7 +47,3 @@ func(ss *StorageService) GetUserdataDb(ctx context.Context) (db.Db, error) { func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, error) { return nil, errors.New("not implemented") } - -func(ss *StorageService) EnsureDbDir() error { - return ss.svc.EnsureDbDir() -} diff --git a/config/config.go b/config/config.go index 02fdaab..4b43b42 100644 --- a/config/config.go +++ b/config/config.go @@ -40,6 +40,7 @@ var ( VoucherTransfersURL string VoucherDataURL string CheckAliasURL string + DbConn string DefaultLanguage string Languages []string ) @@ -69,14 +70,20 @@ func setBase() error { dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006") BearerToken = initializers.GetEnv("BEARER_TOKEN", "") - _, err = url.JoinPath(custodialURLBase, "/foo") + _, err = url.Parse(custodialURLBase) if err != nil { return err } - _, err = url.JoinPath(dataURLBase, "/bar") + _, err = url.Parse(dataURLBase) if err != nil { return err } + + return nil +} + +func setConn() error { + DbConn = initializers.GetEnv("DB_CONN", "") return nil } @@ -86,6 +93,10 @@ func LoadConfig() error { if err != nil { return err } + err = setConn() + if err != nil { + return err + } err = setLanguage() if err != nil { return err diff --git a/devtools/store/dump/main.go b/devtools/store/dump/main.go index dfb9089..c84a134 100644 --- a/devtools/store/dump/main.go +++ b/devtools/store/dump/main.go @@ -37,23 +37,34 @@ func formatItem(k []byte, v []byte) (string, error) { func main() { config.LoadConfig() - var dbDir string + var connStr string var sessionId string var database string var engineDebug bool + var err error flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") - flag.StringVar(&database, "db", "gdbm", "database to be used") - flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") + flag.StringVar(&connStr, "c", ".state", "connection string") flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.Parse() + if connStr != "" { + connStr = config.DbConn + } + connData, err := storage.ToConnData(config.DbConn) + if err != nil { + fmt.Fprintf(os.Stderr, "connstr err: %v", err) + os.Exit(1) + } + + logg.Infof("start command", "conn", connData) + ctx := context.Background() ctx = context.WithValue(ctx, "SessionId", sessionId) ctx = context.WithValue(ctx, "Database", database) resourceDir := scriptDir - menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) + menuStorageService := storage.NewMenuStorageService(connData, resourceDir) store, err := menuStorageService.GetUserdataDb(ctx) if err != nil { diff --git a/devtools/store/generate/main.go b/devtools/store/generate/main.go index f54afb7..58a9808 100644 --- a/devtools/store/generate/main.go +++ b/devtools/store/generate/main.go @@ -29,24 +29,35 @@ func init() { func main() { config.LoadConfig() - var dbDir string + var connStr string var sessionId string var database string var engineDebug bool + var err error flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") - flag.StringVar(&database, "db", "gdbm", "database to be used") - flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") + flag.StringVar(&connStr, "c", "", "connection string") flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.Parse() + if connStr != "" { + connStr = config.DbConn + } + connData, err := storage.ToConnData(config.DbConn) + if err != nil { + fmt.Fprintf(os.Stderr, "connstr err: %v", err) + os.Exit(1) + } + + logg.Infof("start command", "conn", connData) + ctx := context.Background() ctx = context.WithValue(ctx, "SessionId", sessionId) ctx = context.WithValue(ctx, "Database", database) resourceDir := scriptDir - menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) - + menuStorageService := storage.NewMenuStorageService(connData, resourceDir) + store, err := menuStorageService.GetUserdataDb(ctx) if err != nil { fmt.Fprintf(os.Stderr, err.Error()) 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/internal/ssh/ssh.go b/internal/ssh/ssh.go index 1831ebe..8209187 100644 --- a/internal/ssh/ssh.go +++ b/internal/ssh/ssh.go @@ -41,6 +41,7 @@ func NewAuther(ctx context.Context, keyStore *SshKeyStore) *auther { } func(a *auther) Check(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { + logg.TraceCtxf(a.Ctx, "looking for publickey", "pubkey", fmt.Sprintf("%x", pubKey)) va, err := a.keyStore.Get(a.Ctx, pubKey) if err != nil { return nil, err @@ -71,6 +72,20 @@ func(a *auther) Get(k []byte) (string, error) { return v, nil } +type SshRunner struct { + Ctx context.Context + Cfg engine.Config + FlagFile string + Conn storage.ConnData + ResourceDir string + Debug bool + SrvKeyFile string + Host string + Port uint + wg sync.WaitGroup + lst net.Listener +} + func(s *SshRunner) serve(ctx context.Context, sessionId string, ch ssh.NewChannel, en engine.Engine) error { if ch == nil { return errors.New("nil channel") @@ -128,32 +143,13 @@ func(s *SshRunner) serve(ctx context.Context, sessionId string, ch ssh.NewChanne return nil } -type SshRunner struct { - Ctx context.Context - Cfg engine.Config - FlagFile string - DbDir string - ResourceDir string - Debug bool - SrvKeyFile string - Host string - Port uint - wg sync.WaitGroup - lst net.Listener -} - func(s *SshRunner) Stop() error { return s.lst.Close() } func(s *SshRunner) GetEngine(sessionId string) (engine.Engine, func(), error) { ctx := s.Ctx - menuStorageService := storage.NewMenuStorageService(s.DbDir, s.ResourceDir) - - err := menuStorageService.EnsureDbDir() - if err != nil { - return nil, nil, err - } + menuStorageService := storage.NewMenuStorageService(s.Conn, s.ResourceDir) rs, err := menuStorageService.GetResource(ctx) if err != nil { @@ -208,6 +204,7 @@ func(s *SshRunner) GetEngine(sessionId string) (engine.Engine, func(), error) { // adapted example from crypto/ssh package, NewServerConn doc func(s *SshRunner) Run(ctx context.Context, keyStore *SshKeyStore) { + s.Ctx = ctx running := true // TODO: waitgroup should probably not be global diff --git a/internal/storage/parse.go b/internal/storage/parse.go new file mode 100644 index 0000000..bb25627 --- /dev/null +++ b/internal/storage/parse.go @@ -0,0 +1,69 @@ +package storage + +import ( + "fmt" + "net/url" + "path" +) + +const ( + DBTYPE_MEM = iota + DBTYPE_GDBM + DBTYPE_POSTGRES +) + +type ConnData struct { + typ int + str string +} + +func (cd *ConnData) DbType() int { + return cd.typ +} + +func (cd *ConnData) String() string { + return cd.str +} + +func probePostgres(s string) (string, bool) { + v, err := url.Parse(s) + if err != nil { + return "", false + } + if v.Scheme != "postgres" { + return "", false + } + return s, true +} + +func probeGdbm(s string) (string, bool) { + if !path.IsAbs(s) { + return "", false + } + s = path.Clean(s) + return s, true +} + +func ToConnData(connStr string) (ConnData, error) { + var o ConnData + + if connStr == "" { + return o, nil + } + + v, ok := probePostgres(connStr) + if ok { + o.typ = DBTYPE_POSTGRES + o.str = v + return o, nil + } + + v, ok = probeGdbm(connStr) + if ok { + o.typ = DBTYPE_GDBM + o.str = v + return o, nil + } + + return o, fmt.Errorf("invalid connection string: %s", connStr) +} diff --git a/internal/storage/parse_test.go b/internal/storage/parse_test.go new file mode 100644 index 0000000..e18e57c --- /dev/null +++ b/internal/storage/parse_test.go @@ -0,0 +1,28 @@ +package storage + +import ( + "testing" +) + +func TestParseConnStr(t *testing.T) { + _, err := ToConnData("postgres://foo:bar@localhost:5432/baz") + if err != nil { + t.Fatal(err) + } + _, err = ToConnData("/foo/bar") + if err != nil { + t.Fatal(err) + } + _, err = ToConnData("/foo/bar/") + if err != nil { + t.Fatal(err) + } + _, err = ToConnData("foo/bar") + if err == nil { + t.Fatalf("expected error") + } + _, err = ToConnData("http://foo/bar") + if err == nil { + t.Fatalf("expected error") + } +} diff --git a/internal/storage/storageservice.go b/internal/storage/storageservice.go index 6efcd22..f2ac273 100644 --- a/internal/storage/storageservice.go +++ b/internal/storage/storageservice.go @@ -13,7 +13,6 @@ import ( "git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/persist" "git.defalsify.org/vise.git/resource" - "git.grassecon.net/urdt/ussd/initializers" gdbmstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm" "github.com/jackc/pgx/v5/pgxpool" ) @@ -26,11 +25,10 @@ type StorageService interface { GetPersister(ctx context.Context) (*persist.Persister, error) GetUserdataDb(ctx context.Context) db.Db GetResource(ctx context.Context) (resource.Resource, error) - EnsureDbDir() error } type MenuStorageService struct { - dbDir string + conn ConnData resourceDir string poResource resource.Resource resourceStore db.Db @@ -38,29 +36,53 @@ type MenuStorageService struct { userDataStore db.Db } -func BuildConnStr() string { - host := initializers.GetEnv("DB_HOST", "localhost") - user := initializers.GetEnv("DB_USER", "postgres") - password := initializers.GetEnv("DB_PASSWORD", "") - dbName := initializers.GetEnv("DB_NAME", "") - port := initializers.GetEnv("DB_PORT", "5432") - - connString := fmt.Sprintf( - "postgres://%s:%s@%s:%s/%s", - user, password, host, port, dbName, - ) - logg.Debugf("pg conn string", "conn", connString) - - return connString -} - -func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService { +func NewMenuStorageService(conn ConnData, resourceDir string) *MenuStorageService { return &MenuStorageService{ - dbDir: dbDir, + conn: conn, resourceDir: resourceDir, } } +func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, section string) (db.Db, error) { + var newDb db.Db + var err error + + if existingDb != nil { + return existingDb, nil + } + + + connStr := ms.conn.String() + dbTyp := ms.conn.DbType() + if dbTyp == DBTYPE_POSTGRES { +// // Ensure the schema exists +// err = ensureSchemaExists(ctx, connStr, schema) +// if err != nil { +// return nil, fmt.Errorf("failed to ensure schema exists: %w", err) +// } +// +// newDb = postgres.NewPgDb().WithSchema(schema) + + newDb = postgres.NewPgDb() + } else if dbTyp == DBTYPE_GDBM { + err = ms.ensureDbDir() + if err != nil { + return nil, err + } + connStr = path.Join(connStr, section) + newDb = gdbmstorage.NewThreadGdbmDb() + } else { + return nil, fmt.Errorf("unsupported connection string: '%s'\n", ms.conn.String()) + } + logg.DebugCtxf(ctx, "connecting to db", "conn", connStr) + err = newDb.Connect(ctx, connStr) + if err != nil { + return nil, err + } + + return newDb, nil +} + // WithGettext triggers use of gettext for translation of templates and menus. // // The first language in `lns` will be used as default language, to resolve node keys to @@ -83,48 +105,6 @@ func (ms *MenuStorageService) WithGettext(path string, lns []lang.Language) *Men return ms } -func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, fileName string) (db.Db, error) { - database, ok := ctx.Value("Database").(string) - if !ok { - return nil, fmt.Errorf("failed to select the database") - } - - schema, ok := ctx.Value("Schema").(string) - if !ok { - return nil, fmt.Errorf("failed to select the schema") - } - - if existingDb != nil { - return existingDb, nil - } - - var newDb db.Db - var err error - - if database == "postgres" { - connStr := BuildConnStr() - - // Ensure the schema exists - err = ensureSchemaExists(ctx, connStr, schema) - if err != nil { - return nil, fmt.Errorf("failed to ensure schema exists: %w", err) - } - - newDb = postgres.NewPgDb().WithSchema(schema) - err = newDb.Connect(ctx, connStr) - } else { - newDb = gdbmstorage.NewThreadGdbmDb() - storeFile := path.Join(ms.dbDir, fileName) - err = newDb.Connect(ctx, storeFile) - } - - if err != nil { - return nil, err - } - - return newDb, nil -} - // ensureSchemaExists creates a new schema if it does not exist func ensureSchemaExists(ctx context.Context, connStr, schema string) error { conn, err := pgxpool.New(ctx, connStr) @@ -196,8 +176,8 @@ func (ms *MenuStorageService) GetStateStore(ctx context.Context) (db.Db, error) return ms.stateStore, nil } -func (ms *MenuStorageService) EnsureDbDir() error { - err := os.MkdirAll(ms.dbDir, 0700) +func (ms *MenuStorageService) ensureDbDir() error { + err := os.MkdirAll(ms.conn.String(), 0700) if err != nil { return fmt.Errorf("state dir create exited with error: %v\n", err) } diff --git a/internal/testutil/TestEngine.go b/internal/testutil/engine.go similarity index 81% rename from internal/testutil/TestEngine.go rename to internal/testutil/engine.go index 25a1c15..a1eefa8 100644 --- a/internal/testutil/TestEngine.go +++ b/internal/testutil/engine.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path" + "path/filepath" "time" "git.defalsify.org/vise.git/engine" @@ -16,11 +17,9 @@ import ( "git.grassecon.net/urdt/ussd/internal/testutil/testservice" "git.grassecon.net/urdt/ussd/internal/testutil/testtag" "git.grassecon.net/urdt/ussd/remote" - testdataloader "github.com/peteole/testdata-loader" ) var ( - baseDir = testdataloader.GetBasePath() logg = logging.NewVanilla() scriptDir = path.Join(baseDir, "services", "registration") selectedDatabase = "" @@ -28,7 +27,7 @@ var ( ) func init() { - initializers.LoadEnvVariables(baseDir) + initializers.LoadEnvVariables() } // SetDatabase updates the database used by TestEngine @@ -40,9 +39,6 @@ func SetDatabase(dbType string, dbSchema string) { func TestEngine(sessionId string) (engine.Engine, func(), chan bool) { ctx := context.Background() ctx = context.WithValue(ctx, "SessionId", sessionId) - ctx = context.WithValue(ctx, "Database", selectedDatabase) - ctx = context.WithValue(ctx, "Schema", selectedDbSchema) - pfp := path.Join(scriptDir, "pp.csv") var eventChannel = make(chan bool) @@ -54,37 +50,40 @@ func TestEngine(sessionId string) (engine.Engine, func(), chan bool) { FlagCount: uint32(128), } - dbDir := ".test_state" - resourceDir := scriptDir - menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) - - err := menuStorageService.EnsureDbDir() + connStr, err := filepath.Abs(".test_state/state.gdbm") if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) + fmt.Fprintf(os.Stderr, "connstr err: %v", err) os.Exit(1) } + conn, err := storage.ToConnData(connStr) + if err != nil { + fmt.Fprintf(os.Stderr, "connstr parse err: %v", err) + os.Exit(1) + } + resourceDir := scriptDir + menuStorageService := storage.NewMenuStorageService(conn, resourceDir) rs, err := menuStorageService.GetResource(ctx) if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) + fmt.Fprintf(os.Stderr, "resource error: %v", err) os.Exit(1) } pe, err := menuStorageService.GetPersister(ctx) if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) + fmt.Fprintf(os.Stderr, "persister error: %v", err) os.Exit(1) } userDataStore, err := menuStorageService.GetUserdataDb(ctx) if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) + fmt.Fprintf(os.Stderr, "userdb error: %v", err) os.Exit(1) } dbResource, ok := rs.(*resource.DbResource) if !ok { - fmt.Fprintf(os.Stderr, err.Error()) + fmt.Fprintf(os.Stderr, "dbresource cast error") os.Exit(1) } diff --git a/internal/testutil/engine_test.go b/internal/testutil/engine_test.go new file mode 100644 index 0000000..f747468 --- /dev/null +++ b/internal/testutil/engine_test.go @@ -0,0 +1,15 @@ +package testutil + +import ( + "testing" +) + +func TestCreateEngine(t *testing.T) { + o, clean, eventC := TestEngine("foo") + defer clean() + defer func() { + <-eventC + close(eventC) + }() + _ = o +} diff --git a/internal/testutil/testservice/TestAccountService.go b/internal/testutil/testservice/accountservice.go similarity index 100% rename from internal/testutil/testservice/TestAccountService.go rename to internal/testutil/testservice/accountservice.go 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/menutraversal_test/menu_traversal_test.go b/menutraversal_test/menu_traversal_test.go index d2e353d..db4586a 100644 --- a/menutraversal_test/menu_traversal_test.go +++ b/menutraversal_test/menu_traversal_test.go @@ -8,6 +8,7 @@ import ( "log" "math/rand" "os" + "path/filepath" "regexp" "testing" @@ -20,7 +21,6 @@ import ( var ( testData = driver.ReadData() - testStore = ".test_state" sessionID string src = rand.NewSource(42) g = rand.New(src) @@ -30,6 +30,11 @@ var groupTestFile = flag.String("test-file", "group_test.json", "The test file t var database = flag.String("db", "gdbm", "Specify the database (gdbm or postgres)") var dbSchema = flag.String("schema", "test", "Specify the database schema (default test)") +func testStore() string { + v, _ := filepath.Abs(".test_state/state.gdbm") + return v +} + func GenerateSessionId() string { uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g)) v, err := uu.NewV4() @@ -89,8 +94,8 @@ func TestMain(m *testing.M) { sessionID = GenerateSessionId() defer func() { - if err := os.RemoveAll(testStore); err != nil { - log.Fatalf("Failed to delete state store %s: %v", testStore, err) + if err := os.RemoveAll(testStore()); err != nil { + log.Fatalf("Failed to delete state store %s: %v", testStore(), err) } }() 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