towerops/lib/towerops_web/components/consent_prompt.ex
Graham McIntire de986bddf6
Prevent Oban polling/monitoring job stacking per device
When the Oban queue backs up, the 60-second uniqueness window expires
and duplicate jobs stack up per device. Switch to period: :infinity so
only one poll/monitor job exists per device at any time. Add replace
option to supersede stale scheduled jobs with updated scheduled_at.
Remove :executing from unique states so self-scheduling works while
the current job runs. Set max_attempts: 1 since retrying stale polls
is pointless.

Also fix all credo --strict issues across the codebase.
2026-02-16 16:37:48 -06:00

123 lines
5.4 KiB
Elixir

defmodule ToweropsWeb.Components.ConsentPrompt do
@moduledoc """
LiveView component for prompting users to re-consent when policies are updated.
This component displays a modal that users cannot dismiss until they accept
the updated privacy policy and/or terms of service.
"""
use Phoenix.Component
use Gettext, backend: ToweropsWeb.Gettext
import ToweropsWeb.CoreComponents
attr :user_id, :string, required: true
attr :policies, :list, required: true
def consent_modal(assigns) do
~H"""
<%= if @policies != [] do %>
<div
id="consent-modal"
class="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="consent-modal-title"
role="dialog"
aria-modal="true"
>
<div class="flex min-h-screen items-center justify-center p-4">
<!-- Backdrop -->
<div class="fixed inset-0 bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-80 transition-opacity">
</div>
<!-- Modal panel -->
<div class="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900 sm:mx-0 sm:h-10 sm:w-10">
<.icon
name="hero-information-circle"
class="h-6 w-6 text-blue-600 dark:text-blue-400"
/>
</div>
<div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left w-full">
<h3
class="text-lg font-semibold leading-6 text-gray-900 dark:text-white"
id="consent-modal-title"
>
{gettext("Updated Policies - Action Required")}
</h3>
<div class="mt-4 space-y-4">
<p class="text-sm text-gray-600 dark:text-gray-400">
{gettext(
"We've updated our policies. Please review and accept the changes to continue using Towerops."
)}
</p>
<div class="space-y-3">
<%= for policy_type <- @policies do %>
<div class="flex items-start">
<div class="flex items-center h-5">
<input
type="checkbox"
id={"consent-#{policy_type}"}
name={"consent[#{policy_type}]"}
phx-click="toggle_consent"
phx-value-policy={policy_type}
required
class="h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-800 dark:checked:bg-blue-500 dark:checked:border-blue-500"
/>
</div>
<div class="ml-3 text-sm">
<label
for={"consent-#{policy_type}"}
class="text-gray-700 dark:text-gray-300"
>
{gettext("I agree to the updated")}
<.link
href={policy_link(policy_type)}
target="_blank"
class="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
{policy_name(policy_type)}
</.link>
<span class="text-red-600 dark:text-red-400">*</span>
</label>
</div>
</div>
<% end %>
</div>
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<p class="text-xs text-gray-500 dark:text-gray-400">
{gettext(
"By accepting, you acknowledge that you have read and agree to the updated policies. You can review your consent history at any time from your account settings."
)}
</p>
</div>
</div>
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
type="button"
phx-click="accept_updated_policies"
phx-value-user-id={@user_id}
class="inline-flex w-full justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 sm:ml-3 sm:w-auto disabled:opacity-50 disabled:cursor-not-allowed"
id="accept-policies-button"
>
{gettext("Accept and Continue")}
</button>
</div>
</div>
</div>
</div>
<% end %>
"""
end
defp policy_name("privacy_policy"), do: gettext("Privacy Policy")
defp policy_name("terms_of_service"), do: gettext("Terms of Service")
defp policy_name(_), do: gettext("Policy")
defp policy_link("privacy_policy"), do: "/privacy"
defp policy_link("terms_of_service"), do: "/terms"
defp policy_link(_), do: "/"
end