Implement register endpoint for m.login.dummy login type

Add device table
This commit is contained in:
Pim Kunis 2021-06-25 17:43:12 +02:00
parent 183120b5a1
commit d81a42bf8a
7 changed files with 229 additions and 19 deletions

View file

@ -1,9 +1,50 @@
defmodule MatrixServer do
@moduledoc """
MatrixServer keeps the contexts that define your domain
and business logic.
import Ecto.Changeset
alias Ecto.Changeset
Contexts are also responsible for managing your data, regardless
if it comes from the database, an external API or others.
"""
def convert_change(changeset, old_name, new_name) do
convert_change(changeset, old_name, new_name, &Function.identity/1)
end
def convert_change(changeset, old_name, new_name, f) do
case changeset do
%Changeset{valid?: true, changes: changes} ->
case Map.fetch(changes, old_name) do
{:ok, value} ->
changeset
|> put_change(new_name, f.(value))
|> delete_change(old_name)
:error ->
changeset
end
_ ->
changeset
end
end
def validate_api_schema(params, {types, allowed, required}) do
{%{}, types}
|> cast(params, allowed)
|> validate_required(required)
end
def generate_error(errcode) do
{get_errcode_status(errcode), %{errcode: errcode, error: get_errcode_error(errcode)}}
end
def get_errcode_error("M_BAD_JSON"), do: "Bad request."
def get_errcode_error("M_USER_IN_USE"), do: "Username is already taken."
def get_errcode_error("M_INVALID_USERNAME"), do: "Invalid username."
def get_errcode_status(_), do: 400
def get_mxid(localpart) when is_binary(localpart) do
"@#{localpart}:#{server_name()}"
end
def server_name do
Application.get_env(:matrix_server, :server_name)
end
end

View file

@ -1,7 +1,11 @@
defmodule MatrixServer.Account do
use Ecto.Schema
import MatrixServer
import Ecto.{Changeset, Query}
alias MatrixServer.{Repo, Account}
alias MatrixServer.{Repo, Account, Device}
alias Ecto.Multi
@max_mxid_length 255
@localpart_regex ~r/^([a-z0-9\._=\/])+$/
@ -9,7 +13,7 @@ defmodule MatrixServer.Account do
@primary_key {:localpart, :string, []}
schema "accounts" do
field :password_hash, :string, redact: true
has_many :devices, Device, foreign_key: :localpart
timestamps(updated_at: false)
end
@ -30,9 +34,19 @@ defmodule MatrixServer.Account do
end
end
def changeset(%Account{} = account, attrs) do
def register(params) do
Multi.new()
|> Multi.insert(:account, changeset(%Account{}, params))
|> Multi.insert(:device, fn %{account: account} ->
Ecto.build_assoc(account, :devices)
|> Device.changeset(params)
end)
|> Multi.run(:device_with_access_token, &Device.generate_access_token/2)
end
def changeset(account, params \\ %{}) do
account
|> cast(attrs, [:localpart, :password_hash])
|> cast(params, [:localpart, :password_hash])
|> validate_required([:localpart, :password_hash])
|> validate_length(:password_hash, max: 60)
|> validate_format(:localpart, @localpart_regex)
@ -44,8 +58,4 @@ defmodule MatrixServer.Account do
# Subtract the "@" and ":" in the MXID.
@max_mxid_length - 2 - String.length(server_name())
end
defp server_name do
Application.get_env(:matrix_server, :server_name)
end
end

View file

@ -0,0 +1,32 @@
defmodule MatrixServer.Device do
use Ecto.Schema
import Ecto.Changeset
alias MatrixServer.{Account, Device}
@primary_key false
schema "devices" do
field :device_id, :string, primary_key: true
field :access_token, :string
field :display_name, :string
belongs_to :account, Account,
foreign_key: :localpart,
references: :localpart,
type: :string,
primary_key: true
end
def changeset(device, params \\ %{}) do
device
|> cast(params, [:localpart, :device_id, :access_token, :display_name])
|> validate_required([:localpart, :device_id])
|> unique_constraint([:localpart, :device_id], name: :devices_pkey)
end
def generate_access_token(repo, %{device: %Device{localpart: localpart, device_id: device_id} = device}) do
access_token = Phoenix.Token.encrypt(MatrixServerWeb.Endpoint, "access_token", {localpart, device_id})
device
|> change(%{access_token: access_token})
|> repo.update()
end
end

View file

@ -2,10 +2,6 @@ defmodule MatrixServerWeb.AccountController do
use MatrixServerWeb, :controller
alias MatrixServer.Account
def register(conn, _params) do
conn
end
def available(conn, params) do
localpart = Map.get(params, "username", "")

View file

@ -0,0 +1,112 @@
defmodule MatrixServerWeb.AuthController do
use MatrixServerWeb, :controller
import MatrixServer
alias MatrixServer.{Repo, Account}
alias Ecto.Changeset
@login_type "m.login.dummy"
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
{:error, changeset} ->
{:error, get_register_error(changeset)}
end
{status, data} =
case result do
{:ok, %{device_with_access_token: device}} ->
data = %{user_id: get_mxid(device.localpart)}
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
{200, data}
{:error, error} ->
generate_error(error)
end
conn
|> put_status(status)
|> json(data)
end
def register(conn, %{"auth" => _}) do
# Other login types are unsupported for now.
data = %{errcode: "M_FORBIDDEN", error: "Login type not supported"}
conn
|> put_status(400)
|> json(data)
end
def register(conn, _params) do
# User has not started an auth flow.
data = %{
flows: [%{stages: [@login_type]}],
params: %{}
}
conn
|> put_status(401)
|> 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: "M_USER_IN_USE"
defp get_register_error({:localpart, {_, [{:validation, _} | _]}}), do: "M_INVALID_USERNAME"
defp get_register_error(_), do: "M_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
data = %{flows: [%{type: @login_type}]}
conn
|> put_status(200)
|> json(data)
end
end

View file

@ -9,7 +9,8 @@ defmodule MatrixServerWeb.Router do
pipe_through :api
scope "/client/r0", as: :client do
post "/register", AccountController, :register
post "/register", AuthController, :register
get "/login", AuthController, :login
get "/register/available", AccountController, :available
end

View file

@ -0,0 +1,18 @@
defmodule MatrixServer.Repo.Migrations.AddDevicesTable do
use Ecto.Migration
def change do
create table(:devices, primary_key: false) do
add :device_id, :string, primary_key: true, null: false
add :access_token, :string
add :display_name, :string
add :localpart,
references(:accounts, column: :localpart, on_delete: :delete_all, type: :string),
primary_key: true,
null: false
end
create index(:devices, [:localpart])
end
end