Skip to content
Merged
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
77 changes: 65 additions & 12 deletions lib/textbin/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ defmodule Textbin.Accounts do
def get_user_by_email_and_password(email, password)
when is_binary(email) and is_binary(password) do
user = Repo.get_by(User, email: email)
if User.valid_password?(user, password), do: user
valid_password? = User.valid_password?(user, password)

if valid_password? && match?(%User{suspended_at: nil}, user), do: user
end

@doc """
Expand Down Expand Up @@ -245,10 +247,19 @@ defmodule Textbin.Accounts do
@doc """
Generates a session token.
"""
def generate_user_session_token(user) do
{token, user_token} = UserToken.build_session_token(user)
Repo.insert!(user_token)
token
def generate_user_session_token(%User{} = user) do
result =
insert_token_for_active_user(user.id, fn current_user ->
UserToken.build_session_token(%{
current_user
| authenticated_at: user.authenticated_at
})
end)

case result do
{:ok, {token, _user_token, _current_user}} -> token
{:error, :suspended} -> raise ArgumentError, "cannot create a session for a suspended user"
end
end

@doc """
Expand Down Expand Up @@ -341,9 +352,16 @@ defmodule Textbin.Accounts do
"""
def deliver_login_instructions(%User{} = user, magic_link_url_fun)
when is_function(magic_link_url_fun, 1) do
{encoded_token, user_token} = UserToken.build_email_token(user, "login")
Repo.insert!(user_token)
UserNotifier.deliver_login_instructions(user, magic_link_url_fun.(encoded_token))
case insert_token_for_active_user(user.id, &UserToken.build_email_token(&1, "login")) do
{:ok, {encoded_token, _user_token, current_user}} ->
UserNotifier.deliver_login_instructions(
current_user,
magic_link_url_fun.(encoded_token)
)

{:error, :suspended} = error ->
error
end
end

@doc """
Expand All @@ -354,6 +372,21 @@ defmodule Textbin.Accounts do
:ok
end

@doc "Disconnects sockets authenticated by the supplied session tokens."
def disconnect_sessions(tokens) do
Enum.each(tokens, fn %{token: token} ->
topic = user_session_topic(token)

Phoenix.PubSub.broadcast(
Textbin.PubSub,
topic,
%Phoenix.Socket.Broadcast{topic: topic, event: "disconnect", payload: %{}}
)
end)
end

def user_session_topic(token), do: "users_sessions:#{Base.url_encode64(token)}"

## API tokens

@doc """
Expand All @@ -376,11 +409,10 @@ defmodule Textbin.Accounts do
"""
def create_user_api_token(%User{} = user, attrs \\ %{}) do
name = api_token_name(attrs)
{token, user_token} = UserToken.build_api_token(user, name)

case Repo.insert(user_token) do
{:ok, user_token} -> {:ok, {token, user_token}}
{:error, changeset} -> {:error, changeset}
case insert_token_for_active_user(user.id, &UserToken.build_api_token(&1, name)) do
{:ok, {token, user_token, _current_user}} -> {:ok, {token, user_token}}
error -> error
end
end

Expand Down Expand Up @@ -444,6 +476,27 @@ defmodule Textbin.Accounts do
end
end

defp insert_token_for_active_user(user_id, build_token) do
Repo.transact(fn -> insert_token_in_transaction(user_id, build_token) end)
end

defp insert_token_in_transaction(user_id, build_token) do
case Repo.one(from user in User, where: user.id == ^user_id, lock: "FOR UPDATE") do
%User{suspended_at: nil} = user -> insert_built_token(user, build_token)
%User{} -> {:error, :suspended}
nil -> {:error, :suspended}
end
end

defp insert_built_token(user, build_token) do
{encoded_token, user_token} = build_token.(user)

case Repo.insert(user_token) do
{:ok, user_token} -> {:ok, {encoded_token, user_token, user}}
{:error, changeset} -> {:error, changeset}
end
end

## Token helper

defp update_user_and_delete_all_tokens(changeset) do
Expand Down
2 changes: 2 additions & 0 deletions lib/textbin/accounts/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ defmodule Textbin.Accounts.User do
schema "users" do
field :email, :string
field :kind, :string, default: "registered"
field :platform_role, :string
field :suspended_at, :utc_datetime
field :password, :string, virtual: true, redact: true
field :hashed_password, :string, redact: true
field :confirmed_at, :utc_datetime
Expand Down
3 changes: 3 additions & 0 deletions lib/textbin/accounts/user_token.ex
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ defmodule Textbin.Accounts.UserToken do
from token in by_token_and_context_query(token, "session"),
join: user in assoc(token, :user),
where: token.inserted_at > ago(@session_validity_in_days, "day"),
where: is_nil(user.suspended_at),
select: {%{user | authenticated_at: token.authenticated_at}, token.inserted_at}

{:ok, query}
Expand Down Expand Up @@ -138,6 +139,7 @@ defmodule Textbin.Accounts.UserToken do
join: user in assoc(token, :user),
where: token.inserted_at > ago(^@magic_link_validity_in_minutes, "minute"),
where: token.sent_to == user.email,
where: is_nil(user.suspended_at),
select: {user, token}

{:ok, query}
Expand Down Expand Up @@ -184,6 +186,7 @@ defmodule Textbin.Accounts.UserToken do
query =
from token in by_token_and_context_query(hashed_token, @api_token_context),
join: user in assoc(token, :user),
where: is_nil(user.suspended_at),
select: {user, token}

{:ok, query}
Expand Down
Loading
Loading