Hold web-created staff to the same rules the till applies
Two paths write `app_users`: the console's `tenants/createstaff`, and the terminal's `/pos/users`. Only one of them checked anything. `createstaff` wrote whatever it was handed. A cashier could be created there with PIN "0451" — which a bigint column stores as 451 — and would then type four digits at the counter and be refused for ever, with nothing on either screen to explain it. Or with 1234, which live data already has on eleven accounts. Or with a PIN somebody at the same outlet already had, which attributes a bill to whichever row is read first. Or with no way to sign in at all. None of that surfaced where it was caused. It surfaced at a counter, days later, as "the new person cannot log in". So the rules move into `ValidateStaffUser`, and both paths use it: a name, a role that is actually a role, a PIN the schema can hold and nobody guesses first, and at least one way to sign in. The duplicate-PIN check runs too, when the row names an outlet. The handler also stops answering 500 with a body claiming 409. Every one of these is something the person filling in the form can fix, so it is a 400 carrying the reason. `GetStaffs` now returns `rolename` alongside `roleid`, so a console can show "Supervisor" without mapping ids itself — `app_roles` has six rows for four back-office roles and most accounts carry an id absent from it, so any mapping written client-side would be wrong. This is what makes the two role systems one. A supervisor or cashier created from the web behaves at the till exactly like one created at the till, because there is now a single definition of what those are. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -130,3 +130,63 @@ func TestARoleIsReadFromItsNameNotItsNumber(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The web console writes `app_users` too, through `tenants/createstaff`, and
|
||||
// that path had no validation at all. These cover the shared rule set, so a
|
||||
// person created from a browser is subject to the same constraints as one
|
||||
// created at a till — two paths writing one table is how they drift.
|
||||
|
||||
func TestStaffFromTheWebConsoleObeysTheTillsRules(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
user models.User
|
||||
ok bool
|
||||
}{
|
||||
{
|
||||
name: "a usable cashier",
|
||||
user: models.User{Firstname: "Asha", Roleid: models.PosRoleCashier, Pin: 7391},
|
||||
ok: true,
|
||||
},
|
||||
{
|
||||
name: "a password instead of a PIN is fine",
|
||||
user: models.User{Firstname: "Asha", Roleid: models.PosRoleCashier, Password: "s3cret"},
|
||||
ok: true,
|
||||
},
|
||||
{
|
||||
name: "no name",
|
||||
user: models.User{Roleid: models.PosRoleCashier, Pin: 7391},
|
||||
},
|
||||
{
|
||||
name: "no role — 0 is unset, not a role",
|
||||
user: models.User{Firstname: "Asha", Pin: 7391},
|
||||
},
|
||||
{
|
||||
name: "no way at all to sign in",
|
||||
user: models.User{Firstname: "Asha", Roleid: models.PosRoleCashier},
|
||||
},
|
||||
{
|
||||
// 451 is what "0451" becomes in a bigint column. Accepting it here
|
||||
// creates somebody who types four digits and is refused for ever.
|
||||
name: "a PIN the column cannot hold",
|
||||
user: models.User{Firstname: "Asha", Roleid: models.PosRoleCashier, Pin: 451},
|
||||
},
|
||||
{
|
||||
name: "a PIN anyone would guess first",
|
||||
user: models.User{Firstname: "Asha", Roleid: models.PosRoleCashier, Pin: 1234},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
user := tc.user
|
||||
_, err := ValidateStaffUser(&user)
|
||||
|
||||
if tc.ok && err != nil {
|
||||
t.Fatalf("refused a valid staff row: %v", err)
|
||||
}
|
||||
if !tc.ok && err == nil {
|
||||
t.Fatal("accepted a staff row the till could not use")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user