@@ -245,7 +245,8 @@ def list_rooms():
245245 pipeline .append ({"$match" : match })
246246 pipeline .append ({"$addFields" : {"_id_str" : {"$toString" : "$_id" }}})
247247 pipeline .append ({"$lookup" : {"from" : shares_coll .name , "localField" : "_id_str" , "foreignField" : "roomId" , "as" : "members" }})
248- pipeline .append ({"$addFields" : {"memberCount" : {"$size" : {"$ifNull" : ["$members" , []]}}}})
248+ # memberCount = 1 (owner) + number of shared members
249+ pipeline .append ({"$addFields" : {"memberCount" : {"$add" : [1 , {"$size" : {"$ifNull" : ["$members" , []]}}]}}})
249250
250251 sort_map = {
251252 'updatedAt' : ('updatedAt' , - 1 ),
@@ -325,7 +326,8 @@ def list_rooms():
325326 shared = list (rooms_coll .find ({"_id" : {"$in" : oids }, "archived" : {"$ne" : True }}))
326327 def _fmt_single (r ):
327328 rid = str (r ["_id" ])
328- member_count = shares_coll .count_documents ({"roomId" : rid })
329+ # Count includes owner (1) + all shared members
330+ member_count = 1 + shares_coll .count_documents ({"roomId" : rid })
329331 my_role = None
330332 try :
331333 if str (r .get ("ownerId" )) == claims ["sub" ]:
@@ -407,9 +409,10 @@ def suggest_rooms():
407409 for r in cursor :
408410 rid = str (r .get ("_id" ))
409411 try :
410- member_count = shares_coll .count_documents ({"roomId" : rid })
412+ # Count includes owner (1) + all shared members
413+ member_count = 1 + shares_coll .count_documents ({"roomId" : rid })
411414 except Exception :
412- member_count = 0
415+ member_count = 1 # At least the owner
413416 rooms .append ({
414417 "id" : rid ,
415418 "name" : r .get ("name" ),
@@ -498,10 +501,29 @@ def share_room(roomId):
498501 results ["errors" ].append ({"username" : uname , "error" : "user not found" , "suggestions" : suggs })
499502 continue
500503 uid = str (user ["_id" ])
504+
505+ if uid == str (room .get ("ownerId" )) or uname == room .get ("ownerName" ):
506+ results ["errors" ].append ({"username" : uname , "error" : "cannot invite room owner (already has full access)" })
507+ continue
508+
509+ if uid == claims ["sub" ] or uname == claims .get ("username" ):
510+ results ["errors" ].append ({"username" : uname , "error" : "cannot invite yourself" })
511+ continue
512+
501513 existing = shares_coll .find_one ({"roomId" : str (room ["_id" ]), "userId" : uid })
502514 if existing :
503515 results ["errors" ].append ({"username" : uname , "error" : "already shared with this user" })
504516 continue
517+
518+ if room .get ("type" ) in ("private" , "secure" ):
519+ pending_invite = invites_coll .find_one ({
520+ "roomId" : str (room ["_id" ]),
521+ "invitedUserId" : uid ,
522+ "status" : "pending"
523+ })
524+ if pending_invite :
525+ results ["errors" ].append ({"username" : uname , "error" : "already has a pending invite to this room" })
526+ continue
505527
506528 if room .get ("type" ) in ("private" , "secure" ):
507529 invite = {
@@ -2063,9 +2085,21 @@ def update_permissions(roomId):
20632085 target_user_id = data .get ("userId" )
20642086 if not target_user_id :
20652087 return jsonify ({"status" :"error" ,"message" :"Missing userId" }), 400
2088+
2089+ # Validate: cannot remove/modify yourself
2090+ if target_user_id == claims ["sub" ]:
2091+ return jsonify ({"status" :"error" ,"message" :"Cannot modify your own role. Use transfer ownership or leave room instead." }), 400
2092+
20662093 if "role" not in data or data .get ("role" ) is None :
2094+ # Removing user
20672095 if target_user_id == room .get ("ownerId" ):
20682096 return jsonify ({"status" :"error" ,"message" :"Cannot remove owner" }), 400
2097+
2098+ # Check if user is actually a member
2099+ existing_share = shares_coll .find_one ({"roomId" : str (room ["_id" ]), "userId" : target_user_id })
2100+ if not existing_share :
2101+ return jsonify ({"status" :"error" ,"message" :"User is not a member of this room" }), 400
2102+
20692103 shares_coll .delete_one ({"roomId" : str (room ["_id" ]), "userId" : target_user_id })
20702104 try :
20712105 if _notification_allowed_for (target_user_id , 'removed' ):
@@ -2116,13 +2150,24 @@ def update_permissions(roomId):
21162150 except Exception :
21172151 pass
21182152 return jsonify ({"status" :"ok" ,"removed" : target_user_id })
2153+
21192154 role = (data .get ("role" ) or "" ).lower ()
21202155 if role not in ("admin" ,"editor" ,"viewer" ):
21212156 return jsonify ({"status" :"error" ,"message" :"Invalid role" }), 400
21222157 if target_user_id == room .get ("ownerId" ):
2123- return jsonify ({"status" :"error" ,"message" :"Cannot change owner role" }), 400
2158+ return jsonify ({"status" :"error" ,"message" :"Cannot change owner role. Use transfer ownership instead. " }), 400
21242159 if role == "admin" and caller_role != "owner" :
2125- return jsonify ({"status" :"error" ,"message" :"Only owner may invite admin role" }), 403
2160+ return jsonify ({"status" :"error" ,"message" :"Only owner may assign admin role" }), 403
2161+ if role == "owner" :
2162+ return jsonify ({"status" :"error" ,"message" :"Cannot assign owner role. Use transfer ownership endpoint instead." }), 400
2163+
2164+ existing_share = shares_coll .find_one ({"roomId" : str (room ["_id" ]), "userId" : target_user_id })
2165+ if not existing_share :
2166+ return jsonify ({"status" :"error" ,"message" :"User is not a member of this room" }), 400
2167+
2168+ if existing_share .get ("role" ) == role :
2169+ return jsonify ({"status" :"ok" ,"message" :"User already has this role" ,"userId" : target_user_id , "role" : role })
2170+
21262171 shares_coll .update_one ({"roomId" : str (room ["_id" ]), "userId" : target_user_id }, {"$set" : {"role" : role }}, upsert = False )
21272172 try :
21282173 if _notification_allowed_for (target_user_id , 'role_changed' ):
@@ -2325,27 +2370,48 @@ def transfer_ownership(roomId):
23252370
23262371 data = request .get_json () or {}
23272372 target_username = data .get ("username" )
2373+
2374+ if target_username == claims .get ("username" ) or target_username == room .get ("ownerName" ):
2375+ return jsonify ({"status" :"error" ,"message" :"You are already the owner of this room" }), 400
2376+
23282377 target_user = users_coll .find_one ({"username" : target_username })
23292378 if not target_user :
23302379 return jsonify ({"status" :"error" ,"message" :"Target user not found" }), 404
2331- member = shares_coll .find_one ({"roomId" : str (room ["_id" ]), "userId" : str (target_user ["_id" ])})
2380+
2381+ target_user_id = str (target_user ["_id" ])
2382+
2383+ member = shares_coll .find_one ({"roomId" : str (room ["_id" ]), "userId" : target_user_id })
23322384 if not member :
2385+ pending_invite = invites_coll .find_one ({
2386+ "roomId" : str (room ["_id" ]),
2387+ "invitedUserId" : target_user_id ,
2388+ "status" : "pending"
2389+ })
2390+ if pending_invite :
2391+ return jsonify ({"status" :"error" ,"message" :"Cannot transfer ownership to user with pending invite. They must accept the invite first." }), 400
23332392 return jsonify ({"status" :"error" ,"message" :"Target user is not a member of the room" }), 400
2334- rooms_coll .update_one ({"_id" : ObjectId (roomId )}, {"$set" : {"ownerId" : str (target_user ["_id" ]), "ownerName" : target_user ["username" ], "updatedAt" : datetime .utcnow ()}})
2393+
2394+ rooms_coll .update_one ({"_id" : ObjectId (roomId )}, {"$set" : {"ownerId" : target_user_id , "ownerName" : target_user ["username" ], "updatedAt" : datetime .utcnow ()}})
23352395
23362396 # Remove the new owner from shares_coll (owners don't have share records)
2337- shares_coll .delete_one ({"roomId" : str (room ["_id" ]), "userId" : str ( target_user [ "_id" ]) })
2397+ shares_coll .delete_one ({"roomId" : str (room ["_id" ]), "userId" : target_user_id })
23382398
2339- # Add the old owner to shares_coll as editor (create new share record for former owner)
2340- shares_coll .insert_one ({
2341- "roomId" : str (room ["_id" ]),
2342- "userId" : claims ["sub" ],
2343- "username" : claims .get ("username" ),
2344- "role" : "editor" ,
2345- "createdAt" : datetime .utcnow ()
2346- })
2399+ # Ensure the old owner is downgraded to editor in shares_coll
2400+ shares_coll .update_one (
2401+ {"roomId" : str (room ["_id" ]), "userId" : claims ["sub" ]},
2402+ {"$set" : {
2403+ "roomId" : str (room ["_id" ]),
2404+ "userId" : claims ["sub" ],
2405+ "username" : claims .get ("username" ),
2406+ "role" : "editor" ,
2407+ "updatedAt" : datetime .utcnow ()
2408+ }, "$setOnInsert" : {
2409+ "createdAt" : datetime .utcnow ()
2410+ }},
2411+ upsert = True
2412+ )
23472413 notifications_coll .insert_one ({
2348- "userId" : str ( target_user [ "_id" ]) ,
2414+ "userId" : target_user_id ,
23492415 "type" : "ownership_transfer" ,
23502416 "message" : f"You are now the owner of room '{ room .get ('name' )} '" ,
23512417 "link" : f"/rooms/{ roomId } " ,
@@ -2362,7 +2428,7 @@ def transfer_ownership(roomId):
23622428 })
23632429 # Send real-time events for ownership transfer
23642430 try :
2365- push_to_user (str ( target_user [ "_id" ]) , 'role_changed' , {
2431+ push_to_user (target_user_id , 'role_changed' , {
23662432 'roomId' : roomId ,
23672433 'roomName' : room .get ('name' ),
23682434 'newRole' : 'owner' ,
@@ -2387,6 +2453,14 @@ def transfer_ownership(roomId):
23872453 })
23882454 except Exception :
23892455 pass
2456+ try :
2457+ # Also emit member_role_changed to trigger refreshMembers in RoomSettings
2458+ push_to_room (roomId , 'member_role_changed' , {
2459+ 'roomId' : roomId ,
2460+ 'message' : f"Ownership transferred to { target_user ['username' ]} "
2461+ })
2462+ except Exception :
2463+ pass
23902464 return jsonify ({"status" :"ok" })
23912465
23922466@rooms_bp .route ("/rooms/<roomId>/leave" , methods = ["POST" ])
0 commit comments