Packages
phoenix_kit
1.7.191
1.7.211
1.7.210
1.7.209
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit_web/users/qr_login.ex
defmodule PhoenixKitWeb.Users.QrLogin do
@moduledoc """
Desktop "scan to sign in" page.
A signed-out browser lands here, is shown a QR code encoding the phone
confirm URL, and waits. When the user's phone approves the sign-in,
`Keyfob.Live` receives the one-time login token over PubSub and this
LiveView redirects to the completion controller, which establishes the
session.
Redirects already-authenticated users away, and redirects everyone away
when the `qr_login_enabled` setting is off.
"""
use PhoenixKitWeb, :live_view
alias PhoenixKit.Users.QrLogin, as: QrLoginContext
alias PhoenixKit.Users.RateLimiter
alias PhoenixKit.Utils.IpAddress
alias PhoenixKit.Utils.Routes
alias PhoenixKitWeb.Users.Auth
# How long a QR code is valid before the panel offers a fresh one. Kept in
# sync with the request TTL passed to keyfob so the visible expiry matches
# the server-side one.
@request_ttl_ms :timer.minutes(2)
def mount(_params, _session, socket) do
case Auth.maybe_redirect_authenticated(socket) do
{:redirect, socket} ->
{:ok, socket}
:cont ->
cond do
not QrLoginContext.enabled?() ->
{:ok,
socket
|> put_flash(:error, gettext("QR code sign-in is not available."))
|> redirect(to: Routes.path("/users/log-in"))}
connected?(socket) ->
case rate_limit_mint(socket) do
:ok ->
socket =
Keyfob.Live.init_panel(socket,
confirm_url: &confirm_url/1,
meta: QrLoginContext.device_meta(socket),
pubsub: QrLoginContext.pubsub(),
ttl_ms: @request_ttl_ms
)
{:ok, socket |> assign_common() |> schedule_expiry()}
{:error, :rate_limit_exceeded} ->
{:ok,
socket
|> put_flash(:error, gettext("Too many attempts. Please try again shortly."))
|> redirect(to: Routes.path("/users/log-in"))}
end
true ->
# Dead render: the request is minted on connect (so it isn't
# created twice), so there's nothing to show yet.
{:ok, socket |> assign(:keyfob, nil) |> assign_common()}
end
end
end
# Forward keyfob's PubSub message; on approval, hand off to completion.
def handle_info({:keyfob, _token, _payload} = msg, socket),
do: Keyfob.Live.handle_message(msg, socket, on_approved: &complete/2)
# The code's TTL lapsed. Only flip to :expired if we're still waiting on
# *this* code — a refresh (new token) or an approval already moved on.
def handle_info({:keyfob_expire, token}, socket) do
case socket.assigns[:keyfob] do
%{state: :waiting, token: ^token} -> {:noreply, Keyfob.Live.expire(socket)}
_ -> {:noreply, socket}
end
end
# Refresh button on an expired code — mint a new one and re-arm the timer.
def handle_event("keyfob_refresh", _params, socket),
do: {:noreply, socket |> Keyfob.Live.refresh() |> schedule_expiry()}
defp complete(socket, login_token) do
{:noreply, redirect(socket, to: Routes.path("/users/qr-login/finish/#{login_token}"))}
end
# Every connect to this public, pre-auth page mints a live keyfob request
# (an ETS entry) — guard the mint itself, not just the page render.
# extract_from_socket/1 always returns a string ("unknown" when peer_data
# is unavailable), so unknown-IP connects simply share one rate-limit
# bucket rather than bypassing the check.
defp rate_limit_mint(socket) do
socket
|> IpAddress.extract_from_socket()
|> RateLimiter.check_qr_login_rate_limit()
end
defp schedule_expiry(socket) do
case socket.assigns[:keyfob] do
%{token: token} ->
Process.send_after(self(), {:keyfob_expire, token}, @request_ttl_ms)
socket
_ ->
socket
end
end
# The URL encoded in the QR — must be absolute so a phone camera can open
# it. Points at the authenticated phone-confirm LiveView.
defp confirm_url(token), do: Routes.url("/users/qr-login/scan/#{token}")
defp assign_common(socket) do
assign(socket, :project_title, PhoenixKit.Settings.get_project_title())
end
defp panel_labels do
%{
waiting:
gettext(
"Scan this code with your phone's camera, then approve the sign-in on your phone."
),
approved: gettext("Approved — signing you in…"),
denied: gettext("The sign-in request was denied."),
expired: gettext("This code expired."),
refresh: gettext("Show a new code")
}
end
end