From 701ef76dd2ac569f8de4ee6889162e23bfc5efe8 Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Sun, 20 Feb 2022 20:39:56 -0300 Subject: [PATCH 01/22] feat: modify user auth contexts and entity --- lib/fuschia/accounts.ex | 476 ++++++++++++++++++ .../{entities => accounts}/pesquisador.ex | 7 +- lib/fuschia/accounts/user.ex | 237 +++++++++ lib/fuschia/accounts/user_notifier.ex | 49 ++ lib/fuschia/accounts/user_token.ex | 188 +++++++ lib/fuschia/context/user_tokens.ex | 90 ---- lib/fuschia/context/users.ex | 266 ---------- lib/fuschia/entities/auth_log.ex | 2 +- lib/fuschia/entities/contato.ex | 29 +- lib/fuschia/entities/midia.ex | 2 +- lib/fuschia/entities/relatorio.ex | 2 +- lib/fuschia/entities/user.ex | 191 ------- lib/fuschia/entities/user_token.ex | 51 -- lib/fuschia/queries/pesquisadores.ex | 2 +- 14 files changed, 969 insertions(+), 623 deletions(-) create mode 100644 lib/fuschia/accounts.ex rename lib/fuschia/{entities => accounts}/pesquisador.ex (94%) create mode 100644 lib/fuschia/accounts/user.ex create mode 100644 lib/fuschia/accounts/user_notifier.ex create mode 100644 lib/fuschia/accounts/user_token.ex delete mode 100644 lib/fuschia/context/user_tokens.ex delete mode 100644 lib/fuschia/context/users.ex delete mode 100644 lib/fuschia/entities/user.ex delete mode 100644 lib/fuschia/entities/user_token.ex diff --git a/lib/fuschia/accounts.ex b/lib/fuschia/accounts.ex new file mode 100644 index 00000000..a3e54d9d --- /dev/null +++ b/lib/fuschia/accounts.ex @@ -0,0 +1,476 @@ +defmodule Fuschia.Accounts do + @moduledoc """ + The Accounts context. + """ + + import Ecto.Query, warn: false + alias Fuschia.Repo + + alias Fuschia.Accounts.{User, UserToken, UserNotifier} + + ## Database getters + + @doc """ + Obtém um usuário a partir de um email + + ## Examples + + iex> get_user_by_email("foo@example.com") + %User{} + + iex> get_user_by_email("unknown@example.com") + nil + + """ + def get_user_by_email(email) when is_binary(email) do + email = + email + |> String.downcase() + |> String.trim() + + query() + |> where([u, contato], fragment("lower(?)", contato.email) == ^email) + |> where([u], u.ativo? == true) + |> order_by([u], desc: u.created_at) + |> limit(1) + |> preload_all() + |> Repo.one() + end + + @doc """ + Obtém um usuário a partir do email e senha + + ## Examples + + iex> get_user_by_email_and_password("foo@example.com", "correct_password") + %User{} + + iex> get_user_by_email_and_password("foo@example.com", "invalid_password") + nil + + """ + def get_user_by_email_and_password(email, password) + when is_binary(email) and is_binary(password) do + user = get_user_by_email(email) + if User.valid_password?(user, password), do: user + end + + @doc """ + Obtém a listagem de usuários + + ## Examples + + iex> list() + [%User{}] + + iex> list() + [] + + """ + def list do + query() + |> preload_all() + |> Repo.all() + end + + @doc """ + Verifica se um suário existe dado um CPF + + ## Examples + + iex> exist?("999.999.999-99") + true + + iex> exists?("") + false + + """ + def exists?(cpf) do + User + |> where([u], u.cpf == ^cpf) + |> Repo.exists?() + end + + @doc """ + Obtém apenas um usuário + + ## Examples + + iex> get("999.999.999-99") + %User{} + + iex> get("") + nil + + """ + def get(cpf) do + query() + |> preload_all() + |> Repo.get(cpf) + |> put_permissions() + end + + ## User registration + + @doc """ + Cria um usuário admin + + ## Examples + + iex> create(valid_attrs) + {:ok, %User{}} + + iex> create(invalid_attrs) + {:error, %Ecto.Changeset{}} + + """ + def create(attrs) do + with {:ok, user} <- + %User{} + |> User.admin_changeset(attrs) + |> Repo.insert() do + {:ok, preload_all(user)} + end + end + + @doc """ + Cadastra um novo usuário + + ## Examples + + iex> register(%{field: value}) + {:ok, %User{}} + + iex> register(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def register(attrs) do + with {:ok, user} <- + %User{} + |> User.registration_changeset(attrs) + |> Repo.insert() do + {:ok, preload_all(user)} + end + end + + @doc """ + Atualiza um usuário existente + + ## Examples + + iex> update(valid_attrs) + {:ok, %User{}} + + iex> update(invalid_attrs) + {:error, %Ecto.Changeset{}} + + """ + def update(cpf, attrs) do + with %User{} = user <- get(cpf) do + user + |> User.changeset(attrs) + |> Repo.update() + end + end + + @doc """ + Retorna um `%Ecto.Changeset{}` para acompanhar as mudanças + de um usuário. + + ## Examples + + iex> change_user_registration(user) + %Ecto.Changeset{data: %User{}} + + """ + def change_user_registration(%User{} = user, attrs \\ %{}) do + User.registration_changeset(user, attrs, hash_password: false) + end + + ## Settings + + @doc """ + Retorna um `%Ecto.Changeset{}` para mudar o email. + + ## Examples + + iex> change_user_email(user) + %Ecto.Changeset{data: %User{}} + + """ + def change_user_email(user, attrs \\ %{}) do + User.email_changeset(user, attrs) + end + + @doc """ + Emula a atualizaçõa do email de um usuário porém não insere + no banco de dados. + + ## Examples + + iex> apply_user_email(user, "valid password", %{email: ...}) + {:ok, %User{}} + + iex> apply_user_email(user, "invalid password", %{email: ...}) + {:error, %Ecto.Changeset{}} + + """ + def apply_user_email(user, password, attrs) do + with %Ecto.Changeset{valid?: true, params: contact} <- + User.email_changeset(user, attrs), + %Ecto.Changeset{valid?: true} = user <- User.update_changeset(user, %{contato: contact}) do + user + |> User.validate_current_password(password) + |> Ecto.Changeset.apply_action(:update) + else + changeset -> {:error, changeset} + end + end + + @doc """ + Atualiza o email de um susuário dado um token. + + Se o token for válido, o email é atualizado e o token deletado. + O campo `confirmed_at` também é atualizado para a data atual + """ + def update_user_email(user, token) do + context = "change:#{user.contato.email}" + + with {:ok, query} <- UserToken.verify_change_email_token_query(token, context), + %UserToken{sent_to: email} <- Repo.one(query), + {:ok, _} <- Repo.transaction(user_email_multi(user, email, context)) do + :ok + else + _ -> :error + end + end + + defp user_email_multi(user, email, context) do + with %Ecto.Changeset{valid?: true, params: contact} <- + User.email_changeset(user, %{email: email}), + %Ecto.Changeset{valid?: true} = changeset <- + user |> User.update_changeset(%{contato: contact}) |> User.confirm_changeset() do + Ecto.Multi.new() + |> Ecto.Multi.update(:user, changeset) + |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, [context])) + end + end + + @doc """ + Entrega o email para atualização do email de um usuário. + + ## Examples + + iex> deliver_update_email_instructions(user, current_email, &Routes.user_update_email_url(conn, :edit, &1)) + {:ok, %{to: ..., body: ...}} + + """ + def deliver_update_email_instructions(%User{} = user, current_email, update_email_url_fun) + when is_function(update_email_url_fun, 1) do + {encoded_token, user_token} = UserToken.build_email_token(user, "change:#{current_email}") + + Repo.insert!(user_token) + UserNotifier.deliver_update_email_instructions(user, update_email_url_fun.(encoded_token)) + end + + @doc """ + Retorna um `%Ecto.Changeset{}` para troca de senha. + + ## Examples + + iex> change_user_password(user) + %Ecto.Changeset{data: %User{}} + + """ + def change_user_password(user, attrs \\ %{}) do + User.password_changeset(user, attrs, hash_password: false) + end + + @doc """ + Atualiza a senha de um usuário + + ## Examples + + iex> update_user_password(user, "valid password", %{password: ...}) + {:ok, %User{}} + + iex> update_user_password(user, "invalid password", %{password: ...}) + {:error, %Ecto.Changeset{}} + + """ + def update_user_password(user, password, attrs) do + changeset = + user + |> User.password_changeset(attrs) + |> User.validate_current_password(password) + + Ecto.Multi.new() + |> Ecto.Multi.update(:user, changeset) + |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all)) + |> Repo.transaction() + |> case do + {:ok, %{user: user}} -> {:ok, user} + {:error, :user, changeset, _} -> {:error, changeset} + end + end + + ## Session + + @doc """ + Gera um token de sessão. + """ + def generate_user_session_token(user) do + {token, user_token} = UserToken.build_session_token(user) + Repo.insert!(user_token) + token + end + + @doc """ + Obtém um usuário dado um token de sessão. + """ + def get_user_by_session_token(token) do + {:ok, query} = UserToken.verify_session_token_query(token) + Repo.one(query) + end + + @doc """ + Deleta um token registrato dado um contexto. + """ + def delete_session_token(token) do + Repo.delete_all(UserToken.token_and_context_query(token, "session")) + :ok + end + + ## Confirmation + + @doc """ + Envia o email para confirmação de email do usuário. + + ## Examples + + iex> deliver_user_confirmation_instructions(user, &Routes.user_confirmation_url(conn, :edit, &1)) + {:ok, %{to: ..., body: ...}} + + iex> deliver_user_confirmation_instructions(confirmed_user, &Routes.user_confirmation_url(conn, :edit, &1)) + {:error, :already_confirmed} + + """ + def deliver_user_confirmation_instructions(%User{} = user, confirmation_url_fun) + when is_function(confirmation_url_fun, 1) do + if user.confirmed_at do + {:error, :already_confirmed} + else + {encoded_token, user_token} = UserToken.build_email_token(user, "confirm") + Repo.insert!(user_token) + UserNotifier.deliver_confirmation_instructions(user, confirmation_url_fun.(encoded_token)) + end + end + + @doc """ + Confirma um usuário dado um token. + + Caso o token seja válido o usuário é confirmado + e o token deletado. + """ + def confirm_user(token) do + with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"), + %User{} = user <- Repo.one(query), + {:ok, %{user: user}} <- Repo.transaction(confirm_user_multi(user)) do + {:ok, user} + else + _ -> :error + end + end + + defp confirm_user_multi(user) do + Ecto.Multi.new() + |> Ecto.Multi.update(:user, User.confirm_changeset(user)) + |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, ["confirm"])) + end + + ## Reset password + + @doc """ + Entrega o email de recuperação de senha para um usuário. + + ## Examples + + iex> deliver_user_reset_password_instructions(user, &Routes.user_reset_password_url(conn, :edit, &1)) + {:ok, %{to: ..., body: ...}} + + """ + def deliver_user_reset_password_instructions(%User{} = user, reset_password_url_fun) + when is_function(reset_password_url_fun, 1) do + {encoded_token, user_token} = UserToken.build_email_token(user, "reset_password") + Repo.insert!(user_token) + UserNotifier.deliver_reset_password_instructions(user, reset_password_url_fun.(encoded_token)) + end + + @doc """ + Obtém um usuário dado um token de recuperação de senha. + + ## Examples + + iex> get_user_by_reset_password_token("validtoken") + %User{} + + iex> get_user_by_reset_password_token("invalidtoken") + nil + + """ + def get_user_by_reset_password_token(token) do + with {:ok, query} <- UserToken.verify_email_token_query(token, "reset_password"), + %User{} = user <- Repo.one(query) do + user + else + _ -> nil + end + end + + @doc """ + Reseta a senha de um usuário. + + ## Examples + + iex> reset_user_password(user, %{password: "new long password", password_confirmation: "new long password"}) + {:ok, %User{}} + + iex> reset_user_password(user, %{password: "valid", password_confirmation: "not the same"}) + {:error, %Ecto.Changeset{}} + + """ + def reset_user_password(user, attrs) do + Ecto.Multi.new() + |> Ecto.Multi.update(:user, User.password_changeset(user, attrs)) + |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all)) + |> Repo.transaction() + |> case do + {:ok, %{user: user}} -> {:ok, user} + {:error, :user, changeset, _} -> {:error, changeset} + end + end + + # Helpers + + def query do + from u in User, + left_join: contato in assoc(u, :contato), + order_by: [desc: u.created_at] + end + + def preload_all(%Ecto.Query{} = query) do + Ecto.Query.preload(query, [:contato]) + end + + def preload_all(%User{} = user) do + Repo.preload(user, [:contato]) + end + + defp put_permissions(%User{} = user) do + # TODO + Map.put(user, :permissoes, nil) + end + + defp put_permissions(nil), do: nil +end diff --git a/lib/fuschia/entities/pesquisador.ex b/lib/fuschia/accounts/pesquisador.ex similarity index 94% rename from lib/fuschia/entities/pesquisador.ex rename to lib/fuschia/accounts/pesquisador.ex index 27730f51..368fa62d 100644 --- a/lib/fuschia/entities/pesquisador.ex +++ b/lib/fuschia/accounts/pesquisador.ex @@ -1,4 +1,4 @@ -defmodule Fuschia.Entities.Pesquisador do +defmodule Fuschia.Accounts.Pesquisador do @moduledoc """ Pesquisador Schema """ @@ -6,7 +6,8 @@ defmodule Fuschia.Entities.Pesquisador do use Fuschia.Schema import Ecto.Changeset - alias Fuschia.Entities.{Campus, Midia, Pesquisador, Relatorio, User} + alias Fuschia.Accounts.User + alias Fuschia.Entities.{Campus, Midia, Pesquisador, Relatorio} alias Fuschia.Types.{CapitalizedString, TrimmedString} @required_fields ~w( @@ -97,7 +98,7 @@ defmodule Fuschia.Entities.Pesquisador do end defimpl Jason.Encoder, for: __MODULE__ do - alias Fuschia.Entities.Pesquisador + alias Fuschia.Accounts.Pesquisador @spec encode(Pesquisador.t(), map) :: map def encode(struct, opts) do diff --git a/lib/fuschia/accounts/user.ex b/lib/fuschia/accounts/user.ex new file mode 100644 index 00000000..2bd81fab --- /dev/null +++ b/lib/fuschia/accounts/user.ex @@ -0,0 +1,237 @@ +defmodule Fuschia.Accounts.User do + use Fuschia.Schema + + import Ecto.Changeset + import FuschiaWeb.Gettext + + alias Fuschia.Common.Formats + alias Fuschia.Entities.{Contato, User} + alias Fuschia.Types.{CapitalizedString, TrimmedString} + + @required_fields ~w(nome_completo cpf data_nascimento)a + @optional_fields ~w(confirmed_at last_seen nome_completo ativo? role)a + + @valid_roles ~w(pesquisador pescador admin avulso) + + @lower_pass_format ~r/[a-z]/ + @upper_pass_format ~r/[A-Z]/ + @special_pass_format ~r/[!?@#$%^&*_0-9]/ + + @cpf_format Formats.cpf() + + @primary_key {:cpf, TrimmedString, autogenerate: false} + schema "user" do + field :confirmed_at, :naive_datetime + field :password_hash, TrimmedString, redact: true + field :password, TrimmedString, virtual: true, redact: true + field :data_nascimento, :date + field :last_seen, :utc_datetime_usec + field :role, TrimmedString, default: "avulso" + field :nome_completo, CapitalizedString + field :ativo?, :boolean, default: true + field :permissoes, :map, virtual: true + + belongs_to :contato, Contato, on_replace: :update + + timestamps() + end + + def changeset(%__MODULE__{} = struct, attrs) do + struct + |> cast(attrs, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) + |> validate_format(:cpf, @cpf_format) + |> unique_constraint(:cpf, name: :user_pkey) + |> unique_constraint(:cpf, name: :user_nome_completo_index) + |> validate_inclusion(:role, @valid_roles) + |> cast_assoc(:contato, required: true) + |> foreign_key_constraint(:cpf, name: :pesquisador_usuario_cpf_fkey) + end + + @spec update_changeset(%__MODULE__{}, map) :: Ecto.Changeset.t() + def update_changeset(%__MODULE__{} = struct, attrs) do + struct + |> cast(attrs, @required_fields ++ @optional_fields) + |> validate_format(:cpf, @cpf_format) + |> unique_constraint(:cpf, name: :user_pkey) + |> unique_constraint(:cpf, name: :user_nome_completo_index) + |> validate_inclusion(:role, @valid_roles) + |> cast_assoc(:contato) + end + + @doc """ + Um Changeset para cadastro de usuários. + + É importante validar o comprimento do e-mail e da senha. + Caso contrário, os bancos de dados podem truncar o e-mail sem avisos, o que + pode levar a um comportamento imprevisível ou inseguro. Senhas longas podem + também ser muito caro fazer hash para certos algoritmos. + + ## Opções + + * `:password_hash` - Faz o hash da senha para que ela possa ser + armazenada com segurança no banco de dados e garante que o campo + de senha seja limpo para evitar vazamentos nos logs. Se o hash de + senha não for necessário e limpar o campo de senha não é desejado + (como ao usar este conjunto de alterações para validações em um + formulário LiveView), esta opção pode ser definida como `false`. + O padrão é `true`. + """ + def registration_changeset(%__MODULE__{} = struct, attrs, opts \\ []) do + struct + |> changeset(attrs) + |> put_change(:role, "avulso") + |> cast(attrs, [:password]) + |> validate_required([:password]) + |> validate_password(opts) + end + + @doc """ + Changeset para criar usuários admin + """ + @spec admin_changeset(%__MODULE__{}, map) :: Ecto.Changeset.t() + def admin_changeset(%__MODULE__{} = struct, attrs) do + struct + |> changeset(attrs) + |> validate_required([:perfil]) + end + + defp validate_password(changeset, opts) do + changeset + |> validate_length(:password, min: 12, max: 72) + |> validate_format(:password, @lower_pass_format, message: "at least one lower case character") + |> validate_format(:password, @upper_pass_format, message: "at least one upper case character") + |> validate_format(:password, @special_pass_format, + message: "at least one digit or punctuation character" + ) + |> maybe_hash_password(opts) + end + + defp maybe_hash_password(changeset, opts) do + hash_password? = Keyword.get(opts, :hash_password, true) + password = get_change(changeset, :password) + + if hash_password? && password && changeset.valid? do + changeset + # If using Bcrypt, then further validate it is at most 72 bytes long + |> validate_length(:password, max: 72, count: :bytes) + |> put_change(:password_hash, Bcrypt.hash_pwd_salt(password)) + |> delete_change(:password) + else + changeset + end + end + + @doc """ + Um conjunto de alterações do usuário para alterar a senha. + + ## Opções + + * `:password_hash` - Faz o hash da senha para que ela possa ser armazenada com segurança + no banco de dados e garante que o campo de senha seja limpo para evitar + vazamentos nos logs. Se o hash de senha não for necessário e limpar o + campo de senha não é desejado (como ao usar este conjunto de alterações para + validações em um formulário LiveView), esta opção pode ser definida como `false`. + O padrão é `true`. + """ + def password_changeset(user, attrs, opts \\ []) do + user + |> cast(attrs, [:password]) + |> validate_required([:password]) + |> validate_confirmation(:password, + required: true, + message: dgettext("errors", "does not match password") + ) + |> validate_password(opts) + end + + @doc """ + A contact changeset for changing the email. + + It requires the email to change otherwise an error is added. + """ + @spec email_changeset(%__MODULE__{}, map) :: Ecto.Changeset.t() + def email_changeset(%__MODULE__{contato: nil} = user, attrs), + do: user |> cast(attrs, []) |> validate_required([:contato]) + + def email_changeset(%__MODULE__{contato: contact}, attrs) do + contact + |> cast(attrs, [:email]) + |> validate_required([:email]) + |> Contato.validate_email() + |> case do + %{changes: %{email: _}} = changeset -> changeset + %{} = changeset -> add_error(changeset, :email, dgettext("errors", "didn't change")) + end + end + + @doc """ + Confirma um usuário atualizando `confirmed_at`, + """ + def confirm_changeset(user) do + now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) + change(user, confirmed_at: now) + end + + @doc """ + Verifica a senha. + + Se não houver usuário ou o usuário não tiver uma senha, chamamos + `Bcrypt.no_user_verify/0` para evitar ataques de tempo. + """ + def valid_password?(%__MODULE__{password_hash: password_hash}, password) + when is_binary(password_hash) and byte_size(password) > 0 do + Bcrypt.verify_pass(password, password_hash) + end + + def valid_password?(_, _) do + Bcrypt.no_user_verify() + false + end + + @doc """ + Valida a senha atual, caso contrário adiciona erro ao Changeset + """ + def validate_current_password(changeset, password) do + if valid_password?(changeset.data, password) do + changeset + else + add_error(changeset, :current_password, "is not valid") + end + end + + def for_jwt(%__MODULE__{} = struct) do + %{ + email: struct.contato.email, + endereco: struct.contato.endereco, + celular: struct.contato.celular, + nomeCompleto: struct.nome_completo, + perfil: struct.role, + permissoes: struct.permissoes, + cpf: struct.cpf, + dataNascimento: struct.data_nascimento + } + end + + def to_map(%__MODULE__{} = struct) do + %{ + nome_completo: struct.nome_completo, + perfil: struct.role, + ultimo_login: struct.last_seen, + confirmado_em: struct.confirmed_at, + ativo: struct.ativo?, + data_nascimento: struct.data_nascimento + } + end + + defimpl Jason.Encoder, for: User do + alias Fuschia.Accounts.User + + @spec encode(User.t(), map) :: map + def encode(struct, opts) do + struct + |> User.to_map() + |> Fuschia.Encoder.encode(opts) + end + end +end diff --git a/lib/fuschia/accounts/user_notifier.ex b/lib/fuschia/accounts/user_notifier.ex new file mode 100644 index 00000000..3553fe75 --- /dev/null +++ b/lib/fuschia/accounts/user_notifier.ex @@ -0,0 +1,49 @@ +defmodule Fuschia.Accounts.UserNotifier do + require Logger + + alias Fuschia.Jobs.MailerJob + + # Entrega o email por meio de um Job no banco de dados + defp deliver(subject, template, assigns) do + email = %{ + to: Map.get(assigns, :email), + subject: subject, + assigns: assigns, + layout: "notification", + template: template + } + + email + |> MailerJob.new() + |> Oban.insert() + |> case do + {:ok, %Oban.Job{}} -> + {:ok, email |> Map.merge(assigns) |> Map.delete(:email)} + + err -> + Logger.error(Exception.format(:error, err)) + {:error, err} + end + end + + def deliver_confirmation_instructions(user, url) do + deliver("Instruções para confirmação da conta", "email_confirmation", %{ + email: user.contato.email, + url: url + }) + end + + def deliver_reset_password_instructions(user, url) do + deliver("Instruções para recuperar senha", "reset_password", %{ + email: user.contato.email, + url: url + }) + end + + def deliver_update_email_instructions(user, url) do + deliver("Instruções para atualizar email", "update_email", %{ + email: user.contato.email, + url: url + }) + end +end diff --git a/lib/fuschia/accounts/user_token.ex b/lib/fuschia/accounts/user_token.ex new file mode 100644 index 00000000..eff19f6b --- /dev/null +++ b/lib/fuschia/accounts/user_token.ex @@ -0,0 +1,188 @@ +defmodule Fuschia.Accounts.UserToken do + use Fuschia.Schema + + import Ecto.Query + + alias Fuschia.Accounts.User + alias Fuschia.Types.TrimmedString + + @hash_algorithm :sha256 + @rand_size 32 + + # É muito importante manter a expiração do token de redefinição de senha curta, + # já que alguém com acesso ao e-mail pode assumir a conta. + @reset_password_validity_in_days 1 + @confirm_validity_in_days 7 + @change_email_validity_in_days 7 + @session_validity_in_days 60 + + schema "user_token" do + field :token, :binary + field :context, :string + field :sent_to, :string + + belongs_to :user, User, + foreign_key: :user_cpf, + references: :cpf, + type: TrimmedString + + timestamps(updated_at: false) + end + + @doc """ + Gera um token que será armazenado em local assinado, + como sessão ou cookie. À medida que são assinados, aqueles + tokens não precisam ser encriptados. + + A razão pela qual armazenamos tokens de sessão no banco de dados, mesmo + embora o Phoenix já forneça um cookie de sessão, é porque + Os cookies de sessão padrão do Phoenix não são persistentes, eles são + simplesmente assinado e potencialmente criptografado. Isso significa que eles são + válido indefinidamente, a menos que você altere a assinatura/criptografia + sal. + + Portanto, armazená-los permite que o usuário individual + sessões a expirar. O sistema de token também pode ser estendido + para armazenar dados adicionais, como o dispositivo usado para fazer login. + Você pode usar essas informações para exibir todas as sessões válidas + e dispositivos na interface do usuário e permitir que os usuários expirem + explicitamente qualquer sessão que considerem inválida. + """ + def build_session_token(user) do + token = :crypto.strong_rand_bytes(@rand_size) + {token, %__MODULE__{token: token, context: "session", user_cpf: user.cpf}} + end + + @doc """ + Verifica se o token é válido e retorna sua consulta de pesquisa subjacente. + + A consulta retorna o usuário encontrado pelo token, se houver. + + O token é válido se corresponder ao valor no banco de dados e tiver + não expirou (após @session_validity_in_days). + """ + def verify_session_token_query(token) do + query = + from token in token_and_context_query(token, "session"), + join: user in assoc(token, :user), + where: token.created_at > ago(@session_validity_in_days, "day"), + select: user + + {:ok, query} + end + + @doc """ + Cria um token e seu hash para ser entregue no email do usuário. + + O token sem hash é enviado para o e-mail do usuário enquanto o + parte com hash é armazenada no banco de dados. O token original não + pode ser reconstruído, o que significa que qualquer pessoa com acesso + somente leitura ao banco de dados não pode usar diretamente + o token no aplicativo para obter acesso. Além disso, se o usuário alterar + seu e-mail no sistema, os tokens enviados para o e-mail anterior não são mais + válido. + + Os usuários podem facilmente adaptar o código existente para fornecer + outros tipos de métodos de entrega, por exemplo, por números de telefone. + """ + def build_email_token(user, context) do + build_hashed_token(user, context, user.contato.email) + end + + defp build_hashed_token(user, context, sent_to) do + token = :crypto.strong_rand_bytes(@rand_size) + hashed_token = :crypto.hash(@hash_algorithm, token) + + {Base.url_encode64(token, padding: false), + %__MODULE__{ + token: hashed_token, + context: context, + sent_to: sent_to, + user_cpf: user.cpf + }} + end + + @doc """ + Verifica se o token é válido e retorna sua consulta de pesquisa subjacente. + + A consulta retorna o usuário encontrado pelo token, se houver. + + O token fornecido é válido se corresponder à sua contraparte com hash no + banco de dados e o e-mail do usuário não foi alterado. Esta função também verifica + se o token estiver sendo usado dentro de um determinado período, dependendo do + contexto. Os contextos padrão suportados por esta função são + "confirm", para e-mails de confirmação de conta, e "reset_password", + para redefinir a senha. Para verificar solicitações de alteração de e-mail, + veja `verify_change_email_token_query/2`. + """ + def verify_email_token_query(token, context) do + case Base.url_decode64(token, padding: false) do + {:ok, decoded_token} -> + hashed_token = :crypto.hash(@hash_algorithm, decoded_token) + days = days_for_context(context) + + query = + from token in token_and_context_query(hashed_token, context), + join: user in assoc(token, :user), + join: contato in assoc(user, :contato), + where: token.created_at > ago(^days, "day") and token.sent_to == contato.email, + select: user + + {:ok, query} + + :error -> + :error + end + end + + defp days_for_context("confirm"), do: @confirm_validity_in_days + defp days_for_context("reset_password"), do: @reset_password_validity_in_days + + @doc """ + Verifica se o token é válido e retorna sua consulta de pesquisa subjacente. + + A consulta retorna o usuário encontrado pelo token, se houver. + + Isso é usado para validar solicitações para alterar o usuário + o email. É diferente de `verify_email_token_query/2` precisamente porque + `verify_email_token_query/2` valida que o email não foi alterado, o que é + o ponto de partida por esta função. + + O token fornecido é válido se corresponder à sua contraparte com hash no + banco de dados e se não expirou (após @change_email_validity_in_days). + O contexto deve sempre começar com "change:". + """ + def verify_change_email_token_query(token, "change:" <> _ = context) do + case Base.url_decode64(token, padding: false) do + {:ok, decoded_token} -> + hashed_token = :crypto.hash(@hash_algorithm, decoded_token) + + query = + from token in token_and_context_query(hashed_token, context), + where: token.created_at > ago(@change_email_validity_in_days, "day") + + {:ok, query} + + :error -> + :error + end + end + + @doc """ + Retorna a estrutura de token para o valor e o contexto de token fornecidos. + """ + def token_and_context_query(token, context) do + from __MODULE__, where: [token: ^token, context: ^context] + end + + @doc """ + Obtém todos os tokens do usuário fornecido para os contextos fornecidos. + """ + def user_and_contexts_query(user, :all) do + from t in __MODULE__, where: t.user_cpf == ^user.cpf + end + + def user_and_contexts_query(user, [_ | _] = contexts) do + from t in __MODULE__, where: t.user_cpf == ^user.cpf and t.context in ^contexts + end +end diff --git a/lib/fuschia/context/user_tokens.ex b/lib/fuschia/context/user_tokens.ex deleted file mode 100644 index f064087b..00000000 --- a/lib/fuschia/context/user_tokens.ex +++ /dev/null @@ -1,90 +0,0 @@ -defmodule Fuschia.Context.UserTokens do - @moduledoc """ - Public Fuschia UserTOken API - """ - - import Ecto.Query - - alias Fuschia.Entities.UserToken - - @type user :: %Fuschia.Entities.User{} - - @hash_algorithm :sha256 - - # It is very important to keep the reset password token expiry short, - # since someone with access to the email may take over the account. - @reset_password_validity_in_days 1 - @confirm_validity_in_days 5 - @change_email_validity_in_days 5 - - @doc """ - Checks if the token is valid and returns its underlying lookup query. - - The query returns the user found by the token. - """ - @spec verify_email_token_query(String.t(), String.t()) :: {:ok, Ecto.Query.t()} | :error - def verify_email_token_query(token, context) do - case Base.url_decode64(token, padding: false) do - {:ok, decoded_token} -> - hashed_token = :crypto.hash(@hash_algorithm, decoded_token) - days = days_for_context(context) - - query = - from token in token_and_context_query(hashed_token, context), - join: user in assoc(token, :user), - join: contato in assoc(user, :contato), - where: token.inserted_at > ago(^days, "day") and token.sent_to == contato.email, - select: user - - {:ok, query} - - :error -> - :error - end - end - - defp days_for_context("confirm"), do: @confirm_validity_in_days - defp days_for_context("reset_password"), do: @reset_password_validity_in_days - - @doc """ - Checks if the token is valid and returns its underlying lookup query. - - The query returns the user token record. - """ - @spec verify_change_email_token_query(String.t(), String.t()) :: {:ok, Ecto.Query.t()} | :error - def verify_change_email_token_query(token, context) do - case Base.url_decode64(token, padding: false) do - {:ok, decoded_token} -> - hashed_token = :crypto.hash(@hash_algorithm, decoded_token) - - query = - from token in token_and_context_query(hashed_token, context), - where: token.inserted_at > ago(@change_email_validity_in_days, "day") - - {:ok, query} - - :error -> - :error - end - end - - @doc """ - Returns the given token with the given context. - """ - @spec token_and_context_query(String.t(), String.t()) :: Ecto.Query.t() - def token_and_context_query(token, context) do - from UserToken, where: [token: ^token, context: ^context] - end - - @doc """ - Gets all tokens for the given user for the given contexts. - """ - @spec user_and_contexts_query(user, list | :all) :: Ecto.Query.t() - def user_and_contexts_query(user, :all) do - from t in UserToken, where: t.user_cpf == ^user.cpf - end - - def user_and_contexts_query(user, [_ | _] = contexts) do - from t in UserToken, where: t.user_cpf == ^user.cpf and t.context in ^contexts - end -end diff --git a/lib/fuschia/context/users.ex b/lib/fuschia/context/users.ex deleted file mode 100644 index d00a722c..00000000 --- a/lib/fuschia/context/users.ex +++ /dev/null @@ -1,266 +0,0 @@ -defmodule Fuschia.Context.Users do - @moduledoc """ - Public Fuschia Users API - """ - - import Ecto.Query - - alias Fuschia.Context.UserTokens - alias Fuschia.Entities.{Contato, User, UserToken} - alias Fuschia.Repo - - @type user :: User.t() - @type query :: Ecto.Query.t() - @type changeset :: Ecto.Changeset.t() - - @spec list :: [user] - def list do - query() - |> preload_all() - |> Repo.all() - end - - @spec one(String.t()) :: user | nil - def one(cpf) do - query() - |> preload_all() - |> Repo.get(cpf) - |> put_permissions() - |> put_is_admin() - end - - @spec one_by_cpf(String.t()) :: user | nil - def one_by_cpf(cpf) do - cpf = String.trim(cpf) - - query() - |> preload_all() - |> Repo.get_by(cpf: cpf) - end - - @spec one_by_email(String.t()) :: user | nil - def one_by_email(email) do - email = - email - |> String.downcase() - |> String.trim() - - query() - |> where([u, contato], fragment("lower(?)", contato.email) == ^email) - |> where([u], u.ativo == true) - |> order_by([u], desc: u.created_at) - |> limit(1) - |> preload_all() - |> Repo.one() - end - - @spec one_with_permissions(String.t()) :: user | nil - def one_with_permissions(cpf) do - cpf - |> one_by_cpf() - |> put_permissions() - |> put_is_admin() - end - - @doc """ - Gets the user by reset password token. - - ## Examples - - iex> one_by_reset_password_token("validtoken") - %User{} - - iex> one_by_reset_password_token("invalidtoken") - nil - - """ - @spec one_by_reset_password_token(String.t()) :: user | nil - def one_by_reset_password_token(token) do - with {:ok, query} <- UserTokens.verify_email_token_query(token, "reset_password"), - %User{} = user <- Repo.one(query) do - user - end - end - - @spec create(map) :: {:ok, user} | {:error, changeset} - def create(attrs) do - with {:ok, user} <- - %User{} - |> User.admin_changeset(attrs) - |> Repo.insert() do - {:ok, preload_all(user)} - end - end - - @spec update(String.t(), map) :: {:ok, user} | {:error, changeset} - def update(cpf, attrs) do - with %User{} = user <- one(cpf) do - user - |> User.changeset(attrs) - |> Repo.update() - end - end - - @spec register(map) :: {:ok, user} | {:error, changeset} - def register(attrs) do - with {:ok, user} <- - %User{} - |> User.registration_changeset(attrs) - |> Repo.insert() do - {:ok, preload_all(user)} - end - end - - @doc """ - Resets the user password. - - ## Examples - - iex> reset_password(user, %{password: "new long password", password_confirmation: "new long password"}) - {:ok, %User{}} - - iex> reset_password(user, %{password: "valid", password_confirmation: "not the same"}) - {:error, %Ecto.Changeset{}} - - """ - @spec reset_password(String.t(), map) :: {:ok, user} | {:error, changeset} - def reset_password(cpf, attrs) do - with %User{} = user <- one(cpf), - {:ok, %{user: reseted}} <- - Ecto.Multi.new() - |> Ecto.Multi.update(:user, User.password_changeset(user, attrs)) - |> Ecto.Multi.delete_all(:tokens, UserTokens.user_and_contexts_query(user, :all)) - |> Repo.transaction() do - {:ok, reseted} - else - {:error, :user, changeset, _e} -> - {:error, changeset} - end - end - - @doc """ - Confirms a user by the given token. - - If the token matches, the user account is marked as confirmed - and the token is deleted. - """ - @spec confirm_user(String.t()) :: {:ok, user} | :error - def confirm_user(token) do - with {:ok, query} <- UserTokens.verify_email_token_query(token, "confirm"), - %User{} = user <- Repo.one(query), - {:ok, %{user: user}} <- Repo.transaction(confirm_multi(user)) do - {:ok, user} - else - _err -> :error - end - end - - @doc """ - Updates the user email using the given token. - - If the token matches, the user email is updated and the token is deleted. - The confirmed_at date is also updated to the current time. - """ - @spec update_email(String.t(), String.t()) :: :ok | :error - def update_email(cpf, token) do - with %User{} = user <- one(cpf), - context <- "change:#{user.contato.email}", - {:ok, query} <- UserTokens.verify_change_email_token_query(token, context), - %UserToken{sent_to: email} <- Repo.one(query), - {:ok, _} <- Repo.transaction(email_multi(user, email, context)) do - :ok - else - _err -> :error - end - end - - @doc """ - Updates the user password. - - ## Examples - - iex> update_password(user, "valid password", %{password: ...}) - {:ok, %User{}} - - iex> update_password(user, "invalid password", %{password: ...}) - {:error, %Ecto.Changeset{}} - - """ - @spec update_password(String.t(), String.t(), map) :: - {:ok, user} | {:error, changeset} - def update_password(cpf, password, attrs) do - with %User{} = user <- one(cpf), - changeset <- - user - |> User.password_changeset(attrs) - |> User.validate_current_password(password), - {:ok, %{user: user}} <- - Ecto.Multi.new() - |> Ecto.Multi.update(:user, changeset) - |> Ecto.Multi.delete_all(:tokens, UserTokens.user_and_contexts_query(user, :all)) - |> Repo.transaction() do - {:ok, user} - else - {:error, :user, changeset, _e} -> - {:error, changeset} - end - end - - @spec exists?(String.t()) :: boolean - def exists?(cpf) do - User - |> where([u], u.cpf == ^cpf) - |> Repo.exists?() - end - - @spec query :: query - def query do - from u in User, - left_join: contato in assoc(u, :contato), - order_by: [desc: u.created_at] - end - - @spec preload_all(query) :: query - def preload_all(%Ecto.Query{} = query) do - Ecto.Query.preload(query, [:contato]) - end - - @spec preload_all(user) :: user - def preload_all(%User{} = user) do - Repo.preload(user, [:contato]) - end - - defp put_is_admin(%User{perfil: "admin"} = user) do - Map.put(user, :is_admin, true) - end - - defp put_is_admin(%User{} = user) do - Map.put(user, :is_admin, false) - end - - defp put_is_admin(nil), do: nil - - defp put_permissions(%User{} = user) do - # TODO - Map.put(user, :permissoes, nil) - end - - defp put_permissions(nil), do: nil - - defp confirm_multi(user) do - Ecto.Multi.new() - |> Ecto.Multi.update(:user, User.confirm_changeset(user)) - |> Ecto.Multi.delete_all( - :tokens, - UserTokens.user_and_contexts_query(user, ["confirm"]) - ) - end - - defp email_multi(user, email, context) do - changeset = user |> Contato.email_changeset(%{email: email}) |> User.confirm_changeset() - - Ecto.Multi.new() - |> Ecto.Multi.update(:user, changeset) - |> Ecto.Multi.delete_all(:tokens, UserTokens.user_and_contexts_query(user, [context])) - end -end diff --git a/lib/fuschia/entities/auth_log.ex b/lib/fuschia/entities/auth_log.ex index 6fd71128..af6bab6a 100644 --- a/lib/fuschia/entities/auth_log.ex +++ b/lib/fuschia/entities/auth_log.ex @@ -7,7 +7,7 @@ defmodule Fuschia.Entities.AuthLog do import Ecto.Changeset - alias Fuschia.Entities.User + alias Fuschia.Accounts.User alias Fuschia.Types.TrimmedString @required_fields ~w(ip user_agent user_cpf)a diff --git a/lib/fuschia/entities/contato.ex b/lib/fuschia/entities/contato.ex index c85e661c..9a632275 100644 --- a/lib/fuschia/entities/contato.ex +++ b/lib/fuschia/entities/contato.ex @@ -6,11 +6,13 @@ defmodule Fuschia.Entities.Contato do use Fuschia.Schema import Ecto.Changeset + import FuschiaWeb.Gettext alias Fuschia.Common.Formats @required_fields ~w(email endereco celular)a + @email_format Formats.email() @mobile_format Formats.mobile() schema "contato" do @@ -31,27 +33,18 @@ defmodule Fuschia.Entities.Contato do |> validate_email() end - @doc """ - A contact changeset for changing the email. - - It requires the email to change otherwise an error is added. - """ - @spec email_changeset(%__MODULE__{}, map) :: Ecto.Changeset.t() - def email_changeset(contact, attrs) do - contact - |> cast(attrs, [:email]) - |> validate_email() - |> case do - %{changes: %{email: _}} = changeset -> changeset - %{} = changeset -> add_error(changeset, :email, "did not change") - end - end - @spec validate_email(Ecto.Changeset.t()) :: Ecto.Changeset.t() - defp validate_email(changeset) do + def validate_email(changeset) do changeset - |> validate_length(:email, max: 160) + |> validate_length(:email, + max: 160, + message: dgettext("errors", "should be at most 160 character(s)", count: 160) + ) + |> validate_format(:email, @email_format, + message: dgettext("errors", "must have the @ sign and no spaces") + ) |> unsafe_validate_unique(:email, Fuschia.Repo) + |> unique_constraint(:email) end defimpl Jason.Encoder, for: __MODULE__ do diff --git a/lib/fuschia/entities/midia.ex b/lib/fuschia/entities/midia.ex index 819ead50..e1646872 100644 --- a/lib/fuschia/entities/midia.ex +++ b/lib/fuschia/entities/midia.ex @@ -6,7 +6,7 @@ defmodule Fuschia.Entities.Midia do use Fuschia.Schema import Ecto.Changeset - alias Fuschia.Entities.Pesquisador + alias Fuschia.Accounts.Pesquisador alias Fuschia.Types.TrimmedString @required_fields ~w( diff --git a/lib/fuschia/entities/relatorio.ex b/lib/fuschia/entities/relatorio.ex index 8488a07c..701136ef 100644 --- a/lib/fuschia/entities/relatorio.ex +++ b/lib/fuschia/entities/relatorio.ex @@ -6,7 +6,7 @@ defmodule Fuschia.Entities.Relatorio do use Fuschia.Schema import Ecto.Changeset - alias Fuschia.Entities.Pesquisador + alias Fuschia.Accounts.Pesquisador alias Fuschia.Types.TrimmedString @required_fields ~w( diff --git a/lib/fuschia/entities/user.ex b/lib/fuschia/entities/user.ex deleted file mode 100644 index b1919539..00000000 --- a/lib/fuschia/entities/user.ex +++ /dev/null @@ -1,191 +0,0 @@ -defmodule Fuschia.Entities.User do - @moduledoc """ - User schema - """ - - use Fuschia.Schema - - import Ecto.Changeset - - alias Fuschia.Common.Formats - alias Fuschia.Entities.{Contato, User} - alias Fuschia.Types.{CapitalizedString, TrimmedString} - - @required_fields ~w(nome_completo cpf data_nascimento)a - @optional_fields ~w(password confirmed last_seen nome_completo ativo perfil)a - - @valid_perfil ~w(pesquisador pescador admin avulso) - - @lower_pass_format ~r/[a-z]/ - @upper_pass_format ~r/[A-Z]/ - @special_pass_format ~r/[!?@#$%^&*_0-9]/ - - @cpf_format Formats.cpf() - - @primary_key {:cpf, TrimmedString, autogenerate: false} - schema "user" do - field :password_hash, TrimmedString - field :confirmed, :boolean, default: false - field :data_nascimento, :date - field :last_seen, :utc_datetime_usec - field :perfil, TrimmedString, default: "avulso" - field :nome_completo, CapitalizedString - field :ativo, :boolean, default: true - field :password, TrimmedString, virtual: true - field :permissoes, :map, virtual: true - field :is_admin, :boolean, virtual: true, default: false - - belongs_to :contato, Contato, on_replace: :update - - timestamps() - end - - @spec changeset(%__MODULE__{}, map) :: Ecto.Changeset.t() - def changeset(%__MODULE__{} = struct, attrs) do - struct - |> cast(attrs, @required_fields ++ @optional_fields) - |> validate_required(@required_fields) - |> validate_format(:cpf, @cpf_format) - |> unique_constraint(:cpf, name: :user_pkey) - |> unique_constraint(:cpf, name: :user_nome_completo_index) - |> validate_inclusion(:perfil, @valid_perfil) - |> cast_assoc(:contato, required: true) - |> foreign_key_constraint(:cpf, name: :pesquisador_usuario_cpf_fkey) - end - - @spec update_changeset(%__MODULE__{}, map) :: Ecto.Changeset.t() - def update_changeset(%__MODULE__{} = struct, attrs) do - struct - |> cast(attrs, @required_fields ++ @optional_fields) - |> validate_format(:cpf, @cpf_format) - |> unique_constraint(:cpf, name: :user_pkey) - |> unique_constraint(:cpf, name: :user_nome_completo_index) - |> validate_inclusion(:perfil, @valid_perfil) - |> cast_assoc(:contato) - end - - @doc """ - Changeset for user signup - """ - @spec registration_changeset(%__MODULE__{}, map) :: Ecto.Changeset.t() - def registration_changeset(%__MODULE__{} = struct, attrs) do - struct - |> changeset(attrs) - |> put_change(:perfil, "avulso") - |> cast(attrs, [:password]) - |> validate_required([:password]) - |> validate_password() - |> put_hashed_password() - end - - @doc """ - Changeset for users created on admin - """ - @spec admin_changeset(%__MODULE__{}, map) :: Ecto.Changeset.t() - def admin_changeset(%__MODULE__{} = struct, attrs) do - struct - |> changeset(attrs) - |> validate_required([:perfil]) - end - - @doc """ - A user changeset for changing the password. - """ - @spec password_changeset(%__MODULE__{}, map) :: Ecto.Changeset.t() - def password_changeset(user, attrs) do - user - |> cast(attrs, [:password]) - |> validate_required([:password]) - |> validate_password() - |> put_hashed_password() - end - - @doc """ - Confirms the account by setting `confirmed`. - """ - @spec confirm_changeset(%__MODULE__{}) :: Ecto.Changeset.t() - def confirm_changeset(user) do - change(user, confirmed: true) - end - - @spec for_jwt(%__MODULE__{}) :: map - def for_jwt(%__MODULE__{} = struct) do - %{ - email: struct.contato.email, - endereco: struct.contato.endereco, - celular: struct.contato.celular, - nomeCompleto: struct.nome_completo, - perfil: struct.perfil, - permissoes: struct.permissoes, - cpf: struct.cpf, - dataNascimento: struct.data_nascimento - } - end - - @doc """ - Verifies the password. - - If there is no usuario or the usuario doesn't have a password, we call - `Bcrypt.no_user_verify/0` to avoid timing attacks. - """ - @spec valid_password?(%__MODULE__{}, String.t()) :: bool - def valid_password?(%User{password_hash: hashed_password}, password) - when is_binary(hashed_password) and byte_size(password) > 0 do - Bcrypt.verify_pass(password, hashed_password) - end - - def valid_password?(_invalid_hash, _invalid_password) do - Bcrypt.no_user_verify() - false - end - - @doc """ - Validates the current password otherwise adds an error to the changeset. - """ - @spec validate_current_password(Ecto.Changeset.t(), String.t()) :: Ecto.Changeset.t() - def validate_current_password(changeset, password) do - if valid_password?(changeset.data, password) do - changeset - else - add_error(changeset, :current_password, "is not valid") - end - end - - defp validate_password(changeset) do - changeset - |> validate_length(:password, min: 8, max: 80) - |> validate_format(:password, @lower_pass_format, message: "at least one lower case character") - |> validate_format(:password, @upper_pass_format, message: "at least one upper case character") - |> validate_format(:password, @special_pass_format, - message: "at least one digit or punctuation character" - ) - |> validate_confirmation(:password, required: true) - end - - defp put_hashed_password(changeset) do - case changeset do - %Ecto.Changeset{valid?: true, changes: %{password: password}} -> - put_change(changeset, :password_hash, Bcrypt.hash_pwd_salt(password)) - - _no_password -> - changeset - end - end - - defimpl Jason.Encoder, for: User do - @spec encode(Fuschia.Entities.User.t(), map) :: map - def encode(struct, opts) do - Fuschia.Encoder.encode( - %{ - nome_completo: struct.nome_completo, - perfil: struct.perfil, - ultimo_login: struct.last_seen, - confirmado: struct.confirmed, - ativo: struct.ativo, - data_nascimento: struct.data_nascimento - }, - opts - ) - end - end -end diff --git a/lib/fuschia/entities/user_token.ex b/lib/fuschia/entities/user_token.ex deleted file mode 100644 index a62f3193..00000000 --- a/lib/fuschia/entities/user_token.ex +++ /dev/null @@ -1,51 +0,0 @@ -defmodule Fuschia.Entities.UserToken do - @moduledoc """ - Defines structure of a secure and random token to - user be able to reset password, change email or confirm - registration - """ - - use Fuschia.Schema - - alias Fuschia.Entities.User - - @hash_algorithm :sha256 - @rand_size 32 - - @foreign_key_type :string - schema "users_token" do - field :token, :binary - field :context, :string - field :sent_to, :string - - belongs_to :user, User, foreign_key: :user_cpf, references: :cpf - - timestamps(updated_at: false) - end - - @doc """ - Builds a token with a hashed counter part. - - The non-hashed token is sent to the user email while the - hashed part is stored in the database, to avoid reconstruction. - The token is valid for a week as long as users don't change - their email. - """ - @spec build_email_token(User.t(), String.t()) :: {String.t(), %__MODULE__{}} - def build_email_token(user, context) do - build_hashed_token(user, context, user.contato.email) - end - - defp build_hashed_token(user, context, sent_to) do - token = :crypto.strong_rand_bytes(@rand_size) - hashed_token = :crypto.hash(@hash_algorithm, token) - - {Base.url_encode64(token, padding: false), - %Fuschia.Entities.UserToken{ - token: hashed_token, - context: context, - sent_to: sent_to, - user_cpf: user.cpf - }} - end -end diff --git a/lib/fuschia/queries/pesquisadores.ex b/lib/fuschia/queries/pesquisadores.ex index 71052372..d2ce1ad5 100644 --- a/lib/fuschia/queries/pesquisadores.ex +++ b/lib/fuschia/queries/pesquisadores.ex @@ -5,7 +5,7 @@ defmodule Fuschia.Queries.Pesquisadores do import Ecto.Query, only: [from: 2, where: 3, order_by: 3] - alias Fuschia.Entities.Pesquisador + alias Fuschia.Accounts.Pesquisador @behaviour Fuschia.Query From 423ac6b4abe02d4df1557f6816444365709d1da7 Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Sun, 20 Feb 2022 20:40:57 -0300 Subject: [PATCH 02/22] chore: doc translations and minor fixes --- lib/fuschia/common/formats.ex | 3 +++ lib/fuschia/db.ex | 2 +- lib/fuschia/mailer.ex | 17 ++++------------- lib/fuschia/mailer/html.ex | 8 ++++---- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/lib/fuschia/common/formats.ex b/lib/fuschia/common/formats.ex index 54767307..4ba9e523 100644 --- a/lib/fuschia/common/formats.ex +++ b/lib/fuschia/common/formats.ex @@ -3,11 +3,14 @@ defmodule Fuschia.Common.Formats do Our common place for formats """ + @email_regex ~r/^[^\s]+@[^\s]+$/ @cpf_format ~r/^\d{3}\.\d{3}\.\d{3}\-\d{2}$/ @cnpj_format ~r/^\d{2}\.\d{3}\.\d{3}\/\d{4}\-\d{2}$/ @rg_format ~r/^\d{2}\.\d{3}\.\d{3}\-\d{1}$/ @mobile_format ~r/^\(\d{2}\)(\d{5}|\s\d{5})-\d{4}$/ + @spec email :: Regex.t() + def email, do: @email_regex @spec cpf :: Regex.t() def cpf, do: @cpf_format @spec rg :: Regex.t() diff --git a/lib/fuschia/db.ex b/lib/fuschia/db.ex index 80ee1f6e..96fcc3da 100644 --- a/lib/fuschia/db.ex +++ b/lib/fuschia/db.ex @@ -3,7 +3,7 @@ defmodule Fuschia.Db do Este módulo centraliza os efeitos colaterais do banco de dados. - Fontes: https://pt.stackoverflow.com/questions/330341/o-que-s%C3%A3o-efeitos-colaterais + Fontes: https://pt.stackoverflow.com/questions/330341/o-que-s%C3%A3o-efeitos-colaterais """ alias Fuschia.Repo diff --git a/lib/fuschia/mailer.ex b/lib/fuschia/mailer.ex index e08543ee..7c4e3535 100644 --- a/lib/fuschia/mailer.ex +++ b/lib/fuschia/mailer.ex @@ -1,6 +1,6 @@ defmodule Fuschia.Mailer do @moduledoc """ - Mailer public API + API pública para criação de emails """ use Swoosh.Mailer, otp_app: :fuschia @@ -9,19 +9,10 @@ defmodule Fuschia.Mailer do alias Swoosh.Email @doc """ - Returns an email structure populated with a `recipient` and a - `subject` and assembles the email's html body based on the given - templates `layout` and `email` and given `assigns`. + Retorna uma estrutura de e-mail preenchida com um `destinatário` e um + `subject` e monta o corpo html do email com base no dado + templates `layout` e `email` e dados `assigns`. """ - @spec new_email( - String.t() | {String.t(), String.t()} | [String.t()] | [{String.t(), String.t()}], - String.t(), - String.t(), - String.t(), - map, - String.t(), - String.t() | {String.t(), String.t()} | [String.t()] | [{String.t(), String.t()}] | [] - ) :: Email.t() def new_email(recipient, subject, layout, template, assigns \\ %{}, base \\ "base", bcc \\ []) when is_map(assigns) do body = HTML.assemble_body(layout, template, assigns, base) diff --git a/lib/fuschia/mailer/html.ex b/lib/fuschia/mailer/html.ex index 9107e48f..5046c2b4 100644 --- a/lib/fuschia/mailer/html.ex +++ b/lib/fuschia/mailer/html.ex @@ -2,10 +2,10 @@ defmodule Fuschia.Mailer.HTML do @moduledoc false @doc """ - Inject the `assigns` values into the `email` template's .eex tags - from the "homologacao" or "financiamento" `project` and then render - the resulting HTML page into a base template. - Returns an HTML page in string format. + Injete os valores `assigns` nas tags .eex do modelo `email` + do "homologacao" ou "financiamento" `projeto` e então renderizar + a página HTML resultante em um modelo base. + Retorna uma página HTML em formato de string. """ @spec assemble_body(String.t(), String.t(), map, String.t()) :: any def assemble_body(project, email, assigns, base \\ "base") when is_map(assigns) do From b4013aaec433d955c7c93ae1a696e1db1653636f Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Sun, 20 Feb 2022 20:41:45 -0300 Subject: [PATCH 03/22] feat: add gettext translations and fix user migration --- config/config.exs | 2 + config/test.exs | 6 + priv/gettext/default.pot | 36 ++++++ priv/gettext/en/LC_MESSAGES/default.po | 37 ++++++ priv/gettext/en/LC_MESSAGES/errors.po | 35 +++++ priv/gettext/errors.pot | 36 +++++- priv/gettext/pt_BR/LC_MESSAGES/default.po | 37 ++++++ priv/gettext/pt_BR/LC_MESSAGES/errors.po | 122 ++++++++++++++++++ ...20210728061626_create_user_auth_tables.exs | 6 +- 9 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 priv/gettext/default.pot create mode 100644 priv/gettext/en/LC_MESSAGES/default.po create mode 100644 priv/gettext/pt_BR/LC_MESSAGES/default.po create mode 100644 priv/gettext/pt_BR/LC_MESSAGES/errors.po diff --git a/config/config.exs b/config/config.exs index b05ac520..a55607d4 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,5 +1,7 @@ import Config +config :gettext, default_locale: "pt_BR" + # ---------------------------# # Ecto # ---------------------------# diff --git a/config/test.exs b/config/test.exs index d33a8b5d..5673c318 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,5 +1,11 @@ import Config +# Only in tests, remove the complexity from the password hashing algorithm +config :bcrypt_elixir, :log_rounds, 1 + +# Não é necessário traduzir erros em ambiente de teste +config :gettext, default_locale: "en" + config :fuschia, Fuschia.Repo, username: "pescarte", password: "pescarte", diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot new file mode 100644 index 00000000..6d44a659 --- /dev/null +++ b/priv/gettext/default.pot @@ -0,0 +1,36 @@ +## This file is a PO Template file. +## +## "msgid"s here are often extracted from source code. +## Add new translations manually only if they're dynamic +## translations that can't be statically extracted. +## +## Run "mix gettext.extract" to bring this file up to +## date. Leave "msgstr"s empty as changing them here has no +## effect: edit them in PO (.po) files instead. +msgid "" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/fallback_controller.ex:44 +msgid "Access Denied" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/fallback_controller.ex:37 +msgid "Incorrect e-mail or password" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/fallback_controller.ex:60 +msgid "The item already exists" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/fallback_controller.ex:30 +msgid "The item you requested doesn't exist" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/plugs/require_api_key.ex:26 +msgid "You need a valid API Key to access this endpoint" +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po new file mode 100644 index 00000000..b17fd39b --- /dev/null +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -0,0 +1,37 @@ +## "msgid"s in this file come from POT (.pot) files. +## +## Do not add, change, or remove "msgid"s manually here as +## they're tied to the ones in the corresponding POT file +## (with the same domain). +## +## Use "mix gettext.extract --merge" or "mix gettext.merge" +## to merge POT files into PO files. +msgid "" +msgstr "" +"Language: en\n" +"Plural-Forms: nplurals=2\n" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/fallback_controller.ex:44 +msgid "Access Denied" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/fallback_controller.ex:37 +msgid "Incorrect e-mail or password" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/fallback_controller.ex:60 +msgid "The item already exists" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/fallback_controller.ex:30 +msgid "The item you requested doesn't exist" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/plugs/require_api_key.ex:26 +msgid "You need a valid API Key to access this endpoint" +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/errors.po b/priv/gettext/en/LC_MESSAGES/errors.po index a589998c..37644d7b 100644 --- a/priv/gettext/en/LC_MESSAGES/errors.po +++ b/priv/gettext/en/LC_MESSAGES/errors.po @@ -95,3 +95,38 @@ msgstr "" msgid "must be equal to %{number}" msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/plugs/ensure_role_plug.ex:42 +msgid "Unauthorized" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/live/live_helpers.ex:26 +msgid "You must log in to access this page." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia/schema.ex:41 +msgid "does not exist" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia/entities/contato.ex:48 +msgid "didn't change" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia/entities/contato.ex:60 +msgid "must have the @ sign and no spaces" +msgstr "" + +#, elixir-autogen, elixir-format, fuzzy +#: lib/fuschia/entities/contato.ex:57 +msgid "should be at most 160 character(s)" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia/accounts/user.ex:143 +msgid "does not match password" +msgstr "" diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot index 39a220be..fb3b804b 100644 --- a/priv/gettext/errors.pot +++ b/priv/gettext/errors.pot @@ -7,7 +7,6 @@ ## Run `mix gettext.extract` to bring this file up to ## date. Leave `msgstr`s empty as changing them here has no ## effect: edit them in PO (`.po`) files instead. - ## From Ecto.Changeset.cast/4 msgid "can't be blank" msgstr "" @@ -93,3 +92,38 @@ msgstr "" msgid "must be equal to %{number}" msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/plugs/ensure_role_plug.ex:42 +msgid "Unauthorized" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/live/live_helpers.ex:26 +msgid "You must log in to access this page." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia/schema.ex:41 +msgid "does not exist" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia/entities/contato.ex:48 +msgid "didn't change" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia/entities/contato.ex:60 +msgid "must have the @ sign and no spaces" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia/entities/contato.ex:57 +msgid "should be at most 160 character(s)" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia/accounts/user.ex:143 +msgid "does not match password" +msgstr "" diff --git a/priv/gettext/pt_BR/LC_MESSAGES/default.po b/priv/gettext/pt_BR/LC_MESSAGES/default.po new file mode 100644 index 00000000..4d83e74e --- /dev/null +++ b/priv/gettext/pt_BR/LC_MESSAGES/default.po @@ -0,0 +1,37 @@ +## "msgid"s in this file come from POT (.pot) files. +## +## Do not add, change, or remove "msgid"s manually here as +## they're tied to the ones in the corresponding POT file +## (with the same domain). +## +## Use "mix gettext.extract --merge" or "mix gettext.merge" +## to merge POT files into PO files. +msgid "" +msgstr "" +"Language: pt_BR\n" +"Plural-Forms: nplurals=2\n" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/fallback_controller.ex:44 +msgid "Access Denied" +msgstr "Acesso Negado" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/fallback_controller.ex:37 +msgid "Incorrect e-mail or password" +msgstr "Email ou Senha incorretos" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/fallback_controller.ex:60 +msgid "The item already exists" +msgstr "O item já existe" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/fallback_controller.ex:30 +msgid "The item you requested doesn't exist" +msgstr "O item requisitado não existe" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/plugs/require_api_key.ex:26 +msgid "You need a valid API Key to access this endpoint" +msgstr "Você precisa de uma chave API válida para acessar este recurso" diff --git a/priv/gettext/pt_BR/LC_MESSAGES/errors.po b/priv/gettext/pt_BR/LC_MESSAGES/errors.po new file mode 100644 index 00000000..4c324200 --- /dev/null +++ b/priv/gettext/pt_BR/LC_MESSAGES/errors.po @@ -0,0 +1,122 @@ +## "msgid"s in this file come from POT (.pot) files. +## +## Do not add, change, or remove "msgid"s manually here as +## they're tied to the ones in the corresponding POT file +## (with the same domain). +## +## Use "mix gettext.extract --merge" or "mix gettext.merge" +## to merge POT files into PO files. +msgid "" +msgstr "" +"Language: pt_BR\n" +"Plural-Forms: nplurals=2\n" + +msgid "can't be blank" +msgstr "não pode ficar em branco" + +msgid "has already been taken" +msgstr "já está em uso" + +msgid "is invalid" +msgstr "é inválido" + +msgid "must be accepted" +msgstr "deve ser aceito" + +msgid "has invalid format" +msgstr "formato inválido" + +msgid "has an invalid entry" +msgstr "possui valor inválido" + +msgid "is reserved" +msgstr "é reservardo" + +msgid "does not match confirmation" +msgstr "não coincide" + +msgid "is still associated with this entry" +msgstr "continua associado a esse valor" + +msgid "are still associated with this entry" +msgstr "continua associado a esse valor" + +msgid "should be %{count} character(s)" +msgid_plural "should be %{count} character(s)" +msgstr[0] "deve possuir %{count} caracteres" +msgstr[1] "deve possuir %{count} caracteres" + +msgid "should have %{count} item(s)" +msgid_plural "should have %{count} item(s)" +msgstr[0] "deve possuir %{count} item" +msgstr[1] "deve possuir %{count} itens" + +msgid "should be at least %{count} character(s)" +msgid_plural "should be at least %{count} character(s)" +msgstr[0] "deve possuir no mínimo %{count} caracteres" +msgstr[1] "deve possuir no mínimo %{count} caracteres" + +msgid "should have at least %{count} item(s)" +msgid_plural "should have at least %{count} item(s)" +msgstr[0] "deve possuir no mínimo %{count} itens" +msgstr[1] "deve possuir no mínimo %{count} itens" + +msgid "should be at most %{count} character(s)" +msgid_plural "should be at most %{count} character(s)" +msgstr[0] "deve possuir no máximo %{count} caracteres" +msgstr[1] "deve possuir no máximo %{count} caracteres" + +msgid "should have at most %{count} item(s)" +msgid_plural "should have at most %{count} item(s)" +msgstr[0] "deve possuir no máximo %{count} itens" +msgstr[1] "deve possuir no máximo %{count} itens" + +msgid "must be less than %{number}" +msgstr "deve ser menor que %{number}" + +msgid "must be greater than %{number}" +msgstr "deve ser maior que %{number}" + +msgid "must be less than or equal to %{number}" +msgstr "deve ser menor ou igual a %{number}" + +msgid "must be greater than or equal to %{number}" +msgstr "deve ser maior ou igual a %{number}" + +msgid "must be equal to %{number}" +msgstr "deve ser igual a %{number}" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/plugs/ensure_role_plug.ex:42 +msgid "Unauthorized" +msgstr "Sem autorização" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/live/live_helpers.ex:26 +msgid "You must log in to access this page." +msgstr "Você precisa realizar login para acessar esta página." + +#, elixir-autogen, elixir-format +#: lib/fuschia/schema.ex:41 +msgid "does not exist" +msgstr "não existe" + +#, elixir-autogen, elixir-format +#: lib/fuschia/entities/contato.ex:48 +msgid "didn't change" +msgstr "não pode ser igual ao antigo" + +#, elixir-autogen, elixir-format +#: lib/fuschia/entities/contato.ex:60 +msgid "must have the @ sign and no spaces" +msgstr "deve haver um @ e nenhum espaço em branco" + +#, elixir-autogen, elixir-format, fuzzy +#: lib/fuschia/entities/contato.ex:57 +msgid "should be at most 160 character(s)" +msgstr "deve possuir no máximo %{count} caracteres" + +#, elixir-autogen, elixir-format +#: lib/fuschia/accounts/user.ex:143 +msgid "does not match password" +msgstr "as senhas precisam ser iguais" diff --git a/priv/repo/migrations/20210728061626_create_user_auth_tables.exs b/priv/repo/migrations/20210728061626_create_user_auth_tables.exs index 5cf06bac..1884549f 100644 --- a/priv/repo/migrations/20210728061626_create_user_auth_tables.exs +++ b/priv/repo/migrations/20210728061626_create_user_auth_tables.exs @@ -6,11 +6,11 @@ defmodule Fuschia.Repo.Migrations.CreateUsuariosAuthTables do add(:cpf, :citext, primary_key: true, null: false) add(:nome_completo, :string, null: false) add(:data_nascimento, :date, null: false) - add(:perfil, :string, default: "avulso", null: false) + add(:role, :string, default: "avulso", null: false) add(:last_seen, :utc_datetime_usec) - add(:ativo, :boolean, default: true, null: false) + add(:ativo?, :boolean, default: true, null: false) add(:password_hash, :string) - add(:confirmed, :boolean, default: false) + add(:confirmed_at, :naive_datetime) add(:contato_id, references(:contato, on_replace: :update), null: false) From 7d4ec96fc1cc27dbf31e4e9dc1a973c19155e80f Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Sun, 20 Feb 2022 20:42:44 -0300 Subject: [PATCH 04/22] feat: accounts tests --- test/fuschia/accounts_test.exs | 586 +++++++++++++++++++++++++++++++++ 1 file changed, 586 insertions(+) create mode 100644 test/fuschia/accounts_test.exs diff --git a/test/fuschia/accounts_test.exs b/test/fuschia/accounts_test.exs new file mode 100644 index 00000000..66281ada --- /dev/null +++ b/test/fuschia/accounts_test.exs @@ -0,0 +1,586 @@ +defmodule Fuschia.AccountsTest do + use Fuschia.DataCase + + import Fuschia.Factory + + alias Fuschia.Accounts + alias Fuschia.Accounts.{User, UserToken} + + @moduletag :unit + + defp user_fixture(opts \\ []) do + :user + |> insert(opts) + |> Accounts.preload_all() + end + + describe "list/1" do + test "return all users in database" do + user = user_fixture() + + assert [user] == Accounts.list() + end + end + + describe "get_user_by_email/1" do + test "does not return the user if the email does not exist" do + refute Accounts.get_user_by_email("unknown@example.com") + end + + test "returns the user if the email exists" do + %{cpf: cpf} = user = user_fixture() + assert %User{cpf: ^cpf} = Accounts.get_user_by_email(user.contato.email) + end + end + + describe "get_user_by_email_and_password/2" do + test "does not return the user if the email does not exist" do + refute Accounts.get_user_by_email_and_password("unknown@example.com", "hello world!") + end + + test "does not return the user if the password is not valid" do + user = user_fixture() + refute Accounts.get_user_by_email_and_password(user.contato.email, "invalid") + end + + test "returns the user if the email and password are valid" do + %{cpf: cpf} = user = user_fixture() + + assert %User{cpf: ^cpf} = + Accounts.get_user_by_email_and_password(user.contato.email, valid_user_password()) + end + end + + describe "exists?/1" do + test "when id is valid, returns true" do + user = insert(:user) + assert Accounts.exists?(user.cpf) + end + + test "when id is invalid, returns false" do + refute Accounts.exists?("") + end + end + + describe "get/1" do + test "when id is valid, returns a user" do + user = user_fixture() + assert user == Accounts.get(user.cpf) + end + + test "when id is invalid, returns nil" do + refute Accounts.get("") + end + end + + describe "register/1" do + test "requires email and password to be set" do + {:error, changeset} = Accounts.register(%{}) + + assert %{ + password: ["can't be blank"], + contato: ["can't be blank"] + } = errors_on(changeset) + end + + test "validates email and password when given" do + {:error, changeset} = + Accounts.register(%{contato: %{email: "not valid"}, password: "not valid"}) + + assert %{ + contato: %{email: ["must have the @ sign and no spaces"]}, + password: [ + "at least one digit or punctuation character", + "at least one upper case character", + "should be at least 12 character(s)" + ] + } = errors_on(changeset) + end + + test "validates maximum values for email and password for security" do + too_long = String.duplicate("db", 100) + {:error, changeset} = Accounts.register(%{contato: %{email: too_long}, password: too_long}) + assert "should be at most 160 character(s)" in errors_on(changeset).contato.email + assert "should be at most 72 character(s)" in errors_on(changeset).password + end + + test "validates email uniqueness" do + %{contato: %{email: email}} = user_fixture() + {:error, changeset} = Accounts.register(%{contato: %{email: email}}) + assert "has already been taken" in errors_on(changeset).contato.email + + # Now try with the upper cased email too, to check that email case is ignored. + {:error, changeset} = Accounts.register(%{contato: %{email: String.upcase(email)}}) + assert "has already been taken" in errors_on(changeset).contato.email + end + + test "registers user with a hashed password" do + email = unique_user_email() + contact = params_for(:contato, email: email) + password = valid_user_password() + + valid_user_attributes = + :user + |> params_for() + |> Map.put(:contato, contact) + |> Map.merge(%{password: password, password_confirmation: password}) + + {:ok, user} = Accounts.register(valid_user_attributes) + assert user.contato.email == email + assert is_binary(user.password_hash) + assert is_nil(user.confirmed_at) + assert is_nil(user.password) + end + end + + describe "change_user_registration/2" do + test "returns a changeset" do + assert %Ecto.Changeset{} = changeset = Accounts.change_user_registration(%User{}) + assert changeset.required == ~w(password contato nome_completo cpf data_nascimento)a + end + + test "allows fields to be set" do + email = unique_user_email() + password = valid_user_password() + + contact = params_for(:contato, email: email) + + valid_user_attributes = + :user + |> params_for() + |> Map.put(:contato, contact) + |> Map.merge(%{password: password, password_confirmation: password}) + + changeset = + Accounts.change_user_registration( + %User{}, + valid_user_attributes + ) + + assert changeset.valid? + assert changeset |> get_change(:contato) |> get_change(:email) == email + assert get_change(changeset, :password) == password + refute get_change(changeset, :password_hash) + end + end + + describe "change_user_email/2" do + test "returns a user changeset" do + assert %Ecto.Changeset{} = + changeset = %User{} |> Accounts.preload_all() |> Accounts.change_user_email() + + assert :contato in changeset.required + end + end + + describe "apply_user_email/3" do + setup do + %{user: user_fixture()} + end + + test "requires email to change", %{user: user} do + {:error, changeset} = Accounts.apply_user_email(user, valid_user_password(), %{}) + assert %{email: ["didn't change"]} = errors_on(changeset) + end + + test "validates email", %{user: user} do + {:error, changeset} = + Accounts.apply_user_email(user, valid_user_password(), %{email: "not valid"}) + + assert %{email: ["must have the @ sign and no spaces"]} = errors_on(changeset) + end + + test "validates maximum value for email for security", %{user: user} do + too_long = String.duplicate("db", 100) + + {:error, changeset} = + Accounts.apply_user_email(user, valid_user_password(), %{email: too_long}) + + assert "should be at most 160 character(s)" in errors_on(changeset).email + end + + test "validates email uniqueness", %{user: user} do + %{contato: %{email: email}} = user_fixture() + + {:error, changeset} = + Accounts.apply_user_email(user, valid_user_password(), %{email: email}) + + assert "has already been taken" in errors_on(changeset).email + end + + test "validates current password", %{user: user} do + {:error, changeset} = + Accounts.apply_user_email(user, "invalid", %{email: unique_user_email()}) + + assert %{current_password: ["is not valid"]} = errors_on(changeset) + end + + test "applies the email without persisting it", %{user: user} do + email = unique_user_email() + {:ok, user} = Accounts.apply_user_email(user, valid_user_password(), %{email: email}) + assert user.contato.email == email + assert Accounts.get(user.cpf).contato.email != email + end + end + + describe "deliver_update_email_instructions/3" do + setup do + %{user: user_fixture()} + end + + test "sends token through notification", %{user: user} do + token = + extract_user_token(fn url -> + Accounts.deliver_update_email_instructions(user, "current@example.com", url) + end) + + {:ok, token} = Base.url_decode64(token, padding: false) + assert user_token = Repo.get_by(UserToken, token: :crypto.hash(:sha256, token)) + assert user_token.user_cpf == user.cpf + assert user_token.sent_to == user.contato.email + assert user_token.context == "change:current@example.com" + end + end + + describe "update_user_email/2" do + setup do + user = user_fixture() + email = unique_user_email() + + contact = user.contato |> Map.delete([:id, :__struct__]) |> Map.put(:email, email) + + token = + extract_user_token(fn url -> + Accounts.deliver_update_email_instructions( + %{user | contato: contact}, + user.contato.email, + url + ) + end) + + %{user: user, token: token, email: email} + end + + test "updates the email with a valid token", %{user: user, token: token, email: email} do + assert Accounts.update_user_email(user, token) == :ok + changed_user = Accounts.get(user.cpf) + assert changed_user.contato.email != user.contato.email + assert changed_user.contato.email == email + assert changed_user.confirmed_at + assert changed_user.confirmed_at != user.confirmed_at + refute Repo.get_by(UserToken, user_cpf: user.cpf) + end + + test "does not update email with invalid token", %{user: user} do + assert Accounts.update_user_email(user, "oops") == :error + assert Accounts.get(user.cpf).contato.email == user.contato.email + assert Repo.get_by(UserToken, user_cpf: user.cpf) + end + + test "does not update email if user email changed", %{user: user, token: token} do + contact = %{user.contato | email: "current@example.com"} + assert Accounts.update_user_email(%{user | contato: contact}, token) == :error + assert Accounts.get(user.cpf).contato.email == user.contato.email + assert Repo.get_by(UserToken, user_cpf: user.cpf) + end + + test "does not update email if token expired", %{user: user, token: token} do + {1, nil} = Repo.update_all(UserToken, set: [created_at: ~N[2020-01-01 00:00:00]]) + assert Accounts.update_user_email(user, token) == :error + assert Accounts.get(user.cpf).contato.email == user.contato.email + assert Repo.get_by(UserToken, user_cpf: user.cpf) + end + end + + describe "change_user_password/2" do + test "returns a user changeset" do + assert %Ecto.Changeset{} = changeset = Accounts.change_user_password(%User{}) + assert changeset.required == [:password] + end + + test "allows fields to be set" do + changeset = + Accounts.change_user_password(%User{}, %{ + "password" => "New valid password!", + "password_confirmation" => "New valid password!" + }) + + assert changeset.valid? + assert get_change(changeset, :password) == "New valid password!" + refute get_change(changeset, :password_hash) + end + end + + describe "update_user_password/3" do + setup do + %{user: user_fixture()} + end + + test "validates password", %{user: user} do + {:error, changeset} = + Accounts.update_user_password(user, valid_user_password(), %{ + password: "not valid", + password_confirmation: "another" + }) + + assert %{ + password: [ + "at least one digit or punctuation character", + "at least one upper case character", + "should be at least 12 character(s)" + ], + password_confirmation: ["does not match password"] + } = errors_on(changeset) + end + + test "validates maximum values for password for security", %{user: user} do + too_long = String.duplicate("db", 100) + + {:error, changeset} = + Accounts.update_user_password(user, valid_user_password(), %{password: too_long}) + + assert "should be at most 72 character(s)" in errors_on(changeset).password + end + + test "validates current password", %{user: user} do + {:error, changeset} = + Accounts.update_user_password(user, "invalid", %{password: valid_user_password()}) + + assert %{current_password: ["is not valid"]} = errors_on(changeset) + end + + test "updates the password", %{user: user} do + {:ok, user} = + Accounts.update_user_password(user, valid_user_password(), %{ + password: "New valid password!", + password_confirmation: "New valid password!" + }) + + assert is_nil(user.password) + assert Accounts.get_user_by_email_and_password(user.contato.email, "New valid password!") + end + + test "deletes all tokens for the given user", %{user: user} do + _ = Accounts.generate_user_session_token(user) + + {:ok, _} = + Accounts.update_user_password(user, valid_user_password(), %{ + password: "New valid password!", + password_confirmation: "New valid password!" + }) + + refute Repo.get_by(UserToken, user_cpf: user.cpf) + end + end + + describe "generate_user_session_token/1" do + setup do + %{user: insert(:user)} + end + + test "generates a token", %{user: user} do + token = Accounts.generate_user_session_token(user) + assert user_token = Repo.get_by(UserToken, token: token) + assert user_token.context == "session" + + # Creating the same token for another user should fail + assert_raise Ecto.ConstraintError, fn -> + Repo.insert!(%UserToken{ + token: user_token.token, + user_cpf: user_fixture().cpf, + context: "session" + }) + end + end + end + + describe "get_user_by_session_token/1" do + setup do + user = insert(:user) + token = Accounts.generate_user_session_token(user) + %{user: user, token: token} + end + + test "returns user by token", %{user: user, token: token} do + assert session_user = Accounts.get_user_by_session_token(token) + assert session_user.cpf == user.cpf + end + + test "does not return user for invalid token" do + refute Accounts.get_user_by_session_token("oops") + end + + test "does not return user for expired token", %{token: token} do + {1, nil} = Repo.update_all(UserToken, set: [created_at: ~N[2020-01-01 00:00:00]]) + refute Accounts.get_user_by_session_token(token) + end + end + + describe "delete_session_token/1" do + test "deletes the token" do + user = insert(:user) + token = Accounts.generate_user_session_token(user) + assert Accounts.delete_session_token(token) == :ok + refute Accounts.get_user_by_session_token(token) + end + end + + describe "deliver_user_confirmation_instructions/2" do + setup do + %{user: user_fixture()} + end + + test "sends token through notification", %{user: user} do + token = + extract_user_token(fn url -> + Accounts.deliver_user_confirmation_instructions(user, url) + end) + + {:ok, token} = Base.url_decode64(token, padding: false) + assert user_token = Repo.get_by(UserToken, token: :crypto.hash(:sha256, token)) + assert user_token.user_cpf == user.cpf + assert user_token.sent_to == user.contato.email + assert user_token.context == "confirm" + end + end + + describe "confirm_user/1" do + setup do + user = user_fixture() + + token = + extract_user_token(fn url -> + Accounts.deliver_user_confirmation_instructions(user, url) + end) + + %{user: user, token: token} + end + + test "confirms the email with a valid token", %{user: user, token: token} do + assert {:ok, confirmed_user} = Accounts.confirm_user(token) + assert confirmed_user.confirmed_at + assert confirmed_user.confirmed_at != user.confirmed_at + assert Repo.get!(User, user.cpf).confirmed_at + refute Repo.get_by(UserToken, user_cpf: user.cpf) + end + + test "does not confirm with invalid token", %{user: user} do + assert Accounts.confirm_user("oops") == :error + refute Repo.get!(User, user.cpf).confirmed_at + assert Repo.get_by(UserToken, user_cpf: user.cpf) + end + + test "does not confirm email if token expired", %{user: user, token: token} do + {1, nil} = Repo.update_all(UserToken, set: [created_at: ~N[2020-01-01 00:00:00]]) + assert Accounts.confirm_user(token) == :error + refute Repo.get!(User, user.cpf).confirmed_at + assert Repo.get_by(UserToken, user_cpf: user.cpf) + end + end + + describe "deliver_user_reset_password_instructions/2" do + setup do + %{user: user_fixture()} + end + + test "sends token through notification", %{user: user} do + token = + extract_user_token(fn url -> + Accounts.deliver_user_reset_password_instructions(user, url) + end) + + {:ok, token} = Base.url_decode64(token, padding: false) + assert user_token = Repo.get_by(UserToken, token: :crypto.hash(:sha256, token)) + assert user_token.user_cpf == user.cpf + assert user_token.sent_to == user.contato.email + assert user_token.context == "reset_password" + end + end + + describe "get_user_by_reset_password_token/1" do + setup do + user = user_fixture() + + token = + extract_user_token(fn url -> + Accounts.deliver_user_reset_password_instructions(user, url) + end) + + %{user: user, token: token} + end + + test "returns the user with valid token", %{user: %{cpf: cpf}, token: token} do + assert %User{cpf: ^cpf} = Accounts.get_user_by_reset_password_token(token) + assert Repo.get_by(UserToken, user_cpf: cpf) + end + + test "does not return the user with invalid token", %{user: user} do + refute Accounts.get_user_by_reset_password_token("oops") + assert Repo.get_by(UserToken, user_cpf: user.cpf) + end + + test "does not return the user if token expired", %{user: user, token: token} do + {1, nil} = Repo.update_all(UserToken, set: [created_at: ~N[2020-01-01 00:00:00]]) + refute Accounts.get_user_by_reset_password_token(token) + assert Repo.get_by(UserToken, user_cpf: user.cpf) + end + end + + describe "reset_user_password/2" do + setup do + %{user: user_fixture()} + end + + test "validates password", %{user: user} do + {:error, changeset} = + Accounts.reset_user_password(user, %{ + password: "not valid", + password_confirmation: "another" + }) + + assert %{ + password: [ + "at least one digit or punctuation character", + "at least one upper case character", + "should be at least 12 character(s)" + ], + password_confirmation: ["does not match password"] + } = errors_on(changeset) + end + + test "validates maximum values for password for security", %{user: user} do + too_long = String.duplicate("db", 100) + {:error, changeset} = Accounts.reset_user_password(user, %{password: too_long}) + assert "should be at most 72 character(s)" in errors_on(changeset).password + end + + test "updates the password", %{user: user} do + {:ok, updated_user} = + Accounts.reset_user_password(user, %{ + password: "New valid password!", + password_confirmation: "New valid password!" + }) + + assert is_nil(updated_user.password) + assert Accounts.get_user_by_email_and_password(user.contato.email, "New valid password!") + end + + test "deletes all tokens for the given user", %{user: user} do + _ = Accounts.generate_user_session_token(user) + + {:ok, _} = + Accounts.reset_user_password(user, %{ + password: "New valid password!", + password_confirmation: "New valid password!" + }) + + refute Repo.get_by(UserToken, user_cpf: user.cpf) + end + end + + describe "inspect/2" do + test "does not include password" do + refute inspect(%User{password: "123456"}) =~ "password: \"123456\"" + end + end +end From d735b0e788d8b8d51fd2c884eb0af819050694ed Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Sun, 20 Feb 2022 20:48:21 -0300 Subject: [PATCH 05/22] feat: user routes --- lib/fuschia_web/router.ex | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/lib/fuschia_web/router.ex b/lib/fuschia_web/router.ex index afd2b628..a9650e25 100644 --- a/lib/fuschia_web/router.ex +++ b/lib/fuschia_web/router.ex @@ -1,6 +1,8 @@ defmodule FuschiaWeb.Router do use FuschiaWeb, :router + import FuschiaWeb.UserAuth + import Surface.Catalogue.Router pipeline :browser do @@ -10,6 +12,7 @@ defmodule FuschiaWeb.Router do plug :put_root_layout, {FuschiaWeb.LayoutView, :root} plug :protect_from_forgery plug :put_secure_browser_headers + plug :fetch_current_user end pipeline :api do @@ -60,4 +63,40 @@ defmodule FuschiaWeb.Router do forward "/mailbox", Plug.Swoosh.MailboxPreview end end + + ## Authentication routes + + scope "/", FuschiaWeb do + pipe_through [:browser, :redirect_if_user_is_authenticated] + + get "/cadastrar", UserRegistrationController, :new + post "/cadastrar", UserRegistrationController, :create + get "/acessar", UserSessionController, :new + post "/acessar", UserSessionController, :create + get "/resetar_senha", UserResetPasswordController, :new + post "/resetar_senha", UserResetPasswordController, :create + get "/resetar_senha/:token", UserResetPasswordController, :edit + put "/resetar_senha/:token", UserResetPasswordController, :update + end + + scope "/app", FuschiaWeb do + pipe_through [:browser, :require_authenticated_user] + + get "/pesquisadore/configuracoes", UserSettingsController, :edit + put "/pesquisadore/configuracoes", UserSettingsController, :update + + get "/pesquisadore/configuracoes/confirmar_email/:token", + UserSettingsController, + :confirm_email + end + + scope "/app", FuschiaWeb do + pipe_through [:browser] + + delete "/desconectar", UserSessionController, :delete + get "/confirmar", UserConfirmationController, :new + post "/confirmar", UserConfirmationController, :create + get "/confirmar/:token", UserConfirmationController, :edit + post "/confirmar/:token", UserConfirmationController, :update + end end From 7dfeca769486e590842cd8d2589957c530207e36 Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Sun, 20 Feb 2022 20:49:20 -0300 Subject: [PATCH 06/22] feat: fix fuschia tests and add support functions for integration tests --- test/fuschia/context/users_test.exs | 2 +- test/fuschia/entities/pesquisador_test.exs | 4 +-- test/fuschia/entities/user_test.exs | 4 +-- test/fuschia/queries/pesquisadores_test.exs | 2 +- test/support/conn_case.ex | 27 +++++++++++++++++++ test/support/factories/pesquisador_factory.ex | 2 +- test/support/factories/user_factory.ex | 17 +++++++++--- 7 files changed, 47 insertions(+), 11 deletions(-) diff --git a/test/fuschia/context/users_test.exs b/test/fuschia/context/users_test.exs index 3933c22b..1a5db410 100644 --- a/test/fuschia/context/users_test.exs +++ b/test/fuschia/context/users_test.exs @@ -4,7 +4,7 @@ defmodule Fuschia.Context.UsersTest do import Fuschia.Factory alias Fuschia.Context.Users - alias Fuschia.Entities.User + alias Fuschia.Accounts.User @moduletag :unit diff --git a/test/fuschia/entities/pesquisador_test.exs b/test/fuschia/entities/pesquisador_test.exs index 50022564..11ed90eb 100644 --- a/test/fuschia/entities/pesquisador_test.exs +++ b/test/fuschia/entities/pesquisador_test.exs @@ -1,9 +1,9 @@ -defmodule Fuschia.Entities.PesquisadorTest do +defmodule Fuschia.Accounts.PesquisadorTest do use Fuschia.DataCase, async: true import Fuschia.Factory - alias Fuschia.Entities.Pesquisador + alias Fuschia.Accounts.Pesquisador @moduletag :unit diff --git a/test/fuschia/entities/user_test.exs b/test/fuschia/entities/user_test.exs index 92d3b52d..29368e9a 100644 --- a/test/fuschia/entities/user_test.exs +++ b/test/fuschia/entities/user_test.exs @@ -1,9 +1,9 @@ -defmodule Fuschia.Entities.UserTest do +defmodule Fuschia.Accounts.UserTest do use Fuschia.DataCase, async: true import Fuschia.Factory - alias Fuschia.Entities.User + alias Fuschia.Accounts.User @moduletag :unit diff --git a/test/fuschia/queries/pesquisadores_test.exs b/test/fuschia/queries/pesquisadores_test.exs index 10eb5bfc..85aacab4 100644 --- a/test/fuschia/queries/pesquisadores_test.exs +++ b/test/fuschia/queries/pesquisadores_test.exs @@ -4,7 +4,7 @@ defmodule Fuschia.Queries.PesquisadoresTest do import Fuschia.Factory alias Fuschia.Db - alias Fuschia.Entities.Pesquisador + alias Fuschia.Accounts.Pesquisador alias Fuschia.Queries.Pesquisadores @moduletag :unit diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index f79bbfa9..7fac1088 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -18,6 +18,7 @@ defmodule FuschiaWeb.ConnCase do use ExUnit.CaseTemplate alias Ecto.Adapters.SQL.Sandbox + alias Fuschia.Factory using do quote do @@ -40,4 +41,30 @@ defmodule FuschiaWeb.ConnCase do {:ok, conn: Phoenix.ConnTest.build_conn()} end + + @doc """ + Assistente de configuração que registra e efetua login do usuário. + + setup :register_and_log_in_user + + Armazena uma conexão atualizada e um usuário cadastrado no + contexto de teste. + """ + def register_and_log_in_user(%{conn: conn}) do + user = Factory.insert(:user) + %{conn: log_in_user(conn, user), user: user} + end + + @doc """ + Registra o `usuário` fornecido no `conn`. + + Ele retorna um `conn` atualizado. + """ + def log_in_user(conn, user) do + token = Fuschia.Accounts.generate_user_session_token(user) + + conn + |> Phoenix.ConnTest.init_test_session(%{}) + |> Plug.Conn.put_session(:user_token, token) + end end diff --git a/test/support/factories/pesquisador_factory.ex b/test/support/factories/pesquisador_factory.ex index e507a0a4..042fa3a6 100644 --- a/test/support/factories/pesquisador_factory.ex +++ b/test/support/factories/pesquisador_factory.ex @@ -3,7 +3,7 @@ defmodule Fuschia.PesquisadorFactory do defmacro __using__(_opts) do quote do - alias Fuschia.Entities.Pesquisador + alias Fuschia.Accounts.Pesquisador @spec pesquisador_factory :: Pesquisador.t() def pesquisador_factory do diff --git a/test/support/factories/user_factory.ex b/test/support/factories/user_factory.ex index 90cfcd44..7dacfae9 100644 --- a/test/support/factories/user_factory.ex +++ b/test/support/factories/user_factory.ex @@ -3,20 +3,29 @@ defmodule Fuschia.UserFactory do defmacro __using__(_opts) do quote do - alias Fuschia.Entities.User + alias Fuschia.Accounts.User + + def unique_user_email, do: "user#{System.unique_integer()}@example.com" + def valid_user_password, do: "Hello World 42!" @spec user_factory :: User.t() def user_factory do %User{ - perfil: sequence(:perfil, ["avulso", "pesquisador"]), + role: sequence(:role, ["avulso", "pesquisador"]), nome_completo: sequence(:nome_completo, &"User #{&1}"), - ativo: true, + ativo?: true, cpf: sequence(:cpf, ["325.956.490-00", "726.541.170-65"]), data_nascimento: sequence(:data_nascimento, [~D[2001-07-27], ~D[2001-07-28]]), - password_hash: "$2b$12$iWNYYuxNcQhaUuJ82jLKu..jbrQQl8..it6K5AvdVovOwDmLX2OVu", + password_hash: "$2b$12$AZdxCkw/Rb5AlI/5S7Ebb.hIyG.ocs18MGkHAW2gdZibH7a1wHTyu", contato: build(:contato) } end + + def extract_user_token(fun) do + {:ok, captured_email} = fun.(&"[TOKEN]#{&1}[TOKEN]") + [_, token | _] = String.split(captured_email.url, "[TOKEN]") + token + end end end end From b2edacf2be7d6e1c0da4d5fe632658b0c704b548 Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Sun, 20 Feb 2022 20:50:57 -0300 Subject: [PATCH 07/22] feat: add live helpers and plug to ensure user role --- .iex.exs | 16 +++++--- lib/fuschia_web.ex | 4 ++ lib/fuschia_web/live/live_helpers.ex | 36 ++++++++++++++++++ lib/fuschia_web/plugs/ensure_role_plug.ex | 46 +++++++++++++++++++++++ lib/fuschia_web/plugs/locale.ex | 2 +- lib/fuschia_web/views/error_helpers.ex | 10 +++++ 6 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 lib/fuschia_web/live/live_helpers.ex create mode 100644 lib/fuschia_web/plugs/ensure_role_plug.ex diff --git a/.iex.exs b/.iex.exs index db47fd1e..34a46024 100644 --- a/.iex.exs +++ b/.iex.exs @@ -1,4 +1,12 @@ -alias Fuschia.{Context, Entities, Queries} +alias Fuschia.{ + Accounts, + Accounts.Pesquisador, + Accounts.User, + Context, + Database, + Entities, + Queries +} alias Fuschia.Queries.{ Campi, @@ -10,8 +18,6 @@ alias Fuschia.Queries.{ Relatorios } -alias Context.Users - alias Fuschia.Entities.{ Campus, Cidade, @@ -19,9 +25,7 @@ alias Fuschia.Entities.{ LinhaPesquisa, Midia, Nucleo, - Pesquisador, - Relatorio, - User + Relatorio } alias Fuschia.Repo diff --git a/lib/fuschia_web.ex b/lib/fuschia_web.ex index fa8270e6..6d9f773a 100644 --- a/lib/fuschia_web.ex +++ b/lib/fuschia_web.ex @@ -48,6 +48,8 @@ defmodule FuschiaWeb do # Include shared imports and aliases for views unquote(view_helpers()) + import Surface + use Surface.View, root: "lib/fuschia_web/templates" end @@ -59,6 +61,8 @@ defmodule FuschiaWeb do use Surface.LiveView, layout: {FuschiaWeb.LayoutView, "live.html"} + import FuschiaWeb.LiveHelpers + unquote(view_helpers()) end end diff --git a/lib/fuschia_web/live/live_helpers.ex b/lib/fuschia_web/live/live_helpers.ex new file mode 100644 index 00000000..bf7ecdd1 --- /dev/null +++ b/lib/fuschia_web/live/live_helpers.ex @@ -0,0 +1,36 @@ +defmodule FuschiaWeb.LiveHelpers do + @moduledoc """ + Funções comuns ao contexto LiveView, + como autenticação por exemplo + """ + + import FuschiaWeb.Gettext + import Phoenix.LiveView + + alias Fuschia.Accounts + alias Fuschia.Accounts.User + alias FuschiaWeb.Router.Helpers, as: Routes + + def assign_defaults(session, socket) do + socket = + assign_new(socket, :current_user, fn -> + find_current_user(session) + end) + + case socket.assigns.current_user do + %User{} -> + socket + + _other -> + socket + |> put_flash(:error, dgettext("errors", "You must log in to access this page.")) + |> redirect(to: Routes.user_session_path(socket, :new)) + end + end + + defp find_current_user(session) do + with session_token when not is_nil(session_token) <- session["user_token"], + %User{} = user <- Accounts.get_user_by_session_token(session_token), + do: user + end +end diff --git a/lib/fuschia_web/plugs/ensure_role_plug.ex b/lib/fuschia_web/plugs/ensure_role_plug.ex new file mode 100644 index 00000000..e3f18506 --- /dev/null +++ b/lib/fuschia_web/plugs/ensure_role_plug.ex @@ -0,0 +1,46 @@ +defmodule FuschiaWeb.EnsureRolePlug do + @moduledoc """ + Esse plug certifica que um susuário possui um perfil antes de + acessar uma rota. + + ## Example + + plug FuschiaWeb.EnsureRolePlug, :admin + """ + + import FuschiaWeb.Gettext + import Plug.Conn + + alias Fuschia.Accounts + alias Fuschia.Accounts.User + alias FuschiaWeb.UserAuth + alias Phoenix.Controller + + def init(config), do: config + + def call(conn, roles) do + user_token = get_session(conn, :user_token) + + (user_token && + Accounts.get_user_by_session_token(user_token)) + |> has_role?(roles) + |> maybe_halt(conn) + end + + defp has_role?(%User{} = user, roles) when is_list(roles), + do: Enum.any?(roles, &has_role?(user, &1)) + + # Essa cláusula só será exectada quando o ambos + # os argumentos com alias `role` tiverem o mesmo valor + defp has_role?(%User{role: role}, role), do: true + defp has_role?(_user, _role), do: false + + defp maybe_halt(true, conn), do: conn + + defp maybe_halt(_any, conn) do + conn + |> Controller.put_flash(:error, dgettext("errors", "Unauthorized")) + |> Controller.redirect(to: UserAuth.signed_in_path(conn)) + |> halt() + end +end diff --git a/lib/fuschia_web/plugs/locale.ex b/lib/fuschia_web/plugs/locale.ex index e8620535..30d7e69d 100644 --- a/lib/fuschia_web/plugs/locale.ex +++ b/lib/fuschia_web/plugs/locale.ex @@ -5,7 +5,7 @@ defmodule FuschiaWeb.LocalePlug do import Plug.Conn - @default_locale "en" + @default_locale "pt_BR" @spec init(map) :: map def init(default), do: default diff --git a/lib/fuschia_web/views/error_helpers.ex b/lib/fuschia_web/views/error_helpers.ex index 8324f72f..b083e436 100644 --- a/lib/fuschia_web/views/error_helpers.ex +++ b/lib/fuschia_web/views/error_helpers.ex @@ -2,6 +2,16 @@ defmodule FuschiaWeb.ErrorHelpers do @moduledoc """ Conveniences for translating and building error messages. """ + use Phoenix.HTML + + @doc """ + Generates tag for inlined form input errors. + """ + def error_tag(form, field) do + if error = form.errors[field] do + content_tag(:span, translate_error(error), class: "help-block") + end + end @doc """ Translates an error message using gettext. From c4cc2e705e9e7ac79427109e7122f9e58a70b0c6 Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Sun, 20 Feb 2022 20:51:30 -0300 Subject: [PATCH 08/22] feat: fix guardian user modules --- lib/fuschia_web/auth/guardian.ex | 10 +++++----- lib/fuschia_web/controllers/auth_controller.ex | 7 ++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/fuschia_web/auth/guardian.ex b/lib/fuschia_web/auth/guardian.ex index ee28c149..9e8e2534 100644 --- a/lib/fuschia_web/auth/guardian.ex +++ b/lib/fuschia_web/auth/guardian.ex @@ -5,8 +5,8 @@ defmodule FuschiaWeb.Auth.Guardian do use Guardian, otp_app: :fuschia - alias Fuschia.Context.Users - alias Fuschia.Entities.User + alias Fuschia.Accounts + alias Fuschia.Accounts.User @spec subject_for_token(User.t(), map) :: {:ok, String.t()} def subject_for_token(user, _claims) do @@ -20,14 +20,14 @@ defmodule FuschiaWeb.Auth.Guardian do user = claims |> Map.get("sub") - |> Users.one() + |> Accounts.get() {:ok, user} end @spec authenticate(map) :: {:ok, String.t()} | {:error, :unauthorized} def authenticate(%{"cpf" => cpf, "password" => password}) do - case Users.one_by_cpf(cpf) do + case Accounts.get(cpf) do nil -> {:error, :unauthorized} user -> validate_password(user, password) end @@ -35,7 +35,7 @@ defmodule FuschiaWeb.Auth.Guardian do @spec user_claims(map) :: {:ok, User.t()} | {:error, :unauthorized} def user_claims(%{"cpf" => cpf}) do - case Users.one_with_permissions(cpf) do + case Accounts.get(cpf) do nil -> {:error, :unauthorized} user -> {:ok, User.for_jwt(user)} end diff --git a/lib/fuschia_web/controllers/auth_controller.ex b/lib/fuschia_web/controllers/auth_controller.ex index e59218ba..234b1167 100644 --- a/lib/fuschia_web/controllers/auth_controller.ex +++ b/lib/fuschia_web/controllers/auth_controller.ex @@ -6,7 +6,8 @@ defmodule FuschiaWeb.AuthController do use FuschiaWeb, :controller use OpenApiSpex.ControllerSpecs - alias Fuschia.Context.{AuthLogs, Users} + alias Fuschia.Accounts + alias Fuschia.Context.AuthLogs alias FuschiaWeb.Auth.Guardian alias FuschiaWeb.{RemoteIp, UserAgent} alias FuschiaWeb.Swagger.{AuthSchemas, Response, Security, UserSchemas} @@ -46,7 +47,7 @@ defmodule FuschiaWeb.AuthController do with {:ok, token} <- Guardian.authenticate(params), {:ok, user} <- Guardian.user_claims(params), :ok <- AuthLogs.create(ip, user_agent, user) do - render_response(%{user: Map.put(user, :token, token)}, conn) + render_response(%{user: user, token: token}, conn) end end @@ -64,7 +65,7 @@ defmodule FuschiaWeb.AuthController do @spec signup(Plug.Conn.t(), String.t(), String.t(), map) :: Plug.Conn.t() def signup(conn, _ip, _user_agent, params) do - with {:ok, user} <- Users.register(params) do + with {:ok, user} <- Accounts.register(params) do render_response(user, conn, :created) end end From 674321a0bc1c2ba98a574cbd6348fa4e31eba718 Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Sun, 20 Feb 2022 21:01:56 -0300 Subject: [PATCH 09/22] chore: improve users routes --- lib/fuschia_web/router.ex | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/fuschia_web/router.ex b/lib/fuschia_web/router.ex index a9650e25..d078179b 100644 --- a/lib/fuschia_web/router.ex +++ b/lib/fuschia_web/router.ex @@ -79,24 +79,29 @@ defmodule FuschiaWeb.Router do put "/resetar_senha/:token", UserResetPasswordController, :update end - scope "/app", FuschiaWeb do + scope "/apps", FuschiaWeb do pipe_through [:browser, :require_authenticated_user] - get "/pesquisadore/configuracoes", UserSettingsController, :edit - put "/pesquisadore/configuracoes", UserSettingsController, :update + scope "/usuarios" do + get "/:user_id/configuracoes", UserSettingsController, :edit + put "/:user_id/configuracoes", UserSettingsController, :update - get "/pesquisadore/configuracoes/confirmar_email/:token", + get "/:user_id/configuracoes/confirmar_email/:token", UserSettingsController, :confirm_email + end end scope "/app", FuschiaWeb do pipe_through [:browser] delete "/desconectar", UserSessionController, :delete - get "/confirmar", UserConfirmationController, :new - post "/confirmar", UserConfirmationController, :create - get "/confirmar/:token", UserConfirmationController, :edit - post "/confirmar/:token", UserConfirmationController, :update + + scope "/usuarios" do + get "/confirmar", UserConfirmationController, :new + post "/confirmar", UserConfirmationController, :create + get "/confirmar/:token", UserConfirmationController, :edit + post "/confirmar/:token", UserConfirmationController, :update + end end end From e272ec010676c1a119ea07a927d0d2938767eca1 Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Sun, 20 Feb 2022 21:22:19 -0300 Subject: [PATCH 10/22] fix: add `id` field to all public entities --- lib/fuschia/accounts/pesquisador.ex | 4 +- lib/fuschia/accounts/user.ex | 8 +- lib/fuschia/entities/campus.ex | 8 +- lib/fuschia/entities/cidade.ex | 6 +- lib/fuschia/entities/linha_pesquisa.ex | 6 +- lib/fuschia/entities/midia.ex | 6 +- lib/fuschia/entities/nucleo.ex | 6 +- lib/fuschia/entities/relatorio.ex | 6 +- lib/fuschia_web/controllers/user_auth.ex | 159 ++++++++++++++++++ lib/fuschia_web/router.ex | 10 +- ...220127223224_add_external_id_to_tables.exs | 4 +- test/support/factories/campus_factory.ex | 2 +- test/support/factories/cidade_factory.ex | 2 +- .../factories/linha_pesquisa_factory.ex | 2 +- test/support/factories/midia_factory.ex | 2 +- test/support/factories/nucleo_factory.ex | 2 +- test/support/factories/pesquisador_factory.ex | 2 +- test/support/factories/relatorio_factory.ex | 2 +- test/support/factories/user_factory.ex | 1 + 19 files changed, 201 insertions(+), 37 deletions(-) create mode 100644 lib/fuschia_web/controllers/user_auth.ex diff --git a/lib/fuschia/accounts/pesquisador.ex b/lib/fuschia/accounts/pesquisador.ex index 368fa62d..cf2e4535 100644 --- a/lib/fuschia/accounts/pesquisador.ex +++ b/lib/fuschia/accounts/pesquisador.ex @@ -23,7 +23,7 @@ defmodule Fuschia.Accounts.Pesquisador do @primary_key {:usuario_cpf, TrimmedString, autogenerate: false} schema "pesquisador" do - field :id_externo, :string + field :id, :string field :minibiografia, TrimmedString field :tipo_bolsa, TrimmedString field :link_lattes, TrimmedString @@ -67,7 +67,7 @@ defmodule Fuschia.Accounts.Pesquisador do |> cast_assoc(:usuario, required: true) |> foreign_key_constraint(:orientador_cpf) |> foreign_key_constraint(:campus_nome) - |> put_change(:id_externo, Nanoid.generate()) + |> put_change(:id, Nanoid.generate()) end @spec update_changeset(%__MODULE__{}, map) :: Ecto.Changeset.t() diff --git a/lib/fuschia/accounts/user.ex b/lib/fuschia/accounts/user.ex index 2bd81fab..034b24c3 100644 --- a/lib/fuschia/accounts/user.ex +++ b/lib/fuschia/accounts/user.ex @@ -30,6 +30,7 @@ defmodule Fuschia.Accounts.User do field :nome_completo, CapitalizedString field :ativo?, :boolean, default: true field :permissoes, :map, virtual: true + field :id, :string belongs_to :contato, Contato, on_replace: :update @@ -46,6 +47,7 @@ defmodule Fuschia.Accounts.User do |> validate_inclusion(:role, @valid_roles) |> cast_assoc(:contato, required: true) |> foreign_key_constraint(:cpf, name: :pesquisador_usuario_cpf_fkey) + |> put_change(:id, Nanoid.generate()) end @spec update_changeset(%__MODULE__{}, map) :: Ecto.Changeset.t() @@ -209,7 +211,8 @@ defmodule Fuschia.Accounts.User do perfil: struct.role, permissoes: struct.permissoes, cpf: struct.cpf, - dataNascimento: struct.data_nascimento + dataNascimento: struct.data_nascimento, + id: struct.id } end @@ -220,7 +223,8 @@ defmodule Fuschia.Accounts.User do ultimo_login: struct.last_seen, confirmado_em: struct.confirmed_at, ativo: struct.ativo?, - data_nascimento: struct.data_nascimento + data_nascimento: struct.data_nascimento, + id: struct.id } end diff --git a/lib/fuschia/entities/campus.ex b/lib/fuschia/entities/campus.ex index 0eb86aeb..efa7f8b5 100644 --- a/lib/fuschia/entities/campus.ex +++ b/lib/fuschia/entities/campus.ex @@ -13,7 +13,7 @@ defmodule Fuschia.Entities.Campus do @primary_key {:nome, CapitalizedString, autogenerate: false} schema "campus" do - field :id_externo, :string + field :id, :string belongs_to :cidade, Cidade, type: :string, @@ -33,7 +33,7 @@ defmodule Fuschia.Entities.Campus do |> validate_required(@required_fields) |> unique_constraint(:nome) |> cast_assoc(:cidade, required: true) - |> put_change(:id_externo, Nanoid.generate()) + |> put_change(:id, Nanoid.generate()) end @spec foreign_changeset(%__MODULE__{}, map) :: Ecto.Changeset.t() @@ -43,13 +43,13 @@ defmodule Fuschia.Entities.Campus do |> validate_required([:cidade_municipio | @required_fields]) |> unique_constraint([:nome, :cidade_municipio], name: :campus_nome_municipio_index) |> foreign_key_constraint(:cidade_municipio) - |> put_change(:id_externo, Nanoid.generate()) + |> put_change(:id, Nanoid.generate()) end @spec to_map(%__MODULE__{}) :: map def to_map(%__MODULE__{} = struct) do %{ - id: struct.id_externo, + id: struct.id, nome: struct.nome, cidade: struct.cidade, pesquisadores: struct.pesquisadores diff --git a/lib/fuschia/entities/cidade.ex b/lib/fuschia/entities/cidade.ex index 9db2eda4..7e4633eb 100644 --- a/lib/fuschia/entities/cidade.ex +++ b/lib/fuschia/entities/cidade.ex @@ -13,7 +13,7 @@ defmodule Fuschia.Entities.Cidade do @primary_key {:municipio, CapitalizedString, autogenerate: false} schema "cidade" do - field :id_externo, :string + field :id, :string has_many :campi, Campus, on_replace: :delete @@ -26,13 +26,13 @@ defmodule Fuschia.Entities.Cidade do |> cast(attrs, @required_fields) |> unique_constraint(:municipio) |> validate_required(@required_fields) - |> put_change(:id_externo, Nanoid.generate()) + |> put_change(:id, Nanoid.generate()) end @spec to_map(%__MODULE__{}) :: map def to_map(%__MODULE__{} = struct) do %{ - id: struct.id_externo, + id: struct.id, municipio: struct.municipio, campi: struct.campi } diff --git a/lib/fuschia/entities/linha_pesquisa.ex b/lib/fuschia/entities/linha_pesquisa.ex index 930ee8eb..d06c78d8 100644 --- a/lib/fuschia/entities/linha_pesquisa.ex +++ b/lib/fuschia/entities/linha_pesquisa.ex @@ -13,7 +13,7 @@ defmodule Fuschia.Entities.LinhaPesquisa do @primary_key {:numero, :integer, []} schema "linha_pesquisa" do - field :id_externo, :string + field :id, :string field :descricao_curta, TrimmedString field :descricao_longa, TrimmedString @@ -34,13 +34,13 @@ defmodule Fuschia.Entities.LinhaPesquisa do |> unique_constraint([:numero, :nucleo_nome]) |> validate_length(:descricao_curta, max: 50) |> validate_length(:descricao_longa, max: 280) - |> put_change(:id_externo, Nanoid.generate()) + |> put_change(:id, Nanoid.generate()) end @spec to_map(%__MODULE__{}) :: map def to_map(%__MODULE__{} = struct) do %{ - id: struct.id_externo, + id: struct.id, descricao_curta: struct.descricao_curta, descricao_longa: struct.descricao_longa, numero: struct.numero, diff --git a/lib/fuschia/entities/midia.ex b/lib/fuschia/entities/midia.ex index e1646872..54954a09 100644 --- a/lib/fuschia/entities/midia.ex +++ b/lib/fuschia/entities/midia.ex @@ -20,7 +20,7 @@ defmodule Fuschia.Entities.Midia do @primary_key {:link, TrimmedString, autogenerate: false} schema "midia" do - field :id_externo, :string + field :id, :string field :tipo, TrimmedString field :tags, {:array, TrimmedString} @@ -41,13 +41,13 @@ defmodule Fuschia.Entities.Midia do |> unique_constraint(:link) |> validate_inclusion(:tipo, @tipos_midia) |> foreign_key_constraint(:pesquisador_cpf) - |> put_change(:id_externo, Nanoid.generate()) + |> put_change(:id, Nanoid.generate()) end @spec to_map(%__MODULE__{}) :: map def to_map(%__MODULE__{} = struct) do %{ - id: struct.id_externo, + id: struct.id, tipo: struct.tipo, link: struct.link, tags: struct.tags diff --git a/lib/fuschia/entities/nucleo.ex b/lib/fuschia/entities/nucleo.ex index 1633a9f4..a45d8847 100644 --- a/lib/fuschia/entities/nucleo.ex +++ b/lib/fuschia/entities/nucleo.ex @@ -12,7 +12,7 @@ defmodule Fuschia.Entities.Nucleo do @primary_key {:nome, CapitalizedString, []} schema "nucleo" do - field :id_externo, :string + field :id, :string field :descricao, :string has_many :linhas_pesquisa, LinhaPesquisa @@ -27,13 +27,13 @@ defmodule Fuschia.Entities.Nucleo do |> cast(attrs, @required_fields) |> validate_required(@required_fields) |> validate_length(:descricao, max: 400) - |> put_change(:id_externo, Nanoid.generate()) + |> put_change(:id, Nanoid.generate()) end @spec to_map(%__MODULE__{}) :: map def to_map(%__MODULE__{} = struct) do %{ - id: struct.id_externo, + id: struct.id, nome: struct.nome, descricao: struct.descricao, linhas_pesquisa: struct.linhas_pesquisa diff --git a/lib/fuschia/entities/relatorio.ex b/lib/fuschia/entities/relatorio.ex index 701136ef..97bee1a7 100644 --- a/lib/fuschia/entities/relatorio.ex +++ b/lib/fuschia/entities/relatorio.ex @@ -22,7 +22,7 @@ defmodule Fuschia.Entities.Relatorio do @primary_key false schema "relatorio" do - field :id_externo, :string + field :id, :string field :ano, :integer, primary_key: true field :mes, :integer, primary_key: true field :tipo, TrimmedString @@ -46,7 +46,7 @@ defmodule Fuschia.Entities.Relatorio do |> validate_year(:ano) |> validate_inclusion(:tipo, @tipos) |> foreign_key_constraint(:pesquisador_cpf) - |> put_change(:id_externo, Nanoid.generate()) + |> put_change(:id, Nanoid.generate()) end defp validate_month(changeset, field) do @@ -76,7 +76,7 @@ defmodule Fuschia.Entities.Relatorio do @spec to_map(%__MODULE__{}) :: map def to_map(%__MODULE__{} = struct) do %{ - id: struct.id_externo, + id: struct.id, ano: struct.ano, mes: struct.mes, tipo: struct.tipo, diff --git a/lib/fuschia_web/controllers/user_auth.ex b/lib/fuschia_web/controllers/user_auth.ex new file mode 100644 index 00000000..0fcabe3f --- /dev/null +++ b/lib/fuschia_web/controllers/user_auth.ex @@ -0,0 +1,159 @@ +defmodule FuschiaWeb.UserAuth do + import Plug.Conn + import Phoenix.Controller + + alias Fuschia.Accounts + alias Fuschia.Accounts.User + alias FuschiaWeb.Router.Helpers, as: Routes + + # Faça o cookie lembrar do usuário ser válido por 60 dias. + # Se você quiser aumentar ou reduzir esse valor, altere também + # a própria expiração do token em UserToken. + @max_age 60 * 60 * 24 * 60 + @remember_me_cookie "_fuschia_web_user_remember_me" + @remember_me_options [sign: true, max_age: @max_age, same_site: "Lax"] + + @doc """ + Define para qual rota o usuário será + redirecionado após o login + """ + def signed_in_path(conn) do + with user_token when is_binary(user_token) <- get_session(conn, :user_token), + %User{id: user_id} <- Accounts.get_user_by_session_token(user_token) do + "/app/pesquisadores/#{user_id}" + else + _e -> "/not_found" + end + end + + @doc """ + Faz o login do usuário. + + Renova o ID da sessão e limpa toda a sessão + para evitar ataques de fixação. Veja a renovação_sessão + função para personalizar esse comportamento. + + Ele também define uma chave `:live_socket_id` na sessão, + para que as sessões do LiveView sejam identificadas e automaticamente + desconectado no logout. + """ + def log_in_user(conn, user, params \\ %{}) do + token = Accounts.generate_user_session_token(user) + user_return_to = get_session(conn, :user_return_to) + + conn + |> renew_session() + |> put_session(:user_token, token) + |> put_session(:live_socket_id, "user_sessions:#{Base.url_encode64(token)}") + |> maybe_write_remember_me_cookie(token, params) + |> redirect(to: user_return_to || signed_in_path(conn)) + end + + defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do + put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options) + end + + defp maybe_write_remember_me_cookie(conn, _token, _params) do + conn + end + + # Esta função renova o ID da sessão e apaga todo o + # sessão para evitar ataques de fixação. Se houver algum dado + # na sessão que você deseja preservar após o login/logout, + # você deve buscar explicitamente os dados da sessão antes de limpar + # e, em seguida, defina-o imediatamente após a limpeza, por exemplo: + # + # defp renew_session(conn) do + # preferred_locale = get_session(conn, :preferred_locale) + # + # conn + # |> configure_session(renew: true) + # |> clear_session() + # |> put_session(:preferred_locale, preferred_locale) + # end + # + defp renew_session(conn) do + conn + |> configure_session(renew: true) + |> clear_session() + end + + @doc """ + Desconecta o usuário. + + Ele limpa todos os dados da sessão por segurança. Consulte renovação_sessão. + """ + def log_out_user(conn) do + user_token = get_session(conn, :user_token) + user_token && Accounts.delete_session_token(user_token) + + if live_socket_id = get_session(conn, :live_socket_id) do + FuschiaWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{}) + end + + conn + |> renew_session() + |> delete_resp_cookie(@remember_me_cookie) + |> redirect(to: "/") + end + + @doc """ + Autentica o usuário olhando para a sessão e lembre-se do token do usuário. + """ + def fetch_current_user(conn, _opts) do + {user_token, conn} = ensure_user_token(conn) + user = user_token && Accounts.get_user_by_session_token(user_token) + assign(conn, :current_user, user) + end + + defp ensure_user_token(conn) do + if user_token = get_session(conn, :user_token) do + {user_token, conn} + else + conn = fetch_cookies(conn, signed: [@remember_me_cookie]) + + if user_token = conn.cookies[@remember_me_cookie] do + {user_token, put_session(conn, :user_token, user_token)} + else + {nil, conn} + end + end + end + + @doc """ + Usado para rotas que exigem que o usuário não seja autenticado. + """ + def redirect_if_user_is_authenticated(conn, _opts) do + if conn.assigns[:current_user] do + conn + |> redirect(to: signed_in_path(conn)) + |> halt() + else + conn + end + end + + @doc """ + Usado para rotas que exigem que o usuário seja autenticado. + + Se você deseja impor que o e-mail do usuário esteja confirmado antes + deles usam o aplicativo, aqui seria um bom lugar. + """ + def require_authenticated_user(conn, _opts) do + if conn.assigns[:current_user] do + conn + else + conn + |> put_flash(:error, "Você precisa realizar login para acessar esta página") + |> maybe_store_return_to() + |> redirect(to: Routes.user_session_path(conn, :new)) + |> halt() + end + end + + defp maybe_store_return_to(%{method: "GET"} = conn) do + put_session(conn, :user_return_to, current_path(conn)) + end + + defp maybe_store_return_to(conn), do: conn +end diff --git a/lib/fuschia_web/router.ex b/lib/fuschia_web/router.ex index d078179b..91953266 100644 --- a/lib/fuschia_web/router.ex +++ b/lib/fuschia_web/router.ex @@ -83,12 +83,12 @@ defmodule FuschiaWeb.Router do pipe_through [:browser, :require_authenticated_user] scope "/usuarios" do - get "/:user_id/configuracoes", UserSettingsController, :edit - put "/:user_id/configuracoes", UserSettingsController, :update + get "/:user_id/configuracoes", UserSettingsController, :edit + put "/:user_id/configuracoes", UserSettingsController, :update - get "/:user_id/configuracoes/confirmar_email/:token", - UserSettingsController, - :confirm_email + get "/:user_id/configuracoes/confirmar_email/:token", + UserSettingsController, + :confirm_email end end diff --git a/priv/repo/migrations/20220127223224_add_external_id_to_tables.exs b/priv/repo/migrations/20220127223224_add_external_id_to_tables.exs index 56fb1f29..1d5f8110 100644 --- a/priv/repo/migrations/20220127223224_add_external_id_to_tables.exs +++ b/priv/repo/migrations/20220127223224_add_external_id_to_tables.exs @@ -4,12 +4,12 @@ defmodule Fuschia.Repo.Migrations.AddExternalIdToTables do def change do for table_name <- public_tables() do alter table(table_name) do - add :id_externo, :string, null: false + add :id, :string, null: false end end end defp public_tables do - ~w(campus cidade linha_pesquisa midia nucleo pesquisador relatorio) + ~w(campus cidade linha_pesquisa midia nucleo pesquisador relatorio user) end end diff --git a/test/support/factories/campus_factory.ex b/test/support/factories/campus_factory.ex index 4bbeaf16..9b5eada0 100644 --- a/test/support/factories/campus_factory.ex +++ b/test/support/factories/campus_factory.ex @@ -10,7 +10,7 @@ defmodule Fuschia.CampusFactory do cidade = insert(:cidade) %Campus{ - id_externo: Nanoid.generate_non_secure(), + id: Nanoid.generate_non_secure(), nome: sequence(:nome, &"Campus #{&1}"), cidade: cidade } diff --git a/test/support/factories/cidade_factory.ex b/test/support/factories/cidade_factory.ex index 1ecc974e..88e420a1 100644 --- a/test/support/factories/cidade_factory.ex +++ b/test/support/factories/cidade_factory.ex @@ -8,7 +8,7 @@ defmodule Fuschia.CidadeFactory do @spec cidade_factory :: Cidade.t() def cidade_factory do %Cidade{ - id_externo: Nanoid.generate_non_secure(), + id: Nanoid.generate_non_secure(), municipio: sequence(:municipio, &"Cidade #{&1}") } end diff --git a/test/support/factories/linha_pesquisa_factory.ex b/test/support/factories/linha_pesquisa_factory.ex index 30b75470..f45b0d92 100644 --- a/test/support/factories/linha_pesquisa_factory.ex +++ b/test/support/factories/linha_pesquisa_factory.ex @@ -10,7 +10,7 @@ defmodule Fuschia.LinhaPesquisaFactory do nucleo = insert(:nucleo) %LinhaPesquisa{ - id_externo: Nanoid.generate_non_secure(), + id: Nanoid.generate_non_secure(), numero: sequence(:numero, Enum.to_list(1..21)), descricao_curta: sequence(:descricao_curta, &"Descricao LinhaPesquisa Curta #{&1}"), descricao_longa: sequence(:descricao_longa, &"Descricao LinhaPesquisa Longa #{&1}"), diff --git a/test/support/factories/midia_factory.ex b/test/support/factories/midia_factory.ex index f8eb1798..8bdb48d2 100644 --- a/test/support/factories/midia_factory.ex +++ b/test/support/factories/midia_factory.ex @@ -10,7 +10,7 @@ defmodule Fuschia.MidiaFactory do pesquisador = insert(:pesquisador) %Midia{ - id_externo: Nanoid.generate_non_secure(), + id: Nanoid.generate_non_secure(), pesquisador: pesquisador, tipo: sequence(:tipo, ["video", "documento", "imagem"]), link: sequence(:link, &"https://example#{&1}.com"), diff --git a/test/support/factories/nucleo_factory.ex b/test/support/factories/nucleo_factory.ex index e4c8e5db..7c388adc 100644 --- a/test/support/factories/nucleo_factory.ex +++ b/test/support/factories/nucleo_factory.ex @@ -8,7 +8,7 @@ defmodule Fuschia.NucleoFactory do @spec nucleo_factory :: Nucleo.t() def nucleo_factory do %Nucleo{ - id_externo: Nanoid.generate_non_secure(), + id: Nanoid.generate_non_secure(), nome: sequence(:nome, &"Nucleo #{&1}"), descricao: sequence(:descricao, &"Descricao Nucleo #{&1}") } diff --git a/test/support/factories/pesquisador_factory.ex b/test/support/factories/pesquisador_factory.ex index 042fa3a6..de4233a9 100644 --- a/test/support/factories/pesquisador_factory.ex +++ b/test/support/factories/pesquisador_factory.ex @@ -11,7 +11,7 @@ defmodule Fuschia.PesquisadorFactory do usuario = build(:user) %Pesquisador{ - id_externo: Nanoid.generate_non_secure(), + id: Nanoid.generate_non_secure(), usuario: usuario, minibiografia: sequence(:minibiografia, &"Esta e minha minibiografia gerada: #{&1}"), tipo_bolsa: sequence(:tipo_bolsa, ["ic", "pesquisa", "voluntario"]), diff --git a/test/support/factories/relatorio_factory.ex b/test/support/factories/relatorio_factory.ex index b4fc2846..6ffc28cb 100644 --- a/test/support/factories/relatorio_factory.ex +++ b/test/support/factories/relatorio_factory.ex @@ -10,7 +10,7 @@ defmodule Fuschia.RelatorioFactory do pesquisador = insert(:pesquisador) %Relatorio{ - id_externo: Nanoid.generate_non_secure(), + id: Nanoid.generate_non_secure(), pesquisador: pesquisador, tipo: sequence(:tipo, ["mensal", "trimestral", "anual"]), link: sequence(:link, &"https://example#{&1}.com"), diff --git a/test/support/factories/user_factory.ex b/test/support/factories/user_factory.ex index 7dacfae9..40d06eaa 100644 --- a/test/support/factories/user_factory.ex +++ b/test/support/factories/user_factory.ex @@ -11,6 +11,7 @@ defmodule Fuschia.UserFactory do @spec user_factory :: User.t() def user_factory do %User{ + id: Nanoid.generate_non_secure(), role: sequence(:role, ["avulso", "pesquisador"]), nome_completo: sequence(:nome_completo, &"User #{&1}"), ativo?: true, From 4293911fd89ebf60f38904c390f4d8c2dc3e994e Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Sun, 20 Feb 2022 21:24:55 -0300 Subject: [PATCH 11/22] chore: remove `id_externo` from `pesquisador` --- lib/fuschia/accounts/pesquisador.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fuschia/accounts/pesquisador.ex b/lib/fuschia/accounts/pesquisador.ex index cf2e4535..5840eab0 100644 --- a/lib/fuschia/accounts/pesquisador.ex +++ b/lib/fuschia/accounts/pesquisador.ex @@ -85,7 +85,7 @@ defmodule Fuschia.Accounts.Pesquisador do @spec to_map(%__MODULE__{}) :: map def to_map(%__MODULE__{} = struct) do %{ - id: struct.id_externo, + id: struct.id, cpf: struct.usuario_cpf, minibiografia: struct.minibiografia, tipo_bolsa: struct.tipo_bolsa, From 48e9aed37a4d59d59e8440a411b7ca66d3de8af0 Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Sun, 20 Feb 2022 21:50:37 -0300 Subject: [PATCH 12/22] feat: user controllers --- .../user_confirmation_controller.ex | 63 ++++++++++++++ .../user_registration_controller.ex | 32 ++++++++ .../user_reset_password_controller.ex | 66 +++++++++++++++ .../controllers/user_session_controller.ex | 29 +++++++ .../controllers/user_settings_controller.ex | 82 +++++++++++++++++++ priv/gettext/en/LC_MESSAGES/errors.po | 23 +++++- priv/gettext/en/LC_MESSAGES/infos.po | 57 +++++++++++++ priv/gettext/errors.pot | 23 +++++- priv/gettext/infos.pot | 56 +++++++++++++ priv/gettext/pt_BR/LC_MESSAGES/errors.po | 23 +++++- priv/gettext/pt_BR/LC_MESSAGES/infos.po | 57 +++++++++++++ 11 files changed, 499 insertions(+), 12 deletions(-) create mode 100644 lib/fuschia_web/controllers/user_confirmation_controller.ex create mode 100644 lib/fuschia_web/controllers/user_registration_controller.ex create mode 100644 lib/fuschia_web/controllers/user_reset_password_controller.ex create mode 100644 lib/fuschia_web/controllers/user_session_controller.ex create mode 100644 lib/fuschia_web/controllers/user_settings_controller.ex create mode 100644 priv/gettext/en/LC_MESSAGES/infos.po create mode 100644 priv/gettext/infos.pot create mode 100644 priv/gettext/pt_BR/LC_MESSAGES/infos.po diff --git a/lib/fuschia_web/controllers/user_confirmation_controller.ex b/lib/fuschia_web/controllers/user_confirmation_controller.ex new file mode 100644 index 00000000..5f5e835b --- /dev/null +++ b/lib/fuschia_web/controllers/user_confirmation_controller.ex @@ -0,0 +1,63 @@ +defmodule FuschiaWeb.UserConfirmationController do + use FuschiaWeb, :controller + + import FuschiaWeb.Gettext + + alias Fuschia.Accounts + + def new(conn, _params) do + render(conn, "new.html") + end + + def create(conn, %{"user" => %{"email" => email}}) do + if user = Accounts.get_user_by_email(email) do + Accounts.deliver_user_confirmation_instructions( + user, + &Routes.user_confirmation_url(conn, :edit, &1) + ) + end + + conn + |> put_flash( + :info, + dgettext( + "infos", + "If your email is in our system and it has not been confirmed yet, you will receive an email with instructions shortly." + ) + ) + |> redirect(to: "/") + end + + def edit(conn, %{"token" => token}) do + render(conn, "edit.html", token: token) + end + + # Não faça login do usuário após a confirmação para evitar um + # token vazado dando ao usuário acesso à conta. + def update(conn, %{"token" => token}) do + case Accounts.confirm_user(token) do + {:ok, _} -> + conn + |> put_flash(:info, dgettext("infos", "User confirmed successfully.")) + |> redirect(to: "/") + + :error -> + # Se houver um usuário atual e a conta já foi confirmada, + # então as chances são de que o link de confirmação já foi visitado, ou + # por alguma forma automática ou pelo próprio usuário, então redirecionamos sem + # uma mensagem de aviso. + case conn.assigns do + %{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) -> + redirect(conn, to: "/") + + %{} -> + conn + |> put_flash( + :error, + dgettext("errors", "User confirmation link is invalid or it has expired.") + ) + |> redirect(to: "/") + end + end + end +end diff --git a/lib/fuschia_web/controllers/user_registration_controller.ex b/lib/fuschia_web/controllers/user_registration_controller.ex new file mode 100644 index 00000000..6af7ee1f --- /dev/null +++ b/lib/fuschia_web/controllers/user_registration_controller.ex @@ -0,0 +1,32 @@ +defmodule FuschiaWeb.UserRegistrationController do + use FuschiaWeb, :controller + + import FuschiaWeb.Gettext + + alias Fuschia.Accounts + alias Fuschia.Accounts.User + alias FuschiaWeb.UserAuth + + def new(conn, _params) do + changeset = Accounts.change_user_registration(%User{}) + render(conn, "new.html", changeset: changeset) + end + + def create(conn, %{"user" => user_params}) do + case Accounts.register(user_params) do + {:ok, user} -> + {:ok, _} = + Accounts.deliver_user_confirmation_instructions( + user, + &Routes.user_confirmation_url(conn, :edit, &1) + ) + + conn + |> put_flash(:info, dgettext("infos", "User successfully registered.")) + |> UserAuth.log_in_user(user) + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, "new.html", changeset: changeset) + end + end +end diff --git a/lib/fuschia_web/controllers/user_reset_password_controller.ex b/lib/fuschia_web/controllers/user_reset_password_controller.ex new file mode 100644 index 00000000..260efd7f --- /dev/null +++ b/lib/fuschia_web/controllers/user_reset_password_controller.ex @@ -0,0 +1,66 @@ +defmodule FuschiaWeb.UserResetPasswordController do + use FuschiaWeb, :controller + + import FuschiaWeb.Gettext + + alias Fuschia.Accounts + + plug :get_user_by_reset_password_token when action in [:edit, :update] + + def new(conn, _params) do + render(conn, "new.html") + end + + def create(conn, %{"user" => %{"email" => email}}) do + if user = Accounts.get_user_by_email(email) do + Accounts.deliver_user_reset_password_instructions( + user, + &Routes.user_reset_password_url(conn, :edit, &1) + ) + end + + conn + |> put_flash( + :info, + dgettext( + "infos", + "If your email is in our system, you will receive instructions to reset your password shortly." + ) + ) + |> redirect(to: "/") + end + + def edit(conn, _params) do + render(conn, "edit.html", changeset: Accounts.change_user_password(conn.assigns.user)) + end + + # Não faça login do usuário após redefinir a senha para evitar uma + # token vazado dando ao usuário acesso à conta. + def update(conn, %{"user" => user_params}) do + case Accounts.reset_user_password(conn.assigns.user, user_params) do + {:ok, _} -> + conn + |> put_flash(:info, dgettext("infos", "Password reset successfully.")) + |> redirect(to: Routes.user_session_path(conn, :new)) + + {:error, changeset} -> + render(conn, "edit.html", changeset: changeset) + end + end + + defp get_user_by_reset_password_token(conn, _opts) do + %{"token" => token} = conn.params + + if user = Accounts.get_user_by_reset_password_token(token) do + conn |> assign(:user, user) |> assign(:token, token) + else + conn + |> put_flash( + :error, + dgettext("errors", "Reset password link is invalid or it has expired.") + ) + |> redirect(to: "/") + |> halt() + end + end +end diff --git a/lib/fuschia_web/controllers/user_session_controller.ex b/lib/fuschia_web/controllers/user_session_controller.ex new file mode 100644 index 00000000..419da55a --- /dev/null +++ b/lib/fuschia_web/controllers/user_session_controller.ex @@ -0,0 +1,29 @@ +defmodule FuschiaWeb.UserSessionController do + use FuschiaWeb, :controller + + import FuschiaWeb.Gettext + + alias Fuschia.Accounts + alias FuschiaWeb.UserAuth + + def new(conn, _params) do + render(conn, "new.html", error_message: nil) + end + + def create(conn, %{"user" => user_params}) do + %{"email" => email, "password" => password} = user_params + + if user = Accounts.get_user_by_email_and_password(email, password) do + UserAuth.log_in_user(conn, user, user_params) + else + # Para evitar ataques de enumeração de usuários, não divulgue se o email está registrado. + render(conn, "new.html", error_message: "Invalid email or password") + end + end + + def delete(conn, _params) do + conn + |> put_flash(:info, dgettext("infos", "Logged out successfully.")) + |> UserAuth.log_out_user() + end +end diff --git a/lib/fuschia_web/controllers/user_settings_controller.ex b/lib/fuschia_web/controllers/user_settings_controller.ex new file mode 100644 index 00000000..6188cc80 --- /dev/null +++ b/lib/fuschia_web/controllers/user_settings_controller.ex @@ -0,0 +1,82 @@ +defmodule FuschiaWeb.UserSettingsController do + use FuschiaWeb, :controller + + import FuschiaWeb.Gettext + + alias Fuschia.Accounts + alias FuschiaWeb.UserAuth + + plug :assign_email_and_password_changesets + + def edit(conn, _params) do + render(conn, "edit.html") + end + + def update(conn, %{"action" => "update_email"} = params) do + %{"current_password" => password, "user" => user_params} = params + user = conn.assigns.current_user + + case Accounts.apply_user_email(user, password, user_params) do + {:ok, applied_user} -> + Accounts.deliver_update_email_instructions( + applied_user, + user.email, + &Routes.user_settings_url(conn, :confirm_email, &1) + ) + + conn + |> put_flash( + :info, + dgettext( + "infos", + "A link to confirm your email change has been sent to the new address." + ) + ) + |> redirect(to: Routes.user_settings_path(conn, :edit)) + + {:error, changeset} -> + render(conn, "edit.html", email_changeset: changeset) + end + end + + def update(conn, %{"action" => "update_password"} = params) do + %{"current_password" => password, "user" => user_params} = params + user = conn.assigns.current_user + + case Accounts.update_user_password(user, password, user_params) do + {:ok, user} -> + conn + |> put_flash(:info, dgettext("infos", "Password updated successfully.")) + |> put_session(:user_return_to, Routes.user_settings_path(conn, :edit)) + |> UserAuth.log_in_user(user) + + {:error, changeset} -> + render(conn, "edit.html", password_changeset: changeset) + end + end + + def confirm_email(conn, %{"token" => token}) do + case Accounts.update_user_email(conn.assigns.current_user, token) do + :ok -> + conn + |> put_flash(:info, dgettext("infos", "Email changed successfully.")) + |> redirect(to: Routes.user_settings_path(conn, :edit)) + + :error -> + conn + |> put_flash( + :error, + dgettext("errors", "Email change link is invalid or it has expired.") + ) + |> redirect(to: Routes.user_settings_path(conn, :edit)) + end + end + + defp assign_email_and_password_changesets(conn, _opts) do + user = conn.assigns.current_user + + conn + |> assign(:email_changeset, Accounts.change_user_email(user)) + |> assign(:password_changeset, Accounts.change_user_password(user)) + end +end diff --git a/priv/gettext/en/LC_MESSAGES/errors.po b/priv/gettext/en/LC_MESSAGES/errors.po index 37644d7b..f41ac8a8 100644 --- a/priv/gettext/en/LC_MESSAGES/errors.po +++ b/priv/gettext/en/LC_MESSAGES/errors.po @@ -112,21 +112,36 @@ msgid "does not exist" msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia/entities/contato.ex:48 +#: lib/fuschia/accounts/user.ex:166 msgid "didn't change" msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia/entities/contato.ex:60 +#: lib/fuschia/entities/contato.ex:44 msgid "must have the @ sign and no spaces" msgstr "" #, elixir-autogen, elixir-format, fuzzy -#: lib/fuschia/entities/contato.ex:57 +#: lib/fuschia/entities/contato.ex:41 msgid "should be at most 160 character(s)" msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia/accounts/user.ex:143 +#: lib/fuschia/accounts/user.ex:145 msgid "does not match password" msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_settings_controller.ex:69 +msgid "Email change link is invalid or it has expired." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_reset_password_controller.ex:60 +msgid "Reset password link is invalid or it has expired." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_confirmation_controller.ex:57 +msgid "User confirmation link is invalid or it has expired." +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/infos.po b/priv/gettext/en/LC_MESSAGES/infos.po new file mode 100644 index 00000000..315d9aab --- /dev/null +++ b/priv/gettext/en/LC_MESSAGES/infos.po @@ -0,0 +1,57 @@ +## "msgid"s in this file come from POT (.pot) files. +## +## Do not add, change, or remove "msgid"s manually here as +## they're tied to the ones in the corresponding POT file +## (with the same domain). +## +## Use "mix gettext.extract --merge" or "mix gettext.merge" +## to merge POT files into PO files. +msgid "" +msgstr "" +"Language: en\n" +"Plural-Forms: nplurals=2\n" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_settings_controller.ex:30 +msgid "A link to confirm your email change has been sent to the new address." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_settings_controller.ex:62 +msgid "Email changed successfully." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_confirmation_controller.ex:23 +msgid "If your email is in our system and it has not been confirmed yet, you will receive an email with instructions shortly." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_reset_password_controller.ex:25 +msgid "If your email is in our system, you will receive instructions to reset your password shortly." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_session_controller.ex:26 +msgid "Logged out successfully." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_reset_password_controller.ex:43 +msgid "Password reset successfully." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_settings_controller.ex:49 +msgid "Password updated successfully." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_confirmation_controller.ex:41 +msgid "User confirmed successfully." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_registration_controller.ex:25 +msgid "User successfully registered." +msgstr "" diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot index fb3b804b..cee5345e 100644 --- a/priv/gettext/errors.pot +++ b/priv/gettext/errors.pot @@ -109,21 +109,36 @@ msgid "does not exist" msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia/entities/contato.ex:48 +#: lib/fuschia/accounts/user.ex:166 msgid "didn't change" msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia/entities/contato.ex:60 +#: lib/fuschia/entities/contato.ex:44 msgid "must have the @ sign and no spaces" msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia/entities/contato.ex:57 +#: lib/fuschia/entities/contato.ex:41 msgid "should be at most 160 character(s)" msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia/accounts/user.ex:143 +#: lib/fuschia/accounts/user.ex:145 msgid "does not match password" msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_settings_controller.ex:69 +msgid "Email change link is invalid or it has expired." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_reset_password_controller.ex:60 +msgid "Reset password link is invalid or it has expired." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_confirmation_controller.ex:57 +msgid "User confirmation link is invalid or it has expired." +msgstr "" diff --git a/priv/gettext/infos.pot b/priv/gettext/infos.pot new file mode 100644 index 00000000..870a95eb --- /dev/null +++ b/priv/gettext/infos.pot @@ -0,0 +1,56 @@ +## This file is a PO Template file. +## +## "msgid"s here are often extracted from source code. +## Add new translations manually only if they're dynamic +## translations that can't be statically extracted. +## +## Run "mix gettext.extract" to bring this file up to +## date. Leave "msgstr"s empty as changing them here has no +## effect: edit them in PO (.po) files instead. +msgid "" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_settings_controller.ex:30 +msgid "A link to confirm your email change has been sent to the new address." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_settings_controller.ex:62 +msgid "Email changed successfully." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_confirmation_controller.ex:23 +msgid "If your email is in our system and it has not been confirmed yet, you will receive an email with instructions shortly." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_reset_password_controller.ex:25 +msgid "If your email is in our system, you will receive instructions to reset your password shortly." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_session_controller.ex:26 +msgid "Logged out successfully." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_reset_password_controller.ex:43 +msgid "Password reset successfully." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_settings_controller.ex:49 +msgid "Password updated successfully." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_confirmation_controller.ex:41 +msgid "User confirmed successfully." +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_registration_controller.ex:25 +msgid "User successfully registered." +msgstr "" diff --git a/priv/gettext/pt_BR/LC_MESSAGES/errors.po b/priv/gettext/pt_BR/LC_MESSAGES/errors.po index 4c324200..ef3dc4e4 100644 --- a/priv/gettext/pt_BR/LC_MESSAGES/errors.po +++ b/priv/gettext/pt_BR/LC_MESSAGES/errors.po @@ -102,21 +102,36 @@ msgid "does not exist" msgstr "não existe" #, elixir-autogen, elixir-format -#: lib/fuschia/entities/contato.ex:48 +#: lib/fuschia/accounts/user.ex:166 msgid "didn't change" msgstr "não pode ser igual ao antigo" #, elixir-autogen, elixir-format -#: lib/fuschia/entities/contato.ex:60 +#: lib/fuschia/entities/contato.ex:44 msgid "must have the @ sign and no spaces" msgstr "deve haver um @ e nenhum espaço em branco" #, elixir-autogen, elixir-format, fuzzy -#: lib/fuschia/entities/contato.ex:57 +#: lib/fuschia/entities/contato.ex:41 msgid "should be at most 160 character(s)" msgstr "deve possuir no máximo %{count} caracteres" #, elixir-autogen, elixir-format -#: lib/fuschia/accounts/user.ex:143 +#: lib/fuschia/accounts/user.ex:145 msgid "does not match password" msgstr "as senhas precisam ser iguais" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_settings_controller.ex:69 +msgid "Email change link is invalid or it has expired." +msgstr "Link para troca de Email é inválido ou expirou." + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_reset_password_controller.ex:60 +msgid "Reset password link is invalid or it has expired." +msgstr "Link para recuperação de senha é inválido ou expirou." + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_confirmation_controller.ex:57 +msgid "User confirmation link is invalid or it has expired." +msgstr "Link para confirmação da conta é inválido ou expirou." diff --git a/priv/gettext/pt_BR/LC_MESSAGES/infos.po b/priv/gettext/pt_BR/LC_MESSAGES/infos.po new file mode 100644 index 00000000..8bba6cbb --- /dev/null +++ b/priv/gettext/pt_BR/LC_MESSAGES/infos.po @@ -0,0 +1,57 @@ +## "msgid"s in this file come from POT (.pot) files. +## +## Do not add, change, or remove "msgid"s manually here as +## they're tied to the ones in the corresponding POT file +## (with the same domain). +## +## Use "mix gettext.extract --merge" or "mix gettext.merge" +## to merge POT files into PO files. +msgid "" +msgstr "" +"Language: pt_BR\n" +"Plural-Forms: nplurals=2\n" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_settings_controller.ex:30 +msgid "A link to confirm your email change has been sent to the new address." +msgstr "Um link de confirmação da troca do seu email foi enviado para o novo endereço." + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_settings_controller.ex:62 +msgid "Email changed successfully." +msgstr "Email foi atualizado com sucesso." + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_confirmation_controller.ex:23 +msgid "If your email is in our system and it has not been confirmed yet, you will receive an email with instructions shortly." +msgstr "Caso seu email esteja em nosso sistema porém ainda não foi confirmado, você irá receber um email com as instruções em breve." + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_reset_password_controller.ex:25 +msgid "If your email is in our system, you will receive instructions to reset your password shortly." +msgstr "Caso seu email esteja em nosso sistema, você irá receber intruções para recuperar sua senha em breve." + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_session_controller.ex:26 +msgid "Logged out successfully." +msgstr "Você desconectou com sucesso." + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_reset_password_controller.ex:43 +msgid "Password reset successfully." +msgstr "Senha recuperada com sucesso." + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_settings_controller.ex:49 +msgid "Password updated successfully." +msgstr "Senha atualizada com sucesso." + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_confirmation_controller.ex:41 +msgid "User confirmed successfully." +msgstr "Usuário confirmado com sucesso." + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_registration_controller.ex:25 +msgid "User successfully registered." +msgstr "Usuário foi cadastrado com sucesso." From 6fb162ac128804a8e724fe587d2c5936cac0778e Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Sun, 20 Feb 2022 21:54:43 -0300 Subject: [PATCH 13/22] feat: user_auth controller --- .../controllers/user_auth_test.exs | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 test/fuschia_web/controllers/user_auth_test.exs diff --git a/test/fuschia_web/controllers/user_auth_test.exs b/test/fuschia_web/controllers/user_auth_test.exs new file mode 100644 index 00000000..e5049b4f --- /dev/null +++ b/test/fuschia_web/controllers/user_auth_test.exs @@ -0,0 +1,179 @@ +defmodule FuschiaWeb.UserAuthTest do + use FuschiaWeb.ConnCase, async: true + + alias Fuschia.Accounts + alias FuschiaWeb.UserAuth + + import Fuschia.Factory + + @moduletag :integration + + @remember_me_cookie "_fuschia_web_user_remember_me" + + defp user_fixture(opts \\ []) do + :user + |> insert(opts) + |> Accounts.preload_all() + end + + setup %{conn: conn} do + conn = + conn + |> Map.replace!(:secret_key_base, FuschiaWeb.Endpoint.config(:secret_key_base)) + |> init_test_session(%{}) + + %{user: user_fixture(), conn: conn} + end + + describe "log_in_user/3" do + test "stores the user token in the session", %{conn: conn, user: user} do + conn = UserAuth.log_in_user(conn, user) + assert token = get_session(conn, :user_token) + assert get_session(conn, :live_socket_id) == "user_sessions:#{Base.url_encode64(token)}" + assert redirected_to(conn) == "/" + assert Accounts.get_user_by_session_token(token) + end + + test "clears everything previously stored in the session", %{conn: conn, user: user} do + conn = conn |> put_session(:to_be_removed, "value") |> UserAuth.log_in_user(user) + refute get_session(conn, :to_be_removed) + end + + test "redirects to the configured path", %{conn: conn, user: user} do + conn = conn |> put_session(:user_return_to, "/hello") |> UserAuth.log_in_user(user) + assert redirected_to(conn) == "/hello" + end + + test "writes a cookie if remember_me is configured", %{conn: conn, user: user} do + conn = conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"}) + assert get_session(conn, :user_token) == conn.cookies[@remember_me_cookie] + + assert %{value: signed_token, max_age: max_age} = conn.resp_cookies[@remember_me_cookie] + assert signed_token != get_session(conn, :user_token) + assert max_age == 5_184_000 + end + end + + describe "logout_user/1" do + test "erases session and cookies", %{conn: conn, user: user} do + user_token = Accounts.generate_user_session_token(user) + + conn = + conn + |> put_session(:user_token, user_token) + |> put_req_cookie(@remember_me_cookie, user_token) + |> fetch_cookies() + |> UserAuth.log_out_user() + + refute get_session(conn, :user_token) + refute conn.cookies[@remember_me_cookie] + assert %{max_age: 0} = conn.resp_cookies[@remember_me_cookie] + assert redirected_to(conn) == "/" + refute Accounts.get_user_by_session_token(user_token) + end + + test "broadcasts to the given live_socket_id", %{conn: conn} do + live_socket_id = "user_sessions:abcdef-token" + FuschiaWeb.Endpoint.subscribe(live_socket_id) + + conn + |> put_session(:live_socket_id, live_socket_id) + |> UserAuth.log_out_user() + + assert_receive %Phoenix.Socket.Broadcast{event: "disconnect", topic: ^live_socket_id} + end + + test "works even if user is already logged out", %{conn: conn} do + conn = conn |> fetch_cookies() |> UserAuth.log_out_user() + refute get_session(conn, :user_token) + assert %{max_age: 0} = conn.resp_cookies[@remember_me_cookie] + assert redirected_to(conn) == "/" + end + end + + describe "fetch_current_user/2" do + test "authenticates user from session", %{conn: conn, user: user} do + user_token = Accounts.generate_user_session_token(user) + conn = conn |> put_session(:user_token, user_token) |> UserAuth.fetch_current_user([]) + assert conn.assigns.current_user.id == user.id + end + + test "authenticates user from cookies", %{conn: conn, user: user} do + logged_in_conn = + conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"}) + + user_token = logged_in_conn.cookies[@remember_me_cookie] + %{value: signed_token} = logged_in_conn.resp_cookies[@remember_me_cookie] + + conn = + conn + |> put_req_cookie(@remember_me_cookie, signed_token) + |> UserAuth.fetch_current_user([]) + + assert get_session(conn, :user_token) == user_token + assert conn.assigns.current_user.id == user.id + end + + test "does not authenticate if data is missing", %{conn: conn, user: user} do + _ = Accounts.generate_user_session_token(user) + conn = UserAuth.fetch_current_user(conn, []) + refute get_session(conn, :user_token) + refute conn.assigns.current_user + end + end + + describe "redirect_if_user_is_authenticated/2" do + test "redirects if user is authenticated", %{conn: conn, user: user} do + conn = conn |> assign(:current_user, user) |> UserAuth.redirect_if_user_is_authenticated([]) + assert conn.halted + assert redirected_to(conn) == "/" + end + + test "does not redirect if user is not authenticated", %{conn: conn} do + conn = UserAuth.redirect_if_user_is_authenticated(conn, []) + refute conn.halted + refute conn.status + end + end + + describe "require_authenticated_user/2" do + test "redirects if user is not authenticated", %{conn: conn} do + conn = conn |> fetch_flash() |> UserAuth.require_authenticated_user([]) + assert conn.halted + assert redirected_to(conn) == Routes.user_session_path(conn, :new) + assert get_flash(conn, :error) == "You must log in to access this page." + end + + test "stores the path to redirect to on GET", %{conn: conn} do + halted_conn = + %{conn | path_info: ["foo"], query_string: ""} + |> fetch_flash() + |> UserAuth.require_authenticated_user([]) + + assert halted_conn.halted + assert get_session(halted_conn, :user_return_to) == "/foo" + + halted_conn = + %{conn | path_info: ["foo"], query_string: "bar=baz"} + |> fetch_flash() + |> UserAuth.require_authenticated_user([]) + + assert halted_conn.halted + assert get_session(halted_conn, :user_return_to) == "/foo?bar=baz" + + halted_conn = + %{conn | path_info: ["foo"], query_string: "bar", method: "POST"} + |> fetch_flash() + |> UserAuth.require_authenticated_user([]) + + assert halted_conn.halted + refute get_session(halted_conn, :user_return_to) + end + + test "does not redirect if user is authenticated", %{conn: conn, user: user} do + conn = conn |> assign(:current_user, user) |> UserAuth.require_authenticated_user([]) + refute conn.halted + refute conn.status + end + end +end From d7a178c445a5195706c1864131598f167bd7dd65 Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Sun, 20 Feb 2022 22:50:52 -0300 Subject: [PATCH 14/22] feat: user_auth tests fixed and extract `user_fixture` to user factory --- lib/fuschia_web/controllers/user_auth.ex | 15 ++++++++++----- .../controllers/user_session_controller.ex | 2 +- priv/gettext/en/LC_MESSAGES/errors.po | 5 +++++ priv/gettext/en/LC_MESSAGES/erros.po | 17 +++++++++++++++++ priv/gettext/errors.pot | 5 +++++ priv/gettext/erros.pot | 16 ++++++++++++++++ priv/gettext/pt_BR/LC_MESSAGES/errors.po | 5 +++++ priv/gettext/pt_BR/LC_MESSAGES/erros.po | 17 +++++++++++++++++ test/fuschia/accounts_test.exs | 6 ------ test/fuschia_web/controllers/user_auth_test.exs | 16 +++++++--------- test/support/factories/user_factory.ex | 7 +++++++ 11 files changed, 90 insertions(+), 21 deletions(-) create mode 100644 priv/gettext/en/LC_MESSAGES/erros.po create mode 100644 priv/gettext/erros.pot create mode 100644 priv/gettext/pt_BR/LC_MESSAGES/erros.po diff --git a/lib/fuschia_web/controllers/user_auth.ex b/lib/fuschia_web/controllers/user_auth.ex index 0fcabe3f..920c4f27 100644 --- a/lib/fuschia_web/controllers/user_auth.ex +++ b/lib/fuschia_web/controllers/user_auth.ex @@ -1,4 +1,5 @@ defmodule FuschiaWeb.UserAuth do + import FuschiaWeb.Gettext import Plug.Conn import Phoenix.Controller @@ -18,11 +19,15 @@ defmodule FuschiaWeb.UserAuth do redirecionado após o login """ def signed_in_path(conn) do - with user_token when is_binary(user_token) <- get_session(conn, :user_token), - %User{id: user_id} <- Accounts.get_user_by_session_token(user_token) do - "/app/pesquisadores/#{user_id}" + if user = Map.get(conn.assigns, :current_user) do + "/app/pesquisadores/#{user.id}" else - _e -> "/not_found" + with user_token when is_binary(user_token) <- get_session(conn, :user_token), + %User{id: user_id} <- Accounts.get_user_by_session_token(user_token) do + "/app/pesquisadores/#{user_id}" + else + _e -> "/not_found" + end end end @@ -144,7 +149,7 @@ defmodule FuschiaWeb.UserAuth do conn else conn - |> put_flash(:error, "Você precisa realizar login para acessar esta página") + |> put_flash(:error, dgettext("erros", "You must log in to access this page.")) |> maybe_store_return_to() |> redirect(to: Routes.user_session_path(conn, :new)) |> halt() diff --git a/lib/fuschia_web/controllers/user_session_controller.ex b/lib/fuschia_web/controllers/user_session_controller.ex index 419da55a..74b565af 100644 --- a/lib/fuschia_web/controllers/user_session_controller.ex +++ b/lib/fuschia_web/controllers/user_session_controller.ex @@ -17,7 +17,7 @@ defmodule FuschiaWeb.UserSessionController do UserAuth.log_in_user(conn, user, user_params) else # Para evitar ataques de enumeração de usuários, não divulgue se o email está registrado. - render(conn, "new.html", error_message: "Invalid email or password") + render(conn, "new.html", error_message: dgettext("errors", "Invalid email or password")) end end diff --git a/priv/gettext/en/LC_MESSAGES/errors.po b/priv/gettext/en/LC_MESSAGES/errors.po index f41ac8a8..4de79815 100644 --- a/priv/gettext/en/LC_MESSAGES/errors.po +++ b/priv/gettext/en/LC_MESSAGES/errors.po @@ -145,3 +145,8 @@ msgstr "" #: lib/fuschia_web/controllers/user_confirmation_controller.ex:57 msgid "User confirmation link is invalid or it has expired." msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_session_controller.ex:20 +msgid "Invalid email or password" +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/erros.po b/priv/gettext/en/LC_MESSAGES/erros.po new file mode 100644 index 00000000..0b7dcca3 --- /dev/null +++ b/priv/gettext/en/LC_MESSAGES/erros.po @@ -0,0 +1,17 @@ +## "msgid"s in this file come from POT (.pot) files. +## +## Do not add, change, or remove "msgid"s manually here as +## they're tied to the ones in the corresponding POT file +## (with the same domain). +## +## Use "mix gettext.extract --merge" or "mix gettext.merge" +## to merge POT files into PO files. +msgid "" +msgstr "" +"Language: en\n" +"Plural-Forms: nplurals=2\n" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_auth.ex:152 +msgid "You must log in to access this page." +msgstr "" diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot index cee5345e..a79a29b9 100644 --- a/priv/gettext/errors.pot +++ b/priv/gettext/errors.pot @@ -142,3 +142,8 @@ msgstr "" #: lib/fuschia_web/controllers/user_confirmation_controller.ex:57 msgid "User confirmation link is invalid or it has expired." msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_session_controller.ex:20 +msgid "Invalid email or password" +msgstr "" diff --git a/priv/gettext/erros.pot b/priv/gettext/erros.pot new file mode 100644 index 00000000..dc4f784e --- /dev/null +++ b/priv/gettext/erros.pot @@ -0,0 +1,16 @@ +## This file is a PO Template file. +## +## "msgid"s here are often extracted from source code. +## Add new translations manually only if they're dynamic +## translations that can't be statically extracted. +## +## Run "mix gettext.extract" to bring this file up to +## date. Leave "msgstr"s empty as changing them here has no +## effect: edit them in PO (.po) files instead. +msgid "" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_auth.ex:152 +msgid "You must log in to access this page." +msgstr "" diff --git a/priv/gettext/pt_BR/LC_MESSAGES/errors.po b/priv/gettext/pt_BR/LC_MESSAGES/errors.po index ef3dc4e4..d5b0b682 100644 --- a/priv/gettext/pt_BR/LC_MESSAGES/errors.po +++ b/priv/gettext/pt_BR/LC_MESSAGES/errors.po @@ -135,3 +135,8 @@ msgstr "Link para recuperação de senha é inválido ou expirou." #: lib/fuschia_web/controllers/user_confirmation_controller.ex:57 msgid "User confirmation link is invalid or it has expired." msgstr "Link para confirmação da conta é inválido ou expirou." + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_session_controller.ex:20 +msgid "Invalid email or password" +msgstr "Email ou senha inválidos." diff --git a/priv/gettext/pt_BR/LC_MESSAGES/erros.po b/priv/gettext/pt_BR/LC_MESSAGES/erros.po new file mode 100644 index 00000000..7acc4c84 --- /dev/null +++ b/priv/gettext/pt_BR/LC_MESSAGES/erros.po @@ -0,0 +1,17 @@ +## "msgid"s in this file come from POT (.pot) files. +## +## Do not add, change, or remove "msgid"s manually here as +## they're tied to the ones in the corresponding POT file +## (with the same domain). +## +## Use "mix gettext.extract --merge" or "mix gettext.merge" +## to merge POT files into PO files. +msgid "" +msgstr "" +"Language: pt_BR\n" +"Plural-Forms: nplurals=2\n" + +#, elixir-autogen, elixir-format +#: lib/fuschia_web/controllers/user_auth.ex:152 +msgid "You must log in to access this page." +msgstr "" diff --git a/test/fuschia/accounts_test.exs b/test/fuschia/accounts_test.exs index 66281ada..789c04ef 100644 --- a/test/fuschia/accounts_test.exs +++ b/test/fuschia/accounts_test.exs @@ -8,12 +8,6 @@ defmodule Fuschia.AccountsTest do @moduletag :unit - defp user_fixture(opts \\ []) do - :user - |> insert(opts) - |> Accounts.preload_all() - end - describe "list/1" do test "return all users in database" do user = user_fixture() diff --git a/test/fuschia_web/controllers/user_auth_test.exs b/test/fuschia_web/controllers/user_auth_test.exs index e5049b4f..7dc8e735 100644 --- a/test/fuschia_web/controllers/user_auth_test.exs +++ b/test/fuschia_web/controllers/user_auth_test.exs @@ -10,12 +10,6 @@ defmodule FuschiaWeb.UserAuthTest do @remember_me_cookie "_fuschia_web_user_remember_me" - defp user_fixture(opts \\ []) do - :user - |> insert(opts) - |> Accounts.preload_all() - end - setup %{conn: conn} do conn = conn @@ -27,10 +21,14 @@ defmodule FuschiaWeb.UserAuthTest do describe "log_in_user/3" do test "stores the user token in the session", %{conn: conn, user: user} do - conn = UserAuth.log_in_user(conn, user) + conn = + conn + |> put_session(:user_return_to, "/app/pesquisadores/#{user.id}") + |> UserAuth.log_in_user(user) + assert token = get_session(conn, :user_token) assert get_session(conn, :live_socket_id) == "user_sessions:#{Base.url_encode64(token)}" - assert redirected_to(conn) == "/" + assert redirected_to(conn) == "/app/pesquisadores/#{user.id}" assert Accounts.get_user_by_session_token(token) end @@ -126,7 +124,7 @@ defmodule FuschiaWeb.UserAuthTest do test "redirects if user is authenticated", %{conn: conn, user: user} do conn = conn |> assign(:current_user, user) |> UserAuth.redirect_if_user_is_authenticated([]) assert conn.halted - assert redirected_to(conn) == "/" + assert redirected_to(conn) == "/app/pesquisadores/#{user.id}" end test "does not redirect if user is not authenticated", %{conn: conn} do diff --git a/test/support/factories/user_factory.ex b/test/support/factories/user_factory.ex index 40d06eaa..eaa9a53b 100644 --- a/test/support/factories/user_factory.ex +++ b/test/support/factories/user_factory.ex @@ -3,6 +3,7 @@ defmodule Fuschia.UserFactory do defmacro __using__(_opts) do quote do + alias Fuschia.Accounts alias Fuschia.Accounts.User def unique_user_email, do: "user#{System.unique_integer()}@example.com" @@ -22,6 +23,12 @@ defmodule Fuschia.UserFactory do } end + def user_fixture(opts \\ []) do + :user + |> Fuschia.Factory.insert(opts) + |> Accounts.preload_all() + end + def extract_user_token(fun) do {:ok, captured_email} = fun.(&"[TOKEN]#{&1}[TOKEN]") [_, token | _] = String.split(captured_email.url, "[TOKEN]") From 91700317d15ff10a99bdfe0166b4818df4bd2070 Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Sun, 20 Feb 2022 23:01:03 -0300 Subject: [PATCH 15/22] feat: user_confirmation controller --- .../user_confirmation_controller_test.exs | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 test/fuschia_web/controllers/user_confirmation_controller_test.exs diff --git a/test/fuschia_web/controllers/user_confirmation_controller_test.exs b/test/fuschia_web/controllers/user_confirmation_controller_test.exs new file mode 100644 index 00000000..69aa702f --- /dev/null +++ b/test/fuschia_web/controllers/user_confirmation_controller_test.exs @@ -0,0 +1,108 @@ +defmodule FuschiaWeb.UserConfirmationControllerTest do + use FuschiaWeb.ConnCase, async: true + + alias Fuschia.Accounts + alias Fuschia.Repo + + import Fuschia.Factory + + @moduletag :integration + + setup do + %{user: user_fixture()} + end + + describe "GET /app/usuarios/confirmar" do + test "renders the resend confirmation page", %{conn: conn} do + conn = get(conn, Routes.user_confirmation_path(conn, :new)) + response = html_response(conn, 200) + assert response =~ "

Resend confirmation instructions

" + end + end + + describe "POST /app/usuarios/confirmar" do + @tag :capture_log + test "sends a new confirmation token", %{conn: conn, user: user} do + conn = + post(conn, Routes.user_confirmation_path(conn, :create), %{ + "user" => %{"email" => user.contato.email} + }) + + assert redirected_to(conn) == "/" + assert get_flash(conn, :info) =~ "If your email is in our system" + assert Repo.get_by!(Accounts.UserToken, user_cpf: user.cpf).context == "confirm" + end + + test "does not send confirmation token if User is confirmed", %{conn: conn, user: user} do + Repo.update!(Accounts.User.confirm_changeset(user)) + + conn = + post(conn, Routes.user_confirmation_path(conn, :create), %{ + "user" => %{"email" => user.contato.email} + }) + + assert redirected_to(conn) == "/" + assert get_flash(conn, :info) =~ "If your email is in our system" + refute Repo.get_by(Accounts.UserToken, user_cpf: user.cpf) + end + + test "does not send confirmation token if email is invalid", %{conn: conn} do + conn = + post(conn, Routes.user_confirmation_path(conn, :create), %{ + "user" => %{"email" => "unknown@example.com"} + }) + + assert redirected_to(conn) == "/" + assert get_flash(conn, :info) =~ "If your email is in our system" + assert Repo.all(Accounts.UserToken) == [] + end + end + + describe "GET /app/usuarios/confirmar/:token" do + test "renders the confirmation page", %{conn: conn} do + conn = get(conn, Routes.user_confirmation_path(conn, :edit, "some-token")) + response = html_response(conn, 200) + assert response =~ "

Confirm account

" + + form_action = Routes.user_confirmation_path(conn, :update, "some-token") + assert response =~ "action=\"#{form_action}\"" + end + end + + describe "POST /app/usuarios/confirmar/:token" do + test "confirms the given token once", %{conn: conn, user: user} do + token = + extract_user_token(fn url -> + Accounts.deliver_user_confirmation_instructions(user, url) + end) + + conn = post(conn, Routes.user_confirmation_path(conn, :update, token)) + assert redirected_to(conn) == "/" + assert get_flash(conn, :info) =~ "User confirmed successfully" + assert Accounts.get(user.cpf).confirmed_at + refute get_session(conn, :user_token) + assert Repo.all(Accounts.UserToken) == [] + + # When not logged in + conn = post(conn, Routes.user_confirmation_path(conn, :update, token)) + assert redirected_to(conn) == "/" + assert get_flash(conn, :error) =~ "User confirmation link is invalid or it has expired" + + # When logged in + conn = + build_conn() + |> log_in_user(user) + |> post(Routes.user_confirmation_path(conn, :update, token)) + + assert redirected_to(conn) == "/" + refute get_flash(conn, :error) + end + + test "does not confirm email with invalid token", %{conn: conn, user: user} do + conn = post(conn, Routes.user_confirmation_path(conn, :update, "oops")) + assert redirected_to(conn) == "/" + assert get_flash(conn, :error) =~ "User confirmation link is invalid or it has expired" + refute Accounts.get(user.cpf).confirmed_at + end + end +end From 044cb1bf9c345cf60b71666e5cc06fb9887e2c11 Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Sun, 20 Feb 2022 23:36:59 -0300 Subject: [PATCH 16/22] feat: user registration tests --- lib/fuschia/accounts.ex | 19 +++++ test/fuschia/accounts_test.exs | 11 +++ .../user_registration_controller_test.exs | 85 +++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 test/fuschia_web/controllers/user_registration_controller_test.exs diff --git a/lib/fuschia/accounts.ex b/lib/fuschia/accounts.ex index a3e54d9d..1de26356 100644 --- a/lib/fuschia/accounts.ex +++ b/lib/fuschia/accounts.ex @@ -110,6 +110,25 @@ defmodule Fuschia.Accounts do |> put_permissions() end + @doc """ + Obtém apenas um usuário pelo id + + ## Examples + + iex> get("JY85XgrT6NYAcaAYhXMQq") + %User{} + + iex> get("") + nil + + """ + def get_by_id(id) do + query() + |> preload_all() + |> Repo.get_by(id: id) + |> put_permissions() + end + ## User registration @doc """ diff --git a/test/fuschia/accounts_test.exs b/test/fuschia/accounts_test.exs index 789c04ef..30e9ec5d 100644 --- a/test/fuschia/accounts_test.exs +++ b/test/fuschia/accounts_test.exs @@ -67,6 +67,17 @@ defmodule Fuschia.AccountsTest do end end + describe "get_by_id/1" do + test "when id is valid, returns a user" do + user = user_fixture() + assert user == Accounts.get_by_id(user.id) + end + + test "when id is invalid, returns nil" do + refute Accounts.get_by_id("") + end + end + describe "register/1" do test "requires email and password to be set" do {:error, changeset} = Accounts.register(%{}) diff --git a/test/fuschia_web/controllers/user_registration_controller_test.exs b/test/fuschia_web/controllers/user_registration_controller_test.exs new file mode 100644 index 00000000..e67f1ad9 --- /dev/null +++ b/test/fuschia_web/controllers/user_registration_controller_test.exs @@ -0,0 +1,85 @@ +defmodule FuschiaWeb.UserRegistrationControllerTest do + use FuschiaWeb.ConnCase, async: true + + import Fuschia.Factory + + @moduletag :integration + + setup %{conn: conn} do + conn = + conn + |> Map.replace!(:secret_key_base, FuschiaWeb.Endpoint.config(:secret_key_base)) + |> init_test_session(%{}) + + %{conn: conn} + end + + describe "GET /cadastrar" do + test "renders registration page", %{conn: conn} do + conn = get(conn, Routes.user_registration_path(conn, :new)) + assert _response = html_response(conn, 200) + # TODO + # assert response =~ "

Register

" + # assert response =~ "Log in" + # assert response =~ "Register" + end + + test "redirects if already logged in", %{conn: conn} do + user = user_fixture() + + conn = + conn + |> put_session(:user_return_to, "/app/pesquisadores/#{user.id}") + |> log_in_user(user) + |> get(Routes.user_registration_path(conn, :new)) + + assert redirected_to(conn) == "/app/pesquisadores/#{user.id}" + end + end + + describe "POST /cadastrar" do + @tag :capture_log + test "creates account and logs the user in", %{conn: conn} do + email = unique_user_email() + contact = params_for(:contato, email: email) + password = valid_user_password() + + valid_user_attributes = + :user + |> params_for() + |> Map.put(:contato, contact) + |> Map.merge(%{password: password, password_confirmation: password}) + + conn = + conn + |> put_session(:user_return_to, "/app/pesquisadores/#{valid_user_attributes.id}") + |> post(Routes.user_registration_path(conn, :create), %{ + "user" => valid_user_attributes + }) + + assert get_session(conn, :user_token) + assert redirected_to(conn) == "/app/pesquisadores/#{valid_user_attributes.id}" + + # Now do a logged in request and assert on the menu + # TODO + # conn = get(conn, "/") + # response = html_response(conn, 200) + # assert response =~ email + # assert response =~ "Settings" + # assert response =~ "Log out" + end + + test "render errors for invalid data", %{conn: conn} do + conn = + post(conn, Routes.user_registration_path(conn, :create), %{ + "user" => %{"email" => "with spaces", "password" => "too short"} + }) + + assert _response = html_response(conn, 200) + # TODO + # assert response =~ "

Register

" + # assert response =~ "must have the @ sign and no spaces" + # assert response =~ "should be at least 12 character" + end + end +end From 4e8d3b14e0aeead77351cd94fb6e058f901b05f3 Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Sun, 20 Feb 2022 23:44:35 -0300 Subject: [PATCH 17/22] feat: user reset password tests --- lib/fuschia_web/router.ex | 8 +- .../user_reset_password_controller_test.exs | 114 ++++++++++++++++++ 2 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 test/fuschia_web/controllers/user_reset_password_controller_test.exs diff --git a/lib/fuschia_web/router.ex b/lib/fuschia_web/router.ex index 91953266..51d62218 100644 --- a/lib/fuschia_web/router.ex +++ b/lib/fuschia_web/router.ex @@ -73,10 +73,10 @@ defmodule FuschiaWeb.Router do post "/cadastrar", UserRegistrationController, :create get "/acessar", UserSessionController, :new post "/acessar", UserSessionController, :create - get "/resetar_senha", UserResetPasswordController, :new - post "/resetar_senha", UserResetPasswordController, :create - get "/resetar_senha/:token", UserResetPasswordController, :edit - put "/resetar_senha/:token", UserResetPasswordController, :update + get "/rescuperar_senha", UserResetPasswordController, :new + post "/rescuperar_senha", UserResetPasswordController, :create + get "/rescuperar_senha/:token", UserResetPasswordController, :edit + put "/rescuperar_senha/:token", UserResetPasswordController, :update end scope "/apps", FuschiaWeb do diff --git a/test/fuschia_web/controllers/user_reset_password_controller_test.exs b/test/fuschia_web/controllers/user_reset_password_controller_test.exs new file mode 100644 index 00000000..cc8acbc8 --- /dev/null +++ b/test/fuschia_web/controllers/user_reset_password_controller_test.exs @@ -0,0 +1,114 @@ +defmodule FuschiaWeb.UserResetPasswordControllerTest do + use FuschiaWeb.ConnCase, async: true + + alias Fuschia.Accounts + alias Fuschia.Repo + + import Fuschia.Factory + + setup do + %{user: user_fixture()} + end + + describe "GET /recuperar_senha" do + test "renders the reset password page", %{conn: conn} do + conn = get(conn, Routes.user_reset_password_path(conn, :new)) + assert _response = html_response(conn, 200) + # assert response =~ "

Forgot your password?

" + end + end + + describe "POST /recuperar_senha" do + @tag :capture_log + test "sends a new reset password token", %{conn: conn, user: user} do + conn = + post(conn, Routes.user_reset_password_path(conn, :create), %{ + "user" => %{"email" => user.contato.email} + }) + + assert redirected_to(conn) == "/" + assert get_flash(conn, :info) =~ "If your email is in our system" + assert Repo.get_by!(Accounts.UserToken, user_cpf: user.cpf).context == "reset_password" + end + + test "does not send reset password token if email is invalid", %{conn: conn} do + conn = + post(conn, Routes.user_reset_password_path(conn, :create), %{ + "user" => %{"email" => "unknown@example.com"} + }) + + assert redirected_to(conn) == "/" + assert get_flash(conn, :info) =~ "If your email is in our system" + assert Repo.all(Accounts.UserToken) == [] + end + end + + describe "GET /recuperar_senha/:token" do + setup %{user: user} do + token = + extract_user_token(fn url -> + Accounts.deliver_user_reset_password_instructions(user, url) + end) + + %{token: token} + end + + test "renders reset password", %{conn: conn, token: token} do + conn = get(conn, Routes.user_reset_password_path(conn, :edit, token)) + assert html_response(conn, 200) =~ "

Reset password

" + end + + test "does not render reset password with invalid token", %{conn: conn} do + conn = get(conn, Routes.user_reset_password_path(conn, :edit, "oops")) + assert redirected_to(conn) == "/" + assert get_flash(conn, :error) =~ "Reset password link is invalid or it has expired" + end + end + + describe "PUT /recuperar_senha/:token" do + setup %{user: user} do + token = + extract_user_token(fn url -> + Accounts.deliver_user_reset_password_instructions(user, url) + end) + + %{token: token} + end + + test "resets password once", %{conn: conn, user: user, token: token} do + conn = + put(conn, Routes.user_reset_password_path(conn, :update, token), %{ + "user" => %{ + "password" => "New valid password!", + "password_confirmation" => "New valid password!" + } + }) + + assert redirected_to(conn) == Routes.user_session_path(conn, :new) + refute get_session(conn, :user_token) + assert get_flash(conn, :info) =~ "Password reset successfully" + assert Accounts.get_user_by_email_and_password(user.contato.email, "New valid password!") + end + + test "does not reset password on invalid data", %{conn: conn, token: token} do + conn = + put(conn, Routes.user_reset_password_path(conn, :update, token), %{ + "user" => %{ + "password" => "too short", + "password_confirmation" => "does not match" + } + }) + + response = html_response(conn, 200) + assert response =~ "

Reset password

" + assert response =~ "at least one digit or punctuation character" + assert response =~ "does not match password" + end + + test "does not reset password with invalid token", %{conn: conn} do + conn = put(conn, Routes.user_reset_password_path(conn, :update, "oops")) + assert redirected_to(conn) == "/" + assert get_flash(conn, :error) =~ "Reset password link is invalid or it has expired" + end + end +end From 59318540335f649a6a66ed16a02a76ceb24582fb Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Sun, 20 Feb 2022 23:52:27 -0300 Subject: [PATCH 18/22] feat: user settings tests --- lib/fuschia/accounts.ex | 2 + lib/fuschia_web/controllers/user_auth.ex | 4 +- .../controllers/user_settings_controller.ex | 20 +-- lib/fuschia_web/router.ex | 2 +- .../user_session_controller_test.exs | 108 ++++++++++++++ .../user_settings_controller_test.exs | 139 ++++++++++++++++++ 6 files changed, 264 insertions(+), 11 deletions(-) create mode 100644 test/fuschia_web/controllers/user_session_controller_test.exs create mode 100644 test/fuschia_web/controllers/user_settings_controller_test.exs diff --git a/lib/fuschia/accounts.ex b/lib/fuschia/accounts.ex index 1de26356..d5675617 100644 --- a/lib/fuschia/accounts.ex +++ b/lib/fuschia/accounts.ex @@ -486,6 +486,8 @@ defmodule Fuschia.Accounts do Repo.preload(user, [:contato]) end + def preload_all(nil), do: nil + defp put_permissions(%User{} = user) do # TODO Map.put(user, :permissoes, nil) diff --git a/lib/fuschia_web/controllers/user_auth.ex b/lib/fuschia_web/controllers/user_auth.ex index 920c4f27..7f6382cd 100644 --- a/lib/fuschia_web/controllers/user_auth.ex +++ b/lib/fuschia_web/controllers/user_auth.ex @@ -20,6 +20,8 @@ defmodule FuschiaWeb.UserAuth do """ def signed_in_path(conn) do if user = Map.get(conn.assigns, :current_user) do + # TODO + # change to Routes.user_*/* function "/app/pesquisadores/#{user.id}" else with user_token when is_binary(user_token) <- get_session(conn, :user_token), @@ -108,7 +110,7 @@ defmodule FuschiaWeb.UserAuth do def fetch_current_user(conn, _opts) do {user_token, conn} = ensure_user_token(conn) user = user_token && Accounts.get_user_by_session_token(user_token) - assign(conn, :current_user, user) + assign(conn, :current_user, Accounts.preload_all(user)) end defp ensure_user_token(conn) do diff --git a/lib/fuschia_web/controllers/user_settings_controller.ex b/lib/fuschia_web/controllers/user_settings_controller.ex index 6188cc80..9a233b51 100644 --- a/lib/fuschia_web/controllers/user_settings_controller.ex +++ b/lib/fuschia_web/controllers/user_settings_controller.ex @@ -13,15 +13,15 @@ defmodule FuschiaWeb.UserSettingsController do end def update(conn, %{"action" => "update_email"} = params) do - %{"current_password" => password, "user" => user_params} = params + %{"current_password" => password, "contato" => contact_params} = params user = conn.assigns.current_user - case Accounts.apply_user_email(user, password, user_params) do + case Accounts.apply_user_email(user, password, contact_params) do {:ok, applied_user} -> Accounts.deliver_update_email_instructions( applied_user, - user.email, - &Routes.user_settings_url(conn, :confirm_email, &1) + user.contato.email, + &Routes.user_settings_url(conn, :confirm_email, user.id, &1) ) conn @@ -32,7 +32,7 @@ defmodule FuschiaWeb.UserSettingsController do "A link to confirm your email change has been sent to the new address." ) ) - |> redirect(to: Routes.user_settings_path(conn, :edit)) + |> redirect(to: Routes.user_settings_path(conn, :edit, user.id)) {:error, changeset} -> render(conn, "edit.html", email_changeset: changeset) @@ -47,7 +47,7 @@ defmodule FuschiaWeb.UserSettingsController do {:ok, user} -> conn |> put_flash(:info, dgettext("infos", "Password updated successfully.")) - |> put_session(:user_return_to, Routes.user_settings_path(conn, :edit)) + |> put_session(:user_return_to, Routes.user_settings_path(conn, :edit, user.id)) |> UserAuth.log_in_user(user) {:error, changeset} -> @@ -56,11 +56,13 @@ defmodule FuschiaWeb.UserSettingsController do end def confirm_email(conn, %{"token" => token}) do - case Accounts.update_user_email(conn.assigns.current_user, token) do + user = conn.assigns.current_user + + case Accounts.update_user_email(user, token) do :ok -> conn |> put_flash(:info, dgettext("infos", "Email changed successfully.")) - |> redirect(to: Routes.user_settings_path(conn, :edit)) + |> redirect(to: Routes.user_settings_path(conn, :edit, user.id)) :error -> conn @@ -68,7 +70,7 @@ defmodule FuschiaWeb.UserSettingsController do :error, dgettext("errors", "Email change link is invalid or it has expired.") ) - |> redirect(to: Routes.user_settings_path(conn, :edit)) + |> redirect(to: Routes.user_settings_path(conn, :edit, user.id)) end end diff --git a/lib/fuschia_web/router.ex b/lib/fuschia_web/router.ex index 51d62218..6bb0c254 100644 --- a/lib/fuschia_web/router.ex +++ b/lib/fuschia_web/router.ex @@ -79,7 +79,7 @@ defmodule FuschiaWeb.Router do put "/rescuperar_senha/:token", UserResetPasswordController, :update end - scope "/apps", FuschiaWeb do + scope "/app", FuschiaWeb do pipe_through [:browser, :require_authenticated_user] scope "/usuarios" do diff --git a/test/fuschia_web/controllers/user_session_controller_test.exs b/test/fuschia_web/controllers/user_session_controller_test.exs new file mode 100644 index 00000000..27ae8c9b --- /dev/null +++ b/test/fuschia_web/controllers/user_session_controller_test.exs @@ -0,0 +1,108 @@ +defmodule FuschiaWeb.UserSessionControllerTest do + use FuschiaWeb.ConnCase, async: true + + import Fuschia.Factory + + setup %{conn: conn} do + conn = + conn + |> Map.replace!(:secret_key_base, FuschiaWeb.Endpoint.config(:secret_key_base)) + |> init_test_session(%{}) + + %{user: user_fixture(), conn: conn} + end + + describe "GET /acessar" do + test "renders log in page", %{conn: conn} do + conn = get(conn, Routes.user_session_path(conn, :new)) + response = html_response(conn, 200) + assert response =~ "

Log in

" + assert response =~ "Register" + assert response =~ "Forgot your password?" + end + + test "redirects if already logged in", %{conn: conn, user: user} do + conn = conn |> log_in_user(user) |> get(Routes.user_session_path(conn, :new)) + assert redirected_to(conn) == "/app/pesquisadores/#{user.id}" + end + end + + describe "POST /acessar" do + test "logs the user in", %{conn: conn, user: user} do + conn = + conn + |> put_session(:user_return_to, "/app/usuarios/#{user.id}") + |> post(Routes.user_session_path(conn, :create), %{ + "user" => %{"email" => user.contato.email, "password" => valid_user_password()} + }) + + assert get_session(conn, :user_token) + assert redirected_to(conn) == "/app/usuarios/#{user.id}" + + # Now do a logged in request and assert on the menu + # TODO + # conn = get(conn, "/") + # response = html_response(conn, 200) + # assert response =~ user.email + # assert response =~ "Settings" + # assert response =~ "Log out" + end + + test "logs the user in with remember me", %{conn: conn, user: user} do + conn = + conn + |> put_session(:user_return_to, "/app/usuarios/#{user.id}") + |> post(Routes.user_session_path(conn, :create), %{ + "user" => %{ + "email" => user.contato.email, + "password" => valid_user_password(), + "remember_me" => "true" + } + }) + + assert conn.resp_cookies["_fuschia_web_user_remember_me"] + assert redirected_to(conn) == "/app/usuarios/#{user.id}" + end + + test "logs the user in with return to", %{conn: conn, user: user} do + conn = + conn + |> init_test_session(user_return_to: "/foo/bar") + |> post(Routes.user_session_path(conn, :create), %{ + "user" => %{ + "email" => user.contato.email, + "password" => valid_user_password() + } + }) + + assert redirected_to(conn) == "/foo/bar" + end + + test "emits error message with invalid credentials", %{conn: conn, user: user} do + conn = + post(conn, Routes.user_session_path(conn, :create), %{ + "user" => %{"email" => user.contato.email, "password" => "invalid_password"} + }) + + response = html_response(conn, 200) + assert response =~ "

Log in

" + assert response =~ "Invalid email or password" + end + end + + describe "DELETE /user/log_out" do + test "logs the user out", %{conn: conn, user: user} do + conn = conn |> log_in_user(user) |> delete(Routes.user_session_path(conn, :delete)) + assert redirected_to(conn) == "/" + refute get_session(conn, :user_token) + assert get_flash(conn, :info) =~ "Logged out successfully" + end + + test "succeeds even if the user is not logged in", %{conn: conn} do + conn = delete(conn, Routes.user_session_path(conn, :delete)) + assert redirected_to(conn) == "/" + refute get_session(conn, :user_token) + assert get_flash(conn, :info) =~ "Logged out successfully" + end + end +end diff --git a/test/fuschia_web/controllers/user_settings_controller_test.exs b/test/fuschia_web/controllers/user_settings_controller_test.exs new file mode 100644 index 00000000..9d016c0d --- /dev/null +++ b/test/fuschia_web/controllers/user_settings_controller_test.exs @@ -0,0 +1,139 @@ +defmodule FuschiaWeb.UserSettingsControllerTest do + use FuschiaWeb.ConnCase, async: true + + alias Fuschia.Accounts + + import Fuschia.Factory + + setup :register_and_log_in_user + + describe "GET /usuarios/:user_id/configuracoes" do + test "renders settings page", %{conn: conn, user: user} do + conn = get(conn, Routes.user_settings_path(conn, :edit, user.id)) + assert _response = html_response(conn, 200) + # TODO + # assert response =~ "

Settings

" + end + + test "redirects if user is not logged in", %{user: user} do + conn = build_conn() + conn = get(conn, Routes.user_settings_path(conn, :edit, user.id)) + assert redirected_to(conn) == Routes.user_session_path(conn, :new) + end + end + + describe "PUT /usuarios/:user_id/configuracoes (change password form)" do + test "updates the user password and resets tokens", %{conn: conn, user: user} do + new_password_conn = + put(conn, Routes.user_settings_path(conn, :update, user.id), %{ + "action" => "update_password", + "current_password" => valid_user_password(), + "user" => %{ + "password" => "New valid password!", + "password_confirmation" => "New valid password!" + } + }) + + assert redirected_to(new_password_conn) == Routes.user_settings_path(conn, :edit, user.id) + assert get_session(new_password_conn, :user_token) != get_session(conn, :user_token) + assert get_flash(new_password_conn, :info) =~ "Password updated successfully" + assert Accounts.get_user_by_email_and_password(user.contato.email, "New valid password!") + end + + test "does not update password on invalid data", %{conn: conn, user: user} do + old_password_conn = + put(conn, Routes.user_settings_path(conn, :update, user.id), %{ + "action" => "update_password", + "current_password" => "invalid", + "user" => %{ + "password" => "too short", + "password_confirmation" => "does not match" + } + }) + + assert _response = html_response(old_password_conn, 200) + # TODO + # assert response =~ "

Settings

" + # assert response =~ "should be at least 12 character(s)" + # assert response =~ "does not match password" + # assert response =~ "is not valid" + + assert get_session(old_password_conn, :user_token) == get_session(conn, :user_token) + end + end + + describe "PUT /usuarios/:user_id/configuracoes (change email form)" do + @tag :capture_log + test "updates the user email", %{conn: conn, user: user} do + conn = + put(conn, Routes.user_settings_path(conn, :update, user.id), %{ + "action" => "update_email", + "current_password" => valid_user_password(), + "contato" => %{"email" => unique_user_email()} + }) + + assert redirected_to(conn) == Routes.user_settings_path(conn, :edit, user.id) + assert get_flash(conn, :info) =~ "A link to confirm your email" + assert Accounts.get_user_by_email(user.contato.email) + end + + test "does not update email on invalid data", %{conn: conn, user: user} do + conn = + put(conn, Routes.user_settings_path(conn, :update, user.id), %{ + "action" => "update_email", + "current_password" => "invalid", + "contato" => %{"email" => "with spaces"} + }) + + assert _response = html_response(conn, 200) + # TODO + # assert response =~ "

Settings

" + # assert response =~ "must have the @ sign and no spaces" + # assert response =~ "is not valid" + end + end + + describe "GET /usuarios/:user_id/configuracoes/confirmar_email/:token" do + setup %{user: user} do + email = unique_user_email() + + contact = %{user.contato | email: email} + + token = + extract_user_token(fn url -> + Accounts.deliver_update_email_instructions( + %{user | contato: contact}, + user.contato.email, + url + ) + end) + + %{token: token, email: email} + end + + test "updates the user email once", %{conn: conn, user: user, token: token, email: email} do + conn = get(conn, Routes.user_settings_path(conn, :confirm_email, user.id, token)) + assert redirected_to(conn) == Routes.user_settings_path(conn, :edit, user.id) + assert get_flash(conn, :info) =~ "Email changed successfully" + refute Accounts.get_user_by_email(user.contato.email) + assert Accounts.get_user_by_email(email) + + conn = get(conn, Routes.user_settings_path(conn, :confirm_email, user.id, token)) + assert redirected_to(conn) == Routes.user_settings_path(conn, :edit, user.id) + assert get_flash(conn, :error) =~ "Email change link is invalid or it has expired" + end + + test "does not update email with invalid token", %{conn: conn, user: user} do + conn = get(conn, Routes.user_settings_path(conn, :confirm_email, user.id, "oops")) + assert redirected_to(conn) == Routes.user_settings_path(conn, :edit, user.id) + assert get_flash(conn, :error) =~ "Email change link is invalid or it has expired" + assert Accounts.get_user_by_email(user.contato.email) + end + + test "redirects if user is not logged in", %{token: token, user: user} do + conn = build_conn() + conn = get(conn, Routes.user_settings_path(conn, :confirm_email, user.id, token)) + assert redirected_to(conn) == Routes.user_session_path(conn, :new) + end + end +end From aa69bc4e3e59b5e00ab6f5c25a6d9a84be7fe62c Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Mon, 21 Feb 2022 03:36:44 -0300 Subject: [PATCH 19/22] feat: fix unit tests --- lib/fuschia/accounts/pesquisador.ex | 4 +- lib/fuschia/accounts/user_notifier.ex | 3 + lib/fuschia/entities/campus.ex | 3 +- test/fuschia/context/users_test.exs | 196 ------------------ .../user_reset_password_controller_test.exs | 2 + .../user_session_controller_test.exs | 2 + .../user_settings_controller_test.exs | 2 + 7 files changed, 13 insertions(+), 199 deletions(-) delete mode 100644 test/fuschia/context/users_test.exs diff --git a/lib/fuschia/accounts/pesquisador.ex b/lib/fuschia/accounts/pesquisador.ex index 5840eab0..497467e3 100644 --- a/lib/fuschia/accounts/pesquisador.ex +++ b/lib/fuschia/accounts/pesquisador.ex @@ -6,8 +6,8 @@ defmodule Fuschia.Accounts.Pesquisador do use Fuschia.Schema import Ecto.Changeset - alias Fuschia.Accounts.User - alias Fuschia.Entities.{Campus, Midia, Pesquisador, Relatorio} + alias Fuschia.Accounts.{Pesquisador, User} + alias Fuschia.Entities.{Campus, Midia, Relatorio} alias Fuschia.Types.{CapitalizedString, TrimmedString} @required_fields ~w( diff --git a/lib/fuschia/accounts/user_notifier.ex b/lib/fuschia/accounts/user_notifier.ex index 3553fe75..7e905b43 100644 --- a/lib/fuschia/accounts/user_notifier.ex +++ b/lib/fuschia/accounts/user_notifier.ex @@ -28,6 +28,7 @@ defmodule Fuschia.Accounts.UserNotifier do def deliver_confirmation_instructions(user, url) do deliver("Instruções para confirmação da conta", "email_confirmation", %{ + name: user.nome_completo, email: user.contato.email, url: url }) @@ -35,6 +36,7 @@ defmodule Fuschia.Accounts.UserNotifier do def deliver_reset_password_instructions(user, url) do deliver("Instruções para recuperar senha", "reset_password", %{ + name: user.nome_completo, email: user.contato.email, url: url }) @@ -42,6 +44,7 @@ defmodule Fuschia.Accounts.UserNotifier do def deliver_update_email_instructions(user, url) do deliver("Instruções para atualizar email", "update_email", %{ + name: user.nome_completo, email: user.contato.email, url: url }) diff --git a/lib/fuschia/entities/campus.ex b/lib/fuschia/entities/campus.ex index efa7f8b5..daa83586 100644 --- a/lib/fuschia/entities/campus.ex +++ b/lib/fuschia/entities/campus.ex @@ -6,7 +6,8 @@ defmodule Fuschia.Entities.Campus do use Fuschia.Schema import Ecto.Changeset - alias Fuschia.Entities.{Cidade, Pesquisador} + alias Fuschia.Accounts.Pesquisador + alias Fuschia.Entities.Cidade alias Fuschia.Types.CapitalizedString @required_fields ~w(nome)a diff --git a/test/fuschia/context/users_test.exs b/test/fuschia/context/users_test.exs deleted file mode 100644 index 1a5db410..00000000 --- a/test/fuschia/context/users_test.exs +++ /dev/null @@ -1,196 +0,0 @@ -defmodule Fuschia.Context.UsersTest do - use Fuschia.DataCase, async: true - - import Fuschia.Factory - - alias Fuschia.Context.Users - alias Fuschia.Accounts.User - - @moduletag :unit - - describe "list/1" do - test "return all users in database" do - user = - :user - |> insert() - |> Users.preload_all() - - assert [user] == Users.list() - end - end - - describe "one/1" do - test "when id is valid, returns a user" do - user = - :user - |> insert() - |> Users.preload_all() - - assert user == Users.one(user.cpf) - end - - test "when id is invalid, returns nil" do - assert is_nil(Users.one("")) - end - end - - describe "one_by_cpf/1" do - test "when CPF is valid, returns a user" do - user = - :user - |> insert() - |> Users.preload_all() - - assert user == Users.one_by_cpf(user.cpf) - end - - test "when CPF is invalid, return nil" do - assert is_nil(Users.one_by_cpf("")) - end - end - - describe "one_by_email/1" do - test "when email is valid, returns a user" do - user = - :user - |> insert() - |> Users.preload_all() - - assert user == Users.one_by_email(user.contato.email) - end - - test "when email is invalid, return nil" do - assert is_nil(Users.one_by_email("")) - end - end - - describe "create/1" do - @valid_attrs %{ - nome_completo: "Matheus de Souza Pessanha", - cpf: "264.722.590-70", - data_nascimento: ~D[2001-07-27], - perfil: "admin", - contato: %{ - endereco: "Av Teste, Rua Teste, numero 123", - email: "teste@exemplo.com", - celular: "(22)12345-6789" - } - } - - @invalid_attrs %{ - nome_completo: nil, - cpf: nil, - data_nascimento: nil, - perfil: nil, - contato: nil - } - - test "when all params are valid, creates an admin user" do - assert {:ok, %User{}} = Users.create(@valid_attrs) - end - - test "when params are invalid, returns an error changeset" do - assert {:error, %Ecto.Changeset{}} = Users.create(@invalid_attrs) - end - end - - describe "register/1" do - @valid_attrs %{ - nome_completo: "Matheus de Souza Pessanha", - cpf: "264.722.590-70", - data_nascimento: ~D[2001-07-27], - contato: %{ - endereco: "Av Teste, Rua Teste, numero 123", - email: "teste@exemplo.com", - celular: "(22)12345-6789" - }, - password: "Teste1234", - password_confirmation: "Teste1234" - } - - @invalid_attrs %{ - nome_completo: nil, - cpf: nil, - data_nascimento: nil, - contato: nil, - password: nil, - password_confirmation: nil - } - - test "when all params are valid, creates an 'avulso' user" do - assert {:ok, %User{perfil: "avulso"}} = Users.register(@valid_attrs) - end - - test "when params are invalid, returns an error changeset" do - assert {:error, %Ecto.Changeset{}} = Users.register(@invalid_attrs) - end - end - - describe "update/1" do - @valid_attrs %{ - nome_completo: "Matheus de Souza Pessanha", - cpf: "264.722.590-70", - data_nascimento: ~D[2001-07-27], - contato: %{ - endereco: "Av Teste, Rua Teste, numero 123", - email: "teste@exemplo.com", - celular: "(22)12345-6789" - }, - password: "Teste1234", - password_confirmation: "Teste1234" - } - - @update_attrs %{ - cpf: "435.618.970-10", - nome_completo: "Juninho Teste", - data_nascimento: ~D[1990-07-27], - contato: %{ - endereco: "Av Teste, Rua Teste, numero 765", - email: "teste@exemplo2.com", - celular: "(22)12345-6710" - } - } - - @invalid_attrs %{ - nome_completo: nil, - cpf: nil, - data_nascimento: nil, - contato: nil, - password: nil, - password_confirmation: nil - } - - test "when all params are valid, updates a user" do - assert {:ok, user} = Users.register(@valid_attrs) - assert {:ok, updated_user} = Users.update(user.cpf, @update_attrs) - - for key <- Map.keys(@update_attrs) do - if key == :contato do - contato = Map.get(updated_user, key) - contato_attrs = Map.get(@update_attrs, key) - assert contato.email == contato_attrs.email - assert contato.endereco == contato_attrs.endereco - assert contato.celular == contato_attrs.celular - else - assert Map.get(updated_user, key) == Map.get(@update_attrs, key) - end - end - end - - test "when params are invalid, returns an error changeset" do - assert {:ok, user} = Users.register(@valid_attrs) - assert {:error, %Ecto.Changeset{}} = Users.update(user.cpf, @invalid_attrs) - end - end - - describe "exists?/1" do - test "when id is valid, returns true" do - user = insert(:user) - assert true == Users.exists?(user.cpf) - end - - test "when id is invalid, returns false" do - assert false == Users.exists?("") - end - end -end diff --git a/test/fuschia_web/controllers/user_reset_password_controller_test.exs b/test/fuschia_web/controllers/user_reset_password_controller_test.exs index cc8acbc8..dd90d7f6 100644 --- a/test/fuschia_web/controllers/user_reset_password_controller_test.exs +++ b/test/fuschia_web/controllers/user_reset_password_controller_test.exs @@ -6,6 +6,8 @@ defmodule FuschiaWeb.UserResetPasswordControllerTest do import Fuschia.Factory + @moduletag :integration + setup do %{user: user_fixture()} end diff --git a/test/fuschia_web/controllers/user_session_controller_test.exs b/test/fuschia_web/controllers/user_session_controller_test.exs index 27ae8c9b..27cff51d 100644 --- a/test/fuschia_web/controllers/user_session_controller_test.exs +++ b/test/fuschia_web/controllers/user_session_controller_test.exs @@ -3,6 +3,8 @@ defmodule FuschiaWeb.UserSessionControllerTest do import Fuschia.Factory + @moduletag :integration + setup %{conn: conn} do conn = conn diff --git a/test/fuschia_web/controllers/user_settings_controller_test.exs b/test/fuschia_web/controllers/user_settings_controller_test.exs index 9d016c0d..4264e7c0 100644 --- a/test/fuschia_web/controllers/user_settings_controller_test.exs +++ b/test/fuschia_web/controllers/user_settings_controller_test.exs @@ -5,6 +5,8 @@ defmodule FuschiaWeb.UserSettingsControllerTest do import Fuschia.Factory + @moduletag :integration + setup :register_and_log_in_user describe "GET /usuarios/:user_id/configuracoes" do From fac6b8ca1efca0ec63ac6ff1f751560d3236fdda Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Mon, 21 Feb 2022 03:37:23 -0300 Subject: [PATCH 20/22] feat: temporary views and templatesf for user auth --- .../templates/layout/_user_menu.html.heex | 10 ++++ .../templates/layout/root.html.heex | 1 + .../user_confirmation/edit.html.heex | 12 +++++ .../templates/user_confirmation/new.html.heex | 15 ++++++ .../templates/user_registration/new.html.heex | 26 +++++++++ .../user_reset_password/edit.html.heex | 26 +++++++++ .../user_reset_password/new.html.heex | 15 ++++++ .../templates/user_session/new.html.heex | 27 ++++++++++ .../templates/user_settings/edit.html.heex | 53 +++++++++++++++++++ .../views/user_confirmation_view.ex | 3 ++ .../views/user_registration_view.ex | 3 ++ .../views/user_reset_password_view.ex | 3 ++ lib/fuschia_web/views/user_session_view.ex | 3 ++ lib/fuschia_web/views/user_settings_view.ex | 3 ++ 14 files changed, 200 insertions(+) create mode 100644 lib/fuschia_web/templates/layout/_user_menu.html.heex create mode 100644 lib/fuschia_web/templates/user_confirmation/edit.html.heex create mode 100644 lib/fuschia_web/templates/user_confirmation/new.html.heex create mode 100644 lib/fuschia_web/templates/user_registration/new.html.heex create mode 100644 lib/fuschia_web/templates/user_reset_password/edit.html.heex create mode 100644 lib/fuschia_web/templates/user_reset_password/new.html.heex create mode 100644 lib/fuschia_web/templates/user_session/new.html.heex create mode 100644 lib/fuschia_web/templates/user_settings/edit.html.heex create mode 100644 lib/fuschia_web/views/user_confirmation_view.ex create mode 100644 lib/fuschia_web/views/user_registration_view.ex create mode 100644 lib/fuschia_web/views/user_reset_password_view.ex create mode 100644 lib/fuschia_web/views/user_session_view.ex create mode 100644 lib/fuschia_web/views/user_settings_view.ex diff --git a/lib/fuschia_web/templates/layout/_user_menu.html.heex b/lib/fuschia_web/templates/layout/_user_menu.html.heex new file mode 100644 index 00000000..c78ccd48 --- /dev/null +++ b/lib/fuschia_web/templates/layout/_user_menu.html.heex @@ -0,0 +1,10 @@ +
    +<%= if @current_user do %> +
  • <%= @current_user.contato.email %>
  • +
  • <%= link "Settings", to: Routes.user_settings_path(@conn, :edit, @current_user.id) %>
  • +
  • <%= link "Log out", to: Routes.user_session_path(@conn, :delete), method: :delete %>
  • +<% else %> +
  • <%= link "Register", to: Routes.user_registration_path(@conn, :new) %>
  • +
  • <%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
  • +<% end %> +
diff --git a/lib/fuschia_web/templates/layout/root.html.heex b/lib/fuschia_web/templates/layout/root.html.heex index 396eb123..fd8a6e16 100644 --- a/lib/fuschia_web/templates/layout/root.html.heex +++ b/lib/fuschia_web/templates/layout/root.html.heex @@ -29,6 +29,7 @@ + <%= render "_user_menu.html", assigns %> <%= @inner_content %> diff --git a/lib/fuschia_web/templates/user_confirmation/edit.html.heex b/lib/fuschia_web/templates/user_confirmation/edit.html.heex new file mode 100644 index 00000000..e9bf443a --- /dev/null +++ b/lib/fuschia_web/templates/user_confirmation/edit.html.heex @@ -0,0 +1,12 @@ +

Confirm account

+ +<.form let={_f} for={:user} action={Routes.user_confirmation_path(@conn, :update, @token)}> +
+ <%= submit "Confirm my account" %> +
+ + +

+ <%= link "Register", to: Routes.user_registration_path(@conn, :new) %> | + <%= link "Log in", to: Routes.user_session_path(@conn, :new) %> +

diff --git a/lib/fuschia_web/templates/user_confirmation/new.html.heex b/lib/fuschia_web/templates/user_confirmation/new.html.heex new file mode 100644 index 00000000..4d9bee3c --- /dev/null +++ b/lib/fuschia_web/templates/user_confirmation/new.html.heex @@ -0,0 +1,15 @@ +

Resend confirmation instructions

+ +<.form let={f} for={:user} action={Routes.user_confirmation_path(@conn, :create)}> + <%= label f, :email %> + <%= email_input f, :email, required: true %> + +
+ <%= submit "Resend confirmation instructions" %> +
+ + +

+ <%= link "Register", to: Routes.user_registration_path(@conn, :new) %> | + <%= link "Log in", to: Routes.user_session_path(@conn, :new) %> +

diff --git a/lib/fuschia_web/templates/user_registration/new.html.heex b/lib/fuschia_web/templates/user_registration/new.html.heex new file mode 100644 index 00000000..fac2f162 --- /dev/null +++ b/lib/fuschia_web/templates/user_registration/new.html.heex @@ -0,0 +1,26 @@ +

Register

+ +<.form let={f} for={@changeset} action={Routes.user_registration_path(@conn, :create)}> + <%= if @changeset.action do %> +
+

Oops, something went wrong! Please check the errors below.

+
+ <% end %> + + <%= label f, :email %> + <%= email_input f, :email, required: true %> + <%= error_tag f, :email %> + + <%= label f, :password %> + <%= password_input f, :password, required: true %> + <%= error_tag f, :password %> + +
+ <%= submit "Register" %> +
+ + +

+ <%= link "Log in", to: Routes.user_session_path(@conn, :new) %> | + <%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %> +

diff --git a/lib/fuschia_web/templates/user_reset_password/edit.html.heex b/lib/fuschia_web/templates/user_reset_password/edit.html.heex new file mode 100644 index 00000000..d8efb4b9 --- /dev/null +++ b/lib/fuschia_web/templates/user_reset_password/edit.html.heex @@ -0,0 +1,26 @@ +

Reset password

+ +<.form let={f} for={@changeset} action={Routes.user_reset_password_path(@conn, :update, @token)}> + <%= if @changeset.action do %> +
+

Oops, something went wrong! Please check the errors below.

+
+ <% end %> + + <%= label f, :password, "New password" %> + <%= password_input f, :password, required: true %> + <%= error_tag f, :password %> + + <%= label f, :password_confirmation, "Confirm new password" %> + <%= password_input f, :password_confirmation, required: true %> + <%= error_tag f, :password_confirmation %> + +
+ <%= submit "Reset password" %> +
+ + +

+ <%= link "Register", to: Routes.user_registration_path(@conn, :new) %> | + <%= link "Log in", to: Routes.user_session_path(@conn, :new) %> +

diff --git a/lib/fuschia_web/templates/user_reset_password/new.html.heex b/lib/fuschia_web/templates/user_reset_password/new.html.heex new file mode 100644 index 00000000..126cdbaf --- /dev/null +++ b/lib/fuschia_web/templates/user_reset_password/new.html.heex @@ -0,0 +1,15 @@ +

Forgot your password?

+ +<.form let={f} for={:user} action={Routes.user_reset_password_path(@conn, :create)}> + <%= label f, :email %> + <%= email_input f, :email, required: true %> + +
+ <%= submit "Send instructions to reset password" %> +
+ + +

+ <%= link "Register", to: Routes.user_registration_path(@conn, :new) %> | + <%= link "Log in", to: Routes.user_session_path(@conn, :new) %> +

diff --git a/lib/fuschia_web/templates/user_session/new.html.heex b/lib/fuschia_web/templates/user_session/new.html.heex new file mode 100644 index 00000000..49a7d791 --- /dev/null +++ b/lib/fuschia_web/templates/user_session/new.html.heex @@ -0,0 +1,27 @@ +

Log in

+ +<.form let={f} for={@conn} action={Routes.user_session_path(@conn, :create)} as={:user}> + <%= if @error_message do %> +
+

<%= @error_message %>

+
+ <% end %> + + <%= label f, :email %> + <%= email_input f, :email, required: true %> + + <%= label f, :password %> + <%= password_input f, :password, required: true %> + + <%= label f, :remember_me, "Keep me logged in for 60 days" %> + <%= checkbox f, :remember_me %> + +
+ <%= submit "Log in" %> +
+ + +

+ <%= link "Register", to: Routes.user_registration_path(@conn, :new) %> | + <%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %> +

diff --git a/lib/fuschia_web/templates/user_settings/edit.html.heex b/lib/fuschia_web/templates/user_settings/edit.html.heex new file mode 100644 index 00000000..85e767ad --- /dev/null +++ b/lib/fuschia_web/templates/user_settings/edit.html.heex @@ -0,0 +1,53 @@ +

Settings

+ +

Change email

+ +<.form let={f} for={@email_changeset} action={Routes.user_settings_path(@conn, :update, @current_user.id)} id="update_email"> + <%= if @email_changeset.action do %> +
+

Oops, something went wrong! Please check the errors below.

+
+ <% end %> + + <%= hidden_input f, :action, name: "action", value: "update_email" %> + + <%= label f, :email %> + <%= email_input f, :email, required: true %> + <%= error_tag f, :email %> + + <%= label f, :current_password, for: "current_password_for_email" %> + <%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_email" %> + <%= error_tag f, :current_password %> + +
+ <%= submit "Change email" %> +
+ + +

Change password

+ +<.form let={f} for={@password_changeset} action={Routes.user_settings_path(@conn, :update, @current_user.id)} id="update_password"> + <%= if @password_changeset.action do %> +
+

Oops, something went wrong! Please check the errors below.

+
+ <% end %> + + <%= hidden_input f, :action, name: "action", value: "update_password" %> + + <%= label f, :password, "New password" %> + <%= password_input f, :password, required: true %> + <%= error_tag f, :password %> + + <%= label f, :password_confirmation, "Confirm new password" %> + <%= password_input f, :password_confirmation, required: true %> + <%= error_tag f, :password_confirmation %> + + <%= label f, :current_password, for: "current_password_for_password" %> + <%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_password" %> + <%= error_tag f, :current_password %> + +
+ <%= submit "Change password" %> +
+ diff --git a/lib/fuschia_web/views/user_confirmation_view.ex b/lib/fuschia_web/views/user_confirmation_view.ex new file mode 100644 index 00000000..c9570824 --- /dev/null +++ b/lib/fuschia_web/views/user_confirmation_view.ex @@ -0,0 +1,3 @@ +defmodule FuschiaWeb.UserConfirmationView do + use FuschiaWeb, :view +end diff --git a/lib/fuschia_web/views/user_registration_view.ex b/lib/fuschia_web/views/user_registration_view.ex new file mode 100644 index 00000000..f54e63a1 --- /dev/null +++ b/lib/fuschia_web/views/user_registration_view.ex @@ -0,0 +1,3 @@ +defmodule FuschiaWeb.UserRegistrationView do + use FuschiaWeb, :view +end diff --git a/lib/fuschia_web/views/user_reset_password_view.ex b/lib/fuschia_web/views/user_reset_password_view.ex new file mode 100644 index 00000000..eba8fede --- /dev/null +++ b/lib/fuschia_web/views/user_reset_password_view.ex @@ -0,0 +1,3 @@ +defmodule FuschiaWeb.UserResetPasswordView do + use FuschiaWeb, :view +end diff --git a/lib/fuschia_web/views/user_session_view.ex b/lib/fuschia_web/views/user_session_view.ex new file mode 100644 index 00000000..db63d338 --- /dev/null +++ b/lib/fuschia_web/views/user_session_view.ex @@ -0,0 +1,3 @@ +defmodule FuschiaWeb.UserSessionView do + use FuschiaWeb, :view +end diff --git a/lib/fuschia_web/views/user_settings_view.ex b/lib/fuschia_web/views/user_settings_view.ex new file mode 100644 index 00000000..580cabdf --- /dev/null +++ b/lib/fuschia_web/views/user_settings_view.ex @@ -0,0 +1,3 @@ +defmodule FuschiaWeb.UserSettingsView do + use FuschiaWeb, :view +end From 00ab27bf1e00f668e4da9990a0a117d145623578 Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Mon, 21 Feb 2022 12:49:57 -0300 Subject: [PATCH 21/22] feat: solve credo issues and add `brcpfcnpj` lib --- .credo.exs | 4 ++-- lib/fuschia/accounts.ex | 2 +- lib/fuschia/accounts/user.ex | 18 ++++++++++++------ lib/fuschia/accounts/user_notifier.ex | 4 ++++ lib/fuschia/accounts/user_token.ex | 9 +++++++++ lib/fuschia_web/controllers/user_auth.ex | 5 +++++ .../user_confirmation_controller.ex | 2 +- lib/fuschia_web/live/live_helpers.ex | 2 +- mix.exs | 1 + mix.lock | 1 + test/fuschia/queries/pesquisadores_test.exs | 2 +- test/support/factories/user_factory.ex | 2 +- 12 files changed, 39 insertions(+), 13 deletions(-) diff --git a/.credo.exs b/.credo.exs index 53abe8de..0224f6e2 100644 --- a/.credo.exs +++ b/.credo.exs @@ -158,7 +158,7 @@ # Controversial and experimental checks (opt-in, just replace `false` with `[]`) # {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, - {Credo.Check.Consistency.UnusedVariableNames, []}, + {Credo.Check.Consistency.UnusedVariableNames, false}, {Credo.Check.Design.DuplicatedCode, []}, {Credo.Check.Readability.AliasAs, false}, {Credo.Check.Readability.BlockPipe, false}, @@ -166,7 +166,7 @@ {Credo.Check.Readability.MultiAlias, false}, {Credo.Check.Readability.SeparateAliasRequire, false}, {Credo.Check.Readability.SinglePipe, []}, - {Credo.Check.Readability.Specs, []}, + {Credo.Check.Readability.Specs, false}, {Credo.Check.Readability.StrictModuleLayout, false}, {Credo.Check.Readability.WithCustomTaggedTuple, false}, {Credo.Check.Refactor.ABCSize, false}, diff --git a/lib/fuschia/accounts.ex b/lib/fuschia/accounts.ex index d5675617..1e248845 100644 --- a/lib/fuschia/accounts.ex +++ b/lib/fuschia/accounts.ex @@ -6,7 +6,7 @@ defmodule Fuschia.Accounts do import Ecto.Query, warn: false alias Fuschia.Repo - alias Fuschia.Accounts.{User, UserToken, UserNotifier} + alias Fuschia.Accounts.{User, UserNotifier, UserToken} ## Database getters diff --git a/lib/fuschia/accounts/user.ex b/lib/fuschia/accounts/user.ex index 034b24c3..e7315757 100644 --- a/lib/fuschia/accounts/user.ex +++ b/lib/fuschia/accounts/user.ex @@ -1,10 +1,18 @@ defmodule Fuschia.Accounts.User do + @moduledoc """ + Schema que representa um usuário do sistema. + + ## Exemplos + - Pesquisador + - Pescador + """ + use Fuschia.Schema + import Brcpfcnpj.Changeset, only: [validate_cpf: 2] import Ecto.Changeset import FuschiaWeb.Gettext - alias Fuschia.Common.Formats alias Fuschia.Entities.{Contato, User} alias Fuschia.Types.{CapitalizedString, TrimmedString} @@ -17,8 +25,6 @@ defmodule Fuschia.Accounts.User do @upper_pass_format ~r/[A-Z]/ @special_pass_format ~r/[!?@#$%^&*_0-9]/ - @cpf_format Formats.cpf() - @primary_key {:cpf, TrimmedString, autogenerate: false} schema "user" do field :confirmed_at, :naive_datetime @@ -41,7 +47,7 @@ defmodule Fuschia.Accounts.User do struct |> cast(attrs, @required_fields ++ @optional_fields) |> validate_required(@required_fields) - |> validate_format(:cpf, @cpf_format) + |> validate_cpf(:cpf) |> unique_constraint(:cpf, name: :user_pkey) |> unique_constraint(:cpf, name: :user_nome_completo_index) |> validate_inclusion(:role, @valid_roles) @@ -54,7 +60,7 @@ defmodule Fuschia.Accounts.User do def update_changeset(%__MODULE__{} = struct, attrs) do struct |> cast(attrs, @required_fields ++ @optional_fields) - |> validate_format(:cpf, @cpf_format) + |> validate_cpf(:cpf) |> unique_constraint(:cpf, name: :user_pkey) |> unique_constraint(:cpf, name: :user_nome_completo_index) |> validate_inclusion(:role, @valid_roles) @@ -171,7 +177,7 @@ defmodule Fuschia.Accounts.User do Confirma um usuário atualizando `confirmed_at`, """ def confirm_changeset(user) do - now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) + now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) change(user, confirmed_at: now) end diff --git a/lib/fuschia/accounts/user_notifier.ex b/lib/fuschia/accounts/user_notifier.ex index 7e905b43..4a13dc12 100644 --- a/lib/fuschia/accounts/user_notifier.ex +++ b/lib/fuschia/accounts/user_notifier.ex @@ -1,4 +1,8 @@ defmodule Fuschia.Accounts.UserNotifier do + @moduledoc """ + Define funções públicas para o envio de emails transacionais. + """ + require Logger alias Fuschia.Jobs.MailerJob diff --git a/lib/fuschia/accounts/user_token.ex b/lib/fuschia/accounts/user_token.ex index eff19f6b..41954b25 100644 --- a/lib/fuschia/accounts/user_token.ex +++ b/lib/fuschia/accounts/user_token.ex @@ -1,4 +1,13 @@ defmodule Fuschia.Accounts.UserToken do + @moduledoc """ + Schema que representa tokens de usuários. + + ## Exemplos + - confirmação de email + - recuperação de senha + - token de sessão de login + """ + use Fuschia.Schema import Ecto.Query diff --git a/lib/fuschia_web/controllers/user_auth.ex b/lib/fuschia_web/controllers/user_auth.ex index 7f6382cd..af80f943 100644 --- a/lib/fuschia_web/controllers/user_auth.ex +++ b/lib/fuschia_web/controllers/user_auth.ex @@ -1,4 +1,9 @@ defmodule FuschiaWeb.UserAuth do + @moduledoc """ + Funções do contexto de autenticação de usuários + via browser. Apenas Funções puras. + """ + import FuschiaWeb.Gettext import Plug.Conn import Phoenix.Controller diff --git a/lib/fuschia_web/controllers/user_confirmation_controller.ex b/lib/fuschia_web/controllers/user_confirmation_controller.ex index 5f5e835b..55ebc8ee 100644 --- a/lib/fuschia_web/controllers/user_confirmation_controller.ex +++ b/lib/fuschia_web/controllers/user_confirmation_controller.ex @@ -47,7 +47,7 @@ defmodule FuschiaWeb.UserConfirmationController do # por alguma forma automática ou pelo próprio usuário, então redirecionamos sem # uma mensagem de aviso. case conn.assigns do - %{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) -> + %{current_user: %{confirmed_at: %NaiveDateTime{} = _confirmed_at}} -> redirect(conn, to: "/") %{} -> diff --git a/lib/fuschia_web/live/live_helpers.ex b/lib/fuschia_web/live/live_helpers.ex index bf7ecdd1..0209a4d4 100644 --- a/lib/fuschia_web/live/live_helpers.ex +++ b/lib/fuschia_web/live/live_helpers.ex @@ -29,7 +29,7 @@ defmodule FuschiaWeb.LiveHelpers do end defp find_current_user(session) do - with session_token when not is_nil(session_token) <- session["user_token"], + with session_token when is_binary(session_token) <- session["user_token"], %User{} = user <- Accounts.get_user_by_session_token(session_token), do: user end diff --git a/mix.exs b/mix.exs index be7fb8c2..b83c20c5 100644 --- a/mix.exs +++ b/mix.exs @@ -52,6 +52,7 @@ defmodule Fuschia.MixProject do {:phoenix_ecto, "~> 4.1"}, {:swoosh, "~> 1.4"}, {:mail, ">= 0.0.0"}, + {:brcpfcnpj, "~> 1.0.0"}, {:phoenix_html, "~> 3.0"}, {:phoenix_live_reload, "~> 1.2", only: :dev}, {:phoenix_live_view, "~> 0.17"}, diff --git a/mix.lock b/mix.lock index 357f2536..41cef726 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,7 @@ %{ "bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"}, "blankable": {:hex, :blankable, "1.0.0", "89ab564a63c55af117e115144e3b3b57eb53ad43ba0f15553357eb283e0ed425", [:mix], [], "hexpm", "7cf11aac0e44f4eedbee0c15c1d37d94c090cb72a8d9fddf9f7aec30f9278899"}, + "brcpfcnpj": {:hex, :brcpfcnpj, "1.0.0", "23319e5f7e4da533f36dc5b7793e7c505fd80e43b0fa720c7a459daffbaa5034", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "8e6f38910896ea4f39b75c37855a702cebd495c2a74d88cdfd5c3c715c871510"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, "castore": {:hex, :castore, "0.1.15", "dbb300827d5a3ec48f396ca0b77ad47058578927e9ebe792abd99fcbc3324326", [:mix], [], "hexpm", "c69379b907673c7e6eb229f09a0a09b60bb27cfb9625bcb82ea4c04ba82a8442"}, "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, diff --git a/test/fuschia/queries/pesquisadores_test.exs b/test/fuschia/queries/pesquisadores_test.exs index 85aacab4..86848cdd 100644 --- a/test/fuschia/queries/pesquisadores_test.exs +++ b/test/fuschia/queries/pesquisadores_test.exs @@ -3,8 +3,8 @@ defmodule Fuschia.Queries.PesquisadoresTest do import Fuschia.Factory - alias Fuschia.Db alias Fuschia.Accounts.Pesquisador + alias Fuschia.Db alias Fuschia.Queries.Pesquisadores @moduletag :unit diff --git a/test/support/factories/user_factory.ex b/test/support/factories/user_factory.ex index eaa9a53b..831f0a3a 100644 --- a/test/support/factories/user_factory.ex +++ b/test/support/factories/user_factory.ex @@ -16,7 +16,7 @@ defmodule Fuschia.UserFactory do role: sequence(:role, ["avulso", "pesquisador"]), nome_completo: sequence(:nome_completo, &"User #{&1}"), ativo?: true, - cpf: sequence(:cpf, ["325.956.490-00", "726.541.170-65"]), + cpf: Brcpfcnpj.cpf_generate(true), data_nascimento: sequence(:data_nascimento, [~D[2001-07-27], ~D[2001-07-28]]), password_hash: "$2b$12$AZdxCkw/Rb5AlI/5S7Ebb.hIyG.ocs18MGkHAW2gdZibH7a1wHTyu", contato: build(:contato) From 19b592ccd1e377de1110beab2d840641803a57d2 Mon Sep 17 00:00:00 2001 From: zoedsoupe Date: Mon, 21 Feb 2022 13:15:45 -0300 Subject: [PATCH 22/22] feat: separate public api and browser routes --- lib/fuschia/accounts/user.ex | 7 +- lib/fuschia/context/auth_logs.ex | 3 +- lib/fuschia_web/auth/guardian.ex | 2 +- .../controllers/{ => api}/auth_controller.ex | 0 .../{ => api}/campus_controller.ex | 0 .../controllers/{ => browser}/user_auth.ex | 0 .../user_confirmation_controller.ex | 0 .../user_registration_controller.ex | 0 .../user_reset_password_controller.ex | 0 .../{ => browser}/user_session_controller.ex | 0 .../{ => browser}/user_settings_controller.ex | 0 lib/fuschia_web/router.ex | 97 +++++++++++-------- priv/gettext/en/LC_MESSAGES/errors.po | 12 +-- priv/gettext/en/LC_MESSAGES/erros.po | 2 +- priv/gettext/en/LC_MESSAGES/infos.po | 18 ++-- priv/gettext/errors.pot | 12 +-- priv/gettext/erros.pot | 2 +- priv/gettext/infos.pot | 18 ++-- priv/gettext/pt_BR/LC_MESSAGES/errors.po | 12 +-- priv/gettext/pt_BR/LC_MESSAGES/erros.po | 2 +- priv/gettext/pt_BR/LC_MESSAGES/infos.po | 18 ++-- 21 files changed, 114 insertions(+), 91 deletions(-) rename lib/fuschia_web/controllers/{ => api}/auth_controller.ex (100%) rename lib/fuschia_web/controllers/{ => api}/campus_controller.ex (100%) rename lib/fuschia_web/controllers/{ => browser}/user_auth.ex (100%) rename lib/fuschia_web/controllers/{ => browser}/user_confirmation_controller.ex (100%) rename lib/fuschia_web/controllers/{ => browser}/user_registration_controller.ex (100%) rename lib/fuschia_web/controllers/{ => browser}/user_reset_password_controller.ex (100%) rename lib/fuschia_web/controllers/{ => browser}/user_session_controller.ex (100%) rename lib/fuschia_web/controllers/{ => browser}/user_settings_controller.ex (100%) diff --git a/lib/fuschia/accounts/user.ex b/lib/fuschia/accounts/user.ex index e7315757..806a4d9f 100644 --- a/lib/fuschia/accounts/user.ex +++ b/lib/fuschia/accounts/user.ex @@ -213,11 +213,11 @@ defmodule Fuschia.Accounts.User do email: struct.contato.email, endereco: struct.contato.endereco, celular: struct.contato.celular, - nomeCompleto: struct.nome_completo, + nome_completo: struct.nome_completo, perfil: struct.role, permissoes: struct.permissoes, cpf: struct.cpf, - dataNascimento: struct.data_nascimento, + data_nascimento: struct.data_nascimento, id: struct.id } end @@ -230,7 +230,8 @@ defmodule Fuschia.Accounts.User do confirmado_em: struct.confirmed_at, ativo: struct.ativo?, data_nascimento: struct.data_nascimento, - id: struct.id + id: struct.id, + contato: struct.contato } end diff --git a/lib/fuschia/context/auth_logs.ex b/lib/fuschia/context/auth_logs.ex index a6983e2d..dc18579f 100644 --- a/lib/fuschia/context/auth_logs.ex +++ b/lib/fuschia/context/auth_logs.ex @@ -17,6 +17,7 @@ defmodule Fuschia.Context.AuthLogs do @spec create(String.t(), String.t(), User.t()) :: :ok def create(ip, user_agent, user) do - create(%{"ip" => ip, "user_agent" => user_agent, "user_cpf" => user.cpf}) + user_cpf = Map.get(user, :cpf) || Map.get(user, "cpf") + create(%{"ip" => ip, "user_agent" => user_agent, "user_cpf" => user_cpf}) end end diff --git a/lib/fuschia_web/auth/guardian.ex b/lib/fuschia_web/auth/guardian.ex index 9e8e2534..71dc5ec0 100644 --- a/lib/fuschia_web/auth/guardian.ex +++ b/lib/fuschia_web/auth/guardian.ex @@ -37,7 +37,7 @@ defmodule FuschiaWeb.Auth.Guardian do def user_claims(%{"cpf" => cpf}) do case Accounts.get(cpf) do nil -> {:error, :unauthorized} - user -> {:ok, User.for_jwt(user)} + user -> {:ok, user |> User.for_jwt() |> ProperCase.to_camel_case()} end end diff --git a/lib/fuschia_web/controllers/auth_controller.ex b/lib/fuschia_web/controllers/api/auth_controller.ex similarity index 100% rename from lib/fuschia_web/controllers/auth_controller.ex rename to lib/fuschia_web/controllers/api/auth_controller.ex diff --git a/lib/fuschia_web/controllers/campus_controller.ex b/lib/fuschia_web/controllers/api/campus_controller.ex similarity index 100% rename from lib/fuschia_web/controllers/campus_controller.ex rename to lib/fuschia_web/controllers/api/campus_controller.ex diff --git a/lib/fuschia_web/controllers/user_auth.ex b/lib/fuschia_web/controllers/browser/user_auth.ex similarity index 100% rename from lib/fuschia_web/controllers/user_auth.ex rename to lib/fuschia_web/controllers/browser/user_auth.ex diff --git a/lib/fuschia_web/controllers/user_confirmation_controller.ex b/lib/fuschia_web/controllers/browser/user_confirmation_controller.ex similarity index 100% rename from lib/fuschia_web/controllers/user_confirmation_controller.ex rename to lib/fuschia_web/controllers/browser/user_confirmation_controller.ex diff --git a/lib/fuschia_web/controllers/user_registration_controller.ex b/lib/fuschia_web/controllers/browser/user_registration_controller.ex similarity index 100% rename from lib/fuschia_web/controllers/user_registration_controller.ex rename to lib/fuschia_web/controllers/browser/user_registration_controller.ex diff --git a/lib/fuschia_web/controllers/user_reset_password_controller.ex b/lib/fuschia_web/controllers/browser/user_reset_password_controller.ex similarity index 100% rename from lib/fuschia_web/controllers/user_reset_password_controller.ex rename to lib/fuschia_web/controllers/browser/user_reset_password_controller.ex diff --git a/lib/fuschia_web/controllers/user_session_controller.ex b/lib/fuschia_web/controllers/browser/user_session_controller.ex similarity index 100% rename from lib/fuschia_web/controllers/user_session_controller.ex rename to lib/fuschia_web/controllers/browser/user_session_controller.ex diff --git a/lib/fuschia_web/controllers/user_settings_controller.ex b/lib/fuschia_web/controllers/browser/user_settings_controller.ex similarity index 100% rename from lib/fuschia_web/controllers/user_settings_controller.ex rename to lib/fuschia_web/controllers/browser/user_settings_controller.ex diff --git a/lib/fuschia_web/router.ex b/lib/fuschia_web/router.ex index 6bb0c254..9e334611 100644 --- a/lib/fuschia_web/router.ex +++ b/lib/fuschia_web/router.ex @@ -22,49 +22,16 @@ defmodule FuschiaWeb.Router do plug ProperCase.Plug.SnakeCaseParams end + pipeline :api_auth do + plug FuschiaWeb.Auth.Pipeline + end + pipeline :api_swagger do plug :accepts, ["json"] plug OpenApiSpex.Plug.PutApiSpec, module: FuschiaWeb.Swagger.ApiSpec end - scope "/" do - pipe_through :browser - - get "/swaggerui", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi" - - live "/example", FuschiaWeb.ExampleLive - end - - scope "/api" do - pipe_through :api_swagger - - get "/openapi", OpenApiSpex.Plug.RenderSpec, [] - end - - if Mix.env() in [:dev, :test] do - import Phoenix.LiveDashboard.Router - - scope "/" do - pipe_through :browser - - live_dashboard "/dashboard", metrics: FuschiaWeb.Telemetry - end - end - - if Mix.env() == :dev do - scope "/" do - pipe_through :browser - surface_catalogue("/catalogue") - end - - scope "/dev" do - pipe_through :browser - - forward "/mailbox", Plug.Swoosh.MailboxPreview - end - end - - ## Authentication routes + ## Endpoints para versão browser scope "/", FuschiaWeb do pipe_through [:browser, :redirect_if_user_is_authenticated] @@ -104,4 +71,58 @@ defmodule FuschiaWeb.Router do post "/confirmar/:token", UserConfirmationController, :update end end + + scope "/" do + pipe_through :browser + + get "/swaggerui", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi" + + live "/example", FuschiaWeb.ExampleLive + end + + ## Endpoints para API pública + + scope "/api/v1" do + pipe_through :api_swagger + + get "/openapi", OpenApiSpex.Plug.RenderSpec, [] + end + + scope "/api/v1", FuschiaWeb do + pipe_through :api + + post "/acessar", AuthController, :login + post "/cadastrar", AuthController, :signup + end + + scope "/api/v1", FuschiaWeb do + pipe_through [:api_auth, :api] + + resources "/campi", CampusController, only: [:create, :index, :delete] + end + + ## Endpoints para ambiente de desenvolvimento + + if Mix.env() in [:dev, :test] do + import Phoenix.LiveDashboard.Router + + scope "/" do + pipe_through :browser + + live_dashboard "/dashboard", metrics: FuschiaWeb.Telemetry + end + end + + if Mix.env() == :dev do + scope "/" do + pipe_through :browser + surface_catalogue("/catalogue") + end + + scope "/dev" do + pipe_through :browser + + forward "/mailbox", Plug.Swoosh.MailboxPreview + end + end end diff --git a/priv/gettext/en/LC_MESSAGES/errors.po b/priv/gettext/en/LC_MESSAGES/errors.po index 4de79815..90993090 100644 --- a/priv/gettext/en/LC_MESSAGES/errors.po +++ b/priv/gettext/en/LC_MESSAGES/errors.po @@ -112,7 +112,7 @@ msgid "does not exist" msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia/accounts/user.ex:166 +#: lib/fuschia/accounts/user.ex:172 msgid "didn't change" msgstr "" @@ -127,26 +127,26 @@ msgid "should be at most 160 character(s)" msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia/accounts/user.ex:145 +#: lib/fuschia/accounts/user.ex:151 msgid "does not match password" msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_settings_controller.ex:69 +#: lib/fuschia_web/controllers/browser/user_settings_controller.ex:71 msgid "Email change link is invalid or it has expired." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_reset_password_controller.ex:60 +#: lib/fuschia_web/controllers/browser/user_reset_password_controller.ex:60 msgid "Reset password link is invalid or it has expired." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_confirmation_controller.ex:57 +#: lib/fuschia_web/controllers/browser/user_confirmation_controller.ex:57 msgid "User confirmation link is invalid or it has expired." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_session_controller.ex:20 +#: lib/fuschia_web/controllers/browser/user_session_controller.ex:20 msgid "Invalid email or password" msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/erros.po b/priv/gettext/en/LC_MESSAGES/erros.po index 0b7dcca3..2c67db8a 100644 --- a/priv/gettext/en/LC_MESSAGES/erros.po +++ b/priv/gettext/en/LC_MESSAGES/erros.po @@ -12,6 +12,6 @@ msgstr "" "Plural-Forms: nplurals=2\n" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_auth.ex:152 +#: lib/fuschia_web/controllers/browser/user_auth.ex:159 msgid "You must log in to access this page." msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/infos.po b/priv/gettext/en/LC_MESSAGES/infos.po index 315d9aab..93e83534 100644 --- a/priv/gettext/en/LC_MESSAGES/infos.po +++ b/priv/gettext/en/LC_MESSAGES/infos.po @@ -12,46 +12,46 @@ msgstr "" "Plural-Forms: nplurals=2\n" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_settings_controller.ex:30 +#: lib/fuschia_web/controllers/browser/user_settings_controller.ex:30 msgid "A link to confirm your email change has been sent to the new address." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_settings_controller.ex:62 +#: lib/fuschia_web/controllers/browser/user_settings_controller.ex:64 msgid "Email changed successfully." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_confirmation_controller.ex:23 +#: lib/fuschia_web/controllers/browser/user_confirmation_controller.ex:23 msgid "If your email is in our system and it has not been confirmed yet, you will receive an email with instructions shortly." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_reset_password_controller.ex:25 +#: lib/fuschia_web/controllers/browser/user_reset_password_controller.ex:25 msgid "If your email is in our system, you will receive instructions to reset your password shortly." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_session_controller.ex:26 +#: lib/fuschia_web/controllers/browser/user_session_controller.ex:26 msgid "Logged out successfully." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_reset_password_controller.ex:43 +#: lib/fuschia_web/controllers/browser/user_reset_password_controller.ex:43 msgid "Password reset successfully." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_settings_controller.ex:49 +#: lib/fuschia_web/controllers/browser/user_settings_controller.ex:49 msgid "Password updated successfully." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_confirmation_controller.ex:41 +#: lib/fuschia_web/controllers/browser/user_confirmation_controller.ex:41 msgid "User confirmed successfully." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_registration_controller.ex:25 +#: lib/fuschia_web/controllers/browser/user_registration_controller.ex:25 msgid "User successfully registered." msgstr "" diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot index a79a29b9..d42292b5 100644 --- a/priv/gettext/errors.pot +++ b/priv/gettext/errors.pot @@ -109,7 +109,7 @@ msgid "does not exist" msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia/accounts/user.ex:166 +#: lib/fuschia/accounts/user.ex:172 msgid "didn't change" msgstr "" @@ -124,26 +124,26 @@ msgid "should be at most 160 character(s)" msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia/accounts/user.ex:145 +#: lib/fuschia/accounts/user.ex:151 msgid "does not match password" msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_settings_controller.ex:69 +#: lib/fuschia_web/controllers/browser/user_settings_controller.ex:71 msgid "Email change link is invalid or it has expired." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_reset_password_controller.ex:60 +#: lib/fuschia_web/controllers/browser/user_reset_password_controller.ex:60 msgid "Reset password link is invalid or it has expired." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_confirmation_controller.ex:57 +#: lib/fuschia_web/controllers/browser/user_confirmation_controller.ex:57 msgid "User confirmation link is invalid or it has expired." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_session_controller.ex:20 +#: lib/fuschia_web/controllers/browser/user_session_controller.ex:20 msgid "Invalid email or password" msgstr "" diff --git a/priv/gettext/erros.pot b/priv/gettext/erros.pot index dc4f784e..333c498f 100644 --- a/priv/gettext/erros.pot +++ b/priv/gettext/erros.pot @@ -11,6 +11,6 @@ msgid "" msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_auth.ex:152 +#: lib/fuschia_web/controllers/browser/user_auth.ex:159 msgid "You must log in to access this page." msgstr "" diff --git a/priv/gettext/infos.pot b/priv/gettext/infos.pot index 870a95eb..85eef70b 100644 --- a/priv/gettext/infos.pot +++ b/priv/gettext/infos.pot @@ -11,46 +11,46 @@ msgid "" msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_settings_controller.ex:30 +#: lib/fuschia_web/controllers/browser/user_settings_controller.ex:30 msgid "A link to confirm your email change has been sent to the new address." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_settings_controller.ex:62 +#: lib/fuschia_web/controllers/browser/user_settings_controller.ex:64 msgid "Email changed successfully." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_confirmation_controller.ex:23 +#: lib/fuschia_web/controllers/browser/user_confirmation_controller.ex:23 msgid "If your email is in our system and it has not been confirmed yet, you will receive an email with instructions shortly." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_reset_password_controller.ex:25 +#: lib/fuschia_web/controllers/browser/user_reset_password_controller.ex:25 msgid "If your email is in our system, you will receive instructions to reset your password shortly." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_session_controller.ex:26 +#: lib/fuschia_web/controllers/browser/user_session_controller.ex:26 msgid "Logged out successfully." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_reset_password_controller.ex:43 +#: lib/fuschia_web/controllers/browser/user_reset_password_controller.ex:43 msgid "Password reset successfully." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_settings_controller.ex:49 +#: lib/fuschia_web/controllers/browser/user_settings_controller.ex:49 msgid "Password updated successfully." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_confirmation_controller.ex:41 +#: lib/fuschia_web/controllers/browser/user_confirmation_controller.ex:41 msgid "User confirmed successfully." msgstr "" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_registration_controller.ex:25 +#: lib/fuschia_web/controllers/browser/user_registration_controller.ex:25 msgid "User successfully registered." msgstr "" diff --git a/priv/gettext/pt_BR/LC_MESSAGES/errors.po b/priv/gettext/pt_BR/LC_MESSAGES/errors.po index d5b0b682..5566d1a7 100644 --- a/priv/gettext/pt_BR/LC_MESSAGES/errors.po +++ b/priv/gettext/pt_BR/LC_MESSAGES/errors.po @@ -102,7 +102,7 @@ msgid "does not exist" msgstr "não existe" #, elixir-autogen, elixir-format -#: lib/fuschia/accounts/user.ex:166 +#: lib/fuschia/accounts/user.ex:172 msgid "didn't change" msgstr "não pode ser igual ao antigo" @@ -117,26 +117,26 @@ msgid "should be at most 160 character(s)" msgstr "deve possuir no máximo %{count} caracteres" #, elixir-autogen, elixir-format -#: lib/fuschia/accounts/user.ex:145 +#: lib/fuschia/accounts/user.ex:151 msgid "does not match password" msgstr "as senhas precisam ser iguais" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_settings_controller.ex:69 +#: lib/fuschia_web/controllers/browser/user_settings_controller.ex:71 msgid "Email change link is invalid or it has expired." msgstr "Link para troca de Email é inválido ou expirou." #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_reset_password_controller.ex:60 +#: lib/fuschia_web/controllers/browser/user_reset_password_controller.ex:60 msgid "Reset password link is invalid or it has expired." msgstr "Link para recuperação de senha é inválido ou expirou." #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_confirmation_controller.ex:57 +#: lib/fuschia_web/controllers/browser/user_confirmation_controller.ex:57 msgid "User confirmation link is invalid or it has expired." msgstr "Link para confirmação da conta é inválido ou expirou." #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_session_controller.ex:20 +#: lib/fuschia_web/controllers/browser/user_session_controller.ex:20 msgid "Invalid email or password" msgstr "Email ou senha inválidos." diff --git a/priv/gettext/pt_BR/LC_MESSAGES/erros.po b/priv/gettext/pt_BR/LC_MESSAGES/erros.po index 7acc4c84..f34a8454 100644 --- a/priv/gettext/pt_BR/LC_MESSAGES/erros.po +++ b/priv/gettext/pt_BR/LC_MESSAGES/erros.po @@ -12,6 +12,6 @@ msgstr "" "Plural-Forms: nplurals=2\n" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_auth.ex:152 +#: lib/fuschia_web/controllers/browser/user_auth.ex:159 msgid "You must log in to access this page." msgstr "" diff --git a/priv/gettext/pt_BR/LC_MESSAGES/infos.po b/priv/gettext/pt_BR/LC_MESSAGES/infos.po index 8bba6cbb..73d21a38 100644 --- a/priv/gettext/pt_BR/LC_MESSAGES/infos.po +++ b/priv/gettext/pt_BR/LC_MESSAGES/infos.po @@ -12,46 +12,46 @@ msgstr "" "Plural-Forms: nplurals=2\n" #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_settings_controller.ex:30 +#: lib/fuschia_web/controllers/browser/user_settings_controller.ex:30 msgid "A link to confirm your email change has been sent to the new address." msgstr "Um link de confirmação da troca do seu email foi enviado para o novo endereço." #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_settings_controller.ex:62 +#: lib/fuschia_web/controllers/browser/user_settings_controller.ex:64 msgid "Email changed successfully." msgstr "Email foi atualizado com sucesso." #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_confirmation_controller.ex:23 +#: lib/fuschia_web/controllers/browser/user_confirmation_controller.ex:23 msgid "If your email is in our system and it has not been confirmed yet, you will receive an email with instructions shortly." msgstr "Caso seu email esteja em nosso sistema porém ainda não foi confirmado, você irá receber um email com as instruções em breve." #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_reset_password_controller.ex:25 +#: lib/fuschia_web/controllers/browser/user_reset_password_controller.ex:25 msgid "If your email is in our system, you will receive instructions to reset your password shortly." msgstr "Caso seu email esteja em nosso sistema, você irá receber intruções para recuperar sua senha em breve." #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_session_controller.ex:26 +#: lib/fuschia_web/controllers/browser/user_session_controller.ex:26 msgid "Logged out successfully." msgstr "Você desconectou com sucesso." #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_reset_password_controller.ex:43 +#: lib/fuschia_web/controllers/browser/user_reset_password_controller.ex:43 msgid "Password reset successfully." msgstr "Senha recuperada com sucesso." #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_settings_controller.ex:49 +#: lib/fuschia_web/controllers/browser/user_settings_controller.ex:49 msgid "Password updated successfully." msgstr "Senha atualizada com sucesso." #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_confirmation_controller.ex:41 +#: lib/fuschia_web/controllers/browser/user_confirmation_controller.ex:41 msgid "User confirmed successfully." msgstr "Usuário confirmado com sucesso." #, elixir-autogen, elixir-format -#: lib/fuschia_web/controllers/user_registration_controller.ex:25 +#: lib/fuschia_web/controllers/browser/user_registration_controller.ex:25 msgid "User successfully registered." msgstr "Usuário foi cadastrado com sucesso."