api: move to atomic password reset

This commit is contained in:
Abdullah Atta
2026-02-13 11:13:19 +05:00
committed by Abdullah Atta
parent b9385ae112
commit 9424afed68
11 changed files with 203 additions and 83 deletions

View File

@@ -18,7 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Net.Http;
using System.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Timeouts;
@@ -28,13 +30,16 @@ using Notesnook.API.Interfaces;
using Notesnook.API.Models;
using Notesnook.API.Models.Responses;
using Streetwriters.Common;
using Streetwriters.Common.Accessors;
using Streetwriters.Common.Extensions;
using Streetwriters.Common.Messages;
namespace Notesnook.API.Controllers
{
[ApiController]
[Authorize]
[Route("users")]
public class UsersController(IUserService UserService, ILogger<UsersController> logger) : ControllerBase
public class UsersController(IUserService UserService, WampServiceAccessor serviceAccessor, ILogger<UsersController> logger) : ControllerBase
{
[HttpPost]
[AllowAnonymous]
@@ -85,6 +90,43 @@ namespace Notesnook.API.Controllers
}
}
[HttpPatch("password/{type}")]
public async Task<IActionResult> ChangePassword([FromRoute] string type, [FromBody] ChangePasswordForm form)
{
var userId = User.GetUserId();
var clientId = User.FindFirstValue("client_id");
var jti = User.FindFirstValue("jti");
var isPasswordReset = type == "reset";
try
{
var result = isPasswordReset ? await serviceAccessor.UserAccountService.ResetPasswordAsync(userId, form.NewPassword) : await serviceAccessor.UserAccountService.ChangePasswordAsync(userId, form.OldPassword, form.NewPassword);
if (!result)
return BadRequest("Failed to change password.");
await UserService.SetUserKeysAsync(userId, form.UserKeys);
await serviceAccessor.UserAccountService.ClearSessionsAsync(userId, clientId, all: false, jti, null);
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
{
UserId = userId,
OriginTokenId = User.FindFirstValue("jti"),
Message = new Message
{
Type = "logout",
Data = JsonSerializer.Serialize(new { reason = "Password changed." })
}
});
return Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to change password");
return BadRequest(new { error = ex.Message });
}
}
[HttpPost("reset")]
public async Task<IActionResult> Reset([FromForm] bool removeAttachments)
{

View File

@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
namespace Notesnook.API.Models
{
public class ChangePasswordForm
{
public string? OldPassword
{
get; set;
}
[Required]
public required string NewPassword
{
get; set;
}
[Required]
public required UserKeys UserKeys
{
get; set;
}
}
}

View File

@@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
namespace Notesnook.API.Models
{
public class ResetPasswordForm
{
[Required]
public required string NewPassword
{
get; set;
}
[Required]
public required UserKeys UserKeys
{
get; set;
}
}
}

View File

@@ -15,6 +15,11 @@ namespace Notesnook.API.Models.Responses
[JsonPropertyName("monographPasswordsKey")]
public EncryptedData? MonographPasswordsKey { get; set; }
[JsonPropertyName("dataEncryptionKey")]
public EncryptedData? DataEncryptionKey { get; set; }
[JsonPropertyName("legacyDataEncryptionKey")]
public EncryptedData? LegacyDataEncryptionKey { get; set; }
[JsonPropertyName("inboxKeys")]
public InboxKeys? InboxKeys { get; set; }

View File

@@ -98,6 +98,14 @@ namespace Notesnook.API.Models
get; set;
}
[JsonPropertyName("keyVersion")]
[DataMember(Name = "keyVersion")]
[MessagePack.Key("keyVersion")]
public int? KeyVersion
{
get; set;
}
[JsonPropertyName("alg")]
[DataMember(Name = "alg")]
[MessagePack.Key("alg")]

View File

@@ -24,6 +24,8 @@ namespace Notesnook.API.Models
public EncryptedData? AttachmentsKey { get; set; }
public EncryptedData? MonographPasswordsKey { get; set; }
public InboxKeys? InboxKeys { get; set; }
public EncryptedData? DataEncryptionKey { get; set; }
public EncryptedData? LegacyDataEncryptionKey { get; set; }
}
public class InboxKeys

View File

@@ -55,6 +55,8 @@ namespace Notesnook.API.Models
public EncryptedData? VaultKey { get; set; }
public EncryptedData? AttachmentsKey { get; set; }
public EncryptedData? MonographPasswordsKey { get; set; }
public EncryptedData? DataEncryptionKey { get; set; }
public EncryptedData? LegacyDataEncryptionKey { get; set; }
public InboxKeys? InboxKeys { get; set; }
public Limit? StorageLimit { get; set; }

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
@@ -133,6 +134,8 @@ namespace Notesnook.API.Services
PhoneNumber = user.PhoneNumber,
AttachmentsKey = userSettings.AttachmentsKey,
MonographPasswordsKey = userSettings.MonographPasswordsKey,
DataEncryptionKey = userSettings.DataEncryptionKey,
LegacyDataEncryptionKey = userSettings.LegacyDataEncryptionKey,
InboxKeys = userSettings.InboxKeys,
Salt = userSettings.Salt,
Subscription = subscription,
@@ -155,6 +158,11 @@ namespace Notesnook.API.Services
{
userSettings.MonographPasswordsKey = keys.MonographPasswordsKey;
}
if (keys.DataEncryptionKey != null)
userSettings.DataEncryptionKey = keys.DataEncryptionKey;
if (keys.LegacyDataEncryptionKey != null)
userSettings.LegacyDataEncryptionKey = keys.LegacyDataEncryptionKey;
if (keys.InboxKeys != null)
{
if (keys.InboxKeys.Public == null || keys.InboxKeys.Private == null)
@@ -268,6 +276,8 @@ namespace Notesnook.API.Services
userSettings.AttachmentsKey = null;
userSettings.MonographPasswordsKey = null;
userSettings.DataEncryptionKey = null;
userSettings.LegacyDataEncryptionKey = null;
userSettings.VaultKey = null;
userSettings.InboxKeys = null;
userSettings.LastSynced = 0;