feat: migrate equipment features to gettext and fix sudo mode tests

Migrates all equipment-related flash messages to gettext for internationalization:
- DeviceLive.Index: device discovery, reordering, error messages
- DeviceLive.Show: backup messages, permission errors
- DeviceLive.Form: device CRUD operations
- AlertLive.Index: alert acknowledgment messages
- SiteLive.Show: site discovery messages
- SiteLive.Form: site CRUD and SNMP/agent propagation

Fixes sudo mode test suite issues:
- Updates Accounts.sudo_mode?/2 test to check last_sudo_at instead of authenticated_at
- Adds register_and_log_in_user_with_sudo/1 helper to ConnCase
- Fixes UserAuth sudo mode tests to use session-based authentication
- Updates MyData tests to use sudo mode (now required per router config)
- Removes obsolete UserSettingsLive "without sudo mode" test
- Fixes grant_sudo_mode/1 DateTime truncation to seconds

All equipment module tests passing. Ready for Phase 3 (Admin Features).
This commit is contained in:
Graham McIntire 2026-02-02 12:41:58 -06:00
parent f6195cd5c6
commit 89a076fb67
No known key found for this signature in database
18 changed files with 558 additions and 140 deletions

View file

@ -675,7 +675,7 @@ defmodule Towerops.Accounts do
{:ok, %User{last_sudo_at: ~U[2026-02-01 12:00:00Z]}}
"""
def grant_sudo_mode(%User{} = user) do
now = DateTime.utc_now()
now = DateTime.truncate(DateTime.utc_now(), :second)
with {:ok, updated_user} <-
user

View file

@ -40,7 +40,7 @@ defmodule ToweropsWeb.AlertLive.Index do
case Alerts.get_alert(id) do
nil ->
{:noreply, put_flash(socket, :error, "Alert not found")}
{:noreply, put_flash(socket, :error, t_equipment("Alert not found"))}
alert ->
# Preload device and site to check organization access
@ -49,7 +49,7 @@ defmodule ToweropsWeb.AlertLive.Index do
if alert.device.site.organization_id == organization.id do
perform_acknowledge_alert(socket, alert, user_id, organization.id)
else
{:noreply, put_flash(socket, :error, "You don't have access to this alert")}
{:noreply, put_flash(socket, :error, t_equipment("You don't have access to this alert"))}
end
end
end
@ -59,11 +59,11 @@ defmodule ToweropsWeb.AlertLive.Index do
{:ok, _alert} ->
{:noreply,
socket
|> put_flash(:info, "Alert acknowledged")
|> put_flash(:info, t_equipment("Alert acknowledged"))
|> load_alerts(organization_id)}
{:error, _changeset} ->
{:noreply, put_flash(socket, :error, "Unable to acknowledge alert")}
{:noreply, put_flash(socket, :error, t_equipment("Unable to acknowledge alert"))}
end
end

View file

@ -35,7 +35,7 @@ defmodule ToweropsWeb.DeviceLive.Form do
if Enum.empty?(sites) do
{:ok,
socket
|> put_flash(:info, "Please create a site before adding a device.")
|> put_flash(:info, t_equipment("Please create a site before adding a device."))
|> push_navigate(to: ~p"/sites/new")}
else
{:ok,
@ -107,7 +107,7 @@ defmodule ToweropsWeb.DeviceLive.Form do
case Devices.get_device(id) do
nil ->
socket
|> put_flash(:error, "Device not found")
|> put_flash(:error, t_equipment("Device not found"))
|> push_navigate(to: ~p"/devices")
device ->
@ -119,7 +119,7 @@ defmodule ToweropsWeb.DeviceLive.Form do
apply_edit_action(socket, device)
else
socket
|> put_flash(:error, "You don't have access to this device")
|> put_flash(:error, t_equipment("You don't have access to this device"))
|> push_navigate(to: ~p"/devices")
end
end
@ -255,11 +255,11 @@ defmodule ToweropsWeb.DeviceLive.Form do
{:noreply,
socket
|> put_flash(:info, "Device deleted successfully")
|> put_flash(:info, t_equipment("Device deleted successfully"))
|> push_navigate(to: ~p"/devices")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Unable to delete device")}
{:noreply, put_flash(socket, :error, t_equipment("Unable to delete device"))}
end
end
@ -295,10 +295,10 @@ defmodule ToweropsWeb.DeviceLive.Form do
# Navigate to device show page where live updates will appear
{:noreply,
socket
|> put_flash(:info, "Discovery started...")
|> put_flash(:info, t_equipment("Discovery started..."))
|> push_navigate(to: ~p"/devices/#{device.id}")}
else
{:noreply, put_flash(socket, :error, "SNMP is not enabled for this device")}
{:noreply, put_flash(socket, :error, t_equipment("SNMP is not enabled for this device"))}
end
end
@ -400,9 +400,9 @@ defmodule ToweropsWeb.DeviceLive.Form do
base_message =
if device.snmp_enabled do
"Device created. SNMP discovery started. "
t_equipment("Device created. SNMP discovery started. ")
else
"Device created. "
t_equipment("Device created. ")
end
_ =
@ -420,9 +420,9 @@ defmodule ToweropsWeb.DeviceLive.Form do
defp handle_device_update(old_device, device) do
if should_trigger_snmp_discovery?(old_device, device) do
_ = enqueue_discovery(device.id)
"Device updated successfully. SNMP discovery started in background."
t_equipment("Device updated successfully. SNMP discovery started in background.")
else
"Device updated successfully"
t_equipment("Device updated successfully")
end
end

View file

@ -21,7 +21,7 @@ defmodule ToweropsWeb.DeviceLive.Index do
{:ok,
socket
|> assign(:page_title, "Devices")
|> assign(:page_title, t_equipment("Devices"))
|> assign(:timezone, socket.assigns.current_scope.timezone)
|> assign(:device, device)
|> assign(:grouped_devices, grouped_devices)
@ -82,15 +82,16 @@ defmodule ToweropsWeb.DeviceLive.Index do
snmp_devices = Enum.filter(devices, & &1.snmp_enabled)
if Enum.empty?(snmp_devices) do
{:noreply, put_flash(socket, :error, "No SNMP-enabled devices found")}
{:noreply, put_flash(socket, :error, t_equipment("No SNMP-enabled devices found"))}
else
Enum.each(snmp_devices, fn device ->
enqueue_discovery(device.id)
end)
count = length(snmp_devices)
message = t_equipment("Discovery started for %{count} device", count: count) <> if count == 1, do: "", else: "s"
{:noreply, put_flash(socket, :info, "Discovery started for #{count} device#{if count == 1, do: "", else: "s"}")}
{:noreply, put_flash(socket, :info, message)}
end
end
@ -101,13 +102,13 @@ defmodule ToweropsWeb.DeviceLive.Index do
# Verify site belongs to current organization
case Sites.get_site(site_id) do
nil ->
{:noreply, put_flash(socket, :error, "Site not found")}
{:noreply, put_flash(socket, :error, t_equipment("Site not found"))}
site ->
if site.organization_id == organization.id do
perform_site_reorder(socket, site_id, new_position, organization.id)
else
{:noreply, put_flash(socket, :error, "You don't have access to this site")}
{:noreply, put_flash(socket, :error, t_equipment("You don't have access to this site"))}
end
end
end
@ -119,7 +120,7 @@ defmodule ToweropsWeb.DeviceLive.Index do
# Verify device belongs to current organization
case Devices.get_device(device_id) do
nil ->
{:noreply, put_flash(socket, :error, "Device not found")}
{:noreply, put_flash(socket, :error, t_equipment("Device not found"))}
device ->
# Preload site to check organization access
@ -128,7 +129,7 @@ defmodule ToweropsWeb.DeviceLive.Index do
if device.site.organization_id == organization.id do
perform_device_reorder(socket, device_id, new_position, organization.id)
else
{:noreply, put_flash(socket, :error, "You don't have access to this device")}
{:noreply, put_flash(socket, :error, t_equipment("You don't have access to this device"))}
end
end
end
@ -153,7 +154,7 @@ defmodule ToweropsWeb.DeviceLive.Index do
socket
|> assign(:device, devices)
|> assign(:grouped_devices, grouped_devices)
|> put_flash(:info, "Order reset to alphabetical")}
|> put_flash(:info, t_equipment("Order reset to alphabetical"))}
end
def handle_event("add_discovered_device", params, socket) do
@ -183,10 +184,10 @@ defmodule ToweropsWeb.DeviceLive.Index do
socket
|> assign(:device, devices)
|> assign(:grouped_devices, grouped_devices)
|> put_flash(:info, "Site order updated")}
|> put_flash(:info, t_equipment("Site order updated"))}
{:error, _changeset} ->
{:noreply, put_flash(socket, :error, "Failed to reorder site")}
{:noreply, put_flash(socket, :error, t_equipment("Failed to reorder site"))}
end
end
@ -204,10 +205,10 @@ defmodule ToweropsWeb.DeviceLive.Index do
socket
|> assign(:device, devices)
|> assign(:grouped_devices, grouped_devices)
|> put_flash(:info, "Device order updated")}
|> put_flash(:info, t_equipment("Device order updated"))}
{:error, _changeset} ->
{:noreply, put_flash(socket, :error, "Failed to reorder device")}
{:noreply, put_flash(socket, :error, t_equipment("Failed to reorder device"))}
end
end

View file

@ -51,7 +51,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
nil ->
{:noreply,
socket
|> put_flash(:error, "Device not found")
|> put_flash(:error, t_equipment("Device not found"))
|> push_navigate(to: ~p"/devices")}
device ->
@ -70,7 +70,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
else
{:noreply,
socket
|> put_flash(:error, "You don't have access to this device")
|> put_flash(:error, t_equipment("You don't have access to this device"))
|> push_navigate(to: ~p"/devices")}
end
end
@ -84,7 +84,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
# Device was deleted, don't schedule another refresh
{:noreply,
socket
|> put_flash(:error, "Device no longer exists")
|> put_flash(:error, t_equipment("Device no longer exists"))
|> push_navigate(to: ~p"/devices")}
_device ->
@ -103,7 +103,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
{:noreply,
socket
|> load_equipment_data(socket.assigns.device.id)
|> put_flash(:info, "Discovery completed")}
|> put_flash(:info, t_equipment("Discovery completed"))}
end
@impl true
@ -956,7 +956,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
{:noreply, push_event(socket, "download", %{content: config_text, filename: filename, mime_type: "text/plain"})}
else
{:noreply, put_flash(socket, :error, "You don't have permission to access this backup")}
{:noreply, put_flash(socket, :error, t_equipment("You don't have permission to access this backup"))}
end
end
@ -967,7 +967,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
case MikrotikBackups.get_latest_backup(device_id) do
nil ->
{:noreply, put_flash(socket, :error, "No other backups available for comparison")}
{:noreply, put_flash(socket, :error, t_equipment("No other backups available for comparison"))}
latest when latest.id == backup.id ->
# Compare with second-latest
@ -979,7 +979,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
)}
_ ->
{:noreply, put_flash(socket, :error, "No other backups available for comparison")}
{:noreply, put_flash(socket, :error, t_equipment("No other backups available for comparison"))}
end
latest ->
@ -997,10 +997,11 @@ defmodule ToweropsWeb.DeviceLive.Show do
cond do
is_nil(agent_token_id) ->
{:noreply, put_flash(socket, :error, "Device has no agent assigned. Assign an agent to create backups.")}
{:noreply,
put_flash(socket, :error, t_equipment("Device has no agent assigned. Assign an agent to create backups."))}
!device.mikrotik_enabled ->
{:noreply, put_flash(socket, :error, "MikroTik API is not enabled for this device.")}
{:noreply, put_flash(socket, :error, t_equipment("MikroTik API is not enabled for this device."))}
true ->
trigger_manual_backup(socket, device, agent_token_id)
@ -1045,11 +1046,11 @@ defmodule ToweropsWeb.DeviceLive.Show do
{:noreply,
socket
|> put_flash(:info, "Manual backup requested. Results will appear shortly.")
|> put_flash(:info, t_equipment("Manual backup requested. Results will appear shortly."))
|> load_equipment_data(device.id)}
{:error, _reason} ->
{:noreply, put_flash(socket, :error, "Failed to create backup request. Please try again.")}
{:noreply, put_flash(socket, :error, t_equipment("Failed to create backup request. Please try again."))}
end
end
end

View file

@ -85,11 +85,11 @@ defmodule ToweropsWeb.SiteLive.Form do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, "Site deleted successfully")
|> put_flash(:info, t_equipment("Site deleted successfully"))
|> push_navigate(to: ~p"/sites")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Unable to delete site")}
{:noreply, put_flash(socket, :error, t_equipment("Unable to delete site"))}
end
end
@ -97,14 +97,20 @@ defmodule ToweropsWeb.SiteLive.Form do
def handle_event("apply_snmp_to_all", _params, socket) do
{count, _} = Sites.apply_snmp_config_to_all_equipment(socket.assigns.site.id)
{:noreply, put_flash(socket, :info, "Applied SNMP configuration to #{count} device records at this site")}
{:noreply,
put_flash(
socket,
:info,
t_equipment("Applied SNMP configuration to %{count} device records at this site", count: count)
)}
end
@impl true
def handle_event("apply_agent_to_all", _params, socket) do
{count, _} = Sites.apply_agent_to_all_equipment(socket.assigns.site.id)
{:noreply, put_flash(socket, :info, "Applied agent to #{count} device records at this site")}
{:noreply,
put_flash(socket, :info, t_equipment("Applied agent to %{count} device records at this site", count: count))}
end
defp save_site(socket, :new, site_params) do
@ -114,7 +120,7 @@ defmodule ToweropsWeb.SiteLive.Form do
{:ok, site} ->
{:noreply,
socket
|> put_flash(:info, "Site created successfully! Now add your first device.")
|> put_flash(:info, t_equipment("Site created successfully! Now add your first device."))
|> push_navigate(to: ~p"/sites/#{site.id}")}
{:error, %Ecto.Changeset{} = changeset} ->
@ -127,7 +133,7 @@ defmodule ToweropsWeb.SiteLive.Form do
{:ok, _site} ->
{:noreply,
socket
|> put_flash(:info, "Site updated successfully")
|> put_flash(:info, t_equipment("Site updated successfully"))
|> push_navigate(to: ~p"/sites")}
{:error, %Ecto.Changeset{} = changeset} ->

View file

@ -34,7 +34,7 @@ defmodule ToweropsWeb.SiteLive.Show do
snmp_devices = Enum.filter(devices, & &1.snmp_enabled)
if Enum.empty?(snmp_devices) do
{:noreply, put_flash(socket, :error, "No SNMP-enabled devices at this site")}
{:noreply, put_flash(socket, :error, t_equipment("No SNMP-enabled devices at this site"))}
else
# Enqueue discovery for all SNMP-enabled devices
Enum.each(snmp_devices, fn device ->
@ -42,13 +42,9 @@ defmodule ToweropsWeb.SiteLive.Show do
end)
count = length(snmp_devices)
message = t_equipment("Discovery started for %{count} device", count: count) <> if count == 1, do: "", else: "s"
{:noreply,
put_flash(
socket,
:info,
"Discovery started for #{count} device#{if count == 1, do: "", else: "s"}"
)}
{:noreply, put_flash(socket, :info, message)}
end
end

View file

@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Language: en\n"
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:115
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:133
#, elixir-autogen, elixir-format
msgid "Accept invitation and create account"
msgstr ""
@ -35,12 +35,12 @@ msgstr ""
msgid "Confirm new password"
msgstr ""
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:115
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:134
#, elixir-autogen, elixir-format
msgid "Create an account"
msgstr ""
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:114
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:128
#, elixir-autogen, elixir-format
msgid "Creating account..."
msgstr ""
@ -50,10 +50,10 @@ msgstr ""
msgid "Don't have an account?"
msgstr ""
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:52
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:55
#: lib/towerops_web/controllers/user_reset_password_html/new.html.heex:17
#: lib/towerops_web/controllers/user_session_html/new.html.heex:50
#: lib/towerops_web/controllers/user_session_html/new.html.heex:94
#: lib/towerops_web/controllers/user_session_html/new.html.heex:55
#: lib/towerops_web/controllers/user_session_html/new.html.heex:99
#, elixir-autogen, elixir-format
msgid "Email"
msgstr ""
@ -68,7 +68,7 @@ msgstr ""
msgid "Enter your new password below."
msgstr ""
#: lib/towerops_web/controllers/user_session_html/new.html.heex:67
#: lib/towerops_web/controllers/user_session_html/new.html.heex:72
#, elixir-autogen, elixir-format
msgid "Forgot password?"
msgstr ""
@ -78,13 +78,13 @@ msgstr ""
msgid "Forgot your password?"
msgstr ""
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:31
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:34
#, elixir-autogen, elixir-format
msgid "Free tier includes:"
msgstr ""
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:89
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:107
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:92
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:115
#, elixir-autogen, elixir-format
msgid "I agree to the %{link}"
msgstr ""
@ -96,12 +96,12 @@ msgstr ""
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:16
#: lib/towerops_web/controllers/user_session_html/new.html.heex:5
#: lib/towerops_web/controllers/user_session_html/new.html.heex:72
#: lib/towerops_web/controllers/user_session_html/new.html.heex:77
#, elixir-autogen, elixir-format
msgid "Log in"
msgstr ""
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:71
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:74
#, elixir-autogen, elixir-format
msgid "My Company"
msgstr ""
@ -111,18 +111,18 @@ msgstr ""
msgid "New password"
msgstr ""
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:70
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:73
#, elixir-autogen, elixir-format
msgid "Organization Name"
msgstr ""
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:60
#: lib/towerops_web/controllers/user_session_html/new.html.heex:58
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:63
#: lib/towerops_web/controllers/user_session_html/new.html.heex:63
#, elixir-autogen, elixir-format
msgid "Password"
msgstr ""
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:89
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:95
#, elixir-autogen, elixir-format
msgid "Privacy Policy"
msgstr ""
@ -138,7 +138,7 @@ msgstr ""
msgid "Reset password"
msgstr ""
#: lib/towerops_web/controllers/user_session_html/new.html.heex:99
#: lib/towerops_web/controllers/user_session_html/new.html.heex:104
#, elixir-autogen, elixir-format
msgid "Send me a login link instead"
msgstr ""
@ -153,7 +153,7 @@ msgstr ""
msgid "Sign up"
msgstr ""
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:107
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:118
#, elixir-autogen, elixir-format
msgid "Terms of Service"
msgstr ""
@ -183,7 +183,7 @@ msgstr ""
msgid "for an account now."
msgstr ""
#: lib/towerops_web/controllers/user_session_html/new.html.heex:37
#: lib/towerops_web/controllers/user_session_html/new.html.heex:40
#, elixir-autogen, elixir-format
msgid "the mailbox page"
msgstr ""
@ -193,22 +193,22 @@ msgstr ""
msgid "to your account now."
msgstr ""
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:34
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:37
#, elixir-autogen, elixir-format
msgid "✓ Monitor up to 10 devices"
msgstr ""
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:37
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:40
#, elixir-autogen, elixir-format
msgid "✓ No credit card required"
msgstr ""
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:36
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:39
#, elixir-autogen, elixir-format
msgid "✓ Performance charts and historical data"
msgstr ""
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:35
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:38
#, elixir-autogen, elixir-format
msgid "✓ Real-time alerts and notifications"
msgstr ""

View file

@ -87,7 +87,7 @@ msgstr ""
msgid "close"
msgstr ""
#: lib/towerops_web/controllers/user_session_html/new.html.heex:83
#: lib/towerops_web/controllers/user_session_html/new.html.heex:88
#, elixir-autogen, elixir-format
msgid "or"
msgstr ""

View file

@ -11,7 +11,7 @@ msgstr ""
"Language: en\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:115
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:133
#, elixir-autogen, elixir-format
msgid "Accept invitation and create account"
msgstr "Accept invitation and create account"
@ -32,12 +32,12 @@ msgstr "Back to log in"
msgid "Confirm new password"
msgstr "Confirm new password"
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:115
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:134
#, elixir-autogen, elixir-format
msgid "Create an account"
msgstr "Create an account"
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:114
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:128
#, elixir-autogen, elixir-format
msgid "Creating account..."
msgstr "Creating account..."
@ -47,10 +47,10 @@ msgstr "Creating account..."
msgid "Don't have an account?"
msgstr "Don't have an account?"
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:52
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:55
#: lib/towerops_web/controllers/user_reset_password_html/new.html.heex:17
#: lib/towerops_web/controllers/user_session_html/new.html.heex:50
#: lib/towerops_web/controllers/user_session_html/new.html.heex:94
#: lib/towerops_web/controllers/user_session_html/new.html.heex:55
#: lib/towerops_web/controllers/user_session_html/new.html.heex:99
#, elixir-autogen, elixir-format
msgid "Email"
msgstr "Email"
@ -65,7 +65,7 @@ msgstr "Enter your email address and we'll send you a link to reset your passwor
msgid "Enter your new password below."
msgstr "Enter your new password below."
#: lib/towerops_web/controllers/user_session_html/new.html.heex:67
#: lib/towerops_web/controllers/user_session_html/new.html.heex:72
#, elixir-autogen, elixir-format
msgid "Forgot password?"
msgstr "Forgot password?"
@ -75,13 +75,13 @@ msgstr "Forgot password?"
msgid "Forgot your password?"
msgstr "Forgot your password?"
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:31
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:34
#, elixir-autogen, elixir-format
msgid "Free tier includes:"
msgstr "Free tier includes:"
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:89
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:107
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:92
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:115
#, elixir-autogen, elixir-format
msgid "I agree to the %{link}"
msgstr "I agree to the %{link}"
@ -93,12 +93,12 @@ msgstr "Join %{organization}"
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:16
#: lib/towerops_web/controllers/user_session_html/new.html.heex:5
#: lib/towerops_web/controllers/user_session_html/new.html.heex:72
#: lib/towerops_web/controllers/user_session_html/new.html.heex:77
#, elixir-autogen, elixir-format
msgid "Log in"
msgstr "Log in"
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:71
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:74
#, elixir-autogen, elixir-format
msgid "My Company"
msgstr "My Company"
@ -108,18 +108,18 @@ msgstr "My Company"
msgid "New password"
msgstr "New password"
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:70
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:73
#, elixir-autogen, elixir-format
msgid "Organization Name"
msgstr "Organization Name"
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:60
#: lib/towerops_web/controllers/user_session_html/new.html.heex:58
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:63
#: lib/towerops_web/controllers/user_session_html/new.html.heex:63
#, elixir-autogen, elixir-format
msgid "Password"
msgstr "Password"
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:89
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:95
#, elixir-autogen, elixir-format
msgid "Privacy Policy"
msgstr "Privacy Policy"
@ -135,7 +135,7 @@ msgstr "Register for an account"
msgid "Reset password"
msgstr "Reset password"
#: lib/towerops_web/controllers/user_session_html/new.html.heex:99
#: lib/towerops_web/controllers/user_session_html/new.html.heex:104
#, elixir-autogen, elixir-format
msgid "Send me a login link instead"
msgstr "Send me a login link instead"
@ -150,7 +150,7 @@ msgstr "Send reset instructions"
msgid "Sign up"
msgstr "Sign up"
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:107
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:118
#, elixir-autogen, elixir-format
msgid "Terms of Service"
msgstr "Terms of Service"
@ -180,7 +180,7 @@ msgstr "You've been invited to join %{organization} as a %{role}."
msgid "for an account now."
msgstr "for an account now."
#: lib/towerops_web/controllers/user_session_html/new.html.heex:37
#: lib/towerops_web/controllers/user_session_html/new.html.heex:40
#, elixir-autogen, elixir-format
msgid "the mailbox page"
msgstr "the mailbox page"
@ -190,22 +190,22 @@ msgstr "the mailbox page"
msgid "to your account now."
msgstr "to your account now."
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:34
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:37
#, elixir-autogen, elixir-format
msgid "✓ Monitor up to 10 devices"
msgstr "✓ Monitor up to 10 devices"
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:37
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:40
#, elixir-autogen, elixir-format
msgid "✓ No credit card required"
msgstr "✓ No credit card required"
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:36
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:39
#, elixir-autogen, elixir-format
msgid "✓ Performance charts and historical data"
msgstr "✓ Performance charts and historical data"
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:35
#: lib/towerops_web/controllers/user_registration_html/new.html.heex:38
#, elixir-autogen, elixir-format
msgid "✓ Real-time alerts and notifications"
msgstr "✓ Real-time alerts and notifications"

View file

@ -87,7 +87,7 @@ msgstr "We can't find the internet"
msgid "close"
msgstr "close"
#: lib/towerops_web/controllers/user_session_html/new.html.heex:83
#: lib/towerops_web/controllers/user_session_html/new.html.heex:88
#, elixir-autogen, elixir-format
msgid "or"
msgstr "or"

View file

@ -10,3 +10,209 @@ msgid ""
msgstr ""
"Language: en\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: lib/towerops_web/live/device_live/form.ex:405
#, elixir-autogen, elixir-format
msgid "Device created. "
msgstr "Device created. "
#: lib/towerops_web/live/device_live/form.ex:403
#, elixir-autogen, elixir-format
msgid "Device created. SNMP discovery started. "
msgstr "Device created. SNMP discovery started. "
#: lib/towerops_web/live/device_live/form.ex:258
#, elixir-autogen, elixir-format
msgid "Device deleted successfully"
msgstr "Device deleted successfully"
#: lib/towerops_web/live/device_live/show.ex:1001
#, elixir-autogen, elixir-format
msgid "Device has no agent assigned. Assign an agent to create backups."
msgstr "Device has no agent assigned. Assign an agent to create backups."
#: lib/towerops_web/live/device_live/show.ex:87
#, elixir-autogen, elixir-format
msgid "Device no longer exists"
msgstr "Device no longer exists"
#: lib/towerops_web/live/device_live/form.ex:110
#: lib/towerops_web/live/device_live/index.ex:123
#: lib/towerops_web/live/device_live/show.ex:54
#, elixir-autogen, elixir-format
msgid "Device not found"
msgstr "Device not found"
#: lib/towerops_web/live/device_live/index.ex:208
#, elixir-autogen, elixir-format
msgid "Device order updated"
msgstr "Device order updated"
#: lib/towerops_web/live/device_live/form.ex:425
#, elixir-autogen, elixir-format
msgid "Device updated successfully"
msgstr "Device updated successfully"
#: lib/towerops_web/live/device_live/form.ex:423
#, elixir-autogen, elixir-format
msgid "Device updated successfully. SNMP discovery started in background."
msgstr "Device updated successfully. SNMP discovery started in background."
#: lib/towerops_web/live/device_live/index.ex:24
#, elixir-autogen, elixir-format
msgid "Devices"
msgstr "Devices"
#: lib/towerops_web/live/device_live/show.ex:106
#, elixir-autogen, elixir-format
msgid "Discovery completed"
msgstr "Discovery completed"
#: lib/towerops_web/live/device_live/index.ex:92
#: lib/towerops_web/live/site_live/show.ex:45
#, elixir-autogen, elixir-format
msgid "Discovery started for %{count} device"
msgstr "Discovery started for %{count} device"
#: lib/towerops_web/live/device_live/form.ex:298
#, elixir-autogen, elixir-format
msgid "Discovery started..."
msgstr "Discovery started..."
#: lib/towerops_web/live/device_live/show.ex:1053
#, elixir-autogen, elixir-format
msgid "Failed to create backup request. Please try again."
msgstr "Failed to create backup request. Please try again."
#: lib/towerops_web/live/device_live/index.ex:211
#, elixir-autogen, elixir-format
msgid "Failed to reorder device"
msgstr "Failed to reorder device"
#: lib/towerops_web/live/device_live/index.ex:190
#, elixir-autogen, elixir-format
msgid "Failed to reorder site"
msgstr "Failed to reorder site"
#: lib/towerops_web/live/device_live/show.ex:1049
#, elixir-autogen, elixir-format
msgid "Manual backup requested. Results will appear shortly."
msgstr "Manual backup requested. Results will appear shortly."
#: lib/towerops_web/live/device_live/show.ex:1004
#, elixir-autogen, elixir-format
msgid "MikroTik API is not enabled for this device."
msgstr "MikroTik API is not enabled for this device."
#: lib/towerops_web/live/device_live/index.ex:85
#, elixir-autogen, elixir-format
msgid "No SNMP-enabled devices found"
msgstr "No SNMP-enabled devices found"
#: lib/towerops_web/live/device_live/show.ex:970
#: lib/towerops_web/live/device_live/show.ex:982
#, elixir-autogen, elixir-format
msgid "No other backups available for comparison"
msgstr "No other backups available for comparison"
#: lib/towerops_web/live/device_live/index.ex:157
#, elixir-autogen, elixir-format
msgid "Order reset to alphabetical"
msgstr "Order reset to alphabetical"
#: lib/towerops_web/live/device_live/form.ex:38
#, elixir-autogen, elixir-format
msgid "Please create a site before adding a device."
msgstr "Please create a site before adding a device."
#: lib/towerops_web/live/device_live/form.ex:301
#, elixir-autogen, elixir-format
msgid "SNMP is not enabled for this device"
msgstr "SNMP is not enabled for this device"
#: lib/towerops_web/live/device_live/index.ex:105
#, elixir-autogen, elixir-format
msgid "Site not found"
msgstr "Site not found"
#: lib/towerops_web/live/device_live/index.ex:187
#, elixir-autogen, elixir-format
msgid "Site order updated"
msgstr "Site order updated"
#: lib/towerops_web/live/device_live/form.ex:262
#, elixir-autogen, elixir-format
msgid "Unable to delete device"
msgstr "Unable to delete device"
#: lib/towerops_web/live/device_live/form.ex:122
#: lib/towerops_web/live/device_live/index.ex:132
#: lib/towerops_web/live/device_live/show.ex:73
#, elixir-autogen, elixir-format
msgid "You don't have access to this device"
msgstr "You don't have access to this device"
#: lib/towerops_web/live/device_live/index.ex:111
#, elixir-autogen, elixir-format
msgid "You don't have access to this site"
msgstr "You don't have access to this site"
#: lib/towerops_web/live/device_live/show.ex:959
#, elixir-autogen, elixir-format
msgid "You don't have permission to access this backup"
msgstr "You don't have permission to access this backup"
#: lib/towerops_web/live/alert_live/index.ex:62
#, elixir-autogen, elixir-format
msgid "Alert acknowledged"
msgstr "Alert acknowledged"
#: lib/towerops_web/live/alert_live/index.ex:43
#, elixir-autogen, elixir-format, fuzzy
msgid "Alert not found"
msgstr "Site not found"
#: lib/towerops_web/live/site_live/form.ex:104
#, elixir-autogen, elixir-format
msgid "Applied SNMP configuration to %{count} device records at this site"
msgstr "Applied SNMP configuration to %{count} device records at this site"
#: lib/towerops_web/live/site_live/form.ex:113
#, elixir-autogen, elixir-format
msgid "Applied agent to %{count} device records at this site"
msgstr "Applied agent to %{count} device records at this site"
#: lib/towerops_web/live/site_live/show.ex:37
#, elixir-autogen, elixir-format, fuzzy
msgid "No SNMP-enabled devices at this site"
msgstr "No SNMP-enabled devices found"
#: lib/towerops_web/live/site_live/form.ex:123
#, elixir-autogen, elixir-format
msgid "Site created successfully! Now add your first device."
msgstr "Site created successfully! Now add your first device."
#: lib/towerops_web/live/site_live/form.ex:88
#, elixir-autogen, elixir-format, fuzzy
msgid "Site deleted successfully"
msgstr "Device deleted successfully"
#: lib/towerops_web/live/site_live/form.ex:136
#, elixir-autogen, elixir-format, fuzzy
msgid "Site updated successfully"
msgstr "Device updated successfully"
#: lib/towerops_web/live/alert_live/index.ex:66
#, elixir-autogen, elixir-format
msgid "Unable to acknowledge alert"
msgstr "Unable to acknowledge alert"
#: lib/towerops_web/live/site_live/form.ex:92
#, elixir-autogen, elixir-format, fuzzy
msgid "Unable to delete site"
msgstr "Unable to delete device"
#: lib/towerops_web/live/alert_live/index.ex:52
#, elixir-autogen, elixir-format, fuzzy
msgid "You don't have access to this alert"
msgstr "You don't have access to this site"

View file

@ -13,3 +13,209 @@
msgid ""
msgstr ""
"Language: en\n"
#: lib/towerops_web/live/device_live/form.ex:405
#, elixir-autogen, elixir-format
msgid "Device created. "
msgstr ""
#: lib/towerops_web/live/device_live/form.ex:403
#, elixir-autogen, elixir-format
msgid "Device created. SNMP discovery started. "
msgstr ""
#: lib/towerops_web/live/device_live/form.ex:258
#, elixir-autogen, elixir-format
msgid "Device deleted successfully"
msgstr ""
#: lib/towerops_web/live/device_live/show.ex:1001
#, elixir-autogen, elixir-format
msgid "Device has no agent assigned. Assign an agent to create backups."
msgstr ""
#: lib/towerops_web/live/device_live/show.ex:87
#, elixir-autogen, elixir-format
msgid "Device no longer exists"
msgstr ""
#: lib/towerops_web/live/device_live/form.ex:110
#: lib/towerops_web/live/device_live/index.ex:123
#: lib/towerops_web/live/device_live/show.ex:54
#, elixir-autogen, elixir-format
msgid "Device not found"
msgstr ""
#: lib/towerops_web/live/device_live/index.ex:208
#, elixir-autogen, elixir-format
msgid "Device order updated"
msgstr ""
#: lib/towerops_web/live/device_live/form.ex:425
#, elixir-autogen, elixir-format
msgid "Device updated successfully"
msgstr ""
#: lib/towerops_web/live/device_live/form.ex:423
#, elixir-autogen, elixir-format
msgid "Device updated successfully. SNMP discovery started in background."
msgstr ""
#: lib/towerops_web/live/device_live/index.ex:24
#, elixir-autogen, elixir-format
msgid "Devices"
msgstr ""
#: lib/towerops_web/live/device_live/show.ex:106
#, elixir-autogen, elixir-format
msgid "Discovery completed"
msgstr ""
#: lib/towerops_web/live/device_live/index.ex:92
#: lib/towerops_web/live/site_live/show.ex:45
#, elixir-autogen, elixir-format
msgid "Discovery started for %{count} device"
msgstr ""
#: lib/towerops_web/live/device_live/form.ex:298
#, elixir-autogen, elixir-format
msgid "Discovery started..."
msgstr ""
#: lib/towerops_web/live/device_live/show.ex:1053
#, elixir-autogen, elixir-format
msgid "Failed to create backup request. Please try again."
msgstr ""
#: lib/towerops_web/live/device_live/index.ex:211
#, elixir-autogen, elixir-format
msgid "Failed to reorder device"
msgstr ""
#: lib/towerops_web/live/device_live/index.ex:190
#, elixir-autogen, elixir-format
msgid "Failed to reorder site"
msgstr ""
#: lib/towerops_web/live/device_live/show.ex:1049
#, elixir-autogen, elixir-format
msgid "Manual backup requested. Results will appear shortly."
msgstr ""
#: lib/towerops_web/live/device_live/show.ex:1004
#, elixir-autogen, elixir-format
msgid "MikroTik API is not enabled for this device."
msgstr ""
#: lib/towerops_web/live/device_live/index.ex:85
#, elixir-autogen, elixir-format
msgid "No SNMP-enabled devices found"
msgstr ""
#: lib/towerops_web/live/device_live/show.ex:970
#: lib/towerops_web/live/device_live/show.ex:982
#, elixir-autogen, elixir-format
msgid "No other backups available for comparison"
msgstr ""
#: lib/towerops_web/live/device_live/index.ex:157
#, elixir-autogen, elixir-format
msgid "Order reset to alphabetical"
msgstr ""
#: lib/towerops_web/live/device_live/form.ex:38
#, elixir-autogen, elixir-format
msgid "Please create a site before adding a device."
msgstr ""
#: lib/towerops_web/live/device_live/form.ex:301
#, elixir-autogen, elixir-format
msgid "SNMP is not enabled for this device"
msgstr ""
#: lib/towerops_web/live/device_live/index.ex:105
#, elixir-autogen, elixir-format
msgid "Site not found"
msgstr ""
#: lib/towerops_web/live/device_live/index.ex:187
#, elixir-autogen, elixir-format
msgid "Site order updated"
msgstr ""
#: lib/towerops_web/live/device_live/form.ex:262
#, elixir-autogen, elixir-format
msgid "Unable to delete device"
msgstr ""
#: lib/towerops_web/live/device_live/form.ex:122
#: lib/towerops_web/live/device_live/index.ex:132
#: lib/towerops_web/live/device_live/show.ex:73
#, elixir-autogen, elixir-format
msgid "You don't have access to this device"
msgstr ""
#: lib/towerops_web/live/device_live/index.ex:111
#, elixir-autogen, elixir-format
msgid "You don't have access to this site"
msgstr ""
#: lib/towerops_web/live/device_live/show.ex:959
#, elixir-autogen, elixir-format
msgid "You don't have permission to access this backup"
msgstr ""
#: lib/towerops_web/live/alert_live/index.ex:62
#, elixir-autogen, elixir-format
msgid "Alert acknowledged"
msgstr ""
#: lib/towerops_web/live/alert_live/index.ex:43
#, elixir-autogen, elixir-format
msgid "Alert not found"
msgstr ""
#: lib/towerops_web/live/site_live/form.ex:104
#, elixir-autogen, elixir-format
msgid "Applied SNMP configuration to %{count} device records at this site"
msgstr ""
#: lib/towerops_web/live/site_live/form.ex:113
#, elixir-autogen, elixir-format
msgid "Applied agent to %{count} device records at this site"
msgstr ""
#: lib/towerops_web/live/site_live/show.ex:37
#, elixir-autogen, elixir-format
msgid "No SNMP-enabled devices at this site"
msgstr ""
#: lib/towerops_web/live/site_live/form.ex:123
#, elixir-autogen, elixir-format
msgid "Site created successfully! Now add your first device."
msgstr ""
#: lib/towerops_web/live/site_live/form.ex:88
#, elixir-autogen, elixir-format
msgid "Site deleted successfully"
msgstr ""
#: lib/towerops_web/live/site_live/form.ex:136
#, elixir-autogen, elixir-format
msgid "Site updated successfully"
msgstr ""
#: lib/towerops_web/live/alert_live/index.ex:66
#, elixir-autogen, elixir-format
msgid "Unable to acknowledge alert"
msgstr ""
#: lib/towerops_web/live/site_live/form.ex:92
#, elixir-autogen, elixir-format
msgid "Unable to delete site"
msgstr ""
#: lib/towerops_web/live/alert_live/index.ex:52
#, elixir-autogen, elixir-format
msgid "You don't have access to this alert"
msgstr ""

View file

@ -58,6 +58,25 @@ defmodule ToweropsWeb.ConnCase do
%{conn: log_in_user(conn, user, opts), user: user, scope: scope}
end
@doc """
Registers and logs in a user with sudo mode enabled.
It stores an updated connection and a registered user in the
test context.
"""
def register_and_log_in_user_with_sudo(%{conn: conn} = context) do
user = Towerops.AccountsFixtures.user_fixture()
{:ok, user} = Towerops.Accounts.grant_sudo_mode(user)
scope = Scope.for_user(user)
opts =
context
|> Map.take([:token_authenticated_at])
|> Enum.to_list()
%{conn: log_in_user(conn, user, opts), user: user, scope: scope}
end
@doc """
Logs the given `user` into the `conn`.

View file

@ -94,17 +94,17 @@ defmodule Towerops.AccountsTest do
end
describe "sudo_mode?/2" do
test "validates the authenticated_at time" do
test "validates the last_sudo_at time" do
now = DateTime.utc_now()
assert Accounts.sudo_mode?(%User{authenticated_at: DateTime.utc_now()})
assert Accounts.sudo_mode?(%User{authenticated_at: DateTime.add(now, -19, :minute)})
refute Accounts.sudo_mode?(%User{authenticated_at: DateTime.add(now, -21, :minute)})
assert Accounts.sudo_mode?(%User{last_sudo_at: DateTime.utc_now()})
assert Accounts.sudo_mode?(%User{last_sudo_at: DateTime.add(now, -9, :minute)})
refute Accounts.sudo_mode?(%User{last_sudo_at: DateTime.add(now, -11, :minute)})
# minute override
refute Accounts.sudo_mode?(
%User{authenticated_at: DateTime.add(now, -11, :minute)},
-10
%User{last_sudo_at: DateTime.add(now, -6, :minute)},
-5
)
# not authenticated

View file

@ -3,7 +3,7 @@ defmodule ToweropsWeb.AccountLive.MyDataTest do
import Phoenix.LiveViewTest
setup :register_and_log_in_user
setup :register_and_log_in_user_with_sudo
describe "My Data page" do
test "renders for organization owner without additional data", %{conn: conn} do

View file

@ -6,17 +6,6 @@ defmodule ToweropsWeb.UserSettingsLiveTest do
alias Towerops.Accounts
describe "UserSettingsLive without sudo mode" do
setup :register_and_log_in_user
test "allows access without sudo mode", %{conn: conn} do
# User settings page is accessible without sudo mode
{:ok, _view, html} = live(conn, ~p"/users/settings")
assert html =~ "Account Settings"
end
end
describe "UserSettingsLive with sudo mode" do
setup :register_and_log_in_user_with_sudo
@ -177,19 +166,4 @@ defmodule ToweropsWeb.UserSettingsLiveTest do
assert html =~ "Login History"
end
end
# Helper to register user and grant sudo mode
# Note: user_fixture() already creates users with TOTP enabled by default
defp register_and_log_in_user_with_sudo(%{conn: conn}) do
user = user_fixture()
# Log in and grant sudo mode
conn =
conn
|> log_in_user(user)
|> init_test_session(%{})
|> put_session(:user_sudo_granted_at, System.system_time(:second))
%{conn: conn, user: user}
end
end

View file

@ -883,11 +883,14 @@ defmodule ToweropsWeb.UserAuthTest do
{:ok, _device, _secret} =
Accounts.create_totp_device(user.id, "Test Device", NimbleTOTP.secret())
# Create session with user token
token = Accounts.generate_user_session_token(user)
session = %{"user_token" => token}
socket = %Socket{assigns: %{__changed__: %{}, flash: %{}}}
socket = Phoenix.Component.assign(socket, :current_scope, Scope.for_user(user))
assert {:halt, redirected_socket} =
UserAuth.on_mount(:require_sudo_mode, %{}, %{}, socket)
UserAuth.on_mount(:require_sudo_mode, %{}, session, socket)
assert Phoenix.Flash.get(redirected_socket.assigns.flash, :error) =~ "verify your identity"
end
@ -895,11 +898,14 @@ defmodule ToweropsWeb.UserAuthTest do
test "redirects to TOTP enrollment when sudo mode expired and no TOTP" do
user = user_fixture(enable_totp: false)
# Create session with user token
token = Accounts.generate_user_session_token(user)
session = %{"user_token" => token}
socket = %Socket{assigns: %{__changed__: %{}, flash: %{}}}
socket = Phoenix.Component.assign(socket, :current_scope, Scope.for_user(user))
assert {:halt, redirected_socket} =
UserAuth.on_mount(:require_sudo_mode, %{}, %{}, socket)
UserAuth.on_mount(:require_sudo_mode, %{}, session, socket)
assert Phoenix.Flash.get(redirected_socket.assigns.flash, :error) =~ "Two-factor authentication"
end
@ -908,10 +914,13 @@ defmodule ToweropsWeb.UserAuthTest do
user = user_fixture()
{:ok, user} = Accounts.grant_sudo_mode(user)
socket = %Socket{assigns: %{__changed__: %{}, flash: %{}}}
socket = Phoenix.Component.assign(socket, :current_scope, Scope.for_user(user))
# Create session with user token
token = Accounts.generate_user_session_token(user)
session = %{"user_token" => token}
assert {:cont, _socket} = UserAuth.on_mount(:require_sudo_mode, %{}, %{}, socket)
socket = %Socket{assigns: %{__changed__: %{}, flash: %{}}}
assert {:cont, _socket} = UserAuth.on_mount(:require_sudo_mode, %{}, session, socket)
end
test "redirects unauthenticated users" do