diff --git a/api/Controllers/UserController.cs b/api/Controllers/UserController.cs index fa1fc8b1..80fb88bd 100644 --- a/api/Controllers/UserController.cs +++ b/api/Controllers/UserController.cs @@ -25,6 +25,12 @@ public async Task GetUser(string username, [FromQuery] string id) return user is not null ? Ok(user) : NotFound(); } + [HttpDelete("{username}")] + public async Task DeleteUser(string username) + { + await _userProvider.DeleteUserAsync(username); + } + [HttpGet("{username}/pastes")] public async Task> GetUserOwnedPastes(string username, [FromQuery] PageRequest pageRequest, [FromQuery] string tag) { diff --git a/api/Models/Paste.cs b/api/Models/Paste.cs index d710f3b8..ffc93d7a 100644 --- a/api/Models/Paste.cs +++ b/api/Models/Paste.cs @@ -23,7 +23,7 @@ public class Paste public string OwnerId { get; set; } public bool Private { get; set; } - + public bool Pinned { get; set; } public List Tags { get; set; } = new(); diff --git a/api/Services/AuthService.cs b/api/Services/AuthService.cs index 9f95c70a..bfe413f5 100644 --- a/api/Services/AuthService.cs +++ b/api/Services/AuthService.cs @@ -4,7 +4,6 @@ using JWT.Builder; using JWT.Exceptions; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.ObjectPool; using MongoDB.Driver; using pastemyst.Exceptions; using pastemyst.Models; diff --git a/api/Services/UserProvider.cs b/api/Services/UserProvider.cs index 1ffd2060..04c528ac 100644 --- a/api/Services/UserProvider.cs +++ b/api/Services/UserProvider.cs @@ -17,18 +17,24 @@ public interface IUserProvider public Task> GetOwnedPastesAsync(string username, string tag, bool pinnedOnly, PageRequest pageRequest); public Task> GetTagsAsync(string username); + + public Task DeleteUserAsync(string username); } public class UserProvider : IUserProvider { private readonly IUserContext _userContext; private readonly IPasteService _pasteService; + private readonly IActionLogger _actionLogger; + private readonly IImageService _imageService; private readonly IMongoService _mongo; - public UserProvider(IUserContext userContext, IPasteService pasteService, IMongoService mongo) + public UserProvider(IUserContext userContext, IPasteService pasteService, IMongoService mongo, IActionLogger actionLogger, IImageService imageService) { _userContext = userContext; _pasteService = pasteService; + _actionLogger = actionLogger; + _imageService = imageService; _mongo = mongo; } @@ -76,7 +82,7 @@ public async Task> GetOwnedPastesAsync(string username, if (tag is not null) { - if (_userContext.Self != user) + if (_userContext.Self.Id != user.Id) { throw new HttpException(HttpStatusCode.Unauthorized, "You must be authorized to view paste tags."); } @@ -93,7 +99,7 @@ public async Task> GetOwnedPastesAsync(string username, var totalItems = await _mongo.Pastes.CountDocumentsAsync(filter); var totalPages = (int)Math.Ceiling((float)totalItems / pageRequest.PageSize); - if (_userContext.Self != user) + if (_userContext.Self.Id != user.Id) { pastes.ForEach(p => p.Tags = new()); } @@ -123,7 +129,7 @@ public async Task> GetTagsAsync(string username) var user = await GetByUsernameAsync(username); - if (_userContext.Self != user) + if (_userContext.Self.Id != user.Id) throw new HttpException(HttpStatusCode.Unauthorized, "You can only fetch your own tags."); var filter = Builders.Filter.Eq(p => p.OwnerId, user.Id); @@ -133,4 +139,27 @@ public async Task> GetTagsAsync(string username) .Distinct() .ToList(); } + + public async Task DeleteUserAsync(string username) + { + if (!_userContext.IsLoggedIn()) + throw new HttpException(HttpStatusCode.Unauthorized, "You must be authorized to delete your account."); + + var user = await GetByUsernameAsync(username); + + if (_userContext.Self.Id != user.Id) + throw new HttpException(HttpStatusCode.Unauthorized, "You can delete only your account."); + + await _imageService.DeleteAsync(user.AvatarId); + + await _mongo.Pastes.DeleteManyAsync(p => p.OwnerId == user.Id); + await _mongo.Users.DeleteOneAsync(u => u.Id == user.Id); + + // Delete all stars of this user + var starsFilter = Builders.Filter.AnyEq(p => p.Stars, user.Id); + var starsUpdate = Builders.Update.Pull(p => p.Stars, user.Id); + await _mongo.Pastes.UpdateManyAsync(starsFilter, starsUpdate); + + await _actionLogger.LogActionAsync(ActionLogType.UserDeleted, user.Id); + } } diff --git a/client/src/lib/api/user.ts b/client/src/lib/api/user.ts index ba709540..78fe96b4 100755 --- a/client/src/lib/api/user.ts +++ b/client/src/lib/api/user.ts @@ -43,3 +43,12 @@ export const getUserTags = async (fetchFunc: FetchFunc, username: string): Promi return []; }; + +export const deleteUser = async (username: string): Promise => { + const res = await fetch(`${env.PUBLIC_API_BASE}/users/${username}`, { + method: "delete", + credentials: "include" + }); + + return res.ok; +}; diff --git a/client/src/routes/settings/profile/+page.svelte b/client/src/routes/settings/profile/+page.svelte index bac4f99d..22e823a0 100644 --- a/client/src/routes/settings/profile/+page.svelte +++ b/client/src/routes/settings/profile/+page.svelte @@ -2,7 +2,7 @@ import { env } from "$env/dynamic/public"; import { getSelf } from "$lib/api/auth"; import { updateUserSettings } from "$lib/api/settings"; - import { getUserByUsername } from "$lib/api/user"; + import { getUserByUsername, deleteUser } from "$lib/api/user"; import Checkbox from "$lib/Checkbox.svelte"; import { usernameRegex } from "$lib/patterns"; import { currentUserStore } from "$lib/stores"; @@ -95,6 +95,21 @@ const saveSettings = async () => { await updateUserSettings(fetch, data.userSettings); }; + + const onAccountDelete = async () => { + // TODO: better confirm dialog + if ( + confirm( + "are you sure you want to delete your account? this will delete your account and all the associated data, including the pastes" + ) + ) { + const ok = await deleteUser(data.self.username); + + if (!ok) return; + + window.location.href = `${env.PUBLIC_API_BASE}/auth/logout`; + } + }; @@ -203,6 +218,11 @@ toggle whether to show only your pinned pastes or all your public pastes on your profile + + + + this will delete your account and all the associated data, including the pastes +