feat(frontend): self-service delete, force delete pages
This commit is contained in:
parent
3f8f6d0f23
commit
e24c4f9b00
9 changed files with 298 additions and 11 deletions
|
@ -167,7 +167,34 @@
|
||||||
"force-delete-header": "Permanently delete your account",
|
"force-delete-header": "Permanently delete your account",
|
||||||
"reactivate-button": "Reactivate my account",
|
"reactivate-button": "Reactivate my account",
|
||||||
"reactivated-header": "Account reactivated",
|
"reactivated-header": "Account reactivated",
|
||||||
"reactivated-explanation": "Your account has been reactivated!"
|
"reactivated-explanation": "Your account has been reactivated!",
|
||||||
|
"force-delete-input-label": "To delete your account, type your username (@{{username}}), including the @, in the box below:",
|
||||||
|
"force-delete-export-hint": "If you haven't done so yet, we recommend you download an export of your data before continuing:",
|
||||||
|
"force-delete-export-link": "export your data",
|
||||||
|
"force-delete-irreversible": "This process is irreversible.",
|
||||||
|
"force-delete-username-available": "Your username will immediately be available for other users to take.",
|
||||||
|
"force-delete-immediate-delete": "This will immediately delete all of your profiles, including avatars.",
|
||||||
|
"force-delete-page-explanation": "Your account is currently pending deletion. If you want all your data deleted immediately, you can do so here.",
|
||||||
|
"force-delete-page-header": "Permanently delete your account",
|
||||||
|
"force-delete-checkbox-label": "Yes, I understand that my data will be permanently deleted and cannot be recovered.",
|
||||||
|
"force-delete-page-button": "Delete my account",
|
||||||
|
"account-is-deleted-header": "Your account has been deleted",
|
||||||
|
"account-is-deleted-permanently-description": "Your account has been deleted. Note that it may take a few minutes for all of your data to be removed.",
|
||||||
|
"account-is-deleted-close-page": "You may now close this page.",
|
||||||
|
"soft-delete-button": "Deactivate your account",
|
||||||
|
"soft-delete-hint": "If you want to delete your account, use the button below.",
|
||||||
|
"soft-delete-header": "Deactivate your account",
|
||||||
|
"force-delete-page-cancel": "I changed my mind, cancel",
|
||||||
|
"soft-delete-page-header": "Deactivate your account",
|
||||||
|
"soft-delete-page-explanation": "If you want to delete your account, you can do so here.",
|
||||||
|
"soft-delete-90-days": "Your account will be permanently deleted after 90 days.",
|
||||||
|
"soft-delete-can-reactivate": "If you change your mind, you can log in and go to the settings page at any time to reactivate your account.",
|
||||||
|
"soft-delete-keep-username": "You will keep your current username until your account is permanently deleted.",
|
||||||
|
"soft-delete-can-delete-permanently": "If you want to delete all your data early, you can do so by logging in and going to the settings page.",
|
||||||
|
"soft-delete-page-button": "Deactivate my account",
|
||||||
|
"soft-delete-input-label": "To deactivate your account, type your username (@{{username}}), including the @, in the box below:",
|
||||||
|
"account-is-deactivated-header": "Your account has been deactivated",
|
||||||
|
"account-is-deactivated-description": "Your account has been deactivated, and will be deleted in 90 days. If you change your mind, just log in again, and you will have the option to reactivate your account. If you want to delete your data immediately, you should also log in again, and you will be able to request immediate deletion."
|
||||||
},
|
},
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
|
|
|
@ -110,8 +110,9 @@
|
||||||
|
|
||||||
{#if !data.user.deleted}
|
{#if !data.user.deleted}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<h4>Delete your account</h4>
|
<h4>{$t("settings.soft-delete-header")}</h4>
|
||||||
<p></p>
|
<p>{$t("settings.soft-delete-hint")}</p>
|
||||||
|
<a href="/settings/delete" class="btn btn-danger">{$t("settings.soft-delete-button")}</a>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
41
Foxnouns.Frontend/src/routes/settings/delete/+page.server.ts
Normal file
41
Foxnouns.Frontend/src/routes/settings/delete/+page.server.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { fastRequest } from "$api";
|
||||||
|
import ApiError, { ErrorCode, type RawApiError } from "$api/error";
|
||||||
|
import { clearToken } from "$lib";
|
||||||
|
import { redirect } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export const load = async ({ parent }) => {
|
||||||
|
const { meUser } = await parent();
|
||||||
|
if (!meUser) redirect(303, "/");
|
||||||
|
|
||||||
|
if (meUser.deleted)
|
||||||
|
throw new ApiError({
|
||||||
|
message: "You cannot use this page.",
|
||||||
|
status: 403,
|
||||||
|
code: ErrorCode.Forbidden,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { user: meUser! };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
default: async ({ request, fetch, cookies }) => {
|
||||||
|
const body = await request.formData();
|
||||||
|
const username = body.get("username") as string;
|
||||||
|
const currentUsername = body.get("current-username") as string;
|
||||||
|
|
||||||
|
if (!username || username !== currentUsername) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
message: "Username doesn't match your username.",
|
||||||
|
status: 400,
|
||||||
|
code: ErrorCode.BadRequest,
|
||||||
|
} as RawApiError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await fastRequest("POST", "/self-delete/delete", { fetch, cookies, isInternal: true });
|
||||||
|
clearToken(cookies);
|
||||||
|
redirect(303, "/settings/delete/success");
|
||||||
|
},
|
||||||
|
};
|
55
Foxnouns.Frontend/src/routes/settings/delete/+page@.svelte
Normal file
55
Foxnouns.Frontend/src/routes/settings/delete/+page@.svelte
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
|
||||||
|
import { t } from "$lib/i18n";
|
||||||
|
import type { ActionData, PageData } from "./$types";
|
||||||
|
|
||||||
|
type Props = { data: PageData; form: ActionData };
|
||||||
|
let { data, form }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{$t("settings.soft-delete-page-header")} • pronouns.cc</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="w-lg-75 mx-auto">
|
||||||
|
<h3>{$t("settings.soft-delete-page-header")}</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{$t("settings.soft-delete-page-explanation")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>{$t("settings.soft-delete-90-days")}</li>
|
||||||
|
<li>
|
||||||
|
{$t("settings.soft-delete-can-reactivate")}
|
||||||
|
</li>
|
||||||
|
<li>{$t("settings.soft-delete-keep-username")}</li>
|
||||||
|
<li>
|
||||||
|
{$t("settings.soft-delete-can-delete-permanently")}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<FormStatusMarker {form} />
|
||||||
|
<p>
|
||||||
|
{$t("settings.soft-delete-input-label", { username: data.user.username })}
|
||||||
|
<input
|
||||||
|
class="form-control mt-2"
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
required
|
||||||
|
placeholder="@{data.user.username}"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<input type="hidden" value="@{data.user.username}" readonly name="current-username" />
|
||||||
|
</p>
|
||||||
|
<div class="btn-group mb-2">
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
{$t("settings.soft-delete-page-button")}
|
||||||
|
</button>
|
||||||
|
<a href="/settings" class="btn btn-secondary">{$t("settings.force-delete-page-cancel")}</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { t } from "$lib/i18n";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{$t("settings.soft-delete-page-header")} • pronouns.cc</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="w-lg-75 mx-auto">
|
||||||
|
<h3>{$t("settings.account-is-deactivated-header")}</h3>
|
||||||
|
<p>
|
||||||
|
{$t("settings.account-is-deactivated-description")}
|
||||||
|
</p>
|
||||||
|
<p>{$t("settings.account-is-deleted-close-page")}</p>
|
||||||
|
<p>
|
||||||
|
<a href="/" class="btn btn-secondary">{$t("error.back-to-main-page-button")}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { fastRequest } from "$api";
|
||||||
|
import ApiError, { ErrorCode, type RawApiError } from "$api/error";
|
||||||
|
import { clearToken } from "$lib";
|
||||||
|
import { redirect } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export const load = async ({ parent }) => {
|
||||||
|
const { meUser } = await parent();
|
||||||
|
if (!meUser) redirect(303, "/");
|
||||||
|
|
||||||
|
if (!meUser.deleted)
|
||||||
|
throw new ApiError({
|
||||||
|
message: "You cannot use this page.",
|
||||||
|
status: 403,
|
||||||
|
code: ErrorCode.Forbidden,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { user: meUser! };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
default: async ({ request, fetch, cookies }) => {
|
||||||
|
const body = await request.formData();
|
||||||
|
const username = body.get("username") as string;
|
||||||
|
const currentUsername = body.get("current-username") as string;
|
||||||
|
const confirmed = !!body.get("confirm");
|
||||||
|
|
||||||
|
if (!username || username !== currentUsername) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
message: "Username doesn't match your username.",
|
||||||
|
status: 400,
|
||||||
|
code: ErrorCode.BadRequest,
|
||||||
|
} as RawApiError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
message: "You must check the box to continue.",
|
||||||
|
status: 400,
|
||||||
|
code: ErrorCode.BadRequest,
|
||||||
|
} as RawApiError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await fastRequest("POST", "/self-delete/force", { fetch, cookies, isInternal: true });
|
||||||
|
clearToken(cookies);
|
||||||
|
redirect(303, "/settings/force-delete/success");
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,68 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
|
||||||
|
import { t } from "$lib/i18n";
|
||||||
|
import type { ActionData, PageData } from "./$types";
|
||||||
|
|
||||||
|
type Props = { data: PageData; form: ActionData };
|
||||||
|
let { data, form }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{$t("settings.force-delete-page-header")} • pronouns.cc</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="w-lg-75 mx-auto">
|
||||||
|
<h3>{$t("settings.force-delete-page-header")}</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{$t("settings.force-delete-page-explanation")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>{$t("settings.force-delete-immediate-delete")}</li>
|
||||||
|
<li>{$t("settings.force-delete-username-available")}</li>
|
||||||
|
<li><strong>{$t("settings.force-delete-irreversible")}</strong></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{$t("settings.force-delete-export-hint")}
|
||||||
|
<a href="/settings/export">{$t("settings.force-delete-export-link")}</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<FormStatusMarker {form} />
|
||||||
|
<p>
|
||||||
|
{$t("settings.force-delete-input-label", { username: data.user.username })}
|
||||||
|
<input
|
||||||
|
class="form-control mt-2"
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
required
|
||||||
|
placeholder="@{data.user.username}"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<input type="hidden" value="@{data.user.username}" readonly name="current-username" />
|
||||||
|
</p>
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
value="yes"
|
||||||
|
name="confirm"
|
||||||
|
id="confirm"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="confirm">
|
||||||
|
{$t("settings.force-delete-checkbox-label")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group mt-3 mb-2">
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
{$t("settings.force-delete-page-button")}
|
||||||
|
</button>
|
||||||
|
<a href="/settings" class="btn btn-secondary">{$t("settings.force-delete-page-cancel")}</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { t } from "$lib/i18n";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{$t("settings.force-delete-page-header")} • pronouns.cc</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="w-lg-75 mx-auto">
|
||||||
|
<h3>{$t("settings.account-is-deleted-header")}</h3>
|
||||||
|
<p>
|
||||||
|
{$t("settings.account-is-deleted-permanently-description")}
|
||||||
|
</p>
|
||||||
|
<p>{$t("settings.account-is-deleted-close-page")}</p>
|
||||||
|
<p>
|
||||||
|
<a href="/" class="btn btn-secondary">{$t("error.back-to-main-page-button")}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -6,7 +6,8 @@
|
||||||
let { data }: Props = $props();
|
let { data }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-lg-75 mx-auto">
|
<div class="container">
|
||||||
|
<div class="w-lg-75 mx-auto">
|
||||||
<h3>{$t("settings.reactivated-header")}</h3>
|
<h3>{$t("settings.reactivated-header")}</h3>
|
||||||
|
|
||||||
<p>{$t("settings.reactivated-explanation")}</p>
|
<p>{$t("settings.reactivated-explanation")}</p>
|
||||||
|
@ -17,4 +18,5 @@
|
||||||
{$t("error.back-to-profile-button")}
|
{$t("error.back-to-profile-button")}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue