From 99da765a1c734b5e467d73407bff35dc04307ffc Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Fri, 7 Jun 2024 15:35:31 +0500 Subject: [PATCH] api: use wamp services instead of forwarding http requests for internal apis --- Notesnook.API/Controllers/UsersController.cs | 71 ++++---- Notesnook.API/Interfaces/IUserService.cs | 5 +- Notesnook.API/Models/DeleteAccountForm.cs | 13 ++ Notesnook.API/Services/UserService.cs | 172 ++++++++---------- .../Interfaces/IUserAccountService.cs | 16 ++ .../Interfaces/IUserSubscriptionService.cs | 13 ++ .../Controllers/AccountController.cs | 77 ++------ .../Models/DeleteAccountForm.cs | 33 ---- .../Services/UserAccountService.cs | 56 ++++++ .../Services/UserService.cs | 7 + 10 files changed, 230 insertions(+), 233 deletions(-) create mode 100644 Notesnook.API/Models/DeleteAccountForm.cs create mode 100644 Streetwriters.Common/Interfaces/IUserAccountService.cs create mode 100644 Streetwriters.Common/Interfaces/IUserSubscriptionService.cs delete mode 100644 Streetwriters.Identity/Models/DeleteAccountForm.cs create mode 100644 Streetwriters.Identity/Services/UserAccountService.cs diff --git a/Notesnook.API/Controllers/UsersController.cs b/Notesnook.API/Controllers/UsersController.cs index 647e110..a4df0f8 100644 --- a/Notesnook.API/Controllers/UsersController.cs +++ b/Notesnook.API/Controllers/UsersController.cs @@ -18,35 +18,23 @@ along with this program. If not, see . */ using System; -using System.Net.Http; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Timeouts; using Microsoft.AspNetCore.Mvc; using Notesnook.API.Interfaces; +using Notesnook.API.Models; using Notesnook.API.Models.Responses; using Streetwriters.Common; -using Streetwriters.Common.Extensions; -using Streetwriters.Common.Models; namespace Notesnook.API.Controllers { [ApiController] [Authorize] [Route("users")] - public class UsersController : ControllerBase + public class UsersController(IUserService UserService) : ControllerBase { - private readonly HttpClient httpClient; - private readonly IHttpContextAccessor HttpContextAccessor; - private IUserService UserService { get; set; } - public UsersController(IUserService userService, IHttpContextAccessor accessor) - { - httpClient = new HttpClient(); - HttpContextAccessor = accessor; - UserService = userService; - } - [HttpPost] [AllowAnonymous] public async Task Signup() @@ -66,20 +54,35 @@ namespace Notesnook.API.Controllers [HttpGet] public async Task GetUser() { - UserResponse response = await UserService.GetUserAsync(); - if (!response.Success) return BadRequest(response); - return Ok(response); + var userId = User.FindFirstValue("sub"); + try + { + UserResponse response = await UserService.GetUserAsync(userId); + if (!response.Success) return BadRequest(response); + return Ok(response); + } + catch (Exception ex) + { + await Slogger.Error(nameof(GetUser), "Couldn't get user for id.", userId, ex.ToString()); + return BadRequest(new { error = ex.Message }); + } } [HttpPatch] public async Task UpdateUser([FromBody] UserResponse user) { - UserResponse response = await UserService.GetUserAsync(false); - - if (user.AttachmentsKey != null) - await UserService.SetUserAttachmentsKeyAsync(response.UserId, user.AttachmentsKey); - - return Ok(); + var userId = User.FindFirstValue("sub"); + try + { + if (user.AttachmentsKey != null) + await UserService.SetUserAttachmentsKeyAsync(userId, user.AttachmentsKey); + return Ok(); + } + catch (Exception ex) + { + await Slogger.Error(nameof(GetUser), "Couldn't update user with id.", userId, ex.ToString()); + return BadRequest(new { error = ex.Message }); + } } [HttpPost("reset")] @@ -93,24 +96,20 @@ namespace Notesnook.API.Controllers } [HttpPost("delete")] - public async Task Delete() + [RequestTimeout(5 * 60 * 1000)] + public async Task Delete([FromForm] DeleteAccountForm form) { + var userId = this.User.FindFirstValue("sub"); + var jti = User.FindFirstValue("jti"); try { - var userId = this.User.FindFirstValue("sub"); - - if (await UserService.DeleteUserAsync(userId, User.FindFirstValue("jti"))) - { - Response response = await this.httpClient.ForwardAsync(HttpContextAccessor, $"{Servers.IdentityServer}/account/unregister", HttpMethod.Post); - if (!response.Success) return BadRequest(); - - return Ok(); - } - return BadRequest(); + await UserService.DeleteUserAsync(userId, jti, form.Password); + return Ok(); } catch (Exception ex) { - return BadRequest(ex.Message); + await Slogger.Error(nameof(GetUser), "Couldn't delete user with id.", userId, ex.ToString()); + return BadRequest(new { error = ex.Message }); } } } diff --git a/Notesnook.API/Interfaces/IUserService.cs b/Notesnook.API/Interfaces/IUserService.cs index dc84d08..04498f6 100644 --- a/Notesnook.API/Interfaces/IUserService.cs +++ b/Notesnook.API/Interfaces/IUserService.cs @@ -27,9 +27,10 @@ namespace Notesnook.API.Interfaces public interface IUserService { Task CreateUserAsync(); - Task DeleteUserAsync(string userId, string jti); + Task DeleteUserAsync(string userId); + Task DeleteUserAsync(string userId, string jti, string password); Task ResetUserAsync(string userId, bool removeAttachments); - Task GetUserAsync(bool repair = true); + Task GetUserAsync(string userId); Task SetUserAttachmentsKeyAsync(string userId, IEncrypted key); } } \ No newline at end of file diff --git a/Notesnook.API/Models/DeleteAccountForm.cs b/Notesnook.API/Models/DeleteAccountForm.cs new file mode 100644 index 0000000..a504fda --- /dev/null +++ b/Notesnook.API/Models/DeleteAccountForm.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace Notesnook.API.Models +{ + public class DeleteAccountForm + { + [Required] + public string Password + { + get; set; + } + } +} \ No newline at end of file diff --git a/Notesnook.API/Services/UserService.cs b/Notesnook.API/Services/UserService.cs index a122540..d52b973 100644 --- a/Notesnook.API/Services/UserService.cs +++ b/Notesnook.API/Services/UserService.cs @@ -92,10 +92,11 @@ namespace Notesnook.API.Services await Slogger.Info(nameof(CreateUserAsync), "New user created.", JsonSerializer.Serialize(response)); } - public async Task GetUserAsync(bool repair = true) + public async Task GetUserAsync(string userId) { - UserResponse response = await httpClient.ForwardAsync(this.HttpContextAccessor, $"{Servers.IdentityServer.ToString()}/account", HttpMethod.Get); - if (!response.Success) return response; + var userService = await WampServers.IdentityServer.GetServiceAsync(IdentityServerTopics.UserAccountServiceTopic); + + var user = await userService.GetUserAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User not found."); ISubscription subscription = null; if (Constants.IS_SELF_HOSTED) @@ -105,7 +106,7 @@ namespace Notesnook.API.Services AppId = ApplicationType.NOTESNOOK, Provider = SubscriptionProvider.STREETWRITERS, Type = SubscriptionType.PREMIUM, - UserId = response.UserId, + UserId = user.UserId, StartDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), // this date doesn't matter as the subscription is static. ExpiryDate = DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeMilliseconds() @@ -113,117 +114,92 @@ namespace Notesnook.API.Services } else { - SubscriptionResponse subscriptionResponse = await httpClient.ForwardAsync(this.HttpContextAccessor, $"{Servers.SubscriptionServer}/subscriptions", HttpMethod.Get); - if (repair && subscriptionResponse.StatusCode == 404) - { - await Slogger.Error(nameof(GetUserAsync), "Repairing user subscription.", JsonSerializer.Serialize(response)); - // user was partially created. We should continue the process here. - await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.CreateSubscriptionTopic, new CreateSubscriptionMessage - { - AppId = ApplicationType.NOTESNOOK, - Provider = SubscriptionProvider.STREETWRITERS, - Type = SubscriptionType.TRIAL, - UserId = response.UserId, - StartTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), - ExpiryTime = DateTimeOffset.UtcNow.AddDays(7).ToUnixTimeMilliseconds() - }); - // just a dummy object - subscriptionResponse.Subscription = new Subscription - { - AppId = ApplicationType.NOTESNOOK, - Provider = SubscriptionProvider.STREETWRITERS, - Type = SubscriptionType.TRIAL, - UserId = response.UserId, - StartDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), - ExpiryDate = DateTimeOffset.UtcNow.AddDays(7).ToUnixTimeMilliseconds() - }; - } - subscription = subscriptionResponse.Subscription; + var subscriptionService = await WampServers.SubscriptionServer.GetServiceAsync(SubscriptionServerTopics.UserSubscriptionServiceTopic); + subscription = await subscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId); } - var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == response.UserId); - if (repair && userSettings == null) + var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == user.UserId) ?? throw new Exception("User settings not found."); + return new UserResponse { - await Slogger.Error(nameof(GetUserAsync), "Repairing user settings.", JsonSerializer.Serialize(response)); - userSettings = new UserSettings - { - UserId = response.UserId, - LastSynced = 0, - Salt = GetSalt() - }; - await Repositories.UsersSettings.InsertAsync(userSettings); - } - response.AttachmentsKey = userSettings.AttachmentsKey; - response.Salt = userSettings.Salt; - response.Subscription = subscription; - return response; + UserId = user.UserId, + Email = user.Email, + IsEmailConfirmed = user.IsEmailConfirmed, + MarketingConsent = user.MarketingConsent, + MFA = user.MFA, + PhoneNumber = user.PhoneNumber, + AttachmentsKey = userSettings.AttachmentsKey, + Salt = userSettings.Salt, + Subscription = subscription, + Success = true, + StatusCode = 200 + }; } public async Task SetUserAttachmentsKeyAsync(string userId, IEncrypted key) { - var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId); + var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId) ?? throw new Exception("User not found."); userSettings.AttachmentsKey = (EncryptedData)key; await Repositories.UsersSettings.UpdateAsync(userSettings.Id, userSettings); } - public async Task DeleteUserAsync(string userId, string jti) + public async Task DeleteUserAsync(string userId) { - try + new SyncDeviceService(new SyncDevice(ref userId, ref userId)).ResetDevices(); + + var cc = new CancellationTokenSource(); + + Repositories.Notes.DeleteByUserId(userId); + Repositories.Notebooks.DeleteByUserId(userId); + Repositories.Shortcuts.DeleteByUserId(userId); + Repositories.Contents.DeleteByUserId(userId); + Repositories.Settings.DeleteByUserId(userId); + Repositories.LegacySettings.DeleteByUserId(userId); + Repositories.Attachments.DeleteByUserId(userId); + Repositories.Reminders.DeleteByUserId(userId); + Repositories.Relations.DeleteByUserId(userId); + Repositories.Colors.DeleteByUserId(userId); + Repositories.Tags.DeleteByUserId(userId); + Repositories.Vaults.DeleteByUserId(userId); + Repositories.UsersSettings.Delete((u) => u.UserId == userId); + Repositories.Monographs.DeleteMany((m) => m.UserId == userId); + + var result = await unit.Commit(); + await Slogger.Info(nameof(DeleteUserAsync), "User data deleted", userId, result.ToString()); + if (!result) throw new Exception("Could not delete user data."); + + if (!Constants.IS_SELF_HOSTED) { - await Slogger.Info(nameof(DeleteUserAsync), "Deleting user account", userId); - - new SyncDeviceService(new SyncDevice(ref userId, ref userId)).ResetDevices(); - - var cc = new CancellationTokenSource(); - - Repositories.Notes.DeleteByUserId(userId); - Repositories.Notebooks.DeleteByUserId(userId); - Repositories.Shortcuts.DeleteByUserId(userId); - Repositories.Contents.DeleteByUserId(userId); - Repositories.Settings.DeleteByUserId(userId); - Repositories.LegacySettings.DeleteByUserId(userId); - Repositories.Attachments.DeleteByUserId(userId); - Repositories.Reminders.DeleteByUserId(userId); - Repositories.Relations.DeleteByUserId(userId); - Repositories.Colors.DeleteByUserId(userId); - Repositories.Tags.DeleteByUserId(userId); - Repositories.Vaults.DeleteByUserId(userId); - Repositories.UsersSettings.Delete((u) => u.UserId == userId); - Repositories.Monographs.DeleteMany((m) => m.UserId == userId); - - var result = await unit.Commit(); - await Slogger.Info(nameof(DeleteUserAsync), "User account deleted", userId, result.ToString()); - if (!result) return false; - - if (!Constants.IS_SELF_HOSTED) + await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.DeleteSubscriptionTopic, new DeleteSubscriptionMessage { - await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.DeleteSubscriptionTopic, new DeleteSubscriptionMessage - { - AppId = ApplicationType.NOTESNOOK, - UserId = userId - }); - } - - await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage - { - SendToAll = false, - OriginTokenId = jti, - UserId = userId, - Message = new Message - { - Type = "logout", - Data = JsonSerializer.Serialize(new { reason = "Account deleted." }) - } + AppId = ApplicationType.NOTESNOOK, + UserId = userId }); + } - await S3Service.DeleteDirectoryAsync(userId); - return result; - } - catch (Exception ex) + await S3Service.DeleteDirectoryAsync(userId); + } + + public async Task DeleteUserAsync(string userId, string jti, string password) + { + await Slogger.Info(nameof(DeleteUserAsync), "Deleting user account", userId); + + var userService = await WampServers.IdentityServer.GetServiceAsync(IdentityServerTopics.UserAccountServiceTopic); + await userService.DeleteUserAsync(Clients.Notesnook.Id, userId, password); + + await DeleteUserAsync(userId); + + await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage { - await Slogger.Error(nameof(DeleteUserAsync), "User account not deleted", userId, ex.ToString()); - } - return false; + SendToAll = false, + OriginTokenId = jti, + UserId = userId, + Message = new Message + { + Type = "logout", + Data = JsonSerializer.Serialize(new { reason = "Account deleted." }) + } + }); + } public async Task ResetUserAsync(string userId, bool removeAttachments) diff --git a/Streetwriters.Common/Interfaces/IUserAccountService.cs b/Streetwriters.Common/Interfaces/IUserAccountService.cs new file mode 100644 index 0000000..facb452 --- /dev/null +++ b/Streetwriters.Common/Interfaces/IUserAccountService.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using Streetwriters.Common.Models; +using WampSharp.V2.Rpc; + +namespace Streetwriters.Common.Interfaces +{ + public interface IUserAccountService + { + [WampProcedure("co.streetwriters.identity.users.get_user")] + Task GetUserAsync(string clientId, string userId); + [WampProcedure("co.streetwriters.identity.users.delete_user")] + Task DeleteUserAsync(string clientId, string userId, string password); + // [WampProcedure("co.streetwriters.identity.users.create_user")] + // Task CreateUserAsync(); + } +} \ No newline at end of file diff --git a/Streetwriters.Common/Interfaces/IUserSubscriptionService.cs b/Streetwriters.Common/Interfaces/IUserSubscriptionService.cs new file mode 100644 index 0000000..2ed2abd --- /dev/null +++ b/Streetwriters.Common/Interfaces/IUserSubscriptionService.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using Streetwriters.Common.Helpers; +using Streetwriters.Common.Models; +using WampSharp.V2.Rpc; + +namespace Streetwriters.Common.Interfaces +{ + public interface IUserSubscriptionService + { + [WampProcedure("co.streetwriters.subscriptions.subscriptions.get_user_subscription")] + Task GetUserSubscriptionAsync(string clientId, string userId); + } +} \ No newline at end of file diff --git a/Streetwriters.Identity/Controllers/AccountController.cs b/Streetwriters.Identity/Controllers/AccountController.cs index b6f0019..3e20de9 100644 --- a/Streetwriters.Identity/Controllers/AccountController.cs +++ b/Streetwriters.Identity/Controllers/AccountController.cs @@ -33,11 +33,13 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Streetwriters.Common; using Streetwriters.Common.Enums; +using Streetwriters.Common.Interfaces; using Streetwriters.Common.Messages; using Streetwriters.Common.Models; using Streetwriters.Identity.Enums; using Streetwriters.Identity.Interfaces; using Streetwriters.Identity.Models; +using Streetwriters.Identity.Services; using static IdentityServer4.IdentityServerConstants; namespace Streetwriters.Identity.Controllers @@ -52,12 +54,14 @@ namespace Streetwriters.Identity.Controllers private ITokenGenerationService TokenGenerationService { get; set; } private IUserClaimsPrincipalFactory PrincipalFactory { get; set; } private IdentityServerOptions ISOptions { get; set; } + private IUserAccountService UserAccountService { get; set; } public AccountController(UserManager _userManager, IEmailSender _emailSender, SignInManager _signInManager, RoleManager _roleManager, IPersistedGrantStore store, - ITokenGenerationService tokenGenerationService, IMFAService _mfaService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService) + ITokenGenerationService tokenGenerationService, IMFAService _mfaService, IUserAccountService userAccountService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService) { PersistedGrantStore = store; TokenGenerationService = tokenGenerationService; + UserAccountService = userAccountService; } [HttpGet("confirm")] @@ -69,7 +73,7 @@ namespace Streetwriters.Identity.Controllers if (client == null) return BadRequest("Invalid client_id."); var user = await UserManager.FindByIdAsync(userId); - if (!await IsUserValidAsync(user, clientId)) return BadRequest($"Unable to find user with ID '{userId}'."); + if (!await UserService.IsUserValidAsync(UserManager, user, clientId)) return BadRequest($"Unable to find user with ID '{userId}'."); switch (type) { @@ -116,7 +120,7 @@ namespace Streetwriters.Identity.Controllers if (client == null) return BadRequest("Invalid client_id."); var user = await UserManager.GetUserAsync(User); - if (!await IsUserValidAsync(user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'."); + if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'."); if (string.IsNullOrEmpty(newEmail)) { @@ -132,63 +136,13 @@ namespace Streetwriters.Identity.Controllers return Ok(); } - [HttpPost("unregister")] - public async Task UnregisterAccountAync([FromForm] DeleteAccountForm form) - { - var client = Clients.FindClientById(User.FindFirstValue("client_id")); - if (client == null) return BadRequest("Invalid client_id."); - - var user = await UserManager.GetUserAsync(User); - if (!await IsUserValidAsync(user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'."); - - if (!await UserManager.CheckPasswordAsync(user, form.Password)) - { - return Unauthorized(); - } - - await UserManager.DeleteAsync(user); - - // await UserManager.RemoveFromRoleAsync(user, client.Id); - // await MFAService.DisableMFAAsync(user); - - // IdentityUserClaim statusClaim = user.Claims.FirstOrDefault((c) => c.ClaimType == $"{client.Id}:status"); - // await UserManager.RemoveClaimAsync(user, statusClaim.ToClaim()); - return Ok(); - } - [HttpGet] public async Task GetUserAccount() { var client = Clients.FindClientById(User.FindFirstValue("client_id")); if (client == null) return BadRequest("Invalid client_id."); - var user = await UserManager.GetUserAsync(User); - if (!await IsUserValidAsync(user, client.Id)) - return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'."); - - var claims = await UserManager.GetClaimsAsync(user); - var marketingConsentClaim = claims.FirstOrDefault((claim) => claim.Type == $"{client.Id}:marketing_consent"); - - if (await UserManager.IsEmailConfirmedAsync(user) && !await UserManager.GetTwoFactorEnabledAsync(user)) - { - await MFAService.EnableMFAAsync(user, MFAMethods.Email); - user = await UserManager.GetUserAsync(User); - } - - return Ok(new UserModel - { - UserId = user.Id.ToString(), - Email = user.Email, - IsEmailConfirmed = user.EmailConfirmed, - MarketingConsent = marketingConsentClaim == null, - MFA = new MFAConfig - { - IsEnabled = user.TwoFactorEnabled, - PrimaryMethod = MFAService.GetPrimaryMethod(user), - SecondaryMethod = MFAService.GetSecondaryMethod(user), - RemainingValidCodes = await MFAService.GetRemainingValidCodesAsync(user) - } - }); + return Ok(UserAccountService.GetUserAsync(client.Id, user.Id.ToString())); } [HttpPost("recover")] @@ -199,7 +153,7 @@ namespace Streetwriters.Identity.Controllers if (client == null) return BadRequest("Invalid client_id."); var user = await UserManager.FindByEmailAsync(form.Email); - if (!await IsUserValidAsync(user, form.ClientId)) return Ok(); + if (!await UserService.IsUserValidAsync(UserManager, user, form.ClientId)) return Ok(); var code = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword"); var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.RESET_PASSWORD, Request.Scheme); @@ -219,7 +173,7 @@ namespace Streetwriters.Identity.Controllers if (client == null) return BadRequest("Invalid client_id."); var user = await UserManager.GetUserAsync(User); - if (!await IsUserValidAsync(user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'."); + if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'."); var subjectId = User.FindFirstValue("sub"); var jti = User.FindFirstValue("jti"); @@ -246,7 +200,7 @@ namespace Streetwriters.Identity.Controllers { if (!Clients.IsValidClient(form.ClientId)) return BadRequest("Invalid clientId."); var user = await UserManager.FindByIdAsync(form.UserId); - if (!await IsUserValidAsync(user, form.ClientId)) + if (!await UserService.IsUserValidAsync(UserManager, user, form.ClientId)) return BadRequest($"Unable to find user with ID '{form.UserId}'."); if (!await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "PasswordResetAuthorizationCode", form.Code)) @@ -267,7 +221,7 @@ namespace Streetwriters.Identity.Controllers if (client == null) return BadRequest("Invalid client_id."); var user = await UserManager.GetUserAsync(User); - if (!await IsUserValidAsync(user, client.Id)) + if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'."); switch (form.Type) @@ -338,7 +292,7 @@ namespace Streetwriters.Identity.Controllers if (client == null) return BadRequest("Invalid client_id."); var user = await UserManager.GetUserAsync(User); - if (!await IsUserValidAsync(user, client.Id)) return BadRequest($"Unable to find user with ID '{user.Id}'."); + if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{user.Id}'."); var jti = User.FindFirstValue("jti"); @@ -386,10 +340,5 @@ namespace Streetwriters.Identity.Controllers Message = message }); } - - public async Task IsUserValidAsync(User user, string clientId) - { - return user != null && await UserManager.IsInRoleAsync(user, clientId); - } } } \ No newline at end of file diff --git a/Streetwriters.Identity/Models/DeleteAccountForm.cs b/Streetwriters.Identity/Models/DeleteAccountForm.cs deleted file mode 100644 index 83bec8e..0000000 --- a/Streetwriters.Identity/Models/DeleteAccountForm.cs +++ /dev/null @@ -1,33 +0,0 @@ -/* -This file is part of the Notesnook Sync Server project (https://notesnook.com/) - -Copyright (C) 2023 Streetwriters (Private) Limited - -This program is free software: you can redistribute it and/or modify -it under the terms of the Affero GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -Affero GNU General Public License for more details. - -You should have received a copy of the Affero GNU General Public License -along with this program. If not, see . -*/ - -using System.ComponentModel.DataAnnotations; -using System.Runtime.Serialization; - -namespace Streetwriters.Identity.Models -{ - public class DeleteAccountForm - { - [Required] - public string Password - { - get; set; - } - } -} \ No newline at end of file diff --git a/Streetwriters.Identity/Services/UserAccountService.cs b/Streetwriters.Identity/Services/UserAccountService.cs new file mode 100644 index 0000000..37ae44b --- /dev/null +++ b/Streetwriters.Identity/Services/UserAccountService.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Streetwriters.Common.Enums; +using Streetwriters.Common.Interfaces; +using Streetwriters.Common.Models; +using Streetwriters.Identity.Interfaces; +using Streetwriters.Identity.Models; + +namespace Streetwriters.Identity.Services +{ + public class UserAccountService(UserManager userManager, IMFAService mfaService) : IUserAccountService + { + public async Task GetUserAsync(string clientId, string userId) + { + var user = await userManager.FindByIdAsync(userId); + if (!await UserService.IsUserValidAsync(userManager, user, clientId)) + throw new Exception($"Unable to find user with ID '{userId}'."); + + var claims = await userManager.GetClaimsAsync(user); + var marketingConsentClaim = claims.FirstOrDefault((claim) => claim.Type == $"{clientId}:marketing_consent"); + + if (await userManager.IsEmailConfirmedAsync(user) && !await userManager.GetTwoFactorEnabledAsync(user)) + { + await mfaService.EnableMFAAsync(user, MFAMethods.Email); + user = await userManager.FindByIdAsync(userId); + } + + return new UserModel + { + UserId = user.Id.ToString(), + Email = user.Email, + IsEmailConfirmed = user.EmailConfirmed, + MarketingConsent = marketingConsentClaim == null, + MFA = new MFAConfig + { + IsEnabled = user.TwoFactorEnabled, + PrimaryMethod = mfaService.GetPrimaryMethod(user), + SecondaryMethod = mfaService.GetSecondaryMethod(user), + RemainingValidCodes = await mfaService.GetRemainingValidCodesAsync(user) + } + }; + } + + public async Task DeleteUserAsync(string clientId, string userId, string password) + { + var user = await userManager.FindByIdAsync(userId); + if (!await UserService.IsUserValidAsync(userManager, user, clientId)) throw new Exception($"User not found."); + + if (!await userManager.CheckPasswordAsync(user, password)) throw new Exception("Wrong password."); + + await userManager.DeleteAsync(user); + } + } +} \ No newline at end of file diff --git a/Streetwriters.Identity/Services/UserService.cs b/Streetwriters.Identity/Services/UserService.cs index 8067985..a60a46b 100644 --- a/Streetwriters.Identity/Services/UserService.cs +++ b/Streetwriters.Identity/Services/UserService.cs @@ -19,6 +19,8 @@ along with this program. If not, see . using System.Linq; using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; using Streetwriters.Common.Enums; using Streetwriters.Common.Models; @@ -78,5 +80,10 @@ namespace Streetwriters.Identity.Services { return $"{clientId}:status"; } + + public static async Task IsUserValidAsync(UserManager userManager, User user, string clientId) + { + return user != null && await userManager.IsInRoleAsync(user, clientId); + } } } \ No newline at end of file