Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,14 @@ def handle_all_exceptions(e):
from api_v1.invites import invites_v1_bp
from api_v1.notifications import notifications_v1_bp
from api_v1.users import users_v1_bp
from routes.stamps import stamps_bp

app.register_blueprint(auth_v1_bp)
app.register_blueprint(rooms_v1_bp)
app.register_blueprint(invites_v1_bp)
app.register_blueprint(notifications_v1_bp)
app.register_blueprint(users_v1_bp)
app.register_blueprint(stamps_bp, url_prefix='/api')

# Frontend serving must be last to avoid route conflicts
app.register_blueprint(frontend_bp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import asyncio
import json
import logging
import ssl
from typing import Optional

import httpx
Expand Down Expand Up @@ -114,7 +115,7 @@ async def fetch_and_sync_initial_blocks(self):
batch_ranges = []
blocks_fetched = True

async with httpx.AsyncClient() as client:
async with httpx.AsyncClient(verify=False) as client:
while blocks_fetched:
min_seq = self.current_block_number + 1
max_seq = min_seq + batch_size - 1
Expand Down Expand Up @@ -158,7 +159,7 @@ async def fetch_and_sync_initial_blocks(self):
async def fetch_and_sync_batch(self, min_seq: int, max_seq: int, semaphore: asyncio.Semaphore):
async with semaphore:
try:
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient(verify=False) as client:
url = f"{self.http_endpoint}/{min_seq}/{max_seq}"
logger.info(f"Fetching blocks from {min_seq} to {max_seq}")
response = await client.get(url)
Expand Down Expand Up @@ -217,7 +218,7 @@ async def fetch_and_sync_new_blocks(self):
batch_ranges = []
blocks_fetched = True

async with httpx.AsyncClient() as client:
async with httpx.AsyncClient(verify=False) as client:
while blocks_fetched:
min_seq = self.current_block_number + 1
max_seq = min_seq + batch_size - 1
Expand Down Expand Up @@ -251,7 +252,12 @@ async def fetch_and_sync_new_blocks(self):

async def connect_websocket(self):
try:
async with websockets.connect(self.ws_endpoint) as websocket:
# Create SSL context that doesn't verify certificates
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE

async with websockets.connect(self.ws_endpoint, ssl=ssl_context) as websocket:
logger.info(f"Connected to WebSocket: {self.ws_endpoint}")
self.reconnect_attempts = 0
self.emit('connected')
Expand Down Expand Up @@ -318,4 +324,4 @@ async def close(self):
except Exception as e:
logger.error("Error closing connections:")
logger.error(e)
raise ResilientPythonCacheError(str(e)) from e
raise ResilientPythonCacheError(str(e)) from e
40 changes: 40 additions & 0 deletions backend/middleware/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,46 @@

import re
from typing import Tuple
from functools import wraps
from flask import request, jsonify


def validate_json(required_fields=None):
"""
Decorator to validate JSON request data.

Args:
required_fields (list): List of required field names

Returns:
Decorator function that validates request JSON
"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# Check if request has JSON data
if not request.is_json:
return jsonify({'error': 'Request must be JSON'}), 400

data = request.get_json()
if not data:
return jsonify({'error': 'Request body cannot be empty'}), 400

# Check required fields
if required_fields:
missing_fields = []
for field in required_fields:
if field not in data or data[field] is None or data[field] == '':
missing_fields.append(field)

if missing_fields:
return jsonify({
'error': f'Missing required fields: {", ".join(missing_fields)}'
}), 400

return f(*args, **kwargs)
return decorated_function
return decorator


def validate_username(value: str) -> Tuple[bool, str]:
Expand Down
59 changes: 57 additions & 2 deletions backend/routes/rooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,10 +633,31 @@ def post_stroke(roomId):

payload = g.validated_data
stroke = payload["stroke"]

# DEBUG: Log the incoming stroke object to inspect brush metadata
try:
brush_type = stroke.get("brushType", "not found")
brush_params = stroke.get("brushParams", "not found")
logger.warning(f"POST STROKE DEBUG - roomId={roomId}, brushType={brush_type}, brushParams={brush_params}")
logger.warning(f"POST STROKE DEBUG - Full stroke object: {json.dumps(stroke, default=str)}")
except Exception as e:
logger.error(f"POST STROKE DEBUG - Error logging stroke: {e}")

stroke["roomId"] = roomId
stroke["user"] = claims["username"]
stroke["ts"] = int(time.time() * 1000)

# Ensure brush metadata is preserved in stroke object
# Check if brush metadata is in payload root and copy to stroke
if "brushType" in payload and "brushType" not in stroke:
stroke["brushType"] = payload["brushType"]

if "brushParams" in payload and "brushParams" not in stroke:
stroke["brushParams"] = payload["brushParams"]

if "metadata" in payload and "metadata" not in stroke:
stroke["metadata"] = payload["metadata"]

if "drawingId" in stroke and "id" not in stroke:
stroke["id"] = stroke["drawingId"]
elif "id" not in stroke and "drawingId" not in stroke:
Expand Down Expand Up @@ -695,6 +716,11 @@ def post_stroke(roomId):
rooms_coll.update_one({"_id": room["_id"]}, {"$set": {"updatedAt": datetime.utcnow()}})
else:
asset_data = {"roomId": roomId, "type": "public", "stroke": stroke}

# Debug: Log what we're storing in MongoDB and ResilientDB
logger.warning(f"STORING TO MONGODB: brushType={stroke.get('brushType')}, brushParams={stroke.get('brushParams')}")
logger.warning(f"STORING TO RESILIENTDB: asset_data={json.dumps(asset_data, default=str)[:200]}...")

strokes_coll.insert_one({"roomId": roomId, "ts": stroke["ts"], "stroke": stroke})

rooms_coll.update_one({"_id": room["_id"]}, {"$set": {"updatedAt": datetime.utcnow()}})
Expand Down Expand Up @@ -965,6 +991,14 @@ def get_strokes(roomId):
if (start_ts is not None and (st_ts is None or st_ts < start_ts)) or (end_ts is not None and (st_ts is None or st_ts > end_ts)):
continue

# DEBUG: Log the retrieved stroke object to inspect brush metadata
try:
brush_type = stroke_data.get("brushType", "not found")
brush_params = stroke_data.get("brushParams", "not found")
logger.warning(f"GET STROKE DEBUG (private/secure) - roomId={roomId}, strokeId={stroke_id}, brushType={brush_type}, brushParams={brush_params}")
except Exception as e:
logger.error(f"GET STROKE DEBUG (private/secure) - Error logging stroke: {e}")

out.append(stroke_data)
if stroke_id:
seen_stroke_ids.add(stroke_id)
Expand All @@ -973,6 +1007,12 @@ def get_strokes(roomId):

out.sort(key=lambda s: s.get('ts') or s.get('timestamp') or 0)

# Debug: Log first few strokes being returned
if out:
logger.warning(f"GET strokes debug (private/secure) - returning {len(out)} strokes")
for i, stroke in enumerate(out[:2]):
logger.warning(f"Stroke {i}: {json.dumps(stroke, indent=2)}")

return jsonify({"status":"ok","strokes": out})
else:
filtered_strokes = []
Expand Down Expand Up @@ -1048,6 +1088,14 @@ def get_strokes(roomId):
stroke_data['ts'] = st_ts
stroke_data['timestamp'] = st_ts

# DEBUG: Log the retrieved stroke object to inspect brush metadata
try:
brush_type = stroke_data.get("brushType", "not found")
brush_params = stroke_data.get("brushParams", "not found")
logger.warning(f"GET STROKE DEBUG (public) - roomId={roomId}, strokeId={stroke_id}, brushType={brush_type}, brushParams={brush_params}")
except Exception as e:
logger.error(f"GET STROKE DEBUG (public) - Error logging stroke: {e}")

filtered_strokes.append(stroke_data)
if stroke_id:
seen_stroke_ids.add(stroke_id)
Expand Down Expand Up @@ -1089,6 +1137,13 @@ def get_strokes(roomId):
logger.exception("rooms.get_strokes: Mongo history supplement failed for room %s", roomId)

filtered_strokes.sort(key=lambda s: s.get('ts') or s.get('timestamp') or 0)

# Debug: Log first few strokes being returned
if filtered_strokes:
logger.info(f"GET strokes debug - returning {len(filtered_strokes)} strokes")
for i, stroke in enumerate(filtered_strokes[:2]):
logger.info(f"Stroke {i}: {json.dumps(stroke, indent=2)}")

return jsonify({"status":"ok","strokes": filtered_strokes})

@rooms_bp.route("/rooms/<roomId>/undo", methods=["POST"])
Expand Down Expand Up @@ -1569,7 +1624,7 @@ def get_room_members(roomId):
@require_room_access(room_id_param="roomId")
@validate_request_data({
"userId": {"validator": validate_member_id, "required": True},
"role": {"validator": validate_optional_string(), "required": False}
"role": {"validator": validate_optional_string, "required": False}
})
def update_permissions(roomId):
"""
Expand Down Expand Up @@ -1641,7 +1696,7 @@ def update_permissions(roomId):
if target_user_id == room.get("ownerId"):
return jsonify({"status":"error","message":"Cannot change owner role"}), 400
if role == "admin" and caller_role != "owner":
return jsonify({"status":"error","message":"Only owner may assign admin role"}), 403
return jsonify({"status":"error","message":"Only owner may invite admin role"}), 403
shares_coll.update_one({"roomId": str(room["_id"]), "userId": target_user_id}, {"$set": {"role": role}}, upsert=False)
try:
if _notification_allowed_for(target_user_id, 'role_changed'):
Expand Down
Loading
Loading