forked from urdt/ussd
Compare commits
179 Commits
profile-ed
...
master
Author | SHA1 | Date | |
---|---|---|---|
f40e11c267 | |||
b698f08136 | |||
efdb52bccd | |||
2ff9fed3c5 | |||
477b4cf8f6 | |||
ed6651697a | |||
c359d99075 | |||
8d477356f3 | |||
7f3294a8a2 | |||
f49e54a562 | |||
|
5081b6d4ce | ||
4d72ae0313 | |||
4fe64a7747 | |||
3004698d5b | |||
50c7ff1046 | |||
07b061a68b | |||
64fba91670 | |||
c15958a1ad | |||
ee442daefa | |||
656052dc74 | |||
6c5873da6f | |||
11d30583a4 | |||
f83f539046 | |||
562bd4fa24 | |||
90df0eefc3 | |||
b37f2a0a11 | |||
68e4c9af03 | |||
80b96e9bf6 | |||
b5561decd1 | |||
f3d4f35718 | |||
52787bdb4d | |||
824d39908b | |||
a312ea5b84 | |||
4836162f40 | |||
|
2024cc96e2 | ||
|
d2d878d5d7 | ||
c995143543 | |||
44570e20ef | |||
362eb209ef | |||
c69d3896f1 | |||
974af6b2a7 | |||
|
bb4037e73f | ||
|
83857026d3 | ||
|
349051b5ef | ||
47b5ff0435 | |||
|
25867cf05e | ||
d5a2680500 | |||
|
d950b10b50 | ||
|
bcb3ab905e | ||
|
3ed9caf16d | ||
|
86464c31d2 | ||
5ee10d8e14 | |||
62f3681b9e | |||
3ce1435591 | |||
f65c458daa | |||
|
67007fcd48 | ||
|
f1b258fa6d | ||
|
daec816a3e | ||
|
ac0c43cb43 | ||
|
9013cc3618 | ||
|
056d056613 | ||
|
e581ec4771 | ||
|
e16b7445e8 | ||
|
1b12f0ba5f | ||
d2fce05461 | |||
68ac237449 | |||
162e6c1934 | |||
8bd025f2b2 | |||
9d6e25e184 | |||
c26f5683f6 | |||
91dc9ce82f | |||
0fe48a30fa | |||
|
c1e0617bb3 | ||
|
6723884103 | ||
|
b888af446d | ||
|
43b2c3b78d | ||
|
d67853f6d9 | ||
58edfa01a2 | |||
3830c12a57 | |||
f1fd690a7b | |||
|
06230dc557 | ||
491b7424a9 | |||
29ce4b83bd | |||
ca8df5989a | |||
82b4365d16 | |||
98db85511b | |||
99a4d3ff42 | |||
d95c7abea4 | |||
fd1ac85a1b | |||
c899c098f6 | |||
5ca6a74274 | |||
48d63fb43f | |||
|
6ee2c88fe2 | ||
e666c58644 | |||
e980586910 | |||
ffd5be1f1f | |||
ed1aeecf7d | |||
3b69f3d38d | |||
|
cd58f5ae33 | ||
7a535f796a | |||
7c4c73125e | |||
|
c7dbe1d88f | ||
4ea52bf3fb | |||
be2ea3a2f0 | |||
8217ea8fdc | |||
3c73fc7188 | |||
1311a0cab9 | |||
|
3bcd48e5a7 | ||
|
0e12c0ee4e | ||
3caee98cdb | |||
db7c9bf56d | |||
0a332ec501 | |||
90367fe53e | |||
50c006546c | |||
e8c171a82e | |||
58a60f2c81 | |||
0820e1b9f2 | |||
46edf2b819 | |||
11eb61ba35 | |||
813b92af78 | |||
5579991d66 | |||
f4f4fdd3ac | |||
be215d3f75 | |||
235af3519d | |||
1292851226 | |||
dfd0a0994b | |||
97fcdda12f | |||
055c2db790 | |||
ecfdab47a8 | |||
fda68231ea | |||
d08afff443 | |||
17ba6a06ba | |||
dbd59a4023 | |||
5534706189 | |||
5428626c3f | |||
9b33117cb1 | |||
70b2fa4ac2 | |||
fd6ff86579 | |||
549782f230 | |||
8cf4848b45 | |||
9f6c0a1111 | |||
|
1ab49647f6 | ||
|
8d4d8a48e0 | ||
7aea2af9a1 | |||
5cd791aae7 | |||
df5e5f1a4b | |||
64c1fe5276 | |||
f38ea59569 | |||
6cc285d1e8 | |||
0d7f7aaca1 | |||
f8ea2daa73 | |||
|
c820e89cb7 | ||
e05f8e7291 | |||
2383e8ead3 | |||
1a4ee0d3e1 | |||
6f3b30e2fe | |||
b1e4b63c6a | |||
3129e8210e | |||
5d8de80a18 | |||
|
604c16ec90 | ||
|
a3e821fb16 | ||
|
890f50704f | ||
|
3416fdf50c | ||
43892f0d8c | |||
|
ff943a125c | ||
|
c2a4efde2b | ||
|
c9deca1180 | ||
|
6d4f3109f8 | ||
|
35cf3a1cd1 | ||
|
1a782c1db9 | ||
|
3d2ca606ca | ||
584d02db29 | |||
5937c6bf5c | |||
|
10b3083647 | ||
|
bb1a846cb3 | ||
|
967e53d83b | ||
|
d246cdee51 | ||
|
d518a76536 | ||
|
6f65c33be4 |
@ -1,5 +1,6 @@
|
||||
/**
|
||||
!/cmd/africastalking
|
||||
!/cmd/ssh
|
||||
!/common
|
||||
!/config
|
||||
!/initializers
|
||||
|
@ -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
|
||||
|
@ -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"]
|
@ -1,110 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"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/common"
|
||||
"git.grassecon.net/urdt/ussd/config"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
httpserver "git.grassecon.net/urdt/ussd/internal/http"
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla()
|
||||
logg = logging.NewVanilla().WithDomain("AfricasTalking").WithContextKey("at-session-id")
|
||||
scriptDir = path.Join("services", "registration")
|
||||
|
||||
build = "dev"
|
||||
menuSeparator = ": "
|
||||
)
|
||||
|
||||
func init() {
|
||||
initializers.LoadEnvVariables()
|
||||
}
|
||||
|
||||
type atRequestParser struct{}
|
||||
|
||||
func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
log.Printf("got an invalid request:", rq)
|
||||
return "", handlers.ErrInvalidRequest
|
||||
}
|
||||
|
||||
// Capture body (if any) for logging
|
||||
body, err := io.ReadAll(rqv.Body)
|
||||
if err != nil {
|
||||
log.Printf("failed to read request body:", err)
|
||||
return "", fmt.Errorf("failed to read request body: %v", err)
|
||||
}
|
||||
// Reset the body for further reading
|
||||
rqv.Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
// Log the body as JSON
|
||||
bodyLog := map[string]string{"body": string(body)}
|
||||
logBytes, err := json.Marshal(bodyLog)
|
||||
if err != nil {
|
||||
log.Printf("failed to marshal request body:", err)
|
||||
} else {
|
||||
log.Printf("Received request:", string(logBytes))
|
||||
}
|
||||
|
||||
if err := rqv.ParseForm(); err != nil {
|
||||
log.Printf("failed to parse form data: %v", err)
|
||||
return "", fmt.Errorf("failed to parse form data: %v", err)
|
||||
}
|
||||
|
||||
phoneNumber := rqv.FormValue("phoneNumber")
|
||||
if phoneNumber == "" {
|
||||
return "", fmt.Errorf("no phone number found")
|
||||
}
|
||||
|
||||
formattedNumber, err := common.FormatPhoneNumber(phoneNumber)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
return "", fmt.Errorf("failed to format number")
|
||||
}
|
||||
|
||||
return formattedNumber, nil
|
||||
}
|
||||
|
||||
func (arp *atRequestParser) GetInput(rq any) ([]byte, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
return nil, handlers.ErrInvalidRequest
|
||||
}
|
||||
if err := rqv.ParseForm(); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse form data: %v", err)
|
||||
}
|
||||
|
||||
text := rqv.FormValue("text")
|
||||
|
||||
parts := strings.Split(text, "*")
|
||||
if len(parts) == 0 {
|
||||
return nil, fmt.Errorf("no input found")
|
||||
}
|
||||
|
||||
return []byte(parts[len(parts)-1]), nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
config.LoadConfig()
|
||||
|
||||
@ -115,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")
|
||||
@ -122,18 +55,28 @@ 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{
|
||||
Root: "root",
|
||||
OutputSize: uint32(size),
|
||||
FlagCount: uint32(128),
|
||||
MenuSeparator: menuSeparator,
|
||||
}
|
||||
|
||||
if engineDebug {
|
||||
@ -191,7 +134,7 @@ func main() {
|
||||
}
|
||||
defer stateStore.Close()
|
||||
|
||||
rp := &atRequestParser{}
|
||||
rp := &at.ATRequestParser{}
|
||||
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
|
||||
sh := httpserver.NewATSessionHandler(bsh)
|
||||
|
||||
|
@ -12,17 +12,20 @@ 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 (
|
||||
logg = logging.NewVanilla()
|
||||
scriptDir = path.Join("services", "registration")
|
||||
menuSeparator = ": "
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -34,7 +37,7 @@ type asyncRequestParser struct {
|
||||
input []byte
|
||||
}
|
||||
|
||||
func (p *asyncRequestParser) GetSessionId(r any) (string, error) {
|
||||
func (p *asyncRequestParser) GetSessionId(ctx context.Context, r any) (string, error) {
|
||||
return p.sessionId, nil
|
||||
}
|
||||
|
||||
@ -53,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")
|
||||
@ -61,18 +66,29 @@ 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{
|
||||
Root: "root",
|
||||
OutputSize: uint32(size),
|
||||
FlagCount: uint32(128),
|
||||
MenuSeparator: menuSeparator,
|
||||
}
|
||||
|
||||
if engineDebug {
|
||||
|
@ -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,11 +22,13 @@ 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 (
|
||||
logg = logging.NewVanilla()
|
||||
scriptDir = path.Join("services", "registration")
|
||||
menuSeparator = ": "
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -42,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")
|
||||
@ -49,18 +54,29 @@ 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{
|
||||
Root: "root",
|
||||
OutputSize: uint32(size),
|
||||
FlagCount: uint32(128),
|
||||
MenuSeparator: menuSeparator,
|
||||
}
|
||||
|
||||
if engineDebug {
|
||||
|
26
cmd/main.go
26
cmd/main.go
@ -10,22 +10,26 @@ 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"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla()
|
||||
scriptDir = path.Join("services", "registration")
|
||||
menuSeparator = ": "
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
@ -34,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{
|
||||
@ -53,12 +73,16 @@ func main() {
|
||||
SessionId: sessionId,
|
||||
OutputSize: uint32(size),
|
||||
FlagCount: uint32(128),
|
||||
MenuSeparator: menuSeparator,
|
||||
}
|
||||
|
||||
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)
|
||||
|
34
cmd/ssh/README.md
Normal file
34
cmd/ssh/README.md
Normal file
@ -0,0 +1,34 @@
|
||||
# URDT-USSD SSH server
|
||||
|
||||
An SSH server entry point for the vise engine.
|
||||
|
||||
|
||||
## Adding public keys for access
|
||||
|
||||
Map your (client) public key to a session identifier (e.g. phone number)
|
||||
|
||||
```
|
||||
go run -v -tags logtrace ./cmd/ssh/sshkey/main.go -i <session_id> [--dbdir <dbpath>] <client_publickey_filepath>
|
||||
```
|
||||
|
||||
|
||||
## Create a private key for the server
|
||||
|
||||
```
|
||||
ssh-keygen -N "" -f <server_privatekey_filepath>
|
||||
```
|
||||
|
||||
|
||||
## Run the server
|
||||
|
||||
|
||||
```
|
||||
go run -v -tags logtrace ./cmd/ssh/main.go -h <host> -p <port> [--dbdir <dbpath>] <server_privatekey_filepath>
|
||||
```
|
||||
|
||||
|
||||
## Connect to the server
|
||||
|
||||
```
|
||||
ssh [-v] -T -p <port> -i <client_publickey_filepath> <host>
|
||||
```
|
117
cmd/ssh/main.go
Normal file
117
cmd/ssh/main.go
Normal file
@ -0,0 +1,117 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"git.defalsify.org/vise.git/db"
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/ssh"
|
||||
)
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
keyStore db.Db
|
||||
logg = logging.NewVanilla()
|
||||
scriptDir = path.Join("services", "registration")
|
||||
|
||||
build = "dev"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var dbDir 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(&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.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.Parse()
|
||||
|
||||
sshKeyFile := flag.Arg(0)
|
||||
_, err := os.Stat(sshKeyFile)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "cannot open ssh server private key file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
logg.WarnCtxf(ctx, "!!!!! WARNING WARNING WARNING")
|
||||
logg.WarnCtxf(ctx, "!!!!! =======================")
|
||||
logg.WarnCtxf(ctx, "!!!!! This is not a production ready server!")
|
||||
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)
|
||||
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
|
||||
cfg := engine.Config{
|
||||
Root: "root",
|
||||
OutputSize: uint32(size),
|
||||
FlagCount: uint32(16),
|
||||
}
|
||||
if stateDebug {
|
||||
cfg.StateDebug = true
|
||||
}
|
||||
if engineDebug {
|
||||
cfg.EngineDebug = true
|
||||
}
|
||||
|
||||
authKeyStore, err := ssh.NewSshKeyStore(ctx, dbDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "keystore file open error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() {
|
||||
logg.TraceCtxf(ctx, "shutdown auth key store reached")
|
||||
err = authKeyStore.Close()
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "keystore close error", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
cint := make(chan os.Signal)
|
||||
cterm := make(chan os.Signal)
|
||||
signal.Notify(cint, os.Interrupt, syscall.SIGINT)
|
||||
signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
runner := &ssh.SshRunner{
|
||||
Cfg: cfg,
|
||||
Debug: engineDebug,
|
||||
FlagFile: pfp,
|
||||
DbDir: dbDir,
|
||||
ResourceDir: resourceDir,
|
||||
SrvKeyFile: sshKeyFile,
|
||||
Host: host,
|
||||
Port: port,
|
||||
}
|
||||
go func() {
|
||||
select {
|
||||
case _ = <-cint:
|
||||
case _ = <-cterm:
|
||||
}
|
||||
logg.TraceCtxf(ctx, "shutdown runner reached")
|
||||
err := runner.Stop()
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "runner stop error", "err", err)
|
||||
}
|
||||
|
||||
}()
|
||||
runner.Run(ctx, authKeyStore)
|
||||
}
|
44
cmd/ssh/sshkey/main.go
Normal file
44
cmd/ssh/sshkey/main.go
Normal file
@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/ssh"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var dbDir string
|
||||
var sessionId string
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
flag.StringVar(&sessionId, "i", "", "session id")
|
||||
flag.Parse()
|
||||
|
||||
if sessionId == "" {
|
||||
fmt.Fprintf(os.Stderr, "empty session id\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
sshKeyFile := flag.Arg(0)
|
||||
if sshKeyFile == "" {
|
||||
fmt.Fprintf(os.Stderr, "missing key file argument\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
store, err := ssh.NewSshKeyStore(ctx, dbDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
err = store.AddFromFile(ctx, sshKeyFile, sessionId)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
56
common/db.go
56
common/db.go
@ -7,43 +7,85 @@ import (
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
)
|
||||
|
||||
// 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.
|
||||
//
|
||||
// * The first byte is vise/db.DATATYPE_USERDATA
|
||||
// * The last 2 bytes are the DataTyp value, big-endian.
|
||||
// * The intermediate bytes are the id of the user context.
|
||||
//
|
||||
// All values are strings
|
||||
type DataTyp uint16
|
||||
|
||||
const (
|
||||
DATA_ACCOUNT DataTyp = iota
|
||||
DATA_ACCOUNT_CREATED
|
||||
DATA_TRACKING_ID
|
||||
// API Tracking id to follow status of account creation
|
||||
DATA_TRACKING_ID = iota
|
||||
// EVM address returned from API on account creation
|
||||
DATA_PUBLIC_KEY
|
||||
DATA_CUSTODIAL_ID
|
||||
// Currently active PIN used to authenticate ussd state change requests
|
||||
DATA_ACCOUNT_PIN
|
||||
DATA_ACCOUNT_STATUS
|
||||
// The first name of the user
|
||||
DATA_FIRST_NAME
|
||||
// The last name of the user
|
||||
DATA_FAMILY_NAME
|
||||
// The year-of-birth of the user
|
||||
DATA_YOB
|
||||
// The location of the user
|
||||
DATA_LOCATION
|
||||
// The gender of the user
|
||||
DATA_GENDER
|
||||
// The offerings description of the user
|
||||
DATA_OFFERINGS
|
||||
// The ethereum address of the recipient of an ongoing send request
|
||||
DATA_RECIPIENT
|
||||
// The voucher value amount of an ongoing send request
|
||||
DATA_AMOUNT
|
||||
// A general swap field for temporary values
|
||||
DATA_TEMPORARY_VALUE
|
||||
// Currently active voucher symbol of user
|
||||
DATA_ACTIVE_SYM
|
||||
// Voucher balance of user's currently active voucher
|
||||
DATA_ACTIVE_BAL
|
||||
// String boolean indicating whether use of PIN is blocked
|
||||
DATA_BLOCKED_NUMBER
|
||||
// Reverse mapping of a user's evm address to a session id.
|
||||
DATA_PUBLIC_KEY_REVERSE
|
||||
// Decimal count of the currently active voucher
|
||||
DATA_ACTIVE_DECIMAL
|
||||
// EVM address of the currently active voucher
|
||||
DATA_ACTIVE_ADDRESS
|
||||
// Start the sub prefix data at 256 (0x0100)
|
||||
//Holds count of the number of incorrect PIN attempts
|
||||
DATA_INCORRECT_PIN_ATTEMPTS
|
||||
)
|
||||
|
||||
const (
|
||||
// List of valid voucher symbols in the user context.
|
||||
DATA_VOUCHER_SYMBOLS DataTyp = 256 + iota
|
||||
// List of voucher balances for vouchers valid in the user context.
|
||||
DATA_VOUCHER_BALANCES
|
||||
// List of voucher decimal counts for vouchers valid in the user context.
|
||||
DATA_VOUCHER_DECIMALS
|
||||
// List of voucher EVM addresses for vouchers valid in the user context.
|
||||
DATA_VOUCHER_ADDRESSES
|
||||
DATA_TX_SENDERS
|
||||
// List of senders for valid transactions in the user context.
|
||||
)
|
||||
|
||||
const (
|
||||
DATA_TX_SENDERS = 512 + iota
|
||||
// List of recipients for valid transactions in the user context.
|
||||
DATA_TX_RECIPIENTS
|
||||
// List of voucher values for valid transactions in the user context.
|
||||
DATA_TX_VALUES
|
||||
// List of voucher EVM addresses for valid transactions in the user context.
|
||||
DATA_TX_ADDRESSES
|
||||
// List of valid transaction hashes in the user context.
|
||||
DATA_TX_HASHES
|
||||
// List of transaction dates for valid transactions in the user context.
|
||||
DATA_TX_DATES
|
||||
// List of voucher symbols for valid transactions in the user context.
|
||||
DATA_TX_SYMBOLS
|
||||
// List of voucher decimal counts for valid transactions in the user context.
|
||||
DATA_TX_DECIMALS
|
||||
)
|
||||
|
||||
|
37
common/pin.go
Normal file
37
common/pin.go
Normal file
@ -0,0 +1,37 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
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
|
||||
func IsValidPIN(pin string) bool {
|
||||
match, _ := regexp.MatchString(pinPattern, pin)
|
||||
return match
|
||||
}
|
||||
|
||||
// HashPIN uses bcrypt with 8 salt rounds to hash the PIN
|
||||
func HashPIN(pin string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(pin), 8)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
// VerifyPIN compareS the hashed PIN with the plaintext PIN
|
||||
func VerifyPIN(hashedPIN, pin string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(pin))
|
||||
return err == nil
|
||||
}
|
173
common/pin_test.go
Normal file
173
common/pin_test.go
Normal file
@ -0,0 +1,173 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestIsValidPIN(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pin string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Valid PIN with 4 digits",
|
||||
pin: "1234",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Valid PIN with leading zeros",
|
||||
pin: "0001",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN with less than 4 digits",
|
||||
pin: "123",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN with more than 4 digits",
|
||||
pin: "12345",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN with letters",
|
||||
pin: "abcd",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN with special characters",
|
||||
pin: "12@#",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Empty PIN",
|
||||
pin: "",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual := IsValidPIN(tt.pin)
|
||||
if actual != tt.expected {
|
||||
t.Errorf("IsValidPIN(%q) = %v; expected %v", tt.pin, actual, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashPIN(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pin string
|
||||
}{
|
||||
{
|
||||
name: "Valid PIN with 4 digits",
|
||||
pin: "1234",
|
||||
},
|
||||
{
|
||||
name: "Valid PIN with leading zeros",
|
||||
pin: "0001",
|
||||
},
|
||||
{
|
||||
name: "Empty PIN",
|
||||
pin: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hashedPIN, err := HashPIN(tt.pin)
|
||||
if err != nil {
|
||||
t.Errorf("HashPIN(%q) returned an error: %v", tt.pin, err)
|
||||
return
|
||||
}
|
||||
|
||||
if hashedPIN == "" {
|
||||
t.Errorf("HashPIN(%q) returned an empty hash", tt.pin)
|
||||
}
|
||||
|
||||
// Ensure the hash can be verified with bcrypt
|
||||
err = bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(tt.pin))
|
||||
if tt.pin != "" && err != nil {
|
||||
t.Errorf("HashPIN(%q) produced a hash that does not match: %v", tt.pin, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyMigratedHashPin(t *testing.T) {
|
||||
tests := []struct {
|
||||
pin string
|
||||
hash string
|
||||
}{
|
||||
{
|
||||
pin: "1234",
|
||||
hash: "$2b$08$dTvIGxCCysJtdvrSnaLStuylPoOS/ZLYYkxvTeR5QmTFY3TSvPQC6",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.pin, func(t *testing.T) {
|
||||
ok := VerifyPIN(tt.hash, tt.pin)
|
||||
if !ok {
|
||||
t.Errorf("VerifyPIN could not verify migrated PIN: %v", tt.pin)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyPIN(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pin string
|
||||
hashedPIN string
|
||||
shouldPass bool
|
||||
}{
|
||||
{
|
||||
name: "Valid PIN verification",
|
||||
pin: "1234",
|
||||
hashedPIN: hashPINHelper("1234"),
|
||||
shouldPass: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN verification with incorrect PIN",
|
||||
pin: "5678",
|
||||
hashedPIN: hashPINHelper("1234"),
|
||||
shouldPass: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN verification with empty PIN",
|
||||
pin: "",
|
||||
hashedPIN: hashPINHelper("1234"),
|
||||
shouldPass: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN verification with invalid hash",
|
||||
pin: "1234",
|
||||
hashedPIN: "invalidhash",
|
||||
shouldPass: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := VerifyPIN(tt.hashedPIN, tt.pin)
|
||||
if result != tt.shouldPass {
|
||||
t.Errorf("VerifyPIN(%q, %q) = %v; expected %v", tt.hashedPIN, tt.pin, result, tt.shouldPass)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to hash a PIN for testing purposes
|
||||
func hashPINHelper(pin string) string {
|
||||
hashedPIN, err := HashPIN(pin)
|
||||
if err != nil {
|
||||
panic("Failed to hash PIN for test setup: " + err.Error())
|
||||
}
|
||||
return hashedPIN
|
||||
}
|
@ -8,14 +8,15 @@ import (
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||
)
|
||||
|
||||
func StoreToDb(store *UserDataStore) db.Db {
|
||||
return store.Db
|
||||
}
|
||||
|
||||
func StoreToPrefixDb(store *UserDataStore, pfx []byte) storage.PrefixDb {
|
||||
return storage.NewSubPrefixDb(store.Db, pfx)
|
||||
func StoreToPrefixDb(store *UserDataStore, pfx []byte) dbstorage.PrefixDb {
|
||||
return dbstorage.NewSubPrefixDb(store.Db, pfx)
|
||||
}
|
||||
|
||||
type StorageServices interface {
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||
)
|
||||
|
||||
@ -56,7 +56,7 @@ func ProcessTransfers(transfers []dataserviceapi.Last10TxResponse) TransferMetad
|
||||
|
||||
// GetTransferData retrieves and matches transfer data
|
||||
// returns a formatted string of the full transaction/statement
|
||||
func GetTransferData(ctx context.Context, db storage.PrefixDb, publicKey string, index int) (string, error) {
|
||||
func GetTransferData(ctx context.Context, db dbstorage.PrefixDb, publicKey string, index int) (string, error) {
|
||||
keys := []DataTyp{DATA_TX_SENDERS, DATA_TX_RECIPIENTS, DATA_TX_VALUES, DATA_TX_ADDRESSES, DATA_TX_HASHES, DATA_TX_DATES, DATA_TX_SYMBOLS}
|
||||
data := make(map[DataTyp]string)
|
||||
|
||||
@ -84,18 +84,18 @@ func GetTransferData(ctx context.Context, db storage.PrefixDb, publicKey string,
|
||||
|
||||
// Adjust for 0-based indexing
|
||||
i := index - 1
|
||||
transactionType := "received"
|
||||
party := fmt.Sprintf("from: %s", strings.TrimSpace(senders[i]))
|
||||
transactionType := "Received"
|
||||
party := fmt.Sprintf("From: %s", strings.TrimSpace(senders[i]))
|
||||
if strings.TrimSpace(senders[i]) == publicKey {
|
||||
transactionType = "sent"
|
||||
party = fmt.Sprintf("to: %s", strings.TrimSpace(recipients[i]))
|
||||
transactionType = "Sent"
|
||||
party = fmt.Sprintf("To: %s", strings.TrimSpace(recipients[i]))
|
||||
}
|
||||
|
||||
formattedDate := formatDate(strings.TrimSpace(dates[i]))
|
||||
|
||||
// Build the full transaction detail
|
||||
detail := fmt.Sprintf(
|
||||
"%s %s %s\n%s\ncontract address: %s\ntxhash: %s\ndate: %s",
|
||||
"%s %s %s\n%s\nContract address: %s\nTxhash: %s\nDate: %s",
|
||||
transactionType,
|
||||
strings.TrimSpace(values[i]),
|
||||
strings.TrimSpace(syms[i]),
|
||||
|
@ -20,7 +20,7 @@ type UserDataStore struct {
|
||||
func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) {
|
||||
store.SetPrefix(db.DATATYPE_USERDATA)
|
||||
store.SetSession(sessionId)
|
||||
k := PackKey(typ, []byte(sessionId))
|
||||
k := ToBytes(typ)
|
||||
return store.Get(ctx, k)
|
||||
}
|
||||
|
||||
@ -29,6 +29,6 @@ func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ
|
||||
func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error {
|
||||
store.SetPrefix(db.DATATYPE_USERDATA)
|
||||
store.SetSession(sessionId)
|
||||
k := PackKey(typ, []byte(sessionId))
|
||||
k := ToBytes(typ)
|
||||
return store.Put(ctx, k, value)
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||
)
|
||||
|
||||
@ -63,7 +63,7 @@ func ScaleDownBalance(balance, decimals string) string {
|
||||
}
|
||||
|
||||
// GetVoucherData retrieves and matches voucher data
|
||||
func GetVoucherData(ctx context.Context, db storage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
|
||||
func GetVoucherData(ctx context.Context, db dbstorage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
|
||||
keys := []DataTyp{DATA_VOUCHER_SYMBOLS, DATA_VOUCHER_BALANCES, DATA_VOUCHER_DECIMALS, DATA_VOUCHER_ADDRESSES}
|
||||
data := make(map[DataTyp]string)
|
||||
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
|
||||
visedb "git.defalsify.org/vise.git/db"
|
||||
memdb "git.defalsify.org/vise.git/db/mem"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||
)
|
||||
|
||||
@ -86,7 +86,7 @@ func TestGetVoucherData(t *testing.T) {
|
||||
}
|
||||
|
||||
prefix := ToBytes(visedb.DATATYPE_USERDATA)
|
||||
spdb := storage.NewSubPrefixDb(db, prefix)
|
||||
spdb := dbstorage.NewSubPrefixDb(db, prefix)
|
||||
|
||||
// Test voucher data
|
||||
mockData := map[DataTyp][]byte{
|
||||
|
@ -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
|
||||
}
|
||||
|
5
debug/cap.go
Normal file
5
debug/cap.go
Normal file
@ -0,0 +1,5 @@
|
||||
package debug
|
||||
|
||||
var (
|
||||
DebugCap uint32
|
||||
)
|
84
debug/db.go
Normal file
84
debug/db.go
Normal file
@ -0,0 +1,84 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"encoding/binary"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/common"
|
||||
"git.defalsify.org/vise.git/db"
|
||||
)
|
||||
|
||||
var (
|
||||
dbTypStr map[common.DataTyp]string = make(map[common.DataTyp]string)
|
||||
)
|
||||
|
||||
type KeyInfo struct {
|
||||
SessionId string
|
||||
Typ uint8
|
||||
SubTyp common.DataTyp
|
||||
Label string
|
||||
Description string
|
||||
}
|
||||
|
||||
func (k KeyInfo) String() string {
|
||||
v := uint16(k.SubTyp)
|
||||
s := subTypToString(k.SubTyp)
|
||||
if s == "" {
|
||||
v = uint16(k.Typ)
|
||||
s = typToString(k.Typ)
|
||||
}
|
||||
return fmt.Sprintf("Session Id: %s\nTyp: %s (%d)\n", k.SessionId, s, v)
|
||||
}
|
||||
|
||||
func ToKeyInfo(k []byte, sessionId string) (KeyInfo, error) {
|
||||
o := KeyInfo{}
|
||||
b := []byte(sessionId)
|
||||
|
||||
if len(k) <= len(b) {
|
||||
return o, fmt.Errorf("storage key missing")
|
||||
}
|
||||
|
||||
o.SessionId = sessionId
|
||||
|
||||
o.Typ = uint8(k[0])
|
||||
k = k[1:]
|
||||
o.SessionId = string(k[:len(b)])
|
||||
k = k[len(b):]
|
||||
|
||||
if o.Typ == db.DATATYPE_USERDATA {
|
||||
if len(k) == 0 {
|
||||
return o, fmt.Errorf("missing subtype key")
|
||||
}
|
||||
v := binary.BigEndian.Uint16(k[:2])
|
||||
o.SubTyp = common.DataTyp(v)
|
||||
o.Label = subTypToString(o.SubTyp)
|
||||
k = k[2:]
|
||||
} else {
|
||||
o.Label = typToString(o.Typ)
|
||||
}
|
||||
|
||||
if len(k) != 0 {
|
||||
return o, fmt.Errorf("excess key information")
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
func FromKey(k []byte) (KeyInfo, error) {
|
||||
o := KeyInfo{}
|
||||
|
||||
if len(k) < 4 {
|
||||
return o, fmt.Errorf("insufficient key length")
|
||||
}
|
||||
|
||||
sessionIdBytes := k[1:len(k)-2]
|
||||
return ToKeyInfo(k, string(sessionIdBytes))
|
||||
}
|
||||
|
||||
func subTypToString(v common.DataTyp) string {
|
||||
return dbTypStr[v + db.DATATYPE_USERDATA + 1]
|
||||
}
|
||||
|
||||
func typToString(v uint8) string {
|
||||
return dbTypStr[common.DataTyp(uint16(v))]
|
||||
}
|
44
debug/db_debug.go
Normal file
44
debug/db_debug.go
Normal file
@ -0,0 +1,44 @@
|
||||
// +build debugdb
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"git.defalsify.org/vise.git/db"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/common"
|
||||
)
|
||||
|
||||
func init() {
|
||||
DebugCap |= 1
|
||||
dbTypStr[db.DATATYPE_STATE] = "internal state"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TRACKING_ID] = "tracking id"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY] = "public key"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_PIN] = "account pin"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FIRST_NAME] = "first name"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FAMILY_NAME] = "family name"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_YOB] = "year of birth"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_LOCATION] = "location"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_GENDER] = "gender"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_OFFERINGS] = "offerings"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_RECIPIENT] = "recipient"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_AMOUNT] = "amount"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TEMPORARY_VALUE] = "temporary value"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_SYM] = "active sym"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_BAL] = "active bal"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_BLOCKED_NUMBER] = "blocked number"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY_REVERSE] = "public_key_reverse"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_DECIMAL] = "active decimal"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_ADDRESS] = "active address"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_SYMBOLS] = "voucher symbols"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_BALANCES] = "voucher balances"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_DECIMALS] = "voucher decimals"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_ADDRESSES] = "voucher addresses"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_SENDERS] = "tx senders"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_RECIPIENTS] = "tx recipients"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_VALUES] = "tx values"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_ADDRESSES] = "tx addresses"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_HASHES] = "tx hashes"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_DATES] = "tx dates"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_SYMBOLS] = "tx symbols"
|
||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_DECIMALS] = "tx decimals"
|
||||
}
|
78
debug/db_test.go
Normal file
78
debug/db_test.go
Normal file
@ -0,0 +1,78 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/common"
|
||||
"git.defalsify.org/vise.git/db"
|
||||
)
|
||||
|
||||
func TestDebugDbSubKeyInfo(t *testing.T) {
|
||||
s := "foo"
|
||||
b := []byte{0x20}
|
||||
b = append(b, []byte(s)...)
|
||||
b = append(b, []byte{0x00, 0x02}...)
|
||||
r, err := ToKeyInfo(b, s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.SessionId != s {
|
||||
t.Fatalf("expected %s, got %s", s, r.SessionId)
|
||||
}
|
||||
if r.Typ != 32 {
|
||||
t.Fatalf("expected 64, got %d", r.Typ)
|
||||
}
|
||||
if r.SubTyp != 2 {
|
||||
t.Fatalf("expected 2, got %d", r.SubTyp)
|
||||
}
|
||||
if DebugCap & 1 > 0 {
|
||||
if r.Label != "tracking id" {
|
||||
t.Fatalf("expected 'tracking id', got '%s'", r.Label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebugDbKeyInfo(t *testing.T) {
|
||||
s := "bar"
|
||||
b := []byte{0x10}
|
||||
b = append(b, []byte(s)...)
|
||||
r, err := ToKeyInfo(b, s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.SessionId != s {
|
||||
t.Fatalf("expected %s, got %s", s, r.SessionId)
|
||||
}
|
||||
if r.Typ != 16 {
|
||||
t.Fatalf("expected 16, got %d", r.Typ)
|
||||
}
|
||||
if DebugCap & 1 > 0 {
|
||||
if r.Label != "internal state" {
|
||||
t.Fatalf("expected 'internal_state', got '%s'", r.Label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebugDbKeyInfoRestore(t *testing.T) {
|
||||
s := "bar"
|
||||
b := []byte{db.DATATYPE_USERDATA}
|
||||
b = append(b, []byte(s)...)
|
||||
k := common.ToBytes(common.DATA_ACTIVE_SYM)
|
||||
b = append(b, k...)
|
||||
|
||||
r, err := ToKeyInfo(b, s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.SessionId != s {
|
||||
t.Fatalf("expected %s, got %s", s, r.SessionId)
|
||||
}
|
||||
if r.Typ != 32 {
|
||||
t.Fatalf("expected 32, got %d", r.Typ)
|
||||
}
|
||||
if DebugCap & 1 > 0 {
|
||||
if r.Label != "active sym" {
|
||||
t.Fatalf("expected 'active sym', got '%s'", r.Label)
|
||||
}
|
||||
}
|
||||
}
|
126
devtools/lang/main.go
Normal file
126
devtools/lang/main.go
Normal 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)
|
||||
}
|
||||
}
|
89
devtools/store/dump/main.go
Normal file
89
devtools/store/dump/main.go
Normal file
@ -0,0 +1,89 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/config"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
"git.grassecon.net/urdt/ussd/debug"
|
||||
"git.defalsify.org/vise.git/db"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla()
|
||||
scriptDir = path.Join("services", "registration")
|
||||
)
|
||||
|
||||
func init() {
|
||||
initializers.LoadEnvVariables()
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
|
||||
var dbDir string
|
||||
var sessionId string
|
||||
var database string
|
||||
var engineDebug bool
|
||||
|
||||
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.Parse()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
|
||||
resourceDir := scriptDir
|
||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
||||
|
||||
store, err := menuStorageService.GetUserdataDb(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "get userdata db: %v\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
store.SetPrefix(db.DATATYPE_USERDATA)
|
||||
|
||||
d, err := store.Dump(ctx, []byte(sessionId))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "store dump fail: %v\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for true {
|
||||
k, v := d.Next(ctx)
|
||||
if k == nil {
|
||||
break
|
||||
}
|
||||
r, err := formatItem(k, v)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "format db item error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf(r)
|
||||
}
|
||||
|
||||
err = store.Close()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
79
devtools/store/generate/main.go
Normal file
79
devtools/store/generate/main.go
Normal file
@ -0,0 +1,79 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.grassecon.net/urdt/ussd/config"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
"git.grassecon.net/urdt/ussd/common"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla()
|
||||
scriptDir = path.Join("services", "registration")
|
||||
)
|
||||
|
||||
func init() {
|
||||
initializers.LoadEnvVariables()
|
||||
}
|
||||
|
||||
|
||||
func main() {
|
||||
config.LoadConfig()
|
||||
|
||||
var dbDir string
|
||||
var sessionId string
|
||||
var database string
|
||||
var engineDebug bool
|
||||
|
||||
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.Parse()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
|
||||
resourceDir := scriptDir
|
||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
||||
|
||||
store, err := menuStorageService.GetUserdataDb(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
userStore := common.UserDataStore{store}
|
||||
|
||||
h := sha1.New()
|
||||
h.Write([]byte(sessionId))
|
||||
address := h.Sum(nil)
|
||||
addressString := fmt.Sprintf("%x", address)
|
||||
|
||||
err = userStore.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(addressString))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = userStore.WriteEntry(ctx, addressString, common.DATA_PUBLIC_KEY_REVERSE, []byte(sessionId))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = store.Close()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
}
|
28
doc/data.md
Normal file
28
doc/data.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Internals
|
||||
|
||||
## Version
|
||||
|
||||
This document describes component versions:
|
||||
|
||||
* `urdt-ussd` `v0.5.0-beta`
|
||||
* `go-vise` `v0.2.2`
|
||||
|
||||
|
||||
## User profile data
|
||||
|
||||
All user profile items are stored under keys matching the user's session id, prefixed with the 8-bit value `git.defalsify.org/vise.git/db.DATATYPE_USERDATA` (32), and followed with a 16-big big-endian value subprefix.
|
||||
|
||||
For example, given the sessionId `+254123` and the key `git.grassecon.net/urdt-ussd/common.DATA_PUBLIC_KEY` (2) will be stored under the key:
|
||||
|
||||
```
|
||||
0x322b3235343132330002
|
||||
|
||||
prefix sessionid subprefix
|
||||
32 2b323534313233 0002
|
||||
```
|
||||
|
||||
### Sub-prefixes
|
||||
|
||||
All sub-prefixes are defined as constants in the `git.grassecon.net/urdt-ussd/common` package. The constant names have the prefix `DATA_`
|
||||
|
||||
Please refer to inline godoc documentation for the `git.grassecon.net/urdt-ussd/common` package for details on each data item.
|
4
go.mod
4
go.mod
@ -3,7 +3,7 @@ module git.grassecon.net/urdt/ussd
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a
|
||||
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d
|
||||
github.com/alecthomas/assert/v2 v2.2.2
|
||||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/grassrootseconomics/eth-custodial v1.3.0-beta
|
||||
@ -11,6 +11,7 @@ require (
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/peteole/testdata-loader v0.3.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/crypto v0.27.0
|
||||
gopkg.in/leonelquinteros/gotext.v1 v1.3.1
|
||||
)
|
||||
|
||||
@ -32,7 +33,6 @@ require (
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/crypto v0.27.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
4
go.sum
4
go.sum
@ -1,5 +1,5 @@
|
||||
git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a h1:LvGKktk0kUnuRN3nF9r15D8OoV0sFaMmQr52kGq2gtE=
|
||||
git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
|
||||
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d h1:bPAOVZOX4frSGhfOdcj7kc555f8dc9DmMd2YAyC2AMw=
|
||||
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
|
||||
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
|
||||
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
|
||||
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=
|
||||
|
34
internal/args/lang.go
Normal file
34
internal/args/lang.go
Normal 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
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.defalsify.org/vise.git/asm"
|
||||
"git.defalsify.org/vise.git/db"
|
||||
@ -64,7 +65,11 @@ func (ls *LocalHandlerService) SetDataStore(db *db.Db) {
|
||||
}
|
||||
|
||||
func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*ussd.Handlers, error) {
|
||||
ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService)
|
||||
replaceSeparatorFunc := func(input string) string {
|
||||
return strings.ReplaceAll(input, ":", ls.Cfg.MenuSeparator)
|
||||
}
|
||||
|
||||
ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService, replaceSeparatorFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -111,7 +116,7 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn
|
||||
ls.DbRs.AddLocalFunc("set_voucher", ussdHandlers.SetVoucher)
|
||||
ls.DbRs.AddLocalFunc("get_voucher_details", ussdHandlers.GetVoucherDetails)
|
||||
ls.DbRs.AddLocalFunc("reset_valid_pin", ussdHandlers.ResetValidPin)
|
||||
ls.DbRs.AddLocalFunc("check_pin_mismatch", ussdHandlers.CheckPinMisMatch)
|
||||
ls.DbRs.AddLocalFunc("check_pin_mismatch", ussdHandlers.CheckBlockedNumPinMisMatch)
|
||||
ls.DbRs.AddLocalFunc("validate_blocked_number", ussdHandlers.ValidateBlockedNumber)
|
||||
ls.DbRs.AddLocalFunc("retrieve_blocked_number", ussdHandlers.RetrieveBlockedNumber)
|
||||
ls.DbRs.AddLocalFunc("reset_unregistered_number", ussdHandlers.ResetUnregisteredNumber)
|
||||
@ -123,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
|
||||
}
|
||||
|
@ -6,9 +6,9 @@ import (
|
||||
"io"
|
||||
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
)
|
||||
@ -39,7 +39,7 @@ type RequestSession struct {
|
||||
|
||||
// TODO: seems like can remove this.
|
||||
type RequestParser interface {
|
||||
GetSessionId(rq any) (string, error)
|
||||
GetSessionId(context context.Context, rq any) (string, error)
|
||||
GetInput(rq any) ([]byte, error)
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -24,27 +23,16 @@ import (
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
"gopkg.in/leonelquinteros/gotext.v1"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla().WithDomain("ussdmenuhandler")
|
||||
logg = logging.NewVanilla().WithDomain("ussdmenuhandler").WithContextKey("SessionId")
|
||||
scriptDir = path.Join("services", "registration")
|
||||
translationDir = path.Join(scriptDir, "locale")
|
||||
)
|
||||
|
||||
// Define the regex patterns as constants
|
||||
const (
|
||||
pinPattern = `^\d{4}$`
|
||||
)
|
||||
|
||||
// isValidPIN checks whether the given input is a 4 digit number
|
||||
func isValidPIN(pin string) bool {
|
||||
match, _ := regexp.MatchString(pinPattern, pin)
|
||||
return match
|
||||
}
|
||||
|
||||
// FlagManager handles centralized flag management
|
||||
type FlagManager struct {
|
||||
parser *asm.FlagParser
|
||||
@ -76,11 +64,13 @@ type Handlers struct {
|
||||
adminstore *utils.AdminStore
|
||||
flagManager *asm.FlagParser
|
||||
accountService remote.AccountServiceInterface
|
||||
prefixDb storage.PrefixDb
|
||||
prefixDb dbstorage.PrefixDb
|
||||
profile *models.Profile
|
||||
ReplaceSeparatorFunc func(string) string
|
||||
}
|
||||
|
||||
func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *utils.AdminStore, accountService remote.AccountServiceInterface) (*Handlers, error) {
|
||||
// NewHandlers creates a new instance of the Handlers struct with the provided dependencies.
|
||||
func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *utils.AdminStore, accountService remote.AccountServiceInterface, replaceSeparatorFunc func(string) string) (*Handlers, error) {
|
||||
if userdataStore == nil {
|
||||
return nil, fmt.Errorf("cannot create handler with nil userdata store")
|
||||
}
|
||||
@ -90,7 +80,7 @@ func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *util
|
||||
|
||||
// Instantiate the SubPrefixDb with "DATATYPE_USERDATA" prefix
|
||||
prefix := common.ToBytes(db.DATATYPE_USERDATA)
|
||||
prefixDb := storage.NewSubPrefixDb(userdataStore, prefix)
|
||||
prefixDb := dbstorage.NewSubPrefixDb(userdataStore, prefix)
|
||||
|
||||
h := &Handlers{
|
||||
userdataStore: userDb,
|
||||
@ -99,10 +89,12 @@ func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *util
|
||||
accountService: accountService,
|
||||
prefixDb: prefixDb,
|
||||
profile: &models.Profile{Max: 6},
|
||||
ReplaceSeparatorFunc: replaceSeparatorFunc,
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// WithPersister sets persister instance to the handlers.
|
||||
func (h *Handlers) WithPersister(pe *persist.Persister) *Handlers {
|
||||
if h.pe != nil {
|
||||
panic("persister already set")
|
||||
@ -111,6 +103,7 @@ func (h *Handlers) WithPersister(pe *persist.Persister) *Handlers {
|
||||
return h
|
||||
}
|
||||
|
||||
// Init initializes the handler for a new session.
|
||||
func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var r resource.Result
|
||||
if h.pe == nil {
|
||||
@ -124,9 +117,17 @@ func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource
|
||||
h.st = h.pe.GetState()
|
||||
h.ca = h.pe.GetMemory()
|
||||
|
||||
sessionId, _ := ctx.Value("SessionId").(string)
|
||||
flag_admin_privilege, _ := h.flagManager.GetFlag("flag_admin_privilege")
|
||||
if len(input) == 0 {
|
||||
// move to the top node
|
||||
h.st.Code = []byte{}
|
||||
}
|
||||
|
||||
sessionId, ok := ctx.Value("SessionId").(string)
|
||||
if ok {
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
}
|
||||
|
||||
flag_admin_privilege, _ := h.flagManager.GetFlag("flag_admin_privilege")
|
||||
isAdmin, _ := h.adminstore.IsAdmin(sessionId)
|
||||
|
||||
if isAdmin {
|
||||
@ -149,7 +150,7 @@ func (h *Handlers) Exit() {
|
||||
h.pe = nil
|
||||
}
|
||||
|
||||
// SetLanguage sets the language across the menu
|
||||
// SetLanguage sets the language across the menu.
|
||||
func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
|
||||
@ -173,6 +174,7 @@ func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (r
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// handles the account creation when no existing account is present for the session and stores associated data in the user data store.
|
||||
func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, res *resource.Result) error {
|
||||
flag_account_created, _ := h.flagManager.GetFlag("flag_account_created")
|
||||
r, err := h.accountService.CreateAccount(ctx)
|
||||
@ -205,9 +207,9 @@ func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, r
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateAccount checks if any account exists on the JSON data file, and if not
|
||||
// CreateAccount checks if any account exists on the JSON data file, and if not,
|
||||
// creates an account on the API,
|
||||
// sets the default values and flags
|
||||
// sets the default values and flags.
|
||||
func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
var err error
|
||||
@ -216,7 +218,7 @@ func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte)
|
||||
return res, fmt.Errorf("missing session")
|
||||
}
|
||||
store := h.userdataStore
|
||||
_, err = store.ReadEntry(ctx, sessionId, common.DATA_ACCOUNT_CREATED)
|
||||
_, err = store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY)
|
||||
if err != nil {
|
||||
if db.IsNotFound(err) {
|
||||
logg.InfoCtxf(ctx, "Creating an account because it doesn't exist")
|
||||
@ -231,19 +233,22 @@ func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (h *Handlers) CheckPinMisMatch(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
// CheckBlockedNumPinMisMatch checks if the provided PIN matches a temporary PIN stored for a blocked number.
|
||||
func (h *Handlers) CheckBlockedNumPinMisMatch(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
res := resource.Result{}
|
||||
flag_pin_mismatch, _ := h.flagManager.GetFlag("flag_pin_mismatch")
|
||||
sessionId, ok := ctx.Value("SessionId").(string)
|
||||
if !ok {
|
||||
return res, fmt.Errorf("missing session")
|
||||
}
|
||||
// Get blocked number from storage.
|
||||
store := h.userdataStore
|
||||
blockedNumber, err := store.ReadEntry(ctx, sessionId, common.DATA_BLOCKED_NUMBER)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "failed to read blockedNumber entry with", "key", common.DATA_BLOCKED_NUMBER, "error", err)
|
||||
return res, err
|
||||
}
|
||||
// Get temporary PIN for the blocked number.
|
||||
temporaryPin, err := store.ReadEntry(ctx, string(blockedNumber), common.DATA_TEMPORARY_VALUE)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "failed to read temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "error", err)
|
||||
@ -257,6 +262,7 @@ func (h *Handlers) CheckPinMisMatch(ctx context.Context, sym string, input []byt
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// VerifyNewPin checks if a new PIN meets the required format criteria.
|
||||
func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
res := resource.Result{}
|
||||
_, ok := ctx.Value("SessionId").(string)
|
||||
@ -265,8 +271,8 @@ func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (
|
||||
}
|
||||
flag_valid_pin, _ := h.flagManager.GetFlag("flag_valid_pin")
|
||||
pinInput := string(input)
|
||||
// Validate that the PIN is a 4-digit number
|
||||
if isValidPIN(pinInput) {
|
||||
// Validate that the PIN is a 4-digit number.
|
||||
if common.IsValidPIN(pinInput) {
|
||||
res.FlagSet = append(res.FlagSet, flag_valid_pin)
|
||||
} else {
|
||||
res.FlagReset = append(res.FlagReset, flag_valid_pin)
|
||||
@ -275,9 +281,9 @@ func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// SaveTemporaryPin saves the valid PIN input to the DATA_TEMPORARY_VALUE
|
||||
// SaveTemporaryPin saves the valid PIN input to the DATA_TEMPORARY_VALUE,
|
||||
// during the account creation process
|
||||
// and during the change PIN process
|
||||
// and during the change PIN process.
|
||||
func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
var err error
|
||||
@ -290,8 +296,8 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt
|
||||
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
|
||||
accountPIN := string(input)
|
||||
|
||||
// Validate that the PIN is a 4-digit number
|
||||
if !isValidPIN(accountPIN) {
|
||||
// Validate that the PIN is a 4-digit number.
|
||||
if !common.IsValidPIN(accountPIN) {
|
||||
res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
|
||||
return res, nil
|
||||
}
|
||||
@ -306,6 +312,7 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// SaveOthersTemporaryPin allows authorized users to set temporary PINs for blocked numbers.
|
||||
func (h *Handlers) SaveOthersTemporaryPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
var err error
|
||||
@ -316,12 +323,14 @@ func (h *Handlers) SaveOthersTemporaryPin(ctx context.Context, sym string, input
|
||||
return res, fmt.Errorf("missing session")
|
||||
}
|
||||
temporaryPin := string(input)
|
||||
// First, we retrieve the blocked number associated with this session
|
||||
blockedNumber, err := store.ReadEntry(ctx, sessionId, common.DATA_BLOCKED_NUMBER)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "failed to read blockedNumber entry with", "key", common.DATA_BLOCKED_NUMBER, "error", err)
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Then we save the temporary PIN for that blocked number
|
||||
err = store.WriteEntry(ctx, string(blockedNumber), common.DATA_TEMPORARY_VALUE, []byte(temporaryPin))
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "failed to write temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "value", temporaryPin, "error", err)
|
||||
@ -331,6 +340,7 @@ func (h *Handlers) SaveOthersTemporaryPin(ctx context.Context, sym string, input
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ConfirmPinChange validates user's new PIN. If input matches the temporary PIN, saves it as the new account PIN.
|
||||
func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
sessionId, ok := ctx.Value("SessionId").(string)
|
||||
@ -349,10 +359,20 @@ func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byt
|
||||
res.FlagReset = append(res.FlagReset, flag_pin_mismatch)
|
||||
} else {
|
||||
res.FlagSet = append(res.FlagSet, flag_pin_mismatch)
|
||||
return res, nil
|
||||
}
|
||||
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(temporaryPin))
|
||||
|
||||
// Hash the PIN
|
||||
hashedPIN, err := common.HashPIN(string(temporaryPin))
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "failed to write temporaryPin entry with", "key", common.DATA_ACCOUNT_PIN, "value", temporaryPin, "error", err)
|
||||
logg.ErrorCtxf(ctx, "failed to hash temporaryPin", "error", err)
|
||||
return res, err
|
||||
}
|
||||
|
||||
// save the hashed PIN as the new account PIN
|
||||
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(hashedPIN))
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "failed to write DATA_ACCOUNT_PIN entry with", "key", common.DATA_ACCOUNT_PIN, "hashedPIN value", hashedPIN, "error", err)
|
||||
return res, err
|
||||
}
|
||||
return res, nil
|
||||
@ -360,7 +380,7 @@ func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byt
|
||||
|
||||
// VerifyCreatePin checks whether the confirmation PIN is similar to the temporary PIN
|
||||
// If similar, it sets the USERFLAG_PIN_SET flag and writes the account PIN allowing the user
|
||||
// to access the main menu
|
||||
// to access the main menu.
|
||||
func (h *Handlers) VerifyCreatePin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
|
||||
@ -384,18 +404,26 @@ func (h *Handlers) VerifyCreatePin(ctx context.Context, sym string, input []byte
|
||||
res.FlagSet = append(res.FlagSet, flag_pin_set)
|
||||
} else {
|
||||
res.FlagSet = []uint32{flag_pin_mismatch}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(temporaryPin))
|
||||
// Hash the PIN
|
||||
hashedPIN, err := common.HashPIN(string(temporaryPin))
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "failed to write temporaryPin entry with", "key", common.DATA_ACCOUNT_PIN, "value", temporaryPin, "error", err)
|
||||
logg.ErrorCtxf(ctx, "failed to hash temporaryPin", "error", err)
|
||||
return res, err
|
||||
}
|
||||
|
||||
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(hashedPIN))
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "failed to write DATA_ACCOUNT_PIN entry with", "key", common.DATA_ACCOUNT_PIN, "value", hashedPIN, "error", err)
|
||||
return res, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// codeFromCtx retrieves language codes from the context that can be used for handling translations
|
||||
// retrieves language codes from the context that can be used for handling translations.
|
||||
func codeFromCtx(ctx context.Context) string {
|
||||
var code string
|
||||
if ctx.Value("Language") != nil {
|
||||
@ -702,15 +730,27 @@ func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (res
|
||||
return res, err
|
||||
}
|
||||
if len(input) == 4 {
|
||||
if bytes.Equal(input, AccountPin) {
|
||||
if common.VerifyPIN(string(AccountPin), string(input)) {
|
||||
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
|
||||
@ -724,12 +764,38 @@ 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
|
||||
}
|
||||
|
||||
// Setback sets the flag_back_set flag when the navigation is back
|
||||
// Setback sets the flag_back_set flag when the navigation is back.
|
||||
func (h *Handlers) SetBack(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
//TODO:
|
||||
@ -742,7 +808,7 @@ func (h *Handlers) SetBack(ctx context.Context, sym string, input []byte) (resou
|
||||
}
|
||||
|
||||
// CheckAccountStatus queries the API using the TrackingId and sets flags
|
||||
// based on the account status
|
||||
// based on the account status.
|
||||
func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
|
||||
@ -782,7 +848,7 @@ func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []b
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Quit displays the Thank you message and exits the menu
|
||||
// Quit displays the Thank you message and exits the menu.
|
||||
func (h *Handlers) Quit(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
|
||||
@ -797,7 +863,7 @@ func (h *Handlers) Quit(ctx context.Context, sym string, input []byte) (resource
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// QuitWithHelp displays helpline information then exits the menu
|
||||
// QuitWithHelp displays helpline information then exits the menu.
|
||||
func (h *Handlers) QuitWithHelp(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
|
||||
@ -812,7 +878,17 @@ func (h *Handlers) QuitWithHelp(ctx context.Context, sym string, input []byte) (
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// VerifyYob verifies the length of the given input
|
||||
// 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
|
||||
var err error
|
||||
@ -834,7 +910,7 @@ func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (res
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ResetIncorrectYob resets the incorrect date format flag after a new attempt
|
||||
// ResetIncorrectYob resets the incorrect date format flag after a new attempt.
|
||||
func (h *Handlers) ResetIncorrectYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
|
||||
@ -844,7 +920,7 @@ func (h *Handlers) ResetIncorrectYob(ctx context.Context, sym string, input []by
|
||||
}
|
||||
|
||||
// CheckBalance retrieves the balance of the active voucher and sets
|
||||
// the balance as the result content
|
||||
// the balance as the result content.
|
||||
func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
var err error
|
||||
@ -894,9 +970,12 @@ func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// FetchCommunityBalance retrieves and displays the balance for community accounts in user's preferred language.
|
||||
func (h *Handlers) FetchCommunityBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
// retrieve the language code from the context
|
||||
code := codeFromCtx(ctx)
|
||||
// Initialize the localization system with the appropriate translation directory
|
||||
l := gotext.NewLocale(translationDir, code)
|
||||
l.AddDomain("default")
|
||||
//TODO:
|
||||
@ -905,6 +984,10 @@ func (h *Handlers) FetchCommunityBalance(ctx context.Context, sym string, input
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ResetOthersPin handles the PIN reset process for other users' accounts by:
|
||||
// 1. Retrieving the blocked phone number from the session
|
||||
// 2. Fetching the temporary PIN associated with that number
|
||||
// 3. Updating the account PIN with the temporary PIN
|
||||
func (h *Handlers) ResetOthersPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
store := h.userdataStore
|
||||
@ -922,7 +1005,15 @@ func (h *Handlers) ResetOthersPin(ctx context.Context, sym string, input []byte)
|
||||
logg.ErrorCtxf(ctx, "failed to read temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "error", err)
|
||||
return res, err
|
||||
}
|
||||
err = store.WriteEntry(ctx, string(blockedPhonenumber), common.DATA_ACCOUNT_PIN, []byte(temporaryPin))
|
||||
|
||||
// Hash the PIN
|
||||
hashedPIN, err := common.HashPIN(string(temporaryPin))
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "failed to hash temporaryPin", "error", err)
|
||||
return res, err
|
||||
}
|
||||
|
||||
err = store.WriteEntry(ctx, string(blockedPhonenumber), common.DATA_ACCOUNT_PIN, []byte(hashedPIN))
|
||||
if err != nil {
|
||||
return res, nil
|
||||
}
|
||||
@ -930,6 +1021,8 @@ func (h *Handlers) ResetOthersPin(ctx context.Context, sym string, input []byte)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ResetUnregisteredNumber clears the unregistered number flag in the system,
|
||||
// indicating that a number's registration status should no longer be marked as unregistered.
|
||||
func (h *Handlers) ResetUnregisteredNumber(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
flag_unregistered_number, _ := h.flagManager.GetFlag("flag_unregistered_number")
|
||||
@ -937,6 +1030,8 @@ func (h *Handlers) ResetUnregisteredNumber(ctx context.Context, sym string, inpu
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ValidateBlockedNumber performs validation of phone numbers, specifically for blocked numbers in the system.
|
||||
// It checks phone number format and verifies registration status.
|
||||
func (h *Handlers) ValidateBlockedNumber(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
var err error
|
||||
@ -1065,7 +1160,7 @@ func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []by
|
||||
}
|
||||
|
||||
// TransactionReset resets the previous transaction data (Recipient and Amount)
|
||||
// as well as the invalid flags
|
||||
// as well as the invalid flags.
|
||||
func (h *Handlers) TransactionReset(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
var err error
|
||||
@ -1118,7 +1213,7 @@ func (h *Handlers) InviteValidRecipient(ctx context.Context, sym string, input [
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ResetTransactionAmount resets the transaction amount and invalid flag
|
||||
// ResetTransactionAmount resets the transaction amount and invalid flag.
|
||||
func (h *Handlers) ResetTransactionAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
var err error
|
||||
@ -1248,7 +1343,7 @@ func (h *Handlers) RetrieveBlockedNumber(ctx context.Context, sym string, input
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GetSender returns the sessionId (phoneNumber)
|
||||
// GetSender returns the sessionId (phoneNumber).
|
||||
func (h *Handlers) GetSender(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
|
||||
@ -1262,7 +1357,7 @@ func (h *Handlers) GetSender(ctx context.Context, sym string, input []byte) (res
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GetAmount retrieves the amount from teh Gdbm Db
|
||||
// GetAmount retrieves the amount from teh Gdbm Db.
|
||||
func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
|
||||
@ -1286,7 +1381,7 @@ func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (res
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// InitiateTransaction calls the TokenTransfer and returns a confirmation based on the result
|
||||
// InitiateTransaction calls the TokenTransfer and returns a confirmation based on the result.
|
||||
func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
var err error
|
||||
@ -1336,9 +1431,12 @@ func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GetCurrentProfileInfo retrieves specific profile fields based on the current state of the USSD session.
|
||||
// Uses flag management system to track profile field status and handle menu navigation.
|
||||
func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
var profileInfo []byte
|
||||
var defaultValue string
|
||||
var err error
|
||||
|
||||
flag_firstname_set, _ := h.flagManager.GetFlag("flag_firstname_set")
|
||||
@ -1355,6 +1453,17 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
|
||||
if !ok {
|
||||
return res, fmt.Errorf("missing session")
|
||||
}
|
||||
language, ok := ctx.Value("Language").(lang.Language)
|
||||
if !ok {
|
||||
return res, fmt.Errorf("value for 'Language' is not of type lang.Language")
|
||||
}
|
||||
code := language.Code
|
||||
if code == "swa" {
|
||||
defaultValue = "Haipo"
|
||||
} else {
|
||||
defaultValue = "Not Provided"
|
||||
}
|
||||
|
||||
sm, _ := h.st.Where()
|
||||
parts := strings.SplitN(sm, "_", 2)
|
||||
filename := parts[1]
|
||||
@ -1371,7 +1480,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
|
||||
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_FIRST_NAME)
|
||||
if err != nil {
|
||||
if db.IsNotFound(err) {
|
||||
res.Content = "Not provided"
|
||||
res.Content = defaultValue
|
||||
break
|
||||
}
|
||||
logg.ErrorCtxf(ctx, "Failed to read first name entry with", "key", "error", common.DATA_FIRST_NAME, err)
|
||||
@ -1383,7 +1492,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
|
||||
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_FAMILY_NAME)
|
||||
if err != nil {
|
||||
if db.IsNotFound(err) {
|
||||
res.Content = "Not provided"
|
||||
res.Content = defaultValue
|
||||
break
|
||||
}
|
||||
logg.ErrorCtxf(ctx, "Failed to read family name entry with", "key", "error", common.DATA_FAMILY_NAME, err)
|
||||
@ -1396,7 +1505,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
|
||||
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_GENDER)
|
||||
if err != nil {
|
||||
if db.IsNotFound(err) {
|
||||
res.Content = "Not provided"
|
||||
res.Content = defaultValue
|
||||
break
|
||||
}
|
||||
logg.ErrorCtxf(ctx, "Failed to read gender entry with", "key", "error", common.DATA_GENDER, err)
|
||||
@ -1408,7 +1517,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
|
||||
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_YOB)
|
||||
if err != nil {
|
||||
if db.IsNotFound(err) {
|
||||
res.Content = "Not provided"
|
||||
res.Content = defaultValue
|
||||
break
|
||||
}
|
||||
logg.ErrorCtxf(ctx, "Failed to read year of birth(yob) entry with", "key", "error", common.DATA_YOB, err)
|
||||
@ -1420,7 +1529,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
|
||||
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_LOCATION)
|
||||
if err != nil {
|
||||
if db.IsNotFound(err) {
|
||||
res.Content = "Not provided"
|
||||
res.Content = defaultValue
|
||||
break
|
||||
}
|
||||
logg.ErrorCtxf(ctx, "Failed to read location entry with", "key", "error", common.DATA_LOCATION, err)
|
||||
@ -1432,7 +1541,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
|
||||
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS)
|
||||
if err != nil {
|
||||
if db.IsNotFound(err) {
|
||||
res.Content = "Not provided"
|
||||
res.Content = defaultValue
|
||||
break
|
||||
}
|
||||
logg.ErrorCtxf(ctx, "Failed to read offerings entry with", "key", "error", common.DATA_OFFERINGS, err)
|
||||
@ -1447,6 +1556,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GetProfileInfo provides a comprehensive view of a user's profile.
|
||||
func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
var defaultValue string
|
||||
@ -1515,7 +1625,7 @@ func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte)
|
||||
}
|
||||
|
||||
// SetDefaultVoucher retrieves the current vouchers
|
||||
// and sets the first as the default voucher, if no active voucher is set
|
||||
// and sets the first as the default voucher, if no active voucher is set.
|
||||
func (h *Handlers) SetDefaultVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
var err error
|
||||
@ -1600,7 +1710,7 @@ func (h *Handlers) SetDefaultVoucher(ctx context.Context, sym string, input []by
|
||||
}
|
||||
|
||||
// CheckVouchers retrieves the token holdings from the API using the "PublicKey" and stores
|
||||
// them to gdbm
|
||||
// them to gdbm.
|
||||
func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
sessionId, ok := ctx.Value("SessionId").(string)
|
||||
@ -1672,7 +1782,7 @@ func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GetVoucherList fetches the list of vouchers and formats them
|
||||
// GetVoucherList fetches the list of vouchers and formats them.
|
||||
func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
|
||||
@ -1683,13 +1793,15 @@ func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte)
|
||||
return res, err
|
||||
}
|
||||
|
||||
res.Content = string(voucherData)
|
||||
formattedData := h.ReplaceSeparatorFunc(string(voucherData))
|
||||
|
||||
res.Content = string(formattedData)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ViewVoucher retrieves the token holding and balance from the subprefixDB
|
||||
// and displays it to the user for them to select it
|
||||
// and displays it to the user for them to select it.
|
||||
func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
sessionId, ok := ctx.Value("SessionId").(string)
|
||||
@ -1730,7 +1842,7 @@ func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (r
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// SetVoucher retrieves the temp voucher data and sets it as the active data
|
||||
// SetVoucher retrieves the temp voucher data and sets it as the active data.
|
||||
func (h *Handlers) SetVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
|
||||
@ -1756,7 +1868,7 @@ func (h *Handlers) SetVoucher(ctx context.Context, sym string, input []byte) (re
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GetVoucherDetails retrieves the voucher details
|
||||
// GetVoucherDetails retrieves the voucher details.
|
||||
func (h *Handlers) GetVoucherDetails(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
store := h.userdataStore
|
||||
@ -1788,7 +1900,7 @@ func (h *Handlers) GetVoucherDetails(ctx context.Context, sym string, input []by
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// CheckTransactions retrieves the transactions from the API using the "PublicKey" and stores to prefixDb
|
||||
// CheckTransactions retrieves the transactions from the API using the "PublicKey" and stores to prefixDb.
|
||||
func (h *Handlers) CheckTransactions(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
sessionId, ok := ctx.Value("SessionId").(string)
|
||||
@ -1846,13 +1958,14 @@ func (h *Handlers) CheckTransactions(ctx context.Context, sym string, input []by
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GetTransactionsList fetches the list of transactions and formats them
|
||||
// GetTransactionsList fetches the list of transactions and formats them.
|
||||
func (h *Handlers) GetTransactionsList(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
sessionId, ok := ctx.Value("SessionId").(string)
|
||||
if !ok {
|
||||
return res, fmt.Errorf("missing session")
|
||||
}
|
||||
|
||||
store := h.userdataStore
|
||||
publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY)
|
||||
if err != nil {
|
||||
@ -1895,12 +2008,14 @@ func (h *Handlers) GetTransactionsList(ctx context.Context, sym string, input []
|
||||
value := strings.TrimSpace(values[i])
|
||||
date := strings.Split(strings.TrimSpace(dates[i]), " ")[0]
|
||||
|
||||
status := "received"
|
||||
status := "Received"
|
||||
if sender == string(publicKey) {
|
||||
status = "sent"
|
||||
status = "Sent"
|
||||
}
|
||||
|
||||
formattedTransactions = append(formattedTransactions, fmt.Sprintf("%d:%s %s %s %s", i+1, status, value, sym, date))
|
||||
// Use the ReplaceSeparator function for the menu separator
|
||||
transactionLine := fmt.Sprintf("%d%s%s %s %s %s", i+1, h.ReplaceSeparatorFunc(":"), status, value, sym, date)
|
||||
formattedTransactions = append(formattedTransactions, transactionLine)
|
||||
}
|
||||
|
||||
res.Content = strings.Join(formattedTransactions, "\n")
|
||||
@ -1909,7 +2024,7 @@ func (h *Handlers) GetTransactionsList(ctx context.Context, sym string, input []
|
||||
}
|
||||
|
||||
// ViewTransactionStatement retrieves the transaction statement
|
||||
// and displays it to the user
|
||||
// and displays it to the user.
|
||||
func (h *Handlers) ViewTransactionStatement(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
sessionId, ok := ctx.Value("SessionId").(string)
|
||||
@ -1957,6 +2072,7 @@ func (h *Handlers) ViewTransactionStatement(ctx context.Context, sym string, inp
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// handles bulk updates of profile information.
|
||||
func (h *Handlers) insertProfileItems(ctx context.Context, sessionId string, res *resource.Result) error {
|
||||
var err error
|
||||
store := h.userdataStore
|
||||
@ -1979,21 +2095,22 @@ func (h *Handlers) insertProfileItems(ctx context.Context, sessionId string, res
|
||||
for index, profileItem := range h.profile.ProfileItems {
|
||||
// Ensure the profileItem is not "0"(is set)
|
||||
if profileItem != "0" {
|
||||
flag, _ := h.flagManager.GetFlag(profileFlagNames[index])
|
||||
isProfileItemSet := h.st.MatchFlag(flag, true)
|
||||
if !isProfileItemSet {
|
||||
err = store.WriteEntry(ctx, sessionId, profileDataKeys[index], []byte(profileItem))
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "failed to write profile entry with", "key", profileDataKeys[index], "value", profileItem, "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the flag for the current index
|
||||
flag, _ := h.flagManager.GetFlag(profileFlagNames[index])
|
||||
res.FlagSet = append(res.FlagSet, flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAllProfileItems is used to persist all the new profile information and setup the required profile flags
|
||||
// UpdateAllProfileItems is used to persist all the new profile information and setup the required profile flags.
|
||||
func (h *Handlers) UpdateAllProfileItems(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
sessionId, ok := ctx.Value("SessionId").(string)
|
||||
@ -2006,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
|
||||
}
|
||||
|
@ -5,15 +5,19 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.defalsify.org/vise.git/cache"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/state"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||
"git.grassecon.net/urdt/ussd/internal/testutil/mocks"
|
||||
"git.grassecon.net/urdt/ussd/internal/testutil/testservice"
|
||||
"git.grassecon.net/urdt/ussd/internal/utils"
|
||||
"git.grassecon.net/urdt/ussd/models"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/common"
|
||||
@ -32,6 +36,11 @@ var (
|
||||
flagsPath = path.Join(baseDir, "services", "registration", "pp.csv")
|
||||
)
|
||||
|
||||
// mockReplaceSeparator function
|
||||
var mockReplaceSeparator = func(input string) string {
|
||||
return strings.ReplaceAll(input, ":", ": ")
|
||||
}
|
||||
|
||||
// InitializeTestStore sets up and returns an in-memory database and store.
|
||||
func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore) {
|
||||
ctx := context.Background()
|
||||
@ -51,14 +60,14 @@ func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore)
|
||||
return ctx, store
|
||||
}
|
||||
|
||||
func InitializeTestSubPrefixDb(t *testing.T, ctx context.Context) *storage.SubPrefixDb {
|
||||
func InitializeTestSubPrefixDb(t *testing.T, ctx context.Context) *dbstorage.SubPrefixDb {
|
||||
db := memdb.NewMemDb()
|
||||
err := db.Connect(ctx, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prefix := common.ToBytes(visedb.DATATYPE_USERDATA)
|
||||
spdb := storage.NewSubPrefixDb(db, prefix)
|
||||
spdb := dbstorage.NewSubPrefixDb(db, prefix)
|
||||
|
||||
return spdb
|
||||
}
|
||||
@ -67,12 +76,15 @@ func TestNewHandlers(t *testing.T) {
|
||||
_, store := InitializeTestStore(t)
|
||||
|
||||
fm, err := NewFlagManager(flagsPath)
|
||||
accountService := testservice.TestAccountService{}
|
||||
if err != nil {
|
||||
t.Logf(err.Error())
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
accountService := testservice.TestAccountService{}
|
||||
|
||||
// Test case for valid UserDataStore
|
||||
t.Run("Valid UserDataStore", func(t *testing.T) {
|
||||
handlers, err := NewHandlers(fm.parser, store, nil, &accountService)
|
||||
handlers, err := NewHandlers(fm.parser, store, nil, &accountService, mockReplaceSeparator)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
@ -82,23 +94,130 @@ func TestNewHandlers(t *testing.T) {
|
||||
if handlers.userdataStore == nil {
|
||||
t.Fatal("expected userdataStore to be set in handlers")
|
||||
}
|
||||
if handlers.ReplaceSeparatorFunc == nil {
|
||||
t.Fatal("expected ReplaceSeparatorFunc to be set in handlers")
|
||||
}
|
||||
|
||||
// Test ReplaceSeparatorFunc functionality
|
||||
input := "1:Menu item"
|
||||
expectedOutput := "1: Menu item"
|
||||
if handlers.ReplaceSeparatorFunc(input) != expectedOutput {
|
||||
t.Fatalf("ReplaceSeparatorFunc function did not return expected output: got %v, want %v", handlers.ReplaceSeparatorFunc(input), expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
// Test case for nil userdataStore
|
||||
// Test case for nil UserDataStore
|
||||
t.Run("Nil UserDataStore", func(t *testing.T) {
|
||||
handlers, err := NewHandlers(fm.parser, nil, nil, &accountService)
|
||||
handlers, err := NewHandlers(fm.parser, nil, nil, &accountService, mockReplaceSeparator)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error, got none")
|
||||
}
|
||||
if handlers != nil {
|
||||
t.Fatal("expected handlers to be nil")
|
||||
}
|
||||
if err.Error() != "cannot create handler with nil userdata store" {
|
||||
t.Fatalf("expected specific error, got %v", err)
|
||||
expectedError := "cannot create handler with nil userdata store"
|
||||
if err.Error() != expectedError {
|
||||
t.Fatalf("expected error '%s', got '%v'", expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
sessionId := "session123"
|
||||
ctx, store := InitializeTestStore(t)
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
|
||||
fm, err := NewFlagManager(flagsPath)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
adminstore, err := utils.NewAdminStore(ctx, "admin_numbers")
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
st := state.NewState(128)
|
||||
ca := cache.NewCache()
|
||||
|
||||
flag_admin_privilege, _ := fm.GetFlag("flag_admin_privilege")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func() (*Handlers, context.Context)
|
||||
input []byte
|
||||
expectedResult resource.Result
|
||||
}{
|
||||
{
|
||||
name: "Handler not ready",
|
||||
setup: func() (*Handlers, context.Context) {
|
||||
return &Handlers{}, ctx
|
||||
},
|
||||
input: []byte("1"),
|
||||
expectedResult: resource.Result{},
|
||||
},
|
||||
{
|
||||
name: "State and memory initialization",
|
||||
setup: func() (*Handlers, context.Context) {
|
||||
pe := persist.NewPersister(store).WithSession(sessionId).WithContent(st, ca)
|
||||
h := &Handlers{
|
||||
flagManager: fm.parser,
|
||||
adminstore: adminstore,
|
||||
pe: pe,
|
||||
}
|
||||
return h, context.WithValue(ctx, "SessionId", sessionId)
|
||||
},
|
||||
input: []byte("1"),
|
||||
expectedResult: resource.Result{
|
||||
FlagReset: []uint32{flag_admin_privilege},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Non-admin session initialization",
|
||||
setup: func() (*Handlers, context.Context) {
|
||||
pe := persist.NewPersister(store).WithSession("0712345678").WithContent(st, ca)
|
||||
h := &Handlers{
|
||||
flagManager: fm.parser,
|
||||
adminstore: adminstore,
|
||||
pe: pe,
|
||||
}
|
||||
return h, context.WithValue(context.Background(), "SessionId", "0712345678")
|
||||
},
|
||||
input: []byte("1"),
|
||||
expectedResult: resource.Result{
|
||||
FlagReset: []uint32{flag_admin_privilege},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Move to top node on empty input",
|
||||
setup: func() (*Handlers, context.Context) {
|
||||
pe := persist.NewPersister(store).WithSession(sessionId).WithContent(st, ca)
|
||||
h := &Handlers{
|
||||
flagManager: fm.parser,
|
||||
adminstore: adminstore,
|
||||
pe: pe,
|
||||
}
|
||||
st.Code = []byte("some pending bytecode")
|
||||
return h, context.WithValue(ctx, "SessionId", sessionId)
|
||||
},
|
||||
input: []byte(""),
|
||||
expectedResult: resource.Result{
|
||||
FlagReset: []uint32{flag_admin_privilege},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
h, testCtx := tt.setup()
|
||||
res, err := h.Init(testCtx, "", tt.input)
|
||||
|
||||
assert.NoError(t, err, "Unexpected error occurred")
|
||||
assert.Equal(t, res, tt.expectedResult, "Expected result should match actual result")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAccount(t *testing.T) {
|
||||
sessionId := "session123"
|
||||
ctx, store := InitializeTestStore(t)
|
||||
@ -789,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,
|
||||
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)
|
||||
}
|
||||
@ -929,7 +1090,14 @@ func TestAuthorize(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(accountPIN))
|
||||
// Hash the PIN
|
||||
hashedPIN, err := common.HashPIN(accountPIN)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "failed to hash temporaryPin", "error", err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(hashedPIN))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -1381,59 +1549,6 @@ func TestQuit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidPIN(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pin string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Valid PIN with 4 digits",
|
||||
pin: "1234",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Valid PIN with leading zeros",
|
||||
pin: "0001",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN with less than 4 digits",
|
||||
pin: "123",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN with more than 4 digits",
|
||||
pin: "12345",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN with letters",
|
||||
pin: "abcd",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN with special characters",
|
||||
pin: "12@#",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Empty PIN",
|
||||
pin: "",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual := isValidPIN(tt.pin)
|
||||
if actual != tt.expected {
|
||||
t.Errorf("isValidPIN(%q) = %v; expected %v", tt.pin, actual, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAmount(t *testing.T) {
|
||||
fm, err := NewFlagManager(flagsPath)
|
||||
if err != nil {
|
||||
@ -1680,7 +1795,7 @@ func TestGetProfile(t *testing.T) {
|
||||
result: resource.Result{
|
||||
Content: fmt.Sprintf(
|
||||
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
|
||||
"John Doee", "Male", "48", "Kilifi", "Bananas",
|
||||
"John Doee", "Male", "49", "Kilifi", "Bananas",
|
||||
),
|
||||
},
|
||||
},
|
||||
@ -1692,7 +1807,7 @@ func TestGetProfile(t *testing.T) {
|
||||
result: resource.Result{
|
||||
Content: fmt.Sprintf(
|
||||
"Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\n",
|
||||
"John Doee", "Male", "48", "Kilifi", "Bananas",
|
||||
"John Doee", "Male", "49", "Kilifi", "Bananas",
|
||||
),
|
||||
},
|
||||
},
|
||||
@ -1704,7 +1819,7 @@ func TestGetProfile(t *testing.T) {
|
||||
result: resource.Result{
|
||||
Content: fmt.Sprintf(
|
||||
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
|
||||
"John Doee", "Male", "48", "Kilifi", "Bananas",
|
||||
"John Doee", "Male", "49", "Kilifi", "Bananas",
|
||||
),
|
||||
},
|
||||
},
|
||||
@ -1982,26 +2097,31 @@ func TestCheckVouchers(t *testing.T) {
|
||||
|
||||
func TestGetVoucherList(t *testing.T) {
|
||||
sessionId := "session123"
|
||||
|
||||
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
|
||||
|
||||
spdb := InitializeTestSubPrefixDb(t, ctx)
|
||||
|
||||
// Initialize Handlers
|
||||
h := &Handlers{
|
||||
prefixDb: spdb,
|
||||
ReplaceSeparatorFunc: mockReplaceSeparator,
|
||||
}
|
||||
|
||||
expectedSym := []byte("1:SRF\n2:MILO")
|
||||
mockSyms := []byte("1:SRF\n2:MILO")
|
||||
|
||||
// Put voucher sym data from the store
|
||||
err := spdb.Put(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS), expectedSym)
|
||||
err := spdb.Put(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS), mockSyms)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedSyms := []byte("1: SRF\n2: MILO")
|
||||
|
||||
res, err := h.GetVoucherList(ctx, "", []byte(""))
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, res.Content, string(expectedSym))
|
||||
assert.Equal(t, res.Content, string(expectedSyms))
|
||||
}
|
||||
|
||||
func TestViewVoucher(t *testing.T) {
|
||||
@ -2113,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))
|
||||
|
||||
}
|
||||
|
120
internal/http/at/parse.go
Normal file
120
internal/http/at/parse.go
Normal file
@ -0,0 +1,120 @@
|
||||
package at
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/common"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
)
|
||||
|
||||
type ATRequestParser struct {
|
||||
}
|
||||
|
||||
func (arp *ATRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
logg.Warnf("got an invalid request", "req", rq)
|
||||
return "", handlers.ErrInvalidRequest
|
||||
}
|
||||
// Capture body (if any) for logging
|
||||
body, err := io.ReadAll(rqv.Body)
|
||||
if err != nil {
|
||||
logg.Warnf("failed to read request body", "err", err)
|
||||
return "", fmt.Errorf("failed to read request body: %v", err)
|
||||
}
|
||||
// Reset the body for further reading
|
||||
rqv.Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
// Log the body as JSON
|
||||
bodyLog := map[string]string{"body": string(body)}
|
||||
logBytes, err := json.Marshal(bodyLog)
|
||||
if err != nil {
|
||||
logg.Warnf("failed to marshal request body", "err", err)
|
||||
} else {
|
||||
decodedStr := string(logBytes)
|
||||
sessionId, err := extractATSessionId(decodedStr)
|
||||
if err != nil {
|
||||
ctx = context.WithValue(ctx, "AT-SessionId", sessionId)
|
||||
}
|
||||
logg.DebugCtxf(ctx, "Received request:", decodedStr)
|
||||
}
|
||||
|
||||
if err := rqv.ParseForm(); err != nil {
|
||||
logg.Warnf("failed to parse form data", "err", err)
|
||||
return "", fmt.Errorf("failed to parse form data: %v", err)
|
||||
}
|
||||
|
||||
phoneNumber := rqv.FormValue("phoneNumber")
|
||||
if phoneNumber == "" {
|
||||
return "", fmt.Errorf("no phone number found")
|
||||
}
|
||||
|
||||
formattedNumber, err := common.FormatPhoneNumber(phoneNumber)
|
||||
if err != nil {
|
||||
logg.Warnf("failed to format phone number", "err", err)
|
||||
return "", fmt.Errorf("failed to format number")
|
||||
}
|
||||
|
||||
return formattedNumber, nil
|
||||
}
|
||||
|
||||
func (arp *ATRequestParser) GetInput(rq any) ([]byte, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
return nil, handlers.ErrInvalidRequest
|
||||
}
|
||||
if err := rqv.ParseForm(); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse form data: %v", err)
|
||||
}
|
||||
|
||||
text := rqv.FormValue("text")
|
||||
|
||||
parts := strings.Split(text, "*")
|
||||
if len(parts) == 0 {
|
||||
return nil, fmt.Errorf("no input found")
|
||||
}
|
||||
|
||||
trimmedInput := strings.TrimSpace(parts[len(parts)-1])
|
||||
return []byte(trimmedInput), nil
|
||||
}
|
||||
|
||||
func parseQueryParams(query string) map[string]string {
|
||||
params := make(map[string]string)
|
||||
|
||||
queryParams := strings.Split(query, "&")
|
||||
for _, param := range queryParams {
|
||||
// Split each key-value pair by '='
|
||||
parts := strings.SplitN(param, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
params[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func extractATSessionId(decodedStr string) (string, error) {
|
||||
var data map[string]string
|
||||
err := json.Unmarshal([]byte(decodedStr), &data)
|
||||
|
||||
if err != nil {
|
||||
logg.Errorf("Error unmarshalling JSON: %v", err)
|
||||
return "", nil
|
||||
}
|
||||
decodedBody, err := url.QueryUnescape(data["body"])
|
||||
if err != nil {
|
||||
logg.Errorf("Error URL-decoding body: %v", err)
|
||||
return "", nil
|
||||
}
|
||||
params := parseQueryParams(decodedBody)
|
||||
|
||||
sessionId := params["sessionId"]
|
||||
return sessionId, nil
|
||||
|
||||
}
|
@ -1,19 +1,25 @@
|
||||
package http
|
||||
package at
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
httpserver "git.grassecon.net/urdt/ussd/internal/http"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla().WithDomain("atserver").WithContextKey("SessionId").WithContextKey("AT-SessionId")
|
||||
)
|
||||
|
||||
type ATSessionHandler struct {
|
||||
*SessionHandler
|
||||
*httpserver.SessionHandler
|
||||
}
|
||||
|
||||
func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler {
|
||||
return &ATSessionHandler{
|
||||
SessionHandler: ToSessionHandler(h),
|
||||
SessionHandler: httpserver.ToSessionHandler(h),
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,17 +34,17 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
||||
|
||||
rp := ash.GetRequestParser()
|
||||
cfg := ash.GetConfig()
|
||||
cfg.SessionId, err = rp.GetSessionId(req)
|
||||
cfg.SessionId, err = rp.GetSessionId(req.Context(), req)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||
ash.writeError(w, 400, err)
|
||||
ash.WriteError(w, 400, err)
|
||||
return
|
||||
}
|
||||
rqs.Config = cfg
|
||||
rqs.Input, err = rp.GetInput(req)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||
ash.writeError(w, 400, err)
|
||||
ash.WriteError(w, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -53,7 +59,7 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
||||
}
|
||||
|
||||
if code != 200 {
|
||||
ash.writeError(w, 500, err)
|
||||
ash.WriteError(w, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -61,13 +67,13 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
rqs, err = ash.Output(rqs)
|
||||
if err != nil {
|
||||
ash.writeError(w, 500, err)
|
||||
ash.WriteError(w, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
rqs, err = ash.Reset(rqs)
|
||||
if err != nil {
|
||||
ash.writeError(w, 500, err)
|
||||
ash.WriteError(w, 500, err)
|
||||
return
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package http
|
||||
package at
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
@ -16,16 +15,6 @@ import (
|
||||
"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks"
|
||||
)
|
||||
|
||||
// invalidRequestType is a custom type to test invalid request scenarios
|
||||
type invalidRequestType struct{}
|
||||
|
||||
// errorReader is a helper type that always returns an error when Read is called
|
||||
type errorReader struct{}
|
||||
|
||||
func (e *errorReader) Read(p []byte) (n int, err error) {
|
||||
return 0, errors.New("read error")
|
||||
}
|
||||
|
||||
func TestNewATSessionHandler(t *testing.T) {
|
||||
mockHandler := &httpmocks.MockRequestHandler{}
|
||||
ash := NewATSessionHandler(mockHandler)
|
||||
@ -242,208 +231,4 @@ func TestATSessionHandler_Output(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionHandler_ServeHTTP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sessionID string
|
||||
input []byte
|
||||
parserErr error
|
||||
processErr error
|
||||
outputErr error
|
||||
resetErr error
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "Success",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Missing Session ID",
|
||||
sessionID: "",
|
||||
parserErr: handlers.ErrSessionMissing,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Process Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
processErr: handlers.ErrStorage,
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
name: "Output Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
outputErr: errors.New("output error"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Reset Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
resetErr: errors.New("reset error"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockRequestParser := &httpmocks.MockRequestParser{
|
||||
GetSessionIdFunc: func(any) (string, error) {
|
||||
return tt.sessionID, tt.parserErr
|
||||
},
|
||||
GetInputFunc: func(any) ([]byte, error) {
|
||||
return tt.input, nil
|
||||
},
|
||||
}
|
||||
|
||||
mockRequestHandler := &httpmocks.MockRequestHandler{
|
||||
ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.processErr
|
||||
},
|
||||
OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.outputErr
|
||||
},
|
||||
ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.resetErr
|
||||
},
|
||||
GetRequestParserFunc: func() handlers.RequestParser {
|
||||
return mockRequestParser
|
||||
},
|
||||
GetConfigFunc: func() engine.Config {
|
||||
return engine.Config{}
|
||||
},
|
||||
}
|
||||
|
||||
sessionHandler := ToSessionHandler(mockRequestHandler)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input))
|
||||
req.Header.Set("X-Vise-Session", tt.sessionID)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
sessionHandler.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != tt.expectedStatus {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, tt.expectedStatus)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionHandler_writeError(t *testing.T) {
|
||||
handler := &SessionHandler{}
|
||||
mockWriter := &httpmocks.MockWriter{}
|
||||
err := errors.New("test error")
|
||||
|
||||
handler.writeError(mockWriter, http.StatusBadRequest, err)
|
||||
|
||||
if mockWriter.WrittenString != "" {
|
||||
t.Errorf("Expected empty body, got %s", mockWriter.WrittenString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRequestParser_GetSessionId(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
request any
|
||||
expectedID string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Valid Session ID",
|
||||
request: func() *http.Request {
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
req.Header.Set("X-Vise-Session", "123456")
|
||||
return req
|
||||
}(),
|
||||
expectedID: "123456",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Missing Session ID",
|
||||
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
||||
expectedID: "",
|
||||
expectedError: handlers.ErrSessionMissing,
|
||||
},
|
||||
{
|
||||
name: "Invalid Request Type",
|
||||
request: invalidRequestType{},
|
||||
expectedID: "",
|
||||
expectedError: handlers.ErrInvalidRequest,
|
||||
},
|
||||
}
|
||||
|
||||
parser := &DefaultRequestParser{}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
id, err := parser.GetSessionId(tt.request)
|
||||
|
||||
if id != tt.expectedID {
|
||||
t.Errorf("Expected session ID %s, got %s", tt.expectedID, id)
|
||||
}
|
||||
|
||||
if err != tt.expectedError {
|
||||
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRequestParser_GetInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
request any
|
||||
expectedInput []byte
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Valid Input",
|
||||
request: func() *http.Request {
|
||||
return httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString("test input"))
|
||||
}(),
|
||||
expectedInput: []byte("test input"),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Empty Input",
|
||||
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
||||
expectedInput: []byte{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Invalid Request Type",
|
||||
request: invalidRequestType{},
|
||||
expectedInput: nil,
|
||||
expectedError: handlers.ErrInvalidRequest,
|
||||
},
|
||||
{
|
||||
name: "Read Error",
|
||||
request: func() *http.Request {
|
||||
return httptest.NewRequest(http.MethodPost, "/", &errorReader{})
|
||||
}(),
|
||||
expectedInput: nil,
|
||||
expectedError: errors.New("read error"),
|
||||
},
|
||||
}
|
||||
|
||||
parser := &DefaultRequestParser{}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input, err := parser.GetInput(tt.request)
|
||||
|
||||
if !bytes.Equal(input, tt.expectedInput) {
|
||||
t.Errorf("Expected input %s, got %s", tt.expectedInput, input)
|
||||
}
|
||||
|
||||
if err != tt.expectedError && (err == nil || err.Error() != tt.expectedError.Error()) {
|
||||
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
37
internal/http/parse.go
Normal file
37
internal/http/parse.go
Normal file
@ -0,0 +1,37 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
)
|
||||
|
||||
type DefaultRequestParser struct {
|
||||
}
|
||||
|
||||
func (rp *DefaultRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
return "", handlers.ErrInvalidRequest
|
||||
}
|
||||
v := rqv.Header.Get("X-Vise-Session")
|
||||
if v == "" {
|
||||
return "", handlers.ErrSessionMissing
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
return nil, handlers.ErrInvalidRequest
|
||||
}
|
||||
defer rqv.Body.Close()
|
||||
v, err := ioutil.ReadAll(rqv.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@ -14,35 +13,6 @@ var (
|
||||
logg = logging.NewVanilla().WithDomain("httpserver")
|
||||
)
|
||||
|
||||
type DefaultRequestParser struct {
|
||||
}
|
||||
|
||||
|
||||
func(rp *DefaultRequestParser) GetSessionId(rq any) (string, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
return "", handlers.ErrInvalidRequest
|
||||
}
|
||||
v := rqv.Header.Get("X-Vise-Session")
|
||||
if v == "" {
|
||||
return "", handlers.ErrSessionMissing
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func(rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
return nil, handlers.ErrInvalidRequest
|
||||
}
|
||||
defer rqv.Body.Close()
|
||||
v, err := ioutil.ReadAll(rqv.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
type SessionHandler struct {
|
||||
handlers.RequestHandler
|
||||
}
|
||||
@ -53,16 +23,15 @@ func ToSessionHandler(h handlers.RequestHandler) *SessionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
func(f *SessionHandler) writeError(w http.ResponseWriter, code int, err error) {
|
||||
func (f *SessionHandler) WriteError(w http.ResponseWriter, code int, err error) {
|
||||
s := err.Error()
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(s)))
|
||||
w.WriteHeader(code)
|
||||
_, err = w.Write([]byte{})
|
||||
_, err = w.Write([]byte(s))
|
||||
if err != nil {
|
||||
logg.Errorf("error writing error!!", "err", err, "olderr", s)
|
||||
w.WriteHeader(500)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
@ -77,16 +46,16 @@ func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
rp := f.GetRequestParser()
|
||||
cfg := f.GetConfig()
|
||||
cfg.SessionId, err = rp.GetSessionId(req)
|
||||
cfg.SessionId, err = rp.GetSessionId(req.Context(), req)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||
f.writeError(w, 400, err)
|
||||
f.WriteError(w, 400, err)
|
||||
}
|
||||
rqs.Config = cfg
|
||||
rqs.Input, err = rp.GetInput(req)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||
f.writeError(w, 400, err)
|
||||
f.WriteError(w, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -103,7 +72,7 @@ func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
if code != 200 {
|
||||
f.writeError(w, 500, err)
|
||||
f.WriteError(w, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -112,11 +81,11 @@ func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
rqs, err = f.Output(rqs)
|
||||
rqs, perr = f.Reset(rqs)
|
||||
if err != nil {
|
||||
f.writeError(w, 500, err)
|
||||
f.WriteError(w, 500, err)
|
||||
return
|
||||
}
|
||||
if perr != nil {
|
||||
f.writeError(w, 500, perr)
|
||||
f.WriteError(w, 500, perr)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
230
internal/http/server_test.go
Normal file
230
internal/http/server_test.go
Normal file
@ -0,0 +1,230 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks"
|
||||
)
|
||||
|
||||
// invalidRequestType is a custom type to test invalid request scenarios
|
||||
type invalidRequestType struct{}
|
||||
|
||||
// errorReader is a helper type that always returns an error when Read is called
|
||||
type errorReader struct{}
|
||||
|
||||
func (e *errorReader) Read(p []byte) (n int, err error) {
|
||||
return 0, errors.New("read error")
|
||||
}
|
||||
|
||||
func TestSessionHandler_ServeHTTP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sessionID string
|
||||
input []byte
|
||||
parserErr error
|
||||
processErr error
|
||||
outputErr error
|
||||
resetErr error
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "Success",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Missing Session ID",
|
||||
sessionID: "",
|
||||
parserErr: handlers.ErrSessionMissing,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Process Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
processErr: handlers.ErrStorage,
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
name: "Output Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
outputErr: errors.New("output error"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Reset Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
resetErr: errors.New("reset error"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockRequestParser := &httpmocks.MockRequestParser{
|
||||
GetSessionIdFunc: func(any) (string, error) {
|
||||
return tt.sessionID, tt.parserErr
|
||||
},
|
||||
GetInputFunc: func(any) ([]byte, error) {
|
||||
return tt.input, nil
|
||||
},
|
||||
}
|
||||
|
||||
mockRequestHandler := &httpmocks.MockRequestHandler{
|
||||
ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.processErr
|
||||
},
|
||||
OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.outputErr
|
||||
},
|
||||
ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.resetErr
|
||||
},
|
||||
GetRequestParserFunc: func() handlers.RequestParser {
|
||||
return mockRequestParser
|
||||
},
|
||||
GetConfigFunc: func() engine.Config {
|
||||
return engine.Config{}
|
||||
},
|
||||
}
|
||||
|
||||
sessionHandler := ToSessionHandler(mockRequestHandler)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input))
|
||||
req.Header.Set("X-Vise-Session", tt.sessionID)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
sessionHandler.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != tt.expectedStatus {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, tt.expectedStatus)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionHandler_WriteError(t *testing.T) {
|
||||
handler := &SessionHandler{}
|
||||
mockWriter := &httpmocks.MockWriter{}
|
||||
err := errors.New("test error")
|
||||
|
||||
handler.WriteError(mockWriter, http.StatusBadRequest, err)
|
||||
|
||||
if mockWriter.WrittenString != "" {
|
||||
t.Errorf("Expected empty body, got %s", mockWriter.WrittenString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRequestParser_GetSessionId(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
request any
|
||||
expectedID string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Valid Session ID",
|
||||
request: func() *http.Request {
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
req.Header.Set("X-Vise-Session", "123456")
|
||||
return req
|
||||
}(),
|
||||
expectedID: "123456",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Missing Session ID",
|
||||
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
||||
expectedID: "",
|
||||
expectedError: handlers.ErrSessionMissing,
|
||||
},
|
||||
{
|
||||
name: "Invalid Request Type",
|
||||
request: invalidRequestType{},
|
||||
expectedID: "",
|
||||
expectedError: handlers.ErrInvalidRequest,
|
||||
},
|
||||
}
|
||||
|
||||
parser := &DefaultRequestParser{}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
id, err := parser.GetSessionId(context.Background(),tt.request)
|
||||
|
||||
if id != tt.expectedID {
|
||||
t.Errorf("Expected session ID %s, got %s", tt.expectedID, id)
|
||||
}
|
||||
|
||||
if err != tt.expectedError {
|
||||
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRequestParser_GetInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
request any
|
||||
expectedInput []byte
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Valid Input",
|
||||
request: func() *http.Request {
|
||||
return httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString("test input"))
|
||||
}(),
|
||||
expectedInput: []byte("test input"),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Empty Input",
|
||||
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
||||
expectedInput: []byte{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Invalid Request Type",
|
||||
request: invalidRequestType{},
|
||||
expectedInput: nil,
|
||||
expectedError: handlers.ErrInvalidRequest,
|
||||
},
|
||||
{
|
||||
name: "Read Error",
|
||||
request: func() *http.Request {
|
||||
return httptest.NewRequest(http.MethodPost, "/", &errorReader{})
|
||||
}(),
|
||||
expectedInput: nil,
|
||||
expectedError: errors.New("read error"),
|
||||
},
|
||||
}
|
||||
|
||||
parser := &DefaultRequestParser{}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input, err := parser.GetInput(tt.request)
|
||||
|
||||
if !bytes.Equal(input, tt.expectedInput) {
|
||||
t.Errorf("Expected input %s, got %s", tt.expectedInput, input)
|
||||
}
|
||||
|
||||
if err != tt.expectedError && (err == nil || err.Error() != tt.expectedError.Error()) {
|
||||
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
65
internal/ssh/keystore.go
Normal file
65
internal/ssh/keystore.go
Normal file
@ -0,0 +1,65 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"git.defalsify.org/vise.git/db"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm"
|
||||
)
|
||||
|
||||
type SshKeyStore struct {
|
||||
store db.Db
|
||||
}
|
||||
|
||||
func NewSshKeyStore(ctx context.Context, dbDir string) (*SshKeyStore, error) {
|
||||
keyStore := &SshKeyStore{}
|
||||
keyStoreFile := path.Join(dbDir, "ssh_authorized_keys.gdbm")
|
||||
keyStore.store = dbstorage.NewThreadGdbmDb()
|
||||
err := keyStore.store.Connect(ctx, keyStoreFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return keyStore, nil
|
||||
}
|
||||
|
||||
func(s *SshKeyStore) AddFromFile(ctx context.Context, fp string, sessionId string) error {
|
||||
_, err := os.Stat(fp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot open ssh server public key file: %v\n", err)
|
||||
}
|
||||
|
||||
publicBytes, err := os.ReadFile(fp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to load public key: %v", err)
|
||||
}
|
||||
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(publicBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse public key: %v", err)
|
||||
}
|
||||
k := append([]byte{0x01}, pubKey.Marshal()...)
|
||||
s.store.SetPrefix(storage.DATATYPE_EXTEND)
|
||||
logg.Infof("Added key", "sessionId", sessionId, "public key", string(publicBytes))
|
||||
return s.store.Put(ctx, k, []byte(sessionId))
|
||||
}
|
||||
|
||||
func(s *SshKeyStore) Get(ctx context.Context, pubKey ssh.PublicKey) (string, error) {
|
||||
s.store.SetLanguage(nil)
|
||||
s.store.SetPrefix(storage.DATATYPE_EXTEND)
|
||||
k := append([]byte{0x01}, pubKey.Marshal()...)
|
||||
v, err := s.store.Get(ctx, k)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(v), nil
|
||||
}
|
||||
|
||||
func(s *SshKeyStore) Close() error {
|
||||
return s.store.Close()
|
||||
}
|
287
internal/ssh/ssh.go
Normal file
287
internal/ssh/ssh.go
Normal file
@ -0,0 +1,287 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/state"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla().WithDomain("ssh")
|
||||
)
|
||||
|
||||
type auther struct {
|
||||
Ctx context.Context
|
||||
keyStore *SshKeyStore
|
||||
auth map[string]string
|
||||
}
|
||||
|
||||
func NewAuther(ctx context.Context, keyStore *SshKeyStore) *auther {
|
||||
return &auther{
|
||||
Ctx: ctx,
|
||||
keyStore: keyStore,
|
||||
auth: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func(a *auther) Check(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
va, err := a.keyStore.Get(a.Ctx, pubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ka := hex.EncodeToString(conn.SessionID())
|
||||
a.auth[ka] = va
|
||||
fmt.Fprintf(os.Stderr, "connect: %s -> %s\n", ka, va)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func(a *auther) FromConn(c *ssh.ServerConn) (string, error) {
|
||||
if c == nil {
|
||||
return "", errors.New("nil server conn")
|
||||
}
|
||||
if c.Conn == nil {
|
||||
return "", errors.New("nil underlying conn")
|
||||
}
|
||||
return a.Get(c.Conn.SessionID())
|
||||
}
|
||||
|
||||
|
||||
func(a *auther) Get(k []byte) (string, error) {
|
||||
ka := hex.EncodeToString(k)
|
||||
v, ok := a.auth[ka]
|
||||
if !ok {
|
||||
return "", errors.New("not found")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func(s *SshRunner) serve(ctx context.Context, sessionId string, ch ssh.NewChannel, en engine.Engine) error {
|
||||
if ch == nil {
|
||||
return errors.New("nil channel")
|
||||
}
|
||||
if ch.ChannelType() != "session" {
|
||||
ch.Reject(ssh.UnknownChannelType, "that is not the channel you are looking for")
|
||||
return errors.New("not a session")
|
||||
}
|
||||
channel, requests, err := ch.Accept()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer channel.Close()
|
||||
s.wg.Add(1)
|
||||
go func(reqIn <-chan *ssh.Request) {
|
||||
defer s.wg.Done()
|
||||
for req := range reqIn {
|
||||
req.Reply(req.Type == "shell", nil)
|
||||
}
|
||||
_ = requests
|
||||
}(requests)
|
||||
|
||||
cont, err := en.Exec(ctx, []byte{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("initial engine exec err: %v", err)
|
||||
}
|
||||
|
||||
var input [state.INPUT_LIMIT]byte
|
||||
for cont {
|
||||
c, err := en.Flush(ctx, channel)
|
||||
if err != nil {
|
||||
return fmt.Errorf("flush err: %v", err)
|
||||
}
|
||||
_, err = channel.Write([]byte{0x0a})
|
||||
if err != nil {
|
||||
return fmt.Errorf("newline err: %v", err)
|
||||
}
|
||||
c, err = channel.Read(input[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("read input fail: %v", err)
|
||||
}
|
||||
logg.TraceCtxf(ctx, "input read", "c", c, "input", input[:c-1])
|
||||
cont, err = en.Exec(ctx, input[:c-1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("engine exec err: %v", err)
|
||||
}
|
||||
logg.TraceCtxf(ctx, "exec cont", "cont", cont, "en", en)
|
||||
_ = c
|
||||
}
|
||||
c, err := en.Flush(ctx, channel)
|
||||
if err != nil {
|
||||
return fmt.Errorf("last flush err: %v", err)
|
||||
}
|
||||
_ = c
|
||||
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
|
||||
}
|
||||
|
||||
rs, err := menuStorageService.GetResource(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pe, err := menuStorageService.GetPersister(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
userdatastore, err := menuStorageService.GetUserdataDb(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
dbResource, ok := rs.(*resource.DbResource)
|
||||
if !ok {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
lhs, err := handlers.NewLocalHandlerService(ctx, s.FlagFile, true, dbResource, s.Cfg, rs)
|
||||
lhs.SetDataStore(&userdatastore)
|
||||
lhs.SetPersister(pe)
|
||||
lhs.Cfg.SessionId = sessionId
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// TODO: clear up why pointer here and by-value other cmds
|
||||
accountService := &remote.AccountService{}
|
||||
hl, err := lhs.GetHandler(accountService)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
en := lhs.GetEngine()
|
||||
en = en.WithFirst(hl.Init)
|
||||
if s.Debug {
|
||||
en = en.WithDebug(nil)
|
||||
}
|
||||
// TODO: this is getting very hacky!
|
||||
closer := func() {
|
||||
err := menuStorageService.Close()
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "menu storage service cleanup fail", "err", err)
|
||||
}
|
||||
}
|
||||
return en, closer, nil
|
||||
}
|
||||
|
||||
// adapted example from crypto/ssh package, NewServerConn doc
|
||||
func(s *SshRunner) Run(ctx context.Context, keyStore *SshKeyStore) {
|
||||
running := true
|
||||
|
||||
// TODO: waitgroup should probably not be global
|
||||
defer s.wg.Wait()
|
||||
|
||||
auth := NewAuther(ctx, keyStore)
|
||||
cfg := ssh.ServerConfig{
|
||||
PublicKeyCallback: auth.Check,
|
||||
}
|
||||
|
||||
privateBytes, err := os.ReadFile(s.SrvKeyFile)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "Failed to load private key", "err", err)
|
||||
}
|
||||
private, err := ssh.ParsePrivateKey(privateBytes)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "Failed to parse private key", "err", err)
|
||||
}
|
||||
srvPub := private.PublicKey()
|
||||
srvPubStr := base64.StdEncoding.EncodeToString(srvPub.Marshal())
|
||||
logg.InfoCtxf(ctx, "have server key", "type", srvPub.Type(), "public", srvPubStr)
|
||||
cfg.AddHostKey(private)
|
||||
|
||||
s.lst, err = net.Listen("tcp", fmt.Sprintf("%s:%d", s.Host, s.Port))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for running {
|
||||
conn, err := s.lst.Accept()
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "ssh accept error", "err", err)
|
||||
running = false
|
||||
continue
|
||||
}
|
||||
|
||||
go func(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
for true {
|
||||
srvConn, nC, rC, err := ssh.NewServerConn(conn, &cfg)
|
||||
if err != nil {
|
||||
logg.InfoCtxf(ctx, "rejected client", "err", err)
|
||||
return
|
||||
}
|
||||
logg.DebugCtxf(ctx, "ssh client connected", "conn", srvConn)
|
||||
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
ssh.DiscardRequests(rC)
|
||||
s.wg.Done()
|
||||
}()
|
||||
|
||||
sessionId, err := auth.FromConn(srvConn)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "Cannot find authentication")
|
||||
return
|
||||
}
|
||||
en, closer, err := s.GetEngine(sessionId)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "engine won't start", "err", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
err := en.Finish()
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "engine won't stop", "err", err)
|
||||
}
|
||||
closer()
|
||||
}()
|
||||
for ch := range nC {
|
||||
err = s.serve(ctx, sessionId, ch, en)
|
||||
logg.ErrorCtxf(ctx, "ssh server finish", "err", err)
|
||||
}
|
||||
}
|
||||
}(conn)
|
||||
}
|
||||
}
|
@ -6,6 +6,11 @@ import (
|
||||
"git.defalsify.org/vise.git/db"
|
||||
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla().WithDomain("gdbmstorage")
|
||||
)
|
||||
|
||||
var (
|
||||
@ -115,7 +120,8 @@ func(tdb *ThreadGdbmDb) Close() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func(tdb *ThreadGdbmDb) Dump(_ context.Context, _ []byte) (*db.Dumper, error) {
|
||||
logg.Warnf("method not implemented for thread gdbm db")
|
||||
return nil, nil
|
||||
func(tdb *ThreadGdbmDb) Dump(ctx context.Context, key []byte) (*db.Dumper, error) {
|
||||
tdb.reserve()
|
||||
defer tdb.release()
|
||||
return tdb.db.Dump(ctx, key)
|
||||
}
|
@ -5,6 +5,10 @@ import (
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
)
|
||||
|
||||
const (
|
||||
DATATYPE_EXTEND = 128
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
Persister *persist.Persister
|
||||
UserdataDb db.Db
|
||||
|
@ -9,10 +9,12 @@ 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"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
gdbmstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -29,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
|
||||
@ -57,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 {
|
||||
@ -75,7 +100,7 @@ func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.D
|
||||
connStr := buildConnStr()
|
||||
err = newDb.Connect(ctx, connStr)
|
||||
} else {
|
||||
newDb = NewThreadGdbmDb()
|
||||
newDb = gdbmstorage.NewThreadGdbmDb()
|
||||
storeFile := path.Join(ms.dbDir, fileName)
|
||||
err = newDb.Connect(ctx, storeFile)
|
||||
}
|
||||
@ -119,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
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,14 @@
|
||||
package httpmocks
|
||||
|
||||
import "context"
|
||||
|
||||
// MockRequestParser implements the handlers.RequestParser interface for testing
|
||||
type MockRequestParser struct {
|
||||
GetSessionIdFunc func(any) (string, error)
|
||||
GetInputFunc func(any) ([]byte, error)
|
||||
}
|
||||
|
||||
func (m *MockRequestParser) GetSessionId(rq any) (string, error) {
|
||||
func (m *MockRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) {
|
||||
return m.GetSessionIdFunc(rq)
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ package utils
|
||||
var isoCodes = map[string]bool{
|
||||
"eng": true, // English
|
||||
"swa": true, // Swahili
|
||||
|
||||
"default": true, // Default language: English
|
||||
}
|
||||
|
||||
func IsValidISO639(code string) bool {
|
||||
|
@ -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",
|
||||
@ -62,10 +62,10 @@
|
||||
},
|
||||
{
|
||||
"input": "1234",
|
||||
"expectedContent": "Select language:\n0:English\n1:Kiswahili"
|
||||
"expectedContent": "Select language:\n1:English\n2:Kiswahili"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"input": "1",
|
||||
"expectedContent": "Your language change request was successful.\n0:Back\n9:Quit"
|
||||
},
|
||||
{
|
||||
@ -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",
|
||||
@ -108,7 +108,6 @@
|
||||
{
|
||||
"input": "0",
|
||||
"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",
|
||||
@ -154,7 +153,6 @@
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
|
||||
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
@ -258,7 +256,6 @@
|
||||
"input": "0",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -430,7 +427,7 @@
|
||||
},
|
||||
{
|
||||
"input": "1234",
|
||||
"expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 84\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back"
|
||||
"expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 80\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
@ -444,9 +441,3 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -298,9 +298,10 @@ func TestMainMenuSend(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sessions := testData
|
||||
for _, session := range sessions {
|
||||
groups := driver.FilterGroupsByName(session.Groups, "send_with_invalid_inputs")
|
||||
groups := driver.FilterGroupsByName(session.Groups, "send_with_invite")
|
||||
for _, group := range groups {
|
||||
for _, step := range group.Steps {
|
||||
for index, step := range group.Steps {
|
||||
t.Logf("step %v with input %v", index, step.Input)
|
||||
cont, err := en.Exec(ctx, []byte(step.Input))
|
||||
if err != nil {
|
||||
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
|
||||
|
@ -7,14 +7,14 @@
|
||||
"steps": [
|
||||
{
|
||||
"input": "",
|
||||
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:English\n1:Kiswahili"
|
||||
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n1:English\n2:Kiswahili"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "Do you agree to terms and conditions?\n0:Yes\n1:No"
|
||||
"input": "1",
|
||||
"expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n1:Yes\n2:No"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"input": "1",
|
||||
"expectedContent": "Please enter a new four number PIN for your account:\n0:Exit"
|
||||
},
|
||||
{
|
||||
@ -40,14 +40,14 @@
|
||||
"steps": [
|
||||
{
|
||||
"input": "",
|
||||
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:English\n1:Kiswahili"
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "Do you agree to terms and conditions?\n0:Yes\n1:No"
|
||||
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n1:English\n2:Kiswahili"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
"expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n1:Yes\n2:No"
|
||||
},
|
||||
{
|
||||
"input": "2",
|
||||
"expectedContent": "Thank you for using Sarafu. Goodbye!"
|
||||
}
|
||||
]
|
||||
@ -64,8 +64,8 @@
|
||||
"expectedContent": "Enter recipient's phone number/address/alias:\n0:Back"
|
||||
},
|
||||
{
|
||||
"input": "000",
|
||||
"expectedContent": "000 is invalid, please try again:\n1:Retry\n9:Quit"
|
||||
"input": "0@0",
|
||||
"expectedContent": "0@0 is invalid, please try again:\n1:Retry\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
|
@ -7,3 +7,4 @@ HALT
|
||||
INCMP _ 0
|
||||
INCMP my_balance 1
|
||||
INCMP community_balance 2
|
||||
INCMP . *
|
||||
|
2
services/registration/blocked_account.vis
Normal file
2
services/registration/blocked_account.vis
Normal file
@ -0,0 +1,2 @@
|
||||
LOAD show_blocked_account 0
|
||||
HALT
|
@ -2,9 +2,9 @@ LOAD reset_account_authorized 0
|
||||
LOAD reset_incorrect 0
|
||||
CATCH incorrect_pin flag_incorrect_pin 1
|
||||
CATCH pin_entry flag_account_authorized 0
|
||||
MOUT english 0
|
||||
MOUT kiswahili 1
|
||||
MOUT english 1
|
||||
MOUT kiswahili 2
|
||||
HALT
|
||||
INCMP set_default 0
|
||||
INCMP set_swa 1
|
||||
INCMP set_eng 1
|
||||
INCMP set_swa 2
|
||||
INCMP . *
|
||||
|
@ -9,3 +9,4 @@ MOUT quit 9
|
||||
HALT
|
||||
INCMP _ 0
|
||||
INCMP quit 9
|
||||
INCMP . *
|
||||
|
@ -1,2 +1,2 @@
|
||||
Jina la kwanza la sasa {{.get_current_profile_info}}
|
||||
Jina la kwanza la sasa: {{.get_current_profile_info}}
|
||||
Weka majina yako ya kwanza:
|
@ -1,2 +1,2 @@
|
||||
Eneo la sasa {{.get_current_profile_info}}
|
||||
Eneo la sasa: {{.get_current_profile_info}}
|
||||
Weka eneo:
|
@ -10,5 +10,4 @@ CATCH _ flag_back_set 1
|
||||
RELOAD save_offerings
|
||||
INCMP _ 0
|
||||
CATCH pin_entry flag_offerings_set 1
|
||||
CATCH pin_entry flag_offerings_set 0
|
||||
INCMP update_profile_items *
|
||||
|
@ -20,3 +20,4 @@ INCMP edit_yob 4
|
||||
INCMP edit_location 5
|
||||
INCMP edit_offerings 6
|
||||
INCMP view_profile 7
|
||||
INCMP . *
|
||||
|
@ -1,2 +1,2 @@
|
||||
Mwaka wa sasa wa kuzaliwa {{.get_current_profile_info}}
|
||||
Mwaka wa sasa wa kuzaliwa: {{.get_current_profile_info}}
|
||||
Weka mwaka wa kuzaliwa
|
@ -1 +1 @@
|
||||
Incorrect PIN
|
||||
Incorrect PIN. You have: {{.reset_incorrect}} remaining attempt(s).
|
@ -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
|
||||
|
@ -1 +1 @@
|
||||
PIN ulioeka sio sahihi
|
||||
PIN ulioeka sio sahihi, una majaribio: {{.reset_incorrect}} yaliyobaki
|
@ -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"
|
||||
|
||||
|
@ -14,3 +14,4 @@ INCMP balances 3
|
||||
INCMP check_statement 4
|
||||
INCMP pin_management 5
|
||||
INCMP address 6
|
||||
INCMP . *
|
||||
|
@ -9,3 +9,4 @@ MOUT quit 9
|
||||
HALT
|
||||
INCMP _ 0
|
||||
INCMP quit 9
|
||||
INCMP . *
|
||||
|
@ -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,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
|
||||
|
@ -11,3 +11,4 @@ INCMP _ 0
|
||||
INCMP set_male 1
|
||||
INCMP set_female 2
|
||||
INCMP set_unspecified 3
|
||||
INCMP . *
|
||||
|
@ -1,2 +1,2 @@
|
||||
Jinsia ya sasa {{.get_current_profile_info}}
|
||||
Jinsia ya sasa: {{.get_current_profile_info}}
|
||||
Chagua jinsia
|
@ -1,6 +1,6 @@
|
||||
MOUT english 0
|
||||
MOUT kiswahili 1
|
||||
MOUT english 1
|
||||
MOUT kiswahili 2
|
||||
HALT
|
||||
INCMP set_eng 0
|
||||
INCMP set_swa 1
|
||||
INCMP set_eng 1
|
||||
INCMP set_swa 2
|
||||
INCMP . *
|
||||
|
4
services/registration/set_default.vis
Normal file
4
services/registration/set_default.vis
Normal file
@ -0,0 +1,4 @@
|
||||
LOAD set_language 6
|
||||
RELOAD set_language
|
||||
CATCH terms flag_account_created 0
|
||||
MOVE language_changed
|
@ -1 +1,2 @@
|
||||
Do you agree to terms and conditions?
|
||||
https://grassecon.org/pages/terms-and-conditions
|
||||
|
@ -1,5 +1,5 @@
|
||||
MOUT yes 0
|
||||
MOUT no 1
|
||||
MOUT yes 1
|
||||
MOUT no 2
|
||||
HALT
|
||||
INCMP create_pin 0
|
||||
INCMP create_pin 1
|
||||
INCMP quit *
|
||||
|
@ -1 +1,2 @@
|
||||
Kwa kutumia hii huduma umekubali sheria na masharti?
|
||||
https://grassecon.org/pages/terms-and-conditions
|
||||
|
@ -4,5 +4,8 @@ LOAD reset_incorrect 6
|
||||
CATCH incorrect_pin flag_incorrect_pin 1
|
||||
CATCH pin_entry flag_account_authorized 0
|
||||
MOUT back 0
|
||||
MOUT quit 9
|
||||
HALT
|
||||
INCMP _ 0
|
||||
INCMP quit 9
|
||||
INCMP . *
|
||||
|
Loading…
Reference in New Issue
Block a user