diff --git a/apps/admin_api/lib/admin_api/v1/controllers/account_membership_controller.ex b/apps/admin_api/lib/admin_api/v1/controllers/account_membership_controller.ex index 2bb387935..dc71f9820 100644 --- a/apps/admin_api/lib/admin_api/v1/controllers/account_membership_controller.ex +++ b/apps/admin_api/lib/admin_api/v1/controllers/account_membership_controller.ex @@ -3,7 +3,7 @@ defmodule AdminAPI.V1.AccountMembershipController do import AdminAPI.V1.ErrorHandler alias AdminAPI.InviteEmail alias EWallet.{AccountMembershipPolicy, EmailValidator} - alias EWallet.Web.{Inviter, UrlValidator} + alias EWallet.Web.{Inviter, Originator, UrlValidator} alias EWalletDB.{Account, Membership, Role, User} @doc """ @@ -34,7 +34,8 @@ defmodule AdminAPI.V1.AccountMembershipController do {:ok, user_or_email} <- get_user_or_email(attrs), %Role{} = role <- Role.get_by_name(attrs["role_name"]) || {:error, :role_name_not_found}, {:ok, redirect_url} <- validate_redirect_url(attrs["redirect_url"]), - {:ok, _} <- assign_or_invite(user_or_email, account, role, redirect_url) do + originator <- Originator.extract(conn.assigns), + {:ok, _} <- assign_or_invite(user_or_email, account, role, redirect_url, originator) do render(conn, :empty, %{success: true}) else {true, :user_id_not_found} -> @@ -88,17 +89,24 @@ defmodule AdminAPI.V1.AccountMembershipController do end end - defp assign_or_invite(email, account, role, redirect_url) when is_binary(email) do + defp assign_or_invite(email, account, role, redirect_url, originator) when is_binary(email) do case EmailValidator.validate(email) do {:ok, email} -> - Inviter.invite_admin(email, account, role, redirect_url, &InviteEmail.create/2) + Inviter.invite_admin( + email, + account, + role, + redirect_url, + originator, + &InviteEmail.create/2 + ) error -> error end end - defp assign_or_invite(user, account, role, redirect_url) do + defp assign_or_invite(user, account, role, redirect_url, _originator) do case User.get_status(user) do :pending_confirmation -> user diff --git a/apps/admin_api/lib/admin_api/v1/controllers/reset_password_controller.ex b/apps/admin_api/lib/admin_api/v1/controllers/reset_password_controller.ex index b462a62cd..259e06c76 100644 --- a/apps/admin_api/lib/admin_api/v1/controllers/reset_password_controller.ex +++ b/apps/admin_api/lib/admin_api/v1/controllers/reset_password_controller.ex @@ -12,7 +12,7 @@ defmodule AdminAPI.V1.ResetPasswordController do when not is_nil(email) and not is_nil(redirect_url) do with {:ok, redirect_url} <- validate_redirect_url(redirect_url), %User{} = user <- User.get_by_email(email) || :user_email_not_found, - {_, _} <- ForgetPasswordRequest.delete_all(user), + {_, _} <- ForgetPasswordRequest.disable_all_for(user), %ForgetPasswordRequest{} = request <- ForgetPasswordRequest.generate(user), %Email{} = email_object <- ForgetPasswordEmail.create(request, redirect_url), %Email{} <- Mailer.deliver_now(email_object) do @@ -48,8 +48,9 @@ defmodule AdminAPI.V1.ResetPasswordController do ) do with %User{} = user <- get_user(email), %ForgetPasswordRequest{} = request <- get_request(user, token), + attrs <- Map.put(attrs, "originator", request), {:ok, %User{} = user} <- update_password(request, attrs) do - _ = ForgetPasswordRequest.delete_all(user) + _ = ForgetPasswordRequest.disable_all_for(user) render(conn, :empty, %{success: true}) else error when is_atom(error) -> @@ -76,7 +77,8 @@ defmodule AdminAPI.V1.ResetPasswordController do }) do User.update(request.user, %{ password: password, - password_confirmation: password_confirmation + password_confirmation: password_confirmation, + originator: request }) end end diff --git a/apps/admin_api/lib/admin_api/v1/controllers/self_controller.ex b/apps/admin_api/lib/admin_api/v1/controllers/self_controller.ex index f0c397153..99e0ccb4e 100644 --- a/apps/admin_api/lib/admin_api/v1/controllers/self_controller.ex +++ b/apps/admin_api/lib/admin_api/v1/controllers/self_controller.ex @@ -3,7 +3,7 @@ defmodule AdminAPI.V1.SelfController do import AdminAPI.V1.ErrorHandler alias AdminAPI.V1.{AccountHelper, AccountView, UserView} alias Ecto.Changeset - alias EWallet.Web.{Paginator, Preloader, SearchParser, SortParser} + alias EWallet.Web.{Originator, Paginator, Preloader, SearchParser, SortParser} alias EWalletDB.{Account, User} @mapped_fields %{ @@ -30,6 +30,8 @@ defmodule AdminAPI.V1.SelfController do """ def update(conn, attrs) do with {:ok, current_user} <- permit(:update, conn.assigns), + originator <- Originator.extract(conn.assigns), + attrs <- Map.put(attrs, "originator", originator), {:ok, user} <- User.update_without_password(current_user, attrs) do respond_single(user, conn) else @@ -42,7 +44,9 @@ defmodule AdminAPI.V1.SelfController do Uploads an image as avatar for the current user. """ def upload_avatar(conn, %{"avatar" => _} = attrs) do - with {:ok, current_user} <- permit(:update, conn.assigns) do + with {:ok, current_user} <- permit(:update, conn.assigns), + originator <- Originator.extract(conn.assigns), + attrs <- Map.put(attrs, "originator", originator) do current_user |> User.store_avatar(attrs) |> respond_single(conn) @@ -89,6 +93,10 @@ defmodule AdminAPI.V1.SelfController do end # Respond with a single admin + defp respond_single({:ok, user}, conn) do + render(conn, UserView, :user, %{user: user}) + end + defp respond_single(%User{} = user, conn) do render(conn, UserView, :user, %{user: user}) end diff --git a/apps/admin_api/lib/admin_api/v1/controllers/user_controller.ex b/apps/admin_api/lib/admin_api/v1/controllers/user_controller.ex index 29dcd5263..2d74f1f85 100644 --- a/apps/admin_api/lib/admin_api/v1/controllers/user_controller.ex +++ b/apps/admin_api/lib/admin_api/v1/controllers/user_controller.ex @@ -4,7 +4,7 @@ defmodule AdminAPI.V1.UserController do alias AdminAPI.V1.AccountHelper alias Ecto.Changeset alias EWallet.UserPolicy - alias EWallet.Web.{Paginator, SearchParser, SortParser} + alias EWallet.Web.{Originator, Paginator, SearchParser, SortParser} alias EWalletDB.{Account, AccountUser, User, UserQuery} # The field names to be mapped into DB column names. @@ -112,6 +112,8 @@ defmodule AdminAPI.V1.UserController do @spec create(Plug.Conn.t(), map()) :: Plug.Conn.t() def create(conn, attrs) do with :ok <- permit(:create, conn.assigns, nil), + originator <- Originator.extract(conn.assigns), + attrs <- Map.put(attrs, "originator", originator), {:ok, user} <- User.insert(attrs), %Account{} = account <- AccountHelper.get_current_account(conn), {:ok, _account_user} <- AccountUser.link(account.uuid, user.uuid) do @@ -136,7 +138,9 @@ defmodule AdminAPI.V1.UserController do ) when is_binary(id) and byte_size(id) > 0 do with %User{} = user <- User.get(id) || {:error, :unauthorized}, - :ok <- permit(:update, conn.assigns, user) do + :ok <- permit(:update, conn.assigns, user), + originator <- Originator.extract(conn.assigns), + attrs <- Map.put(attrs, "originator", originator) do user |> User.update(attrs) |> respond_single(conn) @@ -154,7 +158,9 @@ defmodule AdminAPI.V1.UserController do ) when is_binary(id) and byte_size(id) > 0 do with %User{} = user <- User.get_by_provider_user_id(id) || {:error, :unauthorized}, - :ok <- permit(:update, conn.assigns, user) do + :ok <- permit(:update, conn.assigns, user), + originator <- Originator.extract(conn.assigns), + attrs <- Map.put(attrs, "originator", originator) do user |> User.update(attrs) |> respond_single(conn) diff --git a/apps/admin_api/mix.exs b/apps/admin_api/mix.exs index 90c5b6e3c..e4bcbb2ec 100644 --- a/apps/admin_api/mix.exs +++ b/apps/admin_api/mix.exs @@ -49,8 +49,6 @@ defmodule AdminAPI.Mixfile do {:cowboy, "~> 1.0"}, {:cors_plug, "~> 1.5"}, {:sentry, "~> 6.2.0"}, - {:bamboo, "~> 0.8"}, - {:bamboo_smtp, "~> 1.4.0"}, {:bodyguard, "~> 2.2"}, {:deferred_config, "~> 0.1.0"}, {:ewallet_db, in_umbrella: true}, diff --git a/apps/admin_api/test/admin_api/emails/invite_email_test.exs b/apps/admin_api/test/admin_api/emails/invite_email_test.exs index 5b376d37a..886c3cada 100644 --- a/apps/admin_api/test/admin_api/emails/invite_email_test.exs +++ b/apps/admin_api/test/admin_api/emails/invite_email_test.exs @@ -1,10 +1,10 @@ defmodule AdminAPI.InviteEmailTest do use AdminAPI.ConnCase alias AdminAPI.InviteEmail - alias EWalletDB.Invite + alias EWalletDB.{Invite, User} defp create_email(email) do - admin = insert(:admin, email: email) + {:ok, admin} = :admin |> params_for(email: email) |> User.insert() {:ok, invite} = Invite.generate(admin) {InviteEmail.create(invite, "https://invite_url/?email={email}&token={token}"), invite.token} diff --git a/apps/admin_api/test/admin_api/v1/auth/admin_api_auth_test.exs b/apps/admin_api/test/admin_api/v1/auth/admin_api_auth_test.exs index 14c6466fe..2810d8aa2 100644 --- a/apps/admin_api/test/admin_api/v1/auth/admin_api_auth_test.exs +++ b/apps/admin_api/test/admin_api/v1/auth/admin_api_auth_test.exs @@ -1,6 +1,7 @@ defmodule AdminAPI.Web.V1.AdminAPIAuthTest do use AdminAPI.ConnCase, async: true alias AdminAPI.V1.AdminAPIAuth + alias EWalletDB.User def authenticate(scheme, user_id, token) do encoded_key = Base.encode64(user_id <> ":" <> token) @@ -11,7 +12,7 @@ defmodule AdminAPI.Web.V1.AdminAPIAuthTest do end setup do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() %{ user: user, @@ -27,7 +28,7 @@ defmodule AdminAPI.Web.V1.AdminAPIAuthTest do assert auth.authenticated == true assert auth.auth_scheme == :admin assert auth.auth_scheme_name == "OMGAdmin" - assert auth.admin_user == meta.user + assert auth.admin_user.uuid == meta.user.uuid assert auth.auth_user_id == meta.user.id assert auth.auth_auth_token == meta.auth_token.token end diff --git a/apps/admin_api/test/admin_api/v1/auth/admin_user_auth_test.exs b/apps/admin_api/test/admin_api/v1/auth/admin_user_auth_test.exs index 2c95f1130..583ac3900 100644 --- a/apps/admin_api/test/admin_api/v1/auth/admin_user_auth_test.exs +++ b/apps/admin_api/test/admin_api/v1/auth/admin_user_auth_test.exs @@ -1,7 +1,7 @@ defmodule AdminAPI.Web.V1.AdminUserAuthTest do use AdminAPI.ConnCase, async: true alias AdminAPI.V1.AdminUserAuth - alias EWalletDB.AuthToken + alias EWalletDB.{AuthToken, User} def auth_header(user_id, token) do encoded_key = Base.encode64(user_id <> ":" <> token) @@ -9,7 +9,7 @@ defmodule AdminAPI.Web.V1.AdminUserAuthTest do end setup do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() %{ user: user, @@ -22,7 +22,7 @@ defmodule AdminAPI.Web.V1.AdminUserAuthTest do auth = auth_header(meta.user.id, meta.auth_token.token) assert auth.authenticated == true - assert auth.admin_user == meta.user + assert auth.admin_user.uuid == meta.user.uuid assert auth.auth_user_id == meta.user.id assert auth.auth_auth_token == meta.auth_token.token end diff --git a/apps/admin_api/test/admin_api/v1/channels/user_channel_test.exs b/apps/admin_api/test/admin_api/v1/channels/user_channel_test.exs index f576e6990..166bd42e2 100644 --- a/apps/admin_api/test/admin_api/v1/channels/user_channel_test.exs +++ b/apps/admin_api/test/admin_api/v1/channels/user_channel_test.exs @@ -2,11 +2,12 @@ defmodule AdminAPI.V1.UserChannelTest do use AdminAPI.ChannelCase, async: false alias AdminAPI.V1.UserChannel + alias EWalletDB.User describe "join/3 as provider" do test "joins the channel with authenticated account and valid user ID" do account = insert(:account) - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() {res, _, socket} = "test" @@ -19,7 +20,7 @@ defmodule AdminAPI.V1.UserChannelTest do test "joins the channel with authenticated account and valid provider user ID" do account = insert(:account) - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() {res, _, socket} = "test" diff --git a/apps/admin_api/test/admin_api/v1/controllers/admin_auth/account_membership_controller_test.exs b/apps/admin_api/test/admin_api/v1/controllers/admin_auth/account_membership_controller_test.exs index 887325ca2..af064f5c1 100644 --- a/apps/admin_api/test/admin_api/v1/controllers/admin_auth/account_membership_controller_test.exs +++ b/apps/admin_api/test/admin_api/v1/controllers/admin_auth/account_membership_controller_test.exs @@ -11,7 +11,7 @@ defmodule AdminAPI.V1.AdminAuth.AccountMembershipControllerTest do master = Account.get_master_account() admin = get_test_admin() account = insert(:account) - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() role = insert(:role) _ = insert(:membership, %{account: account, user: user, role: role}) @@ -187,9 +187,11 @@ defmodule AdminAPI.V1.AdminAuth.AccountMembershipControllerTest do describe "/account.assign_user" do test "returns empty success if assigned with user_id successfully" do + {:ok, user} = :user |> params_for() |> User.insert() + response = admin_user_request("/account.assign_user", %{ - user_id: insert(:user).id, + user_id: user.id, account_id: insert(:account).id, role_name: insert(:role).name, redirect_url: @redirect_url @@ -309,9 +311,11 @@ defmodule AdminAPI.V1.AdminAuth.AccountMembershipControllerTest do end test "returns an error if the given account id does not exist" do + {:ok, user} = :user |> params_for() |> User.insert() + response = admin_user_request("/account.assign_user", %{ - user_id: insert(:user).id, + user_id: user.id, account_id: "acc_12345678901234567890123456", role_name: insert(:role).name, redirect_url: @redirect_url @@ -326,9 +330,11 @@ defmodule AdminAPI.V1.AdminAuth.AccountMembershipControllerTest do end test "returns an error if the given role does not exist" do + {:ok, user} = :user |> params_for() |> User.insert() + response = admin_user_request("/account.assign_user", %{ - user_id: insert(:user).id, + user_id: user.id, account_id: insert(:account).id, role_name: "invalid_role", redirect_url: @redirect_url @@ -346,7 +352,7 @@ defmodule AdminAPI.V1.AdminAuth.AccountMembershipControllerTest do describe "/account.unassign_user" do test "returns empty success if unassigned successfully" do account = insert(:account) - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() _membership = insert(:membership, %{account: account, user: user}) response = @@ -360,7 +366,7 @@ defmodule AdminAPI.V1.AdminAuth.AccountMembershipControllerTest do end test "returns an error if the user was not previously assigned to the account" do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() account = insert(:account) response = @@ -393,9 +399,11 @@ defmodule AdminAPI.V1.AdminAuth.AccountMembershipControllerTest do end test "returns an error if the given account id does not exist" do + {:ok, user} = :user |> params_for() |> User.insert() + response = admin_user_request("/account.unassign_user", %{ - user_id: insert(:user).id, + user_id: user.id, account_id: "acc_12345678901234567890123456" }) diff --git a/apps/admin_api/test/admin_api/v1/controllers/admin_auth/admin_auth_controller_test.exs b/apps/admin_api/test/admin_api/v1/controllers/admin_auth/admin_auth_controller_test.exs index f09ca0c61..b1c0dbb6d 100644 --- a/apps/admin_api/test/admin_api/v1/controllers/admin_auth/admin_auth_controller_test.exs +++ b/apps/admin_api/test/admin_api/v1/controllers/admin_auth/admin_auth_controller_test.exs @@ -1,7 +1,7 @@ defmodule AdminAPI.V1.AdminAuth.AdminAuthControllerTest do use AdminAPI.ConnCase, async: true alias EWallet.Web.V1.{AccountSerializer, UserSerializer} - alias EWalletDB.{Account, AuthToken, Membership, Repo, Role, User} + alias EWalletDB.{Account, AuthToken, Membership, Repo, Role, System, User} describe "/admin.login" do test "responds with a new auth token if the given email and password are valid" do @@ -92,7 +92,10 @@ defmodule AdminAPI.V1.AdminAuth.AdminAuthControllerTest do {:ok, _user} = [email: @user_email] |> User.get_by() - |> User.update_without_password(%{invite_uuid: insert(:invite).uuid}) + |> User.update_without_password(%{ + invite_uuid: insert(:invite).uuid, + originator: %System{} + }) response = unauthenticated_request("/admin.login", %{email: @user_email, password: @password}) diff --git a/apps/admin_api/test/admin_api/v1/controllers/admin_auth/admin_user_controller_test.exs b/apps/admin_api/test/admin_api/v1/controllers/admin_auth/admin_user_controller_test.exs index f28825b5e..21dce325e 100644 --- a/apps/admin_api/test/admin_api/v1/controllers/admin_auth/admin_user_controller_test.exs +++ b/apps/admin_api/test/admin_api/v1/controllers/admin_auth/admin_user_controller_test.exs @@ -1,6 +1,7 @@ defmodule AdminAPI.V1.AdminAuth.AdminUserControllerTest do use AdminAPI.ConnCase, async: true alias Ecto.UUID + alias EWalletDB.User describe "/admin.all" do test "returns a list of admins and pagination data" do @@ -64,7 +65,7 @@ defmodule AdminAPI.V1.AdminAuth.AdminUserControllerTest do end test "returns 'user:id_not_found' if the given ID is not an admin" do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() response = admin_user_request("/admin.get", %{"id" => user.id}) refute response["success"] diff --git a/apps/admin_api/test/admin_api/v1/controllers/admin_auth/invite_controller_test.exs b/apps/admin_api/test/admin_api/v1/controllers/admin_auth/invite_controller_test.exs index 8e817f2fb..f79a6e10c 100644 --- a/apps/admin_api/test/admin_api/v1/controllers/admin_auth/invite_controller_test.exs +++ b/apps/admin_api/test/admin_api/v1/controllers/admin_auth/invite_controller_test.exs @@ -14,7 +14,7 @@ defmodule AdminAPI.V1.AdminAuth.InviteControllerTest do describe "InviteController.accept/2" do test "returns success if invite is accepted successfully" do - user = insert(:admin, is_admin: false) + {:ok, user} = :admin |> params_for(is_admin: false) |> User.insert() {:ok, invite} = Invite.generate(user, preload: :user) response = request(invite.user.email, invite.token, "some_password", "some_password") @@ -44,7 +44,7 @@ defmodule AdminAPI.V1.AdminAuth.InviteControllerTest do end test "returns :invite_not_found error if the email has not been invited" do - user = insert(:admin) + {:ok, user} = :admin |> params_for() |> User.insert() {:ok, invite} = Invite.generate(user) response = request("unknown@example.com", invite.token, "some_password", "some_password") @@ -58,7 +58,7 @@ defmodule AdminAPI.V1.AdminAuth.InviteControllerTest do end test "returns :invite_not_found error if the token is incorrect" do - user = insert(:admin) + {:ok, user} = :admin |> params_for() |> User.insert() {:ok, _invite} = Invite.generate(user) response = request(user.email, "wrong_token", "some_password", "some_password") @@ -72,7 +72,7 @@ defmodule AdminAPI.V1.AdminAuth.InviteControllerTest do end test "returns :passwords_mismatch error if the passwords do not match" do - user = insert(:admin) + {:ok, user} = :admin |> params_for() |> User.insert() {:ok, invite} = Invite.generate(user) response = request(user.email, invite.token, "some_password", "mismatch_password") @@ -84,7 +84,7 @@ defmodule AdminAPI.V1.AdminAuth.InviteControllerTest do end test "returns client:invalid_parameter error if the password has less than 8 characters" do - user = insert(:admin) + {:ok, user} = :admin |> params_for() |> User.insert() {:ok, invite} = Invite.generate(user) response = request(user.email, invite.token, "short", "short") @@ -98,7 +98,7 @@ defmodule AdminAPI.V1.AdminAuth.InviteControllerTest do end test "returns :invalid_parameter error if a required parameter is missing" do - user = insert(:admin) + {:ok, user} = :admin |> params_for() |> User.insert() {:ok, invite} = Invite.generate(user) # Missing passwords diff --git a/apps/admin_api/test/admin_api/v1/controllers/admin_auth/reset_password_controller_test.exs b/apps/admin_api/test/admin_api/v1/controllers/admin_auth/reset_password_controller_test.exs index 4d303e049..ef8aa35ad 100644 --- a/apps/admin_api/test/admin_api/v1/controllers/admin_auth/reset_password_controller_test.exs +++ b/apps/admin_api/test/admin_api/v1/controllers/admin_auth/reset_password_controller_test.exs @@ -8,7 +8,7 @@ defmodule AdminAPI.V1.AdminAuth.ResetPasswordControllerTest do describe "ResetPasswordController.reset/2" do test "returns success if the request was generated successfully" do - user = insert(:admin) + {:ok, user} = :admin |> params_for() |> User.insert() response = unauthenticated_request("/admin.reset_password", %{ @@ -66,7 +66,7 @@ defmodule AdminAPI.V1.AdminAuth.ResetPasswordControllerTest do end test "returns an error if the email is not supplied" do - user = insert(:admin) + {:ok, user} = :admin |> params_for() |> User.insert() response = unauthenticated_request("/admin.reset_password", %{ @@ -84,7 +84,7 @@ defmodule AdminAPI.V1.AdminAuth.ResetPasswordControllerTest do end test "returns an error if the redirect_url is not supplied" do - user = insert(:admin) + {:ok, user} = :admin |> params_for() |> User.insert() response = unauthenticated_request("/admin.reset_password", %{ @@ -104,7 +104,7 @@ defmodule AdminAPI.V1.AdminAuth.ResetPasswordControllerTest do describe "ResetPasswordController.update/2" do test "returns success and updates the password if the password has been reset succesfully" do - user = insert(:admin) + {:ok, user} = :admin |> params_for() |> User.insert() request = ForgetPasswordRequest.generate(user) assert user.password_hash != Crypto.hash_password("password") @@ -120,11 +120,11 @@ defmodule AdminAPI.V1.AdminAuth.ResetPasswordControllerTest do assert response["success"] user = User.get(user.id) assert Crypto.verify_password("password", user.password_hash) - assert ForgetPasswordRequest |> Repo.all() |> length() == 0 + assert ForgetPasswordRequest.all_active() |> length() == 0 end test "returns an email_not_found error when the user is not found" do - user = insert(:admin) + {:ok, user} = :admin |> params_for() |> User.insert() request = ForgetPasswordRequest.generate(user) response = @@ -141,7 +141,7 @@ defmodule AdminAPI.V1.AdminAuth.ResetPasswordControllerTest do end test "returns a token_not_found error when the request is not found" do - user = insert(:admin) + {:ok, user} = :admin |> params_for() |> User.insert() _request = ForgetPasswordRequest.generate(user) assert user.password_hash != Crypto.hash_password("password") @@ -160,7 +160,7 @@ defmodule AdminAPI.V1.AdminAuth.ResetPasswordControllerTest do end test "returns a client:invalid_parameter error when the password is too short" do - user = insert(:admin) + {:ok, user} = :admin |> params_for() |> User.insert() request = ForgetPasswordRequest.generate(user) assert user.password_hash != Crypto.hash_password("password") @@ -183,7 +183,7 @@ defmodule AdminAPI.V1.AdminAuth.ResetPasswordControllerTest do end test "returns an invalid parameter error when the email is not sent" do - user = insert(:admin) + {:ok, user} = :admin |> params_for() |> User.insert() request = ForgetPasswordRequest.generate(user) assert user.password_hash != Crypto.hash_password("password") diff --git a/apps/admin_api/test/admin_api/v1/controllers/admin_auth/user_auth_controller_test.exs b/apps/admin_api/test/admin_api/v1/controllers/admin_auth/user_auth_controller_test.exs index acd369f0a..b42890173 100644 --- a/apps/admin_api/test/admin_api/v1/controllers/admin_auth/user_auth_controller_test.exs +++ b/apps/admin_api/test/admin_api/v1/controllers/admin_auth/user_auth_controller_test.exs @@ -1,10 +1,10 @@ defmodule AdminAPI.V1.AdminAuth.UserAuthControllerTest do use AdminAPI.ConnCase, async: true - alias EWalletDB.AuthToken + alias EWalletDB.{AuthToken, User} describe "/user.login" do test "responds with a new auth token if id is valid" do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() response = admin_user_request("/user.login", %{id: user.id}) auth_token = get_last_inserted(AuthToken) diff --git a/apps/admin_api/test/admin_api/v1/controllers/admin_auth/user_controller_test.exs b/apps/admin_api/test/admin_api/v1/controllers/admin_auth/user_controller_test.exs index 2bab0a5fb..e7dcc2de6 100644 --- a/apps/admin_api/test/admin_api/v1/controllers/admin_auth/user_controller_test.exs +++ b/apps/admin_api/test/admin_api/v1/controllers/admin_auth/user_controller_test.exs @@ -1,7 +1,7 @@ defmodule AdminAPI.V1.AdminAuth.UserControllerTest do use AdminAPI.ConnCase, async: true alias EWallet.Web.Date - alias EWalletDB.{Account, AccountUser} + alias EWalletDB.{Account, AccountUser, User} describe "/user.all" do test "returns a list of users and pagination data" do @@ -354,7 +354,7 @@ defmodule AdminAPI.V1.AdminAuth.UserControllerTest do describe "/user.update" do test "updates the user if attributes are valid" do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() # Prepare the update data while keeping only provider_user_id the same request_data = @@ -386,7 +386,7 @@ defmodule AdminAPI.V1.AdminAuth.UserControllerTest do end test "updates the metadata and encrypted metadata" do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() account = Account.get_master_account() {:ok, _} = AccountUser.link(account.uuid, user.uuid) @@ -451,11 +451,13 @@ defmodule AdminAPI.V1.AdminAuth.UserControllerTest do end test "returns an 'invalid parameter' error when sending nil for metadata/encrypted_metadata" do - user = - insert(:user, %{ + {:ok, user} = + :user + |> params_for(%{ metadata: %{first_name: "updated_first_name"}, encrypted_metadata: %{my_secret_stuff: "123"} }) + |> User.insert() account = Account.get_master_account() {:ok, _} = AccountUser.link(account.uuid, user.uuid) @@ -499,7 +501,7 @@ defmodule AdminAPI.V1.AdminAuth.UserControllerTest do end test "returns an error if username is not provided" do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() # ExMachine will remove the param if set to nil. request_data = diff --git a/apps/admin_api/test/admin_api/v1/controllers/admin_auth/wallet_controller_test.exs b/apps/admin_api/test/admin_api/v1/controllers/admin_auth/wallet_controller_test.exs index 69411f0c5..665d6bc87 100644 --- a/apps/admin_api/test/admin_api/v1/controllers/admin_auth/wallet_controller_test.exs +++ b/apps/admin_api/test/admin_api/v1/controllers/admin_auth/wallet_controller_test.exs @@ -212,6 +212,7 @@ defmodule AdminAPI.V1.AdminAuth.WalletControllerTest do wallets = response["data"]["data"] assert response["success"] + assert Enum.count(wallets) == 4 assert Enum.at(wallets, 0)["address"] == "bbbb111111111111" assert Enum.at(wallets, 1)["address"] == "aaaa333333333333" @@ -458,7 +459,7 @@ defmodule AdminAPI.V1.AdminAuth.WalletControllerTest do end test "fails to insert a primary wallet for a user" do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() response = admin_user_request("/wallet.create", %{ @@ -478,8 +479,7 @@ defmodule AdminAPI.V1.AdminAuth.WalletControllerTest do end test "inserts two secondary wallets for a user" do - user = insert(:user) - assert Wallet |> Repo.all() |> length() == 3 + {:ok, user} = :user |> params_for() |> User.insert() response_1 = admin_user_request("/wallet.create", %{ @@ -507,12 +507,12 @@ defmodule AdminAPI.V1.AdminAuth.WalletControllerTest do assert "secondary_" <> _ = response_2["data"]["identifier"] assert response_2["data"]["name"] == "MyWallet2" - wallets = Repo.all(Wallet) - assert length(wallets) == 5 + wallets = Wallet |> Repo.all() |> Repo.preload(:user) + assert Enum.count(wallets) == 6 end test "fails to insert a burn wallet for a user" do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() response = admin_user_request("/wallet.create", %{ @@ -533,7 +533,7 @@ defmodule AdminAPI.V1.AdminAuth.WalletControllerTest do test "fails to insert a new wallet when both user and account are specified" do account = insert(:account) - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() response = admin_user_request("/wallet.create", %{ diff --git a/apps/admin_api/test/admin_api/v1/controllers/provider_auth/account_membership_controller_test.exs b/apps/admin_api/test/admin_api/v1/controllers/provider_auth/account_membership_controller_test.exs index caeece321..6057fd6eb 100644 --- a/apps/admin_api/test/admin_api/v1/controllers/provider_auth/account_membership_controller_test.exs +++ b/apps/admin_api/test/admin_api/v1/controllers/provider_auth/account_membership_controller_test.exs @@ -11,7 +11,7 @@ defmodule AdminAPI.V1.ProviderAuth.AccountMembershipControllerTest do master = Account.get_master_account() admin = get_test_admin() account = insert(:account) - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() role = insert(:role) _ = insert(:membership, %{account: account, user: user, role: role}) @@ -187,9 +187,11 @@ defmodule AdminAPI.V1.ProviderAuth.AccountMembershipControllerTest do describe "/account.assign_user" do test "returns empty success if assigned with user_id successfully" do + {:ok, user} = :user |> params_for() |> User.insert() + response = provider_request("/account.assign_user", %{ - user_id: insert(:user).id, + user_id: user.id, account_id: insert(:account).id, role_name: insert(:role).name, redirect_url: @redirect_url @@ -309,9 +311,11 @@ defmodule AdminAPI.V1.ProviderAuth.AccountMembershipControllerTest do end test "returns an error if the given account id does not exist" do + {:ok, user} = :user |> params_for() |> User.insert() + response = provider_request("/account.assign_user", %{ - user_id: insert(:user).id, + user_id: user.id, account_id: "acc_12345678901234567890123456", role_name: insert(:role).name, redirect_url: @redirect_url @@ -326,9 +330,11 @@ defmodule AdminAPI.V1.ProviderAuth.AccountMembershipControllerTest do end test "returns an error if the given role does not exist" do + {:ok, user} = :user |> params_for() |> User.insert() + response = provider_request("/account.assign_user", %{ - user_id: insert(:user).id, + user_id: user.id, account_id: insert(:account).id, role_name: "invalid_role", redirect_url: @redirect_url @@ -346,7 +352,7 @@ defmodule AdminAPI.V1.ProviderAuth.AccountMembershipControllerTest do describe "/account.unassign_user" do test "returns empty success if unassigned successfully" do account = insert(:account) - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() _membership = insert(:membership, %{account: account, user: user}) response = @@ -360,7 +366,7 @@ defmodule AdminAPI.V1.ProviderAuth.AccountMembershipControllerTest do end test "returns an error if the user was not previously assigned to the account" do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() account = insert(:account) response = @@ -393,9 +399,11 @@ defmodule AdminAPI.V1.ProviderAuth.AccountMembershipControllerTest do end test "returns an error if the given account id does not exist" do + {:ok, user} = :user |> params_for() |> User.insert() + response = provider_request("/account.unassign_user", %{ - user_id: insert(:user).id, + user_id: user.id, account_id: "acc_12345678901234567890123456" }) diff --git a/apps/admin_api/test/admin_api/v1/controllers/provider_auth/admin_user_controller_test.exs b/apps/admin_api/test/admin_api/v1/controllers/provider_auth/admin_user_controller_test.exs index 5f980d332..492507f92 100644 --- a/apps/admin_api/test/admin_api/v1/controllers/provider_auth/admin_user_controller_test.exs +++ b/apps/admin_api/test/admin_api/v1/controllers/provider_auth/admin_user_controller_test.exs @@ -1,6 +1,7 @@ defmodule AdminAPI.V1.ProviderAuth.AdminControllerTest do use AdminAPI.ConnCase, async: true alias Ecto.UUID + alias EWalletDB.User describe "/admin.all" do test "returns a list of admins and pagination data" do @@ -64,7 +65,7 @@ defmodule AdminAPI.V1.ProviderAuth.AdminControllerTest do end test "returns 'unauthorized' if the given ID is not an admin" do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() response = provider_request("/admin.get", %{"id" => user.id}) refute response["success"] diff --git a/apps/admin_api/test/admin_api/v1/controllers/provider_auth/user_auth_controller_test.exs b/apps/admin_api/test/admin_api/v1/controllers/provider_auth/user_auth_controller_test.exs index 115c5629b..0d47a3082 100644 --- a/apps/admin_api/test/admin_api/v1/controllers/provider_auth/user_auth_controller_test.exs +++ b/apps/admin_api/test/admin_api/v1/controllers/provider_auth/user_auth_controller_test.exs @@ -1,10 +1,10 @@ defmodule AdminAPI.V1.ProviderAuth.UserAuthControllerTest do use AdminAPI.ConnCase, async: true - alias EWalletDB.AuthToken + alias EWalletDB.{AuthToken, User} describe "/user.login" do test "responds with a new auth token if id is valid" do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() response = provider_request("/user.login", %{id: user.id}) auth_token = get_last_inserted(AuthToken) diff --git a/apps/admin_api/test/admin_api/v1/controllers/provider_auth/user_controller_test.exs b/apps/admin_api/test/admin_api/v1/controllers/provider_auth/user_controller_test.exs index 64496ba3e..9a35da6f3 100644 --- a/apps/admin_api/test/admin_api/v1/controllers/provider_auth/user_controller_test.exs +++ b/apps/admin_api/test/admin_api/v1/controllers/provider_auth/user_controller_test.exs @@ -1,7 +1,7 @@ defmodule AdminAPI.V1.ProviderAuth.UserControllerTest do use AdminAPI.ConnCase, async: true alias EWallet.Web.Date - alias EWalletDB.{Account, AccountUser} + alias EWalletDB.{Account, AccountUser, User} describe "/user.all" do test "returns a list of users and pagination data" do @@ -237,7 +237,7 @@ defmodule AdminAPI.V1.ProviderAuth.UserControllerTest do describe "/user.update" do test "updates the user if attributes are valid" do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() # Prepare the update data while keeping only provider_user_id the same request_data = @@ -269,7 +269,7 @@ defmodule AdminAPI.V1.ProviderAuth.UserControllerTest do end test "updates the metadata and encrypted metadata" do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() account = Account.get_master_account() {:ok, _} = AccountUser.link(account.uuid, user.uuid) @@ -382,7 +382,7 @@ defmodule AdminAPI.V1.ProviderAuth.UserControllerTest do end test "returns an error if username is not provided" do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() # ExMachine will remove the param if set to nil. request_data = diff --git a/apps/admin_api/test/admin_api/v1/controllers/provider_auth/wallet_controller_test.exs b/apps/admin_api/test/admin_api/v1/controllers/provider_auth/wallet_controller_test.exs index b7d6edd97..5c488fb4d 100644 --- a/apps/admin_api/test/admin_api/v1/controllers/provider_auth/wallet_controller_test.exs +++ b/apps/admin_api/test/admin_api/v1/controllers/provider_auth/wallet_controller_test.exs @@ -416,7 +416,7 @@ defmodule AdminAPI.V1.ProviderAuth.WalletControllerTest do end test "fails to insert a primary wallet for a user" do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() response = provider_request("/wallet.create", %{ @@ -436,8 +436,8 @@ defmodule AdminAPI.V1.ProviderAuth.WalletControllerTest do end test "inserts two secondary wallets for a user" do - user = insert(:user) - assert Wallet |> Repo.all() |> length() == 3 + {:ok, user} = :user |> params_for() |> User.insert() + assert Wallet |> Repo.all() |> length() == 4 response_1 = provider_request("/wallet.create", %{ @@ -466,7 +466,7 @@ defmodule AdminAPI.V1.ProviderAuth.WalletControllerTest do assert response_2["data"]["name"] == "MyWallet2" wallets = Repo.all(Wallet) - assert length(wallets) == 5 + assert length(wallets) == 6 assert Enum.any?(wallets, fn wallet -> wallet.address == response_1["data"]["address"] @@ -478,7 +478,7 @@ defmodule AdminAPI.V1.ProviderAuth.WalletControllerTest do end test "fails to insert a burn wallet for a user" do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() response = provider_request("/wallet.create", %{ @@ -499,7 +499,7 @@ defmodule AdminAPI.V1.ProviderAuth.WalletControllerTest do test "fails to insert a new wallet when both user and account are specified" do account = insert(:account) - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() response = provider_request("/wallet.create", %{ diff --git a/apps/admin_api/test/admin_api/v1/serializers/membership_serializer_test.exs b/apps/admin_api/test/admin_api/v1/serializers/membership_serializer_test.exs index a6aa9d429..bbf178b5a 100644 --- a/apps/admin_api/test/admin_api/v1/serializers/membership_serializer_test.exs +++ b/apps/admin_api/test/admin_api/v1/serializers/membership_serializer_test.exs @@ -8,7 +8,7 @@ defmodule AdminAPI.V1.MembershipSerializerTest do describe "serialize/1" do test "serializes a membership into user json" do account = insert(:account) - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() role = insert(:role) membership = insert(:membership, %{account: account, user: user, role: role}) @@ -65,7 +65,7 @@ defmodule AdminAPI.V1.MembershipSerializerTest do test "serializes a list of memberships into a list of users json" do account = insert(:account) - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() role = insert(:role) membership = insert(:membership, %{account: account, user: user, role: role}) diff --git a/apps/admin_api/test/admin_api/v1/views/self_view_test.exs b/apps/admin_api/test/admin_api/v1/views/self_view_test.exs index 712f193c2..5d2605fd9 100644 --- a/apps/admin_api/test/admin_api/v1/views/self_view_test.exs +++ b/apps/admin_api/test/admin_api/v1/views/self_view_test.exs @@ -2,10 +2,11 @@ defmodule AdminAPI.V1.SelfViewTest do use AdminAPI.ViewCase, :v1 alias AdminAPI.V1.SelfView alias EWallet.Web.Date + alias EWalletDB.User describe "render/2" do test "renders user.json with correct response structure" do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() # I prefer to keep this test code duplicate with the `UserView.render/2` test, # because in practice they are separate responses. diff --git a/apps/admin_api/test/admin_api/v1/views/user_view_test.exs b/apps/admin_api/test/admin_api/v1/views/user_view_test.exs index 407b196b9..682dd2922 100644 --- a/apps/admin_api/test/admin_api/v1/views/user_view_test.exs +++ b/apps/admin_api/test/admin_api/v1/views/user_view_test.exs @@ -2,10 +2,11 @@ defmodule AdminAPI.V1.UserViewTest do use AdminAPI.ViewCase, :v1 alias AdminAPI.V1.UserView alias EWallet.Web.{Date, Paginator} + alias EWalletDB.User describe "AdminAPI.V1.UserView.render/2" do test "renders user.json with correct response structure" do - user = insert(:user) + {:ok, user} = :user |> params_for() |> User.insert() # I prefer to keep this test code duplicate with the `UserView.render/2` test, # because in practice they are separate responses. @@ -40,8 +41,8 @@ defmodule AdminAPI.V1.UserViewTest do end test "renders users.json with correct response structure" do - user1 = insert(:user) - user2 = insert(:user) + {:ok, user1} = :user |> params_for() |> User.insert() + {:ok, user2} = :user |> params_for() |> User.insert() paginator = %Paginator{ data: [user1, user2], diff --git a/apps/ewallet/lib/ewallet/web/inviter.ex b/apps/ewallet/lib/ewallet/web/inviter.ex index b0cfbcacf..879d19870 100644 --- a/apps/ewallet/lib/ewallet/web/inviter.ex +++ b/apps/ewallet/lib/ewallet/web/inviter.ex @@ -11,7 +11,7 @@ defmodule EWallet.Web.Inviter do @spec invite_user(String.t(), String.t(), String.t(), String.t(), fun()) :: {:ok, %Invite{}} | {:error, atom()} | {:error, atom(), String.t()} def invite_user(email, password, verification_url, success_url, create_email_func) do - with {:ok, user} <- get_or_create_user(email, password), + with {:ok, user} <- get_or_insert_user(email, password, :self), {:ok, invite} <- Invite.generate(user, preload: :user, success_url: success_url), {:ok, account} <- Account.fetch_master_account(), {:ok, _account_user} <- AccountUser.link(account.uuid, user.uuid) do @@ -29,10 +29,10 @@ defmodule EWallet.Web.Inviter do Creates the admin along with the membership if the admin does not exist, then sends the invite email out. """ - @spec invite_admin(String.t(), %Account{}, %Role{}, String.t(), fun()) :: + @spec invite_admin(String.t(), %Account{}, %Role{}, String.t(), map() | atom(), fun()) :: {:ok, %Invite{}} | {:error, atom()} - def invite_admin(email, account, role, redirect_url, create_email_func) do - with {:ok, user} <- get_or_create_user(email), + def invite_admin(email, account, role, redirect_url, originator, create_email_func) do + with {:ok, user} <- get_or_insert_user(email, nil, originator), {:ok, invite} <- Invite.generate(user, preload: :user), {:ok, _membership} <- Membership.assign(invite.user, account, role) do send_email(invite, redirect_url, create_email_func) @@ -42,7 +42,7 @@ defmodule EWallet.Web.Inviter do end end - defp get_or_create_user(email, password \\ nil) do + defp get_or_insert_user(email, password, originator) do case User.get_by_email(email) do %User{} = user -> case User.get_status(user) do @@ -56,7 +56,8 @@ defmodule EWallet.Web.Inviter do nil -> User.insert(%{ email: email, - password: password || Crypto.generate_base64_key(32) + password: password || Crypto.generate_base64_key(32), + originator: originator }) end end diff --git a/apps/ewallet/lib/ewallet/web/originator.ex b/apps/ewallet/lib/ewallet/web/originator.ex new file mode 100644 index 000000000..f37c0750c --- /dev/null +++ b/apps/ewallet/lib/ewallet/web/originator.ex @@ -0,0 +1,16 @@ +defmodule EWallet.Web.Originator do + @moduledoc """ + Module to extract the originator from the conn.assigns. + """ + alias EWalletDB.{Key, User} + + @spec extract(Map.t()) :: [%Key{}] + def extract(%{key: key}) do + key + end + + @spec extract(Map.t()) :: [%User{}] + def extract(%{admin_user: admin_user}) do + admin_user + end +end diff --git a/apps/ewallet/mix.exs b/apps/ewallet/mix.exs index bc60c5813..09809f4f7 100644 --- a/apps/ewallet/mix.exs +++ b/apps/ewallet/mix.exs @@ -45,6 +45,8 @@ defmodule EWallet.Mixfile do {:quantum, "~> 2.2.6"}, {:timex, "~> 3.0"}, {:bodyguard, "~> 2.2"}, + {:bamboo, "~> 0.8"}, + {:bamboo_smtp, "~> 1.4.0"}, {:decimal, "~> 1.0"}, {:deferred_config, "~> 0.1.0"}, {:ewallet_db, in_umbrella: true}, diff --git a/apps/ewallet/test/ewallet/fetchers/user_fetcher_test.exs b/apps/ewallet/test/ewallet/fetchers/user_fetcher_test.exs index ef662528f..10b532fdc 100644 --- a/apps/ewallet/test/ewallet/fetchers/user_fetcher_test.exs +++ b/apps/ewallet/test/ewallet/fetchers/user_fetcher_test.exs @@ -11,12 +11,12 @@ defmodule EWallet.UserFetcherTest do describe "get/1" do test "retrieves a user from its id", meta do {:ok, user} = UserFetcher.fetch(%{"user_id" => meta.user.id}) - assert user == meta.user + assert user.uuid == meta.user.uuid end test "retrieves a user from its provider_user_id", meta do {:ok, user} = UserFetcher.fetch(%{"provider_user_id" => meta.user.provider_user_id}) - assert user == meta.user + assert user.uuid == meta.user.uuid end test "Raise user_id_not_found if the user_id doesn't exist" do diff --git a/apps/ewallet/test/ewallet/gates/signup_gate_test.exs b/apps/ewallet/test/ewallet/gates/signup_gate_test.exs index 89792b810..b68f8386e 100644 --- a/apps/ewallet/test/ewallet/gates/signup_gate_test.exs +++ b/apps/ewallet/test/ewallet/gates/signup_gate_test.exs @@ -2,7 +2,7 @@ defmodule EWallet.SignupGateTest do use EWallet.DBCase, async: true alias EWallet.SignupGate alias EWalletAPI.VerificationEmail - alias EWalletDB.Invite + alias EWalletDB.{Invite, User} @verification_url "http://localhost:4000/verification_url?email={email}&token={token}" @success_url "http://localhost:4000/success_url" @@ -137,7 +137,7 @@ defmodule EWallet.SignupGateTest do describe "verify_email/1" do test "returns the invite when the verification is successful" do - user = insert(:standalone_user) + {:ok, user} = :standalone_user |> params_for() |> User.insert() {:ok, invite} = Invite.generate(user) {res, invite} = @@ -151,7 +151,7 @@ defmodule EWallet.SignupGateTest do end test "returns an error when the email format is invalid" do - user = insert(:standalone_user) + {:ok, user} = :standalone_user |> params_for() |> User.insert() {:ok, invite} = Invite.generate(user) {res, code} = @@ -165,7 +165,7 @@ defmodule EWallet.SignupGateTest do end test "returns :missing_token error when the token is not provided" do - user = insert(:standalone_user) + {:ok, user} = :standalone_user |> params_for() |> User.insert() {:ok, _invite} = Invite.generate(user) {res, code} = @@ -178,7 +178,7 @@ defmodule EWallet.SignupGateTest do end test "returns :email_token_not_found error when the email and token do not match" do - user = insert(:standalone_user) + {:ok, user} = :standalone_user |> params_for() |> User.insert() {:ok, _invite} = Invite.generate(user) {res, code} = diff --git a/apps/ewallet/test/ewallet/web/inviter_test.exs b/apps/ewallet/test/ewallet/web/inviter_test.exs index e4e7dcb35..6a6c8c719 100644 --- a/apps/ewallet/test/ewallet/web/inviter_test.exs +++ b/apps/ewallet/test/ewallet/web/inviter_test.exs @@ -42,7 +42,7 @@ defmodule EWallet.Web.InviterTest do test "resends the verification email if the user has not verified their email" do invite = insert(:invite) - user = insert(:standalone_user, invite: invite) + {:ok, user} = :standalone_user |> params_for(invite: invite) |> User.insert() {res, invite} = Inviter.invite_user( @@ -80,6 +80,7 @@ defmodule EWallet.Web.InviterTest do describe "invite_admin/5" do test "sends email and returns the invite if successful" do + user = insert(:admin, %{email: "activeuser@example.com"}) account = insert(:account) role = insert(:role) @@ -89,6 +90,7 @@ defmodule EWallet.Web.InviterTest do account, role, @admin_redirect_url, + user, &InviteEmail.create/2 ) @@ -98,6 +100,7 @@ defmodule EWallet.Web.InviterTest do end test "sends a new invite if this email has been invited before" do + user = insert(:admin, %{email: "activeuser@example.com"}) account = insert(:account) role = insert(:role) @@ -107,6 +110,7 @@ defmodule EWallet.Web.InviterTest do account, role, @admin_redirect_url, + user, &InviteEmail.create/2 ) @@ -116,6 +120,7 @@ defmodule EWallet.Web.InviterTest do account, role, @admin_redirect_url, + user, &InviteEmail.create/2 ) @@ -124,6 +129,7 @@ defmodule EWallet.Web.InviterTest do end test "assigns the user to account and role" do + user = insert(:admin, %{email: "activeuser@example.com"}) account = insert(:account) role = insert(:role) @@ -133,6 +139,7 @@ defmodule EWallet.Web.InviterTest do account, role, @admin_redirect_url, + user, &InviteEmail.create/2 ) @@ -145,7 +152,7 @@ defmodule EWallet.Web.InviterTest do test "returns :user_already_active error if user is already active" do # This should already be an active user - _user = insert(:admin, %{email: "activeuser@example.com"}) + user = insert(:admin, %{email: "activeuser@example.com"}) account = insert(:account) role = insert(:role) @@ -155,6 +162,7 @@ defmodule EWallet.Web.InviterTest do account, role, @admin_redirect_url, + user, &InviteEmail.create/2 ) @@ -165,7 +173,8 @@ defmodule EWallet.Web.InviterTest do describe "send_email/3" do test "creates and sends the invite email" do - {:ok, invite} = Invite.generate(insert(:admin)) + {:ok, user} = :admin |> params_for() |> User.insert() + {:ok, invite} = Invite.generate(user) {res, _} = Inviter.send_email(invite, @admin_redirect_url, &InviteEmail.create/2) diff --git a/apps/ewallet_api/mix.exs b/apps/ewallet_api/mix.exs index 3c87c1ba3..983c246fd 100644 --- a/apps/ewallet_api/mix.exs +++ b/apps/ewallet_api/mix.exs @@ -52,6 +52,7 @@ defmodule EWalletAPI.Mixfile do [ {:phoenix, "~> 1.3.0"}, {:cowboy, "~> 1.0"}, + {:cors_plug, "~> 1.5"}, {:sentry, "~> 6.2.0"}, {:peerage, "~> 1.0.2"}, {:deferred_config, "~> 0.1.0"}, diff --git a/apps/ewallet_api/test/ewallet_api/emails/verification_email_test.exs b/apps/ewallet_api/test/ewallet_api/emails/verification_email_test.exs index 2eaeb6b68..a87db41d5 100644 --- a/apps/ewallet_api/test/ewallet_api/emails/verification_email_test.exs +++ b/apps/ewallet_api/test/ewallet_api/emails/verification_email_test.exs @@ -1,10 +1,10 @@ defmodule EWalletAPI.VerificationEmailTest do use EWalletAPI.ConnCase alias EWalletAPI.VerificationEmail - alias EWalletDB.Invite + alias EWalletDB.{Invite, User} defp create_email(email) do - user = insert(:standalone_user, email: email) + {:ok, user} = :standalone_user |> params_for(email: email) |> User.insert() {:ok, invite} = Invite.generate(user) {VerificationEmail.create(invite, "https://invite_url/?email={email}&token={token}"), diff --git a/apps/ewallet_api/test/ewallet_api/v1/controllers/auth_controller_test.exs b/apps/ewallet_api/test/ewallet_api/v1/controllers/auth_controller_test.exs index f28927d07..cb6c78b65 100644 --- a/apps/ewallet_api/test/ewallet_api/v1/controllers/auth_controller_test.exs +++ b/apps/ewallet_api/test/ewallet_api/v1/controllers/auth_controller_test.exs @@ -32,7 +32,12 @@ defmodule EWalletAPI.V1.AuthControllerTest do end test "returns user:email_not_verified error when the user has a pending invite", context do - _user = User.update_without_password(context.user, %{invite_uuid: insert(:invite).uuid}) + _user = + User.update_without_password(context.user, %{ + invite_uuid: insert(:invite).uuid, + originator: :self + }) + response = client_request("/user.login", context.request_data) assert response["version"] == @expected_version diff --git a/apps/ewallet_api/test/ewallet_api/v1/controllers/pages/verify_email_controller_test.exs b/apps/ewallet_api/test/ewallet_api/v1/controllers/pages/verify_email_controller_test.exs index 38244e6ca..2ebec8f3c 100644 --- a/apps/ewallet_api/test/ewallet_api/v1/controllers/pages/verify_email_controller_test.exs +++ b/apps/ewallet_api/test/ewallet_api/v1/controllers/pages/verify_email_controller_test.exs @@ -1,6 +1,6 @@ defmodule EWalletAPI.V1.VerifyEmailControllerTest do use EWalletAPI.ConnCase, async: true - alias EWalletDB.Invite + alias EWalletDB.{Invite, User} describe "verify/2" do defp verify_email(email, token) do @@ -9,7 +9,7 @@ defmodule EWalletAPI.V1.VerifyEmailControllerTest do end test "redirects to the default success_url when invite.success_url is not given" do - user = insert(:standalone_user) + {:ok, user} = :standalone_user |> params_for() |> User.insert() {:ok, invite} = Invite.generate(user) conn = verify_email(user.email, invite.token) @@ -18,7 +18,7 @@ defmodule EWalletAPI.V1.VerifyEmailControllerTest do end test "redirects to the invite.success_url on success" do - user = insert(:standalone_user) + {:ok, user} = :standalone_user |> params_for() |> User.insert() {:ok, invite} = Invite.generate(user, success_url: "https://example.com/success_url") conn = verify_email(user.email, invite.token) @@ -27,7 +27,7 @@ defmodule EWalletAPI.V1.VerifyEmailControllerTest do end test "returns an error when the email is invalid" do - user = insert(:standalone_user) + {:ok, user} = :standalone_user |> params_for() |> User.insert() {:ok, invite} = Invite.generate(user) conn = verify_email("wrong@example.com", invite.token) @@ -38,7 +38,7 @@ defmodule EWalletAPI.V1.VerifyEmailControllerTest do end test "returns an error when the token is invalid" do - user = insert(:standalone_user) + {:ok, user} = :standalone_user |> params_for() |> User.insert() {:ok, _invite} = Invite.generate(user) conn = verify_email(user.email, "wrong_token") diff --git a/apps/ewallet_api/test/ewallet_api/v1/controllers/signup_controller_test.exs b/apps/ewallet_api/test/ewallet_api/v1/controllers/signup_controller_test.exs index 97547d4a8..d857629f4 100644 --- a/apps/ewallet_api/test/ewallet_api/v1/controllers/signup_controller_test.exs +++ b/apps/ewallet_api/test/ewallet_api/v1/controllers/signup_controller_test.exs @@ -101,7 +101,9 @@ defmodule EWalletAPI.V1.SignupControllerTest do describe "verify_email/2" do setup do - user = insert(:user, email: "verify_email@example.com") + {:ok, user} = + :standalone_user |> params_for(email: "verify_email@example.com") |> User.insert() + {:ok, invite} = Invite.generate(user, preload: :user) %{ diff --git a/apps/ewallet_db/config/config.exs b/apps/ewallet_db/config/config.exs index 22dc95606..12942ace9 100644 --- a/apps/ewallet_db/config/config.exs +++ b/apps/ewallet_db/config/config.exs @@ -1,9 +1,19 @@ use Mix.Config +audits = %{ + EWalletDB.System => "system", + EWalletDB.User => "user", + EWalletDB.Invite => "invite", + EWalletDB.Key => "key", + EWalletDB.ForgetPasswordRequest => "forget_password_request" +} + config :ewallet_db, ecto_repos: [EWalletDB.Repo], env: Mix.env(), base_url: {:system, "BASE_URL", "http://localhost:4000"}, - min_password_length: 8 + min_password_length: 8, + schemas_to_audit_types: audits, + audit_types_to_schemas: Enum.into(audits, %{}, fn {key, value} -> {value, key} end) import_config "#{Mix.env()}.exs" diff --git a/apps/ewallet_db/lib/ewallet_db/audit.ex b/apps/ewallet_db/lib/ewallet_db/audit.ex new file mode 100644 index 000000000..a6c29b700 --- /dev/null +++ b/apps/ewallet_db/lib/ewallet_db/audit.ex @@ -0,0 +1,183 @@ +defmodule EWalletDB.Audit do + @moduledoc """ + Ecto Schema representing audits. + """ + use Arc.Ecto.Schema + use Ecto.Schema + use EWalletDB.Types.ExternalID + import Ecto.{Changeset, Query} + alias Ecto.{Changeset, Multi, UUID} + + alias EWalletDB.{ + Audit, + Repo + } + + @primary_key {:uuid, UUID, autogenerate: true} + + schema "audit" do + external_id(prefix: "adt_") + + field(:action, :string) + + field(:target_type, :string) + field(:target_uuid, UUID) + field(:target_changes, :map) + field(:target_encrypted_metadata, EWalletDB.Encrypted.Map, default: %{}) + + field(:originator_uuid, UUID) + field(:originator_type, :string) + + field(:metadata, :map, default: %{}) + + field(:inserted_at, :naive_datetime) + end + + defp changeset(changeset, attrs) do + changeset + |> cast(attrs, [ + :action, + :target_type, + :target_uuid, + :target_changes, + :target_encrypted_metadata, + :originator_uuid, + :originator_type, + :metadata, + :inserted_at + ]) + |> validate_required([ + :action, + :target_type, + :target_uuid, + :target_changes, + :originator_uuid, + :originator_type, + :inserted_at + ]) + end + + @spec get_schema(String.t()) :: Atom.t() + def get_schema(type) do + Application.get_env(:ewallet_db, :audit_types_to_schemas)[type] + end + + @spec get_type(Atom.t()) :: String.t() + def get_type(schema) do + Application.get_env(:ewallet_db, :schemas_to_audit_types)[schema] + end + + @spec all_for_target(Map.t()) :: [%Audit{}] + def all_for_target(record) do + all_for_target(record.__struct__, record.uuid) + end + + @spec all_for_target(String.t(), UUID.t()) :: [%Audit{}] + def all_for_target(type, uuid) when is_binary(type) do + Audit + |> where([a], a.target_type == ^type and a.target_uuid == ^uuid) + |> Repo.all() + end + + @spec all_for_target(Atom.t(), UUID.t()) :: [%Audit{}] + def all_for_target(schema, uuid) do + schema + |> get_type() + |> all_for_target(uuid) + end + + @spec get_initial_audit(String.t(), UUID.t()) :: %Audit{} + def get_initial_audit(type, uuid) do + Repo.get_by( + Audit, + action: "insert", + target_type: type, + target_uuid: uuid + ) + end + + @spec get_initial_originator(Map.t()) :: Map.t() + def get_initial_originator(record) do + audit_type = get_type(record.__struct__) + audit = Audit.get_initial_audit(audit_type, record.uuid) + originator_schema = Audit.get_schema(audit.originator_type) + + case originator_schema do + EWalletDB.System -> + %EWalletDB.System{uuid: audit.originator_uuid} + + schema -> + Repo.get(schema, audit.originator_uuid) + end + end + + @spec insert_record_with_audit(%Changeset{}, Multi.t()) :: + {:ok, any()} + | {:error, any()} + | {:error, :no_originator_given} + | {:error, Multi.name(), any(), %{optional(Multi.name()) => any()}} + def insert_record_with_audit(changeset, multi \\ Multi.new()) do + perform(:insert, changeset, multi) + end + + @spec update_record_with_audit(%Changeset{}, Multi.t()) :: + {:ok, any()} + | {:error, any()} + | {:error, :no_originator_given} + | {:error, Multi.name(), any(), %{optional(Multi.name()) => any()}} + def update_record_with_audit(changeset, multi \\ Multi.new()) do + perform(:update, changeset, multi) + end + + defp perform(action, changeset, multi) do + Multi + |> apply(action, [Multi.new(), :record, changeset]) + |> Multi.run(:audit, fn %{record: record} -> + action + |> build_attrs(changeset, record) + |> insert_audit() + end) + |> Multi.append(multi) + |> Repo.transaction() + end + + defp insert_audit(attrs) do + %Audit{} + |> changeset(attrs) + |> Repo.insert() + end + + defp build_attrs(action, changeset, record) do + with {:ok, originator} <- get_originator(changeset, record), + originator_type <- get_type(originator.__struct__), + target_type <- get_type(record.__struct__), + changes <- Map.delete(changeset.changes, :originator), + encrypted_metadata <- changes[:encrypted_metadata], + changes <- Map.delete(changes, :encrypted_metadata) do + %{ + action: Atom.to_string(action), + target_type: target_type, + target_uuid: record.uuid, + target_changes: changes, + target_encrypted_metadata: encrypted_metadata || %{}, + originator_uuid: originator.uuid, + originator_type: originator_type, + inserted_at: NaiveDateTime.utc_now() + } + else + error -> error + end + end + + defp get_originator(%Changeset{changes: %{originator: :self}}, record) do + {:ok, record} + end + + defp get_originator(%Changeset{changes: %{originator: originator}}, _) do + {:ok, originator} + end + + defp get_originator(_, _) do + {:error, :no_originator_given} + end +end diff --git a/apps/ewallet_db/lib/ewallet_db/forget_password_request.ex b/apps/ewallet_db/lib/ewallet_db/forget_password_request.ex index 52fae2d57..3b1de4c8f 100644 --- a/apps/ewallet_db/lib/ewallet_db/forget_password_request.ex +++ b/apps/ewallet_db/lib/ewallet_db/forget_password_request.ex @@ -12,6 +12,7 @@ defmodule EWalletDB.ForgetPasswordRequest do schema "forget_password_request" do field(:token, :string) + field(:enabled, :boolean) belongs_to( :user, @@ -31,6 +32,16 @@ defmodule EWalletDB.ForgetPasswordRequest do |> assoc_constraint(:user) end + @doc """ + Retrieves all active requests. + """ + def all_active do + ForgetPasswordRequest + |> where([c], c.enabled == true) + |> order_by([c], desc: c.inserted_at) + |> Repo.all() + end + @doc """ Retrieves a specific invite by its token. """ @@ -38,6 +49,7 @@ defmodule EWalletDB.ForgetPasswordRequest do request = ForgetPasswordRequest |> where([c], c.user_uuid == ^user.uuid) + |> where([c], c.enabled == true) |> order_by([c], desc: c.inserted_at) |> limit(1) |> Repo.one() @@ -55,10 +67,10 @@ defmodule EWalletDB.ForgetPasswordRequest do @doc """ Deletes all the current requests for a user. """ - def delete_all(user) do + def disable_all_for(user) do ForgetPasswordRequest |> where([f], f.user_uuid == ^user.uuid) - |> Repo.delete_all() + |> Repo.update_all(set: [enabled: false]) end @doc """ diff --git a/apps/ewallet_db/lib/ewallet_db/invite.ex b/apps/ewallet_db/lib/ewallet_db/invite.ex index adc1c1236..1aaf816de 100644 --- a/apps/ewallet_db/lib/ewallet_db/invite.ex +++ b/apps/ewallet_db/lib/ewallet_db/invite.ex @@ -4,8 +4,8 @@ defmodule EWalletDB.Invite do """ use Ecto.Schema import Ecto.{Changeset, Query} - alias Ecto.UUID - alias EWalletDB.{Helpers.Crypto, Invite, Repo, User} + alias Ecto.{Multi, UUID} + alias EWalletDB.{Audit, Helpers.Crypto, Invite, Repo, Types.VirtualStruct, User} @primary_key {:uuid, UUID, autogenerate: true} @token_length 32 @@ -15,6 +15,7 @@ defmodule EWalletDB.Invite do field(:token, :string) field(:success_url, :string) field(:verified_at, :naive_datetime) + field(:originator, VirtualStruct, virtual: true) belongs_to( :user, @@ -29,14 +30,14 @@ defmodule EWalletDB.Invite do defp changeset_insert(changeset, attrs) do changeset - |> cast(attrs, [:user_uuid, :token, :success_url]) - |> validate_required([:user_uuid, :token]) + |> cast(attrs, [:user_uuid, :token, :success_url, :originator]) + |> validate_required([:user_uuid, :token, :originator]) end defp changeset_accept(changeset, attrs) do changeset - |> cast(attrs, [:verified_at]) - |> validate_required([:verified_at]) + |> cast(attrs, [:verified_at, :originator]) + |> validate_required([:verified_at, :originator]) end @doc """ @@ -101,29 +102,32 @@ defmodule EWalletDB.Invite do Generates an invite for the given user. """ def generate(user, opts \\ []) do - # Insert a new invite - {:ok, invite} = - insert(%{ - user_uuid: user.uuid, - token: Crypto.generate_base64_key(@token_length), - success_url: opts[:success_url] - }) - - # Assign the invite to the user - changeset = change(user, %{invite_uuid: invite.uuid}) - {:ok, _user} = Repo.update(changeset) - - if opts[:preload] do - {:ok, Repo.preload(invite, opts[:preload])} - else - {:ok, invite} - end - end + originator = Audit.get_initial_originator(user) - defp insert(attrs) do + # Insert a new invite %Invite{} - |> changeset_insert(attrs) - |> Repo.insert() + |> changeset_insert(%{ + user_uuid: user.uuid, + token: Crypto.generate_base64_key(@token_length), + success_url: opts[:success_url], + originator: originator + }) + |> Audit.insert_record_with_audit( + # Assign the invite to the user + Multi.run(Multi.new(), :user, fn %{record: record} -> + {:ok, _user} = + user + |> change(%{invite_uuid: record.uuid}) + |> Repo.update() + end) + ) + |> case do + {:ok, result} -> + {:ok, Repo.preload(result.record, opts[:preload] || [])} + + {:error, _failed_operation, changeset, _changes_so_far} -> + {:error, changeset} + end end @doc """ @@ -131,13 +135,16 @@ defmodule EWalletDB.Invite do """ @spec accept(%Invite{}) :: {:ok, struct()} | {:error, any()} def accept(invite) do - invite = Repo.preload(invite, :user) - - case User.update_without_password(invite.user, %{invite_uuid: nil}) do - {:ok, _user} -> - invite - |> changeset_accept(%{verified_at: NaiveDateTime.utc_now()}) - |> Repo.update() + with invite <- Repo.preload(invite, :user), + attrs <- %{invite_uuid: nil, originator: :self}, + {:ok, _user} <- User.update_without_password(invite.user, attrs), + invite_attrs <- %{verified_at: NaiveDateTime.utc_now(), originator: invite.user}, + changeset <- changeset_accept(invite, invite_attrs), + {:ok, result} <- Audit.update_record_with_audit(changeset) do + {:ok, result.record} + else + {:error, _failed_operation, changeset, _changes_so_far} -> + {:error, changeset} error -> error @@ -149,13 +156,16 @@ defmodule EWalletDB.Invite do """ @spec accept(%Invite{}, String.t()) :: {:ok, struct()} | {:error, any()} def accept(invite, password) do - invite = Repo.preload(invite, :user) - - case User.update(invite.user, %{invite_uuid: nil, password: password}) do - {:ok, _user} -> - invite - |> changeset_accept(%{verified_at: NaiveDateTime.utc_now()}) - |> Repo.update() + with invite <- Repo.preload(invite, :user), + attrs <- %{invite_uuid: nil, password: password, originator: :self}, + {:ok, _user} <- User.update(invite.user, attrs), + invite_attrs <- %{verified_at: NaiveDateTime.utc_now(), originator: invite.user}, + changeset <- changeset_accept(invite, invite_attrs), + {:ok, result} <- Audit.update_record_with_audit(changeset) do + {:ok, result.record} + else + {:error, _failed_operation, changeset, _changes_so_far} -> + {:error, changeset} error -> error diff --git a/apps/ewallet_db/lib/ewallet_db/structs/system.ex b/apps/ewallet_db/lib/ewallet_db/structs/system.ex new file mode 100644 index 000000000..8e652649c --- /dev/null +++ b/apps/ewallet_db/lib/ewallet_db/structs/system.ex @@ -0,0 +1,6 @@ +defmodule EWalletDB.System do + @moduledoc """ + Module representing the system as originator. + """ + defstruct uuid: "00000000-0000-0000-0000-000000000000" +end diff --git a/apps/ewallet_db/lib/ewallet_db/types/virtual_struct.ex b/apps/ewallet_db/lib/ewallet_db/types/virtual_struct.ex new file mode 100644 index 000000000..537a485c1 --- /dev/null +++ b/apps/ewallet_db/lib/ewallet_db/types/virtual_struct.ex @@ -0,0 +1,23 @@ +defmodule EWalletDB.Types.VirtualStruct do + @moduledoc """ + Useless type used for the virtual struct "originator". + """ + @behaviour Ecto.Type + def type, do: :virtual_struct + + def cast(value) do + {:ok, value} + end + + def load(value) do + {:ok, value} + end + + def load!(nil), do: 0 + + def load!(value), do: value + + def dump(value) do + {:ok, value} + end +end diff --git a/apps/ewallet_db/lib/ewallet_db/user.ex b/apps/ewallet_db/lib/ewallet_db/user.ex index e1ec11072..bbf851b0c 100644 --- a/apps/ewallet_db/lib/ewallet_db/user.ex +++ b/apps/ewallet_db/lib/ewallet_db/user.ex @@ -13,12 +13,14 @@ defmodule EWalletDB.User do alias EWalletDB.{ Account, AccountUser, + Audit, AuthToken, Helpers.Crypto, Invite, Membership, Repo, Role, + Types.VirtualStruct, User, Wallet } @@ -35,6 +37,7 @@ defmodule EWalletDB.User do field(:password_confirmation, :string, virtual: true) field(:password_hash, :string) field(:provider_user_id, :string) + field(:originator, VirtualStruct, virtual: true) field(:metadata, :map, default: %{}) field(:encrypted_metadata, EWalletDB.Encrypted.Map, default: %{}) field(:avatar, EWalletDB.Uploaders.Avatar.Type) @@ -96,9 +99,10 @@ defmodule EWalletDB.User do :password_confirmation, :metadata, :encrypted_metadata, - :invite_uuid + :invite_uuid, + :originator ]) - |> validate_required([:metadata, :encrypted_metadata]) + |> validate_required([:metadata, :encrypted_metadata, :originator]) |> validate_confirmation(:password, message: "does not match password") |> validate_immutable(:provider_user_id) |> unique_constraint(:username) @@ -110,7 +114,10 @@ defmodule EWalletDB.User do end defp avatar_changeset(changeset, attrs) do - cast_attachments(changeset, attrs, [:avatar]) + changeset + |> cast(attrs, [:originator]) + |> cast_attachments(attrs, [:avatar]) + |> validate_required([:originator]) end defp update_changeset(%User{} = user, attrs) do @@ -119,9 +126,10 @@ defmodule EWalletDB.User do :email, :metadata, :encrypted_metadata, - :invite_uuid + :invite_uuid, + :originator ]) - |> validate_required(:email) + |> validate_required([:email, :originator]) |> unique_constraint(:email) |> assoc_constraint(:invite) end @@ -228,19 +236,19 @@ defmodule EWalletDB.User do """ @spec insert(map()) :: {:ok, %User{}} | {:error, Ecto.Changeset.t()} def insert(attrs) do - multi = - Multi.new() - |> Multi.insert(:user, changeset(%User{}, attrs)) - |> Multi.run(:wallet, fn %{user: user} -> - case User.admin?(user) do + %User{} + |> changeset(attrs) + |> Audit.insert_record_with_audit( + Multi.run(Multi.new(), :wallet, fn %{record: record} -> + case User.admin?(record) do true -> {:ok, nil} - false -> insert_wallet(user, Wallet.primary()) + false -> insert_wallet(record, Wallet.primary()) end end) - - case Repo.transaction(multi) do + ) + |> case do {:ok, result} -> - user = Repo.preload(result.user, [:wallets]) + user = Repo.preload(result.record, [:wallets]) {:ok, user} # Only the account insertion should fail. If the wallet insert fails, there is @@ -268,14 +276,15 @@ defmodule EWalletDB.User do """ @spec update(%User{}, map()) :: {:ok, %User{}} | {:error, Ecto.Changeset.t()} def update(%User{} = user, attrs) do - changeset = changeset(user, attrs) - - case Repo.update(changeset) do - {:ok, user} -> - {:ok, get(user.id)} + user + |> changeset(attrs) + |> Audit.update_record_with_audit() + |> case do + {:ok, result} -> + {:ok, get(result.record.id)} - result -> - result + {:error, _failed_operation, changeset, _changes_so_far} -> + {:error, changeset} end end @@ -286,12 +295,12 @@ defmodule EWalletDB.User do def update_without_password(%User{} = user, attrs) do changeset = update_changeset(user, attrs) - case Repo.update(changeset) do - {:ok, user} -> - {:ok, get(user.id)} + case Audit.update_record_with_audit(changeset) do + {:ok, result} -> + {:ok, get(result.record.id)} - result -> - result + {:error, _failed_operation, changeset, _changes_so_far} -> + {:error, changeset} end end @@ -300,18 +309,22 @@ defmodule EWalletDB.User do """ @spec store_avatar(%User{}, map()) :: %User{} | Ecto.Changeset.t() def store_avatar(%User{} = user, attrs) do - attrs = + updated_attrs = case attrs["avatar"] do "" -> %{avatar: nil} "null" -> %{avatar: nil} avatar -> %{avatar: avatar} end - changeset = avatar_changeset(user, attrs) + updated_attrs = Map.put(updated_attrs, :originator, attrs["originator"]) + changeset = avatar_changeset(user, updated_attrs) - case Repo.update(changeset) do - {:ok, user} -> get(user.id) - result -> result + case Audit.update_record_with_audit(changeset) do + {:ok, result} -> + {:ok, get(result.record.id)} + + {:error, _failed_operation, changeset, _changes_so_far} -> + {:error, changeset} end end diff --git a/apps/ewallet_db/priv/repo/migrations/20180907173648_add_audit.exs b/apps/ewallet_db/priv/repo/migrations/20180907173648_add_audit.exs new file mode 100644 index 000000000..121683632 --- /dev/null +++ b/apps/ewallet_db/priv/repo/migrations/20180907173648_add_audit.exs @@ -0,0 +1,27 @@ +defmodule EWalletDB.Repo.Migrations.AddAudit do + use Ecto.Migration + + def change do + create table(:audit, primary_key: false) do + add :uuid, :uuid, primary_key: true + add :id, :string, null: false + + add :action, :string, null: false + + add :target_uuid, :uuid, null: false + add :target_type, :string, null: false + add :target_changes, :map, null: false + add :target_encrypted_metadata, :binary + + add :originator_uuid, :uuid + add :originator_type, :string + + add :metadata, :map + + add :inserted_at, :naive_datetime + end + + create index(:audit, [:target_uuid, :target_type]) + create index(:audit, [:originator_uuid, :originator_type]) + end +end diff --git a/apps/ewallet_db/priv/repo/migrations/20180924092702_add_enabled_to_forget_password_request.exs b/apps/ewallet_db/priv/repo/migrations/20180924092702_add_enabled_to_forget_password_request.exs new file mode 100644 index 000000000..ae74b7abe --- /dev/null +++ b/apps/ewallet_db/priv/repo/migrations/20180924092702_add_enabled_to_forget_password_request.exs @@ -0,0 +1,9 @@ +defmodule EWalletDB.Repo.Migrations.AddEnabledToForgetPasswordRequest do + use Ecto.Migration + + def change do + alter table(:forget_password_request) do + add :enabled, :boolean, null: false, default: true + end + end +end diff --git a/apps/ewallet_db/test/ewallet_db/account_test.exs b/apps/ewallet_db/test/ewallet_db/account_test.exs index 1dc4d3a83..8c3260f1b 100644 --- a/apps/ewallet_db/test/ewallet_db/account_test.exs +++ b/apps/ewallet_db/test/ewallet_db/account_test.exs @@ -84,8 +84,8 @@ defmodule EWalletDB.AccountTest do end describe "update/2" do - test_update_field_ok(Account, :name) - test_update_field_ok(Account, :description) + test_update_field_ok(Account, :name, insert(:admin)) + test_update_field_ok(Account, :description, insert(:admin)) end describe "update/2 with category_ids" do diff --git a/apps/ewallet_db/test/ewallet_db/api_key_test.exs b/apps/ewallet_db/test/ewallet_db/api_key_test.exs index 68519d2d8..ba2887ad1 100644 --- a/apps/ewallet_db/test/ewallet_db/api_key_test.exs +++ b/apps/ewallet_db/test/ewallet_db/api_key_test.exs @@ -52,11 +52,12 @@ defmodule EWalletDB.APIKeyTest do test_update_ignores_changing(APIKey, :key) test_update_ignores_changing(APIKey, :owner_app) - test_update_field_ok(APIKey, :expired, false, true) + test_update_field_ok(APIKey, :expired, insert(:admin), false, true) test_update_field_ok( APIKey, :exchange_address, + insert(:admin), insert(:wallet).address, insert(:wallet).address ) diff --git a/apps/ewallet_db/test/ewallet_db/audit_test.exs b/apps/ewallet_db/test/ewallet_db/audit_test.exs new file mode 100644 index 000000000..951007450 --- /dev/null +++ b/apps/ewallet_db/test/ewallet_db/audit_test.exs @@ -0,0 +1,250 @@ +defmodule EWalletDB.AuditTest do + use EWalletDB.SchemaCase + alias Ecto.{Changeset, Multi} + alias EWalletDB.{Audit, Repo, System, User} + + describe "Audit.get_schema/1" do + test "gets the schema from a type" do + assert Audit.get_schema("user") == EWalletDB.User + end + end + + describe "Audit.get_type/1" do + test "gets the type from a schema" do + assert Audit.get_type(EWalletDB.User) == "user" + end + end + + describe "Audit.all_for_target/1" do + test "returns all audits for a target" do + {:ok, _user} = :user |> params_for() |> User.insert() + {:ok, _user} = :user |> params_for() |> User.insert() + {:ok, user} = :user |> params_for() |> User.insert() + + {:ok, user} = + User.update_without_password(user, %{ + email: "test@mail.com", + originator: %System{} + }) + + audits = Audit.all_for_target(user) + + assert length(audits) == 2 + + results = Enum.map(audits, fn a -> {a.action, a.originator_type, a.target_type} end) + assert Enum.member?(results, {"insert", "user", "user"}) + assert Enum.member?(results, {"update", "system", "user"}) + end + end + + describe "Audit.all_for_target/2" do + test "returns all audits for a target when given a string" do + {:ok, _user} = :user |> params_for() |> User.insert() + {:ok, _user} = :user |> params_for() |> User.insert() + {:ok, user} = :user |> params_for() |> User.insert() + + {:ok, user} = + User.update_without_password(user, %{ + email: "test@mail.com", + originator: %System{} + }) + + audits = Audit.all_for_target("user", user.uuid) + + assert length(audits) == 2 + + results = Enum.map(audits, fn a -> {a.action, a.originator_type, a.target_type} end) + assert Enum.member?(results, {"insert", "user", "user"}) + assert Enum.member?(results, {"update", "system", "user"}) + end + + test "returns all audits for a target when given a module name" do + {:ok, _user} = :user |> params_for() |> User.insert() + {:ok, _user} = :user |> params_for() |> User.insert() + {:ok, user} = :user |> params_for() |> User.insert() + + {:ok, user} = + User.update_without_password(user, %{ + email: "test@mail.com", + originator: %System{} + }) + + audits = Audit.all_for_target(User, user.uuid) + + assert length(audits) == 2 + + results = Enum.map(audits, fn a -> {a.action, a.originator_type, a.target_type} end) + assert Enum.member?(results, {"insert", "user", "user"}) + assert Enum.member?(results, {"update", "system", "user"}) + end + end + + describe "Audit.get_initial_audit/2" do + test "gets the initial audit for a record" do + initial_originator = insert(:admin) + {:ok, user} = :user |> params_for(%{originator: initial_originator}) |> User.insert() + + {:ok, user} = + User.update_without_password(user, %{ + email: "test@mail.com", + originator: %System{} + }) + + audit = Audit.get_initial_audit("user", user.uuid) + + assert audit.originator_type == "user" + assert audit.originator_uuid == initial_originator.uuid + assert audit.target_type == "user" + assert audit.target_uuid == user.uuid + assert audit.action == "insert" + assert audit.inserted_at != nil + end + end + + describe "Audit.get_initial_originator/2" do + test "gets the initial originator for a record" do + initial_originator = insert(:admin) + {:ok, user} = :user |> params_for(%{originator: initial_originator}) |> User.insert() + + {:ok, user} = + User.update_without_password(user, %{ + email: "test@mail.com", + originator: %System{} + }) + + originator = Audit.get_initial_originator(user) + + assert originator.__struct__ == User + assert originator.uuid == initial_originator.uuid + end + end + + describe "Audit.insert_record_with_audit/2" do + test "inserts an audit and a user with encrypted metadata" do + admin = insert(:admin) + + params = + params_for(:user, %{ + encrypted_metadata: %{something: "cool"}, + originator: admin + }) + + changeset = Changeset.change(%User{}, params) + {res, %{audit: audit, record: record}} = Audit.insert_record_with_audit(changeset) + + assert res == :ok + + assert audit.action == "insert" + assert audit.originator_type == "user" + assert audit.originator_uuid == admin.uuid + assert audit.target_type == "user" + assert audit.target_uuid == record.uuid + + changes = + changeset.changes + |> Map.delete(:originator) + |> Map.delete(:encrypted_metadata) + + assert audit.target_changes == changes + assert audit.target_encrypted_metadata == %{something: "cool"} + + assert record |> Audit.all_for_target() |> length() == 1 + end + + test "inserts an audit and a user as well as a wallet" do + admin = insert(:admin) + + params = + params_for(:user, %{ + encrypted_metadata: %{something: "cool"}, + originator: admin + }) + + changeset = Changeset.change(%User{}, params) + + multi = + Multi.new() + |> Multi.run(:wow_user, fn %{record: _record} -> + {:ok, insert(:user, email: "wow@mail.com")} + end) + + {res, %{audit: audit, record: record, wow_user: wow_user}} = + Audit.insert_record_with_audit(changeset, multi) + + assert res == :ok + + assert audit.action == "insert" + assert audit.originator_type == "user" + assert audit.originator_uuid == admin.uuid + assert audit.target_type == "user" + assert audit.target_uuid == record.uuid + + assert wow_user != nil + assert wow_user.email == "wow@mail.com" + + assert record |> Audit.all_for_target() |> length() == 1 + end + end + + describe "Audit.update_record_with_audit/2" do + test "inserts an audit when updating a user" do + admin = insert(:admin) + {:ok, user} = :user |> params_for() |> User.insert() + + params = + params_for(:user, %{ + email: "cool@mail.com", + originator: admin + }) + + changeset = Changeset.change(user, params) + {res, %{audit: audit, record: record}} = Audit.update_record_with_audit(changeset) + + assert res == :ok + + assert audit.action == "update" + assert audit.originator_type == "user" + assert audit.originator_uuid == admin.uuid + assert audit.target_type == "user" + assert audit.target_uuid == record.uuid + changes = Map.delete(changeset.changes, :originator) + assert audit.target_changes == changes + + assert user |> Audit.all_for_target() |> length() == 2 + end + + test "inserts an audit and updates a user as well as saving a wallet" do + admin = insert(:admin) + {:ok, user} = :user |> params_for() |> User.insert() + + params = + params_for(:user, %{ + email: "cool@mail.com", + originator: admin + }) + + changeset = Changeset.change(user, params) + + multi = + Multi.new() + |> Multi.run(:wow_user, fn %{record: _record} -> + {:ok, insert(:user, email: "wow@mail.com")} + end) + + {res, %{audit: audit, record: record, wow_user: _}} = + Audit.update_record_with_audit(changeset, multi) + + assert res == :ok + + assert audit.action == "update" + assert audit.originator_type == "user" + assert audit.originator_uuid == admin.uuid + assert audit.target_type == "user" + assert audit.target_uuid == record.uuid + changes = Map.delete(changeset.changes, :originator) + assert audit.target_changes == changes + + assert user |> Audit.all_for_target() |> length() == 2 + end + end +end diff --git a/apps/ewallet_db/test/ewallet_db/category_test.exs b/apps/ewallet_db/test/ewallet_db/category_test.exs index 35c4ccdef..84f872fff 100644 --- a/apps/ewallet_db/test/ewallet_db/category_test.exs +++ b/apps/ewallet_db/test/ewallet_db/category_test.exs @@ -40,8 +40,8 @@ defmodule EWalletDB.CategoryTest do end describe "update/1" do - test_update_field_ok(Category, :name) - test_update_field_ok(Category, :description) + test_update_field_ok(Category, :name, insert(:admin)) + test_update_field_ok(Category, :description, insert(:admin)) end describe "update/2 with account_ids" do diff --git a/apps/ewallet_db/test/ewallet_db/exchange_pair_test.exs b/apps/ewallet_db/test/ewallet_db/exchange_pair_test.exs index 7ad49e8f7..460236d9c 100644 --- a/apps/ewallet_db/test/ewallet_db/exchange_pair_test.exs +++ b/apps/ewallet_db/test/ewallet_db/exchange_pair_test.exs @@ -88,7 +88,7 @@ defmodule EWalletDB.ExchangePairTest do end describe "update/2" do - test_update_field_ok(ExchangePair, :rate, 2.00, 9.99) + test_update_field_ok(ExchangePair, :rate, insert(:admin), 2.00, 9.99) test_update_prevents_changing( ExchangePair, diff --git a/apps/ewallet_db/test/ewallet_db/invite_test.exs b/apps/ewallet_db/test/ewallet_db/invite_test.exs index 0d1289c3f..13fd8af37 100644 --- a/apps/ewallet_db/test/ewallet_db/invite_test.exs +++ b/apps/ewallet_db/test/ewallet_db/invite_test.exs @@ -85,53 +85,54 @@ defmodule EWalletDB.InviteTest do describe "Invite.generate/2" do test "returns {:ok, invite} for the given user" do - user = insert(:admin) - {result, invite} = Invite.generate(user) + {:ok, admin} = :admin |> params_for() |> User.insert() + {result, invite} = Invite.generate(admin) assert result == :ok assert %Invite{} = invite - assert invite.user_uuid == user.uuid + assert invite.user_uuid == admin.uuid assert invite.verified_at == nil end test "associates the invite_uuid to the user" do - user = insert(:admin) - {:ok, invite} = Invite.generate(user) + {:ok, admin} = :admin |> params_for() |> User.insert() + {:ok, invite} = Invite.generate(admin) - user = User.get(user.id) + user = User.get(admin.id) assert user.invite_uuid == invite.uuid end test "sets the success_url if the option is given" do - user = insert(:admin) - {:ok, invite} = Invite.generate(user, success_url: "http://some_url") + {:ok, admin} = :admin |> params_for() |> User.insert() + {:ok, invite} = Invite.generate(admin, success_url: "http://some_url") assert invite.success_url == "http://some_url" end test "preloads the invite if the option is given" do - user = insert(:admin) - {:ok, invite} = Invite.generate(user, preload: :user) + {:ok, admin} = :admin |> params_for() |> User.insert() + {:ok, invite} = Invite.generate(admin, preload: :user) - assert invite.user.uuid == user.uuid + assert invite.user.uuid == admin.uuid end end describe "Invite.accept/2" do test "sets user to :active status" do - {:ok, invite} = Invite.generate(insert(:admin)) + {:ok, admin} = :admin |> params_for() |> User.insert() + {:ok, invite} = Invite.generate(admin) user = User.get_by(uuid: invite.user_uuid) assert User.get_status(user) == :pending_confirmation {:ok, _invite} = Invite.accept(invite, "some_password") - user = User.get(user.id) + user = User.get(user.id) assert User.get_status(user) == :active end test "sets user with the given password" do - admin = insert(:admin) + {:ok, admin} = :admin |> params_for() |> User.insert() {:ok, invite} = Invite.generate(admin) {res, _invite} = Invite.accept(invite, "some_password") @@ -142,7 +143,7 @@ defmodule EWalletDB.InviteTest do end test "disassociates the invite_uuid from the user" do - admin = insert(:admin) + {:ok, admin} = :admin |> params_for() |> User.insert() {:ok, invite} = Invite.generate(admin) {res, _invite} = Invite.accept(invite, "some_password") @@ -152,7 +153,7 @@ defmodule EWalletDB.InviteTest do end test "sets verified_at date time" do - admin = insert(:admin) + {:ok, admin} = :admin |> params_for() |> User.insert() {:ok, invite} = Invite.generate(admin) {res, invite} = Invite.accept(invite, "some_password") diff --git a/apps/ewallet_db/test/ewallet_db/key_test.exs b/apps/ewallet_db/test/ewallet_db/key_test.exs index 9ce44e351..23727fc07 100644 --- a/apps/ewallet_db/test/ewallet_db/key_test.exs +++ b/apps/ewallet_db/test/ewallet_db/key_test.exs @@ -91,7 +91,7 @@ defmodule EWalletDB.KeyTest do end describe "update/2" do - test_update_field_ok(Key, :expired, false, true) + test_update_field_ok(Key, :expired, insert(:admin), false, true) test_update_ignores_changing(Key, :access_key) test "does not update secret_key_hash when given a new secret_key" do diff --git a/apps/ewallet_db/test/ewallet_db/user_test.exs b/apps/ewallet_db/test/ewallet_db/user_test.exs index fddd08618..aa21676ac 100644 --- a/apps/ewallet_db/test/ewallet_db/user_test.exs +++ b/apps/ewallet_db/test/ewallet_db/user_test.exs @@ -1,6 +1,6 @@ defmodule EWalletDB.UserTest do use EWalletDB.SchemaCase - alias EWalletDB.{Account, Invite, User} + alias EWalletDB.{Account, Audit, Invite, User} describe "User factory" do test_has_valid_factory(User) @@ -17,6 +17,13 @@ defmodule EWalletDB.UserTest do assert user.provider_user_id == inserted_user.provider_user_id assert user.metadata["first_name"] == inserted_user.metadata["first_name"] assert user.metadata["last_name"] == inserted_user.metadata["last_name"] + + audits = Audit.all_for_target(User, user.uuid) + assert length(audits) == 1 + + audit = Enum.at(audits, 0) + assert audit.originator_uuid != nil + assert audit.originator_type == "user" end test_insert_generate_uuid(User, :uuid) @@ -58,8 +65,10 @@ defmodule EWalletDB.UserTest do end describe "update/2" do - test_update_field_ok(User, :username) - test_update_field_ok(User, :metadata, %{"field" => "old"}, %{"field" => "new"}) + test_update_field_ok(User, :username, insert(:admin)) + + test_update_field_ok(User, :metadata, insert(:admin), %{"field" => "old"}, %{"field" => "new"}) + test_update_prevents_changing(User, :provider_user_id) test "prevents updating an admin without email" do @@ -96,7 +105,8 @@ defmodule EWalletDB.UserTest do email: "test_1337@example.com", metadata: %{"key" => "value_1337"}, encrypted_metadata: %{"key" => "value_1337"}, - provider_user_id: "test_1337_puid" + provider_user_id: "test_1337_puid", + originator: insert(:admin) }) assert updated_user.email == "test_1337@example.com" diff --git a/apps/ewallet_db/test/support/factory.ex b/apps/ewallet_db/test/support/factory.ex index 958063f59..9078cc97c 100644 --- a/apps/ewallet_db/test/support/factory.ex +++ b/apps/ewallet_db/test/support/factory.ex @@ -9,6 +9,7 @@ defmodule EWalletDB.Factory do Account, AccountUser, APIKey, + Audit, AuthToken, Category, ExchangePair, @@ -19,6 +20,7 @@ defmodule EWalletDB.Factory do Membership, Mint, Role, + System, Token, Transaction, TransactionConsumption, @@ -101,6 +103,7 @@ defmodule EWalletDB.Factory do email: nil, username: sequence("johndoe"), provider_user_id: sequence("provider_id"), + originator: insert(:admin), metadata: %{ "first_name" => sequence("John"), "last_name" => sequence("Doe") @@ -117,6 +120,7 @@ defmodule EWalletDB.Factory do email: sequence("johndoe") <> "@example.com", password: password, password_hash: Crypto.hash_password(password), + originator: :self, metadata: %{ "first_name" => sequence("John"), "last_name" => sequence("Doe") @@ -134,6 +138,7 @@ defmodule EWalletDB.Factory do password: password, password_hash: Crypto.hash_password(password), invite: nil, + originator: %System{}, metadata: %{ "first_name" => sequence("John"), "last_name" => sequence("Doe") @@ -141,12 +146,43 @@ defmodule EWalletDB.Factory do } end + def audit_factory do + params = params_for(:user) + user = insert(:user, params) + originator = insert(:admin) + + %Audit{ + action: "insert", + target_type: Audit.get_type(User), + target_uuid: user.uuid, + target_changes: params, + originator_uuid: originator.uuid, + originator_type: Audit.get_type(Admin) + } + end + + def sytem_audit_factory do + params = params_for(:admin) + admin = insert(params) + originator = %System{} + + %Audit{ + action: "insert", + target_type: Audit.get_type(admin.__struct__), + target_uuid: admin.uuid, + target_changes: params, + originator_uuid: originator.uuid, + originator_type: Audit.get_type(originator.__struct__) + } + end + def invite_factory do %Invite{ user: nil, token: Crypto.generate_base64_key(32), success_url: nil, - verified_at: nil + verified_at: nil, + originator: insert(:admin) } end diff --git a/apps/ewallet_db/test/support/schema_case.ex b/apps/ewallet_db/test/support/schema_case.ex index a5f4361c5..cd1f9573a 100644 --- a/apps/ewallet_db/test/support/schema_case.ex +++ b/apps/ewallet_db/test/support/schema_case.ex @@ -37,7 +37,7 @@ defmodule EWalletDB.SchemaCase do """ import EWalletDB.Factory alias Ecto.Adapters.SQL - alias EWalletDB.Account + alias EWalletDB.{Account, User} defmacro __using__(_opts) do quote do @@ -66,7 +66,7 @@ defmodule EWalletDB.SchemaCase do role = insert(:role, %{name: role_name}) _membership = insert(:membership, %{user: user, account: account, role: role}) - {user, account} + {User.get(user.id), account} end def get_or_insert_master_account do @@ -429,11 +429,12 @@ defmodule EWalletDB.SchemaCase do @doc """ Test schema's update/2 does update the given field """ - defmacro test_update_field_ok(schema, field, old \\ "old", new \\ "new") do + defmacro test_update_field_ok(schema, field, originator, old \\ "old", new \\ "new") do quote do test "updates #{unquote(field)} successfully" do schema = unquote(schema) field = unquote(field) + originator = unquote(originator) old = unquote(old) new = unquote(new) @@ -443,7 +444,11 @@ defmodule EWalletDB.SchemaCase do |> params_for(%{field => old}) |> schema.insert() - {res, updated} = schema.update(original, %{field => new}) + {res, updated} = + schema.update(original, %{ + :originator => originator, + field => new + }) assert res == :ok assert Map.fetch!(updated, field) == new