From 2ea195475e5662ea503819cbbda32828fdc8c080 Mon Sep 17 00:00:00 2001
From: Sarahtonin
Date: Fri, 4 Sep 2026 18:02:15 +0200
Subject: [PATCH] Added deletion of users and export of data. GDPR..
---
Pages/Guide.php | 34 ++++++
Pages/Profile.php | 42 +++++++
Scripts/Garden/Account.js | 245 ++++++++++++++++++++++++++++++++++++++
Styles/Style.css | 14 +++
4 files changed, 335 insertions(+)
diff --git a/Pages/Guide.php b/Pages/Guide.php
index 7b653bc..dee5190 100644
--- a/Pages/Guide.php
+++ b/Pages/Guide.php
@@ -231,6 +231,40 @@ $PageSection = "Guide";
has it can load the same Garden account and mess stuff up.
+
+
+ Privacy and your data
+
+
+ The Garden API stores the data needed to keep your
+ account synchronized, including your Garden save,
+ username, profile colour, Dew totals, and the time
+ the server save was last updated. The server identifies
+ the account using a one-way hash derived from your
+ Account Key rather than storing the Account Key itself.
+
+
+
+ This browser stores your Account Key and a local copy
+ of your Garden save so the same account can keep working
+ between visits. If you set a username, some profile
+ information and statistics can also appear on the
+ public leaderboard.
+
+
+
+ The Profile
+ page lets you export a JSON copy of the account data
+ stored on the server, or permanently delete the account.
+ Deleting removes the server account and clears the
+ Account Key and local save from the browser where you
+ perform the deletion. Other devices have their own local
+ browser storage and cannot be cleared remotely.
+
+
diff --git a/Pages/Profile.php b/Pages/Profile.php
index 82d7772..0dbe0fe 100644
--- a/Pages/Profile.php
+++ b/Pages/Profile.php
@@ -320,6 +320,48 @@ $PageTitle = "Profile";
>
+
+
+ Account data
+
+
+ Download a JSON copy of the Garden account
+ data currently stored on the server. The
+ Account Key is not included in this export.
+
+
+
+
+ Export account data
+
+
+
+
+ Delete account
+
+
+ Permanently delete this Garden account from
+ the server and clear its Account Key and save
+ from this browser. This cannot be undone.
+
+
+
+
+ Delete account
+
+
+
+
diff --git a/Scripts/Garden/Account.js b/Scripts/Garden/Account.js
index 610c071..7d33eac 100644
--- a/Scripts/Garden/Account.js
+++ b/Scripts/Garden/Account.js
@@ -14,6 +14,16 @@ function StartAccount() {
"ExportAccountKeyButton"
);
+ const ExportDataButton =
+ document.getElementById(
+ "ExportAccountDataButton"
+ );
+
+ const DeleteAccountButton =
+ document.getElementById(
+ "DeleteAccountButton"
+ );
+
const ImportForm =
document.getElementById(
"ImportAccountForm"
@@ -45,6 +55,16 @@ function StartAccount() {
ExportAccountKey
);
+ ExportDataButton.addEventListener(
+ "click",
+ ExportAccountData
+ );
+
+ DeleteAccountButton.addEventListener(
+ "click",
+ DeleteAccount
+ );
+
ImportForm.addEventListener(
"submit",
SubmitAccountImport
@@ -272,6 +292,231 @@ function ExportAccountKey() {
}
+async function ExportAccountData() {
+ const Message =
+ document.getElementById(
+ "AccountDataMessage"
+ );
+
+ const Button =
+ document.getElementById(
+ "ExportAccountDataButton"
+ );
+
+
+ Message.textContent =
+ "Preparing account data...";
+
+ Button.disabled = true;
+
+
+ try {
+ const Result =
+ await RequestAccountData(
+ "Export"
+ );
+
+ if (
+ Result.Exists !== true ||
+ Result.Data === undefined
+ ) {
+ Message.textContent =
+ "No server account data exists to export.";
+
+ return;
+ }
+
+
+ const File =
+ new Blob(
+ [
+ JSON.stringify(
+ Result.Data,
+ null,
+ 4
+ ) + "\n"
+ ],
+ {
+ type:
+ "application/json;charset=utf-8"
+ }
+ );
+
+ const FileUrl =
+ URL.createObjectURL(
+ File
+ );
+
+ const Link =
+ document.createElement(
+ "a"
+ );
+
+ Link.href = FileUrl;
+ Link.download =
+ "SarahtoninGardenAccountData.json";
+
+
+ document.body.appendChild(
+ Link
+ );
+
+ Link.click();
+ Link.remove();
+
+
+ URL.revokeObjectURL(
+ FileUrl
+ );
+
+
+ Message.textContent =
+ "Account data exported.";
+ } catch (Error) {
+ console.error(
+ "Couldn't export account data:",
+ Error
+ );
+
+ Message.textContent =
+ "Couldn't export account data right now.";
+ } finally {
+ Button.disabled = false;
+ }
+}
+
+
+async function DeleteAccount() {
+ const Message =
+ document.getElementById(
+ "AccountDataMessage"
+ );
+
+ const Button =
+ document.getElementById(
+ "DeleteAccountButton"
+ );
+
+
+ const Confirmation =
+ window.prompt(
+ "Permanently delete this Garden account?\n\n" +
+ "The server save and this browser's local " +
+ "Account Key/save will be removed. This cannot " +
+ "be undone.\n\n" +
+ "Type DELETE to continue."
+ );
+
+
+ if (Confirmation !== "DELETE") {
+ Message.textContent =
+ Confirmation === null
+ ? "Account deletion cancelled."
+ : "Account deletion cancelled: type DELETE exactly.";
+
+ return;
+ }
+
+
+ Message.textContent =
+ "Deleting account...";
+
+ Button.disabled = true;
+
+
+ try {
+ await RequestAccountData(
+ "Delete",
+ {
+ Confirm: "DELETE"
+ }
+ );
+
+
+ localStorage.removeItem(
+ LocalSaveName
+ );
+
+ localStorage.removeItem(
+ SaveKeyName
+ );
+
+
+ Message.textContent =
+ "Account deleted. Returning home...";
+
+
+ window.location.replace(
+ "/"
+ );
+ } catch (Error) {
+ console.error(
+ "Couldn't delete account:",
+ Error
+ );
+
+ Message.textContent =
+ "Couldn't delete the account. Nothing was cleared locally.";
+
+ Button.disabled = false;
+ }
+}
+
+
+async function RequestAccountData(
+ Action,
+ ExtraData = {}
+) {
+ const Response =
+ await fetch(
+ ApiUrl + "/AccountData.php",
+ {
+ method: "POST",
+
+ headers: {
+ "Content-Type":
+ "application/json"
+ },
+
+ body: JSON.stringify({
+ SaveKey: GetSaveKey(),
+ Action: Action,
+ ...ExtraData
+ })
+ }
+ );
+
+
+ let Result;
+
+ try {
+ Result =
+ await Response.json();
+ } catch (Error) {
+ throw new Error(
+ "Account data API returned an invalid response."
+ );
+ }
+
+
+ if (
+ !Response.ok ||
+ Result.Success !== true
+ ) {
+ throw new Error(
+ Result.Error ??
+ (
+ "Account data API returned HTTP " +
+ Response.status
+ )
+ );
+ }
+
+
+ return Result;
+}
+
+
async function SubmitAccountImport(
Event
) {
diff --git a/Styles/Style.css b/Styles/Style.css
index 8424134..23526a9 100644
--- a/Styles/Style.css
+++ b/Styles/Style.css
@@ -973,6 +973,20 @@ input, button, textarea, select {
color: var(--Subtext1);
}
+.ProfileDangerText {
+ color: var(--Red);
+}
+
+.ProfileSection .ProfileDangerButton {
+ color: var(--Red);
+ border-color: var(--Red);
+}
+
+.ProfileSection .ProfileDangerButton:hover {
+ color: var(--Red);
+ border-color: var(--Red);
+}
+
.ProfileStatistics {
display: flex;
flex-direction: column;