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
defp generate_slug(changeset) do
case get_change(changeset, :name) do
nil ->
changeset
# Only generate slug if not already set
if get_field(changeset, :slug) do
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 ->
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)
put_change(changeset, :slug, slug)
end
end
end

View file

@ -177,22 +177,26 @@ defmodule ToweropsWeb.EquipmentLiveTest do
test "creates new equipment", %{conn: conn, organization: organization, site: site} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
assert view
|> form("#equipment-form",
equipment: %{
name: "New Router",
ip_address: "192.168.1.100",
site_id: site.id,
description: "Test router",
monitoring_enabled: true,
check_interval_seconds: 300
}
)
|> render_submit() =~ "Equipment created successfully"
view
|> form("#equipment-form",
equipment: %{
name: "New Router",
ip_address: "192.168.1.100",
site_id: site.id,
description: "Test router",
monitoring_enabled: true,
check_interval_seconds: 300
}
)
|> render_submit()
# Verify equipment was created
equipment = organization.id |> Towerops.Equipment.list_organization_equipment() |> List.first()
assert equipment.name == "New Router"
equipment_list = Towerops.Equipment.list_organization_equipment(organization.id)
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
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
{:ok, view, _html} = live(conn, ~p"/orgs/new")
assert view
|> form("#organization-form",
organization: %{
name: "New Organization"
}
)
|> render_submit() =~ "Organization created successfully"
view
|> form("#organization-form",
organization: %{
name: "New Organization"
}
)
|> render_submit()
# Verify organization was created
organizations = Towerops.Organizations.list_user_organizations(user.id)
@ -92,13 +92,13 @@ defmodule ToweropsWeb.OrgLiveTest do
{:ok, view, _html} = live(conn, ~p"/orgs/new")
assert view
|> form("#organization-form",
organization: %{
name: "Existing Org"
}
)
|> render_submit() =~ "Organization created successfully"
view
|> form("#organization-form",
organization: %{
name: "Existing Org"
}
)
|> render_submit()
# Verify both organizations exist with same name but different slugs
organizations = Towerops.Organizations.list_user_organizations(user.id)