// Gives every till account a way to open a closed terminal. // // A PIN cannot do it: the PIN route requires a session that already exists, so // a PIN-only account works only while somebody else is standing there to unlock // the till first. For a supervisor that was an outright deadlock. For a cashier // it means a shop that cannot open until two people have arrived — and whoever // gets in at seven is as often the cashier as the supervisor. // // So both roles get a username and a password. This backfills the ones created // before that was understood; new accounts get them from CreatePosUser. // // go run ./scratch/postilllogin plan // go run ./scratch/postilllogin apply package main import ( "crypto/rand" "fmt" "log" "math/big" "os" "strings" "nearle/models" "nearle/repositories" "github.com/joho/godotenv" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" ) func newPassword() string { // No l/I/O/0/1 — these get read off a screen and typed at a counter. const alphabet = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789" out := make([]byte, 14) for i := range out { n, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphabet)))) if err != nil { log.Fatalf("generating a password: %v", err) } out[i] = alphabet[n.Int64()] } return string(out) } func main() { mode := "plan" if len(os.Args) > 1 { mode = os.Args[1] } _ = godotenv.Load() 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) type target struct { Userid, Tenantid, Locationid, Roleid int Fullname, Locationname string } var targets []target db.Raw(`SELECT a.userid, a.tenantid, a.locationid, COALESCE(a.roleid,0) AS roleid, TRIM(COALESCE(a.firstname,'')||' '||COALESCE(a.lastname,'')) AS fullname, COALESCE(l.locationname,'') AS locationname FROM app_users a LEFT JOIN tenantlocations l ON l.locationid = a.locationid AND l.tenantid = a.tenantid WHERE COALESCE(a.roleid,0) IN (?, ?) AND (COALESCE(a.password,'') = '' OR COALESCE(a.authname,'') = '') AND LOWER(COALESCE(a.status,'active')) <> 'inactive' ORDER BY a.userid`, models.PosRoleSupervisor, models.PosRoleCashier).Scan(&targets) if len(targets) == 0 { fmt.Println("Every till account already has a login. Nothing to do.") return } fmt.Printf("till accounts with no way to open a closed terminal: %d\n\n", len(targets)) for _, t := range targets { authname := fmt.Sprintf("%s.%d@pos.nearle.in", strings.ToLower(models.PosRoleName(t.Roleid)), t.Locationid) password := newPassword() if mode != "apply" { fmt.Printf(" %-6d %-18s outlet %-6d %-26s -> %s / %s\n", t.Userid, t.Fullname, t.Locationid, t.Locationname, authname, password) continue } _, err := repo.UpdatePosUser(t.Tenantid, t.Locationid, models.PosUserRequest{ Userid: t.Userid, Authname: authname, Password: password, }) if err != nil { fmt.Printf(" %-6d FAILED: %v\n", t.Userid, err) continue } // Prove it, rather than assert it — the whole point of this tool is that // a supervisor who cannot sign in is indistinguishable from one who can // until somebody stands at a counter and tries. session, err := repo.PosLogin(models.PosLoginRequest{ Authname: authname, Password: password, }) if err != nil { fmt.Printf(" %-6d written, but sign-in still fails: %v\n", t.Userid, err) continue } fmt.Printf(" %-6d %-18s outlet %-6d %-26s\n", t.Userid, t.Fullname, t.Locationid, t.Locationname) fmt.Printf(" login %s / %s\n", authname, password) fmt.Printf(" opens as %s at %s, can_manage_staff=%v\n", session.Role, session.Locationname, session.Canmanagestaff) } if mode != "apply" { fmt.Println("\nNothing written — run `apply` to commit.") } }