Prefix fire-and-forget calls (PubSub.broadcast, Oban enqueue, Logger, update_*, etc.) with `_ =` so dialyzer knows the return value is intentionally discarded. Wrap a few if/case expressions whose branches produce mixed types the same way. No behavior changes — only explicit acknowledgement of discarded returns. Warnings: 242 → 88.
162 lines
4.9 KiB
Elixir
162 lines
4.9 KiB
Elixir
defmodule ToweropsWeb.UserSettingsLive.TotpManager do
|
|
@moduledoc """
|
|
Handles TOTP (Time-based One-Time Password) device and recovery code management.
|
|
"""
|
|
use Gettext, backend: ToweropsWeb.Gettext
|
|
|
|
import ToweropsWeb.GettextHelpers
|
|
|
|
alias Towerops.Accounts
|
|
alias Towerops.Accounts.UserTotpDevice
|
|
|
|
@doc """
|
|
Shows the modal for adding a new TOTP device.
|
|
"""
|
|
def show_add_device_modal(socket) do
|
|
Phoenix.Component.assign(socket, :show_add_device_modal, true)
|
|
end
|
|
|
|
@doc """
|
|
Cancels adding a new TOTP device.
|
|
"""
|
|
def cancel_add_device(socket) do
|
|
Phoenix.Component.assign(socket, :show_add_device_modal, false)
|
|
end
|
|
|
|
@doc """
|
|
Creates a new TOTP device and displays the QR code.
|
|
"""
|
|
def create_device(socket, name) do
|
|
user = socket.assigns.current_scope.user
|
|
|
|
case Accounts.create_totp_device(user.id, name) do
|
|
{:ok, device, secret} ->
|
|
qr_code = Accounts.generate_totp_qr_code(user, secret)
|
|
|
|
socket
|
|
|> Phoenix.Component.assign(:show_add_device_modal, false)
|
|
|> Phoenix.Component.assign(:show_device_qr_modal, true)
|
|
|> Phoenix.Component.assign(:new_device_id, device.id)
|
|
|> Phoenix.Component.assign(:new_device_secret, secret)
|
|
|> Phoenix.Component.assign(:new_device_qr_code, qr_code)
|
|
|
|
{:error, _changeset} ->
|
|
Phoenix.LiveView.put_flash(socket, :error, t_auth("Failed to create device."))
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Verifies a new TOTP device with a code from the authenticator app.
|
|
"""
|
|
def verify_new_device(socket, code) do
|
|
secret = socket.assigns.new_device_secret
|
|
device_id = socket.assigns.new_device_id
|
|
|
|
if Accounts.verify_totp(secret, code) do
|
|
# Device already created, just mark as verified by touching it
|
|
device = Towerops.Repo.get!(UserTotpDevice, device_id)
|
|
|
|
device
|
|
|> UserTotpDevice.touch_changeset()
|
|
|> Towerops.Repo.update!()
|
|
|
|
socket
|
|
|> Phoenix.LiveView.put_flash(:info, t_auth("Device added successfully!"))
|
|
|> Phoenix.Component.assign(:show_device_qr_modal, false)
|
|
|> Phoenix.Component.assign(:new_device_id, nil)
|
|
|> Phoenix.Component.assign(:new_device_secret, nil)
|
|
|> Phoenix.Component.assign(:new_device_qr_code, nil)
|
|
|> assign_totp_devices()
|
|
else
|
|
Phoenix.LiveView.put_flash(socket, :error, t_auth("Invalid code. Please try again."))
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Closes the QR code modal and deletes the unverified device.
|
|
"""
|
|
def close_device_qr_modal(socket) do
|
|
# Delete the unverified device if user cancels
|
|
_ =
|
|
if device_id = socket.assigns.new_device_id do
|
|
user = socket.assigns.current_scope.user
|
|
Accounts.delete_totp_device(device_id, user.id)
|
|
end
|
|
|
|
socket
|
|
|> Phoenix.Component.assign(:show_device_qr_modal, false)
|
|
|> Phoenix.Component.assign(:new_device_id, nil)
|
|
|> Phoenix.Component.assign(:new_device_secret, nil)
|
|
|> Phoenix.Component.assign(:new_device_qr_code, nil)
|
|
|> assign_totp_devices()
|
|
end
|
|
|
|
@doc """
|
|
Deletes a TOTP device.
|
|
"""
|
|
def delete_device(socket, device_id) do
|
|
user = socket.assigns.current_scope.user
|
|
|
|
case Accounts.delete_totp_device(device_id, user.id) do
|
|
{:ok, _} ->
|
|
socket
|
|
|> Phoenix.LiveView.put_flash(:info, t_auth("Device removed successfully."))
|
|
|> assign_totp_devices()
|
|
|
|
{:error, :last_device} ->
|
|
Phoenix.LiveView.put_flash(
|
|
socket,
|
|
:error,
|
|
t_auth("Cannot remove last device. You must have at least one.")
|
|
)
|
|
|
|
{:error, _} ->
|
|
Phoenix.LiveView.put_flash(socket, :error, t_auth("Failed to remove device."))
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Regenerates recovery codes for the user.
|
|
"""
|
|
def regenerate_recovery_codes(socket) do
|
|
user = socket.assigns.current_scope.user
|
|
|
|
case Accounts.generate_recovery_codes(user.id) do
|
|
{:ok, codes} ->
|
|
socket
|
|
|> Phoenix.Component.assign(:show_recovery_codes_modal, true)
|
|
|> Phoenix.Component.assign(:generated_recovery_codes, codes)
|
|
|> assign_recovery_codes_count()
|
|
|
|
{:error, _} ->
|
|
Phoenix.LiveView.put_flash(socket, :error, t_auth("Failed to generate recovery codes."))
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Closes the recovery codes modal.
|
|
"""
|
|
def close_recovery_codes_modal(socket) do
|
|
socket
|
|
|> Phoenix.Component.assign(:show_recovery_codes_modal, false)
|
|
|> Phoenix.Component.assign(:generated_recovery_codes, nil)
|
|
end
|
|
|
|
@doc """
|
|
Assigns TOTP devices to socket.
|
|
"""
|
|
def assign_totp_devices(socket) do
|
|
user = socket.assigns.current_scope.user
|
|
devices = Accounts.list_user_totp_devices(user.id)
|
|
Phoenix.Component.assign(socket, :totp_devices, devices)
|
|
end
|
|
|
|
@doc """
|
|
Assigns recovery codes count to socket.
|
|
"""
|
|
def assign_recovery_codes_count(socket) do
|
|
user = socket.assigns.current_scope.user
|
|
count = Accounts.count_unused_recovery_codes(user.id)
|
|
Phoenix.Component.assign(socket, :unused_recovery_codes_count, count)
|
|
end
|
|
end
|