From 949cfaf4e0ee2ae0e9ddd7968c1fdf7ec491de91 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 2 Feb 2026 10:45:37 -0600 Subject: [PATCH] add default org --- lib/towerops/accounts/user.ex | 7 +++- lib/towerops_web/live/user_settings_live.ex | 11 ++++- .../live/user_settings_live.html.heex | 42 +++++++++++++++++++ lib/towerops_web/user_auth.ex | 37 +++++++++++++++- ...4115_add_default_organization_to_users.exs | 12 ++++++ 5 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 priv/repo/migrations/20260202164115_add_default_organization_to_users.exs diff --git a/lib/towerops/accounts/user.ex b/lib/towerops/accounts/user.ex index 457f0c1e..48ebb04e 100644 --- a/lib/towerops/accounts/user.ex +++ b/lib/towerops/accounts/user.ex @@ -13,6 +13,7 @@ defmodule Towerops.Accounts.User do alias Ecto.Association.NotLoaded alias Towerops.Organizations.Membership + alias Towerops.Organizations.Organization @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id @@ -36,6 +37,7 @@ defmodule Towerops.Accounts.User do has_many :memberships, Membership has_many :organizations, through: [:memberships, :organization] + belongs_to :default_organization, Organization timestamps(type: :utc_datetime) end @@ -55,7 +57,7 @@ defmodule Towerops.Accounts.User do totp_verified_at: DateTime.t() | nil, last_sudo_at: DateTime.t() | nil, memberships: NotLoaded.t() | [Membership.t()], - organizations: NotLoaded.t() | [Towerops.Organizations.Organization.t()], + organizations: NotLoaded.t() | [Organization.t()], inserted_at: DateTime.t(), updated_at: DateTime.t() } @@ -107,10 +109,11 @@ defmodule Towerops.Accounts.User do """ def profile_changeset(user, attrs) do user - |> cast(attrs, [:first_name, :last_name, :timezone]) + |> cast(attrs, [:first_name, :last_name, :timezone, :default_organization_id]) |> validate_length(:first_name, max: 100) |> validate_length(:last_name, max: 100) |> validate_length(:timezone, max: 100) + |> foreign_key_constraint(:default_organization_id) end @doc """ diff --git a/lib/towerops_web/live/user_settings_live.ex b/lib/towerops_web/live/user_settings_live.ex index b28b998b..80455f14 100644 --- a/lib/towerops_web/live/user_settings_live.ex +++ b/lib/towerops_web/live/user_settings_live.ex @@ -287,8 +287,17 @@ defmodule ToweropsWeb.UserSettingsLive do end defp assign_default_organization(socket) do + user = socket.assigns.current_scope.user organizations = socket.assigns.organizations - default_org = List.first(organizations) + + # Get user's preferred default org, or fall back to first org + default_org = + if user.default_organization_id do + Enum.find(organizations, &(&1.id == user.default_organization_id)) || List.first(organizations) + else + List.first(organizations) + end + assign(socket, :default_organization, default_org) end diff --git a/lib/towerops_web/live/user_settings_live.html.heex b/lib/towerops_web/live/user_settings_live.html.heex index 70342c37..42cce6ef 100644 --- a/lib/towerops_web/live/user_settings_live.html.heex +++ b/lib/towerops_web/live/user_settings_live.html.heex @@ -251,6 +251,48 @@ + +
+ +

+ Choose which organization you see by default when logging in or navigating to pages without an organization context. +

+
+ + +
+
diff --git a/lib/towerops_web/user_auth.ex b/lib/towerops_web/user_auth.ex index 32bcab5a..fdad2669 100644 --- a/lib/towerops_web/user_auth.ex +++ b/lib/towerops_web/user_auth.ex @@ -426,6 +426,7 @@ defmodule ToweropsWeb.UserAuth do conn |> assign(:current_scope, Scope.put_organization(scope, organization)) |> assign(:current_membership, membership) + |> put_session(:current_organization_id, organization.id) else conn |> put_flash(:error, "You don't have access to this organization.") @@ -673,13 +674,45 @@ defmodule ToweropsWeb.UserAuth do end end - defp find_user_organization(org_id, _user_id) when is_binary(org_id) do + defp find_user_organization(org_id, user_id) when is_binary(org_id) do + # Try session org first + org = + try do + Towerops.Organizations.get_organization!(org_id) + rescue + Ecto.NoResultsError -> nil + end + + # Verify user has access to this org + if org do + membership = Towerops.Organizations.get_membership(org.id, user_id) + if membership, do: org + end + end + + defp find_user_organization(_org_id, user_id) do + user = Accounts.get_user(user_id) + default_org_id = user && user.default_organization_id + + # Try user's default organization first + with true <- not is_nil(default_org_id), + default_org = try_get_organization(default_org_id), + true <- not is_nil(default_org), + membership = Towerops.Organizations.get_membership(default_org.id, user_id), + true <- not is_nil(membership) do + default_org + else + _ -> fallback_to_first_org(user_id) + end + end + + defp try_get_organization(org_id) do Towerops.Organizations.get_organization!(org_id) rescue Ecto.NoResultsError -> nil end - defp find_user_organization(_org_id, user_id) do + defp fallback_to_first_org(user_id) do case Towerops.Organizations.list_user_organizations(user_id) do [first_org | _] -> first_org [] -> nil diff --git a/priv/repo/migrations/20260202164115_add_default_organization_to_users.exs b/priv/repo/migrations/20260202164115_add_default_organization_to_users.exs new file mode 100644 index 00000000..c5829430 --- /dev/null +++ b/priv/repo/migrations/20260202164115_add_default_organization_to_users.exs @@ -0,0 +1,12 @@ +defmodule Towerops.Repo.Migrations.AddDefaultOrganizationToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add :default_organization_id, + references(:organizations, type: :binary_id, on_delete: :nilify_all) + end + + create index(:users, [:default_organization_id]) + end +end