Merge frontend and prebuild #4

Merged
IgorVolochay merged 35 commits from frontend into prebuild 2026-09-01 11:58:04 +00:00
2 changed files with 83 additions and 28 deletions
Showing only changes of commit 6eb5e45cce - Show all commits
+11 -6
View File
@@ -47,7 +47,7 @@ config = SecurityConfig(
detection_compiler_timeout=2.0,
detection_max_content_length=10000,
detection_preserve_attack_patterns=True,
detection_semantic_threshold=0.7,
detection_semantic_threshold=0.7,
detection_anomaly_threshold=3.0,
detection_slow_pattern_threshold=0.1,
@@ -201,11 +201,17 @@ async def select_choice(
choice_data: SelectChoice,
response: Response,
mongo: MongoWorker = Depends(lambda: mongo_worker),) -> BaseResponse:
check_visited = await mongo.get_visited_cards(choice_data.user_id)
if check_visited.error:
# Verify that the user exists before proceeding.
if not await mongo.check_user(choice_data.user_id):
response.status_code = status.HTTP_404_NOT_FOUND
return check_visited
if choice_data.card_id in check_visited.result.cards_visited:
return BaseResponse(result="User doesn't exist", error=True)
# Atomically mark the card as visited.
# try_mark_visited uses a conditional MongoDB filter ($ne) so that only one
# concurrent request can "win" — eliminating the TOCTOU race condition where
# two parallel requests both pass the visited-check before either writes.
newly_visited = await mongo.try_mark_visited(choice_data.user_id, choice_data.card_id)
if not newly_visited:
response.status_code = status.HTTP_403_FORBIDDEN
return BaseResponse(result="Card already visited!", error=True)
@@ -214,7 +220,6 @@ async def select_choice(
response.status_code = status.HTTP_404_NOT_FOUND
return select_choice_result
await mongo.update_visited_cards(choice_data.user_id, choice_data.card_id)
return BaseResponse(result="Select choice complete!")
+72 -22
View File
@@ -96,6 +96,39 @@ class MongoWorker:
)
return Visited.model_validate(updated)
async def try_mark_visited(self, user_id: int, card_id: int) -> bool:
"""
Atomically marks a card as visited for the user.
Returns True if the card was newly marked (was not visited before).
Returns False if the card was already in the visited set.
Uses a conditional update filter (cards_visited: {$ne: card_id}) so that
only one concurrent request can "win" the mark — eliminating the TOCTOU
race condition between checking and writing.
"""
result = await self.visited_data.update_one(
{"user_id": user_id, "cards_visited": {"$ne": card_id}},
{"$addToSet": {"cards_visited": card_id}},
)
if result.modified_count == 1:
return True
# No document matched: either the visited doc doesn't exist yet,
# or the card is already in the set.
doc = await self.visited_data.find_one({"user_id": user_id}, {"cards_visited": 1})
if doc is None:
# First vote ever for this user — create the visited document.
await self.visited_data.update_one(
{"user_id": user_id},
{"$addToSet": {"cards_visited": card_id}},
upsert=True,
)
return True
# Card is already present in the visited set.
return False
async def get_card(self, card_id: int) -> Optional[Card]:
document = await self.game_data.find_one({"card_id": card_id})
@@ -187,33 +220,38 @@ class MongoWorker:
return BaseResponse(result=True, error=False)
async def check_user_reactions(self, user_id: int, card_id: int) -> BaseResponse:
user_info: User = await self.get_user(user_id)
if card_id in user_info.liked_card_ids:
return BaseResponse(result="Card already liked", error=True)
if card_id in user_info.disliked_card_ids:
return BaseResponse(result="Card already disliked", error=True)
return BaseResponse(result="No reactions", error=False)
async def like_card(self, card_id: int, user_id: int) -> BaseResponse:
if not await self.check_user(user_id):
return BaseResponse(result="User doesn't exist", error=True)
user_reaction = await self.check_user_reactions(user_id, card_id)
if user_reaction.error:
return user_reaction
# Atomically add card_id to liked_card_ids ONLY IF it is not already
# present in liked_card_ids OR disliked_card_ids.
# Using a conditional filter makes this a single, race-condition-free
# test-and-set: if modified_count == 0, another request already won.
user_update = await self.users_data.find_one_and_update(
{
"user_id": user_id,
"liked_card_ids": {"$ne": card_id},
"disliked_card_ids": {"$ne": card_id},
},
{"$addToSet": {"liked_card_ids": card_id}},
projection={"_id": 1},
)
if not user_update:
return BaseResponse(result="Card already liked or disliked", error=True)
updated_card = await self.game_data.find_one_and_update(
{"card_id": card_id},
{"$inc": {"count_likes": 1}},
)
if not updated_card:
# Card doesn't exist — roll back the user update (best effort).
await self.users_data.update_one(
{"user_id": user_id},
{"$pull": {"liked_card_ids": card_id}},
)
return BaseResponse(result="Card doesn't exist", error=True)
await self.users_data.update_one(
{"user_id": user_id},
{"$push": {"liked_card_ids": card_id}},
)
logger.debug("Card liked: card_id={}, user_id={}", card_id, user_id)
return BaseResponse(result=True, error=False)
@@ -221,21 +259,31 @@ class MongoWorker:
if not await self.check_user(user_id):
return BaseResponse(result="User doesn't exist", error=True)
user_reaction = await self.check_user_reactions(user_id, card_id)
if user_reaction.error:
return user_reaction
# Same atomic test-and-set pattern as like_card.
user_update = await self.users_data.find_one_and_update(
{
"user_id": user_id,
"liked_card_ids": {"$ne": card_id},
"disliked_card_ids": {"$ne": card_id},
},
{"$addToSet": {"disliked_card_ids": card_id}},
projection={"_id": 1},
)
if not user_update:
return BaseResponse(result="Card already liked or disliked", error=True)
updated_card = await self.game_data.find_one_and_update(
{"card_id": card_id},
{"$inc": {"count_dislikes": 1}},
)
if not updated_card:
# Card doesn't exist — roll back the user update (best effort).
await self.users_data.update_one(
{"user_id": user_id},
{"$pull": {"disliked_card_ids": card_id}},
)
return BaseResponse(result="Card doesn't exist", error=True)
await self.users_data.update_one(
{"user_id": user_id},
{"$push": {"disliked_card_ids": card_id}},
)
logger.debug("Card disliked: card_id={}, user_id={}", card_id, user_id)
return BaseResponse(result=True, error=False)
@@ -267,6 +315,8 @@ class MongoWorker:
return BaseResponse(result=new_comment)
async def get_comments(self, card_id: int) -> BaseResponse:
if not await self.get_card(card_id):
return BaseResponse(result="Card doesn't exist", error=True)
comments = await self.comments_data.find({"card_id": card_id}).sort("creation_date", -1).to_list(length=None)
comments = [Comment.model_validate(comment) for comment in comments]
return BaseResponse(result=comments)