diff --git a/pos/blueprints/auth_bp.py b/pos/blueprints/auth_bp.py index 8bc60e6..87a1ca5 100644 --- a/pos/blueprints/auth_bp.py +++ b/pos/blueprints/auth_bp.py @@ -59,6 +59,7 @@ def login_pin(): # Subdomain-resolved tenant takes priority over body param tenant_id = getattr(g, 'tenant_id', None) or data.get('tenant_id') pin = data.get('pin', '') + employee_id = data.get('employee_id') device_id = data.get('device_id', request.headers.get('X-Device-Id', 'unknown')) # Optional: branch_id from the device for PIN search optimization device_branch_id = data.get('branch_id') @@ -89,39 +90,53 @@ def login_pin(): matched_employee = None - if device_branch_id: - # Try branch employees first (fast path for known devices) + 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.is_active = true AND e.pin IS NOT NULL AND e.branch_id = %s - """, (device_branch_id,)) - for emp in cur.fetchall(): + 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 } - break if not matched_employee: - # Fallback: check ALL active employees (covers owners, admins, roaming staff) - cur.execute(""" - SELECT e.id, e.name, e.pin, e.role, e.branch_id, e.max_discount_pct - FROM employees e - WHERE e.is_active = true AND e.pin IS NOT NULL - """) - employees = cur.fetchall() + # Legacy fallback: scan active employees (kept for old clients without employee_id) + if device_branch_id: + cur.execute(""" + SELECT e.id, e.name, e.pin, e.role, e.branch_id, e.max_discount_pct + FROM employees e + WHERE e.is_active = true AND e.pin IS NOT NULL AND e.branch_id = %s + """, (device_branch_id,)) + for emp in cur.fetchall(): + 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 + } + break - for emp in employees: - 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 - } - break + if not matched_employee: + cur.execute(""" + SELECT e.id, e.name, e.pin, e.role, e.branch_id, e.max_discount_pct + FROM employees e + WHERE e.is_active = true AND e.pin IS NOT NULL + """) + for emp in cur.fetchall(): + 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 + } + break if not matched_employee: _record_attempt(device_id, False) diff --git a/pos/templates/login.html b/pos/templates/login.html index 7b2c69f..7608f7c 100644 --- a/pos/templates/login.html +++ b/pos/templates/login.html @@ -155,6 +155,7 @@ ------------------------------------------------------------------ */ const state = { selectedUser: null, + selectedUserId: null, pin: [], maxPinLength: 6, }; @@ -351,6 +352,7 @@ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tenant_id: parseInt(tenantId), + employee_id: state.selectedUserId, pin: enteredPin, device_id: deviceId }) @@ -374,6 +376,7 @@ localStorage.setItem('pos_token', result.data.token); localStorage.setItem('pos_employee', JSON.stringify(result.data.employee)); localStorage.setItem('pos_tenant_id', tenantId); + document.cookie = 'pos_role=' + (result.data.employee.role || '') + '; path=/pos; SameSite=Lax'; btnLogin.innerHTML = ' Bienvenido, ' + result.data.employee.name; btnLogin.style.background = 'var(--color-success)'; @@ -398,6 +401,7 @@ function resetLoginState() { state.selectedUser = null; + state.selectedUserId = null; state.pin = []; userBtns.forEach(b => { @@ -487,6 +491,7 @@ employees.forEach(function(emp) { var btn = document.createElement('button'); btn.className = 'user-avatar-btn'; + btn.setAttribute('data-id', emp.id); btn.setAttribute('data-user', emp.initials); btn.setAttribute('data-name', emp.name); btn.setAttribute('data-role', emp.role_label); @@ -506,6 +511,7 @@ btn.classList.add('selected'); btn.setAttribute('aria-checked', 'true'); state.selectedUser = emp.initials; + state.selectedUserId = emp.id; state.pin = []; enablePinPad(); updatePinDisplay();