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