From 8a2ffd7d69dc630acf5db26109095ea26bafb92e Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 18 Dec 2024 21:38:39 +0100 Subject: [PATCH 1/5] feat(frontend): preference cheatsheet --- .../profile/PreferenceCheatsheet.svelte | 29 +++++++++++++++++++ .../src/routes/@[username]/+page.svelte | 3 +- .../@[username]/[memberName]/+page.svelte | 3 +- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 Foxnouns.Frontend/src/lib/components/profile/PreferenceCheatsheet.svelte diff --git a/Foxnouns.Frontend/src/lib/components/profile/PreferenceCheatsheet.svelte b/Foxnouns.Frontend/src/lib/components/profile/PreferenceCheatsheet.svelte new file mode 100644 index 0000000..5e03a4e --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/profile/PreferenceCheatsheet.svelte @@ -0,0 +1,29 @@ + + +
+ +
diff --git a/Foxnouns.Frontend/src/routes/@[username]/+page.svelte b/Foxnouns.Frontend/src/routes/@[username]/+page.svelte index 0562274..e431e9b 100644 --- a/Foxnouns.Frontend/src/routes/@[username]/+page.svelte +++ b/Foxnouns.Frontend/src/routes/@[username]/+page.svelte @@ -9,6 +9,7 @@ import Paginator from "$components/Paginator.svelte"; import MemberCard from "$components/profile/user/MemberCard.svelte"; import ProfileButtons from "$components/profile/ProfileButtons.svelte"; + import PreferenceCheatsheet from "$components/profile/PreferenceCheatsheet.svelte"; type Props = { data: PageData }; let { data }: Props = $props(); @@ -28,7 +29,7 @@ - + - + Date: Thu, 19 Dec 2024 16:13:05 +0100 Subject: [PATCH 2/5] feat: self-service deletion API, reactivate account page --- .../Controllers/DeleteUserController.cs | 75 +++++++++++++++++++ .../Controllers/ExportsController.cs | 1 + .../Controllers/FlagsController.cs | 2 +- .../Controllers/MembersController.cs | 4 +- .../Controllers/NotificationsController.cs | 4 +- .../Controllers/UsersController.cs | 2 +- .../Middleware/LimitMiddleware.cs | 4 +- .../src/lib/components/Navbar.svelte | 8 +- .../src/lib/i18n/locales/en.json | 18 ++++- .../src/routes/report/[id]/+page.svelte | 2 - .../src/routes/settings/+page.svelte | 37 ++++++++- .../settings/reactivate/+page.server.ts | 23 ++++++ .../routes/settings/reactivate/+page@.svelte | 20 +++++ 13 files changed, 183 insertions(+), 17 deletions(-) create mode 100644 Foxnouns.Backend/Controllers/DeleteUserController.cs create mode 100644 Foxnouns.Frontend/src/routes/settings/reactivate/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/settings/reactivate/+page@.svelte diff --git a/Foxnouns.Backend/Controllers/DeleteUserController.cs b/Foxnouns.Backend/Controllers/DeleteUserController.cs new file mode 100644 index 0000000..b611c35 --- /dev/null +++ b/Foxnouns.Backend/Controllers/DeleteUserController.cs @@ -0,0 +1,75 @@ +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Middleware; +using Microsoft.AspNetCore.Mvc; +using NodaTime; + +namespace Foxnouns.Backend.Controllers; + +[Route("/api/internal/self-delete")] +[Authorize("*")] +[ApiExplorerSettings(IgnoreApi = true)] +public class DeleteUserController(DatabaseContext db, IClock clock, ILogger logger) + : ApiControllerBase +{ + private readonly ILogger _logger = logger.ForContext(); + + [HttpPost("delete")] + public async Task DeleteSelfAsync() + { + _logger.Information( + "User {UserId} has requested their account to be deleted", + CurrentUser!.Id + ); + + CurrentUser.Deleted = true; + CurrentUser.DeletedAt = clock.GetCurrentInstant(); + + db.Update(CurrentUser); + await db.SaveChangesAsync(); + return NoContent(); + } + + [HttpPost("force")] + [Limit(UsableByDeletedUsers = true)] + public async Task ForceDeleteAsync() + { + if (!CurrentUser!.Deleted) + throw new ApiError.BadRequest("Your account isn't deleted."); + + _logger.Information( + "User {UserId} has requested an early full delete of their account", + CurrentUser.Id + ); + + // This is the easiest way to force delete a user, don't judge me + CurrentUser.DeletedAt = clock.GetCurrentInstant() - Duration.FromDays(365); + db.Update(CurrentUser); + await db.SaveChangesAsync(); + return NoContent(); + } + + [HttpPost("undelete")] + [Limit(UsableByDeletedUsers = true)] + public async Task UndeleteSelfAsync() + { + if (!CurrentUser!.Deleted) + throw new ApiError.BadRequest("Your account isn't deleted."); + if (CurrentUser!.DeletedBy != null) + { + throw new ApiError.BadRequest( + "Your account has been suspended and can't be reactivated by yourself." + ); + } + + _logger.Information( + "User {UserId} has requested to undelete their account", + CurrentUser.Id + ); + + CurrentUser.Deleted = false; + CurrentUser.DeletedAt = null; + db.Update(CurrentUser); + await db.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/Foxnouns.Backend/Controllers/ExportsController.cs b/Foxnouns.Backend/Controllers/ExportsController.cs index 315efbc..9d23e41 100644 --- a/Foxnouns.Backend/Controllers/ExportsController.cs +++ b/Foxnouns.Backend/Controllers/ExportsController.cs @@ -26,6 +26,7 @@ namespace Foxnouns.Backend.Controllers; [Route("/api/internal/data-exports")] [Authorize("identify")] +[Limit(UsableByDeletedUsers = true)] [ApiExplorerSettings(IgnoreApi = true)] public class ExportsController( ILogger logger, diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs index 2b145ac..e4f4c77 100644 --- a/Foxnouns.Backend/Controllers/FlagsController.cs +++ b/Foxnouns.Backend/Controllers/FlagsController.cs @@ -34,7 +34,7 @@ public class FlagsController( ) : ApiControllerBase { [HttpGet] - [Limit(UsableBySuspendedUsers = true)] + [Limit(UsableByDeletedUsers = true)] [Authorize("user.read_flags")] [ProducesResponseType>(statusCode: StatusCodes.Status200OK)] public async Task GetFlagsAsync(CancellationToken ct = default) diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 9b94b30..0d145d8 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -44,7 +44,7 @@ public class MembersController( [HttpGet] [ProducesResponseType>(StatusCodes.Status200OK)] - [Limit(UsableBySuspendedUsers = true)] + [Limit(UsableByDeletedUsers = true)] public async Task GetMembersAsync(string userRef, CancellationToken ct = default) { User user = await db.ResolveUserAsync(userRef, CurrentToken, ct); @@ -53,7 +53,7 @@ public class MembersController( [HttpGet("{memberRef}")] [ProducesResponseType(StatusCodes.Status200OK)] - [Limit(UsableBySuspendedUsers = true)] + [Limit(UsableByDeletedUsers = true)] public async Task GetMemberAsync( string userRef, string memberRef, diff --git a/Foxnouns.Backend/Controllers/NotificationsController.cs b/Foxnouns.Backend/Controllers/NotificationsController.cs index 8bea907..f258b3c 100644 --- a/Foxnouns.Backend/Controllers/NotificationsController.cs +++ b/Foxnouns.Backend/Controllers/NotificationsController.cs @@ -17,7 +17,7 @@ public class NotificationsController( { [HttpGet] [Authorize("user.moderation")] - [Limit(UsableBySuspendedUsers = true)] + [Limit(UsableByDeletedUsers = true)] public async Task GetNotificationsAsync([FromQuery] bool all = false) { IQueryable query = db.Notifications.Where(n => n.TargetId == CurrentUser!.Id); @@ -31,7 +31,7 @@ public class NotificationsController( [HttpPut("{id}/ack")] [Authorize("user.moderation")] - [Limit(UsableBySuspendedUsers = true)] + [Limit(UsableByDeletedUsers = true)] public async Task AcknowledgeNotificationAsync(Snowflake id) { Notification? notification = await db.Notifications.FirstOrDefaultAsync(n => diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index d567bdb..e909ef8 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -42,7 +42,7 @@ public class UsersController( [HttpGet("{userRef}")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - [Limit(UsableBySuspendedUsers = true)] + [Limit(UsableByDeletedUsers = true)] public async Task GetUserAsync(string userRef, CancellationToken ct = default) { User user = await db.ResolveUserAsync(userRef, CurrentToken, ct); diff --git a/Foxnouns.Backend/Middleware/LimitMiddleware.cs b/Foxnouns.Backend/Middleware/LimitMiddleware.cs index 1c5f522..6092041 100644 --- a/Foxnouns.Backend/Middleware/LimitMiddleware.cs +++ b/Foxnouns.Backend/Middleware/LimitMiddleware.cs @@ -41,7 +41,7 @@ public class LimitMiddleware : IMiddleware return; } - if (token?.User.Deleted == true && !attribute.UsableBySuspendedUsers) + if (token?.User.Deleted == true && !attribute.UsableByDeletedUsers) throw new ApiError.Forbidden("Deleted users cannot access this endpoint."); if (attribute.RequireAdmin && token?.User.Role != UserRole.Admin) @@ -62,7 +62,7 @@ public class LimitMiddleware : IMiddleware [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class LimitAttribute : Attribute { - public bool UsableBySuspendedUsers { get; init; } + public bool UsableByDeletedUsers { get; init; } public bool RequireAdmin { get; init; } public bool RequireModerator { get; init; } } diff --git a/Foxnouns.Frontend/src/lib/components/Navbar.svelte b/Foxnouns.Frontend/src/lib/components/Navbar.svelte index 4330312..967d688 100644 --- a/Foxnouns.Frontend/src/lib/components/Navbar.svelte +++ b/Foxnouns.Frontend/src/lib/components/Navbar.svelte @@ -25,12 +25,14 @@ {#if user.suspended} {$t("nav.suspended-account-hint")}
- {$t("nav.appeal-suspension-link")} + {$t("nav.delete-permanently-link")} • + {$t("nav.appeal-suspension-link")} • + {$t("nav.export-link")} {:else} {$t("nav.deleted-account-hint")}
- {$t("nav.reactivate-account-link")} • - {$t("nav.delete-permanently-link")} + {$t("nav.reactivate-or-delete-link")} • + {$t("nav.export-link")} {/if} {/if} diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index 16f5527..87a9cdd 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -5,9 +5,11 @@ "settings": "Settings", "suspended-account-hint": "Your account has been suspended. Your profile has been hidden and you will not be able to change any settings.", "appeal-suspension-link": "I want to appeal", - "deleted-account-hint": "You have requested deletion of your account. If you want to reactivate it, click the link below.", + "deleted-account-hint": "You have requested deletion of your account.", "reactivate-account-link": "Reactivate account", - "delete-permanently-link": "I want my account deleted permanently" + "delete-permanently-link": "I want my account deleted permanently", + "reactivate-or-delete-link": "I want to reactivate my account or delete all my data", + "export-link": "I want to export a copy of my data" }, "avatar-tooltip": "Avatar for {{name}}", "profile": { @@ -155,7 +157,17 @@ "flag-description-placeholder": "Description", "flag-name-placeholder": "Name", "flag-upload-success": "Successfully uploaded your flag! It may take a few seconds before it's saved.", - "custom-preferences-title": "Custom preferences" + "custom-preferences-title": "Custom preferences", + "change-username-header": "Change your username", + "force-delete-button": "Delete my account permanently", + "force-delete-warning": "This is irreversible. Consider exporting a copy of your data before doing this.", + "force-delete-explanation": "Your account is currently pending deletion. If you want your data deleted permanently, use the button below.", + "reactivate-explanation": "Your account is currently pending deletion. If you want to cancel this and keep using your account, use the link below.", + "reactivate-header": "Reactivate your account", + "force-delete-header": "Permanently delete your account", + "reactivate-button": "Reactivate my account", + "reactivated-header": "Account reactivated", + "reactivated-explanation": "Your account has been reactivated!" }, "yes": "Yes", "no": "No", diff --git a/Foxnouns.Frontend/src/routes/report/[id]/+page.svelte b/Foxnouns.Frontend/src/routes/report/[id]/+page.svelte index 24458ab..a4ce0ac 100644 --- a/Foxnouns.Frontend/src/routes/report/[id]/+page.svelte +++ b/Foxnouns.Frontend/src/routes/report/[id]/+page.svelte @@ -16,8 +16,6 @@ data.member ? `/@${data.user.username}/${data.member.name}` : `/@${data.user.username}`, ); - console.log(data.user, !!data.member); - let reasons = $derived.by(() => { const reasons = []; for (const value of Object.values(ReportReason)) { diff --git a/Foxnouns.Frontend/src/routes/settings/+page.svelte b/Foxnouns.Frontend/src/routes/settings/+page.svelte index 74b4a49..6963b31 100644 --- a/Foxnouns.Frontend/src/routes/settings/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/+page.svelte @@ -18,9 +18,37 @@

{$t("settings.general-information-tab")}

+{#if data.user.deleted} +
+ {#if !data.user.suspended} +
+

{$t("settings.reactivate-header")}

+

+ {$t("settings.reactivate-explanation")} +

+ + {$t("settings.reactivate-button")} + +
+ {/if} +
+

{$t("settings.force-delete-header")}

+

+ {$t("settings.force-delete-explanation")} + + {$t("settings.force-delete-warning")} + +

+ + {$t("settings.force-delete-button")} + +
+
+{/if} +
-
Change your username
+
{$t("settings.change-username-header")}
@@ -80,6 +108,13 @@ {$t("settings.force-log-out-button")}
+{#if !data.user.deleted} +
+

Delete your account

+

+
+{/if} +

{$t("settings.table-title")}

diff --git a/Foxnouns.Frontend/src/routes/settings/reactivate/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/reactivate/+page.server.ts new file mode 100644 index 0000000..0ac29ae --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/reactivate/+page.server.ts @@ -0,0 +1,23 @@ +import { fastRequest } from "$api"; +import ApiError, { ErrorCode } from "$api/error"; +import { redirect } from "@sveltejs/kit"; + +export const load = async ({ parent, fetch, cookies }) => { + const { meUser } = await parent(); + if (!meUser) redirect(303, "/"); + + if (meUser.suspended || !meUser.deleted) + throw new ApiError({ + message: "You cannot use this page.", + status: 403, + code: ErrorCode.Forbidden, + }); + + await fastRequest("POST", "/self-delete/undelete", { + fetch, + cookies, + isInternal: true, + }); + + return { user: meUser! }; +}; diff --git a/Foxnouns.Frontend/src/routes/settings/reactivate/+page@.svelte b/Foxnouns.Frontend/src/routes/settings/reactivate/+page@.svelte new file mode 100644 index 0000000..acfb617 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/reactivate/+page@.svelte @@ -0,0 +1,20 @@ + + +
+

{$t("settings.reactivated-header")}

+ +

{$t("settings.reactivated-explanation")}

+ + +
From 661c3eab0f91221eaf45d687541146f5f232281b Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 19 Dec 2024 16:19:27 +0100 Subject: [PATCH 3/5] fix(backend): save data exports as data-export.zip change the random base 64 to a directory rather than part of the filename, so that users downloading their exports aren't greeted with a completely incomprehensible file in their downloads folder --- Foxnouns.Backend/Controllers/ExportsController.cs | 2 +- Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs | 2 +- Foxnouns.Backend/Services/DataCleanupService.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Foxnouns.Backend/Controllers/ExportsController.cs b/Foxnouns.Backend/Controllers/ExportsController.cs index 9d23e41..7f40625 100644 --- a/Foxnouns.Backend/Controllers/ExportsController.cs +++ b/Foxnouns.Backend/Controllers/ExportsController.cs @@ -58,7 +58,7 @@ public class ExportsController( } private string ExportUrl(Snowflake userId, string filename) => - $"{config.MediaBaseUrl}/data-exports/{userId}/{filename}.zip"; + $"{config.MediaBaseUrl}/data-exports/{userId}/{filename}/data-export.zip"; [HttpPost] public async Task QueueDataExportAsync() diff --git a/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs b/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs index 4d9e1b0..becd858 100644 --- a/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs +++ b/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs @@ -220,5 +220,5 @@ public class CreateDataExportInvocable( } private static string ExportPath(Snowflake userId, string b64) => - $"data-exports/{userId}/{b64}.zip"; + $"data-exports/{userId}/{b64}/data-export.zip"; } diff --git a/Foxnouns.Backend/Services/DataCleanupService.cs b/Foxnouns.Backend/Services/DataCleanupService.cs index 3d60462..ee60bb8 100644 --- a/Foxnouns.Backend/Services/DataCleanupService.cs +++ b/Foxnouns.Backend/Services/DataCleanupService.cs @@ -128,5 +128,5 @@ public class DataCleanupService( } private static string ExportPath(Snowflake userId, string b64) => - $"data-exports/{userId}/{b64}.zip"; + $"data-exports/{userId}/{b64}/data-export.zip"; } From 3f8f6d0f237d7a89a7e4c2bb8ac2ffce29eec0f0 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 19 Dec 2024 16:24:17 +0100 Subject: [PATCH 4/5] delete stray console.log --- Foxnouns.Frontend/src/routes/settings/flags/+page.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/Foxnouns.Frontend/src/routes/settings/flags/+page.svelte b/Foxnouns.Frontend/src/routes/settings/flags/+page.svelte index 7a3a58b..155d014 100644 --- a/Foxnouns.Frontend/src/routes/settings/flags/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/flags/+page.svelte @@ -48,7 +48,6 @@ const idx = flags.findIndex((f) => f.id === id); if (idx === -1) return; - console.log("yippee"); flags[idx] = { ...flags[idx], name, description }; } catch (e) { log.error("Could not update flag %s:", id, e); From e24c4f9b0033fc82092a7c5c87bd56792d549cb6 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 19 Dec 2024 17:15:50 +0100 Subject: [PATCH 5/5] feat(frontend): self-service delete, force delete pages --- .../src/lib/i18n/locales/en.json | 29 +++++++- .../src/routes/settings/+page.svelte | 5 +- .../routes/settings/delete/+page.server.ts | 41 +++++++++++ .../src/routes/settings/delete/+page@.svelte | 55 +++++++++++++++ .../settings/delete/success/+page@.svelte | 20 ++++++ .../settings/force-delete/+page.server.ts | 53 +++++++++++++++ .../settings/force-delete/+page@.svelte | 68 +++++++++++++++++++ .../force-delete/success/+page@.svelte | 20 ++++++ .../routes/settings/reactivate/+page@.svelte | 18 ++--- 9 files changed, 298 insertions(+), 11 deletions(-) create mode 100644 Foxnouns.Frontend/src/routes/settings/delete/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/settings/delete/+page@.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/delete/success/+page@.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/force-delete/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/settings/force-delete/+page@.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/force-delete/success/+page@.svelte diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index 87a9cdd..73472ad 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -167,7 +167,34 @@ "force-delete-header": "Permanently delete your account", "reactivate-button": "Reactivate my account", "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", "no": "No", diff --git a/Foxnouns.Frontend/src/routes/settings/+page.svelte b/Foxnouns.Frontend/src/routes/settings/+page.svelte index 6963b31..d5f90ac 100644 --- a/Foxnouns.Frontend/src/routes/settings/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/+page.svelte @@ -110,8 +110,9 @@ {#if !data.user.deleted}
-

Delete your account

-

+

{$t("settings.soft-delete-header")}

+

{$t("settings.soft-delete-hint")}

+ {$t("settings.soft-delete-button")}
{/if} diff --git a/Foxnouns.Frontend/src/routes/settings/delete/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/delete/+page.server.ts new file mode 100644 index 0000000..4ac1d19 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/delete/+page.server.ts @@ -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"); + }, +}; diff --git a/Foxnouns.Frontend/src/routes/settings/delete/+page@.svelte b/Foxnouns.Frontend/src/routes/settings/delete/+page@.svelte new file mode 100644 index 0000000..cb5fec2 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/delete/+page@.svelte @@ -0,0 +1,55 @@ + + + + {$t("settings.soft-delete-page-header")} • pronouns.cc + + +
+
+

{$t("settings.soft-delete-page-header")}

+ +

+ {$t("settings.soft-delete-page-explanation")} +

+ +
    +
  • {$t("settings.soft-delete-90-days")}
  • +
  • + {$t("settings.soft-delete-can-reactivate")} +
  • +
  • {$t("settings.soft-delete-keep-username")}
  • +
  • + {$t("settings.soft-delete-can-delete-permanently")} +
  • +
+ + + +

+ {$t("settings.soft-delete-input-label", { username: data.user.username })} + + +

+
+ + {$t("settings.force-delete-page-cancel")} +
+ +
+
diff --git a/Foxnouns.Frontend/src/routes/settings/delete/success/+page@.svelte b/Foxnouns.Frontend/src/routes/settings/delete/success/+page@.svelte new file mode 100644 index 0000000..9b35518 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/delete/success/+page@.svelte @@ -0,0 +1,20 @@ + + + + {$t("settings.soft-delete-page-header")} • pronouns.cc + + +
+
+

{$t("settings.account-is-deactivated-header")}

+

+ {$t("settings.account-is-deactivated-description")} +

+

{$t("settings.account-is-deleted-close-page")}

+

+ {$t("error.back-to-main-page-button")} +

+
+
diff --git a/Foxnouns.Frontend/src/routes/settings/force-delete/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/force-delete/+page.server.ts new file mode 100644 index 0000000..1816ce7 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/force-delete/+page.server.ts @@ -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"); + }, +}; diff --git a/Foxnouns.Frontend/src/routes/settings/force-delete/+page@.svelte b/Foxnouns.Frontend/src/routes/settings/force-delete/+page@.svelte new file mode 100644 index 0000000..4b39e62 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/force-delete/+page@.svelte @@ -0,0 +1,68 @@ + + + + {$t("settings.force-delete-page-header")} • pronouns.cc + + +
+
+

{$t("settings.force-delete-page-header")}

+ +

+ {$t("settings.force-delete-page-explanation")} +

+ +
    +
  • {$t("settings.force-delete-immediate-delete")}
  • +
  • {$t("settings.force-delete-username-available")}
  • +
  • {$t("settings.force-delete-irreversible")}
  • +
+ +

+ {$t("settings.force-delete-export-hint")} + {$t("settings.force-delete-export-link")} +

+ +
+ +

+ {$t("settings.force-delete-input-label", { username: data.user.username })} + + +

+
+ + +
+
+ + {$t("settings.force-delete-page-cancel")} +
+ +
+
diff --git a/Foxnouns.Frontend/src/routes/settings/force-delete/success/+page@.svelte b/Foxnouns.Frontend/src/routes/settings/force-delete/success/+page@.svelte new file mode 100644 index 0000000..7fd5bd5 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/force-delete/success/+page@.svelte @@ -0,0 +1,20 @@ + + + + {$t("settings.force-delete-page-header")} • pronouns.cc + + +
+
+

{$t("settings.account-is-deleted-header")}

+

+ {$t("settings.account-is-deleted-permanently-description")} +

+

{$t("settings.account-is-deleted-close-page")}

+

+ {$t("error.back-to-main-page-button")} +

+
+
diff --git a/Foxnouns.Frontend/src/routes/settings/reactivate/+page@.svelte b/Foxnouns.Frontend/src/routes/settings/reactivate/+page@.svelte index acfb617..cf70c4b 100644 --- a/Foxnouns.Frontend/src/routes/settings/reactivate/+page@.svelte +++ b/Foxnouns.Frontend/src/routes/settings/reactivate/+page@.svelte @@ -6,15 +6,17 @@ let { data }: Props = $props(); -
-

{$t("settings.reactivated-header")}

+
+
+

{$t("settings.reactivated-header")}

-

{$t("settings.reactivated-explanation")}

+

{$t("settings.reactivated-explanation")}

-