@@ -1546,6 +1546,97 @@ def get_undo_redo_status(roomId):
15461546 "redo_count" : redo_count
15471547 })
15481548
1549+ @rooms_bp .route ("/rooms/<roomId>/mark_undone" , methods = ["POST" ])
1550+ @require_auth
1551+ @require_room_access (room_id_param = "roomId" )
1552+ @limiter .limit (f"{ RATE_LIMIT_UNDO_REDO_MINUTE } /minute" )
1553+ def mark_strokes_undone (roomId ):
1554+ """
1555+ Mark specific stroke IDs as undone without requiring them to be in the undo stack.
1556+ Useful for clearing filters after Redis flush.
1557+
1558+ Server-side enforcement:
1559+ - Authentication required via @require_auth
1560+ - Room access required via @require_room_access
1561+ - Viewer role cannot mark undone (read-only)
1562+ """
1563+ logger .info (f"Mark undone request for room { roomId } " )
1564+
1565+ user = g .current_user
1566+ claims = g .token_claims
1567+ room = g .current_room
1568+ user_id = claims ['sub' ]
1569+
1570+ try :
1571+ share = shares_coll .find_one ({"roomId" : roomId , "$or" : [{"userId" : user_id }, {"username" : user_id }]})
1572+ if share and share .get ('role' ) == 'viewer' :
1573+ return jsonify ({"status" :"error" ,"message" :"Forbidden: viewers cannot mark strokes as undone" }), 403
1574+ except Exception :
1575+ pass
1576+
1577+ # Get stroke IDs from request
1578+ data = request .get_json () or {}
1579+ stroke_ids = data .get ('strokeIds' , [])
1580+
1581+ if not stroke_ids or not isinstance (stroke_ids , list ):
1582+ return jsonify ({"status" :"error" ,"message" :"strokeIds array required" }), 400
1583+
1584+ key_base = f"room:{ roomId } :{ user_id } "
1585+ marked_count = 0
1586+
1587+ try :
1588+ # Mark each stroke as undone in Redis
1589+ for stroke_id in stroke_ids :
1590+ if stroke_id :
1591+ redis_client .sadd (f"{ key_base } :undone_strokes" , str (stroke_id ))
1592+ marked_count += 1
1593+
1594+ # Create and persist undo marker for each stroke
1595+ ts = int (time .time () * 1000 )
1596+ marker_rec = {
1597+ "type" : "undo_marker" ,
1598+ "roomId" : roomId ,
1599+ "user" : user_id ,
1600+ "strokeId" : str (stroke_id ),
1601+ "ts" : ts ,
1602+ "undone" : True
1603+ }
1604+
1605+ try :
1606+ marker_asset = {"data" : marker_rec }
1607+ payload = {
1608+ "operation" : "CREATE" , "amount" : 1 ,
1609+ "signerPublicKey" : SIGNER_PUBLIC_KEY ,
1610+ "signerPrivateKey" : SIGNER_PRIVATE_KEY ,
1611+ "recipientPublicKey" : RECIPIENT_PUBLIC_KEY ,
1612+ "asset" : marker_asset
1613+ }
1614+ strokes_coll .insert_one ({"asset" : marker_asset })
1615+ commit_transaction_via_graphql (payload )
1616+ logger .info (f"Persisted undo marker for stroke { stroke_id } " )
1617+ except Exception as e :
1618+ logger .exception (f"Failed to persist undo marker for stroke { stroke_id } : { e } " )
1619+ # Continue with other strokes even if one fails
1620+
1621+ # Broadcast event to other users
1622+ push_to_room (roomId , "strokes_marked_undone" , {
1623+ "roomId" : roomId ,
1624+ "strokeIds" : stroke_ids ,
1625+ "user" : claims .get ("username" , "unknown" ),
1626+ "timestamp" : int (time .time () * 1000 )
1627+ })
1628+
1629+ logger .info (f"Marked { marked_count } strokes as undone" )
1630+ return jsonify ({
1631+ "status" :"ok" ,
1632+ "marked_count" : marked_count ,
1633+ "stroke_ids" : stroke_ids
1634+ })
1635+
1636+ except Exception as e :
1637+ logger .exception ("An error occurred during mark_strokes_undone" )
1638+ return jsonify ({"status" :"error" ,"message" :f"Failed to mark strokes as undone: { str (e )} " }), 500
1639+
15491640@rooms_bp .route ("/rooms/<roomId>/redo" , methods = ["POST" ])
15501641@require_auth
15511642@require_room_access (room_id_param = "roomId" )
0 commit comments