towerops/lib/towerops_web/live/user_settings_live.html.heex

2066 lines
97 KiB
Text

<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
>
<div class="pb-5">
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
{t("Account Settings")}
</h1>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t("Manage your account email address, password, and security settings")}
</p>
</div>
<div class="border-b border-gray-200 dark:border-white/10">
<nav class="-mb-px flex space-x-8 overflow-x-auto">
<.link
patch={~p"/users/settings?tab=personal"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if(@active_tab == "personal",
do: "border-indigo-500 text-indigo-600 dark:text-indigo-400 font-semibold",
else:
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
)
]}
>
<span class="flex items-center gap-1.5">
<.icon name="hero-user" class="h-4 w-4" /> {t("Personal")}
</span>
</.link>
<.link
patch={~p"/users/settings?tab=account"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if(@active_tab == "account",
do: "border-indigo-500 text-indigo-600 dark:text-indigo-400 font-semibold",
else:
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
)
]}
>
<span class="flex items-center gap-1.5">
<.icon name="hero-envelope" class="h-4 w-4" /> {t("Account")}
</span>
</.link>
<.link
patch={~p"/users/settings?tab=sessions"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if(@active_tab == "sessions",
do: "border-indigo-500 text-indigo-600 dark:text-indigo-400 font-semibold",
else:
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
)
]}
>
<span class="flex items-center gap-1.5">
<.icon name="hero-computer-desktop" class="h-4 w-4" /> {t("Sessions")}
</span>
</.link>
<.link
patch={~p"/users/settings?tab=api"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if(@active_tab == "api",
do: "border-indigo-500 text-indigo-600 dark:text-indigo-400 font-semibold",
else:
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
)
]}
>
<span class="flex items-center gap-1.5">
<.icon name="hero-code-bracket" class="h-4 w-4" /> {t("API")}
</span>
</.link>
<.link
patch={~p"/users/settings?tab=notifications"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if(@active_tab == "notifications",
do: "border-indigo-500 text-indigo-600 dark:text-indigo-400 font-semibold",
else:
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
)
]}
>
<span class="flex items-center gap-1.5">
<.icon name="hero-bell" class="h-4 w-4" /> {t("Notifications")}
</span>
</.link>
<.link
patch={~p"/users/settings?tab=security"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if(@active_tab == "security",
do: "border-indigo-500 text-indigo-600 dark:text-indigo-400 font-semibold",
else:
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
)
]}
>
<span class="flex items-center gap-1.5">
<.icon name="hero-shield-check" class="h-4 w-4" /> {t("Security")}
</span>
</.link>
<.link
navigate={~p"/users/my-data"}
class="whitespace-nowrap py-4 px-1 border-b-2 border-transparent text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 transition-colors"
>
<span class="flex items-center gap-1.5">
<.icon name="hero-arrow-down-tray" class="h-4 w-4" /> {t("My Data")}
</span>
</.link>
</nav>
</div>
<div class="divide-y divide-gray-200 dark:divide-white/10">
<!-- Personal Information Section -->
<%= if @active_tab == "personal" 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">
{t("Personal Information")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Update your name and timezone preferences.")}
</p>
</div>
<.form
for={@profile_form}
phx-submit="update_profile"
id="update_profile"
class="md:col-span-2"
>
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:max-w-xl sm:grid-cols-6">
<div class="sm:col-span-3">
<label
for="first_name"
class="block text-sm/6 font-medium text-gray-900 dark:text-white"
>
{t("First name")}
</label>
<div class="mt-2">
<input
type="text"
name={@profile_form[:first_name].name}
id="first_name"
value={@profile_form[:first_name].value}
autocomplete="given-name"
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
/>
</div>
</div>
<div class="sm:col-span-3">
<label
for="last_name"
class="block text-sm/6 font-medium text-gray-900 dark:text-white"
>
{t("Last name")}
</label>
<div class="mt-2">
<input
type="text"
name={@profile_form[:last_name].name}
id="last_name"
value={@profile_form[:last_name].value}
autocomplete="family-name"
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
/>
</div>
</div>
<div class="col-span-full">
<label
for="timezone"
class="block text-sm/6 font-medium text-gray-900 dark:text-white"
>
{t("Timezone")}
</label>
<div class="mt-2 grid grid-cols-1">
<select
name={@profile_form[:timezone].name}
id="timezone"
class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pr-8 pl-3 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:*:bg-gray-800 dark:focus:outline-indigo-500"
>
<option value="UTC" selected={@profile_form[:timezone].value == "UTC"}>
{t("UTC (Coordinated Universal Time)")}
</option>
<option
value="America/New_York"
selected={@profile_form[:timezone].value == "America/New_York"}
>
{t("Eastern Time (US & Canada)")}
</option>
<option
value="America/Chicago"
selected={@profile_form[:timezone].value == "America/Chicago"}
>
{t("Central Time (US & Canada)")}
</option>
<option
value="America/Denver"
selected={@profile_form[:timezone].value == "America/Denver"}
>
{t("Mountain Time (US & Canada)")}
</option>
<option
value="America/Los_Angeles"
selected={@profile_form[:timezone].value == "America/Los_Angeles"}
>
{t("Pacific Time (US & Canada)")}
</option>
<option
value="Europe/London"
selected={@profile_form[:timezone].value == "Europe/London"}
>
{t("London")}
</option>
<option
value="Europe/Paris"
selected={@profile_form[:timezone].value == "Europe/Paris"}
>
{t("Paris")}
</option>
<option
value="Europe/Berlin"
selected={@profile_form[:timezone].value == "Europe/Berlin"}
>
{t("Berlin")}
</option>
<option
value="Asia/Tokyo"
selected={@profile_form[:timezone].value == "Asia/Tokyo"}
>
{t("Tokyo")}
</option>
<option
value="Asia/Shanghai"
selected={@profile_form[:timezone].value == "Asia/Shanghai"}
>
{t("Shanghai")}
</option>
<option
value="Australia/Sydney"
selected={@profile_form[:timezone].value == "Australia/Sydney"}
>
{t("Sydney")}
</option>
</select>
<svg
viewBox="0 0 16 16"
fill="currentColor"
data-slot="icon"
aria-hidden="true"
class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-gray-400 sm:size-4"
>
<path
d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
fill-rule="evenodd"
/>
</svg>
</div>
</div>
<div class="col-span-full">
<label
for="time_format"
class="block text-sm/6 font-medium text-gray-900 dark:text-white"
>
{t("Time Format")}
</label>
<div class="mt-2 grid grid-cols-1">
<select
name={@profile_form[:time_format].name}
id="time_format"
class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pr-8 pl-3 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:*:bg-gray-800 dark:focus:outline-indigo-500"
>
<option value="24h" selected={@profile_form[:time_format].value == "24h"}>
24-hour (14:30)
</option>
<option value="12h" selected={@profile_form[:time_format].value == "12h"}>
12-hour (2:30 PM)
</option>
</select>
<svg
viewBox="0 0 16 16"
fill="currentColor"
data-slot="icon"
aria-hidden="true"
class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-gray-400 sm:size-4"
>
<path
d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
fill-rule="evenodd"
/>
</svg>
</div>
</div>
<div class="col-span-full">
<label
for="language"
class="block text-sm/6 font-medium text-gray-900 dark:text-white"
>
{t("Language")}
</label>
<div class="mt-2 grid grid-cols-1">
<select
name={@profile_form[:language].name}
id="language"
class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pr-8 pl-3 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:*:bg-gray-800 dark:focus:outline-indigo-500"
>
<option value="en" selected={@profile_form[:language].value == "en"}>
English
</option>
<option value="es" selected={@profile_form[:language].value == "es"}>
Español
</option>
</select>
<svg
viewBox="0 0 16 16"
fill="currentColor"
data-slot="icon"
aria-hidden="true"
class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-gray-400 sm:size-4"
>
<path
fill-rule="evenodd"
d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
</svg>
</div>
</div>
<div :if={@organizations != []} class="col-span-full">
<label
for="default_organization_id"
class="block text-sm/6 font-medium text-gray-900 dark:text-white"
>
{t("Default Organization")}
</label>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Choose which organization you see by default when logging in or navigating to pages without an organization context."
)}
</p>
<div class="mt-2 grid grid-cols-1">
<select
name={@profile_form[:default_organization_id].name}
id="default_organization_id"
class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pr-8 pl-3 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:*:bg-gray-800 dark:focus:outline-indigo-500"
>
<option value="">None (use first organization)</option>
<%= for org <- @organizations do %>
<option
value={org.id}
selected={@profile_form[:default_organization_id].value == org.id}
>
{org.name}
</option>
<% end %>
</select>
<svg
viewBox="0 0 16 16"
fill="currentColor"
data-slot="icon"
aria-hidden="true"
class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-gray-400 sm:size-4"
>
<path
d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
fill-rule="evenodd"
/>
</svg>
</div>
</div>
</div>
<div class="mt-8 flex">
<button
type="submit"
phx-disable-with={t("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"
>
{t("Save")}
</button>
</div>
</.form>
</div>
<% end %>
<!-- Appearance Section -->
<%= if @active_tab == "personal" 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">
{t("Appearance")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Choose your preferred interface theme.")}
</p>
</div>
<div class="md:col-span-2">
<div
id="theme-selector"
phx-hook="ThemeSelector"
phx-update="ignore"
class="flex gap-3"
>
<button
type="button"
aria-pressed="false"
data-phx-theme="system"
phx-click={JS.dispatch("phx:set-theme")}
class="flex flex-1 flex-col items-center gap-2 rounded-lg border border-gray-200 px-4 py-5 text-sm font-medium text-gray-700 transition-colors hover:border-indigo-300 hover:bg-indigo-50 dark:border-white/10 dark:text-gray-300 dark:hover:border-indigo-500 dark:hover:bg-indigo-900/20"
>
<.icon name="hero-computer-desktop" class="h-6 w-6" />
{t("Auto")}
<span class="text-xs font-normal text-gray-400 dark:text-gray-500">
{t("System default")}
</span>
</button>
<button
type="button"
aria-pressed="false"
data-phx-theme="light"
phx-click={JS.dispatch("phx:set-theme")}
class="flex flex-1 flex-col items-center gap-2 rounded-lg border border-gray-200 px-4 py-5 text-sm font-medium text-gray-700 transition-colors hover:border-indigo-300 hover:bg-indigo-50 dark:border-white/10 dark:text-gray-300 dark:hover:border-indigo-500 dark:hover:bg-indigo-900/20"
>
<.icon name="hero-sun" class="h-6 w-6" />
{t("Light")}
<span class="text-xs font-normal text-gray-400 dark:text-gray-500">
{t("Always light")}
</span>
</button>
<button
type="button"
aria-pressed="false"
data-phx-theme="dark"
phx-click={JS.dispatch("phx:set-theme")}
class="flex flex-1 flex-col items-center gap-2 rounded-lg border border-gray-200 px-4 py-5 text-sm font-medium text-gray-700 transition-colors hover:border-indigo-300 hover:bg-indigo-50 dark:border-white/10 dark:text-gray-300 dark:hover:border-indigo-500 dark:hover:bg-indigo-900/20"
>
<.icon name="hero-moon" class="h-6 w-6" />
{t("Dark")}
<span class="text-xs font-normal text-gray-400 dark:text-gray-500">
{t("Always dark")}
</span>
</button>
</div>
</div>
</div>
<% end %>
<!-- Account Tab -->
<%= if @active_tab == "account" do %>
<!-- Email Address 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">
{t("Email Address")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Update your email address. You'll receive a confirmation link to verify the new address."
)}
</p>
</div>
<.form
for={@email_form}
phx-submit="update_email"
id="update_email"
class="md:col-span-2"
>
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:max-w-xl sm:grid-cols-6">
<div class="col-span-full">
<label for="email" class="block text-sm/6 font-medium text-gray-900 dark:text-white">
{t("Email address")}
</label>
<div class="mt-2">
<input
type="email"
name={@email_form[:email].name}
id="email"
value={@email_form[:email].value}
autocomplete="email"
required
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
/>
</div>
</div>
</div>
<div class="mt-8 flex">
<button
type="submit"
phx-disable-with={t("Changing...")}
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"
>
{t("Change Email")}
</button>
</div>
</.form>
</div>
<!-- Password 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">
{t("Change Password")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Update your password. You'll be logged out and need to sign in again with your new password."
)}
</p>
</div>
<.form
for={@password_form}
phx-submit="update_password"
id="update_password"
class="md:col-span-2"
>
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:max-w-xl sm:grid-cols-6">
<div class="col-span-full">
<label
for="new-password"
class="block text-sm/6 font-medium text-gray-900 dark:text-white"
>
{t("New password")}
</label>
<div class="mt-2">
<input
type="password"
name={@password_form[:password].name}
id="new-password"
autocomplete="new-password"
phx-blur="check_password_breach"
required
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
/>
</div>
<%= if @password_breach_count && @password_breach_count > 0 do %>
<div class="mt-2 rounded-md bg-yellow-50 dark:bg-yellow-900/20 p-4">
<div class="flex">
<div class="flex-shrink-0">
<.icon name="hero-exclamation-triangle" class="h-5 w-5 text-yellow-400" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
{t("Password found in data breaches")}
</h3>
<div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
<p>
This password has appeared {@password_breach_count}
{if @password_breach_count == 1, do: "time", else: "times"} in known data breaches. We strongly recommend choosing a different,
more unique password to protect your account.
</p>
</div>
</div>
</div>
</div>
<% end %>
</div>
<div class="col-span-full">
<label
for="confirm-password"
class="block text-sm/6 font-medium text-gray-900 dark:text-white"
>
{t("Confirm password")}
</label>
<div class="mt-2">
<input
type="password"
name={@password_form[:password_confirmation].name}
id="confirm-password"
autocomplete="new-password"
required
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
/>
</div>
</div>
</div>
<div class="mt-8 flex">
<button
type="submit"
phx-disable-with={t("Changing...")}
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"
>
{t("Save Password")}
</button>
</div>
</.form>
</div>
<!-- Delete Account Danger Zone -->
<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 border-t border-red-200 dark:border-red-900/50">
<div>
<h2 class="text-base/7 font-semibold text-red-600 dark:text-red-400">
{t("Delete Account")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Permanently delete your account and all associated data. This action cannot be undone."
)}
</p>
</div>
<div class="md:col-span-2">
<div class="rounded-lg border border-red-200 bg-red-50 p-6 dark:border-red-900/50 dark:bg-red-950/20">
<div class="flex items-start gap-3">
<.icon
name="hero-exclamation-triangle"
class="h-6 w-6 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5"
/>
<div>
<h3 class="text-sm font-semibold text-red-800 dark:text-red-300">
{t("Danger Zone")}
</h3>
<p class="mt-1 text-sm text-red-700 dark:text-red-400">
{t(
"Once you delete your account, all of your data will be permanently removed. Before deleting, you can export your data from the My Data page."
)}
</p>
<div class="mt-4">
<button
type="button"
phx-click="show_delete_modal"
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-red-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 dark:bg-red-500 dark:shadow-none dark:hover:bg-red-400"
>
{t("Delete My Account")}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<% end %>
<!-- API Tab -->
<%= if @active_tab == "api" do %>
<!-- API Tokens 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">
{t("API Tokens")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Create API tokens to access Towerops programmatically. Tokens are scoped to a specific organization."
)}
</p>
</div>
<div class="md:col-span-2">
<%= if Enum.empty?(@api_tokens) do %>
<div class="text-center rounded-lg border border-dashed border-gray-300 px-6 py-10 dark:border-gray-700">
<.icon name="hero-code-bracket" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
{t("No API tokens")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("Get started by creating a new API token.")}
</p>
<div class="mt-6">
<button
id="show-add-token-modal-empty"
type="button"
phx-click="show_add_token_modal"
class="inline-flex items-center gap-x-1.5 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:hover:bg-indigo-400"
>
<.icon name="hero-plus" class="-ml-0.5 h-5 w-5" /> New Token
</button>
</div>
</div>
<% else %>
<ul role="list" class="divide-y divide-gray-200 dark:divide-white/10">
<%= for token <- @api_tokens do %>
<li class="flex items-center justify-between gap-x-6 py-5">
<div class="min-w-0">
<div class="flex items-start gap-x-3">
<p class="text-sm/6 font-semibold text-gray-900 dark:text-white">
{token.name}
</p>
</div>
<div class="mt-1 flex items-center gap-x-2 text-xs/5 text-gray-500 dark:text-gray-400">
<p class="whitespace-nowrap">{token.organization.name}</p>
<svg viewBox="0 0 2 2" class="h-0.5 w-0.5 fill-current">
<circle r="1" cx="1" cy="1" />
</svg>
<p class="whitespace-nowrap">
<%= if token.last_used_at do %>
Last used {ToweropsWeb.TimeHelpers.format_date(
token.last_used_at,
@current_scope.timezone
)}
<% else %>
{t("Never used")}
<% end %>
</p>
</div>
</div>
<button
type="button"
phx-click="delete_api_token"
phx-value-token-id={token.id}
data-confirm={
t(
"Are you sure you want to delete this API token? This action cannot be undone."
)
}
class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-xs inset-ring-1 inset-ring-gray-300 hover:bg-gray-100 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/10 dark:hover:bg-white/20"
>
{t("Delete")}
</button>
</li>
<% end %>
</ul>
<div class="mt-6 flex">
<button
type="button"
phx-click="show_add_token_modal"
class="inline-flex items-center gap-x-1.5 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:hover:bg-indigo-400"
>
<.icon name="hero-plus" class="-ml-0.5 h-5 w-5" /> New Token
</button>
</div>
<% end %>
</div>
</div>
<% end %>
<!-- Notifications Tab -->
<%= if @active_tab == "notifications" do %>
<!-- Mobile Devices 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">
{t("Mobile Devices")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Manage mobile devices that receive push notifications for alerts.")}
</p>
</div>
<div class="md:col-span-2">
<%= if Enum.empty?(@mobile_sessions) do %>
<div class="text-center rounded-lg border border-dashed border-gray-300 px-6 py-10 dark:border-gray-700">
<.icon name="hero-device-phone-mobile" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
{t("No mobile devices")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("Add a mobile device to receive push notifications for alerts.")}
</p>
<div class="mt-6">
<.link
navigate={~p"/mobile/qr-login"}
class="inline-flex items-center gap-x-1.5 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:hover:bg-indigo-400"
>
<.icon name="hero-qr-code" class="-ml-0.5 h-5 w-5" /> Add Device
</.link>
</div>
</div>
<% else %>
<ul role="list" class="divide-y divide-gray-200 dark:divide-white/10">
<%= for session <- @mobile_sessions do %>
<li class="flex items-center justify-between gap-x-6 py-5">
<div class="min-w-0 flex-1">
<div class="flex items-start gap-x-3">
<p class="text-sm/6 font-semibold text-gray-900 dark:text-white">
{session.device_name || "Unknown Device"}
</p>
<%= if session.alerts_enabled do %>
<span class="inline-flex items-center gap-x-0.5 rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 inset-ring-1 inset-ring-green-600/20 dark:bg-green-500/10 dark:text-green-400 dark:inset-ring-green-500/20">
<.icon name="hero-bell" class="h-3 w-3" /> Alerts On
</span>
<% else %>
<span class="inline-flex items-center gap-x-0.5 rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 inset-ring-1 inset-ring-gray-500/10 dark:bg-gray-400/10 dark:text-gray-400 dark:inset-ring-gray-400/20">
<.icon name="hero-bell-slash" class="h-3 w-3" /> Alerts Off
</span>
<% end %>
</div>
<div class="mt-1 flex items-center gap-x-2 text-xs/5 text-gray-500 dark:text-gray-400">
<p>{session.device_os} • {session.app_version}</p>
<svg viewBox="0 0 2 2" class="h-0.5 w-0.5 fill-current">
<circle r="1" cx="1" cy="1" />
</svg>
<p>
Last used {ToweropsWeb.TimeHelpers.format_date(
session.last_used_at,
@current_scope.timezone
)}
</p>
</div>
</div>
<div class="flex flex-none items-center gap-x-4">
<button
type="button"
phx-click="toggle_device_alerts"
phx-value-session-id={session.id}
phx-value-enabled={if session.alerts_enabled, do: "false", else: "true"}
class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-xs inset-ring-1 inset-ring-gray-300 hover:bg-gray-100 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/10 dark:hover:bg-white/20"
>
{t("Toggle")}
</button>
<button
type="button"
phx-click="revoke_mobile_device"
phx-value-session-id={session.id}
data-confirm={t("Are you sure you want to remove this device?")}
class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-xs inset-ring-1 inset-ring-gray-300 hover:bg-gray-100 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/10 dark:hover:bg-white/20"
>
{t("Remove")}
</button>
</div>
</li>
<% end %>
</ul>
<div class="mt-6 flex">
<.link
navigate={~p"/mobile/qr-login"}
class="inline-flex items-center gap-x-1.5 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:hover:bg-indigo-400"
>
<.icon name="hero-qr-code" class="-ml-0.5 h-5 w-5" /> Add Device
</.link>
</div>
<% end %>
</div>
</div>
<% end %>
<!-- Sessions Tab -->
<%= if @active_tab == "sessions" do %>
<!-- Security Alert Banner -->
<%= if @show_security_alert do %>
<div class="px-4 py-6 sm:px-6 lg:px-8">
<div class="rounded-lg bg-amber-50 p-4 dark:bg-amber-900/20">
<div class="flex">
<div class="flex-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">
{t("Security Alert")}
</h3>
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
<p>
We detected {@failed_login_count} failed login attempts in the last 24 hours. {t(
"Please review your login history below and ensure all activity is authorized."
)}
</p>
</div>
</div>
</div>
</div>
</div>
<% end %>
<!-- Active Sessions 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">
{t("Active Sessions")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Manage browser sessions where you're currently logged in.")}
</p>
</div>
<div class="md:col-span-2">
<%= if Enum.empty?(@browser_sessions) do %>
<div class="text-center rounded-lg border border-dashed border-gray-300 px-6 py-10 dark:border-gray-700">
<.icon name="hero-computer-desktop" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
{t("No active sessions")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("Your current session will appear here after your next login.")}
</p>
</div>
<% else %>
<ul role="list" class="divide-y divide-gray-200 dark:divide-white/10">
<%= for session <- @browser_sessions do %>
<% is_current = current_session?(session, get_current_token_id(assigns)) %>
<li class="flex items-center justify-between gap-x-6 py-5">
<div class="flex items-start gap-x-3 min-w-0 flex-1">
<div class="flex-shrink-0 mt-1">
<%= if session.device_type in ["mobile", "tablet"] do %>
<.icon name="hero-device-phone-mobile" class="h-6 w-6 text-gray-400" />
<% else %>
<.icon name="hero-computer-desktop" class="h-6 w-6 text-gray-400" />
<% end %>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-start gap-x-3">
<p class="text-sm/6 font-semibold text-gray-900 dark:text-white">
{format_browser_info(session)}
</p>
<%= if is_current do %>
<span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 inset-ring-1 inset-ring-green-600/20 dark:bg-green-500/10 dark:text-green-400 dark:inset-ring-green-500/20">
{t("Current Session")}
</span>
<% end %>
</div>
<div class="mt-1 flex flex-col gap-y-1 text-xs/5 text-gray-500 dark:text-gray-400">
<div class="flex items-center gap-x-2">
<.icon name="hero-map-pin" class="h-3 w-3" />
<span>{format_location(session)}</span>
<span class="text-gray-400 dark:text-gray-600">•</span>
<span class="font-mono">{session.ip_address}</span>
</div>
<div class="flex items-center gap-x-2">
<.icon name="hero-clock" class="h-3 w-3" />
<span>
Last active {format_relative_time(session.last_activity_at)}
</span>
</div>
</div>
</div>
</div>
<button
type="button"
phx-click="revoke_session"
phx-value-session-id={session.id}
disabled={is_current}
data-confirm={
unless is_current,
do:
"Are you sure you want to revoke this session? You will be logged out from that device."
}
class={"rounded-md px-2.5 py-1.5 text-sm font-semibold shadow-xs inset-ring-1 " <>
if is_current do
"bg-gray-100 text-gray-400 inset-ring-gray-200 cursor-not-allowed dark:bg-gray-800 dark:text-gray-600 dark:inset-ring-gray-700"
else
"bg-white text-gray-900 inset-ring-gray-300 hover:bg-gray-100 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/10 dark:hover:bg-white/20"
end
}
>
{t("Revoke")}
</button>
</li>
<% end %>
</ul>
<%= if length(@browser_sessions) > 1 do %>
<div class="mt-6 flex">
<button
type="button"
phx-click="show_revoke_all_modal"
class="inline-flex items-center gap-x-1.5 rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-red-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 dark:bg-red-500 dark:hover:bg-red-400"
>
<.icon name="hero-x-mark" class="-ml-0.5 h-5 w-5" /> Revoke All Other Sessions
</button>
</div>
<% end %>
<% end %>
</div>
</div>
<!-- Login History Section -->
<div class="max-w-7xl px-4 py-16 sm:px-6 lg:px-8 border-t border-gray-200 dark:border-white/10">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Login History")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Recent login attempts to your account, including successful and failed attempts."
)}
</p>
</div>
</div>
<div class="mt-8">
<%= if Enum.empty?(@login_history) do %>
<div class="text-center rounded-lg border border-dashed border-gray-300 px-6 py-10 dark:border-gray-700">
<.icon name="hero-clock" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
{t("No login history")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("Your login attempts will appear here.")}
</p>
</div>
<% else %>
<div class="overflow-x-auto shadow ring-1 ring-black ring-opacity-5 dark:ring-white/10 sm:rounded-lg">
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th
scope="col"
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-white sm:pl-6"
>
{t("Date & Time")}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white"
>
{t("Status")}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white"
>
{t("Method")}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white"
>
{t("Location")}
</th>
<th
scope="col"
class="py-3.5 pl-3 pr-4 text-right text-sm font-semibold text-gray-900 dark:text-white sm:pr-6"
>
{t("IP Address")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-800 dark:bg-gray-900">
<%= for attempt <- @login_history do %>
<tr>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm text-gray-900 dark:text-white sm:pl-6">
{format_timestamp_in_timezone(
attempt.inserted_at,
@current_scope.timezone,
@current_scope.time_format
)}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm">
<%= if attempt.success do %>
<span class="inline-flex items-center gap-x-1 rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 inset-ring-1 inset-ring-green-600/20 dark:bg-green-500/10 dark:text-green-400 dark:inset-ring-green-500/20">
<.icon name="hero-check-circle" class="h-3 w-3" /> Success
</span>
<% else %>
<span class="inline-flex items-center gap-x-1 rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 inset-ring-1 inset-ring-red-600/20 dark:bg-red-500/10 dark:text-red-400 dark:inset-ring-red-500/20">
<.icon name="hero-x-circle" class="h-3 w-3" /> Failed
</span>
<% end %>
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
<% {icon, text} = login_method_info(attempt.method) %>
<span class="inline-flex items-center gap-x-1">
<.icon name={icon} class="h-3 w-3" /> {text}
</span>
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
{format_location(attempt)}
</td>
<td class="whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-mono text-gray-500 dark:text-gray-400 sm:pr-6">
{attempt.ip_address}
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<%= if @login_history_pagination.total_pages > 1 do %>
<div class="mt-6 flex items-center justify-between border-t border-gray-200 dark:border-white/10 pt-6">
<!-- Mobile: Previous/Next only -->
<div class="flex flex-1 justify-between sm:hidden">
<.link
:if={@login_history_pagination.page > 1}
patch={
~p"/users/settings?tab=#{@active_tab}&page=#{@login_history_pagination.page - 1}"
}
class="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
{t("Previous")}
</.link>
<.link
:if={@login_history_pagination.page < @login_history_pagination.total_pages}
patch={
~p"/users/settings?tab=#{@active_tab}&page=#{@login_history_pagination.page + 1}"
}
class="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
{t("Next")}
</.link>
</div>
<!-- Desktop: Full pagination with page numbers -->
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700 dark:text-gray-400">
{t("Showing")}
<span class="font-medium">
{(@login_history_pagination.page - 1) * @login_history_pagination.per_page +
1}
</span>
to
<span class="font-medium">
{min(
@login_history_pagination.page * @login_history_pagination.per_page,
@login_history_pagination.total_count
)}
</span>
of <span class="font-medium">{@login_history_pagination.total_count}</span>
results
</p>
</div>
<div>
<nav
class="isolate inline-flex -space-x-px rounded-md shadow-sm"
aria-label={t("Pagination")}
>
<!-- Previous button -->
<.link
:if={@login_history_pagination.page > 1}
patch={
~p"/users/settings?tab=#{@active_tab}&page=#{@login_history_pagination.page - 1}"
}
class="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:ring-white/10 dark:hover:bg-gray-800"
>
<span class="sr-only">Previous</span>
<.icon name="hero-chevron-left" class="h-5 w-5" />
</.link>
<!-- Page numbers with ellipsis -->
<%= for page_num <- pagination_range(@login_history_pagination.page, @login_history_pagination.total_pages) do %>
<%= if page_num == :ellipsis do %>
<span class="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-700 ring-1 ring-inset ring-gray-300 dark:text-gray-400 dark:ring-white/10">
...
</span>
<% else %>
<.link
patch={~p"/users/settings?tab=#{@active_tab}&page=#{page_num}"}
class={[
"relative inline-flex items-center px-4 py-2 text-sm font-semibold ring-1 ring-inset ring-gray-300 dark:ring-white/10",
if @login_history_pagination.page == page_num do
"z-10 bg-blue-600 text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
else
"text-gray-900 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
end
]}
>
{page_num}
</.link>
<% end %>
<% end %>
<!-- Next button -->
<.link
:if={
@login_history_pagination.page < @login_history_pagination.total_pages
}
patch={
~p"/users/settings?tab=#{@active_tab}&page=#{@login_history_pagination.page + 1}"
}
class="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:ring-white/10 dark:hover:bg-gray-800"
>
<span class="sr-only">Next</span>
<.icon name="hero-chevron-right" class="h-5 w-5" />
</.link>
</nav>
</div>
</div>
</div>
<% end %>
<% end %>
</div>
</div>
<% end %>
<!-- Security Tab -->
<%= if @active_tab == "security" do %>
<!-- Authenticator Apps 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">
{t("Authenticator Apps")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Manage TOTP devices for two-factor authentication. You must have at least one device configured."
)}
</p>
</div>
<div class="md:col-span-2">
<%= if Enum.empty?(@totp_devices) do %>
<div class="text-center rounded-lg border border-dashed border-gray-300 px-6 py-10 dark:border-gray-700">
<.icon name="hero-device-phone-mobile" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
{t("No authenticator apps")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("Add your first authenticator app to enable two-factor authentication.")}
</p>
<div class="mt-6">
<button
id="show-add-device-modal"
type="button"
phx-click="show_add_device_modal"
class="inline-flex items-center gap-x-1.5 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:hover:bg-indigo-400"
>
<.icon name="hero-plus" class="-ml-0.5 h-5 w-5" /> Add Device
</button>
</div>
</div>
<% else %>
<ul role="list" class="divide-y divide-gray-200 dark:divide-white/10">
<%= for device <- @totp_devices do %>
<li class="flex items-center justify-between gap-x-6 py-5">
<div class="min-w-0 flex-1">
<div class="flex items-start gap-x-3">
<p class="text-sm/6 font-semibold text-gray-900 dark:text-white">
{device.name}
</p>
</div>
<div class="mt-1 flex items-center gap-x-2 text-xs/5 text-gray-500 dark:text-gray-400">
<p class="whitespace-nowrap">
Added {ToweropsWeb.TimeHelpers.format_date(
device.inserted_at,
@current_scope.timezone
)}
</p>
<%= if device.last_used_at do %>
<svg viewBox="0 0 2 2" class="h-0.5 w-0.5 fill-current">
<circle r="1" cx="1" cy="1" />
</svg>
<p class="whitespace-nowrap">
Last used {format_relative_time(device.last_used_at)}
</p>
<% end %>
</div>
</div>
<button
type="button"
phx-click="delete_device"
phx-value-device-id={device.id}
disabled={length(@totp_devices) == 1}
data-confirm={
if length(@totp_devices) > 1,
do: "Are you sure you want to remove this device?"
}
class={
"rounded-md px-2.5 py-1.5 text-sm font-semibold shadow-xs inset-ring-1 " <>
if length(@totp_devices) == 1 do
"bg-gray-100 text-gray-400 inset-ring-gray-200 cursor-not-allowed dark:bg-gray-800 dark:text-gray-600 dark:inset-ring-gray-700"
else
"bg-white text-gray-900 inset-ring-gray-300 hover:bg-gray-100 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/10 dark:hover:bg-white/20"
end
}
>
{t("Remove")}
</button>
</li>
<% end %>
</ul>
<div class="mt-6 flex">
<button
id="show-add-device-modal-with-devices"
type="button"
phx-click="show_add_device_modal"
class="inline-flex items-center gap-x-1.5 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:hover:bg-indigo-400"
>
<.icon name="hero-plus" class="-ml-0.5 h-5 w-5" /> Add Device
</button>
</div>
<% end %>
</div>
</div>
<!-- Recovery Codes 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 border-t border-gray-200 dark:border-white/10">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Recovery Codes")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Single-use backup codes for account access if you lose your authenticator app.")}
</p>
</div>
<div class="md:col-span-2">
<div class="rounded-lg border border-gray-200 bg-white px-6 py-5 dark:border-white/10 dark:bg-white/5">
<div class="flex items-center justify-between">
<div class="flex-1">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Available Recovery Codes")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
<%= if @unused_recovery_codes_count == 0 do %>
<span class="text-red-600 dark:text-red-400 font-medium">
{t("No recovery codes available")}
</span>
- Generate new codes immediately
<% else %>
{t("You have")}
<span class="font-medium text-gray-900 dark:text-white">
{@unused_recovery_codes_count}
</span>
unused recovery code{if @unused_recovery_codes_count != 1, do: "s"}
<% end %>
</p>
</div>
<div class="ml-4">
<button
type="button"
phx-click="regenerate_recovery_codes"
data-confirm={
t("This will invalidate all existing unused recovery codes. Continue?")
}
class="inline-flex items-center gap-x-1.5 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:hover:bg-indigo-400"
>
<.icon name="hero-arrow-path" class="-ml-0.5 h-5 w-5" /> Regenerate
</button>
</div>
</div>
</div>
<%= if @unused_recovery_codes_count == 0 do %>
<div class="mt-4 rounded-lg bg-amber-50 p-4 dark:bg-amber-900/20">
<div class="flex">
<div class="flex-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">
{t("No Recovery Codes")}
</h3>
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
<p>
{t(
"You have no recovery codes available. If you lose access to your authenticator app,"
)} you won't be able to log in. Generate new codes now.
</p>
</div>
</div>
</div>
</div>
<% end %>
</div>
</div>
<% end %>
</div>
<!-- Revoke All Sessions Confirmation Modal -->
<%= if @show_revoke_all_modal do %>
<div
class="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="revoke-all-modal-title"
role="dialog"
aria-modal="true"
>
<div class="flex min-h-screen items-end justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0">
<div
phx-click="cancel_revoke_all"
class="fixed inset-0 z-0 bg-gray-500 bg-opacity-75 transition-opacity dark:bg-gray-950 dark:bg-opacity-75"
aria-hidden="true"
>
</div>
<span
class="relative z-10 hidden sm:inline-block sm:h-screen sm:align-middle"
aria-hidden="true"
>
&#8203;
</span>
<div class="relative z-10 inline-block transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left align-bottom shadow-xl transition-all dark:bg-gray-800/95 sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
<div>
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100 dark:bg-red-900">
<.icon
name="hero-exclamation-triangle"
class="h-6 w-6 text-red-600 dark:text-red-400"
/>
</div>
<div class="mt-3 text-center sm:mt-5">
<h3
class="text-lg font-semibold leading-6 text-gray-900 dark:text-white"
id="revoke-all-modal-title"
>
{t("Revoke All Other Sessions?")}
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t(
"This will log you out from all other browsers and devices. Your current session will remain active."
)}
</p>
</div>
</div>
</div>
<div class="mt-5 sm:mt-6 sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:gap-3">
<button
type="button"
phx-click="confirm_revoke_all"
class="inline-flex w-full justify-center rounded-lg bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 sm:col-start-2"
>
{t("Revoke All")}
</button>
<button
type="button"
phx-click="cancel_revoke_all"
class="mt-3 inline-flex w-full justify-center rounded-lg bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-white dark:ring-white/10 dark:hover:bg-gray-800 sm:col-start-1 sm:mt-0"
>
{t("Cancel")}
</button>
</div>
</div>
</div>
</div>
<% end %>
<!-- API Token Creation Modal -->
<%= if @show_add_token_modal do %>
<div
class="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true"
>
<div class="flex min-h-screen items-end justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0">
<div
phx-click="cancel_add_token"
class="fixed inset-0 z-0 bg-gray-500 bg-opacity-75 transition-opacity dark:bg-gray-950 dark:bg-opacity-75"
aria-hidden="true"
>
</div>
<span
class="relative z-10 hidden sm:inline-block sm:h-screen sm:align-middle"
aria-hidden="true"
>
&#8203;
</span>
<div class="relative z-10 inline-block transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left align-bottom shadow-xl transition-all dark:bg-gray-800/95 sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
<.form id="create-api-token-form" for={%{}} as={:token} phx-submit="create_api_token">
<div>
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900">
<.icon name="hero-code-bracket" class="h-6 w-6 text-blue-600 dark:text-blue-400" />
</div>
<div class="mt-3 text-center sm:mt-5">
<h3
class="text-lg font-semibold leading-6 text-gray-900 dark:text-white"
id="create-api-token-modal-title"
>
{t("Create API Token")}
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t(
"Give this token a name and select the organization it will have access to."
)}
</p>
</div>
<div class="mt-4 space-y-4">
<input
type="text"
name="name"
placeholder="e.g., Production API"
required
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
/>
<div class="grid grid-cols-1">
<select
name="organization_id"
required
class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pr-8 pl-3 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:*:bg-gray-800 dark:focus:outline-indigo-500"
>
<%= for org <- @organizations do %>
<option value={org.id}>{org.name}</option>
<% end %>
</select>
<svg
viewBox="0 0 16 16"
fill="currentColor"
data-slot="icon"
aria-hidden="true"
class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-gray-400 sm:size-4"
>
<path
d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
fill-rule="evenodd"
/>
</svg>
</div>
</div>
</div>
</div>
<div class="mt-5 sm:mt-6 sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:gap-3">
<button
type="submit"
class="inline-flex w-full justify-center rounded-lg bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:opacity-50 disabled:cursor-not-allowed sm:col-start-2"
>
{t("Create Token")}
</button>
<button
id="cancel-add-token"
type="button"
phx-click="cancel_add_token"
class="mt-3 inline-flex w-full justify-center rounded-lg bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-white dark:ring-white/10 dark:hover:bg-gray-800 sm:col-start-1 sm:mt-0"
>
{t("Cancel")}
</button>
</div>
</.form>
</div>
</div>
</div>
<% end %>
<!-- Token Created Success Modal -->
<%= if @show_token_modal && @created_token do %>
<div
id="token-success-modal"
class="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="token-success-modal-title"
role="dialog"
aria-modal="true"
>
<div class="flex min-h-screen items-end justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0">
<div
phx-click="close_token_modal"
class="fixed inset-0 z-0 bg-gray-500 bg-opacity-75 transition-opacity dark:bg-gray-950 dark:bg-opacity-75"
aria-hidden="true"
>
</div>
<span
class="relative z-10 hidden sm:inline-block sm:h-screen sm:align-middle"
aria-hidden="true"
>
&#8203;
</span>
<div class="relative z-10 inline-block transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left align-bottom shadow-xl transition-all dark:bg-gray-800/95 sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
<div>
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100 dark:bg-green-900">
<.icon name="hero-check" class="h-6 w-6 text-green-600 dark:text-green-400" />
</div>
<div class="mt-3 text-center sm:mt-5">
<h3
id="token-success-modal-title"
class="text-lg font-semibold leading-6 text-gray-900 dark:text-white"
>
{t("API Token Created")}
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t("Copy this token now. You won't be able to see it again.")}
</p>
</div>
<div class="mt-4">
<div class="relative">
<input
type="text"
readonly
value={String.trim(@created_token)}
class="w-full font-mono text-sm bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded px-3 py-2 pr-28 text-gray-900 dark:text-white select-all"
id="api-token-input"
onclick="this.select()"
/>
<button
type="button"
phx-hook="CopyToClipboard"
phx-update="ignore"
data-target="#api-token-input"
id="copy-api-token"
class="absolute right-2 top-1/2 -translate-y-1/2 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 rounded shadow-sm transition-colors flex items-center gap-1.5"
>
<.icon name="hero-clipboard" class="h-3.5 w-3.5" />
<span>{t("Copy")}</span>
</button>
</div>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{t("Click the token to select all, or use the Copy button")}
</p>
</div>
</div>
</div>
<div class="mt-5 sm:mt-6">
<button
type="button"
phx-click="close_token_modal"
class="inline-flex w-full justify-center rounded-lg bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
{t("Done")}
</button>
</div>
</div>
</div>
</div>
<% end %>
<!-- Add TOTP Device Modal -->
<%= if @show_add_device_modal do %>
<div
class="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="add-device-modal-title"
role="dialog"
aria-modal="true"
>
<div class="flex min-h-screen items-end justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0">
<div
phx-click="cancel_add_device"
class="fixed inset-0 z-0 bg-gray-500 bg-opacity-75 transition-opacity dark:bg-gray-950 dark:bg-opacity-75"
aria-hidden="true"
>
</div>
<span
class="relative z-10 hidden sm:inline-block sm:h-screen sm:align-middle"
aria-hidden="true"
>
&#8203;
</span>
<div class="relative z-10 inline-block transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left align-bottom shadow-xl transition-all dark:bg-gray-800/95 sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
<.form id="create-device-form" for={%{}} as={:device} phx-submit="create_device">
<div>
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-indigo-100 dark:bg-indigo-900">
<.icon
name="hero-device-phone-mobile"
class="h-6 w-6 text-indigo-600 dark:text-indigo-400"
/>
</div>
<div class="mt-3 text-center sm:mt-5">
<h3
class="text-lg font-semibold leading-6 text-gray-900 dark:text-white"
id="add-device-modal-title"
>
{t("Add Authenticator Device")}
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
Give this device a name to help you identify it (e.g., "iPhone 15", "iPad Pro").
</p>
</div>
<div class="mt-4">
<input
type="text"
name="name"
placeholder="e.g., iPhone 15"
required
autofocus
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
/>
</div>
</div>
</div>
<div class="mt-5 sm:mt-6 sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:gap-3">
<button
type="submit"
class="inline-flex w-full justify-center rounded-lg bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 sm:col-start-2"
>
{t("Continue")}
</button>
<button
id="cancel-add-device"
type="button"
phx-click="cancel_add_device"
class="mt-3 inline-flex w-full justify-center rounded-lg bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-white dark:ring-white/10 dark:hover:bg-gray-800 sm:col-start-1 sm:mt-0"
>
{t("Cancel")}
</button>
</div>
</.form>
</div>
</div>
</div>
<% end %>
<!-- Device QR Code Modal -->
<%= if @show_device_qr_modal && @new_device_qr_code do %>
<div
class="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="device-qr-modal-title"
role="dialog"
aria-modal="true"
>
<div class="flex min-h-screen items-end justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0">
<div
phx-click="close_device_qr_modal"
class="fixed inset-0 z-0 bg-gray-500 bg-opacity-75 transition-opacity dark:bg-gray-950 dark:bg-opacity-75"
aria-hidden="true"
>
</div>
<span
class="relative z-10 hidden sm:inline-block sm:h-screen sm:align-middle"
aria-hidden="true"
>
&#8203;
</span>
<div class="relative z-10 inline-block transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left align-bottom shadow-xl transition-all dark:bg-gray-800/95 sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
<div>
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-indigo-100 dark:bg-indigo-900">
<.icon name="hero-qr-code" class="h-6 w-6 text-indigo-600 dark:text-indigo-400" />
</div>
<div class="mt-3 text-center sm:mt-5">
<h3
class="text-lg font-semibold leading-6 text-gray-900 dark:text-white"
id="device-qr-modal-title"
>
{t("Scan QR Code")}
</h3>
<div class="mt-4 text-left">
<div class="space-y-3">
<div class="flex items-start gap-3">
<span class="flex-shrink-0 flex h-6 w-6 items-center justify-center rounded-full bg-indigo-100 text-xs font-bold text-indigo-600 dark:bg-indigo-900 dark:text-indigo-400">
1
</span>
<p class="text-sm text-gray-600 dark:text-gray-300">
{t(
"Install an authenticator app (Google Authenticator, Authy, 1Password, etc.)"
)}
</p>
</div>
<div class="flex items-start gap-3">
<span class="flex-shrink-0 flex h-6 w-6 items-center justify-center rounded-full bg-indigo-100 text-xs font-bold text-indigo-600 dark:bg-indigo-900 dark:text-indigo-400">
2
</span>
<p class="text-sm text-gray-600 dark:text-gray-300">
{t("Scan the QR code below with your authenticator app")}
</p>
</div>
<div class="flex items-start gap-3">
<span class="flex-shrink-0 flex h-6 w-6 items-center justify-center rounded-full bg-indigo-100 text-xs font-bold text-indigo-600 dark:bg-indigo-900 dark:text-indigo-400">
3
</span>
<p class="text-sm text-gray-600 dark:text-gray-300">
{t("Enter the 6-digit code from your app to verify setup")}
</p>
</div>
</div>
</div>
<!-- QR Code -->
<div class="mt-6 flex justify-center">
<div class="inline-block rounded-lg bg-white p-4">
<img src={@new_device_qr_code} alt="TOTP QR Code" class="h-48 w-48" />
</div>
</div>
<!-- Verification Code Input -->
<div class="mt-6">
<p class="mb-2 text-sm font-medium text-gray-900 dark:text-white">
{t("Enter the 6-digit code from your app to verify")}
</p>
<.form
id="verify-device-form"
for={%{}}
as={:verification}
phx-submit="verify_new_device"
>
<div class="flex justify-center gap-3">
<input
type="text"
name="code"
placeholder="000000"
maxlength="6"
pattern="[0-9]{6}"
inputmode="numeric"
autocomplete="one-time-code"
required
autofocus
class="block w-32 rounded-md bg-white px-3 py-2 text-center text-lg font-mono text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
/>
<button
type="submit"
class="inline-flex items-center gap-x-1.5 rounded-md bg-indigo-600 px-4 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"
>
{t("Verify")}
</button>
</div>
</.form>
</div>
</div>
</div>
<div class="mt-5 sm:mt-6">
<button
type="button"
phx-click="close_device_qr_modal"
class="inline-flex w-full justify-center rounded-lg bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-white dark:ring-white/10 dark:hover:bg-gray-800"
>
{t("Cancel")}
</button>
</div>
</div>
</div>
</div>
<% end %>
<!-- Recovery Codes Display Modal -->
<%= if @show_recovery_codes_modal && @generated_recovery_codes do %>
<div
class="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="recovery-codes-modal-title"
role="dialog"
aria-modal="true"
>
<div class="flex min-h-screen items-end justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0">
<div
class="fixed inset-0 z-0 bg-gray-500 bg-opacity-75 transition-opacity dark:bg-gray-950 dark:bg-opacity-75"
aria-hidden="true"
>
</div>
<span
class="relative z-10 hidden sm:inline-block sm:h-screen sm:align-middle"
aria-hidden="true"
>
&#8203;
</span>
<div class="relative z-10 inline-block transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left align-bottom shadow-xl transition-all dark:bg-gray-800/95 sm:my-8 sm:w-full sm:max-w-2xl sm:p-6 sm:align-middle">
<div>
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100 dark:bg-green-900">
<.icon name="hero-shield-check" class="h-6 w-6 text-green-600 dark:text-green-400" />
</div>
<div class="mt-3 text-center sm:mt-5">
<h3
class="text-lg font-semibold leading-6 text-gray-900 dark:text-white"
id="recovery-codes-modal-title"
>
{t("Recovery Codes Generated")}
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t("Save these codes in a secure location. Each code can only be used once.")}
</p>
</div>
<!-- Recovery Codes Grid -->
<div class="mt-6">
<div class="rounded-lg bg-gray-50 p-6 dark:bg-gray-900/50">
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3">
<%= for code <- @generated_recovery_codes do %>
<div class="rounded-md bg-white px-3 py-2 font-mono text-sm text-gray-900 shadow-xs dark:bg-gray-800 dark:text-white">
{code}
</div>
<% end %>
</div>
</div>
</div>
<!-- Copy to Clipboard -->
<div class="mt-4 flex justify-center gap-3">
<button
type="button"
phx-hook="CopyToClipboard"
phx-update="ignore"
data-target="#recovery-codes-text"
id="copy-recovery-codes"
class="inline-flex items-center gap-x-1.5 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"
>
<.icon name="hero-clipboard" class="-ml-0.5 h-5 w-5" /> Copy All
</button>
</div>
<!-- Hidden textarea for copy functionality -->
<textarea
id="recovery-codes-text"
class="sr-only"
readonly
>{Enum.join(@generated_recovery_codes, "\n")}</textarea>
<!-- Warning -->
<div class="mt-6 rounded-lg bg-amber-50 p-4 dark:bg-amber-900/20">
<div class="flex">
<div class="flex-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">
{t("Important")}
</h3>
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
<p>
{t(
"These codes won't be shown again. Store them securely (password manager, encrypted file, etc.)."
)}
{t(
"If you lose your authenticator and these codes, you won't be able to access your account."
)}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mt-5 sm:mt-6">
<button
type="button"
phx-click="close_recovery_codes_modal"
class="inline-flex w-full justify-center rounded-lg bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
{t("I've Saved These Codes")}
</button>
</div>
</div>
</div>
</div>
<% end %>
<!-- Delete Account Confirmation Modal -->
<%= if @show_delete_modal do %>
<div
class="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="delete-account-modal-title"
role="dialog"
aria-modal="true"
>
<div class="flex min-h-screen items-end justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0">
<div
phx-click="close_delete_modal"
class="fixed inset-0 z-0 bg-gray-500 bg-opacity-75 transition-opacity dark:bg-gray-950 dark:bg-opacity-75"
aria-hidden="true"
>
</div>
<span
class="relative z-10 hidden sm:inline-block sm:h-screen sm:align-middle"
aria-hidden="true"
>
&#8203;
</span>
<div class="relative z-10 inline-block transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left align-bottom shadow-xl transition-all dark:bg-gray-800/95 sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
<div>
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100 dark:bg-red-900">
<.icon
name="hero-exclamation-triangle"
class="h-6 w-6 text-red-600 dark:text-red-400"
/>
</div>
<div class="mt-3 text-center sm:mt-5">
<h3
class="text-lg font-semibold leading-6 text-gray-900 dark:text-white"
id="delete-account-modal-title"
>
{t("Are you sure you want to delete your account?")}
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t(
"This action cannot be undone. All of your data will be permanently removed."
)}
</p>
</div>
</div>
</div>
<%= if @sole_owner_orgs != [] do %>
<div class="mt-4 rounded-md bg-yellow-50 p-4 dark:bg-yellow-900/20">
<div class="flex">
<div class="flex-shrink-0">
<.icon name="hero-exclamation-triangle" class="h-5 w-5 text-yellow-400" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
{t("You are the sole owner of these organizations:")}
</h3>
<div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
<ul class="list-disc space-y-1 pl-5">
<%= for org <- @sole_owner_orgs do %>
<li>{org.name}</li>
<% end %>
</ul>
<p class="mt-2">
{t(
"Please transfer ownership or delete these organizations before deleting your account."
)}
</p>
</div>
</div>
</div>
</div>
<div class="mt-5 sm:mt-6">
<button
type="button"
phx-click="close_delete_modal"
class="inline-flex w-full justify-center rounded-lg bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-white dark:ring-white/10 dark:hover:bg-gray-800"
>
{t("Close")}
</button>
</div>
<% else %>
<.form
for={@delete_form}
phx-submit="delete_account"
id="delete_account_form"
class="mt-4"
>
<div class="space-y-4">
<div>
<label
for="delete-password"
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{t("Enter your password")}
</label>
<div class="mt-1">
<input
type="password"
name="password"
id="delete-password"
required
autocomplete="current-password"
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-red-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-red-500"
/>
</div>
</div>
<div>
<label
for="delete-confirmation"
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{t("Type DELETE to confirm")}
</label>
<div class="mt-1">
<input
type="text"
name="confirmation"
id="delete-confirmation"
required
placeholder="DELETE"
autocomplete="off"
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-red-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-red-500"
/>
</div>
</div>
</div>
<div class="mt-5 sm:mt-6 sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:gap-3">
<button
type="submit"
phx-disable-with={t("Deleting...")}
class="inline-flex w-full justify-center rounded-lg bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 sm:col-start-2"
>
{t("Permanently Delete Account")}
</button>
<button
type="button"
phx-click="close_delete_modal"
class="mt-3 inline-flex w-full justify-center rounded-lg bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-white dark:ring-white/10 dark:hover:bg-gray-800 sm:col-start-1 sm:mt-0"
>
{t("Cancel")}
</button>
</div>
</.form>
<% end %>
</div>
</div>
</div>
<% end %>
</Layouts.authenticated>