Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: rate limiters #501

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.dev.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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
FRONTEND_URL=http://localhost:4000
ACTIVITIES_LIMIT_PER_DAY=10
ANNOUNCEMENTS_LIMIT_PER_DAY=10
5 changes: 4 additions & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
66 changes: 44 additions & 22 deletions lib/atomic/activities.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 """
Expand Down
71 changes: 49 additions & 22 deletions lib/atomic/organizations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 """
Expand Down
71 changes: 71 additions & 0 deletions lib/atomic/rate_limiter.ex
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion test/atomic/activities_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions test/atomic/organizations_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading