✨ Account deletion
This commit is contained in:
parent
80b7812a87
commit
59bc9edd4b
@ -219,6 +219,24 @@ public class AccountController(
|
||||
return profile;
|
||||
}
|
||||
|
||||
[HttpDelete("me")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult> 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; }
|
||||
|
@ -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<string, object>(),
|
||||
SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
|
||||
preventRepeat: true
|
||||
);
|
||||
await spells.NotifyMagicSpell(spell);
|
||||
}
|
||||
|
||||
/// Maintenance methods for server administrator
|
||||
public async Task EnsureAccountProfileCreated()
|
||||
{
|
||||
|
@ -1,7 +0,0 @@
|
||||
namespace DysonNetwork.Sphere.Account.Email;
|
||||
|
||||
public class LandingEmailModel
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public required string VerificationLink { get; set; }
|
||||
}
|
@ -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<string, object> 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<AccountDeletionEmail, AccountDeletionEmailModel>(
|
||||
contact.Account.Name,
|
||||
contact.Content,
|
||||
localizer["EmailAccountDeletionTitle"],
|
||||
new AccountDeletionEmailModel
|
||||
{
|
||||
Name = contact.Account.Name,
|
||||
Link = link
|
||||
}
|
||||
);
|
||||
break;
|
||||
|
13
DysonNetwork.Sphere/Email/EmailModels.cs
Normal file
13
DysonNetwork.Sphere/Email/EmailModels.cs
Normal file
@ -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; }
|
||||
}
|
@ -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
|
||||
{
|
@ -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,
|
@ -2,5 +2,4 @@ namespace DysonNetwork.Sphere.Localization;
|
||||
|
||||
public class EmailResource
|
||||
{
|
||||
|
||||
}
|
65
DysonNetwork.Sphere/Pages/Emails/AccountDeletionEmail.razor
Normal file
65
DysonNetwork.Sphere/Pages/Emails/AccountDeletionEmail.razor
Normal file
@ -0,0 +1,65 @@
|
||||
@using DysonNetwork.Sphere.Localization
|
||||
@using Microsoft.Extensions.Localization
|
||||
@using EmailResource = DysonNetwork.Sphere.Localization.EmailResource
|
||||
|
||||
<EmailLayout>
|
||||
<table class="container">
|
||||
<tr>
|
||||
<td class="columns">
|
||||
<h1 style="font-size: 1.875rem; font-weight: 700; color: #111827; margin: 0; text-align: center;">
|
||||
@(Localizer["AccountDeletionHeader"])
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="columns">
|
||||
<p style="color: #374151; margin: 0;">
|
||||
@(Localizer["AccountDeletionPara1"]) @@@Name,
|
||||
</p>
|
||||
<p style="color: #374151; margin: 0;">
|
||||
@(Localizer["AccountDeletionPara2"])
|
||||
</p>
|
||||
<p style="color: #374151; margin: 0;">
|
||||
@(Localizer["AccountDeletionPara3"])
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="columns">
|
||||
<div style="text-align: center;">
|
||||
<a href="@Link" target="_blank"
|
||||
style="background-color: #2563eb; color: #ffffff; padding: 0.75rem 1.5rem; border-radius: 0.5rem; font-weight: 600; text-decoration: none;">
|
||||
@(Localizer["AccountDeletionButton"])
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="columns">
|
||||
<p style="color: #374151; margin: 0;">
|
||||
@(LocalizerShared["EmailLinkHint"])
|
||||
<br>
|
||||
<a href="@Link" style="color: #2563eb; word-break: break-all;">@Link</a>
|
||||
</p>
|
||||
<p style="color: #374151; margin: 0;">
|
||||
@(Localizer["AccountDeletionPara4"])
|
||||
</p>
|
||||
<p style="color: #374151; margin: 2rem 0 0 0;">
|
||||
@(LocalizerShared["EmailFooter1"]) <br />
|
||||
@(LocalizerShared["EmailFooter2"])
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</EmailLayout>
|
||||
|
||||
@code {
|
||||
[Parameter] public required string Name { get; set; }
|
||||
[Parameter] public required string Link { get; set; }
|
||||
|
||||
[Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!;
|
||||
[Inject] IStringLocalizer<SharedResource> LocalizerShared { get; set; } = null!;
|
||||
}
|
@ -29,7 +29,7 @@
|
||||
<tr>
|
||||
<td class="columns">
|
||||
<div style="text-align: center;">
|
||||
<a href="@VerificationLink" target="_blank"
|
||||
<a href="@Link" target="_blank"
|
||||
style="background-color: #2563eb; color: #ffffff; padding: 0.75rem 1.5rem; border-radius: 0.5rem; font-weight: 600; text-decoration: none;">
|
||||
@(Localizer["LandingButton1"])
|
||||
</a>
|
||||
@ -42,7 +42,7 @@
|
||||
<p style="color: #374151; margin: 0;">
|
||||
@(LocalizerShared["EmailLinkHint"])
|
||||
<br>
|
||||
<a href="@VerificationLink" style="color: #2563eb; word-break: break-all;">@VerificationLink</a>
|
||||
<a href="@Link" style="color: #2563eb; word-break: break-all;">@Link</a>
|
||||
</p>
|
||||
<p style="color: #374151; margin: 0;">
|
||||
@(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<EmailResource> Localizer { get; set; } = null!;
|
||||
[Inject] IStringLocalizer<SharedResource> LocalizerShared { get; set; } = null!;
|
||||
|
@ -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;
|
||||
|
@ -39,4 +39,25 @@
|
||||
<data name="EmailLandingTitle" xml:space="preserve">
|
||||
<value>Confirm your registration</value>
|
||||
</data>
|
||||
<data name="AccountDeletionHeader" xml:space="preserve">
|
||||
<value>Account Deletion Confirmation</value>
|
||||
</data>
|
||||
<data name="AccountDeletionPara1" xml:space="preserve">
|
||||
<value>Dear, </value>
|
||||
</data>
|
||||
<data name="AccountDeletionPara2" xml:space="preserve">
|
||||
<value>We've received a request to delete your Solar Network account. We're sorry to see you go.</value>
|
||||
</data>
|
||||
<data name="AccountDeletionPara3" xml:space="preserve">
|
||||
<value>To confirm your account deletion, please click the button below. Please note that this action is permanent and cannot be undone.</value>
|
||||
</data>
|
||||
<data name="AccountDeletionButton" xml:space="preserve">
|
||||
<value>Confirm Account Deletion</value>
|
||||
</data>
|
||||
<data name="AccountDeletionPara4" xml:space="preserve">
|
||||
<value>If you did not request to delete your account, please ignore this email or contact our support team immediately.</value>
|
||||
</data>
|
||||
<data name="EmailAccountDeletionTitle" xml:space="preserve">
|
||||
<value>Confirm your account deletion</value>
|
||||
</data>
|
||||
</root>
|
@ -32,4 +32,25 @@
|
||||
<data name="EmailLandingTitle" xml:space="preserve">
|
||||
<value>确认你的注册</value>
|
||||
</data>
|
||||
<data name="AccountDeletionHeader" xml:space="preserve">
|
||||
<value>账户删除确认</value>
|
||||
</data>
|
||||
<data name="AccountDeletionPara1" xml:space="preserve">
|
||||
<value>尊敬的 </value>
|
||||
</data>
|
||||
<data name="AccountDeletionPara2" xml:space="preserve">
|
||||
<value>我们收到了删除您 Solar Network 账户的请求。我们很遗憾看到您的离开。</value>
|
||||
</data>
|
||||
<data name="AccountDeletionPara3" xml:space="preserve">
|
||||
<value>请点击下方按钮确认删除您的账户。请注意,此操作是永久性的,无法撤销。</value>
|
||||
</data>
|
||||
<data name="AccountDeletionButton" xml:space="preserve">
|
||||
<value>确认删除账户</value>
|
||||
</data>
|
||||
<data name="AccountDeletionPara4" xml:space="preserve">
|
||||
<value>如果您并未请求删除账户,请忽略此邮件或立即联系我们的支持团队。</value>
|
||||
</data>
|
||||
<data name="EmailAccountDeletionTitle" xml:space="preserve">
|
||||
<value>确认删除您的账户</value>
|
||||
</data>
|
||||
</root>
|
Loading…
x
Reference in New Issue
Block a user