// Does the mobile-number-and-PIN sign-in actually work against the live data? // // Picks a supervisor and a cashier that already have both halves of the // credential, prints them so they can be typed into a terminal, then runs the // real repository and service — the same code path the HTTP handler calls — and // reports what came back. // // Read-only. PosLogin issues SELECTs and mints a token in memory; nothing here // writes, and the token is not persisted anywhere by design. // // Numbers and PINs are masked unless -show is passed. They belong to real // people at real shops, and the default should not be to print them into // whatever is capturing this program's output. // // go run ./scratch/poslivecheck # picks a ready pair, masked // go run ./scratch/poslivecheck -show # prints the credentials // go run ./scratch/poslivecheck -show 1087 1137 # ...at a named outlet package main import ( "encoding/json" "fmt" "log" "os" "strconv" "strings" "nearle/models" "nearle/repositories" "nearle/services" "github.com/joho/godotenv" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" ) type account struct { Userid int Tenantid int Locationid int Roleid int Fullname string Contactno string Pin int64 } func main() { _ = godotenv.Load() if strings.TrimSpace(os.Getenv("POS_TOKEN_SECRET")) == "" { log.Fatal("POS_TOKEN_SECRET is not set; sign-in mints a token and will fail without it") } dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_NAME")) db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) if err != nil { log.Fatal(err) } // Only accounts holding both halves are candidates. An account missing // either cannot sign in at all, and picking one would prove nothing except // that the rejection works. where := `COALESCE(roleid,0) = ? AND COALESCE(pin,0) BETWEEN 1000 AND 9999 AND TRIM(COALESCE(contactno,'')) <> '' AND LOWER(COALESCE(status,'active')) <> 'inactive'` args := []interface{}{} // -show opts into printing the credentials themselves. show := false rest := []string{} for _, arg := range os.Args[1:] { if arg == "-show" || arg == "--show" { show = true continue } rest = append(rest, arg) } if len(rest) > 1 { tenantID, _ := strconv.Atoi(rest[0]) locationID, _ := strconv.Atoi(rest[1]) where += ` AND tenantid = ? AND locationid = ?` args = append(args, tenantID, locationID) } pick := func(roleID int) *account { var a account params := append([]interface{}{roleID}, args...) err := db.Raw(` SELECT userid, COALESCE(tenantid,0) AS tenantid, COALESCE(locationid,0) AS locationid, COALESCE(roleid,0) AS roleid, TRIM(CONCAT(COALESCE(firstname,''),' ',COALESCE(lastname,''))) AS fullname, COALESCE(contactno,'') AS contactno, COALESCE(pin,0) AS pin FROM app_users WHERE `+where+` ORDER BY userid LIMIT 1`, params...).Scan(&a).Error if err != nil || a.Userid == 0 { return nil } return &a } supervisor := pick(models.PosRoleSupervisor) cashier := pick(models.PosRoleCashier) fmt.Println("Accounts that can sign in today") fmt.Println(strings.Repeat("-", 78)) for _, pair := range []struct { label string a *account }{{"supervisor", supervisor}, {"cashier", cashier}} { a := pair.a if a == nil { fmt.Printf(" %-11s none — no account of this role has both a number and a PIN\n", pair.label) continue } fmt.Printf(" %-11s userid %-6d tenant %-6d outlet %-6d %s\n", pair.label, a.Userid, a.Tenantid, a.Locationid, a.Fullname) fmt.Printf(" %-11s mobile %s PIN %s\n\n", "", mask(a.Contactno, show), maskPin(a.Pin, show)) } if !show { fmt.Println(" (masked — re-run with -show to print them)") } if supervisor == nil && cashier == nil { log.Fatal("nothing to test with") } repo := repositories.NewPosRepository(db) svc := services.NewPosService(repo, nil) fmt.Println("\nSigning in (real service, live data)") fmt.Println(strings.Repeat("-", 78)) pass, fail := 0, 0 check := func(name string, ok bool, detail string) { if ok { pass++ fmt.Printf(" PASS %-46s %s\n", name, detail) return } fail++ fmt.Printf(" FAIL %-46s %s\n", name, detail) } signIn := func(label string, a *account) *models.PosSession { if a == nil { return nil } session, err := svc.Login(models.PosLoginRequest{ Contactno: a.Contactno, Pin: strconv.FormatInt(a.Pin, 10), }) if err != nil { check(label+" signs in", false, err.Error()) return nil } check(label+" signs in", true, fmt.Sprintf( "%s at %s (outlet %d), can_manage_staff=%v", session.Role, session.Locationname, session.Locationid, session.Canmanagestaff)) check(label+" gets a token", session.Token != "", fmt.Sprintf("%d chars, expires %s", len(session.Token), session.Expiresat)) return session } supSession := signIn("supervisor", supervisor) cashSession := signIn("cashier", cashier) if supSession != nil { check("supervisor can manage staff", supSession.Canmanagestaff, "role 7 grants it") } if cashSession != nil { check("cashier cannot manage staff", !cashSession.Canmanagestaff, "role 8 does not") } // The response must not carry anyone's PIN — the whole point of the change // that removed staff[].pin. // // Checked against the serialised JSON, not the Go struct. PosStaffMember.Pin // is still *populated* — the query needs it to drop two people sharing a PIN // — and is kept off the wire by `json:"-"`. Asserting on the struct field // tests the wrong layer and fails a correct implementation. if supSession != nil { encoded, merr := json.Marshal(supSession) check("session serialises", merr == nil, errText(merr)) if merr == nil { check("no PIN travels in the session", !strings.Contains(string(encoded), `"pin"`), fmt.Sprintf("%d staff listed, %d bytes of JSON", len(supSession.Staff), len(encoded))) } } // A number typed the way a person actually types it. if supervisor != nil && len(supervisor.Contactno) == 10 { for label, typed := range map[string]string{ "+91 with spaces": "+91 " + supervisor.Contactno[:5] + " " + supervisor.Contactno[5:], "leading zero": "0" + supervisor.Contactno, "bare ten digits": supervisor.Contactno, } { _, err := svc.Login(models.PosLoginRequest{ Contactno: typed, Pin: strconv.FormatInt(supervisor.Pin, 10), }) check("number accepted as typed", err == nil, label) } } // And the refusals. if supervisor != nil { wrong := supervisor.Pin + 1 if wrong > 9999 { wrong = 1000 } _, err := svc.Login(models.PosLoginRequest{ Contactno: supervisor.Contactno, Pin: strconv.FormatInt(wrong, 10), }) check("a wrong PIN is refused", err != nil, errText(err)) } _, err = svc.Login(models.PosLoginRequest{Contactno: "9999999999", Pin: "4821"}) check("an unknown number is refused", err != nil, errText(err)) // Switching operator at an open terminal, which is what the till does when // a colleague takes over. if supSession != nil && cashier != nil && cashier.Locationid == supSession.Locationid { switched, err := repo.PosLoginByPin(supSession.Tenantid, supSession.Locationid, strconv.FormatInt(cashier.Pin, 10)) if err != nil { check("operator switch by PIN", false, err.Error()) } else { check("operator switch by PIN", true, fmt.Sprintf("now %s, can_manage_staff=%v", switched.Role, switched.Canmanagestaff)) } } fmt.Printf("\n%d passed, %d failed\n", pass, fail) if fail > 0 { os.Exit(1) } } // mask shows enough of a number to recognise the account, not enough to sign in // as it. func mask(number string, show bool) string { if show { return number } if len(number) != 10 { return strings.Repeat("*", len(number)) } return number[:2] + "******" + number[8:] } func maskPin(pin int64, show bool) string { if show { return strconv.FormatInt(pin, 10) } return "****" } func errText(err error) string { if err == nil { return "no error" } return err.Error() }