diff --git a/apps/fg_http/lib/fg_http/devices.ex b/apps/fg_http/lib/fg_http/devices.ex
index 72ec5a3b7..bc8fa3944 100644
--- a/apps/fg_http/lib/fg_http/devices.ex
+++ b/apps/fg_http/lib/fg_http/devices.ex
@@ -17,8 +17,10 @@ defmodule FgHttp.Devices do
[%Device{}, ...]
"""
- def list_devices do
- Repo.all(Device)
+ def list_devices, do: Repo.all(Device)
+
+ def list_devices(user_id) do
+ Repo.all(from d in Device, where: d.user_id == ^user_id)
end
@doc """
@@ -52,6 +54,14 @@ defmodule FgHttp.Devices do
"""
def get_device!(id), do: Repo.get!(Device, id)
+ def get_device!(id, with_rules: true) do
+ Repo.one(
+ from d in Device,
+ where: d.id == ^id,
+ preload: :rules
+ )
+ end
+
@doc """
Creates a device.
diff --git a/apps/fg_http/lib/fg_http/devices/device.ex b/apps/fg_http/lib/fg_http/devices/device.ex
index ecf4a34c5..794575654 100644
--- a/apps/fg_http/lib/fg_http/devices/device.ex
+++ b/apps/fg_http/lib/fg_http/devices/device.ex
@@ -6,12 +6,15 @@ defmodule FgHttp.Devices.Device do
use Ecto.Schema
import Ecto.Changeset
+ alias FgHttp.{Rules.Rule, Users.User}
+
schema "devices" do
field :name, :string
field :public_key, :string
- field :user_id, :id
+ field :last_ip, EctoNetwork.INET
- has_many :rules, FgHttp.Rules.Rule
+ has_many :rules, Rule
+ belongs_to :user, User
timestamps()
end
@@ -19,7 +22,7 @@ defmodule FgHttp.Devices.Device do
@doc false
def changeset(device, attrs) do
device
- |> cast(attrs, [:user_id, :name, :public_key])
+ |> cast(attrs, [:last_ip, :user_id, :name, :public_key])
|> validate_required([:user_id])
end
end
diff --git a/apps/fg_http/lib/fg_http/ecto_enums.ex b/apps/fg_http/lib/fg_http/ecto_enums.ex
new file mode 100644
index 000000000..579ce3acd
--- /dev/null
+++ b/apps/fg_http/lib/fg_http/ecto_enums.ex
@@ -0,0 +1,18 @@
+import EctoEnum
+
+# We only allow dropping or accepting packets for now
+defenum(RuleActionEnum, :action, [:drop, :accept])
+
+# See http://ipset.netfilter.org/iptables.man.html
+defenum(RuleProtocolEnum, :protocol, [
+ :all,
+ :tcp,
+ :udp,
+ :udplite,
+ :icmp,
+ :icmpv6,
+ :esp,
+ :ah,
+ :sctp,
+ :mh
+])
diff --git a/apps/fg_http/lib/fg_http/rules.ex b/apps/fg_http/lib/fg_http/rules.ex
index 362479eca..fd426adae 100644
--- a/apps/fg_http/lib/fg_http/rules.ex
+++ b/apps/fg_http/lib/fg_http/rules.ex
@@ -17,6 +17,10 @@ defmodule FgHttp.Rules do
[%Rule{}, ...]
"""
+ def list_rules(device_id) do
+ Repo.all(from r in Rule, where: r.device_id == ^device_id)
+ end
+
def list_rules do
Repo.all(Rule)
end
diff --git a/apps/fg_http/lib/fg_http/rules/rule.ex b/apps/fg_http/lib/fg_http/rules/rule.ex
index 89597b348..f5b7fe1c0 100644
--- a/apps/fg_http/lib/fg_http/rules/rule.ex
+++ b/apps/fg_http/lib/fg_http/rules/rule.ex
@@ -6,13 +6,17 @@ defmodule FgHttp.Rules.Rule do
use Ecto.Schema
import Ecto.Changeset
- schema "rules" do
- field :destination, :string
- field :enabled, :boolean, default: false
- field :port, :string
- field :protocol, :string
+ alias FgHttp.{Devices.Device}
- belongs_to :device, FgHttp.Devices.Device
+ schema "rules" do
+ field :destination, EctoNetwork.INET
+ field :action, RuleActionEnum, default: "drop"
+ field :priority, :integer, default: 0
+ field :enabled, :boolean, default: true
+ field :port, :string
+ field :protocol, RuleProtocolEnum, default: "all"
+
+ belongs_to :device, Device
timestamps()
end
@@ -20,7 +24,7 @@ defmodule FgHttp.Rules.Rule do
@doc false
def changeset(rule, attrs) do
rule
- |> cast(attrs, [:destination, :port, :protocol, :enabled])
- |> validate_required([:destination, :port, :protocol, :enabled])
+ |> cast(attrs, [:device_id, :priority, :action, :destination, :port, :protocol, :enabled])
+ |> validate_required([:device_id, :priority, :action, :destination, :protocol, :enabled])
end
end
diff --git a/apps/fg_http/lib/fg_http/sessions/session.ex b/apps/fg_http/lib/fg_http/sessions/session.ex
index e9f2174a1..506e6075c 100644
--- a/apps/fg_http/lib/fg_http/sessions/session.ex
+++ b/apps/fg_http/lib/fg_http/sessions/session.ex
@@ -6,8 +6,10 @@ defmodule FgHttp.Sessions.Session do
use Ecto.Schema
import Ecto.Changeset
+ alias FgHttp.{Users.User}
+
schema "sessions" do
- field :user_id, :id
+ belongs_to :user, User
timestamps()
end
diff --git a/apps/fg_http/lib/fg_http/users/user.ex b/apps/fg_http/lib/fg_http/users/user.ex
index c4fe12691..2df6319fe 100644
--- a/apps/fg_http/lib/fg_http/users/user.ex
+++ b/apps/fg_http/lib/fg_http/users/user.ex
@@ -6,12 +6,17 @@ defmodule FgHttp.Users.User do
use Ecto.Schema
import Ecto.Changeset
+ alias FgHttp.{Devices.Device, Sessions.Session}
+
schema "users" do
field :email, :string
field :confirmed_at, :utc_datetime
field :last_signed_in_at, :utc_datetime
field :password_digest, :string
+ has_many :devices, Device
+ has_many :sessions, Session
+
timestamps()
end
diff --git a/apps/fg_http/lib/fg_http_web/controllers/device_controller.ex b/apps/fg_http/lib/fg_http_web/controllers/device_controller.ex
index 6fe3802ba..97aab5114 100644
--- a/apps/fg_http/lib/fg_http_web/controllers/device_controller.ex
+++ b/apps/fg_http/lib/fg_http_web/controllers/device_controller.ex
@@ -9,7 +9,7 @@ defmodule FgHttpWeb.DeviceController do
plug FgHttpWeb.Plugs.Authenticator
def index(conn, _params) do
- devices = Devices.list_devices()
+ devices = Devices.list_devices(conn.assigns.current_user.id)
render(conn, "index.html", devices: devices)
end
diff --git a/apps/fg_http/lib/fg_http_web/controllers/rule_controller.ex b/apps/fg_http/lib/fg_http_web/controllers/rule_controller.ex
index 129ebfb0b..8ad21d26c 100644
--- a/apps/fg_http/lib/fg_http_web/controllers/rule_controller.ex
+++ b/apps/fg_http/lib/fg_http_web/controllers/rule_controller.ex
@@ -9,8 +9,7 @@ defmodule FgHttpWeb.RuleController do
plug FgHttpWeb.Plugs.Authenticator
def index(conn, %{"device_id" => device_id}) do
- device = Devices.get_device!(device_id)
-
+ device = Devices.get_device!(device_id, with_rules: true)
render(conn, "index.html", device: device, rules: device.rules)
end
@@ -21,15 +20,19 @@ defmodule FgHttpWeb.RuleController do
render(conn, "new.html", changeset: changeset, device: device)
end
- def create(conn, %{"rule" => rule_params}) do
- case Rules.create_rule(rule_params) do
+ def create(conn, %{"device_id" => device_id, "rule" => rule_params}) do
+ # XXX RBAC
+ all_params = Map.merge(rule_params, %{"device_id" => device_id})
+
+ case Rules.create_rule(all_params) do
{:ok, rule} ->
conn
|> put_flash(:info, "Rule created successfully.")
|> redirect(to: Routes.rule_path(conn, :show, rule))
{:error, %Ecto.Changeset{} = changeset} ->
- render(conn, "new.html", changeset: changeset)
+ device = Devices.get_device!(device_id)
+ render(conn, "new.html", device: device, changeset: changeset)
end
end
diff --git a/apps/fg_http/lib/fg_http_web/templates/device/show.html.eex b/apps/fg_http/lib/fg_http_web/templates/device/show.html.eex
index 8db5dae66..03c74a793 100644
--- a/apps/fg_http/lib/fg_http_web/templates/device/show.html.eex
+++ b/apps/fg_http/lib/fg_http_web/templates/device/show.html.eex
@@ -12,5 +12,9 @@
-<%= link "Edit", to: Routes.device_path(@conn, :edit, @device) %>
-<%= link "Back", to: Routes.device_path(@conn, :index) %>
+
+ <%= link "Show Rules for This Device", to: Routes.device_rule_path(@conn, :index, @device) %>
+
+
+<%= link "Edit", to: Routes.device_path(@conn, :edit, @device) %>
+<%= link "Back", to: Routes.device_path(@conn, :index) %>
diff --git a/apps/fg_http/lib/fg_http_web/templates/rule/edit.html.eex b/apps/fg_http/lib/fg_http_web/templates/rule/edit.html.eex
index e49613818..473e03412 100644
--- a/apps/fg_http/lib/fg_http_web/templates/rule/edit.html.eex
+++ b/apps/fg_http/lib/fg_http_web/templates/rule/edit.html.eex
@@ -2,4 +2,4 @@
<%= render "form.html", Map.put(assigns, :action, Routes.rule_path(@conn, :update, @rule)) %>
-<%= link "Back", to: Routes.rule_path(@conn, :index, @rule.device) %>
+<%= link "Back", to: Routes.device_rule_path(@conn, :index, @rule.device_id) %>
diff --git a/apps/fg_http/lib/fg_http_web/templates/rule/form.html.eex b/apps/fg_http/lib/fg_http_web/templates/rule/form.html.eex
index 8a4461fbf..bdf4a802a 100644
--- a/apps/fg_http/lib/fg_http_web/templates/rule/form.html.eex
+++ b/apps/fg_http/lib/fg_http_web/templates/rule/form.html.eex
@@ -1,27 +1,41 @@
-<%= form_for @changeset, @action, fn f -> %>
+<%= form_for @changeset, @action, [class: "black-80"], fn f -> %>
<%= if @changeset.action do %>
-
+
Oops, something went wrong! Please check the errors below.
<% end %>
- <%= label f, :destination %>
- <%= text_input f, :destination %>
- <%= error_tag f, :destination %>
+
+ <%= label f, :destination, class: "f6 b db mb2" %>
+ <%= text_input f, :destination, class: "input-reset ba b--black-20 pa2 mb2 db w-100" %>
+ <%= error_tag f, :destination %>
+
- <%= label f, :port %>
- <%= text_input f, :port %>
- <%= error_tag f, :port %>
+
+ <%= label f, :port, class: "f6 b db mb2" %>
+ <%= text_input f, :port, class: "input-reset ba b--black-20 pa2 mb2 db w-100" %>
+ <%= error_tag f, :port %>
+
- <%= label f, :protocol %>
- <%= text_input f, :protocol %>
- <%= error_tag f, :protocol %>
+
+ <%= label f, :protocol, class: "f6 b db mb2" %>
+ <%= text_input f, :protocol, class: "input-reset ba b--black-20 pa2 mb2 db w-100" %>
+ <%= error_tag f, :protocol %>
+
- <%= label f, :enabled %>
- <%= checkbox f, :enabled %>
- <%= error_tag f, :enabled %>
+
+ <%= label f, :priority, class: "f6 b db mb2" %>
+ <%= text_input f, :priority, class: "input-reset ba b--black-20 pa2 mb2 db w-100" %>
+ <%= error_tag f, :priority %>
+
-
+
+ <%= label f, :enabled, class: "f6 b db mb2" %>
+ <%= checkbox f, :enabled %>
+ <%= error_tag f, :enabled %>
+
+
+
<%= submit "Save" %>
<% end %>
diff --git a/apps/fg_http/lib/fg_http_web/templates/rule/index.html.eex b/apps/fg_http/lib/fg_http_web/templates/rule/index.html.eex
index 63ab8b4e3..6624cef3a 100644
--- a/apps/fg_http/lib/fg_http_web/templates/rule/index.html.eex
+++ b/apps/fg_http/lib/fg_http_web/templates/rule/index.html.eex
@@ -1,23 +1,25 @@
-
Listing Rules
+
Listing Rules for Device <%= @device.name %>
-
-
-
- | Destination |
- Port |
- Protocol |
- Enabled |
+
+
+
+ | Action |
+ Destination |
+ Port |
+ Protocol |
+ Priority |
+ Enabled |
|
-
-
<%= for rule <- @rules do %>
-
- | <%= rule.destination %> |
- <%= rule.port %> |
- <%= rule.protocol %> |
- <%= rule.enabled %> |
+
+ | <%= rule.action %> |
+ <%= rule.destination %> |
+ <%= rule.port %> |
+ <%= rule.protocol %> |
+ <%= rule.priority %> |
+ <%= rule.enabled %> |
<%= link "Show", to: Routes.rule_path(@conn, :show, rule) %>
@@ -30,5 +32,5 @@
|
- <%= link "New Rule", to: Routes.rule_path(@conn, :new, @device) %>
+ <%= link "New Rule", to: Routes.device_rule_path(@conn, :new, @device) %>
diff --git a/apps/fg_http/lib/fg_http_web/templates/rule/new.html.eex b/apps/fg_http/lib/fg_http_web/templates/rule/new.html.eex
index 5c88667cd..e7963ed7d 100644
--- a/apps/fg_http/lib/fg_http_web/templates/rule/new.html.eex
+++ b/apps/fg_http/lib/fg_http_web/templates/rule/new.html.eex
@@ -1,5 +1,5 @@
-New Rule
+New Rule for Device <%= @device.name %>
-<%= render "form.html", Map.put(assigns, :action, Routes.rule_path(@conn, :create, @device)) %>
+<%= render "form.html", Map.put(assigns, :action, Routes.device_rule_path(@conn, :create, @device)) %>
-<%= link "Back", to: Routes.rule_path(@conn, :index, @device) %>
+<%= link "Back", to: Routes.device_rule_path(@conn, :index, @device) %>
diff --git a/apps/fg_http/lib/fg_http_web/templates/rule/show.html.eex b/apps/fg_http/lib/fg_http_web/templates/rule/show.html.eex
index 6c785c6bb..90198176b 100644
--- a/apps/fg_http/lib/fg_http_web/templates/rule/show.html.eex
+++ b/apps/fg_http/lib/fg_http_web/templates/rule/show.html.eex
@@ -2,6 +2,11 @@
+ -
+ Action:
+ <%= @rule.action %>
+
+
-
Destination:
<%= @rule.destination %>
@@ -17,6 +22,11 @@
<%= @rule.protocol %>
+ -
+ Priority
+ <%= @rule.priority %>
+
+
-
Enabled:
<%= @rule.enabled %>
@@ -25,4 +35,4 @@
<%= link "Edit", to: Routes.rule_path(@conn, :edit, @rule) %>
-<%= link "Back", to: Routes.rule_path(@conn, :index, @rule.device) %>
+<%= link "Back", to: Routes.device_rule_path(@conn, :index, @rule.device_id) %>
diff --git a/apps/fg_http/mix.exs b/apps/fg_http/mix.exs
index f471eef57..e0bbd12d3 100644
--- a/apps/fg_http/mix.exs
+++ b/apps/fg_http/mix.exs
@@ -41,7 +41,7 @@ defmodule FgHttp.MixProject do
{:phoenix_pubsub, "~> 2.0"},
{:phoenix_ecto, "~> 4.0"},
{:ecto_sql, "~> 3.1"},
- # Exposes Postgres inet, cidr, macaddr types
+ {:ecto_enum, "~> 1.4.0"},
{:ecto_network, "~> 1.3.0"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.11"},
diff --git a/apps/fg_http/priv/repo/migrations/20200228145810_create_devices.exs b/apps/fg_http/priv/repo/migrations/20200228145810_create_devices.exs
index f2d57d058..1b0c692d9 100644
--- a/apps/fg_http/priv/repo/migrations/20200228145810_create_devices.exs
+++ b/apps/fg_http/priv/repo/migrations/20200228145810_create_devices.exs
@@ -5,7 +5,8 @@ defmodule FgHttp.Repo.Migrations.CreateDevices do
create table(:devices) do
add :name, :string
add :public_key, :string
- add :user_id, references(:users, on_delete: :delete_all)
+ add :last_ip, :inet
+ add :user_id, references(:users, on_delete: :delete_all), null: false
timestamps()
end
diff --git a/apps/fg_http/priv/repo/migrations/20200228154815_create_rules.exs b/apps/fg_http/priv/repo/migrations/20200228154815_create_rules.exs
index e83298300..eda009fb9 100644
--- a/apps/fg_http/priv/repo/migrations/20200228154815_create_rules.exs
+++ b/apps/fg_http/priv/repo/migrations/20200228154815_create_rules.exs
@@ -2,12 +2,17 @@ defmodule FgHttp.Repo.Migrations.CreateRules do
use Ecto.Migration
def change do
+ RuleActionEnum.create_type()
+ RuleProtocolEnum.create_type()
+
create table(:rules) do
add :destination, :inet
- add :port, :string
- add :protocol, :string
+ add :protocol, RuleProtocolEnum.type(), default: "all", null: false
+ add :action, RuleActionEnum.type(), default: "drop", null: false
+ add :priority, :integer, default: 0, null: false
add :enabled, :boolean, default: false, null: false
- add :device_id, references(:devices, on_delete: :delete_all)
+ add :port, :string
+ add :device_id, references(:devices, on_delete: :delete_all), null: false
timestamps()
end
diff --git a/apps/fg_http/priv/repo/seeds.exs b/apps/fg_http/priv/repo/seeds.exs
index 177a1aa39..adccbb974 100644
--- a/apps/fg_http/priv/repo/seeds.exs
+++ b/apps/fg_http/priv/repo/seeds.exs
@@ -9,3 +9,23 @@
#
# We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong.
+
+alias FgHttp.Repo
+
+Repo.transaction(fn ->
+ {:ok, user} = FgHttp.Users.create_user(%{email: "testuser@fireguard.network"})
+
+ {:ok, device} =
+ FgHttp.Devices.create_device(%{
+ name: "Seed",
+ public_key: "Seed",
+ last_ip: %Postgrex.INET{address: {127, 0, 0, 1}},
+ user_id: user.id
+ })
+
+ {:ok, _rule} =
+ FgHttp.Rules.create_rule(%{
+ device_id: device.id,
+ destination: %Postgrex.INET{address: {0, 0, 0, 0}, netmask: 0}
+ })
+end)
diff --git a/apps/fg_http/test/fg_http_web/controllers/device_controller_test.exs b/apps/fg_http/test/fg_http_web/controllers/device_controller_test.exs
index 0b5f37b0e..351710262 100644
--- a/apps/fg_http/test/fg_http_web/controllers/device_controller_test.exs
+++ b/apps/fg_http/test/fg_http_web/controllers/device_controller_test.exs
@@ -20,6 +20,9 @@ defmodule FgHttpWeb.DeviceControllerTest do
describe "index" do
test "lists all devices", %{conn: conn} do
+ # Mock authentication
+ conn = Plug.Conn.assign(conn, :current_user, fixture(:user))
+
conn = get(conn, Routes.device_path(conn, :index))
assert html_response(conn, 200) =~ "Listing Devices"
end
diff --git a/mix.lock b/mix.lock
index 9cfefa1cc..11058b065 100644
--- a/mix.lock
+++ b/mix.lock
@@ -6,23 +6,24 @@
"credo": {:hex, :credo, "1.4.0", "92339d4cbadd1e88b5ee43d427b639b68a11071b6f73854e33638e30a0ea11f5", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1fd3b70dce216574ce3c18bdf510b57e7c4c85c2ec9cad4bff854abaf7e58658"},
"db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"},
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
- "ecto": {:hex, :ecto, "3.4.3", "3a14c2500c3964165245a4f24a463e080762f7ccd0c632c763ea589f75ca205f", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b6f18dea95f2004d0369f6a8346513ca3f706614f4ede219a5f3fe5db5dd962"},
+ "ecto": {:hex, :ecto, "3.4.4", "a2c881e80dc756d648197ae0d936216c0308370332c5e77a2325a10293eef845", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4bd3ad62abc3b21fb629f0f7a3dab23a192fca837d257dd08449fba7373561"},
+ "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
"ecto_network": {:hex, :ecto_network, "1.3.0", "1e77fa37c20e0f6a426d3862732f3317b0fa4c18f123d325f81752a491d7304e", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 0.0.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.14.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "053a5e46ef2837e8ea5ea97c82fa0f5494699209eddd764e663c85f11b2865bd"},
"ecto_sql": {:hex, :ecto_sql, "3.4.3", "c552aa8a7ccff2b64024f835503b3155d8e73452c180298527fbdbcd6e79710b", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ec9e59d6fa3f8cfda9963ada371e9e6659167c2338a997bd7ea23b10b245842b"},
"file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"},
- "gettext": {:hex, :gettext, "0.17.4", "f13088e1ec10ce01665cf25f5ff779e7df3f2dc71b37084976cf89d1aa124d5c", [:mix], [], "hexpm", "3c75b5ea8288e2ee7ea503ff9e30dfe4d07ad3c054576a6e60040e79a801e14d"},
+ "gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"},
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
"phoenix": {:hex, :phoenix, "1.5.1", "95156589879dc69201d5fc0ebdbfdfc7901a09a3616ea611ec297f81340275a2", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc272b38e79d2881790fccae6f67a9fbe9b790103d6878175ea03d23003152eb"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"},
"phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"},
- "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.1", "274a4b07c4adbdd7785d45a8b0bb57634d0b4f45b18d2c508b26c0344bd59b8f", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "41b4103a2fa282cfd747d377233baf213c648fdcc7928f432937676532490eee"},
+ "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.2", "38d94c30df5e2ef11000697a4fbe2b38d0fbf79239d492ff1be87bbc33bc3a84", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "a3dec3d28ddb5476c96a7c8a38ea8437923408bc88da43e5c45d97037b396280"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.12.1", "42f591c781edbf9fab921319076b7ac635d43aa23e6748d2644563326236d7e4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.4.16 or ~> 1.5.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "585321e98df1cd5943e370b9784e950a37ca073744eb534660c9048967c52ab6"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"plug": {:hex, :plug, "1.10.0", "6508295cbeb4c654860845fb95260737e4a8838d34d115ad76cd487584e2fc4d", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "422a9727e667be1bf5ab1de03be6fa0ad67b775b2d84ed908f3264415ef29d4a"},
"plug_cowboy": {:hex, :plug_cowboy, "2.2.1", "fcf58aa33227a4322a050e4783ee99c63c031a2e7f9a2eb7340d55505e17f30f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3b43de24460d87c0971887286e7a20d40462e48eb7235954681a20cee25ddeb6"},
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
- "postgrex": {:hex, :postgrex, "0.15.3", "5806baa8a19a68c4d07c7a624ccdb9b57e89cbc573f1b98099e3741214746ae4", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4737ce62a31747b4c63c12b20c62307e51bb4fcd730ca0c32c280991e0606c90"},
+ "postgrex": {:hex, :postgrex, "0.15.4", "5d691c25fc79070705a2ff0e35ce0822b86a0ee3c6fdb7a4fb354623955e1aed", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "306515b9d975fcb2478dc337a1d27dc3bf8af7cd71017c333fe9db3a3d211b0a"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
}