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