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