Yeah, in the long run, an ETS (or mnesia?) based approach that doesn’t query the database with every attempt would probably be better.
Here’s what I’ve got for now. (don’t ask me why I made options even though I’ll probably never not use the defaults, I don’t know, it’s a sickness
)
def check_login_attempts(ip_address, email, opts \\ []) do
{limit, opts} = Keyword.pop(opts, :limit, 5)
if get_login_attempt_rate(ip_address, email, opts) < limit do
:ok
else
{:error, :too_many_requests}
end
end
def get_login_attempt_rate(ip_address, email, opts \\ []) do
time_ago_opts = Keyword.get(opts, :time_ago, [minutes: -1])
time_ago = Timex.shift(Timex.now(), time_ago_opts)
query =
from a in LoginAttempt,
where: a.ip_address == ^ip_address or a.email == ^email,
where: a.attempted_at >= ^time_ago,
where: not a.success
Repo.aggregate(query, :count)
end
I didn’t limit it to just ip_address but also email address (which is a login credential). In case, for example and for whatever reason, someone is using a botnet or something to attempt to login to a single account, not only will the IPs get throttled, the account itself will be as well.






















