From 678aab4b9ccd952a0e0f31284a63a6889a784ef8 Mon Sep 17 00:00:00 2001 From: Tuchiro Yoshimura Date: Fri, 11 Jan 2019 12:56:39 +0900 Subject: [PATCH 1/7] issue #5 --- 1} | 0 RequestSample_accounts.http | 15 +- RequestSample_authenticator.http | 9 +- config/config.exs | 15 +- config/test.exs | 2 +- lib/materia/accounts/account.ex | 45 ++++ lib/materia/accounts/accounts.ex | 238 +++++++++++++----- .../authenticator/account_auth_pipline.ex | 14 ++ .../authenticator/account_authenticator.ex | 214 ++++++++++++++++ ...authenticator.ex => authenticator_base.ex} | 236 +++++++---------- .../password_reset_auth_pipline.ex | 2 +- ...ticate_pipline.ex => user_auth_pipline.ex} | 4 +- .../authenticator/user_authenticator.ex | 129 ++++++++++ .../user_registration_auth_pipline.ex | 2 +- lib/materia/organizations/organizations.ex | 6 +- lib/materia_web/controller_base.ex | 25 +- .../controllers/account_controller.ex | 53 ++++ .../controllers/authenticator_controller.ex | 33 ++- .../controllers/user_controller.ex | 23 +- lib/materia_web/router.ex | 14 +- lib/materia_web/views/account_view.ex | 39 +++ lib/mix/templates/6_create_accounts.exs | 25 ++ ...20190110020429_materia_1_craete_users.exs} | 0 ...020430_materia_2_craete_organizations.exs} | 0 ...190110020431_materia_3_craete_address.exs} | 0 ...0190110020432_materia_4_craete_grants.exs} | 0 ...20433_materia_5_craete_mail_templates.exs} | 0 ...190110020434_materia_6_create_accounts.exs | 25 ++ priv/repo/seeds.exs | 2 + .../controllers/account_controller_test.exs | 113 +++++++++ .../authenticator_conrtoller_test.exs | 209 +++++++++++++++ 31 files changed, 1241 insertions(+), 251 deletions(-) create mode 100644 1} create mode 100644 lib/materia/accounts/account.ex create mode 100644 lib/materia/authenticator/account_auth_pipline.ex create mode 100644 lib/materia/authenticator/account_authenticator.ex rename lib/materia/authenticator/{authenticator.ex => authenticator_base.ex} (55%) rename lib/materia/authenticator/{authenticate_pipline.ex => user_auth_pipline.ex} (82%) create mode 100644 lib/materia/authenticator/user_authenticator.ex create mode 100644 lib/materia_web/controllers/account_controller.ex create mode 100644 lib/materia_web/views/account_view.ex create mode 100644 lib/mix/templates/6_create_accounts.exs rename priv/repo/migrations/{20181211023323_materia_1_craete_users.exs => 20190110020429_materia_1_craete_users.exs} (100%) rename priv/repo/migrations/{20181211023324_materia_2_craete_organizations.exs => 20190110020430_materia_2_craete_organizations.exs} (100%) rename priv/repo/migrations/{20181211023325_materia_3_craete_address.exs => 20190110020431_materia_3_craete_address.exs} (100%) rename priv/repo/migrations/{20181211023326_materia_4_craete_grants.exs => 20190110020432_materia_4_craete_grants.exs} (100%) rename priv/repo/migrations/{20181211023327_materia_5_craete_mail_templates.exs => 20190110020433_materia_5_craete_mail_templates.exs} (100%) create mode 100644 priv/repo/migrations/20190110020434_materia_6_create_accounts.exs create mode 100644 test/materia_web/controllers/account_controller_test.exs create mode 100644 test/materia_web/controllers/authenticator_conrtoller_test.exs diff --git a/1} b/1} new file mode 100644 index 0000000..e69de29 diff --git a/RequestSample_accounts.http b/RequestSample_accounts.http index 9b310ed..35434e2 100644 --- a/RequestSample_accounts.http +++ b/RequestSample_accounts.http @@ -6,17 +6,26 @@ @url = http://localhost:4001 #@url = https:// -###ログイン +###ログイン(ユーザー認証) POST {{url}}/api/sign-in HTTP/1.1 Content-Type: application/json { + "email": "fugafuga@example.com", + "password": "fugafuga" + } + +###ログイン(アカウント+ユーザー認証) +POST {{url}}/api/sign-in HTTP/1.1 +Content-Type: application/json + +{ + "account": "hogehoge account", "email": "hogehoge@example.com", "password": "hogehoge" } -@token = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJNYXRlcmlhIiwiZW1haWwiOiJob2dlaG9nZUBleGFtcGxlLmNvbSIsImV4cCI6MTU0NTgxMTc4OSwiaWF0IjoxNTQ1ODExMTg5LCJpc3MiOiJNYXRlcmlhIiwianRpIjoiMWEzYmM0ZGMtZWJmYS00MWUyLWJiNjctNDYxMTJmMzU4Njc2IiwibmJmIjoxNTQ1ODExMTg4LCJzdWIiOiJ7XCJ1c2VyX2lkXCI6MX0iLCJ0eXAiOiJhY2Nlc3MifQ.taF2To9CACyPMyZcmDjCi82VKmhA-fq8QApVhfiA8DY - +@token = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJNYXRlcmlhIiwiZW1haWwiOiJmdWdhZnVnYUBleGFtcGxlLmNvbSIsImV4cCI6MTU0NzExNTc0MiwiaWF0IjoxNTQ3MTE1MTQyLCJpc3MiOiJNYXRlcmlhIiwianRpIjoiZDdlNTY1N2MtYmJmYi00ZWVlLTgyOWMtNTMzMWQ3YzYxNGJmIiwibmJmIjoxNTQ3MTE1MTQxLCJzdWIiOiJ7XCJ1c2VyX2lkXCI6Mn0iLCJ0eXAiOiJhY2Nlc3MifQ.BRWP0jtVN47tCg94ssfQJ6WLldP_FHgjoz7r4HSIM4U ### ユーザー汎用検索 POST {{url}}/api/search-users HTTP/1.1 diff --git a/RequestSample_authenticator.http b/RequestSample_authenticator.http index 251e2ac..ca49bfc 100644 --- a/RequestSample_authenticator.http +++ b/RequestSample_authenticator.http @@ -16,7 +16,7 @@ Content-Type: application/json } ###仮トークン認証 -@tmp_token = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJNYXRlcmlhIiwiZW1haWwiOiJ0Y3IueW9zaGltdXJhQGthcmFiaW5lci50ZWNoIiwiZXhwIjoxNTQzOTc1OTM1LCJpYXQiOjE1NDM5NzM4MzUsImlzcyI6Ik1hdGVyaWEiLCJqdGkiOiJjZDcyMDczMi01MmM0LTQyZGEtODYzNC01ODEyMGZjMzc4NDMiLCJuYmYiOjE1NDM5NzM4MzQsInN1YiI6IjMiLCJ0eXAiOiJ1c2VyX3JlZ2lzdHJhdGlvbiJ9.oktmhWQAp9dS9hZlzB0ZXdeG34trEbT3xXfiZf_yBZg +@tmp_token = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJNYXRlcmlhIiwiZW1haWwiOiJ0Y3IueW9zaGltdXJhQGthcmFiaW5lci50ZWNoIiwiZXhwIjoxNTQ3MTE3MjgwLCJpYXQiOjE1NDcxMTUxODAsImlzcyI6Ik1hdGVyaWEiLCJqdGkiOiI5Zjg4NDA3Ni1iNGRlLTQ5ZGUtYTdjMi0xYjg5YTVmNzkyNmEiLCJuYmYiOjE1NDcxMTUxNzksInN1YiI6IntcInVzZXJfaWRcIjozfSIsInR5cCI6InVzZXJfcmVnaXN0cmF0aW9uIn0.HH-40L3Cv5wtKca5EveuG_xMJF0EqChgRuJhzNULkVk GET {{url}}/api/varidation-tmp-user HTTP/1.1 Content-Type: application/json @@ -45,14 +45,13 @@ Authorization: {{tmp_token}} ###パスワード再登録申請 POST {{url}}/api/request-password-reset HTTP/1.1 Content-Type: application/json -Authorization: {{tmp_token}} { "email": "tcr.yoshimura@karabiner.tech" } ### PWリセットトークン認証 -@pw_reset_token = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJNYXRlcmlhIiwiZW1haWwiOiJ0Y3IueW9zaGltdXJhQGthcmFiaW5lci50ZWNoIiwiZXhwIjoxNTQzOTc1OTkyLCJpYXQiOjE1NDM5NzM4OTIsImlzcyI6Ik1hdGVyaWEiLCJqdGkiOiIxZGM3MjQwNi1iZjM0LTRhYjItYTA2NC1hZTFmMjdjYmI1YWMiLCJuYmYiOjE1NDM5NzM4OTEsInN1YiI6IjMiLCJ0eXAiOiJwYXNzd29yZF9yZXNldCJ9.fER3oonKbywoW8NcelqNnpZs8yain7LFcjdqK1jQi7M +@pw_reset_token = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJNYXRlcmlhIiwiZW1haWwiOiJ0Y3IueW9zaGltdXJhQGthcmFiaW5lci50ZWNoIiwiZXhwIjoxNTQ3MTE3MzgwLCJpYXQiOjE1NDcxMTUyODAsImlzcyI6Ik1hdGVyaWEiLCJqdGkiOiJmZjYwY2M1Mi1hZDJhLTQ3N2QtODRhNi1hZTA2MTk1MjU4MzUiLCJuYmYiOjE1NDcxMTUyNzksInN1YiI6IntcInVzZXJfaWRcIjozfSIsInR5cCI6InBhc3N3b3JkX3Jlc2V0In0.JuwU6cmX_W_cPomoQfqDC0apyxMV_vyv8XYyTCy63hY GET {{url}}/api/varidation-pw-reset HTTP/1.1 Content-Type: application/json @@ -114,7 +113,7 @@ Content-Type: application/json "password": "fugafuga" } -@token = Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJTZXJ2aWNleCIsImVtYWlsIjoiaG9nZWhvZ2VAZXhhbXBsZS5jb20iLCJleHAiOjE1NDA4MDUwMjcsImlhdCI6MTU0MDgwNDQyNywiaXNzIjoiU2VydmljZXgiLCJqdGkiOiIyMWM4MmNkMy01ZmRjLTRkYzItYjE4MS1kZWY4OTNhZmI4YmEiLCJuYmYiOjE1NDA4MDQ0MjYsInN1YiI6IjEiLCJ0eXAiOiJhY2Nlc3MifQ.H6sMFoAb5FW27MguIdfqEgC6yMBGLeNeehsEjutKulBL0PMlFBIXruPaCa-Nf_9sXI4Cd-KSmsTsQR5yEL7stA +@token = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJNYXRlcmlhIiwiZW1haWwiOiJmdWdhZnVnYUBleGFtcGxlLmNvbSIsImV4cCI6MTU0NzExNjAxMywiaWF0IjoxNTQ3MTE1NDEzLCJpc3MiOiJNYXRlcmlhIiwianRpIjoiYmJkMzhhZDItNzkzNy00NzQyLWE1YTAtODNiYWYxNzc4YzNkIiwibmJmIjoxNTQ3MTE1NDEyLCJzdWIiOiJ7XCJ1c2VyX2lkXCI6Mn0iLCJ0eXAiOiJhY2Nlc3MifQ.U50hzVYNyv5u4Z2FixJ23zTdMc5Z6Qoy229c5ylXGMw ###認証チェック @@ -128,7 +127,7 @@ POST {{url}}/api/refresh HTTP/1.1 Content-Type: application/json { - "refresh_token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJTZXJ2aWNleCIsImVtYWlsIjoiaG9nZWhvZ2VAZXhhbXBsZS5jb20iLCJleHAiOjE1NDA4MDUwMjcsImlhdCI6MTU0MDgwNDQyNywiaXNzIjoiU2VydmljZXgiLCJqdGkiOiIyMWM4MmNkMy01ZmRjLTRkYzItYjE4MS1kZWY4OTNhZmI4YmEiLCJuYmYiOjE1NDA4MDQ0MjYsInN1YiI6IjEiLCJ0eXAiOiJhY2Nlc3MifQ.H6sMFoAb5FW27MguIdfqEgC6yMBGLeNeehsEjutKulBL0PMlFBIXruPaCa-Nf_9sXI4Cd-KSmsTsQR5yEL7stA" + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJNYXRlcmlhIiwiZW1haWwiOiJmdWdhZnVnYUBleGFtcGxlLmNvbSIsImV4cCI6MTU0NzIwMzM3OCwiaWF0IjoxNTQ3MTE2OTc4LCJpc3MiOiJNYXRlcmlhIiwianRpIjoiN2ZmZWM4ZWItYjgzZS00ZWFjLWFmOTktN2Q4MzFjM2RlMjU0IiwibmJmIjoxNTQ3MTE2OTc3LCJzdWIiOiJ7XCJ1c2VyX2lkXCI6Mn0iLCJ0eXAiOiJyZWZyZXNoIn0.lYNVP4I7xtWX842AeRIYY8vf324ItS7i3HiT_6cyp3Q" } diff --git a/config/config.exs b/config/config.exs index e9876d0..6a4927e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -22,13 +22,22 @@ config :logger, :console, format: "$time $metadata[$level] $message\n", metadata: [:user_id] -# Configures Guardian +# Configures Materia.Authenticator config :materia, Materia.Authenticator, - issuer: "Materia", access_token_ttl: {10, :minutes}, #必須 refresh_token_ttl: {1, :days}, # refresh_tokenを定義しない場合sign-inはaccess_tokenのみ返す user_registration_token_ttl: {35, :minutes}, - password_reset_token_ttl: {35, :minutes}, + password_reset_token_ttl: {35, :minutes} + +# Configures Guardian +config :materia, Materia.UserAuthenticator, + issuer: "Materia", + secret_key: "VlY6rTO8s+oM6/l4tPY0mmpKubd1zLEDSKxOjHA4r90ifZzCOYVY5IBEhdicZStw", + allowed_algos: ["HS256"] + +# Configures Guardian +config :materia, Materia.AccountAuthenticator, + issuer: "Materia", secret_key: "VlY6rTO8s+oM6/l4tPY0mmpKubd1zLEDSKxOjHA4r90ifZzCOYVY5IBEhdicZStw", allowed_algos: ["HS256"] diff --git a/config/test.exs b/config/test.exs index ab00305..3179529 100644 --- a/config/test.exs +++ b/config/test.exs @@ -11,7 +11,7 @@ config :materia, MateriaWeb.Test.Endpoint, watchers: [] # Print only warnings and errors during test -config :logger, level: :info +config :logger, level: :debug # Configure your database config :materia, Materia.Test.Repo, diff --git a/lib/materia/accounts/account.ex b/lib/materia/accounts/account.ex new file mode 100644 index 0000000..f8badff --- /dev/null +++ b/lib/materia/accounts/account.ex @@ -0,0 +1,45 @@ +defmodule Materia.Accounts.Account do + use Ecto.Schema + import Ecto.Changeset + + + schema "accounts" do + field :external_code, :string + field :name, :string + field :start_datetime, :utc_datetime + field :frozen_datetime, :utc_datetime + field :expired_datetime, :utc_datetime + field :descriptions, :string + field :status, :integer, default: 1 + field :lock_version, :integer, default: 0 + belongs_to :organization ,Materia.Organizations.Organization + belongs_to :main_user ,Materia.Accounts.User, [foreign_key: :main_user_id] + + timestamps() + end + + @doc false + def create_changeset(account, attrs) do + account + |> cast(attrs, [:external_code, :name, :start_datetime, :descriptions, :frozen_datetime, :expired_datetime,:status, :organization_id, :main_user_id, :lock_version]) + |> validate_required([:name, :start_datetime]) + |> unique_constraint(:code) + end + + @doc false + def update_changeset(account, attrs) do + account + |> cast(attrs, [:external_code, :name, :start_datetime, :descriptions, :frozen_datetime, :expired_datetime, :status, :organization_id, :main_user_id, :lock_version]) + |> validate_required([:lock_version]) + |> unique_constraint(:code) + |> optimistic_lock(:lock_version) + end + + def status() do + %{ + activated: 1, # アカウント有効 + frozen: 8, # アカウント凍結中 + expired: 9, #アカウント無効 + } + end +end diff --git a/lib/materia/accounts/accounts.ex b/lib/materia/accounts/accounts.ex index dccf6b3..975f5fc 100644 --- a/lib/materia/accounts/accounts.ex +++ b/lib/materia/accounts/accounts.ex @@ -6,14 +6,18 @@ defmodule Materia.Accounts do import Ecto.Query, warn: false alias Materia.Accounts.User - alias Materia.Authenticator + alias Materia.Accounts.Account + alias Materia.UserAuthenticator alias Materia.Mails alias MateriaUtils.Ecto.EctoUtil + alias MateriaUtils.Calendar.CalendarUtil alias Materia.Errors.BusinessError require Logger + @repo Application.get_env(:materia, :repo) + @msg_err_duplicate_emal "this email address was already registered." @msg_user_not_active "user not active." @@ -103,11 +107,10 @@ defmodule Materia.Accounts do """ def list_users do - repo = Application.get_env(:materia, :repo) User - |> repo.all() - |> repo.preload([:organization, :addresses]) + |> @repo.all() + |> @repo.preload([:organization, :addresses]) end @doc """ @@ -197,11 +200,10 @@ defmodule Materia.Accounts do """ def list_users_by_params(params) do - repo = Application.get_env(:materia, :repo) - repo + @repo |> EctoUtil.select_by_param(User, params) - |> repo.preload(:organization) + |> @repo.preload(:organization) end @doc """ @@ -273,11 +275,11 @@ defmodule Materia.Accounts do """ def get_user!(id) do - repo = Application.get_env(:materia, :repo) + User - |> repo.get!(id) - |> repo.preload([:organization, :addresses]) + |> @repo.get!(id) + |> @repo.preload([:organization, :addresses]) end @doc """ @@ -319,14 +321,14 @@ defmodule Materia.Accounts do """ def get_user_by_email(email) do - repo = Application.get_env(:materia, :repo) + user = with [user] <- User |> where(email: ^email) - |> repo.all() - |> repo.preload(:organization) do + |> @repo.all() + |> @repo.preload(:organization) do user else _ -> nil @@ -379,11 +381,11 @@ defmodule Materia.Accounts do """ def create_user(attrs \\ %{}) do - repo = Application.get_env(:materia, :repo) + %User{} |> User.changeset_create(attrs) - |> repo.insert() + |> @repo.insert() end @doc """ @@ -455,11 +457,10 @@ defmodule Materia.Accounts do """ def update_user(%User{} = user, attrs) do - repo = Application.get_env(:materia, :repo) user |> User.changeset_update(attrs) - |> repo.update() + |> @repo.update() end @doc """ @@ -471,35 +472,20 @@ defmodule Materia.Accounts do ``` - iex(1)> user = Materia.Accounts.get_user!(1) - iex(2)> Materia.Accounts.delete_user(user) - iex(3)> users = Materia.Accounts.list_users() - iex(4)> MateriaWeb.UserView.render("index.json", %{users: users}) - [ - %{ - addresses: [], - back_ground_img_url: nil, - descriptions: nil, - email: "fugafuga@example.com", - external_user_id: nil, - icon_img_url: nil, - id: 2, - lock_version: 1, - name: "fugafuga", - organization: nil, - role: "operator", - phone_number: nil, - status: 1 - } - ] + iex(1)> {:ok, user} = Materia.Accounts.create_user(%{name: "test_delete_user001", email: "test_delete_user001@example.com", password: "test_delete_user001", role: "operator", organization_id: 1}) + iex(2)> Materia.Accounts.list_users_by_params(%{ "and" => [%{"name" => "test_delete_user001"}]}) |> length() + 1 + iex(3)> Materia.Accounts.delete_user(user) + iex(4)> Materia.Accounts.list_users_by_params(%{ "and" => [%{"name" => "test_delete_user001"}]}) |> length() + 0 ``` """ def delete_user(%User{} = user) do - repo = Application.get_env(:materia, :repo) - repo.delete(user) + + @repo.delete(user) end alias Materia.Accounts.Grant @@ -532,8 +518,8 @@ defmodule Materia.Accounts do """ def list_grants do - repo = Application.get_env(:materia, :repo) - repo.all(Grant) + + @repo.all(Grant) end @doc """ @@ -553,8 +539,8 @@ defmodule Materia.Accounts do """ def get_grant!(id) do - repo = Application.get_env(:materia, :repo) - repo.get!(Grant, id) + + @repo.get!(Grant, id) end @doc """ @@ -587,12 +573,12 @@ defmodule Materia.Accounts do """ def get_grant_by_role(role) do - repo = Application.get_env(:materia, :repo) + dc_role = String.downcase(role) Grant |> where([g], g.role == ^dc_role or g.role == ^Grant.role().anybody) - |> repo.all() + |> @repo.all() end @doc """ @@ -610,11 +596,10 @@ defmodule Materia.Accounts do """ def create_grant(attrs \\ %{}) do - repo = Application.get_env(:materia, :repo) %Grant{} |> Grant.changeset(attrs) - |> repo.insert() + |> @repo.insert() end @doc """ @@ -634,11 +619,10 @@ defmodule Materia.Accounts do """ def update_grant(%Grant{} = grant, attrs) do - repo = Application.get_env(:materia, :repo) grant |> Grant.changeset(attrs) - |> repo.update() + |> @repo.update() end @doc """ @@ -670,8 +654,8 @@ defmodule Materia.Accounts do """ def delete_grant(%Grant{} = grant) do - repo = Application.get_env(:materia, :repo) - repo.delete(grant) + + @repo.delete(grant) end @doc """ @@ -693,11 +677,10 @@ defmodule Materia.Accounts do @doc false defp create_tmp_user(attrs \\ %{}) do - repo = Application.get_env(:materia, :repo) %User{} |> User.changeset_tmp_registration(attrs) - |> repo.insert() + |> @repo.insert() end @doc """ @@ -745,7 +728,7 @@ defmodule Materia.Accounts do end end - {:ok, user_registration_token} = Authenticator.get_user_registration_token(email) + {:ok, user_registration_token} = UserAuthenticator.get_user_registration_token(email) email = merged_user.email template_type = config[:user_registration_request_mail_template_type] @@ -770,10 +753,10 @@ defmodule Materia.Accounts do @doc false defp registration_user(%User{} = user, attrs) do - repo = Application.get_env(:materia, :repo) + user |> User.changeset_registration(attrs) - |> repo.update() + |> @repo.update() end @doc """ @@ -936,7 +919,7 @@ defmodule Materia.Accounts do {:ok, user} = registration_user(_result, user, attr) - {:ok, result} = Authenticator.sign_in(user.email, user.password) + {:ok, result} = UserAuthenticator.sign_in(user.email, user.password) user_with_token = %{ user: user, @@ -970,7 +953,7 @@ defmodule Materia.Accounts do # ユーザーが存在しない場合も素知らぬ顔で正常終了する {:ok, %{password_reset_token: ""}} else - {:ok, password_reset_token} = Authenticator.get_password_reset_token(email) + {:ok, password_reset_token} = UserAuthenticator.get_password_reset_token(email) email = email template_type = config[:password_reset_request_mail_template_type] @@ -1117,5 +1100,142 @@ defmodule Materia.Accounts do end end + alias Materia.Accounts.Account + + @doc """ + Returns the list of accounts. + + ## Examples + + iex(1)> accounts = Materia.Accounts.list_accounts() + iex(2)> length(accounts) + 1 + + """ + def list_accounts do + @repo.all(Account) + |> @repo.preload(:organization) + |> @repo.preload(:main_user) + end + + @doc """ + Gets a single account. + + Raises `Ecto.NoResultsError` if the Account does not exist. + + ## Examples + + iex(1)> account = Materia.Accounts.get_account!(1) + iex(2)> MateriaWeb.AccountView.render("show.json", %{account: account}) |> Map.delete(:start_datetime) + %{ + descriptions: nil, + expired_datetime: nil, + external_code: nil, + frozen_datetime: nil, + id: 1, + lock_version: 0, + main_user: nil, + name: "hogehoge account", + organization: nil, + status: 1 + } + + """ + def get_account!(id), do: @repo.get!(Account, id) + + + @doc """ + iex(1)> accounts = Materia.Accounts.list_accounts_by_params(%{"and" => [ %{"status" => 1}, %{"organization_id" => 1} ]}) + iex(2)> length(accounts) + 1 + + """ + def list_accounts_by_params(params) do + @repo + |> EctoUtil.select_by_param(Account, params) + |> @repo.preload(:organization) + |> @repo.preload(:main_user) + end + + @doc """ + Creates a accounts. + + ## Examples + + iex(1)> {:ok, account} = Materia.Accounts.create_account(%{"name" => "craete_account_test001"}) + iex(2)> MateriaWeb.AccountView.render("show.json", %{account: account}) |> Map.delete(:id) |> Map.delete(:start_datetime) + %{ + descriptions: nil, + expired_datetime: nil, + external_code: nil, + frozen_datetime: nil, + lock_version: 0, + main_user: nil, + name: "craete_account_test001", + organization: nil, + status: 1 + } + + """ + def create_account(attrs \\ %{}) do + attrs = + if Map.has_key?(attrs, "start_datetime") do + attrs + else + Map.put(attrs, "start_datetime", CalendarUtil.now()) + end + %Account{} + |> Account.create_changeset(attrs) + |> @repo.insert() + end + + @doc """ + Updates a account. + + ## Examples + + iex(1)> {:ok, account} = Materia.Accounts.create_account(%{"name" => "update_account_test001"}) + iex(2)> {:ok, updated_account} = Materia.Accounts.update_account(account, %{"status" => 8}) + iex(3)> updated_account.status + 8 + iex(4)> updated_account.frozen_datetime != nil + true + + """ + def update_account(%Account{} = account, attrs) do + + # ステータスが更新されている場合、 + if Map.has_key?(attrs, "status") do + attrs = + cond do + attrs["status"] == Account.status.activated -> + Map.put(attrs, "start_datetime", CalendarUtil.now()) + attrs["status"] == Account.status.frozen -> + Map.put(attrs, "frozen_datetime", CalendarUtil.now()) + attrs["status"] == Account.status.expired -> + Map.put(attrs, "expired_datetime", CalendarUtil.now()) + end + end + + account + |> Account.update_changeset(attrs) + |> @repo.update() + end + + @doc """ + Deletes a Account. + + ## Examples + + iex(1)> {:ok, account} = Materia.Accounts.create_account(%{"name" => "delete_account_test001"}) + iex(2)> {:ok, deleted_account} = Materia.Accounts.delete_account(account) + iex(3)> Materia.Accounts.list_accounts_by_params(%{"and" => [ %{"id" => account.id} ] }) + [] + + """ + def delete_account(%Account{} = account) do + @repo.delete(account) + end + end diff --git a/lib/materia/authenticator/account_auth_pipline.ex b/lib/materia/authenticator/account_auth_pipline.ex new file mode 100644 index 0000000..e117814 --- /dev/null +++ b/lib/materia/authenticator/account_auth_pipline.ex @@ -0,0 +1,14 @@ +defmodule Materia.AccountAuthPipeline do + @moduledoc false + use Guardian.Plug.Pipeline, otp_app: :materia, + module: Materia.AccountAuthenticator, + error_handler: Materia.AuthenticateErrorHandler + plug Guardian.Plug.VerifySession, claims: %{"typ" => "access"} + #plug Materia.Plug.Debug + plug Guardian.Plug.VerifyHeader, claims: %{"typ" => "access"} + #plug Materia.Plug.Debug + plug Guardian.Plug.EnsureAuthenticated + #plug Materia.Plug.Debug + plug Guardian.Plug.LoadResource, allow_blank: true + #plug Materia.Plug.Debug +end diff --git a/lib/materia/authenticator/account_authenticator.ex b/lib/materia/authenticator/account_authenticator.ex new file mode 100644 index 0000000..c979fbc --- /dev/null +++ b/lib/materia/authenticator/account_authenticator.ex @@ -0,0 +1,214 @@ +defmodule Materia.AccountAuthenticator do + @moduledoc false + + require Logger + + use Guardian, otp_app: :materia + + #use Materia.AuthenticatorBase, except: [subject_for_token: 2, resource_from_claims: 1,on_verify: 3] + use Materia.AuthenticatorBase, except: [subject_for_token: 2, resource_from_claims: 1,on_verify: 3] + + alias Materia.Errors.BusinessError + alias Materia.Accounts.User + alias Materia.Accounts.Account + + alias Materia.Accounts + + @msg_err_invalid_token "invalid token" + @msg_err_invalid_email_or_pass "account or email or password is invalid" + + def subject_for_token(resource, _claims) do + # Override Materia.Authenticator + Logger.debug("--- #{__MODULE__} subject_for_token --------------") + {:ok, sub} = Poison.encode(%{user_id: resource.user.id, account_id: resource.account.id}) + end + + def subject_for_token(_, _) do + Logger.debug("--- #{__MODULE__} subject_for_token reason_for_error--------------") + {:error, :reason_for_error} + end + + def resource_from_claims(_claims = %{"email" => email, "account" => account}) do + # Override Materia.Authenticator + Logger.debug("--- #{__MODULE__} resource_from_claims by email--------------") + user = Materia.Accounts.get_user_by_email(email) + if user.organization_id == nil do + {:error, "account #{account} not found."} + else + org_accounts = Accounts.list_accounts_by_params(%{"and" => [%{"organization_id" => user.organization_id}]}) + org_account = Enum.find(org_accounts, fn(org_account) -> org_account.name === account end) + if org_account == nil do + {:error, "account #{account} not found."} + else + resource = %{user: user, account: org_account} + {:ok, resource} + end + end + end + + def resource_from_claims(_claims) do + Logger.debug("--- #{__MODULE__} resource_from_claims by other--------------") + {:error, :reason_for_error} + end + + def on_verify(claims, token, _options) do + Logger.debug("--- #{__MODULE__} on_verify --------------") + + user = Materia.Accounts.get_user!(get_user_id_from_claims(claims)) + + case claims["typ"] do + "access" -> + if user.status != User.status.activated do + raise BusinessError, message: @msg_err_invalid_token + end + "user_registration" -> + if user.status != User.status.unactivated do + raise BusinessError, message: @msg_err_invalid_token + end + "password_reset" -> + if user.status != User.status.activated do + raise BusinessError, message: @msg_err_invalid_token + end + _ -> + raise BusinessError, message: @msg_err_invalid_token + end + + with {:ok, _} <- Guardian.DB.on_verify(claims, token) do + {:ok, claims} + end + end + + def sign_in(account, email, password) do + config = get_config() + access_token_ttl = config[:access_token_ttl] + Logger.debug("#{__MODULE__} --- sign_in access_token_ttl:#{inspect(access_token_ttl)}") + + if access_token_ttl == nil do + raise BusinessError, message: "materia.Authenticator access_token_ttl config not found." + end + + claims = %{"email" => email, "account" => account} + Logger.debug("#{__MODULE__} --- sign_in claims:#{inspect(claims)}") + + with {:ok, nil} <- resource_from_claims(claims) do + {:error, @msg_err_invalid_email_or_pass} + else + {:ok, resource} -> + if resource.user.status != User.status.activated or resource.account.status != Account.status.activated do + {:error, @msg_err_invalid_email_or_pass} + else + if Comeonin.Bcrypt.checkpw(password, resource.user.hashed_password) do + Logger.debug("#{__MODULE__} --- sign_in Comeonin.Bcrypt.checkpw == true") + with {:ok, access_token, _access_claims} <- + encode_and_sign(resource, claims, token_type: "access", ttl: access_token_ttl) do + Logger.debug("#{__MODULE__} --- sign_in Materia.Authenticator.encode_and_sign ok") + Logger.debug("#{__MODULE__} --- sign_in access_token:#{access_token}") + result = %{id: resource.user.id, access_token: access_token} + refresh_token_ttl = config[:refresh_token_ttl] + + Logger.debug( + "#{__MODULE__} --- sign_in refresh_token_ttl:#{inspect(refresh_token_ttl)}" + ) + + if refresh_token_ttl != nil do + refresh_token_ttl = config[:refresh_token_ttl] + + Logger.debug( + "#{__MODULE__} --- sign_in refresh_token_ttl:#{inspect(refresh_token_ttl)}" + ) + + with {:ok, refresh_token, _refresh_claims} <- + encode_and_sign( + resource, + claims, + token_type: "refresh", + ttl: refresh_token_ttl + ) do + result = Map.put(result, :refresh_token, refresh_token) + {:ok, result} + else + _ -> + {:error, @msg_err_invalid_email_or_pass} + end + else + {:ok, result} + end + else + _ -> + {:error, @msg_err_invalid_email_or_pass} + end + else + {:error, @msg_err_invalid_email_or_pass} + end + end + + _ -> + {:error, @msg_err_invalid_email_or_pass} + end + end + +# def refresh_tokens(refresh_token) do +# config = get_config() +# access_token_ttl = config[:access_token_ttl] +# Logger.debug("#{__MODULE__} --- refresh_tokens access_token_ttl:#{inspect(access_token_ttl)}") +# +# if access_token_ttl == nil do +# raise BusinessError, message: "materia.Authenticator access_token_ttl config not found." +# end +# +# with {:ok, _refresh, new_access} <- +# exchange(refresh_token, "refresh", "access", ttl: access_token_ttl) do +# {token, claims} = new_access +# Logger.debug("#{__MODULE__} --- refresh_tokens new_access_token:#{inspect(token)}") +# +# with {:ok, _} <- Guardian.DB.after_encode_and_sign(%{}, "access", claims, token) do +# refresh_token_ttl = config[:refresh_token_ttl] +# +# Logger.debug( +# "#{__MODULE__} --- refresh_tokens refresh_token_ttl:#{inspect(refresh_token_ttl)}" +# ) +# +# if refresh_token_ttl == nil do +# raise BusinessError, +# message: "materia.Authenticator refresh_token_ttl config not found." +# end +# +# with {:ok, _old_refresh, new_refresh} <- refresh(refresh_token, ttl: refresh_token_ttl) do +# {:ok, new_access, new_refresh} +# else +# _ -> {:error, @msg_err_invalid_token} +# end +# else +# _ -> {:error, @msg_err_invalid_token} +# end +# else +# _ -> {:error, @msg_err_invalid_token} +# end +# end + + def get_account_id_from_claims(claims) do + _account_code = + try do + sub = claims["sub"] + {:ok, sub} = Poison.decode(sub) + user_id = sub["account_id"] + rescue + _e in KeyError -> + Logger.debug("#{__MODULE__} conn.private.guardian_default_claims is not found. anonymus operation!") + raise BusinessError, message: "conn.private.guardian_default_claims is not found. anonymus operation!\rthis endpoint need Materia.AccountAuthPipeline. check your app's router.ex" + end + end + + def encode_and_sign_cb(resource, claims, options) do + encode_and_sign(resource, claims, options) + end + + def exchange_cb(old_token, from_type, to_type, options) do + exchange(old_token, from_type, to_type, options) + end + + def refresh_cb(refresh_token, options) do + refresh(refresh_token, options) + end + +end diff --git a/lib/materia/authenticator/authenticator.ex b/lib/materia/authenticator/authenticator_base.ex similarity index 55% rename from lib/materia/authenticator/authenticator.ex rename to lib/materia/authenticator/authenticator_base.ex index cae5d31..552a7f0 100644 --- a/lib/materia/authenticator/authenticator.ex +++ b/lib/materia/authenticator/authenticator_base.ex @@ -1,44 +1,29 @@ -defmodule Materia.Authenticator do +defmodule Materia.AuthenticatorBase do @moduledoc false - use Guardian, otp_app: :materia alias Materia.Errors.BusinessError alias Materia.Accounts.User require Logger - @msg_err_invalid_token "invalid token" - @msg_err_invalid_email_or_pass "email or password is invalid" - - def subject_for_token(resource, _claims) do - # You can use any value for the subject of your token but - # it should be useful in retrieving the resource later, see - # how it being used on `resource_from_claims/1` function. - # A unique `id` is a good subject, a non-unique email address - # is a poor subject. - Logger.debug("--- #{__MODULE__} subject_for_token --------------") - #sub = to_string(resource.id) - {:ok, sub} = Poison.encode(%{user_id: resource.id}) - end - - def subject_for_token(_, _) do - Logger.debug("--- #{__MODULE__} subject_for_token --------------") - {:error, :reason_for_error} - end - - def resource_from_claims(_claims = %{"email" => email}) do - # Here we'll look up our resource from the claims, the subject can be - # found in the `"sub"` key. In `above subject_for_token/2` we returned - # the resource id so here we'll rely on that to look it up. - Logger.debug("--- #{__MODULE__} resource_from_claims by email--------------") - resource = Materia.Accounts.get_user_by_email(email) - {:ok, resource} - end - - def resource_from_claims(_claims) do - Logger.debug("--- #{__MODULE__} resource_from_claims by other--------------") - {:error, :reason_for_error} - end + @callback encode_and_sign_cb(any(), Guardian.Token.claims(), List) :: {:ok, Guardian.Token.token(), Guardian.Token.claims()} | {:error, any()} + @callback exchange_cb( + Guardian.Token.token(), + String.t() | [String.t(), ...], + String.t(), + List + ) :: + {:ok, {Guardian.Token.token(), Guardian.Token.claims()}, + {Guardian.Token.token(), Guardian.Token.claims()}} + | {:error, any()} + + @callback refresh_cb( Guardian.Token.token(), List) :: + {:ok, {Guardian.Token.token(), Guardian.Token.claims()}, + {Guardian.Token.token(), Guardian.Token.claims()}} + | {:error, any()} + + defmacro __using__(_opts) do + quote do def after_encode_and_sign(resource, claims, token, _options) do Logger.debug("--- #{__MODULE__} after_encode_and_sign --------------") @@ -67,6 +52,8 @@ defmodule Materia.Authenticator do if user.status != User.status.activated do raise BusinessError, message: @msg_err_invalid_token end + "refresh" -> + Logger.debug("--- #{__MODULE__} claimes[type] == refresh --------------") _ -> raise BusinessError, message: @msg_err_invalid_token end @@ -94,112 +81,11 @@ defmodule Materia.Authenticator do end end - def sign_in(email, password) do - config = get_config() - access_token_ttl = config[:access_token_ttl] - Logger.debug("#{__MODULE__} --- sign_in access_token_ttl:#{inspect(access_token_ttl)}") - - if access_token_ttl == nil do - raise BusinessError, message: "materia.Authenticator access_token_ttl config not found." - end - - claims = %{"email" => email} - Logger.debug("#{__MODULE__} --- sign_in claims:#{inspect(claims)}") - - with {:ok, nil} <- resource_from_claims(claims) do - {:error, @msg_err_invalid_email_or_pass} - else - {:ok, resource} -> - if resource.status != Materia.Accounts.User.status().activated do - {:error, @msg_err_invalid_email_or_pass} - else - if Comeonin.Bcrypt.checkpw(password, resource.hashed_password) do - Logger.debug("#{__MODULE__} --- sign_in Comeonin.Bcrypt.checkpw == true") - with {:ok, access_token, _access_claims} <- - encode_and_sign(resource, claims, token_type: "access", ttl: access_token_ttl) do - Logger.debug("#{__MODULE__} --- sign_in Materia.Authenticator.encode_and_sign ok") - Logger.debug("#{__MODULE__} --- sign_in access_token:#{access_token}") - result = %{id: resource.id, access_token: access_token} - refresh_token_ttl = config[:refresh_token_ttl] - - Logger.debug( - "#{__MODULE__} --- sign_in refresh_token_ttl:#{inspect(refresh_token_ttl)}" - ) - - if refresh_token_ttl != nil do - refresh_token_ttl = config[:refresh_token_ttl] - - Logger.debug( - "#{__MODULE__} --- sign_in refresh_token_ttl:#{inspect(refresh_token_ttl)}" - ) - - with {:ok, refresh_token, _refresh_claims} <- - encode_and_sign( - resource, - claims, - token_type: "refresh", - ttl: refresh_token_ttl - ) do - result = Map.put(result, :refresh_token, refresh_token) - {:ok, result} - else - _ -> - {:error, @msg_err_invalid_email_or_pass} - end - else - {:ok, result} - end - else - _ -> - {:error, @msg_err_invalid_email_or_pass} - end - else - {:error, @msg_err_invalid_email_or_pass} - end - end - - _ -> - {:error, @msg_err_invalid_email_or_pass} - end - end - - def refresh_tokens(refresh_token) do - config = get_config() - access_token_ttl = config[:access_token_ttl] - Logger.debug("#{__MODULE__} --- refresh_tokens access_token_ttl:#{inspect(access_token_ttl)}") - - if access_token_ttl == nil do - raise BusinessError, message: "materia.Authenticator access_token_ttl config not found." - end - - with {:ok, _refresh, new_access} <- - exchange(refresh_token, "refresh", "access", ttl: access_token_ttl) do - {token, claims} = new_access - Logger.debug("#{__MODULE__} --- refresh_tokens new_access_token:#{inspect(token)}") - - with {:ok, _} <- Guardian.DB.after_encode_and_sign(%{}, "access", claims, token) do - refresh_token_ttl = config[:refresh_token_ttl] - - Logger.debug( - "#{__MODULE__} --- refresh_tokens refresh_token_ttl:#{inspect(refresh_token_ttl)}" - ) - - if refresh_token_ttl == nil do - raise BusinessError, - message: "materia.Authenticator refresh_token_ttl config not found." - end + def on_refresh({old_token, old_claims}, {new_token, new_claims}) do + Guardian.DB.on_revoke(old_claims, old_token) + Guardian.DB.after_encode_and_sign(%{}, new_claims["typ"], new_claims, new_token) - with {:ok, _old_refresh, new_refresh} <- refresh(refresh_token, ttl: refresh_token_ttl) do - {:ok, new_access, new_refresh} - else - _ -> {:error, @msg_err_invalid_token} - end - else - _ -> {:error, @msg_err_invalid_token} - end - else - _ -> {:error, @msg_err_invalid_token} - end + {:ok, {old_token, old_claims}, {new_token, new_claims}} end def get_user_registration_token(email) do @@ -217,6 +103,7 @@ defmodule Materia.Authenticator do end def get_password_reset_token(email) do + Logger.debug("#{__MODULE__} get_password_reset_token -------") config = get_config() password_reset_token_ttl = config[:password_reset_token_ttl] @@ -231,12 +118,22 @@ defmodule Materia.Authenticator do end def get_user_id_from_claims(claims) do - {:ok, sub} = Poison.decode(claims["sub"]) - user_id = sub["user_id"] + Logger.debug("#{__MODULE__} get_user_id_from_claims -------") + _user_id = + try do + sub = claims["sub"] + {:ok, sub} = Poison.decode(sub) + user_id = sub["user_id"] + rescue + _e in KeyError -> + Logger.debug("#{__MODULE__} conn.private.guardian_default_claims is not found. anonymus operation!") + raise BusinessError, message: "conn.private.guardian_default_claims is not found. anonymus operation!\rthis endpoint need Materia.UserAuthPipeline. check your app's router.ex" + end end @doc false - defp get_custom_token(email, token_type, token_ttl) do + def get_custom_token(email, token_type, token_ttl) do + Logger.debug("#{__MODULE__} get_custom_token -------") claims = %{"email" => email} Logger.debug("#{__MODULE__} --- get_custom_token start. claims:#{inspect(claims)}") @@ -245,7 +142,8 @@ defmodule Materia.Authenticator do else {:ok, resource} -> with {:ok, user_registration_token, _user_claims} <- - encode_and_sign( + #Materia.AuthenticatorBase.encode_and_sign_cb( + encode_and_sign_cb( resource, claims, token_type: token_type, @@ -265,7 +163,49 @@ defmodule Materia.Authenticator do end - defp get_config() do + def refresh_tokens(refresh_token) do + Logger.debug("#{__MODULE__} refresh_tokens -------") + config = get_config() + access_token_ttl = config[:access_token_ttl] + Logger.debug("#{__MODULE__} --- refresh_tokens access_token_ttl:#{inspect(access_token_ttl)}") + + if access_token_ttl == nil do + raise BusinessError, message: "materia.Authenticator access_token_ttl config not found." + end + + with {:ok, _refresh, new_access} <- + #Materia.AuthenticatorBase.exchange_cb(refresh_token, "refresh", "access", ttl: access_token_ttl) do + exchange_cb(refresh_token, "refresh", "access", ttl: access_token_ttl) do + {token, claims} = new_access + Logger.debug("#{__MODULE__} --- refresh_tokens new_access_token:#{inspect(token)}") + + with {:ok, _} <- Guardian.DB.after_encode_and_sign(%{}, "access", claims, token) do + refresh_token_ttl = config[:refresh_token_ttl] + + Logger.debug( + "#{__MODULE__} --- refresh_tokens refresh_token_ttl:#{inspect(refresh_token_ttl)}" + ) + + if refresh_token_ttl == nil do + raise BusinessError, + message: "materia.Authenticator refresh_token_ttl config not found." + end + + with {:ok, _old_refresh, new_refresh} <- refresh_cb(refresh_token, ttl: refresh_token_ttl) do + {:ok, new_access, new_refresh} + else + _ -> {:error, @msg_err_invalid_token} + end + else + _ -> {:error, @msg_err_invalid_token} + end + else + _ -> {:error, @msg_err_invalid_token} + end + end + + def get_config() do + Logger.debug("#{__MODULE__} get_config -------") config = Application.get_env(:materia, Materia.Authenticator) if config == nil do @@ -275,11 +215,7 @@ defmodule Materia.Authenticator do end end - defp on_refresh({old_token, old_claims}, {new_token, new_claims}) do - Guardian.DB.on_revoke(old_claims, old_token) - Guardian.DB.after_encode_and_sign(%{}, new_claims["typ"], new_claims, new_token) - - {:ok, {old_token, old_claims}, {new_token, new_claims}} - end +end +end end diff --git a/lib/materia/authenticator/password_reset_auth_pipline.ex b/lib/materia/authenticator/password_reset_auth_pipline.ex index 20f5468..570b0e9 100644 --- a/lib/materia/authenticator/password_reset_auth_pipline.ex +++ b/lib/materia/authenticator/password_reset_auth_pipline.ex @@ -1,7 +1,7 @@ defmodule Materia.PasswordResetAuthPipeline do @moduledoc false use Guardian.Plug.Pipeline, otp_app: :materia, - module: Materia.Authenticator, + module: Materia.UserAuthenticator, error_handler: Materia.AuthenticateErrorHandler plug Guardian.Plug.VerifySession, claims: %{"typ" => "password_reset"} plug Guardian.Plug.VerifyHeader, claims: %{"typ" => "password_reset"} diff --git a/lib/materia/authenticator/authenticate_pipline.ex b/lib/materia/authenticator/user_auth_pipline.ex similarity index 82% rename from lib/materia/authenticator/authenticate_pipline.ex rename to lib/materia/authenticator/user_auth_pipline.ex index 73aa847..7ba0a5c 100644 --- a/lib/materia/authenticator/authenticate_pipline.ex +++ b/lib/materia/authenticator/user_auth_pipline.ex @@ -1,7 +1,7 @@ -defmodule Materia.AuthenticatePipeline do +defmodule Materia.UserAuthPipeline do @moduledoc false use Guardian.Plug.Pipeline, otp_app: :materia, - module: Materia.Authenticator, + module: Materia.UserAuthenticator, error_handler: Materia.AuthenticateErrorHandler plug Guardian.Plug.VerifySession, claims: %{"typ" => "access"} #plug Materia.Plug.Debug diff --git a/lib/materia/authenticator/user_authenticator.ex b/lib/materia/authenticator/user_authenticator.ex new file mode 100644 index 0000000..1f21729 --- /dev/null +++ b/lib/materia/authenticator/user_authenticator.ex @@ -0,0 +1,129 @@ +defmodule Materia.UserAuthenticator do + @moduledoc false + + require Logger + + use Guardian, otp_app: :materia + + use Materia.AuthenticatorBase + + alias Materia.Errors.BusinessError + + @behaviour Materia.AuthenticatorBase + + @msg_err_invalid_token "invalid token" + @msg_err_invalid_email_or_pass "email or password is invalid" + + def subject_for_token(resource, _claims) do + # You can use any value for the subject of your token but + # it should be useful in retrieving the resource later, see + # how it being used on `resource_from_claims/1` function. + # A unique `id` is a good subject, a non-unique email address + # is a poor subject. + Logger.debug("--- #{__MODULE__} subject_for_token --------------") + #sub = to_string(resource.id) + {:ok, sub} = Poison.encode(%{user_id: resource.id}) + {:ok, sub} + end + + def subject_for_token(_, _) do + Logger.debug("--- #{__MODULE__} subject_for_token reason_for_error--------------") + {:error, :reason_for_error} + end + + def resource_from_claims(_claims = %{"email" => email}) do + # Here we'll look up our resource from the claims, the subject can be + # found in the `"sub"` key. In `above subject_for_token/2` we returned + # the resource id so here we'll rely on that to look it up. + Logger.debug("--- #{__MODULE__} resource_from_claims by email--------------") + resource = Materia.Accounts.get_user_by_email(email) + {:ok, resource} + end + + def resource_from_claims(_claims) do + Logger.debug("--- #{__MODULE__} resource_from_claims by other--------------") + {:error, :reason_for_error} + end + + def sign_in(email, password) do + config = get_config() + access_token_ttl = config[:access_token_ttl] + Logger.debug("#{__MODULE__} --- sign_in access_token_ttl:#{inspect(access_token_ttl)}") + + if access_token_ttl == nil do + raise BusinessError, message: "materia.Authenticator access_token_ttl config not found." + end + + claims = %{"email" => email} + Logger.debug("#{__MODULE__} --- sign_in claims:#{inspect(claims)}") + + with {:ok, nil} <- resource_from_claims(claims) do + {:error, @msg_err_invalid_email_or_pass} + else + {:ok, resource} -> + if resource.status != Materia.Accounts.User.status().activated do + {:error, @msg_err_invalid_email_or_pass} + else + if Comeonin.Bcrypt.checkpw(password, resource.hashed_password) do + Logger.debug("#{__MODULE__} --- sign_in Comeonin.Bcrypt.checkpw == true") + with {:ok, access_token, _access_claims} <- + encode_and_sign_cb(resource, claims, token_type: "access", ttl: access_token_ttl) do + Logger.debug("#{__MODULE__} --- sign_in Materia.Authenticator.encode_and_sign ok") + Logger.debug("#{__MODULE__} --- sign_in access_token:#{access_token}") + result = %{id: resource.id, access_token: access_token} + refresh_token_ttl = config[:refresh_token_ttl] + + Logger.debug( + "#{__MODULE__} --- sign_in refresh_token_ttl:#{inspect(refresh_token_ttl)}" + ) + + if refresh_token_ttl != nil do + refresh_token_ttl = config[:refresh_token_ttl] + + Logger.debug( + "#{__MODULE__} --- sign_in refresh_token_ttl:#{inspect(refresh_token_ttl)}" + ) + + with {:ok, refresh_token, _refresh_claims} <- + encode_and_sign( + resource, + claims, + token_type: "refresh", + ttl: refresh_token_ttl + ) do + result = Map.put(result, :refresh_token, refresh_token) + {:ok, result} + else + _ -> + {:error, @msg_err_invalid_email_or_pass} + end + else + {:ok, result} + end + else + _ -> + {:error, @msg_err_invalid_email_or_pass} + end + else + {:error, @msg_err_invalid_email_or_pass} + end + end + + _ -> + {:error, @msg_err_invalid_email_or_pass} + end + end + + def encode_and_sign_cb(resource, claims, options) do + encode_and_sign(resource, claims, options) + end + + def exchange_cb(old_token, from_type, to_type, options) do + exchange(old_token, from_type, to_type, options) + end + + def refresh_cb(refresh_token, options) do + refresh(refresh_token, options) + end + +end diff --git a/lib/materia/authenticator/user_registration_auth_pipline.ex b/lib/materia/authenticator/user_registration_auth_pipline.ex index bca2a8d..41450bf 100644 --- a/lib/materia/authenticator/user_registration_auth_pipline.ex +++ b/lib/materia/authenticator/user_registration_auth_pipline.ex @@ -1,7 +1,7 @@ defmodule Materia.UserRegistrationAuthPipeline do @moduledoc false use Guardian.Plug.Pipeline, otp_app: :materia, - module: Materia.Authenticator, + module: Materia.UserAuthenticator, error_handler: Materia.AuthenticateErrorHandler plug Guardian.Plug.VerifySession, claims: %{"typ" => "user_registration"} #plug Materia.Plug.Debug diff --git a/lib/materia/organizations/organizations.ex b/lib/materia/organizations/organizations.ex index 006f8c0..2ea627d 100644 --- a/lib/materia/organizations/organizations.ex +++ b/lib/materia/organizations/organizations.ex @@ -279,11 +279,11 @@ defmodule Materia.Organizations do ## Examples ``` - iex(1)> organization = Materia.Organizations.get_organization!(1) + iex(1)> {:ok, organization} = Materia.Organizations.create_organization(%{name: "test_delete_organiztion_001"}) iex(2)> {:ok, organization} = Materia.Organizations.delete_organization(organization) iex(3)> organizations = Materia.Organizations.list_organizations() - iex(4)> MateriaWeb.OrganizationView.render("index.json", %{organizations: organizations}) - [] + iex(4)> MateriaWeb.OrganizationView.render("index.json", %{organizations: organizations}) |> length() + 1 ``` """ diff --git a/lib/materia_web/controller_base.ex b/lib/materia_web/controller_base.ex index 8a33375..f109c87 100644 --- a/lib/materia_web/controller_base.ex +++ b/lib/materia_web/controller_base.ex @@ -3,19 +3,34 @@ defmodule MateriaWeb.ControllerBase do alias Ecto.Multi alias Materia.Errors.BusinessError + alias Materia.UserAuthenticator + alias Materia.AccountAuthenticator + require Logger def get_user_id(conn) do _user_id = + try do + clames = conn.private.guardian_default_claims + UserAuthenticator.get_user_id_from_claims(clames) + rescue + _e in KeyError -> + Logger.debug("#{__MODULE__} conn.private.guardian_default_claims is not found. anonymus operation!") + raise BusinessError, message: "conn.private.guardian_default_claims is not found. anonymus operation!\rthis endpoint need Materia.UserAuthPipeline. check your app's router.ex" + end + end + + def get_account_id(conn) do + _account_code = try do - sub = conn.private.guardian_default_claims["sub"] - {:ok, sub_map} = Poison.decode(sub) - sub_map["user_id"] + clames = conn.private.guardian_default_claims + AccountAuthenticator.get_account_id_from_claims(clames) rescue _e in KeyError -> - Logger.debug("#{__MODULE__} conn.private.guardian_default_claims[\"sub\"] is not found. anonymus operation!") - raise BusinessError, message: "conn.private.guardian_default_claims[\"sub\"] is not found. anonymus operation!\rthis endpoint need Materia.AuthenticatePipeline. check your app's router.ex" + Logger.debug("#{__MODULE__} conn.private.guardian_default_claims is not found. anonymus operation!") + raise BusinessError, message: "conn.private.guardian_default_claims is not found. anonymus operation!\rthis endpoint need Materia.AccountAuthPipeline. check your app's router.ex" end + end def transaction_flow(conn, controler_atom, module, function_atom, attr \\ [%{}]) do diff --git a/lib/materia_web/controllers/account_controller.ex b/lib/materia_web/controllers/account_controller.ex new file mode 100644 index 0000000..a54cb00 --- /dev/null +++ b/lib/materia_web/controllers/account_controller.ex @@ -0,0 +1,53 @@ +defmodule MateriaWeb.AccountController do + use MateriaWeb, :controller + + alias Materia.Accounts + alias Materia.Accounts.Account + alias MateriaWeb.ControllerBase + + action_fallback MateriaWeb.FallbackController + + def index(conn, _params) do + accounts = Accounts.list_accounts() + render(conn, "index.json", accounts: accounts) + end + + def list_accounts_by_params(conn, params) do + accounts = Accounts.list_accounts_by_params(params) + render(conn, "index.json", accounts: accounts) + end + + def create(conn, account_params) do + with {:ok, %Account{} = account} <-Accounts.create_account(account_params) do + conn + |> put_status(:created) + |> put_resp_header("location", account_path(conn, :show, account)) + |> render("show.json", account: account) + end + end + + def show(conn, %{"id" => id}) do + account = Accounts.get_account!(id) + render(conn, "show.json", account: account) + end + + def show_my_account(conn, _params) do + id = ControllerBase.get_account_id(conn) + account = Accounts.get_account!(id) + render(conn, "show.json", account: account) + end + + def update(conn, account_params) do + account = Accounts.get_account!(account_params["id"]) + with {:ok, %Account{} = account} <- Accounts.update_account(account, account_params) do + render(conn, "show.json", account: account) + end + end + + def delete(conn, %{"id" => id}) do + account = Accounts.get_account!(id) + with {:ok, %Account{}} <- Accounts.delete_account(account) do + send_resp(conn, :no_content, "") + end + end +end diff --git a/lib/materia_web/controllers/authenticator_controller.ex b/lib/materia_web/controllers/authenticator_controller.ex index 3bd5df8..cb4ab0e 100644 --- a/lib/materia_web/controllers/authenticator_controller.ex +++ b/lib/materia_web/controllers/authenticator_controller.ex @@ -8,6 +8,8 @@ defmodule MateriaWeb.AuthenticatorController do #alias Materia.Accounts #alias Materia.Accounts.User + action_fallback MateriaWeb.FallbackController + require Logger #def sign_in(conn, _params = %{ "email" => email, "password" => password }) do @@ -37,11 +39,30 @@ defmodule MateriaWeb.AuthenticatorController do # end # #end + def sign_in(conn, _params = %{ "account" => account, "email" => email, "password" => password }) do + Logger.debug("--- MateriaWeb.AuthenticateController sign_in with account-----------------") + + with {:ok, result} <- Materia.AccountAuthenticator.sign_in(account, email, password) do + authenticator = + if Map.has_key?(result, :refresh_token) do + %{id: result.id ,access_token: result.access_token, refresh_token: result.refresh_token} + else + %{id: result.id ,access_token: result.access_token} + end + conn + |> put_status(:created) + |> render("show.json", authenticator: authenticator) + else + {:error, message} -> + handle_unauthenticated(conn, message) + end + + end def sign_in(conn, _params = %{ "email" => email, "password" => password }) do - Logger.debug("--- MateriaWeb.AuthenticateController sign_in_with_refresh-----------------") + Logger.debug("--- MateriaWeb.AuthenticateController sign_in-----------------") - with {:ok, result} <- Materia.Authenticator.sign_in(email, password) do + with {:ok, result} <- Materia.UserAuthenticator.sign_in(email, password) do authenticator = if Map.has_key?(result, :refresh_token) do %{id: result.id ,access_token: result.access_token, refresh_token: result.refresh_token} @@ -59,12 +80,14 @@ defmodule MateriaWeb.AuthenticatorController do end def refresh(conn, _params = %{ "refresh_token" => refresh_token}) do - with {:ok, access, _refresh } <- Materia.Authenticator.refresh_tokens(refresh_token) do + with {:ok, access, _refresh } <- Materia.UserAuthenticator.refresh_tokens(refresh_token) do {access_token, _access_claims} = access {refresh_token, refresh_claims} = access + {:ok, sub} = Poison.decode(refresh_claims["sub"]) + IO.inspect(sub) conn |> put_status(:created) - |> render("show.json", authenticator: %{id: refresh_claims["sub"] ,access_token: access_token, refresh_token: refresh_token}) + |> render("show.json", authenticator: %{id: sub["user_id"] ,access_token: access_token, refresh_token: refresh_token}) else {:error, message} -> handle_unauthenticated(conn, message) @@ -74,7 +97,7 @@ defmodule MateriaWeb.AuthenticatorController do def sign_out(conn, _params) do Logger.debug("--- MateriaWeb.AuthenticateController sign_out-----------------") token = conn.private[:guardian_default_token] - with {:ok, _claims} = Materia.Authenticator.revoke(token) do + with {:ok, _claims} = Materia.UserAuthenticator.revoke(token) do conn |> put_status(200) |> render("delete.json", []) diff --git a/lib/materia_web/controllers/user_controller.ex b/lib/materia_web/controllers/user_controller.ex index 7c60f15..44b1d40 100644 --- a/lib/materia_web/controllers/user_controller.ex +++ b/lib/materia_web/controllers/user_controller.ex @@ -36,7 +36,6 @@ defmodule MateriaWeb.UserController do def show_me(conn, _params) do Logger.debug("show_me") - #id = String.to_integer(conn.private.guardian_default_claims["sub"]) id = ControllerBase.get_user_id(conn) user = Accounts.get_user!(id) render(conn, "show.json", user: user) @@ -57,14 +56,14 @@ defmodule MateriaWeb.UserController do end end - def send_verify_mail(conn, %{"id" => id}) do - # メールアドレス検証用のメール送信 - user = Accounts.get_user!(id) - - with {:ok, _result} = Accounts.send_verify_mail(user) do - send_resp(conn, :no_content, "") - end - end + #def send_verify_mail(conn, %{"id" => id}) do + # # メールアドレス検証用のメール送信 + # user = Accounts.get_user!(id) +# + # with {:ok, _result} = Accounts.send_verify_mail(user) do + # send_resp(conn, :no_content, "") + # end + #end def registration_tmp_user(conn, %{"email" => email, "role" => role}) do MateriaWeb.ControllerBase.transaction_flow(conn, :tmp_user, Materia.Accounts, :regster_tmp_user, [email, role]) @@ -77,7 +76,7 @@ defmodule MateriaWeb.UserController do conn = MateriaWeb.ControllerBase.transaction_flow(conn, :user, Materia.Accounts, :registration_user, [user, params]) if Map.has_key?(conn, :private) do token = conn.private.guardian_default_token - Materia.Authenticator.revoke(token) + Materia.UserAuthenticator.revoke(token) end conn end @@ -88,7 +87,7 @@ defmodule MateriaWeb.UserController do user = Accounts.get_user!(id) conn = MateriaWeb.ControllerBase.transaction_flow(conn, :user_token, Materia.Accounts, :registration_user_and_sign_in, [user, params]) token = conn.private.guardian_default_token - Materia.Authenticator.revoke(token) + Materia.UserAuthenticator.revoke(token) conn end @@ -102,7 +101,7 @@ defmodule MateriaWeb.UserController do user = Accounts.get_user!(id) conn = MateriaWeb.ControllerBase.transaction_flow(conn, :user, Materia.Accounts, :reset_my_password, [user, password]) token = conn.private.guardian_default_token - Materia.Authenticator.revoke(token) + Materia.UserAuthenticator.revoke(token) conn end diff --git a/lib/materia_web/router.ex b/lib/materia_web/router.ex index 10d40d7..54a1124 100644 --- a/lib/materia_web/router.ex +++ b/lib/materia_web/router.ex @@ -14,7 +14,11 @@ defmodule MateriaWeb.Router do end pipeline :guardian_auth do - plug Materia.AuthenticatePipeline + plug Materia.UserAuthPipeline + end + + pipeline :guardian_auth_acount do + plug Materia.AccountAuthPipeline end pipeline :tmp_user_auth do @@ -67,7 +71,15 @@ defmodule MateriaWeb.Router do post "search-users", UserController, :list_users_by_params resources "/addresses", AddressController, except: [:new, :edit] post "create-my-addres", AddressController, :create_my_address + resources "/accounts", AccountController, except: [:new, :edit] + post "search-accounts", AccountController, :list_accounts_by_params + + end + + scope "/api", MateriaWeb do + pipe_through [ :api, :guardian_auth_acount] + get "/my-account", AccountController, :show_my_account end scope "/api/ops", MateriaWeb do diff --git a/lib/materia_web/views/account_view.ex b/lib/materia_web/views/account_view.ex new file mode 100644 index 0000000..6da73ca --- /dev/null +++ b/lib/materia_web/views/account_view.ex @@ -0,0 +1,39 @@ +defmodule MateriaWeb.AccountView do + use MateriaWeb, :view + alias MateriaWeb.AccountView + + alias MateriaWeb.OrganizationView + alias MateriaWeb.UserView + + def render("index.json", %{accounts: accounts}) do + render_many(accounts, AccountView, "account.json") + end + + def render("show.json", %{account: account}) do + render_one(account, AccountView, "account.json") + end + + def render("account.json", %{account: account}) do + result_map = %{id: account.id, + external_code: account.external_code, + name: account.name, + start_datetime: account.start_datetime, + frozen_datetime: account.frozen_datetime, + expired_datetime: account.expired_datetime, + descriptions: account.descriptions, + status: account.status, + lock_version: account.lock_version} + result_map = + if Map.has_key?(account, :main_user) && Ecto.assoc_loaded?(account.main_user) do + Map.put(result_map, :main_user, UserView.render("show.json", %{user: account.main_user})) + else + Map.put(result_map, :main_user, nil) + end + result_map = + if Map.has_key?(account, :organization) && Ecto.assoc_loaded?(account.organization) do + Map.put(result_map, :organization, OrganizationView.render("show.json", %{organization: account.organization})) + else + Map.put(result_map, :organization, nil) + end + end +end diff --git a/lib/mix/templates/6_create_accounts.exs b/lib/mix/templates/6_create_accounts.exs new file mode 100644 index 0000000..50092e5 --- /dev/null +++ b/lib/mix/templates/6_create_accounts.exs @@ -0,0 +1,25 @@ +defmodule Materia.Repo.Migrations.CreateAccounts do + use Ecto.Migration + + def change do + create table(:accounts) do + add :external_code, :string + add :name, :string + add :start_datetime, :utc_datetime + add :frozen_datetime, :utc_datetime + add :expired_datetime, :utc_datetime + add :descriptions, :string, size: 1000 + add :organization_id, references(:organizations, on_delete: :nothing) + add :main_user_id, references(:users, on_delete: :nothing) + add :status, :integer + add :lock_version, :bigint + + timestamps() + end + + create unique_index(:accounts, [:external_code]) + create index(:accounts, [:name]) + create index(:accounts, [:organization_id]) + create index(:accounts, [:status, :start_datetime]) + end +end diff --git a/priv/repo/migrations/20181211023323_materia_1_craete_users.exs b/priv/repo/migrations/20190110020429_materia_1_craete_users.exs similarity index 100% rename from priv/repo/migrations/20181211023323_materia_1_craete_users.exs rename to priv/repo/migrations/20190110020429_materia_1_craete_users.exs diff --git a/priv/repo/migrations/20181211023324_materia_2_craete_organizations.exs b/priv/repo/migrations/20190110020430_materia_2_craete_organizations.exs similarity index 100% rename from priv/repo/migrations/20181211023324_materia_2_craete_organizations.exs rename to priv/repo/migrations/20190110020430_materia_2_craete_organizations.exs diff --git a/priv/repo/migrations/20181211023325_materia_3_craete_address.exs b/priv/repo/migrations/20190110020431_materia_3_craete_address.exs similarity index 100% rename from priv/repo/migrations/20181211023325_materia_3_craete_address.exs rename to priv/repo/migrations/20190110020431_materia_3_craete_address.exs diff --git a/priv/repo/migrations/20181211023326_materia_4_craete_grants.exs b/priv/repo/migrations/20190110020432_materia_4_craete_grants.exs similarity index 100% rename from priv/repo/migrations/20181211023326_materia_4_craete_grants.exs rename to priv/repo/migrations/20190110020432_materia_4_craete_grants.exs diff --git a/priv/repo/migrations/20181211023327_materia_5_craete_mail_templates.exs b/priv/repo/migrations/20190110020433_materia_5_craete_mail_templates.exs similarity index 100% rename from priv/repo/migrations/20181211023327_materia_5_craete_mail_templates.exs rename to priv/repo/migrations/20190110020433_materia_5_craete_mail_templates.exs diff --git a/priv/repo/migrations/20190110020434_materia_6_create_accounts.exs b/priv/repo/migrations/20190110020434_materia_6_create_accounts.exs new file mode 100644 index 0000000..50092e5 --- /dev/null +++ b/priv/repo/migrations/20190110020434_materia_6_create_accounts.exs @@ -0,0 +1,25 @@ +defmodule Materia.Repo.Migrations.CreateAccounts do + use Ecto.Migration + + def change do + create table(:accounts) do + add :external_code, :string + add :name, :string + add :start_datetime, :utc_datetime + add :frozen_datetime, :utc_datetime + add :expired_datetime, :utc_datetime + add :descriptions, :string, size: 1000 + add :organization_id, references(:organizations, on_delete: :nothing) + add :main_user_id, references(:users, on_delete: :nothing) + add :status, :integer + add :lock_version, :bigint + + timestamps() + end + + create unique_index(:accounts, [:external_code]) + create index(:accounts, [:name]) + create index(:accounts, [:organization_id]) + create index(:accounts, [:status, :start_datetime]) + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index af4bf0b..594a03d 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -31,6 +31,8 @@ Accounts.update_user(user_hogehoge, %{organization_id: organization_hogehoge.id} Locations.create_address(%{organization_id: organization_hogehoge.id, subject: "registry", location: "福岡県", zip_code: "810-ZZZZ", address1: "福岡市中央区", address2: "天神 x-x-xx"}) Locations.create_address(%{organization_id: organization_hogehoge.id, subject: "branch", location: "福岡県", zip_code: "812-ZZZZ", address1: "北九州市小倉北区", address2: "浅野 x-x-xx"}) +Accounts.create_account(%{"name" => "hogehoge account", "start_datetime" => "2019-01-10T10:03:50.293740Z", "main_user_id" => user_hogehoge.id, "organization_id" => organization_hogehoge.id}) + alias Materia.Mails Mails.create_mail_template(%{ mail_template_type: "user_registration_request", subject: "【注意!登録は完了していません】{!email}様 本登録のご案内", body: "{!email}様\nこの度は当サービスへ仮登録をいただき誠にありがとうございます。\n\n本登録のご案内をいたます。\n\n下記URLのリンクをクリックし、必要情報を入力の上、30分以内に本登録操作の完了をお願いいたします。\n操作完了後\"【本登録完了しました】\"のタイトルのメールが届きましたら本登録完了となります。\n\n https://{!user_registration_url}?param=!{user_regstration_token} \n\n------------------------------\nカラビナテクノロジー株式会社\n〒810-0001 \n福岡市中央区天神1-2-4 農業共済ビル2F\n------------------------------" }) diff --git a/test/materia_web/controllers/account_controller_test.exs b/test/materia_web/controllers/account_controller_test.exs new file mode 100644 index 0000000..371f4f8 --- /dev/null +++ b/test/materia_web/controllers/account_controller_test.exs @@ -0,0 +1,113 @@ +defmodule MateriaWeb.AccountControllerTest do + use MateriaWeb.ConnCase + + alias Materia.Accounts + alias Materia.Accounts.Account + + @admin_user_attrs %{ + account: "hogehoge account", + email: "hogehoge@example.com", + password: "hogehoge", + } + + setup %{conn: conn} do + {:ok, conn: put_req_header(conn, "accept", "application/json")} + end + + # :location, :zip_code, :address1, :address2, :latitude, :longitude, :user_id, :organization_id, :subject, :lock_version] + + describe "Account REST API" do + test "CRUD test", %{conn: conn} do + # オペレーターログイン + token_conn = post(conn, authenticator_path(conn, :sign_in), @admin_user_attrs) + resp_sgin_in = json_response(token_conn, 201) + %{"access_token" => token} = resp_sgin_in + %{"id" => user_id} = resp_sgin_in + conn_auth = put_req_header(conn, "authorization", "Bearer " <> token) + + # 一覧紹介 + conn_index0 = get(conn_auth, account_path(conn, :index)) + resp_index0 = json_response(conn_index0, 200) + assert length(resp_index0) == 1 + + # 登録 + conn_create = + post(conn_auth, account_path(conn, :create), %{ + "external_code" => "some external_code", + "name" => "some name", + "descriptions" => "some descriptions", + "organization" => 1, + "main_user_id" => 1 + }) + resp_create = json_response(conn_create, 201) + + # 照会 + conn_show = get(conn_auth, account_path(conn, :show, resp_create["id"])) + + resp_show = json_response(conn_show, 200) + assert Map.delete(resp_show, "id") |> Map.delete("start_datetime") == %{ + "descriptions" => "some descriptions", + "expired_datetime" => nil, + "external_code" => "some external_code", + "frozen_datetime" => nil, + "lock_version" => 0, + "main_user" => nil, + "name" => "some name", + "organization" => nil, + "status" => 1 + } + assert resp_create["start_datetime"] != nil + + # 汎用検索 + conn_show1 = + post(conn_auth, account_path(conn, :list_accounts_by_params), %{ + "and" => [ %{"external_code" => "some external_code"}] + }) + resp_show1 = json_response(conn_show1, 200) + assert length(resp_show1) == 1 + + #所属組織のアカウントを取得 + conn_show2 = get(conn_auth, account_path(conn, :show_my_account)) + resp_show2 = json_response(conn_show2, 200) + assert resp_show2["name"] == "hogehoge account" + + # アカウント凍結 + conn_update = + put(conn_auth, account_path(conn, :update, resp_create["id"]), %{ + "external_code" => "updated external_code", + "name" => "updated name", + "descriptions" => "updated descriptions", + "status" => Account.status.frozen + }) + + resp_update = json_response(conn_update, 200) + + assert resp_update["frozen_datetime"] != nil + + # アカウント閉鎖 + conn_update2 = + put(conn_auth, account_path(conn, :update, resp_create["id"]), %{ + "status" => Account.status.expired + }) + + resp_update2 = json_response(conn_update2, 200) + + assert resp_update2["expired_datetime"] != nil + + # 削除 + conn_del = delete(conn_auth, account_path(conn, :delete, resp_create["id"])) + resp_del = response(conn_del, 204) + + # 汎用検索 + conn_show4 = + post(conn_auth, account_path(conn, :list_accounts_by_params), %{ + "and" => [ %{"external_code" => "updated external_code"}] + }) + resp_show4 = json_response(conn_show4, 200) + assert resp_show4 == [] + assert length(resp_show4) == 0 + + end + + end +end diff --git a/test/materia_web/controllers/authenticator_conrtoller_test.exs b/test/materia_web/controllers/authenticator_conrtoller_test.exs new file mode 100644 index 0000000..f7a8ff4 --- /dev/null +++ b/test/materia_web/controllers/authenticator_conrtoller_test.exs @@ -0,0 +1,209 @@ +defmodule MateriaWeb.AuthenticatorControllerTest do + use MateriaWeb.ConnCase + + alias Materia.Accounts + + setup %{conn: conn} do + {:ok, conn: put_req_header(conn, "accept", "application/json")} + end + + describe "sign-in by user " do + test "sign-in life cycle by user", %{conn: conn} do + + attr = %{ + "email" => "fugafuga@example.com", + "password" => "fugafuga", + } + token_conn = post(conn, authenticator_path(conn, :sign_in), attr) + resp_token = json_response(token_conn, 201) + access_token = resp_token["access_token"] + conn_auth = put_req_header(conn, "authorization", "Bearer " <> access_token) + + conn_show_me = get(conn_auth, user_path(conn, :show_me)) + resp_show_me = json_response(conn_show_me, 200) + assert resp_show_me == %{ + "addresses" => [], + "back_ground_img_url" => nil, + "descriptions" => nil, + "email" => "fugafuga@example.com", + "external_user_id" => nil, + "icon_img_url" => nil, + "id" => 2, + "lock_version" => 1, + "name" => "fugafuga", + "organization" => nil, + "phone_number" => nil, + "role" => "operator", + "status" => 1 + } + + conn_valid = get(conn_auth, authenticator_path(conn, :is_authenticated)) + resp_valid = response(conn_valid, 200) + assert resp_valid == "{\"message\":\"authenticated\"}" + + # refresh token + refresh_token = resp_token["refresh_token"] + conn_refresh = post(conn, authenticator_path(conn, :refresh), %{"refresh_token" => refresh_token}) + resp_refresh = json_response(conn_refresh, 201) + assert resp_refresh["access_token"] != access_token + + conn_auth2 = put_req_header(conn, "authorization", "Bearer " <> access_token) + + conn_show_me2 = get(conn_auth2, user_path(conn, :show_me)) + resp_show_me2 = json_response(conn_show_me2, 200) + assert resp_show_me2 == %{ + "addresses" => [], + "back_ground_img_url" => nil, + "descriptions" => nil, + "email" => "fugafuga@example.com", + "external_user_id" => nil, + "icon_img_url" => nil, + "id" => 2, + "lock_version" => 1, + "name" => "fugafuga", + "organization" => nil, + "phone_number" => nil, + "role" => "operator", + "status" => 1 + } + + # sign-out + conn_sign_out = post(conn_auth2, authenticator_path(conn, :sign_out)) + resp_sing_out = response(conn_sign_out, 200) + assert resp_sing_out == "{\"ok\":true}" + + conn_valid3 = get(conn_auth2, authenticator_path(conn, :is_authenticated)) + resp_valid3 = response(conn_valid3, 401) + assert resp_valid3 == "{\"message\":\"invalid_token\"}" + + + end + end + + describe "sign-in by account and user" do + test "sign-in life cycle by account and user", %{conn: conn} do + + attr = %{ + "account" => "hogehoge account", + "email" => "hogehoge@example.com", + "password" => "hogehoge", + } + token_conn = post(conn, authenticator_path(conn, :sign_in), attr) + resp_token = json_response(token_conn, 201) + access_token = resp_token["access_token"] + conn_auth = put_req_header(conn, "authorization", "Bearer " <> access_token) + + conn_show_me = get(conn_auth, user_path(conn, :show_me)) + resp_show_me = json_response(conn_show_me, 200) + assert resp_show_me == %{ + "addresses" => [ + %{ + "address1" => "福岡市中央区", + "address2" => "大名 x-x-xx", + "id" => 2, + "latitude" => nil, + "location" => "福岡県", + "lock_version" => 0, + "longitude" => nil, + "organization" => [], + "subject" => "billing", + "user" => [], + "zip_code" => "810-ZZZZ" + }, + %{ + "address1" => "福岡市中央区", + "address2" => "港 x-x-xx", + "id" => 1, + "latitude" => nil, + "location" => "福岡県", + "lock_version" => 0, + "longitude" => nil, + "organization" => [], + "subject" => "living", + "user" => [], + "zip_code" => "810-ZZZZ" + } + ], + "back_ground_img_url" => nil, + "descriptions" => nil, + "email" => "hogehoge@example.com", + "external_user_id" => nil, + "icon_img_url" => nil, + "id" => 1, + "lock_version" => 2, + "name" => "hogehoge", + "organization" => %{ + "addresses" => [], + "back_ground_img_url" => "https://hogehoge.com/ib_img.jpg", + "hp_url" => "https://hogehoge.inc", + "id" => 1, + "lock_version" => 1, + "name" => "hogehoge.inc", + "one_line_message" => "let's do this.", + "phone_number" => nil, + "profile_img_url" => "https://hogehoge.com/prof_img.jpg", + "status" => 1, + "users" => [] + }, + "phone_number" => nil, + "role" => "admin", + "status" => 1 + } + + conn_show_my_account = get(conn_auth, account_path(conn, :show_my_account)) + resp_show_my_account = json_response(conn_show_my_account, 200) + assert resp_show_my_account == %{ + "descriptions" => nil, + "expired_datetime" => nil, + "external_code" => nil, + "frozen_datetime" => nil, + "id" => 1, + "lock_version" => 0, + "main_user" => nil, + "name" => "hogehoge account", + "organization" => nil, + "start_datetime" => "2019-01-10T10:03:50.293740Z", + "status" => 1 + } + + conn_valid = get(conn_auth, authenticator_path(conn, :is_authenticated)) + resp_valid = response(conn_valid, 200) + assert resp_valid == "{\"message\":\"authenticated\"}" + + # refresh token + refresh_token = resp_token["refresh_token"] + conn_refresh = post(conn, authenticator_path(conn, :refresh), %{"refresh_token" => refresh_token}) + resp_refresh = json_response(conn_refresh, 201) + assert resp_refresh["access_token"] != access_token + + conn_auth2 = put_req_header(conn, "authorization", "Bearer " <> access_token) + + conn_show_my_account2 = get(conn_auth2, account_path(conn, :show_my_account)) + resp_show_my_account2 = json_response(conn_show_my_account2, 200) + assert resp_show_my_account2 == %{ + "descriptions" => nil, + "expired_datetime" => nil, + "external_code" => nil, + "frozen_datetime" => nil, + "id" => 1, + "lock_version" => 0, + "main_user" => nil, + "name" => "hogehoge account", + "organization" => nil, + "start_datetime" => "2019-01-10T10:03:50.293740Z", + "status" => 1 + } + + # sign-out + conn_sign_out = post(conn_auth2, authenticator_path(conn, :sign_out)) + resp_sing_out = response(conn_sign_out, 200) + assert resp_sing_out == "{\"ok\":true}" + + conn_valid3 = get(conn_auth2, authenticator_path(conn, :is_authenticated)) + resp_valid3 = response(conn_valid3, 401) + assert resp_valid3 == "{\"message\":\"invalid_token\"}" + + end + end + +end From 540b5104fced48cfdb5ff594a78a75a62f8dfbb3 Mon Sep 17 00:00:00 2001 From: Tuchiro Yoshimura Date: Fri, 11 Jan 2019 14:31:53 +0900 Subject: [PATCH 2/7] fix test.exs log level --- config/test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/test.exs b/config/test.exs index 3179529..ab00305 100644 --- a/config/test.exs +++ b/config/test.exs @@ -11,7 +11,7 @@ config :materia, MateriaWeb.Test.Endpoint, watchers: [] # Print only warnings and errors during test -config :logger, level: :debug +config :logger, level: :info # Configure your database config :materia, Materia.Test.Repo, From 8dff31c759786f98ea57c56ae1d8fcd33819fb98 Mon Sep 17 00:00:00 2001 From: Tuchiro Yoshimura Date: Fri, 18 Jan 2019 14:41:33 +0900 Subject: [PATCH 3/7] #5 --- lib/materia/accounts/account.ex | 2 +- lib/materia/accounts/accounts.ex | 12 ++++++------ lib/materia/accounts/user.ex | 2 +- .../authenticator/account_authenticator.ex | 2 +- lib/materia/organizations/organizations.ex | 17 +++++++++++++++++ priv/repo/seeds.exs | 2 +- .../controllers/account_controller_test.exs | 2 +- .../authenticator_conrtoller_test.exs | 6 +++--- 8 files changed, 31 insertions(+), 14 deletions(-) diff --git a/lib/materia/accounts/account.ex b/lib/materia/accounts/account.ex index f8badff..b8e7907 100644 --- a/lib/materia/accounts/account.ex +++ b/lib/materia/accounts/account.ex @@ -22,7 +22,7 @@ defmodule Materia.Accounts.Account do def create_changeset(account, attrs) do account |> cast(attrs, [:external_code, :name, :start_datetime, :descriptions, :frozen_datetime, :expired_datetime,:status, :organization_id, :main_user_id, :lock_version]) - |> validate_required([:name, :start_datetime]) + |> validate_required([:external_code, :start_datetime]) |> unique_constraint(:code) end diff --git a/lib/materia/accounts/accounts.ex b/lib/materia/accounts/accounts.ex index 975f5fc..0139e50 100644 --- a/lib/materia/accounts/accounts.ex +++ b/lib/materia/accounts/accounts.ex @@ -1130,7 +1130,7 @@ defmodule Materia.Accounts do %{ descriptions: nil, expired_datetime: nil, - external_code: nil, + external_code: "hogehoge_code", frozen_datetime: nil, id: 1, lock_version: 0, @@ -1162,16 +1162,16 @@ defmodule Materia.Accounts do ## Examples - iex(1)> {:ok, account} = Materia.Accounts.create_account(%{"name" => "craete_account_test001"}) + iex(1)> {:ok, account} = Materia.Accounts.create_account(%{"external_code" => "craete_account_test001"}) iex(2)> MateriaWeb.AccountView.render("show.json", %{account: account}) |> Map.delete(:id) |> Map.delete(:start_datetime) %{ descriptions: nil, expired_datetime: nil, - external_code: nil, + external_code: "craete_account_test001", frozen_datetime: nil, lock_version: 0, main_user: nil, - name: "craete_account_test001", + name: nil, organization: nil, status: 1 } @@ -1194,7 +1194,7 @@ defmodule Materia.Accounts do ## Examples - iex(1)> {:ok, account} = Materia.Accounts.create_account(%{"name" => "update_account_test001"}) + iex(1)> {:ok, account} = Materia.Accounts.create_account(%{"external_code" => "update_account_test001"}) iex(2)> {:ok, updated_account} = Materia.Accounts.update_account(account, %{"status" => 8}) iex(3)> updated_account.status 8 @@ -1227,7 +1227,7 @@ defmodule Materia.Accounts do ## Examples - iex(1)> {:ok, account} = Materia.Accounts.create_account(%{"name" => "delete_account_test001"}) + iex(1)> {:ok, account} = Materia.Accounts.create_account(%{"external_code" => "delete_account_test001"}) iex(2)> {:ok, deleted_account} = Materia.Accounts.delete_account(account) iex(3)> Materia.Accounts.list_accounts_by_params(%{"and" => [ %{"id" => account.id} ] }) [] diff --git a/lib/materia/accounts/user.ex b/lib/materia/accounts/user.ex index 177a809..49c03ac 100644 --- a/lib/materia/accounts/user.ex +++ b/lib/materia/accounts/user.ex @@ -48,7 +48,7 @@ defmodule Materia.Accounts.User do def changeset_create(user, attrs) do user |> cast(attrs, [:organization_id, :name, :email, :password, :role, :status, :external_user_id, :back_ground_img_url, :icon_img_url, :one_line_message, :descriptions, :phone_number, :lock_version]) - |> validate_required([:name, :email, :password, :role]) + |> validate_required([:email, :password, :role]) |> unique_constraint(:email) |> put_password_hash() |> optimistic_lock(:lock_version) diff --git a/lib/materia/authenticator/account_authenticator.ex b/lib/materia/authenticator/account_authenticator.ex index c979fbc..dbc262c 100644 --- a/lib/materia/authenticator/account_authenticator.ex +++ b/lib/materia/authenticator/account_authenticator.ex @@ -36,7 +36,7 @@ defmodule Materia.AccountAuthenticator do {:error, "account #{account} not found."} else org_accounts = Accounts.list_accounts_by_params(%{"and" => [%{"organization_id" => user.organization_id}]}) - org_account = Enum.find(org_accounts, fn(org_account) -> org_account.name === account end) + org_account = Enum.find(org_accounts, fn(org_account) -> org_account.external_code === account end) if org_account == nil do {:error, "account #{account} not found."} else diff --git a/lib/materia/organizations/organizations.ex b/lib/materia/organizations/organizations.ex index 2ea627d..afb4d92 100644 --- a/lib/materia/organizations/organizations.ex +++ b/lib/materia/organizations/organizations.ex @@ -8,6 +8,7 @@ defmodule Materia.Organizations do alias Materia.Organizations.Organization alias MateriaUtils.Calendar.CalendarUtil + alias MateriaUtils.Ecto.EctoUtil alias Materia.Errors.BusinessError require Logger @@ -74,6 +75,22 @@ defmodule Materia.Organizations do |> repo.preload(:addresses) end + @doc """ + + ## Examples + ``` + iex(1)> orgs = Materia.Organizations.list_organizations_by_params(%{"and" => [%{"name" => "hogehoge.inc"}]}) + iex(2)> length(orgs) + 1 + ``` + """ + def list_organizations_by_params(params) do + repo = Application.get_env(:materia, :repo) + repo + |> EctoUtil.select_by_param(Organization, params) + |> repo.preload(:addresses) + end + @doc """ Gets a single organization. diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 594a03d..2c8a96f 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -31,7 +31,7 @@ Accounts.update_user(user_hogehoge, %{organization_id: organization_hogehoge.id} Locations.create_address(%{organization_id: organization_hogehoge.id, subject: "registry", location: "福岡県", zip_code: "810-ZZZZ", address1: "福岡市中央区", address2: "天神 x-x-xx"}) Locations.create_address(%{organization_id: organization_hogehoge.id, subject: "branch", location: "福岡県", zip_code: "812-ZZZZ", address1: "北九州市小倉北区", address2: "浅野 x-x-xx"}) -Accounts.create_account(%{"name" => "hogehoge account", "start_datetime" => "2019-01-10T10:03:50.293740Z", "main_user_id" => user_hogehoge.id, "organization_id" => organization_hogehoge.id}) +{:ok, account} = Accounts.create_account(%{"external_code" => "hogehoge_code", "name" => "hogehoge account", "start_datetime" => "2019-01-10T10:03:50.293740Z", "main_user_id" => user_hogehoge.id, "organization_id" => organization_hogehoge.id}) alias Materia.Mails diff --git a/test/materia_web/controllers/account_controller_test.exs b/test/materia_web/controllers/account_controller_test.exs index 371f4f8..17f1b44 100644 --- a/test/materia_web/controllers/account_controller_test.exs +++ b/test/materia_web/controllers/account_controller_test.exs @@ -5,7 +5,7 @@ defmodule MateriaWeb.AccountControllerTest do alias Materia.Accounts.Account @admin_user_attrs %{ - account: "hogehoge account", + account: "hogehoge_code", email: "hogehoge@example.com", password: "hogehoge", } diff --git a/test/materia_web/controllers/authenticator_conrtoller_test.exs b/test/materia_web/controllers/authenticator_conrtoller_test.exs index f7a8ff4..5d1e089 100644 --- a/test/materia_web/controllers/authenticator_conrtoller_test.exs +++ b/test/materia_web/controllers/authenticator_conrtoller_test.exs @@ -84,7 +84,7 @@ defmodule MateriaWeb.AuthenticatorControllerTest do test "sign-in life cycle by account and user", %{conn: conn} do attr = %{ - "account" => "hogehoge account", + "account" => "hogehoge_code", "email" => "hogehoge@example.com", "password" => "hogehoge", } @@ -155,7 +155,7 @@ defmodule MateriaWeb.AuthenticatorControllerTest do assert resp_show_my_account == %{ "descriptions" => nil, "expired_datetime" => nil, - "external_code" => nil, + "external_code" => "hogehoge_code", "frozen_datetime" => nil, "id" => 1, "lock_version" => 0, @@ -183,7 +183,7 @@ defmodule MateriaWeb.AuthenticatorControllerTest do assert resp_show_my_account2 == %{ "descriptions" => nil, "expired_datetime" => nil, - "external_code" => nil, + "external_code" => "hogehoge_code", "frozen_datetime" => nil, "id" => 1, "lock_version" => 0, From 188c4fa7521163d1a19c7930da301ae91015cd2d Mon Sep 17 00:00:00 2001 From: Tuchiro Yoshimura Date: Fri, 18 Jan 2019 15:03:45 +0900 Subject: [PATCH 4/7] update ReadMe --- README.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5ef8b00..3105ebd 100644 --- a/README.md +++ b/README.md @@ -45,10 +45,21 @@ config/config.exs ``` # Configures Guardian +# Configures Materia.Authenticator common settings config :materia, Materia.Authenticator, - issuer: "your_app_name", #<- mod your app name - # Generate mix task - # > mix phx.gen.secret + access_token_ttl: {10, :minutes}, #必須 + refresh_token_ttl: {1, :days}, # refresh_tokenを定義しない場合sign-inはaccess_tokenのみ返す + user_registration_token_ttl: {35, :minutes}, + password_reset_token_ttl: {35, :minutes} + +# Configures UserAuthenticator (if you wont user user authenticator) +config :materia, Materia.UserAuthenticator, + issuer: "your_app_name", + secret_key: "your secusecret token" + +# Configures AccountAuthenticator (if you wont user account authenticator) +config :materia, Materia.AccountAuthenticator, + issuer: "your_app_name", secret_key: "your secusecret token" # Configures GuardianDB @@ -127,7 +138,7 @@ lib/your_app_web/router.ex ``` pipeline :guardian_auth do - plug Materia.AuthenticatePipeline #<-- guardian jwt token authentication by user model. + plug Materia.UserAuthePipeline #<-- guardian jwt token authentication by user model. you can use AccountAuthPiplein if you wont account authentication end pipeline :grant_check do plug Materia.Plug.GrantChecker, repo: YourApp.Repo #<-- Grant check by user ,role and grant model. From 95db8a8a06292aa4e24e7161a0c8102dd5c702a9 Mon Sep 17 00:00:00 2001 From: Tuchiro Yoshimura Date: Fri, 18 Jan 2019 15:12:20 +0900 Subject: [PATCH 5/7] fix batch --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3105ebd..4598d58 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Materia -[![hex.pm](https://img.shields.io/apm/l/materia.svg)](https://github.com/karabiner-inc/materia) +[![hex.pm](https://img.shields.io/bower/l/materia.svg)](https://github.com/karabiner-inc/materia) [![Coverage Status](https://coveralls.io/repos/github/karabiner-inc/materia/badge.svg?branch=master)](https://coveralls.io/github/karabiner-inc/materia?branch=master) To start your Phoenix server: From 5d9432ce333a6beea22e42a8f179ebdee270886f Mon Sep 17 00:00:00 2001 From: Tuchiro Yoshimura Date: Fri, 18 Jan 2019 15:14:37 +0900 Subject: [PATCH 6/7] fix batch --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4598d58..abd99b4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Materia -[![hex.pm](https://img.shields.io/bower/l/materia.svg)](https://github.com/karabiner-inc/materia) +[![hex.pm](https://img.shields.io/hexpm/l/materia.svg)](https://github.com/karabiner-inc/materia) [![Coverage Status](https://coveralls.io/repos/github/karabiner-inc/materia/badge.svg?branch=master)](https://coveralls.io/github/karabiner-inc/materia?branch=master) To start your Phoenix server: From 5f6900c347d0fc7776367e759a03a744ae6f9f45 Mon Sep 17 00:00:00 2001 From: Tuchiro Yoshimura Date: Fri, 18 Jan 2019 16:00:10 +0900 Subject: [PATCH 7/7] update version --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 2906f0c..d6024e7 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Materia.Mixfile do def project do [ app: :materia, - version: "0.1.1", + version: "0.1.2", elixir: "~> 1.6", test_coverage: [tool: ExCoveralls], preferred_cli_env: ["coveralls": :test, "coveralls.detail": :test, "coveralls.post": :test, "coveralls.html": :test],