diff --git a/assets/package-lock.json b/assets/package-lock.json index 426b2f6..455eb99 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -1439,6 +1439,12 @@ "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", "dev": true }, + "alpinejs": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-2.8.0.tgz", + "integrity": "sha512-UHvE71BBYHJa6+RxjMwM0g4eokq+5yG1QO5C6VieUu0MVPlsUSlwvrh19VVZQApNIlQsgcvQUhIalzAIgoAPoA==", + "dev": true + }, "amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", @@ -5036,6 +5042,9 @@ "phoenix_html": { "version": "file:../deps/phoenix_html" }, + "phoenix_live_view": { + "version": "file:../deps/phoenix_live_view" + }, "picomatch": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", @@ -7651,6 +7660,12 @@ "is-number": "^7.0.0" } }, + "topbar": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/topbar/-/topbar-1.0.1.tgz", + "integrity": "sha512-HZqQSMBiG29vcjOrqKCM9iGY/h69G5gQH7ae83ZCPz5uPmbQKwK0sMEqzVDBiu64tWHJ+kk9NApECrF+FAAvRA==", + "dev": true + }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", diff --git a/assets/package.json b/assets/package.json index 6292b61..af674a3 100644 --- a/assets/package.json +++ b/assets/package.json @@ -9,12 +9,14 @@ }, "dependencies": { "phoenix": "file:../deps/phoenix", - "phoenix_html": "file:../deps/phoenix_html" + "phoenix_html": "file:../deps/phoenix_html", + "phoenix_live_view": "file:../deps/phoenix_live_view" }, "devDependencies": { "@babel/core": "^7.0.0", "@babel/preset-env": "^7.0.0", "@fullhuman/postcss-purgecss": "^3.0.0", + "alpinejs": "^2.8.0", "babel-loader": "^8.0.0", "bulma": "^0.9.1", "copy-webpack-plugin": "^6.3.2", @@ -29,6 +31,7 @@ "sass-loader": "^10.1.0", "standard": "^16.0.3", "terser-webpack-plugin": "^5.0.3", + "topbar": "^1.0.1", "webpack": "5.10.0", "webpack-cli": "^4.2.0" } diff --git a/assets/scripts/main.js b/assets/scripts/main.js index 31f3a82..c35bba5 100644 --- a/assets/scripts/main.js +++ b/assets/scripts/main.js @@ -1,26 +1,27 @@ -import '../../deps/phoenix_html/priv/static/phoenix_html.js' +import { Socket } from 'phoenix' +import LiveSocket from 'phoenix_live_view' +import topbar from 'topbar' -import '../styles/main.scss' +import 'alpinejs' +import 'phoenix_html/priv/static/phoenix_html.js' -function documentReady (fn) { - if (document.readyState === 'complete' || document.readyState === 'interactive') { - setTimeout(fn, 1) - } else { - document.addEventListener('DOMContentLoaded', fn) +const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content') +const liveSocket = new LiveSocket('/live', Socket, { + params: { + _csrf_token: csrfToken } -} +}) -function toggleDisplay (selector, value) { - const input = document.querySelector(selector) - input.style.display = (value) ? 'block' : 'none' -} +topbar.config({barColors: {0: '#63b1bc'}, shadowBlur: 0}) +window.addEventListener('phx:page-loading-start', info => topbar.show()) +window.addEventListener('phx:page-loading-stop', info => topbar.hide()) -documentReady(function () { - document - .querySelectorAll('input[name="user[type]"]') - .forEach((field) => { - field.addEventListener('change', (e) => { - toggleDisplay('div.company_name', (e.target.value === 'business')) - }) - }) -}) +liveSocket.connect() + +// Expose liveSocket on window for web console debug logs and latency simulation: +// >> liveSocket.enableDebug() +// >> liveSocket.enableLatencySim(1000) +// The latency simulator is enabled for the duration of the browser session. +// Call disableLatencySim() to disable: +// >> liveSocket.disableLatencySim() +window.liveSocket = liveSocket diff --git a/lib/recognizer/accounts.ex b/lib/recognizer/accounts.ex index c491a98..b5ede4b 100644 --- a/lib/recognizer/accounts.ex +++ b/lib/recognizer/accounts.ex @@ -555,12 +555,18 @@ defmodule Recognizer.Accounts do Redix.noreply_command(:redix, ["SET", "two_factor_settings:#{user.id}", Jason.encode!(attrs)]) + send_new_two_factor_notification(user, new_seed, preference) + end + + @doc """ + Sends a two factor confirmation code for new settings. This is a no-op if the + user is trying to setup an app. + """ + def send_new_two_factor_notification(user, seed, preference) do if preference != "app" do - token = Authentication.generate_token(new_seed) + token = Authentication.generate_token(seed) Notification.deliver_two_factor_token(user, token, String.to_existing_atom(preference)) end - - attrs end @doc """ diff --git a/lib/recognizer_web.ex b/lib/recognizer_web.ex index f194c8a..9c9dfe3 100644 --- a/lib/recognizer_web.ex +++ b/lib/recognizer_web.ex @@ -21,6 +21,7 @@ defmodule RecognizerWeb do quote do use Phoenix.Controller, namespace: RecognizerWeb + import Phoenix.LiveView.Controller import Plug.Conn import RecognizerWeb.Gettext @@ -47,12 +48,30 @@ defmodule RecognizerWeb do end end + def live_view do + quote do + use Phoenix.LiveView, + layout: {RecognizerWeb.LayoutView, "live.html"} + + unquote(view_helpers()) + end + end + + def live_component do + quote do + use Phoenix.LiveComponent + + unquote(view_helpers()) + end + end + def router do quote do use Phoenix.Router import Plug.Conn import Phoenix.Controller + import Phoenix.LiveView.Router end end @@ -70,6 +89,7 @@ defmodule RecognizerWeb do # Import basic rendering functionality (render, render_layout, etc) import Phoenix.View + import Phoenix.LiveView.Helpers import RecognizerWeb.ErrorHelpers import RecognizerWeb.FormHelpers diff --git a/lib/recognizer_web/controllers/accounts/user_settings_controller.ex b/lib/recognizer_web/controllers/accounts/user_settings_controller.ex index 98f51c7..81096e5 100644 --- a/lib/recognizer_web/controllers/accounts/user_settings_controller.ex +++ b/lib/recognizer_web/controllers/accounts/user_settings_controller.ex @@ -14,33 +14,6 @@ defmodule RecognizerWeb.Accounts.UserSettingsController do end end - def two_factor(conn, _params) do - user = Authentication.fetch_current_user(conn) - {:ok, %{"two_factor_seed" => seed}} = Accounts.get_new_two_factor_settings(user) - - render(conn, "confirm_two_factor.html", - barcode: Authentication.generate_totp_barcode(user, seed), - totp_app_url: Authentication.get_totp_app_url(user, seed) - ) - end - - def two_factor_confirm(conn, params) do - two_factor_code = Map.get(params, "two_factor_code", "") - user = Authentication.fetch_current_user(conn) - - case Accounts.confirm_and_save_two_factor_settings(two_factor_code, user) do - {:ok, _updated_user} -> - conn - |> put_flash(:info, "Two factor code verified.") - |> redirect(to: Routes.user_settings_path(conn, :edit)) - - _ -> - conn - |> put_flash(:error, "Two factor code is invalid.") - |> redirect(to: Routes.user_settings_path(conn, :confirm_two_factor)) - end - end - def update(conn, %{"action" => "update", "user" => user_params}) do user = Authentication.fetch_current_user(conn) @@ -83,13 +56,12 @@ defmodule RecognizerWeb.Accounts.UserSettingsController do end end - def update(conn, %{"action" => "update_two_factor", "user" => user_params}) do + def update(conn, %{"action" => "update_two_factor", "user" => %{"two_factor_enabled" => "1"}}) do user = Authentication.fetch_current_user(conn) - preference = get_in(user_params, ["notification_preference", "two_factor"]) - Accounts.generate_and_cache_new_two_factor_settings(user, preference) - - redirect(conn, to: Routes.user_settings_path(conn, :two_factor)) + conn + |> put_session(:two_factor_user_id, user.id) + |> redirect(to: Routes.live_path(conn, RecognizerWeb.Accounts.TwoFactorSettingsLive)) end defp assign_email_and_password_changesets(conn, _opts) do diff --git a/lib/recognizer_web/endpoint.ex b/lib/recognizer_web/endpoint.ex index 464085b..6f35618 100644 --- a/lib/recognizer_web/endpoint.ex +++ b/lib/recognizer_web/endpoint.ex @@ -8,6 +8,8 @@ defmodule RecognizerWeb.Endpoint do signing_salt: "juvsYHmf" ] + socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] + plug RecognizerWeb.HealthcheckPlug plug Plug.Static, diff --git a/lib/recognizer_web/live/accounts/two-factor-settings.ex b/lib/recognizer_web/live/accounts/two-factor-settings.ex new file mode 100644 index 0000000..452b343 --- /dev/null +++ b/lib/recognizer_web/live/accounts/two-factor-settings.ex @@ -0,0 +1,108 @@ +defmodule RecognizerWeb.Accounts.TwoFactorSettingsLive do + use RecognizerWeb, :live_view + + alias Recognizer.Accounts + + def mount(_params, %{"two_factor_user_id" => user_id}, socket) do + user = Accounts.get_user!(user_id) + changeset = Accounts.change_user_two_factor(user, %{}) + + {:ok, + socket + |> assign(:user, user) + |> assign(:changeset, changeset) + |> assign(:params, %{}) + |> assign(:settings, %{}) + |> assign(:step, :choice)} + end + + def handle_event("choice", %{"user" => user_params}, socket) do + user = socket.assigns.user + + changeset = Accounts.change_user_two_factor(user, user_params) + {:ok, settings} = Accounts.get_new_two_factor_settings(user) + + {:noreply, + socket + |> assign(:changeset, changeset) + |> assign(:params, user_params) + |> assign(:settings, settings) + |> assign(:step, :backup)} + end + + def handle_event("backup", _params, socket) do + notification_preferences = Ecto.Changeset.get_field(socket.assigns.changeset, :notification_preference) + + case notification_preferences.two_factor do + :app -> + {:noreply, assign(socket, :step, :app_validate)} + + _ -> + {:noreply, assign(socket, :step, :phone_number)} + end + end + + def handle_event("app_validate", %{"user" => %{"two_factor_code" => code}}, socket) do + user = socket.assigns.user + + with {:ok, semi_updated_user} <- Accounts.confirm_and_save_two_factor_settings(code, user), + {:ok, updated_user} <- Accounts.update_user(semi_updated_user, socket.assigns.params) do + {:noreply, + socket + |> put_flash(:info, "Two factor authentication enabled.") + |> push_redirect(to: Routes.user_settings_path(socket, :edit))} + else + _ -> {:noreply, put_flash(socket, :error, "Two factor code is invalid.")} + end + end + + def handle_event("phone_number", %{"user" => user_attrs}, socket) do + user = socket.assigns.user + params = Map.merge(socket.assigns.params, user_attrs) + changeset = Accounts.change_user_two_factor(user, params) + + if changeset.valid? do + {:noreply, + socket + |> assign(:changeset, changeset) + |> assign(:params, params) + |> assign(:step, :phone_number_validate)} + else + {:noreply, + socket + |> assign(:changeset, changeset) + |> assign(:params, params)} + end + end + + def handle_event("phone_number_validate", %{"user" => %{"two_factor_code" => code}}, socket) do + user = socket.assigns.user + + with {:ok, semi_updated_user} <- Accounts.confirm_and_save_two_factor_settings(code, user), + {:ok, updated_user} <- Accounts.update_user(semi_updated_user, socket.assigns.params) do + {:noreply, + socket + |> put_flash(:info, "Two factor authentication enabled.") + |> push_redirect(to: Routes.user_settings_path(socket, :edit))} + else + _ -> {:noreply, put_flash(socket, :error, "Two factor code is invalid.")} + end + end + + def handle_event("resend", _params, socket) do + %{user: user, settings: settings} = socket.assigns + + Accounts.send_new_two_factor_notification( + user, + settings["two_factor_seed"], + settings["notification_preference"]["two_factor"] + ) + + {:noreply, put_flash(socket, :info, "Two factor code sent.")} + end + + def render(assigns) do + template = to_string(assigns.step) <> ".html" + RecognizerWeb.Accounts.TwoFactorSettingsView.render(template, assigns) + end +end diff --git a/lib/recognizer_web/router.ex b/lib/recognizer_web/router.ex index bb88620..7ca63e1 100644 --- a/lib/recognizer_web/router.ex +++ b/lib/recognizer_web/router.ex @@ -4,9 +4,10 @@ defmodule RecognizerWeb.Router do pipeline :browser do plug :accepts, ["html"] plug :fetch_session - plug :fetch_flash + plug :fetch_live_flash plug :protect_from_forgery plug :put_secure_browser_headers + plug :put_root_layout, {RecognizerWeb.LayoutView, :root} end pipeline :api do @@ -96,7 +97,6 @@ defmodule RecognizerWeb.Router do get "/settings", UserSettingsController, :edit put "/settings", UserSettingsController, :update - get "/settings/two-factor", UserSettingsController, :two_factor - post "/settings/two-factor", UserSettingsController, :two_factor_confirm + live "/settings/two-factor", TwoFactorSettingsLive end end diff --git a/lib/recognizer_web/templates/accounts/two_factor_settings/app_validate.html.leex b/lib/recognizer_web/templates/accounts/two_factor_settings/app_validate.html.leex new file mode 100644 index 0000000..4c4b13b --- /dev/null +++ b/lib/recognizer_web/templates/accounts/two_factor_settings/app_validate.html.leex @@ -0,0 +1,47 @@ +
+

Two Factor App

+ +
+

+ Scan the image below with the two-factor authentication app on your phone. + If you can’t use a barcode, use the URL instead. +

+
+ +
+ <%= raw two_factor_barcode(@user, @settings) %> +
+ +
+
<%= two_factor_url(@user, @settings) %>
+
+ + <%= form_for @changeset, "#", [phx_submit: :app_validate], fn f -> %> +
+ <%= label f, :two_factor_code, "Enter the six-digit authentication code", class: "label" %> + +
+ <%= text_input f, :two_factor_code, + inputmode: "numeric", + pattern: "[0-9]*", + autocomplete: "one-time-code", + required: true, + class: "is-medium #{input_classes(f, :two_factor_code)}" + %> +
+ + <%= error_tag f, :two_factor_code %> +
+ +
+

+ After scanning the barcode image, the app will display a six-digit code + that you can enter above. +

+
+ +
+ <%= submit "Enable", class: "button is-secondary" %> +
+ <% end %> +
diff --git a/lib/recognizer_web/templates/accounts/two_factor_settings/backup.html.leex b/lib/recognizer_web/templates/accounts/two_factor_settings/backup.html.leex new file mode 100644 index 0000000..98e93e0 --- /dev/null +++ b/lib/recognizer_web/templates/accounts/two_factor_settings/backup.html.leex @@ -0,0 +1,34 @@ +
+

Backup Codes

+ +
+

+ Recovery codes are used to access your account if you have lost the + access to your phone. +

+ +

+ + Download, print or copy your recovery codes before continuing + two-factor authentication setup. + +

+
+ +
<%= @settings["recovery_codes"] |> Enum.map(&Map.get(&1, "code")) |> Enum.join("\n") %>
+ +
+

+ + Treat your recovery codes with the same level of attention as you would + your password! + +

+
+ +
+
+ <%= submit "Next", class: "button is-secondary", phx_click: "backup" %> +
+
+
diff --git a/lib/recognizer_web/templates/accounts/two_factor_settings/choice.html.leex b/lib/recognizer_web/templates/accounts/two_factor_settings/choice.html.leex new file mode 100644 index 0000000..6a87ca4 --- /dev/null +++ b/lib/recognizer_web/templates/accounts/two_factor_settings/choice.html.leex @@ -0,0 +1,50 @@ +
+

Enable Two Factor

+ +
+

+ Two-factor authentication adds an additional layer of security to your + account by requiring more than just a password to log in. We + strongly urge you to enable 2FA for the safety of your + account. +

+
+ + <%= form_for @changeset, "#", [phx_submit: :choice], fn f -> %> + <%= inputs_for f, :notification_preference, fn n -> %> +
+ Authentication Method Preference +
+ +
+
+ <%= label class: "label" do %> + <%= radio_button n, :two_factor, "app" %> + Authenticator App + <% end %> + + <%= label class: "label" do %> + <%= radio_button n, :two_factor, "text" %> + Text Message + <% end %> + + <%= label class: "label" do %> + <%= radio_button n, :two_factor, "voice" %> + Phone Call + <% end %> +
+
+ <% end %> + +

+ Message and data rates may apply for text message and phone call + methods. +

+ +
+
+ <%= submit "Update", class: "button is-secondary" %> +
+
+ <% end %> +
diff --git a/lib/recognizer_web/templates/accounts/two_factor_settings/phone_number.html.leex b/lib/recognizer_web/templates/accounts/two_factor_settings/phone_number.html.leex new file mode 100644 index 0000000..02c7ff5 --- /dev/null +++ b/lib/recognizer_web/templates/accounts/two_factor_settings/phone_number.html.leex @@ -0,0 +1,40 @@ +
+

Confirm Phone Number

+ +
+ <%= if has_phone_number?(@user) do %> +

+ Please confirm your phone number to use text or voice two-factor + authentication. +

+ <% else %> +

+ Please add a valid phone number to use text or voice two-factor + authentication. +

+ <% end %> +
+ + <%= form_for @changeset, "#", [phx_submit: :phone_number], fn f -> %> +
+ <%= label f, :phone_number, "Phone Number", class: "label" %> + +
+ <%= email_input f, :phone_number, + autocapitalize: "none", + autocomplete: "tel", + autocorrect: "off", + class: input_classes(f, :phone_number), + spellcheck: "false", + type: "tel" + %> +
+ + <%= error_tag f, :phone_number %> +
+ +
+ <%= submit "Confirm", class: "button is-secondary" %> +
+ <% end %> +
diff --git a/lib/recognizer_web/templates/accounts/two_factor_settings/phone_number_validate.html.leex b/lib/recognizer_web/templates/accounts/two_factor_settings/phone_number_validate.html.leex new file mode 100644 index 0000000..1a81d5a --- /dev/null +++ b/lib/recognizer_web/templates/accounts/two_factor_settings/phone_number_validate.html.leex @@ -0,0 +1,31 @@ +
+

Two Factor Verification

+ + <%= form_for @changeset, "#", [phx_submit: :phone_number_validate], fn f -> %> +
+ <%= label f, :two_factor_code, "Enter the six-digit authentication code", class: "label" %> + +
+ <%= text_input f, :two_factor_code, + inputmode: "numeric", + pattern: "[0-9]*", + autocomplete: "one-time-code", + required: true, + class: "is-medium #{input_classes(f, :two_factor_code)}" + %> +
+ + <%= error_tag f, :two_factor_code %> +
+ +
+ <%= submit "Enable", class: "button is-secondary" %> +
+ <% end %> + +
+ +

+ Didn't get the code? <%= link "Resend", to: "#", phx_click: "resend" %> +

+
diff --git a/lib/recognizer_web/templates/accounts/user_registration/new.html.eex b/lib/recognizer_web/templates/accounts/user_registration/new.html.eex index 9cf8e36..944683f 100644 --- a/lib/recognizer_web/templates/accounts/user_registration/new.html.eex +++ b/lib/recognizer_web/templates/accounts/user_registration/new.html.eex @@ -1,4 +1,4 @@ -
+

Create Account

<%= form_for @changeset, Routes.user_registration_path(@conn, :create), fn f -> %> @@ -40,18 +40,18 @@
<%= label class: "label" do %> - <%= radio_button f, :type, :individual %> + <%= radio_button f, :type, :individual, "x-model": "accountType" %> Personal Account <% end %> <%= label class: "label" do %> - <%= radio_button f, :type, :business %> + <%= radio_button f, :type, :business, "x-model": "accountType" %> Organization Account <% end %>
-
+
<%= label f, :email, "Company Name", class: "label" %>
diff --git a/lib/recognizer_web/templates/accounts/user_settings/confirm_two_factor.html.eex b/lib/recognizer_web/templates/accounts/user_settings/confirm_two_factor.html.eex deleted file mode 100644 index 44da4de..0000000 --- a/lib/recognizer_web/templates/accounts/user_settings/confirm_two_factor.html.eex +++ /dev/null @@ -1,37 +0,0 @@ -
-

Configure App

- -
-

Complete the following steps to configure your mobile app:

-

1. Install an authenticator app on your mobile device.

-

2. In the app choose to add by QR code.

-

3. Scan the barcode below.

-
- -
- <%= raw @barcode %> -
- -
-

If you are unable to scan the QR code you can enter the URL manually:

- -

<%= @totp_app_url %>
- -

4. Enter the displayed 6-digit code:

-
- - <%= form_for @conn, Routes.user_settings_path(@conn, :two_factor_confirm), fn f -> %> -
-
- <%= text_input f, :two_factor_code, inputmode: "numeric", pattern: "[0-9]*", autocomplete: "one-time-code", required: true, class: "is-medium #{input_classes(f, :two_factor_code)}" %> -
- - <%= error_tag f, :two_factor_code %> -
- -
- <%= submit "Verify Code", class: "button is-secondary" %> - Cancel -
- <% end %> -
diff --git a/lib/recognizer_web/templates/accounts/user_settings/edit.html.eex b/lib/recognizer_web/templates/accounts/user_settings/edit.html.eex index 4840653..76c7076 100644 --- a/lib/recognizer_web/templates/accounts/user_settings/edit.html.eex +++ b/lib/recognizer_web/templates/accounts/user_settings/edit.html.eex @@ -13,7 +13,7 @@
-
+

Change Profile

<%= form_for @changeset, Routes.user_settings_path(@conn, :update), fn f -> %> @@ -56,18 +56,18 @@
<%= label class: "label" do %> - <%= radio_button f, :type, :individual %> + <%= radio_button f, :type, :individual, "x-model": "accountType" %> Personal Account <% end %> <%= label class: "label" do %> - <%= radio_button f, :type, :business %> + <%= radio_button f, :type, :business, "x-model": "accountType" %> Organization Account <% end %>
-
+
<%= label f, :email, "Company Name", class: "label" %>
@@ -220,36 +220,6 @@ every time you log in.

- <%= inputs_for f, :notification_preference, fn n -> %> -
- Authentication Method Preference -
- -
-
- <%= label class: "label" do %> - <%= radio_button n, :two_factor, "app" %> - Authenticator App - <% end %> - - <%= label class: "label" do %> - <%= radio_button n, :two_factor, "text" %> - Text Message - <% end %> - - <%= label class: "label" do %> - <%= radio_button n, :two_factor, "voice" %> - Phone Call - <% end %> -
-
- <% end %> - -

- Message and data rates may apply for text message and phone call - methods. -

-
<%= submit "Enable Two Factor", class: "button is-secondary" %> diff --git a/lib/recognizer_web/templates/accounts/user_two_factor/new.html.eex b/lib/recognizer_web/templates/accounts/user_two_factor/new.html.eex deleted file mode 100644 index f15a013..0000000 --- a/lib/recognizer_web/templates/accounts/user_two_factor/new.html.eex +++ /dev/null @@ -1,60 +0,0 @@ -
-

Enter Security Code

- -
- <%= case @two_factor_method do %> - <% :text -> %> -

- We have sent a text message to your registered phone number. -

- <% :voice -> %> -

- You will receive an automated phone call with your security code. -

- <% _ -> %> -

- Please use your two factor application to generate a security code. -

- <% end %> -
- - <%= form_for @conn, Routes.user_two_factor_path(@conn, :create), [as: :user], fn f -> %> -
- <%= label f, :two_factor_code, "Security Code", class: "label" %> - -
- <%= text_input f, :two_factor_code, - autocapitalize: "none", - autocomplete: "on", - autocomplete: "one-time-code", - autocorrect: "off", - autofocus: true, - class: "is-medium #{input_classes(f, :two_factor_code)}", - inputmode: "numeric", - pattern: "[0-9]*", - required: true - %> -
- - <%= error_tag f, :two_factor_code %> -
- -
-
- <%= submit "Log in", class: "button is-secondary" %> -
-
- <% end %> - -
- -

- <%= link "Use Recovery Code Instead", to: Routes.user_recovery_code_path(@conn, :new) %> -

- - <%= if @two_factor_method != :app do %> -

- <%= link "Resend Two Factor Code", to: Routes.user_two_factor_path(@conn, :resend), method: :create %> -

- <% end %> -
diff --git a/lib/recognizer_web/templates/layout/app.html.eex b/lib/recognizer_web/templates/layout/app.html.eex index bab7b71..682dc1e 100644 --- a/lib/recognizer_web/templates/layout/app.html.eex +++ b/lib/recognizer_web/templates/layout/app.html.eex @@ -1,41 +1,25 @@ - - - - - - +
+
+
+
+ <%= render("logo.html") %> +
- Accounts · System76 - - "/> - - - - -
-
-
-
- <%= render("logo.html") %> -
- - <%= if get_flash(@conn, :info) do %> -
- <%= get_flash(@conn, :info) %> -
- <% end %> - - <%= if get_flash(@conn, :error) do %> -
- <%= get_flash(@conn, :error) %> -
- <% end %> + <%= if get_flash(@conn, :info) do %> +
+ <%= get_flash(@conn, :info) %> +
+ <% end %> -
- <%= @inner_content %> -
+ <%= if get_flash(@conn, :error) do %> +
+ <%= get_flash(@conn, :error) %>
-
+ <% end %> + +
+ <%= @inner_content %> +
- - +
+
diff --git a/lib/recognizer_web/templates/layout/live.html.leex b/lib/recognizer_web/templates/layout/live.html.leex new file mode 100644 index 0000000..a35f0cf --- /dev/null +++ b/lib/recognizer_web/templates/layout/live.html.leex @@ -0,0 +1,29 @@ +
+
+
+
+ <%= render("logo.html") %> +
+ +
+ <%= live_flash(@flash, :info) %> +
+ +
+ <%= live_flash(@flash, :error) %> +
+ +
+ <%= @inner_content %> +
+
+
+
diff --git a/lib/recognizer_web/templates/layout/root.html.leex b/lib/recognizer_web/templates/layout/root.html.leex new file mode 100644 index 0000000..96ef671 --- /dev/null +++ b/lib/recognizer_web/templates/layout/root.html.leex @@ -0,0 +1,18 @@ + + + + + + + <%= csrf_meta_tag() %> + + <%= live_title_tag assigns[:page_title] || "Accounts", suffix: " · System76" %> + + "/> + + + + + <%= @inner_content %> + + diff --git a/lib/recognizer_web/views/accounts/two_factor_settings_view.ex b/lib/recognizer_web/views/accounts/two_factor_settings_view.ex new file mode 100644 index 0000000..69ba4ef --- /dev/null +++ b/lib/recognizer_web/views/accounts/two_factor_settings_view.ex @@ -0,0 +1,17 @@ +defmodule RecognizerWeb.Accounts.TwoFactorSettingsView do + use RecognizerWeb, :view + + alias RecognizerWeb.Authentication + + def has_phone_number?(%{phone_number: nil}), do: false + def has_phone_number?(%{phone_number: ""}), do: false + def has_phone_number?(%{phone_number: _phone_number}), do: true + + def two_factor_barcode(user, settings) do + Authentication.generate_totp_barcode(user, settings["two_factor_seed"]) + end + + def two_factor_url(user, settings) do + Authentication.get_totp_app_url(user, settings["two_factor_seed"]) + end +end diff --git a/lib/recognizer_web/views/accounts/user_registration_view.ex b/lib/recognizer_web/views/accounts/user_registration_view.ex index 0752ad6..641e70f 100644 --- a/lib/recognizer_web/views/accounts/user_registration_view.ex +++ b/lib/recognizer_web/views/accounts/user_registration_view.ex @@ -1,6 +1,13 @@ defmodule RecognizerWeb.Accounts.UserRegistrationView do use RecognizerWeb, :view + def account_type(changeset) do + case Ecto.Changeset.get_field(changeset, :type) do + :individual -> "individual" + :business -> "business" + end + end + def business_type_class(changeset) do case Ecto.Changeset.get_field(changeset, :type) do :individual -> "none" diff --git a/lib/recognizer_web/views/accounts/user_settings_view.ex b/lib/recognizer_web/views/accounts/user_settings_view.ex index f7cf821..50b21db 100644 --- a/lib/recognizer_web/views/accounts/user_settings_view.ex +++ b/lib/recognizer_web/views/accounts/user_settings_view.ex @@ -1,6 +1,13 @@ defmodule RecognizerWeb.Accounts.UserSettingsView do use RecognizerWeb, :view + def account_type(changeset) do + case Ecto.Changeset.get_field(changeset, :type) do + :individual -> "individual" + :business -> "business" + end + end + def business_type_class(changeset) do case Ecto.Changeset.get_field(changeset, :type) do :individual -> "none" diff --git a/lib/recognizer_web/views/accounts/user_two_factor_view.ex b/lib/recognizer_web/views/accounts/user_two_factor_view.ex deleted file mode 100644 index 3b643bc..0000000 --- a/lib/recognizer_web/views/accounts/user_two_factor_view.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule RecognizerWeb.Accounts.UserTwoFactorView do - use RecognizerWeb, :view -end diff --git a/mix.exs b/mix.exs index 2ef9fc3..7807409 100644 --- a/mix.exs +++ b/mix.exs @@ -48,6 +48,7 @@ defmodule Recognizer.MixProject do {:ex_aws_sqs, "~> 3.2"}, {:ex_aws, "~> 2.0"}, {:ex_oauth2_provider, "~> 0.5.6"}, + {:floki, ">= 0.27.0", only: :test}, {:gettext, "~> 0.11"}, {:guardian, "~> 2.0"}, {:guardian_db, "~> 2.1"}, @@ -59,6 +60,7 @@ defmodule Recognizer.MixProject do {:phoenix_ecto, "~> 4.1"}, {:phoenix_html, "~> 2.11"}, {:phoenix_live_reload, "~> 1.2", only: :dev}, + {:phoenix_live_view, "~> 0.15.4"}, {:phoenix, "~> 1.5.7"}, {:phx_gen_auth, "~> 0.6", only: [:dev], runtime: false}, {:plug_cowboy, "~> 2.4"}, diff --git a/mix.lock b/mix.lock index d90babc..ad3133f 100644 --- a/mix.lock +++ b/mix.lock @@ -26,6 +26,7 @@ "ex_machina": {:hex, :ex_machina, "2.5.0", "8143cd1bf25364f197b089230c0e463941d5909b84c1a8491393ebf97a4b53fa", [: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", "8f24851c32b3f9f8adb11335f1e4801ea76a2e0dfa21d8c4bc40ee0d6156c084"}, "ex_oauth2_provider": {:hex, :ex_oauth2_provider, "0.5.6", "e1d5130c9062d3a24c5106e7a556b3335aaa7150801c89f6f5494c5f9ad8a2ba", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:plug, ">= 1.5.0 and < 2.0.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "0288475aa3a6224b70c990e61c89375dcb7c0b2669ba9a1114e94238cae615cf"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "floki": {:hex, :floki, "0.29.0", "b1710d8c93a2f860dc2d7adc390dd808dc2fb8f78ee562304457b75f4c640881", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "008585ce64b9f74c07d32958ec9866f4b8a124bf4da1e2941b28e41384edaaad"}, "gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"}, "google_protos": {:hex, :google_protos, "0.1.0", "c6b9e12092d17571b093d4156d004494ca143b65dbbcbfc3ffff463ea03467c0", [:mix], [{:protobuf, "~> 0.5", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "ff5564525f89d2638a4cfa9fb4d31e9ee9d9d7cb937b3e8a95f558440c039e1b"}, "grpc": {:hex, :grpc, "0.5.0-beta.1", "7d43f52e138fe261f5b4981f1ada515dfc2e1bfa9dc92c7022e8f41e7e49b571", [:mix], [{:cowboy, "~> 2.7.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:gun, "~> 2.0.0", [hex: :grpc_gun, repo: "hexpm", optional: false]}, {:protobuf, "~> 0.5", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "fbbf8872935c295b7575435fe4128372c23c6ded89c2ef8058af3c6167bb3f65"}, @@ -33,6 +34,7 @@ "guardian_db": {:hex, :guardian_db, "2.1.0", "ec95a9d99cdd1e550555d09a7bb4a340d8887aad0697f594590c2fd74be02426", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:guardian, "~> 1.0 or ~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "f8e7d543ac92c395f3a7fd5acbe6829faeade57d688f7562e2f0fca8f94a0d70"}, "gun": {:hex, :grpc_gun, "2.0.0", "f99678a2ab975e74372a756c86ec30a8384d3ac8a8b86c7ed6243ef4e61d2729", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "03dbbca1a9c604a0267a40ea1d69986225091acb822de0b2dbea21d5815e410b"}, "hackney": {:hex, :hackney, "1.17.0", "717ea195fd2f898d9fe9f1ce0afcc2621a41ecfe137fae57e7fe6e9484b9aa99", [:rebar3], [{:certifi, "~>2.5", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "64c22225f1ea8855f584720c0e5b3cd14095703af1c9fbc845ba042811dc671c"}, + "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"}, "httpoison": {:hex, :httpoison, "0.13.0", "bfaf44d9f133a6599886720f3937a7699466d23bb0cd7a88b6ba011f53c6f562", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "4846958172d6401c4f34ecc5c2c4607b5b0d90b8eec8f6df137ca4907942ed0f"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, @@ -51,6 +53,7 @@ "phoenix_ecto": {:hex, :phoenix_ecto, "4.2.1", "13f124cf0a3ce0f1948cf24654c7b9f2347169ff75c1123f44674afee6af3b03", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "478a1bae899cac0a6e02be1deec7e2944b7754c04e7d4107fc5a517f877743c0"}, "phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.0", "f35f61c3f959c9a01b36defaa1f0624edd55b87e236b606664a556d6f72fd2e7", [: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", "02c1007ae393f2b76ec61c1a869b1e617179877984678babde131d716f95b582"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "0.15.4", "86908dc9603cc81c07e84725ee42349b5325cb250c9c20d3533856ff18dbb7dc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 0.5", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "35d78f3c35fe10a995dca5f4ab50165b7a90cbe02e23de245381558f821e9462"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, "phx_gen_auth": {:hex, :phx_gen_auth, "0.6.0", "4ffbfa5b34ad8178c3dfcb996fed776df425903595cbc8d56a9ae5bc53136810", [:mix], [{:phoenix, "~> 1.5.2", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9a801c0f0bc251d8d91d62cecba0ebb6a90b8580fa8843029d931d15164e6ad9"}, "plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [: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: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"},