fix(auth): restringe workshop/mecanico solo por servidor y cookie
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

- Elimina el script inline que redirigia a todos los usuarios.
- auth_bp.py: la respuesta de login ahora setea cookie pos_role.
- app.py: before_request redirige a /pos/workshop solo cuando la cookie
  indica rol workshop/mechanic (no depende de JS cacheado).
- login.js/login.min.js/app-init.js: sincronizan/limpian la cookie pos_role
  junto con el token en login y logout.

Tests: 35 passed
This commit is contained in:
2026-06-30 16:57:11 +00:00
parent 688ea8c70c
commit a37e60eb2a
22 changed files with 42 additions and 243 deletions

View File

@@ -1,4 +1,4 @@
from flask import Flask
from flask import Flask, request, redirect
from json_provider import OrjsonProvider
@@ -10,6 +10,18 @@ def create_app():
from middleware_tenant import resolve_tenant
app.before_request(resolve_tenant)
# ─── Server-side guard: workshop/mechanic users only see /pos/workshop ──────
@app.before_request
def restrict_workshop_users():
path = request.path
if not path.startswith('/pos/'):
return
if path == '/pos/workshop' or path.startswith('/pos/static/') or path.startswith('/pos/api/') or path == '/pos/sw.js' or path == '/pos/login' or path.startswith('/pos/login'):
return
role = request.cookies.get('pos_role', '').lower()
if role in ('workshop', 'mechanic'):
return redirect('/pos/workshop')
# ─── PWA: Service Worker must be served from /pos/ scope ──────
@app.route('/pos/sw.js')
def pos_sw():

View File

@@ -5,7 +5,7 @@ import jwt
import bcrypt
import time
from datetime import datetime, timezone, timedelta
from flask import Blueprint, request, jsonify, g
from flask import Blueprint, request, jsonify, g, make_response
from config import JWT_SECRET, JWT_ACCESS_EXPIRES, PIN_MAX_ATTEMPTS_PER_MINUTE, PIN_LOCKOUT_THRESHOLD, PIN_LOCKOUT_MINUTES
from tenant_db import get_tenant_conn, get_master_conn
@@ -157,11 +157,17 @@ def login_pin():
}
token = jwt.encode(payload, JWT_SECRET, algorithm='HS256')
return jsonify({
response = make_response(jsonify({
'token': token,
'employee': matched_employee,
'permissions': permissions
})
}))
# Cookie used by server-side route guards; JS can also read it for quick checks.
response.set_cookie(
'pos_role', matched_employee['role'],
path='/pos', samesite='Lax', httponly=False
)
return response
@auth_bp.route('/employees/<int:tenant_id>', methods=['GET'])

View File

@@ -118,6 +118,7 @@
localStorage.removeItem('pos_employee');
localStorage.removeItem('pos_tenant_id');
localStorage.removeItem('pos_cart');
document.cookie = 'pos_role=; path=/pos; SameSite=Lax; expires=Thu, 01 Jan 1970 00:00:00 GMT';
window.location.href = '/pos/login';
};

View File

@@ -82,6 +82,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';
window.location.href = '/pos/catalog';
})
.catch(function() {

View File

@@ -1 +1 @@
!function(){"use strict";var t="",e=document.querySelectorAll("#pinDots .pin-dot"),n=document.getElementById("loginError"),o=new URLSearchParams(window.location.search).get("tenant")||localStorage.getItem("pos_tenant_id"),a=localStorage.getItem("pos_device_id");function i(){e.forEach((function(e,n){e.classList.toggle("filled",n<t.length)}))}a||(a="dev-"+Date.now()+"-"+Math.random().toString(36).substr(2,9),localStorage.setItem("pos_device_id",a)),window.addDigit=function(e){t.length>=4||(t+=e,i(),n.textContent="",4===t.length&&submitPin())},window.clearPin=function(){t="",i(),n.textContent=""},window.submitPin=function(){4===t.length&&(n.textContent="",fetch("/pos/api/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tenant_id:parseInt(o),pin:t,device_id:a})}).then((function(t){return t.json().then((function(e){return{ok:t.ok,data:e}}))})).then((function(t){if(!t.ok)return n.textContent=t.data.error||"Error de autenticacion",void clearPin();localStorage.setItem("pos_token",t.data.token),localStorage.setItem("pos_employee",JSON.stringify(t.data.employee)),localStorage.setItem("pos_tenant_id",o),window.location.href="/pos/catalog"})).catch((function(){n.textContent="Error de conexion",clearPin()})))},document.addEventListener("keydown",(function(t){t.key>="0"&&t.key<="9"?addDigit(t.key):"Backspace"===t.key?clearPin():"Enter"===t.key&&submitPin()}));var r=localStorage.getItem("pos_token");r&&o&&(!function(t){try{var e=t.split(".");if(3!==e.length)return!1;var n=e[1].replace(/-/g,"+").replace(/_/g,"/"),o=JSON.parse(atob(n));return!!o.exp&&1e3*o.exp>Date.now()+3e4}catch(t){return!1}}(r)?(localStorage.removeItem("pos_token"),localStorage.removeItem("pos_employee")):window.location.href="/pos/catalog")}();
!function(){"use strict";var t="",e=document.querySelectorAll("#pinDots .pin-dot"),n=document.getElementById("loginError"),o=new URLSearchParams(window.location.search).get("tenant")||localStorage.getItem("pos_tenant_id"),a=localStorage.getItem("pos_device_id");function i(){e.forEach((function(e,n){e.classList.toggle("filled",n<t.length)}))}a||(a="dev-"+Date.now()+"-"+Math.random().toString(36).substr(2,9),localStorage.setItem("pos_device_id",a)),window.addDigit=function(e){t.length>=4||(t+=e,i(),n.textContent="",4===t.length&&submitPin())},window.clearPin=function(){t="",i(),n.textContent=""},window.submitPin=function(){4===t.length&&(n.textContent="",fetch("/pos/api/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tenant_id:parseInt(o),pin:t,device_id:a})}).then((function(t){return t.json().then((function(e){return{ok:t.ok,data:e}}))})).then((function(t){if(!t.ok)return n.textContent=t.data.error||"Error de autenticacion",void clearPin();localStorage.setItem("pos_token",t.data.token),localStorage.setItem("pos_employee",JSON.stringify(t.data.employee)),localStorage.setItem("pos_tenant_id",o),document.cookie="pos_role="+(t.data.employee.role||"")+"; path=/pos; SameSite=Lax",window.location.href="/pos/catalog"})).catch((function(){n.textContent="Error de conexion",clearPin()})))},document.addEventListener("keydown",(function(t){t.key>="0"&&t.key<="9"?addDigit(t.key):"Backspace"===t.key?clearPin():"Enter"===t.key&&submitPin()}));var r=localStorage.getItem("pos_token");r&&o&&(!function(t){try{var e=t.split(".");if(3!==e.length)return!1;var n=e[1].replace(/-/g,"+").replace(/_/g,"/"),o=JSON.parse(atob(n));return!!o.exp&&1e3*o.exp>Date.now()+3e4}catch(t){return!1}}(r)?(localStorage.removeItem("pos_token"),localStorage.removeItem("pos_employee")):window.location.href="/pos/catalog")}();

View File

@@ -15,20 +15,7 @@
<meta name="theme-color" content="#F5A623" />
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
<link rel="stylesheet" href="/pos/static/css/accounting.css?v=33">
<script id="workshop-mechanic-redirect">
(function(){
var t=localStorage.getItem("pos_token");
if(!t)return;
try{var p=JSON.parse(atob(t.split(".")[1]));var r=(p.role||"").toLowerCase();
if((r==="workshop"||r==="mechanic")&&window.location.pathname!=="/pos/workshop"){
if("serviceWorker" in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(reg){reg.unregister();});});}
window.location.replace("/pos/workshop");
}
}catch(e){}
})();
</script>
</head>
<link rel="stylesheet" href="/pos/static/css/accounting.css?v=33"></head>
<body>

View File

@@ -17,20 +17,7 @@
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
<script src="/pos/static/js/native-bridge.js" defer></script>
<link rel="stylesheet" href="/pos/static/css/catalog.css">
<script id="workshop-mechanic-redirect">
(function(){
var t=localStorage.getItem("pos_token");
if(!t)return;
try{var p=JSON.parse(atob(t.split(".")[1]));var r=(p.role||"").toLowerCase();
if((r==="workshop"||r==="mechanic")&&window.location.pathname!=="/pos/workshop"){
if("serviceWorker" in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(reg){reg.unregister();});});}
window.location.replace("/pos/workshop");
}
}catch(e){}
})();
</script>
</head>
<link rel="stylesheet" href="/pos/static/css/catalog.css"></head>
<body>

View File

@@ -15,20 +15,7 @@
<meta name="theme-color" content="#F5A623" />
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
<link rel="stylesheet" href="/pos/static/css/config.css?v=33">
<script id="workshop-mechanic-redirect">
(function(){
var t=localStorage.getItem("pos_token");
if(!t)return;
try{var p=JSON.parse(atob(t.split(".")[1]));var r=(p.role||"").toLowerCase();
if((r==="workshop"||r==="mechanic")&&window.location.pathname!=="/pos/workshop"){
if("serviceWorker" in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(reg){reg.unregister();});});}
window.location.replace("/pos/workshop");
}
}catch(e){}
})();
</script>
</head>
<link rel="stylesheet" href="/pos/static/css/config.css?v=33"></head>
<body>

View File

@@ -15,20 +15,7 @@
<meta name="theme-color" content="#F5A623" />
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
<link rel="stylesheet" href="/pos/static/css/customers.css">
<script id="workshop-mechanic-redirect">
(function(){
var t=localStorage.getItem("pos_token");
if(!t)return;
try{var p=JSON.parse(atob(t.split(".")[1]));var r=(p.role||"").toLowerCase();
if((r==="workshop"||r==="mechanic")&&window.location.pathname!=="/pos/workshop"){
if("serviceWorker" in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(reg){reg.unregister();});});}
window.location.replace("/pos/workshop");
}
}catch(e){}
})();
</script>
</head>
<link rel="stylesheet" href="/pos/static/css/customers.css"></head>
<body>

View File

@@ -15,20 +15,7 @@
<meta name="theme-color" content="#F5A623" />
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
<link rel="stylesheet" href="/pos/static/css/dashboard.css?v=33">
<script id="workshop-mechanic-redirect">
(function(){
var t=localStorage.getItem("pos_token");
if(!t)return;
try{var p=JSON.parse(atob(t.split(".")[1]));var r=(p.role||"").toLowerCase();
if((r==="workshop"||r==="mechanic")&&window.location.pathname!=="/pos/workshop"){
if("serviceWorker" in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(reg){reg.unregister();});});}
window.location.replace("/pos/workshop");
}
}catch(e){}
})();
</script>
</head>
<link rel="stylesheet" href="/pos/static/css/dashboard.css?v=33"></head>
<body>

View File

@@ -17,20 +17,7 @@
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
<script src="/pos/static/js/native-bridge.js" defer></script>
<link rel="stylesheet" href="/pos/static/css/diagrams.css">
<script id="workshop-mechanic-redirect">
(function(){
var t=localStorage.getItem("pos_token");
if(!t)return;
try{var p=JSON.parse(atob(t.split(".")[1]));var r=(p.role||"").toLowerCase();
if((r==="workshop"||r==="mechanic")&&window.location.pathname!=="/pos/workshop"){
if("serviceWorker" in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(reg){reg.unregister();});});}
window.location.replace("/pos/workshop");
}
}catch(e){}
})();
</script>
</head>
<link rel="stylesheet" href="/pos/static/css/diagrams.css"></head>
<body>
<div class="app-shell">

View File

@@ -15,20 +15,7 @@
<meta name="theme-color" content="#F5A623" />
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
<link rel="stylesheet" href="/pos/static/css/fleet.css">
<script id="workshop-mechanic-redirect">
(function(){
var t=localStorage.getItem("pos_token");
if(!t)return;
try{var p=JSON.parse(atob(t.split(".")[1]));var r=(p.role||"").toLowerCase();
if((r==="workshop"||r==="mechanic")&&window.location.pathname!=="/pos/workshop"){
if("serviceWorker" in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(reg){reg.unregister();});});}
window.location.replace("/pos/workshop");
}
}catch(e){}
})();
</script>
</head>
<link rel="stylesheet" href="/pos/static/css/fleet.css"></head>
<body>
<div class="page-shell">

View File

@@ -34,20 +34,7 @@
.pagination button:disabled { opacity: 0.5; cursor: not-allowed; }
.pagination span { font-size: 13px; color: #4b5563; }
.loading { text-align: center; padding: 40px; color: #6b7280; }
</style>
<script id="workshop-mechanic-redirect">
(function(){
var t=localStorage.getItem("pos_token");
if(!t)return;
try{var p=JSON.parse(atob(t.split(".")[1]));var r=(p.role||"").toLowerCase();
if((r==="workshop"||r==="mechanic")&&window.location.pathname!=="/pos/workshop"){
if("serviceWorker" in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(reg){reg.unregister();});});}
window.location.replace("/pos/workshop");
}
}catch(e){}
})();
</script>
</head>
</style></head>
<body>
<div class="header">
<h1>📊 Ventas Históricas - Atlas</h1>

View File

@@ -15,20 +15,7 @@
<meta name="theme-color" content="#F5A623" />
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
<link rel="stylesheet" href="/pos/static/css/inventory.css?v=33">
<script id="workshop-mechanic-redirect">
(function(){
var t=localStorage.getItem("pos_token");
if(!t)return;
try{var p=JSON.parse(atob(t.split(".")[1]));var r=(p.role||"").toLowerCase();
if((r==="workshop"||r==="mechanic")&&window.location.pathname!=="/pos/workshop"){
if("serviceWorker" in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(reg){reg.unregister();});});}
window.location.replace("/pos/workshop");
}
}catch(e){}
})();
</script>
</head>
<link rel="stylesheet" href="/pos/static/css/inventory.css?v=33"></head>
<body>

View File

@@ -15,20 +15,7 @@
<meta name="theme-color" content="#F5A623" />
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
<link rel="stylesheet" href="/pos/static/css/invoicing.css">
<script id="workshop-mechanic-redirect">
(function(){
var t=localStorage.getItem("pos_token");
if(!t)return;
try{var p=JSON.parse(atob(t.split(".")[1]));var r=(p.role||"").toLowerCase();
if((r==="workshop"||r==="mechanic")&&window.location.pathname!=="/pos/workshop"){
if("serviceWorker" in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(reg){reg.unregister();});});}
window.location.replace("/pos/workshop");
}
}catch(e){}
})();
</script>
</head>
<link rel="stylesheet" href="/pos/static/css/invoicing.css"></head>
<body>

View File

@@ -15,20 +15,7 @@
<meta name="theme-color" content="#F5A623" />
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
<link rel="stylesheet" href="/pos/static/css/marketplace.css">
<script id="workshop-mechanic-redirect">
(function(){
var t=localStorage.getItem("pos_token");
if(!t)return;
try{var p=JSON.parse(atob(t.split(".")[1]));var r=(p.role||"").toLowerCase();
if((r==="workshop"||r==="mechanic")&&window.location.pathname!=="/pos/workshop"){
if("serviceWorker" in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(reg){reg.unregister();});});}
window.location.replace("/pos/workshop");
}
}catch(e){}
})();
</script>
</head>
<link rel="stylesheet" href="/pos/static/css/marketplace.css"></head>
<body>
<header class="page-header">

View File

@@ -28,20 +28,7 @@
.meli-config-row { display:flex;gap:var(--space-4);flex-wrap:wrap;margin-bottom:var(--space-4); }
.meli-config-row label { display:block;font-size:var(--text-caption);color:var(--color-text-muted);margin-bottom:4px; }
.meli-config-row input, .meli-config-row select { padding:8px 12px;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-surface-0);color:var(--color-text-primary); }
</style>
<script id="workshop-mechanic-redirect">
(function(){
var t=localStorage.getItem("pos_token");
if(!t)return;
try{var p=JSON.parse(atob(t.split(".")[1]));var r=(p.role||"").toLowerCase();
if((r==="workshop"||r==="mechanic")&&window.location.pathname!=="/pos/workshop"){
if("serviceWorker" in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(reg){reg.unregister();});});}
window.location.replace("/pos/workshop");
}
}catch(e){}
})();
</script>
</head>
</style></head>
<body>

View File

@@ -16,20 +16,7 @@
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
<script src="/pos/static/js/native-bridge.js" defer></script>
<link rel="stylesheet" href="/pos/static/css/pos.css?v=33">
<script id="workshop-mechanic-redirect">
(function(){
var t=localStorage.getItem("pos_token");
if(!t)return;
try{var p=JSON.parse(atob(t.split(".")[1]));var r=(p.role||"").toLowerCase();
if((r==="workshop"||r==="mechanic")&&window.location.pathname!=="/pos/workshop"){
if("serviceWorker" in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(reg){reg.unregister();});});}
window.location.replace("/pos/workshop");
}
}catch(e){}
})();
</script>
</head>
<link rel="stylesheet" href="/pos/static/css/pos.css?v=33"></head>
<body class="pos-shell" id="appBody">

View File

@@ -11,20 +11,7 @@
<link rel="stylesheet" href="/pos/static/css/pos-ui.css?v=33" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="stylesheet" href="/pos/static/css/quotations.css">
<script id="workshop-mechanic-redirect">
(function(){
var t=localStorage.getItem("pos_token");
if(!t)return;
try{var p=JSON.parse(atob(t.split(".")[1]));var r=(p.role||"").toLowerCase();
if((r==="workshop"||r==="mechanic")&&window.location.pathname!=="/pos/workshop"){
if("serviceWorker" in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(reg){reg.unregister();});});}
window.location.replace("/pos/workshop");
}
}catch(e){}
})();
</script>
</head>
<link rel="stylesheet" href="/pos/static/css/quotations.css"></head>
<body>
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
<script src="/pos/static/js/i18n.js" defer></script>

View File

@@ -15,20 +15,7 @@
<meta name="theme-color" content="#F5A623" />
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
<link rel="stylesheet" href="/pos/static/css/reports.css">
<script id="workshop-mechanic-redirect">
(function(){
var t=localStorage.getItem("pos_token");
if(!t)return;
try{var p=JSON.parse(atob(t.split(".")[1]));var r=(p.role||"").toLowerCase();
if((r==="workshop"||r==="mechanic")&&window.location.pathname!=="/pos/workshop"){
if("serviceWorker" in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(reg){reg.unregister();});});}
window.location.replace("/pos/workshop");
}
}catch(e){}
})();
</script>
</head>
<link rel="stylesheet" href="/pos/static/css/reports.css"></head>
<body>

View File

@@ -49,20 +49,7 @@
.sc-interchange-list { display:flex; flex-wrap:wrap; gap:var(--space-2); }
.sc-interchange-chip { background:var(--color-surface-2); border:1px solid var(--color-border); border-radius:var(--radius-full); padding:2px 10px; font-size:var(--text-caption); }
.sc-close { background:none; border:none; font-size:20px; color:var(--color-text-muted); cursor:pointer; }
</style>
<script id="workshop-mechanic-redirect">
(function(){
var t=localStorage.getItem("pos_token");
if(!t)return;
try{var p=JSON.parse(atob(t.split(".")[1]));var r=(p.role||"").toLowerCase();
if((r==="workshop"||r==="mechanic")&&window.location.pathname!=="/pos/workshop"){
if("serviceWorker" in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(reg){reg.unregister();});});}
window.location.replace("/pos/workshop");
}
}catch(e){}
})();
</script>
</head>
</style></head>
<body>
<!-- Theme bar -->

View File

@@ -18,20 +18,7 @@
<meta name="theme-color" content="#F5A623" />
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
<link rel="stylesheet" href="/pos/static/css/whatsapp.css">
<script id="workshop-mechanic-redirect">
(function(){
var t=localStorage.getItem("pos_token");
if(!t)return;
try{var p=JSON.parse(atob(t.split(".")[1]));var r=(p.role||"").toLowerCase();
if((r==="workshop"||r==="mechanic")&&window.location.pathname!=="/pos/workshop"){
if("serviceWorker" in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(reg){reg.unregister();});});}
window.location.replace("/pos/workshop");
}
}catch(e){}
})();
</script>
</head>
<link rel="stylesheet" href="/pos/static/css/whatsapp.css"></head>
<body>
<div class="page-shell">