✨ Account deletion
This commit is contained in:
parent
80b7812a87
commit
59bc9edd4b
@ -219,6 +219,24 @@ public class AccountController(
|
|||||||
return profile;
|
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 class StatusRequest
|
||||||
{
|
{
|
||||||
public StatusAttitude Attitude { get; set; }
|
public StatusAttitude Attitude { get; set; }
|
||||||
|
@ -8,17 +8,18 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
public class AccountService(
|
public class AccountService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
ICacheService cache,
|
MagicSpellService spells,
|
||||||
IStringLocalizerFactory factory
|
ICacheService cache
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
public const string AccountCachePrefix = "Account_";
|
public const string AccountCachePrefix = "Account_";
|
||||||
|
|
||||||
public async Task PurgeAccountCache(Account account)
|
public async Task PurgeAccountCache(Account account)
|
||||||
{
|
{
|
||||||
await cache.RemoveGroupAsync($"{AccountCachePrefix}{account.Id}");
|
await cache.RemoveGroupAsync($"{AccountCachePrefix}{account.Id}");
|
||||||
@ -46,6 +47,18 @@ public class AccountService(
|
|||||||
return profile?.Level;
|
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
|
/// Maintenance methods for server administrator
|
||||||
public async Task EnsureAccountProfileCreated()
|
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.Globalization;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using DysonNetwork.Sphere.Account.Email;
|
using DysonNetwork.Sphere.Email;
|
||||||
using DysonNetwork.Sphere.Pages.Emails;
|
using DysonNetwork.Sphere.Pages.Emails;
|
||||||
using DysonNetwork.Sphere.Permission;
|
using DysonNetwork.Sphere.Permission;
|
||||||
using DysonNetwork.Sphere.Resources.Localization;
|
using DysonNetwork.Sphere.Resources.Localization;
|
||||||
@ -24,9 +24,25 @@ public class MagicSpellService(
|
|||||||
MagicSpellType type,
|
MagicSpellType type,
|
||||||
Dictionary<string, object> meta,
|
Dictionary<string, object> meta,
|
||||||
Instant? expiredAt = null,
|
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 spellWord = _GenerateRandomString(128);
|
||||||
var spell = new MagicSpell
|
var spell = new MagicSpell
|
||||||
{
|
{
|
||||||
@ -79,7 +95,19 @@ public class MagicSpellService(
|
|||||||
new LandingEmailModel
|
new LandingEmailModel
|
||||||
{
|
{
|
||||||
Name = contact.Account.Name,
|
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;
|
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 Microsoft.AspNetCore.Components;
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account.Email;
|
namespace DysonNetwork.Sphere.Email;
|
||||||
|
|
||||||
public class EmailServiceConfiguration
|
public class EmailServiceConfiguration
|
||||||
{
|
{
|
@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Mvc.ViewEngines;
|
|||||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||||
using RouteData = Microsoft.AspNetCore.Routing.RouteData;
|
using RouteData = Microsoft.AspNetCore.Routing.RouteData;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account.Email;
|
namespace DysonNetwork.Sphere.Email;
|
||||||
|
|
||||||
public class RazorViewRenderer(
|
public class RazorViewRenderer(
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
@ -2,5 +2,4 @@ namespace DysonNetwork.Sphere.Localization;
|
|||||||
|
|
||||||
public class EmailResource
|
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>
|
<tr>
|
||||||
<td class="columns">
|
<td class="columns">
|
||||||
<div style="text-align: center;">
|
<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;">
|
style="background-color: #2563eb; color: #ffffff; padding: 0.75rem 1.5rem; border-radius: 0.5rem; font-weight: 600; text-decoration: none;">
|
||||||
@(Localizer["LandingButton1"])
|
@(Localizer["LandingButton1"])
|
||||||
</a>
|
</a>
|
||||||
@ -42,7 +42,7 @@
|
|||||||
<p style="color: #374151; margin: 0;">
|
<p style="color: #374151; margin: 0;">
|
||||||
@(LocalizerShared["EmailLinkHint"])
|
@(LocalizerShared["EmailLinkHint"])
|
||||||
<br>
|
<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>
|
||||||
<p style="color: #374151; margin: 0;">
|
<p style="color: #374151; margin: 0;">
|
||||||
@(Localizer["LandingPara4"])
|
@(Localizer["LandingPara4"])
|
||||||
@ -58,7 +58,7 @@
|
|||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter] public required string Name { get; set; }
|
[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<EmailResource> Localizer { get; set; } = null!;
|
||||||
[Inject] IStringLocalizer<SharedResource> LocalizerShared { get; set; } = null!;
|
[Inject] IStringLocalizer<SharedResource> LocalizerShared { get; set; } = null!;
|
||||||
|
@ -5,7 +5,7 @@ using System.Text.Json;
|
|||||||
using System.Threading.RateLimiting;
|
using System.Threading.RateLimiting;
|
||||||
using DysonNetwork.Sphere;
|
using DysonNetwork.Sphere;
|
||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Sphere.Account;
|
||||||
using DysonNetwork.Sphere.Account.Email;
|
using DysonNetwork.Sphere.Email;
|
||||||
using DysonNetwork.Sphere.Activity;
|
using DysonNetwork.Sphere.Activity;
|
||||||
using DysonNetwork.Sphere.Auth;
|
using DysonNetwork.Sphere.Auth;
|
||||||
using DysonNetwork.Sphere.Chat;
|
using DysonNetwork.Sphere.Chat;
|
||||||
|
@ -39,4 +39,25 @@
|
|||||||
<data name="EmailLandingTitle" xml:space="preserve">
|
<data name="EmailLandingTitle" xml:space="preserve">
|
||||||
<value>Confirm your registration</value>
|
<value>Confirm your registration</value>
|
||||||
</data>
|
</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>
|
</root>
|
@ -32,4 +32,25 @@
|
|||||||
<data name="EmailLandingTitle" xml:space="preserve">
|
<data name="EmailLandingTitle" xml:space="preserve">
|
||||||
<value>确认你的注册</value>
|
<value>确认你的注册</value>
|
||||||
</data>
|
</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>
|
</root>
|
Loading…
x
Reference in New Issue
Block a user