Compare commits

...

51 Commits

Author SHA1 Message Date
f40e11c267 Merge pull request 'account-pin-block-v2' (#256) from account-pin-block-v2 into master
Some checks failed
release / docker (push) Has been cancelled
Reviewed-on: #256
2025-01-08 13:30:39 +01:00
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
alfred-mk
6c5873da6f trim any leading whitespace in the input 2025-01-07 12:15:15 +03: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
80b96e9bf6 Merge pull request 'Add gettext capability to template and menu resources' (#239) from lash/gettext into master
Reviewed-on: #239
2025-01-06 09:49:53 +01:00
b5561decd1 Merge branch 'master' into lash/gettext 2025-01-06 09:48:33 +01:00
f3d4f35718 Merge pull request 'Factor out db dump formatting' (#243) from lash/dump-format into master
Reviewed-on: #243
2025-01-06 09:44:29 +01:00
52787bdb4d Merge branch 'master' into lash/dump-format 2025-01-06 09:42:26 +01:00
824d39908b ci: fix missing ssh dir
Some checks failed
release / docker (push) Has been cancelled
2025-01-06 11:19:36 +03:00
a312ea5b84 feat: inject build string in ssh binary, expose default ssh port
Some checks failed
release / docker (push) Has been cancelled
2025-01-06 11:09:51 +03:00
4836162f40 ci: add ssh build
Some checks failed
release / docker (push) Has been cancelled
2025-01-06 10:51:20 +03:00
lash
bb4037e73f Add languages env example 2025-01-05 21:25:09 +00:00
lash
83857026d3 Merge branch 'master' into lash/dump-format 2025-01-04 10:00:25 +00:00
lash
349051b5ef Merge branch 'master' into lash/gettext 2025-01-04 09:59:26 +00:00
lash
daec816a3e Move store devtools location 2025-01-03 17:21:52 +00:00
lash
ac0c43cb43 Factor out formatting method 2025-01-03 17:18:23 +00:00
lash
9013cc3618 Improve error messages 2025-01-03 15:10:20 +00:00
lash
056d056613 Add language source and template file generator 2025-01-03 14:43:08 +00:00
lash
e581ec4771 Merge tag 'v0.8.0-beta.4' into lash/gettext 2025-01-03 10:29:17 +00:00
lash
e16b7445e8 Move arg var to same spot as other runners 2025-01-03 10:28:27 +00:00
lash
1b12f0ba5f Add po language alternative to all runners 2025-01-03 10:00:52 +00:00
lash
c1e0617bb3 Update go-vise 2025-01-02 21:13:06 +00:00
lash
6723884103 Update go-vise 2025-01-02 21:02:01 +00:00
lash
b888af446d update govise 2025-01-02 18:49:16 +00:00
lash
43b2c3b78d Rehabilitate gettext resource 2025-01-02 18:13:37 +00:00
lash
d67853f6d9 Merge branch 'master' into lash/gettext 2025-01-02 14:53:18 +00:00
lash
06230dc557 Add todo comment 2025-01-02 14:31:13 +00:00
lash
6ee2c88fe2 Implement gettext spec in local vm cmd 2025-01-02 09:39:49 +00:00
28 changed files with 548 additions and 43 deletions

View File

@@ -1,5 +1,6 @@
/**
!/cmd/africastalking
!/cmd/ssh
!/common
!/config
!/initializers

View File

@@ -18,3 +18,7 @@ DB_TIMEZONE=Africa/Nairobi
CUSTODIAL_URL_BASE=http://localhost:5003
BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr
DATA_URL_BASE=http://localhost:5006
#Language
DEFAULT_LANGUAGE=eng
LANGUAGES=eng, swa

View File

@@ -19,6 +19,7 @@ WORKDIR /build
RUN echo "Building on $BUILDPLATFORM, building for $TARGETPLATFORM"
RUN go mod download
RUN go build -tags logtrace -o ussd-africastalking -ldflags="-X main.build=${BUILD} -s -w" cmd/africastalking/main.go
RUN go build -tags logtrace -o ussd-ssh -ldflags="-X main.build=${BUILD} -s -w" cmd/ssh/main.go
FROM debian:bookworm-slim
@@ -30,6 +31,7 @@ RUN apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /service
COPY --from=build /build/ussd-africastalking .
COPY --from=build /build/ussd-ssh .
COPY --from=build /build/LICENSE .
COPY --from=build /build/README.md .
COPY --from=build /build/services ./services
@@ -37,5 +39,6 @@ COPY --from=build /build/.env.example .
RUN mv .env.example .env
EXPOSE 7123
EXPOSE 7122
CMD ["./ussd-africastalking"]

View File

@@ -14,6 +14,7 @@ import (
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/lang"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers"
@@ -22,6 +23,7 @@ import (
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"
)
var (
@@ -44,6 +46,8 @@ func main() {
var engineDebug bool
var host string
var port uint
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")
@@ -51,12 +55,21 @@ func main() {
flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
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", "build", build, "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
ctx := context.Background()
ctx = context.WithValue(ctx, "Database", database)
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
if err != nil {
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
os.Exit(1)
}
ctx = context.WithValue(ctx, "Language", ln)
pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{

View File

@@ -12,12 +12,14 @@ import (
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/lang"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/remote"
"git.grassecon.net/urdt/ussd/internal/args"
)
var (
@@ -54,6 +56,8 @@ func main() {
var engineDebug bool
var host string
var port uint
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")
@@ -62,12 +66,22 @@ func main() {
flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
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, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId)
ctx := context.Background()
ctx = context.WithValue(ctx, "Database", database)
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
if err != nil {
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
os.Exit(1)
}
ctx = context.WithValue(ctx, "Language", ln)
pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{

View File

@@ -14,6 +14,7 @@ import (
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/lang"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers"
@@ -21,6 +22,7 @@ import (
httpserver "git.grassecon.net/urdt/ussd/internal/http"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/remote"
"git.grassecon.net/urdt/ussd/internal/args"
)
var (
@@ -43,6 +45,8 @@ func main() {
var engineDebug bool
var host string
var port uint
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")
@@ -50,12 +54,22 @@ func main() {
flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
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, "resourcedir", resourceDir, "outputsize", size)
ctx := context.Background()
ctx = context.WithValue(ctx, "Database", database)
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
if err != nil {
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
os.Exit(1)
}
ctx = context.WithValue(ctx, "Language", ln)
pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{

View File

@@ -10,10 +10,12 @@ import (
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/lang"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/internal/args"
"git.grassecon.net/urdt/ussd/remote"
)
@@ -27,6 +29,7 @@ func init() {
initializers.LoadEnvVariables()
}
// TODO: external script automatically generate language handler list from select language vise code OR consider dynamic menu generation script possibility
func main() {
config.LoadConfig()
@@ -35,18 +38,34 @@ func main() {
var sessionId string
var database string
var engineDebug bool
var gettextDir string
var langs args.LangVar
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.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 len(langs.Langs()) == 0 {
langs.Set(config.DefaultLanguage)
}
ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId)
ctx = context.WithValue(ctx, "Database", database)
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
if err != nil {
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
os.Exit(1)
}
ctx = context.WithValue(ctx, "Language", ln)
pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{
@@ -59,8 +78,11 @@ func main() {
resourceDir := scriptDir
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
if gettextDir != "" {
menuStorageService = menuStorageService.WithGettext(gettextDir, langs.Langs())
}
err := menuStorageService.EnsureDbDir()
err = menuStorageService.EnsureDbDir()
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)

View File

@@ -4,9 +4,9 @@ import (
"context"
"flag"
"fmt"
"path"
"os"
"os/signal"
"path"
"sync"
"syscall"
@@ -18,10 +18,12 @@ import (
)
var (
wg sync.WaitGroup
keyStore db.Db
wg sync.WaitGroup
keyStore db.Db
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
build = "dev"
)
func main() {
@@ -76,7 +78,7 @@ func main() {
fmt.Fprintf(os.Stderr, "keystore file open error: %v", err)
os.Exit(1)
}
defer func () {
defer func() {
logg.TraceCtxf(ctx, "shutdown auth key store reached")
err = authKeyStore.Close()
if err != nil {
@@ -90,14 +92,14 @@ 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,
DbDir: dbDir,
ResourceDir: resourceDir,
SrvKeyFile: sshKeyFile,
Host: host,
Port: port,
SrvKeyFile: sshKeyFile,
Host: host,
Port: port,
}
go func() {
select {
@@ -109,7 +111,7 @@ func main() {
if err != nil {
logg.ErrorCtxf(ctx, "runner stop error", "err", err)
}
}()
runner.Run(ctx, authKeyStore)
}

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package config
import (
"net/url"
"strings"
"git.grassecon.net/urdt/ussd/initializers"
)
@@ -18,6 +19,11 @@ const (
AliasPrefix = "api/v1/alias"
)
var (
defaultLanguage = "eng"
languages []string
)
var (
custodialURLBase string
dataURLBase string
@@ -34,8 +40,28 @@ var (
VoucherTransfersURL string
VoucherDataURL string
CheckAliasURL string
DefaultLanguage string
Languages []string
)
func setLanguage() error {
defaultLanguage = initializers.GetEnv("DEFAULT_LANGUAGE", defaultLanguage)
languages = strings.Split(initializers.GetEnv("LANGUAGES", defaultLanguage), ",")
haveDefaultLanguage := false
for i, v := range(languages) {
languages[i] = strings.ReplaceAll(v, " ", "")
if languages[i] == defaultLanguage {
haveDefaultLanguage = true
}
}
if !haveDefaultLanguage {
languages = append([]string{defaultLanguage}, languages...)
}
return nil
}
func setBase() error {
var err error
@@ -60,6 +86,10 @@ func LoadConfig() error {
if err != nil {
return err
}
err = setLanguage()
if err != nil {
return err
}
CreateAccountURL, _ = url.JoinPath(custodialURLBase, createAccountPath)
TrackStatusURL, _ = url.JoinPath(custodialURLBase, trackStatusPath)
BalanceURL, _ = url.JoinPath(custodialURLBase, balancePathPrefix)
@@ -69,6 +99,8 @@ func LoadConfig() error {
VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix)
VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix)
CheckAliasURL, _ = url.JoinPath(dataURLBase, AliasPrefix)
DefaultLanguage = defaultLanguage
Languages = languages
return nil
}

126
devtools/lang/main.go Normal file
View File

@@ -0,0 +1,126 @@
// create language files from environment
package main
import (
"flag"
"fmt"
"os"
"path"
"strings"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/lang"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers"
)
const (
changeHeadSrc = `LOAD reset_account_authorized 0
LOAD reset_incorrect 0
CATCH incorrect_pin flag_incorrect_pin 1
CATCH pin_entry flag_account_authorized 0
`
selectSrc = `LOAD set_language 6
RELOAD set_language
CATCH terms flag_account_created 0
MOVE language_changed
`
)
var (
logg = logging.NewVanilla()
mouts string
incmps string
)
func init() {
initializers.LoadEnvVariables()
}
func toLanguageLabel(ln lang.Language) string {
s := ln.Name
v := strings.Split(s, " (")
if len(v) > 1 {
s = v[0]
}
return s
}
func toLanguageKey(ln lang.Language) string {
s := toLanguageLabel(ln)
return strings.ToLower(s)
}
func main() {
var srcDir string
flag.StringVar(&srcDir, "o", ".", "resource dir write to")
flag.Parse()
logg.Infof("start command", "dir", srcDir)
err := config.LoadConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "config load error: %v", err)
os.Exit(1)
}
logg.Tracef("using languages", "lang", config.Languages)
for i, v := range(config.Languages) {
ln, err := lang.LanguageFromCode(v)
if err != nil {
fmt.Fprintf(os.Stderr, "error parsing language: %s\n", v)
os.Exit(1)
}
n := i + 1
s := toLanguageKey(ln)
mouts += fmt.Sprintf("MOUT %s %v\n", s, n)
v = "set_" + ln.Code
incmps += fmt.Sprintf("INCMP %s %v\n", v, n)
p := path.Join(srcDir, v)
w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600)
if err != nil {
fmt.Fprintf(os.Stderr, "failed open language set template output: %v\n", err)
os.Exit(1)
}
s = toLanguageLabel(ln)
defer w.Close()
_, err = w.Write([]byte(s))
if err != nil {
fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err)
os.Exit(1)
}
}
src := mouts + "HALT\n" + incmps
src += "INCMP . *\n"
p := path.Join(srcDir, "select_language.vis")
w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600)
if err != nil {
fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err)
os.Exit(1)
}
defer w.Close()
_, err = w.Write([]byte(src))
if err != nil {
fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err)
os.Exit(1)
}
src = changeHeadSrc + src
p = path.Join(srcDir, "change_language.vis")
w, err = os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600)
if err != nil {
fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err)
os.Exit(1)
}
defer w.Close()
_, err = w.Write([]byte(src))
if err != nil {
fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err)
os.Exit(1)
}
}

View File

@@ -25,6 +25,15 @@ func init() {
}
func formatItem(k []byte, v []byte) (string, error) {
o, err := debug.FromKey(k)
if err != nil {
return "", err
}
s := fmt.Sprintf("%vValue: %v\n\n", o, string(v))
return s, nil
}
func main() {
config.LoadConfig()
@@ -64,12 +73,12 @@ func main() {
if k == nil {
break
}
o, err := debug.FromKey(k)
r, err := formatItem(k, v)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
fmt.Fprintf(os.Stderr, "format db item error: %v", err)
os.Exit(1)
}
fmt.Printf("%vValue: %v\n\n", o, string(v))
fmt.Printf(r)
}
err = store.Close()

34
internal/args/lang.go Normal file
View File

@@ -0,0 +1,34 @@
package args
import (
"strings"
"git.defalsify.org/vise.git/lang"
)
type LangVar struct {
v []lang.Language
}
func(lv *LangVar) Set(s string) error {
v, err := lang.LanguageFromCode(s)
if err != nil {
return err
}
lv.v = append(lv.v, v)
return err
}
func(lv *LangVar) String() string {
var s []string
for _, v := range(lv.v) {
s = append(s, v.Code)
}
return strings.Join(s, ",")
}
func(lv *LangVar) Langs() []lang.Language {
return lv.v
}

View File

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

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) {
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
}
@@ -835,11 +873,21 @@ func (h *Handlers) QuitWithHelp(ctx context.Context, sym string, input []byte) (
l := gotext.NewLocale(translationDir, code)
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)
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
}

View File

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

View File

@@ -81,7 +81,8 @@ func (arp *ATRequestParser) GetInput(rq any) ([]byte, error) {
return nil, fmt.Errorf("no input found")
}
return []byte(parts[len(parts)-1]), nil
trimmedInput := strings.TrimSpace(parts[len(parts)-1])
return []byte(trimmedInput), nil
}
func parseQueryParams(query string) map[string]string {

View File

@@ -9,6 +9,7 @@ import (
"git.defalsify.org/vise.git/db"
fsdb "git.defalsify.org/vise.git/db/fs"
"git.defalsify.org/vise.git/db/postgres"
"git.defalsify.org/vise.git/lang"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource"
@@ -30,6 +31,7 @@ type StorageService interface {
type MenuStorageService struct {
dbDir string
resourceDir string
poResource resource.Resource
resourceStore db.Db
stateStore db.Db
userDataStore db.Db
@@ -58,6 +60,28 @@ func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService
}
}
// 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
// language strings.
//
// If `lns` is an empty array, gettext will not be used.
func (ms *MenuStorageService) WithGettext(path string, lns []lang.Language) *MenuStorageService {
if len(lns) == 0 {
logg.Warnf("Gettext requested but no languages supplied")
return ms
}
rs := resource.NewPoResource(lns[0], path)
for _, ln := range(lns) {
rs = rs.WithLanguage(ln)
}
ms.poResource = rs
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 {
@@ -120,6 +144,11 @@ func (ms *MenuStorageService) GetResource(ctx context.Context) (resource.Resourc
return nil, err
}
rfs := resource.NewDbResource(ms.resourceStore)
if ms.poResource != nil {
logg.InfoCtxf(ctx, "using poresource for menu and template")
rfs.WithMenuGetter(ms.poResource.GetMenu)
rfs.WithTemplateGetter(ms.poResource.GetTemplate)
}
return rfs, nil
}

View File

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

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
RELOAD reset_incorrect
MAP reset_incorrect
CATCH blocked_account flag_account_blocked 1
MOUT retry 1
MOUT quit 9
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!"
msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!"
msgid "For more help,please call: 0757628885"
msgstr "Kwa usaidizi zaidi,piga: 0757628885"
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"

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_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
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 terms flag_account_created 0
LOAD check_account_status 0