This module contains the signin
function
During sigin its the second function that gets executed
defmodule MyApiWeb.UserController do
@moduledoc """
This is the controller of the resource related to the users.
"""
use MyApiWeb, :controller
alias MyApi.Account
alias MyApi.Email
alias MyApi.Utils
action_fallback MyApiWeb.FallbackController
plug MyApiWeb.Plug.Auth when action in [:update_profile, :get_myprofile, :change_password, :update_mailaddress, :signup]
plug MyApiWeb.Plug.RateLimit when action in [:update_profile, :get_myprofile, :change_password, :update_mailaddress]
def verify_mailaddress(conn, params) do
source_service = conn |> MyApi.Utils.source_service?
resp =
params
|> Map.put("source_service", source_service)
|> Account.verify_mailaddress
with {:ok, user} <- resp do
send_register_mail(user)
json conn, %{message: "Success"}
end
end
def signup(conn, params) do
with {:ok, _} <- Account.signup(params, conn.assigns.user.id),
do: json conn, %{message: "Success"}
end
def signin(conn, %{"code" => code}) do
ip =
to_string(:inet_parse.ntoa(conn.remote_ip))
case Account.signin_with_code(code) do
{:ok, token, refresh_token, user} ->
Task.start_link(fn -> Account.create_login_history(user.id, ip) end)
json conn, %{user: user, token: token, refresh_token: refresh_token}
other ->
other
end
end
def signin(conn, params) do
ip =
to_string(:inet_parse.ntoa(conn.remote_ip))
case Account.signin(params, ip) do
{:ok, token, refresh_token, user} ->
Task.start_link(fn -> Account.create_login_history(user.id, ip) end)
json conn, %{user: user, token: token, refresh_token: refresh_token}
other ->
other
end
end
def refresh(conn, %{"refresh_token" => refresh_token}) do
with {:ok, access_token, refresh_token} <- MyApi.Guardian.refresh_token(refresh_token),
do: json conn, %{token: access_token, refresh_token: refresh_token}
end
def refresh(_, _) do
{:error, "No token provided"}
end
def update_profile(conn, params) do
params |> Account.edit_profile(conn.assigns.user)
end
def update_mailaddress(conn, %{"mailaddress" => _} = params) do
with {:ok, user} <- Account.update_mailaddress(params, conn.assigns.user.id) do
send_verify_mailaddress_mail(user)
json conn, %{message: "Success"}
end
end
def update_mailaddress(_, _), do: {:error, :bad_request}
def get_profile(conn, %{"user_id" => user_id} = _params) do
resp =
user_id
|> Account.get_profile
with {:ok, user} <- resp,
do: json conn, %{user: user}
end
def get_myprofile(conn, _params) do
resp =
conn.assigns.user.id
|> Account.get_profile
with {:ok, user} <- resp,
do: json conn, %{user: user}
end
def change_password(conn, %{"current_password" => cp, "new_password" => np}) do
with {:ok, _} <- Account.change_password(conn.assigns.user.id, cp, np),
do: json conn, %{message: "Success"}
end
def change_password(_, _), do: {:error, :bad_request}
def generate_reset_code(conn, %{"mailaddress" => mailaddress}) do
case Account.generate_reset_code(mailaddress) do
{:ok, resp} ->
send_reset_mail(resp, mailaddress)
json conn, %{message: "Success"}
{:error, err} ->
{:error, err}
end
end
def generate_reset_code(_, _), do: {:error, :bad_request}
def reset_password(conn, %{"code" => code, "new_password" => np}) do
with {:ok, _} <- Account.reset_password(code, np),
do: json conn, %{message: "Success"}
end
def reset_password(_, _), do: {:error, :bad_request}
defp send_register_mail(user) do
user.activate_code
|> Email.register_mail(user.mailaddress)
end
defp send_reset_mail(%MyApi.Account.ResetCode{} = reset_code, mailaddress) do
"#{Utils.fetch_my_url}/password-reset/#{reset_code.code}"
|> Email.password_reset_mail(mailaddress)
end
defp send_verify_mailaddress_mail(user) do
user.activate_code
|> Email.verify_mailaddress_mail(user.mailaddress)
end
end
Here is the Account
module
defmodule MyApi.Account do
@moduledoc """
The Account context.
"""
@type query :: Ecto.Query.t() | MyApi.Account.User
@type doctor_params :: map()
@type user :: %MyApi.Account.User{}
@type users :: [user()]
@type reset_code :: %MyApi.Account.ResetCode{}
@type offset :: number() | nil
@type limit :: number() | nil
import Ecto.Query, warn: false
alias Ecto.Multi
alias MyApi.{Repo, Utils, Stripe, Paginator, Email}
alias MyApi.Account.User
alias MyApi.Account.DoctorProfile
alias MyApi.Account.DoctorTreatmentRelationship
alias MyApi.Account.ResetCode
alias MyApi.Account.LoginHistory
alias MyApi.Account.LoginCode
alias MyApi.Data.TreatmentCategories
def verify_mailaddress(params) do
%User{}
|> User.verify_mailaddress_changeset(params)
|> Repo.insert()
end
def signup(%{"is_doctor" => true} = params, user_id) do
with {:ok, user} <- get_user(user_id),
{:ok, resp} <- user |> create_doctor(params) |> Repo.transaction(),
do: get_profile(resp.user.id)
end
def signup(%{"is_doctor" => false} = params, user_id) do
with {:ok, user} <- get_user(user_id) do
user
|> User.signup_user_changeset(params)
|> Repo.update()
end
end
def signup(_) do
{:error, %{errors: ["パラメーターが不正です"]}}
end
defp get_user(user_id) do
case User |> Repo.get(user_id) do
nil -> {:not_found_resource}
user -> {:ok, user}
end
end
def activate(activate_code) do
match_user = User
|> Repo.get_by(activate_code: activate_code)
case match_user do
nil ->
{:error, "activate code is invalid"}
_ ->
match_user
|> Ecto.Changeset.change(is_activated: true)
|> Repo.update
end
end
def signin(%{"mailaddress" => mailaddress, "password" => password}, ip_address) do
user =
User
|> Repo.get_by(mailaddress: mailaddress)
valid_res =
user
|> valid_signin(password)
|> check_suspicious_ip(ip_address)
with {:ok, _} <- valid_res do
user
|> Repo.preload(:good_treatments)
|> MyApi.Guardian.gen_token
else
{:error, _, %{type: "SUSPINCIOUS_IP"}} = err ->
send_login_code(user)
err
err ->
err
end
end
def signin(_, _) do
{:error, "Invalid params"}
end
def signin_with_code(%LoginCode{} = code) do
case get_user(code.user_id) do
{:ok, user} ->
Repo.delete(code)
user
|> Repo.preload(:good_treatments)
|> MyApi.Guardian.gen_token
_ ->
{:error, :not_found_resource}
end
end
def signin_with_code(nil) do
{:error, "Code is invalid.", %{type: "INVALID_CODE"}}
end
def signin_with_code(code) do
before_one_hour =
Timex.now("Asia/Tokyo")
|> Timex.shift(hours: -1)
|> DateTime.truncate(:second)
|> DateTime.to_naive
LoginCode
|> where([c], c.code == ^code)
|> where([c], c.inserted_at > ^before_one_hour)
|> order_by(desc: :id)
|> limit(1)
|> Repo.one()
|> signin_with_code()
end
defp create_doctor(user, params) do
changeset = User.signup_doctor_changeset(user, params)
Multi.new
|> Multi.update(:user, changeset)
|> Multi.run(:good_treatments, &(insert_good_treatments(params["good_treatment_ids"], &1.user.id)))
|> Multi.run(:doctor_profile, &(update_doctor_profile_assoc(%DoctorProfile{}, params["doctor_profile"], &1.user)))
end
def edit_profile(params, %{is_doctor: true} = current) do
resp =
current
|> update_doctor_profile(params)
|> Repo.transaction
with {:ok, resp} <- resp,
do: get_profile(resp.user.id)
end
def edit_profile(params, %{is_doctor: false} = current) do
current
|> User.update_profile_user_changeset(params)
|> Repo.update
end
def edit_profile(_, _) do
{:error, "Invalid user."}
end
def update_mailaddress(params, user_id) do
case User |> Repo.get(user_id) do
nil -> {:error, "存在しないユーザーです"}
user ->
user
|> User.update_mailaddress_changeset(params)
|> Repo.update()
end
end
def get_profile(user_id) do
user_id = Utils.any_to_integer(user_id)
user =
User
|> preload([:profile_image, :header_image, :good_treatments])
|> Repo.get(user_id)
case user do
nil -> {:not_found_resource}
_ -> {:ok, user}
end
end
defp send_login_code(user) do
{:ok, %{code: code}} =
%LoginCode{}
|> LoginCode.changeset(%{user_id: user.id})
|> Repo.insert()
Task.start_link(fn -> Email.login_code(code, user.mailaddress) end)
end
defp check_suspicious_ip({:ok, user}, ip_address) do
check_suspicious_ip(user, ip_address)
end
defp check_suspicious_ip(%{id: user_id} = user, ip_address) do
history_ips =
LoginHistory
|> where([h], h.user_id == ^user_id)
|> select([h], h.ip_address)
|> limit(100)
|> Repo.all
if Enum.count(history_ips) !== 0 and ip_address not in history_ips,
do: {:error, "Detected suspincious ip address. check your email.", %{type: "SUSPINCIOUS_IP"}},
else: {:ok, user}
end
defp check_suspicious_ip(resp) do
resp
end
defp valid_signin(%{is_activated: false} = _user, _password) do
{:error, "Not activated", additional: %{type: "NOT_ACTIVATED"}}
end
defp valid_signin(%{is_doctor: true, is_activated_doctor: false} = _user, _password) do
{:error, "You have not been approved by the administrator yet.", additional: %{type: "NOT_ACTIVATED_DOCTOR"}}
end
defp valid_signin(nil, _password) do
{:error, "Not found user", additional: %{type: "NOT_FOUND"}}
end
defp valid_signin(user, password) do
if check_valid_password(user, password),
do: {:ok, user},
else: {:error, "Invalid mailaddress or password", additional: %{type: "INVALID_INFO"}}
end
@spec check_valid_password(user(), String.t()) :: boolean()
defp check_valid_password(user, password) do
Comeonin.Bcrypt.checkpw(password, user.password)
end
defp update_doctor_profile(current, params) do
changeset =
current
|> User.update_profile_doctor_changeset(params)
Multi.new
|> Multi.update(:user, changeset)
|> Multi.run(:good_treatments, fn %{user: user} ->
if params["good_treatment_ids"],
do: update_good_treatments(params["good_treatment_ids"], user.id),
else: {:ok, 0}
end)
|> Multi.run(:doctor_profile, &(update_doctor_profile_assoc(&1.user.doctor_profile, params["doctor_profile"], &1.user)))
end
defp insert_good_treatments(ids, user_id) do
treatments =
ids
|> TreatmentCategories.filter_treatment_ids
|> Enum.map(fn(treatment_id) ->
%{user_id: user_id, treatment_category_id: treatment_id}
|> Map.put(:inserted_at, Ecto.DateTime.utc)
|> Map.put(:updated_at, Ecto.DateTime.utc)
end)
with {count, _} <- Repo.insert_all(DoctorTreatmentRelationship, treatments),
do: {:ok, count}
end
defp update_good_treatments(ids, user_id) do
DoctorTreatmentRelationship
|> where([q], q.user_id == ^user_id)
|> Repo.delete_all
insert_good_treatments(ids, user_id)
end
defp update_doctor_profile_assoc(struct, params, user) do
doctor_profile_chageset =
struct
|> DoctorProfile.changeset(Map.put(params || %{}, "user_id", user.id))
assoc_changeset =
user
|> Ecto.Changeset.change
|> Ecto.Changeset.put_embed(:doctor_profile, doctor_profile_chageset)
assoc_changeset =
if doctor_profile_chageset.errors != [],
do: assoc_changeset |> Ecto.Changeset.add_error(:child_invalid, "ドクタープロフィールに未入力項目があります"),
else: assoc_changeset
Repo.update assoc_changeset
end
# admin
@spec get_offset_doctors(map()) :: users
def get_offset_doctors(params) do
User
|> get_doctors(params)
end
@spec get_not_approve_doctor(map()) :: users
def get_not_approve_doctor(params) do
User
|> where([u], u.is_activated_doctor == false)
|> get_doctors(params)
end
@spec get_already_approve_doctor(map()) :: users
def get_already_approve_doctor(params) do
User
|> where([u], u.is_activated_doctor == true)
|> get_doctors(params)
end
def approve_doctor(user_id) do
doctor = User |> Repo.get_by([id: user_id, is_doctor: true])
case doctor do
nil -> {:not_found_resource}
%{} ->
doctor
|> User.doctor_approve_changeset(%{is_activated_doctor: true})
|> Repo.update
end
end
@spec search_doctors(map()) :: users
def search_doctors(%{"doctor" => doctor_params = %{}} = params) do
User
|> join(:inner, [u], gt in assoc(u, :good_treatments))
|> preload([_, gt], [good_treatments: gt])
|> where_doctors_have_params(doctor_params)
|> get_doctors(params)
end
def search_doctors(_), do: {:error, :bad_request}
@spec where_doctors_have_params(query, map()) :: query
defp where_doctors_have_params(query, params) do
query
|> where_doctor_have_treatments(params["treatments"])
|> where_doctor_have_location(params["location"])
end
@spec where_doctor_have_treatments(query, list()) :: query
defp where_doctor_have_treatments(query, nil), do: query
defp where_doctor_have_treatments(query, treatments) when is_list(treatments) do
query
|> where([_, gt], gt.id in ^treatments)
end
@spec where_doctor_have_location(query, String.t()) :: query
defp where_doctor_have_location(query, nil), do: query
defp where_doctor_have_location(query, location) do
query
|> where([], fragment("doctor_profile->>'hospital_location' ILIKE ?",
^"%#{location}%"))
end
@spec get_random_doctors(limit :: non_neg_integer) :: [%User{}] | no_return
def get_random_doctors(limit \\ 50)
def get_random_doctors(nil), do: get_random_doctors
def get_random_doctors(limit) do
User
|> preload(:good_treatments)
|> where([u], u.is_doctor == true)
|> order_by(fragment("RANDOM()"))
|> limit(^limit)
|> Repo.all()
end
@spec get_doctors(query, map()) :: users
defp get_doctors(query, params) do
query
|> where([u], u.is_doctor == true)
|> order_by(desc: :id)
|> Paginator.new(params)
end
# password
@spec change_password(integer(), String.t(), String.t()) :: {:ok, user()} | {:error, any()}
def change_password(user_id, current_password, new_password) do
user =
Repo.get(User, user_id)
case check_valid_password(user, current_password) do
true ->
update_password(user, new_password)
false ->
{:error, "Password is incorrect"}
end
end
@spec generate_reset_code(String.t()) :: {:ok, reset_code} | {:error, Ecto.Changeset.t()}
def generate_reset_code(mailaddress) do
user =
User
|> where([u], u.mailaddress == ^mailaddress)
|> limit(1)
|> Repo.one
case user do
nil ->
{:error, "Not found user."}
%User{} ->
%ResetCode{}
|> ResetCode.changeset(%{user_id: user.id})
|> Repo.insert
end
end
@spec reset_password(String.t(), String.t()) :: {:ok, any()} | {:error, any()}
def reset_password(code, new_password) do
reset_code =
ResetCode
|> where([c], c.code == ^code)
|> limit(1)
|> Repo.one
|> Repo.preload(:user)
case reset_code do
nil ->
{:error, "Code is invalid"}
%ResetCode{user: user} ->
update_password_with_code(reset_code, user, new_password)
_ ->
{:error, "Not found user"}
end
end
@spec update_password_with_code(reset_code, user, String.t()) :: {:ok, any()} | {:error, any()}
defp update_password_with_code(%ResetCode{} = reset_code, %User{} = user, new_password) do
case update_password(user, new_password) do
{:ok, _} ->
reset_code
|> ResetCode.use_changeset(%{is_used: true})
|> Repo.update
{:error, err} ->
{:error, err}
end
end
@spec update_password(user(), String.t()) :: {:ok, user()} | {:error, Ecto.Changeset.t()}
defp update_password(user, new_password) do
user
|> User.update_password_changeset(%{password: new_password})
|> Repo.update
end
# Stripe
@spec get_stripe_sources(map(), String.t()) :: {:ok, [map()]} | {:error, any()}
def get_stripe_sources(params, stripe_customer_id) do
Stripe.get_sources(params, stripe_customer_id)
end
@spec create_stripe_source(map(), String.t()) :: {:ok, map()} | {:error, any()}
def create_stripe_source(params, stripe_customer_id) do
Stripe.create_source(params, stripe_customer_id)
end
@spec update_stripe_source(map(), String.t()) :: {:ok, map()} | {:error, any()}
def update_stripe_source(params, stripe_customer_id) do
Stripe.update_source(params, stripe_customer_id)
end
@spec delete_stripe_source(map(), String.t()) :: {:ok, map()} | {:error, any()}
def delete_stripe_source(params, stripe_customer_id) do
Stripe.delete_source(params, stripe_customer_id)
end
@spec create_login_history(number(), String.t()) :: {:ok, map() | {:error, any()}}
def create_login_history(user_id, ip_address) do
%LoginHistory{}
|> LoginHistory.changeset(%{user_id: user_id, ip_address: ip_address})
|> Repo.insert
end
@spec get_login_histories(number()) :: [%LoginHistory{}]
def get_login_histories(user_id) do
LoginHistory
|> where([lh], lh.user_id == ^user_id)
|> order_by(desc: :id)
|> limit(20)
|> Repo.all()
end
end
And this is the guardian
module defined. It is a separate module defined in the project and it call some functions in the dependency guardian
also
defmodule MyApi.Guardian do
@moduledoc """
The Guardian module.
"""
use Guardian, otp_app: :my_api
alias MyApi.Account.User
alias MyApi.Repo
def gen_token(resource) do
{:ok, token, claims} = resource
|> __MODULE__.encode_and_sign(%{})
{:ok, refresh_token, refresh_claims} = resource
|> __MODULE__.encode_and_sign(%{}, token_type: "refresh")
{:ok, _} = signin_save(resource, claims, token)
{:ok, _} = signin_save(resource, refresh_claims, refresh_token)
{:ok, token, refresh_token, resource}
end
def refresh_token(refresh_token) do
claims = case MyApi.Guardian.decode_and_verify(refresh_token) do
{:ok, claims} -> claims
{:error, _} -> nil
end
cond do
claims == nil ->
{:error, "Not found token"}
is_verify(claims, refresh_token) == false ->
{:error, "Invalid token"}
true ->
exchange_token(refresh_token)
end
end
def subject_for_token(resource, _) do
{:ok, to_string(resource.uuid) }
end
def resource_from_claims(claims) do
if Ecto.UUID.dump(claims["sub"]) == :error,
do: {:ok, nil} ,
else: {:ok, User |> Repo.get_by(uuid: claims["sub"])}
end
defp signin_save(resource, claims, token) do
with {:ok, _} <- Guardian.DB.after_encode_and_sign(resource, claims["typ"], claims, token) do
{:ok, token}
end
end
defp token_revoke(claims, token) do
with {:ok, _} <- Guardian.DB.on_revoke(claims, token) do
{:ok, claims}
end
end
defp is_verify(claims, refresh_token) do
case Guardian.DB.on_verify(claims, refresh_token) do
{:ok, _} -> true
{:error, _} -> false
end
end
defp exchange_token(token) do
case MyApi.Guardian.exchange(token, "refresh", "access") do
{:ok, {old_token, old_claims}, {new_token, new_claims}} ->
{:ok, resource} = resource_from_claims(new_claims)
# save new access token
{:ok, _} = resource |> signin_save(new_claims, new_token)
# revoke old refresh token
{:ok, _} = token_revoke(old_claims, old_token)
# get new refresh_token
{:ok, new_refresh_token, new_refresh_claims} = resource
|> __MODULE__.encode_and_sign(%{}, token_type: "refresh")
# save new refresh token
{:ok, _} = resource
|> signin_save(new_refresh_claims, new_refresh_token)
{:ok, new_token, new_refresh_token}
{:error, err} ->
{:error, err}
end
end
end