gdpr cookie consent
This commit is contained in:
parent
f350a9ade4
commit
588639a47a
7 changed files with 183 additions and 0 deletions
|
|
@ -33,6 +33,7 @@ import "./passkey_login"
|
|||
import "./passkey_discoverable"
|
||||
import type { SensorChartHook } from "./types/liveview"
|
||||
import { DeviceListReorder } from "./device_list_reorder"
|
||||
import { initCookieConsent } from "./cookie_consent"
|
||||
|
||||
// Helper function to convert range string to milliseconds
|
||||
function getTimeRangeMs(range: string): number {
|
||||
|
|
@ -694,6 +695,11 @@ window.addEventListener("phx:copy", (event: any) => {
|
|||
// connect if there are any LiveViews on the page
|
||||
liveSocket.connect()
|
||||
|
||||
// Initialize cookie consent handler
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initCookieConsent()
|
||||
})
|
||||
|
||||
// expose liveSocket on window for web console debug logs and latency simulation:
|
||||
// >> liveSocket.enableDebug()
|
||||
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
|
||||
|
|
|
|||
49
assets/js/cookie_consent.js
Normal file
49
assets/js/cookie_consent.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* Cookie Consent Handler
|
||||
*
|
||||
* Handles cookie consent acceptance for GDPR compliance.
|
||||
* Sets a cookie that expires in 1 year when the user accepts.
|
||||
*/
|
||||
|
||||
export function initCookieConsent() {
|
||||
// Check if consent cookie already exists on page load
|
||||
if (hasConsentCookie()) {
|
||||
hideBanner();
|
||||
}
|
||||
|
||||
// Listen for click events on the accept button
|
||||
document.addEventListener('click', (event) => {
|
||||
if (event.target.matches('[data-cookie-consent="accept"]') ||
|
||||
event.target.closest('[data-cookie-consent="accept"]')) {
|
||||
acceptCookies();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function hasConsentCookie() {
|
||||
return document.cookie.split('; ').some(cookie => cookie.startsWith('cookie_consent=accepted'));
|
||||
}
|
||||
|
||||
function hideBanner() {
|
||||
const banner = document.getElementById('cookie-consent-banner');
|
||||
if (banner) {
|
||||
banner.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function acceptCookies() {
|
||||
// Set cookie that expires in 1 year
|
||||
const expiryDate = new Date();
|
||||
expiryDate.setFullYear(expiryDate.getFullYear() + 1);
|
||||
|
||||
document.cookie = `cookie_consent=accepted; expires=${expiryDate.toUTCString()}; path=/; SameSite=Lax; Secure`;
|
||||
|
||||
// Hide the banner with animation
|
||||
const banner = document.getElementById('cookie-consent-banner');
|
||||
if (banner) {
|
||||
banner.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
banner.remove();
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
72
lib/towerops_web/components/cookie_consent.ex
Normal file
72
lib/towerops_web/components/cookie_consent.ex
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
defmodule ToweropsWeb.Components.CookieConsent do
|
||||
@moduledoc """
|
||||
Cookie consent banner component for GDPR compliance.
|
||||
|
||||
Only displays to users from EU/EEA countries and for users who haven't
|
||||
yet accepted the cookie policy.
|
||||
|
||||
Since we only use essential cookies (session, CSRF), the banner is
|
||||
informational and provides an "Acknowledge" button.
|
||||
"""
|
||||
use Phoenix.Component
|
||||
|
||||
import ToweropsWeb.CoreComponents, only: [icon: 1]
|
||||
|
||||
@doc """
|
||||
Renders a cookie consent banner at the bottom of the page.
|
||||
|
||||
## Attributes
|
||||
* `requires_consent` - boolean, whether to show the banner (EU/EEA users)
|
||||
|
||||
Note: The banner is always rendered if requires_consent is true.
|
||||
JavaScript will hide it immediately if the consent cookie exists.
|
||||
"""
|
||||
attr :requires_consent, :boolean, required: true
|
||||
|
||||
def banner(assigns) do
|
||||
~H"""
|
||||
<%= if @requires_consent do %>
|
||||
<div
|
||||
id="cookie-consent-banner"
|
||||
class="fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 shadow-lg"
|
||||
role="dialog"
|
||||
aria-live="polite"
|
||||
aria-label="Cookie consent"
|
||||
>
|
||||
<div class="max-w-7xl mx-auto px-4 py-4 sm:px-6 lg:px-8">
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
||||
<div class="flex-1 flex items-start gap-3">
|
||||
<.icon
|
||||
name="hero-information-circle"
|
||||
class="h-6 w-6 text-blue-600 dark:text-blue-400 flex-shrink-0"
|
||||
/>
|
||||
<div class="text-sm text-gray-700 dark:text-gray-300">
|
||||
<p class="font-medium mb-1">We respect your privacy</p>
|
||||
<p>
|
||||
We use essential cookies to maintain your session and ensure the security of our service. We do not use tracking or advertising cookies.
|
||||
<a
|
||||
href="/privacy"
|
||||
class="underline hover:text-gray-900 dark:hover:text-gray-100 font-medium"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 w-full sm:w-auto">
|
||||
<button
|
||||
type="button"
|
||||
data-cookie-consent="accept"
|
||||
class="flex-1 sm:flex-none px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-900"
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
@ -5,6 +5,8 @@ defmodule ToweropsWeb.Layouts do
|
|||
"""
|
||||
use ToweropsWeb, :html
|
||||
|
||||
alias ToweropsWeb.Components.CookieConsent
|
||||
|
||||
# Embed all files in layouts/* within this module.
|
||||
# The default root.html.heex file contains the HTML
|
||||
# skeleton of your application, namely HTML headers
|
||||
|
|
@ -74,6 +76,7 @@ defmodule ToweropsWeb.Layouts do
|
|||
</div>
|
||||
|
||||
<.flash_group flash={@flash} />
|
||||
<CookieConsent.banner requires_consent={Map.get(assigns, :requires_cookie_consent, false)} />
|
||||
"""
|
||||
end
|
||||
|
||||
|
|
@ -392,6 +395,7 @@ defmodule ToweropsWeb.Layouts do
|
|||
</div>
|
||||
|
||||
<.flash_group flash={@flash} />
|
||||
<CookieConsent.banner requires_consent={Map.get(assigns, :requires_cookie_consent, false)} />
|
||||
"""
|
||||
end
|
||||
|
||||
|
|
@ -653,6 +657,7 @@ defmodule ToweropsWeb.Layouts do
|
|||
</div>
|
||||
|
||||
<.flash_group flash={@flash} />
|
||||
<CookieConsent.banner requires_consent={Map.get(assigns, :requires_cookie_consent, false)} />
|
||||
"""
|
||||
end
|
||||
|
||||
|
|
|
|||
49
lib/towerops_web/plugs/detect_eu_user.ex
Normal file
49
lib/towerops_web/plugs/detect_eu_user.ex
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
defmodule ToweropsWeb.Plugs.DetectEUUser do
|
||||
@moduledoc """
|
||||
Detects if a user is from an EU/EEA country that requires GDPR cookie consent.
|
||||
|
||||
This plug checks the CloudFlare CF-IPCountry header (if available) or other
|
||||
geolocation headers to determine if the user is from an EU/EEA country.
|
||||
|
||||
The result is stored in conn.assigns.requires_cookie_consent
|
||||
"""
|
||||
import Plug.Conn
|
||||
|
||||
# EU/EEA country codes that require GDPR cookie consent
|
||||
# EU member states + EEA countries (Norway, Iceland, Liechtenstein)
|
||||
@eu_eea_countries ~w(
|
||||
AT BE BG HR CY CZ DK EE FI FR DE GR HU IE IT LV LT LU MT NL PL PT RO SK SI ES SE
|
||||
IS NO LI
|
||||
)
|
||||
|
||||
@doc false
|
||||
def init(opts), do: opts
|
||||
|
||||
@doc false
|
||||
def call(conn, _opts) do
|
||||
requires_consent = detect_eu_user(conn)
|
||||
assign(conn, :requires_cookie_consent, requires_consent)
|
||||
end
|
||||
|
||||
defp detect_eu_user(conn) do
|
||||
# Try CloudFlare's CF-IPCountry header first (most reliable if behind CF)
|
||||
case get_req_header(conn, "cf-ipcountry") do
|
||||
[country_code] when country_code != "" ->
|
||||
country_code = String.upcase(country_code)
|
||||
country_code in @eu_eea_countries
|
||||
|
||||
_ ->
|
||||
# Try X-Country header (some proxies set this)
|
||||
case get_req_header(conn, "x-country") do
|
||||
[country_code] when country_code != "" ->
|
||||
country_code = String.upcase(country_code)
|
||||
country_code in @eu_eea_countries
|
||||
|
||||
_ ->
|
||||
# No reliable country detection available
|
||||
# Default to showing consent banner (conservative approach for GDPR compliance)
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -19,6 +19,7 @@ defmodule ToweropsWeb.Router do
|
|||
plug :put_secure_browser_headers
|
||||
plug :fetch_current_scope_for_user
|
||||
plug :store_return_to_for_liveview
|
||||
plug ToweropsWeb.Plugs.DetectEUUser
|
||||
end
|
||||
|
||||
pipeline :api do
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ Devices Working
|
|||
|
||||
2026-01-28
|
||||
* Major SNMP Discovery overhaul and improvements
|
||||
* GDPR: Cookie consent banner
|
||||
|
||||
2025-01-27
|
||||
* Infrastructure improvements
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue