Features: - Users can set a default organization in org settings - First organization created is automatically set as default - Default org selector only shown if user has 2+ organizations - Migration auto-sets first org as default for existing users Database changes: - Added is_default boolean field to organization_memberships - Added partial index for efficient default org lookups - Migration sets oldest org as default for each existing user UI changes: - Organization settings page shows default org toggle - Green badge displayed when org is already default - Button to set org as default if it's not current default Backend: - Organizations.set_default_organization/2 manages defaults - Organizations.get_default_membership/1 retrieves default - Auto-set first org as default in create_organization/3
485 lines
20 KiB
Text
485 lines
20 KiB
Text
<Layouts.authenticated
|
|
flash={@flash}
|
|
current_scope={@current_scope}
|
|
>
|
|
<div class="border-b border-gray-200 pb-5 dark:border-white/5">
|
|
<div class="mb-4">
|
|
<.link
|
|
navigate={~p"/orgs/#{@organization.slug}"}
|
|
class="inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
|
|
>
|
|
<.icon name="hero-arrow-left" class="h-4 w-4" /> Back to Dashboard
|
|
</.link>
|
|
</div>
|
|
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
|
|
Organization Settings
|
|
</h1>
|
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
|
Manage organization defaults for SNMP, agents, and MikroTik API configuration
|
|
</p>
|
|
</div>
|
|
|
|
<.form
|
|
for={@form}
|
|
id="organization-form"
|
|
phx-change="validate"
|
|
phx-submit="save"
|
|
data-1p-ignore
|
|
data-form-type="other"
|
|
>
|
|
<div class="divide-y divide-gray-200 dark:divide-white/10">
|
|
<!-- Organization Name Section -->
|
|
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-10 px-4 py-16 sm:px-6 md:grid-cols-3 lg:px-8">
|
|
<div>
|
|
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
|
|
Organization Name
|
|
</h2>
|
|
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
|
|
Update the name of your organization.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="md:col-span-2">
|
|
<.input
|
|
field={@form[:name]}
|
|
type="text"
|
|
label="Organization Name"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<%= if @user_orgs_count > 1 do %>
|
|
<!-- Default Organization Section -->
|
|
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-10 px-4 py-16 sm:px-6 md:grid-cols-3 lg:px-8">
|
|
<div>
|
|
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
|
|
Default Organization
|
|
</h2>
|
|
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
|
|
Set this organization as your default. When you log in, you'll be directed to your default organization.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="md:col-span-2">
|
|
<%= if @membership.is_default do %>
|
|
<div class="rounded-md bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 p-4">
|
|
<div class="flex items-center">
|
|
<.icon
|
|
name="hero-check-circle"
|
|
class="h-5 w-5 text-green-600 dark:text-green-400"
|
|
/>
|
|
<p class="ml-3 text-sm text-green-800 dark:text-green-200">
|
|
This is your default organization
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<% else %>
|
|
<button
|
|
type="button"
|
|
phx-click="toggle_default_org"
|
|
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:shadow-none dark:hover:bg-indigo-400"
|
|
>
|
|
<.icon name="hero-star" class="h-4 w-4 inline" /> Set as Default Organization
|
|
</button>
|
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
|
Click to make this your default organization
|
|
</p>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<!-- Use Sites Section -->
|
|
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-10 px-4 py-16 sm:px-6 md:grid-cols-3 lg:px-8">
|
|
<div>
|
|
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
|
|
Site Organization
|
|
</h2>
|
|
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
|
|
Enable site organization to group devices into physical locations (offices, datacenters, etc.).
|
|
</p>
|
|
</div>
|
|
|
|
<div class="md:col-span-2">
|
|
<.input
|
|
field={@form[:use_sites]}
|
|
type="checkbox"
|
|
label="Use sites to organize devices"
|
|
/>
|
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
|
When enabled, you can organize devices into sites. When disabled, all devices belong directly to the organization.
|
|
</p>
|
|
|
|
<%= if @organization.use_sites && @form[:use_sites].value == false do %>
|
|
<div class="mt-4 rounded-md bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 p-4">
|
|
<div class="flex">
|
|
<div class="shrink-0">
|
|
<.icon name="hero-exclamation-triangle" class="h-5 w-5 text-amber-400" />
|
|
</div>
|
|
<div class="ml-3">
|
|
<h3 class="text-sm font-medium text-amber-800 dark:text-amber-200">
|
|
Warning: Disabling Sites
|
|
</h3>
|
|
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
|
|
<p>
|
|
All devices will be removed from their current sites and assigned directly to the organization.
|
|
Site assignments will be lost, but devices will remain in the organization.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- SNMP Configuration Section -->
|
|
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-10 px-4 py-16 sm:px-6 md:grid-cols-3 lg:px-8">
|
|
<div>
|
|
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
|
|
SNMP Configuration
|
|
</h2>
|
|
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
|
|
Set default SNMP settings for all devices in this organization. These can be overridden at the site or device level.
|
|
</p>
|
|
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400 italic">
|
|
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
|
|
Hierarchy: Device > Site > Organization
|
|
</p>
|
|
</div>
|
|
|
|
<div class="md:col-span-2">
|
|
<.input
|
|
field={@form[:snmp_version]}
|
|
type="select"
|
|
label="SNMP Version"
|
|
prompt="Select SNMP version"
|
|
options={[{"v1", "1"}, {"v2c", "2c"}, {"v3", "3"}]}
|
|
/>
|
|
|
|
<!-- v1/v2c Community String -->
|
|
<%= if @form[:snmp_version].value in ["1", "2c"] do %>
|
|
<.input
|
|
field={@form[:snmp_community]}
|
|
type="text"
|
|
label="SNMP Community String"
|
|
placeholder="e.g., public"
|
|
/>
|
|
<% end %>
|
|
|
|
<!-- v3 Credentials -->
|
|
<%= if @form[:snmp_version].value == "3" do %>
|
|
<.input
|
|
field={@form[:snmpv3_security_level]}
|
|
type="select"
|
|
label="Security Level"
|
|
prompt="Select security level"
|
|
options={[
|
|
{"No Auth, No Priv", "noAuthNoPriv"},
|
|
{"Auth, No Priv", "authNoPriv"},
|
|
{"Auth, Priv", "authPriv"}
|
|
]}
|
|
/>
|
|
|
|
<.input
|
|
field={@form[:snmpv3_username]}
|
|
type="text"
|
|
label="Username"
|
|
placeholder="e.g., snmpuser"
|
|
/>
|
|
|
|
<!-- Auth fields (shown for authNoPriv and authPriv) -->
|
|
<%= if @form[:snmpv3_security_level].value in ["authNoPriv", "authPriv"] do %>
|
|
<.input
|
|
field={@form[:snmpv3_auth_protocol]}
|
|
type="select"
|
|
label="Auth Protocol"
|
|
prompt="Select protocol"
|
|
options={[
|
|
{"SHA-256 (recommended)", "SHA-256"},
|
|
{"SHA-512", "SHA-512"},
|
|
{"SHA-384", "SHA-384"},
|
|
{"SHA-224", "SHA-224"},
|
|
{"SHA (SHA-1)", "SHA"},
|
|
{"MD5", "MD5"}
|
|
]}
|
|
/>
|
|
|
|
<.input
|
|
field={@form[:snmpv3_auth_password]}
|
|
type="password"
|
|
label="Auth Password"
|
|
placeholder="Min 8 characters"
|
|
autocomplete="off"
|
|
/>
|
|
<% end %>
|
|
|
|
<!-- Priv fields (shown only for authPriv) -->
|
|
<%= if @form[:snmpv3_security_level].value == "authPriv" do %>
|
|
<.input
|
|
field={@form[:snmpv3_priv_protocol]}
|
|
type="select"
|
|
label="Privacy Protocol"
|
|
prompt="Select protocol"
|
|
options={[
|
|
{"AES-128 (recommended)", "AES"},
|
|
{"AES-256", "AES-256"},
|
|
{"AES-192", "AES-192"},
|
|
{"AES-256-C", "AES-256-C"},
|
|
{"DES (legacy)", "DES"}
|
|
]}
|
|
/>
|
|
|
|
<.input
|
|
field={@form[:snmpv3_priv_password]}
|
|
type="password"
|
|
label="Privacy Password"
|
|
placeholder="Min 8 characters"
|
|
autocomplete="off"
|
|
/>
|
|
<% end %>
|
|
<% end %>
|
|
|
|
<.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">
|
|
Force Apply to All Devices
|
|
</h3>
|
|
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
|
|
This will override SNMP settings for ALL devices across all sites in this organization.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
phx-click="apply_snmp_to_all"
|
|
data-confirm="This will replace SNMP settings for ALL devices in this organization. Are you sure?"
|
|
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-red-500 dark:bg-red-500 dark:shadow-none dark:hover:bg-red-400"
|
|
>
|
|
<.icon name="hero-arrow-path" class="h-4 w-4 inline" />
|
|
Apply SNMP Config to All Devices
|
|
</button>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<%= if @current_scope.user.is_superuser do %>
|
|
<!-- MikroTik API Configuration Section -->
|
|
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-10 px-4 py-16 sm:px-6 md:grid-cols-3 lg:px-8">
|
|
<div>
|
|
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
|
|
MikroTik API Configuration
|
|
</h2>
|
|
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
|
|
Set default MikroTik RouterOS API credentials for all devices in this organization. Only applies to devices detected as MikroTik.
|
|
</p>
|
|
<div class="mt-3 rounded-md bg-blue-50 dark:bg-blue-900/20 p-3">
|
|
<p class="text-xs text-blue-700 dark:text-blue-300">
|
|
<.icon name="hero-beaker" class="h-4 w-4 inline" />
|
|
<strong>Experimental Feature:</strong>
|
|
MikroTik API integration is under active development.
|
|
</p>
|
|
</div>
|
|
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400 italic">
|
|
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
|
|
Hierarchy: Device > Site > Organization
|
|
</p>
|
|
</div>
|
|
|
|
<div class="md:col-span-2">
|
|
<.input
|
|
field={@form[:mikrotik_enabled]}
|
|
type="checkbox"
|
|
label="Enable MikroTik API"
|
|
/>
|
|
|
|
<div class={"#{unless @form[:mikrotik_enabled].value, do: "hidden"}"}>
|
|
<.input
|
|
field={@form[:mikrotik_username]}
|
|
type="text"
|
|
label="Username"
|
|
placeholder="e.g., admin"
|
|
autocomplete="off"
|
|
/>
|
|
</div>
|
|
|
|
<div class={"#{unless @form[:mikrotik_enabled].value, do: "hidden"}"}>
|
|
<.input
|
|
field={@form[:mikrotik_password]}
|
|
type="password"
|
|
label="Password"
|
|
placeholder="RouterOS API password"
|
|
autocomplete="off"
|
|
/>
|
|
</div>
|
|
|
|
<div class={"#{unless @form[:mikrotik_enabled].value, do: "hidden"}"}>
|
|
<.input
|
|
field={@form[:mikrotik_port]}
|
|
type="number"
|
|
label="API Port"
|
|
placeholder="8729"
|
|
autocomplete="off"
|
|
/>
|
|
</div>
|
|
|
|
<div class={"#{unless @form[:mikrotik_enabled].value, do: "hidden"}"}>
|
|
<.input
|
|
field={@form[:mikrotik_use_ssl]}
|
|
type="checkbox"
|
|
label="Use SSL (API-SSL)"
|
|
/>
|
|
</div>
|
|
|
|
<div class={"#{unless @form[:mikrotik_enabled].value, do: "hidden"} #{if @form[:mikrotik_use_ssl].value != false, do: "hidden"}"}>
|
|
<div class="rounded-md bg-red-50 dark:bg-red-900/20 p-4">
|
|
<p class="text-sm text-red-700 dark:text-red-300">
|
|
<strong>Critical Security Warning:</strong>
|
|
Plain API (port 8728) sends credentials unencrypted over the network. This setting is
|
|
<strong>blocked for devices using cloud pollers</strong>
|
|
and should only be used with local agents on trusted networks.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class={"#{unless @form[:mikrotik_enabled].value, do: "hidden"} #{if @form[:mikrotik_use_ssl].value == false, do: "hidden"}"}>
|
|
<div class="rounded-md bg-yellow-50 dark:bg-yellow-900/20 p-4">
|
|
<p class="text-sm text-yellow-700 dark:text-yellow-300">
|
|
<strong>Security Warning:</strong>
|
|
Plain API (port 8728) sends credentials unencrypted. Use SSL (port 8729) whenever possible.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<!-- Default Agent Section -->
|
|
<%= if @available_agents != [] do %>
|
|
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-10 px-4 py-16 sm:px-6 md:grid-cols-3 lg:px-8">
|
|
<div>
|
|
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
|
|
Default Agent
|
|
</h2>
|
|
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
|
|
Select a default agent for SNMP polling. This will be used for all devices in this organization unless overridden at the site or device level.
|
|
</p>
|
|
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400 italic">
|
|
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
|
|
Hierarchy: Device > Site > Organization
|
|
</p>
|
|
</div>
|
|
|
|
<div class="md:col-span-2">
|
|
<.input
|
|
field={@form[:default_agent_token_id]}
|
|
type="select"
|
|
label="Default Remote Agent"
|
|
prompt="No default agent - cloud polling"
|
|
options={Enum.map(@available_agents, &{&1.name, &1.id})}
|
|
/>
|
|
|
|
<div class="rounded-md bg-gray-50 dark:bg-gray-800/50 p-4">
|
|
<p class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Current Device Assignment:
|
|
</p>
|
|
<div class="space-y-1 text-sm text-gray-600 dark:text-gray-400">
|
|
<div class="flex items-center gap-2">
|
|
<.icon name="hero-server" class="h-4 w-4" />
|
|
<span>
|
|
<strong>{@assignment_breakdown.direct}</strong> device-level override
|
|
</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<.icon name="hero-building-office" class="h-4 w-4" />
|
|
<span>
|
|
<strong>{@assignment_breakdown.site}</strong> inherited from site
|
|
</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<.icon name="hero-building-office-2" class="h-4 w-4" />
|
|
<span>
|
|
<strong>{@assignment_breakdown.organization}</strong>
|
|
inheriting organization default
|
|
</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<.icon name="hero-cloud" class="h-4 w-4" />
|
|
<span>
|
|
<strong>{@assignment_breakdown.cloud}</strong> cloud polling (no agent)
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<%= if @organization.default_agent_token_id 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">
|
|
Force Apply to All Devices
|
|
</h3>
|
|
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
|
|
This will assign the default agent to ALL devices across all sites in this organization.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
phx-click="apply_agent_to_all"
|
|
data-confirm="This will replace agent assignments for ALL devices in this organization. Are you sure?"
|
|
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-red-500 dark:bg-red-500 dark:shadow-none dark:hover:bg-red-400"
|
|
>
|
|
<.icon name="hero-arrow-path" class="h-4 w-4 inline" />
|
|
Apply Default Agent to All Devices
|
|
</button>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<% else %>
|
|
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-10 px-4 py-16 sm:px-6 md:grid-cols-3 lg:px-8">
|
|
<div>
|
|
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
|
|
Default Agent
|
|
</h2>
|
|
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
|
|
No agents configured yet.
|
|
<.link
|
|
navigate={~p"/agents"}
|
|
class="text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300"
|
|
>
|
|
Create an agent
|
|
</.link>
|
|
to enable remote polling.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="md:col-span-2">
|
|
<!-- Empty space for layout consistency -->
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
|
|
<!-- Save Button at Bottom -->
|
|
<div class="flex items-center justify-end gap-x-6 border-t border-gray-200 px-4 py-4 sm:px-6 lg:px-8 dark:border-white/10">
|
|
<.link
|
|
navigate={~p"/orgs/#{@organization.slug}"}
|
|
class="text-sm font-semibold text-gray-900 dark:text-white"
|
|
>
|
|
Cancel
|
|
</.link>
|
|
<button
|
|
type="submit"
|
|
phx-disable-with="Saving..."
|
|
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:shadow-none dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
|
|
>
|
|
Save Settings
|
|
</button>
|
|
</div>
|
|
</.form>
|
|
</Layouts.authenticated>
|