From f7647829bd12125a1515e2bdbdfb24fa13bda0e5 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Wed, 20 Aug 2025 21:04:55 +0200 Subject: [PATCH] create a delete-my-data button --- locales/de.yml | 6 ++++++ locales/en.yml | 6 ++++++ src/index.rs | 28 ++++++++++++++++++++++++---- src/main.rs | 29 ++++++++++++++++++++++++++++- src/model/client.rs | 24 ++++++++++++++++++++++++ src/page.rs | 9 +++++++++ 6 files changed, 97 insertions(+), 5 deletions(-) diff --git a/locales/de.yml b/locales/de.yml index 2b8e6e4..5e1062f 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -104,3 +104,9 @@ data_protection_officer_contact: "Stabsstelle Datenschutz der Johannes Kepler Un data_protection_officer_contact_full: "Stabsstelle Datenschutz der Johannes Kepler Universität Linz
Altenberger Straße 69, 4040 Linz
+43 732 2468 3802
datenschutz@jku.at" data_collection_timing: "Wann werden Daten gesammelt" data_collection_timing_description: "Daten werden gesammelt, wenn die Website besucht wird und QR Codes gescannt werden." +delete_personal_data: "Persönliche Daten löschen" +delete_data_description: "Sie können die vollständige Löschung aller Ihrer auf unseren Servern gespeicherten persönlichen Daten beantragen. Dies umfasst Ihren gewählten Namen, Spielfortschritt und alle Sichtungen. Diese Aktion kann nicht rückgängig gemacht werden." +delete_my_data: "Meine Daten löschen" +delete_confirmation: "Sind Sie sicher, dass Sie alle Ihre persönlichen Daten löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden und Sie verlieren Ihren gesamten Spielfortschritt." +data_deletion_success_title: "Daten erfolgreich gelöscht" +data_deletion_success_body: "Alle Ihre persönlichen Daten wurden erfolgreich von unseren Servern entfernt. Ihr Sitzungs-Cookie wurde ebenfalls zerstört." diff --git a/locales/en.yml b/locales/en.yml index 2e920ea..bf77505 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -104,3 +104,9 @@ data_protection_officer_contact: "Data Protection Office of Johannes Kepler Univ data_protection_officer_contact_full: "Data Protection Office of Johannes Kepler University Linz
Altenberger Straße 69, 4040 Linz
+43 732 2468 3802
datenschutz@jku.at" data_collection_timing: "When data is collected" data_collection_timing_description: "Data is collected when the website is visited and QR codes are scanned." +delete_personal_data: "Delete Personal Data" +delete_data_description: "You can request the complete deletion of all your personal data stored on our servers. This includes your chosen name, game progress, and all sightings. This action cannot be undone." +delete_my_data: "Delete My Data" +delete_confirmation: "Are you sure you want to delete all your personal data? This action cannot be undone and you will lose all your game progress." +data_deletion_success_title: "Data Successfully Deleted" +data_deletion_success_body: "All your personal data has been successfully removed from our servers. Your session cookie has also been destroyed." diff --git a/src/index.rs b/src/index.rs index 0b11bda..c2dc3a6 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1,7 +1,8 @@ -use crate::{language::language, page::Page}; -use axum::http::HeaderMap; +use crate::{language::language, page::{MyMessage, Page}}; +use axum::{extract::Query, http::HeaderMap}; use axum_extra::extract::CookieJar; use maud::{html, Markup, PreEscaped}; +use serde::Deserialize; pub(super) async fn index(cookies: CookieJar, headers: HeaderMap) -> Markup { let lang = language(&cookies, &headers); @@ -54,11 +55,21 @@ pub(super) async fn index(cookies: CookieJar, headers: HeaderMap) -> Markup { }) } -pub(super) async fn data(cookies: CookieJar, headers: HeaderMap) -> Markup { +#[derive(Deserialize)] +pub(super) struct PrivacyQuery { + deleted: Option, +} + +pub(super) async fn data(cookies: CookieJar, headers: HeaderMap, Query(query): Query) -> Markup { let lang = language(&cookies, &headers); rust_i18n::set_locale(lang.to_locale()); - let page = Page::new(lang); + let mut page = Page::new(lang); + + // Show success message if data was deleted + if query.deleted == Some(1) { + page.set_message(MyMessage::DataDeleted); + } page.content(html! { h1 { (t!("privacy_policy_title")) } h2 { (t!("data_controller")) } @@ -152,5 +163,14 @@ pub(super) async fn data(cookies: CookieJar, headers: HeaderMap) -> Markup { (PreEscaped(t!("contact_us"))) } } + h3 { (t!("delete_personal_data")) } + p { + (t!("delete_data_description")) + } + form method="POST" action="/delete-data" onsubmit={"return confirm('" (t!("delete_confirmation")) "');"} { + button type="submit" class="secondary" { + (t!("delete_my_data")) + } + } }) } diff --git a/src/main.rs b/src/main.rs index 95c2908..6f01cb7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use crate::model::client::Client; -use axum::{http::HeaderMap, routing::get, Router}; +use axum::{http::HeaderMap, response::Redirect, routing::{get, post}, Router}; use axum_extra::extract::{ cookie::{Cookie, Expiration, Key}, CookieJar, PrivateCookieJar, @@ -260,6 +260,32 @@ fn load_or_create_key() -> Result> { Ok(Key::from(&config.key)) } +async fn delete_personal_data( + axum::extract::State(state): axum::extract::State, + cookies: PrivateCookieJar, +) -> (PrivateCookieJar, Redirect) { + let backend = &state.backend; + // Get the client from cookies + if let Some(client_cookie) = cookies.get("client_id") { + if let Ok(uuid) = Uuid::parse_str(client_cookie.value()) { + // Delete all client data from database + let _ = backend.delete_client_data(&uuid).await; + } + } + + // Remove the client_id cookie by setting an expired cookie + let expired_cookie = Cookie::build(("client_id", "")) + .expires(Expiration::DateTime(OffsetDateTime::now_utc() - time::Duration::days(1))) + .http_only(true) + .secure(true) + .build(); + + let updated_cookies = cookies.add(expired_cookie); + + // Redirect back to privacy page with success message + (updated_cookies, Redirect::to("/privacy?deleted=1")) +} + #[tokio::main] async fn main() { tracing_subscriber::registry() @@ -282,6 +308,7 @@ async fn main() { let app = Router::new() .route("/", get(index::index)) .route("/privacy", get(index::data)) + .route("/delete-data", post(delete_personal_data)) .nest_service("/static", ServeDir::new("./static/serve")) .merge(game::routes()) .with_state(state); diff --git a/src/model/client.rs b/src/model/client.rs index 8dbbcfc..4811f7f 100644 --- a/src/model/client.rs +++ b/src/model/client.rs @@ -41,4 +41,28 @@ impl Backend { } } } + + pub(crate) async fn delete_client_data(&self, uuid: &Uuid) -> Result<(), sqlx::Error> { + let uuid_str = uuid.to_string(); + + match self { + Backend::Sqlite(db) => { + // Start a transaction to ensure data consistency + let mut tx = db.begin().await?; + + // Delete sightings first (foreign key constraint) + sqlx::query!("DELETE FROM sightings WHERE client_uuid = ?", uuid_str) + .execute(&mut *tx) + .await?; + + // Delete client record + sqlx::query!("DELETE FROM client WHERE uuid = ?", uuid_str) + .execute(&mut *tx) + .await?; + + tx.commit().await?; + Ok(()) + } + } + } } diff --git a/src/page.rs b/src/page.rs index 7fc4593..e0f5dcd 100644 --- a/src/page.rs +++ b/src/page.rs @@ -10,6 +10,7 @@ pub(crate) enum MyMessage { NameChanged, FoundCam(String, i64), Error(String, String, String), + DataDeleted, } impl Page { @@ -97,6 +98,14 @@ impl Page { } } } + MyMessage::DataDeleted => { + div.flex { + article class="succ msg" { + header { (t!("data_deletion_success_title")) } + (t!("data_deletion_success_body")) + } + } + } } } section { (content) }