Give a cashier their own till login, not just a PIN behind a supervisor
A PIN cannot open a closed terminal. The PIN route needs 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 and was fixed last commit. For a cashier it is subtler and just as wrong: the shop cannot open until two people have arrived, and whoever gets in at seven is as often the cashier as the supervisor. So every till account now gets a username and a password, and the role decides the shell rather than the credential deciding it. A cashier signs in exactly the way a supervisor does and is still held to billing only, because that comes from roleid 8 and not from how they got in. The earlier reasoning — that a second password is one more credential to leak for no capability gained — was measuring the wrong thing. It counted the cost of the credential and not the cost of the shop that cannot open without one. CreatePosUser generates both when the request omits them, so provisioning is one call per person and nobody has to invent a naming scheme. An explicit value always wins. A generated name that collides walks to the next free one, because a second cashier at one counter is ordinary rather than an error; a name the caller supplied is refused instead, because silently signing somebody in as another person's address is worse than a message. Uniqueness is checked against authname and email together, since the insert writes the same value to both and app_users_email_unique would otherwise fail the transaction rather than return something anyone can act on. The password comes back exactly once, in the creation response. Listing till users still reports only has_password, so an admin who loses it reissues rather than looks it up — the right shape even while the column behind it is plaintext. The domain is deliberately unroutable. These are till credentials, never a mailbox, and an address that looks deliverable invites somebody to try sending a reset to it. Verified against live rows by scratch/posseparation, which now checks the cashier path too: cashier.1185@pos.nearle.in opens a closed terminal alone and comes back can_manage_staff=false. All five outlets that stock products have both accounts, each proved by an actual sign-in. Also drops a stray `print(queryBuilder.String())` from GetAllUsers, which was writing the whole SQL statement to stderr on every call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,24 +29,6 @@ import (
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
// newPassword generates a password for a supervisor's till login.
|
||||
//
|
||||
// From crypto/rand and printed once, like the PINs. Deliberately not derived
|
||||
// from the shop's name or id: a credential anybody could guess from the sign
|
||||
// above the door is not a credential.
|
||||
func newPassword() string {
|
||||
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, tenantID, locationID := "plan", 1087, 1135
|
||||
if len(os.Args) > 1 {
|
||||
@@ -96,29 +78,17 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
// The supervisor gets a username and password as well as a PIN, because a
|
||||
// PIN cannot open a *closed* terminal — the PIN route requires a session
|
||||
// that already exists. Without these, an outlet whose only accounts are POS
|
||||
// accounts has no way in at all: the back-office logins are refused by role
|
||||
// and the till logins have no password. That deadlock is not hypothetical;
|
||||
// it is what the first cut of strict mode actually produced.
|
||||
// Both roles get a username and a password as well as a PIN, and neither is
|
||||
// stated here: CreatePosUser generates them and returns them once.
|
||||
//
|
||||
// The cashier deliberately gets neither. They sign on at a terminal a
|
||||
// supervisor has already opened, so a second password would be one more
|
||||
// credential to leak for no capability gained.
|
||||
//
|
||||
// The username is derived from the outlet rather than from a person, so it
|
||||
// survives staff turnover. `authname` is not unique in this schema, but
|
||||
// scoping it to the outlet keeps it unambiguous in practice, and `email` is
|
||||
// left null on purpose — that column *is* unique, and blank strings collide.
|
||||
// A PIN cannot open a *closed* terminal — 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. Whoever gets in at seven is as often the cashier
|
||||
// as the supervisor.
|
||||
wanted := []models.PosUserRequest{
|
||||
{
|
||||
Fullname: "Store Supervisor",
|
||||
Role: "supervisor",
|
||||
Pin: newPin(),
|
||||
Authname: fmt.Sprintf("supervisor.%d@pos.nearle.in", locationID),
|
||||
Password: newPassword(),
|
||||
},
|
||||
{Fullname: "Store Supervisor", Role: "supervisor", Pin: newPin()},
|
||||
{Fullname: "Counter Cashier", Role: "cashier", Pin: newPin()},
|
||||
}
|
||||
for wanted[0].Pin == wanted[1].Pin {
|
||||
@@ -127,11 +97,8 @@ func main() {
|
||||
|
||||
fmt.Println("\nwould create:")
|
||||
for _, w := range wanted {
|
||||
fmt.Printf(" %-22s %-12s pin=%s", w.Fullname, w.Role, w.Pin)
|
||||
if w.Authname != "" {
|
||||
fmt.Printf(" login=%s / %s", w.Authname, w.Password)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Printf(" %-22s %-12s pin=%s (login generated on create)\n",
|
||||
w.Fullname, w.Role, w.Pin)
|
||||
}
|
||||
|
||||
if mode != "apply" {
|
||||
@@ -145,12 +112,9 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatalf("creating %s: %v", w.Fullname, err)
|
||||
}
|
||||
fmt.Printf(" created userid %-6d %-22s %-12s PIN %s",
|
||||
fmt.Printf(" created userid %-6d %-22s %-12s PIN %s\n",
|
||||
created.Userid, created.Fullname, created.Role, created.Pin)
|
||||
if w.Authname != "" {
|
||||
fmt.Printf(" login=%s / %s", w.Authname, w.Password)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Printf(" login %s / %s\n", created.Authname, created.Password)
|
||||
}
|
||||
|
||||
// The point of the exercise: does the till now see real staff?
|
||||
|
||||
Reference in New Issue
Block a user