Files
backend_fiesta/scratch/posseparation/main.go
Suriyakumarvijayanayagam 1633617dc4 Stop reporting a failed login lookup as "Invalid Email"
GetUserByAuthname / GetUserByContactNo / GetUserLogin discarded the
Scan error, so a database that could not answer — down, pool exhausted,
or booted without its config (2026-07-20) — came back as uid 0 and every
user was told their email was wrong.

One lookup, GetUserLogin, now returns an error; sql.ErrNoRows is "not
found" and anything else reaches the service, which answers 500 "Login
is temporarily unavailable" and logs the cause. 409 "Invalid Email" is
unchanged for a genuine no-match: the console reads that exact shape as
"not registered". NULL password/role columns scan through sql.Null* so
they do not become 500s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 17:04:33 +05:30

176 lines
5.9 KiB
Go

// Proves the till and the Nearle Daily application no longer share accounts.
//
// Read-only. Four claims, each checked against live rows rather than asserted:
//
// 1. a provisioned supervisor can open a closed terminal;
//
// 2. a back-office account cannot, however senior it is;
//
// 3. a till account cannot reach the Nearle Daily application; and
//
// 4. a till account is not listed as though it were an app user.
//
// go run ./scratch/posseparation
package main
import (
"fmt"
"os"
"strings"
"nearle/models"
"nearle/repositories"
"github.com/joho/godotenv"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var failures int
func check(claim string, ok bool, detail string) {
mark := "PASS"
if !ok {
mark = "FAIL"
failures++
}
fmt.Printf(" [%s] %s\n %s\n", mark, claim, detail)
}
func main() {
_ = 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 {
fmt.Println(err)
os.Exit(1)
}
pos := repositories.NewPosRepository(db)
users := repositories.NewUserRepository(db)
// A real provisioned supervisor, and its password, read back out.
var sup struct {
Userid int
Authname, Password string
Configid, Tenantid, Location int
}
db.Raw(`SELECT userid, COALESCE(authname,'') authname, COALESCE(password,'') password,
COALESCE(configid,0) configid, COALESCE(tenantid,0) tenantid,
COALESCE(locationid,0) location
FROM app_users
WHERE COALESCE(roleid,0) = ? AND COALESCE(authname,'') <> ''
ORDER BY userid LIMIT 1`, models.PosRoleSupervisor).Scan(&sup)
if sup.Userid == 0 {
fmt.Println("no provisioned supervisor to test with")
os.Exit(1)
}
fmt.Printf("supervisor under test: %d %s (tenant %d, outlet %d)\n\n",
sup.Userid, sup.Authname, sup.Tenantid, sup.Location)
fmt.Println("1. a provisioned supervisor opens a closed terminal")
session, err := pos.PosLogin(models.PosLoginRequest{
Authname: sup.Authname, Password: sup.Password,
})
check("supervisor signs in at the till",
err == nil && session != nil,
fmt.Sprintf("err=%v", err))
if session != nil {
check("and gets the supervisor shell",
session.Canmanagestaff && session.Roleid == models.PosRoleSupervisor,
fmt.Sprintf("role=%s can_manage_staff=%v", session.Role, session.Canmanagestaff))
}
// The same, for a cashier. A cashier opening a till on their own credentials
// is the point of this: a shop should not need two people present before it
// can sell anything.
var cash struct {
Userid int
Authname, Password string
}
db.Raw(`SELECT userid, COALESCE(authname,'') authname, COALESCE(password,'') password
FROM app_users
WHERE COALESCE(roleid,0) = ? AND COALESCE(authname,'') <> ''
ORDER BY userid LIMIT 1`, models.PosRoleCashier).Scan(&cash)
if cash.Userid == 0 {
check("a cashier has their own login", false, "no cashier has an authname")
} else {
cs, err := pos.PosLogin(models.PosLoginRequest{
Authname: cash.Authname, Password: cash.Password,
})
check(fmt.Sprintf("cashier %s opens a closed terminal alone", cash.Authname),
err == nil && cs != nil,
fmt.Sprintf("err=%v", err))
if cs != nil {
check("and is held to the billing-only shell",
!cs.Canmanagestaff && cs.Roleid == models.PosRoleCashier,
fmt.Sprintf("role=%s can_manage_staff=%v", cs.Role, cs.Canmanagestaff))
}
}
fmt.Println("\n2. back-office accounts cannot open a terminal at all")
var backOffice []struct {
Userid int
Authname, Password string
Roleid int
}
db.Raw(`SELECT userid, COALESCE(authname,'') authname, COALESCE(password,'') password,
COALESCE(roleid,0) roleid
FROM app_users
WHERE COALESCE(roleid,0) IN (1,2,3,4,5,6)
AND COALESCE(authname,'') <> '' AND COALESCE(password,'') <> ''
AND LOWER(COALESCE(status,'active')) <> 'inactive'
ORDER BY userid LIMIT 5`).Scan(&backOffice)
for _, b := range backOffice {
_, err := pos.PosLogin(models.PosLoginRequest{
Authname: b.Authname, Password: b.Password,
})
check(fmt.Sprintf("roleid %d (%s) refused at the till", b.Roleid, b.Authname),
err != nil && strings.Contains(err.Error(), "not set up for the till"),
fmt.Sprintf("err=%v", err))
}
fmt.Println("\n3. a till account cannot reach the Nearle Daily application")
// Both the app and the console sign-in now go through GetUserLogin.
uid2, _, _, _, lookupErr := users.GetUserLogin("authname", sup.Authname, sup.Configid)
check("app and tenant web login do not find the supervisor",
uid2 == 0 && lookupErr == nil,
fmt.Sprintf("GetUserLogin(%s) -> userid %d err=%v", sup.Authname, uid2, lookupErr))
uid3, _ := users.FindUserID(sup.Authname, "", sup.Configid)
check("password-setup lookup does not find the supervisor",
uid3 == 0,
fmt.Sprintf("FindUserID(%s) -> userid %d", sup.Authname, uid3))
fmt.Println("\n4. till accounts are not listed as app users")
list, err := users.GetAllUsers(0, sup.Tenantid, 1, 500, "")
leaked := 0
for _, u := range list {
if u.Roleid == models.PosRoleSupervisor || u.Roleid == models.PosRoleCashier {
leaked++
}
}
check("getallusers hides till accounts",
err == nil && leaked == 0,
fmt.Sprintf("%d of %d rows were till accounts", leaked, len(list)))
// ...but the POS console can still read its own people by asking for them.
sups, err := users.GetAllUsers(models.PosRoleSupervisor, sup.Tenantid, 1, 500, "")
check("asking for role 7 explicitly still works",
err == nil && len(sups) > 0,
fmt.Sprintf("%d supervisor(s) returned", len(sups)))
fmt.Println()
if failures > 0 {
fmt.Printf("%d CHECK(S) FAILED\n", failures)
os.Exit(1)
}
fmt.Println("all checks passed")
}