mirror of
https://github.com/streetwriters/notesnook-sync-server.git
synced 2026-02-12 19:22:45 +00:00
inbox: add GET public inbox key & POST inbox items endpoint (#51)
* inbox: add GET public inbox key && POST inbox items endpoint * inbox: update SyncItem to support inbox items * inbox: update post inbox items request payload * inbox: update post inbox item endpoint
This commit is contained in:
@@ -44,6 +44,7 @@ namespace Notesnook.API.Accessors
|
||||
public Repository<UserSettings> UsersSettings { get; }
|
||||
public Repository<Monograph> Monographs { get; }
|
||||
public Repository<InboxApiKey> InboxApiKey { get; }
|
||||
public SyncItemsRepository InboxItems { get; }
|
||||
|
||||
public SyncItemsRepositoryAccessor(IDbContext dbContext,
|
||||
|
||||
@@ -71,6 +72,8 @@ namespace Notesnook.API.Accessors
|
||||
IMongoCollection<SyncItem> vaults,
|
||||
[FromKeyedServices(Collections.TagsKey)]
|
||||
IMongoCollection<SyncItem> tags,
|
||||
[FromKeyedServices(Collections.InboxItems)]
|
||||
IMongoCollection<SyncItem> inboxItems,
|
||||
|
||||
Repository<UserSettings> usersSettings, Repository<Monograph> monographs,
|
||||
Repository<InboxApiKey> inboxApiKey)
|
||||
@@ -90,6 +93,7 @@ namespace Notesnook.API.Accessors
|
||||
Colors = new SyncItemsRepository(dbContext, colors);
|
||||
Vaults = new SyncItemsRepository(dbContext, vaults);
|
||||
Tags = new SyncItemsRepository(dbContext, tags);
|
||||
InboxItems = new SyncItemsRepository(dbContext, inboxItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
101
Notesnook.API/Authorization/InboxApiKeyAuthenticationHandler.cs
Normal file
101
Notesnook.API/Authorization/InboxApiKeyAuthenticationHandler.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Notesnook.API.Models;
|
||||
using Streetwriters.Data.Repositories;
|
||||
|
||||
namespace Notesnook.API.Authorization
|
||||
{
|
||||
public static class InboxApiKeyAuthenticationDefaults
|
||||
{
|
||||
public const string AuthenticationScheme = "InboxApiKey";
|
||||
}
|
||||
|
||||
public class InboxApiKeyAuthenticationSchemeOptions : AuthenticationSchemeOptions
|
||||
{
|
||||
}
|
||||
|
||||
public class InboxApiKeyAuthenticationHandler : AuthenticationHandler<InboxApiKeyAuthenticationSchemeOptions>
|
||||
{
|
||||
private readonly Repository<InboxApiKey> _inboxApiKeyRepository;
|
||||
|
||||
public InboxApiKeyAuthenticationHandler(
|
||||
IOptionsMonitor<InboxApiKeyAuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
Repository<InboxApiKey> inboxApiKeyRepository)
|
||||
: base(options, logger, encoder)
|
||||
{
|
||||
_inboxApiKeyRepository = inboxApiKeyRepository;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
if (!Request.Headers.ContainsKey("Authorization"))
|
||||
{
|
||||
return AuthenticateResult.Fail("Missing Authorization header");
|
||||
}
|
||||
|
||||
var apiKey = Request.Headers["Authorization"].ToString().Trim();
|
||||
if (string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
return AuthenticateResult.Fail("Missing API key");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var inboxApiKey = await _inboxApiKeyRepository.FindOneAsync(k => k.Key == apiKey);
|
||||
if (inboxApiKey == null)
|
||||
{
|
||||
return AuthenticateResult.Fail("Invalid API key");
|
||||
}
|
||||
|
||||
if (inboxApiKey.ExpiryDate > 0 && DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() > inboxApiKey.ExpiryDate)
|
||||
{
|
||||
return AuthenticateResult.Fail("API key has expired");
|
||||
}
|
||||
|
||||
inboxApiKey.LastUsedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
await _inboxApiKeyRepository.UpsertAsync(inboxApiKey, k => k.Key == apiKey);
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("sub", inboxApiKey.UserId),
|
||||
};
|
||||
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||
|
||||
return AuthenticateResult.Success(ticket);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error validating inbox API key");
|
||||
return AuthenticateResult.Fail("Error validating API key");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ namespace Notesnook.API
|
||||
public const string TagsKey = "tags";
|
||||
public const string ColorsKey = "colors";
|
||||
public const string VaultsKey = "vaults";
|
||||
public const string InboxItems = "inbox_items";
|
||||
public const string InboxApiKeysKey = "inbox_api_keys";
|
||||
}
|
||||
}
|
||||
@@ -22,25 +22,36 @@ using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MongoDB.Bson;
|
||||
using Notesnook.API.Authorization;
|
||||
using Notesnook.API.Interfaces;
|
||||
using Notesnook.API.Models;
|
||||
using Notesnook.API.Repositories;
|
||||
using Streetwriters.Common;
|
||||
using Streetwriters.Data.Repositories;
|
||||
|
||||
namespace Notesnook.API.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("inbox")]
|
||||
public class InboxController : ControllerBase
|
||||
{
|
||||
private readonly Repository<InboxApiKey> InboxApiKey;
|
||||
private readonly Repository<UserSettings> UserSetting;
|
||||
private SyncItemsRepository InboxItems;
|
||||
|
||||
public InboxController(Repository<InboxApiKey> inboxApiKeysRepository)
|
||||
public InboxController(
|
||||
Repository<InboxApiKey> inboxApiKeysRepository,
|
||||
Repository<UserSettings> userSettingsRepository,
|
||||
ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor)
|
||||
{
|
||||
InboxApiKey = inboxApiKeysRepository;
|
||||
UserSetting = userSettingsRepository;
|
||||
InboxItems = syncItemsRepositoryAccessor.InboxItems;
|
||||
}
|
||||
|
||||
[HttpGet("api-keys")]
|
||||
[Authorize(Policy = "Notesnook")]
|
||||
public async Task<IActionResult> GetApiKeysAsync()
|
||||
{
|
||||
var userId = User.FindFirstValue("sub");
|
||||
@@ -57,6 +68,7 @@ namespace Notesnook.API.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("api-keys")]
|
||||
[Authorize(Policy = "Notesnook")]
|
||||
public async Task<IActionResult> CreateApiKeyAsync([FromBody] InboxApiKey request)
|
||||
{
|
||||
var userId = User.FindFirstValue("sub");
|
||||
@@ -96,6 +108,7 @@ namespace Notesnook.API.Controllers
|
||||
}
|
||||
|
||||
[HttpDelete("api-keys/{apiKey}")]
|
||||
[Authorize(Policy = "Notesnook")]
|
||||
public async Task<IActionResult> DeleteApiKeyAsync(string apiKey)
|
||||
{
|
||||
var userId = User.FindFirstValue("sub");
|
||||
@@ -115,5 +128,74 @@ namespace Notesnook.API.Controllers
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("public-encryption-key")]
|
||||
[Authorize(Policy = InboxApiKeyAuthenticationDefaults.AuthenticationScheme)]
|
||||
public async Task<IActionResult> GetPublicKeyAsync()
|
||||
{
|
||||
var userId = User.FindFirstValue("sub");
|
||||
try
|
||||
{
|
||||
var userSetting = await UserSetting.FindOneAsync(u => u.UserId == userId);
|
||||
if (string.IsNullOrWhiteSpace(userSetting?.InboxKeys?.Public))
|
||||
{
|
||||
return BadRequest(new { error = "Inbox public key is not configured." });
|
||||
}
|
||||
return Ok(new { key = userSetting.InboxKeys.Public });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Slogger<InboxController>.Error(nameof(GetPublicKeyAsync), "Couldn't get user's inbox's public key.", userId, ex.ToString());
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("items")]
|
||||
[Authorize(Policy = InboxApiKeyAuthenticationDefaults.AuthenticationScheme)]
|
||||
public async Task<IActionResult> CreateInboxItemAsync([FromBody] InboxSyncItem request)
|
||||
{
|
||||
var userId = User.FindFirstValue("sub");
|
||||
try
|
||||
{
|
||||
if (request.Key.Algorithm != Algorithms.XSAL_X25519_7)
|
||||
{
|
||||
return BadRequest(new { error = $"Only {Algorithms.XSAL_X25519_7} is supported for inbox item password." });
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(request.Key.Cipher))
|
||||
{
|
||||
return BadRequest(new { error = "Inbox item password cipher is required." });
|
||||
}
|
||||
if (request.Key.Length <= 0)
|
||||
{
|
||||
return BadRequest(new { error = "Valid inbox item password length is required." });
|
||||
}
|
||||
if (request.Algorithm != Algorithms.Default)
|
||||
{
|
||||
return BadRequest(new { error = $"Only {Algorithms.Default} is supported for inbox item." });
|
||||
}
|
||||
if (request.Version <= 0)
|
||||
{
|
||||
return BadRequest(new { error = "Valid inbox item version is required." });
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(request.Cipher) || string.IsNullOrWhiteSpace(request.IV))
|
||||
{
|
||||
return BadRequest(new { error = "Inbox item cipher and iv is required." });
|
||||
}
|
||||
if (request.Length <= 0)
|
||||
{
|
||||
return BadRequest(new { error = "Valid inbox item length is required." });
|
||||
}
|
||||
|
||||
request.UserId = userId;
|
||||
request.ItemId = ObjectId.GenerateNewId().ToString();
|
||||
await InboxItems.InsertAsync(request);
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Slogger<InboxController>.Error(nameof(CreateInboxItemAsync), "Couldn't create inbox item.", userId, ex.ToString());
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace Notesnook.API.Interfaces
|
||||
SyncItemsRepository Colors { get; }
|
||||
SyncItemsRepository Vaults { get; }
|
||||
SyncItemsRepository Tags { get; }
|
||||
SyncItemsRepository InboxItems { get; }
|
||||
Repository<UserSettings> UsersSettings { get; }
|
||||
Repository<Monograph> Monographs { get; }
|
||||
Repository<InboxApiKey> InboxApiKey { get; }
|
||||
|
||||
@@ -22,5 +22,6 @@ namespace Notesnook.API.Models
|
||||
public class Algorithms
|
||||
{
|
||||
public static string Default => "xcha-argon2i13-7";
|
||||
public static string XSAL_X25519_7 => "xsal-x25519-7";
|
||||
}
|
||||
}
|
||||
70
Notesnook.API/Models/InboxSyncItem.cs
Normal file
70
Notesnook.API/Models/InboxSyncItem.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Notesnook.API.Models
|
||||
{
|
||||
|
||||
[MessagePack.MessagePackObject]
|
||||
public class InboxSyncItem : SyncItem
|
||||
{
|
||||
[DataMember(Name = "key")]
|
||||
[JsonPropertyName("key")]
|
||||
[MessagePack.Key("key")]
|
||||
[Required]
|
||||
public EncryptedKey Key
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
[MessagePack.MessagePackObject]
|
||||
public class EncryptedKey
|
||||
{
|
||||
[DataMember(Name = "alg")]
|
||||
[JsonPropertyName("alg")]
|
||||
[MessagePack.Key("alg")]
|
||||
[Required]
|
||||
public string Algorithm
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[DataMember(Name = "cipher")]
|
||||
[JsonPropertyName("cipher")]
|
||||
[MessagePack.Key("cipher")]
|
||||
[Required]
|
||||
public string Cipher
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonPropertyName("length")]
|
||||
[DataMember(Name = "length")]
|
||||
[MessagePack.Key("length")]
|
||||
[Required]
|
||||
public long Length
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,7 +111,7 @@ namespace Notesnook.API.Models
|
||||
public string Algorithm
|
||||
{
|
||||
get; set;
|
||||
} = Algorithms.Default;
|
||||
}
|
||||
}
|
||||
|
||||
public class SyncItemBsonSerializer : SerializerBase<SyncItem>
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace Notesnook.API.Repositories
|
||||
this.collectionName = collection.CollectionNamespace.CollectionName;
|
||||
}
|
||||
|
||||
private readonly List<string> ALGORITHMS = [Algorithms.Default];
|
||||
private readonly List<string> ALGORITHMS = [Algorithms.Default, Algorithms.XSAL_X25519_7];
|
||||
private bool IsValidAlgorithm(string algorithm)
|
||||
{
|
||||
return ALGORITHMS.Contains(algorithm);
|
||||
|
||||
@@ -119,6 +119,11 @@ namespace Notesnook.API
|
||||
policy.Requirements.Add(new SyncRequirement());
|
||||
policy.Requirements.Add(new ProUserRequirement());
|
||||
});
|
||||
options.AddPolicy(InboxApiKeyAuthenticationDefaults.AuthenticationScheme, policy =>
|
||||
{
|
||||
policy.AuthenticationSchemes.Add(InboxApiKeyAuthenticationDefaults.AuthenticationScheme);
|
||||
policy.RequireAuthenticatedUser();
|
||||
});
|
||||
|
||||
options.DefaultPolicy = options.GetPolicy("Notesnook");
|
||||
}).AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizationResultTransformer>(); ;
|
||||
@@ -152,7 +157,11 @@ namespace Notesnook.API
|
||||
options.SaveToken = true;
|
||||
options.EnableCaching = true;
|
||||
options.CacheDuration = TimeSpan.FromMinutes(30);
|
||||
});
|
||||
})
|
||||
.AddScheme<InboxApiKeyAuthenticationSchemeOptions, InboxApiKeyAuthenticationHandler>(
|
||||
InboxApiKeyAuthenticationDefaults.AuthenticationScheme,
|
||||
options => { }
|
||||
);
|
||||
|
||||
// Serializer.RegisterSerializer(new SyncItemBsonSerializer());
|
||||
if (!BsonClassMap.IsClassMapRegistered(typeof(UserSettings)))
|
||||
@@ -184,6 +193,7 @@ namespace Notesnook.API
|
||||
.AddMongoCollection(Collections.TagsKey)
|
||||
.AddMongoCollection(Collections.ColorsKey)
|
||||
.AddMongoCollection(Collections.VaultsKey)
|
||||
.AddMongoCollection(Collections.InboxItems)
|
||||
.AddMongoCollection(Collections.InboxApiKeysKey);
|
||||
|
||||
services.AddScoped<ISyncItemsRepositoryAccessor, SyncItemsRepositoryAccessor>();
|
||||
|
||||
Reference in New Issue
Block a user