create a delete-my-data button
This commit is contained in:
@@ -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."
|
||||
|
@@ -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."
|
||||
|
28
src/index.rs
28
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<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"))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
29
src/main.rs
29
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<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);
|
||||
|
@@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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) }
|
||||
|
Reference in New Issue
Block a user