mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
@@ -1,3 +1,3 @@
|
||||
elixir 1.10.3-otp-22
|
||||
erlang 22.3.3
|
||||
elixir 1.10.3-otp-23
|
||||
erlang 23.0
|
||||
nodejs 10.20.1
|
||||
|
||||
@@ -11,6 +11,7 @@ defmodule FgHttp.Devices.Device do
|
||||
schema "devices" do
|
||||
field :name, :string
|
||||
field :public_key, :string
|
||||
field :ifname, :string
|
||||
field :last_ip, EctoNetwork.INET
|
||||
|
||||
has_many :rules, Rule
|
||||
@@ -22,7 +23,7 @@ defmodule FgHttp.Devices.Device do
|
||||
@doc false
|
||||
def changeset(device, attrs) do
|
||||
device
|
||||
|> cast(attrs, [:last_ip, :user_id, :name, :public_key])
|
||||
|> validate_required([:user_id])
|
||||
|> cast(attrs, [:last_ip, :ifname, :user_id, :name, :public_key])
|
||||
|> validate_required([:user_id, :ifname, :name, :public_key])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ defmodule FgHttp.Sessions do
|
||||
import Ecto.Query, warn: false
|
||||
alias FgHttp.Repo
|
||||
|
||||
alias FgHttp.Sessions.Session
|
||||
alias FgHttp.{Sessions.Session, Users.User}
|
||||
|
||||
@doc """
|
||||
Returns the list of sessions.
|
||||
@@ -51,7 +51,7 @@ defmodule FgHttp.Sessions do
|
||||
"""
|
||||
def create_session(attrs \\ %{}) do
|
||||
%Session{}
|
||||
|> Session.changeset(attrs)
|
||||
|> Session.create_changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@@ -85,10 +85,32 @@ defmodule FgHttp.Sessions do
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_session(%Session{} = session) do
|
||||
def delete_session(%Session{} = session, really_delete: true) do
|
||||
Repo.delete(session)
|
||||
end
|
||||
|
||||
def delete_session(%Session{} = session) do
|
||||
update_session(session, %{deleted_at: DateTime.utc_now()})
|
||||
end
|
||||
|
||||
def load_session(_session_id) do
|
||||
query =
|
||||
from s in Session,
|
||||
where: is_nil(s.deleted_at),
|
||||
join: u in User,
|
||||
on: s.user_id == u.id,
|
||||
select: {s, u},
|
||||
preload: :user
|
||||
|
||||
case session = Repo.one(query) do
|
||||
nil ->
|
||||
{:error, "Valid session not found"}
|
||||
|
||||
_ ->
|
||||
{:ok, session}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking session changes.
|
||||
|
||||
|
||||
@@ -6,18 +6,49 @@ defmodule FgHttp.Sessions.Session do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
alias FgHttp.{Users.User}
|
||||
alias FgHttp.{Users, Users.User}
|
||||
|
||||
schema "sessions" do
|
||||
field :deleted_at, :utc_datetime
|
||||
belongs_to :user, User
|
||||
|
||||
# VIRTUAL FIELDS
|
||||
field :user_email, :string, virtual: true
|
||||
field :user_password, :string, virtual: true
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(session, attrs \\ %{}) do
|
||||
session
|
||||
|> cast(attrs, [])
|
||||
|> cast(attrs, [:deleted_at])
|
||||
|> validate_required([])
|
||||
end
|
||||
|
||||
def create_changeset(session, attrs \\ %{}) do
|
||||
session
|
||||
|> cast(attrs, [:user_email, :user_password])
|
||||
|> validate_required([:user_email, :user_password])
|
||||
|> authenticate_user()
|
||||
end
|
||||
|
||||
defp authenticate_user(
|
||||
%Ecto.Changeset{
|
||||
valid?: true,
|
||||
changes: %{user_email: email, user_password: password}
|
||||
} = changeset
|
||||
) do
|
||||
user = Users.get_user!(email: email)
|
||||
|
||||
case User.authenticate_user(user, password) do
|
||||
{:ok, _} ->
|
||||
change(changeset, user_id: user.id)
|
||||
|
||||
{:error, error_msg} ->
|
||||
raise("There was an issue with your password: #{error_msg}")
|
||||
end
|
||||
end
|
||||
|
||||
defp authenticate_user(changeset), do: changeset
|
||||
end
|
||||
|
||||
@@ -35,6 +35,14 @@ defmodule FgHttp.Users do
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_user!(email: email) do
|
||||
Repo.get_by!(User, email: email)
|
||||
end
|
||||
|
||||
def get_user!(reset_token: reset_token) do
|
||||
Repo.get_by!(User, reset_token: reset_token)
|
||||
end
|
||||
|
||||
def get_user!(id), do: Repo.get!(User, id)
|
||||
|
||||
@doc """
|
||||
@@ -51,7 +59,7 @@ defmodule FgHttp.Users do
|
||||
"""
|
||||
def create_user(attrs \\ %{}) do
|
||||
%User{}
|
||||
|> User.changeset(attrs)
|
||||
|> User.create_changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@@ -69,7 +77,7 @@ defmodule FgHttp.Users do
|
||||
"""
|
||||
def update_user(%User{} = user, attrs) do
|
||||
user
|
||||
|> User.changeset(attrs)
|
||||
|> User.update_changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
|
||||
@@ -11,20 +11,79 @@ defmodule FgHttp.Users.User do
|
||||
schema "users" do
|
||||
field :email, :string
|
||||
field :confirmed_at, :utc_datetime
|
||||
field :reset_sent_at, :utc_datetime
|
||||
field :reset_token, :string
|
||||
field :last_signed_in_at, :utc_datetime
|
||||
field :password_digest, :string
|
||||
field :password_hash, :string
|
||||
|
||||
has_many :devices, Device
|
||||
has_many :sessions, Session
|
||||
# VIRTUAL FIELDS
|
||||
field :password, :string, virtual: true
|
||||
field :password_confirmation, :string, virtual: true
|
||||
field :current_password, :string, virtual: true
|
||||
|
||||
has_many :devices, Device, on_delete: :delete_all
|
||||
has_many :sessions, Session, on_delete: :delete_all
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(user, attrs \\ %{}) do
|
||||
def create_changeset(user, attrs \\ %{}) do
|
||||
user
|
||||
|> cast(attrs, [:email, :confirmed_at, :password_digest, :last_signed_in_at])
|
||||
|> validate_required([:email])
|
||||
|> cast(attrs, [:email, :password_hash, :password, :password_confirmation])
|
||||
|> validate_required([:email, :password, :password_confirmation])
|
||||
|> unique_constraint(:email)
|
||||
|> put_password_hash()
|
||||
|> validate_required([:password_hash])
|
||||
end
|
||||
|
||||
# Only password being updated
|
||||
def update_changeset(
|
||||
user,
|
||||
%{
|
||||
user: %{
|
||||
password: _password,
|
||||
password_confirmation: _password_confirmation,
|
||||
current_password: _current_password
|
||||
}
|
||||
} = attrs
|
||||
) do
|
||||
user
|
||||
|> cast(attrs, [:email, :password, :password_confirmation, :current_password])
|
||||
|> verify_current_password(attrs[:current_password])
|
||||
|> validate_required([:password, :password_confirmation, :current_password])
|
||||
|> put_password_hash()
|
||||
|> validate_required([:password_hash])
|
||||
end
|
||||
|
||||
# Only email being updated
|
||||
def update_changeset(user, %{user: %{email: _email}} = attrs) do
|
||||
user
|
||||
|> cast(attrs, [:email])
|
||||
|> validate_required([:email])
|
||||
end
|
||||
|
||||
def changeset(%__MODULE__{} = _user, _attrs \\ %{}) do
|
||||
change(%__MODULE__{})
|
||||
end
|
||||
|
||||
def authenticate_user(user, password_candidate) do
|
||||
Argon2.check_pass(user, password_candidate)
|
||||
end
|
||||
|
||||
defp verify_current_password(user, current_password) do
|
||||
{:ok, user} = authenticate_user(user, current_password)
|
||||
user
|
||||
end
|
||||
|
||||
defp put_password_hash(
|
||||
%Ecto.Changeset{
|
||||
valid?: true,
|
||||
changes: %{password: password}
|
||||
} = changeset
|
||||
) do
|
||||
change(changeset, password_hash: Argon2.hash_pwd_salt(password))
|
||||
end
|
||||
|
||||
defp put_password_hash(changeset), do: changeset
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ defmodule FgHttpWeb.DeviceController do
|
||||
use FgHttpWeb, :controller
|
||||
alias FgHttp.{Devices, Devices.Device}
|
||||
|
||||
plug FgHttpWeb.Plugs.Authenticator
|
||||
plug FgHttpWeb.Plugs.SessionLoader
|
||||
|
||||
def index(conn, _params) do
|
||||
devices = Devices.list_devices(conn.assigns.current_user.id)
|
||||
@@ -18,13 +18,14 @@ defmodule FgHttpWeb.DeviceController do
|
||||
render(conn, "new.html", changeset: changeset)
|
||||
end
|
||||
|
||||
def create(conn, %{"device" => device_params}) do
|
||||
create_params = %{
|
||||
def create(conn, %{"device" => %{"public_key" => _public_key} = device_params}) do
|
||||
our_params = %{
|
||||
"user_id" => conn.assigns.current_user.id,
|
||||
"name" => "Auto"
|
||||
"name" => "Default",
|
||||
"ifname" => "wg0"
|
||||
}
|
||||
|
||||
all_params = Map.merge(device_params, create_params)
|
||||
all_params = Map.merge(device_params, our_params)
|
||||
|
||||
case Devices.create_device(all_params) do
|
||||
{:ok, device} ->
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
defmodule FgHttpWeb.PasswordResetController do
|
||||
@moduledoc """
|
||||
Implements the CRUD for password resets
|
||||
"""
|
||||
|
||||
use FgHttpWeb, :controller
|
||||
alias FgHttp.{Users, Users.User}
|
||||
|
||||
plug FgHttpWeb.Plugs.RedirectAuthenticated
|
||||
|
||||
def new(conn, _params) do
|
||||
conn
|
||||
|> render("new.html", changeset: User.changeset(%User{}))
|
||||
end
|
||||
|
||||
# Don't actually create anything. Instead, update the user with a reset token and send
|
||||
# the password reset email.
|
||||
def create(conn, %{
|
||||
"password_reset" =>
|
||||
%{
|
||||
reset_token: reset_token,
|
||||
password: _password,
|
||||
password_confirmation: _password_confirmation,
|
||||
current_password: _current_password
|
||||
} = user_params
|
||||
}) do
|
||||
user = Users.get_user!(reset_token: reset_token)
|
||||
|
||||
case Users.update_user(user, user_params) do
|
||||
{:ok, _user} ->
|
||||
conn
|
||||
|> render("success.html")
|
||||
|
||||
{:error, changeset} ->
|
||||
conn
|
||||
|> render("new.html", changeset: changeset)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -6,7 +6,7 @@ defmodule FgHttpWeb.RuleController do
|
||||
use FgHttpWeb, :controller
|
||||
alias FgHttp.{Devices, Rules, Rules.Rule}
|
||||
|
||||
plug FgHttpWeb.Plugs.Authenticator
|
||||
plug FgHttpWeb.Plugs.SessionLoader
|
||||
|
||||
def index(conn, %{"device_id" => device_id}) do
|
||||
device = Devices.get_device!(device_id, with_rules: true)
|
||||
|
||||
@@ -3,11 +3,11 @@ defmodule FgHttpWeb.SessionController do
|
||||
Implements the CRUD for a Session
|
||||
"""
|
||||
|
||||
alias FgHttp.{Sessions, Sessions.Session}
|
||||
use FgHttpWeb, :controller
|
||||
alias FgHttp.{Repo, Sessions.Session, Users.User}
|
||||
|
||||
plug :redirect_authenticated when action in [:new]
|
||||
plug FgHttpWeb.Plugs.Authenticator when action in [:delete]
|
||||
plug FgHttpWeb.Plugs.RedirectAuthenticated when action in [:new]
|
||||
plug FgHttpWeb.Plugs.SessionLoader when action in [:delete]
|
||||
|
||||
# GET /sessions/new
|
||||
def new(conn, _params) do
|
||||
@@ -18,12 +18,13 @@ defmodule FgHttpWeb.SessionController do
|
||||
|
||||
# Sign In
|
||||
# POST /sessions
|
||||
def create(conn, params) do
|
||||
changeset = Session.changeset(%Session{}, params)
|
||||
|
||||
case Repo.insert(changeset) do
|
||||
def create(conn, %{"session" => session_params}) do
|
||||
case Sessions.create_session(session_params) do
|
||||
{:ok, session} ->
|
||||
conn
|
||||
# Prevent session fixation
|
||||
|> clear_session()
|
||||
|> put_session(:session_id, session.id)
|
||||
|> assign(:current_session, session)
|
||||
|> put_flash(:info, "Session created successfully")
|
||||
|> redirect(to: Routes.device_path(conn, :index))
|
||||
@@ -31,29 +32,21 @@ defmodule FgHttpWeb.SessionController do
|
||||
{:error, changeset} ->
|
||||
conn
|
||||
|> put_flash(:error, "Error creating session.")
|
||||
|> render("new.html", changeset: changeset)
|
||||
|> render("new.html", changeset: changeset, user_signed_in?: false)
|
||||
end
|
||||
end
|
||||
|
||||
# Sign Out
|
||||
# DELETE /session
|
||||
def delete(conn, _params) do
|
||||
case Repo.delete(conn.current_session) do
|
||||
session = conn.assigns.current_session
|
||||
|
||||
case Sessions.delete_session(session) do
|
||||
{:ok, _session} ->
|
||||
conn
|
||||
|> assign(:current_session, nil)
|
||||
|> put_flash(:info, "Session deleted successfully.")
|
||||
|> clear_session
|
||||
|> put_flash(:info, "Signed out successfully.")
|
||||
|> redirect(to: "/")
|
||||
end
|
||||
end
|
||||
|
||||
defp redirect_authenticated(conn, _) do
|
||||
user = %User{id: 1, email: "dev_user@fireguard.network"}
|
||||
session = %Session{user_id: user.id}
|
||||
|
||||
conn
|
||||
|> assign(:current_session, session)
|
||||
|> redirect(to: "/")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,9 +4,9 @@ defmodule FgHttpWeb.UserController do
|
||||
"""
|
||||
|
||||
use FgHttpWeb, :controller
|
||||
alias FgHttp.{Repo, Users, Users.User}
|
||||
alias FgHttp.{Users, Users.User}
|
||||
|
||||
plug FgHttpWeb.Plugs.Authenticator when action in [:show, :edit, :update, :delete]
|
||||
plug FgHttpWeb.Plugs.SessionLoader when action in [:show, :edit, :update, :delete]
|
||||
|
||||
# GET /users/new
|
||||
def new(conn, _params) do
|
||||
@@ -46,9 +46,7 @@ defmodule FgHttpWeb.UserController do
|
||||
|
||||
# PATCH /user
|
||||
def update(conn, params) do
|
||||
changeset = User.changeset(conn.current_user, params)
|
||||
|
||||
case Repo.update(changeset) do
|
||||
case Users.update_user(conn.current_user, params) do
|
||||
{:ok, user} ->
|
||||
conn
|
||||
|> assign(:current_user, user)
|
||||
@@ -64,7 +62,7 @@ defmodule FgHttpWeb.UserController do
|
||||
|
||||
# DELETE /user
|
||||
def delete(conn, _params) do
|
||||
case Repo.delete(conn.current_user) do
|
||||
case Users.delete_user(conn.current_user) do
|
||||
{:ok, _user} ->
|
||||
conn
|
||||
|> assign(:current_user, nil)
|
||||
|
||||
@@ -15,13 +15,13 @@ defmodule FgHttpWeb.NewDeviceLive do
|
||||
{:ok, assign(socket, :device, device)}
|
||||
end
|
||||
|
||||
defp wait_for_device_connect(_socket) do
|
||||
# XXX: pass socket to fg_vpn somehow
|
||||
:timer.send_after(3000, self(), :update)
|
||||
# XXX: Receive other device details to create an intelligent name
|
||||
def handle_info({:pubkey, pubkey}, socket) do
|
||||
device = %Device{public_key: pubkey}
|
||||
{:noreply, assign(socket, :device, device)}
|
||||
end
|
||||
|
||||
def handle_info(:update, socket) do
|
||||
new_device = Map.merge(socket.assigns.device, %{public_key: "foobar"})
|
||||
{:noreply, assign(socket, :device, new_device)}
|
||||
defp wait_for_device_connect(_socket) do
|
||||
:timer.send_after(3000, self(), {:pubkey, "foobar"})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -42,8 +42,7 @@ Endpoint = <%= Application.fetch_env!(:fg_http, :vpn_endpoint) %>
|
||||
<p>
|
||||
<%=
|
||||
link("<- Something's wrong. Don't add this device and go back.",
|
||||
to: Routes.device_path(@socket, :index),
|
||||
method: :delete)
|
||||
to: Routes.device_path(@socket, :index))
|
||||
%>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
defmodule FgHttpWeb.Plugs.Authenticator do
|
||||
@moduledoc """
|
||||
Loads the user's session from cookie
|
||||
"""
|
||||
|
||||
import Plug.Conn
|
||||
alias FgHttp.{Repo, Users.User}
|
||||
|
||||
def init(default), do: default
|
||||
|
||||
def call(conn, _default) do
|
||||
user = Repo.one(User)
|
||||
assign(conn, :current_user, user)
|
||||
end
|
||||
end
|
||||
21
apps/fg_http/lib/fg_http_web/plugs/redirect_authenticated.ex
Normal file
21
apps/fg_http/lib/fg_http_web/plugs/redirect_authenticated.ex
Normal file
@@ -0,0 +1,21 @@
|
||||
defmodule FgHttpWeb.Plugs.RedirectAuthenticated do
|
||||
@moduledoc """
|
||||
Redirects users when he/she tries to access an open resource while authenticated.
|
||||
"""
|
||||
|
||||
import Plug.Conn
|
||||
import Phoenix.Controller, only: [redirect: 2]
|
||||
|
||||
def init(default), do: default
|
||||
|
||||
def call(conn, _default) do
|
||||
if get_session(conn, :session_id) do
|
||||
conn
|
||||
|> redirect(to: "/")
|
||||
|> halt()
|
||||
else
|
||||
conn
|
||||
|> assign(:user_signed_in?, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
27
apps/fg_http/lib/fg_http_web/plugs/session_loader.ex
Normal file
27
apps/fg_http/lib/fg_http_web/plugs/session_loader.ex
Normal file
@@ -0,0 +1,27 @@
|
||||
defmodule FgHttpWeb.Plugs.SessionLoader do
|
||||
@moduledoc """
|
||||
Loads the user's session from cookie
|
||||
"""
|
||||
|
||||
import Plug.Conn
|
||||
import Phoenix.Controller, only: [redirect: 2]
|
||||
alias FgHttp.Sessions
|
||||
alias FgHttpWeb.Router.Helpers, as: Routes
|
||||
|
||||
def init(default), do: default
|
||||
|
||||
def call(conn, _default) do
|
||||
case Sessions.load_session(get_session(conn, :session_id)) do
|
||||
{:ok, {session, user}} ->
|
||||
conn
|
||||
|> assign(:current_session, session)
|
||||
|> assign(:current_user, user)
|
||||
|> assign(:user_signed_in?, true)
|
||||
|
||||
{:error, _} ->
|
||||
conn
|
||||
|> redirect(to: Routes.session_path(conn, :new))
|
||||
|> halt
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,11 @@
|
||||
<h1>New Device</h1>
|
||||
|
||||
<%= if assigns[:changeset] do %>
|
||||
The following errors occurred when creating this Device:
|
||||
|
||||
<%= aggregated_errors(@changeset) %>
|
||||
<% end %>
|
||||
|
||||
<%= live_render(@conn, FgHttpWeb.NewDeviceLive, session: %{"current_user_id" => @conn.assigns.current_user.id}) %>
|
||||
|
||||
<p>
|
||||
|
||||
@@ -12,11 +12,16 @@
|
||||
<body>
|
||||
<header class="w-100 fixed bg-black-90 ph3 pv3 ph4-m ph5-l">
|
||||
<nav class="f6 fw6 ttu tracked fl">
|
||||
<a class="link dim white dib mr3" href="<%= FgHttpWeb.Endpoint.url() %>">FireGuard</a>
|
||||
</nav>
|
||||
<nav class="f6 fw6 ttu tracked fr">
|
||||
<a class="link dim white dib mr3" href="<%= Routes.device_path(@conn, :index) %>">Devices</a>
|
||||
<%= link("Fireguard", to: FgHttpWeb.Endpoint.url(), class: "link dim white dib mr3") %>
|
||||
</nav>
|
||||
<%= if @user_signed_in? do %>
|
||||
<nav class="f6 fw6 ttu tracked fr">
|
||||
<%= link("Devices", to: Routes.device_path(@conn, :index), class: "link dim white dib mr3") %>
|
||||
<%= link("Sign out", to: Routes.session_path(@conn, :delete, @current_session), method: :delete, class: "link dim white dib mr3") %>
|
||||
</nav>
|
||||
<% else %>
|
||||
|
||||
<% end %>
|
||||
</header>
|
||||
<main class="mw7 mw9-ns center pa3 pt5 ph5-ns">
|
||||
<%= @inner_content %>
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
<main class="pa4 black-80">
|
||||
<form class="measure center">
|
||||
<%= form_for(@changeset, Routes.session_path(@conn, :create), [class: "measure center"], fn f -> %>
|
||||
<%= if f.errors do %>
|
||||
<!-- Errors -->
|
||||
<% end %>
|
||||
|
||||
<fieldset id="sign_up" class="ba b--transparent ph0 mh0">
|
||||
<legend class="f4 fw6 ph0 mh0">Sign In</legend>
|
||||
<div class="mt3">
|
||||
<label class="db fw6 lh-copy f6" for="email-address">Email</label>
|
||||
<input class="pa2 input-reset ba bg-transparent hover-bg-black hover-white w-100" type="email" name="email-address" id="email-address">
|
||||
<%= label(:session_user, :email, class: "db fw6 lh-copy f6") %>
|
||||
<%= text_input(f, :user_email, class: "pa2 input-reset ba bg-transparent hover-bg-black hover-white w-100") %>
|
||||
</div>
|
||||
<div class="mv3">
|
||||
<label class="db fw6 lh-copy f6" for="password">Password</label>
|
||||
<input class="b pa2 input-reset ba bg-transparent hover-bg-black hover-white w-100" type="password" name="password" id="password">
|
||||
<%= label(:session_user, :password, class: "db fw6 lh-copy f6") %>
|
||||
<%= password_input(f, :user_password, class: "pa2 input-reset ba bg-transparent hover-bg-black hover-white w-100") %>
|
||||
</div>
|
||||
<label class="pa0 ma0 lh-copy f6 pointer"><input type="checkbox"> Remember me</label>
|
||||
</fieldset>
|
||||
<div class="">
|
||||
<input class="b ph3 pv2 input-reset ba b--black bg-transparent grow pointer f6 dib" type="submit" value="Sign in">
|
||||
<%= submit "Sign in", class: "b ph3 pv2 input-reset ba b--black bg-transparent grow pointer f6 dib" %>
|
||||
</div>
|
||||
<div class="lh-copy mt3">
|
||||
<a href="#0" class="f6 link dim black db">Sign up</a>
|
||||
<a href="#0" class="f6 link dim black db">Forgot your password?</a>
|
||||
</div>
|
||||
</form>
|
||||
<% end) %>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<h3>Account</h3>
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
defmodule FgHttpWeb.DeviceView do
|
||||
use FgHttpWeb, :view
|
||||
import Ecto.Changeset, only: [traverse_errors: 2]
|
||||
|
||||
def aggregated_errors(changeset) do
|
||||
traverse_errors(changeset, fn {msg, opts} ->
|
||||
Enum.reduce(opts, msg, fn {key, value}, acc ->
|
||||
String.replace(acc, "%{#{key}}", to_string(value))
|
||||
end)
|
||||
end)
|
||||
|> Enum.reduce("", fn {key, value}, acc ->
|
||||
joined_errors = Enum.join(value, "; ")
|
||||
"#{acc}#{key}: #{joined_errors}\n"
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
3
apps/fg_http/lib/fg_http_web/views/session_view.ex
Normal file
3
apps/fg_http/lib/fg_http_web/views/session_view.ex
Normal file
@@ -0,0 +1,3 @@
|
||||
defmodule FgHttpWeb.SessionView do
|
||||
use FgHttpWeb, :view
|
||||
end
|
||||
@@ -38,11 +38,13 @@ defmodule FgHttp.MixProject do
|
||||
defp deps do
|
||||
[
|
||||
{:phoenix, "~> 1.5.1"},
|
||||
{:argon2_elixir, "~> 2.0"},
|
||||
{:phoenix_pubsub, "~> 2.0"},
|
||||
{:phoenix_ecto, "~> 4.0"},
|
||||
{:ecto_sql, "~> 3.1"},
|
||||
{:ecto_enum, "~> 1.4.0"},
|
||||
{:ecto_network, "~> 1.3.0"},
|
||||
{:bamboo, "~> 1.5"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
{:phoenix_html, "~> 2.11"},
|
||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
|
||||
@@ -5,12 +5,15 @@ defmodule FgHttp.Repo.Migrations.CreateUsers do
|
||||
create table(:users) do
|
||||
add :email, :string
|
||||
add :confirmed_at, :utc_datetime
|
||||
add :password_digest, :string
|
||||
add :password_hash, :string
|
||||
add :last_signed_in_at, :utc_datetime
|
||||
add :reset_sent_at, :utc_datetime
|
||||
add :reset_token, :utc_datetime
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create unique_index(:users, [:email])
|
||||
create unique_index(:users, [:reset_token])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,8 +3,9 @@ defmodule FgHttp.Repo.Migrations.CreateDevices do
|
||||
|
||||
def change do
|
||||
create table(:devices) do
|
||||
add :name, :string
|
||||
add :public_key, :string
|
||||
add :name, :string, null: false
|
||||
add :public_key, :string, null: false
|
||||
add :ifname, :string, null: false
|
||||
add :last_ip, :inet
|
||||
add :user_id, references(:users, on_delete: :delete_all), null: false
|
||||
|
||||
@@ -12,5 +13,6 @@ defmodule FgHttp.Repo.Migrations.CreateDevices do
|
||||
end
|
||||
|
||||
create index(:devices, [:user_id])
|
||||
create unique_index(:devices, [:public_key])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,11 +3,13 @@ defmodule FgHttp.Repo.Migrations.CreateSessions do
|
||||
|
||||
def change do
|
||||
create table(:sessions) do
|
||||
add :user_id, references(:users, on_delete: :delete_all)
|
||||
add :user_id, references(:users, on_delete: :delete_all), null: false
|
||||
add :deleted_at, :utc_datetime
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create index(:sessions, [:user_id])
|
||||
create index(:sessions, [:deleted_at])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,22 +10,26 @@
|
||||
# We recommend using the bang functions (`insert!`, `update!`
|
||||
# and so on) as they will fail if something goes wrong.
|
||||
|
||||
alias FgHttp.Repo
|
||||
alias FgHttp.{Devices, Rules, Users}
|
||||
|
||||
Repo.transaction(fn ->
|
||||
{:ok, user} = FgHttp.Users.create_user(%{email: "testuser@fireguard.network"})
|
||||
{:ok, user} =
|
||||
Users.create_user(%{
|
||||
email: "factory@factory",
|
||||
password: "factory",
|
||||
password_confirmation: "factory"
|
||||
})
|
||||
|
||||
{: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, device} =
|
||||
Devices.create_device(%{
|
||||
user_id: user.id,
|
||||
ifname: "wg0",
|
||||
name: "Factory Device",
|
||||
public_key: "factory public key",
|
||||
last_ip: %Postgrex.INET{address: {127, 0, 0, 1}}
|
||||
})
|
||||
|
||||
{:ok, _rule} =
|
||||
FgHttp.Rules.create_rule(%{
|
||||
device_id: device.id,
|
||||
destination: %Postgrex.INET{address: {0, 0, 0, 0}, netmask: 0}
|
||||
})
|
||||
end)
|
||||
{:ok, _rule} =
|
||||
Rules.create_rule(%{
|
||||
device_id: device.id,
|
||||
destination: %Postgrex.INET{address: {0, 0, 0, 0}, netmask: 0}
|
||||
})
|
||||
|
||||
@@ -1,47 +1,42 @@
|
||||
defmodule FgHttpWeb.DeviceControllerTest do
|
||||
use FgHttpWeb.ConnCase
|
||||
use FgHttpWeb.ConnCase, async: true
|
||||
|
||||
alias FgHttp.Devices
|
||||
alias FgHttp.Users
|
||||
import FgHttp.Fixtures
|
||||
|
||||
@create_attrs %{name: "some name"}
|
||||
@create_attrs %{public_key: "foobar"}
|
||||
@update_attrs %{name: "some updated name"}
|
||||
@invalid_attrs %{user_id: nil}
|
||||
|
||||
def fixture(:user) do
|
||||
{:ok, user} = Users.create_user(%{email: "test"})
|
||||
user
|
||||
end
|
||||
|
||||
def fixture(:device) do
|
||||
{:ok, device} = Devices.create_device(Map.merge(%{user_id: fixture(:user).id}, @create_attrs))
|
||||
device
|
||||
end
|
||||
@invalid_attrs %{public_key: nil}
|
||||
|
||||
describe "index" do
|
||||
test "lists all devices", %{conn: conn} do
|
||||
# Mock authentication
|
||||
conn = Plug.Conn.assign(conn, :current_user, fixture(:user))
|
||||
|
||||
test "lists all devices", %{authed_conn: conn} do
|
||||
conn = get(conn, Routes.device_path(conn, :index))
|
||||
assert html_response(conn, 200) =~ "Listing Devices"
|
||||
end
|
||||
end
|
||||
|
||||
describe "new device" do
|
||||
test "renders form", %{conn: conn} do
|
||||
# Mock authentication
|
||||
conn = Plug.Conn.assign(conn, :current_user, fixture(:user))
|
||||
|
||||
test "renders form", %{authed_conn: conn} do
|
||||
conn = get(conn, Routes.device_path(conn, :new))
|
||||
assert html_response(conn, 200) =~ "New Device"
|
||||
end
|
||||
end
|
||||
|
||||
describe "create device" do
|
||||
test "redirects when data is valid", %{authed_conn: conn} do
|
||||
conn = post(conn, Routes.device_path(conn, :create), device: @create_attrs)
|
||||
assert html_response(conn, 302) =~ "redirected"
|
||||
end
|
||||
|
||||
test "renders errors when data is invalid", %{authed_conn: conn} do
|
||||
conn = post(conn, Routes.device_path(conn, :create), device: @invalid_attrs)
|
||||
assert html_response(conn, 200) =~ "public_key: can't be blank"
|
||||
end
|
||||
end
|
||||
|
||||
describe "edit device" do
|
||||
setup [:create_device]
|
||||
|
||||
test "renders form for editing chosen device", %{conn: conn, device: device} do
|
||||
test "renders form for editing chosen device", %{authed_conn: conn, device: device} do
|
||||
conn = get(conn, Routes.device_path(conn, :edit, device))
|
||||
assert html_response(conn, 200) =~ "Edit Device"
|
||||
end
|
||||
@@ -50,7 +45,7 @@ defmodule FgHttpWeb.DeviceControllerTest do
|
||||
describe "update device" do
|
||||
setup [:create_device]
|
||||
|
||||
test "redirects when data is valid", %{conn: conn, device: device} do
|
||||
test "redirects when data is valid", %{authed_conn: conn, device: device} do
|
||||
conn = put(conn, Routes.device_path(conn, :update, device), device: @update_attrs)
|
||||
assert redirected_to(conn) == Routes.device_path(conn, :show, device)
|
||||
|
||||
@@ -58,7 +53,7 @@ defmodule FgHttpWeb.DeviceControllerTest do
|
||||
assert html_response(conn, 200) =~ "some updated name"
|
||||
end
|
||||
|
||||
test "renders errors when data is invalid", %{conn: conn, device: device} do
|
||||
test "renders errors when data is invalid", %{authed_conn: conn, device: device} do
|
||||
conn = put(conn, Routes.device_path(conn, :update, device), device: @invalid_attrs)
|
||||
assert html_response(conn, 200) =~ "Edit Device"
|
||||
end
|
||||
@@ -67,7 +62,7 @@ defmodule FgHttpWeb.DeviceControllerTest do
|
||||
describe "delete device" do
|
||||
setup [:create_device]
|
||||
|
||||
test "deletes chosen device", %{conn: conn, device: device} do
|
||||
test "deletes chosen device", %{authed_conn: conn, device: device} do
|
||||
conn = delete(conn, Routes.device_path(conn, :delete, device))
|
||||
assert redirected_to(conn) == Routes.device_path(conn, :index)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
defmodule FgHttpWeb.RuleControllerTest do
|
||||
use FgHttpWeb.ConnCase
|
||||
use FgHttpWeb.ConnCase, async: true
|
||||
|
||||
describe "index" do
|
||||
end
|
||||
|
||||
@@ -19,6 +19,8 @@ defmodule FgHttpWeb.ConnCase do
|
||||
|
||||
alias Ecto.Adapters.SQL.Sandbox
|
||||
|
||||
import FgHttp.Fixtures
|
||||
|
||||
using do
|
||||
quote do
|
||||
# Import conveniences for testing with connections
|
||||
@@ -31,6 +33,26 @@ defmodule FgHttpWeb.ConnCase do
|
||||
end
|
||||
end
|
||||
|
||||
def new_conn do
|
||||
Phoenix.ConnTest.build_conn()
|
||||
end
|
||||
|
||||
def authed_conn do
|
||||
user = fixture(:user)
|
||||
|
||||
session =
|
||||
fixture(:session, %{
|
||||
user_id: user.id,
|
||||
user_password: "test",
|
||||
user_email: "test"
|
||||
})
|
||||
|
||||
new_conn()
|
||||
|> Plug.Conn.assign(:current_user, user)
|
||||
|> Plug.Conn.assign(:current_session, session)
|
||||
|> Plug.Conn.assign(:user_signed_in?, true)
|
||||
end
|
||||
|
||||
setup tags do
|
||||
:ok = Sandbox.checkout(FgHttp.Repo)
|
||||
|
||||
@@ -38,6 +60,6 @@ defmodule FgHttpWeb.ConnCase do
|
||||
Sandbox.mode(FgHttp.Repo, {:shared, self()})
|
||||
end
|
||||
|
||||
{:ok, conn: Phoenix.ConnTest.build_conn()}
|
||||
{:ok, unauthed_conn: new_conn(), authed_conn: authed_conn()}
|
||||
end
|
||||
end
|
||||
|
||||
28
apps/fg_http/test/support/fixtures.ex
Normal file
28
apps/fg_http/test/support/fixtures.ex
Normal file
@@ -0,0 +1,28 @@
|
||||
defmodule FgHttp.Fixtures do
|
||||
@moduledoc """
|
||||
Convenience helpers for inserting records
|
||||
"""
|
||||
alias FgHttp.{Devices, Repo, Sessions, Users, Users.User}
|
||||
|
||||
def fixture(:user) do
|
||||
case Repo.get_by(User, email: "test") do
|
||||
nil ->
|
||||
attrs = %{email: "test", password: "test", password_confirmation: "test"}
|
||||
{:ok, user} = Users.create_user(attrs)
|
||||
user
|
||||
|
||||
%User{} = user ->
|
||||
user
|
||||
end
|
||||
end
|
||||
|
||||
def fixture(:device) do
|
||||
attrs = %{public_key: "foobar", ifname: "wg0", name: "factory"}
|
||||
{:ok, device} = Devices.create_device(Map.merge(%{user_id: fixture(:user).id}, attrs))
|
||||
device
|
||||
end
|
||||
|
||||
def fixture(:session, attrs \\ %{}) do
|
||||
{:ok, _session} = Sessions.create_session(attrs)
|
||||
end
|
||||
end
|
||||
13
mix.lock
13
mix.lock
@@ -1,5 +1,9 @@
|
||||
%{
|
||||
"argon2_elixir": {:hex, :argon2_elixir, "2.3.0", "e251bdafd69308e8c1263e111600e6d68bd44f23d2cccbe43fcb1a417a76bc8e", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "28ccb63bff213aecec1f7f3dde9648418b031f822499973281d8f494b9d5a3b3"},
|
||||
"bamboo": {:hex, :bamboo, "1.5.0", "1926107d58adba6620450f254dfe8a3686637a291851fba125686fa8574842af", [:mix], [{:hackney, ">= 1.13.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d5f3d04d154e80176fd685e2531e73870d8700679f14d25a567e448abce6298d"},
|
||||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
|
||||
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"},
|
||||
"comeonin": {:hex, :comeonin, "5.3.1", "7fe612b739c78c9c1a75186ef2d322ce4d25032d119823269d0aa1e2f1e20025", [:mix], [], "hexpm", "d6222483060c17f0977fad1b7401ef0c5863c985a64352755f366aee3799c245"},
|
||||
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
|
||||
"cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"},
|
||||
"cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"},
|
||||
@@ -10,10 +14,17 @@
|
||||
"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"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
|
||||
"ex_machina": {:hex, :ex_machina, "2.4.0", "09a34c5d371bfb5f78399029194a8ff67aff340ebe8ba19040181af35315eabb", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "a20bc9ddc721b33ea913b93666c5d0bdca5cbad7a67540784ae277228832d72c"},
|
||||
"file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"},
|
||||
"gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"},
|
||||
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"},
|
||||
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"},
|
||||
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
|
||||
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
|
||||
"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"},
|
||||
@@ -25,5 +36,7 @@
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
|
||||
"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"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
|
||||
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user