From c198fdeaa14c43b80ca52ff4eb0ad4c2f8e5a487 Mon Sep 17 00:00:00 2001
From: Graham McIntire
Date: Tue, 3 Feb 2026 12:51:50 -0600
Subject: [PATCH] add snmp port
---
assets/css/app.css | 3 +++
config/dev.exs | 3 +++
lib/towerops/accounts.ex | 4 +--
lib/towerops/devices.ex | 9 ++++++-
lib/towerops/organizations/organization.ex | 4 +++
lib/towerops/sites/site.ex | 4 +++
.../controllers/user_session_controller.ex | 5 ++--
.../user_session_html/new.html.heex | 13 ++++-----
.../live/org/settings_live.html.heex | 7 +++++
.../live/site_live/form.html.heex | 7 +++++
.../live/user_registration_live.ex | 27 +++++++++++++++++--
...d_snmp_port_to_organizations_and_sites.exs | 15 +++++++++++
priv/static/changelog.txt | 4 +++
13 files changed, 92 insertions(+), 13 deletions(-)
create mode 100644 priv/repo/migrations/20260203184229_add_snmp_port_to_organizations_and_sites.exs
diff --git a/assets/css/app.css b/assets/css/app.css
index 30425cfb..f3ba08d3 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -102,4 +102,7 @@
/* Make LiveView wrapper divs transparent for layout */
[data-phx-session], [data-phx-teleported-src] { display: contents }
+/* Hide 1Password live region */
+[id="1p-live-region"] { display: none !important; }
+
/* This file is for your main application CSS */
diff --git a/config/dev.exs b/config/dev.exs
index 521cb535..51f85790 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -164,6 +164,9 @@ config :towerops, :mib_dirs, [
"/usr/share/snmp/mibs"
]
+# Disable rate limiting in development
+config :towerops, :rate_limiting_enabled, false
+
# Configure Redis/Valkey for development
config :towerops, :redis,
host: "localhost",
diff --git a/lib/towerops/accounts.ex b/lib/towerops/accounts.ex
index 75031fa1..31e25395 100644
--- a/lib/towerops/accounts.ex
+++ b/lib/towerops/accounts.ex
@@ -183,7 +183,7 @@ defmodule Towerops.Accounts do
terms_consent = attrs["terms_of_service_consent"] || attrs[:terms_of_service_consent]
multi =
- if privacy_consent in ["true", true] do
+ if privacy_consent in ["true", true, "on"] do
Ecto.Multi.run(multi, :privacy_policy_consent, fn _repo, %{user: user} ->
grant_consent(user.id, "privacy_policy")
end)
@@ -191,7 +191,7 @@ defmodule Towerops.Accounts do
multi
end
- if terms_consent in ["true", true] do
+ if terms_consent in ["true", true, "on"] do
Ecto.Multi.run(multi, :terms_of_service_consent, fn _repo, %{user: user} ->
grant_consent(user.id, "terms_of_service")
end)
diff --git a/lib/towerops/devices.ex b/lib/towerops/devices.ex
index 40e1de8a..73402958 100644
--- a/lib/towerops/devices.ex
+++ b/lib/towerops/devices.ex
@@ -297,7 +297,7 @@ defmodule Towerops.Devices do
end
defp resolve_snmp_config(device) do
- # Resolve community and version independently with hierarchical fallback
+ # Resolve community, version, and port independently with hierarchical fallback
# This allows setting version at device level while inheriting community from org
community =
device.snmp_community ||
@@ -310,11 +310,18 @@ defmodule Towerops.Devices do
device.site.organization.snmp_version ||
"2c"
+ port =
+ device.snmp_port ||
+ device.site.snmp_port ||
+ device.site.organization.snmp_port ||
+ 161
+
source = determine_snmp_source(device)
%{
version: version,
community: community,
+ port: port,
source: source
}
end
diff --git a/lib/towerops/organizations/organization.ex b/lib/towerops/organizations/organization.ex
index 4d1df0d0..df93d785 100644
--- a/lib/towerops/organizations/organization.ex
+++ b/lib/towerops/organizations/organization.ex
@@ -29,6 +29,7 @@ defmodule Towerops.Organizations.Organization do
# device)
field :snmp_version, :string, default: "2c"
field :snmp_community, :string
+ field :snmp_port, :integer, default: 161
# MikroTik API credentials (organization-level defaults cascade to site → device)
field :mikrotik_username, Binary
@@ -54,6 +55,7 @@ defmodule Towerops.Organizations.Organization do
subscription_plan: String.t(),
snmp_version: String.t() | nil,
snmp_community: String.t() | nil,
+ snmp_port: integer(),
mikrotik_username: String.t() | nil,
mikrotik_password: String.t() | nil,
mikrotik_port: integer() | nil,
@@ -78,6 +80,7 @@ defmodule Towerops.Organizations.Organization do
:default_agent_token_id,
:snmp_version,
:snmp_community,
+ :snmp_port,
:mikrotik_username,
:mikrotik_password,
:mikrotik_port,
@@ -89,6 +92,7 @@ defmodule Towerops.Organizations.Organization do
|> validate_length(:name, min: 2, max: 100)
|> validate_inclusion(:subscription_plan, ["free"])
|> validate_inclusion(:snmp_version, ["1", "2c", "3"], message: "must be 1, 2c, or 3")
+ |> validate_number(:snmp_port, greater_than: 0, less_than: 65_536)
|> validate_mikrotik_fields()
|> generate_slug()
|> validate_required([:slug])
diff --git a/lib/towerops/sites/site.ex b/lib/towerops/sites/site.ex
index 429d7974..febeb3a2 100644
--- a/lib/towerops/sites/site.ex
+++ b/lib/towerops/sites/site.ex
@@ -26,6 +26,7 @@ defmodule Towerops.Sites.Site do
# SNMP configuration (overrides organization default)
field :snmp_version, :string
field :snmp_community, :string
+ field :snmp_port, :integer
# MikroTik API credentials (overrides organization default)
field :mikrotik_username, Binary
@@ -52,6 +53,7 @@ defmodule Towerops.Sites.Site do
display_order: integer() | nil,
snmp_version: String.t() | nil,
snmp_community: String.t() | nil,
+ snmp_port: integer() | nil,
mikrotik_username: String.t() | nil,
mikrotik_password: String.t() | nil,
mikrotik_port: integer() | nil,
@@ -83,6 +85,7 @@ defmodule Towerops.Sites.Site do
:parent_site_id,
:snmp_version,
:snmp_community,
+ :snmp_port,
:mikrotik_username,
:mikrotik_password,
:mikrotik_port,
@@ -95,6 +98,7 @@ defmodule Towerops.Sites.Site do
|> validate_length(:description, max: 1000)
|> validate_length(:location, max: 200)
|> validate_inclusion(:snmp_version, ["1", "2c", "3"], message: "must be 1, 2c, or 3")
+ |> validate_number(:snmp_port, greater_than: 0, less_than: 65_536)
|> validate_mikrotik_fields()
|> foreign_key_constraint(:organization_id)
|> foreign_key_constraint(:agent_token_id)
diff --git a/lib/towerops_web/controllers/user_session_controller.ex b/lib/towerops_web/controllers/user_session_controller.ex
index a44e2d0f..5052ff3c 100644
--- a/lib/towerops_web/controllers/user_session_controller.ex
+++ b/lib/towerops_web/controllers/user_session_controller.ex
@@ -6,8 +6,9 @@ defmodule ToweropsWeb.UserSessionController do
alias Towerops.Accounts
alias ToweropsWeb.UserAuth
- def new(conn, _params) do
- email = get_in(conn.assigns, [:current_scope, Access.key(:user), Access.key(:email)])
+ def new(conn, params) do
+ # Check for email in query params (from registration redirect) or current user
+ email = params["email"] || get_in(conn.assigns, [:current_scope, Access.key(:user), Access.key(:email)])
form = Phoenix.Component.to_form(%{"email" => email}, as: "user")
render(conn, :new, form: form)
diff --git a/lib/towerops_web/controllers/user_session_html/new.html.heex b/lib/towerops_web/controllers/user_session_html/new.html.heex
index 46b4581a..c362b81d 100644
--- a/lib/towerops_web/controllers/user_session_html/new.html.heex
+++ b/lib/towerops_web/controllers/user_session_html/new.html.heex
@@ -34,12 +34,10 @@
{t_auth("You are running the local mail adapter.")}
- {t_auth("To see sent emails, visit %{link}.",
- link:
- raw(
- ~s(#{t_auth("the mailbox page")})
- )
- )}
+ {t_auth("To see sent emails, visit")}
+
+ {t_auth("the mailbox page")}
+ .
@@ -51,6 +49,7 @@
<.input
readonly={!!@current_scope}
field={f[:email]}
+ id="user_email_password"
type="email"
label={t_auth("Email")}
autocomplete="email"
@@ -59,6 +58,7 @@
/>
<.input
field={f[:password]}
+ id="user_password"
type="password"
label={t_auth("Password")}
autocomplete="current-password"
@@ -95,6 +95,7 @@
<.input
readonly={!!@current_scope}
field={f[:email]}
+ id="user_email_magic"
type="email"
label={t_auth("Email")}
autocomplete="email"
diff --git a/lib/towerops_web/live/org/settings_live.html.heex b/lib/towerops_web/live/org/settings_live.html.heex
index 62a6d4ea..7b5abba2 100644
--- a/lib/towerops_web/live/org/settings_live.html.heex
+++ b/lib/towerops_web/live/org/settings_live.html.heex
@@ -80,6 +80,13 @@
placeholder="e.g., public"
/>
+ <.input
+ field={@form[:snmp_port]}
+ type="number"
+ label="SNMP Port"
+ placeholder="161"
+ />
+
<%= if @organization.snmp_community do %>
diff --git a/lib/towerops_web/live/site_live/form.html.heex b/lib/towerops_web/live/site_live/form.html.heex
index 61453fd0..3e8af009 100644
--- a/lib/towerops_web/live/site_live/form.html.heex
+++ b/lib/towerops_web/live/site_live/form.html.heex
@@ -158,6 +158,13 @@
placeholder="Leave blank to inherit from organization"
/>
+ <.input
+ field={@form[:snmp_port]}
+ type="number"
+ label="SNMP Port"
+ placeholder="Leave blank to inherit from organization (default: 161)"
+ />
+
<%= if @live_action == :edit and @site.snmp_community do %>
diff --git a/lib/towerops_web/live/user_registration_live.ex b/lib/towerops_web/live/user_registration_live.ex
index 1bf41e94..4f196946 100644
--- a/lib/towerops_web/live/user_registration_live.ex
+++ b/lib/towerops_web/live/user_registration_live.ex
@@ -26,7 +26,9 @@ defmodule ToweropsWeb.UserRegistrationLive do
|> assign(:invitation_token, invitation_token)
|> assign(:password_breach_count, nil)
|> assign(:detected_timezone, detected_timezone)
- |> assign(:trigger_action, false)}
+ |> assign(:trigger_action, false)
+ |> assign(:privacy_policy_consent, false)
+ |> assign(:terms_of_service_consent, false)}
end
def handle_event("check_password_breach", %{"value" => password}, socket)
@@ -50,12 +52,19 @@ defmodule ToweropsWeb.UserRegistrationLive do
|> Accounts.change_user_registration(user_params)
|> Map.put(:action, :validate)
- {:noreply, assign(socket, :form, to_form(changeset))}
+ {:noreply,
+ socket
+ |> assign(:form, to_form(changeset))
+ |> assign(:privacy_policy_consent, user_params["privacy_policy_consent"] == "on")
+ |> assign(:terms_of_service_consent, user_params["terms_of_service_consent"] == "on")}
end
def handle_event("save", %{"user" => user_params}, socket) do
invitation_token = socket.assigns.invitation_token
+ # Normalize checkbox values: HTML sends "on", but changeset expects true
+ user_params = normalize_consent_params(user_params)
+
if invitation_token do
# Registration via invitation - don't create personal org
create_via_invitation(socket, user_params, invitation_token)
@@ -65,6 +74,18 @@ defmodule ToweropsWeb.UserRegistrationLive do
end
end
+ defp normalize_consent_params(params) do
+ params
+ |> Map.update("privacy_policy_consent", nil, fn
+ "on" -> true
+ val -> val
+ end)
+ |> Map.update("terms_of_service_consent", nil, fn
+ "on" -> true
+ val -> val
+ end)
+ end
+
defp create_with_organization(socket, user_params) do
# Merge detected timezone into params
user_params_with_timezone = Map.put(user_params, "timezone", socket.assigns.detected_timezone)
@@ -238,6 +259,7 @@ defmodule ToweropsWeb.UserRegistrationLive do
id="privacy_policy_consent"
name="user[privacy_policy_consent]"
type="checkbox"
+ checked={@privacy_policy_consent}
required
class="h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-800 dark:checked:bg-blue-500 dark:checked:border-blue-500"
/>
@@ -267,6 +289,7 @@ defmodule ToweropsWeb.UserRegistrationLive do
id="terms_of_service_consent"
name="user[terms_of_service_consent]"
type="checkbox"
+ checked={@terms_of_service_consent}
required
class="h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-800 dark:checked:bg-blue-500 dark:checked:border-blue-500"
/>
diff --git a/priv/repo/migrations/20260203184229_add_snmp_port_to_organizations_and_sites.exs b/priv/repo/migrations/20260203184229_add_snmp_port_to_organizations_and_sites.exs
new file mode 100644
index 00000000..3aa038d4
--- /dev/null
+++ b/priv/repo/migrations/20260203184229_add_snmp_port_to_organizations_and_sites.exs
@@ -0,0 +1,15 @@
+defmodule Towerops.Repo.Migrations.AddSnmpPortToOrganizationsAndSites do
+ use Ecto.Migration
+
+ def change do
+ # Add snmp_port to organizations with default 161
+ alter table(:organizations) do
+ add :snmp_port, :integer, default: 161, null: false
+ end
+
+ # Add snmp_port to sites (nullable for inheritance)
+ alter table(:sites) do
+ add :snmp_port, :integer, null: true
+ end
+ end
+end
diff --git a/priv/static/changelog.txt b/priv/static/changelog.txt
index 8c6dcf69..1de7d176 100644
--- a/priv/static/changelog.txt
+++ b/priv/static/changelog.txt
@@ -3,6 +3,10 @@ Devices Tested & Working
* Ubiquiti AC, LTU, AirFiber
* Cambium ePMP
+2026-02-03
+* More tests
+* Bug fix: user registration
+
2026-02-02
* Experimental mikrotik api and backup
* Starting i18n