295 lines
12 KiB
Text
295 lines
12 KiB
Text
<Layouts.authenticated flash={@flash} current_scope={@current_scope} current_organization={nil}>
|
|
<div class="text-center">
|
|
<.header>
|
|
Account Settings
|
|
<:subtitle>Manage your account email address and password settings</:subtitle>
|
|
</.header>
|
|
</div>
|
|
|
|
<.form :let={f} for={@email_changeset} action={~p"/users/settings"} id="update_email">
|
|
<input type="hidden" name="action" value="update_email" />
|
|
|
|
<.input field={f[:email]} type="email" label="Email" autocomplete="email" required />
|
|
|
|
<.button variant="primary" phx-disable-with="Changing...">Change Email</.button>
|
|
</.form>
|
|
|
|
<div class="divider" />
|
|
|
|
<.form :let={f} for={@password_changeset} action={~p"/users/settings"} id="update_password">
|
|
<input type="hidden" name="action" value="update_password" />
|
|
|
|
<.input
|
|
field={f[:password]}
|
|
type="password"
|
|
label="New password"
|
|
autocomplete="new-password"
|
|
required
|
|
/>
|
|
<.input
|
|
field={f[:password_confirmation]}
|
|
type="password"
|
|
label="Confirm new password"
|
|
autocomplete="new-password"
|
|
required
|
|
/>
|
|
<.button variant="primary" phx-disable-with="Changing...">
|
|
Save Password
|
|
</.button>
|
|
</.form>
|
|
|
|
<div class="divider" />
|
|
|
|
<div>
|
|
<h3 class="text-lg font-semibold leading-6 text-gray-900 dark:text-white">
|
|
Alert Notification Devices
|
|
</h3>
|
|
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
Manage mobile devices that receive push notifications for alerts
|
|
</p>
|
|
|
|
<div class="mt-6 space-y-4">
|
|
<%= if Enum.empty?(@mobile_sessions) do %>
|
|
<div class="rounded-lg border border-dashed border-gray-300 p-8 text-center dark:border-white/10">
|
|
<.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">
|
|
No mobile devices registered
|
|
</h3>
|
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
Add a mobile device to receive push notifications for alerts
|
|
</p>
|
|
</div>
|
|
<% else %>
|
|
<div class="space-y-3">
|
|
<%= for session <- @mobile_sessions do %>
|
|
<div class="flex items-center justify-between rounded-lg border border-gray-200 p-4 dark:border-white/10">
|
|
<div class="flex items-center gap-3">
|
|
<.icon name="hero-device-phone-mobile" class="h-5 w-5 text-gray-400" />
|
|
<div>
|
|
<p class="font-medium text-gray-900 dark:text-white">
|
|
{session.device_name || "Unknown Device"}
|
|
</p>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
{session.device_os} • {session.app_version}
|
|
</p>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
Last used {Calendar.strftime(session.last_used_at, "%B %d, %Y")}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<.form
|
|
:let={_f}
|
|
for={%{}}
|
|
action={~p"/users/settings"}
|
|
method="put"
|
|
class="inline"
|
|
>
|
|
<input type="hidden" name="action" value="toggle_device_alerts" />
|
|
<input type="hidden" name="session_id" value={session.id} />
|
|
<input
|
|
type="hidden"
|
|
name="enabled"
|
|
value={if session.alerts_enabled, do: "false", else: "true"}
|
|
/>
|
|
<button
|
|
type="submit"
|
|
class={[
|
|
"inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-semibold shadow-sm",
|
|
if(session.alerts_enabled,
|
|
do:
|
|
"bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/20 dark:text-green-400 dark:hover:bg-green-900/30",
|
|
else:
|
|
"bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800/50 dark:text-gray-400 dark:hover:bg-gray-800"
|
|
)
|
|
]}
|
|
>
|
|
<%= if session.alerts_enabled do %>
|
|
<.icon name="hero-bell" class="h-4 w-4" /> Alerts On
|
|
<% else %>
|
|
<.icon name="hero-bell-slash" class="h-4 w-4" /> Alerts Off
|
|
<% end %>
|
|
</button>
|
|
</.form>
|
|
<.form
|
|
:let={_f}
|
|
for={%{}}
|
|
action={~p"/users/settings"}
|
|
method="put"
|
|
class="inline"
|
|
>
|
|
<input type="hidden" name="action" value="revoke_mobile_device" />
|
|
<input type="hidden" name="session_id" value={session.id} />
|
|
<button
|
|
type="submit"
|
|
data-confirm="Are you sure you want to remove this device?"
|
|
class="text-sm font-semibold text-red-600 hover:text-red-500 dark:text-red-400 dark:hover:text-red-300"
|
|
>
|
|
Remove
|
|
</button>
|
|
</.form>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
|
|
<.link
|
|
navigate={~p"/mobile/qr-login"}
|
|
class="inline-flex items-center gap-2 rounded-lg bg-gray-900 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-700 dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-300"
|
|
>
|
|
<.icon name="hero-qr-code" class="h-4 w-4" /> Add Mobile Device
|
|
</.link>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="divider" />
|
|
|
|
<div>
|
|
<h3 class="text-lg font-semibold leading-6 text-gray-900 dark:text-white">
|
|
Passkeys
|
|
</h3>
|
|
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
Use your device's biometrics (Face ID, Touch ID, Windows Hello) or security keys to sign in
|
|
</p>
|
|
|
|
<%= if !@can_register_passkey do %>
|
|
<div class="mt-4 rounded-lg bg-amber-50 p-4 dark:bg-amber-900/20">
|
|
<p class="text-sm text-amber-800 dark:text-amber-200">
|
|
Please confirm your email address before registering a passkey. Check your inbox for the confirmation link.
|
|
</p>
|
|
</div>
|
|
<% end %>
|
|
|
|
<div class="mt-6 space-y-4">
|
|
<%= if Enum.empty?(@credentials) do %>
|
|
<div class="rounded-lg border border-dashed border-gray-300 p-8 text-center dark:border-white/10">
|
|
<.icon name="hero-key" class="mx-auto h-12 w-12 text-gray-400" />
|
|
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
|
|
No passkeys registered
|
|
</h3>
|
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
Add a passkey to enable quick, secure sign-in with biometrics
|
|
</p>
|
|
</div>
|
|
<% else %>
|
|
<div class="space-y-3">
|
|
<%= for credential <- @credentials do %>
|
|
<div class="flex items-center justify-between rounded-lg border border-gray-200 p-4 dark:border-white/10">
|
|
<div class="flex items-center gap-3">
|
|
<.icon name="hero-key" class="h-5 w-5 text-gray-400" />
|
|
<div>
|
|
<p class="font-medium text-gray-900 dark:text-white">
|
|
{credential.name}
|
|
</p>
|
|
<%= if credential.last_used_at do %>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
Last used {Calendar.strftime(credential.last_used_at, "%B %d, %Y")}
|
|
</p>
|
|
<% else %>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">Never used</p>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<.link
|
|
href={~p"/users/credentials/#{credential.id}"}
|
|
method="delete"
|
|
data-confirm="Are you sure you want to delete this passkey?"
|
|
class="text-sm font-semibold text-red-600 hover:text-red-500 dark:text-red-400 dark:hover:text-red-300"
|
|
>
|
|
Delete
|
|
</.link>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
|
|
<%= if @can_register_passkey do %>
|
|
<button
|
|
type="button"
|
|
id="add-passkey-btn"
|
|
class="inline-flex items-center gap-2 rounded-lg bg-gray-900 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-700 dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-300"
|
|
>
|
|
<.icon name="hero-plus" class="h-4 w-4" /> Add Passkey
|
|
</button>
|
|
|
|
<!-- Passkey Registration Modal -->
|
|
<div
|
|
id="passkey-modal"
|
|
class="hidden 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">
|
|
<!-- Background overlay -->
|
|
<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>
|
|
|
|
<!-- Center modal -->
|
|
<span
|
|
class="relative z-10 hidden sm:inline-block sm:h-screen sm:align-middle"
|
|
aria-hidden="true"
|
|
>
|
|
​
|
|
</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-blue-100 dark:bg-blue-900">
|
|
<.icon name="hero-key" 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="modal-title"
|
|
>
|
|
Add Passkey
|
|
</h3>
|
|
<div class="mt-2">
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
Give this passkey a name to help you identify it later (e.g., "MacBook Touch ID", "iPhone").
|
|
</p>
|
|
</div>
|
|
<div class="mt-4">
|
|
<input
|
|
type="text"
|
|
id="passkey-name-input"
|
|
placeholder="e.g., MacBook Touch ID"
|
|
class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-white/10 dark:bg-gray-800/50 dark:text-white sm:text-sm"
|
|
/>
|
|
</div>
|
|
<div
|
|
id="passkey-error"
|
|
class="mt-3 hidden rounded-lg bg-red-50 p-3 dark:bg-red-900/20"
|
|
>
|
|
<p class="text-sm text-red-800 dark:text-red-200"></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"
|
|
id="confirm-add-passkey"
|
|
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"
|
|
>
|
|
Continue
|
|
</button>
|
|
<button
|
|
type="button"
|
|
id="cancel-add-passkey"
|
|
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"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</Layouts.authenticated>
|