add snmp port

This commit is contained in:
Graham McIntire 2026-02-03 12:51:50 -06:00
parent 88d549cd92
commit c198fdeaa1
No known key found for this signature in database
13 changed files with 92 additions and 13 deletions

View file

@ -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 */

View file

@ -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",

View file

@ -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)

View file

@ -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

View file

@ -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])

View file

@ -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)

View file

@ -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)

View file

@ -34,12 +34,10 @@
{t_auth("You are running the local mail adapter.")}
</p>
<p class="mt-1 text-sm text-blue-700 dark:text-blue-200">
{t_auth("To see sent emails, visit %{link}.",
link:
raw(
~s(<a href="/dev/mailbox" class="font-medium underline">#{t_auth("the mailbox page")}</a>)
)
)}
{t_auth("To see sent emails, visit")}
<a href="/dev/mailbox" class="font-medium underline">
{t_auth("the mailbox page")}
</a>.
</p>
</div>
</div>
@ -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"

View file

@ -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 %>
<div class="mt-6 rounded-md bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 p-4">
<h3 class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">

View file

@ -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 %>
<div class="mt-4 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
<p class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">

View file

@ -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"
/>

View file

@ -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

View file

@ -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