mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
basic login logout
This commit is contained in:
@@ -23,6 +23,6 @@ defmodule FgHttp.Devices.Device do
|
||||
def changeset(device, attrs) do
|
||||
device
|
||||
|> cast(attrs, [:last_ip, :user_id, :name, :public_key])
|
||||
|> validate_required([:user_id])
|
||||
|> validate_required([:user_id, :name, :public_key])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
defmodule FgHttp.Factory do
|
||||
@moduledoc """
|
||||
Fixtures generator
|
||||
"""
|
||||
use ExMachina.Ecto, repo: FgHttp.Repo
|
||||
|
||||
alias FgHttp.{Devices.Device, Rules.Rule, Users.User}
|
||||
|
||||
def user_factory do
|
||||
%User{
|
||||
email: "factory@factory",
|
||||
password: "factory",
|
||||
password_confirmation: "factory"
|
||||
}
|
||||
end
|
||||
|
||||
def device_factory do
|
||||
%Device{
|
||||
user: build(:user),
|
||||
name: "Factory Device",
|
||||
public_key: "factory public key",
|
||||
last_ip: %Postgrex.INET{address: {127, 0, 0, 1}}
|
||||
}
|
||||
end
|
||||
|
||||
def rule_factory do
|
||||
%Rule{
|
||||
device: build(:device),
|
||||
destination: %Postgrex.INET{address: {0, 0, 0, 0}, netmask: 0}
|
||||
}
|
||||
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,10 @@ defmodule FgHttp.Users do
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_user!(email: email) do
|
||||
Repo.get_by!(User, email: email)
|
||||
end
|
||||
|
||||
def get_user!(id), do: Repo.get!(User, id)
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -29,10 +29,11 @@ defmodule FgHttp.Users.User do
|
||||
@doc false
|
||||
def create_changeset(user, attrs \\ %{}) do
|
||||
user
|
||||
|> cast(attrs, [:email, :password, :password_confirmation])
|
||||
|> 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
|
||||
@@ -50,6 +51,8 @@ defmodule FgHttp.Users.User do
|
||||
|> 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
|
||||
@@ -65,7 +68,7 @@ defmodule FgHttp.Users.User do
|
||||
end
|
||||
|
||||
def authenticate_user(user, password_candidate) do
|
||||
Argon2.check_pass(user.password, password_candidate, hash_key: :password)
|
||||
Argon2.check_pass(user, password_candidate)
|
||||
end
|
||||
|
||||
defp verify_current_password(user, current_password) do
|
||||
@@ -79,7 +82,7 @@ defmodule FgHttp.Users.User do
|
||||
changes: %{password: password}
|
||||
} = changeset
|
||||
) do
|
||||
change(changeset, password: Argon2.hash_pwd_salt(password))
|
||||
change(changeset, password_hash: Argon2.hash_pwd_salt(password))
|
||||
end
|
||||
|
||||
defp put_password_hash(changeset), do: changeset
|
||||
|
||||
@@ -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,9 @@ defmodule FgHttpWeb.DeviceController do
|
||||
render(conn, "new.html", changeset: changeset)
|
||||
end
|
||||
|
||||
def create(conn, %{"device" => device_params}) do
|
||||
create_params = %{
|
||||
"user_id" => conn.assigns.current_user.id,
|
||||
"name" => "Auto"
|
||||
}
|
||||
|
||||
all_params = Map.merge(device_params, create_params)
|
||||
def create(conn, %{"device" => %{"public_key" => _public_key} = device_params}) do
|
||||
our_params = %{user_id: conn.assigns.current_user.id, name: "Default"}
|
||||
all_params = Map.merge(device_params, our_params)
|
||||
|
||||
case Devices.create_device(all_params) do
|
||||
{:ok, device} ->
|
||||
|
||||
@@ -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.SessionLoader when action in [:delete]
|
||||
|
||||
# GET /sessions/new
|
||||
def new(conn, _params) do
|
||||
@@ -18,10 +18,8 @@ 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
|
||||
|> assign(:current_session, session)
|
||||
@@ -31,29 +29,32 @@ 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()
|
||||
if Map.get(conn.assigns, :user_signed_in?) do
|
||||
conn
|
||||
|> redirect(to: "/")
|
||||
|> halt()
|
||||
else
|
||||
conn
|
||||
|> assign(:user_signed_in?, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ defmodule FgHttpWeb.UserController do
|
||||
use FgHttpWeb, :controller
|
||||
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
|
||||
|
||||
@@ -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
|
||||
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("blah 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
|
||||
@@ -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>
|
||||
|
||||
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
|
||||
@@ -43,7 +43,6 @@ defmodule FgHttp.MixProject do
|
||||
{:phoenix_ecto, "~> 4.0"},
|
||||
{:ecto_sql, "~> 3.1"},
|
||||
{:ecto_enum, "~> 1.4.0"},
|
||||
{:ex_machina, "~> 2.4"},
|
||||
{:ecto_network, "~> 1.3.0"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
{:phoenix_html, "~> 2.11"},
|
||||
|
||||
@@ -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,7 +10,25 @@
|
||||
# We recommend using the bang functions (`insert!`, `update!`
|
||||
# and so on) as they will fail if something goes wrong.
|
||||
|
||||
import FgHttp.Factory
|
||||
alias FgHttp.{Devices, Rules, Users}
|
||||
|
||||
# Inserts needed associations
|
||||
insert(:rule)
|
||||
{:ok, user} =
|
||||
Users.create_user(%{
|
||||
email: "factory@factory",
|
||||
password: "factory",
|
||||
password_confirmation: "factory"
|
||||
})
|
||||
|
||||
{:ok, device} =
|
||||
Devices.create_device(%{
|
||||
user_id: user.id,
|
||||
name: "Factory Device",
|
||||
public_key: "factory public key",
|
||||
last_ip: %Postgrex.INET{address: {127, 0, 0, 1}}
|
||||
})
|
||||
|
||||
{:ok, _rule} =
|
||||
Rules.create_rule(%{
|
||||
device_id: device.id,
|
||||
destination: %Postgrex.INET{address: {0, 0, 0, 0}, netmask: 0}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user