// Proves POS sign-in against the live database, read-only. // // Written because the interesting half of this feature is not the token — that // has unit tests — but whether the *account model* actually holds up against // real rows: does a shop's user resolve to the right tenant, does the outlet // list come back non-empty, and does an account from one tenant get refused at // another tenant's outlet. // // Passwords are read out of the database and handed straight back into the // login so the happy path can be proven without anyone typing or printing one. // Nothing here is ever echoed. // // go run ./scratch/posloginproof users 1087 # who could open a till // go run ./scratch/posloginproof login 1087 # sign the first one in // go run ./scratch/posloginproof cross # refuse another tenant's outlet package main import ( "fmt" "log" "os" "strconv" "strings" "nearle/models" "nearle/repositories" "github.com/joho/godotenv" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" ) func main() { mode := "users" if len(os.Args) > 1 { mode = os.Args[1] } tenantID := 1087 if len(os.Args) > 2 { tenantID, _ = strconv.Atoi(os.Args[2]) } _ = godotenv.Load() if strings.TrimSpace(os.Getenv("POS_TOKEN_SECRET")) == "" { // Only needed by the service layer; the repository probes below work // without it. Set a throwaway so `login` can mint. os.Setenv("POS_TOKEN_SECRET", "scratch-proof-signing-key-not-real") } 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) } repo := repositories.NewPosRepository(db) switch mode { case "users": listUsers(db, tenantID) case "login": proveLogin(db, repo, tenantID) case "cross": proveCrossTenantRefusal(db, repo) default: log.Fatalf("unknown mode %q", mode) } } // listUsers shows who could open a till for a tenant, and whether their record // is complete enough to do it. func listUsers(db *gorm.DB, tenantID int) { type row struct { Userid int Authname string Roleid int Status string Locationid int Haspw bool } var rows []row err := db.Raw(` SELECT userid, COALESCE(authname,'') AS authname, COALESCE(roleid,0) AS roleid, COALESCE(status,'') AS status, COALESCE(locationid,0) AS locationid, (COALESCE(password,'') <> '') AS haspw FROM app_users WHERE tenantid = ? ORDER BY userid`, tenantID).Scan(&rows).Error if err != nil { log.Fatal(err) } fmt.Printf("tenant %d — %d user(s)\n", tenantID, len(rows)) fmt.Printf("%-8s %-34s %-6s %-10s %-10s %s\n", "userid", "authname", "role", "status", "location", "password set") fmt.Println(strings.Repeat("-", 92)) for _, r := range rows { loc := "any" if r.Locationid > 0 { loc = strconv.Itoa(r.Locationid) } fmt.Printf("%-8d %-34s %-6d %-10s %-10s %v\n", r.Userid, r.Authname, r.Roleid, r.Status, loc, r.Haspw) } var locs []struct { Locationid int Locationname string Status string } db.Raw(`SELECT locationid, COALESCE(locationname,'') AS locationname, COALESCE(status,'') AS status FROM tenantlocations WHERE tenantid = ? ORDER BY locationid`, tenantID).Scan(&locs) fmt.Printf("\noutlets: %d\n", len(locs)) for _, l := range locs { fmt.Printf(" %-8d %-40s %s\n", l.Locationid, l.Locationname, l.Status) } } // proveLogin signs in the tenant's first usable account and reports what the // session resolved to. func proveLogin(db *gorm.DB, repo repositories.PosRepository, tenantID int) { // An explicit account, when the interesting case is a particular one — the // till's own outlet, or a proprietor who reaches several. wanted := "" if len(os.Args) > 3 { wanted = strings.TrimSpace(os.Args[3]) } var cred struct { Authname string Password string } query := ` SELECT COALESCE(authname,'') AS authname, COALESCE(password,'') AS password FROM app_users WHERE tenantid = ? AND COALESCE(password,'') <> '' AND COALESCE(authname,'') <> '' AND LOWER(COALESCE(status,'active')) <> 'inactive'` params := []interface{}{tenantID} if wanted != "" { query += ` AND LOWER(authname) = LOWER(?)` params = append(params, wanted) } query += ` ORDER BY userid LIMIT 1` err := db.Raw(query, params...).Scan(&cred).Error if err != nil { log.Fatal(err) } if cred.Authname == "" { log.Fatalf("tenant %d has no active account with a password set", tenantID) } fmt.Printf("signing in %s (password read from the database, not printed)\n\n", cred.Authname) session, err := repo.PosLogin(models.PosLoginRequest{ Authname: cred.Authname, Password: cred.Password, Terminalid: "T5EDD", }) if err != nil { log.Fatalf("REFUSED: %v", err) } fmt.Printf(" user %d %s\n", session.Userid, session.Fullname) fmt.Printf(" tenant %d %s\n", session.Tenantid, session.Tenantname) fmt.Printf(" store_id %s\n", session.Storeid) fmt.Printf(" outlet %d %s\n", session.Locationid, session.Locationname) fmt.Printf(" gstin %s\n", session.Gstin) fmt.Printf(" outlets %d reachable\n", len(session.Locations)) for _, l := range session.Locations { fmt.Printf(" %-8d %s\n", l.Locationid, l.Locationname) } // The check that matters: a wrong password must be refused, and refused // with the same message a wrong email gets. if _, err := repo.PosLogin(models.PosLoginRequest{ Authname: cred.Authname, Password: cred.Password + "x", }); err == nil { fmt.Println("\n !! a wrong password was ACCEPTED") } else { fmt.Printf("\n wrong password refused: %v\n", err) } if _, err := repo.PosLogin(models.PosLoginRequest{ Authname: "nobody@nowhere.invalid", Password: "whatever", }); err != nil { fmt.Printf(" unknown account refused: %v\n", err) } } // proveCrossTenantRefusal is the authorisation test: an account from one tenant // must not be able to open a till at another tenant's outlet, which is exactly // what a till could do before by editing one field in Settings. func proveCrossTenantRefusal(db *gorm.DB, repo repositories.PosRepository) { var cred struct { Authname string Password string Tenantid int } err := db.Raw(` SELECT COALESCE(a.authname,'') AS authname, COALESCE(a.password,'') AS password, a.tenantid FROM app_users a WHERE COALESCE(a.password,'') <> '' AND COALESCE(a.authname,'') <> '' AND a.tenantid > 0 AND LOWER(COALESCE(a.status,'active')) <> 'inactive' ORDER BY a.userid LIMIT 1`).Scan(&cred).Error if err != nil || cred.Authname == "" { log.Fatalf("no usable account to test with: %v", err) } // Somebody else's outlet. var foreign int db.Raw(`SELECT locationid FROM tenantlocations WHERE tenantid <> ? ORDER BY locationid LIMIT 1`, cred.Tenantid).Scan(&foreign) if foreign == 0 { log.Fatal("only one tenant has outlets; nothing to cross") } fmt.Printf("account belongs to tenant %d; asking for outlet %d, which does not\n\n", cred.Tenantid, foreign) if _, err := repo.PosLogin(models.PosLoginRequest{ Authname: cred.Authname, Password: cred.Password, Locationid: foreign, }); err == nil { fmt.Println(" !! ACCEPTED — a tenant signed a till into another tenant's outlet") } else { fmt.Printf(" refused: %v\n", err) } allowed, err := repo.PosLocationAllowed(cred.Tenantid, foreign) if err != nil { log.Fatal(err) } fmt.Printf(" PosLocationAllowed(tenant %d, outlet %d) = %v (want false)\n", cred.Tenantid, foreign, allowed) }