diff --git a/.env.dev.sample b/.env.dev.sample index b07735197..35dd83e53 100644 --- a/.env.dev.sample +++ b/.env.dev.sample @@ -5,4 +5,6 @@ DB_PORT=5432 DB_NAME=atomic_dev HOST_URL=http://localhost:4000 ASSET_HOST=http://localhost:4000 -FRONTEND_URL=http://localhost:4000 \ No newline at end of file +FRONTEND_URL=http://localhost:4000 +ACTIVITIES_LIMIT_PER_DAY=10 +ANNOUNCEMENTS_LIMIT_PER_DAY=10 diff --git a/config/config.exs b/config/config.exs index 0ded8ad72..ac11180b8 100644 --- a/config/config.exs +++ b/config/config.exs @@ -11,7 +11,10 @@ config :atomic, ecto_repos: [Atomic.Repo], generators: [binary_id: true], name: "Atomic", - timezone: "Europe/Lisbon" + timezone: "Europe/Lisbon", + activities_limit_per_day: String.to_integer(System.get_env("ACTIVITIES_LIMIT_PER_DAY") || "10"), + announcements_limit_per_day: + String.to_integer(System.get_env("ANNOUNCEMENTS_LIMIT_PER_DAY") || "10") # Configures the endpoint config :atomic, AtomicWeb.Endpoint, diff --git a/lib/atomic/activities.ex b/lib/atomic/activities.ex index fc917fe1b..0098e2d4e 100644 --- a/lib/atomic/activities.ex +++ b/lib/atomic/activities.ex @@ -9,6 +9,8 @@ defmodule Atomic.Activities do alias Atomic.Activities.ActivityEnrollment alias Atomic.Activities.Speaker alias Atomic.Feed.Post + alias Atomic.Organizations + alias Atomic.RateLimiter @doc """ Returns the list of activities. @@ -194,32 +196,52 @@ defmodule Atomic.Activities do """ def create_activity_with_post(attrs \\ %{}, after_save \\ &{:ok, &1}) do - Multi.new() - |> Multi.insert(:post, fn _ -> - %Post{} - |> Post.changeset(%{ - type: "activity" - }) - end) - |> Multi.insert(:activity, fn %{post: post} -> - %Activity{} - |> Activity.changeset(attrs) - |> Ecto.Changeset.put_assoc(:post, post) - end) - |> Repo.transaction() - |> case do - {:ok, %{activity: activity, post: _post}} -> - after_save({:ok, activity}, after_save) - - {:error, _reason, changeset, _actions} -> - {:error, changeset} + case Organizations.verify_organization_id?(attrs) and + RateLimiter.limit_activities(Map.get(attrs, :organization_id)) do + :ok -> + Multi.new() + |> Multi.insert(:post, fn _ -> + %Post{} + |> Post.changeset(%{ + type: "activity" + }) + end) + |> Multi.insert(:activity, fn %{post: post} -> + %Activity{} + |> Activity.changeset(attrs) + |> Ecto.Changeset.put_assoc(:post, post) + end) + |> Repo.transaction() + |> case do + {:ok, %{activity: activity, post: _post}} -> + after_save({:ok, activity}, after_save) + + {:error, _reason, changeset, _actions} -> + {:error, changeset} + end + + {:error, reason} -> + {:error, reason} + + false -> + {:error, "Organization ID is required."} end end def create_activity(attrs \\ %{}) do - %Activity{} - |> Activity.changeset(attrs) - |> Repo.insert() + case Organizations.verify_organization_id?(attrs) and + RateLimiter.limit_activities(Map.get(attrs, :organization_id)) do + :ok -> + %Activity{} + |> Activity.changeset(attrs) + |> Repo.insert() + + {:error, reason} -> + {:error, reason} + + false -> + {:error, "Organization ID is required."} + end end @doc """ diff --git a/lib/atomic/organizations.ex b/lib/atomic/organizations.ex index 3024a6cf5..2cf4d84da 100644 --- a/lib/atomic/organizations.ex +++ b/lib/atomic/organizations.ex @@ -7,6 +7,7 @@ defmodule Atomic.Organizations do alias Atomic.Accounts.User alias Atomic.Feed.Post alias Atomic.Organizations.{Announcement, Membership, Organization, UserOrganization} + alias Atomic.RateLimiter @doc """ Returns the list of organizations. @@ -731,32 +732,58 @@ defmodule Atomic.Organizations do """ def create_announcement_with_post(attrs \\ %{}) do - Multi.new() - |> Multi.insert(:post, fn _ -> - %Post{} - |> Post.changeset(%{ - type: "announcement" - }) - end) - |> Multi.insert(:announcement, fn %{post: post} -> - %Announcement{} - |> Announcement.changeset(attrs) - |> Ecto.Changeset.put_assoc(:post, post) - end) - |> Repo.transaction() - |> case do - {:ok, %{announcement: announcement, post: _post}} -> - {:ok, announcement} - - {:error, _reason, changeset, _actions} -> - {:error, changeset} + case verify_organization_id?(attrs) and + RateLimiter.limit_announcements(Map.get(attrs, :organization_id)) do + :ok -> + Multi.new() + |> Multi.insert(:post, fn _ -> + %Post{} + |> Post.changeset(%{ + type: "announcement" + }) + end) + |> Multi.insert(:announcement, fn %{post: post} -> + %Announcement{} + |> Announcement.changeset(attrs) + |> Ecto.Changeset.put_assoc(:post, post) + end) + |> Repo.transaction() + |> case do + {:ok, %{announcement: announcement, post: _post}} -> + {:ok, announcement} + + {:error, _reason, changeset, _actions} -> + {:error, changeset} + end + + {:error, reason} -> + {:error, reason} + + false -> + {:error, "Organization ID is required"} end end def create_announcement(attrs \\ %{}) do - %Announcement{} - |> Announcement.changeset(attrs) - |> Repo.insert() + case verify_organization_id?(attrs) and + RateLimiter.limit_announcements(Map.get(attrs, :organization_id)) do + :ok -> + %Announcement{} + |> Announcement.changeset(attrs) + |> Repo.insert() + + {:error, reason} -> + {:error, reason} + end + end + + def verify_organization_id?(attrs) do + if not Map.has_key?(attrs, :organization_id) or + (Map.has_key?(attrs, :organization_id) and is_nil(Map.get(attrs, :organization_id))) do + false + else + true + end end @doc """ diff --git a/lib/atomic/rate_limiter.ex b/lib/atomic/rate_limiter.ex new file mode 100644 index 000000000..2b8026993 --- /dev/null +++ b/lib/atomic/rate_limiter.ex @@ -0,0 +1,71 @@ +defmodule Atomic.RateLimiter do + @moduledoc """ + Rate limiter module for Atomic. + """ + use Atomic.Context + alias Atomic.Activities.Activity + alias Atomic.Organizations.Announcement + alias Atomic.Repo + @activities_limit_per_day Application.compile_env!(:atomic, :activities_limit_per_day) + @announcements_limit_per_day Application.compile_env!(:atomic, :announcements_limit_per_day) + + @doc """ + Returns if the organization has reached the limit of activities for today. + + ## Examples + + iex> limit_activities("99d7c9e5-4212-4f59-a097-28aaa33c2621") + :ok + + iex> limit_activities("99d7c9e5-4212-4f59-a097-28aaa33c2621") + {:error, "You have reached the daily limit of activities for today"} + """ + def limit_activities(organization_id) do + current_time = DateTime.utc_now() + twenty_four_hours_ago = DateTime.add(current_time, -86_400) + + activity_count = + Repo.all( + from a in Activity, + where: a.organization_id == ^organization_id, + where: a.inserted_at >= ^twenty_four_hours_ago + ) + |> Enum.count() + + if activity_count >= @activities_limit_per_day do + {:error, "You have reached the daily limit of activities for today"} + else + :ok + end + end + + @doc """ + Returns if the organization has reached the limit of announcements for today. + + ## Examples + + iex> limit_announcements("99d7c9e5-4212-4f59-a097-28aaa33c2621") + :ok + + iex> limit_announcements("99d7c9e5-4212-4f59-a097-28aaa33c2621") + {:error, "You have reached the daily limit of announcements for today"} + """ + def limit_announcements(organization_id) do + current_time = DateTime.utc_now() + twenty_four_hours_ago = DateTime.add(current_time, -86_400) + + announcement_count = + Repo.all( + from a in Announcement, + where: a.organization_id == ^organization_id, + where: a.inserted_at >= ^twenty_four_hours_ago + ) + |> Enum.count() + + if announcement_count >= @announcements_limit_per_day do + {:error, "You have reached the daily limit of announcements for today"} + else + :ok + end + end +end diff --git a/test/atomic/activities_test.exs b/test/atomic/activities_test.exs index 0dfdb5186..bb80537a9 100644 --- a/test/atomic/activities_test.exs +++ b/test/atomic/activities_test.exs @@ -32,7 +32,7 @@ defmodule Atomic.ActivitiesTest do end test "create_activity_with_post/2 with invalid data returns error changeset" do - assert {:error, %Ecto.Changeset{}} = Activities.create_activity_with_post(@invalid_attrs) + assert {:error, _reason} = Activities.create_activity_with_post(@invalid_attrs) end test "create_activity_with_post/2 with maximum_entries lower than minimum_entries" do diff --git a/test/atomic/organizations_test.exs b/test/atomic/organizations_test.exs index bbe3465d3..8d7e8c73f 100644 --- a/test/atomic/organizations_test.exs +++ b/test/atomic/organizations_test.exs @@ -220,13 +220,11 @@ defmodule Atomic.OrganizationsTest do test "create_announcement_with_post/2 with valid data creates an announcement" do valid_attrs = params_for(:announcement) - assert {:ok, %Announcement{}} = Organizations.create_announcement_with_post(valid_attrs) end test "create_announcement_with_post/2 with invalid data returns error changeset" do - assert {:error, %Ecto.Changeset{}} = - Organizations.create_announcement_with_post(@invalid_attrs) + assert {:error, _reason} = Organizations.create_announcement_with_post(@invalid_attrs) end test "update_announcement/2 with valid data updates the announcement" do