332 lines
11 KiB
Python
332 lines
11 KiB
Python
# dashboard.py
|
|
import base64
|
|
import re
|
|
from functools import wraps
|
|
from flask import Blueprint, request, jsonify, current_app
|
|
from werkzeug.security import check_password_hash
|
|
from admin_login import get_db # <-- corrected import (was admin_login)
|
|
|
|
dashboard_bp = Blueprint("dashboard", __name__)
|
|
|
|
# -------------------------
|
|
# Helper validations
|
|
# -------------------------
|
|
def validate_product_name(name):
|
|
if not name:
|
|
return False, "product_name is required"
|
|
if len(name) > 255:
|
|
return False, "product_name too long"
|
|
if not re.match(r"^[A-Za-z0-9\s\-']+$", name):
|
|
return False, "product_name contains invalid characters"
|
|
return True, None
|
|
|
|
def validate_quantity(quantity):
|
|
try:
|
|
qty = int(quantity)
|
|
if qty < 1:
|
|
return False, "quantity must be at least 1"
|
|
return True, None
|
|
except (ValueError, TypeError):
|
|
return False, "quantity must be a valid integer"
|
|
|
|
def validate_price(price):
|
|
try:
|
|
p = float(price)
|
|
if p <= 0:
|
|
return False, "price must be greater than 0"
|
|
return True, None
|
|
except (ValueError, TypeError):
|
|
return False, "price must be a valid number"
|
|
|
|
def validate_order_id(order_id):
|
|
try:
|
|
oid = int(order_id)
|
|
if oid < 1:
|
|
return False, "order_id must be a positive integer"
|
|
return True, None
|
|
except (ValueError, TypeError):
|
|
return False, "order_id must be a valid integer"
|
|
|
|
# -------------------------
|
|
# Basic auth helper & decorator
|
|
# -------------------------
|
|
def _auth_from_basic_header(auth_header):
|
|
if not auth_header:
|
|
return None, None
|
|
parts = auth_header.split(None, 1)
|
|
if len(parts) != 2:
|
|
return None, None
|
|
scheme, creds = parts
|
|
if scheme.lower() != "basic":
|
|
return None, None
|
|
try:
|
|
decoded = base64.b64decode(creds).decode("utf-8")
|
|
except Exception:
|
|
return None, None
|
|
if ":" not in decoded:
|
|
return None, None
|
|
username, password = decoded.split(":", 1)
|
|
return username, password
|
|
|
|
def basic_auth_required(f):
|
|
@wraps(f)
|
|
def wrapper(*args, **kwargs):
|
|
auth_header = request.headers.get("Authorization", "")
|
|
username, password = _auth_from_basic_header(auth_header)
|
|
|
|
# fallback: JSON body credentials
|
|
if not username:
|
|
try:
|
|
body = request.get_json(silent=True) or {}
|
|
username = (body.get("username") or "").strip()
|
|
password = (body.get("password") or "")
|
|
except Exception:
|
|
username = None
|
|
password = None
|
|
|
|
if not username or not password:
|
|
return jsonify({"error": "authentication required (Basic auth header or JSON username/password)"}), 401
|
|
|
|
cnx = None
|
|
cur = None
|
|
try:
|
|
cnx = get_db()
|
|
cur = cnx.cursor(dictionary=True)
|
|
cur.execute("SELECT id, username, password FROM admins_cred WHERE username = %s", (username,))
|
|
admin = cur.fetchone()
|
|
if not admin or not check_password_hash(admin["password"], password):
|
|
return jsonify({"error": "invalid credentials"}), 401
|
|
|
|
request.admin_id = int(admin["id"])
|
|
request.admin_username = admin["username"]
|
|
return f(*args, **kwargs)
|
|
except Exception:
|
|
current_app.logger.exception("auth verify error")
|
|
return jsonify({"error": "internal server error"}), 500
|
|
finally:
|
|
try:
|
|
if cur: cur.close()
|
|
except: pass
|
|
try:
|
|
if cnx: cnx.close()
|
|
except: pass
|
|
return wrapper
|
|
|
|
# -------------------------
|
|
# Create order item
|
|
# -------------------------
|
|
@dashboard_bp.route("/order-item", methods=["POST"])
|
|
@basic_auth_required
|
|
def create_order_item():
|
|
admin_id = getattr(request, "admin_id", None)
|
|
if not request.is_json:
|
|
return jsonify({"error": "expected JSON body"}), 400
|
|
|
|
body = request.get_json()
|
|
order_id = body.get("order_id")
|
|
product_name = (body.get("product_name") or "").strip()
|
|
quantity = body.get("quantity", 1)
|
|
price = body.get("price")
|
|
|
|
ok, err = validate_order_id(order_id)
|
|
if not ok:
|
|
return jsonify({"error": err}), 400
|
|
ok, err = validate_product_name(product_name)
|
|
if not ok:
|
|
return jsonify({"error": err}), 400
|
|
ok, err = validate_quantity(quantity)
|
|
if not ok:
|
|
return jsonify({"error": err}), 400
|
|
ok, err = validate_price(price)
|
|
if not ok:
|
|
return jsonify({"error": err}), 400
|
|
|
|
order_id_int = int(order_id)
|
|
qty_int = int(quantity)
|
|
price_f = float(price)
|
|
|
|
|
|
cnx = get_db()
|
|
cur = cnx.cursor(dictionary=True)
|
|
try:
|
|
cur.execute(
|
|
"SELECT id FROM order_items WHERE product_name = %s AND order_id = %s AND admin_id = %s",
|
|
(product_name, order_id_int, admin_id)
|
|
)
|
|
existing = cur.fetchone()
|
|
if existing:
|
|
return jsonify({"error": "order item already exists"}), 400
|
|
|
|
cur2 = cnx.cursor()
|
|
try:
|
|
cur2.execute(
|
|
"INSERT INTO order_items (admin_id, order_id, product_name, quantity, price, status, created_at) "
|
|
"VALUES (%s, %s, %s, %s, %s, %s, NOW())",
|
|
(admin_id, order_id_int, product_name, qty_int, price_f, "pending")
|
|
)
|
|
cnx.commit()
|
|
new_id = cur2.lastrowid
|
|
finally:
|
|
try: cur2.close()
|
|
except: pass
|
|
|
|
|
|
return jsonify({
|
|
"message": "order item created",
|
|
"id": new_id,
|
|
"admin_id": admin_id,
|
|
"order_id": order_id_int,
|
|
"product_name": product_name,
|
|
"quantity": qty_int,
|
|
"price": price_f,
|
|
"status": "pending"
|
|
}), 201
|
|
|
|
except Exception as e:
|
|
cnx.rollback()
|
|
current_app.logger.exception("create_order_item error")
|
|
return jsonify({"error": str(e)}), 500
|
|
finally:
|
|
try: cur.close()
|
|
except: pass
|
|
try: cnx.close()
|
|
except: pass
|
|
|
|
# -------------------------
|
|
# List / Get order item(s)
|
|
# -------------------------
|
|
@dashboard_bp.route("/order-item", methods=["GET"])
|
|
@basic_auth_required
|
|
def list_or_get_order_item():
|
|
admin_id = getattr(request, "admin_id", None)
|
|
id_param = request.args.get("id", "").strip()
|
|
order_id_param = request.args.get("order_id", "").strip()
|
|
|
|
cnx = get_db()
|
|
cur = cnx.cursor(dictionary=True)
|
|
try:
|
|
if id_param == "" and order_id_param == "":
|
|
cur.execute(
|
|
"SELECT id, admin_id, order_id, product_name, quantity, price, status, created_at "
|
|
"FROM order_items WHERE admin_id = %s",
|
|
(admin_id,)
|
|
)
|
|
rows = cur.fetchall()
|
|
return jsonify({"rows": rows}), 200
|
|
|
|
elif order_id_param != "":
|
|
try:
|
|
order_id_int = int(order_id_param)
|
|
except ValueError:
|
|
return jsonify({"error": "order_id must be integer"}), 400
|
|
|
|
cur.execute(
|
|
"SELECT id, admin_id, order_id, product_name, quantity, price, status, created_at "
|
|
"FROM order_items WHERE order_id = %s AND admin_id = %s",
|
|
(order_id_int, admin_id)
|
|
)
|
|
rows = cur.fetchall()
|
|
return jsonify({"rows": rows}), 200
|
|
|
|
else:
|
|
try:
|
|
id_int = int(id_param)
|
|
except ValueError:
|
|
return jsonify({"error": "id must be integer"}), 400
|
|
|
|
cur.execute(
|
|
"SELECT id, admin_id, order_id, product_name, quantity, price, status, created_at "
|
|
"FROM order_items WHERE id = %s AND admin_id = %s",
|
|
(id_int, admin_id)
|
|
)
|
|
row = cur.fetchone()
|
|
if not row:
|
|
return jsonify({"error": "order item not found"}), 404
|
|
return jsonify({"row": row}), 200
|
|
|
|
except Exception as e:
|
|
current_app.logger.exception("list_or_get_order_item error")
|
|
return jsonify({"error": str(e)}), 500
|
|
finally:
|
|
try: cur.close()
|
|
except: pass
|
|
try: cnx.close()
|
|
except: pass
|
|
|
|
# -------------------------
|
|
# Delete order item
|
|
# -------------------------
|
|
@dashboard_bp.route("/order-item/<int:id>", methods=["DELETE"])
|
|
@basic_auth_required
|
|
def delete_order_item(id):
|
|
admin_id = getattr(request, "admin_id", None)
|
|
try:
|
|
id_int = int(id)
|
|
except (ValueError, TypeError):
|
|
return jsonify({"error": "id must be integer"}), 400
|
|
|
|
cnx = get_db()
|
|
cur = cnx.cursor(dictionary=True)
|
|
try:
|
|
cur.execute("SELECT admin_id FROM order_items WHERE id = %s", (id_int,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
return jsonify({"error": "order item not found"}), 404
|
|
if int(row["admin_id"]) != int(admin_id):
|
|
return jsonify({"error": "not authorized to delete this order item"}), 403
|
|
|
|
cur2 = cnx.cursor()
|
|
try:
|
|
cur2.execute("DELETE FROM order_items WHERE id = %s", (id_int,))
|
|
cnx.commit()
|
|
finally:
|
|
try: cur2.close()
|
|
except: pass
|
|
|
|
return jsonify({"message": "order item deleted", "id": id_int}), 200
|
|
except Exception:
|
|
cnx.rollback()
|
|
current_app.logger.exception("Error deleting order_item id=%s", id_int)
|
|
return jsonify({"error": "internal server error"}), 500
|
|
finally:
|
|
try: cur.close()
|
|
except: pass
|
|
try: cnx.close()
|
|
except: pass
|
|
|
|
# -------------------------
|
|
# Unsafe demo (intentional SQL injection vulnerability)
|
|
# -------------------------
|
|
@dashboard_bp.route("/order-item/unsafe", methods=["GET"])
|
|
@basic_auth_required
|
|
def get_order_item_unsafe():
|
|
"""
|
|
UNSAFE VERSION - DO NOT USE IN PRODUCTION
|
|
- No validation
|
|
- Uses raw f-string SQL (SQL Injection vulnerability demo)
|
|
"""
|
|
id_param = request.args.get("id", "").strip()
|
|
|
|
if id_param == "":
|
|
return jsonify({'error': 'missing id parameter'}), 400
|
|
|
|
# Completely unsafe SQL (demo only - VULNERABLE TO SQL INJECTION)
|
|
query = f"SELECT id, admin_id, order_id, product_name, quantity, price, total, status, created_at FROM order_items WHERE id = {id_param}"
|
|
|
|
cnx = get_db()
|
|
cur = cnx.cursor(dictionary=True)
|
|
|
|
try:
|
|
cur.execute(query) # SQL Injection works here
|
|
rows = cur.fetchall()
|
|
print(f"Executed unsafe query: {rows}")
|
|
except Exception as e:
|
|
return jsonify({'error': str(e), 'query': query}), 400
|
|
finally:
|
|
cur.close()
|
|
cnx.close()
|
|
|
|
return jsonify({
|
|
"executed_sql": query,
|
|
"rows": rows
|
|
}), 200 |