defmodule Asciinema.Users do import Ecto.Query, warn: false import Ecto, only: [assoc: 2] alias Asciinema.{Repo, User, ApiToken, Asciicasts, Email, Mailer, Auth} def create_asciinema_user!() do attrs = %{username: "asciinema", name: "asciinema", email: "support@asciinema.org"} user = case Repo.get_by(User, username: "asciinema") do nil -> %User{} |> User.create_changeset(attrs) |> Repo.insert! user -> user end if Repo.count(assoc(user, :asciicasts)) == 0 do upload = %Plug.Upload{path: "resources/welcome.json", filename: "asciicast.json", content_type: "application/json"} {:ok, _} = Asciicasts.create_asciicast(user, upload, %{private: false, snapshot_at: 76.2}) end :ok end def send_login_email(email_or_username) do with {:ok, %User{} = user} <- lookup_user(email_or_username) do do_send_login_email(user) end end defp lookup_user(email_or_username) do if String.contains?(email_or_username, "@") do lookup_user_by_email(email_or_username) else lookup_user_by_username(email_or_username) end end defp lookup_user_by_email(email) do case Repo.get_by(User, email: email) do %User{} = user -> {:ok, user} nil -> case User.signup_changeset(%{email: email}) do %{errors: [{:email, _}]} -> {:error, :email_invalid} %{errors: []} -> {:ok, %User{email: email}} end end end defp lookup_user_by_username(username) do case Repo.get_by(User, username: username) do %User{} = user -> {:ok, user} nil -> {:error, :user_not_found} end end defp do_send_login_email(%User{email: nil}) do {:error, :email_missing} end defp do_send_login_email(%User{id: nil, email: email}) do url = signup_url(email) Email.signup_email(email, url) |> Mailer.deliver_later {:ok, url} end defp do_send_login_email(%User{} = user) do url = login_url(user) Email.login_email(user.email, url) |> Mailer.deliver_later {:ok, url} end defp signup_url(email) do token = Phoenix.Token.sign(Asciinema.Endpoint, "signup", email) Asciinema.Router.Helpers.users_url(Asciinema.Endpoint, :new, t: token) end defp login_url(%User{id: id, last_login_at: last_login_at}) do last_login_at = last_login_at && Timex.to_unix(last_login_at) token = Phoenix.Token.sign(Asciinema.Endpoint, "login", {id, last_login_at}) Asciinema.Router.Helpers.session_url(Asciinema.Endpoint, :new, t: token) end @login_token_max_age 15 * 60 # 15 minutes alias Phoenix.Token alias Asciinema.Endpoint def verify_signup_token(token) do with {:ok, email} <- Token.verify(Endpoint, "signup", token, max_age: @login_token_max_age), {:ok, %User{} = user} <- User.signup_changeset(%{email: email}) |> Repo.insert do {:ok, user} else {:error, :invalid} -> {:error, :token_invalid} {:error, %Ecto.Changeset{}} -> {:error, :email_taken} {:error, _} -> {:error, :token_expired} end end def verify_login_token(token) do with {:ok, {user_id, last_login_at}} <- Token.verify(Endpoint, "login", token, max_age: @login_token_max_age), %User{} = user <- Repo.get(User, user_id), ^last_login_at <- user.last_login_at && Timex.to_unix(user.last_login_at) do {:ok, user} else {:error, :invalid} -> {:error, :token_invalid} nil -> {:error, :user_not_found} _ -> {:error, :token_expired} end end def log_in(conn, %User{} = user) do user = user |> User.login_changeset |> Repo.update! Auth.login(conn, user) end def authenticate(api_token) do q = from u in User, join: at in ApiToken, on: at.user_id == u.id, select: {u, at.revoked_at}, where: at.token == ^api_token case Repo.one(q) do nil -> {:error, :token_not_found} {%User{} = user, nil} -> {:ok, user} {%User{}, _} -> {:error, :token_revoked} end end def get_user_with_api_token(api_token, tmp_username \\ nil) do case authenticate(api_token) do {:ok, %User{}} = res -> res {:error, :token_revoked} = res -> res {:error, :token_not_found} -> create_user_with_api_token(api_token, tmp_username) end end def create_user_with_api_token(api_token, tmp_username) do user_changeset = User.temporary_changeset(tmp_username) {_, result} = Repo.transaction(fn -> with {:ok, %User{} = user} <- Repo.insert(user_changeset), api_token_changeset = ApiToken.create_changeset(user, api_token), {:ok, %ApiToken{}} <- Repo.insert(api_token_changeset) do {:ok, user} else {:error, %Ecto.Changeset{}} -> {:error, :token_invalid} {:error, _} = err -> Repo.rollback(err) result -> Repo.rollback({:error, result}) end end) result end def get_api_token!(token) do Repo.get_by!(ApiToken, token: token) end def revoke_api_token!(api_token) do api_token |> ApiToken.revoke_changeset |> Repo.update! end def merge!(dst_user, src_user) do Repo.transaction(fn -> asciicasts_q = from(assoc(src_user, :asciicasts)) Repo.update_all(asciicasts_q, set: [user_id: dst_user.id, updated_at: Timex.now]) api_tokens_q = from(assoc(src_user, :api_tokens)) Repo.update_all(api_tokens_q, set: [user_id: dst_user.id, updated_at: Timex.now]) Repo.delete!(src_user) dst_user end) end end