fix(login): el login ahora respeta el empleado seleccionado
- auth_bp.py: login acepta employee_id y verifica el PIN solo de ese empleado. Fallback legacy se mantiene para clientes antiguos. - login.html: guarda el id del empleado seleccionado y lo envia en la peticion. - Se agrega cookie pos_role en el login inline del HTML. Esto corrige el problema donde seleccionar cualquier usuario accedia siempre al mismo empleado (el primero con el mismo PIN). Tests: 35 passed
This commit is contained in:
@@ -59,6 +59,7 @@ def login_pin():
|
|||||||
# Subdomain-resolved tenant takes priority over body param
|
# Subdomain-resolved tenant takes priority over body param
|
||||||
tenant_id = getattr(g, 'tenant_id', None) or data.get('tenant_id')
|
tenant_id = getattr(g, 'tenant_id', None) or data.get('tenant_id')
|
||||||
pin = data.get('pin', '')
|
pin = data.get('pin', '')
|
||||||
|
employee_id = data.get('employee_id')
|
||||||
device_id = data.get('device_id', request.headers.get('X-Device-Id', 'unknown'))
|
device_id = data.get('device_id', request.headers.get('X-Device-Id', 'unknown'))
|
||||||
# Optional: branch_id from the device for PIN search optimization
|
# Optional: branch_id from the device for PIN search optimization
|
||||||
device_branch_id = data.get('branch_id')
|
device_branch_id = data.get('branch_id')
|
||||||
@@ -89,8 +90,25 @@ def login_pin():
|
|||||||
|
|
||||||
matched_employee = None
|
matched_employee = None
|
||||||
|
|
||||||
|
if employee_id:
|
||||||
|
# Secure flow: user selected an employee, verify only that PIN.
|
||||||
|
cur.execute("""
|
||||||
|
SELECT e.id, e.name, e.pin, e.role, e.branch_id, e.max_discount_pct
|
||||||
|
FROM employees e
|
||||||
|
WHERE e.id = %s AND e.is_active = true AND e.pin IS NOT NULL
|
||||||
|
""", (employee_id,))
|
||||||
|
emp = cur.fetchone()
|
||||||
|
if emp:
|
||||||
|
emp_id, emp_name, emp_pin_hash, emp_role, emp_branch, emp_discount = emp
|
||||||
|
if emp_pin_hash and bcrypt.checkpw(pin.encode(), emp_pin_hash.encode()):
|
||||||
|
matched_employee = {
|
||||||
|
'id': emp_id, 'name': emp_name, 'role': emp_role,
|
||||||
|
'branch_id': emp_branch, 'max_discount_pct': float(emp_discount) if emp_discount else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if not matched_employee:
|
||||||
|
# Legacy fallback: scan active employees (kept for old clients without employee_id)
|
||||||
if device_branch_id:
|
if device_branch_id:
|
||||||
# Try branch employees first (fast path for known devices)
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT e.id, e.name, e.pin, e.role, e.branch_id, e.max_discount_pct
|
SELECT e.id, e.name, e.pin, e.role, e.branch_id, e.max_discount_pct
|
||||||
FROM employees e
|
FROM employees e
|
||||||
@@ -106,15 +124,12 @@ def login_pin():
|
|||||||
break
|
break
|
||||||
|
|
||||||
if not matched_employee:
|
if not matched_employee:
|
||||||
# Fallback: check ALL active employees (covers owners, admins, roaming staff)
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT e.id, e.name, e.pin, e.role, e.branch_id, e.max_discount_pct
|
SELECT e.id, e.name, e.pin, e.role, e.branch_id, e.max_discount_pct
|
||||||
FROM employees e
|
FROM employees e
|
||||||
WHERE e.is_active = true AND e.pin IS NOT NULL
|
WHERE e.is_active = true AND e.pin IS NOT NULL
|
||||||
""")
|
""")
|
||||||
employees = cur.fetchall()
|
for emp in cur.fetchall():
|
||||||
|
|
||||||
for emp in employees:
|
|
||||||
emp_id, emp_name, emp_pin_hash, emp_role, emp_branch, emp_discount = emp
|
emp_id, emp_name, emp_pin_hash, emp_role, emp_branch, emp_discount = emp
|
||||||
if emp_pin_hash and bcrypt.checkpw(pin.encode(), emp_pin_hash.encode()):
|
if emp_pin_hash and bcrypt.checkpw(pin.encode(), emp_pin_hash.encode()):
|
||||||
matched_employee = {
|
matched_employee = {
|
||||||
|
|||||||
@@ -155,6 +155,7 @@
|
|||||||
------------------------------------------------------------------ */
|
------------------------------------------------------------------ */
|
||||||
const state = {
|
const state = {
|
||||||
selectedUser: null,
|
selectedUser: null,
|
||||||
|
selectedUserId: null,
|
||||||
pin: [],
|
pin: [],
|
||||||
maxPinLength: 6,
|
maxPinLength: 6,
|
||||||
};
|
};
|
||||||
@@ -351,6 +352,7 @@
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
tenant_id: parseInt(tenantId),
|
tenant_id: parseInt(tenantId),
|
||||||
|
employee_id: state.selectedUserId,
|
||||||
pin: enteredPin,
|
pin: enteredPin,
|
||||||
device_id: deviceId
|
device_id: deviceId
|
||||||
})
|
})
|
||||||
@@ -374,6 +376,7 @@
|
|||||||
localStorage.setItem('pos_token', result.data.token);
|
localStorage.setItem('pos_token', result.data.token);
|
||||||
localStorage.setItem('pos_employee', JSON.stringify(result.data.employee));
|
localStorage.setItem('pos_employee', JSON.stringify(result.data.employee));
|
||||||
localStorage.setItem('pos_tenant_id', tenantId);
|
localStorage.setItem('pos_tenant_id', tenantId);
|
||||||
|
document.cookie = 'pos_role=' + (result.data.employee.role || '') + '; path=/pos; SameSite=Lax';
|
||||||
|
|
||||||
btnLogin.innerHTML = '<span class="btn-login__icon" aria-hidden="true">✓</span> Bienvenido, ' + result.data.employee.name;
|
btnLogin.innerHTML = '<span class="btn-login__icon" aria-hidden="true">✓</span> Bienvenido, ' + result.data.employee.name;
|
||||||
btnLogin.style.background = 'var(--color-success)';
|
btnLogin.style.background = 'var(--color-success)';
|
||||||
@@ -398,6 +401,7 @@
|
|||||||
|
|
||||||
function resetLoginState() {
|
function resetLoginState() {
|
||||||
state.selectedUser = null;
|
state.selectedUser = null;
|
||||||
|
state.selectedUserId = null;
|
||||||
state.pin = [];
|
state.pin = [];
|
||||||
|
|
||||||
userBtns.forEach(b => {
|
userBtns.forEach(b => {
|
||||||
@@ -487,6 +491,7 @@
|
|||||||
employees.forEach(function(emp) {
|
employees.forEach(function(emp) {
|
||||||
var btn = document.createElement('button');
|
var btn = document.createElement('button');
|
||||||
btn.className = 'user-avatar-btn';
|
btn.className = 'user-avatar-btn';
|
||||||
|
btn.setAttribute('data-id', emp.id);
|
||||||
btn.setAttribute('data-user', emp.initials);
|
btn.setAttribute('data-user', emp.initials);
|
||||||
btn.setAttribute('data-name', emp.name);
|
btn.setAttribute('data-name', emp.name);
|
||||||
btn.setAttribute('data-role', emp.role_label);
|
btn.setAttribute('data-role', emp.role_label);
|
||||||
@@ -506,6 +511,7 @@
|
|||||||
btn.classList.add('selected');
|
btn.classList.add('selected');
|
||||||
btn.setAttribute('aria-checked', 'true');
|
btn.setAttribute('aria-checked', 'true');
|
||||||
state.selectedUser = emp.initials;
|
state.selectedUser = emp.initials;
|
||||||
|
state.selectedUserId = emp.id;
|
||||||
state.pin = [];
|
state.pin = [];
|
||||||
enablePinPad();
|
enablePinPad();
|
||||||
updatePinDisplay();
|
updatePinDisplay();
|
||||||
|
|||||||
Reference in New Issue
Block a user