forked from urdt/ussd
		
	Compare commits
	
		
			229 Commits
		
	
	
		
			force-rest
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b11f11b5fa | |||
| 3d35a5de78 | |||
| a19ace85f8 | |||
| 8f5ed0cd4f | |||
| c29abfe21e | |||
| 9a6d8e5158 | |||
| db431c750e | |||
| 2b9c6d641e | |||
| 3747f87a7c | |||
| 1f0568df32 | |||
|  | 24c513d4f0 | ||
| b3fd6f5c1a | |||
| 73eb765408 | |||
| f660f6c19a | |||
| 3fccfaab61 | |||
|  | b50a51df9b | ||
|  | df8c9aab0c | ||
|  | ddefdd7fb3 | ||
| 5734011f96 | |||
|  | 379d98ccd5 | ||
| f40e11c267 | |||
| b698f08136 | |||
| 4d7589ad95 | |||
| efdb52bccd | |||
| 2ff9fed3c5 | |||
| 477b4cf8f6 | |||
| ed6651697a | |||
| c359d99075 | |||
| 8d477356f3 | |||
| 7f3294a8a2 | |||
| 4b5f08e25e | |||
| ea9cab930e | |||
| a37f6e6da3 | |||
| f59c3a53ef | |||
| 81c3378ea6 | |||
| 46a6d2bc6e | |||
|  | 721f80d0f2 | ||
| f49e54a562 | |||
|  | 5081b6d4ce | ||
| 4d72ae0313 | |||
| 4fe64a7747 | |||
| 3004698d5b | |||
| 50c7ff1046 | |||
| 07b061a68b | |||
| 6339f0c2e5 | |||
|  | 1fa830f286 | ||
| 64fba91670 | |||
| c15958a1ad | |||
| ee442daefa | |||
| 656052dc74 | |||
| 6c5873da6f | |||
| 11d30583a4 | |||
| f83f539046 | |||
| 562bd4fa24 | |||
| 90df0eefc3 | |||
| b37f2a0a11 | |||
| 68e4c9af03 | |||
| c12e867ac3 | |||
| 79de0a9092 | |||
| 3ee15497a5 | |||
|  | 599815c343 | ||
|  | 462c0d7677 | ||
| 80b96e9bf6 | |||
| b5561decd1 | |||
|  | 02823fd64e | ||
|  | cd575c2edb | ||
| f3d4f35718 | |||
| 52787bdb4d | |||
| 824d39908b | |||
|  | 52fd1eced2 | ||
| a312ea5b84 | |||
|  | 5c7a535288 | ||
| 4836162f40 | |||
|  | cc2f7b41df | ||
|  | d39740a09a | ||
|  | 2024cc96e2 | ||
|  | d2d878d5d7 | ||
| c995143543 | |||
| 44570e20ef | |||
| 362eb209ef | |||
| c69d3896f1 | |||
| 974af6b2a7 | |||
|  | bb4037e73f | ||
|  | 51b6fc0dde | ||
|  | cc9760125a | ||
|  | 3a9f3fa373 | ||
|  | 89c21847b9 | ||
|  | 450dfa02cc | ||
|  | f61e65f4fe | ||
|  | a4d6cef9c0 | ||
|  | 2992f7ae8e | ||
|  | dc61d05584 | ||
|  | 83857026d3 | ||
|  | 349051b5ef | ||
| 47b5ff0435 | |||
|  | e92e498726 | ||
|  | 25867cf05e | ||
|  | c3cbe1cd92 | ||
|  | 418080d093 | ||
|  | 2e30739ec9 | ||
| d5a2680500 | |||
|  | dc1674ec55 | ||
|  | 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 | |||
| 9b33117cb1 | |||
| 70b2fa4ac2 | |||
| fd6ff86579 | |||
| 549782f230 | |||
| 8cf4848b45 | |||
| 9f6c0a1111 | |||
|  | 1ab49647f6 | ||
|  | 8d4d8a48e0 | ||
| 7aea2af9a1 | |||
| 5cd791aae7 | |||
| df5e5f1a4b | |||
| 64c1fe5276 | |||
| f38ea59569 | |||
| 6cc285d1e8 | |||
| 0d7f7aaca1 | |||
|  | c820e89cb7 | ||
| e05f8e7291 | |||
| 2383e8ead3 | |||
| 1a4ee0d3e1 | |||
| 6f3b30e2fe | |||
| b1e4b63c6a | |||
| 3129e8210e | |||
|  | 604c16ec90 | ||
|  | a3e821fb16 | ||
|  | 890f50704f | ||
|  | 3416fdf50c | ||
|  | 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 | ||||
|  | ||||
							
								
								
									
										14
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								.env.example
									
									
									
									
									
								
							| @ -6,15 +6,15 @@ HOST=127.0.0.1 | ||||
| AT_ENDPOINT=/ussd/africastalking | ||||
| 
 | ||||
| #PostgreSQL | ||||
| DB_HOST=localhost | ||||
| DB_USER=postgres | ||||
| DB_PASSWORD=strongpass | ||||
| DB_NAME=urdt_ussd | ||||
| DB_PORT=5432 | ||||
| DB_SSLMODE=disable | ||||
| DB_TIMEZONE=Africa/Nairobi | ||||
| DB_CONN=postgres://postgres:strongpass@localhost:5432/urdt_ussd | ||||
| #DB_TIMEZONE=Africa/Nairobi | ||||
| #DB_SCHEMA=vise | ||||
| 
 | ||||
| #External API Calls | ||||
| CUSTODIAL_URL_BASE=http://localhost:5003 | ||||
| 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,153 +1,103 @@ | ||||
| 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/lang" | ||||
| 	"git.defalsify.org/vise.git/logging" | ||||
| 	"git.defalsify.org/vise.git/resource" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/common" | ||||
| 	"git.grassecon.net/urdt/ussd/config" | ||||
| 	"git.grassecon.net/urdt/ussd/initializers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/args" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||
| 	httpserver "git.grassecon.net/urdt/ussd/internal/http" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/http/at" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| 	"git.grassecon.net/urdt/ussd/remote" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logg      = logging.NewVanilla() | ||||
| 	scriptDir = path.Join("services", "registration") | ||||
| 
 | ||||
| 	build = "dev" | ||||
| 	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() | ||||
| 
 | ||||
| 	var dbDir string | ||||
| 	var connStr string | ||||
| 	var resourceDir string | ||||
| 	var size uint | ||||
| 	var database string | ||||
| 	var engineDebug bool | ||||
| 	var host string | ||||
| 	var port uint | ||||
| 	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") | ||||
| 	var err error | ||||
| 	var gettextDir string | ||||
| 	var langs args.LangVar | ||||
| 
 | ||||
| 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") | ||||
| 	flag.StringVar(&database, "db", "gdbm", "database to be used") | ||||
| 	flag.StringVar(&connStr, "c", "", "connection string") | ||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||
| 	flag.UintVar(&size, "s", 160, "max size of output") | ||||
| 	flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") | ||||
| 	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) | ||||
| 	if connStr == "" { | ||||
| 		connStr = config.DbConn | ||||
| 	} | ||||
| 	connData, err := storage.ToConnData(connStr) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	logg.Infof("start command", "build", build, "conn", connData, "resourcedir", resourceDir, "outputsize", size) | ||||
| 
 | ||||
| 	ctx := context.Background() | ||||
| 	ctx = context.WithValue(ctx, "Database", database) | ||||
| 	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), | ||||
| 		Root:          "root", | ||||
| 		OutputSize:    uint32(size), | ||||
| 		FlagCount:     uint32(128), | ||||
| 		MenuSeparator: menuSeparator, | ||||
| 	} | ||||
| 
 | ||||
| 	if engineDebug { | ||||
| 		cfg.EngineDebug = true | ||||
| 	} | ||||
| 
 | ||||
| 	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) | ||||
| 	rs, err := menuStorageService.GetResource(ctx) | ||||
| 	menuStorageService := storage.NewMenuStorageService(connData, resourceDir) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	err = menuStorageService.EnsureDbDir() | ||||
| 	rs, err := menuStorageService.GetResource(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| @ -191,9 +141,9 @@ func main() { | ||||
| 	} | ||||
| 	defer stateStore.Close() | ||||
| 
 | ||||
| 	rp := &atRequestParser{} | ||||
| 	rp := &at.ATRequestParser{} | ||||
| 	bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl) | ||||
| 	sh := httpserver.NewATSessionHandler(bsh) | ||||
| 	sh := at.NewATSessionHandler(bsh) | ||||
| 
 | ||||
| 	mux := http.NewServeMux() | ||||
| 	mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh) | ||||
|  | ||||
| @ -10,19 +10,22 @@ import ( | ||||
| 	"syscall" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/engine" | ||||
| 	"git.defalsify.org/vise.git/lang" | ||||
| 	"git.defalsify.org/vise.git/logging" | ||||
| 	"git.defalsify.org/vise.git/resource" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/config" | ||||
| 	"git.grassecon.net/urdt/ussd/initializers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/args" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| 	"git.grassecon.net/urdt/ussd/remote" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logg      = logging.NewVanilla() | ||||
| 	scriptDir = path.Join("services", "registration") | ||||
| 	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 | ||||
| } | ||||
| 
 | ||||
| @ -45,48 +48,68 @@ func (p *asyncRequestParser) GetInput(r any) ([]byte, error) { | ||||
| func main() { | ||||
| 	config.LoadConfig() | ||||
| 
 | ||||
| 	var connStr string | ||||
| 	var sessionId string | ||||
| 	var dbDir string | ||||
| 	var resourceDir string | ||||
| 	var size uint | ||||
| 	var database string | ||||
| 	var engineDebug bool | ||||
| 	var host string | ||||
| 	var port uint | ||||
| 	var err error | ||||
| 	var gettextDir string | ||||
| 	var langs args.LangVar | ||||
| 
 | ||||
| 	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") | ||||
| 	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") | ||||
| 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") | ||||
| 	flag.StringVar(&database, "db", "gdbm", "database to be used") | ||||
| 	flag.StringVar(&connStr, "c", "", "connection string") | ||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||
| 	flag.UintVar(&size, "s", 160, "max size of output") | ||||
| 	flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") | ||||
| 	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) | ||||
| 	if connStr == "" { | ||||
| 		connStr = config.DbConn | ||||
| 	} | ||||
| 	connData, err := storage.ToConnData(connStr) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	logg.Infof("start command", "conn", connData, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId) | ||||
| 
 | ||||
| 	ctx := context.Background() | ||||
| 	ctx = context.WithValue(ctx, "Database", database) | ||||
| 
 | ||||
| 	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), | ||||
| 		Root:          "root", | ||||
| 		OutputSize:    uint32(size), | ||||
| 		FlagCount:     uint32(128), | ||||
| 		MenuSeparator: menuSeparator, | ||||
| 	} | ||||
| 
 | ||||
| 	if engineDebug { | ||||
| 		cfg.EngineDebug = true | ||||
| 	} | ||||
| 
 | ||||
| 	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) | ||||
| 	rs, err := menuStorageService.GetResource(ctx) | ||||
| 	menuStorageService := storage.NewMenuStorageService(connData, resourceDir) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	err = menuStorageService.EnsureDbDir() | ||||
| 	rs, err := menuStorageService.GetResource(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
|  | ||||
| @ -12,11 +12,13 @@ import ( | ||||
| 	"syscall" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/engine" | ||||
| 	"git.defalsify.org/vise.git/lang" | ||||
| 	"git.defalsify.org/vise.git/logging" | ||||
| 	"git.defalsify.org/vise.git/resource" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/config" | ||||
| 	"git.grassecon.net/urdt/ussd/initializers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/args" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||
| 	httpserver "git.grassecon.net/urdt/ussd/internal/http" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| @ -24,8 +26,9 @@ import ( | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logg      = logging.NewVanilla() | ||||
| 	scriptDir = path.Join("services", "registration") | ||||
| 	logg          = logging.NewVanilla() | ||||
| 	scriptDir     = path.Join("services", "registration") | ||||
| 	menuSeparator = ": " | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| @ -35,46 +38,62 @@ func init() { | ||||
| func main() { | ||||
| 	config.LoadConfig() | ||||
| 
 | ||||
| 	var dbDir string | ||||
| 	var connStr string | ||||
| 	var resourceDir string | ||||
| 	var size uint | ||||
| 	var database string | ||||
| 	var engineDebug bool | ||||
| 	var host string | ||||
| 	var port uint | ||||
| 	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") | ||||
| 	var err error | ||||
| 	var gettextDir string | ||||
| 	var langs args.LangVar | ||||
| 
 | ||||
| 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") | ||||
| 	flag.StringVar(&database, "db", "gdbm", "database to be used") | ||||
| 	flag.StringVar(&connStr, "c", "", "connection string") | ||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||
| 	flag.UintVar(&size, "s", 160, "max size of output") | ||||
| 	flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") | ||||
| 	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) | ||||
| 	if connStr == "" { | ||||
| 		connStr = config.DbConn | ||||
| 	} | ||||
| 	connData, err := storage.ToConnData(connStr) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	logg.Infof("start command", "conn", connData, "resourcedir", resourceDir, "outputsize", size) | ||||
| 
 | ||||
| 	ctx := context.Background() | ||||
| 	ctx = context.WithValue(ctx, "Database", database) | ||||
| 
 | ||||
| 	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), | ||||
| 		Root:          "root", | ||||
| 		OutputSize:    uint32(size), | ||||
| 		FlagCount:     uint32(128), | ||||
| 		MenuSeparator: menuSeparator, | ||||
| 	} | ||||
| 
 | ||||
| 	if engineDebug { | ||||
| 		cfg.EngineDebug = true | ||||
| 	} | ||||
| 
 | ||||
| 	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) | ||||
| 	rs, err := menuStorageService.GetResource(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	menuStorageService := storage.NewMenuStorageService(connData, resourceDir) | ||||
| 
 | ||||
| 	err = menuStorageService.EnsureDbDir() | ||||
| 	rs, err := menuStorageService.GetResource(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
|  | ||||
							
								
								
									
										64
									
								
								cmd/main.go
									
									
									
									
									
								
							
							
						
						
									
										64
									
								
								cmd/main.go
									
									
									
									
									
								
							| @ -8,60 +8,88 @@ import ( | ||||
| 	"path" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/engine" | ||||
| 	"git.defalsify.org/vise.git/lang" | ||||
| 	"git.defalsify.org/vise.git/logging" | ||||
| 	"git.defalsify.org/vise.git/resource" | ||||
| 	"git.grassecon.net/urdt/ussd/config" | ||||
| 	"git.grassecon.net/urdt/ussd/initializers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/args" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| 	"git.grassecon.net/urdt/ussd/remote" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logg      = logging.NewVanilla() | ||||
| 	scriptDir = path.Join("services", "registration") | ||||
| 	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() | ||||
| 
 | ||||
| 	var dbDir string | ||||
| 	var connStr string | ||||
| 	var size uint | ||||
| 	var sessionId string | ||||
| 	var database string | ||||
| 	var engineDebug bool | ||||
| 	var resourceDir string | ||||
| 	var err error | ||||
| 	var gettextDir string | ||||
| 	var langs args.LangVar | ||||
| 
 | ||||
| 	flag.StringVar(&resourceDir, "resourcedir", scriptDir, "resource dir") | ||||
| 	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") | ||||
| 	flag.StringVar(&database, "db", "gdbm", "database to be used") | ||||
| 	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") | ||||
| 	flag.StringVar(&connStr, "c", "", "connection string") | ||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||
| 	flag.UintVar(&size, "s", 160, "max size of output") | ||||
| 	flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory") | ||||
| 	flag.Var(&langs, "language", "add symbol resolution for language") | ||||
| 	flag.Parse() | ||||
| 
 | ||||
| 	logg.Infof("start command", "dbdir", dbDir, "outputsize", size) | ||||
| 	if connStr == "" { | ||||
| 		connStr = config.DbConn | ||||
| 	} | ||||
| 	connData, err := storage.ToConnData(connStr) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	logg.Infof("start command", "conn", connData, "outputsize", size) | ||||
| 
 | ||||
| 	if len(langs.Langs()) == 0 { | ||||
| 		langs.Set(config.DefaultLanguage) | ||||
| 	} | ||||
| 
 | ||||
| 	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{ | ||||
| 		Root:       "root", | ||||
| 		SessionId:  sessionId, | ||||
| 		OutputSize: uint32(size), | ||||
| 		FlagCount:  uint32(128), | ||||
| 		Root:          "root", | ||||
| 		SessionId:     sessionId, | ||||
| 		OutputSize:    uint32(size), | ||||
| 		FlagCount:     uint32(128), | ||||
| 		MenuSeparator: menuSeparator, | ||||
| 	} | ||||
| 
 | ||||
| 	resourceDir := scriptDir | ||||
| 	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) | ||||
| 	menuStorageService := storage.NewMenuStorageService(connData, resourceDir) | ||||
| 
 | ||||
| 	err := menuStorageService.EnsureDbDir() | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	if gettextDir != "" { | ||||
| 		menuStorageService = menuStorageService.WithGettext(gettextDir, langs.Langs()) | ||||
| 	} | ||||
| 
 | ||||
| 	rs, err := menuStorageService.GetResource(ctx) | ||||
|  | ||||
							
								
								
									
										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> | ||||
| ``` | ||||
							
								
								
									
										144
									
								
								cmd/ssh/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								cmd/ssh/main.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,144 @@ | ||||
| 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/config" | ||||
| 	"git.grassecon.net/urdt/ussd/initializers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/ssh" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	wg        sync.WaitGroup | ||||
| 	keyStore  db.Db | ||||
| 	logg      = logging.NewVanilla() | ||||
| 	scriptDir = path.Join("services", "registration") | ||||
| 
 | ||||
| 	build = "dev" | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	initializers.LoadEnvVariables() | ||||
| } | ||||
| 
 | ||||
| func main() { | ||||
| 	config.LoadConfig() | ||||
| 
 | ||||
| 	var connStr string | ||||
| 	var authConnStr string | ||||
| 	var resourceDir string | ||||
| 	var size uint | ||||
| 	var engineDebug bool | ||||
| 	var stateDebug bool | ||||
| 	var host string | ||||
| 	var port uint | ||||
| 	flag.StringVar(&connStr, "c", "", "connection string") | ||||
| 	flag.StringVar(&authConnStr, "authdb", "", "auth connection string") | ||||
| 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") | ||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||
| 	flag.UintVar(&size, "s", 160, "max size of output") | ||||
| 	flag.StringVar(&host, "h", "127.0.0.1", "socket host") | ||||
| 	flag.UintVar(&port, "p", 7122, "socket port") | ||||
| 	flag.Parse() | ||||
| 
 | ||||
| 	if connStr == "" { | ||||
| 		connStr = config.DbConn | ||||
| 	} | ||||
| 	if authConnStr == "" { | ||||
| 		authConnStr = connStr | ||||
| 	} | ||||
| 	connData, err := storage.ToConnData(connStr) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	authConnData, err := storage.ToConnData(authConnStr) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, "auth connstr err: %v", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	sshKeyFile := flag.Arg(0) | ||||
| 	_, err = os.Stat(sshKeyFile) | ||||
| 	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", "conn", connData, "authconn", authConnData, "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, authConnData.String()) | ||||
| 	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, | ||||
| 		Conn: connData, | ||||
| 		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) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										58
									
								
								common/db.go
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								common/db.go
									
									
									
									
									
								
							| @ -7,43 +7,87 @@ 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 | ||||
| 	//ISO 639 code for the selected language.
 | ||||
| 	DATA_SELECTED_LANGUAGE_CODE | ||||
| ) | ||||
| 
 | ||||
| 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,31 +8,32 @@ 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 { | ||||
| 	GetPersister(ctx context.Context) (*persist.Persister, error) | ||||
| 	GetUserdataDb(ctx context.Context) (db.Db, error) | ||||
| 	GetResource(ctx context.Context) (resource.Resource, error) | ||||
| 	EnsureDbDir() error | ||||
| } | ||||
| 
 | ||||
| type StorageService struct { | ||||
| 	svc *storage.MenuStorageService | ||||
| } | ||||
| 
 | ||||
| func NewStorageService(dbDir string) *StorageService { | ||||
| 	return &StorageService{ | ||||
| 		svc: storage.NewMenuStorageService(dbDir, ""), | ||||
| func NewStorageService(conn storage.ConnData) (*StorageService, error) { | ||||
| 	svc := &StorageService{ | ||||
| 		svc: storage.NewMenuStorageService(conn, ""), | ||||
| 	} | ||||
| 	return svc, nil | ||||
| } | ||||
| 
 | ||||
| func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) { | ||||
| @ -46,7 +47,3 @@ func(ss *StorageService) GetUserdataDb(ctx context.Context) (db.Db, error) { | ||||
| func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, error) { | ||||
| 	return nil, errors.New("not implemented") | ||||
| } | ||||
| 
 | ||||
| func(ss *StorageService) EnsureDbDir() error { | ||||
| 	return ss.svc.EnsureDbDir() | ||||
| } | ||||
|  | ||||
| @ -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,29 @@ var ( | ||||
| 	VoucherTransfersURL string | ||||
| 	VoucherDataURL      string | ||||
| 	CheckAliasURL       string | ||||
| 	DbConn		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 | ||||
| 
 | ||||
| @ -43,14 +70,20 @@ func setBase() error { | ||||
| 	dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006") | ||||
| 	BearerToken = initializers.GetEnv("BEARER_TOKEN", "") | ||||
| 
 | ||||
| 	_, err = url.JoinPath(custodialURLBase, "/foo") | ||||
| 	_, err = url.Parse(custodialURLBase) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	_, err = url.JoinPath(dataURLBase, "/bar") | ||||
| 	_, err = url.Parse(dataURLBase) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func setConn() error { | ||||
| 	DbConn = initializers.GetEnv("DB_CONN", "") | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| @ -60,6 +93,14 @@ func LoadConfig() error { | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	err = setConn() | ||||
| 	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 +110,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) | ||||
| 	} | ||||
| } | ||||
| @ -1,77 +0,0 @@ | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/logging" | ||||
| 	"git.grassecon.net/urdt/ussd/config" | ||||
| 	"git.grassecon.net/urdt/ussd/initializers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| ) | ||||
| 
 | ||||
| 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 | ||||
| 
 | ||||
| 	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.Parse() | ||||
| 
 | ||||
| 	ctx := context.Background() | ||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||
| 	ctx = context.WithValue(ctx, "Database", database) | ||||
| 
 | ||||
| 	resourceDir := scriptDir | ||||
| 	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) | ||||
| 
 | ||||
| 	err := menuStorageService.EnsureDbDir() | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	pe, err := menuStorageService.GetPersister(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	// initialize the persister
 | ||||
| 
 | ||||
| 	// get the state
 | ||||
| 
 | ||||
| 	// restart the state
 | ||||
| 
 | ||||
| 	// persist the state
 | ||||
| 
 | ||||
| 	// exit
 | ||||
| 
 | ||||
| 	st := pe.GetState() | ||||
| 
 | ||||
| 	if st == nil { | ||||
| 		logg.ErrorCtxf(ctx, "state fail in devtool", "state", st) | ||||
| 		fmt.Errorf("cannot get state") | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	st.Restart() | ||||
| 
 | ||||
| 	os.Exit(1) | ||||
| } | ||||
							
								
								
									
										100
									
								
								devtools/store/dump/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								devtools/store/dump/main.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,100 @@ | ||||
| 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 connStr string | ||||
| 	var sessionId string | ||||
| 	var database string | ||||
| 	var engineDebug bool | ||||
| 	var err error | ||||
| 
 | ||||
| 	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") | ||||
| 	flag.StringVar(&connStr, "c", ".state", "connection string") | ||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||
| 	flag.Parse() | ||||
| 
 | ||||
| 	if connStr != "" { | ||||
| 		connStr = config.DbConn | ||||
| 	} | ||||
| 	connData, err := storage.ToConnData(config.DbConn) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	logg.Infof("start command", "conn", connData) | ||||
| 
 | ||||
| 	ctx := context.Background() | ||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||
| 	ctx = context.WithValue(ctx, "Database", database) | ||||
| 
 | ||||
| 	resourceDir := scriptDir | ||||
| 	menuStorageService := storage.NewMenuStorageService(connData, 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) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										90
									
								
								devtools/store/generate/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								devtools/store/generate/main.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,90 @@ | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/sha1" | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/logging" | ||||
| 	"git.grassecon.net/urdt/ussd/common" | ||||
| 	"git.grassecon.net/urdt/ussd/config" | ||||
| 	"git.grassecon.net/urdt/ussd/initializers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| 	testdataloader "github.com/peteole/testdata-loader" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logg      = logging.NewVanilla() | ||||
| 	baseDir   = testdataloader.GetBasePath() | ||||
| 	scriptDir = path.Join("services", "registration") | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	initializers.LoadEnvVariables() | ||||
| } | ||||
| 
 | ||||
| func main() { | ||||
| 	config.LoadConfig() | ||||
| 
 | ||||
| 	var connStr string | ||||
| 	var sessionId string | ||||
| 	var database string | ||||
| 	var engineDebug bool | ||||
| 	var err error | ||||
| 
 | ||||
| 	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") | ||||
| 	flag.StringVar(&connStr, "c", "", "connection string") | ||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||
| 	flag.Parse() | ||||
| 
 | ||||
| 	if connStr != "" { | ||||
| 		connStr = config.DbConn | ||||
| 	} | ||||
| 	connData, err := storage.ToConnData(config.DbConn) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	logg.Infof("start command", "conn", connData) | ||||
| 
 | ||||
| 	ctx := context.Background() | ||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||
| 	ctx = context.WithValue(ctx, "Database", database) | ||||
| 
 | ||||
| 	resourceDir := scriptDir | ||||
| 	menuStorageService := storage.NewMenuStorageService(connData, 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= | ||||
|  | ||||
| @ -3,24 +3,30 @@ package initializers | ||||
| import ( | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"strconv" | ||||
| 
 | ||||
| 	"github.com/joho/godotenv" | ||||
| ) | ||||
| 
 | ||||
| func LoadEnvVariables() { | ||||
| 	err := godotenv.Load() | ||||
| 	LoadEnvVariablesPath(".") | ||||
| } | ||||
| 
 | ||||
| func LoadEnvVariablesPath(dir string) { | ||||
| 	fp := path.Join(dir, ".env") | ||||
| 	err := godotenv.Load(fp) | ||||
| 	if err != nil { | ||||
| 		log.Fatal("Error loading .env file") | ||||
| 		log.Fatal("Error loading .env file", err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Helper to get environment variables with a default fallback
 | ||||
| func GetEnv(key, defaultVal string) string { | ||||
|   if value, exists := os.LookupEnv(key); exists { | ||||
|    	return value | ||||
| 	if value, exists := os.LookupEnv(key); exists { | ||||
| 		return value | ||||
| 	} | ||||
|   return defaultVal | ||||
| 	return defaultVal | ||||
| } | ||||
| 
 | ||||
| // Helper to safely convert environment variables to uint
 | ||||
|  | ||||
							
								
								
									
										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 | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @ -1,11 +1,10 @@ | ||||
| package ussd | ||||
| package application | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"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 | ||||
| @ -69,18 +57,20 @@ func (fm *FlagManager) GetFlag(label string) (uint32, error) { | ||||
| } | ||||
| 
 | ||||
| type Handlers struct { | ||||
| 	pe             *persist.Persister | ||||
| 	st             *state.State | ||||
| 	ca             cache.Memory | ||||
| 	userdataStore  common.DataStore | ||||
| 	adminstore     *utils.AdminStore | ||||
| 	flagManager    *asm.FlagParser | ||||
| 	accountService remote.AccountServiceInterface | ||||
| 	prefixDb       storage.PrefixDb | ||||
| 	profile        *models.Profile | ||||
| 	pe                   *persist.Persister | ||||
| 	st                   *state.State | ||||
| 	ca                   cache.Memory | ||||
| 	userdataStore        common.DataStore | ||||
| 	adminstore           *utils.AdminStore | ||||
| 	flagManager          *asm.FlagParser | ||||
| 	accountService       remote.AccountServiceInterface | ||||
| 	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,19 +80,21 @@ 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, | ||||
| 		flagManager:    appFlags, | ||||
| 		adminstore:     adminstore, | ||||
| 		accountService: accountService, | ||||
| 		prefixDb:       prefixDb, | ||||
| 		profile:        &models.Profile{Max: 6}, | ||||
| 		userdataStore:        userDb, | ||||
| 		flagManager:          appFlags, | ||||
| 		adminstore:           adminstore, | ||||
| 		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,19 +117,17 @@ func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource | ||||
| 	h.st = h.pe.GetState() | ||||
| 	h.ca = h.pe.GetMemory() | ||||
| 
 | ||||
| 	 | ||||
| 	fmt.Println("Pre restart state:", h.st)	 | ||||
| 
 | ||||
| 	if len(input) == 0 { | ||||
| 		h.st.Restart() | ||||
| 		h.st = h.pe.GetState() | ||||
| 		// move to the top node
 | ||||
| 		h.st.Code = []byte{} | ||||
| 	} | ||||
| 
 | ||||
| 	fmt.Println("New state:", h.st) | ||||
| 	sessionId, ok := ctx.Value("SessionId").(string) | ||||
| 	if ok { | ||||
| 		ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||
| 	} | ||||
| 
 | ||||
| 	sessionId, _ := ctx.Value("SessionId").(string) | ||||
| 	flag_admin_privilege, _ := h.flagManager.GetFlag("flag_admin_privilege") | ||||
| 
 | ||||
| 	isAdmin, _ := h.adminstore.IsAdmin(sessionId) | ||||
| 
 | ||||
| 	if isAdmin { | ||||
| @ -159,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 | ||||
| 
 | ||||
| @ -170,9 +161,12 @@ func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (r | ||||
| 		//Fallback to english instead?
 | ||||
| 		code = "eng" | ||||
| 	} | ||||
| 	res.FlagSet = append(res.FlagSet, state.FLAG_LANG) | ||||
| 	err := h.persistLanguageCode(ctx, code) | ||||
| 	if err != nil { | ||||
| 		return res, err | ||||
| 	} | ||||
| 	res.Content = code | ||||
| 
 | ||||
| 	res.FlagSet = append(res.FlagSet, state.FLAG_LANG) | ||||
| 	languageSetFlag, err := h.flagManager.GetFlag("flag_language_set") | ||||
| 	if err != nil { | ||||
| 		logg.ErrorCtxf(ctx, "Error setting the languageSetFlag", "error", err) | ||||
| @ -183,6 +177,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) | ||||
| @ -215,9 +210,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 | ||||
| @ -226,7 +221,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") | ||||
| @ -241,19 +236,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) | ||||
| @ -267,6 +265,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) | ||||
| @ -275,8 +274,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) | ||||
| @ -285,9 +284,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 | ||||
| @ -300,8 +299,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 | ||||
| 	} | ||||
| @ -316,6 +315,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 | ||||
| @ -326,12 +326,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) | ||||
| @ -341,6 +343,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) | ||||
| @ -359,10 +362,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 | ||||
| @ -370,7 +383,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 | ||||
| 
 | ||||
| @ -394,18 +407,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 { | ||||
| @ -712,15 +733,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 | ||||
| @ -734,12 +767,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:
 | ||||
| @ -752,7 +811,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 | ||||
| 
 | ||||
| @ -792,7 +851,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 | ||||
| 
 | ||||
| @ -807,7 +866,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 | ||||
| 
 | ||||
| @ -817,12 +876,22 @@ func (h *Handlers) QuitWithHelp(ctx context.Context, sym string, input []byte) ( | ||||
| 	l := gotext.NewLocale(translationDir, code) | ||||
| 	l.AddDomain("default") | ||||
| 
 | ||||
| 	res.Content = l.Get("For more help,please call: 0757628885") | ||||
| 	res.Content = l.Get("For more help, please call: 0757628885") | ||||
| 	res.FlagReset = append(res.FlagReset, flag_account_authorized) | ||||
| 	return res, nil | ||||
| } | ||||
| 
 | ||||
| // 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 | ||||
| @ -844,7 +913,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 | ||||
| 
 | ||||
| @ -854,7 +923,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 | ||||
| @ -904,9 +973,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:
 | ||||
| @ -915,6 +987,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 | ||||
| @ -932,7 +1008,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 | ||||
| 	} | ||||
| @ -940,6 +1024,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") | ||||
| @ -947,6 +1033,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 | ||||
| @ -1075,7 +1163,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 | ||||
| @ -1128,7 +1216,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 | ||||
| @ -1258,7 +1346,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 | ||||
| 
 | ||||
| @ -1272,7 +1360,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 | ||||
| 
 | ||||
| @ -1296,7 +1384,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 | ||||
| @ -1346,9 +1434,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") | ||||
| @ -1365,6 +1456,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] | ||||
| @ -1381,7 +1483,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) | ||||
| @ -1393,7 +1495,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) | ||||
| @ -1406,7 +1508,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) | ||||
| @ -1418,7 +1520,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) | ||||
| @ -1430,7 +1532,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) | ||||
| @ -1442,7 +1544,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) | ||||
| @ -1457,6 +1559,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 | ||||
| @ -1525,7 +1628,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 | ||||
| @ -1610,7 +1713,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) | ||||
| @ -1682,7 +1785,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 | ||||
| 
 | ||||
| @ -1693,13 +1796,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) | ||||
| @ -1740,7 +1845,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 | ||||
| 
 | ||||
| @ -1766,7 +1871,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 | ||||
| @ -1798,7 +1903,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) | ||||
| @ -1856,13 +1961,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 { | ||||
| @ -1905,12 +2011,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") | ||||
| @ -1919,7 +2027,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) | ||||
| @ -1967,6 +2075,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 | ||||
| @ -1989,21 +2098,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" { | ||||
| 			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) | ||||
| 			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 | ||||
| 				} | ||||
| 				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) | ||||
| @ -2016,3 +2126,68 @@ 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 | ||||
| } | ||||
| 
 | ||||
| // persistLanguageCode persists the selected ISO 639 language code
 | ||||
| func (h *Handlers) persistLanguageCode(ctx context.Context, code string) error { | ||||
| 	store := h.userdataStore | ||||
| 	sessionId, ok := ctx.Value("SessionId").(string) | ||||
| 	if !ok { | ||||
| 		return fmt.Errorf("missing session") | ||||
| 	} | ||||
| 	err := store.WriteEntry(ctx, sessionId, common.DATA_SELECTED_LANGUAGE_CODE, []byte(code)) | ||||
| 	if err != nil { | ||||
| 		logg.ErrorCtxf(ctx, "failed to persist language code", "key", common.DATA_SELECTED_LANGUAGE_CODE, "value", code, "error", err) | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| @ -1,19 +1,23 @@ | ||||
| package ussd | ||||
| package application | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"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) | ||||
| @ -656,6 +775,11 @@ func TestSetLanguage(t *testing.T) { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	sessionId := "session123" | ||||
| 	ctx, store := InitializeTestStore(t) | ||||
| 
 | ||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||
| 
 | ||||
| 	// Define test cases
 | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| @ -688,12 +812,13 @@ func TestSetLanguage(t *testing.T) { | ||||
| 
 | ||||
| 			// Create the Handlers instance with the mock flag manager
 | ||||
| 			h := &Handlers{ | ||||
| 				flagManager: fm.parser, | ||||
| 				st:          mockState, | ||||
| 				flagManager:   fm.parser, | ||||
| 				userdataStore: store, | ||||
| 				st:            mockState, | ||||
| 			} | ||||
| 
 | ||||
| 			// Call the method
 | ||||
| 			res, err := h.SetLanguage(context.Background(), "set_language", nil) | ||||
| 			res, err := h.SetLanguage(ctx, "set_language", nil) | ||||
| 			if err != nil { | ||||
| 				t.Error(err) | ||||
| 			} | ||||
| @ -789,37 +914,79 @@ func TestResetAccountAuthorized(t *testing.T) { | ||||
| } | ||||
| 
 | ||||
| func TestIncorrectPinReset(t *testing.T) { | ||||
| 	sessionId := "session123" | ||||
| 	ctx, store := InitializeTestStore(t) | ||||
| 	fm, err := NewFlagManager(flagsPath) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	flag_incorrect_pin, _ := fm.parser.GetFlag("flag_incorrect_pin") | ||||
| 	flag_account_blocked, _ := fm.parser.GetFlag("flag_account_blocked") | ||||
| 
 | ||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||
| 
 | ||||
| 	// Define test cases
 | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		input          []byte | ||||
| 		attempts       uint8 | ||||
| 		expectedResult resource.Result | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:  "Test incorrect pin reset", | ||||
| 			name:  "Test when incorrect PIN attempts is 2", | ||||
| 			input: []byte(""), | ||||
| 			expectedResult: resource.Result{ | ||||
| 				FlagReset: []uint32{flag_incorrect_pin}, | ||||
| 				Content:   "1", //Expected remaining PIN attempts
 | ||||
| 			}, | ||||
| 			attempts: 2, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:  "Test incorrect pin reset when incorrect PIN attempts is 1", | ||||
| 			input: []byte(""), | ||||
| 			expectedResult: resource.Result{ | ||||
| 				FlagReset: []uint32{flag_incorrect_pin}, | ||||
| 				Content:   "2", //Expected remaining PIN attempts
 | ||||
| 			}, | ||||
| 			attempts: 1, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:  "Test incorrect pin reset when incorrect PIN attempts is 1", | ||||
| 			input: []byte(""), | ||||
| 			expectedResult: resource.Result{ | ||||
| 				FlagReset: []uint32{flag_incorrect_pin}, | ||||
| 				Content:   "2", //Expected remaining PIN attempts
 | ||||
| 			}, | ||||
| 			attempts: 1, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:  "Test incorrect pin reset when incorrect PIN attempts is 3(account expected to be blocked)", | ||||
| 			input: []byte(""), | ||||
| 			expectedResult: resource.Result{ | ||||
| 				FlagReset: []uint32{flag_incorrect_pin}, | ||||
| 				FlagSet:   []uint32{flag_account_blocked}, | ||||
| 			}, | ||||
| 			attempts: 3, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 
 | ||||
| 			if err := store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(strconv.Itoa(int(tt.attempts)))); err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 
 | ||||
| 			// Create the Handlers instance with the mock flag manager
 | ||||
| 			h := &Handlers{ | ||||
| 				flagManager: fm.parser, | ||||
| 				flagManager:   fm.parser, | ||||
| 				userdataStore: store, | ||||
| 			} | ||||
| 
 | ||||
| 			// Call the method
 | ||||
| 			res, err := h.ResetIncorrectPin(context.Background(), "reset_incorrect_pin", tt.input) | ||||
| 			res, err := h.ResetIncorrectPin(ctx, "reset_incorrect_pin", tt.input) | ||||
| 			if err != nil { | ||||
| 				t.Error(err) | ||||
| 			} | ||||
| @ -929,7 +1096,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 +1555,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 +1801,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 +1813,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 +1825,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 +2103,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, | ||||
| 		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 +2239,93 @@ 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)) | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| func TestPersistLanguageCode(t *testing.T) { | ||||
| 	ctx, store := InitializeTestStore(t) | ||||
| 
 | ||||
| 	sessionId := "session123" | ||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||
| 
 | ||||
| 	h := &Handlers{ | ||||
| 		userdataStore: store, | ||||
| 	} | ||||
| 	tests := []struct { | ||||
| 		name                 string | ||||
| 		code                 string | ||||
| 		expectedLanguageCode string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:                 "Set Default Language (English)", | ||||
| 			code:                 "eng", | ||||
| 			expectedLanguageCode: "eng", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                 "Set Swahili Language", | ||||
| 			code:                 "swa", | ||||
| 			expectedLanguageCode: "swa", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, test := range tests { | ||||
| 		err := h.persistLanguageCode(ctx, test.code) | ||||
| 		if err != nil { | ||||
| 			t.Logf(err.Error()) | ||||
| 		} | ||||
| 		code, err := store.ReadEntry(ctx, sessionId, common.DATA_SELECTED_LANGUAGE_CODE) | ||||
| 
 | ||||
| 		assert.Equal(t, test.expectedLanguageCode, string(code)) | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -6,46 +6,46 @@ import ( | ||||
| 	"git.defalsify.org/vise.git/persist" | ||||
| 	"git.defalsify.org/vise.git/resource" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers/ussd" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers/application" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| ) | ||||
| 
 | ||||
| type BaseSessionHandler struct { | ||||
| 	cfgTemplate engine.Config | ||||
| 	rp RequestParser | ||||
| 	rs resource.Resource | ||||
| 	hn *ussd.Handlers | ||||
| 	provider storage.StorageProvider | ||||
| 	rp          RequestParser | ||||
| 	rs          resource.Resource | ||||
| 	hn          *application.Handlers | ||||
| 	provider    storage.StorageProvider | ||||
| } | ||||
| 
 | ||||
| func NewBaseSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn *ussd.Handlers) *BaseSessionHandler { | ||||
| func NewBaseSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn *application.Handlers) *BaseSessionHandler { | ||||
| 	return &BaseSessionHandler{ | ||||
| 		cfgTemplate: cfg, | ||||
| 		rs: rs, | ||||
| 		hn: hn, | ||||
| 		rp: rp, | ||||
| 		provider: storage.NewSimpleStorageProvider(stateDb, userdataDb), | ||||
| 		rs:          rs, | ||||
| 		hn:          hn, | ||||
| 		rp:          rp, | ||||
| 		provider:    storage.NewSimpleStorageProvider(stateDb, userdataDb), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func(f* BaseSessionHandler) Shutdown() { | ||||
| func (f *BaseSessionHandler) Shutdown() { | ||||
| 	err := f.provider.Close() | ||||
| 	if err != nil { | ||||
| 		logg.Errorf("handler shutdown error", "err", err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func(f *BaseSessionHandler) GetEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine { | ||||
| func (f *BaseSessionHandler) GetEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine { | ||||
| 	en := engine.NewEngine(cfg, rs) | ||||
| 	en = en.WithPersister(pr) | ||||
| 	return en | ||||
| } | ||||
| 
 | ||||
| func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) { | ||||
| func (f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) { | ||||
| 	var r bool | ||||
| 	var err error | ||||
| 	var ok bool | ||||
| 	 | ||||
| 
 | ||||
| 	logg.InfoCtxf(rqs.Ctx, "new request", "data", rqs) | ||||
| 
 | ||||
| 	rqs.Storage, err = f.provider.Get(rqs.Config.SessionId) | ||||
| @ -84,25 +84,25 @@ func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) | ||||
| 		return rqs, err | ||||
| 	} | ||||
| 
 | ||||
| 	rqs.Continue = r  | ||||
| 	rqs.Continue = r | ||||
| 	return rqs, nil | ||||
| } | ||||
| 
 | ||||
| func(f *BaseSessionHandler) Output(rqs RequestSession) (RequestSession,  error) { | ||||
| func (f *BaseSessionHandler) Output(rqs RequestSession) (RequestSession, error) { | ||||
| 	var err error | ||||
| 	_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer) | ||||
| 	return rqs, err | ||||
| } | ||||
| 
 | ||||
| func(f *BaseSessionHandler) Reset(rqs RequestSession) (RequestSession, error) { | ||||
| func (f *BaseSessionHandler) Reset(rqs RequestSession) (RequestSession, error) { | ||||
| 	defer f.provider.Put(rqs.Config.SessionId, rqs.Storage) | ||||
| 	return rqs, rqs.Engine.Finish() | ||||
| } | ||||
| 
 | ||||
| func(f *BaseSessionHandler) GetConfig() engine.Config { | ||||
| func (f *BaseSessionHandler) GetConfig() engine.Config { | ||||
| 	return f.cfgTemplate | ||||
| } | ||||
| 
 | ||||
| func(f *BaseSessionHandler) GetRequestParser() RequestParser { | ||||
| func (f *BaseSessionHandler) GetRequestParser() RequestParser { | ||||
| 	return f.rp | ||||
| } | ||||
|  | ||||
							
								
								
									
										141
									
								
								internal/handlers/handler_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								internal/handlers/handler_service.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,141 @@ | ||||
| package handlers | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/asm" | ||||
| 	"git.defalsify.org/vise.git/db" | ||||
| 	"git.defalsify.org/vise.git/engine" | ||||
| 	"git.defalsify.org/vise.git/persist" | ||||
| 	"git.defalsify.org/vise.git/resource" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers/application" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/utils" | ||||
| 	"git.grassecon.net/urdt/ussd/remote" | ||||
| ) | ||||
| 
 | ||||
| type HandlerService interface { | ||||
| 	GetHandler() (*application.Handlers, error) | ||||
| } | ||||
| 
 | ||||
| func getParser(fp string, debug bool) (*asm.FlagParser, error) { | ||||
| 	flagParser := asm.NewFlagParser().WithDebug() | ||||
| 	_, err := flagParser.Load(fp) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return flagParser, nil | ||||
| } | ||||
| 
 | ||||
| type LocalHandlerService struct { | ||||
| 	Parser        *asm.FlagParser | ||||
| 	DbRs          *resource.DbResource | ||||
| 	Pe            *persist.Persister | ||||
| 	UserdataStore *db.Db | ||||
| 	AdminStore    *utils.AdminStore | ||||
| 	Cfg           engine.Config | ||||
| 	Rs            resource.Resource | ||||
| } | ||||
| 
 | ||||
| func NewLocalHandlerService(ctx context.Context, fp string, debug bool, dbResource *resource.DbResource, cfg engine.Config, rs resource.Resource) (*LocalHandlerService, error) { | ||||
| 	parser, err := getParser(fp, debug) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	adminstore, err := utils.NewAdminStore(ctx, "admin_numbers") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &LocalHandlerService{ | ||||
| 		Parser:     parser, | ||||
| 		DbRs:       dbResource, | ||||
| 		AdminStore: adminstore, | ||||
| 		Cfg:        cfg, | ||||
| 		Rs:         rs, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (ls *LocalHandlerService) SetPersister(Pe *persist.Persister) { | ||||
| 	ls.Pe = Pe | ||||
| } | ||||
| 
 | ||||
| func (ls *LocalHandlerService) SetDataStore(db *db.Db) { | ||||
| 	ls.UserdataStore = db | ||||
| } | ||||
| 
 | ||||
| func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*application.Handlers, error) { | ||||
| 	replaceSeparatorFunc := func(input string) string { | ||||
| 		return strings.ReplaceAll(input, ":", ls.Cfg.MenuSeparator) | ||||
| 	} | ||||
| 
 | ||||
| 	appHandlers, err := application.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService, replaceSeparatorFunc) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	appHandlers = appHandlers.WithPersister(ls.Pe) | ||||
| 	ls.DbRs.AddLocalFunc("set_language", appHandlers.SetLanguage) | ||||
| 	ls.DbRs.AddLocalFunc("create_account", appHandlers.CreateAccount) | ||||
| 	ls.DbRs.AddLocalFunc("save_temporary_pin", appHandlers.SaveTemporaryPin) | ||||
| 	ls.DbRs.AddLocalFunc("verify_create_pin", appHandlers.VerifyCreatePin) | ||||
| 	ls.DbRs.AddLocalFunc("check_identifier", appHandlers.CheckIdentifier) | ||||
| 	ls.DbRs.AddLocalFunc("check_account_status", appHandlers.CheckAccountStatus) | ||||
| 	ls.DbRs.AddLocalFunc("authorize_account", appHandlers.Authorize) | ||||
| 	ls.DbRs.AddLocalFunc("quit", appHandlers.Quit) | ||||
| 	ls.DbRs.AddLocalFunc("check_balance", appHandlers.CheckBalance) | ||||
| 	ls.DbRs.AddLocalFunc("validate_recipient", appHandlers.ValidateRecipient) | ||||
| 	ls.DbRs.AddLocalFunc("transaction_reset", appHandlers.TransactionReset) | ||||
| 	ls.DbRs.AddLocalFunc("invite_valid_recipient", appHandlers.InviteValidRecipient) | ||||
| 	ls.DbRs.AddLocalFunc("max_amount", appHandlers.MaxAmount) | ||||
| 	ls.DbRs.AddLocalFunc("validate_amount", appHandlers.ValidateAmount) | ||||
| 	ls.DbRs.AddLocalFunc("reset_transaction_amount", appHandlers.ResetTransactionAmount) | ||||
| 	ls.DbRs.AddLocalFunc("get_recipient", appHandlers.GetRecipient) | ||||
| 	ls.DbRs.AddLocalFunc("get_sender", appHandlers.GetSender) | ||||
| 	ls.DbRs.AddLocalFunc("get_amount", appHandlers.GetAmount) | ||||
| 	ls.DbRs.AddLocalFunc("reset_incorrect", appHandlers.ResetIncorrectPin) | ||||
| 	ls.DbRs.AddLocalFunc("save_firstname", appHandlers.SaveFirstname) | ||||
| 	ls.DbRs.AddLocalFunc("save_familyname", appHandlers.SaveFamilyname) | ||||
| 	ls.DbRs.AddLocalFunc("save_gender", appHandlers.SaveGender) | ||||
| 	ls.DbRs.AddLocalFunc("save_location", appHandlers.SaveLocation) | ||||
| 	ls.DbRs.AddLocalFunc("save_yob", appHandlers.SaveYob) | ||||
| 	ls.DbRs.AddLocalFunc("save_offerings", appHandlers.SaveOfferings) | ||||
| 	ls.DbRs.AddLocalFunc("reset_account_authorized", appHandlers.ResetAccountAuthorized) | ||||
| 	ls.DbRs.AddLocalFunc("reset_allow_update", appHandlers.ResetAllowUpdate) | ||||
| 	ls.DbRs.AddLocalFunc("get_profile_info", appHandlers.GetProfileInfo) | ||||
| 	ls.DbRs.AddLocalFunc("verify_yob", appHandlers.VerifyYob) | ||||
| 	ls.DbRs.AddLocalFunc("reset_incorrect_date_format", appHandlers.ResetIncorrectYob) | ||||
| 	ls.DbRs.AddLocalFunc("initiate_transaction", appHandlers.InitiateTransaction) | ||||
| 	ls.DbRs.AddLocalFunc("verify_new_pin", appHandlers.VerifyNewPin) | ||||
| 	ls.DbRs.AddLocalFunc("confirm_pin_change", appHandlers.ConfirmPinChange) | ||||
| 	ls.DbRs.AddLocalFunc("quit_with_help", appHandlers.QuitWithHelp) | ||||
| 	ls.DbRs.AddLocalFunc("fetch_community_balance", appHandlers.FetchCommunityBalance) | ||||
| 	ls.DbRs.AddLocalFunc("set_default_voucher", appHandlers.SetDefaultVoucher) | ||||
| 	ls.DbRs.AddLocalFunc("check_vouchers", appHandlers.CheckVouchers) | ||||
| 	ls.DbRs.AddLocalFunc("get_vouchers", appHandlers.GetVoucherList) | ||||
| 	ls.DbRs.AddLocalFunc("view_voucher", appHandlers.ViewVoucher) | ||||
| 	ls.DbRs.AddLocalFunc("set_voucher", appHandlers.SetVoucher) | ||||
| 	ls.DbRs.AddLocalFunc("get_voucher_details", appHandlers.GetVoucherDetails) | ||||
| 	ls.DbRs.AddLocalFunc("reset_valid_pin", appHandlers.ResetValidPin) | ||||
| 	ls.DbRs.AddLocalFunc("check_pin_mismatch", appHandlers.CheckBlockedNumPinMisMatch) | ||||
| 	ls.DbRs.AddLocalFunc("validate_blocked_number", appHandlers.ValidateBlockedNumber) | ||||
| 	ls.DbRs.AddLocalFunc("retrieve_blocked_number", appHandlers.RetrieveBlockedNumber) | ||||
| 	ls.DbRs.AddLocalFunc("reset_unregistered_number", appHandlers.ResetUnregisteredNumber) | ||||
| 	ls.DbRs.AddLocalFunc("reset_others_pin", appHandlers.ResetOthersPin) | ||||
| 	ls.DbRs.AddLocalFunc("save_others_temporary_pin", appHandlers.SaveOthersTemporaryPin) | ||||
| 	ls.DbRs.AddLocalFunc("get_current_profile_info", appHandlers.GetCurrentProfileInfo) | ||||
| 	ls.DbRs.AddLocalFunc("check_transactions", appHandlers.CheckTransactions) | ||||
| 	ls.DbRs.AddLocalFunc("get_transactions", appHandlers.GetTransactionsList) | ||||
| 	ls.DbRs.AddLocalFunc("view_statement", appHandlers.ViewTransactionStatement) | ||||
| 	ls.DbRs.AddLocalFunc("update_all_profile_items", appHandlers.UpdateAllProfileItems) | ||||
| 	ls.DbRs.AddLocalFunc("set_back", appHandlers.SetBack) | ||||
| 	ls.DbRs.AddLocalFunc("show_blocked_account", appHandlers.ShowBlockedAccount) | ||||
| 
 | ||||
| 	return appHandlers, nil | ||||
| } | ||||
| 
 | ||||
| // TODO: enable setting of sessionId on engine init time
 | ||||
| func (ls *LocalHandlerService) GetEngine() *engine.DefaultEngine { | ||||
| 	en := engine.NewEngine(ls.Cfg, ls.Rs) | ||||
| 	en = en.WithPersister(ls.Pe) | ||||
| 	return en | ||||
| } | ||||
| @ -1,135 +0,0 @@ | ||||
| package handlers | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/asm" | ||||
| 	"git.defalsify.org/vise.git/db" | ||||
| 	"git.defalsify.org/vise.git/engine" | ||||
| 	"git.defalsify.org/vise.git/persist" | ||||
| 	"git.defalsify.org/vise.git/resource" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers/ussd" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/utils" | ||||
| 	"git.grassecon.net/urdt/ussd/remote" | ||||
| ) | ||||
| 
 | ||||
| type HandlerService interface { | ||||
| 	GetHandler() (*ussd.Handlers, error) | ||||
| } | ||||
| 
 | ||||
| func getParser(fp string, debug bool) (*asm.FlagParser, error) { | ||||
| 	flagParser := asm.NewFlagParser().WithDebug() | ||||
| 	_, err := flagParser.Load(fp) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return flagParser, nil | ||||
| } | ||||
| 
 | ||||
| type LocalHandlerService struct { | ||||
| 	Parser        *asm.FlagParser | ||||
| 	DbRs          *resource.DbResource | ||||
| 	Pe            *persist.Persister | ||||
| 	UserdataStore *db.Db | ||||
| 	AdminStore    *utils.AdminStore | ||||
| 	Cfg           engine.Config | ||||
| 	Rs            resource.Resource | ||||
| } | ||||
| 
 | ||||
| func NewLocalHandlerService(ctx context.Context, fp string, debug bool, dbResource *resource.DbResource, cfg engine.Config, rs resource.Resource) (*LocalHandlerService, error) { | ||||
| 	parser, err := getParser(fp, debug) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	adminstore, err := utils.NewAdminStore(ctx, "admin_numbers") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &LocalHandlerService{ | ||||
| 		Parser:     parser, | ||||
| 		DbRs:       dbResource, | ||||
| 		AdminStore: adminstore, | ||||
| 		Cfg:        cfg, | ||||
| 		Rs:         rs, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (ls *LocalHandlerService) SetPersister(Pe *persist.Persister) { | ||||
| 	ls.Pe = Pe | ||||
| } | ||||
| 
 | ||||
| func (ls *LocalHandlerService) SetDataStore(db *db.Db) { | ||||
| 	ls.UserdataStore = db | ||||
| } | ||||
| 
 | ||||
| func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*ussd.Handlers, error) { | ||||
| 	ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	ussdHandlers = ussdHandlers.WithPersister(ls.Pe) | ||||
| 	ls.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage) | ||||
| 	ls.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount) | ||||
| 	ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin) | ||||
| 	ls.DbRs.AddLocalFunc("verify_create_pin", ussdHandlers.VerifyCreatePin) | ||||
| 	ls.DbRs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier) | ||||
| 	ls.DbRs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus) | ||||
| 	ls.DbRs.AddLocalFunc("authorize_account", ussdHandlers.Authorize) | ||||
| 	ls.DbRs.AddLocalFunc("quit", ussdHandlers.Quit) | ||||
| 	ls.DbRs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance) | ||||
| 	ls.DbRs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient) | ||||
| 	ls.DbRs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset) | ||||
| 	ls.DbRs.AddLocalFunc("invite_valid_recipient", ussdHandlers.InviteValidRecipient) | ||||
| 	ls.DbRs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount) | ||||
| 	ls.DbRs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount) | ||||
| 	ls.DbRs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount) | ||||
| 	ls.DbRs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient) | ||||
| 	ls.DbRs.AddLocalFunc("get_sender", ussdHandlers.GetSender) | ||||
| 	ls.DbRs.AddLocalFunc("get_amount", ussdHandlers.GetAmount) | ||||
| 	ls.DbRs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin) | ||||
| 	ls.DbRs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname) | ||||
| 	ls.DbRs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname) | ||||
| 	ls.DbRs.AddLocalFunc("save_gender", ussdHandlers.SaveGender) | ||||
| 	ls.DbRs.AddLocalFunc("save_location", ussdHandlers.SaveLocation) | ||||
| 	ls.DbRs.AddLocalFunc("save_yob", ussdHandlers.SaveYob) | ||||
| 	ls.DbRs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings) | ||||
| 	ls.DbRs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized) | ||||
| 	ls.DbRs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate) | ||||
| 	ls.DbRs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo) | ||||
| 	ls.DbRs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob) | ||||
| 	ls.DbRs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob) | ||||
| 	ls.DbRs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction) | ||||
| 	ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin) | ||||
| 	ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange) | ||||
| 	ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp) | ||||
| 	ls.DbRs.AddLocalFunc("fetch_community_balance", ussdHandlers.FetchCommunityBalance) | ||||
| 	ls.DbRs.AddLocalFunc("set_default_voucher", ussdHandlers.SetDefaultVoucher) | ||||
| 	ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers) | ||||
| 	ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList) | ||||
| 	ls.DbRs.AddLocalFunc("view_voucher", ussdHandlers.ViewVoucher) | ||||
| 	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("validate_blocked_number", ussdHandlers.ValidateBlockedNumber) | ||||
| 	ls.DbRs.AddLocalFunc("retrieve_blocked_number", ussdHandlers.RetrieveBlockedNumber) | ||||
| 	ls.DbRs.AddLocalFunc("reset_unregistered_number", ussdHandlers.ResetUnregisteredNumber) | ||||
| 	ls.DbRs.AddLocalFunc("reset_others_pin", ussdHandlers.ResetOthersPin) | ||||
| 	ls.DbRs.AddLocalFunc("save_others_temporary_pin", ussdHandlers.SaveOthersTemporaryPin) | ||||
| 	ls.DbRs.AddLocalFunc("get_current_profile_info", ussdHandlers.GetCurrentProfileInfo) | ||||
| 	ls.DbRs.AddLocalFunc("check_transactions", ussdHandlers.CheckTransactions) | ||||
| 	ls.DbRs.AddLocalFunc("get_transactions", ussdHandlers.GetTransactionsList) | ||||
| 	ls.DbRs.AddLocalFunc("view_statement", ussdHandlers.ViewTransactionStatement) | ||||
| 	ls.DbRs.AddLocalFunc("update_all_profile_items", ussdHandlers.UpdateAllProfileItems) | ||||
| 	ls.DbRs.AddLocalFunc("set_back", ussdHandlers.SetBack) | ||||
| 
 | ||||
| 	return ussdHandlers, nil | ||||
| } | ||||
| 
 | ||||
| // TODO: enable setting of sessionId on engine init time
 | ||||
| func (ls *LocalHandlerService) GetEngine() *engine.DefaultEngine { | ||||
| 	en := engine.NewEngine(ls.Cfg, ls.Rs) | ||||
| 	en = en.WithPersister(ls.Pe) | ||||
| 	return en | ||||
| } | ||||
| @ -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" | ||||
| ) | ||||
| @ -20,33 +20,33 @@ var ( | ||||
| var ( | ||||
| 	ErrInvalidRequest = errors.New("invalid request for context") | ||||
| 	ErrSessionMissing = errors.New("missing session") | ||||
| 	ErrInvalidInput = errors.New("invalid input") | ||||
| 	ErrStorage = errors.New("storage retrieval fail") | ||||
| 	ErrEngineType = errors.New("incompatible engine") | ||||
| 	ErrEngineInit = errors.New("engine init fail") | ||||
| 	ErrEngineExec = errors.New("engine exec fail") | ||||
| 	ErrInvalidInput   = errors.New("invalid input") | ||||
| 	ErrStorage        = errors.New("storage retrieval fail") | ||||
| 	ErrEngineType     = errors.New("incompatible engine") | ||||
| 	ErrEngineInit     = errors.New("engine init fail") | ||||
| 	ErrEngineExec     = errors.New("engine exec fail") | ||||
| ) | ||||
| 
 | ||||
| type RequestSession struct { | ||||
| 	Ctx context.Context | ||||
| 	Config engine.Config | ||||
| 	Engine engine.Engine | ||||
| 	Input []byte | ||||
| 	Storage *storage.Storage | ||||
| 	Writer io.Writer | ||||
| 	Ctx      context.Context | ||||
| 	Config   engine.Config | ||||
| 	Engine   engine.Engine | ||||
| 	Input    []byte | ||||
| 	Storage  *storage.Storage | ||||
| 	Writer   io.Writer | ||||
| 	Continue bool | ||||
| } | ||||
| 
 | ||||
| // 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) | ||||
| } | ||||
| 
 | ||||
| type RequestHandler interface { | ||||
| 	GetConfig() engine.Config | ||||
| 	GetRequestParser() RequestParser | ||||
| 	GetEngine(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine  | ||||
| 	GetEngine(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine | ||||
| 	Process(rs RequestSession) (RequestSession, error) | ||||
| 	Output(rs RequestSession) (RequestSession, error) | ||||
| 	Reset(rs RequestSession) (RequestSession, error) | ||||
|  | ||||
							
								
								
									
										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,21 +34,21 @@ 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 | ||||
| 	} | ||||
| 
 | ||||
| 	rqs, err = ash.Process(rqs)  | ||||
| 	rqs, err = ash.Process(rqs) | ||||
| 	switch err { | ||||
| 	case nil: // set code to 200 if no err
 | ||||
| 		code = 200 | ||||
| @ -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 | ||||
| 	} | ||||
| } | ||||
| @ -89,4 +95,4 @@ func (ash *ATSessionHandler) Output(rqs handlers.RequestSession) (handlers.Reque | ||||
| 
 | ||||
| 	_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer) | ||||
| 	return rqs, err | ||||
| } | ||||
| } | ||||
| @ -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,34 +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 | ||||
| } | ||||
| @ -52,7 +23,7 @@ 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) | ||||
| @ -75,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 | ||||
| 	} | ||||
| 
 | ||||
| @ -101,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 | ||||
| 	} | ||||
| 
 | ||||
| @ -110,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() | ||||
| } | ||||
							
								
								
									
										284
									
								
								internal/ssh/ssh.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										284
									
								
								internal/ssh/ssh.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,284 @@ | ||||
| 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) { | ||||
| 	logg.TraceCtxf(a.Ctx, "looking for publickey", "pubkey", fmt.Sprintf("%x", pubKey)) | ||||
| 	va, err := a.keyStore.Get(a.Ctx, pubKey) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	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 | ||||
| } | ||||
| 
 | ||||
| type SshRunner struct { | ||||
| 	Ctx context.Context | ||||
| 	Cfg engine.Config | ||||
| 	FlagFile string | ||||
| 	Conn storage.ConnData | ||||
| 	ResourceDir string | ||||
| 	Debug bool | ||||
| 	SrvKeyFile string | ||||
| 	Host string | ||||
| 	Port uint | ||||
| 	wg sync.WaitGroup | ||||
| 	lst net.Listener | ||||
| } | ||||
| 
 | ||||
| func(s *SshRunner) serve(ctx context.Context, sessionId string, ch ssh.NewChannel, en engine.Engine) error { | ||||
| 	if ch == nil { | ||||
| 		return errors.New("nil channel") | ||||
| 	} | ||||
| 	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 | ||||
| } | ||||
| 
 | ||||
| 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.Conn, s.ResourceDir) | ||||
| 
 | ||||
| 	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) { | ||||
| 	s.Ctx = ctx | ||||
| 	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) | ||||
| } | ||||
							
								
								
									
										86
									
								
								internal/storage/parse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								internal/storage/parse.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,86 @@ | ||||
| package storage | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
| 	"path" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	DBTYPE_MEM = iota | ||||
| 	DBTYPE_GDBM | ||||
| 	DBTYPE_POSTGRES | ||||
| ) | ||||
| 
 | ||||
| type ConnData struct { | ||||
| 	typ int | ||||
| 	str string | ||||
| 	domain string | ||||
| } | ||||
| 
 | ||||
| func (cd *ConnData) DbType() int { | ||||
| 	return cd.typ | ||||
| } | ||||
| 
 | ||||
| func (cd *ConnData) String() string { | ||||
| 	return cd.str | ||||
| } | ||||
| 
 | ||||
| func (cd *ConnData) Domain() string { | ||||
| 	return cd.domain | ||||
| } | ||||
| 
 | ||||
| func (cd *ConnData) Path() string { | ||||
| 	v, _ := url.Parse(cd.str) | ||||
| 	v.RawQuery = "" | ||||
| 	return v.String() | ||||
| } | ||||
| 
 | ||||
| func probePostgres(s string) (string, string, bool) { | ||||
| 	domain := "public" | ||||
| 	v, err := url.Parse(s) | ||||
| 	if err != nil { | ||||
| 		return "", "", false | ||||
| 	} | ||||
| 	if v.Scheme != "postgres" { | ||||
| 		return "", "", false | ||||
| 	} | ||||
| 	vv := v.Query() | ||||
| 	if vv.Has("search_path") { | ||||
| 		domain = vv.Get("search_path") | ||||
| 	} | ||||
| 	return s, domain, true | ||||
| } | ||||
| 
 | ||||
| func probeGdbm(s string) (string, string, bool) { | ||||
| 	if !path.IsAbs(s) { | ||||
| 		return "", "", false | ||||
| 	} | ||||
| 	s = path.Clean(s) | ||||
| 	return s, "", true | ||||
| } | ||||
| 
 | ||||
| func ToConnData(connStr string) (ConnData, error) { | ||||
| 	var o ConnData | ||||
| 
 | ||||
| 	if connStr == "" { | ||||
| 		return o, nil | ||||
| 	} | ||||
| 
 | ||||
| 	v, domain, ok := probePostgres(connStr) | ||||
| 	if ok { | ||||
| 		o.typ = DBTYPE_POSTGRES | ||||
| 		o.str = v | ||||
| 		o.domain = domain | ||||
| 		return o, nil | ||||
| 	} | ||||
| 
 | ||||
| 	v, _, ok = probeGdbm(connStr) | ||||
| 	if ok { | ||||
| 		o.typ = DBTYPE_GDBM | ||||
| 		o.str = v | ||||
| 		return o, nil | ||||
| 	} | ||||
| 
 | ||||
| 	return o, fmt.Errorf("invalid connection string: %s", connStr) | ||||
| } | ||||
							
								
								
									
										28
									
								
								internal/storage/parse_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								internal/storage/parse_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| package storage | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestParseConnStr(t *testing.T) { | ||||
| 	_, err := ToConnData("postgres://foo:bar@localhost:5432/baz") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err)	 | ||||
| 	} | ||||
| 	_, err = ToConnData("/foo/bar") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err)	 | ||||
| 	} | ||||
| 	_, err = ToConnData("/foo/bar/") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err)	 | ||||
| 	} | ||||
| 	_, err = ToConnData("foo/bar") | ||||
| 	if err == nil { | ||||
| 		t.Fatalf("expected error") | ||||
| 	} | ||||
| 	_, err = ToConnData("http://foo/bar") | ||||
| 	if err == nil { | ||||
| 		t.Fatalf("expected error") | ||||
| 	} | ||||
| } | ||||
| @ -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" | ||||
| 	"github.com/jackc/pgx/v5/pgxpool" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| @ -23,63 +25,54 @@ type StorageService interface { | ||||
| 	GetPersister(ctx context.Context) (*persist.Persister, error) | ||||
| 	GetUserdataDb(ctx context.Context) db.Db | ||||
| 	GetResource(ctx context.Context) (resource.Resource, error) | ||||
| 	EnsureDbDir() error | ||||
| } | ||||
| 
 | ||||
| type MenuStorageService struct { | ||||
| 	dbDir         string | ||||
| 	conn ConnData | ||||
| 	resourceDir   string | ||||
| 	poResource    resource.Resource | ||||
| 	resourceStore db.Db | ||||
| 	stateStore    db.Db | ||||
| 	userDataStore db.Db | ||||
| } | ||||
| 
 | ||||
| func buildConnStr() string { | ||||
| 	host := initializers.GetEnv("DB_HOST", "localhost") | ||||
| 	user := initializers.GetEnv("DB_USER", "postgres") | ||||
| 	password := initializers.GetEnv("DB_PASSWORD", "") | ||||
| 	dbName := initializers.GetEnv("DB_NAME", "") | ||||
| 	port := initializers.GetEnv("DB_PORT", "5432") | ||||
| 
 | ||||
| 	connString := fmt.Sprintf( | ||||
| 		"postgres://%s:%s@%s:%s/%s", | ||||
| 		user, password, host, port, dbName, | ||||
| 	) | ||||
| 	logg.Debugf("pg conn string", "conn", connString) | ||||
| 
 | ||||
| 	return connString | ||||
| } | ||||
| 
 | ||||
| func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService { | ||||
| func NewMenuStorageService(conn ConnData, resourceDir string) *MenuStorageService { | ||||
| 	return &MenuStorageService{ | ||||
| 		dbDir:       dbDir, | ||||
| 		conn: conn, | ||||
| 		resourceDir: resourceDir, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, fileName string) (db.Db, error) { | ||||
| 	database, ok := ctx.Value("Database").(string) | ||||
| 	if !ok { | ||||
| 		return nil, fmt.Errorf("failed to select the database") | ||||
| 	} | ||||
| func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, section string) (db.Db, error) { | ||||
| 	var newDb db.Db | ||||
| 	var err error | ||||
| 
 | ||||
| 	if existingDb != nil { | ||||
| 		return existingDb, nil | ||||
| 	} | ||||
| 
 | ||||
| 	var newDb db.Db | ||||
| 	var err error | ||||
| 
 | ||||
| 	if database == "postgres" { | ||||
| 		newDb = postgres.NewPgDb() | ||||
| 		connStr := buildConnStr() | ||||
| 		err = newDb.Connect(ctx, connStr) | ||||
| 	connStr := ms.conn.String() | ||||
| 	dbTyp := ms.conn.DbType() | ||||
| 	if dbTyp == DBTYPE_POSTGRES { | ||||
| 		// TODO: move to vise
 | ||||
| 		err = ensureSchemaExists(ctx, ms.conn) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		newDb = postgres.NewPgDb().WithSchema(ms.conn.Domain()) | ||||
| 	} else if dbTyp == DBTYPE_GDBM { | ||||
| 		err = ms.ensureDbDir() | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		connStr = path.Join(connStr, section) | ||||
| 		newDb = gdbmstorage.NewThreadGdbmDb() | ||||
| 	} else { | ||||
| 		newDb = NewThreadGdbmDb() | ||||
| 		storeFile := path.Join(ms.dbDir, fileName) | ||||
| 		err = newDb.Connect(ctx, storeFile) | ||||
| 		return nil, fmt.Errorf("unsupported connection string: '%s'\n", ms.conn.String()) | ||||
| 	} | ||||
| 
 | ||||
| 	logg.DebugCtxf(ctx, "connecting to db", "conn", connStr, "conndata", ms.conn) | ||||
| 	err = newDb.Connect(ctx, connStr) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| @ -87,6 +80,45 @@ func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.D | ||||
| 	return newDb, nil | ||||
| } | ||||
| 
 | ||||
| // WithGettext triggers use of gettext for translation of templates and menus.
 | ||||
| //
 | ||||
| // The first language in `lns` will be used as default language, to resolve node keys to 
 | ||||
| // 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 | ||||
| } | ||||
| 
 | ||||
| // ensureSchemaExists creates a new schema if it does not exist
 | ||||
| func ensureSchemaExists(ctx context.Context, conn ConnData) error { | ||||
| 	h, err := pgxpool.New(ctx, conn.Path()) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to connect to the database: %w", err) | ||||
| 	} | ||||
| 	defer h.Close() | ||||
| 
 | ||||
| 	query := fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", conn.Domain()) | ||||
| 	_, err = h.Exec(ctx, query) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to create schema: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (ms *MenuStorageService) GetPersister(ctx context.Context) (*persist.Persister, error) { | ||||
| 	stateStore, err := ms.GetStateStore(ctx) | ||||
| 	if err != nil { | ||||
| @ -119,6 +151,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 | ||||
| } | ||||
| 
 | ||||
| @ -136,8 +173,8 @@ func (ms *MenuStorageService) GetStateStore(ctx context.Context) (db.Db, error) | ||||
| 	return ms.stateStore, nil | ||||
| } | ||||
| 
 | ||||
| func (ms *MenuStorageService) EnsureDbDir() error { | ||||
| 	err := os.MkdirAll(ms.dbDir, 0700) | ||||
| func (ms *MenuStorageService) ensureDbDir() error { | ||||
| 	err := os.MkdirAll(ms.conn.String(), 0700) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("state dir create exited with error: %v\n", err) | ||||
| 	} | ||||
| @ -1,124 +0,0 @@ | ||||
| package testutil | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/engine" | ||||
| 	"git.defalsify.org/vise.git/logging" | ||||
| 	"git.defalsify.org/vise.git/resource" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/testutil/testservice" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/testutil/testtag" | ||||
| 	testdataloader "github.com/peteole/testdata-loader" | ||||
| 	"git.grassecon.net/urdt/ussd/remote" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	baseDir   = testdataloader.GetBasePath() | ||||
| 	logg      = logging.NewVanilla() | ||||
| 	scriptDir = path.Join(baseDir, "services", "registration") | ||||
| ) | ||||
| 
 | ||||
| func TestEngine(sessionId string) (engine.Engine, func(), chan bool) { | ||||
| 	ctx := context.Background() | ||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||
| 	ctx = context.WithValue(ctx, "Database", "gdbm") | ||||
| 	pfp := path.Join(scriptDir, "pp.csv") | ||||
| 
 | ||||
| 	var eventChannel = make(chan bool) | ||||
| 
 | ||||
| 	cfg := engine.Config{ | ||||
| 		Root:       "root", | ||||
| 		SessionId:  sessionId, | ||||
| 		OutputSize: uint32(160), | ||||
| 		FlagCount:  uint32(128), | ||||
| 	} | ||||
| 
 | ||||
| 	dbDir := ".test_state" | ||||
| 	resourceDir := scriptDir | ||||
| 	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) | ||||
| 
 | ||||
| 	err := menuStorageService.EnsureDbDir() | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	rs, err := menuStorageService.GetResource(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	pe, err := menuStorageService.GetPersister(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	userDataStore, err := menuStorageService.GetUserdataDb(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	dbResource, ok := rs.(*resource.DbResource) | ||||
| 	if !ok { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) | ||||
| 	lhs.SetDataStore(&userDataStore) | ||||
| 	lhs.SetPersister(pe) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	if testtag.AccountService == nil { | ||||
| 		testtag.AccountService = &remote.AccountService{} | ||||
| 	} | ||||
| 
 | ||||
| 	switch testtag.AccountService.(type) { | ||||
| 	case *testservice.TestAccountService: | ||||
| 		go func() { | ||||
| 			eventChannel <- false | ||||
| 		}() | ||||
| 	case *remote.AccountService: | ||||
| 		go func() { | ||||
| 			time.Sleep(5 * time.Second) // Wait for 5 seconds
 | ||||
| 			eventChannel <- true | ||||
| 		}() | ||||
| 	default: | ||||
| 		panic("Unknown account service type") | ||||
| 	} | ||||
| 
 | ||||
| 	hl, err := lhs.GetHandler(testtag.AccountService) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	en := lhs.GetEngine() | ||||
| 	en = en.WithFirst(hl.Init) | ||||
| 	cleanFn := func() { | ||||
| 		err := en.Finish() | ||||
| 		if err != nil { | ||||
| 			logg.Errorf(err.Error()) | ||||
| 		} | ||||
| 
 | ||||
| 		err = menuStorageService.Close() | ||||
| 		if err != nil { | ||||
| 			logg.Errorf(err.Error()) | ||||
| 		} | ||||
| 		logg.Infof("testengine storage closed") | ||||
| 	} | ||||
| 	return en, cleanFn, eventChannel | ||||
| } | ||||
							
								
								
									
										209
									
								
								internal/testutil/engine.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								internal/testutil/engine.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,209 @@ | ||||
| package testutil | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"path/filepath" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/engine" | ||||
| 	"git.defalsify.org/vise.git/logging" | ||||
| 	"git.defalsify.org/vise.git/resource" | ||||
| 	"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/testutil/testservice" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/testutil/testtag" | ||||
| 	"git.grassecon.net/urdt/ussd/remote" | ||||
| 	"github.com/jackc/pgx/v5/pgxpool" | ||||
| 	testdataloader "github.com/peteole/testdata-loader" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logg        = logging.NewVanilla() | ||||
| 	baseDir     = testdataloader.GetBasePath() | ||||
| 	scriptDir   = path.Join(baseDir, "services", "registration") | ||||
| 	setDbType   string | ||||
| 	setConnStr  string | ||||
| 	setDbSchema string | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	initializers.LoadEnvVariablesPath(baseDir) | ||||
| 	config.LoadConfig() | ||||
| } | ||||
| 
 | ||||
| // SetDatabase updates the database used by TestEngine
 | ||||
| func SetDatabase(database, connStr, dbSchema string) { | ||||
| 	setDbType = database | ||||
| 	setConnStr = connStr | ||||
| 	setDbSchema = dbSchema | ||||
| } | ||||
| 
 | ||||
| // CleanDatabase removes all test data from the database
 | ||||
| func CleanDatabase() { | ||||
| 	if setDbType == "postgres" { | ||||
| 		ctx := context.Background() | ||||
| 		// Update the connection string with the new search path
 | ||||
| 		updatedConnStr, err := updateSearchPath(setConnStr, setDbSchema) | ||||
| 		if err != nil { | ||||
| 			log.Fatalf("Failed to update search path: %v", err) | ||||
| 		} | ||||
| 
 | ||||
| 		dbConn, err := pgxpool.New(ctx, updatedConnStr) | ||||
| 		if err != nil { | ||||
| 			log.Fatalf("Failed to connect to database for cleanup: %v", err) | ||||
| 		} | ||||
| 		defer dbConn.Close() | ||||
| 
 | ||||
| 		query := fmt.Sprintf("DELETE FROM %s.kv_vise;", setDbSchema) | ||||
| 		_, execErr := dbConn.Exec(ctx, query) | ||||
| 		if execErr != nil { | ||||
| 			log.Printf("Failed to cleanup table %s.kv_vise: %v", setDbSchema, execErr) | ||||
| 		} else { | ||||
| 			log.Printf("Successfully cleaned up table %s.kv_vise", setDbSchema) | ||||
| 		} | ||||
| 	} else { | ||||
| 		setConnStr, _ := filepath.Abs(setConnStr) | ||||
| 		if err := os.RemoveAll(setConnStr); err != nil { | ||||
| 			log.Fatalf("Failed to delete state store %s: %v", setConnStr, err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // updateSearchPath updates the search_path (schema) to be used in the connection
 | ||||
| func updateSearchPath(connStr string, newSearchPath string) (string, error) { | ||||
| 	u, err := url.Parse(connStr) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("invalid connection string: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Parse the query parameters
 | ||||
| 	q := u.Query() | ||||
| 
 | ||||
| 	// Update or add the search_path parameter
 | ||||
| 	q.Set("search_path", newSearchPath) | ||||
| 
 | ||||
| 	// Rebuild the connection string with updated parameters
 | ||||
| 	u.RawQuery = q.Encode() | ||||
| 
 | ||||
| 	return u.String(), nil | ||||
| } | ||||
| 
 | ||||
| func TestEngine(sessionId string) (engine.Engine, func(), chan bool) { | ||||
| 	var err error | ||||
| 	ctx := context.Background() | ||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||
| 	pfp := path.Join(scriptDir, "pp.csv") | ||||
| 
 | ||||
| 	var eventChannel = make(chan bool) | ||||
| 
 | ||||
| 	cfg := engine.Config{ | ||||
| 		Root:       "root", | ||||
| 		SessionId:  sessionId, | ||||
| 		OutputSize: uint32(160), | ||||
| 		FlagCount:  uint32(128), | ||||
| 	} | ||||
| 
 | ||||
| 	if setDbType == "postgres" { | ||||
| 		setConnStr = config.DbConn | ||||
| 		setConnStr, err = updateSearchPath(setConnStr, setDbSchema) | ||||
| 		if err != nil { | ||||
| 			fmt.Println("Error:", err) | ||||
| 			os.Exit(1) | ||||
| 		} | ||||
| 	} else { | ||||
| 		setConnStr, err = filepath.Abs(setConnStr) | ||||
| 		if err != nil { | ||||
| 			fmt.Fprintf(os.Stderr, "connstr err: %v", err) | ||||
| 			os.Exit(1) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	conn, err := storage.ToConnData(setConnStr) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, "connstr parse err: %v", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	resourceDir := scriptDir | ||||
| 	menuStorageService := storage.NewMenuStorageService(conn, resourceDir) | ||||
| 
 | ||||
| 	rs, err := menuStorageService.GetResource(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, "resource error: %v", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	pe, err := menuStorageService.GetPersister(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, "persister error: %v", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	userDataStore, err := menuStorageService.GetUserdataDb(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, "userdb error: %v", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	dbResource, ok := rs.(*resource.DbResource) | ||||
| 	if !ok { | ||||
| 		fmt.Fprintf(os.Stderr, "dbresource cast error") | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) | ||||
| 	lhs.SetDataStore(&userDataStore) | ||||
| 	lhs.SetPersister(pe) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	if testtag.AccountService == nil { | ||||
| 		testtag.AccountService = &remote.AccountService{} | ||||
| 	} | ||||
| 
 | ||||
| 	switch testtag.AccountService.(type) { | ||||
| 	case *testservice.TestAccountService: | ||||
| 		go func() { | ||||
| 			eventChannel <- false | ||||
| 		}() | ||||
| 	case *remote.AccountService: | ||||
| 		go func() { | ||||
| 			time.Sleep(5 * time.Second) // Wait for 5 seconds
 | ||||
| 			eventChannel <- true | ||||
| 		}() | ||||
| 	default: | ||||
| 		panic("Unknown account service type") | ||||
| 	} | ||||
| 
 | ||||
| 	hl, err := lhs.GetHandler(testtag.AccountService) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	en := lhs.GetEngine() | ||||
| 	en = en.WithFirst(hl.Init) | ||||
| 	cleanFn := func() { | ||||
| 		err := en.Finish() | ||||
| 		if err != nil { | ||||
| 			logg.Errorf(err.Error()) | ||||
| 		} | ||||
| 
 | ||||
| 		err = menuStorageService.Close() | ||||
| 		if err != nil { | ||||
| 			logg.Errorf(err.Error()) | ||||
| 		} | ||||
| 		logg.Infof("testengine storage closed") | ||||
| 	} | ||||
| 	return en, cleanFn, eventChannel | ||||
| } | ||||
							
								
								
									
										15
									
								
								internal/testutil/engine_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								internal/testutil/engine_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| package testutil | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestCreateEngine(t *testing.T) { | ||||
| 	o, clean, eventC := TestEngine("foo") | ||||
| 	defer clean() | ||||
| 	defer func() { | ||||
| 		<-eventC | ||||
| 		close(eventC) | ||||
| 	}() | ||||
| 	_ = o | ||||
| } | ||||
| @ -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) | ||||
| } | ||||
| 
 | ||||
| @ -1,9 +1,9 @@ | ||||
| package utils | ||||
| 
 | ||||
| var isoCodes = map[string]bool{ | ||||
| 	"eng": true, // English
 | ||||
| 	"swa": true, // Swahili
 | ||||
| 
 | ||||
| 	"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", | ||||
| @ -107,8 +107,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "0", | ||||
|                      "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" | ||||
|                      | ||||
|                     "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "0", | ||||
| @ -141,7 +140,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "1235", | ||||
|                     "expectedContent": "Incorrect PIN\n1:Retry\n9:Quit" | ||||
|                     "expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "1", | ||||
| @ -153,8 +152,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "0", | ||||
|                      "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" | ||||
|                      | ||||
|                     "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "0", | ||||
| @ -195,7 +193,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "1", | ||||
|                     "expectedContent":  "Enter your year of birth\n0:Back" | ||||
|                     "expectedContent": "Enter your year of birth\n0:Back" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "1940", | ||||
| @ -258,7 +256,6 @@ | ||||
|                     "input": "0", | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 } | ||||
|                 | ||||
|             ] | ||||
|         }, | ||||
|         { | ||||
| @ -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", | ||||
| @ -443,10 +440,4 @@ | ||||
|             ] | ||||
|         } | ||||
|     ] | ||||
| } | ||||
|        | ||||
|          | ||||
|         | ||||
|          | ||||
|      | ||||
| 
 | ||||
| } | ||||
| @ -6,7 +6,6 @@ import ( | ||||
| 	"flag" | ||||
| 	"log" | ||||
| 	"math/rand" | ||||
| 	"os" | ||||
| 	"regexp" | ||||
| 	"testing" | ||||
| 
 | ||||
| @ -17,13 +16,15 @@ import ( | ||||
| 
 | ||||
| var ( | ||||
| 	testData  = driver.ReadData() | ||||
| 	testStore = ".test_state" | ||||
| 	sessionID string | ||||
| 	src       = rand.NewSource(42) | ||||
| 	g         = rand.New(src) | ||||
| ) | ||||
| 
 | ||||
| var groupTestFile = flag.String("test-file", "group_test.json", "The test file to use for running the group tests") | ||||
| var database = flag.String("db", "gdbm", "Specify the database (gdbm or postgres)") | ||||
| var connStr = flag.String("conn", ".test_state", "connection string") | ||||
| var dbSchema = flag.String("schema", "test", "Specify the database schema (default test)") | ||||
| 
 | ||||
| func GenerateSessionId() string { | ||||
| 	uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g)) | ||||
| @ -79,12 +80,15 @@ func extractSendAmount(response []byte) string { | ||||
| } | ||||
| 
 | ||||
| func TestMain(m *testing.M) { | ||||
| 	// Parse the flags
 | ||||
| 	flag.Parse() | ||||
| 	sessionID = GenerateSessionId() | ||||
| 	defer func() { | ||||
| 		if err := os.RemoveAll(testStore); err != nil { | ||||
| 			log.Fatalf("Failed to delete state store %s: %v", testStore, err) | ||||
| 		} | ||||
| 	}() | ||||
| 	// set the db
 | ||||
| 	testutil.SetDatabase(*database, *connStr, *dbSchema) | ||||
| 
 | ||||
| 	// Cleanup the db after tests
 | ||||
| 	defer testutil.CleanDatabase() | ||||
| 
 | ||||
| 	m.Run() | ||||
| } | ||||
| 
 | ||||
| @ -121,7 +125,6 @@ func TestAccountCreationSuccessful(t *testing.T) { | ||||
| 		} | ||||
| 	} | ||||
| 	<-eventChannel | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| func TestAccountRegistrationRejectTerms(t *testing.T) { | ||||
| @ -298,9 +301,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 | ||||
| @ -7,8 +7,11 @@ msgstr "Ombi lako limetumwa. %s atapokea %s %s kutoka kwa %s." | ||||
| msgid "Thank you for using Sarafu. Goodbye!" | ||||
| msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!" | ||||
| 
 | ||||
| msgid "For more help,please call: 0757628885" | ||||
| msgstr "Kwa usaidizi zaidi,piga: 0757628885" | ||||
| msgid "For more help, please call: 0757628885" | ||||
| msgstr "Kwa usaidizi zaidi, piga: 0757628885" | ||||
| 
 | ||||
| msgid "Your account has been locked. For help on how to unblock your account, contact support at: 0757628885" | ||||
| msgstr "Akaunti yako imefungwa. Kwa usaidizi wa jinsi ya kufungua akaunti yako, wasiliana na usaidizi kwa: 0757628885" | ||||
| 
 | ||||
| msgid "Balance: %s\n" | ||||
| msgstr "Salio: %s\n" | ||||
|  | ||||
| @ -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? | ||||
| 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? | ||||
| 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