Skip to content

Commit fc79ce4

Browse files
fix: Resolve merge conflicts in api_controller.ex - integrate atomic operations with upstream improvements
1 parent 828f97d commit fc79ce4

File tree

1 file changed

+33
-54
lines changed

1 file changed

+33
-54
lines changed

copi.owasp.org/lib/copi_web/controllers/api_controller.ex

Lines changed: 33 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,71 +7,50 @@ defmodule CopiWeb.ApiController do
77
def play_card(conn, %{"game_id" => game_id, "player_id" => player_id, "dealt_card_id" => dealt_card_id}) do
88
with {:ok, game} <- Game.find(game_id) do
99
player = Enum.find(game.players, fn player -> player.id == player_id end)
10-
11-
if player do
12-
dealt_card = Enum.find(player.dealt_cards, fn dealt_card -> Integer.to_string(dealt_card.id) == dealt_card_id end)
10+
dealt_card = Enum.find(player.dealt_cards, fn dealt_card -> Integer.to_string(dealt_card.id) == dealt_card_id end)
1311

14-
if dealt_card do
15-
current_round = game.rounds_played + 1
12+
if player && dealt_card do
13+
current_round = game.rounds_played + 1
14+
15+
cond do
16+
dealt_card.played_in_round ->
17+
conn |> put_status(:not_acceptable) |> json(%{"error" => "Card already played"})
18+
Enum.find(player.dealt_cards, fn dealt_card -> dealt_card.played_in_round == current_round end) ->
19+
conn |> put_status(:forbidden) |> json(%{"error" => "Player already played a card in this round"})
20+
true ->
21+
# Atomic update to prevent race conditions
22+
case play_card_atomically(dealt_card, current_round) do
23+
{:ok, updated_card} ->
24+
with {:ok, updated_game} <- Game.find(game.id) do
25+
CopiWeb.Endpoint.broadcast(topic(game.id), "game:updated", updated_game)
26+
else
27+
{:error, _reason} ->
28+
conn |> put_status(:internal_server_error) |> json(%{"error" => "Could not find updated game"})
29+
end
1630

17-
# Atomic update to prevent race conditions and enforce one-card-per-round invariant
18-
case play_card_atomically(dealt_card, player.id, current_round) do
19-
{:ok, updated_card} ->
20-
with {:ok, updated_game} <- Game.find(game.id) do
21-
CopiWeb.Endpoint.broadcast(topic(game.id), "game:updated", updated_game)
2231
conn |> json(%{"id" => updated_card.id})
23-
else
24-
{:error, _reason} ->
25-
conn |> put_status(:internal_server_error) |> json(%{"error" => "Could not find updated game"})
26-
end
27-
{:error, :already_played} ->
28-
conn |> put_status(:conflict) |> json(%{"error" => "Card was already played by another request"})
29-
{:error, :player_already_played} ->
30-
conn |> put_status(:forbidden) |> json(%{"error" => "Player already played a card in this round"})
31-
{:error, _changeset} ->
32-
conn |> put_status(:internal_server_error) |> json(%{"error" => "Could not update card"})
33-
end
34-
else
35-
conn |> put_status(:not_found) |> json(%{"error" => "Could not find dealt card"})
32+
{:error, :already_played} ->
33+
conn |> put_status(:conflict) |> json(%{"error" => "Card was already played by another request"})
34+
{:error, _changeset} ->
35+
conn |> put_status(:internal_server_error) |> json(%{"error" => "Could not update dealt card"})
36+
end
3637
end
3738
else
38-
conn |> put_status(:not_found) |> json(%{"error" => "Could not find player"})
39+
conn |> put_status(:not_found) |> json(%{"error" => "Player not found in this game"})
3940
end
4041
else
4142
{:error, _reason} -> conn |> put_status(:not_found) |> json(%{"error" => "Could not find game"})
4243
end
4344
end
4445

45-
# Atomic card play operation to prevent race conditions and enforce one-card-per-round invariant
46-
defp play_card_atomically(dealt_card, player_id, current_round) do
47-
# Use database transaction to ensure atomicity and prevent race conditions
48-
result = Repo.transaction(fn ->
49-
# First, try to lock player's cards for this round by checking existing plays
50-
existing_cards = Repo.all(
51-
from(dc in Copi.Cornucopia.DealtCard,
52-
where: dc.player_id == ^player_id and dc.played_in_round == ^current_round)
53-
)
54-
55-
if length(existing_cards) > 0 do
56-
{:error, :player_already_played}
57-
else
58-
# Now atomically update the card with played_in_round
59-
from(dc in Copi.Cornucopia.DealtCard,
60-
where: dc.id == ^dealt_card.id and is_nil(dc.played_in_round))
61-
|> Repo.update_all([set: [played_in_round: current_round]], returning: true)
62-
|> case do
63-
{1, [updated_card]} -> {:ok, updated_card}
64-
{0, []} -> {:error, :already_played}
65-
end
66-
end
67-
end)
68-
69-
# Unwrap transaction result to return flat tuple
70-
case result do
71-
{:ok, {:ok, val}} -> {:ok, val}
72-
{:ok, {:error, reason}} -> {:error, reason}
73-
{:error, reason} -> {:error, reason}
74-
other -> other
46+
# Atomic card play operation to prevent race conditions
47+
defp play_card_atomically(dealt_card, current_round) do
48+
# Use Ecto's atomic operations to prevent race conditions
49+
from(dc in Copi.Cornucopia.DealtCard, where: dc.id == ^dealt_card.id and is_nil(dc.played_in_round))
50+
|> Repo.update_all([set: [played_in_round: current_round]], returning: true)
51+
|> case do
52+
{1, [updated_card]} -> {:ok, updated_card}
53+
{0, []} -> {:error, :already_played}
7554
end
7655
end
7756
def topic(game_id) do

0 commit comments

Comments
 (0)