diff --git a/.gitignore b/.gitignore index baf5cbc..a58a312 100644 --- a/.gitignore +++ b/.gitignore @@ -31,8 +31,13 @@ birdy_chat-*.tar # Ignore digested assets cache. /priv/static/cache_manifest.json +/priv/static/favicon-*.ico +/priv/static/images/logo-*.svg +/priv/static/robots-*.txt + # Ignore messages folder /priv/messages/ +!priv/messages/README.md # In case you use Node.js/npm, you want to ignore these. npm-debug.log diff --git a/README.md b/README.md index 5626ecc..8cfed24 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,60 @@ -# BirdyChat +# BirdyChat Tech Challenge -To start your Phoenix server: +This repository implements BirdyChat tech challenge. -* Run `mix setup` to install and setup dependencies -* Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` +# Start here -Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. +Firstly, check out this repository locally. +Then install required versions of Elixir and Erlang from .tool-versions. asdf-vm will pick them up automatically. +Then you run the test suite with `mix test`. +Then, a scripted demo release is prepared in the prod environment: -Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). +``` +MIX_ENV=prod mix build_release +``` -## Learn more +Then you can run 3 servers connected to one another: -* Official website: https://www.phoenixframework.org/ -* Guides: https://hexdocs.pm/phoenix/overview.html -* Docs: https://hexdocs.pm/phoenix -* Forum: https://elixirforum.com/c/phoenix-forum -* Source: https://github.com/phoenixframework/phoenix +`_build/prod/rel/birdy_chat/bin/server_1` - runs at localhost:4001 +`_build/prod/rel/birdy_chat/bin/server_2` - runs at localhost:4002 +`_build/prod/rel/birdy_chat/bin/server_3` - runs at localhost:4003 + +Out of the box they communicate with one another. Send a json request to one of them and observe the results. Feel free to modify the examples below. + +### A local request: + +``` +curl --request POST \ + --url http://localhost:4001/api/messages \ + --header 'content-type: application/json' \ + --data '{"message":"123","to":"1-user","from":"1-user"}' +``` + +### A remote request: + +``` +curl --request POST \ + --url http://localhost:4001/api/messages \ + --header 'content-type: application/json' \ + --data '{"message":"123","to":"2-user","from":"1-user"}' +``` + +### A request to unknown server: + +``` +curl --request POST \ + --url http://localhost:4001/api/messages \ + --header 'content-type: application/json' \ + --data '{"message":"123","to":"4-user","from":"1-user"}' +``` + +Files are saved to `priv/messages`. + + +## Key rundown of technical details + +First and foremost, I tried to keep it as simple as possible - stick to know conventions, leverage existing libraries or frameworks, hence the usage of both Phoenix and Ecto - conventions they provide are understandable to pretty much any Elixir developer. + +All important modules are documented and there is a test suite that exacutes all known code paths. + +Servers authenticate with one another using Phoenix tokens. Servers communicate via HTTP using JSON. There were other options but I decided to use this one for reasons enumerated in documentation for BirdyChatWeb.Api.Server.Internal.Controller. diff --git a/config/config.exs b/config/config.exs index 51096d6..5dbfd9b 100644 --- a/config/config.exs +++ b/config/config.exs @@ -21,15 +21,6 @@ config :birdy_chat, BirdyChatWeb.Endpoint, pubsub_server: BirdyChat.PubSub, live_view: [signing_salt: "6cr1V8uA"] -# Configure the mailer -# -# By default it uses the "Local" adapter which stores the emails -# locally. You can see the emails in your browser, at "/dev/mailbox". -# -# For production it's recommended to configure a different adapter -# at the `config/runtime.exs`. -config :birdy_chat, BirdyChat.Mailer, adapter: Swoosh.Adapters.Local - # Configure esbuild (the version is required) config :esbuild, version: "0.25.4", diff --git a/config/dev.exs b/config/dev.exs index 5138034..dec572c 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -77,6 +77,3 @@ config :phoenix_live_view, debug_attributes: true, # Enable helpful, but potentially expensive runtime checks enable_expensive_runtime_checks: true - -# Disable swoosh api client as it is only required for production adapters. -config :swoosh, :api_client, false diff --git a/config/prod.exs b/config/prod.exs index 4e08023..ecdb679 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -18,12 +18,6 @@ config :birdy_chat, BirdyChatWeb.Endpoint, hosts: ["localhost", "127.0.0.1"] ] -# Configure Swoosh API Client -config :swoosh, api_client: Swoosh.ApiClient.Req - -# Disable Swoosh Local Memory Storage -config :swoosh, local: false - # Do not print debug messages in production config :logger, level: :info diff --git a/config/runtime.exs b/config/runtime.exs index 26eec65..4c7f23e 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -21,7 +21,7 @@ if System.get_env("PHX_SERVER") do end config :birdy_chat, BirdyChatWeb.Endpoint, - http: [port: String.to_integer(System.get_env("PORT", "4000"))] + http: [port: String.to_integer(System.get_env("BIRDY_CHAT_PORT", "4000"))] if config_env() == :prod do # The secret key base is used to sign/encrypt cookies and other secrets. @@ -82,22 +82,4 @@ if config_env() == :prod do # force_ssl: [hsts: true] # # Check `Plug.SSL` for all available options in `force_ssl`. - - # ## Configuring the mailer - # - # In production you need to configure the mailer to use a different adapter. - # Here is an example configuration for Mailgun: - # - # config :birdy_chat, BirdyChat.Mailer, - # adapter: Swoosh.Adapters.Mailgun, - # api_key: System.get_env("MAILGUN_API_KEY"), - # domain: System.get_env("MAILGUN_DOMAIN") - # - # Most non-SMTP adapters require an API client. Swoosh supports Req, Hackney, - # and Finch out-of-the-box. This configuration is typically done at - # compile-time in your config/prod.exs: - # - # config :swoosh, :api_client, Swoosh.ApiClient.Req - # - # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details. end diff --git a/config/test.exs b/config/test.exs index 3d83e54..4a31ecc 100644 --- a/config/test.exs +++ b/config/test.exs @@ -7,12 +7,6 @@ config :birdy_chat, BirdyChatWeb.Endpoint, secret_key_base: "DsOg8g/AU3wogZIm99JWnoDyijeinMJFFfkFdwkSkFcjvHywCXjCxl//NY1cvm7Y", server: false -# In test we don't send emails -config :birdy_chat, BirdyChat.Mailer, adapter: Swoosh.Adapters.Test - -# Disable swoosh api client as it is only required for production adapters -config :swoosh, :api_client, false - # Print only warnings and errors during test config :logger, level: :warning @@ -23,6 +17,9 @@ config :phoenix, :plug_init_mode, :runtime config :phoenix_live_view, enable_expensive_runtime_checks: true +# Mock out HTTP requests in test +config :birdy_chat, BirdyChat.Dispatcher, req_opts: [plug: {Req.Test, BirdyChat.Dispatcher}] + # Sort query params output of verified routes for robust url comparisons config :phoenix, sort_verified_routes_query_params: true diff --git a/lib/birdy_chat/dispatcher.ex b/lib/birdy_chat/dispatcher.ex new file mode 100644 index 0000000..e0a3b7d --- /dev/null +++ b/lib/birdy_chat/dispatcher.ex @@ -0,0 +1,54 @@ +defmodule BirdyChat.Dispatcher do + @moduledoc """ + Main dispatcher of messages - decides to either write them to local file system or send them via + HTTP to peers. + + It originally started as a websocket connection between servers, but then I decided to rip + it out and replace with simple HTTP request-response for the following reasons: + + 1. HTTP guarantees immediate feedback (request succeeds or not), making the addition of caching + or retries easy. + 2. HTTP requests have well-know semantic so I can i.e use HTTP statuses for error signals + instead of inventing my own error language. + """ + + @spec dispatch(Ecto.Changeset.t()) :: :ok | {:error, String.t()} + def dispatch(%Ecto.Changeset{changes: changes} = changeset) do + case changes do + %{routing: :local} -> BirdyChat.MessageWriter.write(changeset.changes) + %{routing: :remote} -> send_to_remote(changeset.changes) + end + end + + defp send_to_remote(%{server: server, from: from} = message) do + {name, base_url} = + BirdyChat.Identity.peers() + |> Enum.find(fn {name, _url} -> name == server end) + + api_url = base_url <> "/api/internal" + token = Phoenix.Token.sign(BirdyChatWeb.Endpoint, "serverAuth", from) + + {_request, result} = + Req.new(url: api_url, retry: false, method: :post) + |> Req.merge(req_opts()) + |> Req.Request.put_new_header("authorization", token) + |> Req.merge(json: message) + |> Req.Request.run_request() + + # Handle more when you encounter errors + case result do + %Req.Response{status: 201} -> :ok + # This should never happen under normal circumstances so I am commenting this out but maybe + # needs a second look. + # %Req.Response{status: 403} -> {:error, "Unauthorised"} + + # Peer is down. + %Req.TransportError{reason: :econnrefused} -> {:error, "peer #{name} is unreachable"} + end + end + + def req_opts do + Application.get_env(:birdy_chat, __MODULE__) + |> Access.get(:req_opts, []) + end +end diff --git a/lib/birdy_chat/identity.ex b/lib/birdy_chat/identity.ex index 080386b..3c62f1b 100644 --- a/lib/birdy_chat/identity.ex +++ b/lib/birdy_chat/identity.ex @@ -1,4 +1,15 @@ defmodule BirdyChat.Identity do + @moduledoc """ + Server identity, populated on startup from the following environment variables: + + - BIRDY_CHAT_IDENTITY: Name of the server, a string that can be formatted into an integer. + - BIRDY_CHAT_PEERS: List of peer servers in format of + `1::http://localhost:4001;2::http://localhost:4002` + + When the environment variables are not present the server starts in test mode and can only + communicate with itself. + """ + use Agent defstruct [:identity, :peers, :mode] @@ -49,7 +60,18 @@ defmodule BirdyChat.Identity do peers = System.get_env("BIRDY_CHAT_PEERS") case {identity, peers} do - {nil, nil} -> %__MODULE__{identity: "test", peers: [], mode: :singleton} + {nil, nil} -> + %__MODULE__{ + identity: "test1", + peers: %{"test2" => "http://localhost:4001"}, + mode: :test + } + + {identity, peers} -> + peers = parse_peers(peers) + identity = parse_identity(identity) + + %__MODULE__{identity: identity, peers: peers, mode: :connected} end end end diff --git a/lib/birdy_chat/mailer.ex b/lib/birdy_chat/mailer.ex deleted file mode 100644 index be833ef..0000000 --- a/lib/birdy_chat/mailer.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule BirdyChat.Mailer do - use Swoosh.Mailer, otp_app: :birdy_chat -end diff --git a/lib/birdy_chat/message.ex b/lib/birdy_chat/message.ex index 3c86d43..a28e00c 100644 --- a/lib/birdy_chat/message.ex +++ b/lib/birdy_chat/message.ex @@ -1,17 +1,33 @@ defmodule BirdyChat.Message do + @moduledoc """ + Main module for input validation. I decided to re-use Ecto because of existence of Phoenix.Ecto + that clearly integrates the error messages produced from Ecto into HTTP plumbing of Phoenix. + """ + use Ecto.Schema embedded_schema do field :from, :string field :to, :string field :message, :string + field :routing, Ecto.Enum, values: [:remote, :local] + field :server, :string end - def validate(params) do + @doc """ + Validation for inter-server communication. It is essentially the same as validate/1 + but without the requirement to communicate with home server only. + + This can be also designed as a function that accepts other function or some configuration option + but two separately named functions are easier to understand and less prone to misuse. + """ + def validate_for_inter_server_use(params) do changeset = %__MODULE__{} |> Ecto.Changeset.cast(params, [:from, :to, :message]) |> Ecto.Changeset.validate_required([:from, :to, :message]) + |> put_routing() + |> validate_is_local() if changeset.valid? do {:ok, changeset} @@ -19,4 +35,72 @@ defmodule BirdyChat.Message do {:error, changeset} end end + + def validate(params) do + changeset = + %__MODULE__{} + |> Ecto.Changeset.cast(params, [:from, :to, :message]) + |> Ecto.Changeset.validate_required([:from, :to, :message]) + |> put_routing() + |> validate_home_server() + + if changeset.valid? do + {:ok, changeset} + else + {:error, changeset} + end + end + + defp validate_is_local(%Ecto.Changeset{changes: %{routing: :local}} = changeset) do + changeset + end + + defp validate_is_local(%Ecto.Changeset{} = changeset) do + changeset + |> Ecto.Changeset.add_error(:from, "you can only communicate with your home server") + end + + defp validate_home_server(%Ecto.Changeset{changes: %{from: from}} = changeset) do + identity = BirdyChat.Identity.identity() + + if String.starts_with?(from, identity) do + changeset + else + changeset + |> Ecto.Changeset.add_error(:from, "you can only communicate with your home server") + end + end + + defp validate_home_server(%Ecto.Changeset{} = changeset) do + changeset + end + + defp put_routing(%Ecto.Changeset{changes: %{to: to}} = changeset) do + identity = BirdyChat.Identity.identity() + + if String.starts_with?(to, identity) do + changeset + |> Ecto.Changeset.put_change(:routing, :local) + |> Ecto.Changeset.put_change(:server, identity) + else + server = + BirdyChat.Identity.peers() + |> Enum.find(fn {name, _url} -> String.starts_with?(to, name) end) + + case server do + {name, _url} -> + changeset + |> Ecto.Changeset.put_change(:routing, :remote) + |> Ecto.Changeset.put_change(:server, name) + + nil -> + changeset + |> Ecto.Changeset.add_error(:server, "unknown 'to' server") + end + end + end + + defp put_routing(%Ecto.Changeset{} = changeset) do + changeset + end end diff --git a/lib/birdy_chat/message_writer.ex b/lib/birdy_chat/message_writer.ex index b6f53aa..c9d565a 100644 --- a/lib/birdy_chat/message_writer.ex +++ b/lib/birdy_chat/message_writer.ex @@ -1,5 +1,7 @@ defmodule BirdyChat.MessageWriter do - @moduledoc false + @moduledoc """ + Simple file writer that stores messages in priv folder of Elixir application/release. + """ @spec write(%{to: String.t(), from: String.t(), message: String.t()}) :: :ok def write(message) do diff --git a/lib/birdy_chat_web.ex b/lib/birdy_chat_web.ex index 6d766a2..f38f95c 100644 --- a/lib/birdy_chat_web.ex +++ b/lib/birdy_chat_web.ex @@ -88,8 +88,8 @@ defmodule BirdyChatWeb do import BirdyChatWeb.CoreComponents # Common modules used in templates - alias Phoenix.LiveView.JS alias BirdyChatWeb.Layouts + alias Phoenix.LiveView.JS # Routes generation with the ~p sigil unquote(verified_routes()) diff --git a/lib/birdy_chat_web/api/messages/controller.ex b/lib/birdy_chat_web/api/messages/controller.ex index 79afd64..ee0693b 100644 --- a/lib/birdy_chat_web/api/messages/controller.ex +++ b/lib/birdy_chat_web/api/messages/controller.ex @@ -1,14 +1,23 @@ defmodule BirdyChatWeb.Api.Messages.Controller do + @moduledoc """ + The endpoint to be used by users from the "home server". + """ + use BirdyChatWeb, :controller def create(conn, params) do case BirdyChat.Message.validate(params) do {:ok, changeset} -> - case BirdyChat.MessageWriter.write(changeset.changes) do + case BirdyChat.Dispatcher.dispatch(changeset) do :ok -> conn |> put_status(:created) |> render(:create, message: changeset.changes) + + {:error, error} -> + conn + |> put_status(:unprocessable_entity) + |> render(:error, message: error) end {:error, changeset} -> diff --git a/lib/birdy_chat_web/api/messages/json.ex b/lib/birdy_chat_web/api/messages/json.ex index a238764..194bb7e 100644 --- a/lib/birdy_chat_web/api/messages/json.ex +++ b/lib/birdy_chat_web/api/messages/json.ex @@ -1,8 +1,14 @@ defmodule BirdyChatWeb.Api.Messages.JSON do + @moduledoc false + def render("create.json", %{message: message}) do message end + def render("error.json", %{message: message}) do + %{errors: %{"general" => Gettext.dgettext(BirdyChatWeb.Gettext, "errors", message, [])}} + end + def render("error.json", %{changeset: changeset}) do errors = Ecto.Changeset.traverse_errors(changeset, &get_error/1) %{errors: errors} diff --git a/lib/birdy_chat_web/api/server/internal/controller.ex b/lib/birdy_chat_web/api/server/internal/controller.ex new file mode 100644 index 0000000..c368d5b --- /dev/null +++ b/lib/birdy_chat_web/api/server/internal/controller.ex @@ -0,0 +1,49 @@ +defmodule BirdyChatWeb.Api.Server.Internal.Controller do + @moduledoc """ + A controller for handling inter-server communication. It started off with using Erlang term + format instead of JSON as communication language but then I removed it for the following + reasons: + + 1. The messages are mostly binaries anyway, there is no big efficiency gain from skipping JSON. + 2. Testing JSON is much easier than testing erlang term format. + 3. Erlang term format can give an illusion of extra security but unless the transport is HTTPS + then the communication is still inherently unsafe. + 4. Erlang term format is difficult to handle for unfamiliar developers, you need to remember + about safe conversion to avoid atom exhaustion attacks or sending an `rm -rf /` function over + the wire. + + The endpoint is protected by simple authentication that requires the secret key of all servers + being the same. It is good enough for a demo, but for any real application it would need to be + reconsidered. HTTPS would be a non-negotiable requirement for any user-facing deployment. + """ + + use BirdyChatWeb, :controller + + def create(conn, params) do + with true <- authorised?(conn.req_headers, params), + {:ok, changeset} <- BirdyChat.Message.validate_for_inter_server_use(params), + :ok <- BirdyChat.MessageWriter.write(changeset.changes) do + conn + |> put_status(:created) + |> render(:create, message: changeset.changes) + else + _any -> + conn + |> put_status(:forbidden) + |> render(:error, message: "Unauthorised") + end + end + + defp authorised?(headers, %{"from" => from}) do + case Enum.find(headers, fn {key, _value} -> key == "authorization" end) do + nil -> + false + + {"authorization", token} -> + case Phoenix.Token.verify(BirdyChatWeb.Endpoint, "serverAuth", token, max_age: 1200) do + {:ok, id} -> id == from + {:error, :invalid} -> false + end + end + end +end diff --git a/lib/birdy_chat_web/api/server/internal/json.ex b/lib/birdy_chat_web/api/server/internal/json.ex new file mode 100644 index 0000000..56352d7 --- /dev/null +++ b/lib/birdy_chat_web/api/server/internal/json.ex @@ -0,0 +1,20 @@ +defmodule BirdyChatWeb.Api.Server.Internal.JSON do + @moduledoc false + + def render("create.json", %{message: message}) do + message + end + + def render("error.json", %{message: message}) do + %{errors: %{"general" => Gettext.dgettext(BirdyChatWeb.Gettext, "errors", message, [])}} + end + + def render("error.json", %{changeset: changeset}) do + errors = Ecto.Changeset.traverse_errors(changeset, &get_error/1) + %{errors: errors} + end + + def get_error({msg, opts}) do + Gettext.dgettext(BirdyChatWeb.Gettext, "errors", msg, opts) + end +end diff --git a/lib/birdy_chat_web/channels/server_channel.ex b/lib/birdy_chat_web/channels/server_channel.ex deleted file mode 100644 index cb83c2c..0000000 --- a/lib/birdy_chat_web/channels/server_channel.ex +++ /dev/null @@ -1,35 +0,0 @@ -defmodule BirdyChatWeb.ServerChannel do - use BirdyChatWeb, :channel - - @impl true - def join("server:" <> server_id, payload, socket) do - if authorised?(payload, server_id) do - {:ok, socket} - else - {:error, %{reason: "unauthorised"}} - end - end - - @impl true - def handle_in("new_message", {:binary, message}, socket) do - result = :erlang.binary_to_term(message, [:safe]) - - # TODO: Add validation that this is the right server. - case BirdyChat.Message.validate(result) do - {:ok, changeset} -> - case BirdyChat.MessageWriter.write(changeset.changes) do - :ok -> - {:reply, {:ok, {:binary, message}}, socket} - end - end - end - - # Simple token-based authentication. Servers should use the same Phoenix secret key so they will - # have the basis for generating tokens. - defp authorised?(%{"token" => token}, server_id) do - case Phoenix.Token.verify(BirdyChatWeb.Endpoint, "serverAuth", token, max_age: 86400) do - {:ok, id} -> id == server_id - {:error, :invalid} -> false - end - end -end diff --git a/lib/birdy_chat_web/channels/server_socket.ex b/lib/birdy_chat_web/channels/server_socket.ex deleted file mode 100644 index 8843d1b..0000000 --- a/lib/birdy_chat_web/channels/server_socket.ex +++ /dev/null @@ -1,44 +0,0 @@ -defmodule BirdyChatWeb.ServerSocket do - use Phoenix.Socket - - # A Socket handler - # - # It's possible to control the websocket connection and - # assign values that can be accessed by your channel topics. - - ## Channels - - channel "server:*", BirdyChatWeb.ServerChannel - - # Socket params are passed from the client and can - # be used to verify and authenticate a user. After - # verification, you can put default assigns into - # the socket that will be set for all channels, ie - # - # {:ok, assign(socket, :user_id, verified_user_id)} - # - # To deny connection, return `:error` or `{:error, term}`. To control the - # response the client receives in that case, [define an error handler in the - # websocket - # configuration](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#socket/3-websocket-configuration). - # - # See `Phoenix.Token` documentation for examples in - # performing token verification on connect. - @impl true - def connect(_params, socket, _connect_info) do - {:ok, socket} - end - - # Socket IDs are topics that allow you to identify all sockets for a given user: - # - # def id(socket), do: "user_socket:#{socket.assigns.user_id}" - # - # Would allow you to broadcast a "disconnect" event and terminate - # all active sockets and channels for a given user: - # - # Elixir.BirdyChatWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) - # - # Returning `nil` makes this socket anonymous. - @impl true - def id(socket), do: "user_socket:#{socket.assigns.server_id}" -end diff --git a/lib/birdy_chat_web/components/core_components.ex b/lib/birdy_chat_web/components/core_components.ex index b50a31b..59a56ef 100644 --- a/lib/birdy_chat_web/components/core_components.ex +++ b/lib/birdy_chat_web/components/core_components.ex @@ -29,6 +29,7 @@ defmodule BirdyChatWeb.CoreComponents do use Phoenix.Component use Gettext, backend: BirdyChatWeb.Gettext + alias Phoenix.HTML.Form alias Phoenix.LiveView.JS @doc """ @@ -200,9 +201,7 @@ defmodule BirdyChatWeb.CoreComponents do def input(%{type: "checkbox"} = assigns) do assigns = - assign_new(assigns, :checked, fn -> - Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value]) - end) + assign_new(assigns, :checked, fn -> Form.normalize_value("checkbox", assigns[:value]) end) ~H"""
diff --git a/lib/birdy_chat_web/controllers/page_controller.ex b/lib/birdy_chat_web/controllers/page_controller.ex deleted file mode 100644 index 6350031..0000000 --- a/lib/birdy_chat_web/controllers/page_controller.ex +++ /dev/null @@ -1,7 +0,0 @@ -defmodule BirdyChatWeb.PageController do - use BirdyChatWeb, :controller - - def home(conn, _params) do - render(conn, :home) - end -end diff --git a/lib/birdy_chat_web/controllers/page_html.ex b/lib/birdy_chat_web/controllers/page_html.ex deleted file mode 100644 index fe2290c..0000000 --- a/lib/birdy_chat_web/controllers/page_html.ex +++ /dev/null @@ -1,10 +0,0 @@ -defmodule BirdyChatWeb.PageHTML do - @moduledoc """ - This module contains pages rendered by PageController. - - See the `page_html` directory for all templates available. - """ - use BirdyChatWeb, :html - - embed_templates "page_html/*" -end diff --git a/lib/birdy_chat_web/controllers/page_html/home.html.heex b/lib/birdy_chat_web/controllers/page_html/home.html.heex deleted file mode 100644 index b107fd0..0000000 --- a/lib/birdy_chat_web/controllers/page_html/home.html.heex +++ /dev/null @@ -1,202 +0,0 @@ - - -
-
- -
-

- Phoenix Framework - - v{Application.spec(:phoenix, :vsn)} - -

- -
- -

- Peace of mind from prototype to production. -

-

- Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale. -

- -
-
diff --git a/lib/birdy_chat_web/endpoint.ex b/lib/birdy_chat_web/endpoint.ex index 972f7ee..72a9ccf 100644 --- a/lib/birdy_chat_web/endpoint.ex +++ b/lib/birdy_chat_web/endpoint.ex @@ -15,10 +15,6 @@ defmodule BirdyChatWeb.Endpoint do websocket: [connect_info: [session: @session_options]], longpoll: [connect_info: [session: @session_options]] - socket "/socket", BirdyChatWeb.ServerSocket, - websocket: true, - longpoll: false - # Serve at "/" the static files from "priv/static" directory. # # When code reloading is disabled (e.g., in production), diff --git a/lib/birdy_chat_web/router.ex b/lib/birdy_chat_web/router.ex index 0dc6d3b..e4b210a 100644 --- a/lib/birdy_chat_web/router.ex +++ b/lib/birdy_chat_web/router.ex @@ -14,16 +14,11 @@ defmodule BirdyChatWeb.Router do plug :accepts, ["json"] end - scope "/", BirdyChatWeb do - pipe_through :browser - - get "/", PageController, :home - end - scope "/api", BirdyChatWeb.Api do pipe_through [:api] post "/messages", Messages.Controller, :create + post "/internal", Server.Internal.Controller, :create end # Other scopes may use custom stacks. @@ -31,7 +26,7 @@ defmodule BirdyChatWeb.Router do # pipe_through :api # end - # Enable LiveDashboard and Swoosh mailbox preview in development + # Enable LiveDashboard in development if Application.compile_env(:birdy_chat, :dev_routes) do # If you want to use the LiveDashboard in production, you should put # it behind authentication and allow only admins to access it. @@ -44,7 +39,6 @@ defmodule BirdyChatWeb.Router do pipe_through :browser live_dashboard "/dashboard", metrics: BirdyChatWeb.Telemetry - forward "/mailbox", Plug.Swoosh.MailboxPreview end end end diff --git a/mix.exs b/mix.exs index b72f908..ab10555 100644 --- a/mix.exs +++ b/mix.exs @@ -21,7 +21,7 @@ defmodule BirdyChat.MixProject do def application do [ mod: {BirdyChat.Application, []}, - extra_applications: [:logger, :runtime_tools, :opentelemetry] + extra_applications: [:logger, :runtime_tools] ] end @@ -56,7 +56,6 @@ defmodule BirdyChat.MixProject do app: false, compile: false, depth: 1}, - {:swoosh, "~> 1.16"}, {:req, "~> 0.5"}, {:telemetry_metrics, "~> 1.0"}, {:telemetry_poller, "~> 1.0"}, @@ -64,14 +63,10 @@ defmodule BirdyChat.MixProject do {:jason, "~> 1.2"}, {:dns_cluster, "~> 0.2.0"}, {:bandit, "~> 1.5"}, - {:credo, "~> 1.0", only: [:dev, :test]}, - # Phoenix sockets client - {:slipstream, "~> 1.0"}, - - # Telemetry - {:opentelemetry, "~> 1.0"}, - {:opentelemetry_exporter, "~> 1.0"} + # Static analysis tools + {:credo, "~> 1.0", only: [:dev, :test], runtime: false}, + {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false} ] end @@ -91,6 +86,7 @@ defmodule BirdyChat.MixProject do "esbuild birdy_chat --minify", "phx.digest" ], + build_release: ["setup", "assets.deploy", "release"], precommit: [ "compile --warnings-as-errors", "credo --strict", diff --git a/mix.lock b/mix.lock index 8f01ae6..e5ebd8e 100644 --- a/mix.lock +++ b/mix.lock @@ -1,37 +1,28 @@ %{ - "acceptor_pool": {:hex, :acceptor_pool, "1.0.1", "d88c2e8a0be9216cf513fbcd3e5a4beb36bee3ff4168e85d6152c6f899359cdb", [:rebar3], [], "hexpm", "f172f3d74513e8edd445c257d596fc84dbdd56d2c6fa287434269648ae5a421e"}, "bandit": {:hex, :bandit, "1.10.2", "d15ea32eb853b5b42b965b24221eb045462b2ba9aff9a0bda71157c06338cbff", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "27b2a61b647914b1726c2ced3601473be5f7aa6bb468564a688646a689b3ee45"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, - "chatterbox": {:hex, :ts_chatterbox, "0.15.1", "5cac4d15dd7ad61fc3c4415ce4826fc563d4643dee897a558ec4ea0b1c835c9c", [:rebar3], [{:hpack, "~> 0.3.0", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "4f75b91451338bc0da5f52f3480fa6ef6e3a2aeecfc33686d6b3d0a0948f31aa"}, "credo": {:hex, :credo, "1.7.16", "a9f1389d13d19c631cb123c77a813dbf16449a2aebf602f590defa08953309d4", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d0562af33756b21f248f066a9119e3890722031b6d199f22e3cf95550e4f1579"}, - "ctx": {:hex, :ctx, "0.6.0", "8ff88b70e6400c4df90142e7f130625b82086077a45364a78d208ed3ed53c7fe", [:rebar3], [], "hexpm", "a14ed2d1b67723dbebbe423b28d7615eb0bdcba6ff28f2d1f1b0a7e1d4aa5fc2"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, + "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, "ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, + "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"}, "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"}, "fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"}, "gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"}, - "gproc": {:hex, :gproc, "0.9.1", "f1df0364423539cf0b80e8201c8b1839e229e5f9b3ccb944c5834626998f5b8c", [:rebar3], [], "hexpm", "905088e32e72127ed9466f0bac0d8e65704ca5e73ee5a62cb073c3117916d507"}, - "grpcbox": {:hex, :grpcbox, "0.17.1", "6e040ab3ef16fe699ffb513b0ef8e2e896da7b18931a1ef817143037c454bcce", [:rebar3], [{:acceptor_pool, "~> 1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~> 0.15.1", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~> 0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~> 0.9.1", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "4a3b5d7111daabc569dc9cbd9b202a3237d81c80bf97212fbc676832cb0ceb17"}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]}, - "hpack": {:hex, :hpack_erl, "0.3.0", "2461899cc4ab6a0ef8e970c1661c5fc6a52d3c25580bc6dd204f84ce94669926", [:rebar3], [], "hexpm", "d6137d7079169d8c485c6962dfe261af5b9ef60fbc557344511c1e65e3d95fb0"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, - "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "lazy_html": {:hex, :lazy_html, "0.1.10", "ffe42a0b4e70859cf21a33e12a251e0c76c1dff76391609bd56702a0ef5bc429", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "50f67e5faa09d45a99c1ddf3fac004f051997877dc8974c5797bb5ccd8e27058"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, - "mint_web_socket": {:hex, :mint_web_socket, "1.0.5", "60354efeb49b1eccf95dfb75f55b08d692e211970fe735a5eb3188b328be2a90", [:mix], [{:mint, ">= 1.4.1 and < 2.0.0-0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "04b35663448fc758f3356cce4d6ac067ca418bbafe6972a3805df984b5f12e61"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "opentelemetry": {:hex, :opentelemetry, "1.7.0", "20d0f12d3d1c398d3670fd44fd1a7c495dd748ab3e5b692a7906662e2fb1a38a", [:rebar3], [{:opentelemetry_api, "~> 1.5.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "a9173b058c4549bf824cbc2f1d2fa2adc5cdedc22aa3f0f826951187bbd53131"}, - "opentelemetry_api": {:hex, :opentelemetry_api, "1.5.0", "1a676f3e3340cab81c763e939a42e11a70c22863f645aa06aafefc689b5550cf", [:mix, :rebar3], [], "hexpm", "f53ec8a1337ae4a487d43ac89da4bd3a3c99ddf576655d071deed8b56a2d5dda"}, - "opentelemetry_exporter": {:hex, :opentelemetry_exporter, "1.10.0", "972e142392dbfa679ec959914664adefea38399e4f56ceba5c473e1cabdbad79", [:rebar3], [{:grpcbox, ">= 0.0.0", [hex: :grpcbox, repo: "hexpm", optional: false]}, {:opentelemetry, "~> 1.7.0", [hex: :opentelemetry, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.5.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:tls_certificate_check, "~> 1.18", [hex: :tls_certificate_check, repo: "hexpm", optional: false]}], "hexpm", "33a116ed7304cb91783f779dec02478f887c87988077bfd72840f760b8d4b952"}, "phoenix": {:hex, :phoenix, "1.8.3", "49ac5e485083cb1495a905e47eb554277bdd9c65ccb4fc5100306b350151aa95", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "36169f95cc2e155b78be93d9590acc3f462f1e5438db06e6248613f27c80caec"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"}, "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, @@ -43,16 +34,11 @@ "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, - "slipstream": {:hex, :slipstream, "1.2.2", "6b07124ac5f62a50327aa38c84edd0284920ac8aba548e04738827838f233ed0", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mint_web_socket, "~> 0.2 or ~> 1.0", [hex: :mint_web_socket, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.1 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ccb873ddb21aadb37c5c7745014febe6da0aa2cef0c4e73e7d08ce11d18aacd0"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, - "swoosh": {:hex, :swoosh, "1.22.0", "0d65a95f89aedb5011af13295742294e309b4b4aaca556858d81e3b372b58abc", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c01ced23d8786d1ee1a03e4c16574290b2ccd6267beb8c81d081c4a34574ef6e"}, "tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"}, - "tls_certificate_check": {:hex, :tls_certificate_check, "1.31.0", "9a910b54d8cb96cc810cabf4c0129f21360f82022b20180849f1442a25ccbb04", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "9d2b41b128d5507bd8ad93e1a998e06d0ab2f9a772af343f4c00bf76c6be1532"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, } diff --git a/priv/messages/README.md b/priv/messages/README.md new file mode 100644 index 0000000..d677767 --- /dev/null +++ b/priv/messages/README.md @@ -0,0 +1 @@ +This folder is used for storing messages local to this server. The contents are otherwise ignored. diff --git a/rel/overlays/bin/server_1 b/rel/overlays/bin/server_1 new file mode 100755 index 0000000..a2ec8ca --- /dev/null +++ b/rel/overlays/bin/server_1 @@ -0,0 +1,12 @@ +#!/bin/sh +set -eu + +cd -P -- "$(dirname -- "$0")" + +export SECRET_KEY_BASE=Yhmq6FzYQt4g5AFHfSdMBKKf4oRo4KRo703FK6b7RwmH5pXlyQNompUOF7/EEC5t +export BIRDY_CHAT_PORT=4001 +export BIRDY_CHAT_IDENTITY=1 +export BIRDY_CHAT_PEERS="2::http://localhost:4002;3::http://localhost:4003" +export PHX_SERVER=true +export RELEASE_NAME=server_1 +exec ./birdy_chat start diff --git a/rel/overlays/bin/server_2 b/rel/overlays/bin/server_2 new file mode 100755 index 0000000..8572ff4 --- /dev/null +++ b/rel/overlays/bin/server_2 @@ -0,0 +1,12 @@ +#!/bin/sh +set -eu + +cd -P -- "$(dirname -- "$0")" + +export SECRET_KEY_BASE=Yhmq6FzYQt4g5AFHfSdMBKKf4oRo4KRo703FK6b7RwmH5pXlyQNompUOF7/EEC5t +export BIRDY_CHAT_PORT=4002 +export BIRDY_CHAT_IDENTITY=2 +export BIRDY_CHAT_PEERS="1::http://localhost:4001g;3::http://localhost:4003" +export PHX_SERVER=true +export RELEASE_NAME=server_2 +exec ./birdy_chat start diff --git a/rel/overlays/bin/server_3 b/rel/overlays/bin/server_3 new file mode 100755 index 0000000..21cb303 --- /dev/null +++ b/rel/overlays/bin/server_3 @@ -0,0 +1,12 @@ +#!/bin/sh +set -eu + +cd -P -- "$(dirname -- "$0")" + +export SECRET_KEY_BASE=Yhmq6FzYQt4g5AFHfSdMBKKf4oRo4KRo703FK6b7RwmH5pXlyQNompUOF7/EEC5t +export BIRDY_CHAT_PORT=4003 +export BIRDY_CHAT_IDENTITY=3 +export BIRDY_CHAT_PEERS="1::http://localhost:4001;2::http://localhost:4002" +export PHX_SERVER=true +export RELEASE_NAME=server_3 +exec ./birdy_chat start diff --git a/test/birdy_chat/identity_test.exs b/test/birdy_chat/identity_test.exs index 33bd710..e424e3a 100644 --- a/test/birdy_chat/identity_test.exs +++ b/test/birdy_chat/identity_test.exs @@ -1,5 +1,5 @@ defmodule BirdyChat.IdentityTest do - use BirdyChat.DataCase + use BirdyChat.DataCase, async: true setup do registry = start_supervised!({Registry, keys: :unique, name: __MODULE__}) @@ -11,7 +11,7 @@ defmodule BirdyChat.IdentityTest do name = {:via, Registry, {__MODULE__, "identity"}} {:ok, process} = BirdyChat.Identity.start_link(name: name) mode = BirdyChat.Identity.mode(process) - assert mode == :singleton + assert mode == :test end test "can be started by giving values like environment variables" do diff --git a/test/birdy_chat/peer_test.exs b/test/birdy_chat/peer_test.exs new file mode 100644 index 0000000..600db31 --- /dev/null +++ b/test/birdy_chat/peer_test.exs @@ -0,0 +1,3 @@ +defmodule BirdyChat.PeerTest do + use BirdyChat.DataCase, async: true +end diff --git a/test/birdy_chat_web/api/internal/messages_test.exs b/test/birdy_chat_web/api/internal/messages_test.exs new file mode 100644 index 0000000..60b85c7 --- /dev/null +++ b/test/birdy_chat_web/api/internal/messages_test.exs @@ -0,0 +1,82 @@ +defmodule BirdyChatWeb.Api.Internal.MessagesTest do + use BirdyChatWeb.ConnCase, async: true + + setup do + url = ~p"/api/internal" + %{url: url} + end + + describe "authorisation" do + setup %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + + %{conn: conn} + end + + test "returns 403 when authorisation token is missing", %{url: url, conn: conn} do + message = %{"from" => "test2-user", "to" => "test2-someone", "message" => "123"} + body = Jason.encode!(message) + conn = post(conn, url, body) + assert result = json_response(conn, :forbidden) + assert result == %{"errors" => %{"general" => "Unauthorised"}} + end + + test "returns 403 when authorisation token is invalid", %{url: url, conn: conn} do + server_id = "test2-user" + token = Phoenix.Token.sign(BirdyChatWeb.Endpoint, "FakeAuth", server_id) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> put_req_header("authorization", token) + + message = %{"from" => "test2-user", "to" => "test2-someone", "message" => "123"} + body = Jason.encode!(message) + conn = post(conn, url, body) + assert result = json_response(conn, :forbidden) + assert result == %{"errors" => %{"general" => "Unauthorised"}} + end + end + + describe "POST /api/server/messages" do + setup %{conn: conn} do + unique_user_id = System.unique_integer([:positive]) + username = "test1-user#{unique_user_id}" + + path = Application.app_dir(:birdy_chat, ["priv", "messages", "#{username}.txt"]) + on_exit(fn -> File.rm(path) end) + + server_id = "test2-user" + token = Phoenix.Token.sign(BirdyChatWeb.Endpoint, "serverAuth", server_id) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> put_req_header("authorization", token) + + %{conn: conn, username: username, path: path} + end + + test "saves message locally", %{conn: conn, username: username, url: url, path: path} do + message = %{"from" => "test2-user", "to" => username, "message" => "123"} + body = Jason.encode!(message) + conn = post(conn, url, body) + assert result = json_response(conn, :created) + + expected_result = %{ + "from" => "test2-user", + "message" => "123", + "to" => username, + "routing" => "local", + "server" => "test1" + } + + assert result == expected_result + + contents = File.read!(path) + assert contents == "test2-user: 123\n" + end + end +end diff --git a/test/birdy_chat_web/api/messages_test.exs b/test/birdy_chat_web/api/messages_test.exs index ec2b102..e945288 100644 --- a/test/birdy_chat_web/api/messages_test.exs +++ b/test/birdy_chat_web/api/messages_test.exs @@ -1,17 +1,81 @@ defmodule BirdyChatWeb.Api.MessagesTest do - use BirdyChatWeb.ConnCase + use BirdyChatWeb.ConnCase, async: true setup %{conn: conn} do url = ~p"/api/messages" - path = Application.app_dir(:birdy_chat, ["priv", "messages", "test-user.txt"]) + unique_user_id = System.unique_integer([:positive]) + username = "test1-user#{unique_user_id}" + + path = Application.app_dir(:birdy_chat, ["priv", "messages", "#{username}.txt"]) on_exit(fn -> File.rm(path) end) conn = conn |> put_req_header("content-type", "application/json") - %{conn: conn, url: url} + %{conn: conn, url: url, username: username, path: path} + end + + describe "POST /api/messages to other server" do + test "returns error when the peer is down", %{conn: conn, url: url} do + message = %{from: "test1-user", to: "test2-user", message: "123"} + Req.Test.expect(BirdyChat.Dispatcher, &Req.Test.transport_error(&1, :econnrefused)) + + payload = Jason.encode!(message) + conn = post(conn, url, payload) + assert result = json_response(conn, :unprocessable_entity) + assert result == %{"errors" => %{"general" => "peer test2 is unreachable"}} + end + + test "returns error when a peer is unknown", %{conn: conn, url: url} do + message = %{from: "test1-user", to: "fake-user", message: "123"} + + payload = Jason.encode!(message) + conn = post(conn, url, payload) + assert result = json_response(conn, :unprocessable_entity) + + expected_result = %{"errors" => %{"server" => ["unknown 'to' server"]}} + + assert result == expected_result + end + + test "send message to the other server via HTTP", %{conn: conn, url: url} do + message = %{from: "test1-user", to: "test2-user", message: "123"} + + string_message = %{ + "from" => "test1-user", + "message" => "123", + "to" => "test2-user", + "routing" => "remote", + "server" => "test2" + } + + Req.Test.expect(BirdyChat.Dispatcher, fn conn -> + expected_body_params = %{ + "from" => "test1-user", + "message" => "123", + "routing" => "remote", + "server" => "test2", + "to" => "test2-user" + } + + {"authorization", token} = + Enum.find(conn.req_headers, fn {key, _v} -> key == "authorization" end) + + {:ok, "test1-user"} = + Phoenix.Token.verify(BirdyChatWeb.Endpoint, "serverAuth", token, max_age: 1200) + + assert conn.body_params == expected_body_params + resp = Jason.encode!(expected_body_params) + Plug.Conn.send_resp(conn, :created, resp) + end) + + payload = Jason.encode!(message) + conn = post(conn, url, payload) + assert result = json_response(conn, :created) + assert result == string_message + end end describe "POST /api/messages" do @@ -31,39 +95,70 @@ defmodule BirdyChatWeb.Api.MessagesTest do assert result == expected_result end - test "returns message and 201 when successful", %{conn: conn, url: url} do - message = %{"from" => "2-user", "to" => "1-user", "message" => "123"} + test "returns error when you post a message to other server", %{conn: conn, url: url} do + message = %{"from" => "nothomeserver-user", "to" => "test1-user", "message" => "123"} + payload = Jason.encode!(message) conn = post(conn, url, payload) - assert result = json_response(conn, :created) - assert result == message + assert result = json_response(conn, :unprocessable_entity) + + expected_result = %{ + "errors" => %{"from" => ["you can only communicate with your home server"]} + } + + assert result == expected_result end - test "writes message to file", %{conn: conn, url: url} do - message = %{"from" => "2-user", "to" => "test-user", "message" => "123"} + test "returns message and 201 when successful", %{conn: conn, url: url, username: username} do + message = %{"from" => "test1-user", "to" => username, "message" => "123"} + payload = Jason.encode!(message) conn = post(conn, url, payload) assert result = json_response(conn, :created) - assert result == message - path = Application.app_dir(:birdy_chat, ["priv", "messages", "test-user.txt"]) + expected_result = %{ + "from" => "test1-user", + "to" => username, + "message" => "123", + "routing" => "local", + "server" => "test1" + } + + assert result == expected_result + end + + test "writes message to file", %{conn: conn, url: url, username: username, path: path} do + message = %{"from" => "test1-user", "to" => username, "message" => "123"} + payload = Jason.encode!(message) + conn = post(conn, url, payload) + assert result = json_response(conn, :created) + + expected_result = %{ + "from" => "test1-user", + "to" => username, + "message" => "123", + "routing" => "local", + "server" => "test1" + } + + assert result == expected_result + contents = File.read!(path) - assert contents == "2-user: 123\n" + assert contents == "test1-user: 123\n" end - test "appends message to file", %{conn: conn, url: url} do - message = %{"from" => "2-user", "to" => "test-user", "message" => "123"} + test "appends message to file", %{conn: conn, url: url, username: username, path: path} do + message = %{"from" => "test1-user", "to" => username, "message" => "123"} payload = Jason.encode!(message) post(conn, url, payload) - message = %{"from" => "2-user", "to" => "test-user", "message" => "456"} + message = %{"from" => "test1-user", "to" => username, "message" => "456"} payload = Jason.encode!(message) conn = post(conn, url, payload) assert json_response(conn, :created) - path = Application.app_dir(:birdy_chat, ["priv", "messages", "test-user.txt"]) contents = File.read!(path) - assert contents == "2-user: 123\n2-user: 456\n" + assert contents == "test1-user: 123\ntest1-user: 456\n" end end end diff --git a/test/birdy_chat_web/channels/server_channel_test.exs b/test/birdy_chat_web/channels/server_channel_test.exs deleted file mode 100644 index 5a4fe8d..0000000 --- a/test/birdy_chat_web/channels/server_channel_test.exs +++ /dev/null @@ -1,81 +0,0 @@ -defmodule BirdyChatWeb.ServerChannelTest do - use BirdyChatWeb.ChannelCase - - setup do - server_id = "test2" - token = Phoenix.Token.sign(BirdyChatWeb.Endpoint, "serverAuth", server_id) - - path = Application.app_dir(:birdy_chat, ["priv", "messages", "test-user.txt"]) - on_exit(fn -> File.rm(path) end) - - {:ok, _, socket} = - BirdyChatWeb.ServerSocket - |> socket("server_socket", %{server_id: server_id}) - |> subscribe_and_join(BirdyChatWeb.ServerChannel, "server:#{server_id}", %{token: token}) - - %{socket: socket} - end - - describe "authorization" do - test "requires a signed token to join a channel" do - server_id = "test2" - token = Phoenix.Token.sign(BirdyChatWeb.Endpoint, "fakeAuth", server_id) - - result = - BirdyChatWeb.ServerSocket - |> socket("server_socket", %{server_id: server_id}) - |> subscribe_and_join(BirdyChatWeb.ServerChannel, "server:#{server_id}", %{token: token}) - - assert result == {:error, %{reason: "unauthorised"}} - end - - test "requires server id in token and in channel to match" do - server_id = "test2" - token = Phoenix.Token.sign(BirdyChatWeb.Endpoint, "serverAuth", server_id) - - result = - BirdyChatWeb.ServerSocket - |> socket("server_socket", %{server_id: server_id}) - |> subscribe_and_join(BirdyChatWeb.ServerChannel, "server:fake", %{token: token}) - - assert result == {:error, %{reason: "unauthorised"}} - end - end - - describe "new_message" do - test "receives message from other server and saves it locally", %{socket: socket} do - message = %{from: "test2-user", to: "test-user", message: "123"} - bin = :erlang.term_to_binary(message) - ref = push(socket, "new_message", {:binary, bin}) - assert_reply ref, :ok, {:binary, ^bin} - - path = Application.app_dir(:birdy_chat, ["priv", "messages", "test-user.txt"]) - contents = File.read!(path) - assert contents == "test2-user: 123\n" - end - - test "blows up when sent payload is not safe to decode", %{socket: socket} do - # This unsafe binary is taken from erlang docs directly: - bin = <<131, 119, 8, "tjenixen">> - Process.flag(:trap_exit, true) - push(socket, "new_message", {:binary, bin}) - - assert_receive {:EXIT, _pid, - {:badarg, - [ - {:erlang, :binary_to_term, - [<<131, 119, 8, 116, 106, 101, 110, 105, 120, 101, 110>>, [:safe]], - [error_info: %{module: :erl_erts_errors}]}, - {BirdyChatWeb.ServerChannel, :handle_in, 3, - [file: ~c"lib/birdy_chat_web/channels/server_channel.ex", line: 15]}, - {Phoenix.Channel.Server, :handle_info, 2, - [file: ~c"lib/phoenix/channel/server.ex", line: 332]}, - {:gen_server, :try_handle_info, 3, - [file: ~c"gen_server.erl", line: 2434]}, - {:gen_server, :handle_msg, 3, [file: ~c"gen_server.erl", line: 2420]}, - {:proc_lib, :init_p_do_apply, 3, [file: ~c"proc_lib.erl", line: 333]} - ]}}, - 100 - end - end -end diff --git a/test/birdy_chat_web/controllers/page_controller_test.exs b/test/birdy_chat_web/controllers/page_controller_test.exs deleted file mode 100644 index dd8766c..0000000 --- a/test/birdy_chat_web/controllers/page_controller_test.exs +++ /dev/null @@ -1,8 +0,0 @@ -defmodule BirdyChatWeb.PageControllerTest do - use BirdyChatWeb.ConnCase - - test "GET /", %{conn: conn} do - conn = get(conn, ~p"/") - assert html_response(conn, 200) =~ "Peace of mind from prototype to production" - end -end diff --git a/test/support/channel_case.ex b/test/support/channel_case.ex deleted file mode 100644 index 0cd840c..0000000 --- a/test/support/channel_case.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule BirdyChatWeb.ChannelCase do - @moduledoc """ - This module defines the test case to be used by - channel tests. - - Such tests rely on `Phoenix.ChannelTest` and also - import other functionality to make it easier - to build common data structures and query the data layer. - - Finally, if the test case interacts with the database, - we enable the SQL sandbox, so changes done to the database - are reverted at the end of every test. If you are using - PostgreSQL, you can even run database tests asynchronously - by setting `use BirdyChatWeb.ChannelCase, async: true`, although - this option is not recommended for other databases. - """ - - use ExUnit.CaseTemplate - - using do - quote do - # Import conveniences for testing with channels - import Phoenix.ChannelTest - import BirdyChatWeb.ChannelCase - - # The default endpoint for testing - @endpoint BirdyChatWeb.Endpoint - end - end - - setup _tags do - :ok - end -end