towerops/lib/towerops_web/live/user_settings_live/totp_manager.ex
Graham McIntire 91e3181bbc dialyzer: fix all unmatched_return warnings (154 → 0)
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.
2026-04-21 10:03:55 -05:00

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