api: use wamp services instead of forwarding http requests for internal apis

This commit is contained in:
Abdullah Atta
2024-06-07 15:35:31 +05:00
parent 353e866cda
commit 99da765a1c
10 changed files with 230 additions and 233 deletions

View File

@@ -18,35 +18,23 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
using System; using System;
using System.Net.Http;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Timeouts;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Notesnook.API.Interfaces; using Notesnook.API.Interfaces;
using Notesnook.API.Models;
using Notesnook.API.Models.Responses; using Notesnook.API.Models.Responses;
using Streetwriters.Common; using Streetwriters.Common;
using Streetwriters.Common.Extensions;
using Streetwriters.Common.Models;
namespace Notesnook.API.Controllers namespace Notesnook.API.Controllers
{ {
[ApiController] [ApiController]
[Authorize] [Authorize]
[Route("users")] [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] [HttpPost]
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> Signup() public async Task<IActionResult> Signup()
@@ -66,20 +54,35 @@ namespace Notesnook.API.Controllers
[HttpGet] [HttpGet]
public async Task<IActionResult> GetUser() public async Task<IActionResult> GetUser()
{ {
UserResponse response = await UserService.GetUserAsync(); var userId = User.FindFirstValue("sub");
if (!response.Success) return BadRequest(response); try
return Ok(response); {
UserResponse response = await UserService.GetUserAsync(userId);
if (!response.Success) return BadRequest(response);
return Ok(response);
}
catch (Exception ex)
{
await Slogger<UsersController>.Error(nameof(GetUser), "Couldn't get user for id.", userId, ex.ToString());
return BadRequest(new { error = ex.Message });
}
} }
[HttpPatch] [HttpPatch]
public async Task<IActionResult> UpdateUser([FromBody] UserResponse user) public async Task<IActionResult> UpdateUser([FromBody] UserResponse user)
{ {
UserResponse response = await UserService.GetUserAsync(false); var userId = User.FindFirstValue("sub");
try
if (user.AttachmentsKey != null) {
await UserService.SetUserAttachmentsKeyAsync(response.UserId, user.AttachmentsKey); if (user.AttachmentsKey != null)
await UserService.SetUserAttachmentsKeyAsync(userId, user.AttachmentsKey);
return Ok(); return Ok();
}
catch (Exception ex)
{
await Slogger<UsersController>.Error(nameof(GetUser), "Couldn't update user with id.", userId, ex.ToString());
return BadRequest(new { error = ex.Message });
}
} }
[HttpPost("reset")] [HttpPost("reset")]
@@ -93,24 +96,20 @@ namespace Notesnook.API.Controllers
} }
[HttpPost("delete")] [HttpPost("delete")]
public async Task<IActionResult> Delete() [RequestTimeout(5 * 60 * 1000)]
public async Task<IActionResult> Delete([FromForm] DeleteAccountForm form)
{ {
var userId = this.User.FindFirstValue("sub");
var jti = User.FindFirstValue("jti");
try try
{ {
var userId = this.User.FindFirstValue("sub"); await UserService.DeleteUserAsync(userId, jti, form.Password);
return Ok();
if (await UserService.DeleteUserAsync(userId, User.FindFirstValue("jti")))
{
Response response = await this.httpClient.ForwardAsync<Response>(HttpContextAccessor, $"{Servers.IdentityServer}/account/unregister", HttpMethod.Post);
if (!response.Success) return BadRequest();
return Ok();
}
return BadRequest();
} }
catch (Exception ex) catch (Exception ex)
{ {
return BadRequest(ex.Message); await Slogger<UsersController>.Error(nameof(GetUser), "Couldn't delete user with id.", userId, ex.ToString());
return BadRequest(new { error = ex.Message });
} }
} }
} }

View File

@@ -27,9 +27,10 @@ namespace Notesnook.API.Interfaces
public interface IUserService public interface IUserService
{ {
Task CreateUserAsync(); Task CreateUserAsync();
Task<bool> DeleteUserAsync(string userId, string jti); Task DeleteUserAsync(string userId);
Task DeleteUserAsync(string userId, string jti, string password);
Task<bool> ResetUserAsync(string userId, bool removeAttachments); Task<bool> ResetUserAsync(string userId, bool removeAttachments);
Task<UserResponse> GetUserAsync(bool repair = true); Task<UserResponse> GetUserAsync(string userId);
Task SetUserAttachmentsKeyAsync(string userId, IEncrypted key); Task SetUserAttachmentsKeyAsync(string userId, IEncrypted key);
} }
} }

View File

@@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace Notesnook.API.Models
{
public class DeleteAccountForm
{
[Required]
public string Password
{
get; set;
}
}
}

View File

@@ -92,10 +92,11 @@ namespace Notesnook.API.Services
await Slogger<UserService>.Info(nameof(CreateUserAsync), "New user created.", JsonSerializer.Serialize(response)); await Slogger<UserService>.Info(nameof(CreateUserAsync), "New user created.", JsonSerializer.Serialize(response));
} }
public async Task<UserResponse> GetUserAsync(bool repair = true) public async Task<UserResponse> GetUserAsync(string userId)
{ {
UserResponse response = await httpClient.ForwardAsync<UserResponse>(this.HttpContextAccessor, $"{Servers.IdentityServer.ToString()}/account", HttpMethod.Get); var userService = await WampServers.IdentityServer.GetServiceAsync<IUserAccountService>(IdentityServerTopics.UserAccountServiceTopic);
if (!response.Success) return response;
var user = await userService.GetUserAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User not found.");
ISubscription subscription = null; ISubscription subscription = null;
if (Constants.IS_SELF_HOSTED) if (Constants.IS_SELF_HOSTED)
@@ -105,7 +106,7 @@ namespace Notesnook.API.Services
AppId = ApplicationType.NOTESNOOK, AppId = ApplicationType.NOTESNOOK,
Provider = SubscriptionProvider.STREETWRITERS, Provider = SubscriptionProvider.STREETWRITERS,
Type = SubscriptionType.PREMIUM, Type = SubscriptionType.PREMIUM,
UserId = response.UserId, UserId = user.UserId,
StartDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), StartDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
// this date doesn't matter as the subscription is static. // this date doesn't matter as the subscription is static.
ExpiryDate = DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeMilliseconds() ExpiryDate = DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeMilliseconds()
@@ -113,117 +114,92 @@ namespace Notesnook.API.Services
} }
else else
{ {
SubscriptionResponse subscriptionResponse = await httpClient.ForwardAsync<SubscriptionResponse>(this.HttpContextAccessor, $"{Servers.SubscriptionServer}/subscriptions", HttpMethod.Get); var subscriptionService = await WampServers.SubscriptionServer.GetServiceAsync<IUserSubscriptionService>(SubscriptionServerTopics.UserSubscriptionServiceTopic);
if (repair && subscriptionResponse.StatusCode == 404) subscription = await subscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId);
{
await Slogger<UserService>.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 userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == response.UserId); var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == user.UserId) ?? throw new Exception("User settings not found.");
if (repair && userSettings == null) return new UserResponse
{ {
await Slogger<UserService>.Error(nameof(GetUserAsync), "Repairing user settings.", JsonSerializer.Serialize(response)); UserId = user.UserId,
userSettings = new UserSettings Email = user.Email,
{ IsEmailConfirmed = user.IsEmailConfirmed,
UserId = response.UserId, MarketingConsent = user.MarketingConsent,
LastSynced = 0, MFA = user.MFA,
Salt = GetSalt() PhoneNumber = user.PhoneNumber,
}; AttachmentsKey = userSettings.AttachmentsKey,
await Repositories.UsersSettings.InsertAsync(userSettings); Salt = userSettings.Salt,
} Subscription = subscription,
response.AttachmentsKey = userSettings.AttachmentsKey; Success = true,
response.Salt = userSettings.Salt; StatusCode = 200
response.Subscription = subscription; };
return response;
} }
public async Task SetUserAttachmentsKeyAsync(string userId, IEncrypted key) 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; userSettings.AttachmentsKey = (EncryptedData)key;
await Repositories.UsersSettings.UpdateAsync(userSettings.Id, userSettings); await Repositories.UsersSettings.UpdateAsync(userSettings.Id, userSettings);
} }
public async Task<bool> 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<UserService>.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<UserService>.Info(nameof(DeleteUserAsync), "Deleting user account", userId); await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.DeleteSubscriptionTopic, new DeleteSubscriptionMessage
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<UserService>.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 AppId = ApplicationType.NOTESNOOK,
{ UserId = userId
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." })
}
}); });
}
await S3Service.DeleteDirectoryAsync(userId); await S3Service.DeleteDirectoryAsync(userId);
return result; }
}
catch (Exception ex) public async Task DeleteUserAsync(string userId, string jti, string password)
{
await Slogger<UserService>.Info(nameof(DeleteUserAsync), "Deleting user account", userId);
var userService = await WampServers.IdentityServer.GetServiceAsync<IUserAccountService>(IdentityServerTopics.UserAccountServiceTopic);
await userService.DeleteUserAsync(Clients.Notesnook.Id, userId, password);
await DeleteUserAsync(userId);
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
{ {
await Slogger<UserService>.Error(nameof(DeleteUserAsync), "User account not deleted", userId, ex.ToString()); SendToAll = false,
} OriginTokenId = jti,
return false; UserId = userId,
Message = new Message
{
Type = "logout",
Data = JsonSerializer.Serialize(new { reason = "Account deleted." })
}
});
} }
public async Task<bool> ResetUserAsync(string userId, bool removeAttachments) public async Task<bool> ResetUserAsync(string userId, bool removeAttachments)

View File

@@ -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<UserModel> 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<UserModel> CreateUserAsync();
}
}

View File

@@ -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<Subscription> GetUserSubscriptionAsync(string clientId, string userId);
}
}

View File

@@ -33,11 +33,13 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Streetwriters.Common; using Streetwriters.Common;
using Streetwriters.Common.Enums; using Streetwriters.Common.Enums;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Messages; using Streetwriters.Common.Messages;
using Streetwriters.Common.Models; using Streetwriters.Common.Models;
using Streetwriters.Identity.Enums; using Streetwriters.Identity.Enums;
using Streetwriters.Identity.Interfaces; using Streetwriters.Identity.Interfaces;
using Streetwriters.Identity.Models; using Streetwriters.Identity.Models;
using Streetwriters.Identity.Services;
using static IdentityServer4.IdentityServerConstants; using static IdentityServer4.IdentityServerConstants;
namespace Streetwriters.Identity.Controllers namespace Streetwriters.Identity.Controllers
@@ -52,12 +54,14 @@ namespace Streetwriters.Identity.Controllers
private ITokenGenerationService TokenGenerationService { get; set; } private ITokenGenerationService TokenGenerationService { get; set; }
private IUserClaimsPrincipalFactory<User> PrincipalFactory { get; set; } private IUserClaimsPrincipalFactory<User> PrincipalFactory { get; set; }
private IdentityServerOptions ISOptions { get; set; } private IdentityServerOptions ISOptions { get; set; }
private IUserAccountService UserAccountService { get; set; }
public AccountController(UserManager<User> _userManager, IEmailSender _emailSender, public AccountController(UserManager<User> _userManager, IEmailSender _emailSender,
SignInManager<User> _signInManager, RoleManager<MongoRole> _roleManager, IPersistedGrantStore store, SignInManager<User> _signInManager, RoleManager<MongoRole> _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; PersistedGrantStore = store;
TokenGenerationService = tokenGenerationService; TokenGenerationService = tokenGenerationService;
UserAccountService = userAccountService;
} }
[HttpGet("confirm")] [HttpGet("confirm")]
@@ -69,7 +73,7 @@ namespace Streetwriters.Identity.Controllers
if (client == null) return BadRequest("Invalid client_id."); if (client == null) return BadRequest("Invalid client_id.");
var user = await UserManager.FindByIdAsync(userId); 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) switch (type)
{ {
@@ -116,7 +120,7 @@ namespace Streetwriters.Identity.Controllers
if (client == null) return BadRequest("Invalid client_id."); if (client == null) return BadRequest("Invalid client_id.");
var user = await UserManager.GetUserAsync(User); 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)) if (string.IsNullOrEmpty(newEmail))
{ {
@@ -132,63 +136,13 @@ namespace Streetwriters.Identity.Controllers
return Ok(); return Ok();
} }
[HttpPost("unregister")]
public async Task<IActionResult> 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<string> statusClaim = user.Claims.FirstOrDefault((c) => c.ClaimType == $"{client.Id}:status");
// await UserManager.RemoveClaimAsync(user, statusClaim.ToClaim());
return Ok();
}
[HttpGet] [HttpGet]
public async Task<IActionResult> GetUserAccount() public async Task<IActionResult> GetUserAccount()
{ {
var client = Clients.FindClientById(User.FindFirstValue("client_id")); var client = Clients.FindClientById(User.FindFirstValue("client_id"));
if (client == null) return BadRequest("Invalid client_id."); if (client == null) return BadRequest("Invalid client_id.");
var user = await UserManager.GetUserAsync(User); var user = await UserManager.GetUserAsync(User);
if (!await IsUserValidAsync(user, client.Id)) return Ok(UserAccountService.GetUserAsync(client.Id, user.Id.ToString()));
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)
}
});
} }
[HttpPost("recover")] [HttpPost("recover")]
@@ -199,7 +153,7 @@ namespace Streetwriters.Identity.Controllers
if (client == null) return BadRequest("Invalid client_id."); if (client == null) return BadRequest("Invalid client_id.");
var user = await UserManager.FindByEmailAsync(form.Email); 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 code = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword");
var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.RESET_PASSWORD, Request.Scheme); 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."); if (client == null) return BadRequest("Invalid client_id.");
var user = await UserManager.GetUserAsync(User); 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 subjectId = User.FindFirstValue("sub");
var jti = User.FindFirstValue("jti"); var jti = User.FindFirstValue("jti");
@@ -246,7 +200,7 @@ namespace Streetwriters.Identity.Controllers
{ {
if (!Clients.IsValidClient(form.ClientId)) return BadRequest("Invalid clientId."); if (!Clients.IsValidClient(form.ClientId)) return BadRequest("Invalid clientId.");
var user = await UserManager.FindByIdAsync(form.UserId); 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}'."); return BadRequest($"Unable to find user with ID '{form.UserId}'.");
if (!await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "PasswordResetAuthorizationCode", form.Code)) 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."); if (client == null) return BadRequest("Invalid client_id.");
var user = await UserManager.GetUserAsync(User); 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)}'."); return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'.");
switch (form.Type) switch (form.Type)
@@ -338,7 +292,7 @@ namespace Streetwriters.Identity.Controllers
if (client == null) return BadRequest("Invalid client_id."); if (client == null) return BadRequest("Invalid client_id.");
var user = await UserManager.GetUserAsync(User); 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"); var jti = User.FindFirstValue("jti");
@@ -386,10 +340,5 @@ namespace Streetwriters.Identity.Controllers
Message = message Message = message
}); });
} }
public async Task<bool> IsUserValidAsync(User user, string clientId)
{
return user != null && await UserManager.IsInRoleAsync(user, clientId);
}
} }
} }

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
namespace Streetwriters.Identity.Models
{
public class DeleteAccountForm
{
[Required]
public string Password
{
get; set;
}
}
}

View File

@@ -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<User> userManager, IMFAService mfaService) : IUserAccountService
{
public async Task<UserModel> 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);
}
}
}

View File

@@ -19,6 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Streetwriters.Common.Enums; using Streetwriters.Common.Enums;
using Streetwriters.Common.Models; using Streetwriters.Common.Models;
@@ -78,5 +80,10 @@ namespace Streetwriters.Identity.Services
{ {
return $"{clientId}:status"; return $"{clientId}:status";
} }
public static async Task<bool> IsUserValidAsync(UserManager<User> userManager, User user, string clientId)
{
return user != null && await userManager.IsInRoleAsync(user, clientId);
}
} }
} }