Compare commits
No commits in common. "64e1deca92b62fdfa2b03214020bb9101f299354" and "3afaf346c7a00b9561a9b0284bc525257a1f4b0a" have entirely different histories.
64e1deca92
...
3afaf346c7
38 changed files with 541 additions and 559 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -31,13 +31,8 @@ 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
|
||||
|
|
|
|||
66
README.md
66
README.md
|
|
@ -1,60 +1,18 @@
|
|||
# BirdyChat Tech Challenge
|
||||
# BirdyChat
|
||||
|
||||
This repository implements BirdyChat tech challenge.
|
||||
To start your Phoenix server:
|
||||
|
||||
# Start here
|
||||
* Run `mix setup` to install and setup dependencies
|
||||
* Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
|
||||
|
||||
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:
|
||||
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
|
||||
|
||||
```
|
||||
MIX_ENV=prod mix build_release
|
||||
```
|
||||
Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
|
||||
|
||||
Then you can run 3 servers connected to one another:
|
||||
## Learn more
|
||||
|
||||
`_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.
|
||||
* 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
|
||||
|
|
|
|||
|
|
@ -21,6 +21,15 @@ 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",
|
||||
|
|
|
|||
|
|
@ -77,3 +77,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@ 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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("BIRDY_CHAT_PORT", "4000"))]
|
||||
http: [port: String.to_integer(System.get_env("PORT", "4000"))]
|
||||
|
||||
if config_env() == :prod do
|
||||
# The secret key base is used to sign/encrypt cookies and other secrets.
|
||||
|
|
@ -82,4 +82,22 @@ 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
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@ 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
|
||||
|
||||
|
|
@ -17,9 +23,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,15 +1,4 @@
|
|||
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]
|
||||
|
|
@ -60,18 +49,7 @@ defmodule BirdyChat.Identity do
|
|||
peers = System.get_env("BIRDY_CHAT_PEERS")
|
||||
|
||||
case {identity, peers} do
|
||||
{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}
|
||||
{nil, nil} -> %__MODULE__{identity: "test", peers: [], mode: :singleton}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
3
lib/birdy_chat/mailer.ex
Normal file
3
lib/birdy_chat/mailer.ex
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
defmodule BirdyChat.Mailer do
|
||||
use Swoosh.Mailer, otp_app: :birdy_chat
|
||||
end
|
||||
|
|
@ -1,39 +1,10 @@
|
|||
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
|
||||
|
||||
@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}
|
||||
else
|
||||
{:error, changeset}
|
||||
end
|
||||
end
|
||||
|
||||
def validate(params) do
|
||||
|
|
@ -41,8 +12,6 @@ defmodule BirdyChat.Message do
|
|||
%__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}
|
||||
|
|
@ -50,57 +19,4 @@ defmodule BirdyChat.Message do
|
|||
{: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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
defmodule BirdyChat.MessageWriter do
|
||||
@moduledoc """
|
||||
Simple file writer that stores messages in priv folder of Elixir application/release.
|
||||
"""
|
||||
@moduledoc false
|
||||
|
||||
@spec write(%{to: String.t(), from: String.t(), message: String.t()}) :: :ok
|
||||
def write(message) do
|
||||
|
|
|
|||
|
|
@ -88,8 +88,8 @@ defmodule BirdyChatWeb do
|
|||
import BirdyChatWeb.CoreComponents
|
||||
|
||||
# Common modules used in templates
|
||||
alias BirdyChatWeb.Layouts
|
||||
alias Phoenix.LiveView.JS
|
||||
alias BirdyChatWeb.Layouts
|
||||
|
||||
# Routes generation with the ~p sigil
|
||||
unquote(verified_routes())
|
||||
|
|
|
|||
|
|
@ -1,23 +1,14 @@
|
|||
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.Dispatcher.dispatch(changeset) do
|
||||
case BirdyChat.MessageWriter.write(changeset.changes) 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} ->
|
||||
|
|
|
|||
|
|
@ -1,14 +1,8 @@
|
|||
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}
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
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
|
||||
35
lib/birdy_chat_web/channels/server_channel.ex
Normal file
35
lib/birdy_chat_web/channels/server_channel.ex
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
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
|
||||
44
lib/birdy_chat_web/channels/server_socket.ex
Normal file
44
lib/birdy_chat_web/channels/server_socket.ex
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
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
|
||||
|
|
@ -29,7 +29,6 @@ defmodule BirdyChatWeb.CoreComponents do
|
|||
use Phoenix.Component
|
||||
use Gettext, backend: BirdyChatWeb.Gettext
|
||||
|
||||
alias Phoenix.HTML.Form
|
||||
alias Phoenix.LiveView.JS
|
||||
|
||||
@doc """
|
||||
|
|
@ -201,7 +200,9 @@ defmodule BirdyChatWeb.CoreComponents do
|
|||
|
||||
def input(%{type: "checkbox"} = assigns) do
|
||||
assigns =
|
||||
assign_new(assigns, :checked, fn -> Form.normalize_value("checkbox", assigns[:value]) end)
|
||||
assign_new(assigns, :checked, fn ->
|
||||
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
|
||||
end)
|
||||
|
||||
~H"""
|
||||
<div class="fieldset mb-2">
|
||||
|
|
|
|||
7
lib/birdy_chat_web/controllers/page_controller.ex
Normal file
7
lib/birdy_chat_web/controllers/page_controller.ex
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
defmodule BirdyChatWeb.PageController do
|
||||
use BirdyChatWeb, :controller
|
||||
|
||||
def home(conn, _params) do
|
||||
render(conn, :home)
|
||||
end
|
||||
end
|
||||
10
lib/birdy_chat_web/controllers/page_html.ex
Normal file
10
lib/birdy_chat_web/controllers/page_html.ex
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
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
|
||||
202
lib/birdy_chat_web/controllers/page_html/home.html.heex
Normal file
202
lib/birdy_chat_web/controllers/page_html/home.html.heex
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
<Layouts.flash_group flash={@flash} />
|
||||
<div class="left-[40rem] fixed inset-y-0 right-0 z-0 hidden lg:block xl:left-[50rem]">
|
||||
<svg
|
||||
viewBox="0 0 1480 957"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
class="absolute inset-0 h-full w-full"
|
||||
preserveAspectRatio="xMinYMid slice"
|
||||
>
|
||||
<path fill="#EE7868" d="M0 0h1480v957H0z" />
|
||||
<path
|
||||
d="M137.542 466.27c-582.851-48.41-988.806-82.127-1608.412 658.2l67.39 810 3083.15-256.51L1535.94-49.622l-98.36 8.183C1269.29 281.468 734.115 515.799 146.47 467.012l-8.928-.742Z"
|
||||
fill="#FF9F92"
|
||||
/>
|
||||
<path
|
||||
d="M371.028 528.664C-169.369 304.988-545.754 149.198-1361.45 665.565l-182.58 792.025 3014.73 694.98 389.42-1689.25-96.18-22.171C1505.28 697.438 924.153 757.586 379.305 532.09l-8.277-3.426Z"
|
||||
fill="#FA8372"
|
||||
/>
|
||||
<path
|
||||
d="M359.326 571.714C-104.765 215.795-428.003-32.102-1349.55 255.554l-282.3 1224.596 3047.04 722.01 312.24-1354.467C1411.25 1028.3 834.355 935.995 366.435 577.166l-7.109-5.452Z"
|
||||
fill="#E96856"
|
||||
fill-opacity=".6"
|
||||
/>
|
||||
<path
|
||||
d="M1593.87 1236.88c-352.15 92.63-885.498-145.85-1244.602-613.557l-5.455-7.105C-12.347 152.31-260.41-170.8-1225-131.458l-368.63 1599.048 3057.19 704.76 130.31-935.47Z"
|
||||
fill="#C42652"
|
||||
fill-opacity=".2"
|
||||
/>
|
||||
<path
|
||||
d="M1411.91 1526.93c-363.79 15.71-834.312-330.6-1085.883-863.909l-3.822-8.102C72.704 125.95-101.074-242.476-1052.01-408.907l-699.85 1484.267 2837.75 1338.01 326.02-886.44Z"
|
||||
fill="#A41C42"
|
||||
fill-opacity=".2"
|
||||
/>
|
||||
<path
|
||||
d="M1116.26 1863.69c-355.457-78.98-720.318-535.27-825.287-1115.521l-1.594-8.816C185.286 163.833 112.786-237.016-762.678-643.898L-1822.83 608.665 571.922 2635.55l544.338-771.86Z"
|
||||
fill="#A41C42"
|
||||
fill-opacity=".2"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="px-4 py-10 sm:px-6 sm:py-28 lg:px-8 xl:px-28 xl:py-32">
|
||||
<div class="mx-auto max-w-xl lg:mx-0">
|
||||
<svg viewBox="0 0 71 48" class="h-12" aria-hidden="true">
|
||||
<path
|
||||
d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z"
|
||||
fill="#FD4F00"
|
||||
/>
|
||||
</svg>
|
||||
<div class="mt-10 flex justify-between items-center">
|
||||
<h1 class="flex items-center text-sm font-semibold leading-6">
|
||||
Phoenix Framework
|
||||
<small class="badge badge-warning badge-sm ml-3">
|
||||
v{Application.spec(:phoenix, :vsn)}
|
||||
</small>
|
||||
</h1>
|
||||
<Layouts.theme_toggle />
|
||||
</div>
|
||||
|
||||
<p class="text-[2rem] mt-4 font-semibold leading-10 tracking-tighter text-balance">
|
||||
Peace of mind from prototype to production.
|
||||
</p>
|
||||
<p class="mt-4 leading-7 text-base-content/70">
|
||||
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.
|
||||
</p>
|
||||
<div class="flex">
|
||||
<div class="w-full sm:w-auto">
|
||||
<div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-3">
|
||||
<a
|
||||
href="https://hexdocs.pm/phoenix/overview.html"
|
||||
class="group relative rounded-box px-6 py-4 text-sm font-semibold leading-6 sm:py-6"
|
||||
>
|
||||
<span class="absolute inset-0 rounded-box bg-base-200 transition group-hover:bg-base-300 sm:group-hover:scale-105">
|
||||
</span>
|
||||
<span class="relative flex items-center gap-4 sm:flex-col">
|
||||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" class="h-6 w-6">
|
||||
<path d="m12 4 10-2v18l-10 2V4Z" fill="currentColor" fill-opacity=".15" />
|
||||
<path
|
||||
d="M12 4 2 2v18l10 2m0-18v18m0-18 10-2v18l-10 2"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Guides & Docs
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/phoenixframework/phoenix"
|
||||
class="group relative rounded-box px-6 py-4 text-sm font-semibold leading-6 sm:py-6"
|
||||
>
|
||||
<span class="absolute inset-0 rounded-box bg-base-200 transition group-hover:bg-base-300 sm:group-hover:scale-105">
|
||||
</span>
|
||||
<span class="relative flex items-center gap-4 sm:flex-col">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" class="h-6 w-6">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 0C5.37 0 0 5.506 0 12.303c0 5.445 3.435 10.043 8.205 11.674.6.107.825-.262.825-.585 0-.292-.015-1.261-.015-2.291C6 21.67 5.22 20.346 4.98 19.654c-.135-.354-.72-1.446-1.23-1.738-.42-.23-1.02-.8-.015-.815.945-.015 1.62.892 1.845 1.261 1.08 1.86 2.805 1.338 3.495 1.015.105-.8.42-1.338.765-1.645-2.67-.308-5.46-1.37-5.46-6.075 0-1.338.465-2.446 1.23-3.307-.12-.308-.54-1.569.12-3.26 0 0 1.005-.323 3.3 1.26.96-.276 1.98-.415 3-.415s2.04.139 3 .416c2.295-1.6 3.3-1.261 3.3-1.261.66 1.691.24 2.952.12 3.26.765.861 1.23 1.953 1.23 3.307 0 4.721-2.805 5.767-5.475 6.075.435.384.81 1.122.81 2.276 0 1.645-.015 2.968-.015 3.383 0 .323.225.707.825.585a12.047 12.047 0 0 0 5.919-4.489A12.536 12.536 0 0 0 24 12.304C24 5.505 18.63 0 12 0Z"
|
||||
/>
|
||||
</svg>
|
||||
Source Code
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href={"https://github.com/phoenixframework/phoenix/blob/v#{Application.spec(:phoenix, :vsn)}/CHANGELOG.md"}
|
||||
class="group relative rounded-box px-6 py-4 text-sm font-semibold leading-6 sm:py-6"
|
||||
>
|
||||
<span class="absolute inset-0 rounded-box bg-base-200 transition group-hover:bg-base-300 sm:group-hover:scale-105">
|
||||
</span>
|
||||
<span class="relative flex items-center gap-4 sm:flex-col">
|
||||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" class="h-6 w-6">
|
||||
<path
|
||||
d="M12 1v6M12 17v6"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="4"
|
||||
fill="currentColor"
|
||||
fill-opacity=".15"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Changelog
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-10 grid grid-cols-1 gap-y-4 text-sm leading-6 text-base-content/80 sm:grid-cols-2">
|
||||
<div>
|
||||
<a
|
||||
href="https://elixirforum.com"
|
||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
aria-hidden="true"
|
||||
class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
|
||||
>
|
||||
<path d="M8 13.833c3.866 0 7-2.873 7-6.416C15 3.873 11.866 1 8 1S1 3.873 1 7.417c0 1.081.292 2.1.808 2.995.606 1.05.806 2.399.086 3.375l-.208.283c-.285.386-.01.905.465.85.852-.098 2.048-.318 3.137-.81a3.717 3.717 0 0 1 1.91-.318c.263.027.53.041.802.041Z" />
|
||||
</svg>
|
||||
Discuss on the Elixir Forum
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://discord.gg/elixir"
|
||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
aria-hidden="true"
|
||||
class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
|
||||
>
|
||||
<path d="M13.545 2.995c-1.02-.46-2.114-.8-3.257-.994a.05.05 0 0 0-.052.024c-.141.246-.297.567-.406.82a12.377 12.377 0 0 0-3.658 0 8.238 8.238 0 0 0-.412-.82.052.052 0 0 0-.052-.024 13.315 13.315 0 0 0-3.257.994.046.046 0 0 0-.021.018C.356 6.063-.213 9.036.066 11.973c.001.015.01.029.02.038a13.353 13.353 0 0 0 3.996 1.987.052.052 0 0 0 .056-.018c.308-.414.582-.85.818-1.309a.05.05 0 0 0-.028-.069 8.808 8.808 0 0 1-1.248-.585.05.05 0 0 1-.005-.084c.084-.062.168-.126.248-.191a.05.05 0 0 1 .051-.007c2.619 1.176 5.454 1.176 8.041 0a.05.05 0 0 1 .053.006c.08.065.164.13.248.192a.05.05 0 0 1-.004.084c-.399.23-.813.423-1.249.585a.05.05 0 0 0-.027.07c.24.457.514.893.817 1.307a.051.051 0 0 0 .056.019 13.31 13.31 0 0 0 4.001-1.987.05.05 0 0 0 .021-.037c.334-3.396-.559-6.345-2.365-8.96a.04.04 0 0 0-.021-.02Zm-8.198 7.19c-.789 0-1.438-.712-1.438-1.587 0-.874.637-1.586 1.438-1.586.807 0 1.45.718 1.438 1.586 0 .875-.637 1.587-1.438 1.587Zm5.316 0c-.788 0-1.438-.712-1.438-1.587 0-.874.637-1.586 1.438-1.586.807 0 1.45.718 1.438 1.586 0 .875-.63 1.587-1.438 1.587Z" />
|
||||
</svg>
|
||||
Join our Discord server
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://elixir-slack.community/"
|
||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
aria-hidden="true"
|
||||
class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
|
||||
>
|
||||
<path d="M3.361 10.11a1.68 1.68 0 1 1-1.68-1.681h1.68v1.682ZM4.209 10.11a1.68 1.68 0 1 1 3.361 0v4.21a1.68 1.68 0 1 1-3.361 0v-4.21ZM5.89 3.361a1.68 1.68 0 1 1 1.681-1.68v1.68H5.89ZM5.89 4.209a1.68 1.68 0 1 1 0 3.361H1.68a1.68 1.68 0 1 1 0-3.361h4.21ZM12.639 5.89a1.68 1.68 0 1 1 1.68 1.681h-1.68V5.89ZM11.791 5.89a1.68 1.68 0 1 1-3.361 0V1.68a1.68 1.68 0 0 1 3.361 0v4.21ZM10.11 12.639a1.68 1.68 0 1 1-1.681 1.68v-1.68h1.682ZM10.11 11.791a1.68 1.68 0 1 1 0-3.361h4.21a1.68 1.68 0 1 1 0 3.361h-4.21Z" />
|
||||
</svg>
|
||||
Join us on Slack
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://fly.io/docs/elixir/getting-started/"
|
||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
|
||||
>
|
||||
<path d="M1 12.5A4.5 4.5 0 005.5 17H15a4 4 0 001.866-7.539 3.504 3.504 0 00-4.504-4.272A4.5 4.5 0 004.06 8.235 4.502 4.502 0 001 12.5z" />
|
||||
</svg>
|
||||
Deploy your application
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -15,6 +15,10 @@ 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),
|
||||
|
|
|
|||
|
|
@ -14,11 +14,16 @@ 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.
|
||||
|
|
@ -26,7 +31,7 @@ defmodule BirdyChatWeb.Router do
|
|||
# pipe_through :api
|
||||
# end
|
||||
|
||||
# Enable LiveDashboard in development
|
||||
# Enable LiveDashboard and Swoosh mailbox preview 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.
|
||||
|
|
@ -39,6 +44,7 @@ defmodule BirdyChatWeb.Router do
|
|||
pipe_through :browser
|
||||
|
||||
live_dashboard "/dashboard", metrics: BirdyChatWeb.Telemetry
|
||||
forward "/mailbox", Plug.Swoosh.MailboxPreview
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
14
mix.exs
14
mix.exs
|
|
@ -21,7 +21,7 @@ defmodule BirdyChat.MixProject do
|
|||
def application do
|
||||
[
|
||||
mod: {BirdyChat.Application, []},
|
||||
extra_applications: [:logger, :runtime_tools]
|
||||
extra_applications: [:logger, :runtime_tools, :opentelemetry]
|
||||
]
|
||||
end
|
||||
|
||||
|
|
@ -56,6 +56,7 @@ defmodule BirdyChat.MixProject do
|
|||
app: false,
|
||||
compile: false,
|
||||
depth: 1},
|
||||
{:swoosh, "~> 1.16"},
|
||||
{:req, "~> 0.5"},
|
||||
{:telemetry_metrics, "~> 1.0"},
|
||||
{:telemetry_poller, "~> 1.0"},
|
||||
|
|
@ -63,10 +64,14 @@ defmodule BirdyChat.MixProject do
|
|||
{:jason, "~> 1.2"},
|
||||
{:dns_cluster, "~> 0.2.0"},
|
||||
{:bandit, "~> 1.5"},
|
||||
{:credo, "~> 1.0", only: [:dev, :test]},
|
||||
|
||||
# Static analysis tools
|
||||
{:credo, "~> 1.0", only: [:dev, :test], runtime: false},
|
||||
{:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false}
|
||||
# Phoenix sockets client
|
||||
{:slipstream, "~> 1.0"},
|
||||
|
||||
# Telemetry
|
||||
{:opentelemetry, "~> 1.0"},
|
||||
{:opentelemetry_exporter, "~> 1.0"}
|
||||
]
|
||||
end
|
||||
|
||||
|
|
@ -86,7 +91,6 @@ defmodule BirdyChat.MixProject do
|
|||
"esbuild birdy_chat --minify",
|
||||
"phx.digest"
|
||||
],
|
||||
build_release: ["setup", "assets.deploy", "release"],
|
||||
precommit: [
|
||||
"compile --warnings-as-errors",
|
||||
"credo --strict",
|
||||
|
|
|
|||
18
mix.lock
18
mix.lock
|
|
@ -1,28 +1,37 @@
|
|||
%{
|
||||
"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"},
|
||||
|
|
@ -34,11 +43,16 @@
|
|||
"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"},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
This folder is used for storing messages local to this server. The contents are otherwise ignored.
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
#!/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
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
#!/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
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
#!/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
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule BirdyChat.IdentityTest do
|
||||
use BirdyChat.DataCase, async: true
|
||||
use BirdyChat.DataCase
|
||||
|
||||
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 == :test
|
||||
assert mode == :singleton
|
||||
end
|
||||
|
||||
test "can be started by giving values like environment variables" do
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
defmodule BirdyChat.PeerTest do
|
||||
use BirdyChat.DataCase, async: true
|
||||
end
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,81 +1,17 @@
|
|||
defmodule BirdyChatWeb.Api.MessagesTest do
|
||||
use BirdyChatWeb.ConnCase, async: true
|
||||
use BirdyChatWeb.ConnCase
|
||||
|
||||
setup %{conn: conn} do
|
||||
url = ~p"/api/messages"
|
||||
|
||||
unique_user_id = System.unique_integer([:positive])
|
||||
username = "test1-user#{unique_user_id}"
|
||||
|
||||
path = Application.app_dir(:birdy_chat, ["priv", "messages", "#{username}.txt"])
|
||||
path = Application.app_dir(:birdy_chat, ["priv", "messages", "test-user.txt"])
|
||||
on_exit(fn -> File.rm(path) end)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|
||||
%{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
|
||||
%{conn: conn, url: url}
|
||||
end
|
||||
|
||||
describe "POST /api/messages" do
|
||||
|
|
@ -95,70 +31,39 @@ defmodule BirdyChatWeb.Api.MessagesTest do
|
|||
assert result == expected_result
|
||||
end
|
||||
|
||||
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, :unprocessable_entity)
|
||||
|
||||
expected_result = %{
|
||||
"errors" => %{"from" => ["you can only communicate with your home server"]}
|
||||
}
|
||||
|
||||
assert result == expected_result
|
||||
end
|
||||
|
||||
test "returns message and 201 when successful", %{conn: conn, url: url, username: username} do
|
||||
message = %{"from" => "test1-user", "to" => username, "message" => "123"}
|
||||
|
||||
test "returns message and 201 when successful", %{conn: conn, url: url} do
|
||||
message = %{"from" => "2-user", "to" => "1-user", "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
|
||||
assert result == message
|
||||
end
|
||||
|
||||
test "writes message to file", %{conn: conn, url: url, username: username, path: path} do
|
||||
message = %{"from" => "test1-user", "to" => username, "message" => "123"}
|
||||
test "writes message to file", %{conn: conn, url: url} do
|
||||
message = %{"from" => "2-user", "to" => "test-user", "message" => "123"}
|
||||
payload = Jason.encode!(message)
|
||||
conn = post(conn, url, payload)
|
||||
assert result = json_response(conn, :created)
|
||||
assert result == message
|
||||
|
||||
expected_result = %{
|
||||
"from" => "test1-user",
|
||||
"to" => username,
|
||||
"message" => "123",
|
||||
"routing" => "local",
|
||||
"server" => "test1"
|
||||
}
|
||||
|
||||
assert result == expected_result
|
||||
|
||||
path = Application.app_dir(:birdy_chat, ["priv", "messages", "test-user.txt"])
|
||||
contents = File.read!(path)
|
||||
assert contents == "test1-user: 123\n"
|
||||
assert contents == "2-user: 123\n"
|
||||
end
|
||||
|
||||
test "appends message to file", %{conn: conn, url: url, username: username, path: path} do
|
||||
message = %{"from" => "test1-user", "to" => username, "message" => "123"}
|
||||
test "appends message to file", %{conn: conn, url: url} do
|
||||
message = %{"from" => "2-user", "to" => "test-user", "message" => "123"}
|
||||
payload = Jason.encode!(message)
|
||||
post(conn, url, payload)
|
||||
|
||||
message = %{"from" => "test1-user", "to" => username, "message" => "456"}
|
||||
message = %{"from" => "2-user", "to" => "test-user", "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 == "test1-user: 123\ntest1-user: 456\n"
|
||||
assert contents == "2-user: 123\n2-user: 456\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
81
test/birdy_chat_web/channels/server_channel_test.exs
Normal file
81
test/birdy_chat_web/channels/server_channel_test.exs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
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
|
||||
8
test/birdy_chat_web/controllers/page_controller_test.exs
Normal file
8
test/birdy_chat_web/controllers/page_controller_test.exs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
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
|
||||
34
test/support/channel_case.ex
Normal file
34
test/support/channel_case.ex
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue