Compare commits

...
Author SHA1 Message Date
Abdullah Atta cdde244c69 s3: temporarily disable attachment uploads 2026-09-21 12:57:23 +05:00
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
7 changed files with 133 additions and 86 deletions

No files matched your search

+31 -29
View File
@@ -45,28 +45,29 @@ namespace Notesnook.API.Controllers
[HttpPut] [HttpPut]
public async Task<IActionResult> Upload([FromQuery] string name) public async Task<IActionResult> Upload([FromQuery] string name)
{ {
try return BadRequest(new { error = "Attachment storage is temporarily unavailable. Please try again later." });
{ // try
var userId = this.User.GetUserId(); // {
// var userId = this.User.GetUserId();
var fileSize = HttpContext.Request.ContentLength ?? 0; // var fileSize = HttpContext.Request.ContentLength ?? 0;
bool hasBody = fileSize > 0; // bool hasBody = fileSize > 0;
if (!hasBody) // if (!hasBody)
{ // {
return Ok(Request.GetEncodedUrl() + "&access_token=" + Request.Headers.Authorization.ToString().Replace("Bearer ", "")); // return Ok(Request.GetEncodedUrl() + "&access_token=" + Request.Headers.Authorization.ToString().Replace("Bearer ", ""));
} // }
if (Constants.IS_SELF_HOSTED) await UploadFileAsync(userId, name, fileSize); // if (Constants.IS_SELF_HOSTED) await UploadFileAsync(userId, name, fileSize);
else await UploadFileWithChecksAsync(userId, name, fileSize); // else await UploadFileWithChecksAsync(userId, name, fileSize);
return Ok(); // return Ok();
} // }
catch (Exception ex) // catch (Exception ex)
{ // {
logger.LogError(ex, "Error uploading attachment for user."); // logger.LogError(ex, "Error uploading attachment for user.");
return BadRequest(new { error = "Failed to upload attachment." }); // return BadRequest(new { error = "Failed to upload attachment." });
} // }
} }
private async Task UploadFileWithChecksAsync(string userId, string name, long fileSize) private async Task UploadFileWithChecksAsync(string userId, string name, long fileSize)
@@ -115,17 +116,18 @@ namespace Notesnook.API.Controllers
[HttpGet("multipart")] [HttpGet("multipart")]
public async Task<IActionResult> MultipartUpload([FromQuery] string name, [FromQuery] int parts, [FromQuery] string? uploadId) public async Task<IActionResult> MultipartUpload([FromQuery] string name, [FromQuery] int parts, [FromQuery] string? uploadId)
{ {
var userId = this.User.GetUserId(); return BadRequest(new { error = "Attachment storage is temporarily unavailable. Please try again later." });
try // var userId = this.User.GetUserId();
{ // try
var meta = await s3Service.StartMultipartUploadAsync(userId, name, parts, uploadId); // {
return Ok(meta); // var meta = await s3Service.StartMultipartUploadAsync(userId, name, parts, uploadId);
} // return Ok(meta);
catch (Exception ex) // }
{ // catch (Exception ex)
logger.LogError(ex, "Error starting multipart upload for user."); // {
return BadRequest(new { error = "Failed to start multipart upload." }); // logger.LogError(ex, "Error starting multipart upload for user.");
} // return BadRequest(new { error = "Failed to start multipart upload." });
// }
} }
[HttpDelete("multipart")] [HttpDelete("multipart")]
+46 -29
View File
@@ -93,39 +93,56 @@ namespace Notesnook.API.Controllers
[HttpPatch("password/{type}")] [HttpPatch("password/{type}")]
public async Task<IActionResult> ChangePassword([FromRoute] string type, [FromBody] ChangePasswordForm form) 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 userId = User.GetUserId(); var clientId = User.FindFirstValue("client_id");
// var clientId = User.FindFirstValue("client_id"); var jti = User.FindFirstValue("jti");
// var jti = User.FindFirstValue("jti"); var isPasswordReset = type == "reset";
// var isPasswordReset = type == "reset"; try
// try {
// { var result = isPasswordReset ? await serviceAccessor.UserAccountService.ResetPasswordAsync(userId, form.NewPassword) : await serviceAccessor.UserAccountService.ChangePasswordAsync(userId, form.OldPassword, form.NewPassword);
// var result = isPasswordReset ? await serviceAccessor.UserAccountService.ResetPasswordAsync(userId, form.NewPassword) : await serviceAccessor.UserAccountService.ChangePasswordAsync(userId, form.OldPassword, form.NewPassword); if (!result)
// if (!result) return BadRequest("Failed to change password.");
// 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 await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
// { {
// UserId = userId, UserId = userId,
// OriginTokenId = jti, OriginTokenId = jti,
// Message = new Message Message = new Message
// { {
// Type = "logout", Type = "logout",
// Data = JsonSerializer.Serialize(new { reason = "Password changed." }) Data = JsonSerializer.Serialize(new { reason = "Password changed." })
// } }
// }); });
// return Ok(); return Ok();
// } }
// catch (Exception ex) catch (Exception ex)
// { {
// logger.LogError(ex, "Failed to change password"); logger.LogError(ex, "Failed to change password");
// return BadRequest(new { error = ex.Message }); 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")] [HttpPost("reset")]
+1
View File
@@ -31,6 +31,7 @@ namespace Notesnook.API.Interfaces
Task DeleteUserAsync(string userId, string? jti, string password); 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(string userId); Task<UserResponse> GetUserAsync(string userId);
Task<EncryptedData?> GetEncryptionVerifier(string userId);
Task SetUserKeysAsync(string userId, UserKeys keys); Task SetUserKeysAsync(string userId, UserKeys keys);
} }
} }
@@ -36,6 +36,7 @@ using Streetwriters.Common;
using Streetwriters.Data.DbContexts; using Streetwriters.Data.DbContexts;
using Streetwriters.Data.Interfaces; using Streetwriters.Data.Interfaces;
using Streetwriters.Data.Repositories; using Streetwriters.Data.Repositories;
using AspNetCore.Identity.Mongo.Mongo;
namespace Notesnook.API.Repositories namespace Notesnook.API.Repositories
{ {
+21
View File
@@ -30,6 +30,7 @@ using Notesnook.API.Helpers;
using Notesnook.API.Interfaces; using Notesnook.API.Interfaces;
using Notesnook.API.Models; using Notesnook.API.Models;
using Notesnook.API.Models.Responses; using Notesnook.API.Models.Responses;
using Notesnook.API.Repositories;
using Streetwriters.Common; using Streetwriters.Common;
using Streetwriters.Common.Accessors; using Streetwriters.Common.Accessors;
using Streetwriters.Common.Enums; using Streetwriters.Common.Enums;
@@ -192,6 +193,26 @@ namespace Notesnook.API.Services
await Repositories.UsersSettings.UpdateAsync(userSettings.Id, userSettings); 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) public async Task DeleteUserAsync(string userId)
{ {
logger.LogInformation("Deleting user {UserId}", userId); logger.LogInformation("Deleting user {UserId}", userId);
+22 -25
View File
@@ -14,9 +14,8 @@ using Streetwriters.Common.Models;
namespace Streetwriters.Common.Services namespace Streetwriters.Common.Services
{ {
public class EmailSender : IEmailSender, IAsyncDisposable public class EmailSender : IEmailSender
{ {
private readonly SmtpClient mailClient = new();
private readonly ILogger<EmailSender> logger; private readonly ILogger<EmailSender> logger;
public EmailSender(ILogger<EmailSender> logger) public EmailSender(ILogger<EmailSender> logger)
@@ -32,27 +31,25 @@ namespace Streetwriters.Common.Services
Dictionary<string, byte[]>? attachments = null 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,
await mailClient.ConnectAsync( port,
Common.Constants.SMTP_HOST, MailKit.Security.SecureSocketOptions.Auto
port, );
MailKit.Security.SecureSocketOptions.Auto }
); else
} {
else throw new InvalidDataException("SMTP_PORT is not a valid integer value.");
{
throw new InvalidDataException("SMTP_PORT is not a valid integer value.");
}
} }
if (!mailClient.IsAuthenticated) await mailClient.AuthenticateAsync(
await mailClient.AuthenticateAsync( Common.Constants.SMTP_USERNAME,
Common.Constants.SMTP_USERNAME, Common.Constants.SMTP_PASSWORD
Common.Constants.SMTP_PASSWORD );
);
var message = new MimeMessage(); var message = new MimeMessage();
message.From.Add(new MailboxAddress(from.DisplayName, from.Address)); message.From.Add(new MailboxAddress(from.DisplayName, from.Address));
@@ -70,6 +67,11 @@ namespace Streetwriters.Common.Services
); );
await mailClient.SendAsync(message); await mailClient.SendAsync(message);
if (mailClient.IsConnected)
{
await mailClient.DisconnectAsync(true);
}
} }
private async Task<MimeEntity> GetEmailBodyAsync( private async Task<MimeEntity> GetEmailBodyAsync(
@@ -129,10 +131,5 @@ namespace Streetwriters.Common.Services
} }
} }
async ValueTask IAsyncDisposable.DisposeAsync()
{
await mailClient.DisconnectAsync(true);
mailClient.Dispose();
}
} }
} }
+11 -3
View File
@@ -279,8 +279,8 @@ function serveYouTubeEmbed(url: string) {
</style> </style>
</head> </head>
<body> <body>
<iframe src="${transformYouTubeUrl( <iframe src="${escapeHtmlAttr(
url, 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> )}" 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> </body>
</html>`; </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 // Transform YouTube URLs to use youtube-nocookie.com for enhanced privacy
function transformYouTubeUrl(urlString: string): string { function transformYouTubeUrl(urlString: string): string {
try { try {
@@ -315,7 +323,7 @@ function transformYouTubeUrl(urlString: string): string {
return url.toString(); return url.toString();
} }
return urlString; return url.toString();
} catch { } catch {
return urlString; return urlString;
} }