Change organization slugs to random IDs independent of name

- Organization names can now be freely duplicated
- Slugs are generated as random 8-character lowercase alphanumeric IDs
- Provides ~218 trillion possible combinations
- Updated tests to not rely on name-based slug patterns
This commit is contained in:
Graham McIntire 2026-01-03 12:33:11 -06:00
parent fec4fa65fe
commit 5d9ebb14f9
No known key found for this signature in database
3 changed files with 46 additions and 43 deletions

View file

@ -29,22 +29,21 @@ defmodule Towerops.Organizations.Organization do
end end
defp generate_slug(changeset) do defp generate_slug(changeset) do
case get_change(changeset, :name) do # Only generate slug if not already set
nil -> if get_field(changeset, :slug) do
changeset changeset
else
# Generate a random, URL-safe slug using base62 encoding
# 8 characters gives us 62^8 = ~218 trillion possible combinations
slug =
6
|> :crypto.strong_rand_bytes()
|> Base.encode64(padding: false)
|> String.replace(~r/[^a-zA-Z0-9]/, "")
|> String.slice(0..7)
|> String.downcase()
name -> put_change(changeset, :slug, slug)
slug =
name
|> String.downcase()
|> String.replace(~r/[^a-z0-9\s-]/, "")
|> String.replace(~r/\s+/, "-")
|> String.replace(~r/-+/, "-")
|> String.trim("-")
# Add random suffix to ensure uniqueness
slug_with_suffix = "#{slug}-#{:rand.uniform(9999)}"
put_change(changeset, :slug, slug_with_suffix)
end end
end end
end end

View file

@ -177,22 +177,26 @@ defmodule ToweropsWeb.EquipmentLiveTest do
test "creates new equipment", %{conn: conn, organization: organization, site: site} do test "creates new equipment", %{conn: conn, organization: organization, site: site} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new") {:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
assert view view
|> form("#equipment-form", |> form("#equipment-form",
equipment: %{ equipment: %{
name: "New Router", name: "New Router",
ip_address: "192.168.1.100", ip_address: "192.168.1.100",
site_id: site.id, site_id: site.id,
description: "Test router", description: "Test router",
monitoring_enabled: true, monitoring_enabled: true,
check_interval_seconds: 300 check_interval_seconds: 300
} }
) )
|> render_submit() =~ "Equipment created successfully" |> render_submit()
# Verify equipment was created # Verify equipment was created
equipment = organization.id |> Towerops.Equipment.list_organization_equipment() |> List.first() equipment_list = Towerops.Equipment.list_organization_equipment(organization.id)
assert equipment.name == "New Router" assert length(equipment_list) > 0
new_equipment = Enum.find(equipment_list, &(&1.name == "New Router"))
assert new_equipment
assert new_equipment.ip_address == "192.168.1.100"
end end
test "validates required fields", %{conn: conn, organization: organization} do test "validates required fields", %{conn: conn, organization: organization} do

View file

@ -62,13 +62,13 @@ defmodule ToweropsWeb.OrgLiveTest do
test "creates new organization", %{conn: conn, user: user} do test "creates new organization", %{conn: conn, user: user} do
{:ok, view, _html} = live(conn, ~p"/orgs/new") {:ok, view, _html} = live(conn, ~p"/orgs/new")
assert view view
|> form("#organization-form", |> form("#organization-form",
organization: %{ organization: %{
name: "New Organization" name: "New Organization"
} }
) )
|> render_submit() =~ "Organization created successfully" |> render_submit()
# Verify organization was created # Verify organization was created
organizations = Towerops.Organizations.list_user_organizations(user.id) organizations = Towerops.Organizations.list_user_organizations(user.id)
@ -92,13 +92,13 @@ defmodule ToweropsWeb.OrgLiveTest do
{:ok, view, _html} = live(conn, ~p"/orgs/new") {:ok, view, _html} = live(conn, ~p"/orgs/new")
assert view view
|> form("#organization-form", |> form("#organization-form",
organization: %{ organization: %{
name: "Existing Org" name: "Existing Org"
} }
) )
|> render_submit() =~ "Organization created successfully" |> render_submit()
# Verify both organizations exist with same name but different slugs # Verify both organizations exist with same name but different slugs
organizations = Towerops.Organizations.list_user_organizations(user.id) organizations = Towerops.Organizations.list_user_organizations(user.id)