Compare commits

...
Author SHA1 Message Date
Abdullah Atta 294d885dbf common: expose x-object-size & content-length header via cors 2026-06-06 10:18:30 +05:00
Abdullah Atta bdd5017394 s3: add x-object-size header alongwith content-length header 2026-06-06 08:57:53 +05:00
01zulfiandGitHub 7ad70c63ee api: move inboxitemhistory collection sync in RequestFetchV4 (#101) 2026-05-19 13:39:52 +05:00
Abdullah Atta 0367ab6f80 sync: get rid of ._id fallback for inbox items 2026-05-15 08:58:21 +05:00
01zulfiandGitHub f3bfe0957b inbox: create InboxItemsHistory synced collection (#96) 2026-05-14 11:20:17 +05:00
01zulfiandGitHub 7f614f6954 inbox: remove 'Default' api key creation (#100) 2026-05-14 11:09:28 +05:00
Abdullah Atta 6663778e3e identity: fix email templates 2026-05-14 10:45:10 +05:00
14f0a3b37e inbox: improve http response status codes (#99)
* inbox: improve http response status codes
* inbox api: respond with 401 unauthorized for invalid inbox api key
* notesnook api (get public encryption key): respond with 404 if not found

* Update Notesnook.Inbox.API/src/index.ts

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-05-14 09:37:28 +05:00
01zulfiandGitHub 82a1152f9f inbox: fix expiry date validation check (#97) 2026-05-13 09:51:24 +05:00
01zulfiandGitHub 580524b855 inbox: require non-empty source in inbox item (#98) 2026-05-13 09:50:18 +05:00
14 changed files with 84 additions and 68 deletions
@@ -42,6 +42,7 @@ namespace Notesnook.API.Accessors
public SyncItemsRepository Colors { get; }
public SyncItemsRepository Vaults { get; }
public SyncItemsRepository Tags { get; }
public SyncItemsRepository InboxItemsHistory { get; }
public Repository<UserSettings> UsersSettings { get; }
public Repository<Monograph> Monographs { get; }
public Repository<InboxApiKey> InboxApiKey { get; }
@@ -75,6 +76,8 @@ namespace Notesnook.API.Accessors
IMongoCollection<SyncItem> vaults,
[FromKeyedServices(Collections.TagsKey)]
IMongoCollection<SyncItem> tags,
[FromKeyedServices(Collections.InboxItemsHistoryKey)]
IMongoCollection<SyncItem> inboxItemsHistory,
Repository<UserSettings> usersSettings,
Repository<Monograph> monographs,
@@ -102,6 +105,7 @@ namespace Notesnook.API.Accessors
Colors = new SyncItemsRepository(dbContext, colors, logger);
Vaults = new SyncItemsRepository(dbContext, vaults, logger);
Tags = new SyncItemsRepository(dbContext, tags, logger);
InboxItemsHistory = new SyncItemsRepository(dbContext, inboxItemsHistory, logger);
}
}
}
+1
View File
@@ -18,5 +18,6 @@ namespace Notesnook.API
public const string InboxApiKeysKey = "inbox_api_keys";
public const string SyncDevicesKey = "sync_devices";
public const string DeviceIdsChunksKey = "device_ids_chunks";
public const string InboxItemsHistoryKey = "inbox_items_history";
}
}
+4 -3
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.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;
@@ -72,9 +73,9 @@ namespace Notesnook.API.Controllers
{
return BadRequest(new { error = "Api key name is required." });
}
if (request.ExpiryDate <= -1)
if (request.ExpiryDate == null)
{
return BadRequest(new { error = "Valid expiry date is required." });
return BadRequest(new { error = "Expiry date is required." });
}
var count = await inboxApiKeysRepository.CountAsync(t => t.UserId == userId);
@@ -133,7 +134,7 @@ namespace Notesnook.API.Controllers
var userSetting = await userSettingsRepository.FindOneAsync(u => u.UserId == userId);
if (string.IsNullOrWhiteSpace(userSetting?.InboxKeys?.Public))
{
return BadRequest(new { error = "Inbox public key is not configured." });
return NotFound(new { error = "Inbox public key is not configured." });
}
return Ok(new { key = userSetting.InboxKeys.Public });
}
+2 -2
View File
@@ -183,8 +183,8 @@ namespace Notesnook.API.Controllers
try
{
var userId = this.User.GetUserId();
var size = await s3Service.GetObjectSizeAsync(userId, name);
HttpContext.Response.Headers.ContentLength = size;
var size = await s3Service.GetObjectSizeAsync(userId, name); Response.Headers.ContentLength = size;
Response.Headers["X-Object-Size"] = size.ToString();
return Ok();
}
catch (Exception ex)
+50 -36
View File
@@ -57,21 +57,9 @@ namespace Notesnook.API.Hubs
private ISyncItemsRepositoryAccessor Repositories { get; }
private SyncDeviceService SyncDeviceService { get; }
private readonly IUnitOfWork unit;
private static readonly string[] CollectionKeys = [
"settingitem",
"attachment",
"note",
"notebook",
"content",
"shortcut",
"reminder",
"color",
"tag",
"vault",
"relation", // relations must sync at the end to prevent invalid state
];
private readonly FrozenDictionary<string, Action<IEnumerable<SyncItem>, string, long>> UpsertActionsMap;
private readonly Func<string, IEnumerable<string>, bool, int, Task<IAsyncCursor<SyncItem>>>[] Collections;
private readonly CollectionDef[] BaseCollectionDefs;
private readonly CollectionDef[] V4CollectionDefs;
ILogger<SyncV2Hub> Logger { get; }
public SyncV2Hub(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, IUnitOfWork unitOfWork, SyncDeviceService syncDeviceService, ILogger<SyncV2Hub> logger)
@@ -81,18 +69,32 @@ namespace Notesnook.API.Hubs
unit = unitOfWork;
SyncDeviceService = syncDeviceService;
Collections = [
Repositories.Settings.FindItemsById,
Repositories.Attachments.FindItemsById,
Repositories.Notes.FindItemsById,
Repositories.Notebooks.FindItemsById,
Repositories.Contents.FindItemsById,
Repositories.Shortcuts.FindItemsById,
Repositories.Reminders.FindItemsById,
Repositories.Colors.FindItemsById,
Repositories.Tags.FindItemsById,
Repositories.Vaults.FindItemsById,
Repositories.Relations.FindItemsById,
BaseCollectionDefs = [
new("settingitem", Repositories.Settings.FindItemsById),
new("attachment", Repositories.Attachments.FindItemsById),
new("note", Repositories.Notes.FindItemsById),
new("notebook", Repositories.Notebooks.FindItemsById),
new("content", Repositories.Contents.FindItemsById),
new("shortcut", Repositories.Shortcuts.FindItemsById),
new("reminder", Repositories.Reminders.FindItemsById),
new("color", Repositories.Colors.FindItemsById),
new("tag", Repositories.Tags.FindItemsById),
new("vault", Repositories.Vaults.FindItemsById),
new("relation", Repositories.Relations.FindItemsById), // relations must sync at the end to prevent invalid state
];
V4CollectionDefs = [
new("settingitem", Repositories.Settings.FindItemsById),
new("attachment", Repositories.Attachments.FindItemsById),
new("note", Repositories.Notes.FindItemsById),
new("notebook", Repositories.Notebooks.FindItemsById),
new("content", Repositories.Contents.FindItemsById),
new("shortcut", Repositories.Shortcuts.FindItemsById),
new("reminder", Repositories.Reminders.FindItemsById),
new("color", Repositories.Colors.FindItemsById),
new("tag", Repositories.Tags.FindItemsById),
new("vault", Repositories.Vaults.FindItemsById),
new("inboxitemhistory", Repositories.InboxItemsHistory.FindItemsById),
new("relation", Repositories.Relations.FindItemsById), // relations must sync at the end to prevent invalid state
];
UpsertActionsMap = new Dictionary<string, Action<IEnumerable<SyncItem>, string, long>> {
{ "settingitem", Repositories.Settings.UpsertMany },
@@ -106,6 +108,7 @@ namespace Notesnook.API.Hubs
{ "color", Repositories.Colors.UpsertMany },
{ "vault", Repositories.Vaults.UpsertMany },
{ "tag", Repositories.Tags.UpsertMany },
{ "inboxitemhistory", Repositories.InboxItemsHistory.UpsertMany },
}.ToFrozenDictionary();
}
@@ -181,17 +184,17 @@ namespace Notesnook.API.Hubs
return true;
}
private async IAsyncEnumerable<SyncTransferItemV2> PrepareChunks(string userId, HashSet<ItemKey> ids, int size, bool resetSync, long maxBytes)
private async IAsyncEnumerable<SyncTransferItemV2> PrepareChunks(string userId, HashSet<ItemKey> ids, int size, bool resetSync, long maxBytes, CollectionDef[] collectionDefs)
{
var itemsProcessed = 0;
for (int i = 0; i < Collections.Length; i++)
foreach (var def in collectionDefs)
{
var type = CollectionKeys[i];
var type = def.Key;
var filteredIds = ids.Where((id) => id.Type == type).Select((id) => id.ItemId).ToArray();
if (!resetSync && filteredIds.Length == 0) continue;
using var cursor = await Collections[i](userId, filteredIds, resetSync, size);
using var cursor = await def.FindItems(userId, filteredIds, resetSync, size);
var chunk = new List<SyncItem>();
long totalBytes = 0;
@@ -233,20 +236,25 @@ namespace Notesnook.API.Hubs
public async Task<SyncV2Metadata> RequestFetch(string deviceId)
{
return await HandleRequestFetch(deviceId, false, false);
return await HandleRequestFetch(deviceId, false, false, BaseCollectionDefs);
}
public async Task<SyncV2Metadata> RequestFetchV2(string deviceId)
{
return await HandleRequestFetch(deviceId, true, false);
return await HandleRequestFetch(deviceId, true, false, BaseCollectionDefs);
}
public async Task<SyncV2Metadata> RequestFetchV3(string deviceId)
{
return await HandleRequestFetch(deviceId, true, true);
return await HandleRequestFetch(deviceId, true, true, BaseCollectionDefs);
}
private async Task<SyncV2Metadata> HandleRequestFetch(string deviceId, bool includeMonographs, bool includeInboxItems)
public async Task<SyncV2Metadata> RequestFetchV4(string deviceId)
{
return await HandleRequestFetch(deviceId, true, true, V4CollectionDefs);
}
private async Task<SyncV2Metadata> HandleRequestFetch(string deviceId, bool includeMonographs, bool includeInboxItems, CollectionDef[] collectionDefs)
{
var userId = Context.User?.FindFirstValue("sub") ?? throw new HubException("Please login to sync.");
@@ -270,7 +278,8 @@ namespace Notesnook.API.Hubs
ids,
size: 100,
resetSync: device.IsSyncReset,
maxBytes: 3 * 1024 * 1024
maxBytes: 3 * 1024 * 1024,
collectionDefs
);
await foreach (var chunk in chunks)
@@ -325,7 +334,7 @@ namespace Notesnook.API.Hubs
var unsyncedInboxItemIds = ids.Where(k => k.Type == "inbox_item").Select(k => k.ItemId);
var userInboxItems = device.IsSyncReset
? await Repositories.InboxItems.FindAsync(m => m.UserId == userId)
: await Repositories.InboxItems.FindAsync(m => m.UserId == userId && unsyncedInboxItemIds.Contains(m.ItemId ?? m.Id.ToString()));
: await Repositories.InboxItems.FindAsync(m => m.UserId == userId && unsyncedInboxItemIds.Contains(m.ItemId));
if (userInboxItems.Any() && !await Clients.Caller.SendInboxItems(userInboxItems).WaitAsync(TimeSpan.FromMinutes(10)))
{
throw new HubException("Client rejected inbox items.");
@@ -344,6 +353,11 @@ namespace Notesnook.API.Hubs
SyncEventCounterSource.Log.RecordFetchDuration(stopwatch.ElapsedMilliseconds);
}
}
private record CollectionDef(
string Key,
Func<string, IEnumerable<string>, bool, int, Task<IAsyncCursor<SyncItem>>> FindItems
);
}
[MessagePack.MessagePackObject]
@@ -38,6 +38,7 @@ namespace Notesnook.API.Interfaces
SyncItemsRepository Colors { get; }
SyncItemsRepository Vaults { get; }
SyncItemsRepository Tags { get; }
SyncItemsRepository InboxItemsHistory { get; }
Repository<UserSettings> UsersSettings { get; }
Repository<Monograph> Monographs { get; }
Repository<InboxApiKey> InboxApiKey { get; }
+2 -9
View File
@@ -174,15 +174,6 @@ namespace Notesnook.API.Services
else
{
userSettings.InboxKeys = keys.InboxKeys;
var defaultInboxKey = new InboxApiKey
{
UserId = userId,
Name = "Default",
DateCreated = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
ExpiryDate = DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeMilliseconds(),
LastUsedAt = 0
};
await Repositories.InboxApiKey.InsertAsync(defaultInboxKey);
}
await Repositories.InboxItems.DeleteManyAsync(t => t.UserId == userId);
@@ -208,6 +199,7 @@ namespace Notesnook.API.Services
Repositories.Colors.DeleteByUserId(userId);
Repositories.Tags.DeleteByUserId(userId);
Repositories.Vaults.DeleteByUserId(userId);
Repositories.InboxItemsHistory.DeleteByUserId(userId);
Repositories.UsersSettings.Delete((u) => u.UserId == userId);
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId);
@@ -269,6 +261,7 @@ namespace Notesnook.API.Services
Repositories.Colors.DeleteByUserId(userId);
Repositories.Tags.DeleteByUserId(userId);
Repositories.Vaults.DeleteByUserId(userId);
Repositories.InboxItemsHistory.DeleteByUserId(userId);
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId);
if (!await unit.Commit()) return false;
+2 -1
View File
@@ -197,7 +197,8 @@ namespace Notesnook.API
.AddMongoCollection(Collections.ColorsKey)
.AddMongoCollection(Collections.VaultsKey)
.AddMongoCollection(Collections.InboxItemsKey)
.AddMongoCollection(Collections.InboxApiKeysKey);
.AddMongoCollection(Collections.InboxApiKeysKey)
.AddMongoCollection(Collections.InboxItemsHistoryKey);
services.AddScoped<ISyncItemsRepositoryAccessor, SyncItemsRepositoryAccessor>();
services.AddScoped<SyncDeviceService>();
+15 -6
View File
@@ -17,7 +17,7 @@ const RawInboxItemSchema = z.object({
notebookIds: z.array(z.string()).optional(),
tagIds: z.array(z.string()).optional(),
type: z.enum(["note"]),
source: z.string(),
source: z.string().min(1, "Source is required"),
version: z.literal(1),
content: z
.object({
@@ -56,7 +56,9 @@ async function encrypt(
};
}
async function getInboxPublicEncryptionKey(apiKey: string) {
async function getInboxPublicEncryptionKey(
apiKey: string,
): Promise<{ status: "unauthorized" } | { status: "ok"; key: string | null }> {
const response = await fetch(
`${NOTESNOOK_API_SERVER_URL}/inbox/public-encryption-key`,
{
@@ -65,6 +67,9 @@ async function getInboxPublicEncryptionKey(apiKey: string) {
},
},
);
if (response.status === 401) {
return { status: "unauthorized" };
}
if (!response.ok) {
throw new Error(
`failed to fetch inbox public encryption key: ${await response.text()}`,
@@ -72,7 +77,7 @@ async function getInboxPublicEncryptionKey(apiKey: string) {
}
const data = (await response.json()) as unknown as any;
return (data?.key as string) || null;
return { status: "ok", key: (data?.key as string) || null };
}
async function postEncryptedInboxItem(
@@ -110,10 +115,14 @@ app.post("/", async (req, res) => {
return res.status(401).json({ error: "unauthorized" });
}
const inboxPublicKey = await getInboxPublicEncryptionKey(apiKey);
if (!inboxPublicKey) {
return res.status(403).json({ error: "inbox public key not found" });
const encryptionKeyResult = await getInboxPublicEncryptionKey(apiKey);
if (encryptionKeyResult.status === "unauthorized") {
return res.status(401).json({ error: "unauthorized" });
}
if (!encryptionKeyResult.key) {
return res.status(404).json({ error: "inbox public key not found" });
}
const inboxPublicKey = encryptionKeyResult.key;
console.log("[info] fetched inbox public key");
const validationResult = RawInboxItemSchema.safeParse(req.body);
@@ -52,7 +52,8 @@ namespace Streetwriters.Common.Extensions
b.WithOrigins(Constants.NOTESNOOK_CORS_ORIGINS);
b.AllowAnyMethod()
.AllowAnyHeader();
.AllowAnyHeader()
.WithExposedHeaders(["X-Object-Size", "Content-Length"]);
});
});
return services;
@@ -504,7 +504,6 @@
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
@@ -536,7 +535,6 @@
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
@@ -567,7 +565,6 @@
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
@@ -309,7 +309,6 @@
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
@@ -401,7 +401,6 @@
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
background-color: rgb(
@@ -467,7 +467,6 @@
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
@@ -483,9 +482,7 @@
display: inline;
"
><em
>If you did not request to reset
your account password, you can
safely ignore this email.</em
>If you did not request to reset your account password, you can safely ignore this email.</em
></span
>
</div>
@@ -554,7 +551,6 @@
>
<tbody>
<tr>
.
<td
style="
padding: 18px 0px 18px 0px;