Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PETAL #81

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions assets/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
Expand Down
43 changes: 22 additions & 21 deletions assets/scripts/main.js
Original file line number Diff line number Diff line change
@@ -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, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also need the dom key here so alpine components don't lose their state when liveview patches the dom. docs

  dom: {
    onBeforeElUpdated(from, to) {
      if (from.__x) {
        window.Alpine.clone(from.__x, to)
      }
    }
  },

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())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdyt of adding a delay before showing the loading bar? the loading bar could be distracting if it shows and 50ms later clears; so we could only show it if the request is taking longer than (eg) 200ms.

let topbarDelay = null;

window.addEventListener("phx:page-loading-start", _info => {
  clearTimeout(topbarDelay);
  topbarDelay = setTimeout(() => topbar.show(), 200);
})

window.addEventListener("phx:page-loading-stop", _info => {
  clearTimeout(topbarDelay);
  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
12 changes: 9 additions & 3 deletions lib/recognizer/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down
20 changes: 20 additions & 0 deletions lib/recognizer_web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ defmodule RecognizerWeb do
quote do
use Phoenix.Controller, namespace: RecognizerWeb

import Phoenix.LiveView.Controller
import Plug.Conn
import RecognizerWeb.Gettext

Expand All @@ -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

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/recognizer_web/endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
108 changes: 108 additions & 0 deletions lib/recognizer_web/live/accounts/two-factor-settings.ex
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions lib/recognizer_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link
Contributor

@dbernheisel dbernheisel Feb 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

heads up, you could specify the :index action here, which gives you Routes.two_factor_settings_path(@conn, :index). I found this more consistent with other path helpers instead of all liveview paths going through live_path.

it's a little awkward though because there's no real :index action; we're just informing how to create the route helper.

end
end
Loading