diff --git a/Streetwriters.Identity/Controllers/AccountController.cs b/Streetwriters.Identity/Controllers/AccountController.cs
index 3e20de9..e6f7ff9 100644
--- a/Streetwriters.Identity/Controllers/AccountController.cs
+++ b/Streetwriters.Identity/Controllers/AccountController.cs
@@ -1,344 +1,344 @@
-/*
-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.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using System.Security.Claims;
-using System.Text.Json;
-using System.Threading.Tasks;
-using AspNetCore.Identity.Mongo.Model;
-using IdentityServer4;
-using IdentityServer4.Configuration;
-using IdentityServer4.Models;
-using IdentityServer4.Stores;
-using Microsoft.AspNetCore.Authorization;
-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
-{
- [ApiController]
- [DisplayName("Account")]
- [Route("account")]
- [Authorize(LocalApi.PolicyName)]
- public class AccountController : IdentityControllerBase
- {
- private IPersistedGrantStore PersistedGrantStore { get; set; }
- 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, IUserAccountService userAccountService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService)
- {
- PersistedGrantStore = store;
- TokenGenerationService = tokenGenerationService;
- UserAccountService = userAccountService;
- }
-
- [HttpGet("confirm")]
- [AllowAnonymous]
- [ResponseCache(NoStore = true)]
- public async Task ConfirmToken(string userId, string code, string clientId, TokenType type)
- {
- var client = Clients.FindClientById(clientId);
- if (client == null) return BadRequest("Invalid client_id.");
-
- var user = await UserManager.FindByIdAsync(userId);
- if (!await UserService.IsUserValidAsync(UserManager, user, clientId)) return BadRequest($"Unable to find user with ID '{userId}'.");
-
- switch (type)
- {
- case TokenType.CONFRIM_EMAIL:
- {
- if (await UserManager.IsEmailConfirmedAsync(user)) return Ok("Email already verified.");
-
- var result = await UserManager.ConfirmEmailAsync(user, code);
- if (!result.Succeeded) return BadRequest(result.Errors.ToErrors());
-
- if (await UserManager.IsInRoleAsync(user, client.Id))
- {
- await client.OnEmailConfirmed(userId);
- }
-
- if (!await UserManager.GetTwoFactorEnabledAsync(user))
- {
- await MFAService.EnableMFAAsync(user, MFAMethods.Email);
- user = await UserManager.GetUserAsync(User);
- }
-
- var redirectUrl = $"{client.EmailConfirmedRedirectURL}?userId={userId}";
- return RedirectPermanent(redirectUrl);
- }
- case TokenType.RESET_PASSWORD:
- {
- if (!await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword", code))
- return BadRequest("Invalid token.");
-
- var authorizationCode = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "PasswordResetAuthorizationCode");
- var redirectUrl = $"{client.AccountRecoveryRedirectURL}?userId={userId}&code={authorizationCode}";
- return RedirectPermanent(redirectUrl);
- }
- default:
- return BadRequest("Invalid type.");
- }
-
- }
-
- [HttpPost("verify")]
- public async Task SendVerificationEmail([FromForm] string newEmail)
- {
- var client = Clients.FindClientById(User.FindFirstValue("client_id"));
- if (client == null) return BadRequest("Invalid client_id.");
-
- var user = await UserManager.GetUserAsync(User);
- if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'.");
-
- if (string.IsNullOrEmpty(newEmail))
- {
- var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
- var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.CONFRIM_EMAIL, Request.Scheme);
- await EmailSender.SendConfirmationEmailAsync(user.Email, callbackUrl, client);
- }
- else
- {
- var code = await UserManager.GenerateChangeEmailTokenAsync(user, newEmail);
- await EmailSender.SendChangeEmailConfirmationAsync(newEmail, code, client);
- }
- 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);
- return Ok(UserAccountService.GetUserAsync(client.Id, user.Id.ToString()));
- }
-
- [HttpPost("recover")]
- [AllowAnonymous]
- public async Task ResetUserPassword([FromForm] ResetPasswordForm form)
- {
- var client = Clients.FindClientById(form.ClientId);
- if (client == null) return BadRequest("Invalid client_id.");
-
- var user = await UserManager.FindByEmailAsync(form.Email);
- 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);
-#if DEBUG
- return Ok(callbackUrl);
-#else
- await Slogger.Info("ResetUserPassword", user.Email, callbackUrl);
- await EmailSender.SendPasswordResetEmailAsync(user.Email, callbackUrl, client);
- return Ok();
-#endif
- }
-
- [HttpPost("logout")]
- public async Task Logout()
- {
- var client = Clients.FindClientById(User.FindFirstValue("client_id"));
- if (client == null) return BadRequest("Invalid client_id.");
-
- var user = await UserManager.GetUserAsync(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");
-
- var grants = await PersistedGrantStore.GetAllAsync(new PersistedGrantFilter
- {
- ClientId = client.Id,
- SubjectId = subjectId
- });
- grants = grants.Where((grant) => grant.Data.Contains(jti));
- if (grants.Any())
- {
- foreach (var grant in grants)
- {
- await PersistedGrantStore.RemoveAsync(grant.Key);
- }
- }
- return Ok();
- }
-
- [HttpPost("token")]
- [AllowAnonymous]
- public async Task GetAccessTokenFromCode([FromForm] GetAccessTokenForm form)
- {
- if (!Clients.IsValidClient(form.ClientId)) return BadRequest("Invalid clientId.");
- var user = await UserManager.FindByIdAsync(form.UserId);
- 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))
- return BadRequest("Invalid authorization_code.");
- var token = await TokenGenerationService.CreateAccessTokenAsync(user, form.ClientId);
- return Ok(new
- {
- access_token = token,
- scope = string.Join(' ', Config.ApiScopes.Select(s => s.Name)),
- expires_in = 18000
- });
- }
-
- [HttpPatch]
- public async Task UpdateAccount([FromForm] UpdateUserForm 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 UserService.IsUserValidAsync(UserManager, user, client.Id))
- return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'.");
-
- switch (form.Type)
- {
- case "change_email":
- {
- var result = await UserManager.ChangeEmailAsync(user, form.NewEmail, form.VerificationCode);
- if (result.Succeeded)
- {
- result = await UserManager.RemovePasswordAsync(user);
- if (result.Succeeded)
- {
- result = await UserManager.AddPasswordAsync(user, form.Password);
- if (result.Succeeded)
- {
- await UserManager.SetUserNameAsync(user, form.NewEmail);
- await SendLogoutMessageAsync(user.Id.ToString(), "Email changed.");
- return Ok();
- }
- }
- }
- return BadRequest(result.Errors.ToErrors());
- }
- case "change_password":
- {
- var result = await UserManager.ChangePasswordAsync(user, form.OldPassword, form.NewPassword);
- if (result.Succeeded)
- {
- await SendLogoutMessageAsync(user.Id.ToString(), "Password changed.");
- return Ok();
- }
- return BadRequest(result.Errors.ToErrors());
- }
- case "reset_password":
- {
- var result = await UserManager.RemovePasswordAsync(user);
- if (result.Succeeded)
- {
- await MFAService.ResetMFAAsync(user);
- result = await UserManager.AddPasswordAsync(user, form.NewPassword);
- if (result.Succeeded)
- {
- await SendLogoutMessageAsync(user.Id.ToString(), "Password reset.");
- return Ok();
- }
- }
- return BadRequest(result.Errors.ToErrors());
- }
- case "change_marketing_consent":
- {
- var claimType = $"{client.Id}:marketing_consent";
- var claims = await UserManager.GetClaimsAsync(user);
- var marketingConsentClaim = claims.FirstOrDefault((claim) => claim.Type == claimType);
- if (marketingConsentClaim != null) await UserManager.RemoveClaimAsync(user, marketingConsentClaim);
- if (!form.Enabled)
- await UserManager.AddClaimAsync(user, new Claim(claimType, "false"));
- return Ok();
- }
-
- }
- return BadRequest("Invalid type.");
- }
-
- [HttpPost("sessions/clear")]
- public async Task ClearUserSessions([FromQuery] bool all, [FromForm] string refresh_token)
- {
- var client = Clients.FindClientById(User.FindFirstValue("client_id"));
- if (client == null) return BadRequest("Invalid client_id.");
-
- var user = await UserManager.GetUserAsync(User);
- if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{user.Id}'.");
-
- var jti = User.FindFirstValue("jti");
-
- var grants = await PersistedGrantStore.GetAllAsync(new PersistedGrantFilter
- {
- ClientId = client.Id,
- SubjectId = user.Id.ToString()
- });
- var refreshTokenKey = GetHashedKey(refresh_token, PersistedGrantTypes.RefreshToken);
- var removedKeys = new List();
- foreach (var grant in grants)
- {
- if (!all && (grant.Data.Contains(jti) || grant.Key == refreshTokenKey)) continue;
- await PersistedGrantStore.RemoveAsync(grant.Key);
- removedKeys.Add(grant.Key);
- }
-
- await WampServers.NotesnookServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys));
- await WampServers.MessengerServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys));
- await WampServers.SubscriptionServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys));
- await SendLogoutMessageAsync(user.Id.ToString(), "Session revoked.");
- return Ok();
- }
-
- private static string GetHashedKey(string value, string grantType)
- {
- return (value + ":" + grantType).Sha256();
- }
-
- private async Task SendLogoutMessageAsync(string userId, string reason)
- {
- await SendMessageAsync(userId, new Message
- {
- Type = "logout",
- Data = JsonSerializer.Serialize(new { reason })
- });
- }
-
- private async Task SendMessageAsync(string userId, Message message)
- {
- await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
- {
- UserId = userId,
- OriginTokenId = User.FindFirstValue("jti"),
- Message = message
- });
- }
- }
+/*
+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.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Security.Claims;
+using System.Text.Json;
+using System.Threading.Tasks;
+using AspNetCore.Identity.Mongo.Model;
+using IdentityServer4;
+using IdentityServer4.Configuration;
+using IdentityServer4.Models;
+using IdentityServer4.Stores;
+using Microsoft.AspNetCore.Authorization;
+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
+{
+ [ApiController]
+ [DisplayName("Account")]
+ [Route("account")]
+ [Authorize(LocalApi.PolicyName)]
+ public class AccountController : IdentityControllerBase
+ {
+ private IPersistedGrantStore PersistedGrantStore { get; set; }
+ 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, IUserAccountService userAccountService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService)
+ {
+ PersistedGrantStore = store;
+ TokenGenerationService = tokenGenerationService;
+ UserAccountService = userAccountService;
+ }
+
+ [HttpGet("confirm")]
+ [AllowAnonymous]
+ [ResponseCache(NoStore = true)]
+ public async Task ConfirmToken(string userId, string code, string clientId, TokenType type)
+ {
+ var client = Clients.FindClientById(clientId);
+ if (client == null) return BadRequest("Invalid client_id.");
+
+ var user = await UserManager.FindByIdAsync(userId);
+ if (!await UserService.IsUserValidAsync(UserManager, user, clientId)) return BadRequest($"Unable to find user with ID '{userId}'.");
+
+ switch (type)
+ {
+ case TokenType.CONFRIM_EMAIL:
+ {
+ if (await UserManager.IsEmailConfirmedAsync(user)) return Ok("Email already verified.");
+
+ var result = await UserManager.ConfirmEmailAsync(user, code);
+ if (!result.Succeeded) return BadRequest(result.Errors.ToErrors());
+
+ if (await UserManager.IsInRoleAsync(user, client.Id))
+ {
+ await client.OnEmailConfirmed(userId);
+ }
+
+ if (!await UserManager.GetTwoFactorEnabledAsync(user))
+ {
+ await MFAService.EnableMFAAsync(user, MFAMethods.Email);
+ user = await UserManager.GetUserAsync(User);
+ }
+
+ var redirectUrl = $"{client.EmailConfirmedRedirectURL}?userId={userId}";
+ return RedirectPermanent(redirectUrl);
+ }
+ case TokenType.RESET_PASSWORD:
+ {
+ if (!await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword", code))
+ return BadRequest("Invalid token.");
+
+ var authorizationCode = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "PasswordResetAuthorizationCode");
+ var redirectUrl = $"{client.AccountRecoveryRedirectURL}?userId={userId}&code={authorizationCode}";
+ return RedirectPermanent(redirectUrl);
+ }
+ default:
+ return BadRequest("Invalid type.");
+ }
+
+ }
+
+ [HttpPost("verify")]
+ public async Task SendVerificationEmail([FromForm] string newEmail)
+ {
+ var client = Clients.FindClientById(User.FindFirstValue("client_id"));
+ if (client == null) return BadRequest("Invalid client_id.");
+
+ var user = await UserManager.GetUserAsync(User);
+ if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'.");
+
+ if (string.IsNullOrEmpty(newEmail))
+ {
+ var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
+ var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.CONFRIM_EMAIL);
+ await EmailSender.SendConfirmationEmailAsync(user.Email, callbackUrl, client);
+ }
+ else
+ {
+ var code = await UserManager.GenerateChangeEmailTokenAsync(user, newEmail);
+ await EmailSender.SendChangeEmailConfirmationAsync(newEmail, code, client);
+ }
+ 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);
+ return Ok(UserAccountService.GetUserAsync(client.Id, user.Id.ToString()));
+ }
+
+ [HttpPost("recover")]
+ [AllowAnonymous]
+ public async Task ResetUserPassword([FromForm] ResetPasswordForm form)
+ {
+ var client = Clients.FindClientById(form.ClientId);
+ if (client == null) return BadRequest("Invalid client_id.");
+
+ var user = await UserManager.FindByEmailAsync(form.Email);
+ 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);
+#if DEBUG
+ return Ok(callbackUrl);
+#else
+ await Slogger.Info("ResetUserPassword", user.Email, callbackUrl);
+ await EmailSender.SendPasswordResetEmailAsync(user.Email, callbackUrl, client);
+ return Ok();
+#endif
+ }
+
+ [HttpPost("logout")]
+ public async Task Logout()
+ {
+ var client = Clients.FindClientById(User.FindFirstValue("client_id"));
+ if (client == null) return BadRequest("Invalid client_id.");
+
+ var user = await UserManager.GetUserAsync(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");
+
+ var grants = await PersistedGrantStore.GetAllAsync(new PersistedGrantFilter
+ {
+ ClientId = client.Id,
+ SubjectId = subjectId
+ });
+ grants = grants.Where((grant) => grant.Data.Contains(jti));
+ if (grants.Any())
+ {
+ foreach (var grant in grants)
+ {
+ await PersistedGrantStore.RemoveAsync(grant.Key);
+ }
+ }
+ return Ok();
+ }
+
+ [HttpPost("token")]
+ [AllowAnonymous]
+ public async Task GetAccessTokenFromCode([FromForm] GetAccessTokenForm form)
+ {
+ if (!Clients.IsValidClient(form.ClientId)) return BadRequest("Invalid clientId.");
+ var user = await UserManager.FindByIdAsync(form.UserId);
+ 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))
+ return BadRequest("Invalid authorization_code.");
+ var token = await TokenGenerationService.CreateAccessTokenAsync(user, form.ClientId);
+ return Ok(new
+ {
+ access_token = token,
+ scope = string.Join(' ', Config.ApiScopes.Select(s => s.Name)),
+ expires_in = 18000
+ });
+ }
+
+ [HttpPatch]
+ public async Task UpdateAccount([FromForm] UpdateUserForm 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 UserService.IsUserValidAsync(UserManager, user, client.Id))
+ return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'.");
+
+ switch (form.Type)
+ {
+ case "change_email":
+ {
+ var result = await UserManager.ChangeEmailAsync(user, form.NewEmail, form.VerificationCode);
+ if (result.Succeeded)
+ {
+ result = await UserManager.RemovePasswordAsync(user);
+ if (result.Succeeded)
+ {
+ result = await UserManager.AddPasswordAsync(user, form.Password);
+ if (result.Succeeded)
+ {
+ await UserManager.SetUserNameAsync(user, form.NewEmail);
+ await SendLogoutMessageAsync(user.Id.ToString(), "Email changed.");
+ return Ok();
+ }
+ }
+ }
+ return BadRequest(result.Errors.ToErrors());
+ }
+ case "change_password":
+ {
+ var result = await UserManager.ChangePasswordAsync(user, form.OldPassword, form.NewPassword);
+ if (result.Succeeded)
+ {
+ await SendLogoutMessageAsync(user.Id.ToString(), "Password changed.");
+ return Ok();
+ }
+ return BadRequest(result.Errors.ToErrors());
+ }
+ case "reset_password":
+ {
+ var result = await UserManager.RemovePasswordAsync(user);
+ if (result.Succeeded)
+ {
+ await MFAService.ResetMFAAsync(user);
+ result = await UserManager.AddPasswordAsync(user, form.NewPassword);
+ if (result.Succeeded)
+ {
+ await SendLogoutMessageAsync(user.Id.ToString(), "Password reset.");
+ return Ok();
+ }
+ }
+ return BadRequest(result.Errors.ToErrors());
+ }
+ case "change_marketing_consent":
+ {
+ var claimType = $"{client.Id}:marketing_consent";
+ var claims = await UserManager.GetClaimsAsync(user);
+ var marketingConsentClaim = claims.FirstOrDefault((claim) => claim.Type == claimType);
+ if (marketingConsentClaim != null) await UserManager.RemoveClaimAsync(user, marketingConsentClaim);
+ if (!form.Enabled)
+ await UserManager.AddClaimAsync(user, new Claim(claimType, "false"));
+ return Ok();
+ }
+
+ }
+ return BadRequest("Invalid type.");
+ }
+
+ [HttpPost("sessions/clear")]
+ public async Task ClearUserSessions([FromQuery] bool all, [FromForm] string refresh_token)
+ {
+ var client = Clients.FindClientById(User.FindFirstValue("client_id"));
+ if (client == null) return BadRequest("Invalid client_id.");
+
+ var user = await UserManager.GetUserAsync(User);
+ if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{user.Id}'.");
+
+ var jti = User.FindFirstValue("jti");
+
+ var grants = await PersistedGrantStore.GetAllAsync(new PersistedGrantFilter
+ {
+ ClientId = client.Id,
+ SubjectId = user.Id.ToString()
+ });
+ var refreshTokenKey = GetHashedKey(refresh_token, PersistedGrantTypes.RefreshToken);
+ var removedKeys = new List();
+ foreach (var grant in grants)
+ {
+ if (!all && (grant.Data.Contains(jti) || grant.Key == refreshTokenKey)) continue;
+ await PersistedGrantStore.RemoveAsync(grant.Key);
+ removedKeys.Add(grant.Key);
+ }
+
+ await WampServers.NotesnookServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys));
+ await WampServers.MessengerServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys));
+ await WampServers.SubscriptionServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys));
+ await SendLogoutMessageAsync(user.Id.ToString(), "Session revoked.");
+ return Ok();
+ }
+
+ private static string GetHashedKey(string value, string grantType)
+ {
+ return (value + ":" + grantType).Sha256();
+ }
+
+ private async Task SendLogoutMessageAsync(string userId, string reason)
+ {
+ await SendMessageAsync(userId, new Message
+ {
+ Type = "logout",
+ Data = JsonSerializer.Serialize(new { reason })
+ });
+ }
+
+ private async Task SendMessageAsync(string userId, Message message)
+ {
+ await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
+ {
+ UserId = userId,
+ OriginTokenId = User.FindFirstValue("jti"),
+ Message = message
+ });
+ }
+ }
}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Controllers/SignupController.cs b/Streetwriters.Identity/Controllers/SignupController.cs
index 2ea1997..c28cb4e 100644
--- a/Streetwriters.Identity/Controllers/SignupController.cs
+++ b/Streetwriters.Identity/Controllers/SignupController.cs
@@ -1,139 +1,139 @@
-/*
-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.Collections.Generic;
-using System.Linq;
-using System.Security.Claims;
-using System.Threading.Tasks;
-using AspNetCore.Identity.Mongo.Model;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Identity;
-using Microsoft.AspNetCore.Mvc;
-using Streetwriters.Common;
-using Streetwriters.Common.Enums;
-using Streetwriters.Common.Models;
-using Streetwriters.Identity.Enums;
-using Streetwriters.Identity.Interfaces;
-using Streetwriters.Identity.Models;
-using Streetwriters.Identity.Services;
-
-namespace Streetwriters.Identity.Controllers
-{
- [ApiController]
- [Route("signup")]
- public class SignupController : IdentityControllerBase
- {
- public SignupController(UserManager _userManager, IEmailSender _emailSender,
- SignInManager _signInManager, RoleManager _roleManager, IMFAService _mfaService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService)
- { }
-
- private async Task AddClientRoleAsync(string clientId)
- {
- if (await RoleManager.FindByNameAsync(clientId) == null)
- await RoleManager.CreateAsync(new MongoRole(clientId));
- }
-
- [HttpPost]
- [AllowAnonymous]
- public async Task Signup([FromForm] SignupForm form)
- {
- if (Constants.DISABLE_ACCOUNT_CREATION)
- return BadRequest(new string[] { "Creating new accounts is not allowed." });
- try
- {
- var client = Clients.FindClientById(form.ClientId);
- if (client == null) return BadRequest(new string[] { "Invalid client id." });
-
- await AddClientRoleAsync(client.Id);
-
- // email addresses must be case-insensitive
- form.Email = form.Email.ToLowerInvariant();
- form.Username = form.Username?.ToLowerInvariant();
-
- if (!await EmailAddressValidator.IsEmailAddressValidAsync(form.Email)) return BadRequest(new string[] { "Invalid email address." });
-
- var result = await UserManager.CreateAsync(new User
- {
- Email = form.Email,
- EmailConfirmed = Constants.IS_SELF_HOSTED,
- UserName = form.Username ?? form.Email,
- }, form.Password);
-
- if (result.Errors.Any((e) => e.Code == "DuplicateEmail"))
- {
- var user = await UserManager.FindByEmailAsync(form.Email);
-
- if (!await UserManager.IsInRoleAsync(user, client.Id))
- {
- if (!await UserManager.CheckPasswordAsync(user, form.Password))
- {
- // TODO
- await UserManager.RemovePasswordAsync(user);
- await UserManager.AddPasswordAsync(user, form.Password);
- }
- await MFAService.DisableMFAAsync(user);
- await UserManager.AddToRoleAsync(user, client.Id);
- }
- else
- {
- return BadRequest(new string[] { "Invalid email address.." });
- }
-
- return Ok(new
- {
- userId = user.Id.ToString()
- });
- }
-
- if (result.Succeeded)
- {
- var user = await UserManager.FindByEmailAsync(form.Email);
- await UserManager.AddToRoleAsync(user, client.Id);
- if (Constants.IS_SELF_HOSTED)
- {
- await UserManager.AddClaimAsync(user, UserService.SubscriptionTypeToClaim(client.Id, Common.Enums.SubscriptionType.PREMIUM));
- }
- else
- {
- await UserManager.AddClaimAsync(user, new Claim("platform", PlatformFromUserAgent(base.HttpContext.Request.Headers.UserAgent)));
- var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
- var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.CONFRIM_EMAIL, Request.Scheme);
- await EmailSender.SendConfirmationEmailAsync(user.Email, callbackUrl, client);
- }
- return Ok(new
- {
- userId = user.Id.ToString()
- });
- }
-
- return BadRequest(result.Errors.ToErrors());
- }
- catch (System.Exception ex)
- {
- await Slogger.Error("Signup", ex.ToString());
- return BadRequest("Failed to create an account.");
- }
- }
-
- string PlatformFromUserAgent(string userAgent)
- {
- return userAgent.Contains("okhttp/") ? "android" : userAgent.Contains("Darwin/") || userAgent.Contains("CFNetwork/") ? "ios" : "web";
- }
- }
-}
+/*
+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.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using AspNetCore.Identity.Mongo.Model;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Streetwriters.Common;
+using Streetwriters.Common.Enums;
+using Streetwriters.Common.Models;
+using Streetwriters.Identity.Enums;
+using Streetwriters.Identity.Interfaces;
+using Streetwriters.Identity.Models;
+using Streetwriters.Identity.Services;
+
+namespace Streetwriters.Identity.Controllers
+{
+ [ApiController]
+ [Route("signup")]
+ public class SignupController : IdentityControllerBase
+ {
+ public SignupController(UserManager _userManager, IEmailSender _emailSender,
+ SignInManager _signInManager, RoleManager _roleManager, IMFAService _mfaService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService)
+ { }
+
+ private async Task AddClientRoleAsync(string clientId)
+ {
+ if (await RoleManager.FindByNameAsync(clientId) == null)
+ await RoleManager.CreateAsync(new MongoRole(clientId));
+ }
+
+ [HttpPost]
+ [AllowAnonymous]
+ public async Task Signup([FromForm] SignupForm form)
+ {
+ if (Constants.DISABLE_ACCOUNT_CREATION)
+ return BadRequest(new string[] { "Creating new accounts is not allowed." });
+ try
+ {
+ var client = Clients.FindClientById(form.ClientId);
+ if (client == null) return BadRequest(new string[] { "Invalid client id." });
+
+ await AddClientRoleAsync(client.Id);
+
+ // email addresses must be case-insensitive
+ form.Email = form.Email.ToLowerInvariant();
+ form.Username = form.Username?.ToLowerInvariant();
+
+ if (!await EmailAddressValidator.IsEmailAddressValidAsync(form.Email)) return BadRequest(new string[] { "Invalid email address." });
+
+ var result = await UserManager.CreateAsync(new User
+ {
+ Email = form.Email,
+ EmailConfirmed = Constants.IS_SELF_HOSTED,
+ UserName = form.Username ?? form.Email,
+ }, form.Password);
+
+ if (result.Errors.Any((e) => e.Code == "DuplicateEmail"))
+ {
+ var user = await UserManager.FindByEmailAsync(form.Email);
+
+ if (!await UserManager.IsInRoleAsync(user, client.Id))
+ {
+ if (!await UserManager.CheckPasswordAsync(user, form.Password))
+ {
+ // TODO
+ await UserManager.RemovePasswordAsync(user);
+ await UserManager.AddPasswordAsync(user, form.Password);
+ }
+ await MFAService.DisableMFAAsync(user);
+ await UserManager.AddToRoleAsync(user, client.Id);
+ }
+ else
+ {
+ return BadRequest(new string[] { "Invalid email address.." });
+ }
+
+ return Ok(new
+ {
+ userId = user.Id.ToString()
+ });
+ }
+
+ if (result.Succeeded)
+ {
+ var user = await UserManager.FindByEmailAsync(form.Email);
+ await UserManager.AddToRoleAsync(user, client.Id);
+ if (Constants.IS_SELF_HOSTED)
+ {
+ await UserManager.AddClaimAsync(user, UserService.SubscriptionTypeToClaim(client.Id, Common.Enums.SubscriptionType.PREMIUM));
+ }
+ else
+ {
+ await UserManager.AddClaimAsync(user, new Claim("platform", PlatformFromUserAgent(base.HttpContext.Request.Headers.UserAgent)));
+ var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
+ var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.CONFRIM_EMAIL);
+ await EmailSender.SendConfirmationEmailAsync(user.Email, callbackUrl, client);
+ }
+ return Ok(new
+ {
+ userId = user.Id.ToString()
+ });
+ }
+
+ return BadRequest(result.Errors.ToErrors());
+ }
+ catch (System.Exception ex)
+ {
+ await Slogger.Error("Signup", ex.ToString());
+ return BadRequest("Failed to create an account.");
+ }
+ }
+
+ string PlatformFromUserAgent(string userAgent)
+ {
+ return userAgent.Contains("okhttp/") ? "android" : userAgent.Contains("Darwin/") || userAgent.Contains("CFNetwork/") ? "ios" : "web";
+ }
+ }
+}
diff --git a/Streetwriters.Identity/Extensions/UrlExtensions.cs b/Streetwriters.Identity/Extensions/UrlExtensions.cs
index 9f51ec2..3fb085c 100644
--- a/Streetwriters.Identity/Extensions/UrlExtensions.cs
+++ b/Streetwriters.Identity/Extensions/UrlExtensions.cs
@@ -1,48 +1,49 @@
-/*
-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;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using Streetwriters.Common;
-using Streetwriters.Identity.Controllers;
-using Streetwriters.Identity.Enums;
-
-namespace Microsoft.AspNetCore.Mvc
-{
- public static class UrlHelperExtensions
- {
- public static string TokenLink(this IUrlHelper urlHelper, string userId, string code, string clientId, TokenType type, string scheme)
- {
-
- return urlHelper.ActionLink(
-#if DEBUG
- host: $"{Servers.IdentityServer.Hostname}:{Servers.IdentityServer.Port}",
-#else
- host: Servers.IdentityServer.Domain,
-#endif
- action: nameof(AccountController.ConfirmToken),
- controller: "Account",
- values: new { userId, code, clientId, type },
- protocol: scheme);
-
- }
- }
+/*
+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;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Streetwriters.Common;
+using Streetwriters.Identity.Controllers;
+using Streetwriters.Identity.Enums;
+
+namespace Microsoft.AspNetCore.Mvc
+{
+ public static class UrlHelperExtensions
+ {
+ public static string TokenLink(this IUrlHelper urlHelper, string userId, string code, string clientId, TokenType type)
+ {
+
+ return urlHelper.ActionLink(
+#if DEBUG
+ host: $"{Servers.IdentityServer.Hostname}:{Servers.IdentityServer.Port}",
+ protocol: "http",
+#else
+ host: Servers.IdentityServer.PublicURL.Host,
+ protocol: Servers.IdentityServer.PublicURL.Scheme,
+#endif
+ action: nameof(AccountController.ConfirmToken),
+ controller: "Account",
+ values: new { userId, code, clientId, type });
+
+ }
+ }
}
\ No newline at end of file