From 7c73c2c424b8303a77a0c05e783ee70388465d50 Mon Sep 17 00:00:00 2001 From: Pim Kunis Date: Sat, 26 Jun 2021 22:02:18 +0200 Subject: [PATCH] Create schemas for JSON API endpoints --- lib/matrix_server.ex | 14 +++ lib/matrix_server/account.ex | 7 +- lib/matrix_server/device.ex | 9 ++ lib/matrix_server_web/api/register.ex | 38 ++++++ .../controllers/auth_controller.ex | 115 +++++++----------- .../controllers/info_controller.ex | 6 + lib/matrix_server_web/plug/authenticate.ex | 1 - lib/matrix_server_web/plug/error.ex | 1 + lib/matrix_server_web/router.ex | 7 +- lib/matrix_server_web/views/error_helpers.ex | 16 --- lib/matrix_server_web/views/error_view.ex | 16 --- 11 files changed, 125 insertions(+), 105 deletions(-) create mode 100644 lib/matrix_server_web/api/register.ex delete mode 100644 lib/matrix_server_web/views/error_helpers.ex delete mode 100644 lib/matrix_server_web/views/error_view.ex diff --git a/lib/matrix_server.ex b/lib/matrix_server.ex index 5171ecc..cdc7364 100644 --- a/lib/matrix_server.ex +++ b/lib/matrix_server.ex @@ -37,4 +37,18 @@ defmodule MatrixServer do def server_name do Application.get_env(:matrix_server, :server_name) end + + def update_map_entry(map, old_key, new_key) do + update_map_entry(map, old_key, new_key, &Function.identity/1) + end + + def update_map_entry(map, old_key, new_key, fun) when is_map_key(map, old_key) do + value = Map.fetch!(map, old_key) + + map + |> Map.put(new_key, fun.(value)) + |> Map.delete(old_key) + end + + def update_map_entry(map, _, _, _), do: map end diff --git a/lib/matrix_server/account.ex b/lib/matrix_server/account.ex index 93d6a4a..bb5a493 100644 --- a/lib/matrix_server/account.ex +++ b/lib/matrix_server/account.ex @@ -34,11 +34,14 @@ defmodule MatrixServer.Account do end end - def register(params) do + def register(account, params) do Multi.new() - |> Multi.insert(:account, changeset(%Account{}, params)) + |> Multi.insert(:account, changeset(account, params)) |> Multi.insert(:device, fn %{account: account} -> + device_id = Device.generate_device_id(account) + Ecto.build_assoc(account, :devices) + |> Map.put(:device_id, device_id) |> Device.changeset(params) end) |> Multi.run(:device_with_access_token, &Device.generate_access_token/2) diff --git a/lib/matrix_server/device.ex b/lib/matrix_server/device.ex index 607ff11..3e1da5e 100644 --- a/lib/matrix_server/device.ex +++ b/lib/matrix_server/device.ex @@ -33,4 +33,13 @@ defmodule MatrixServer.Device do |> change(%{access_token: access_token}) |> repo.update() end + + def generate_device_id(%Account{localpart: localpart}) do + time_string = + DateTime.utc_now() + |> DateTime.to_unix() + |> Integer.to_string() + + "#{localpart}_#{time_string}" + end end diff --git a/lib/matrix_server_web/api/register.ex b/lib/matrix_server_web/api/register.ex new file mode 100644 index 0000000..09c996d --- /dev/null +++ b/lib/matrix_server_web/api/register.ex @@ -0,0 +1,38 @@ +defmodule MatrixServerWeb.API.Register do + use Ecto.Schema + + import Ecto.Changeset + import MatrixServerWeb.Plug.Error + + alias __MODULE__ + alias Ecto.Changeset + + embedded_schema do + field :device_id, :string + field :initial_device_display_name, :string + field :password, :string + field :username, :string + field :inhibit_login, :boolean, default: false + end + + def changeset(params) do + %Register{} + |> cast(params, [ + :device_id, + :initial_device_display_name, + :password, + :username, + :inhibit_login + ]) + |> validate_required([:password, :username]) + end + + def handle_error(conn, cs) do + put_error(conn, get_register_error(cs)) + end + + defp get_register_error(%Changeset{errors: [error | _]}), do: get_register_error(error) + defp get_register_error({:localpart, {_, [{:constraint, :unique} | _]}}), do: :user_in_use + defp get_register_error({:localpart, {_, [{:validation, _} | _]}}), do: :invalid_username + defp get_register_error(_), do: :bad_json +end diff --git a/lib/matrix_server_web/controllers/auth_controller.ex b/lib/matrix_server_web/controllers/auth_controller.ex index bb6aa60..0f4ec4e 100644 --- a/lib/matrix_server_web/controllers/auth_controller.ex +++ b/lib/matrix_server_web/controllers/auth_controller.ex @@ -3,48 +3,49 @@ defmodule MatrixServerWeb.AuthController do import MatrixServer import MatrixServerWeb.Plug.Error + import Ecto.Changeset, only: [apply_changes: 1] alias MatrixServer.{Repo, Account} + alias MatrixServerWeb.API.Register alias Ecto.Changeset - @login_type "m.login.dummy" + @register_type "m.login.dummy" + @login_type "m.login.password" - def register(conn, %{"auth" => %{"type" => @login_type}} = params) do - # User has started an auth flow. - result = - case sanitize_register_params(params) do - {:ok, params} -> - case Repo.transaction(Account.register(params)) do - {:ok, changeset} -> {:ok, changeset} - {:error, _, changeset, _} -> {:error, get_register_error(changeset)} - end + def register(conn, %{"auth" => %{"type" => @register_type}} = params) do + case Register.changeset(params) do + %Changeset{valid?: true} = cs -> + input = + apply_changes(cs) + |> Map.from_struct() + |> update_map_entry(:initial_device_display_name, :device_name) + |> update_map_entry(:username, :localpart) + |> update_map_entry(:password, :password_hash, &Bcrypt.hash_pwd_salt/1) - {:error, changeset} -> - {:error, get_register_error(changeset)} - end + case Account.register(%Account{}, input) |> Repo.transaction() do + {:ok, %{device_with_access_token: device}} -> + data = %{user_id: get_mxid(device.localpart)} - {status, data} = - case result do - {:ok, %{device_with_access_token: device}} -> - data = %{user_id: get_mxid(device.localpart)} + data = + if not input.inhibit_login do + data + |> Map.put(:device_id, device.device_id) + |> Map.put(:access_token, device.access_token) + else + data + end - data = - if Map.get(params, "inhibit_login", false) == false do - extra = %{device_id: device.device_id, access_token: device.access_token} - Map.merge(data, extra) - else - data - end + conn + |> put_status(200) + |> json(data) - {200, data} + {:error, _, cs, _} -> + Register.handle_error(conn, cs) + end - {:error, error} -> - put_error(conn, error) - end - - conn - |> put_status(status) - |> json(data) + _ -> + put_error(conn, :bad_json) + end end def register(conn, %{"auth" => _}) do @@ -55,7 +56,7 @@ defmodule MatrixServerWeb.AuthController do def register(conn, _params) do # User has not started an auth flow. data = %{ - flows: [%{stages: [@login_type]}], + flows: [%{stages: [@register_type]}], params: %{} } @@ -64,46 +65,22 @@ defmodule MatrixServerWeb.AuthController do |> json(data) end - defp sanitize_register_params(params) do - changeset = - validate_api_schema(params, register_schema()) - |> convert_change(:username, :localpart) - |> convert_change(:password, :password_hash, &Bcrypt.hash_pwd_salt/1) - - case changeset do - %Changeset{valid?: true, changes: changes} -> {:ok, changes} - _ -> {:error, changeset} - end - end - - defp get_register_error(%Changeset{errors: [error | _]}), do: get_register_error(error) - defp get_register_error({:localpart, {_, [{:constraint, :unique} | _]}}), do: :user_in_use - defp get_register_error({:localpart, {_, [{:validation, _} | _]}}), do: :invalid_username - defp get_register_error(_), do: :bad_json - - defp register_schema do - types = %{ - device_id: :string, - initial_device_display_name: :string, - display_name: :string, - password: :string, - username: :string, - localpart: :string, - password_hash: :string, - access_token: :string - } - - allowed = [:device_id, :initial_device_display_name, :username, :password] - required = [:username, :password] - - {types, allowed, required} - end - - def login(conn, _params) do + def login_types(conn, _params) do data = %{flows: [%{type: @login_type}]} conn |> put_status(200) |> json(data) end + + def login(conn, %{"type" => "m.login.password"}) do + conn + |> put_status(200) + |> json(%{}) + end + + def login(conn, _params) do + # Login type m.login.token is unsupported for now. + put_error(conn, :forbidden) + end end diff --git a/lib/matrix_server_web/controllers/info_controller.ex b/lib/matrix_server_web/controllers/info_controller.ex index 508ea43..4384a73 100644 --- a/lib/matrix_server_web/controllers/info_controller.ex +++ b/lib/matrix_server_web/controllers/info_controller.ex @@ -1,6 +1,8 @@ defmodule MatrixServerWeb.InfoController do use MatrixServerWeb, :controller + import MatrixServerWeb.Plug.Error + @supported_versions ["r0.6.1"] def versions(conn, _params) do @@ -10,4 +12,8 @@ defmodule MatrixServerWeb.InfoController do |> put_status(200) |> json(data) end + + def unrecognized(conn, _params) do + put_error(conn, :unrecognized) + end end diff --git a/lib/matrix_server_web/plug/authenticate.ex b/lib/matrix_server_web/plug/authenticate.ex index b30e7f2..7879912 100644 --- a/lib/matrix_server_web/plug/authenticate.ex +++ b/lib/matrix_server_web/plug/authenticate.ex @@ -1,7 +1,6 @@ defmodule MatrixServerWeb.Plug.Authenticate do import MatrixServerWeb.Plug.Error import Plug.Conn - import Phoenix.Controller, only: [json: 2] alias MatrixServer.Account alias Plug.Conn diff --git a/lib/matrix_server_web/plug/error.ex b/lib/matrix_server_web/plug/error.ex index 74ad8db..9d0b038 100644 --- a/lib/matrix_server_web/plug/error.ex +++ b/lib/matrix_server_web/plug/error.ex @@ -7,6 +7,7 @@ defmodule MatrixServerWeb.Plug.Error do user_in_use: {400, "M_USE_IN_USE", "Username is already taken."}, invalid_username: {400, "M_INVALID_USERNAME", "Invalid username."}, forbidden: {400, "M_FORBIDDEN", "The requested action is forbidden."}, + unrecognized: {400, "M_UNRECOGNIZED", "Unrecognized request."}, unknown_token: {401, "M_UNKNOWN_TOKEN", "Invalid access token."}, missing_token: {401, "M_MISSING_TOKEN", "Access token required."} } diff --git a/lib/matrix_server_web/router.ex b/lib/matrix_server_web/router.ex index 4e2e215..086b016 100644 --- a/lib/matrix_server_web/router.ex +++ b/lib/matrix_server_web/router.ex @@ -17,7 +17,8 @@ defmodule MatrixServerWeb.Router do scope "/client/r0", as: :client do post "/register", AuthController, :register - get "/login", AuthController, :login + get "/login", AuthController, :login_types + post "/login", AuthController, :login get "/register/available", AccountController, :available end @@ -31,4 +32,8 @@ defmodule MatrixServerWeb.Router do get "/account/whoami", AccountController, :whoami end end + + scope "/", MatrixServerWeb do + match :*, "/*path", InfoController, :unrecognized + end end diff --git a/lib/matrix_server_web/views/error_helpers.ex b/lib/matrix_server_web/views/error_helpers.ex deleted file mode 100644 index f9baf80..0000000 --- a/lib/matrix_server_web/views/error_helpers.ex +++ /dev/null @@ -1,16 +0,0 @@ -defmodule MatrixServerWeb.ErrorHelpers do - @moduledoc """ - Conveniences for translating and building error messages. - """ - - @doc """ - Translates an error message. - """ - def translate_error({msg, opts}) do - # Because the error messages we show in our forms and APIs - # are defined inside Ecto, we need to translate them dynamically. - Enum.reduce(opts, msg, fn {key, value}, acc -> - String.replace(acc, "%{#{key}}", to_string(value)) - end) - end -end diff --git a/lib/matrix_server_web/views/error_view.ex b/lib/matrix_server_web/views/error_view.ex deleted file mode 100644 index c860f6b..0000000 --- a/lib/matrix_server_web/views/error_view.ex +++ /dev/null @@ -1,16 +0,0 @@ -defmodule MatrixServerWeb.ErrorView do - use MatrixServerWeb, :view - - # If you want to customize a particular status code - # for a certain format, you may uncomment below. - # def render("500.json", _assigns) do - # %{errors: %{detail: "Internal Server Error"}} - # end - - # By default, Phoenix returns the status message from - # the template name. For example, "404.json" becomes - # "Not Found". - def template_not_found(template, _assigns) do - %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}} - end -end