From 59bc9edd4bf9aaf1d497303548edcadc7bd3cf54 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 24 May 2025 23:29:36 +0800 Subject: [PATCH] :sparkles: Account deletion --- .../Account/AccountController.cs | 18 +++++ DysonNetwork.Sphere/Account/AccountService.cs | 19 +++++- .../Account/Email/LandingEmail.cs | 7 -- .../Account/MagicSpellService.cs | 34 +++++++++- DysonNetwork.Sphere/Email/EmailModels.cs | 13 ++++ .../{Account => }/Email/EmailService.cs | 2 +- .../{Account => }/Email/RazorViewRenderer.cs | 2 +- .../Localization/EmailResource.cs | 1 - .../Pages/Emails/AccountDeletionEmail.razor | 65 +++++++++++++++++++ .../Pages/Emails/LandingEmail.razor | 6 +- DysonNetwork.Sphere/Program.cs | 2 +- .../Resources/Localization/EmailResource.resx | 21 ++++++ .../Localization/EmailResource.zh-hans.resx | 21 ++++++ 13 files changed, 191 insertions(+), 20 deletions(-) delete mode 100644 DysonNetwork.Sphere/Account/Email/LandingEmail.cs create mode 100644 DysonNetwork.Sphere/Email/EmailModels.cs rename DysonNetwork.Sphere/{Account => }/Email/EmailService.cs (98%) rename DysonNetwork.Sphere/{Account => }/Email/RazorViewRenderer.cs (97%) create mode 100644 DysonNetwork.Sphere/Pages/Emails/AccountDeletionEmail.razor diff --git a/DysonNetwork.Sphere/Account/AccountController.cs b/DysonNetwork.Sphere/Account/AccountController.cs index 05f0d27..6172457 100644 --- a/DysonNetwork.Sphere/Account/AccountController.cs +++ b/DysonNetwork.Sphere/Account/AccountController.cs @@ -219,6 +219,24 @@ public class AccountController( return profile; } + [HttpDelete("me")] + [Authorize] + public async Task RequestDeleteAccount() + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + + try + { + await accounts.RequestAccountDeletion(currentUser); + } + catch (InvalidOperationException) + { + return BadRequest("You already requested account deletion within 24 hours."); + } + + return Ok(); + } + public class StatusRequest { public StatusAttitude Attitude { get; set; } diff --git a/DysonNetwork.Sphere/Account/AccountService.cs b/DysonNetwork.Sphere/Account/AccountService.cs index 4f9669f..e55dfb4 100644 --- a/DysonNetwork.Sphere/Account/AccountService.cs +++ b/DysonNetwork.Sphere/Account/AccountService.cs @@ -8,17 +8,18 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging.Abstractions; +using NodaTime; namespace DysonNetwork.Sphere.Account; public class AccountService( AppDatabase db, - ICacheService cache, - IStringLocalizerFactory factory + MagicSpellService spells, + ICacheService cache ) { public const string AccountCachePrefix = "Account_"; - + public async Task PurgeAccountCache(Account account) { await cache.RemoveGroupAsync($"{AccountCachePrefix}{account.Id}"); @@ -46,6 +47,18 @@ public class AccountService( return profile?.Level; } + public async Task RequestAccountDeletion(Account account) + { + var spell = await spells.CreateMagicSpell( + account, + MagicSpellType.AccountRemoval, + new Dictionary(), + SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)), + preventRepeat: true + ); + await spells.NotifyMagicSpell(spell); + } + /// Maintenance methods for server administrator public async Task EnsureAccountProfileCreated() { diff --git a/DysonNetwork.Sphere/Account/Email/LandingEmail.cs b/DysonNetwork.Sphere/Account/Email/LandingEmail.cs deleted file mode 100644 index c8b1596..0000000 --- a/DysonNetwork.Sphere/Account/Email/LandingEmail.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace DysonNetwork.Sphere.Account.Email; - -public class LandingEmailModel -{ - public required string Name { get; set; } - public required string VerificationLink { get; set; } -} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Account/MagicSpellService.cs b/DysonNetwork.Sphere/Account/MagicSpellService.cs index 8bd9a7f..1b42b5d 100644 --- a/DysonNetwork.Sphere/Account/MagicSpellService.cs +++ b/DysonNetwork.Sphere/Account/MagicSpellService.cs @@ -1,6 +1,6 @@ using System.Globalization; using System.Security.Cryptography; -using DysonNetwork.Sphere.Account.Email; +using DysonNetwork.Sphere.Email; using DysonNetwork.Sphere.Pages.Emails; using DysonNetwork.Sphere.Permission; using DysonNetwork.Sphere.Resources.Localization; @@ -24,9 +24,25 @@ public class MagicSpellService( MagicSpellType type, Dictionary meta, Instant? expiredAt = null, - Instant? affectedAt = null + Instant? affectedAt = null, + bool preventRepeat = false ) { + if (preventRepeat) + { + var now = SystemClock.Instance.GetCurrentInstant(); + var existingSpell = await db.MagicSpells + .Where(s => s.AccountId == account.Id) + .Where(s => s.Type == type) + .Where(s => s.ExpiresAt == null || s.ExpiresAt > now) + .FirstOrDefaultAsync(); + + if (existingSpell != null) + { + throw new InvalidOperationException($"Account already has an active magic spell of type {type}"); + } + } + var spellWord = _GenerateRandomString(128); var spell = new MagicSpell { @@ -79,7 +95,19 @@ public class MagicSpellService( new LandingEmailModel { Name = contact.Account.Name, - VerificationLink = link + Link = link + } + ); + break; + case MagicSpellType.AccountRemoval: + await email.SendTemplatedEmailAsync( + contact.Account.Name, + contact.Content, + localizer["EmailAccountDeletionTitle"], + new AccountDeletionEmailModel + { + Name = contact.Account.Name, + Link = link } ); break; diff --git a/DysonNetwork.Sphere/Email/EmailModels.cs b/DysonNetwork.Sphere/Email/EmailModels.cs new file mode 100644 index 0000000..2b03e37 --- /dev/null +++ b/DysonNetwork.Sphere/Email/EmailModels.cs @@ -0,0 +1,13 @@ +namespace DysonNetwork.Sphere.Email; + +public class LandingEmailModel +{ + public required string Name { get; set; } + public required string Link { get; set; } +} + +public class AccountDeletionEmailModel +{ + public required string Name { get; set; } + public required string Link { get; set; } +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Account/Email/EmailService.cs b/DysonNetwork.Sphere/Email/EmailService.cs similarity index 98% rename from DysonNetwork.Sphere/Account/Email/EmailService.cs rename to DysonNetwork.Sphere/Email/EmailService.cs index 5a49815..f533e57 100644 --- a/DysonNetwork.Sphere/Account/Email/EmailService.cs +++ b/DysonNetwork.Sphere/Email/EmailService.cs @@ -2,7 +2,7 @@ using MailKit.Net.Smtp; using Microsoft.AspNetCore.Components; using MimeKit; -namespace DysonNetwork.Sphere.Account.Email; +namespace DysonNetwork.Sphere.Email; public class EmailServiceConfiguration { diff --git a/DysonNetwork.Sphere/Account/Email/RazorViewRenderer.cs b/DysonNetwork.Sphere/Email/RazorViewRenderer.cs similarity index 97% rename from DysonNetwork.Sphere/Account/Email/RazorViewRenderer.cs rename to DysonNetwork.Sphere/Email/RazorViewRenderer.cs index 964cd5a..72331f9 100644 --- a/DysonNetwork.Sphere/Account/Email/RazorViewRenderer.cs +++ b/DysonNetwork.Sphere/Email/RazorViewRenderer.cs @@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; using RouteData = Microsoft.AspNetCore.Routing.RouteData; -namespace DysonNetwork.Sphere.Account.Email; +namespace DysonNetwork.Sphere.Email; public class RazorViewRenderer( IServiceProvider serviceProvider, diff --git a/DysonNetwork.Sphere/Localization/EmailResource.cs b/DysonNetwork.Sphere/Localization/EmailResource.cs index 19d588e..c18e407 100644 --- a/DysonNetwork.Sphere/Localization/EmailResource.cs +++ b/DysonNetwork.Sphere/Localization/EmailResource.cs @@ -2,5 +2,4 @@ namespace DysonNetwork.Sphere.Localization; public class EmailResource { - } \ No newline at end of file diff --git a/DysonNetwork.Sphere/Pages/Emails/AccountDeletionEmail.razor b/DysonNetwork.Sphere/Pages/Emails/AccountDeletionEmail.razor new file mode 100644 index 0000000..0abeafe --- /dev/null +++ b/DysonNetwork.Sphere/Pages/Emails/AccountDeletionEmail.razor @@ -0,0 +1,65 @@ +@using DysonNetwork.Sphere.Localization +@using Microsoft.Extensions.Localization +@using EmailResource = DysonNetwork.Sphere.Localization.EmailResource + + + + + + + + + + + + + + + + + + +
+

+ @(Localizer["AccountDeletionHeader"]) +

+
+

+ @(Localizer["AccountDeletionPara1"]) @@@Name, +

+

+ @(Localizer["AccountDeletionPara2"]) +

+

+ @(Localizer["AccountDeletionPara3"]) +

+
+ +
+

+ @(LocalizerShared["EmailLinkHint"]) +
+ @Link +

+

+ @(Localizer["AccountDeletionPara4"]) +

+

+ @(LocalizerShared["EmailFooter1"])
+ @(LocalizerShared["EmailFooter2"]) +

+
+
+ +@code { + [Parameter] public required string Name { get; set; } + [Parameter] public required string Link { get; set; } + + [Inject] IStringLocalizer Localizer { get; set; } = null!; + [Inject] IStringLocalizer LocalizerShared { get; set; } = null!; +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Pages/Emails/LandingEmail.razor b/DysonNetwork.Sphere/Pages/Emails/LandingEmail.razor index 437ae06..1009e4d 100644 --- a/DysonNetwork.Sphere/Pages/Emails/LandingEmail.razor +++ b/DysonNetwork.Sphere/Pages/Emails/LandingEmail.razor @@ -29,7 +29,7 @@
- @(Localizer["LandingButton1"]) @@ -42,7 +42,7 @@

@(LocalizerShared["EmailLinkHint"])
- @VerificationLink + @Link

@(Localizer["LandingPara4"]) @@ -58,7 +58,7 @@ @code { [Parameter] public required string Name { get; set; } - [Parameter] public required string VerificationLink { get; set; } + [Parameter] public required string Link { get; set; } [Inject] IStringLocalizer Localizer { get; set; } = null!; [Inject] IStringLocalizer LocalizerShared { get; set; } = null!; diff --git a/DysonNetwork.Sphere/Program.cs b/DysonNetwork.Sphere/Program.cs index 4c16c3c..390b3ab 100644 --- a/DysonNetwork.Sphere/Program.cs +++ b/DysonNetwork.Sphere/Program.cs @@ -5,7 +5,7 @@ using System.Text.Json; using System.Threading.RateLimiting; using DysonNetwork.Sphere; using DysonNetwork.Sphere.Account; -using DysonNetwork.Sphere.Account.Email; +using DysonNetwork.Sphere.Email; using DysonNetwork.Sphere.Activity; using DysonNetwork.Sphere.Auth; using DysonNetwork.Sphere.Chat; diff --git a/DysonNetwork.Sphere/Resources/Localization/EmailResource.resx b/DysonNetwork.Sphere/Resources/Localization/EmailResource.resx index ac09e68..e19d3bf 100644 --- a/DysonNetwork.Sphere/Resources/Localization/EmailResource.resx +++ b/DysonNetwork.Sphere/Resources/Localization/EmailResource.resx @@ -39,4 +39,25 @@ Confirm your registration + + Account Deletion Confirmation + + + Dear, + + + We've received a request to delete your Solar Network account. We're sorry to see you go. + + + To confirm your account deletion, please click the button below. Please note that this action is permanent and cannot be undone. + + + Confirm Account Deletion + + + If you did not request to delete your account, please ignore this email or contact our support team immediately. + + + Confirm your account deletion + \ No newline at end of file diff --git a/DysonNetwork.Sphere/Resources/Localization/EmailResource.zh-hans.resx b/DysonNetwork.Sphere/Resources/Localization/EmailResource.zh-hans.resx index bc06cb4..46356d3 100644 --- a/DysonNetwork.Sphere/Resources/Localization/EmailResource.zh-hans.resx +++ b/DysonNetwork.Sphere/Resources/Localization/EmailResource.zh-hans.resx @@ -32,4 +32,25 @@ 确认你的注册 + + 账户删除确认 + + + 尊敬的 + + + 我们收到了删除您 Solar Network 账户的请求。我们很遗憾看到您的离开。 + + + 请点击下方按钮确认删除您的账户。请注意,此操作是永久性的,无法撤销。 + + + 确认删除账户 + + + 如果您并未请求删除账户,请忽略此邮件或立即联系我们的支持团队。 + + + 确认删除您的账户 + \ No newline at end of file