diff --git a/Notesnook.API/Controllers/SyncDeviceController.cs b/Notesnook.API/Controllers/SyncDeviceController.cs new file mode 100644 index 0000000..7eb0c25 --- /dev/null +++ b/Notesnook.API/Controllers/SyncDeviceController.cs @@ -0,0 +1,74 @@ +/* +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.Net.Http; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Notesnook.API.Interfaces; +using Notesnook.API.Models.Responses; +using Notesnook.API.Services; +using Streetwriters.Common; +using Streetwriters.Common.Extensions; +using Streetwriters.Common.Models; + +namespace Notesnook.API.Controllers +{ + [ApiController] + [Authorize] + [Route("devices")] + public class SyncDeviceController : ControllerBase + { + [HttpPost] + public async Task RegisterDevice([FromQuery] string deviceId) + { + try + { + var userId = this.User.FindFirstValue("sub"); + SyncDeviceService.RegisterDevice(userId, deviceId); + return Ok(); + } + catch (Exception ex) + { + await Slogger.Error(nameof(UnregisterDevice), "Couldn't register device.", ex.ToString()); + return BadRequest(new { error = ex.Message }); + } + } + + + [HttpDelete] + public async Task UnregisterDevice([FromQuery] string deviceId) + { + try + { + var userId = this.User.FindFirstValue("sub"); + SyncDeviceService.UnregisterDevice(userId, deviceId); + return Ok(); + } + catch (Exception ex) + { + await Slogger.Error(nameof(UnregisterDevice), "Couldn't unregister device.", ex.ToString()); + return BadRequest(new { error = ex.Message }); + } + } + } +} diff --git a/Notesnook.API/Hubs/SyncV2Hub.cs b/Notesnook.API/Hubs/SyncV2Hub.cs new file mode 100644 index 0000000..106ed4e --- /dev/null +++ b/Notesnook.API/Hubs/SyncV2Hub.cs @@ -0,0 +1,284 @@ +/* +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.Security.Claims; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.SignalR; +using MongoDB.Driver; +using Notesnook.API.Authorization; +using Notesnook.API.Interfaces; +using Notesnook.API.Models; +using Notesnook.API.Services; +using Streetwriters.Data.Interfaces; + +namespace Notesnook.API.Hubs +{ + public interface ISyncV2HubClient + { + Task SendItems(SyncTransferItemV2 transferItem); + Task SendVaultKey(EncryptedData vaultKey); + Task PushCompleted(); + } + + [Authorize("Sync")] + public class SyncV2Hub : Hub + { + private ISyncItemsRepositoryAccessor Repositories { get; } + private readonly IUnitOfWork unit; + private readonly string[] CollectionKeys = [ + "settingitem", + "attachment", + "note", + "notebook", + "content", + "shortcut", + "reminder", + "color", + "tag", + "vault", + "relation", // relations must sync at the end to prevent invalid state + ]; + + public SyncV2Hub(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, IUnitOfWork unitOfWork) + { + Repositories = syncItemsRepositoryAccessor; + unit = unitOfWork; + } + + public override async Task OnConnectedAsync() + { + var result = new SyncRequirement().IsAuthorized(Context.User, new PathString("/hubs/sync/v2")); + if (!result.Succeeded) + { + var reason = result.AuthorizationFailure.FailureReasons.FirstOrDefault(); + throw new HubException(reason?.Message ?? "Unauthorized"); + } + var id = Context.User.FindFirstValue("sub"); + await Groups.AddToGroupAsync(Context.ConnectionId, id); + await base.OnConnectedAsync(); + } + + private Action MapTypeToUpsertAction(string type) + { + return type switch + { + "settingitem" => Repositories.Settings.Upsert, + "attachment" => Repositories.Attachments.Upsert, + "note" => Repositories.Notes.Upsert, + "notebook" => Repositories.Notebooks.Upsert, + "content" => Repositories.Contents.Upsert, + "shortcut" => Repositories.Shortcuts.Upsert, + "reminder" => Repositories.Reminders.Upsert, + "relation" => Repositories.Relations.Upsert, + "color" => Repositories.Colors.Upsert, + "vault" => Repositories.Vaults.Upsert, + "tag" => Repositories.Tags.Upsert, + _ => null, + }; + } + + private Func, bool, int, Task>> MapTypeToFindItemsAction(string type) + { + return type switch + { + "settingitem" => Repositories.Settings.FindItemsById, + "attachment" => Repositories.Attachments.FindItemsById, + "note" => Repositories.Notes.FindItemsById, + "notebook" => Repositories.Notebooks.FindItemsById, + "content" => Repositories.Contents.FindItemsById, + "shortcut" => Repositories.Shortcuts.FindItemsById, + "reminder" => Repositories.Reminders.FindItemsById, + "relation" => Repositories.Relations.FindItemsById, + "color" => Repositories.Colors.FindItemsById, + "vault" => Repositories.Vaults.FindItemsById, + "tag" => Repositories.Tags.FindItemsById, + _ => null, + }; + } + + public async Task PushItems(string deviceId, SyncTransferItemV2 pushItem) + { + var userId = Context.User.FindFirstValue("sub"); + if (string.IsNullOrEmpty(userId)) return 0; + + var UpsertItem = MapTypeToUpsertAction(pushItem.Type) ?? throw new Exception($"Invalid item type: {pushItem.Type}."); + foreach (var item in pushItem.Items) + { + UpsertItem(item, userId, 1); + } + + if (!await unit.Commit()) return 0; + + await SyncDeviceService.AddIdsToOtherDevicesAsync(userId, deviceId, pushItem.Items.Select((i) => $"{i.ItemId}:{pushItem.Type}").ToList()); + return 1; + } + + public async Task PushCompleted() + { + var userId = Context.User.FindFirstValue("sub"); + await Clients.OthersInGroup(userId).PushCompleted(); + return true; + } + + private static async IAsyncEnumerable PrepareChunks(Func>>[] collections, string[] types, string userId, string[] ids, int size, bool resetSync, long maxBytes) + { + var chunksProcessed = 0; + for (int i = 0; i < collections.Length; i++) + { + var type = types[i]; + + var filteredIds = ids.Where((id) => id.EndsWith($":{type}")).Select((id) => id.Split(":")[0]).ToArray(); + if (!resetSync && filteredIds.Length == 0) continue; + + using var cursor = await collections[i](userId, filteredIds, resetSync, size); + + var chunk = new List(); + long totalBytes = 0; + long METADATA_BYTES = 5 * 1024; + + while (await cursor.MoveNextAsync()) + { + foreach (var item in cursor.Current) + { + chunk.Add(item); + totalBytes += item.Length + METADATA_BYTES; + if (totalBytes >= maxBytes) + { + yield return new SyncTransferItemV2 + { + Items = chunk, + Type = type, + Count = chunksProcessed + }; + + totalBytes = 0; + chunk.Clear(); + } + } + } + if (chunk.Count > 0) + { + yield return new SyncTransferItemV2 + { + Items = chunk, + Type = type, + Count = chunksProcessed + }; + } + } + } + + public async Task RequestFetch(string deviceId) + { + var userId = Context.User.FindFirstValue("sub"); + + if (!SyncDeviceService.IsDeviceRegistered(userId, deviceId)) + SyncDeviceService.RegisterDevice(userId, deviceId); + + var isResetSync = SyncDeviceService.IsSyncReset(userId, deviceId); + if (!SyncDeviceService.IsUnsynced(userId, deviceId) && + !SyncDeviceService.IsSyncPending(userId, deviceId) && + !isResetSync) + return new SyncV2Metadata { Synced = true }; + + string[] ids = await SyncDeviceService.FetchUnsyncedIdsAsync(userId, deviceId); + + var chunks = PrepareChunks( + 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, + ], + types: CollectionKeys, + userId, + ids, + size: 1000, + resetSync: isResetSync, + maxBytes: 7 * 1024 * 1024 + ); + + var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId.Equals(userId)); + if (userSettings.VaultKey != null) + { + if (!await Clients.Caller.SendVaultKey(userSettings.VaultKey).WaitAsync(TimeSpan.FromMinutes(10))) throw new HubException("Client rejected vault key."); + } + + await foreach (var chunk in chunks) + { + if (!await Clients.Caller.SendItems(chunk).WaitAsync(TimeSpan.FromMinutes(10))) throw new HubException("Client rejected sent items."); + + if (!isResetSync) + { + var syncedIds = chunk.Items.Select((i) => $"{i.ItemId}:{chunk.Type}").ToHashSet(); + ids = ids.Where((id) => !syncedIds.Contains(id)).ToArray(); + await SyncDeviceService.WritePendingIdsAsync(userId, deviceId, ids); + } + } + + SyncDeviceService.Reset(userId, deviceId); + + return new SyncV2Metadata + { + Synced = true, + }; + } + } + + [MessagePack.MessagePackObject] + public struct SyncV2Metadata + { + [MessagePack.Key("synced")] + [JsonPropertyName("synced")] + public bool Synced { get; set; } + } + + [MessagePack.MessagePackObject] + public struct SyncV2TransferItem + { + [MessagePack.Key("items")] + [JsonPropertyName("items")] + public IEnumerable Items { get; set; } + + [MessagePack.Key("type")] + [JsonPropertyName("type")] + public string Type { get; set; } + + [MessagePack.Key("final")] + [JsonPropertyName("final")] + public bool Final { get; set; } + + [MessagePack.Key("vaultKey")] + [JsonPropertyName("vaultKey")] + public EncryptedData VaultKey { get; set; } + } +} \ No newline at end of file diff --git a/Notesnook.API/Repositories/SyncItemsRepository.cs b/Notesnook.API/Repositories/SyncItemsRepository.cs index a2625d9..a44f719 100644 --- a/Notesnook.API/Repositories/SyncItemsRepository.cs +++ b/Notesnook.API/Repositories/SyncItemsRepository.cs @@ -24,6 +24,7 @@ using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; +using IdentityModel; using Microsoft.VisualBasic; using MongoDB.Bson; using MongoDB.Driver; @@ -68,6 +69,35 @@ namespace Notesnook.API.Repositories Sort = new SortDefinitionBuilder().Ascending((a) => a.Id) }); } + + public Task> FindItemsById(string userId, IEnumerable ids, bool all, int batchSize) + { + var filters = new List>(new[] { Builders.Filter.Eq((i) => i.UserId, userId) }); + + if (!all) filters.Add(Builders.Filter.In((i) => i.ItemId, ids)); + + return Collection.FindAsync(Builders.Filter.And(filters), new FindOptions + { + BatchSize = batchSize, + AllowDiskUse = true, + AllowPartialResults = false, + NoCursorTimeout = true + }); + } + + public Task> GetIdsAsync(string userId, int batchSize) + { + var filter = Builders.Filter.Eq((i) => i.UserId, userId); + return Collection.FindAsync(filter, new FindOptions + { + BatchSize = batchSize, + AllowDiskUse = true, + AllowPartialResults = false, + NoCursorTimeout = true, + Sort = new SortDefinitionBuilder().Ascending((a) => a.Id), + Projection = Builders.Projection.Include((i) => i.ItemId) + }); + } // public async Task DeleteIdsAsync(string[] ids, string userId, CancellationToken token = default(CancellationToken)) // { // await Collection.DeleteManyAsync((i) => ids.Contains(i.Id) && i.UserId == userId, token); diff --git a/Notesnook.API/Services/SyncDeviceService.cs b/Notesnook.API/Services/SyncDeviceService.cs new file mode 100644 index 0000000..85a7a3b --- /dev/null +++ b/Notesnook.API/Services/SyncDeviceService.cs @@ -0,0 +1,168 @@ +/* +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.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Notesnook.API.Services +{ + public class SyncDeviceService + { + private static string UserSyncDirectoryPath(string userId) => Path.Join("sync", userId); + private static string UserDeviceDirectoryPath(string userId, string deviceId) => Path.Join(SyncDeviceService.UserSyncDirectoryPath(userId), deviceId); + + private static string PendingIdsFilePath(string userId, string deviceId) => Path.Join(SyncDeviceService.UserDeviceDirectoryPath(userId, deviceId), "pending"); + + private static string UnsyncedIdsFilePath(string userId, string deviceId) => Path.Join(SyncDeviceService.UserDeviceDirectoryPath(userId, deviceId), "unsynced"); + + private static string ResetSyncFilePath(string userId, string deviceId) => Path.Join(SyncDeviceService.UserDeviceDirectoryPath(userId, deviceId), "reset-sync"); + + public static async Task GetUnsyncedIdsAsync(string userId, string deviceId) + { + try + { + return await File.ReadAllLinesAsync(UnsyncedIdsFilePath(userId, deviceId)); + } + catch + { + return Array.Empty(); + } + } + + public static async Task FetchUnsyncedIdsAsync(string userId, string deviceId) + { + if (IsSyncReset(userId, deviceId)) return Array.Empty(); + if (UnsyncedIdsFileLocks.TryGetValue(deviceId, out SemaphoreSlim fileLock) && fileLock.CurrentCount == 0) + await fileLock.WaitAsync(); + try + { + var unsyncedIds = await GetUnsyncedIdsAsync(userId, deviceId); + if (IsSyncPending(userId, deviceId)) + { + unsyncedIds = unsyncedIds.Union(await File.ReadAllLinesAsync(PendingIdsFilePath(userId, deviceId))).ToArray(); + } + + if (unsyncedIds.Length == 0) return Array.Empty(); + + File.Delete(UnsyncedIdsFilePath(userId, deviceId)); + await File.WriteAllLinesAsync(PendingIdsFilePath(userId, deviceId), unsyncedIds); + + return unsyncedIds; + } + catch + { + return Array.Empty(); + } + finally + { + if (fileLock != null && fileLock.CurrentCount == 0) fileLock.Release(); + } + } + + + public static async Task WritePendingIdsAsync(string userId, string deviceId, IEnumerable ids) + { + await File.WriteAllLinesAsync(PendingIdsFilePath(userId, deviceId), ids); + } + + public static bool IsSyncReset(string userId, string deviceId) + { + return File.Exists(ResetSyncFilePath(userId, deviceId)); + } + + public static bool IsSyncPending(string userId, string deviceId) + { + return File.Exists(PendingIdsFilePath(userId, deviceId)); + } + + public static bool IsUnsynced(string userId, string deviceId) + { + return File.Exists(UnsyncedIdsFilePath(userId, deviceId)); + } + + public static void Reset(string userId, string deviceId) + { + File.Delete(ResetSyncFilePath(userId, deviceId)); + File.Delete(PendingIdsFilePath(userId, deviceId)); + } + + public static bool IsDeviceRegistered(string userId, string deviceId) + { + return Directory.Exists(UserDeviceDirectoryPath(userId, deviceId)); + } + + public static IEnumerable ListDevices(string userId) + { + return Directory.EnumerateDirectories(UserSyncDirectoryPath(userId)).Select((path) => Path.GetFileName(path)); + } + + public static void ResetDevices(string userId) + { + if (File.Exists(UserSyncDirectoryPath(userId))) File.Delete(UserSyncDirectoryPath(userId)); + Directory.CreateDirectory(UserSyncDirectoryPath(userId)); + } + + private static readonly Dictionary UnsyncedIdsFileLocks = new(); + public static async Task AddIdsToOtherDevicesAsync(string userId, string deviceId, List ids) + { + foreach (var id in ListDevices(userId)) + { + if (id == deviceId || IsSyncReset(userId, id)) continue; + if (!UnsyncedIdsFileLocks.TryGetValue(id, out SemaphoreSlim fileLock)) + { + fileLock = new SemaphoreSlim(1, 1); + UnsyncedIdsFileLocks.Add(id, fileLock); + } + + await fileLock.WaitAsync(); + try + { + if (!IsDeviceRegistered(userId, id)) Directory.CreateDirectory(UserDeviceDirectoryPath(userId, id)); + + var oldIds = await GetUnsyncedIdsAsync(userId, id); + await File.WriteAllLinesAsync(UnsyncedIdsFilePath(userId, id), ids.Union(oldIds)); + } + finally + { + fileLock.Release(); + } + } + + } + + public static void RegisterDevice(string userId, string deviceId) + { + Directory.CreateDirectory(UserDeviceDirectoryPath(userId, deviceId)); + File.Create(ResetSyncFilePath(userId, deviceId)).Close(); + } + + public static void UnregisterDevice(string userId, string deviceId) + { + try + { + Directory.Delete(UserDeviceDirectoryPath(userId, deviceId), true); + } + catch { } + } + } +} \ No newline at end of file diff --git a/Notesnook.API/Services/UserService.cs b/Notesnook.API/Services/UserService.cs index 927efa7..c2581fc 100644 --- a/Notesnook.API/Services/UserService.cs +++ b/Notesnook.API/Services/UserService.cs @@ -174,40 +174,68 @@ namespace Notesnook.API.Services public async Task DeleteUserAsync(string userId, string jti) { - var cc = new CancellationTokenSource(); + try + { + await Slogger.Info(nameof(DeleteUserAsync), "Deleting user account", userId); + SyncDeviceService.ResetDevices(userId); + + var cc = new CancellationTokenSource(); + + Repositories.Notes.DeleteByUserId(userId); + Repositories.Notebooks.DeleteByUserId(userId); + Repositories.Shortcuts.DeleteByUserId(userId); + Repositories.Contents.DeleteByUserId(userId); Repositories.Settings.DeleteByUserId(userId); Repositories.LegacySettings.DeleteByUserId(userId); + Repositories.Attachments.DeleteByUserId(userId); + Repositories.Reminders.DeleteByUserId(userId); + Repositories.Relations.DeleteByUserId(userId); + Repositories.Colors.DeleteByUserId(userId); + Repositories.Tags.DeleteByUserId(userId); Repositories.Vaults.DeleteByUserId(userId); + Repositories.UsersSettings.Delete((u) => u.UserId == userId); + Repositories.Monographs.DeleteMany((m) => m.UserId == userId); - if (!Constants.IS_SELF_HOSTED) - { - await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.DeleteSubscriptionTopic, new DeleteSubscriptionMessage - { - AppId = ApplicationType.NOTESNOOK, - UserId = userId - }); - } + var result = await unit.Commit(); + await Slogger.Info(nameof(DeleteUserAsync), "User account deleted", userId, result.ToString()); + if (!result) return false; - await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage - { - SendToAll = false, - OriginTokenId = jti, - UserId = userId, - Message = new Message + if (!Constants.IS_SELF_HOSTED) { - Type = "logout", - Data = JsonSerializer.Serialize(new { reason = "Account deleted." }) + await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.DeleteSubscriptionTopic, new DeleteSubscriptionMessage + { + AppId = ApplicationType.NOTESNOOK, + UserId = userId + }); } - }); - await S3Service.DeleteDirectoryAsync(userId); + await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage + { + SendToAll = false, + OriginTokenId = jti, + UserId = userId, + Message = new Message + { + Type = "logout", + Data = JsonSerializer.Serialize(new { reason = "Account deleted." }) + } + }); - return await unit.Commit(); + await S3Service.DeleteDirectoryAsync(userId); + return result; + } + catch (Exception ex) + { + await Slogger.Error(nameof(DeleteUserAsync), "User account not deleted", userId, ex.ToString()); + } + return false; } public async Task ResetUserAsync(string userId, bool removeAttachments) { + SyncDeviceService.ResetDevices(userId); + var cc = new CancellationTokenSource(); Repositories.Notes.DeleteByUserId(userId); @@ -229,7 +257,6 @@ namespace Notesnook.API.Services userSettings.AttachmentsKey = null; userSettings.VaultKey = null; - userSettings.Profile = null; userSettings.LastSynced = 0; await Repositories.UsersSettings.UpsertAsync(userSettings, (s) => s.UserId == userId); diff --git a/Notesnook.API/Startup.cs b/Notesnook.API/Startup.cs index 0a055a7..fe1cfd6 100644 --- a/Notesnook.API/Startup.cs +++ b/Notesnook.API/Startup.cs @@ -160,41 +160,69 @@ namespace Notesnook.API }); if (!BsonClassMap.IsClassMapRegistered(typeof(UserSettings))) - { BsonClassMap.RegisterClassMap(); - } if (!BsonClassMap.IsClassMapRegistered(typeof(EncryptedData))) - { BsonClassMap.RegisterClassMap(); - } if (!BsonClassMap.IsClassMapRegistered(typeof(CallToAction))) - { BsonClassMap.RegisterClassMap(); - } - if (!BsonClassMap.IsClassMapRegistered(typeof(Announcement))) - { - BsonClassMap.RegisterClassMap(); - } + if (!BsonClassMap.IsClassMapRegistered(typeof(Attachment))) + BsonClassMap.RegisterClassMap(); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Content))) + BsonClassMap.RegisterClassMap(); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Note))) + BsonClassMap.RegisterClassMap(); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Notebook))) + BsonClassMap.RegisterClassMap(); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Relation))) + BsonClassMap.RegisterClassMap(); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Reminder))) + BsonClassMap.RegisterClassMap(); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Setting))) + BsonClassMap.RegisterClassMap(); + + if (!BsonClassMap.IsClassMapRegistered(typeof(SettingItem))) + BsonClassMap.RegisterClassMap(); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Shortcut))) + BsonClassMap.RegisterClassMap(); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Tag))) + BsonClassMap.RegisterClassMap(); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Color))) + BsonClassMap.RegisterClassMap(); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Vault))) + BsonClassMap.RegisterClassMap(); services.AddScoped(); services.AddScoped(); - services.AddScoped((provider) => new Repository(provider.GetRequiredService(), "notesnook", "user_settings")); - services.AddScoped((provider) => new Repository(provider.GetRequiredService(), "notesnook", "monographs")); - services.AddScoped((provider) => new Repository(provider.GetRequiredService(), "notesnook", "announcements")); - services.AddScoped((provider) => new SyncItemsRepository(provider.GetRequiredService(), "notesnook", "attachments")); - services.AddScoped((provider) => new SyncItemsRepository(provider.GetRequiredService(), "notesnook", "content")); - services.AddScoped((provider) => new SyncItemsRepository(provider.GetRequiredService(), "notesnook", "notes")); - services.AddScoped((provider) => new SyncItemsRepository(provider.GetRequiredService(), "notesnook", "notebooks")); - services.AddScoped((provider) => new SyncItemsRepository(provider.GetRequiredService(), "notesnook", "relations")); - services.AddScoped((provider) => new SyncItemsRepository(provider.GetRequiredService(), "notesnook", "reminders")); - services.AddScoped((provider) => new SyncItemsRepository(provider.GetRequiredService(), "notesnook", "settings")); - services.AddScoped((provider) => new SyncItemsRepository(provider.GetRequiredService(), "notesnook", "shortcuts")); - services.AddScoped((provider) => new SyncItemsRepository(provider.GetRequiredService(), "notesnook", "tags")); - services.AddScoped((provider) => new SyncItemsRepository(provider.GetRequiredService(), "notesnook", "colors")); + services.AddRepository("user_settings") + .AddRepository("monographs") + .AddRepository("announcements"); + + services.AddSyncRepository("settingsv2") + .AddSyncRepository("attachments") + .AddSyncRepository("content") + .AddSyncRepository("notes") + .AddSyncRepository("notebooks") + .AddSyncRepository("relations") + .AddSyncRepository("reminders") + .AddSyncRepository("settings") + .AddSyncRepository("shortcuts") + .AddSyncRepository("tags") + .AddSyncRepository("colors") + .AddSyncRepository("vaults"); services.TryAddTransient(); services.TryAddTransient(); @@ -202,7 +230,7 @@ namespace Notesnook.API services.AddControllers(); - services.AddHealthChecks().AddMongoDb(dbSettings.ConnectionString, dbSettings.DatabaseName, "database-check"); + services.AddHealthChecks(); // .AddMongoDb(dbSettings.ConnectionString, dbSettings.DatabaseName, "database-check"); services.AddSignalR((hub) => { hub.MaximumReceiveMessageSize = 100 * 1024 * 1024; @@ -245,15 +273,17 @@ namespace Notesnook.API app.UseWamp(WampServers.NotesnookServer, (realm, server) => { - IUserService service = app.GetScopedService(); - realm.Subscribe(IdentityServerTopics.DeleteUserTopic, async (ev) => { + IUserService service = app.GetScopedService(); await service.DeleteUserAsync(ev.UserId, null); }); - IDistributedCache cache = app.GetScopedService(); - realm.Subscribe(IdentityServerTopics.ClearCacheTopic, (ev) => ev.Keys.ForEach((key) => cache.Remove(key))); + realm.Subscribe(IdentityServerTopics.ClearCacheTopic, (ev) => + { + IDistributedCache cache = app.GetScopedService(); + ev.Keys.ForEach((key) => cache.Remove(key)); + }); }); app.UseRouting(); @@ -270,7 +300,27 @@ namespace Notesnook.API options.CloseOnAuthenticationExpiration = false; options.Transports = HttpTransportType.WebSockets; }); + endpoints.MapHub("/hubs/sync/v2", options => + { + options.CloseOnAuthenticationExpiration = false; + options.Transports = HttpTransportType.WebSockets; + }); }); } } + + public static class ServiceCollectionRepositoryExtensions + { + public static IServiceCollection AddRepository(this IServiceCollection services, string collectionName, string database = "notesnook") where T : class + { + services.AddScoped((provider) => new Repository(provider.GetRequiredService(), database, collectionName)); + return services; + } + + public static IServiceCollection AddSyncRepository(this IServiceCollection services, string collectionName, string database = "notesnook") where T : SyncItem + { + services.AddScoped((provider) => new SyncItemsRepository(provider.GetRequiredService(), database, collectionName)); + return services; + } + } }