- Replace gray->cool-steel, blue/indigo->cerulean, red->sweet-salmon, yellow/amber->wheat - Dark sidebar: sidebar/footer use cool-steel-800 bg, text lightened for contrast - ~11,600 color replacements across ~99 files - Update tests for new color class names
213 lines
8.6 KiB
Text
213 lines
8.6 KiB
Text
<Layouts.admin flash={@flash} timezone={@timezone}>
|
|
<div class="space-y-6">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-cool-steel-900 dark:text-white">
|
|
{t("IP Access Control")}
|
|
</h1>
|
|
<p class="text-cool-steel-600 dark:text-cool-steel-400">
|
|
{t("Manage IP allowlist and view denied IPs")}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="border-b border-cool-steel-200 dark:border-white/10">
|
|
<nav class="-mb-px flex space-x-8">
|
|
<button
|
|
id="security-tab-whitelist"
|
|
phx-click="change_tab"
|
|
phx-value-tab="whitelist"
|
|
class={[
|
|
"border-b-2 py-4 px-1 text-sm font-medium",
|
|
if(@current_tab == "whitelist",
|
|
do: "border-cerulean-500 text-cerulean-600 dark:text-cerulean-400",
|
|
else:
|
|
"border-transparent text-cool-steel-500 hover:border-cool-steel-300 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:text-cool-steel-300"
|
|
)
|
|
]}
|
|
>
|
|
{t("Allowlist")}
|
|
</button>
|
|
|
|
<button
|
|
id="security-tab-blocked"
|
|
phx-click="change_tab"
|
|
phx-value-tab="blocked"
|
|
class={[
|
|
"border-b-2 py-4 px-1 text-sm font-medium",
|
|
if(@current_tab == "blocked",
|
|
do: "border-cerulean-500 text-cerulean-600 dark:text-cerulean-400",
|
|
else:
|
|
"border-transparent text-cool-steel-500 hover:border-cool-steel-300 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:text-cool-steel-300"
|
|
)
|
|
]}
|
|
>
|
|
{t("Denylist")}
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Whitelist Tab -->
|
|
<%= if @current_tab == "whitelist" do %>
|
|
<div class="space-y-4">
|
|
<div class="flex justify-between items-center">
|
|
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white">
|
|
{t("Allowed IPs and CIDR Ranges")}
|
|
</h2>
|
|
<.button id="show-whitelist-form" phx-click="show_whitelist_form">
|
|
<.icon name="hero-plus" class="w-4 h-4 mr-1" /> Add to Allowlist
|
|
</.button>
|
|
</div>
|
|
|
|
<%= if @show_whitelist_form do %>
|
|
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-6">
|
|
<.form
|
|
for={@whitelist_form}
|
|
phx-submit="add_whitelist"
|
|
class="space-y-4"
|
|
>
|
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<.input
|
|
field={@whitelist_form[:ip_or_cidr]}
|
|
type="text"
|
|
label={t("IP Address or CIDR")}
|
|
placeholder="192.168.1.100 or 10.0.0.0/8"
|
|
required
|
|
/>
|
|
|
|
<.input
|
|
field={@whitelist_form[:description]}
|
|
type="text"
|
|
label={t("Description")}
|
|
placeholder={t("Office network")}
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex gap-2">
|
|
<.button type="submit">{t("Add to Allowlist")}</.button>
|
|
<.button id="hide-whitelist-form" type="button" phx-click="hide_whitelist_form">
|
|
{t("Cancel")}
|
|
</.button>
|
|
</div>
|
|
</.form>
|
|
</div>
|
|
<% end %>
|
|
|
|
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10">
|
|
<%= if Enum.empty?(@whitelist) do %>
|
|
<div class="p-8 text-center text-cool-steel-500 dark:text-cool-steel-400">
|
|
{t("No allowed IPs or CIDR ranges")}
|
|
</div>
|
|
<% else %>
|
|
<.table id="whitelist" rows={@whitelist}>
|
|
<:col :let={entry} label={t("IP/CIDR")}>{entry.ip_or_cidr}</:col>
|
|
<:col :let={entry} label={t("Description")}>{entry.description}</:col>
|
|
<:col :let={entry} label={t("Added By")}>
|
|
<%= if entry.added_by do %>
|
|
{entry.added_by.email}
|
|
<% else %>
|
|
<span class="text-cool-steel-400">Unknown</span>
|
|
<% end %>
|
|
</:col>
|
|
<:col :let={entry} label={t("Added")}>
|
|
{ToweropsWeb.TimeHelpers.format_date(entry.inserted_at, @timezone)}
|
|
</:col>
|
|
<:col :let={entry} label="">
|
|
<.button
|
|
phx-click="remove_whitelist"
|
|
phx-value-id={entry.id}
|
|
data-confirm={t("Remove this IP/CIDR from the allowlist?")}
|
|
variant="danger"
|
|
>
|
|
{t("Remove")}
|
|
</.button>
|
|
</:col>
|
|
</.table>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
<!-- Blocked IPs Tab -->
|
|
<%= if @current_tab == "blocked" do %>
|
|
<div class="space-y-4">
|
|
<div class="flex justify-between items-center">
|
|
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white">
|
|
{t("Denied IP Addresses")}
|
|
</h2>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<label class="text-sm text-cool-steel-600 dark:text-cool-steel-400">Filter:</label>
|
|
<select
|
|
phx-change="change_filter"
|
|
name="filter"
|
|
class="rounded-md border-cool-steel-300 dark:border-white/10 dark:bg-cool-steel-900 text-sm"
|
|
>
|
|
<option value="all" selected={@filter == "all"}>All Bans</option>
|
|
<option value="permanent" selected={@filter == "permanent"}>Permanent Only</option>
|
|
<option value="temporary" selected={@filter == "temporary"}>Temporary Only</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10">
|
|
<%= if Enum.empty?(@blocked_ips) do %>
|
|
<div class="p-8 text-center text-cool-steel-500 dark:text-cool-steel-400">
|
|
{t("No denied IPs")}
|
|
</div>
|
|
<% else %>
|
|
<.table id="blocked_ips" rows={@blocked_ips}>
|
|
<:col :let={block} label={t("IP Address")}>{block.ip_address}</:col>
|
|
<:col :let={block} label={t("Offense Count")}>
|
|
<span class={[
|
|
"inline-flex items-center px-2 py-1 rounded-full text-xs font-medium",
|
|
case block.offense_count do
|
|
1 ->
|
|
"bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400"
|
|
|
|
2 ->
|
|
"bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400"
|
|
|
|
_ ->
|
|
"bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"
|
|
end
|
|
]}>
|
|
{block.offense_count}
|
|
</span>
|
|
</:col>
|
|
<:col :let={block} label={t("Status")}>
|
|
<%= if Towerops.Security.IpBlock.permanent?(block) do %>
|
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400">
|
|
{t("Permanent")}
|
|
</span>
|
|
<% else %>
|
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400">
|
|
{Towerops.Security.IpBlock.ban_duration(block)}
|
|
</span>
|
|
<% end %>
|
|
</:col>
|
|
<:col :let={block} label={t("Last Violation")}>
|
|
{ToweropsWeb.TimeHelpers.format_time_ago(block.last_violation_at)}
|
|
</:col>
|
|
<:col :let={block} label={t("Reason")}>
|
|
<div class="text-sm text-cool-steel-600 dark:text-cool-steel-400 max-w-xs truncate">
|
|
{block.reason}
|
|
</div>
|
|
</:col>
|
|
<:col :let={block} label="">
|
|
<button
|
|
phx-click="unblock_ip"
|
|
phx-value-ip={block.ip_address}
|
|
data-confirm={
|
|
t("Unblock this IP address? This will remove all offense history.")
|
|
}
|
|
class="text-sm text-cerulean-600 dark:text-cerulean-400 hover:text-cerulean-800 dark:hover:text-cerulean-300"
|
|
>
|
|
{t("Unblock")}
|
|
</button>
|
|
</:col>
|
|
</.table>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</Layouts.admin>
|