|
@@ -14,8 +14,12 @@ import slicer_utils
|
|
|
from fastapi import APIRouter, Request, Form, Depends, HTTPException, BackgroundTasks, UploadFile, File
|
|
from fastapi import APIRouter, Request, Form, Depends, HTTPException, BackgroundTasks, UploadFile, File
|
|
|
from services import pricing, order_processing, event_hooks
|
|
from services import pricing, order_processing, event_hooks
|
|
|
from services.audit_service import audit_service
|
|
from services.audit_service import audit_service
|
|
|
|
|
+from services.rate_limit_service import rate_limit_service
|
|
|
|
|
+from dependencies import get_current_user, require_admin, get_current_user_optional
|
|
|
from pydantic import BaseModel
|
|
from pydantic import BaseModel
|
|
|
from datetime import datetime
|
|
from datetime import datetime
|
|
|
|
|
+import locales
|
|
|
|
|
+from services.global_manager import global_manager
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/orders", tags=["orders"])
|
|
router = APIRouter(prefix="/orders", tags=["orders"])
|
|
|
|
|
|
|
@@ -42,13 +46,20 @@ async def create_order(
|
|
|
company_name: Optional[str] = Form(None),
|
|
company_name: Optional[str] = Form(None),
|
|
|
company_pib: Optional[str] = Form(None),
|
|
company_pib: Optional[str] = Form(None),
|
|
|
company_address: Optional[str] = Form(None),
|
|
company_address: Optional[str] = Form(None),
|
|
|
- token: str = Depends(auth_utils.oauth2_scheme_optional)
|
|
|
|
|
|
|
+ user: Optional[dict] = Depends(get_current_user_optional)
|
|
|
):
|
|
):
|
|
|
- user_id = None
|
|
|
|
|
- if token:
|
|
|
|
|
- payload = auth_utils.decode_token(token)
|
|
|
|
|
- if payload:
|
|
|
|
|
- user_id = payload.get("id")
|
|
|
|
|
|
|
+ ip = request.client.host if request.client else "unknown"
|
|
|
|
|
+ email_addr = email.lower()
|
|
|
|
|
+ lang = request.query_params.get("lang", "en")
|
|
|
|
|
+
|
|
|
|
|
+ is_admin = user.get("role") == "admin" if user else False
|
|
|
|
|
+ if not is_admin and rate_limit_service.is_order_flooding(email_addr, ip):
|
|
|
|
|
+ raise HTTPException(
|
|
|
|
|
+ status_code=429,
|
|
|
|
|
+ detail=locales.translate_error("flood_control", lang)
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ user_id = user.get("id") if user else None
|
|
|
|
|
|
|
|
parsed_ids = []
|
|
parsed_ids = []
|
|
|
parsed_quantities = []
|
|
parsed_quantities = []
|
|
@@ -59,7 +70,6 @@ async def create_order(
|
|
|
except:
|
|
except:
|
|
|
pass
|
|
pass
|
|
|
|
|
|
|
|
- lang = request.query_params.get("lang", "en")
|
|
|
|
|
name_col = f"name_{lang}" if lang in ["en", "ru", "me"] else "name_en"
|
|
name_col = f"name_{lang}" if lang in ["en", "ru", "me"] else "name_en"
|
|
|
|
|
|
|
|
mat_info = db.execute_query(f"SELECT {name_col}, price_per_cm3 FROM materials WHERE id = %s", (material_id,))
|
|
mat_info = db.execute_query(f"SELECT {name_col}, price_per_cm3 FROM materials WHERE id = %s", (material_id,))
|
|
@@ -117,6 +127,10 @@ async def create_order(
|
|
|
)
|
|
)
|
|
|
background_tasks.add_task(order_processing.process_order_slicing, order_insert_id)
|
|
background_tasks.add_task(order_processing.process_order_slicing, order_insert_id)
|
|
|
background_tasks.add_task(event_hooks.on_order_created, order_insert_id)
|
|
background_tasks.add_task(event_hooks.on_order_created, order_insert_id)
|
|
|
|
|
+
|
|
|
|
|
+ # Record placement for rate limiting
|
|
|
|
|
+ rate_limit_service.record_order_placement(email_addr, ip)
|
|
|
|
|
+
|
|
|
return {"status": "success", "order_id": order_insert_id, "message": "Order submitted successfully"}
|
|
return {"status": "success", "order_id": order_insert_id, "message": "Order submitted successfully"}
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
print(f"Error creating order: {e}")
|
|
print(f"Error creating order: {e}")
|
|
@@ -126,13 +140,9 @@ async def create_order(
|
|
|
async def get_my_orders(
|
|
async def get_my_orders(
|
|
|
page: int = 1,
|
|
page: int = 1,
|
|
|
size: int = 10,
|
|
size: int = 10,
|
|
|
- token: str = Depends(auth_utils.oauth2_scheme)
|
|
|
|
|
|
|
+ user: dict = Depends(get_current_user)
|
|
|
):
|
|
):
|
|
|
- payload = auth_utils.decode_token(token)
|
|
|
|
|
- if not payload:
|
|
|
|
|
- raise HTTPException(status_code=401, detail="Invalid token")
|
|
|
|
|
-
|
|
|
|
|
- user_id = payload.get("id")
|
|
|
|
|
|
|
+ user_id = user.get("id")
|
|
|
offset = (page - 1) * size
|
|
offset = (page - 1) * size
|
|
|
|
|
|
|
|
# Get total count
|
|
# Get total count
|
|
@@ -189,11 +199,8 @@ async def get_admin_orders(
|
|
|
status: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
|
date_from: Optional[str] = None,
|
|
date_from: Optional[str] = None,
|
|
|
date_to: Optional[str] = None,
|
|
date_to: Optional[str] = None,
|
|
|
- token: str = Depends(auth_utils.oauth2_scheme)
|
|
|
|
|
|
|
+ admin: dict = Depends(require_admin)
|
|
|
):
|
|
):
|
|
|
- payload = auth_utils.decode_token(token)
|
|
|
|
|
- if not payload or payload.get("role") != 'admin':
|
|
|
|
|
- raise HTTPException(status_code=403, detail="Admin role required")
|
|
|
|
|
|
|
|
|
|
where_clauses = []
|
|
where_clauses = []
|
|
|
params = []
|
|
params = []
|
|
@@ -241,22 +248,20 @@ async def get_admin_orders(
|
|
|
row['photos'] = photos
|
|
row['photos'] = photos
|
|
|
return results
|
|
return results
|
|
|
|
|
|
|
|
-@router.patch("/{order_id}/admin")
|
|
|
|
|
-async def update_order_admin(
|
|
|
|
|
- order_id: int,
|
|
|
|
|
- data: schemas.AdminOrderUpdate,
|
|
|
|
|
- background_tasks: BackgroundTasks,
|
|
|
|
|
|
|
+@router.patch("/{order_id}")
|
|
|
|
|
+async def update_order(
|
|
|
request: Request,
|
|
request: Request,
|
|
|
- token: str = Depends(auth_utils.oauth2_scheme)
|
|
|
|
|
|
|
+ order_id: int,
|
|
|
|
|
+ data: schemas.AdminOrderUpdate,
|
|
|
|
|
+ background_tasks: BackgroundTasks,
|
|
|
|
|
+ admin: dict = Depends(require_admin)
|
|
|
):
|
|
):
|
|
|
- payload = auth_utils.decode_token(token)
|
|
|
|
|
- if not payload or payload.get("role") != 'admin':
|
|
|
|
|
- raise HTTPException(status_code=403, detail="Admin role required")
|
|
|
|
|
-
|
|
|
|
|
|
|
+ order_info = db.execute_query("SELECT * FROM orders WHERE id = %s", (order_id,))
|
|
|
|
|
+ if not order_info: raise HTTPException(status_code=404, detail="Order not found")
|
|
|
|
|
+
|
|
|
update_fields = []
|
|
update_fields = []
|
|
|
params = []
|
|
params = []
|
|
|
if data.status:
|
|
if data.status:
|
|
|
- order_info = db.execute_query("SELECT * FROM orders WHERE id = %s", (order_id,))
|
|
|
|
|
if order_info:
|
|
if order_info:
|
|
|
background_tasks.add_task(
|
|
background_tasks.add_task(
|
|
|
event_hooks.on_order_status_changed,
|
|
event_hooks.on_order_status_changed,
|
|
@@ -346,24 +351,25 @@ async def update_order_admin(
|
|
|
|
|
|
|
|
# LOG ACTION
|
|
# LOG ACTION
|
|
|
await audit_service.log(
|
|
await audit_service.log(
|
|
|
- user_id=payload.get("id"),
|
|
|
|
|
|
|
+ user_id=admin.get("id"),
|
|
|
action="update_order",
|
|
action="update_order",
|
|
|
target_type="order",
|
|
target_type="order",
|
|
|
target_id=order_id,
|
|
target_id=order_id,
|
|
|
details={"updated_fields": {k.split(" = ")[0]: v for k, v in zip(update_fields, params)}},
|
|
details={"updated_fields": {k.split(" = ")[0]: v for k, v in zip(update_fields, params)}},
|
|
|
request=request
|
|
request=request
|
|
|
)
|
|
)
|
|
|
|
|
+
|
|
|
|
|
+ # NOTIFY USER VIA WEBSOCKET
|
|
|
|
|
+ await global_manager.notify_order_update(order_info[0]['user_id'], order_id)
|
|
|
|
|
+
|
|
|
return {"id": order_id, "status": "updated"}
|
|
return {"id": order_id, "status": "updated"}
|
|
|
|
|
|
|
|
@router.post("/{order_id}/attach-file")
|
|
@router.post("/{order_id}/attach-file")
|
|
|
async def admin_attach_file(
|
|
async def admin_attach_file(
|
|
|
order_id: int,
|
|
order_id: int,
|
|
|
file: UploadFile = File(...),
|
|
file: UploadFile = File(...),
|
|
|
- token: str = Depends(auth_utils.oauth2_scheme)
|
|
|
|
|
|
|
+ admin: dict = Depends(require_admin)
|
|
|
):
|
|
):
|
|
|
- payload = auth_utils.decode_token(token)
|
|
|
|
|
- if not payload or payload.get("role") != 'admin':
|
|
|
|
|
- raise HTTPException(status_code=403, detail="Admin role required")
|
|
|
|
|
|
|
|
|
|
unique_filename = f"{uuid.uuid4()}{os.path.splitext(file.filename)[1]}"
|
|
unique_filename = f"{uuid.uuid4()}{os.path.splitext(file.filename)[1]}"
|
|
|
file_path = os.path.join(config.UPLOAD_DIR, unique_filename)
|
|
file_path = os.path.join(config.UPLOAD_DIR, unique_filename)
|
|
@@ -394,6 +400,11 @@ async def admin_attach_file(
|
|
|
query = "INSERT INTO order_files (order_id, filename, file_path, file_size, quantity, file_hash, print_time, filament_g, preview_path) VALUES (%s, %s, %s, %s, 1, %s, %s, %s, %s)"
|
|
query = "INSERT INTO order_files (order_id, filename, file_path, file_size, quantity, file_hash, print_time, filament_g, preview_path) VALUES (%s, %s, %s, %s, 1, %s, %s, %s, %s)"
|
|
|
f_id = db.execute_commit(query, (order_id, file.filename, db_file_path, file.size, sha256_hash.hexdigest(), print_time, filament_g, db_preview_path))
|
|
f_id = db.execute_commit(query, (order_id, file.filename, db_file_path, file.size, sha256_hash.hexdigest(), print_time, filament_g, db_preview_path))
|
|
|
|
|
|
|
|
|
|
+ # NOTIFY USER VIA WEBSOCKET
|
|
|
|
|
+ order_info = db.execute_query("SELECT user_id FROM orders WHERE id = %s", (order_id,))
|
|
|
|
|
+ if order_info:
|
|
|
|
|
+ await global_manager.notify_order_update(order_info[0]['user_id'], order_id)
|
|
|
|
|
+
|
|
|
return {"file_id": f_id, "filename": file.filename, "preview_path": db_preview_path, "filament_g": filament_g, "print_time": print_time}
|
|
return {"file_id": f_id, "filename": file.filename, "preview_path": db_preview_path, "filament_g": filament_g, "print_time": print_time}
|
|
|
|
|
|
|
|
@router.delete("/{order_id}/files/{file_id}")
|
|
@router.delete("/{order_id}/files/{file_id}")
|
|
@@ -401,11 +412,8 @@ async def admin_delete_file(
|
|
|
order_id: int,
|
|
order_id: int,
|
|
|
file_id: int,
|
|
file_id: int,
|
|
|
request: Request,
|
|
request: Request,
|
|
|
- token: str = Depends(auth_utils.oauth2_scheme)
|
|
|
|
|
|
|
+ admin: dict = Depends(require_admin)
|
|
|
):
|
|
):
|
|
|
- payload = auth_utils.decode_token(token)
|
|
|
|
|
- if not payload or payload.get("role") != 'admin':
|
|
|
|
|
- raise HTTPException(status_code=403, detail="Admin role required")
|
|
|
|
|
|
|
|
|
|
file_record = db.execute_query("SELECT file_path, preview_path FROM order_files WHERE id = %s AND order_id = %s", (file_id, order_id))
|
|
file_record = db.execute_query("SELECT file_path, preview_path FROM order_files WHERE id = %s AND order_id = %s", (file_id, order_id))
|
|
|
if not file_record:
|
|
if not file_record:
|
|
@@ -424,13 +432,19 @@ async def admin_delete_file(
|
|
|
|
|
|
|
|
# LOG ACTION
|
|
# LOG ACTION
|
|
|
await audit_service.log(
|
|
await audit_service.log(
|
|
|
- user_id=payload.get("id"),
|
|
|
|
|
|
|
+ user_id=admin.get("id"),
|
|
|
action="delete_order_file",
|
|
action="delete_order_file",
|
|
|
target_type="order",
|
|
target_type="order",
|
|
|
target_id=order_id,
|
|
target_id=order_id,
|
|
|
details={"file_id": file_id},
|
|
details={"file_id": file_id},
|
|
|
request=request
|
|
request=request
|
|
|
)
|
|
)
|
|
|
|
|
+
|
|
|
|
|
+ # NOTIFY USER VIA WEBSOCKET
|
|
|
|
|
+ order_info = db.execute_query("SELECT user_id FROM orders WHERE id = %s", (order_id,))
|
|
|
|
|
+ if order_info:
|
|
|
|
|
+ await global_manager.notify_order_update(order_info[0]['user_id'], order_id)
|
|
|
|
|
+
|
|
|
return {"status": "success"}
|
|
return {"status": "success"}
|
|
|
|
|
|
|
|
class OrderItemSchema(BaseModel):
|
|
class OrderItemSchema(BaseModel):
|
|
@@ -444,11 +458,7 @@ async def get_order_items(order_id: int):
|
|
|
return items
|
|
return items
|
|
|
|
|
|
|
|
@router.put("/{order_id}/items")
|
|
@router.put("/{order_id}/items")
|
|
|
-async def update_order_items(order_id: int, items: List[OrderItemSchema], token: str = Depends(auth_utils.oauth2_scheme)):
|
|
|
|
|
- payload = auth_utils.decode_token(token)
|
|
|
|
|
- if not payload or payload.get("role") != 'admin':
|
|
|
|
|
- raise HTTPException(status_code=403, detail="Admin role required")
|
|
|
|
|
-
|
|
|
|
|
|
|
+async def update_order_items(order_id: int, items: List[OrderItemSchema], admin: dict = Depends(require_admin)):
|
|
|
db.execute_commit("DELETE FROM order_items WHERE order_id = %s", (order_id,))
|
|
db.execute_commit("DELETE FROM order_items WHERE order_id = %s", (order_id,))
|
|
|
|
|
|
|
|
total_order_price = 0
|
|
total_order_price = 0
|
|
@@ -463,17 +473,23 @@ async def update_order_items(order_id: int, items: List[OrderItemSchema], token:
|
|
|
# Sync main order total_price
|
|
# Sync main order total_price
|
|
|
db.execute_commit("UPDATE orders SET total_price = %s WHERE id = %s", (total_order_price, order_id))
|
|
db.execute_commit("UPDATE orders SET total_price = %s WHERE id = %s", (total_order_price, order_id))
|
|
|
|
|
|
|
|
|
|
+ # NOTIFY USER VIA WEBSOCKET
|
|
|
|
|
+ order_info = db.execute_query("SELECT user_id FROM orders WHERE id = %s", (order_id,))
|
|
|
|
|
+ if order_info:
|
|
|
|
|
+ await global_manager.notify_order_update(order_info[0]['user_id'], order_id)
|
|
|
|
|
+
|
|
|
return {"status": "success", "total_price": total_order_price}
|
|
return {"status": "success", "total_price": total_order_price}
|
|
|
|
|
|
|
|
@router.delete("/{order_id}/admin")
|
|
@router.delete("/{order_id}/admin")
|
|
|
async def delete_order_admin(
|
|
async def delete_order_admin(
|
|
|
order_id: int,
|
|
order_id: int,
|
|
|
request: Request,
|
|
request: Request,
|
|
|
- token: str = Depends(auth_utils.oauth2_scheme)
|
|
|
|
|
|
|
+ admin: dict = Depends(require_admin)
|
|
|
):
|
|
):
|
|
|
- payload = auth_utils.decode_token(token)
|
|
|
|
|
- if not payload or payload.get("role") != 'admin':
|
|
|
|
|
- raise HTTPException(status_code=403, detail="Admin role required")
|
|
|
|
|
|
|
+ # Fetch user_id before deletion to notify later
|
|
|
|
|
+ order_info = db.execute_query("SELECT user_id FROM orders WHERE id = %s", (order_id,))
|
|
|
|
|
+ if not order_info: raise HTTPException(status_code=404, detail="Order not found")
|
|
|
|
|
+ customer_id = order_info[0]['user_id']
|
|
|
|
|
|
|
|
# 1. Find all related files to delete from disk
|
|
# 1. Find all related files to delete from disk
|
|
|
files = db.execute_query("SELECT file_path, preview_path FROM order_files WHERE order_id = %s", (order_id,))
|
|
files = db.execute_query("SELECT file_path, preview_path FROM order_files WHERE order_id = %s", (order_id,))
|
|
@@ -512,13 +528,17 @@ async def delete_order_admin(
|
|
|
|
|
|
|
|
# LOG ACTION
|
|
# LOG ACTION
|
|
|
await audit_service.log(
|
|
await audit_service.log(
|
|
|
- user_id=payload.get("id"),
|
|
|
|
|
|
|
+ user_id=admin.get("id"),
|
|
|
action="delete_order_entirely",
|
|
action="delete_order_entirely",
|
|
|
target_type="order",
|
|
target_type="order",
|
|
|
target_id=order_id,
|
|
target_id=order_id,
|
|
|
details={"order_id": order_id},
|
|
details={"order_id": order_id},
|
|
|
request=request
|
|
request=request
|
|
|
)
|
|
)
|
|
|
|
|
+
|
|
|
|
|
+ # NOTIFY USER VIA WEBSOCKET
|
|
|
|
|
+ await global_manager.notify_order_update(customer_id, order_id)
|
|
|
|
|
+
|
|
|
return {"status": "success", "message": f"Order {order_id} deleted entirely"}
|
|
return {"status": "success", "message": f"Order {order_id} deleted entirely"}
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
print(f"Failed to delete order {order_id}: {e}")
|
|
print(f"Failed to delete order {order_id}: {e}")
|