feat: self-service deletion API, reactivate account page
This commit is contained in:
parent
8a2ffd7d69
commit
96725cc304
13 changed files with 183 additions and 17 deletions
75
Foxnouns.Backend/Controllers/DeleteUserController.cs
Normal file
75
Foxnouns.Backend/Controllers/DeleteUserController.cs
Normal file
|
@ -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<DeleteUserController>();
|
||||
|
||||
[HttpPost("delete")]
|
||||
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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();
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -34,7 +34,7 @@ public class FlagsController(
|
|||
) : ApiControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[Limit(UsableBySuspendedUsers = true)]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
[Authorize("user.read_flags")]
|
||||
[ProducesResponseType<IEnumerable<PrideFlagResponse>>(statusCode: StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetFlagsAsync(CancellationToken ct = default)
|
||||
|
|
|
@ -44,7 +44,7 @@ public class MembersController(
|
|||
|
||||
[HttpGet]
|
||||
[ProducesResponseType<IEnumerable<PartialMember>>(StatusCodes.Status200OK)]
|
||||
[Limit(UsableBySuspendedUsers = true)]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
public async Task<IActionResult> GetMembersAsync(string userRef, CancellationToken ct = default)
|
||||
{
|
||||
User user = await db.ResolveUserAsync(userRef, CurrentToken, ct);
|
||||
|
@ -53,7 +53,7 @@ public class MembersController(
|
|||
|
||||
[HttpGet("{memberRef}")]
|
||||
[ProducesResponseType<MemberResponse>(StatusCodes.Status200OK)]
|
||||
[Limit(UsableBySuspendedUsers = true)]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
public async Task<IActionResult> GetMemberAsync(
|
||||
string userRef,
|
||||
string memberRef,
|
||||
|
|
|
@ -17,7 +17,7 @@ public class NotificationsController(
|
|||
{
|
||||
[HttpGet]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(UsableBySuspendedUsers = true)]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
public async Task<IActionResult> GetNotificationsAsync([FromQuery] bool all = false)
|
||||
{
|
||||
IQueryable<Notification> 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<IActionResult> AcknowledgeNotificationAsync(Snowflake id)
|
||||
{
|
||||
Notification? notification = await db.Notifications.FirstOrDefaultAsync(n =>
|
||||
|
|
|
@ -42,7 +42,7 @@ public class UsersController(
|
|||
|
||||
[HttpGet("{userRef}")]
|
||||
[ProducesResponseType<UserResponse>(statusCode: StatusCodes.Status200OK)]
|
||||
[Limit(UsableBySuspendedUsers = true)]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
public async Task<IActionResult> GetUserAsync(string userRef, CancellationToken ct = default)
|
||||
{
|
||||
User user = await db.ResolveUserAsync(userRef, CurrentToken, ct);
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue