Stop the till and Nearle Daily from sharing accounts
app_users is the only thing the two products have in common, and the code was treating it as though it were the whole relationship. Both directions leaked. Back-office roles were leaking into the till. PosRoleCanManageStaff returned true for roleid 1 to 6, on the reasoning that somebody who already administers a shop from a browser is not made less privileged by standing at the counter. That sounds fine and is wrong: measured against live data it handed till-supervisor powers to 68 accounts, 59 of them Nearle Daily Super admins, not one of whom is the administrator of anybody's POS. Meanwhile the actual shop accounts carry roleid 0 and were refused, so the mapping was backwards from intent in both halves at once. Till accounts were leaking into the application. GetStaffs is WHERE tenantid with no role filter, so a Counter Cashier appeared in the tenant staff list beside the delivery riders — a row every action on that page would fail against, since a cashier has no app login, no rider shift and no back-office screen. So: eligibility for a till is now granted explicitly by provisioning a Supervisor or a Cashier, never inherited from a back-office role, and roles 7 and 8 are excluded from every Nearle Daily lookup. The exclusion lives in the queries rather than in a check after them, because a check bolted on afterwards has to be repeated at six call sites and is one edit away from being forgotten at one of them — and that one would be the hole. A till account is not rejected by the app login; it is not found. Two things this surfaced that were not visible before. A Supervisor could not open a till. PIN sign-in needs a session that already exists, so once back-office roles were refused, an outlet whose only POS accounts were PIN-only had no way in at all. Supervisors are now provisioned with a username and password as well as a PIN; cashiers deliberately get neither, because they sign on at a counter somebody has already opened and a second password would be one more credential to leak for no capability gained. UpdatePosUser silently dropped authname. It wrote the password, reported success, and left the account unreachable by either lookup — the failure surfaced at a counter as "not recognised" rather than on the screen that caused it. Contactno had the same gap. Verified against live rows rather than asserted, by scratch/posseparation: a provisioned supervisor signs in and gets the supervisor shell; five real back-office accounts including Super admins are refused; the supervisor is invisible to applogin, tenant weblogin and the password-setup lookup; and no till account appears in getallusers, while asking for role 7 by name still returns them so the console can read its own people. All five outlets that stock products now have a Supervisor and a Cashier. Also moves the loose markdown into docs/, which was already staged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,18 @@ func (r *userRepository) GetAllUsers(roleID, tenantID, pageno, pagesize int, key
|
||||
LEFT JOIN ridershifts c ON a.shiftid = c.shiftid
|
||||
WHERE 1=1`)
|
||||
|
||||
// Till accounts are not Nearle Daily users and must not be listed as though
|
||||
// they were. The two products share this table and nothing else: a cashier
|
||||
// has no app login, no rider shift and no back-office screen, so a row
|
||||
// returned here is one every action on the page would fail against.
|
||||
//
|
||||
// Asking for 7 or 8 by name still works, so the POS console can read its own
|
||||
// people through the same endpoint — this hides them from the general list,
|
||||
// it does not make them unreachable.
|
||||
if roleID != models.PosRoleSupervisor && roleID != models.PosRoleCashier {
|
||||
queryBuilder.WriteString(" AND COALESCE(a.roleid, 0) NOT IN (7, 8)")
|
||||
}
|
||||
|
||||
if roleID != 0 {
|
||||
queryBuilder.WriteString(" AND a.roleid = ?")
|
||||
params = append(params, roleID)
|
||||
@@ -119,14 +131,16 @@ func (r *userRepository) Login(user models.User) (models.UserInfo, error) {
|
||||
|
||||
var q string
|
||||
if user.Authname != "" {
|
||||
q = `SELECT a.userid FROM app_users a
|
||||
WHERE a.authname = ? AND a.configid = ?`
|
||||
q = `SELECT a.userid FROM app_users a
|
||||
WHERE a.authname = ? AND a.configid = ?
|
||||
AND COALESCE(a.roleid, 0) NOT IN (7, 8)`
|
||||
if err := r.db.Raw(q, user.Authname, user.Configid).Scan(&uid).Error; err != nil {
|
||||
return models.UserInfo{}, err
|
||||
}
|
||||
} else {
|
||||
q = `SELECT a.userid FROM app_users a
|
||||
WHERE a.contactno = ? AND a.configid = ?`
|
||||
q = `SELECT a.userid FROM app_users a
|
||||
WHERE a.contactno = ? AND a.configid = ?
|
||||
AND COALESCE(a.roleid, 0) NOT IN (7, 8)`
|
||||
if err := r.db.Raw(q, user.Contactno, user.Configid).Scan(&uid).Error; err != nil {
|
||||
return models.UserInfo{}, err
|
||||
}
|
||||
@@ -159,12 +173,16 @@ func (r *userRepository) FindUserID(authname, contactno string, configid int) (i
|
||||
var query string
|
||||
|
||||
if authname != "" {
|
||||
query = `SELECT a.userid FROM app_users a WHERE a.authname = ? AND a.configid = ?`
|
||||
query = `SELECT a.userid FROM app_users a
|
||||
WHERE a.authname = ? AND a.configid = ?
|
||||
AND COALESCE(a.roleid, 0) NOT IN (7, 8)`
|
||||
if err := r.db.Raw(query, authname, configid).Scan(&uid).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
} else {
|
||||
query = `SELECT a.userid FROM app_users a WHERE a.contactno = ? AND a.configid = ?`
|
||||
query = `SELECT a.userid FROM app_users a
|
||||
WHERE a.contactno = ? AND a.configid = ?
|
||||
AND COALESCE(a.roleid, 0) NOT IN (7, 8)`
|
||||
if err := r.db.Raw(query, contactno, configid).Scan(&uid).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -189,10 +207,19 @@ func (r *userRepository) UpdateStaff(user models.User) error {
|
||||
return r.db.Table("app_users").Where("userid = ?", user.Userid).Updates(&user).Error
|
||||
}
|
||||
|
||||
// A till account is not a Nearle Daily user. The two products share this table
|
||||
// and nothing else, so every way into the application excludes roles 7 and 8 in
|
||||
// the lookup itself: a cashier is not "refused", they are simply not found.
|
||||
//
|
||||
// Doing it in the query rather than after it is deliberate. A check bolted on
|
||||
// afterwards has to be repeated at each of these call sites and is one edit away
|
||||
// from being forgotten at one of them, and that one would be the hole.
|
||||
func (r *userRepository) GetUserByAuthname(authname string, configid int) (int, string, string) {
|
||||
var uid int
|
||||
var password, status string
|
||||
query := `SELECT userid, password, status FROM app_users WHERE authname = ? AND configid = ?`
|
||||
query := `SELECT userid, password, status FROM app_users
|
||||
WHERE authname = ? AND configid = ?
|
||||
AND COALESCE(roleid, 0) NOT IN (7, 8)`
|
||||
r.db.Raw(query, authname, configid).Row().Scan(&uid, &password, &status)
|
||||
return uid, password, status
|
||||
}
|
||||
@@ -200,7 +227,9 @@ func (r *userRepository) GetUserByAuthname(authname string, configid int) (int,
|
||||
func (r *userRepository) GetUserByContactNo(contactno string, configid int) (int, string, string) {
|
||||
var uid int
|
||||
var password, status string
|
||||
query := `SELECT userid, password, status FROM app_users WHERE contactno = ? AND configid = ?`
|
||||
query := `SELECT userid, password, status FROM app_users
|
||||
WHERE contactno = ? AND configid = ?
|
||||
AND COALESCE(roleid, 0) NOT IN (7, 8)`
|
||||
r.db.Raw(query, contactno, configid).Row().Scan(&uid, &password, &status)
|
||||
return uid, password, status
|
||||
}
|
||||
@@ -282,7 +311,8 @@ func (r *userRepository) GetUserLogin(field, value string, configid int) (int, s
|
||||
query := fmt.Sprintf(`
|
||||
SELECT userid, password, status, roleid
|
||||
FROM app_users
|
||||
WHERE %s = ? AND configid = ?`, field)
|
||||
WHERE %s = ? AND configid = ?
|
||||
AND COALESCE(roleid, 0) NOT IN (7, 8)`, field)
|
||||
|
||||
r.db.Raw(query, value, configid).Row().Scan(&uid, &password, &status, &roleid)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user