diff --git a/Notesnook.API/Accessors/SyncItemsRepositoryAccessor.cs b/Notesnook.API/Accessors/SyncItemsRepositoryAccessor.cs index 3d2d1c1..97eabc2 100644 --- a/Notesnook.API/Accessors/SyncItemsRepositoryAccessor.cs +++ b/Notesnook.API/Accessors/SyncItemsRepositoryAccessor.cs @@ -43,6 +43,7 @@ namespace Notesnook.API.Accessors public SyncItemsRepository Tags { get; } public Repository UsersSettings { get; } public Repository Monographs { get; } + public Repository InboxApiKey { get; } public SyncItemsRepositoryAccessor(IDbContext dbContext, @@ -71,10 +72,12 @@ namespace Notesnook.API.Accessors [FromKeyedServices(Collections.TagsKey)] IMongoCollection tags, - Repository usersSettings, Repository monographs) + Repository usersSettings, Repository monographs, + Repository inboxApiKey) { UsersSettings = usersSettings; Monographs = monographs; + InboxApiKey = inboxApiKey; Notebooks = new SyncItemsRepository(dbContext, notebooks); Notes = new SyncItemsRepository(dbContext, notes); Contents = new SyncItemsRepository(dbContext, content); diff --git a/Notesnook.API/Constants.cs b/Notesnook.API/Constants.cs index 47909e1..e77818f 100644 --- a/Notesnook.API/Constants.cs +++ b/Notesnook.API/Constants.cs @@ -14,5 +14,6 @@ namespace Notesnook.API public const string TagsKey = "tags"; public const string ColorsKey = "colors"; public const string VaultsKey = "vaults"; + public const string InboxApiKeysKey = "inbox_api_keys"; } } \ No newline at end of file diff --git a/Notesnook.API/Controllers/InboxController.cs b/Notesnook.API/Controllers/InboxController.cs new file mode 100644 index 0000000..a65833d --- /dev/null +++ b/Notesnook.API/Controllers/InboxController.cs @@ -0,0 +1,119 @@ +/* +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.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Notesnook.API.Models; +using Streetwriters.Common; +using Streetwriters.Data.Repositories; + +namespace Notesnook.API.Controllers +{ + [ApiController] + [Authorize] + [Route("inbox")] + public class InboxController : ControllerBase + { + private readonly Repository InboxApiKey; + + public InboxController(Repository inboxApiKeysRepository) + { + InboxApiKey = inboxApiKeysRepository; + } + + [HttpGet("api-keys")] + public async Task GetApiKeysAsync() + { + var userId = User.FindFirstValue("sub"); + try + { + var apiKeys = await InboxApiKey.FindAsync(t => t.UserId == userId); + return Ok(apiKeys); + } + catch (Exception ex) + { + await Slogger.Error(nameof(GetApiKeysAsync), "Couldn't get inbox api keys.", userId, ex.ToString()); + return BadRequest(new { error = ex.Message }); + } + } + + [HttpPost("api-keys")] + public async Task CreateApiKeyAsync([FromBody] InboxApiKey request) + { + var userId = User.FindFirstValue("sub"); + try + { + if (string.IsNullOrWhiteSpace(request.Name)) + { + return BadRequest(new { error = "Api key name is required." }); + } + if (request.ExpiryDate <= -1) + { + return BadRequest(new { error = "Valid expiry date is required." }); + } + + var count = await InboxApiKey.CountAsync(t => t.UserId == userId); + if (count >= 10) + { + return BadRequest(new { error = "Maximum of 10 inbox api keys allowed." }); + } + + var inboxApiKey = new InboxApiKey + { + UserId = userId, + Name = request.Name, + DateCreated = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + ExpiryDate = request.ExpiryDate, + LastUsedAt = 0 + }; + await InboxApiKey.InsertAsync(inboxApiKey); + return Ok(inboxApiKey); + } + catch (Exception ex) + { + await Slogger.Error(nameof(CreateApiKeyAsync), "Couldn't create inbox api key.", userId, ex.ToString()); + return BadRequest(new { error = ex.Message }); + } + } + + [HttpDelete("api-keys/{apiKey}")] + public async Task DeleteApiKeyAsync(string apiKey) + { + var userId = User.FindFirstValue("sub"); + try + { + if (string.IsNullOrWhiteSpace(apiKey)) + { + return BadRequest(new { error = "Api key is required." }); + } + + await InboxApiKey.DeleteAsync(t => t.UserId == userId && t.Key == apiKey); + return Ok(new { message = "Api key deleted successfully." }); + } + catch (Exception ex) + { + await Slogger.Error(nameof(DeleteApiKeyAsync), "Couldn't delete inbox api key.", userId, ex.ToString()); + return BadRequest(new { error = ex.Message }); + } + } + } +} diff --git a/Notesnook.API/Interfaces/ISyncItemsRepositoryAccessor.cs b/Notesnook.API/Interfaces/ISyncItemsRepositoryAccessor.cs index f8baab0..6e7eaca 100644 --- a/Notesnook.API/Interfaces/ISyncItemsRepositoryAccessor.cs +++ b/Notesnook.API/Interfaces/ISyncItemsRepositoryAccessor.cs @@ -40,5 +40,6 @@ namespace Notesnook.API.Interfaces SyncItemsRepository Tags { get; } Repository UsersSettings { get; } Repository Monographs { get; } + Repository InboxApiKey { get; } } } \ No newline at end of file diff --git a/Notesnook.API/Models/InboxApiKey.cs b/Notesnook.API/Models/InboxApiKey.cs new file mode 100644 index 0000000..1809ad7 --- /dev/null +++ b/Notesnook.API/Models/InboxApiKey.cs @@ -0,0 +1,60 @@ +/* +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.Text.Json.Serialization; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using NanoidDotNet; + +namespace Notesnook.API.Models +{ + public class InboxApiKey + { + public InboxApiKey() + { + var random = Nanoid.Generate(size: 64); + Key = "nn__" + random; + } + + [BsonId] + [BsonIgnoreIfDefault] + [BsonRepresentation(BsonType.ObjectId)] + [JsonIgnore] + [MessagePack.IgnoreMember] + public string Id { get; set; } + + [JsonPropertyName("userId")] + public string UserId { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("key")] + public string Key { get; set; } + + [JsonPropertyName("dateCreated")] + public long DateCreated { get; set; } + + [JsonPropertyName("expiryDate")] + public long ExpiryDate { get; set; } + + [JsonPropertyName("lastUsedAt")] + public long LastUsedAt { get; set; } + } +} diff --git a/Notesnook.API/Notesnook.API.csproj b/Notesnook.API/Notesnook.API.csproj index eada3ac..670558d 100644 --- a/Notesnook.API/Notesnook.API.csproj +++ b/Notesnook.API/Notesnook.API.csproj @@ -16,6 +16,7 @@ + diff --git a/Notesnook.API/Services/UserService.cs b/Notesnook.API/Services/UserService.cs index 55b3525..671386b 100644 --- a/Notesnook.API/Services/UserService.cs +++ b/Notesnook.API/Services/UserService.cs @@ -18,9 +18,7 @@ along with this program. If not, see . */ using System; -using System.IO; using System.Net.Http; -using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -154,10 +152,20 @@ namespace Notesnook.API.Services if (keys.InboxKeys.Public == null || keys.InboxKeys.Private == null) { userSettings.InboxKeys = null; + await Repositories.InboxApiKey.DeleteManyAsync(t => t.UserId == userId); } 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); } } @@ -184,6 +192,7 @@ namespace Notesnook.API.Services Repositories.Vaults.DeleteByUserId(userId); Repositories.UsersSettings.Delete((u) => u.UserId == userId); Repositories.Monographs.DeleteMany((m) => m.UserId == userId); + Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId); var result = await unit.Commit(); await Slogger.Info(nameof(DeleteUserAsync), "User data deleted", userId, result.ToString()); @@ -243,6 +252,7 @@ namespace Notesnook.API.Services Repositories.Tags.DeleteByUserId(userId); Repositories.Vaults.DeleteByUserId(userId); Repositories.Monographs.DeleteMany((m) => m.UserId == userId); + Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId); if (!await unit.Commit()) return false; var userSettings = await Repositories.UsersSettings.FindOneAsync((s) => s.UserId == userId); diff --git a/Notesnook.API/Startup.cs b/Notesnook.API/Startup.cs index ff7d37d..d91b6dc 100644 --- a/Notesnook.API/Startup.cs +++ b/Notesnook.API/Startup.cs @@ -169,7 +169,8 @@ namespace Notesnook.API services.AddRepository("user_settings", "notesnook") .AddRepository("monographs", "notesnook") - .AddRepository("announcements", "notesnook"); + .AddRepository("announcements", "notesnook") + .AddRepository(Collections.InboxApiKeysKey, "notesnook"); services.AddMongoCollection(Collections.SettingsKey) .AddMongoCollection(Collections.AttachmentsKey) @@ -182,7 +183,8 @@ namespace Notesnook.API .AddMongoCollection(Collections.ShortcutsKey) .AddMongoCollection(Collections.TagsKey) .AddMongoCollection(Collections.ColorsKey) - .AddMongoCollection(Collections.VaultsKey); + .AddMongoCollection(Collections.VaultsKey) + .AddMongoCollection(Collections.InboxApiKeysKey); services.AddScoped(); services.AddScoped(); diff --git a/Streetwriters.Data/Repositories/Repository.cs b/Streetwriters.Data/Repositories/Repository.cs index 86f9175..62a8bf9 100644 --- a/Streetwriters.Data/Repositories/Repository.cs +++ b/Streetwriters.Data/Repositories/Repository.cs @@ -84,6 +84,11 @@ namespace Streetwriters.Data.Repositories return all.ToList(); } + public virtual async Task CountAsync(Expression> filterExpression) + { + return await Collection.CountDocumentsAsync(filterExpression); + } + public virtual void Update(string id, TEntity obj) { dbContext.AddCommand((handle, ct) => Collection.ReplaceOneAsync(handle, Builders.Filter.Eq("_id", ObjectId.Parse(id)), obj, cancellationToken: ct));