diff --git a/apps/fg_http/lib/fg_http/devices/device.ex b/apps/fg_http/lib/fg_http/devices/device.ex index 794575654..1625e53fe 100644 --- a/apps/fg_http/lib/fg_http/devices/device.ex +++ b/apps/fg_http/lib/fg_http/devices/device.ex @@ -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 diff --git a/apps/fg_http/lib/fg_http/factory.ex b/apps/fg_http/lib/fg_http/factory.ex deleted file mode 100644 index 4407bdffd..000000000 --- a/apps/fg_http/lib/fg_http/factory.ex +++ /dev/null @@ -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 diff --git a/apps/fg_http/lib/fg_http/sessions.ex b/apps/fg_http/lib/fg_http/sessions.ex index 3d1f7d325..de3cb808d 100644 --- a/apps/fg_http/lib/fg_http/sessions.ex +++ b/apps/fg_http/lib/fg_http/sessions.ex @@ -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. diff --git a/apps/fg_http/lib/fg_http/sessions/session.ex b/apps/fg_http/lib/fg_http/sessions/session.ex index 506e6075c..bc191d2a8 100644 --- a/apps/fg_http/lib/fg_http/sessions/session.ex +++ b/apps/fg_http/lib/fg_http/sessions/session.ex @@ -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 diff --git a/apps/fg_http/lib/fg_http/users.ex b/apps/fg_http/lib/fg_http/users.ex index 0bb024cb1..08f0a2b85 100644 --- a/apps/fg_http/lib/fg_http/users.ex +++ b/apps/fg_http/lib/fg_http/users.ex @@ -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 """ diff --git a/apps/fg_http/lib/fg_http/users/user.ex b/apps/fg_http/lib/fg_http/users/user.ex index bc28dee0f..897c2b6b0 100644 --- a/apps/fg_http/lib/fg_http/users/user.ex +++ b/apps/fg_http/lib/fg_http/users/user.ex @@ -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 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 97aab5114..b07295ebc 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 @@ -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} -> 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 8ad21d26c..deb6df527 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 @@ -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) diff --git a/apps/fg_http/lib/fg_http_web/controllers/session_controller.ex b/apps/fg_http/lib/fg_http_web/controllers/session_controller.ex index c2cd30e7a..197b4c391 100644 --- a/apps/fg_http/lib/fg_http_web/controllers/session_controller.ex +++ b/apps/fg_http/lib/fg_http_web/controllers/session_controller.ex @@ -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 diff --git a/apps/fg_http/lib/fg_http_web/controllers/user_controller.ex b/apps/fg_http/lib/fg_http_web/controllers/user_controller.ex index 5ab086f44..a3912bbf4 100644 --- a/apps/fg_http/lib/fg_http_web/controllers/user_controller.ex +++ b/apps/fg_http/lib/fg_http_web/controllers/user_controller.ex @@ -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 diff --git a/apps/fg_http/lib/fg_http_web/live/new_device_live.ex b/apps/fg_http/lib/fg_http_web/live/new_device_live.ex index 8e68ccf47..8a8137b67 100644 --- a/apps/fg_http/lib/fg_http_web/live/new_device_live.ex +++ b/apps/fg_http/lib/fg_http_web/live/new_device_live.ex @@ -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 diff --git a/apps/fg_http/lib/fg_http_web/live/new_device_live.html.leex b/apps/fg_http/lib/fg_http_web/live/new_device_live.html.leex index 1bdcfd0a9..5ff1ed6cc 100644 --- a/apps/fg_http/lib/fg_http_web/live/new_device_live.html.leex +++ b/apps/fg_http/lib/fg_http_web/live/new_device_live.html.leex @@ -42,8 +42,7 @@ Endpoint = <%= Application.fetch_env!(:fg_http, :vpn_endpoint) %>
<%= 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)) %>
<% end %> diff --git a/apps/fg_http/lib/fg_http_web/plugs/authenticator.ex b/apps/fg_http/lib/fg_http_web/plugs/authenticator.ex deleted file mode 100644 index 53c347906..000000000 --- a/apps/fg_http/lib/fg_http_web/plugs/authenticator.ex +++ /dev/null @@ -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 diff --git a/apps/fg_http/lib/fg_http_web/plugs/session_loader.ex b/apps/fg_http/lib/fg_http_web/plugs/session_loader.ex new file mode 100644 index 000000000..7abed5e24 --- /dev/null +++ b/apps/fg_http/lib/fg_http_web/plugs/session_loader.ex @@ -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 diff --git a/apps/fg_http/lib/fg_http_web/templates/layout/app.html.eex b/apps/fg_http/lib/fg_http_web/templates/layout/app.html.eex index b47b82095..a4f9211f2 100644 --- a/apps/fg_http/lib/fg_http_web/templates/layout/app.html.eex +++ b/apps/fg_http/lib/fg_http_web/templates/layout/app.html.eex @@ -12,11 +12,16 @@