security(crm): закрыть операторские роуты + авто-токен на все вызовы CRM

- gate /api/project/crm|tasks|approve|delete под is_operator() (только crm.html их зовёт)
- crm.html: обёртка window.fetch — X-Operator-Token на все /api/ вызовы оператора
This commit is contained in:
wasrusgen 2026-06-02 07:05:04 +03:00
parent a3fc1b6d9d
commit 7c79cd305a
2 changed files with 16 additions and 0 deletions

View File

@ -994,6 +994,8 @@ def build_spec_client():
@app.route("/api/project/crm", methods=["POST"]) @app.route("/api/project/crm", methods=["POST"])
def update_crm(): def update_crm():
if not is_operator():
return jsonify({"error": "unauthorized"}), 401
data = request.get_json(force=True) or {} data = request.get_json(force=True) or {}
proj = get_project(data.get("token")) proj = get_project(data.get("token"))
if not proj: if not proj:
@ -1409,6 +1411,8 @@ def payment_webhook():
@app.route("/api/project/delete", methods=["POST"]) @app.route("/api/project/delete", methods=["POST"])
def delete_project(): def delete_project():
if not is_operator():
return jsonify({"error": "unauthorized"}), 401
data = request.get_json(force=True) or {} data = request.get_json(force=True) or {}
proj = get_project(data.get("token")) proj = get_project(data.get("token"))
if not proj: if not proj:
@ -1429,6 +1433,8 @@ def delete_project():
@app.route("/api/project/tasks", methods=["POST"]) @app.route("/api/project/tasks", methods=["POST"])
def update_tasks(): def update_tasks():
if not is_operator():
return jsonify({"error": "unauthorized"}), 401
data = request.get_json(force=True) or {} data = request.get_json(force=True) or {}
proj = get_project(data.get("token")) proj = get_project(data.get("token"))
if not proj: if not proj:
@ -1439,6 +1445,8 @@ def update_tasks():
@app.route("/api/project/approve", methods=["POST"]) @app.route("/api/project/approve", methods=["POST"])
def approve_stage(): def approve_stage():
if not is_operator():
return jsonify({"error": "unauthorized"}), 401
data = request.get_json(force=True) or {} data = request.get_json(force=True) or {}
proj = get_project(data.get("token")) proj = get_project(data.get("token"))
if not proj: if not proj:

View File

@ -215,6 +215,14 @@ function chips(cur,opts,fn){return opts.map(([k,n])=>`<button class="fchip ${cur
// ── Операторская авторизация ── // ── Операторская авторизация ──
let OP_TOKEN=localStorage.getItem('op_token')||''; let OP_TOKEN=localStorage.getItem('op_token')||'';
function opHeaders(extra){return Object.assign({'X-Operator-Token':OP_TOKEN||''},extra||{});} function opHeaders(extra){return Object.assign({'X-Operator-Token':OP_TOKEN||''},extra||{});}
// Все вызовы оператора несут X-Operator-Token (crm.html — операторский контекст)
const _origFetch=window.fetch.bind(window);
window.fetch=function(url,opts){
opts=opts||{};
const u=typeof url==='string'?url:((url&&url.url)||'');
if(u.indexOf('/api/')>=0){opts.headers=Object.assign({},opts.headers||{},{'X-Operator-Token':OP_TOKEN||''});}
return _origFetch(url,opts);
};
async function loadProjects(){ async function loadProjects(){
const r=await fetch(`${API}/api/projects`,{headers:opHeaders()}); const r=await fetch(`${API}/api/projects`,{headers:opHeaders()});
if(r.status===401){return false;} if(r.status===401){return false;}