create a delete-my-data button

This commit is contained in:
2025-08-20 21:04:55 +02:00
parent ea65f51704
commit f7647829bd
6 changed files with 97 additions and 5 deletions

View File

@@ -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<br>Altenberger Straße 69, 4040 Linz<br>+43 732 2468 3802<br>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."

View File

@@ -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<br>Altenberger Straße 69, 4040 Linz<br>+43 732 2468 3802<br>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."

View File

@@ -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<u8>,
}
pub(super) async fn data(cookies: CookieJar, headers: HeaderMap, Query(query): Query<PrivacyQuery>) -> 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"))
}
}
})
}

View File

@@ -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<Key, Box<dyn std::error::Error>> {
Ok(Key::from(&config.key))
}
async fn delete_personal_data(
axum::extract::State(state): axum::extract::State<AppState>,
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);

View File

@@ -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(())
}
}
}
}

View File

@@ -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) }