changes
This commit is contained in:
parent
9713ad5a55
commit
6584c2c8c5
@ -1 +1 @@
|
||||
7550
|
||||
3856
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from admin_login import jwt_required # import decorator
|
||||
from admin_login import get_db # reuse DB helper from admin_signin
|
||||
# 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__)
|
||||
|
||||
@ -13,7 +16,6 @@ def validate_product_name(name):
|
||||
return False, "product_name is required"
|
||||
if len(name) > 255:
|
||||
return False, "product_name too long"
|
||||
# allow letters, numbers, space, dash and apostrophe
|
||||
if not re.match(r"^[A-Za-z0-9\s\-']+$", name):
|
||||
return False, "product_name contains invalid characters"
|
||||
return True, None
|
||||
@ -45,20 +47,78 @@ def validate_order_id(order_id):
|
||||
except (ValueError, TypeError):
|
||||
return False, "order_id must be a valid integer"
|
||||
|
||||
def validate_status(status):
|
||||
valid_statuses = ['pending', 'processing', 'completed', 'cancelled']
|
||||
if status and status not in valid_statuses:
|
||||
return False, f"status must be one of: {', '.join(valid_statuses)}"
|
||||
return True, None
|
||||
# -------------------------
|
||||
# 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"])
|
||||
@jwt_required
|
||||
@basic_auth_required
|
||||
def create_order_item():
|
||||
admin_id = request.admin_id # set by decorator
|
||||
|
||||
admin_id = getattr(request, "admin_id", None)
|
||||
if not request.is_json:
|
||||
return jsonify({"error": "expected JSON body"}), 400
|
||||
|
||||
@ -68,158 +128,138 @@ def create_order_item():
|
||||
quantity = body.get("quantity", 1)
|
||||
price = body.get("price")
|
||||
|
||||
# Validations
|
||||
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:
|
||||
# 🔍 1. CHECK IF ITEM ALREADY EXISTS (same product in same order by same admin)
|
||||
cur.execute(
|
||||
"SELECT id FROM order_items WHERE product_name = %s AND order_id = %s AND admin_id = %s",
|
||||
(product_name, order_id, admin_id)
|
||||
(product_name, order_id_int, admin_id)
|
||||
)
|
||||
existing = cur.fetchone()
|
||||
|
||||
if existing:
|
||||
return jsonify({"error": "order item already exists"}), 400
|
||||
|
||||
# 🔥 2. INSERT NEW ORDER ITEM
|
||||
cur2 = cnx.cursor()
|
||||
cur2.execute(
|
||||
"INSERT INTO order_items (admin_id, order_id, product_name, quantity, price) VALUES (%s, %s, %s, %s, %s)",
|
||||
(admin_id, order_id, product_name, quantity, price)
|
||||
)
|
||||
cnx.commit()
|
||||
new_id = cur2.lastrowid
|
||||
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,
|
||||
"order_id": order_id_int,
|
||||
"product_name": product_name,
|
||||
"quantity": quantity,
|
||||
"price": float(price)
|
||||
"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()
|
||||
try: cur.close()
|
||||
except: pass
|
||||
try:
|
||||
cnx.close()
|
||||
try: cnx.close()
|
||||
except: pass
|
||||
|
||||
|
||||
# -------------------------
|
||||
# List / Get order item(s)
|
||||
# -------------------------
|
||||
@dashboard_bp.route("/order-item", methods=["GET"])
|
||||
@jwt_required
|
||||
@basic_auth_required
|
||||
def list_or_get_order_item():
|
||||
admin_id = request.admin_id
|
||||
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 == "":
|
||||
# List all order items for this admin
|
||||
cur.execute(
|
||||
"SELECT id, admin_id, order_id, product_name, quantity, price, total, status, created_at FROM order_items WHERE admin_id = %s",
|
||||
"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
|
||||
|
||||
return jsonify({"rows": rows}), 200
|
||||
|
||||
elif order_id_param != "":
|
||||
# List all items for a specific order
|
||||
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, total, status, created_at FROM order_items WHERE order_id = %s AND admin_id = %s",
|
||||
"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:
|
||||
# Get specific order item by id
|
||||
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, total, status, created_at FROM order_items WHERE id = %s AND admin_id = %s",
|
||||
"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({
|
||||
"rows": rows
|
||||
}), 200
|
||||
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 Exception:
|
||||
pass
|
||||
try:
|
||||
cnx.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# -------------------------
|
||||
# Update order item
|
||||
# -------------------------
|
||||
try: cur.close()
|
||||
except: pass
|
||||
try: cnx.close()
|
||||
except: pass
|
||||
|
||||
# -------------------------
|
||||
# Delete order item
|
||||
# -------------------------
|
||||
from flask import current_app, jsonify, request
|
||||
# optionally: from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||
|
||||
@dashboard_bp.route("/order-item/<int:id>", methods=["DELETE"])
|
||||
@jwt_required
|
||||
@basic_auth_required
|
||||
def delete_order_item(id):
|
||||
# get admin id from jwt (adjust according to your JWT middleware)
|
||||
admin_id = getattr(request, "admin_id", None)
|
||||
# or if you use flask_jwt_extended: admin_id = get_jwt_identity()
|
||||
|
||||
if admin_id is None:
|
||||
current_app.logger.warning("delete_order_item: missing admin_id on request")
|
||||
return jsonify({"error": "unauthenticated"}), 401
|
||||
|
||||
try:
|
||||
id_int = int(id)
|
||||
except (ValueError, TypeError):
|
||||
@ -232,31 +272,33 @@ def delete_order_item(id):
|
||||
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
|
||||
|
||||
cur.execute("DELETE FROM order_items WHERE id = %s", (id_int,))
|
||||
cnx.commit()
|
||||
return jsonify({"message": "order item deleted", "id": id_int}), 200
|
||||
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
|
||||
|
||||
|
||||
# -------------------------
|
||||
# Demo endpoint: UNSAFE SQL (for educational purposes only)
|
||||
# Unsafe demo (intentional SQL injection vulnerability)
|
||||
# -------------------------
|
||||
@dashboard_bp.route("/order-item/unsafe", methods=["GET"])
|
||||
@jwt_required
|
||||
@basic_auth_required
|
||||
def get_order_item_unsafe():
|
||||
"""
|
||||
UNSAFE VERSION - DO NOT USE IN PRODUCTION
|
||||
|
||||
@ -1,15 +1,12 @@
|
||||
# admin_signin.py
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from functools import wraps
|
||||
from flask import Blueprint, request, jsonify
|
||||
import mysql.connector
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
import jwt
|
||||
|
||||
# -------------------------
|
||||
# Config from env
|
||||
# Config from env (adjust if needed)
|
||||
# -------------------------
|
||||
DB_HOST = os.getenv("MYSQL_HOST", "127.0.0.1")
|
||||
DB_USER = os.getenv("MYSQL_USER", "root")
|
||||
@ -17,11 +14,6 @@ DB_PASS = os.getenv("MYSQL_PASSWORD", "12345!")
|
||||
DB_NAME = os.getenv("MYSQL_DB", "sqlilab")
|
||||
DB_PORT = int(os.getenv("MYSQL_PORT", 3306))
|
||||
|
||||
JWT_SECRET = os.getenv("JWT_SECRET", "SUPER_SECRET_KEY_SHOULD_CHANGE")
|
||||
JWT_ALGO = "HS256"
|
||||
ACCESS_TOKEN_EXPIRES_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRES_MINUTES", 15))
|
||||
REFRESH_TOKEN_EXPIRES_DAYS = int(os.getenv("REFRESH_TOKEN_EXPIRES_DAYS", 7))
|
||||
|
||||
auth_bp = Blueprint("auth", __name__)
|
||||
|
||||
# -------------------------
|
||||
@ -39,173 +31,46 @@ def get_db():
|
||||
return mysql.connector.connect(**conn_args)
|
||||
|
||||
# -------------------------
|
||||
# Ensure admins table has refresh token columns (runs at import)
|
||||
# Ensure admins table has refresh columns (best-effort, safe)
|
||||
# -------------------------
|
||||
def ensure_admin_refresh_columns():
|
||||
"""
|
||||
Adds columns refresh_token and refresh_expires_at to admins table if missing.
|
||||
Safe to call at app startup.
|
||||
Best-effort: ensure admins_cred table exists and has common columns.
|
||||
This will not throw if table missing; useful at startup.
|
||||
"""
|
||||
cnx = None
|
||||
cur = None
|
||||
try:
|
||||
cnx = get_db()
|
||||
cur = cnx.cursor(dictionary=True)
|
||||
# check if 'admins' table exists and columns exist
|
||||
cur.execute("""
|
||||
SELECT COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = 'admins_cred' AND COLUMN_NAME IN ('refresh_token','refresh_expires_at')
|
||||
""", (DB_NAME,))
|
||||
cur = cnx.cursor()
|
||||
# Try a harmless check; if table doesn't exist this will raise and we ignore
|
||||
cur.execute(
|
||||
"SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS "
|
||||
"WHERE TABLE_SCHEMA = %s AND TABLE_NAME = 'admins_cred' AND COLUMN_NAME IN ('refresh_token','refresh_expires_at')",
|
||||
(DB_NAME,)
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
existing = {r["COLUMN_NAME"] for r in rows}
|
||||
to_add = []
|
||||
if "refresh_token" not in existing:
|
||||
to_add.append("ADD COLUMN refresh_token VARCHAR(255) DEFAULT NULL")
|
||||
if "refresh_expires_at" not in existing:
|
||||
to_add.append("ADD COLUMN refresh_expires_at DATETIME DEFAULT NULL")
|
||||
if to_add:
|
||||
alter_sql = "ALTER TABLE admins " + ", ".join(to_add)
|
||||
cur.execute(alter_sql)
|
||||
# add an index on refresh_token (if not exists)
|
||||
try:
|
||||
cur.execute("CREATE INDEX idx_admins_refresh_token ON admins (refresh_token)")
|
||||
except Exception:
|
||||
# index may already exist or DB may throw; ignore silently
|
||||
pass
|
||||
cnx.commit()
|
||||
except mysql.connector.Error:
|
||||
# If admins table doesn't exist or other DB error, don't crash import.
|
||||
# The app should create 'admins' table via your normal migrations/SQL.
|
||||
if cnx:
|
||||
cnx.rollback()
|
||||
# If you want ALTER logic, paste here — keeping minimal to avoid accidental schema changes.
|
||||
except Exception:
|
||||
# ignore errors (table may not exist in dev)
|
||||
pass
|
||||
finally:
|
||||
if cur:
|
||||
cur.close()
|
||||
if cnx:
|
||||
cnx.close()
|
||||
try:
|
||||
if cur:
|
||||
cur.close()
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
if cnx:
|
||||
cnx.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Try to ensure columns exist (best-effort, won't raise on missing table)
|
||||
# call it so import won't fail
|
||||
ensure_admin_refresh_columns()
|
||||
|
||||
# -------------------------
|
||||
# Token helpers (use admins table for single refresh token per-admin)
|
||||
# -------------------------
|
||||
def create_access_token(admin_id):
|
||||
exp = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRES_MINUTES)
|
||||
payload = {"admin_id": admin_id, "exp": exp, "typ": "access"}
|
||||
token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGO)
|
||||
# pyjwt may return bytes in some versions
|
||||
return token if isinstance(token, str) else token.decode("utf-8")
|
||||
|
||||
def create_refresh_token_str():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def store_refresh_token(admin_id, token_str):
|
||||
"""Store (or replace) the refresh token for the given admin id."""
|
||||
expires_at = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRES_DAYS)
|
||||
cnx = get_db()
|
||||
cur = cnx.cursor()
|
||||
try:
|
||||
cur.execute(
|
||||
"UPDATE admins_cred SET refresh_token = %s, refresh_expires_at = %s WHERE id = %s",
|
||||
(token_str, expires_at, admin_id),
|
||||
)
|
||||
# If no row was updated maybe admin doesn't exist; commit anyway
|
||||
cnx.commit()
|
||||
except Exception:
|
||||
cnx.rollback()
|
||||
raise
|
||||
finally:
|
||||
cur.close()
|
||||
cnx.close()
|
||||
|
||||
def find_refresh_token(token_str):
|
||||
"""Find the admin row by refresh token. Returns dict with admin_id and refresh_expires_at or None."""
|
||||
cnx = get_db()
|
||||
cur = cnx.cursor(dictionary=True)
|
||||
try:
|
||||
cur.execute(
|
||||
"SELECT id AS admin_id, refresh_token, refresh_expires_at FROM admins WHERE refresh_token = %s",
|
||||
(token_str,),
|
||||
)
|
||||
return cur.fetchone()
|
||||
finally:
|
||||
cur.close()
|
||||
cnx.close()
|
||||
|
||||
def delete_refresh_token(token_str):
|
||||
"""Remove refresh token by clearing fields on admins table. Returns rowcount."""
|
||||
cnx = get_db()
|
||||
cur = cnx.cursor()
|
||||
try:
|
||||
cur.execute(
|
||||
"UPDATE admins_cred SET refresh_token = NULL, refresh_expires_at = NULL WHERE refresh_token = %s",
|
||||
(token_str,),
|
||||
)
|
||||
rowcount = cur.rowcount
|
||||
cnx.commit()
|
||||
return rowcount
|
||||
except Exception:
|
||||
cnx.rollback()
|
||||
raise
|
||||
finally:
|
||||
cur.close()
|
||||
cnx.close()
|
||||
|
||||
def rotate_refresh_token(old_token, admin_id):
|
||||
"""
|
||||
Atomically rotate the refresh token for the admin.
|
||||
This updates the admins row only if it currently belongs to that admin (or is NULL).
|
||||
Returns new token string on success, or None if rotation failed.
|
||||
"""
|
||||
new_token = create_refresh_token_str()
|
||||
expires_at = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRES_DAYS)
|
||||
cnx = get_db()
|
||||
cur = cnx.cursor()
|
||||
try:
|
||||
cur.execute(
|
||||
"UPDATE admins_cred SET refresh_token = %s, refresh_expires_at = %s "
|
||||
"WHERE id = %s AND (refresh_token = %s OR refresh_token IS NULL)",
|
||||
(new_token, expires_at, admin_id, old_token),
|
||||
)
|
||||
cnx.commit()
|
||||
if cur.rowcount == 0:
|
||||
# rotation failed (token mismatch or admin doesn't have that token)
|
||||
return None
|
||||
return new_token
|
||||
except Exception:
|
||||
cnx.rollback()
|
||||
raise
|
||||
finally:
|
||||
cur.close()
|
||||
cnx.close()
|
||||
|
||||
# -------------------------
|
||||
# jwt_required decorator
|
||||
# -------------------------
|
||||
def jwt_required(f):
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
auth = request.headers.get("Authorization", "")
|
||||
if not auth or not auth.startswith("Bearer "):
|
||||
return jsonify({"error": "Missing Authorization header"}), 401
|
||||
token = auth.split(" ", 1)[1]
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGO])
|
||||
if payload.get("typ") != "access":
|
||||
return jsonify({"error": "Invalid token type"}), 401
|
||||
# attach admin_id for handlers to use
|
||||
request.admin_id = int(payload["admin_id"])
|
||||
except jwt.ExpiredSignatureError:
|
||||
return jsonify({"error": "Access token expired"}), 401
|
||||
except jwt.InvalidTokenError:
|
||||
return jsonify({"error": "Invalid access token"}), 401
|
||||
return f(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
# -------------------------
|
||||
# Routes: signup, login, refresh, logout
|
||||
# Signup & Login endpoints (simple)
|
||||
# -------------------------
|
||||
@auth_bp.route("/admin_signup", methods=["POST"])
|
||||
def admin_signup():
|
||||
@ -218,98 +83,59 @@ def admin_signup():
|
||||
return jsonify({"error": "username and password required"}), 400
|
||||
|
||||
hashed = generate_password_hash(password)
|
||||
cnx = get_db()
|
||||
cur = cnx.cursor()
|
||||
cnx = None
|
||||
cur = None
|
||||
try:
|
||||
cur.execute("INSERT INTO admins_cred (username, password) VALUES (%s, %s)", (username, hashed))
|
||||
cnx = get_db()
|
||||
cur = cnx.cursor()
|
||||
cur.execute("INSERT INTO admins_cred (username, password, created_at) VALUES (%s, %s, %s)",
|
||||
(username, hashed, datetime.utcnow()))
|
||||
cnx.commit()
|
||||
new_id = cur.lastrowid
|
||||
return jsonify({"message": "admin signed up", "id": new_id}), 201
|
||||
except mysql.connector.IntegrityError:
|
||||
cnx.rollback()
|
||||
if cnx:
|
||||
cnx.rollback()
|
||||
return jsonify({"error": "username already exists"}), 400
|
||||
except Exception as e:
|
||||
cnx.rollback()
|
||||
if cnx:
|
||||
cnx.rollback()
|
||||
return jsonify({"error": str(e)}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
cnx.close()
|
||||
try:
|
||||
if cur: cur.close()
|
||||
except: pass
|
||||
try:
|
||||
if cnx: cnx.close()
|
||||
except: pass
|
||||
|
||||
@auth_bp.route("/safe/admin/login", methods=["POST"])
|
||||
def safe_admin_login():
|
||||
data = request.get_json() or {}
|
||||
@auth_bp.route("/admin_login", methods=["POST"])
|
||||
def admin_login():
|
||||
if not request.is_json:
|
||||
return jsonify({"error": "expected JSON body"}), 400
|
||||
data = request.get_json()
|
||||
username = (data.get("username") or "").strip()
|
||||
password = (data.get("password") or "").strip()
|
||||
if not username or not password:
|
||||
return jsonify({"error": "username and password required"}), 400
|
||||
|
||||
cnx = get_db()
|
||||
cur = cnx.cursor(dictionary=True)
|
||||
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
|
||||
|
||||
admin_id = int(admin["id"])
|
||||
access_token = create_access_token(admin_id)
|
||||
refresh_token = create_refresh_token_str()
|
||||
store_refresh_token(admin_id, refresh_token)
|
||||
|
||||
return jsonify({
|
||||
"message": "login success",
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": ACCESS_TOKEN_EXPIRES_MINUTES * 60
|
||||
}), 200
|
||||
return jsonify({"message": "login success", "admin": {"id": int(admin["id"]), "username": admin["username"]}}), 200
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
cnx.close()
|
||||
|
||||
@auth_bp.route("/token/refresh", methods=["POST"])
|
||||
def token_refresh():
|
||||
body = request.get_json() or {}
|
||||
old_refresh = (body.get("refresh_token") or "").strip()
|
||||
if not old_refresh:
|
||||
return jsonify({"error": "refresh_token required"}), 400
|
||||
|
||||
row = find_refresh_token(old_refresh)
|
||||
if not row:
|
||||
return jsonify({"error": "invalid refresh token"}), 401
|
||||
|
||||
expires_at = row.get("refresh_expires_at")
|
||||
if expires_at and datetime.utcnow() > expires_at:
|
||||
# expired - remove it and return error
|
||||
delete_refresh_token(old_refresh)
|
||||
return jsonify({"error": "refresh token expired"}), 401
|
||||
|
||||
admin_id = int(row["admin_id"])
|
||||
# rotate (atomic update). rotate_refresh_token returns None on failure.
|
||||
new_refresh = rotate_refresh_token(old_refresh, admin_id)
|
||||
if not new_refresh:
|
||||
# rotation failed - treat as invalid
|
||||
return jsonify({"error": "invalid refresh token"}), 401
|
||||
|
||||
new_access = create_access_token(admin_id)
|
||||
|
||||
return jsonify({
|
||||
"access_token": new_access,
|
||||
"refresh_token": new_refresh,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": ACCESS_TOKEN_EXPIRES_MINUTES * 60
|
||||
}), 200
|
||||
|
||||
@auth_bp.route("/safe/admin/logout", methods=["POST"])
|
||||
def admin_logout():
|
||||
body = request.get_json() or {}
|
||||
refresh = (body.get("refresh_token") or "").strip()
|
||||
if refresh:
|
||||
try:
|
||||
delete_refresh_token(refresh)
|
||||
except Exception:
|
||||
# ignore errors during logout - still respond success to client
|
||||
pass
|
||||
return jsonify({"message": "logged out"}), 200
|
||||
if cur: cur.close()
|
||||
except: pass
|
||||
try:
|
||||
if cnx: cnx.close()
|
||||
except: pass
|
||||
|
||||
58
deploy.sh
58
deploy.sh
@ -266,32 +266,32 @@ else
|
||||
BACKEND_PIDFILE="$ROOT_DIR/backend.pid"
|
||||
start_bg "$BACKEND_CMD" "$ROOT_DIR/$BACKEND_LOG" "$BACKEND_PIDFILE"
|
||||
fi
|
||||
# ---------- FRONTEND ----------
|
||||
info "Starting frontend..."
|
||||
if [ -f "$ROOT_DIR/$FRONTEND_DIR/package.json" ]; then
|
||||
cd "$ROOT_DIR/$FRONTEND_DIR"
|
||||
if [[ "$(pwd)" == /mnt/* ]]; then
|
||||
info "Detected WSL/Windows FS. Cleaning node_modules..."
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
else
|
||||
if [ -f "package-lock.json" ]; then
|
||||
if ! npm ci; then
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
fi
|
||||
else
|
||||
npm install
|
||||
fi
|
||||
fi
|
||||
# # ---------- FRONTEND ----------
|
||||
# info "Starting frontend..."
|
||||
# if [ -f "$ROOT_DIR/$FRONTEND_DIR/package.json" ]; then
|
||||
# cd "$ROOT_DIR/$FRONTEND_DIR"
|
||||
# if [[ "$(pwd)" == /mnt/* ]]; then
|
||||
# info "Detected WSL/Windows FS. Cleaning node_modules..."
|
||||
# rm -rf node_modules package-lock.json
|
||||
# npm install
|
||||
# else
|
||||
# if [ -f "package-lock.json" ]; then
|
||||
# if ! npm ci; then
|
||||
# rm -rf node_modules
|
||||
# npm install
|
||||
# fi
|
||||
# else
|
||||
# npm install
|
||||
# fi
|
||||
# fi
|
||||
|
||||
FRONTEND_START_CMD=${FRONTEND_START_CMD:-"npm run dev"}
|
||||
FRONTEND_PIDFILE="$ROOT_DIR/frontend.pid"
|
||||
start_bg "$FRONTEND_START_CMD" "$ROOT_DIR/$FRONTEND_LOG" "$FRONTEND_PIDFILE"
|
||||
cd "$ROOT_DIR"
|
||||
else
|
||||
warn "No package.json in frontend/ — skipping frontend start."
|
||||
fi
|
||||
# FRONTEND_START_CMD=${FRONTEND_START_CMD:-"npm run dev"}
|
||||
# FRONTEND_PIDFILE="$ROOT_DIR/frontend.pid"
|
||||
# start_bg "$FRONTEND_START_CMD" "$ROOT_DIR/$FRONTEND_LOG" "$FRONTEND_PIDFILE"
|
||||
# cd "$ROOT_DIR"
|
||||
# else
|
||||
# warn "No package.json in frontend/ — skipping frontend start."
|
||||
# fi
|
||||
|
||||
# ---------- FINISH ----------
|
||||
echo ""
|
||||
@ -306,14 +306,14 @@ echo "$(timestamp) Backend → http://localhost:5000"
|
||||
echo " Log: $BACKEND_LOG"
|
||||
echo " PID: $(cat backend.pid 2>/dev/null || echo 'N/A')"
|
||||
echo ""
|
||||
echo "$(timestamp) Frontend → http://localhost:5173"
|
||||
echo " Log: $FRONTEND_LOG"
|
||||
echo " PID: $(cat frontend.pid 2>/dev/null || echo 'N/A')"
|
||||
# echo "$(timestamp) Frontend → http://localhost:5173"
|
||||
# echo " Log: $FRONTEND_LOG"
|
||||
# echo " PID: $(cat frontend.pid 2>/dev/null || echo 'N/A')"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " tail -f $BACKEND_LOG"
|
||||
echo " tail -f $FRONTEND_LOG"
|
||||
# echo " tail -f $FRONTEND_LOG"
|
||||
echo " $DOCKER_CMD logs -f $MYSQL_CONTAINER"
|
||||
echo " $DOCKER_CMD exec -it $MYSQL_CONTAINER mysql -uroot -p$MYSQL_ROOT_PASSWORD $MYSQL_DATABASE"
|
||||
echo " kill \$(cat backend.pid 2>/dev/null || echo '') && kill \$(cat frontend.pid 2>/dev/null || echo '') && $DOCKER_CMD stop $MYSQL_CONTAINER"
|
||||
|
||||
@ -1 +1 @@
|
||||
7587
|
||||
3920
|
||||
|
||||
2398
frontend/package-lock.json
generated
2398
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user