Compare commits

..
Author SHA1 Message Date
Abdullah Atta 8b610e0952 cors: fix possible XSS vuln when embeding youtube-nocookie.com videos 2026-08-18 09:15:32 +05:00
Abdullah Atta 0a3ee07b95 api: add encryption verifier
encryption verifier is used for verifying the encryption key during password resets. It can be any encrypted item.
2026-08-18 08:36:02 +05:00
Abdullah Atta 768384011d api: re-enable password change 2026-08-10 12:08:35 +05:00
Abdullah Atta a8e73069c2 common: avoid long lived mail client 2026-08-05 22:16:56 +05:00
Abdullah Atta 594f81a3b9 identity: log on email confirmation 2026-08-05 17:58:31 +05:00
Abdullah Atta 518e396079 common: fix NOTESNOOK_CORS_ORIGINS env var 2026-08-05 17:50:43 +05:00
9 changed files with 104 additions and 59 deletions

No files matched your search

+46 -29
View File
@@ -93,39 +93,56 @@ namespace Notesnook.API.Controllers
[HttpPatch("password/{type}")]
public async Task<IActionResult> ChangePassword([FromRoute] string type, [FromBody] ChangePasswordForm form)
{
return BadRequest(new { error = "Password change is currently disabled." });
// 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.");
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 UserService.SetUserKeysAsync(userId, form.UserKeys);
// await serviceAccessor.UserAccountService.ClearSessionsAsync(userId, clientId, all: false, jti, null);
await serviceAccessor.UserAccountService.ClearSessionsAsync(userId, clientId, all: false, jti, null);
// await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
// {
// UserId = userId,
// OriginTokenId = jti,
// Message = new Message
// {
// Type = "logout",
// Data = JsonSerializer.Serialize(new { reason = "Password changed." })
// }
// });
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
{
UserId = userId,
OriginTokenId = 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 });
// }
return Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to change password");
return BadRequest(new { error = ex.Message });
}
}
[HttpGet("verifier")]
public async Task<IActionResult> GetEncryptionVerifier()
{
var userId = User.GetUserId();
try
{
var response = await UserService.GetEncryptionVerifier(userId);
if (response == null) return NotFound();
return Ok(response);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to get encryption verifier for user id: {UserId}", userId);
return BadRequest(new { error = ex.Message });
}
}
[HttpPost("reset")]
+1
View File
@@ -31,6 +31,7 @@ namespace Notesnook.API.Interfaces
Task DeleteUserAsync(string userId, string? jti, string password);
Task<bool> ResetUserAsync(string userId, bool removeAttachments);
Task<UserResponse> GetUserAsync(string userId);
Task<EncryptedData?> GetEncryptionVerifier(string userId);
Task SetUserKeysAsync(string userId, UserKeys keys);
}
}
@@ -36,6 +36,7 @@ using Streetwriters.Common;
using Streetwriters.Data.DbContexts;
using Streetwriters.Data.Interfaces;
using Streetwriters.Data.Repositories;
using AspNetCore.Identity.Mongo.Mongo;
namespace Notesnook.API.Repositories
{
+21
View File
@@ -30,6 +30,7 @@ using Notesnook.API.Helpers;
using Notesnook.API.Interfaces;
using Notesnook.API.Models;
using Notesnook.API.Models.Responses;
using Notesnook.API.Repositories;
using Streetwriters.Common;
using Streetwriters.Common.Accessors;
using Streetwriters.Common.Enums;
@@ -192,6 +193,26 @@ namespace Notesnook.API.Services
await Repositories.UsersSettings.UpdateAsync(userSettings.Id, userSettings);
}
public async Task<EncryptedData?> GetEncryptionVerifier(string userId)
{
SyncItemsRepository[] repositories = [Repositories.Notes, Repositories.Notebooks, Repositories.Shortcuts, Repositories.Contents, Repositories.Settings, Repositories.LegacySettings, Repositories.Attachments, Repositories.Reminders, Repositories.Relations, Repositories.Colors, Repositories.Tags, Repositories.Vaults, Repositories.InboxItemsHistory];
foreach (var repo in repositories)
{
var item = await repo.FindOneAsync((s) => s.UserId == userId && (s.KeyVersion == null || s.KeyVersion == 0));
if (item != null)
{
return new EncryptedData
{
Cipher = item.Cipher,
IV = item.IV,
Salt = "",
Length = item.Length
};
}
}
return null;
}
public async Task DeleteUserAsync(string userId)
{
logger.LogInformation("Deleting user {UserId}", userId);
+1 -1
View File
@@ -78,7 +78,7 @@ namespace Streetwriters.Common
public static string? SUBSCRIPTIONS_SERVER_HOST => ReadSecret("SUBSCRIPTIONS_SERVER_HOST");
public static string? SUBSCRIPTIONS_CERT_PATH => ReadSecret("SUBSCRIPTIONS_CERT_PATH");
public static string? SUBSCRIPTIONS_CERT_KEY_PATH => ReadSecret("SUBSCRIPTIONS_CERT_KEY_PATH");
public static string[] NOTESNOOK_CORS_ORIGINS => ReadSecret("NOTESNOOK_CORS")?.Split(",") ?? [];
public static string[] NOTESNOOK_CORS_ORIGINS => ReadSecret("NOTESNOOK_CORS_ORIGINS")?.Split(",") ?? [];
public static string? SIGNALR_REDIS_CONNECTION_STRING => ReadSecret("SIGNALR_REDIS_CONNECTION_STRING");
public static string MONOGRAPH_PUBLIC_URL => ReadSecret("MONOGRAPH_PUBLIC_URL") ?? "https://monogr.ph";
+22 -25
View File
@@ -14,9 +14,8 @@ using Streetwriters.Common.Models;
namespace Streetwriters.Common.Services
{
public class EmailSender : IEmailSender, IAsyncDisposable
public class EmailSender : IEmailSender
{
private readonly SmtpClient mailClient = new();
private readonly ILogger<EmailSender> logger;
public EmailSender(ILogger<EmailSender> logger)
@@ -32,27 +31,25 @@ namespace Streetwriters.Common.Services
Dictionary<string, byte[]>? attachments = null
)
{
if (!mailClient.IsConnected)
using var mailClient = new SmtpClient();
if (int.TryParse(Common.Constants.SMTP_PORT, out int port))
{
if (int.TryParse(Common.Constants.SMTP_PORT, out int port))
{
await mailClient.ConnectAsync(
Common.Constants.SMTP_HOST,
port,
MailKit.Security.SecureSocketOptions.Auto
);
}
else
{
throw new InvalidDataException("SMTP_PORT is not a valid integer value.");
}
await mailClient.ConnectAsync(
Common.Constants.SMTP_HOST,
port,
MailKit.Security.SecureSocketOptions.Auto
);
}
else
{
throw new InvalidDataException("SMTP_PORT is not a valid integer value.");
}
if (!mailClient.IsAuthenticated)
await mailClient.AuthenticateAsync(
Common.Constants.SMTP_USERNAME,
Common.Constants.SMTP_PASSWORD
);
await mailClient.AuthenticateAsync(
Common.Constants.SMTP_USERNAME,
Common.Constants.SMTP_PASSWORD
);
var message = new MimeMessage();
message.From.Add(new MailboxAddress(from.DisplayName, from.Address));
@@ -70,6 +67,11 @@ namespace Streetwriters.Common.Services
);
await mailClient.SendAsync(message);
if (mailClient.IsConnected)
{
await mailClient.DisconnectAsync(true);
}
}
private async Task<MimeEntity> GetEmailBodyAsync(
@@ -129,10 +131,5 @@ namespace Streetwriters.Common.Services
}
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
await mailClient.DisconnectAsync(true);
mailClient.Dispose();
}
}
}
@@ -116,6 +116,7 @@ namespace Streetwriters.Identity.Controllers
if (await UserManager.IsInRoleAsync(user, client.Id) && client.OnEmailConfirmed != null)
{
logger.LogInformation("Email confirmed for user {UserId} on client {ClientId}. Triggering OnEmailConfirmed callback.", userId, client.Id);
await client.OnEmailConfirmed(userId);
}
+11 -3
View File
@@ -279,8 +279,8 @@ function serveYouTubeEmbed(url: string) {
</style>
</head>
<body>
<iframe src="${transformYouTubeUrl(
url,
<iframe src="${escapeHtmlAttr(
transformYouTubeUrl(url),
)}" allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture;web-share" allowfullscreen referrerpolicy="strict-origin-when-cross-origin" title="Video player"></iframe>
</body>
</html>`;
@@ -299,6 +299,14 @@ function isYouTubeEmbed(urlString: string) {
);
}
function escapeHtmlAttr(str: string): string {
return str
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
// Transform YouTube URLs to use youtube-nocookie.com for enhanced privacy
function transformYouTubeUrl(urlString: string): string {
try {
@@ -315,7 +323,7 @@ function transformYouTubeUrl(urlString: string): string {
return url.toString();
}
return urlString;
return url.toString();
} catch {
return urlString;
}
-1
View File
@@ -167,7 +167,6 @@ services:
S3_SERVICE_URL: "${ATTACHMENTS_SERVER_PUBLIC_URL}"
S3_REGION: "us-east-1"
S3_BUCKET_NAME: "attachments"
NOTESNOOK_CORS: ${NOTESNOOK_CORS_ORIGINS:-}
sse-server:
image: streetwriters/sse:latest